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

  • 相关阅读:
    七种性能测试方法
    衡量软件性能三大常用指标及其相互关系
    提高CUI测试稳定性技术
    GUI自动化测试中优化测试用例思维方法
    安装MySQL
    关系数据库基本介绍
    适合做自动化测试的项目
    自动化测试优势与劣势
    如何制定测试计划?
    Selenium1.0与2.0介绍
  • 原文地址:https://www.cnblogs.com/jinkun113/p/12995056.html
Copyright © 2011-2022 走看看