rolling hash的两种实现

我想以leetcode这道题目来讲讲rolling hash的两种实现 https://leetcode.com/problems/longest-duplicate-substring/

这道题目一个关键点是计算rolling hash. 好的hash算法可以使得冲突减少,减少长字符串的比较。 什么是好的hash算法不太好说,但是我可以举出一个比较差的hash算法如下:

def make_hash(s):
    h = 0
    for c in s:
        v = ord(c) - ord('a')
        h += v
    return h

def add_hash(h, c):
    return h + (ord(c) - ord('a'))

def sub_hash(h, c):
    return h - (ord(c) - ord('a'))

这个算法只是去考虑字符串中所有字符的和,那么冲突率自然是比较高的,比如"abcd"和"dcba"就会被计算成为一个hash.


一个简单的改进就是给每个字符应该考虑相应的位置,算法大致如下。这里需要对hash取模,否则字符串太长的话整数会溢出。

def make_hash(s):
    P = 10000019
    h = 0
    for c in s:
        h = h * 26 + ord(c) - ord('a')
        h = h % P
    return h

这里有两个选择lsb(least significant bit)和msb(most significant bit). 以"abcd"来说:

乍看下觉得两者差不多,但是在实现rolling的时候差别还是蛮大的。我一开始使用的msb稍微有点麻烦,而lsb则简单得多。

简单地说下lsb的实现,假设字符串是[x0,x1…,xn-1],下一个字符是xn,当前hash值是h

msb的实现过程和lsb接近:

但是msb的实现在右移这步出现一个问题,h应该如何计算???


假设上面右移之后的值是h', 那么有这个等式 (h'*26) % P = h. 问题就在于我们怎么计算出h'. 这里可以用到 扩展欧几里得算法, 上面等式其实就是 h'*26+P*u=h.

def ext_gcd(a, b, init_y2 = 0):
    if b == 0:
        return a, 1, 0
    d, x2, y2 = ext_gcd(b, a % b, init_y2)
    x1, y1 = y2, x2 - (a // b) * y2
    return d, x1, y1

我们先求得ext_gcd(26, P)=1的解(因为P是素数)"d, x, y = ext_gcd(26, P)",然后 h' = x*h. 另外这里x只需要计算一次就行。

P = 10000019
C = 26
_, AX, _ = ext_gcd(C, P)

def add_hash(h, c, exp_sz):
    v = ord(c) - ord('a')
    h = h + (v * exp_sz) % P
    h = h % P
    return h

def sub_hash(h, c):
    v = ord(c) - ord('a')
    h = (h - v + P) % P
    h = (h * AX) % P
    return h