[知识点] 5.2 字符串hash
总目录 > 5 字符串 > 5.2 字符串 hash
前言
这是一篇新的字符串 hash 介绍文章,5 年前的那篇其实也讲的差不多了,但也有许多问题,而且也不知道当时为什么前前后后提了那么多次暴雪,看起来像是一篇暴雪的软文 = =。
文章虽然归类为字符串部分,但知识是属于 hash 的一部分,所以如果不了解 hash 的概念请参见:7.2 哈希表
子目录列表
1、概述
2、各种字符串 hash 算法
3、MOD 模数取值
4、多 hash 取值
5.2 字符串 hash
1、概述
顾名思义,字符串 hash 是指以字符串为 key 值,通过某种算法获取其 hash 值,以便于访问。上述以整数为 key 值的 hash,其必要性在于整数往往很大,空间存不下,而字符串 hash 呢?
【例子】给出 n 个字符串 和 m 个询问,对于第 i 个询问给定一个字符串 a[i],判断 a[i] 是否出现在 n 个字符串中。
最简单的字符串匹配题。常规做法为将 n 个字符串保存,对于 m 次询问,每次与 n 个字符串进行一一比对,时间复杂度为 O(n * m * len),len 表示字符串的平均长度。而使用字符串 hash 之后,对于每个字符串求出一个 hash 值,则直接比较 hash 值即可,再加上使用二分查找,数据复杂度降为 O(m log n)。
那么,具体如何定义 hash 函数?字符串 hash 一般情况下是通过将各个字符的 ASCII 码进行某种运算并取模后求得。前人对其进行诸多研究,并总结出了一些不错的方法,下面对各种字符串 hash 算法进行介绍。
2、各种字符串 hash 算法
① BKDRHash
介绍:
本算法在 Brian Kernighan 与 Dennis Ritchie 的《The C Programming Language》一书被展示而得名,简单快捷,正确率高,也是 Java 目前采用的字符串的 hash 算法。
代码:
1 #define x 131 2 3 int BKDRHash() { 4 int hash = 0; 5 for (int i = 0; i < len; i++) 6 hash = hash * x + a[i]; 7 return hash; 8 }
a 为字符串,len 为字符串长度,下同。
原理:
BKDRHash 属于多项式 hash,有点类似于进制转换,字符串可能出现的字符包括大小写字母,数字和特殊字符等,ASCII 码最大可能为 127,可以把字符串理解为一个 127 进制数,将其转化为十进制数,能理解进制转换的原理,对于多项式 Hash 也就很好理解了。一般情况下,x 的取值除了可以为 131,还可以是 31, 1313, 13131, 131313, ...,诸如此类。
② SDBMHash
介绍:
本算法在开源项目 SDBM(一种简单的数据库引擎)中被应用而得名,和 BKDRHash 一样属于多项式 hash,只是 x 取值为 65599。
代码略。
③ RSHash
介绍:
本算法因 Robert Sedgwicks 在其《Algorithms in C》一书中展示而得名。
代码:
1 #define x 63689 2 3 int RSHash() { 4 int hash = 0; 5 for (int i = 0; i < len; i++) { 6 hash = hash * x + a[i]; 7 x *= 378551; 8 } 9 return hash; 10 }
与前面的算法不同,RSHash 的 x 值一直在变化,每次累乘一个 378551。
④ APHash
代码:
1 int APHash() { 2 int hash = 0; 3 for (long i = 0; i < len; i++) 4 if ((i & 1) == 0) 5 hash ^= ((hash << 7) ^ a[i] ^ (hash >> 3)); 6 else 7 hash ^= (~((hash << 11) ^ a[i] ^ (hash >> 5))); 8 return hash; 9 }
⑤ JSHash
代码:
1 int JSHash() { 2 int hash = 1315423911; 3 for (int i = 0; i < len; i++) 4 hash ^= ((hash << 5) + a[i] + (hash >> 2)); 5 return hash; 6 }
⑥ DJBHash
1 int DJBHash() { 2 int hash = 5381; 3 for (int i = 0; i < len; i++) 4 hash += (hash << 5) + ch; 5 return hash; 6 }
还有 DEKHash, FNVHash, DJB2Hash, PJWHash, ELFHash,不一一介绍了。
3、MOD 模数取值
有了算法,第二步就是对 hash 值取模。可以看到各种算法所求得 hash 值在字符串较长的情况下是相当大的,而我们在存储时希望能压缩到 long long 范围甚至 int 范围,这时候就需要对 hash 值取模了,而显然,取模必然会导致 hash 冲突的情况出现,那么要如何尽可能降低错误率?
上面介绍的若干种算法,BKDRHash 为最经典的,也是在许多测试中错误率最低的一种,使用较多,下面以 BKDRHash 为首的多项式 hash 举例。
对于多项式 hash,其 hash 值可以表示为 ∑(a[i] * x ^ i) % MOD。首先,x 和 MOD 必须互质。在互质的前提下,理论上 hash 值在 [0, MOD) 范围内的每个值出现概率是相等的,其错误率可认为是 1 / MOD,所以 MOD 在 int 或 long long 范围内尽可能大,并且为质数,平时题目中经常见到的一个模数便是个不错的选择 —— 1e9 + 7,除此之外,还有如下模数也经常被使用:
12255871, 16341163, 21788233, 29050993, 38734667, 51646229, 68861641, 91815541, 1e9 + 9
4、多 hash 取值
假如进行 n 次字符串比较,每次错误率为 1 / MOD,则总错误率为 1 - (1 - 1 / MOD) ^ n。假设 MOD = 1e9 + 7,n = 10 ^ 6,则错误率约为 1 / 1000,其实并不是完全忽略不计的,所以为了进一步提高正确性,可以采取多 hash 取值的办法。
① 多次取模
采用同一种 hash 算法,但取至少两个模数,当且仅当在对两个模数取模得到的 hash 值均相等,key 值才被认为是相同的,这样,错误率起码降低至原来的平方数,当然还可以取更多模数以进一步降低。在上述常用模数里进行选择即可。
② 多次 hash
采用至少两种 hash 算法,当且仅当两种算法得到的 hash 值均相等,key 值才被认为是相同的,同样可以大幅降低错误率,比如 BKDRHash 和 RSHash 同时使用。
5、应用
例子中已经体现出字符串匹配使用字符串 hash 的作用了,其实只要需要对字符串进行是否相等的判断的,都可以使用字符串 hash,诸如最长回文子串等等。
下面给出例子的代码。
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 #define MAXN 1005 5 #define x 131 6 #define M1 1000000007 7 #define M2 21788233 8 9 typedef long long ll; 10 11 ll n, m, bh1, bh2, ah1[MAXN], ah2[MAXN]; 12 char a[MAXN], b[MAXN]; 13 14 ll h1(const char* a, int len) { 15 ll h = 0; 16 for (int i = 0; i < len; i++) 17 h = (h * x + a[i]) % M1; 18 return h; 19 } 20 21 ll h2(const char* a, int len) { 22 ll h = 0; 23 for (int i = 0; i < len; i++) 24 h = (h * x + a[i]) % M2; 25 return h; 26 } 27 28 bool find2(ll o) { 29 int l = 1, r = n; 30 while (l <= r) { 31 int m = (l + r) >> 1; 32 if (ah2[m] > o) r = m - 1; 33 else if (ah2[m] < o) l = m + 1; 34 else return 1; 35 } 36 return 0; 37 } 38 39 bool find1(ll o) { 40 int l = 1, r = n; 41 while (l <= r) { 42 int m = (l + r) >> 1; 43 if (ah1[m] > o) r = m - 1; 44 else if (ah1[m] < o) l = m + 1; 45 else return find2(bh2); 46 } 47 return 0; 48 } 49 50 int main() { 51 cin >> n >> m; 52 for (int i = 1; i <= n; i++) { 53 cin >> a; 54 ah1[i] = h1(a, strlen(a)); 55 ah2[i] = h2(a, strlen(a)); 56 } 57 sort(ah1 + 1, ah1 + n + 1), sort(ah2 + 1, ah2 + n + 1); 58 for (int i = 1; i <= m; i++) { 59 cin >> b; 60 bh1 = h1(b, strlen(b)), bh2 = h2(b, strlen(b)); 61 cout << (find1(bh1) ? "yp" : "nob") << endl; 62 } 63 return 0; 64 }
使用了 2 个模数。
本文参考了 https://blog.csdn.net/l919898756/article/details/81170326,里面对各种 hash 算法有着详细的介绍与分析。