滚动哈希(Rolling Hash)是一种 哈希函数,允许在 O(1) 时间内计算 相邻子串 的哈希值,而不需要重新遍历整个子串。它常用于字符串匹配、子串查找等问题,比如 Rabin-Karp 算法 就是基于滚动哈希的。
在说滚动哈希方法之前,先谈谈一般窗口内哈希值是如何计算的
例如对于字符串hello
,那么哈希值是
h
a
s
h
C
o
d
e
(
h
e
l
l
o
)
=
(
h
−
a
)
∗
p
o
w
(
131
,
0
)
+
(
e
−
a
)
∗
p
o
w
(
131
,
1
)
+
(
l
−
a
)
∗
p
o
w
(
131
,
2
)
+
(
l
−
a
)
∗
p
o
w
(
131
,
3
)
+
(
o
−
a
)
∗
p
o
w
(
131
,
4
)
hashCode(hello)=(h-a)*pow(131, 0)+(e-a)*pow(131, 1)+(l-a)*pow(131, 2)+(l-a)*pow(131, 3)+(o-a)*pow(131,4)
hashCode(hello)=(h−a)∗pow(131,0)+(e−a)∗pow(131,1)+(l−a)∗pow(131,2)+(l−a)∗pow(131,3)+(o−a)∗pow(131,4)
当然由于hash值可能会非常大,所以通常还会进行取模
其中,131 是一个常用的进制基数,a 是字符的起始偏移量(例如,‘a’ 可以为 97),pow(131, i) 表示基数的幂。由于哈希值可能会非常大,通常还会对其进行取模操作,常见的模数 MOD 可能是一个大质数,如 1000000007,用于避免溢出。
滚动哈希的核心概念是什么?
一言以蔽之,滚动哈希即利用前缀和思想简化求取窗口哈希值的方法
请看下面的代码
hashCode(l, r) = ((hash[r] - hash[l - 1] * pow(b, r - l + 1)) % p + p) % p
-
hash[r] - hash[l - 1] 实际上表示从位置 l 到位置 r 这段子串的哈希值,但直接这样做会有问题,因为哈希值是按位数的权重计算的,低位字符的影响会被高位字符的影响掩盖。所以,我们还需要做一个调整。
-
由于哈希是按字符的位权计算的,即字符在字符串中的位置越靠后,其在哈希值中的贡献越大(权重越大)。为了从整体哈希值中去除不需要的部分,需要用到一个幂 pow(b, r - l + 1),其中 b 是基数,r - l + 1 是子串长度
-
哈希值很容易溢出,因此我们通常对其进行取模运算,这里使用了模数 p。
由于计算过程中可能会出现负数,我们需要确保取模后的值是非负的,因此加上了 + p,然后再对 p 取模。
完整的实现
const (
B = 31 // 进制
MOD = 1000000007 // 取模
)
func rollingHash(s string, L int) []int {
n := len(s)
if n < L {
return []int{}
}
hashes := make([]int, n-L+1)
hash, power := 0, 1
// 计算第一个子串的hash值
for i := 0; i < L; i++ {
hash = (hash*B + int(s[i]-'a'+1)) % MOD
if i < L-1 {
power = (power * B) % MOD
}
}
hashes[0] = hash
// 滚动计算后续子串的hash值
for i := 1; i <= n-L; i++ {
// 使用滚动哈希公式计算新子串的哈希值
hash = (hash - (int(s[i-1]-'a'+1) * power % MOD) + MOD) % MOD
hash = (hash * B + int(s[i+L-1]-'a'+1)) % MOD
hashes[i] = hash
}
return hashes
}
为什么需要滚动哈希?
- 文件对比:查找两个文件的相似部分
- 数据去重:发现重复的数据片段
- 文件分块:将文件分成小块进行存储