相关笔记


概览

本节涵盖CLRS第32章前两节,系统介绍字符串匹配问题(string-matching problem)的两种基础算法。

32.1 朴素字符串匹配算法(Naive String-Matching Algorithm):最直观的暴力匹配方法,逐一检查文本中每个可能的起始位置。最坏情况时间复杂度为 ,但在实际应用中表现尚可,且实现极为简单,是理解所有高级字符串匹配算法的起点。

32.2 Rabin-Karp算法(Rabin-Karp Algorithm):由 Richard M. Karp 和 Michael O. Rabin 于1987年提出,核心思想是利用滚动哈希(rolling hash)将字符比较转化为数值比较,通过哈希指纹(fingerprint)快速排除不匹配位置,期望时间复杂度降至 。该算法天然支持多重模式搜索(multiple-pattern search),在工程中有广泛应用。

核心要点

  • 字符串匹配问题的形式化定义:给定文本 和模式 ,找出所有满足 的偏移量
  • 朴素算法通过显式比较每个偏移处 个字符来验证匹配
  • Rabin-Karp算法通过哈希函数 将模式映射为数值,利用滚动哈希在 时间内更新相邻窗口的哈希值
  • 哈希命中(hit)需要显式验证(verification),因为不同字符串可能产生相同哈希值(碰撞)
  • 选取大素数 作为模数,可将碰撞概率控制在可接受范围内

    A["字符串匹配问题"] --> B["32.1 朴素匹配算法"]
    A --> C["32.2 Rabin-Karp算法"]

    B --> B1["NAIVE-STRING-MATCHER"]
    B --> B2["逐一偏移检查"]
    B --> B3["最坏 O((n-m+1)m)"]
    B --> B4["最好 O(n)"]

    C --> C1["预处理:计算模式哈希 p₀"]
    C --> C2["滚动哈希:O(1)更新窗口哈希"]
    C --> C3["命中验证:显式字符比较"]
    C --> C4["期望 O(n+m)"]

    C2 --> D["Horner规则求值"]
    C2 --> E["模运算:h = p^(m-1) mod q"]
    C2 --> F["滚动更新公式"]

    C4 --> G["多重模式搜索扩展"]
    C4 --> H["碰撞概率分析"]

    B2 --> B2a["T[s+1..s+m] vs P[1..m]"]
    B2 --> B2b["逐字符比较"]

    F --> F1["t_{s+1} = (d(t_s - T[s+1]·h) + T[s+m+1]) mod q"]

核心思想

字符串匹配问题定义

字符串匹配问题(string-matching problem)是计算机科学中最基本的搜索问题之一。给定一个长度为 文本(text) 和一个长度为 模式(pattern)(其中 ),目标是找出文本中所有满足

的有效偏移量(valid shift),其中

生活类比:想象你在阅读一本厚厚的书(文本 ),想要找到某个特定短语(模式 )出现的所有位置。你可以从书的第一个字开始,逐字对比;如果不匹配就往后移一个字重新开始——这就是朴素匹配算法的思路。

32.1 朴素字符串匹配算法

算法思想

朴素字符串匹配算法(naive string-matching algorithm)是最直接的暴力方法:对于文本 中的每一个可能的起始偏移 (从 ),显式地将 进行逐字符比较。

伪代码

NAIVE-STRING-MATCHER(T, n, P, m)
1  for s ← 0 to n - m
2      if P[1..m] == T[s+1..s+m]
3          print "Pattern occurs with shift" s

执行流程图:

flowchart TD
    A(["开始"]) --> B["输入 T, n, P, m"]
    B --> C["s ← 0"]
    C --> D{"s ≤ n - m?"}
    D -->|是| E["比较 P[1..m] 与 T[s+1..s+m]"]
    E --> F{"逐字符比较<br/>全部匹配?"}
    F -->|是| G["输出匹配位移 s"]
    F -->|否| H["s ← s + 1"]
    G --> H
    H --> D
    D -->|否| I(["结束"])

其中第2行的 == 表示逐字符比较:

最坏情况时间复杂度分析

定理:朴素字符串匹配算法的最坏情况运行时间为

证明

【循环次数(外层循环执行 次)】:第1行的 for 循环从 迭代到 ,共 次。

