二、数据结构——单链表,双链表,栈,队列,单调栈,单调队列,KMP,Trie,并查集,堆,哈希表等内容。

        对于链表来说,由于new操作时间太长,因此,算法题中一般使用静态链表。

1.单链表

        采用数组实现单链表,可以直接开两个数据,一个数组存放数值,另外一个数据存放下一个元素(指针)。

示例:实现一个单链表,链表初始为空,支持三种操作:

向链表头插入一个数;删除第 k 个插入的数后面的数;
在第 k 个插入的数后插入一个数。
现在要对该链表进行 M 次操作,进行完所有操作后,从头到尾输出整个链表。

注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n个数,则按照插入的时间顺序,这 n 个数依次为:第 1个插入的数,第 2个插入的数,…第 n 个插入的数。

#include <iostream>
#include <algorithm>using namespace std;const int N = 100010;
int c = 10;int value[N];int ne[N];int head,tal; // tal表示尾部元素在数组中的位置void init(){head = -1;tal = 0;fill_n(&ne[0], sizeof(ne) / sizeof(ne[0]), -1);}void insert_head(int x){value[tal] = x;    // 存放元素ne[tal] = head;    // x指向head指向的元素head = tal++;      // 新的head指向x}void insert_k(int k,int x){ // 在数组第k个位置后插入元素,注意不是链表value[tal] = x;         // 存放节点元素ne[tal] = ne[k];        // value[k]指向xne[k] = tal++;          // x指向原value[k]下一个元素}void remove_k(int k){ // 此处没有释放空间,算法题一般不必考虑释放空间int tmp = ne[k];   // 保留删除前第k个元素的下一个位置ne[k] = ne[ne[k]]; // 第k个元素指向下一个元素的下个元素ne[tmp] = -1;     // 删除元素的下一个元素位置指向-1}int main(){int m;cin >> m;  // m次操作int k,x;   // k为位置,x为元素char O;    // 操作符init();    // 初始化while(m--){cin >> O;if(O == 'H'){cin >> x; insert_head(x);}  // 头差一个元素else if(O == 'D')   // 删除一个元素{cin >> k; if(k == 0) head = ne[head];else remove_k(k-1);}else if(O == 'I'){cin >> k >> x; insert_k(k-1,x);} // 第k个位置插入一个元素}for(int i = head;i!=-1;i=ne[i])printf("%d ",value[i]);return 0;}

 2.双链表

双链表使用两个数组分别表示前驱和后继,并用1个数据存储值。

插入操作:头插入,需要改头指针

中间插入,需要防止断链 

尾部插入需注意改尾指针。

实现一个双链表,双链表初始为空,支持 5种操作:

在最左侧插入一个数;
在最右侧插入一个数;
将第 k 个插入的数删除;
在第 k 个插入的数左侧插入一个数;
在第 k 个插入的数右侧插入一个数
现在要对该链表进行 M次操作,进行完所有操作后,从左到右输出整个链表。

注意:题目中第 k个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。

代码实现:在代码实现中,左侧插入考虑了节点为第一个节点,右侧插入考虑了最后一个节点。同时,为了在O(1)时间复杂度完成尾插,设置了表尾指针。

#include <iostream>
#include <algorithm>
using namespace std;const int N = 100010;
int value[N];
int l[N],r[N];
int head,tal,idx; // tal表示尾部元素在数组中的位置void init(){idx = 0;fill_n(&l[0], sizeof(l) / sizeof(l[0]), -1); // 数组初始化为-1fill_n(&r[0], sizeof(r) / sizeof(r[0]), -1);head = -1;tal = -1;}void insert_head(int x){value[idx] = x;    // 存放元素r[idx] = head;  // x指向第一个元素if(tal == -1) tal = idx; // 当前表为空,尾指针指向xelse l[head] = idx;    // 表不为空,第一个元素的前驱指向xhead = idx++;      // 新的head指向x}void insert_tal(int x){value[idx] = x;    // 存放元素l[idx] = tal;      // x的左指针指向最后一个元素if(head == -1) head = idx; // 表位空,头指针指向xelse r[tal] = idx;  // 表不为空,最后一个元素的后继指向xtal = idx++;      // 新的tal指向x}void insert_Rk(int k,int x){ // 在数组第k个位置后插入元素,注意不是链表value[idx] = x;         // 存放节点元素if(r[k] == -1) {l[idx] = tal; r[idx] = -1; r[tal] = idx;tal = idx++;} // 当前节点为最后一个节点else{l[idx] = k;        r[idx] = r[k];l[r[k]] = idx;r[k] = idx++;  }}void insert_Lk(int k,int x){ // 在数组第k个位置前插入元素,注意不是链表value[idx] = x;         // 存放节点元素if(l[k] == -1) {r[idx] = head; l[idx] = -1; l[head] = idx;head = idx++;} // 当前节点在第一个节点else{l[idx] = l[k];r[idx] = k;r[l[k]] = idx;l[k] = idx++;   }}void remove_k(int k){ // 此处没有释放空间,算法题一般不必考虑释放空间if(head == k) head = r[k];if(tal == k) tal = l[k];l[r[k]] = l[k];r[l[k]] = r[k];}int main(){int m;cin >> m;  // m次操作int k,x;   // k为位置,x为元素string O;    // 操作符init();    // 初始化while(m--){cin >> O;if(O == "L"){cin >> x; insert_head(x);}  // 头差一个元素else if(O == "R"){cin >> x; insert_tal(x);} else if(O == "D")   // 删除一个元素{cin >> k;remove_k(k-1);}else if(O == "IL"){cin >> k >> x;insert_Lk(k-1,x);} // 第k个位置插入一个元素else{cin >> k >> x; insert_Rk(k-1,x);}}for(int i = head;i!=-1;i=r[i])printf("%d ",value[i]);return 0;}

