如果手写哈希的话,主键key只能是int类型,如果主键是字符串类型,值是int类型的映射时,就不能手写哈希了,就要使用unordered_map了。
第一次见链接:哈希表、字符串哈希初步学习
字符串哈希
将一个字符串转化成一个整数,并保证字符串不同,其哈希值不同。类似于整数的二进制数表示的思想一样。
因此将一个字符串看成一个R进制数,对于一个字符串:S=s1s2…sn,如果是全都是小写字母,可以将每个字符转换成idx(si)=si-'a'+1
,如果有大写,有小写,有数字不方便处理,可以直接用字符的ASCII码表示。
对字符的再次转换编码要注意不能从0开始,这就是为什么+1的原因。
假设a的编码为0,那么"aa"、“aaa”、“aaaa”的哈希值都是0,冲突严重。
哈希函数
将字符串(R进制数)转换成十进制数通过哈希函数来完成:Hash[i] = ( Hash[i-1]*R + idx(si) )%mod
,i 从1 到 n,通过递推中途可以求出前缀子串"s1"、“s1s2”、“s1s2s3”……,一直到"s1s2……sn"的哈希值,即最终的Hash[n],我们的字符串S的哈希值,它的展开写法为:Hash[n]=(s1 × Rn-1 + s2 × Rn-2 + …… +sn-1 × R + sn )%mod
规定:为了统一化递推公式,Hash[0]=0
一般地,
R取131 或者13331 ;
mod取264 ,常利用unsigned long long 自然溢出,相当于自动对264取模。因为ULL也是64位
要注意,这种单哈希方法的转换并不能保证百分之百不冲突的,即可能存在两个字符串对应的哈希值(十进制数)一样的情况,但是不安全因素很小,可以忽略,如果实在碰到了,考虑双哈希方法。
前缀子串
在我们求一个字符串的哈希值的时候,通常使用的是递推的方法实现的,而不使用它的展开式,因此必然会求出这个字符串的前缀子串,可以用一个数组记录下来,有了它的前缀子串,我们就可以只用O(1)的复杂度求出这个字符串任意子串的哈希值:通过前缀和的思想。
结论:求父串中从下标l到下标r的字符串对应的哈希值:h[r]-h[l-1]×Rr-l+1 ULL h[N];
字符串:S=s1s2…sn,假设l=4,r=6,那么就是要求出s4s5s6的哈希值,即s4×R2 +s5×R+s6
我们已知h[6]=s1×R5 +s2×R4+s3×R3 + s4×R2 +s5×R+s6
h[4-1]=h[3]=s1×R2 +s2×R+s3
R3 ×h[3] =s1×R5 +s2×R4+s3×R3
但是观察到h[6]和h[3]由于幂不匹配不能直接减,所以要先给h[3]乘一个数,让其和h[6]的前三位幂匹配,得到后三位,这个数就是R6-4+1 ,即r和l-1之间相差的位数,直接减即可。
Rr-l+1 怎么求呢?
在我们求hash[n]递推求其前缀的子串的时候,也可以求出Rn ,这也是一个递推的过程:R^n = R^n-1 × R
,用一个数组来维护,R[i]即为Ri
最后,存储字符串的数组,从下标0存储还是从下标1存储都行,但是为了和递推数组hash以及R保持一致,他们的递推式里都涉及了i-1,所以下标都从1开始比较好。
“hash[0]代表没有子串,hash[1]代表只有1个字符的前缀子串,hash[k]表示有k个字符的前缀子串”
题目描述1
给定一个长度为n的字符串,再给定m个询问,每个询问包含四个整数l1,r1,l2,r2,请你判断[l1,r1]和[l2,r2]这两个区间所包含的字符串子串是否完全相同。
字符串中只包含大小写英文字母和数字。
输入格式
第一行包含整数n和m,表示字符串长度和询问次数。
第二行包含一个长度为n的字符串,字符串中只包含大小写英文字母和数字。
接下来m行,每行包含四个整数l1,r1,l2,r2,表示一次询问所涉及的两个区间。
注意,字符串的位置从1开始编号。 这是题目要求的
输出格式
对于每个询问输出一个结果,如果两个字符串子串完全相同则输出“Yes”,否则输出“No”。
每个结果占一行。
数据范围
1≤n,m≤105
输入样例:
8 3
aabbaabb
1 3 5 7
1 3 6 8
1 2 1 2
输出样例:
Yes
No
Yes
算法实现
#include <iostream>
#define read(x,y) scanf("%d%d",&x,&y)
using namespace std;
typedef unsigned long long ULL;
const int N=1e5+10,R=131;
char str[N];
ULL hs[N]; 维护每个前缀子串的哈希值,"hs[0]代表没有子串,hs[1]代表只有1个字符的前缀子串,hs[k]表示有k个字符的前缀子串"
ULL r[N]={1}; //维护R^i,r^0=1,其它初始为0,也定义为ULL类型,以防万一
ULL getSub(int l,int h) // low high
{
return hs[h]-hs[l-1]*r[h-l+1];
}
int main()
{
int n,num;
read(n,num);
scanf("%s",str+1);//从下标1开始存储
//预处理
for (int i=1;i<=n;i++) {
hs[i]=hs[i-1]*R+str[i];
r[i]=r[i-1]*R; "前提,r[0]=1"
}
int l1,r1,l2,r2;
while (num--) {
read(l1,r1),read(l2,r2);
getSub(l1,r1)==getSub(l2,r2)?puts("Yes"):puts("No");
}
return 0;
}
题目描述2
给定一个模式串S,以及一个模板串P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模板串P在模式串S中多次作为子串出现。
求出模板串P在模式串S中所有出现的位置的起始下标。
输入格式
第一行输入整数N,表示字符串P的长度。
第二行输入字符串P。
第三行输入整数M,表示字符串S的长度。
第四行输入字符串S。
输出格式
共一行,输出所有出现位置的起始下标(下标从0开始计数),整数之间用空格隔开。
数据范围
1≤N≤105
1≤M≤106
输入样例:
3
aba
5
ababa
输出样例:
0 2
算法实现
#include <iostream>
#define read(x) scanf("%d",&x)
using namespace std;
typedef unsigned long long ULL;
const int N=1e5+10,M=1e6+10,R=131;
char ft[M],sn[N]; //ft父串,sn子串
ULL hs[M],h,r[M]={1};
//hs数组记录ft父串的每个前缀子串的哈希值,h只记录sn子串的哈希值,r记录r^i
"hs[0]代表没有子串,hs[1]代表只有1个字符的前缀子串,hs[k]表示有k个字符的前缀子串"
ULL getSub(int l,int h)
{
return hs[h]-hs[l]*r[h-l];
}
int main()
{
int n,m;
read(n);
scanf("%s",sn+1); //都从下标1开始存储字符串,和hs数组保持一致
read(m);
scanf("%s",ft+1);
//预处理父串
for (int i=1;i<=m;i++) {
hs[i]=hs[i-1]*R+ft[i];
r[i]=r[i-1]*R;
}
//预处理子串
for (int i=1;i<=n;i++) h=h*R+sn[i];
for (int i=1;i<=m-n+1;i++) { //子串能到父串的的最后一个位置下标:m-n+1
if (getSub(i-1,i+n-1)==h) printf("%d ",i-1); //减一是因为我们的数组下标从1开始,题目里是从1开始
} "提前在这里i-1, hs[i+n-1]-hs[i-1] "
return 0;
}