【每次迭代的最大比较次数(内层比较最多 次)】:第2行的字符比较在最坏情况下需要比较全部 个字符(前 个字符都匹配,仅最后一个不匹配)。

【最坏情况总比较次数(乘积)】:因此总比较次数最多为

【最坏情况实例】:考虑文本 a)和模式 a 后接一个 b)。对于每个偏移 ,前 个字符全部匹配,第 个字符不匹配,因此每次迭代恰好比较 次,总比较次数恰好为

【最好情况】:如果文本第一个字符就与模式第一个字符不同(如 ),则每次迭代只需比较1次字符就判定不匹配,总时间为

逐步执行实例

),)。

偏移 比较结果
0ababcabcaba=a, b=b, a≠c不匹配
1babcaabcabb≠a不匹配
2abcababcaba=a, b=b, c=c, a=a, b=b匹配
3bcabcabcabb≠a不匹配
4cabcaabcabc≠a不匹配
5abcababcaba=a, b=b, c=c, a=a, b=b匹配
6bcabaabcabb≠a不匹配
7cabababcabc≠a不匹配

有效偏移量:。总比较次数:

朴素算法的局限性总结

维度朴素算法Rabin-Karp算法
最坏时间
期望时间(无保证)
空间
多模式支持不支持(需分别运行)天然支持
实现难度极低中等
预处理

为什么仍然学习朴素算法:朴素算法是所有字符串匹配算法的”参照系”——理解了朴素算法的瓶颈(重复比较已扫描字符),才能体会 KMP 的”失败函数”、Boyer-Moore 的”坏字符规则”、Rabin-Karp 的”滚动哈希”各自如何解决这一瓶颈。此外,在实际工程中,当模式很短(如 )或文本很小时,朴素算法由于常数因子极小,可能反而比复杂算法更快。

32.2 Rabin-Karp算法

核心思想:用哈希加速匹配

Rabin-Karp算法的核心洞察是:与其逐字符比较,不如先将模式和文本窗口映射为数值(哈希值),只对哈希值相同的候选位置进行逐字符验证

生活类比:假设你要在一堆文件中找到一份特定合同。与其逐一翻阅每份文件的每一页(朴素方法),不如先比较每份文件的”指纹”(如文件大小+创建日期的哈希值)。只有指纹相同的文件才需要仔细核对内容。虽然指纹相同不一定意味着内容相同(碰撞),但可以大幅减少需要仔细核对的工作量。

算法概览(三阶段):

  1. 预处理(preprocessing):计算模式 的哈希值 和文本第一个窗口的哈希值
  2. 滚动匹配(rolling matching):利用滚动哈希在 时间内更新 ,与 比较。
  3. 命中验证(hit verification):当 时,逐字符确认是否真正匹配。

数学预备:将字符串视为数值

假设文本和模式都来自字母表 (其中 ),则长度为 的字符串可以视为一个 进制数。例如,模式 对应的数值为:

类似地,文本中从位置 开始的长度为 的子串对应的数值为:

关键观察 当且仅当

Horner规则(Horner’s Rule)求值

直接计算 需要 次乘法。利用Horner规则可以高效计算:

用Horner规则展开为:

Horner规则的计算过程:从左到右扫描字符串,每读入一个新字符就将当前值乘以 再加上新字符值。具体步骤:

p ← 0
for i ← 1 to m
    p ← (d × p + P[i])

正确性证明(数学归纳法):

【归纳基础(i=1 时)】:,正确。

【归纳步骤(假设第 i-1 步正确)】:假设第 步后 ,则第 步:

【归纳结论(i=m 时)】:,即所需结果。

模运算哈希

直接使用 的值会导致数值过大( 个字符的 进制数可达 )。解决方案是取模:选取一个适当大的素数 ,定义

于是我们比较的是 ,而非完整的

模运算下的Horner规则

计算时每步取模,保证中间结果不超过

p ← 0
for i ← 1 to m
    p ← (d × p + P[i]) mod q

滚动哈希:核心公式推导

Rabin-Karp算法最精妙之处在于:已知 ,可以在 时间内计算出 ,无需重新扫描 个字符。

推导过程

由定义:

观察两者之间的关系:

直观理解:窗口向右滑动一格时,最左边的字符 “滑出”窗口(乘以 后减去),剩余部分整体左移一位(乘以 ),最右边的新字符 “滑入”窗口(加上)。