3.栈

        栈是先进后出的数据结构,top初始时指向0,代表指向下一个空位置,

  • 入栈时,先入元素,top++,
  • 出栈时,top--;再出元素。
  • top==0判空,top==n-1判满 
#include <iostream>
#include <algorithm>
using namespace std;const int N = 100010;
int sstack[N],top;void init(){top = 0; // top 指向下一个空位置
}void s_empty(){  // top指向0栈空,否则不空if(top) printf("NO\n");else printf("YES\n");
}void push(int x){  // 入栈,元素先入栈,top++sstack[top++] = x;
}int pop(){   // 出栈:top先减减,再出栈return sstack[--top];
}int query(){  // 查询栈顶元素int id = top-1;return sstack[id];
}int main(){int m;cin >> m;string op;int x;init();while(m--){cin >> op;if(op=="push") {cin >> x; push(x);}else if(op=="pop") int r = pop();else if(op == "empty") s_empty();else printf("%d\n",query());}
}

应用:表达式求值

算法思想:

使用一个数栈,一个符号栈,从左向右扫描表达式,若:

  • 符号栈为空,直接入栈 
  • 栈顶元素为(,直接入栈
  • 当前符号优先级大于栈顶元素,直接入栈
  • 否则从符号栈中弹出一个符号,并从数栈中弹出2个数字进行运算,直到满足相应入栈条件
#include <iostream>
#include <algorithm>
#include <stack>
#include <unordered_map>
#include <cstring>
using namespace std;const int N = 100010;
stack<char> op;
stack<int> number;void eval(){    // 实现运算int b = number.top(); // 栈是后进先出,第一个先出的元素是第二个操作数,第二个出的元素是第一个操作数number.pop();int a = number.top();number.pop();char c = op.top();    // 符号op.pop();if(c=='+') number.push(a+b);else if(c=='-') number.push(a-b);else if(c=='*') number.push(a*b);else number.push(a/b);
}int main(){unordered_map <char,int> pr = {{'+',1},{'-',1},{'*',2},{'/',2}};string A; // 表达式cin >> A;int n,j;for(int i=0;i<A.size();i++){n = 0;if(isdigit(A[i])){  // 处理数字j = i;while(isdigit(A[j])) // 数字可能有多位n = 10 * n + (A[j++]-'0'); // 数字是字符形式,转变为整数number.push(n);i = j-1;}else if(op.empty()) op.push(A[i]); // 符号栈为空,直接入栈else{ if(A[i]=='(') op.push(A[i]); // 栈顶元素为(,直接入栈else if(A[i]==')') {        // 当前符号为),从符号栈中弹出一个符号,并从数栈中弹出2个数字进行运算,直到遇到(while(op.top()!='(')eval();op.pop();  // 弹出(}else if(pr[A[i]]>pr[op.top()]) op.push(A[i]); // 当前符号优先级大于栈顶元素直接入栈else{while(!op.empty() && pr[A[i]]<=pr[op.top()] && op.top()!='(') // 否则从符号栈中弹出一个符号,并从数栈中弹出2个数字进行运算,直到当前符号优先级大于栈顶元素eval();op.push(A[i]); // 当前符号入栈}}}while(!op.empty()) eval(); // 计算剩余部分printf("%d ",number.top());return 0;
}

4.队列

为了解决假溢出问题,现在队列一般采用循环队列,舍弃一个存储单元

  • 初始化:r=f=0;即r指向队尾下一个空位置,f指向队首元素
  • 入队,先入元素,r=(r+1)%n
  •  出队,先出元素,f=(f+1)%n;
  • 判空:r==f;
  • 判满:(r+1)%n==f;
#include <iostream>
#include <algorithm>
using namespace std;const int N = 100010;
int Queue[N],r,f;void init(){r = f = 0; // r指向下一个空位置,f指向队首元素
}bool q_empty(){return r == f;
}bool full(){return (r+1) % N == f;
}void en_queue(int x){Queue[r] = x;r = (r+1)%N;
}void de_queue(){f = (f+1)%N;
}int query(){return Queue[f];
}int main(){int m;cin >> m;string op;int x;init();while(m--){cin >> op;if(op=="push") {cin >> x; en_queue(x);}else if(op=="pop") printf("%d\n",de_queue());else if(op == "empty") {if(q_empty()) printf("YES");else printf("NO");}else printf("%d\n",query());}
}

5.单调栈

        单调栈的应用场景为给定一个长度为 N的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。

        算法分析:如果采用暴力解,则需要2重循环,时间复杂度为O(n^2)。降低时间复杂度可以采用单调栈的方式,即对于元素a[i],若栈中元素大于a[i],则退栈,这样,每个元素进战出栈1次,时间复杂度为O(n).

#include <iostream>
#include <algorithm>
using namespace std;const int N = 100010;
int s[N];   // 栈
int top =0; // 栈顶指针int main(){int n; // 整数序列长度int x; // 当前数xscanf("%d",&n);for(int i=0;i<n;i++){scanf("%d",&x);if(top == 0) {s[top++] =x;printf("-1 ");} // 当前栈为空,输出结果为-1else{while(s[top-1]>=x) top--;   // 如果栈顶元素大于当前元素,元素出栈if(top == 0) printf("-1 "); // 出栈后栈顶为空,输出-1else printf("%d ",s[top-1]); // 如果栈不为空,栈顶元素即为所求s[top++] = x;}}}

6.单调队列

        单调队列的应用是找滑动窗口的最大值和最小值,这里的单调队列可以看做一个两头出一头进的双端队列,首先,使用队列实现滑动窗口,然后在队尾一侧使用栈构建单调队列,从而找最大值与最小值。

#include <iostream>
#include <algorithm>
using namespace std;
const int N= 1e6+10;
int q[N],a[N];   // q为双端队列,存储数组下标,a为数组
int r,f;        // 队列首尾指针int main(){int n,k; // n为整数序列长度,k为窗口大小scanf("%d%d",&n,&k); // n为序列长度,k为滑动窗口大小for(int i=0;i<n;i++) scanf("%d",&a[i]);r=f=0;  // 初始化,f指向队首元素,r指向队尾元素的下一个空位置for(int i=0;i<n;i++){ // 最小值if(f<r && q[f]<i-k+1) f=(f+1)%N;  // 队列不空,且队首元素不在滑动窗口,队首元素出队while(a[i]<= a[q[r-1]] && f<r) r=(r-1)%N; // 队列不空且队尾元素大于当前元素,队尾元素出队q[r]= i;            // 当前元素入队r= (r+1)%N;         // 队尾指针++if(i>=k-1) printf("%d ",a[q[f]]); // i=k-1为第一个窗口,开始输出元素}printf("\n");r=f=0;for(int i=0;i<n;i++){ // 最大值if(f<r && q[f]<i-k+1) f=(f+1)%N;while(a[i]>= a[q[r-1]] && f<r) r=(r-1)%N;q[r]= i;r= (r+1)%N;if(i>=k-1) printf("%d ",a[q[f]]);}return 0;
}

