目录
(*注:本文中所有图片截自AcWing算法基础课yxc视频讲解)
哈希表主要作用:将一个比较复杂的比较多的数据结构映射到0~n一个相对小的范围内
哈希函数:
假设将一个−10^9 到 10^9范围内的数映射到1 到10^5范围内的一个数。
①如何写哈希函数:x mod 10^5 将一个比较大的数映射到10^5范围内的一个数。(mod模的数要取一个质数,并且离2的整次幂尽可能的远,这样冲突的概率是最小的)
②冲突:可能会把若干不同的数映射到同一个数。处理冲突:开放寻址法或拉链法。
一、存储结构
模板题:AcWing 840. 模拟散列表 - AcWing
题目概述:对一个集合进行插入(I)和查询(Q)两种操作共N次,输出每次查询结果,某个数在集合中输出YES,否则输出NO。
数据范围:
1 ≤ N ≤ 10^5
−10^9 ≤ x ≤ 10^9
拉链法
拉链法解决冲突的方式
建立一个长度为 10^5 的数组,求出某个元素x的映射地址h(x),然后将x插入对应的位置,如上图将值11插到位置3。若第二个元素y的映射地址h(y) = h(x),则在已有值的下面再拉一条链存储值y,如在值11下再存储23。该条链的存储结构为之前学过的单链表。每条链的长度平均下来可以看作是一个常数,所以一般情况是哈希表的时间复杂度可以看作O(1)。在算法题中一般只有对哈希表的添加和查找两个操作。如果要实现删除操作,一般不会是真的删除,一般情况下是开一个数组,开一个bool变量,要删除时在该点标记一下。
拉链法的插入操作,插到距离槽最近的位置。
拉链法模板题代码实现(注释详解版)
#include <cstring>
#include <iostream>
using namespace std;
const int N = 100003;
int h[N];//拉链槽(一维数组)
int e[N], ne[N];//等同于链表中的e[N]和ne[N],分别表示当前值以及下一个位置指向哪里
int idx;//等同于链表中的idx,表示当前用到了哪个位置
void insert(int x)
{
int k = (x % N + N) % N;//k为哈希值
//由于在c++中x%N可能为负数,所以给x%N再加上N后再取模就一定是正数
//链表的插入
e[idx] = x;
ne[idx] = h[k];
h[k] = idx ++ ;
}
bool find(int x)
{
int k = (x % N + N) % N;//1.先映射
for (int i = h[k]; i != -1; i = ne[i])//在哈希值k对应的链表里面找存不存在x
if (e[i] == x)
return true;
return false;
}
int main()
{
int n;
scanf("%d", &n);
memset(h, -1, sizeof h);//将拉链槽清空操作,头文件为<cstring>
while (n -- )
{
char op[2];
int x;
scanf("%s%d", op, &x);
//如果用scanf读入字符串,scanf会自动将所有空格,回车和制表符忽略掉
if (*op == 'I') insert(x);
else
{
if (find(x)) puts("Yes");
else puts("No");
}
}
return 0;
}
开放寻址法
开放寻址法解决冲突方式
只开了一个一维数组,没有用到链表。但是一维数组的长度一般要开到题目数据范围的2~3倍,这样冲突的概率才会比较低。处理冲突方式:例如求出h(x) = k,先看一下第k个位置是否为空,如果不为空就到下一个位置,直到找到一个空位,再将x放进去。查找操作,从位置h(x) = k的位置k开始往后找,先看一下是否为空,非空且为x就找到了,非空但是不是x就一直往后找,为空的话表示x不存在。
开放寻址法模板题代码实现(注释详解版)
#include <cstring>
#include <iostream>
using namespace std;
const int N = 200003;//数据范围的2倍
const int null = 0x3f3f3f3f;//约定一个标志,如果数组里面的一个数等于null,表示该位置上没有人
int h[N];
//核心操作 如果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;//t == N表示已经看找到了最后一个位置,此时要循环看第一个位置(?这里我还没懂)
}
return k;//如果x不在哈希表中,k就是x应该存储的位置
}
int main()
{
memset(h, 0x3f, sizeof h);//把h里面的每一个数x初始化为0x3f,
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;
}
二、字符串哈希(字符串前缀哈希法)
字符串哈希,一个比较重要的哈希方式
模板题:AcWing 841. 字符串哈希 - AcWing
题目概述:给定一个长度为n的字符串,对该字符串进行m次询问,每次询问字符串中的两个区间,判断这两个字符串子串是否完全相同。
数据范围
1 ≤ n,m ≤ 10^5
预处理
例如一个字符串str = "abcdefghijk",求哈希时,需要先预处理出所有前缀的哈希
h[0] = 0
h[1] = "a" //**h[1]等于这个字符串的哈希值,不是等于该字符串,下面同理
h[2] = "ab"(前两个字符的哈希)
h[3] = "abc"(前三个字符的哈希)
①定义某个前缀的哈希值:把字符串看成是一个p进制的数(所以字符串左边为高位,右边为低位),每个字符表示成p进制数的一个数(p一般取131或13331),然后将字符串转化为一个数字,由于该数可能比较大,所以需要对该转化后的数字模上一个比较大的数Q(Q一般取2^64),映射到从0到Q - 1的一个数。这样就能把任何一个字符串映射到从0到Q - 1的一个数了。(取模那里也可以用一个unsigned long long来存储所有的h,因为溢出等价于模上2^64)
②冲突:和前面的哈希不一样,前面的哈希容忍出现冲突并且可以处理冲突,而字符串哈希不考虑冲突,不能将字符映射成0,避免冲突,此外还要将p和Q分别取上述值,此时99%的情况下是不会出现冲突的
③用此种哈希方式配合前缀哈希的好处:可以利用前缀哈希,利用一个公式求得所有子段的哈希值
如何求出公式:
例如:已知1~(L-1)和1~R段前缀哈希值h(L-1)和h(R),要求字符串中L到R段的哈希值。
h[R] 最高位:p^(R-1) 最低位:p^0
h[L-1]最高位:p^(L-2) 最低位:p^0
将字符串转化为p进制数,左边为高位,右边为低位。1~L比1~R要短
第一步:将h(L-1)段往左移移到和h(R)对齐为止//**是哈希值的大小往左移,不是字符串往左移
所以将h(L-1)乘p^(R-L+1)
第二步:得出从L到R段的哈希值 = h(R) - h(L)*p^(R-L+1)
预处理完每一个前缀的哈希值后就可以用O(1)的时间算出任意一个子段的哈希值了
处理前缀哈希:h(i) = h(i-1)*p +str[i]
字符串哈希方式代码实现
#include <iostream>
#include <algorithm>
using namespace std;
typedef unsigned long long ULL;//C++11标准中,unsigned long long = ULL,用ULL表示某一种类型的变量
const int N = 100010, P = 131;
int n, m;
char str[N];
ULL h[N];//用来存储前缀哈希
ULL p[N];//用来存储p的多少次放,方便预处理
//公式
ULL get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
int main()
{
scanf("%d%d", &n, &m);
scanf("%s", str + 1);
//预处理
p[0] = 1;
for (int i = 1; i <= n; i ++ )
{
h[i] = h[i - 1] * P + str[i];//str[i]不取0即可
p[i] = p[i - 1] * P;
}
//询问操作
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;
}