双链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #include<iostream> using namespace std; const int N = 100010; int m; int e[N],l[N],r[N],idx; void insert(int k,int x) { e[idx]=x; l[idx]=k; r[idx]=r[k]; l[r[k]]=idx; r[k]=idx; idx++;
} void del(int k) {
l[r[k]]=l[k]; r[l[k]]=r[k]; } void init() { r[0]=1; l[1]=0;
idx=2; }
C++
|
模拟栈
s.empty()
判断非空
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <iostream> #include <stack>
int main() { std::stack<int> s; s.push(10); s.push(20); s.push(30);
std::cout << "栈顶元素: " << s.top() << std::endl;
s.pop(); std::cout << "弹出后栈顶: " << s.top() << std::endl; return 0; }
C++
|
模拟队列
q.empty()
判断非空
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include<iostream> #include<string> using namespace std; const int N=100001; int a[N],hh=0,tt=-1; int main() { int M; cin>>M; for(int i=0;i<M;i++) { string s; cin>> s; if(s == "push") { int x; cin>>x; tt++; a[tt]= x; } else if(s=="empty") { if(hh>tt) cout<<"YES"<<endl; else cout<<"NO"<<endl; } else if(s=="pop") { hh++; } else{ cout<<a[hh]<<endl; } } }
C++
|
单调栈
来源问题:给定一个长度为 N
的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1
单调栈:用一个栈,维护一个单调的不定长序列,举个例子:
7 6 2 1 3 8 栈内维护降序列
7 7,6 7,6,2 7,6,2,1 7,6,3 8
手模一遍之后思路很清晰了,对所有数进行遍历,如果新数小于栈顶的数直接入栈,如果新数大于栈顶的数,则不断弹栈直到栈顶元素小于新数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include<iostream> using namespace std; const int N=1e5+10; int s[N],top=-1; int main() { int n; cin>>n; for(int i=0;i<n;i++) { int x; scanf("%d",&x); if(top==-1) cout<<"-1"<<" "; else{ while(s[top]>=x) top--; if(top>-1) cout<<s[top]<<" "; else cout<<"-1"<<" "; } s[++top]=x; } }
C++
|
滑动窗口/单调双端队列
问题描述
给定一个大小为n ≤ 106的数组。,有一个大小为k的滑动窗口,它从数组的最左边移动到最右边。你只能在窗口中看到k个数字。每次滑动窗口向右移动一个位置。
以下是一个例子:
数组 2 6 5 7 8 6
每次的窗口中的值分别为 265 657 578 786
你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
解决这一问题可以直接暴力搜索,但是时间复杂度近似于O(n2)。我们引入一种使用单调双端队列的方式,即维护一个定长k的升序/降序区间。首先来确定双端队列的结构。