7.kmp算法

        kmp算法是利用子串的性质降低暴力解的时间复杂度。利用子串的最长公共前后缀使每次前缀的位置移动到后缀的位置。

 

最后next数组为最长公共前后缀的长度+1 

#include <iostream>
#include <algorithm>
using namespace std;const int N = 1e5+10;
const int M = 1e6+10;
char p[N],s[M];
int ne[N];int main(){int n,m;cin >> n;cin >> p+1;cin >> m;cin >> s+1;int i,j;for(i=2,j=0;i<=n;i++){ // 构建next数组while(j && p[i]!=p[j+1]) j = ne[j]; // 当前不匹配,子串移动,while循环主要用于解决多个子串匹配的情形if(p[i]==p[j+1]) j++;  // 当前匹配,继续匹配ne[i] = j;        // 构建当前元素的next值}//for(i=0;i<n;i++) printf("%d ",ne[i]);//printf("\n");for(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); // 输出下标从0开始j = ne[j];  // 继续匹配下一个子串}}return 0;
}

8.trile树

         trile最重要的作用为高效的存储字符串,并能够很好的统计字符串出现的次数。其结构如下:

实例:

维护一个字符串集合,支持两种操作:

  1. I x 向集合中插入一个字符串 x;
  2. Q x 询问一个字符串在集合中出现了多少次。

共有 N 个操作,所有输入的字符串总长度不超过 105105,字符串仅包含小写英文字母。

#include <iostream>using namespace std;const int N = 1e5+10;
int son[N][26]; // 存储子节点行号
int cnt[N];     // 字符串出现次数,注意,这是以字符串最后字符的创建为id
int idx = 0;    // 创建节点的id
char str[N];    // 创建节点void insert_t(char str[]){int p = 0;int u;for(int i=0;str[i];i++){u = str[i] - 'a'; // 字符索引if(!son[p][u]) son[p][u] = ++idx; // 无节点,创建节点,值为创建时的id,可通过此id索引字符串,子节点也为对应的行p = son[p][u];   // 有子节点,进入子节点}cnt[p]++;  // 字符串出现次数+1
}int query(char str[]){int p = 0;int u;for(int i=0;str[i];i++){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[0]=='I') insert_t(str);else printf("%d\n",query(str));}return 0;
}

