在大屏或电脑上体验更佳——动画和操作区域更宽敞。
输入

Rabin-Karp 字符串匹配

Rabin-Karp 比较固定长度文本窗口的哈希指纹。窗口每右移一位,滚动哈希就在常数时间内更新它的指纹,因此多数窗口只需一次哈希比较就能排除;哈希相同的窗口还要逐字符验证,全部相等才接受。

哈希把字符串压缩成小整数,不同字符串可能得到同一值。精确性来自最终字符检查:哈希相等只允许进入验证,不证明匹配。本课使用十进制数字展示滚动算术,普通文本也可以把字符映射成数值后处理。

滚动更新指纹

设模式长度为 m、基数为 10、模数为 q。窗口 31428 可看成十进制数,算法只保存除以 q 的余数。

先计算最高位因子:high = 10^(m - 1) mod q。模式和第一个窗口逐位初始化:hash = (10 × hash + digit) mod q。

窗口右移一格,左侧退出数字为 out,右侧进入数字为 in,则:

next = (10 × (windowHash - out × high) + in) mod q

减去最高位贡献,乘 10 将余下数字左移,再加入新个位。只需旧指纹、退出数字和进入数字,不必重读其他 m - 1 个字符。

JavaScript 的 % 可能返回负余数,实现用 (raw % q + q) % q 规范化。负中间值本身不错误,它属于同一个模剩余类,但保存和比较时要使用一致表示。

Rabin-Karp 逐步演示

默认文本为 3142831415,模式为 31415,q = 13。

  1. 模式长 5,所以 high = 10^4 mod 13 = 3。
  2. 模式逐位计算出的指纹为 7,第一个窗口 31428 的指纹也为 7。
  3. 哈希相同,进入逐字符验证。3、1、4 相同,偏移 3 处 2 != 1,因此是碰撞,不是匹配。
  4. 右移一格,退出 3、进入 3。原始计算为 10 × (7 - 3 × 3) + 3 = -17。规范化模 13 后为 9,对应窗口 14283。
  5. 继续滚动并比较,哈希不同的窗口不做字符检查。
  6. 位移 5 时窗口为 31415,指纹再次为 7。五个字符全部相等,返回 5。

完整轨迹共有六次哈希检查、五次滚动、九次字符检查。前四次字符检查排除位移 0 的假候选,后五次确认位移 5 的匹配。

模数如何改变工作量

保持文本和模式不变,把 q 从 13 改成 101,答案不变,但首个假候选消失。

31428 与 31415 相差 13,所以模 13 都为 7。模 101 时分别为 17、4,第一个窗口在哈希比较时就被排除,只有真实匹配进入字符验证。

较大模数通常减少偶然相等的机会,但不能消除碰撞。实际实现可用更大模数、机器字哈希或两个独立哈希;若要求精确搜索,仍需最终相等检查,除非应用明确接受碰撞风险。

为什么结果正确

相同字符串在相同基数和模数下必有相同哈希,因此哈希不同的窗口可以安全排除。反过来,哈希相同不保证字符串相同,所以必须逐字符确认。

滚动递推与重新计算整个窗口得到同一多项式指纹:移除最高位项,将剩余项左移,加入新项,就是下一个窗口的多项式。中间取模不改变最终余数。

时间与空间复杂度

设文本长 n、模式长 m,表中的窗口扫描假设 m ≤ n。

工作时间辅助空间
初始化模式与首窗口O(m)O(1)
滚动所有窗口O(n - m + 1)O(1)
验证一个候选O(m)O(1)
碰撞稀少时的期望完整搜索O(n + m)O(1)
最坏O(nm)O(1)

滚动扫描本身是线性的。最坏成本来自大量哈希候选,每个都比较接近 m 个字符才失败。碰撞风暴预设有五个窗口通过哈希筛选,却没有一个是真匹配。弱哈希、对抗输入或不合适的模数都会削弱筛选作用;期望线性依赖相应的哈希和输入假设,不是固定小模数对任意输入的保证。

何时使用 Rabin-Karp

一次普通子串查询通常用语言内置搜索。多个相同长度模式可以共享一次滚动扫描:先存储模式哈希,只验证哈希命中的窗口。重复片段、抄袭候选检测也能采用滚动窗口,并处理碰撞;二维匹配可先沿行再沿列滚动。

单模式的 KMP 有确定的 O(n + m) 上界,不依赖碰撞频率。Rabin-Karp 的固定宽度扫描便于扩展到等长模式批量匹配,选择应由任务决定。

边界情况

  • 模式长于文本,没有合法窗口,返回 -1。
  • 空文本配非空模式,也返回 -1。
  • 单个数字模式仍适用,high = 1。
  • 前导零安全,因为窗口宽度固定,0012 和 12 不会被当成相同长度比较。
  • 必须检查最后一个合法位移 n - m。
  • UI 要求非空模式;展示函数另有已说明的约定,空模式返回 0。

常见错误

  • 哈希相等就返回。默认首窗口会产生假匹配。
  • 每次重新读取 m 个字符计算窗口哈希,使滚动阶段也变成 O(nm)。
  • 减去 out 而非 out × high,没有删除正确位权。
  • 直接用可能为负的余数与标准指纹比较。先规范化,后续保存和比较才一致。
  • 把教学用小模数直接用于实际负载,造成过多候选验证。
  • 将每次哈希命中称为碰撞。验证前只是候选,字符串不同才算碰撞。