字符串哈希

1、概念

将一个字符串转化成一个整数,并保证字符串不同,得到的哈希值不同,当然字符串相同的时候保证哈希值相同。这样就可以用来判断一个该字串是否重复出现过。

为什么需要有这种算法,例如在java中,定义一个map,如果直接把string当做键,则每次在map中查找时要一个一个字符地找,跟存在数组中区别不大,而比较数值自然更快。

下面介绍一种常用方法

 

2、字符串哈希算法

由哈希函数的性质,对于一个字符串:S=s1s2...sn,我们把每个字符转换成idx(si)=si-'a',当然直接用字符串的ASCII码表示也可以,则哈希模型为Hash(i)=Hash(i-1)*p+idx(si),其中p为素数。最终算出的Hash(n)作为该字符串的哈希值。

所以构造哈希函数的关键点在于使不同字符串的哈希冲突率尽可能小。

2.1 一般过程

取一固定值P,P为质数,把字符串看作P进制数,并分配一个大于0的数值,代表每种字符。 一般来说,我们分配的数值都远小于P。例如,对于小写字母构成的字符串,可以令a = 1 , b = 2 , . . . , z = 26 。 a=1,b=2,...,z=26。a=1,b=2,...,z=26,或者直接使用ASC码。

取一固定值M,求出该P进制数对M的余数,作为该字符串的Hash值。

一般来说,我们取P=131或P=131313,此时Hash值产生冲突的概率极低,只要Hash值相同,我们就可以认为原字符串是相等的。

通常我们取M=2^64,即直接使用unsigned long long类型存储这个Hash值,在计算时不处理算术溢出问题,产生溢出时相当于自动对2^64取模,这样可以避免低效的取模运算。

除了在及特殊构造的数据上,上述Hash很难产生冲突,一般情况下上述Hash算法完全可以出现在题目的标准解答中。

 

2.2 计算

对字符串的各种操作,都可以直接对P进制数进行算数运算反映到Hash值上。

  • 如果我们已知字符串S的Hash值为H(S),在S后添加一个字符c构成的新字符串S+c的Hash值就是

H(S+cH(S∗ value[cmoM

    其中乘P就相当于P进制下的左移运算,value[c]是我们的为c选定的代表数值。

  • 如果我们已知字符串S的Hash值为H(S),字符串S+T的Hash值为H(S+T),那么字符串T的Hash值

H(TH(S+T− H(S∗ P ^length(TmoM

    这就相当于通过P进制下在S后边补0的方式,把S左移到与S+T的左端对其,然后二者相减就得到了H(T)。

    看着这个公式人畜无害,但是对于取模运算来说要更加谨慎,注意括号里面是剪发,有可能是负数,故可以如下修正:

    H(T= ( H(S+T− H(S∗ P ^length(TmoM + M )mod M

 

可能看着不是很好理解,举个栗子:

例如,S=“abc”,c=“d”,T=“xyz”,则:
S表示为P进制数: 1 2 3
H(S) = 1 ∗ P2 + 2 ∗ P + 3
H(S+c) = 1 ∗ P3 + 2 ∗ P2 + 3 ∗ P + 4 = H(S) ∗ P + 4

 

S+T表示为P进制数: 1 2 3 24 25 26
H(S+T) = 1 ∗ P5 + 2 ∗ P4 + 3 ∗ P3 + 24 ∗ P2 + 25 ∗ P + 26
S在P进制下左移length(T) 位: 1 2 3 0 0 0
二者相减就是T表示为P进制数: 24 25 26
H(T) = H(S+T) − ( 1∗P2 + 2 ∗ P + 3 ) ∗ P3 = 24 ∗ P2 + 25 ∗ P + 26

 

 

根据上面两种操作,我们可以通过O(N)的时间预处理字符串所有前缀Hash值,并在O(1)的时间内查询它的任意子串的Hash值。

 

2.3 代码

放个python版本

def getStrHash(s):
    """
    注意 python 没有溢出
    :param s: 
    :return: 
    """
    n = len(s)
    h = [0] * (n + 1) # 存储字符串前缀的hashcode
    p = [0] * (n + 1)
    p[0] = 1 # 预处理p的n次方
    prime = 13131 # p进制

    for i in range(1, n + 1):
        h[i] = h[i - 1] * prime + ord(s[i - 1])
        p[i] = p[i-1] * prime

    print(h)
    print(p)

    length = 4 #子串的长度
    res = [] #存储子串
    resHash = [] # 存储子串的hashcode
    for i in range(n):
        j = i + length
        if j <= n:
            res.append(s[i:j])
            hash = h[j]-h[i]*p[j-i]
            resHash.append(hash)

    for i in range(len(res)):
        print(res[i], resHash[i])


if __name__ == '__main__':
    s = "abcdefgabcdefg"
    getStrHash(s)

 

2.4 一些tips

  • 双哈希

上述使用的单哈希方式,p,mod均为质数,p<mod,p、mod取尽量大时冲突很小。除此之外,我们也可以使用双哈希方法,来减小冲突

双哈希方法:将字符串用不同mod单哈希两次,结果用二元组表示

Hash1[i] = ( Hash1[i-1] * p + idx(si) )% mod

Hash2[i] = ( Hash2[i-1] * p + idx(si) )% mod

Hash[i]:<Hash1[i],Hash2[i]>

这种方法很安全。

 

  • 质数的选择

像1e9+7等常见素数很可能被出题人卡,所以可以选择一些其他的素数:

比如,131  1313 131313 字符串哈希本身存在哈希冲突的可能,一般会在尝试131之后尝试使用1313之类,然后再尝试使用更大的质数。

 

  • 取模

取模不一定是必要的,例如Python可以不用取模 java可以靠自动溢出

posted @ 2021-12-24 14:08  r1-12king  阅读(1721)  评论(0编辑  收藏  举报