来自AcWing算法基础课第二章笔记。
欢迎访问我的个人网站:
我的个人网站
将不定时更新一些笔记。
哈希表
两大块内容:
- 存储结构
- 开放寻址法
- 拉链法
- 字符串哈希方式
存储结构——开放寻址法与拉链法
哈希函数h(x)将 [ − 1 0 9 , 1 0 9 ] [-10^{9},10^{9}] [−109,109]的数映射到 [ 0 , 1 0 5 ] [0, 10^{5}] [0,105]之间的数。
离散化是需要保序的,h(x)需要保序。
可以看作是一种特殊的哈希函数。
-
哈希函数怎么写? x m o d 1 0 5 x mod 10 ^{5} xmod105 【一般模的长度要取成质数,要离2的整次幂尽可能远 (冲突的概率小)】
-
冲突(将若干不同的数映射到了同一个数)如何解决?
-
开放寻址法:【数组一般开两倍】
-
添加:先h(x)找到一个坑位,如果已被占用就找下一个坑位,以此类推
-
查找:先h(x)找到一个坑位,如果不是就继续往下找,直到找到一个空的坑位说明不存在
-
删除:查找后,打标记表示删除
-
-
拉链法:在每个槽上拉一个链。
-
算法题一般只会在哈希表中添加或查找数。如果要删除,开一个数组做标记表示是否删除。
模板题1 模拟散列表
题目
维护一个集合,支持如下几种操作:
I x
,插入一个数 x x x;Q x
,询问数 x x x是否在集合中出现过;
现在要进行 N N N次操作,对于每个询问操作输出对应的结果。
输入格式
第一行包含整数
N
N
N,表示操作数量。
接下来
N
N
N行,每行包含一个操作指令,操作指令为 I x
,Q x
中的一种。
输出格式
对于每个询问指令 Q x
,输出一个询问结果,如果
x
x
x在集合中出现过,则输出 Yes
,否则输出 No
。
每个结果占一行。
数据范围
1
≤
N
≤
1
0
5
1 \le N \le 10^{5}
1≤N≤105
− 1 0 9 ≤ x ≤ 1 0 9 -10^{9} \le x \le 10^{9} −109≤x≤109
输入样例:
5
I 1
I 2
I 3
Q 2
Q 5
输出样例:
Yes
No
模板
首先找到质数:
int main() {
//从200000开始往后找,找到最小的质数为止
for(int i = 200000; ; i ++ ) {
//flag表示是否是质数,一开始默认质数,如果后面能被整除,则认为不是质数
bool flag = true;
// 判断i能否被整除
for(int j = 2; j * j <= i; j ++) {
if(i % j == 0) {
flag = false;
break;
}
}
if(flag) {
cout << i << endl;
break;
}
}
}
拉链法:
#include <iostream>
using namespace std;
const int N = 100003;
// e[i]表示第i个位置上的值
// ne[i]表示下一个的位置
// idx表示当前的位置
int h[N], e[N], ne[N], idx;
void insert(int x) {
// 在第k个位置上插入x
int k = (x % N + N) % N; // 保证k属于[0, N] c++中负数mod负数仍为负数
e[idx] = x;
ne[idx] = h[k];
h[k] = idx ++ ;
}
bool find(int x) {
int k = (x % N + N) % N;
//从h[k]的位置开始寻找,每次用ne找下一个值,如果i=-1则说明没找到
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); //将所有槽清空,空指针用-1来表示
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;
}
开放寻址法:
#include <iostream>
using namespace std;
// null 表示未被占用(要在数据范围之外)
const int N = 200003, null = 0x3f3f3f3f; //N要开两倍
int h[N];
// 查找x,如果存在,返回x的位置,如果不存在,返回x应该存储的位置
int find(int x) {
int k = (x % N + N) % N;
//从应该存储的位置k开始往后找,如果找到第N-1个还没找到,从0开始重新找
while (h[k] != null && h[k] != x) {
k ++;
if (k == N) k = 0;
}
return k;
}
int main() {
int n;
scanf("%d", &n);
//memset是按字节,h是int型的数据,有四个字节,每个字节设置为3f
memset(h, 0x3f, sizeof h);
while(n --) {
char op[2];
int x;
scanf("%s%d", op, &x);
int k = find(x);
if(*op == 'I') {
h[k] = x;
}
else {
if (h[k] != null) puts("Yes");
else puts("No");
}
}
return 0;
}
字符串前缀哈希
基本思路
对于字符串str = “ABCDEFHJH”
h[0] = 0
h[1] = "A"的hash值
h[2] = "AB"的hash值
h[3] = "ABC"的hash值。。。
字符串hash值的定义:
eg. 对字符串"ABCD",将ABCD看作为4位数字组成的p进制的数字(如1 2 3 4 ),然后将该p进制数字转为十进制,后modQ
这样,就可以将任何一个字符串映射到0~Q-1之间的数字
注意:
- 一般情况下不能将字母映射为0(从1开始即可)。
- 假定完全不存在冲突。
当:
P = 131 或 13331 P = 131 或 13331 P=131或13331
Q = 2 64 Q = 2^{64} Q=264
在大部分情况下不会发生冲突。
这样,可以利用前缀哈希+字符串hash计算出任何一个字串hash值:
在字符串中,L-R位的hash值为:
h
[
R
]
−
h
[
L
−
1
]
×
p
R
−
L
+
1
h[R] - h[L -1] \times p^{R - L + 1}
h[R]−h[L−1]×pR−L+1
用unsigned long long 存储h,就可以默认mod 2 64 2^{64} 264,不用mod Q
预处理时,
h
[
i
]
=
h
[
i
−
1
]
×
p
+
s
t
r
[
i
]
h[i] = h [i - 1] \times p + str[i]
h[i]=h[i−1]×p+str[i]
模板题2 字符串哈希
#include <iostream>
using namespace std;
typedef unsigned long long ULL;
const int N = 100010, P = 131;
int n, m;
char str[N];
ULL h[N], p[N]; // h[i]用来表示0-i的字符串的hash值,p[i]用来表示P的i次方
// 求字符串l-r的hash值
ULL get(int l, int r) {
return h[r] - h[l-1] * p[r - l + 1];
}
int main() {
scanf("%d%d%s", &n, &m, 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;
scanf("%d%d%d%d", &l1, &r1, &l2, &r2);
if (get(l1, r1) == get(l2, r2)) puts("Yes");
else puts("No");
}
return 0;
}