AcWing:哈希表
创始人
2024-05-29 15:50:21

哈希表理论基础

哈希表有两大要点:

  1. 存储结构

  1. 开放寻址法

  1. 拉链法

  1. 字符串哈希

哈希表的最主要作用是将一个比较大的范围(如10^9)映射到一个比较小的空间(0 ~ N,其中N一般为10^5 - 10^6),通过一个哈希函数h(x)完成上述操作。

一般情况下哈希函数的写法,以上述为例:

h(x) = x % 1e5;    // x mod 10^5
// 一般来说哈希函数中模的那个数要取成一个质数且离2的整次幂尽可能的远
// 数学上证明这样取的概率造成哈希冲突的概率是最低的

这里很容易产生哈希冲突,可能会把若干不同的数映射为同一个数。处理冲突的方法有两种:开放寻址法和拉链法。

拉链法

开辟一个10^5大小的空间,每个空间相当于一个槽,每次映射到某个位置就在下面连接一条链。

其实就是单链表的存储方式。

拉链法是把所有的同义词用单链表链接起来的方法。在这种方法中,哈希表的每个单元存储的不再是元素本身,而是相应同义词单链表的头指针(注意是头指针而不是头节点)。

对于单链表,我们可以采用数组的方式进行实现。此外,使用拉链法时,m的大小通常和 n差不多。例如,若 n ≤ 10^5 ,我们可以寻找大于等于10^5 的第一个质数,即 m = 100003 。

// 求符合要求的数:
#include 
using namespace std;
int main(){for(int i = 100000; ; i++){bool flag = true;for(int j = 2; j * j <= i; j++){if(i % j == 0){flag = false;break;}}if(flag){cout << i << endl;break;}}return 0;
}
// 输出100003,说明大于100000的最小的质数为100003,于是用这个数作为哈希函数的模的数

平均情况下我们可以将每条链看作“很短”,因此一般情况下哈希表的时间复杂度可以看成O(1)。一般算法题中不需要我们对哈希表进行删除操作,基本都是添加和查找操作。

#include 
#include using namespace std;const int N = 100003;
int n;
int h[N], e[N], ne[N], idx;void insert(int x){// 防止k为负数int k = (x % N + N) % N;e[idx] = x;ne[idx] = h[k];// h[k]看成head头指针。h[k]=idx++的原因是由于头插法导致头指针在不断改变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(){scanf("%d", &n);// 初始化链表(链表中-1代表NULL)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");}}
}

看似有很多链表其实只开了两个数组构造了一个链表,通过把hk改成各个链表起始指针的方式吧各个链表拼接起来。

过程中开了next[N]想用这个表示链表元素的下一个元素,但是由于next是c++中的关键字:

a.cpp: In function 'void insert(int)':
a.cpp:13:5: error: reference to 'next' is ambiguous13 |     next[idx] = h[k];|     ^~~~

更改next[N]为ne[N]后问题得到解决。

开放寻址法

开放寻址法就是在插入一个关键字为 k 的元素时,若发生哈希冲突,则通过某种哈希冲突解决函数(也称为再哈希)得到一个新空闲地址再插入该元素的方法。

线性探测法是从发生冲突的地址开始,依次探测下一个地址,直到找到一个空闲单元为止。当到达下标为 m − 1的哈希表表尾时,下一个探测地址是表首地址0。当 m ≥ n 时一定能找到一个空闲单元。

使用开放寻址法时,m 通常取 n 的 2 ∼ 3 倍左右。(经验值)

那么这里取n的2倍(200000),我们要先找到一个大于200000的最小的质数:

#include 
using namespace std;
int main(){for(int i = 200000; ; i++){bool flag = true;for(int j = 2; j * j <= i; j++){if(i % j == 0){flag = false;break;}}if(flag){cout << i << endl;break;}}return 0;
}
// 输出200003,则我们取N = 200003