应用:最大异或对

在给定的 N 个整数 A1,A2 ......An 中选出两个进行 or (异或)运算,得到的结果最大是多少?

        算法思想:如果采用暴力解,则需要2重循环,时间复杂度为O(N^2)。分析异或运算过程,从最高位开始, 相同结果为0,不同结果为1,因此,我们可以以各位数字建立一颗二叉树,从而简化查找长度,查找某个数的最大异或的数时,从高位开始,尽量进入具有不同数的分支中

#include <iostream>using namespace std;const int N = 1e7+10;
int son[N][2];
int a[N];
int idx = 0;void insert_t(int x){int p = 0;int s;for(int i=30;i>=0;i--){ // 创建trile树s = x >> i & 1; // 右移i位或1,即判断第i位是0还是1if(!son[p][s]) son[p][s] = ++idx; // 以创建节点的id作为子节点索引p = son[p][s]; // 进入子节点}
}int query(int x){int p = 0;int s;int res = 0;for(int i=30;i>=0;i--){s = x >> i & 1; // 右移i位或1,即判断第i位是0还是1if(son[p][!s]) {res += 1 << i;p = son[p][!s];} // 异或运算,该位不同结果最大,进入该位不同的分支else p = son[p][s]; // 所有数该位相同,进入子树}return res;
}int main(){int n;scanf("%d",&n);for(int i=0;i<n;i++){scanf("%d",&a[i]);insert_t(a[i]);}int res = 0;for(int i=0;i<n;i++){ res = max(res,query(a[i]));}printf("%d",res);return 0;
}

9.并查集

作用:在近似于O(1)的时间复杂度内完成以下2种操作:
1.将两个集合合并
2. 询问两个元素是否在一个集合当中
基本原理: 每个集合用一棵树来表示。树根的编号就是整个集合的编号。每个节点存储
它的父节点,p[x]表示x的父节点
问题1:如何判断树根: if (p[x] == x)
问题2:如何求x的集合编号: while (p[x] != x) x = p[x];优化:路径压缩

问题3:如何合并两个集合: px 是x的集合编号,py是y的集合编号。p[x] = y

一共有 n 个数,编号是 1∼n,最开始每个数各自在一个集合中。

现在要进行 m个操作,操作共有两种:

  1. M a b,将编号为 a和 b的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
  2. Q a b,询问编号为 a和 b的两个数是否在同一个集合中;
#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]);  // 路径压缩,x的父节点指向根return p[x];
}int main(){int n,m;scanf("%d%d",&n,&m);for(int i=1;i<=n;i++) p[i] = i; // 初始化,每个节点指向自己的集合char op[2];int a,b;while(m--){scanf("%s%d%d",op,&a,&b);if(op[0]=='M') p[find(a)] = find(b); // 合并,a集合的根指向belse{if(find(a)==find(b)) printf("Yes\n"); // 同一集合else printf("No\n");}}return 0;
}

变体: 

给定一个包含 n 个点(编号为 1~ n)的无向图,初始时图中没有边
现在要进行 m个操作,操作共有三种
1. c a b ,在点a和点6之间连 条边,a和6可能相等
2. Q1 a b ,询问点 a 和点6 是否在同一个连通块中,a 和6可能相等
3.Q2 a ,询问点 a 所在连通块中点的数量 

        分析:此处需要求连通块中点的数量,因此需要额外维护一个信息,即每个集合元素的个数(只有根节点有意义)。

#include <iostream>
using namespace std;const int N = 1e5+10;
int p[N];   // 父节点
int s[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;s[i] = 1;}char op[5];int a,b;while(m--){scanf("%s",op);if(op[0]=='C') {   scanf("%d%d",&a,&b);if(find(a)==find(b)) continue;s[find(b)] += s[find(a)];  // 集合节点数目增加p[find(a)] = find(b);}else if(op[1]=='1'){scanf("%d%d",&a,&b);if(find(a)==find(b)) printf("Yes\n");else printf("No\n");}else{scanf("%d",&a);printf("%d\n",s[find(a)]);}}return 0;
}

食物链问题

动物王国中有三类动物 4,B,C,这三类动物的食物链构成了有趣的环形。A吃B,B吃C,C吃A.
现有个动物,以1~ N 编号每个动物都是 A,B,C中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这 N个动物所构成的食物链关系进行描述:
第一种说法是 1XY,表示X和Y是同类
第二种说法是 2 XY,表示X吃Y
此人对 N 个动物,用上述两种说法,一句接一句地说出 K 句话,这 K 句话有的是真的,有的是假的
当一句话满足下列三条之一时,这句话就是假话,否则就是真话.
1.当前的话与前面的某些真的话冲突,就是假话
2.当前的话中 X或Y比N大,就是假话:
3.当前的话表示 X 吃 X,就是假话.
你的任务是根据给定的 N 和 K 句话,输出假话的总数 

算法思想:该问题为3个种群的关系问题,可以采用并查集处理。额外维护一个距离根节点的深度信息,若d[find(x)]-d[find(y)]模3为0,表明是同类。若d[find(x)]-d[find(y)]模3为1,则表明x是y的天敌。代码如下:

10.堆

如何手写一个堆?

1. 插入一个数               heap[ ++ size] = x; up(size);  插到末尾,自下向上调整堆
2.求集合当中的最小值  heap[1];(小根堆)
3. 删除最小值                heap[1] = heap[size]; szie -- ; down(1);
4.删除任意一个元素      heap[k] = heap[size]; size -- ; down(k); up(k)
5.修改任意一个元素      heap[k] = x; down(k); up(k);

输入一个长度为 n 的整数数列,从小到大输出前 m 小的数

#include <iostream>
#include <algorithm>
using namespace std;const int N = 1e5+10;
int heap[N],hsize; // hsize表示堆的大小void down(int i){  // 元素下沉操作int t;while(2*i <= hsize){  // 节点i(从1开始),其做孩子为2i,右孩子为2i+1,t = 2 * i;if(2*i+1 <= hsize && heap[2*i+1] < heap[2*i]) t++;  // 孩子中的较小者比根小,则交换//printf("%d ",heap[t]);if(heap[t]<heap[i]) {swap(heap[t],heap[i]);i = t;}  else break;}
}int main(){int n,m;scanf("%d%d",&n,&m);for(int i=1;i<=n;i++) scanf("%d",&heap[i]);hsize = n;for(int i=n/2;i>0;i--) down(i); // 从n/2开始下沉,表示从第n-1开始下沉,可实现在O(n)时间复杂度建堆while(m--){printf("%d ",heap[1]);swap(heap[1],heap[hsize]); // 将堆首元素输出hsize--;down(1); // 向下调整堆}return 0;
}

维护一个集合,初始时集合为空,支持如下几种操作
1.I x,插入一个数2;
2.PM ,输出当前集合中的最小值
3.DM,删除当前集合中的最小值 (数据保证此时的最小值唯一)
4.DK,删除第k个插入的数;
5.C k x ,修改第k个插入的数,将其变为 a
现在要进行N次操作,对于所有第 2 个操作,输出当前集合的最小值 

#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;const int N = 1e5+10;
int heap[N],hp[N],ph[N],hsize; // ph[k]将插入的第k个元素对应到堆的索引j;hp[j]将堆中数组下标为i对应到第k个插入元素void h_swap(int a,int b){   // 不仅需要交换堆元素,还需要维护额外的信息swap(ph[hp[a]],ph[hp[b]]); // 交换元素在堆中的索引swap(heap[a],heap[b]);  // 交换元素swap(hp[a],hp[b]);      // 交换堆索引对应插入元素的顺序
}void down(int i){  // 自上而下调整堆int t;while(2*i <= hsize){t = 2 * i;if(2*i+1 <= hsize && heap[2*i+1] < heap[2*i]) t++;//printf("%d ",heap[t]);if(heap[t]<heap[i]) {h_swap(t,i);i = t;}else break;}
}void up(int i){   // 自下而上调整堆while(i/2 && heap[i/2]>heap[i]){h_swap(i/2,i);i = i/2;}
}int main(){int n;scanf("%d",&n);char op[5];int k,x;int c=0;    // 计数while(n--){scanf("%s",op);if(!strcmp(op,"I")){hsize++,c++;  // 索引++,堆大小++scanf("%d",&heap[hsize]);hp[hsize] =  c;  // 堆中数组下标为i对应到第k个插入元素ph[c] = hsize;   // 插入的第k个元素对应到堆的索引jup(hsize);   // 插入元素,自上而下调整堆}else if(!strcmp(op,"PM")) printf("%d\n",heap[1]); // 输出堆首元素else if(!strcmp(op,"DM")) { // 输出堆首元素h_swap(1,hsize);  // 交换堆首和堆尾元素hsize--;down(1);         // 自上而下调整堆}else if(!strcmp(op,"D")){scanf("%d",&k);  // 删除第k个插入的元素k = ph[k];    // 第k个插入的元素对应堆的索引h_swap(k,hsize); // 交换该元素和堆尾元素hsize--;down(k);up(k);  // 实际只会执行一种调整,为了书写方便}else{  // 更改第k个插入元素的值scanf("%d%d",&k,&x);k = ph[k];  // 第k个插入的元素对应堆的索引 heap[k] = x; // 更改值down(k);up(k); // 实际只会执行一种调整,为了书写方便}}return 0;
}

