字符串hash
原理
我的理解是:这是一种很玄学的字符串匹配算法
比如我们现在有一个字符串:str = "abcde"
通过一个hash函数,我们假设为int mhash()
[为什么要写成mhash?因为hash()在库函数已经有了]
那我们得到的str的哈希值str_hash = mhash(str)
接下来是mhash的实现代码:听说这种是比较不会发生冲突的了
typedef unsigned long long ull; // 因为hash值一般都很大,所以我们采用ull
ull mhash(char *str)
{
// base 和 mod 都最好为质数
ull base = 131;// 这里的base选择比较小,如果题目要求的str的length比较大的话,最好把base也选择大一点
ull mod = 2147483587;
ull ret = 0;
int len = strlen(str);
for(int i = 0;i<len;i++)
{
ret = (ret*base%mod+str[i])%mod;
}
return ret;
}
接下来讲一下这代码具体都在干什么
首先ret = 0,在ASCII中a = 97,b = 98 以此类推
在for循环了要循环5次
- ret = a = 97;
- ret = a * base + b = 97 * 131 + 98;
- ret = a * base * base + b * base +c;
- ret = a * base * base * base+ b * base * base +c * base + d;
- ret = a * base * base * base * base + b * base * base * base +c * base * base + d * base + e;
通过观察以上我们可以知道,这是一条与str有关的多项式,这就是hash
即 str_hash = str[0]*base^(len-1)+str[1]*base^(len-2)+....+str[len-2]*base+str[len-1]
将一个字符串转换成一个数字,其中base是基数,mod 是 取余的,防止出来的数太大
接下来是重点如何快速求子串的哈希值
比如我们有一个字符串str = "abcde"
我们知道hash(str) = h1
那么hash("bcd")=?
我们的第一个想法当然是直接再hash一次,但是如果我们的str_len = 10000000呢?
再hash一次显然又费时又费力
这个时候我们就可以用前缀和的思想来解决这个问题(不懂前缀和的同学,可以先去百度学习一下前缀和,特别简单的一种思想)
我们来分析一下 h1 = a*base^3+b*base^2+c*base^1+d
h2 = b*base^2+c*base^1+d
也就是说 h2 = h1 - a*base^3
,知道这个之后就简单了,我直接上代码
ull gethash(int l, int r)
{
// 获得l-r的哈希值
// 获取子串的哈希值
// mhash[r] 表示从开头到r的哈希值
// por[k] 表示base^k
if (l > r)
return -1;
return mhash[r] - mhash[l - 1] * por[r - l + 1];
}
例题
很有意思的一道题[狗头]
https://ac.nowcoder.com/acm/problem/15253
下面是我写的ac代码:
// https://ac.nowcoder.com/acm/problem/15253
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <algorithm>
#include <set>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int maxl = 1e6 + 10;
// const int mod = 1e9+7;
const int mod = 2147483587;
// const ull base = 131;
const ull base = 25165843;
char t[maxl];
char s[maxl];
int tlen = 0;
ull thashs[maxl];
int thashs_l = 0;
ull por = 1;
ull mhash(char *str, int len)
{
ull ans = 0;
for (int i = 0; i < len; i++)
{
ans = (ans * base % mod + (ull)str[i]) % mod;
}
return ans;
}
void init()
{
tlen = strlen(t);
ull thash = mhash(t, tlen);
thashs[thashs_l++] = thash;
for (int i = 1; i < tlen; i++)
{
por = (por * base) % mod;
}
// 这个for循环减少了时间复杂度
for (int i = 0; i < tlen; i++)
{
thash = (thash - (por * (ull)t[i])%mod + mod) % mod;
thash = ((thash * base) % mod + t[i]) % mod;
thashs[thashs_l++] = thash;
}
// 因为用set之后会超时,所以我就自己写了一个二分查找,查找之前先排序一下
sort(thashs,thashs+thashs_l);
}
bool find(ull shash)
{
// 二分查找
int mid;
int l = 0;
int r = thashs_l;
while(1)
{
if(l>r) return 0;
mid = (l+r)/2;
if(thashs[mid]==shash) return 1;
if(thashs[mid]>shash){
r = mid-1;
}
else{
l = mid+1;
}
}
return 0;
}
int main()
{
int n;
scanf("%s%d", t, &n);
init();
while (n--)
{
int ans = 0;
scanf("%s", s);
int len = strlen(s);
if(len<tlen) {
printf("0\n");
continue;
}
ull shash = mhash(s, tlen);
if (find(shash))
ans++;
// 这个for循环也是为了来减少时间复杂度的
for (int i = 0; i < len - tlen + 1; i++)
{
shash = (shash - (s[i] * por) % mod + mod) % mod;
shash = (shash * base) % mod + s[tlen + i];
if (find(shash))
ans++;
}
printf("%d\n", ans);
}
return 0;
}