开放寻址法的find函数为int类型,用于寻找x所在位置,如果x存在,那么就返回其存在的位置,如果x不存在,就返回其应该存在的位置。

#include 
#include using namespace std;// 约定一个null值作为标准,如果h数组上的某个位置等于这个标准的话,就说明这个位置是空的
// 要求这个数不在 -10^9 <= x <= 10^9 范围内即可
const int N = 200003, null = 0x3f3f3f3f;
int n;
int h[N];int find(int x){int k = (x % N + N) % N;// 该位置有人且值不为x,就要向后看while(h[k] != null && h[k] != x){k++;if(k == N) k = 0;}return k;
}int main(){scanf("%d", &n);memset(h, 0x3f, sizeof h);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;
}

为什么使用null = 0x3f3f3f3f和memset(h, 0x3f, sizeof h)可以看这里

字符串哈希

当key为字符串时,h(key)称为字符串哈希。

将字符串看成一个p进制的数,将它转化为十进制数再对Q取模,即可得到一个映射到0-Q范围内的数

str : "ABCD"
令A = 1, B = 2, C = 3, D = 4
h["ABCD"] = (1*p^3 + 2*p^2 + 3*p^1 + 4*p^0) % Q

注意:

  1. 不能将字符映射成0:例如将"A"映射成0,则"AA","AAA"的值均为零。所以应当从1开始。

  1. 经验值:

当p = 131 或 13331
Q = 2^64时
几乎不存在冲突

那么这样做的好处是什么呢?我们可以通过求前缀哈希,通过某一个公式求出任意子串的哈希值:

前缀哈希:例如"ABCDE"
h[0] = 0;
h[1] = h["A"];
h[2] = h["AB"];
h[3] = h["ABC"];
...

类似于前缀和的思路:

string:      _______|__________|__________高位      L          R        低位
现在已知h[R]和h[L - 1]
我们有:
h[R]    =  p^R-1 * "_" + p^R-2 * "_" + ... + p^0 * "_"
h[L - 1] = p^L-2 * "_" + p^L-3 * "_" + ... + p^0 * "_"
容易知道只需:h[R] - h[L - 1]*p^(R - L + 1)
就能求得L-R间子串的哈希值
同时由上面的字符串哈希的定义,有:
h[i] = h[i - 1]*p + str[i];

AcWing 841.字符串哈希

#include using namespace std;typedef unsigned long long ULL;const int N = 100010, P = 131;
int n, m;
char str[N];
ULL p[N], h[N];ULL find(int l, int r){return h[r] - h[l - 1] * p[r - l + 1];
}int main(){scanf("%d %d", &n, &m);// str从str[1]开始填入数据,因为h[i] = h[i - 1]*p + str[i]、h[0] = 0scanf("%s", 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(find(l1, r1) == find(l2, r2)) puts("Yes");else puts("No");}return 0;
}

因为上面提到:当p = 131、Q = 2^64时几乎不存在冲突,所以这里有一个小技巧,使用unsighed long long的数据类型,它的大小就是2^64,这样我们就无需进行取模的操作了,因为它溢出后就相当于帮我们取模了。

相关内容

热门资讯

脑机接口遇到音乐治疗,AI真能... 志愿者体验“央音一号”。受访者供图 在走进中央音乐学院“央音一号”实验室之前,中青报·中青网记者对脑...
伊朗警告:若遭攻击必将还击 据外媒报道,伊朗议长卡利巴夫11日说,如果美国对伊朗发动打击,伊朗将把以色列以及美国在中东地区的军事...
SpaceX再部署7500颗星... 来源:@央视财经微博 【#SpaceX再部署7500颗星...
商络电子:向不特定对象发行可转... 商络电子公告,公司于2026年1月9日收到深圳证券交易所出具的《关于受理南京商络电子股份有限公司向不...
王毅原定访问索马里计划推迟 中... 新京报讯 据中国驻索马里使馆消息,有媒体报道,中共中央政治局委员、外交部长王毅原定1月9日访问索马里...