第二章 数据结构
一、数组模拟链表、栈、队列
单链表基本操作
详见: https://www.acwing.com/problem/content/828/
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 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #include <iostream> using namespace std;const int N=100010 ;int e[N],ne[N],head,idx;void init () { head=-1 ; } void add_head (int x) { e[idx]=x;ne[idx]=head;head=idx++; } void add_k (int k,int x) { e[idx]=x;ne[idx]=ne[k];ne[k]=idx++; } void remove (int k) { ne[k]=ne[ne[k]]; } int main () { int n; cin>>n; init (); while (n--) { char op; int k,x; cin>>op; if (op=='I' ) { cin>>k>>x; add_k (k-1 ,x); } else if (op=='H' ) { cin>>x; add_head (x); } else { cin>>k; if (!k) head=ne[head]; else remove (k-1 ); } } for (int i=head;i!=-1 ;i=ne[i]) cout<<e[i]<<' ' ; return 0 ; }
双链表基本操作
详见: https://www.acwing.com/problem/content/829/
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 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 #include <iostream> using namespace std;const int N = 100010 ;int m;int e[N], l[N], r[N], idx;void insert (int a, int x) { e[idx] = x; l[idx] = a, r[idx] = r[a]; l[r[a]] = idx, r[a] = idx ++ ; } void remove (int a) { l[r[a]] = l[a]; r[l[a]] = r[a]; } int main () { cin >> m; r[0 ] = 1 , l[1 ] = 0 ; idx = 2 ; while (m -- ) { string op; cin >> op; int k, x; if (op == "L" ) { cin >> x; insert (0 , x); } else if (op == "R" ) { cin >> x; insert (l[1 ], x); } else if (op == "D" ) { cin >> k; remove (k + 1 ); } else if (op == "IL" ) { cin >> k >> x; insert (l[k + 1 ], x); } else { cin >> k >> x; insert (k + 1 , x); } } for (int i = r[0 ]; i != 1 ; i = r[i]) cout << e[i] << ' ' ; cout << endl; return 0 ; }
栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int stk[N], tt = 0 ;stk[ ++ tt] = x; tt -- ; stk[tt]; if (tt > 0 ){ }
队列:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int q[N], hh = 0 , tt = -1 ;q[ ++ tt] = x; hh ++ ; q[hh]; q[tt]; if (hh <= tt){ }
二、单调栈 & 单调队列
单调栈 O(n):
应用1:求左边第一个比当前数小的数
1 2 3 4 5 6 7 8 9 10 int n;cin>>n; for (int i=0 ;i<n;i++) scanf ("%d" ,&a[i]);for (int i=0 ;i<n;i++){ while (tt && stk[tt]>=a[i]) tt--; if (!tt) printf ("-1 " ); else printf ("%d " ,stk[tt]); stk[++tt]=a[i]; }
应用2:求左边第一个比当前数大的数
1 2 3 4 5 6 7 8 9 10 int n;cin>>n; for (int i=0 ;i<n;i++) scanf ("%d" ,&a[i]);for (int i=0 ;i<n;i++){ while (tt && stk[tt]<=a[i]) tt--; if (!tt) printf ("-1 " ); else printf ("%d " ,stk[tt]); stk[++tt]=a[i]; }
应用3:求右边第一个比当前数小的数
1 2 3 4 5 6 7 8 9 10 11 12 int n;cin>>n; for (int i=0 ;i<n;i++) scanf ("%d" ,&a[i]);reverse (a,a+n);for (int i=0 ;i<n;i++){ while (tt && stk[tt]>=a[i]) tt--; if (!tt) b[i]=-1 ; else b[i]=stk[tt]; stk[++tt]=a[i]; } for (int i=n-1 ;i>=0 ;i--) printf ("%d " ,b[i]);
应用4:求右边第一个比当前数大的数
1 2 3 4 5 6 7 8 9 10 11 12 int n;cin>>n; for (int i=0 ;i<n;i++) scanf ("%d" ,&a[i]);reverse (a,a+n);for (int i=0 ;i<n;i++){ while (tt && stk[tt]<=a[i]) tt--; if (!tt) b[i]=-1 ; else b[i]=stk[tt]; stk[++tt]=a[i]; } for (int i=n-1 ;i>=0 ;i--) printf ("%d " ,b[i]);
单调队列(滑动窗口)
详见:https://www.acwing.com/problem/content/156/
思路:最小值和最大值分开来做,两个for循环完全类似(四步走战略)
解决队首已经出窗口问题;
解决队尾与当前元素a[i]不满足单调性问题;
将当前元素下标加入队尾;
如果满足条件则输出
注意:上面四个步骤中,一定要先3后4 ,因为有可能输出的正是新加入的那个元素;队列中存的是原数组的下标 ,取值时要在套一层,a[q[]]; 算最大值前注意将hh 和 tt重置;hh从0开始,数组下标也要从0开始
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int n,k; cin>>n>>k; for (int i=0 ;i<n;i++) cin>>a[i]; for (int i=0 ;i<n;i++) { if (hh<=tt && i-q[hh]+1 >k) hh++; while (hh<=tt && a[q[tt]]>=a[i]) tt--; q[++tt]=i; if (i-k+1 >=0 ) cout<<a[q[hh]]<<' ' ; } cout<<endl; hh=0 ,tt=-1 ; for (int i=0 ;i<n;i++) { if (hh<=tt && i-q[hh]+1 >k) hh++; while (hh<=tt && a[q[tt]]<=a[i]) tt--; q[++tt]=i; if (i-k+1 >=0 ) cout<<a[q[hh]]<<' ' ; }
三、KMP (字符串匹配问题)
全称:Knuth-Morris-Pratt算法
通俗易懂 :"字符串A是否为字符串B的子串?如果是的话出现在B的哪些位置?字符串A称为 模板串 ,字符串B称为模式串 。
详见:https://www.acwing.com/problem/content/833/
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 char p[N],S[M]; 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) { cout<<i-n<<' ' ; j=ne[j]; } }
四、Trie 树
概念:高效地存储 和查找 字符串集合的数据结构。Trie树也叫做字典树,它是一个树形结构。是一种专门处理字符串匹配的数据结构,用来解决在一组字符串集合中快速查找某个字符串。
性质:
根节点不包含字符,除根节点外每一个节点都只包含一个字符
从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串
每个节点的所有子节点包含的字符都不相同
详见:https://www.acwing.com/problem/content/837/
代码:
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 #include <iostream> using namespace std;const int N=100010 ;int son[N][26 ]; int cnt[N],idx; char str[N];void insert (char *str) { int p=0 ; for (int i=0 ;str[i];i++) { int u=str[i]-'a' ; if (!son[p][u]) son[p][u]=++idx; p=son[p][u]; } cnt[p]++; } int query (char *str) { int p=0 ; for (int i=0 ;str[i];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; while (n--) { char op; cin>>op>>str; if (op=='I' ) insert (str); else cout<<query (str)<<endl; } return 0 ; }
应用:son[][]
不仅能存字符串也能存二进制数,也能存整数
最大异或对:
详情:https://www.acwing.com/problem/content/145/
暴力算法 :O(n^2)
Trie :O(n)
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 48 49 50 51 52 53 54 55 56 57 58 #include <iostream> #include <algorithm> #include <cstring> #include <cstdio> using namespace std;const int N=100010 ,M=31 *N; int son[M][2 ],idx;int a[N];void insert (int x) { int p=0 ; for (int i=30 ;i>=0 ;i--) { int u=(x>>i)&1 ; if (!son[p][u]) son[p][u]=++idx; p=son[p][u]; } } int query (int x) { int res=0 ,p=0 ; for (int i=30 ;i>=0 ;i--) { int u=(x>>i)&1 ; if (son[p][!u]) { p=son[p][!u]; res = res*2 +1 ; } else { p=son[p][u]; res = res*2 +0 ; } } return res; } int main () { int n; cin>>n; for (int i=0 ;i<n;i++) { cin>>a[i]; insert (a[i]); } int res=0 ; for (int i=0 ;i<n;i++) { res=max (res,query (a[i])); } cout<<res<<endl; return 0 ; }
五、并查集
功能:
基本原理:每个集合用一棵树来表示。树根的编号就是整个集合的编号。每个节存储它的父节点,p[x]表示x的父节点
问题:
如何判断树根 :if (p[x] == x)
如何求x的集合编号 :while (p[x] != x) x = p[x]
如何合并两个集合 :p[x] 是 x 的集合编号 p[y] 是 y 的集合编号 p[x] = y
由于每次求x的集合编号 每次需要遍历到根节点,复杂度与树的高度成正比,于是就有了并查集路径压缩优化
详见:https://www.acwing.com/problem/content/838/
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 #include <iostream> using namespace std;const int N=100010 ;int n,m;int p[N];int find (int x) { if (p[x]!=x) p[x]=find (p[x]); return p[x]; } int main () { cin>>n>>m; string op; for (int i=1 ;i<=n;i++) p[i]=i; while (m--) { int x,y; cin>>op>>x>>y; if (op[0 ]=='M' ) p[find (x)]=find (y); else { if (find (x)==find (y)) puts ("Yes" ); else puts ("No" ); } } return 0 ; }
应用:
维护一个集合中元素个数,增加一个cnt[] 数组即可,注意初始化为 1 ,只维护根节点的cnt[] ,在合并集合时更新cnt[]
维护一个集合中每个元素到根结点的距离
六、堆(数组模拟)
本质:一颗完全二叉树
小根堆:每一个点都是小于等于左右儿子,根节点是最小值
大跟堆:每一个点都是大于等于左右儿子,根节点是最大值
堆的存储:一个一维数组
x 的左儿子是2x
x的右儿子是2x+1
功能 (以小顶堆为例):
插入一个数 : 插入到最后一个位置,不断上移 heap[ ++ size] = x; up(size)
O(longn)
求集合当中的最小值 : 取出堆顶 heap[1]
O(1)
删除最小值 :将最后一个元素赋给堆顶,然后不断下移 heap[1] = heap[size--]; down(1)
O(longn)
-------------(以下stl中的堆实现不了)
删除任意一个元素 :将最后一个元素赋给该元素,然后不断移动 heap[k] = heap[size--]; down(k); up(k)
; O(longn)
修改任意一个元素 :heap[k] = x; down(k); up(x)
O(longn)
两个基本操作 :(上述五个功能可全部由这两个基本操作来实现)
down(x)
:将一个数变大,应该下移
1 2 3 4 5 6 7 8 9 10 11 void down (int u) { int t = u; if (u*2 <= n && h[u*2 ] < h[t]) t = 2 *u; if (u*2 +1 <= n && h[u*2 +1 ] < h[t]) t = 2 *u + 1 ; if (u!=t) { swap (h[t],h[u]); down (t); } }
up(x)
:将一个数变小,应该上移
1 2 3 4 5 6 7 8 void up (int x) { while (u/2 && h[u/2 ] > h[u]) { swap (h[u/2 ],h[u]); u >>= 1 ; } }
堆排序
详见:https://www.acwing.com/problem/content/840/
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 #include <iostream> #include <algorithm> #include <cstring> #include <cstdio> using namespace std;const int N = 100010 ;int h[N];int n,m;void down (int u) { int t = u; if (u*2 <=n && h[u*2 ] < h[t]) t = u*2 ; if (u*2 +1 <=n && h[u*2 +1 ] < h[t]) t= u*2 +1 ; if (t!=u) { swap (h[u],h[t]); down (t); } } int main () { cin>>n>>m; for (int i=1 ;i<=n;i++) cin>>h[i]; for (int i=n/2 ;i>=1 ;i--) down (i); while (m--) { cout<<h[1 ]<<' ' ; h[1 ] = h[n--]; down (1 ); } return 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 void heap_swap (int a, int b) { swap (ph[hp[a]],ph[hp[b]]); swap (hp[a], hp[b]); swap (h[a], h[b]); } 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 ; } }
详见:https://www.acwing.com/problem/content/841/
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 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 #include <iostream> #include <algorithm> #include <cstdio> #include <cstring> using namespace std;const int N = 100010 ;int h[N],ph[N],hp[N],cnt;void heap_swap (int a,int b) { swap (ph[hp[a]],ph[hp[b]]); swap (hp[a],hp[b]); swap (h[a],h[b]); } void down (int u) { int t=u; if (2 *u<=cnt && h[u*2 ]<h[t]) t=u*2 ; if (2 *u+1 <=cnt && h[u*2 +1 ]<h[t]) t=u*2 +1 ; if (t!=u) { heap_swap (u,t); down (t); } } void up (int u) { while (u/2 && h[u/2 ]>h[u]) { heap_swap (u/2 ,u); u>>=1 ; } } int main () { int n; cin>>n; char op[5 ]; int m=0 ; while (n--) { int k,x; cin>>op; if (!strcmp (op,"I" )) { cin>>x; cnt++; ph[++m]=cnt; hp[cnt]=m; h[cnt]=x; up (cnt); } else if (!strcmp (op,"PM" )) cout<<h[1 ]<<endl; else if (!strcmp (op,"DM" )) { heap_swap (1 ,cnt); cnt--; down (1 ); } else if (!strcmp (op,"D" )) { cin>>k; k=ph[k]; heap_swap (k,cnt); cnt--; up (k); down (k); } else { cin>>k>>x; k=ph[k]; h[k]=x; up (k); down (k); } } return 0 ; }
七、哈希表
普通哈希
注意离散化和哈希的区别:后者是将一个范围很大的区间保持原有的顺序映射到一个小区间内,后者可能产生冲突,不一定保持原有的顺序;离散化是特殊的哈希
对哈希表产生冲突的两种解决方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 int h[N]; int find (int x) { int t = (x % N + N) % N; while (h[t] != null && h[t] != x) { t ++ ; if (t == N) t = 0 ; } return t; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void insert (int x) { int k = (x%N+N)%N; e[idx] = x; ne[idx] = h[k]; h[k] = idx++; } bool query (int x) { int k = (x%N+N)%N; for (int i=h[k];i!=-1 ;i=ne[i]) { if (e[i]==x) return true ; } return false ; }
注:哈希时取模一定要质数吗:
假如关键字是随机分布的,那么无所谓一定要模质数。但在实际中往往关键字有某种规律,例如大量的等差数列,那么公差和模数不互质的时候发生碰撞的概率会变大,而用质数就可以很大程度上回避这个问题
字符串哈希(kmp算法的劲敌)
核心:全称字符串前缀哈希法,把字符串变成一个p进制数字(哈希值),实现不同的字符串映射到不同的数字
功能:快速判断两个字符串是否相等时,使用字符串哈希
1 2 核心思想:将字符串看成P进制数,P的经验值是131 或13331 ,取这两个值的冲突概率低 小技巧:取模的数用2 ^64 ,这样直接用unsigned long long 存储,溢出的结果就是取模的结果
板子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 typedef unsigned long long ULL;ULL h[N], p[N]; p[0 ] = 1 ; for (int i = 1 ; i <= n; i ++ ){ h[i] = h[i - 1 ] * P + str[i]; p[i] = p[i - 1 ] * P; } ULL get (int l, int r) { return h[r] - h[l - 1 ] * p[r - l + 1 ]; }
详见:https://www.acwing.com/problem/content/843/
参考题解:https://www.acwing.com/solution/content/24738/
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 #include <iostream> #include <algorithm> #include <cstring> #include <cstdio> using namespace std;typedef unsigned long long ULL;const int N = 100010 ,P = 131 ;ULL h[N],p[N]; char str[N];int n,m;ULL get (int l,int r) { return h[r]-h[l-1 ]*p[r - l + 1 ]; } int main () { cin>>n>>m; scanf ("%s" ,str+1 ); p[0 ]=1 ; for (int i=1 ;i<=n;i++) { p[i] = p[i-1 ]*P; h[i] = h[i-1 ]*P + str[i]; } while (m--) { int l1,r1,l2,r2; cin>>l1>>r1>>l2>>r2; if (get (l1,r1)!=get (l2,r2)) puts ("No" ); else puts ("Yes" ); } return 0 ; }