[知识点] 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 算法有着详细的介绍与分析。

posted @ 2020-05-30 21:20  jinkun113  阅读(488)  评论(0编辑  收藏  举报