基于滚动哈希的字符串模式匹配:Rabin-Karp 算法详细解析
1. 题目描述
给定一个文本字符串 text 和一个模式字符串 pattern,要求在文本中查找所有模式字符串出现的位置。你需要实现一个基于哈希的高效算法,在平均情况下达到近似线性时间复杂度,并能处理较长的文本和模式。
示例
输入:text = "abracadabra", pattern = "abra"
输出:[0, 7](模式"abra"在文本中出现在索引0和索引7处)
2. 解题思路
常规的暴力匹配法需要 O(n·m) 的时间复杂度(n、m 分别为文本和模式长度)。Rabin-Karp 算法通过滚动哈希(Rolling Hash)将比较模式与子串的耗时从 O(m) 降为平均 O(1),从而将整体时间复杂度优化到平均 O(n+m)。
核心思想
- 将模式字符串映射成一个哈希值(hash(pattern))。
- 在文本中依次截取与模式等长的子串,计算其哈希值。
- 如果子串哈希值与模式哈希值相等,再逐字符验证(避免哈希冲突导致的误判)。
- 计算下一个子串哈希值时,利用上一个子串的哈希结果进行“滚动”更新,只需 O(1) 时间。
3. 详细步骤
步骤 1:选择哈希函数
Rabin-Karp 算法通常使用多项式滚动哈希(Polynomial Rolling Hash)。我们将字符串视为一个多进制数(基数 base,常取大质数,如 131、257 等),并对一个大质数(如 10^9+7)取模,以减少碰撞概率。
假设字符串 s 的长度为 m,字符 c 映射为整数(如 ASCII 值),则哈希值为:
hash(s) = (s[0] * base^(m-1) + s[1] * base^(m-2) + ... + s[m-1] * base^0) mod mod
其中 base 是进制基数,mod 是模数。
步骤 2:计算模式串的哈希值
以示例 pattern = "abra" 说明(为简化,此处用 ASCII 值,base=131, mod=10^9+7):
- 字符值:a=97, b=98, r=114, a=97
- hash(pattern) = (97131^3 + 98131^2 + 114131^1 + 97131^0) mod mod
计算过程可迭代进行:
hash = 0
for char in pattern:
hash = (hash * base + ord(char)) % mod
步骤 3:预计算 base 的幂
在滚动时,我们需要减去“离开字符”的贡献,这个贡献是 ord(离开字符) * base^(m-1)。因此需要预先计算 base^(m-1) % mod,记为 high_pow。
步骤 4:计算文本第一个子串的哈希值
取 text 前 m 个字符(m 为 pattern 长度),用同样方法计算初始哈希值。
步骤 5:滚动哈希
从 i=0 到 i=n-m(n 为 text 长度),依次处理:
- 比较当前子串哈希值与模式哈希值。
- 如果相等,则逐字符验证子串是否与 pattern 完全相同。
- 将窗口右移一位:去掉最左字符,加入右边新字符,重新计算哈希值。
滚动更新公式(从子串 text[i..i+m-1] 到 text[i+1..i+m]):
new_hash = ( (old_hash - ord(text[i]) * high_pow) * base + ord(text[i+m]) ) mod mod
注意:为避免负数,可先加 mod 再取模。
步骤 6:处理边界
当 i = n-m 时,已经是最后一个可能的子串,更新后不再需要继续滚动。
4. 完整示例
以 text="abracadabra", pattern="abra", base=131, mod=10^9+7 为例:
- 计算 pattern 哈希值:h_pattern = hash("abra") ≈ 某值 H。
- 计算 high_pow = base^(m-1) % mod = 131^3 % mod。
- 计算 text 第一个子串 "abra" 的哈希值 h = hash("abra"),与 H 相等,逐字符验证通过 → 记录索引 0。
- 滚动:
- 从 "abra" 到 "brac":h = ( (h - ord('a')high_pow) base + ord('c') ) % mod
- 比较 h 与 H,不等,继续。
- 重复直到结束,找到第二个匹配 "abra" 在索引 7。
5. 复杂度分析
- 时间复杂度:计算 pattern 哈希 O(m),预处理 high_pow O(log m)(快速幂),滚动 n-m+1 次,每次 O(1)。平均总时间 O(n+m),最坏情况(大量哈希冲突)退化到 O(n·m),但概率极低。
- 空间复杂度:O(1)(除了存储结果)。
6. 关键点
- 哈希冲突:即使哈希值相等,也必须逐字符验证,确保结果正确。
- 参数选择:base 和 mod 应互质,mod 取大质数可减少碰撞。
- 滚动效率:核心优势在于用 O(1) 时间更新哈希,而不是每次重新计算 O(m)。
通过以上步骤,你可以实现一个高效的 Rabin-Karp 算法,用于在长文本中快速查找模式串的出现位置。