数据结构基础学习笔记二
1. Trie
作用:高效的储存和查找字符串集合的数据结构
原理:其实就是使用一种类似于链表的方法,储存字符串集合的时候,从根结点出发,判断根节点是否有链接到该字符串的第一个字符,没有的话就创建一个结点,让它和根结点链接,如果存在的话,就直接移动到该结点,然后查找该结点是否有和第二个字符有链接,依次类推,当每一个字符串的最后一个字符存储进去后,在这个结点使用一个标记,代表有一个以这个结点结束的字符,避免子串被母串所覆盖,或者有相同的字符串相覆盖的情况存在,查找字符串的时候类似,从根结点开始查找第一个字符,查找到了后就移动到第二个字符的结点然后查找这个结点是否和第三个结点相连接,一次类推。
代码实现
#include <iostream>
using namespace std;
const int N = 100010;、
int son[N][26],cnt[N],idx;//son[N][26]表示所有的结点,每一个结点有26个字母(不同的题目不一样),当需要创建结点的时候就直接从其中使用就是了,cnt[N]记录所有结点以当前结点为终点的字符串的数量是多少,idx记录当前使用到的下标
char str[N];
//向Trie树中存储字符串
void insert(char str[]){
int p = 0;//p表示当前从存储到了哪个结点,最开始是从根节点开始,所以p=0
for(int i = 0; str[i]; i ++){
int u = str[i] - 'a';
if(!son[p][u]) son[p][u] = ++ idx;//如果该结点的这个地方没有链接字符,就使用新的结点,让其链接起来,并且将idx++
p = son[p][u];//移动到下一个结点,注意,如果这是最后一重循环的话,下一个结点才是你当前存储的字符串的最后一个结点。
}
cnt[p] ++;//当前字符串存储完毕,该字符串存储的最后一个结点代表的字符串数量加一
}
//在Trie数中查找字符串
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];//如果按照链接的查找能够遍历完目标字符串的所有字符,那么就返回尾结点的字符串数量,如果为0的话说明该字符串集合中存在的其实是目标字符串的一个母串
}
int main()
{
int n;
cin >> n;
char op[2];
while(n --){
cin >> op >> str;
if (*op == 'I') insert(str);//插入
else printf("%d\n", query(str));//搜索
}
return 0;
}
2.并查集
作用:将两个集合合并,询问两个元素是否在一个集合当中
原理:每个集合用一棵树来表示,树根的编号就是整个集合的编号,每个结点存储它的父节点,p[x]表示x的父节点,根结点的父节点就让他等于自己的编号,所以查找x的集合编号的时候就依次向上遍历父节点是否等于自身的编号就是了,需要将两个集合合并的时候,只需要将其中任意一个的根节点的父节点指向另一个集合的父节点就行了。
路径压缩优化:朴素版本的并查集的时间复杂度依旧较高,主要原因是查找的必须向上遍历多次,并且每次都需要遍历,所以路径优化的原理就是,对于查找过根结点的结点,直接将它们的父节点指向根结点,这样在需要重复查找的时候就能更加快速,使用路径压缩优化后的时间复杂度接近O(1)
代码实现
#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;
for(int i = 1; i <= n; i ++) p[i] = i;//初始化所有点的父节点
while(m --){
char op[2];//如果使用字符变量而不是使用字符串的话,使用scanf读入的时候会读入些莫名其妙的空格或者回车
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;
}
其余变形的并查集无非就是多维护一些别的数据,接下来将给出两个例题。
连通块中点的数量
给定一个包含 n 个点(编号为 1∼n)的无向图,初始时图中没有边。
现在要进行 m 个操作,操作共有三种:
C a b
,在点 a 和点 b 之间连一条边,a 和 b 可能相等;Q1 a b
,询问点 a 和点 b 是否在同一个连通块中,a 和 b 可能相等;Q2 a
,询问点 a 所在连通块中点的数量;输入格式
第一行输入整数 n 和 m。
接下来 m 行,每行包含一个操作指令,指令为
C a b
,Q1 a b
或Q2 a
中的一种。输出格式
对于每个询问指令
Q1 a b
,如果 a 和 b 在同一个连通块中,则输出Yes
,否则输出No
。对于每个询问指令
Q2 a
,输出一个整数表示点 a 所在连通块中点的数量每个结果占一行。
数据范围
1 ≤ n , m ≤ 1 0 5 1≤n,m≤10^5 1≤n,m≤105
输入样例:
5 5 C 1 2 Q1 1 2 Q2 1 C 2 5 Q2 5
输出样例:
Yes 2 3
思路:此题是给定了n个点的无向图,这些点之间连接线后,可以形成连通块,同一个连通块可以只用一个集合来维护,所以就很容易的想到使用并查集,但是,操作当中需要寻找到连通块中点的数量,所以就需要有一个数组来维护连通块中点的数量是多少,注意,只需要维护每个根节点的这个变量就行了,只需要每个根节点的这个数字是正确的就能完成对应的操作。
#include <iostream>
#include <string>
using namespace std;
const int N = 100010;
int n,m;
int p[N],cnt[N];
int find(int x){
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++) p[i] = i, cnt[i] = 1;
while(m --){
char op[2];
int a,b;
cin >> op;
if(op[0] == 'C'){
cin >> a >> b;
if(find(a) == find(b)) continue;
p[find(a)] = find(b);
cnt[b] += cnt[a]; //连通块中点的数量的维护
}
else if(op[1] == '1'){
cin >> a >> b;
if(find(a) == find(b)) puts("Yes");
else puts("No");
}else{
cin >> a;
cout << cnt[p[a]] << endl;
}
}
return 0;
}
3.堆
手写堆最基本的操作:
- 插入一个数
- 求集合当中的最小值
- 删除最小值
- 删除任意一个元素
- 修改任意一个元素
前三个要求STL是可以实现的,最后两个是STL不能直接实现的。
堆是一棵完全二叉树,除了最后一层结点之外上层的所有结点都是满的,最后一层的结点是从左到右排列的,同时,(小根堆)堆满足所有的结点都小于左右子结点,所以很明显根节点就是最小值。
堆的存储是一种全新的存储方式,使用的是一维数组,x 结点的左儿子是 2x,右儿子是 2x+1,所以数组的下标从1开始比较方便。
主要函数是down()函数和up()函数,对传入的数据进行向上的维护或者向下的维护。
实现方式:heap[]数组模拟堆,size记录最后一个元素
- 插入一个数 heap[++size] = x; up(size);
- 求集合当中的最小值 heap[1];
- 删除最小值 heap[1] = heap[size];size–;down(1);
- 删除任意一个元素 heap[k] = heap[size];size–;down(k);up(k);
- 修改任意一个元素 heap[k] = x;down(k);up(k);
代码实现:
//堆的初始化,首先可以使用一个一个插入的方法,但是这个初始化的方法的时间复杂度是O(logn),另一种方法是将所有的数据先读入数组中,然后对n/2前的所有数进行一次down的操作,就能实现时间复杂度为O(n)的堆初始化
for(int i = n / 2; i; i --) down(i);
//down()函数 注意:size的初始化一定要放在调用down函数之前
void down(int x){
int t = x;
if(x * 2 <= size && h[x * 2] > h[t]) t = x * 2;
if(x * 2 + 1 <= size && h[x * 2 + 1] > h[t]) t = x * 2 + 1;
if(t != x)
{
swap(h[t],h[x]);
down(t);
}
}
//up函数
void up(int x){
while(x / 2 && h[x/2] > h[x]){
swip(h[x/2],h[x]);
x /= 2;
}
}