1.Trie树-字典树
用来高效的存储和查找字符串集合的数据结构
const N int = 100010 //下标是0的节点,既是根节点,又是空节点 var son [N][26]int //串中每个节点的所有儿子,son[n][i]指的是节点n的儿子i,用来存储其在树中的下标,每个节点最多连26条边,因为一共26个字母 var cnt [N]int //以当前这个点结尾的单词有多少个 var idx int //存储当前用到的下标,trie树从空开始进行插入,不断地插入作为树的第几个节点 func insert(str []byte){ p := 0 for i:= 0 ; i < len(str) ; i++ { u := str[i]-'a' if son[p][u]==0 {//如果p节点不存在u儿子 idx ++ son[p][u] = idx } p = son[p][u] } cnt[p]++ } func query(str []byte) int {//字符串str出现的次数 p := 0 for i:= 0 ; i < len(str) ; i++ { u := str[i]-'a' if son[p][u] == 0 { return 0 } p = son[p][u] } return cnt[p] }
2.并查集 --重点重点
用来快速的处理:
1、将两个集合合并
2、询问两个元素是否在一个集合当中
并查集可以在接近O(1)的时间复杂度内完成上述两个操作
基本原理:用树的形式来维护集合,树的根节点的编号就是整个集合的编号,每个节点存储其父节点p[x]
问题1:如何判断节点是不是根节点 : if p[x] == x
问题2:如何求x的集合的编号: for p[x] != x {x = p[x]}
问题3:如何合并两个集合,在两个树之间加一条边:p[x]是x的集合编号,p[y]是y的集合编号,p[x]=y
目前针对解决问题2的时间复杂度比较高,对此可以进行优化:
当我们搜索找到x所在树的根节点后,我们将x所在的整个路径上所有节点都直接指向根节点
维护每个集合中节点的个数,在这里我们只需要保持根节点的size是正确的即可
3.堆(手写堆)
如何手写一个堆?
基本操作:
1、插入一个数
2、求这个集合中的最小值
3、删除最小值
4、删除任意一个元素
5、修改任意一个元素
堆的基本结构
堆是一棵完全二叉树
以小根堆为例,每个节点的值都是小于等于左右儿子的值,所以根节点是整棵树中最小的值
堆的存储
使用一维数组来进行存储
基本操作
down(x)
{
将节点x下移,如果节点x变大,则每次都和左右儿子比较,选择最小的那个进行交换,直到找到属于自己的位置
}
up(x)
{
将节点x上移,将x变小,则每次和父节点进行比较,如果我比父节点小,则和父节点进行交换
}
heap为堆,size是当前堆的大小
下标是从1开始的
1、插入一个数 size++ heap[size]=x up(size) 2、求这个集合中的最小值 heap[1] 3、删除最小值 heap[1]=heap[size] size-- down(heap[1]) 4、删除任意一个元素 heap[k] = heap[size] size-- if 值变大{ down(heap[k]) }else{ up(heap[k]) } 5、修改任意一个元素 heap[k]=x if 值变大{ down(heap[k]) }else{ up(heap[k]) }
//后续操作
#include <iostream> #include <algorithm> #include <string.h> 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 (u * 2 <= cnt && h[u * 2] < h[t]) t = u * 2; if (u * 2 + 1 <= cnt && 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; } } int main() { int n, m = 0; scanf("%d", &n); while (n -- ) { char op[5]; int k, x; scanf("%s", op); if (!strcmp(op, "I")) { scanf("%d", &x); cnt ++ ; m ++ ; ph[m] = cnt, hp[cnt] = m; h[cnt] = x; up(cnt); } else if (!strcmp(op, "PM")) printf("%d\n", h[1]); else if (!strcmp(op, "DM")) { heap_swap(1, cnt); cnt -- ; down(1); } else if (!strcmp(op, "D")) { scanf("%d", &k); k = ph[k]; heap_swap(k, cnt); cnt -- ; up(k); down(k); } else { scanf("%d%d", &k, &x); k = ph[k]; h[k] = x; up(k); down(k); } } return 0; }
4 hash表
hash表最主要的作用是将大规模数据映射到小的空间中
哈希表的存储结构
将值映射到hash表中可能会引起冲突,根据处理冲突的方式不同,将hash表的存储分为两种:
开放寻址法
取模的数一般是一个质数,并且离2的整数次幂尽可能远一些
#include <cstring> #include <iostream> using namespace std; const int N = 200003, null = 0x3f3f3f3f; int h[N]; int insert int find(int x)//如果x存在,则返回它当前所在的位置,否则返回她应该存储的位置 { int t = (x % N + N) % N; while (h[t] != null && h[t] != x) { t ++ ; if (t == N) t = 0; } return t; } int main() { memset(h, 0x3f, sizeof h); int n; scanf("%d", &n); while (n -- ) { char op[2]; int x; scanf("%s%d", op, &x); if (*op == 'I') h[find(x)] = x;//插入操作 else //查找操作 { if (h[find(x)] == null) puts("No"); else puts("Yes"); } } return 0; }
拉链法
取模的数一般取一个质数
#include <cstring> #include <iostream> using namespace std; const int N = 100003; 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; } int main() { int n; scanf("%d", &n); memset(h, -1, sizeof h); while (n -- ) { char op[2]; int x; scanf("%s%d", op, &x); if (*op == 'I') insert(x); else { if (find(x)) puts("Yes"); else puts("No"); } } return 0; }
常用的字符串的哈希方式
把字符串看成一个p进制的数
通过取模,把整个数字映射到从0~Q-1
注意:
1、一般情况下不要把其映射成0,映射成从1开始的值
2、假设在hash的过程中不存在冲突
3、经验值:p=131或者p=13331 Q=2的64次方
预处理以i为终点的前缀的hash值
应用:可以用来判断两段字符串是否相同--判断两段字符串的hash值是否相同