哈希表的作用:把一个比较大的空间,通过一个函数映射到一个比较小的空间
一般做哈希运算时,取一个质数作为模,会使得冲突的概率降低。
哈希表的冲突解决方法:
- 拉链法
- 开放寻址法
下面详细介绍这两种方法的原理及其实现:
1.拉链法
创建一个数组h[],插入一个值时通过哈希函数映射到数组对应位置,每个位置维护一个链表,映射到相同位置加入当前链表中。
数组h[i]类似于链表的头指针,存储的是其下链表的第一个结点x的数组下标**,而不是结点的值x,取值范围0~N,所以可以让数组h的默认值为-1,以此判断该位置下是否为空
- 插入操作:采用头插法,根据哈希函数计算哈希值,每次冲突的值,插入到链表的第一个位置;
- 查询操作:根据哈希值找到对应头指针即对应链表,再对链表遍历判断;
- 删除操作:删除操作并不是真正的删除该元素,而是设置一个标记值,来表示该位置的值已被删除(用得少)。
板子:
const int N = 100003; // 大于10^5的最小质数,作为哈希表的大小
int h[N], e[N], ne[N], idx = 0;
// h[] 用于存储每个哈希值对应的链表头结点
// e[] 用于存储插入的值
// ne[] 用于存储每个元素的下一个节点的索引
// idx 是当前插入元素的索引
// 插入操作,使用拉链法实现
void insert(int x){
int k = (x % N + N) % N; // 计算哈希值,处理负数时确保结果为正数
e[idx] = x; // 将x存入数组e,idx是当前存储的索引
ne[idx] = h[k]; // 将当前元素的下一个节点设置为链表的第一个节点
h[k] = idx++; // 将哈希表h[k]指向当前元素,并更新idx
}
// 查找操作,查找元素x是否存在
bool find(int x){
int k = (x % N + N) % N; // 计算哈希值
for(int i = h[k]; i != -1; i = ne[i]){ // 遍历哈希表k对应的链表
if(e[i] == x) return true; // 如果找到,返回true
}
return false; // 没找到,返回false
}
哈希表常用取余法,代码实现找一个大于等于N且最小的质数:
int get_Prime(int N){
for(int i = N;;i++){
bool flag = true;
for(int j = 2; j * j <= i;j ++){
if(i % j== 0){
flag = false;
break;
}
}
if(flag){
return i;
break;
}
}
}
2.开放寻址法:
开放寻址法:当冲突发生时,使用某种探测算法(得出一个偏移量)在散列表中寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到。探测法有线性探测法、平方探测法、双散列法、伪随机序列。这里直接使用线性探测法,即冲突则自增1。
数组h[]存储的时具体的节点值x,而x的取值范围是 − 1 0 9 − − 1 0 9 -10^9 --10^9 −109−−109.故应该让数组x的默认值不在x的取值范围内(定义null = 0x3f3f3f3f),这样才好判断h[k[位置上是否为空(注意和拉链法区分)
- 查找和查询操作合为一个find()函数:首先根据哈希函数计算的哈希值查找当前元素是否在初始位置。若该位置为空,则在这个位置插入该元素;若不为空且与该元素不等,则向后继续查找,直到找到该元素或有空位置则插入该元素。最后返回该位置。
板子:
const int N = 200003, null = 0x3f3f3f3f; // N为哈希表的大小,null为标志无效值的常量
int h[N]; // 哈希表,存储元素
// 开放寻址法查找函数
// 如果x在哈希表中,返回x的下标;
// 如果x不在哈希表中,返回x应该插入的位置
int find(int x) {
int k = (x % N + N) % N; // 计算哈希值,处理负数使其为正
while(h[k] != null && h[k] != x) { // 当位置不为空且不等于x时,进行线性探测
k++; // 如果发生冲突,继续探测下一个位置
if(k == N) k = 0; // 如果到达末尾,回到哈希表开头
}
return k; // 返回找到的插入位置或x所在的位置
}
好的,下面给出一道题目,我们采用两种方法解答:
Acwing 840. 模拟散列表
输入样例:
5
I 1
I 2
I 3
Q 2
Q 5
输出样例:
Yes
No
具体实现(2种):
//拉链法
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100003; // 大于10^5的最小质数,作为哈希表的大小
int h[N], e[N], ne[N], idx = 0;
// h[] 用于存储每个哈希值对应的链表头结点
// e[] 用于存储插入的值
// ne[] 用于存储每个元素的下一个节点的索引
// idx 是当前插入元素的索引
// 插入操作,使用拉链法实现
void insert(int x){
int k = (x % N + N) % N; // 计算哈希值,处理负数时确保结果为正数
e[idx] = x; // 将x存入数组e,idx是当前存储的索引
ne[idx] = h[k]; // 将当前元素的下一个节点设置为链表的第一个节点
h[k] = idx++; // 将哈希表h[k]指向当前元素,并更新idx
}
// 查找操作,查找元素x是否存在
bool find(int x){
int k = (x % N + N) % N; // 计算哈希值
for(int i = h[k]; i != -1; i = ne[i]){ // 遍历哈希表k对应的链表
if(e[i] == x) return true; // 如果找到,返回true
}
return false; // 没找到,返回false
}
int main(){
memset(h, -1, sizeof h); // 初始化哈希表h,令所有值为-1,表示链表为空
int n; // 操作数
cin >> n;
while(n--){ // 处理n个操作
char op[2]; // 操作符
int x; // 操作的数值
cin >> op >> x;
if(op[0] == 'I') insert(x); // 插入操作
else { // 查找操作
if(find(x)) puts("Yes");
else puts("No");
}
}
return 0;
}
//开放寻址法
#include <iostream>
#include <cstring>
using namespace std;
const int N = 200003, null = 0x3f3f3f3f; // N为哈希表的大小,null为标志无效值的常量
int h[N]; // 哈希表,存储元素
// 开放寻址法查找函数
// 如果x在哈希表中,返回x的下标;
// 如果x不在哈希表中,返回x应该插入的位置
int find(int x) {
int k = (x % N + N) % N; // 计算哈希值,处理负数使其为正
while(h[k] != null && h[k] != x) { // 当位置不为空且不等于x时,进行线性探测
k++; // 如果发生冲突,继续探测下一个位置
if(k == N) k = 0; // 如果到达末尾,回到哈希表开头
}
return k; // 返回找到的插入位置或x所在的位置
}
int main() {
//memset按字节赋值,int4个字节,每个字节赋值0x3f,则h默认值就为0x3f3f3f3f 即null
memset(h, 0x3f, sizeof h); // 初始化哈希表,所有位置都标记为null
int n; // 操作次数
cin >> n;
while(n--) {
char op[2]; // 操作类型
int x; // 操作的数值
cin >> op >> x;
int k = find(x); // 通过find函数得到x的位置
if(op[0] == 'I') h[k] = x; // 插入操作,将x放入找到的位置
else {
if(h[k] != null) puts("Yes"); // 如果h[k]不是null,说明x在哈希表中
else puts("No"); // 否则x不在哈希表中
}
}
return 0;
}
以上两种方法各有利弊,开放寻址法通过线性探测有效解决哈希冲突,适合负载率较低的哈希表。在实际操作中,开放寻址法避免了链表(拉链法)的额外存储开销,但在负载率高时可能会导致较多的探测,影响性能。个人倾向于开放寻址法,只需要开一个数组~
下面还有个无敌的算法:字符串哈希
可以求解任意的字串的哈希值!这是KMP望而却步的!可以通过字符串哈希值,快速判断两个字符串是否相等或者两个字符串中某个部分是否相等。(用模式匹配至少 O ( n ) O(n) O(n),而字符串哈希只需要 O ( 1 ) . O(1). O(1).
- 字符串哈希值:实际为字符串前缀哈希值,如有字符串s = ABCDE,用数组h[]存储各个前缀哈希值,则h[1] = A的哈希值;h[2] =AB的哈希值;h[3] = ABC的哈希值…
- 如何求解一个字符串的哈希值:将字符串看成一个P禁进制的数,比如字符串ABCD,假设我们把A映射为1,B映射为2,C映射为3,D映射为4(实际上字母也直接取它的ASCII值也可)。将ABCD这个P进制数转化为10进制即为哈希值,则ABCD这个字符串的哈希值为:
(
1
∗
P
3
(1* P^3
(1∗P3+
2
∗
P
2
2* P^2
2∗P2+
3
∗
P
1
3*P^1
3∗P1+
4
∗
P
0
)
4*P^0)
4∗P0)
m
o
d
Q
mod Q
modQ. 最后mod Q 即防止转化的十进制数过大,用Q来缩小数据范围,就是哈希散列的意义;
–注意通常不要把一个字母映射为0,这样会导致重复。比如把A映射为0,则A也是0,AA也是0,AAA还是0;
–对于P、Q的取值,有一个经验值,将冲突概率降到极低。我们可以取 P = 131或13331,Q = 2 64 2^{64} 264 。为简化mod Q运算,可以将h数组的类型取成unsigned long long(64位),这样就无需对 2 64 2^{64} 264取模,溢出就直接相当于取模
可得一个递推公式,求解某个字符串s[]的(前缀)哈希值:即h[i]=h[i-1]*P+s[i]
(类似求前缀和,只是这里每次要乘一个P)注意:i从1开始;字符串是顺序存储在数组中,低位字符的权值大
- 求解一个字符串区间[l,r]上的字符串哈希值
这就和前缀和有区别,不是单纯的h[r] - h[l-1].因为字符串是顺序存储在数组中,低位字符的权重大,区间[1,l-1]的字符串在相减过程中后面应该补0,即抬高各字符的权重,达到与[1,r]区间的字符串位数相等,有同等地位。如[1,l-1]是“ABC”,而[1,r]是“ABCDE”,求DE的哈希值,就应该让哈希值(ABCDE)- 哈希值(ABC00)
即体现到公式上,先算出两者相差的位数:r-l+1
,然后h[l-1]*P^(r-l+1)
,再相减得[l,r]区间字符串的哈希值=h[r]-h[l-1]*P^(r-l+1)
注:为了简便的得到P^i
的值,用一个数组来存储,即p[i]=P^i
- 由以上步骤,即可通过哈希值来判断任意两个字符串是否相等
具体的题目为:Acwing 841 字符串哈希
输入样例:
8 3
aabbaabb
1 3 5 7
1 3 6 8
1 2 1 2
输出样例:
Yes
No
Yes
下面直接给出具体实现代码(详解版)
#include <iostream>
#include <string>
using namespace std;
const int N = 1e5 + 10, p = 131; // N 为最大字符串长度,p 是哈希基数,用于计算哈希值
typedef unsigned long long ULL; // 使用无符号长整型存储哈希值,避免溢出问题
int n, m; // n 是字符串长度,m 是查询次数
int h[N], P[N]; // h[] 存储前缀哈希值,P[] 存储 p 的幂
char str[N]; // 字符串,索引从 1 开始
// 得到区间 [l, r] 的字符串哈希值
ULL get(int l, int r) {
// 利用前缀哈希公式:哈希值为 h[r] - h[l-1] * p^(r-l+1)
return h[r] - h[l-1] * P[r-l+1];
}
int main() {
cin >> n >> m >> str + 1; // 输入字符串长度 n、查询次数 m、字符串 str(索引从 1 开始)
P[0] = 1; // p^0 = 1
for (int i = 1; i <= n; i++) {
P[i] = P[i - 1] * p; // 计算 p^i,用于后续哈希值的计算
h[i] = h[i - 1] * p + str[i]; // 计算前缀哈希值
}
while (m--) {
int l1, r1, l2, r2; // 查询两个子串的区间 [l1, r1] 和 [l2, r2]
cin >> l1 >> r1 >> l2 >> r2;
// 如果两个区间的哈希值相等,说明两个子串相等
if (get(l1, r1) == get(l2, r2)) puts("Yes");
else puts("No");
}
return 0;
}
以上就是哈希的一些知识点,还是很好用的~