zoukankan      html  css  js  c++  java
  • [知识点] 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 算法有着详细的介绍与分析。

  • 相关阅读:
    ini_set /ini_get函数功能-----PHP
    【转】那个什么都懂的家伙
    word 2007为不同页插入不同页眉页脚
    August 26th 2017 Week 34th Saturday
    【2017-11-08】Linux与openCV:opencv版本查看及库文件位置等
    August 25th 2017 Week 34th Friday
    August 24th 2017 Week 34th Thursday
    August 23rd 2017 Week 34th Wednesday
    August 22nd 2017 Week 34th Tuesday
    August 21st 2017 Week 34th Monday
  • 原文地址:https://www.cnblogs.com/jinkun113/p/12995056.html
Copyright © 2011-2022 走看看