11.哈希表

        哈希表是一种高效的数据结构,它通过哈希函数将键映射到存储位置,从而实现快速的插入、删除和查找操作,理想情况下时间复杂度为O(1)。然而,在实际应用中可能会遇到冲突,即不同的键映射到同一位置。为解决这些冲突,哈希表采用了链地址法、开放地址法等方法。

实例:此处采用链地址法解决冲突

维护一个集合,支持如下几种操作
1.I x ,插入一个整数a;
2.Q x ,询问整数 是否在集合中出现过
现在要进行 N 次操作,对于每个询问操作输出对应的结果 

#include <iostream>using namespace std;
const int N = 1e5+7;typedef struct Node
{int data;Node* next;};Node* H[N];void insert(int x){Node* node = new Node();node->data = x;node->next = NULL;int k = (x % N + N)%N; // 哈希函数映射位置,此处处理了负值的情况Node* p = H[k];if(!p) H[k] = node;  // 表位置为空,直接插入else{       // 位置不空,插入到表头node->next = p->next;p->next = node;}
}void find(int x){int k = (x % N + N)%N;Node* p = H[k];while (p) // 表位置不空,进入查找{   if(p->data == x) break;p = p->next;}if(!p) printf("No\n");else printf("Yes\n");
}int main(){char op[5]; // 运算符int x;  // 值int m; // m次操作scanf("%d",&m);while (m--){scanf("%s%d",op,&x);if(op[0]=='I') insert(x);else find(x);}return 0;
}