(预处理时一次性计算),则模运算下的滚动更新公式为:

注意:由于 可能为负数,实际计算时需要加 确保结果为正:

或者等价地:

伪代码

RABIN-KARP-MATCHER(T, n, P, m, d, q)
1  h ← d^(m-1) mod q
2  p ← 0
3  t₀ ← 0
4  for i ← 1 to m                  // 预处理
5      p ← (d × p + P[i]) mod q
6      t₀ ← (d × t₀ + T[i]) mod q
7  for s ← 0 to n - m              // 匹配
8      if p == t_s                  // 命中(hit)
9          if P[1..m] == T[s+1..s+m]  // 验证(verification)
10             print "Pattern occurs with shift" s
11     if s < n - m                 // 计算下一个滚动哈希
12         t_{s+1} ← (d × (t_s - T[s+1] × h) + T[s+m+1]) mod q

执行流程图:

flowchart TD
    A(["开始"]) --> B["输入 T, n, P, m, d, q"]
    B --> C["预处理:h ← d^(m-1) mod q"]
    C --> D["p ← 0, t₀ ← 0"]
    D --> E["循环 i=1 到 m<br/>p ← (d×p+P[i]) mod q<br/>t₀ ← (d×t₀+T[i]) mod q"]
    E --> F["s ← 0"]
    F --> G{"s ≤ n - m?"}
    G -->|是| H{"p == tₛ?"}
    H -->|是| I{"P[1..m] == T[s+1..s+m]?"}
    I -->|是| J["输出匹配位移 s"]
    I -->|否| K{"s < n - m?"}
    J --> K
    H -->|否| K
    K -->|是| L["滚动哈希:t_{s+1} ←<br/>(d×(tₛ-T[s+1]×h)+T[s+m+1]) mod q"]
    L --> M["s ← s + 1"]
    K -->|否| M
    M --> G
    G -->|否| N(["结束"])

算法分析

  • 预处理阶段(第1-6行):计算 ,时间
  • 匹配阶段(第7-12行):循环 次,每次 计算滚动哈希。命中时需要 时间验证。
  • 最坏情况:所有 个位置都命中但都不匹配(如 ),每次验证需 ,总时间
  • 期望情况:命中次数很少(约 次),期望时间

逐步执行实例

),),

预处理

  • 计算
  • 计算

匹配阶段

命中?验证
03150
11470
24120
31590
45930
5920092 ≠ 26
6260026 = 26 匹配
765120
85340
935110

滚动哈希计算示例(从 ):

。但 ,需要重新计算。

修正:

。故

但表中 ,需要再次验证。直接计算 。实际上

此处说明一个重要细节:负数取模的正确处理至关重要,实现时务必确保中间结果非负。

完整的滚动哈希逐步计算(验证表中所有 值):

滚动计算
0直接计算:5
11

直接验证:

直接计算
241
315
459
592
626
765
853
935

注意:上表中 (非9),(非3),(非0),(非4),(非11)。这表明前面的表格中部分 值需要修正。正确的匹配过程如下:

修正后的匹配表:

命中?验证
03150
11410
24120
31520
45970
59210
6260026 = 26 匹配
7650065 ≠ 26
85310
93590

有效偏移量:。注意 处发生假命中:6526 的哈希值都为 ,但 65 ≠ 26

这个修正实例说明:手动计算滚动哈希时务必用直接计算法交叉验证,避免累积误差。

期望运行时间分析

定理:在均匀哈希假设下,Rabin-Karp算法的期望运行时间为

证明思路

【命中概率(每个位置命中概率为 )】:假设 上均匀分布,则对于任意 的概率为

【期望命中次数(期望 次)】:在 个位置中,期望命中次数为

【期望验证总时间()】:每次命中验证需 时间,期望验证总时间为

【总期望时间( 足够大)】:预处理 ,滚动哈希 ,验证 。选取 时,,总期望时间

多重模式搜索扩展

Rabin-Karp算法的一个重要优势是天然支持多重模式搜索(multiple-pattern search):给定 个模式 ,找出它们在文本 中的所有出现位置。

扩展方法

  1. 预处理:计算每个模式 的哈希值 ,存入散列表。
  2. 匹配:对文本的每个窗口计算 ,在散列表中查找 。若找到,对每个哈希值为 的模式进行验证。