- 使用数组模拟,队头在左,队尾在右,这是因为队尾会不断有新元素入队,倘若队尾在左的话会导致数组越界
- 符号法则:初始
hh=0
tt=-1
,检查队列非空判断hh<=tt
即可,队尾入队总是使用q[++tt]
,队头出队总是使用hh++
.
- 队尾出队的条件:队列不空且新元素更优,则队尾不断出队
- 队头出队的条件:滑出了窗口范围
本题的思路是,使用一个单调双端队列维护窗口内的一个单调序列,遍历每一个元素入队的过程,做两次判断,先判断队头元素是否需要滑出,再判断多少队尾元素会被等待入队的更优元素淘汰。然后入队新元素,如果i>k-1
则输出队头元素。
从这一思路我们也可以看出,应当使用q存储元素在数组中的下标而非元素值本身,因为算法涉及到判断队头元素是否需要划出,这是不能使用元素值判断的,必须通过比较队头元素下标q[hh]
和当前窗口尾坐标i-k+1
来判断
综上所述,由于每个元素最多入队出队一次,因此本题得以在O(2N)的时间复杂度内求解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| #include<iostream> using namespace std; const int N=1e6+10;
int q[N],a[N],hh=0,tt=-1; int main() { int n,k; cin>>n>>k; for(int i=0;i<n;i++) cin>>a[i]; for(int i=0;i<n;i++) { if(tt>=hh&&q[hh]<i-k+1) hh++; while(tt>=hh&&a[q[tt]]>a[i]) tt--; q[++tt]=i; if(i>=k-1) cout<<a[q[hh]]<<' '; } hh=0,tt=-1; cout<<endl; for(int i=0;i<n;i++) { if(tt>=hh&&q[hh]<i-k+1) hh++; while(tt>=hh&&a[q[tt]]<a[i]) tt--; q[++tt]=i; if(i>=k-1) cout<<a[q[hh]]<<' '; }
}
C++
|
KMP字符串
用小字符串在大字符串中匹配,找最开始出现的脚标。
算法的核心在于引入了next函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <iostream> using namespace std; const int N = 100010, M = 1000010;
int n, m; int ne[N]; char s[M], p[N];
int main() { cin >> n >> p + 1 >> m >> s + 1;
for (int i = 2, j = 0; i <= n; i ++ ) { while (j && p[i] != p[j + 1]) j = ne[j]; if (p[i] == p[j + 1]) j ++ ; ne[i] = j; }
for (int i = 1, j = 0; i <= m; i ++ ) { while (j && s[i] != p[j + 1]) j = ne[j]; if (s[i] == p[j + 1]) j ++ ; if (j == n) { printf("%d ", i - n); j = ne[j]; } }
return 0; }
C++
|
trie树/字典树
需求描述:要求维护一个字符串集合,这个集合应当拥有两个功能
- 查询一个字符串是否出现过
- 查询一个字符串的出现次数
引入一种高效的数据结构:字典树
- 子节点的唯一标识是idx
- 儿子数组son[a][i]存储节点a沿着i这条边走到的子节点
- 每个节点最多有26个分支,用数字025映射字母az
idx=0
为整个树的根

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| #include<iostream> #include<cstdio> #include<string> using namespace std; const int N=1e5+10;
int son[N][27],cnt[N]={0},idx=0; void insertt(string str){ int p=0; for(int i=0;i<str.size();i++) { int u=str[i]-'a'; if(!son[p][u]) son[p][u]=++idx; p=son[p][u]; } cnt[p]++;
} int queryt(string str){ int p=0; for(int i=0;i<str.size();i++) { int u=str[i]-'a'; if(!son[p][u]) { return 0; } p=son[p][u]; } return cnt[p]; } int main() { int n; cin>>n; for(int i=0;i<n;i++) { string c, s; cin>>c>>s; if(c=="I") insertt(s); else cout<<queryt(s)<<endl; } }
C++
|
并查集
并查集,顾名思义,它的功能应当有:
考虑经典情形:一开始1-n n个数,处理多次合并与查询的操作
算法的核心在于:只存父节点,并通过find函数不断优化整个树的结构
只存父节点是因为只需要维护是否属于同一个集合的信息,不同以往的还需要维护整个序列的信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| #include<iostream> using namespace std; const int N = 1e5+10; int p[N]; int find(int x) { if(p[x]!=x) p[x]=find(p[x]); return p[x]; } int main() { int n,m; scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) p[i]=i; for(int i=0;i<m;i++) { char c; cin>>c; int a,b; scanf("%d%d",&a,&b); if(c=='M') p[find(a)]=find(b); else{ if(find(a)==find(b)) cout<<"Yes"; else cout<<"No"; cout<<endl; } } }
C++
|
堆排序
解决的问题:给定一个数列,从小到大输出前m个小的数
堆是一颗完全二叉树,任何一个节点的儿子都比父亲大,为此维护堆有两个基本的操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| void down(int u) { int t = u; if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2; if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1; if (u != t) { heap_swap(u, t); down(t); } }
void up(int u) { while (u / 2 && h[u] < h[u / 2]) { heap_swap(u, u / 2); u >>= 1; } }
C
|
两个函数接受的都是idx,考虑这个idx的节点data是否需要down下去或者up上来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| #include<iostream> using namespace std; const int N = 1e5+10; int n,m;
int h[N],idx;
void down(int u) { int t=u; if(2*u<=idx&&h[2*u]<h[t]) t=2*u; if(2*u+1<=idx&&h[2*u+1]<h[t]) t=2*u+1; if(t!=u) { swap(h[t],h[u]); down(t); } } int main() { cin>>n>>m; idx=n; for(int i=1;i<=n;i++) scanf("%d",&h[i]); for(int i=n/2;i;i--) down(i); for(int i=0;i<m;i++) { printf("%d ",h[1]); h[1]=h[idx]; idx--; down(1); } }
C++
|
哈希
一般情况下的hash都可以用stl写,
1 2 3 4 5 6 7 8 9
| #include<iostream> #include<unordered_map> unordered_map<int,bool> mp int main() { mp[1]=false; mp[2]=true; }
C++
|
字符串哈希遇到基本需要手写
给定一个长度为 𝔫 的字符串,再给定 m
个询问,每个询问包含四个整数l1, r1, l2, r2,请你判断[l1, r1]和
[l2, r2]这两个区间所包含的字符串子串是否完全相同。
字符串中只包含大小写英文字母和数字。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| #include<iostream> #include<cstdio> #include<string> using namespace std; typedef unsigned long long ULL; const int N = 1e5+5,P = 131; ULL h[N],p[N];
ULL query(int l,int r){ return h[r] - h[l-1]*p[r-l+1]; } int main(){ int n,m; cin>>n>>m; string x; cin>>x;
p[0] = 1; h[0] = 0; for(int i=0;i<n;i++){ p[i+1] = p[i]*P; h[i+1] = h[i]*P +x[i]; }
while(m--){ int l1,r1,l2,r2; cin>>l1>>r1>>l2>>r2; if(query(l1,r1) == query(l2,r2)) printf("Yes\n"); else printf("No\n");
} return 0; }
C++
|
一些常用的stl数据结构
queue/priority_queue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include<queue>
queue<int> q; q.push(b); int a = q.top(); q.pop();
priority_queue<int> q;
q.push(b); int a = q.top(); q.pop();
typedef pair<int,int> pii; priority_queue<pii,vector<pii>,greater<pii>> heap;
C++
|
map/unordered_map
map和umap都是建立由键到值的映射,不过 -
map原理是红黑树,会按照键进行升序排序,查找、插入和删除操作的时间复杂度是O(log
n)。 -
而umap基于哈希,是无序的,查找、插入和删除操作的平均时间复杂度是O(1)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #include <iostream> #include <map> #include <string>
int main() { std::map<std::string, int> wordCount;
wordCount["banana"] = 1; wordCount["apple"] = 2; wordCount["cherry"] = 1;
for (const auto& pair : wordCount) { std::cout << pair.first << ": " << pair.second << std::endl; }
auto it = wordCount.find("banana"); if (it != wordCount.end()) { std::cout << "Found banana with count: " << it->second << std::endl; }
return 0; }
C++
|
运行结果为
apple: 2
banana: 1
cherry: 1
Found banana with count: 1
但是unordered结果完全无序:
apple: 2
cherry: 1
banana: 1
Found banana with count: 1