字符串哈希

(1)字符串哈希值的表示

        将字符串哈希用p进制的数表示。例如:“ABCD”的值用H=ASCII(A)*p^3+ASCII(B)*p^2+ASCII(C)*p^1+ASCII(D)*p^0,然后字符串的哈希值为H%Q,其中当p=131或13331,Q=2^64时,不会发生冲突,此时unsigned long long类型的表示即为对应的哈希值。

(2)用处

        可以在O(1)的时间复杂度内求出[l,r]区间子串的哈希值。

12.STL库

在C++中,vector是一个动态数组,其大小可以在运行时更改。初始化vector的方式有多种,这取决于你想如何初始化它。以下是一些示例:

  1. 默认初始化​​​​​​​

std::vector<int> vec;

这将创建一个没有任何元素的空vector
2. 使用给定的大小初始化

std::vector<int> vec(10); // 创建一个大小为10的vector,所有元素默认初始化为0(对于int类型)

3.使用给定的大小和值初始化

std::vector<int> vec(10, 5); // 创建一个大小为10的vector,所有元素初始化为5

4.使用初始化列表初始化

std::vector<int> vec = {1, 2, 3, 4, 5}; // 创建一个包含5个元素的vector

5.通过复制另一个vector初始化

std::vector<int> vec1 = {1, 2, 3, 4, 5};
std::vector<int> vec2(vec1); // 创建一个与vec1相同的vector

在C++中,std::vector 提供了大量的成员函数来支持各种操作,包括访问元素、修改元素、管理大小和容量等。以下是一些常用的 std::vector 成员函数:

访问元素

  • at(size_type pos): 访问指定位置的元素,并进行边界检查。
  • operator[]: 通过索引访问元素,不进行边界检查(更快,但使用时要小心)。
  • front(): 访问第一个元素。
  • back(): 访问最后一个元素。