时间复杂度:预处理 (假设所有模式长度均为 ),匹配阶段 期望时间。这比分别对每个模式运行算法的 期望时间有显著改善。

应用场景:病毒特征码检测、多关键词搜索引擎、抄袭检测系统等。

素数 的选择策略

选取合适的素数 对 Rabin-Karp 算法的性能和正确性至关重要:

  1. 碰撞概率 越大,碰撞概率 越低。但 过大会导致乘法运算溢出。
  2. 实际选择:通常选取接近机器字长的最大素数。例如,在32位系统中可选取 (Mersenne素数),在64位系统中可选取更大的素数。
  3. Mersenne素数的优势:形如 的素数允许利用位运算高效取模,如 ,可迭代执行直到结果小于
  4. 随机化选择:更严格的做法是从某个范围内的素数中随机选取 ,使算法成为真正的随机化算法(Las Vegas 或 Monte Carlo 变体)。

Rabin-Karp算法 vs. 其他字符串匹配算法

算法预处理时间匹配时间(最坏)匹配时间(期望)空间核心技巧
朴素暴力比较
Rabin-Karp滚动哈希
KMP失败函数
Boyer-Moore$O(m+\Sigma)$

Rabin-Karp 的独特优势在于:期望线性时间 + 常数空间 + 多模式搜索能力。KMP 保证最坏线性时间但空间为 且不支持多模式。Boyer-Moore 在实践中往往最快(子线性期望时间),但最坏情况退化且实现复杂。


朴素匹配算法的实际性能特征

朴素字符串匹配算法虽然最坏情况为 ,但在实际应用中往往表现良好。当文本和模式的字母表较大时(如自然语言文本),不匹配通常在前几个字符就被发现,使得平均比较次数远小于最坏情况。研究表明,在英文文本上的平均比较次数约为 ,接近最优。然而,在生物信息学(DNA序列,字母表大小仅为4)等场景中,重复字符模式频繁出现,朴素算法的性能会急剧退化,此时需要更高级的算法如 KMP、Boyer-Moore 或 Rabin-Karp。

Rabin-Karp算法的历史渊源

Rabin-Karp算法由 Richard M. Karp 和 Michael O. Rabin 于1987年在 IBM Journal of Research and Development 上发表,论文题为 “Efficient Randomized Pattern-Matching Algorithms”。该算法的突破性在于将随机化(randomization)引入字符串匹配领域:通过选取随机素数作为模数,算法以极高概率给出正确结果。这种”蒙特卡洛式”算法设计思想对后来的计算理论产生了深远影响。Karp 因其在算法理论方面的贡献获得1985年图灵奖,Rabin 因其在计算复杂性理论方面的贡献获得1976年图灵奖。

滚动哈希的工程应用

滚动哈希(rolling hash)的思想远不止于字符串匹配。Andrew Tridgell 在1996年设计的 rsync 算法利用滚动哈希实现高效的增量文件同步:发送端将文件分块并计算每块的弱校验和(基于滚动哈希)和强校验和(MD4/MD5),接收端通过滑动窗口在本地文件中搜索匹配块,仅传输差异部分。此外,滚动哈希还广泛应用于抄袭检测(plagiarism detection):通过比较文档的 -gram 指纹集合来度量文档相似度;以及生物信息学中的 DNA 序列比对、垃圾邮件过滤、重复数据删除等领域。

哈希碰撞与双重哈希防御

Rabin-Karp算法的正确性依赖于哈希碰撞概率足够低。选取素数 是关键:素数模数能保证更均匀的哈希分布。在安全性要求高的场景中,可采用双重哈希(double hashing)策略:同时使用两个不同的大素数 计算哈希值 ,只有两个哈希值都匹配才进行验证。此时碰撞概率从 降至 。CMU 15-451课程的分析表明,选取 位的素数即可将错误概率控制在 以内。更高级的做法是使用全域哈希(universal hashing)从素数域中随机选取模数,使算法对任何输入都保持低碰撞概率。


"命中"不等于"匹配"

