数据结构 链表 单链表的操作
单链表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const int N=100010 ;int head,e[N],ne[N],idx;void init () { head=-1 ; idx=0 ; } void add_to_head (int x) { e[idx]=x,ne[idx]=head,head=idx++; } void add (int k,int x) { e[idx]=x,ne[idx]=ne[k],ne[k]=idx++; } void remove (int k) { ne[k]=ne[ne[k]]; }
双链表 示例:
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 #include <iostream> using namespace std;const int N = 1e5 + 10 ;int m;int e[N], l[N], r[N];int idx;void init () { l[1 ] = 0 , r[0 ] = 1 ; idx = 2 ; } void add (int k, int x) { e[idx] = x; l[idx] = k; r[idx] = r[k]; l[r[k]] = idx; r[k] = idx; idx++; } void remove (int k) { r[l[k]] = r[k]; l[r[k]] = l[k]; } int main (void ) { ios::sync_with_stdio (false ); cin >> m; init (); while (m--) { string op; cin >> op; int k, x; if (op=="R" ) { cin >> x; add (l[1 ], x); } else if (op=="L" ) { cin >> x; add (0 , x); } else if (op=="D" ) { cin >> k; remove (k + 1 ); } else if (op=="IL" ) { cin >> k >> x; add (l[k + 1 ], x); } else { cin >> k >> x; add (k + 1 , x); } } for (int i = r[0 ]; i != 1 ; i = r[i]) cout << e[i] << ' ' ; return 0 ; }
栈(先进后出) 用数组模拟:
1 2 3 4 5 6 7 8 9 10 11 12 13 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 21 22 23 24 25 const int N=100010 ;int stk[N],tt;int main () { int m; cin>>m; while (m--){ string op; int x; cin>>op; if (op=="push" ){ cin>>x; stk[tt++]=x; }else if (op=="pop" ){ tt--; }else if (op=="query" ){ cout<<stk[tt-1 ]<<endl; }else { if (!tt)cout<<"YES" <<endl; else cout<<"NO" <<endl; } } 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 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 #include <iostream> #include <stack> #include <string> #include <unordered_map> using namespace std;stack<int > num; stack<char > op; unordered_map<char , int > h{ {'+' , 1 }, {'-' , 1 }, {'*' ,2 }, {'/' , 2 } }; void eval () { int a = num.top (); num.pop (); int b = num.top (); num.pop (); char p = op.top (); op.pop (); int r = 0 ; if (p == '+' ) r = b + a; if (p == '-' ) r = b - a; if (p == '*' ) r = b * a; if (p == '/' ) r = b / a; num.push (r); } int main () { string s; cin >> s; for (int i = 0 ; i < s.size (); i++) { if (isdigit (s[i])) { int x = 0 , j = i; while (j < s.size () && isdigit (s[j])) { x = x * 10 + s[j] - '0' ; j++; } num.push (x); i = j - 1 ; } else if (s[i] == '(' ) { op.push (s[i]); } else if (s[i] == ')' ) { while (op.top () != '(' ) eval (); op.pop (); } else { while (op.size () && h[op.top ()] >= h[s[i]]) eval (); op.push (s[i]); } } while (op.size ()) eval (); cout << num.top () << endl; return 0 ; }
单调栈 常见模型:找出每个数左边离它最近的比它大/小的数,当该元素可以入栈的时候,栈顶元素就是它左侧第一个比它小的元素。
1 2 3 4 5 6 int tt = 0 ;for (int i = 1 ; i <= n; i ++ ){ while (tt && check (stk[tt], i)) tt -- ; stk[ ++ tt] = i; }
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 stack<int > stk; int main () { int n; cin >> n; stk.push (-1 ); for (int i = 0 ; i < n; i ++){ int x; cin >> x; while (stk.size () && stk.top () >= x) stk.pop (); cout << stk.top () << " " ; stk.push (x); } return 0 ; }
队列(先进后出) 就是一个特殊的数组。这个数组,最前面叫队头,最后面叫队尾。只允许在最后面添加元素,只允许在最前面删除元素。
普通队列 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int q[N], hh = 0 , tt = -1 ;q[ ++ tt] = x; hh ++ ; q[hh]; if (hh <= tt){ }
循环队列 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int q[N], hh = 0 , tt = 0 ;q[tt ++ ] = x; if (tt == N) tt = 0 ;hh ++ ; if (hh == N) hh = 0 ;q[hh]; if (hh != tt){ }
单调队列(滑动窗口) 什么是滑动窗口:滑动窗口是一种算法思想,就像在数据序列(可以是数组、字符串等 )上放一个大小固定或可变的 “窗口”。这个窗口会在数据序列上按照一定规则滑动,在滑动过程中,通过计算窗口内数据满足的条件(比如求和、计数等),来高效解决诸如寻找满足特定条件的最短子数组、最大子数组和等问题常见模型:找出滑动窗口中的最大值/最小值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 分析:如果当前的滑动窗口中有两个下标 i 和 j ,其中i在j的左侧(i<j),并且i对应的元素不大于j对应的元素(nums[i]≤nums[j]),则: 当滑动窗口向右移动时,只要 i 还在窗口中,那么 j 一定也还在窗口中。这是由于 i 在 j 的左侧所保证的。 因此,由于 nums[j] 的存在,nums[i] 一定不会是滑动窗口中的最大值了,我们可以将nums[i]永久地移除。 解题思路:因此我们可以使用一个队列存储所有还没有被移除的下标。在队列中,这些下标按照从小到大的顺序被存储,并且它们在数组nums中对应的值是严格单调递减的。 当滑动窗口向右移动时,我们需要把一个新的元素放入队列中。 为了保持队列的性质,我们会不断地将新的元素与队尾的元素相比较,如果新元素大于等于队尾元素,那么队尾的元素就可以被永久地移除,我们将其弹出队列。我们需要不断地进行此项操作,直到队列为空或者新的元素小于队尾的元素。 由于队列中下标对应的元素是严格单调递减的,因此此时队首下标对应的元素就是滑动窗口中的最大值。 窗口向右移动的时候。因此我们还需要不断从队首弹出元素保证队列中的所有元素都是窗口中的,因此当队头元素在窗口的左边的时候,弹出队头。
注意事项:
解决队首已经出窗口的问题;
解决队尾与当前元素 a[i]不满足单调性的问题;
将当前元素下标加入队尾;
如果满足条件则输出结果;
1 2 3 4 5 6 7 int hh = 0 , tt = -1 ;for (int i = 0 ; i < n; i ++ ){ while (hh <= tt && check_out (q[hh])) hh ++ ; while (hh <= tt && check (q[tt], i)) tt -- ; q[ ++ tt] = i; }
示例:
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 #include <iostream> #include <cstring> #include <algorithm> #include <deque> using namespace std;const int N = 1000010 ;int a[N];int main () { int n, k; cin >> n >> k; for (int i = 1 ; i <= n; i ++ ) cin >> a[i]; deque<int > q; for (int i = 1 ; i <= n; i++) { while (q.size () && q.back () > a[i]) q.pop_back (); q.push_back (a[i]); if (i - k >= 1 && q.front () == a[i - k]) q.pop_front (); if (i >= k) cout << q.front () <<" " ; } q.clear (); cout << endl; for (int i = 1 ; i <= n; i++) { while (q.size () && q.back () < a[i]) q.pop_back (); q.push_back (a[i]); if (i - k >= 1 && a[i - k] == q.front ()) q.pop_front (); if (i >= k) cout << q.front () << " " ; } }
KMP 字符串匹配 作用:
字符串模式匹配:判断一个模式串(子串)是否存在于一个主串中,以及找到模式串在主串中所有出现的位置。
预处理模式串构建 “部分匹配表,能够在匹配过程中跳过一些不可能匹配的位置,从而将时间复杂度从 O (n*m)(n 为主串长度,m 为模式串长度)优化到 O (n+m)。
解决字符串相关问题:
查找字符串中重复出现的子串
判断一个字符串是否为另一个字符串的子串
计算字符串的前缀函数,用于其他字符串算法中
核心思想:在每次失配时,不是把 p 串往后移一位,而是把 p 串往后移动至下一次可以和前面部分匹配的位置,这样就可以跳过大多数的失配步骤。而每次 p 串移动的步数就是通过查找 next[ ]数组确定的。
next 数组:对 next[ j ] ,是 p[ 1, j ]串中前缀和后缀相同的最大长度(部分匹配值),即 p[ 1, next[ j ] ] = p[ j - next[ j ] + 1, j ]。 KMP 的实现:求 next 数组和匹配字符串。下标从 1 开始的 kmp 算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 求模式串的Next数组: for (int i = 2 , j = 0 ; i <= m; 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 <= n; i ++ ){ while (j && s[i] != p[j + 1 ]) j = ne[j]; if (s[i] == p[j + 1 ]) j ++ ; if (j == m) { j = ne[j]; } }
示例:
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 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 ; }
下标从 0 开始的 kmp 算法
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 const int N = 1000010 ;int n, m;char s[N], p[N];int ne[N];int main () { cin >> m >> p >> n >> s; ne[0 ] = -1 ; for (int i = 1 , j = -1 ; i < m; i ++ ) { while (j >= 0 && p[j + 1 ] != p[i]) j = ne[j]; if (p[j + 1 ] == p[i]) j ++ ; ne[i] = j; } for (int i = 0 , j = -1 ; i < n; i ++ ) { while (j != -1 && s[i] != p[j + 1 ]) j = ne[j]; if (s[i] == p[j + 1 ]) j ++ ; if (j == m - 1 ) { cout << i - j << ' ' ; j = ne[j]; } } 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 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 const int N = 1e6 + 10 ;int l[N], r[N];char w[N];int idx = 0 ;int pre_create (int n) { cin >> w[n]; if (w[n] == '#' ) return -1 ; l[n] = pre_create (++idx); r[n] = pre_create (++idx); return n; } int in_create (int n) { if (w[n] == '#' ) return -1 ; l[n] = in_create (++idx); cin >> w[n]; r[n] = in_create (++idx); return n; } int back_create (int n) { if (w[n] == '#' ) return -1 ; l[n] = back_create (++idx); r[n] = back_create (++idx); cin >> w[n]; return n; } void pre_print (int n) { if (w[n] != '#' ) cout << w[n] << ' ' ; if (l[n] > 0 ) pre_print (l[n]); if (r[n] > 0 ) pre_print (r[n]); } void in_print (int n) { if (l[n] > 0 ) in_print (l[n]); if (w[n] != '#' ) cout << w[n] << ' ' ; if (r[n] > 0 ) in_print (r[n]); } void back_print (int n) { if (l[n] > 0 ) back_print (l[n]); if (r[n] > 0 ) back_print (r[n]); if (w[n] != '#' ) cout << w[n] << ' ' ; } void bfs (int root) { queue<int > que; que.push (root); while (!que.empty ()) { int t = que.front (); cout << w[t] << ' ' ; que.pop (); if (l[t] > 0 && w[l[t]] != '#' ) que.push (l[t]); if (r[t] > 0 && w[r[t]] != '#' ) que.push (r[t]); } }
Tire 树 又称字典树、前缀树高效地存储和查找字符串集合的数据结构,Trie 树是一种多叉树的结构,每个节点保存一个字符,一条路径表示一个字符串。 作用:
字符串前缀匹配:高效查找具有相同前缀的所有字符串
字符串快速检索:判断一个字符串是否存在于集合中,时间复杂度为 O (k)(k 为字符串长度),比哈希表在处理大量字符串时更有优势。
词频统计:统计某个字符串在集合中出现的次数,只需在 Trie 树的节点中记录计数信息。
自动补全与拼写检查:在搜索引擎、输入法等场景中,根据输入的前缀提示可能的完整字符串,或检查输入的字符串是否存在拼写错误。
字符串排序:对一组字符串进行排序时,通过 Trie 树的遍历(如先序遍历)可直接得到字典序排列,时间复杂度为 O (nk)
Trie 树的核心特点是通过共享字符串的公共前缀来节省存储空间,同时利用前缀特性实现高效的字符串操作,特别适合处理大量字符串且存在共同前缀的场景。
Trie 树中有个二维数组 son[N][26],表示当前结点的儿子,如果没有的话,可以等于++idx。Trie 树本质上是一颗多叉树,对于字母而言最多有 26 个子结点。所以这个数组包含了两条信息。比如:son[1][0]=2 表示 1 结点的一个值为 a 的子结点为结点 2;如果 son[1][0] = 0,则意味着没有值为 a 子结点。这里的 son[N][26]相当于链表中的 ne[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 int son[N][26 ], cnt[N], idx;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]; }
示例:
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 const int N = 100010 ;int son[N][26 ], 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; scanf ("%d" , &n); while (n -- ) { char op[2 ]; scanf ("%s%s" , op, str); if (*op == 'I' ) insert (str); else printf ("%d\n" , query (str)); } 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 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 #include <iostream> #include <algorithm> using namespace std;int const N=100010 ,M=31 *N;int n;int a[N];int son[M][2 ],idx;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 search (int x) { int p=0 ;int res=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 (void ) { cin.tie (0 ); cin>>n; idx=0 ; 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,search (a[i])); } cout<<res<<endl; }
并查集 开始时每个集合都是一个独立的集合,每个节点都指向自己的祖宗节点,当集合只有本身时,自身就指向自己。若想使得某两节点合并,那么只需要指向相同的祖宗就可以实现。并查集的核心优势是通过路径压缩和按秩合并两种优化策略,使查询和合并操作的时间复杂度接近常数级适合处理大规模数据。 作用:
判断元素是否属于同一集合
合并多个集合
处理动态连通性问题
解决等价类问题:识别具有等价关系的元素集合(如在数学中判断多个元素间的等价关系,或在图像分割中标记连通区域)。
优化某些算法的实现:在 Kruskal 算法(最小生成树)、求无向图的连通分量等场景中,用于快速判断边是否会形成环。
朴素并查集 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int p[N]; int find (int x) { if (p[x] != x) p[x] = find (p[x]); return p[x]; } for (int i = 1 ; i <= n; i ++ ) p[i] = i;p[find (a)] = find (b);
维护 size 的并查集 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int p[N], size[N]; int find (int x) { if (p[x] != x) p[x] = find (p[x]); return p[x]; } for (int i = 1 ; i <= n; i ++ ) { p[i] = i; size[i] = 1 ; } size[find (b)] += size[find (a)]; p[find (a)] = find (b);
维护到祖宗节点距离的并查集 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 int p[N], d[N]; int find (int x) { if (p[x] != x) { int u = find (p[x]); d[x] += d[p[x]]; p[x] = u; } return p[x]; } for (int i = 1 ; i <= n; i ++ ) { p[i] = i; d[i] = 0 ; } p[find (a)] = find (b); d[find (a)] = distance;
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const int N=100010 ;int p[N],n,m;int find (int x) { if (p[x]!=x)p[x]=find (p[x]); return p[x]; } int main () { scanf ("%d%d" ,&n,&m); for (int i=1 ;i<=n;i++)p[i]=i; while (m--){ char op[2 ]; int a,b; scanf ("%s%d%d" ,op,&a,&b); if (op[0 ]=='M' )p[find (a)]=find (b); else { if (find (a)==find (b))puts ("Yes" ); else puts ("No" ); } } return 0 ; }
堆(完全二叉树) 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值, 这种情况称为大顶堆,注意:没有要求结点的左孩子的值和右孩子的值的大小关系。
每个结点的值都小于或等于其左右孩子结点的值, 这种情况称为小顶堆。 作用:
堆是实现优先队列的最佳数据结构,能快速获取和删除最大 / 最小值(大顶堆取最大值,小顶堆取最小值),时间复杂度为 O (1),插入和删除操作复杂度为 O (log n)。
Top K 问题:在海量数据中快速找到前 K 个最大 / 最小元素
中位数查找:维护两个堆(一个大顶堆存较小一半元素,一个小顶堆存较大一半元素),能在 O (1) 时间内获取当前数据集的中位数,插入操作复杂度为 O (log n)。
堆排序:利用堆的特性进行排序,时间复杂度为 O (n log n),是一种原地排序算法(仅需 O (1) 额外空间)。
任务调度:在操作系统中用于进程调度,根据优先级动态选择下一个要执行的任务。 操作:
插入一个数,数据的插入都是插入到堆尾,然后再 up
求集合的最小或最大值
删除最小值
删除,修改任意一个元素,将该元素与堆尾元素进行交换,然后在该位置进行 up()或 down()操作,删除队尾元素
核心函数 :down(x),up(x)向上或向下调整一个数 使用一维数组模拟堆: 根节点位于头部,设某个元素的坐标为 i,那么它的左儿子为 2i,右儿子为 2i+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 26 27 28 29 30 31 32 33 34 35 36 37 int h[N], ph[N], hp[N], size;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 ; } } for (int i = n / 2 ; i; i -- ) down (i);
示例:
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 const int N=100010 ;int heap[N],cnt;void down (int u) { int t=u; if (u*2 <=cnt&&heap[u*2 ]<=heap[t])t=u*2 ; if (u*2 +1 <=cnt&&heap[u*2 +1 ]<=heap[t])t=u*2 +1 ; if (t!=u){ swap (heap[t],heap[u]); down (t); } } void up (int u) { while (u/2 &&heap[u/2 ]>heap[u]){ swap (heap[u/2 ],heap[u]); u>>=1 ; } } int main () { int n,m; scanf ("%d%d" ,&n,&m); for (int i=1 ;i<=n;i++)scanf ("%d" ,&heap[i]); cnt=n; for (int i=n/2 ;i;i--)down (i); while (m--){ printf ("%d " ,heap[1 ]); heap[1 ]=heap[cnt--]; down (1 ); } return 0 ; }
哈希 hash 哈希(Hash)是一种将任意长度的输入数据映射到固定长度输出的技术 作用:
快速查找与检索:通过哈希函数将键(Key)映射到哈希表的索引,实现 O (1) 平均时间复杂度的插入、删除和查找操作。
数据去重:对数据计算哈希值,通过比较哈希值快速判断数据是否重复
数据完整性校验:计算文件或消息的哈希值(如 MD5、SHA 系列),通过比对哈希值判断数据是否被篡改。
一般 hash(用数组模拟) 拉链法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int h[N], e[N], ne[N], idx;void insert (int x) { int k = (x % N + N) % N; e[idx] = x; ne[idx] = h[k]; h[k] = idx ++ ; } bool find (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 ; }
开放寻址法
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; }
字符串 hash 思想:
将字符串看成 P 进制的数,实现不同的字符串映射到不同的数字。(P 常取 131 或 13331,冲突概率低)
对 P 进制的数取模(1p^3+2 p^2+3p^1+4 p^0)mod Q(Q 取 2^64)
由上式可得: 使用 unsigned long long 来存储 h[],可实现自动取模
某个子串的哈希值:h[l,r]=h[R]-h[L]*p^(R-L+1)
前缀和公式:h[i+1]=h[i]*p+str[i];
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 ; }
容器 STL
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 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 vector, 变长数组,倍增的思想 size () 返回元素个数 empty () 返回是否为空 clear () 清空 front ()/back () push_back ()/pop_back () begin ()/end () [] 支持比较运算,按字典序 pair<int , int > first, 第一个元素 second, 第二个元素 支持比较运算,以first为第一关键字,以second为第二关键字(字典序) string,字符串 size ()/length () 返回字符串长度 empty () clear () substr (起始下标,(子串长度)) 返回子串 c_str () 返回字符串所在字符数组的起始地址 queue, 队列 size () empty () push () 向队尾插入一个元素 front () 返回队头元素 back () 返回队尾元素 pop () 弹出队头元素 priority_queue, 优先队列,默认是大根堆 size () empty () push () 插入一个元素 top () 返回堆顶元素 pop () 弹出堆顶元素 定义成小根堆的方式:priority_queue<int , vector<int >, greater<int >> q; stack, 栈 size () empty () push () 向栈顶插入一个元素 top () 返回栈顶元素 pop () 弹出栈顶元素 deque, 双端队列 size () empty () clear () front ()/back () push_back ()/pop_back () push_front ()/pop_front () begin ()/end () [] set, map, multiset, multimap, 基于平衡二叉树(红黑树),动态维护有序序列 size () empty () clear () begin ()/end () ++, -- 返回前驱和后继,时间复杂度 O (logn) set/multiset insert () 插入一个数 find () 查找一个数 count () 返回某一个数的个数 erase () (1 ) 输入是一个数x,删除所有x O (k + logn) (2 ) 输入一个迭代器,删除这个迭代器 lower_bound () /upper_bound () lower_bound (x) 返回大于等于x的最小的数的迭代器 upper_bound (x) 返回大于x的最小的数的迭代器 map/multimap insert () 插入的数是一个pair erase () 输入的参数是pair或者迭代器 find () [] 注意multimap不支持此操作。 时间复杂度是 O (logn) lower_bound () /upper_bound () unordered_set, unordered_map, unordered_multiset, unordered_multimap, 哈希表 和上面类似,增删改查的时间复杂度是 O (1 ) 不支持 lower_bound () /upper_bound () , 迭代器的++,-- bitset, 圧位 bitset<10000> s ; ~, &, |, ^ >>, << ==, != [] count () 返回有多少个1 any () 判断是否至少有一个1 none () 判断是否全为0 set () 把所有位置成1 set (k, v) 将第k位变成v reset () 把所有位变成0 flip () 等价于~ flip (k) 把第k位取反