修改元素

  • push_back(const T& value): 在末尾添加一个元素。
  • pop_back(): 移除末尾的元素。
  • insert(iterator pos, const T& value): 在指定位置插入一个元素。
  • erase(iterator pos): 移除指定位置的元素。
  • erase(iterator first, iterator last): 移除一系列元素。
  • swap(vector& other): 交换两个向量的内容。
  • clear(): 清除所有元素。
  • emplace(const_iterator pos, args): 在指定位置构造并插入一个元素(C++11起)。
  • emplace_back(args): 在末尾构造并添加一个元素(C++11起)。

大小和容量

  • size(): 返回元素的数量。
  • empty(): 检查是否为空。
  • capacity(): 返回当前已分配的空间能容纳的元素数量。
  • resize(size_type n): 改变大小。
  • reserve(size_type n): 预留空间。
  • shrink_to_fit(): 请求移除未使用的容量(C++11起,非绑定性)。

其他

  • assign(size_type n, const T& value): 替换所有元素。
  • begin(): 返回指向第一个元素的迭代器。
  • end(): 返回指向最后一个元素之后位置的迭代器。
  • cbegin()cend(): 返回常量迭代器(C++11起)。
  • rbegin()rend(): 返回反向迭代器。
  • crbegin()crend(): 返回常量反向迭代器(C++11起)。
  • emplace_iteratorbegin(size_type)end(size_type): (C++20起)用于支持结构化绑定的新接口。

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位取反

参考:常用代码模板2——数据结构 - AcWing

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/718065.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一个教材上的CMS网站源码在Linux服务器上登录时验证码正常,但在windows下不能正常显示

一个教材上的CMS网站源码在Linux服务器上登录时验证码正常&#xff0c;但在windows下不能正常显示。 在linux服务器上能正常显示。显示界面如下所示&#xff1a;

蜻蜓FM语音下载(mediadown)

一、介绍 蜻蜓FM语音下载&#xff08;mediadown&#xff09;&#xff0c;能够帮助你下载蜻蜓FM音频节目。如果你是蜻蜓FM会员&#xff0c;它还能帮你下载会员节目。 二、下载地址 本站下载&#xff1a;蜻蜓FM语音下载&#xff08;mediadown&#xff09; 百度网盘下载&#…

【Redis 主从复制】

文章目录 1 :peach:环境配置:peach:1.1 :apple:三种配置方式:apple:1.2 :apple:验证:apple:1.3 :apple:断开复制和切主:apple:1.4 :apple:安全性:apple:1.5 :apple:只读:apple:1.6 :apple:传输延迟:apple: 2 :peach:拓扑结构:peach:2.1 :apple:⼀主⼀从结构:apple:2.2 :apple:⼀…

【MetaGPT】配置教程

MetaGPT配置教程&#xff08;使用智谱AI的GLM-4&#xff09; 文章目录 MetaGPT配置教程&#xff08;使用智谱AI的GLM-4&#xff09;零、为什么要学MetaGPT一、配置环境二、克隆代码仓库三、设置智谱AI配置四、 示例demo&#xff08;狼羊对决&#xff09;五、参考链接 零、为什么…

大数据技术(一)

大数据技术概述 大数据技术层面及其功能 数据采集与预处理 利用ETL(extract-transform-load)工具将分布的、异构数据源中的数据&#xff0c;如关系数据、平面数据文件等&#xff0c;抽取到临时中间层后进行清洗、转换、集成&#xff0c;最后加载到数据仓库或数据集市中&…

前端canvas项目实战——简历制作网站(五):右侧属性栏(字体、字号、行间距)

目录 前言一、效果展示二、实现步骤1. 优化代码&#xff0c;提取常量2. 实现3个编辑模块3. 实现updateFontProperty方法4. 一个常见的用法&#xff1a;仅更新当前选中文字的样式 三、Show u the code后记 前言 上一篇博文中&#xff0c;我们扩充了线条对象&#xff08;fabric.…

Dockerfile构建过程详解

Dockerfile介绍 docker是用来构建docker镜像的文件&#xff01;命令参数脚本&#xff01; 构建步骤&#xff1a; 1、编写一个dockerfile文件 2、docker build构建成为一个镜像 3、docker run 运行镜像 …

PDF转Excel的未来:人工智能技术如何提升转换效率和准确性

随着信息技术的快速发展&#xff0c;PDF和Excel作为两种重要的文件格式&#xff0c;在日常生活和工作中扮演着至关重要的角色。PDF以其独特的跨平台阅读特性&#xff0c;成为了文件分享和传输的首选格式&#xff1b;而Excel则以其强大的数据处理能力&#xff0c;成为了数据分析…

【二分查找】【C++算法】378. 有序矩阵中第 K 小的元素