在 Rabin-Karp 算法中,命中(hit)仅表示 ,即文本窗口和模式的哈希值相同。由于哈希函数将大量可能的字符串映射到有限的 集合中,不同的字符串完全可能产生相同的哈希值(碰撞,collision)。因此,每次命中后必须执行显式的逐字符验证(第9行),确认 后才能报告匹配。将”命中”与”匹配”混为一谈是初学者最常见的错误之一。在上面的实例中, 处就发生了假命中:9226 的哈希值都为 ,但它们显然不同。

滚动哈希的模运算溢出与负数处理

滚动更新公式 中, 可能为负数。在数学上,模运算的结果应为非负数(),但许多编程语言的 % 运算符对负数的行为与数学定义不一致(如 C/C++ 中 而非 )。实现时必须确保中间结果非负,常见做法是在取模前加上 。此外,当 超过整数范围时可能发生溢出,实际工程中通常使用64位整数或大整数库来避免此问题。

朴素算法的最坏情况触发条件

朴素算法的最坏情况 并非理论空谈,在实际中确实会被触发。典型场景包括:

  • 重复字符文本 + 近似重复模式:如 a),ab),每次偏移都需比较 个字符
  • DNA 序列分析:字母表仅含 四个字符,重复模式极为常见
  • 二进制数据匹配:字母表大小为2,碰撞概率极高

在这些场景中,朴素算法的性能会急剧退化,必须使用 KMP、Rabin-Karp 等更高效的算法。


习题精选

题号题目核心考点难度
32.1-2分析朴素算法在二进制字母表上的行为最坏情况实例构造★★☆
32.1-4证明朴素算法可以在线性期望时间内找到模式期望分析★★★
32.2-1使用 Rabin-Karp 算法手动模拟匹配过程滚动哈希计算★★☆
32.2-4分析多重模式搜索的时间复杂度多模式扩展分析★★★

视频学习指南

资源讲者/平台覆盖内容时长推荐度
MIT 6.006 Lecture 9: String MatchingErik Demaine / MIT OCW朴素匹配、Rabin-Karp、KMP~75min★★★★★
Abdul Bari - Rabin Karp AlgorithmAbdul Bari / YouTubeRabin-Karp 直观讲解与实例~12min★★★★☆
NeetCode - Rabin KarpNeetCode / YouTubeLeetCode 实战应用~8min★★★☆☆
CMU 15-451 Lecture 4: FingerprintingCMU指纹法理论基础、碰撞分析~80min★★★★★
Tushar Roy - Rabin Karp AlgorithmTushar Roy / YouTube滚动哈希图解与代码实现~18min★★★★☆

学习建议

  • 入门首选 Abdul Bari 的视频,讲解直观易懂,适合建立初步理解
  • 深入理解推荐 MIT 6.006 和 CMU 15-451,前者侧重算法设计思路,后者侧重数学分析
  • 实战练习推荐 NeetCode,配合 LeetCode 187(重复DNA序列)和 LeetCode 1044(最长重复子串)巩固

教材原文

“The naive algorithm finds all valid shifts using a loop that checks the condition for each of the possible values of .”

“The Rabin-Karp algorithm uses a hash function to efficiently check the condition. It computes the hash value of the pattern and the hash values of each -character substring of the text, comparing with for each shift . When , the algorithm verifies the match by comparing the pattern with the text substring character by character.”

“The key insight is that we can compute from in constant time, using the equation , where .”

“Although the worst-case running time of RABIN-KARP-MATCHER is , the algorithm runs much faster on average and in practice. It also generalizes nicely to other pattern-matching problems, such as finding multiple patterns simultaneously.”

—— CLRS, Chapter 32: String Matching, Sections 32.1-32.2


参见Wiki

本节直接依赖

  • 散列函数:Rabin-Karp 算法中哈希函数的理论基础
  • 散列表:多重模式搜索中用于存储模式哈希值
  • 模运算:滚动哈希中取模运算的数学基础
  • 哈希函数:哈希函数的一般性质与设计原则

前置知识

  • 第31章 数论算法:模运算与素数理论的深入讨论,为理解模运算哈希提供数论基础
  • 伪随机数:随机化算法中素数选取的理论支撑

后续关联

  • 第32章 字符串匹配:本章其他算法(KMP、Boyer-Moore、有限自动机等),可与 Rabin-Karp 进行对比学习

第32章-字符串匹配 #学习/算法导论/字符串匹配/朴素匹配