作者推荐 视频算法专题 本文涉及的基础知识点 二分查找算法合集 LeetCode378. 有序矩阵中第 K 小的元素 给你一个 n x n 矩阵 matrix &#xff0c;其中每行和每列元素均按升序排序&#xff0c;找到矩阵中第 k 小的元素。 请注意&#xff0c;它是 排序后 的第 k 小元素&…

机器人持续学习基准LIBERO系列10——文件结构

0.前置 机器人持续学习基准LIBERO系列1——基本介绍与安装测试机器人持续学习基准LIBERO系列2——路径与基准基本信息机器人持续学习基准LIBERO系列3——相机画面可视化及单步移动更新机器人持续学习基准LIBERO系列4——robosuite最基本demo机器人持续学习基准LIBERO系列5——…

力扣日记3.3-【回溯算法篇】332. 重新安排行程

力扣日记&#xff1a;【回溯算法篇】332. 重新安排行程 日期&#xff1a;2023.3.3 参考&#xff1a;代码随想录、力扣 ps&#xff1a;因为是困难题&#xff0c;望而却步了一星期。。。T^T 332. 重新安排行程 题目描述 难度&#xff1a;困难 给你一份航线列表 tickets &#xf…

关于脉冲负载应用中电阻器,您需要了解的 11 件事?

不幸的是&#xff0c;电阻器在脉冲负载下可能会失效。当脉冲功率耗散到器件的电阻元件时&#xff0c;它会产生热量并增加电阻器的温度。过热会损坏电阻元件&#xff0c;导致电阻变化甚至设备开路。为了避免在设计中出现这种情况&#xff0c;以下是您在选择元件时应了解的有关电…

excel统计分析——拉丁方设计

参考资料&#xff1a;生物统计学 拉丁方设计也是随机区组设计&#xff0c;是对随机区组设计的一种改进。它在行的方向和列的方向都可以看成区组&#xff0c;因此能实现双向误差的控制。在一般的试验设计中&#xff0c;拉丁方常被看作双区组设计&#xff0c;用于提高发现处理效应…

Skipped breakpoint at because it happened inside debugger evaluation亲测可用

问题描述&#xff1a; 在多线程项目中&#xff0c;在idea中打断点时&#xff0c;有时会遇到下面这种情况&#xff1a; idea左下角出现一行红底或者绿底文字提示&#xff1a; Skipped breakpoint at because it happened inside debugger evaluation 然后我们能感受到的就是…

HTML中自定义鼠标右键菜单

今天突然有人跟我提到了HTML中如何自定义鼠标右键菜单&#xff0c;这里大概记录一下吧&#xff0c;方便下次直接复制。免得还去看API文档。 文章目录 HTML中自定义鼠标右键菜单结果如下所示可以稍微改一下鼠标悬浮到右键菜单时的样式结果如下所示 只在某个特定的div才可以显示…

C++自学精简实践教程

一、介绍 1.1 教程特点 一篇文章从入门到就业有图有真相&#xff0c;有测试用例&#xff0c;有作业&#xff1b;提供框架代码&#xff0c;作业只需要代码填空规范开发习惯&#xff0c;培养设计能力 1.2 参考书 唯一参考书《C Primer 第5版》​参考书下载&#xff1a; 蓝奏云…

STL——stack

目录 stack stack都有哪些接口 模拟实现一个stack stack 1. stack是一种容器适配器&#xff0c;专门用在具有后进先出操作的上下文环境中&#xff0c;其删除只能从容器的一端进行元素的插入与提取操作。 2. stack是作为容器适配器被实现的&#xff0c;容器适配器即…

数据分析-Pandas数据的画图设置

数据分析-Pandas数据的画图设置 数据分析和处理中&#xff0c;难免会遇到各种数据&#xff0c;那么数据呈现怎样的规律呢&#xff1f;不管金融数据&#xff0c;风控数据&#xff0c;营销数据等等&#xff0c;莫不如此。如何通过图示展示数据的规律&#xff1f; 数据表&#x…

春招!启动了

大家好&#xff0c;我是洋子。今年的春招很多企业已经开始招聘了&#xff0c;像美团今年继续发力&#xff0c;24届春招以及25届暑期转正实习一共招聘4000人。另外&#xff0c;阿里&#xff0c;京东&#xff0c;顺丰等公司也已经开始春招&#xff0c;可以说招聘的号角已经正式吹…

十二、类与声明

类与声明 什么是类&#xff1f; 前情总结 前面22讲的课基本上就做了两件事 学习C#的基本元素学习类的成员 析构函数&#xff1a; 当对象不再被引用的时候&#xff0c;就会被垃圾回收器gc&#xff0c;回收。而收回的过程当中&#xff0c;如果需要做什么事情&#xff0c;就放在…