KMP算法
KMP算法通过预处理模式串构建failure function,在失配时避免无谓的重复比较,实现O(n+m)的线性时间复杂度,是字符串算法的经典范例。
- 核心思想:利用模式串自身的重复结构避免回退
- failure function π[i]:最长真前缀同时也是后缀的长度
- 时间复杂度:预处理O(m),匹配O(n+m),最坏情况保证
- 理论意义:证明字符串匹配可以在线性时间内完成
问题背景与动机
Section titled “问题背景与动机”Knuth-Morris-Pratt(KMP)算法解决的是经典的精确字符串匹配问题:
给定文本
T和模式串P,在T中找出所有与P完全相同的出现位置。
与朴素扫描不同,KMP的关键创新是:当发生失配时,不是简单地回退到下一个位置重新开始,而是利用已经匹配过的信息,跳过那些肯定不会匹配的位置。
KMP算法由 Donald Knuth、James H. Morris 和 Vaughan Pratt 于1977年发表,是首个在最坏情况下具有线性时间复杂度的字符串匹配算法。
发表背景:
- Morris 在 IBM 工作时独立发现了该算法(1970年)
- Knuth 和 Pratt 随后独立发现了类似结果
- 三人合作发表了经典论文 “Fast pattern matching in strings”
历史意义:
- 证明了字符串匹配可以在 时间内完成
- 开创了”预处理模式串以加速匹配”的算法范式
- failure function 的概念影响了后续众多算法
在生物信息学中的价值
Section titled “在生物信息学中的价值”虽然现代生物信息学工具很少直接使用KMP算法,但其核心思想具有深远影响:
算法思想传承:
- Aho-Corasick算法:将failure function扩展到多模式匹配场景
- Suffix Automaton:利用类似的重复结构进行高效字符串处理
- Read Mapper设计:seed-and-extend策略中避免重复计算的思想
教学价值:
- 理解”预处理如何避免重复计算”的经典案例
- 理解复杂度分析的实际意义
- 建立字符串算法的理论基础
局限性: KMP在生物信息学中的直接应用有限,主要因为:
- 实际生物序列查询通常需要近似匹配(允许错配)
- 大规模基因组查询更适合基于索引的方法(BWT/FM-index)
- 多模式匹配场景更常用Aho-Corasick
朴素扫描的问题
Section titled “朴素扫描的问题”考虑在文本 T = "ABABABAB..." 中匹配模式串 P = "ABABC":
T: A B A B A B A B ...P: A B A B C ^ 失配朴素扫描会:
- 从位置0开始,匹配到第4个字符时失配(C vs B)
- 回退到位置1重新开始
- 重复比较前面已经匹配过的”ABAB”
问题是:我们已经知道位置1-3是”ABA”,但朴素方法没有利用这个信息。
KMP的洞察
Section titled “KMP的洞察”KMP的关键观察是:当在位置 i 失配时,我们已经知道 T[i-j:i] 与 P[0:j] 完全匹配。如果 P 的某个前缀同时也是这个匹配后缀的后缀,我们就可以利用这个信息跳过一些位置。
具体来说,如果 P[0:j] 的某个真前缀等于它的后缀,那么我们可以直接跳到下一个可能匹配的位置,而无需重新比较。
Failure Function
Section titled “Failure Function”- failure function `π[i]`
- 表示模式串 `P` 的前缀 `P[0:i]` 的最长真前缀同时也是该前缀后缀的长度。
- 真前缀
- 不等于整个字符串的前缀。
- 失配时跳转
- 当在位置 `i` 失配时,模式串跳转到 `π[i-1]` 位置继续比较。
对于模式串 P = p_0p_1...p_{m-1},定义 π[i] 为:
即:P[0:i] 的最长真前缀同时也是该前缀后缀的长度。
计算failure function
Section titled “计算failure function”算法1:计算failure function
输入:模式串 P = p_0p_1...p_\{m-1\}输出:failure function π[0:m]
1. π[0] = 02. j = 03. for i = 1 to m-1: a. while j > 0 and P[i] ≠ P[j]: j = π[j-1] b. if P[i] == P[j]: j = j + 1 c. π[i] = j4. return π时间复杂度:
空间复杂度:
考虑模式串 P = "ABABC":
| i | P[i] | j (匹配长度) | π[i] |
|---|---|---|---|
| 0 | A | 0 | 0 |
| 1 | B | 0 → 1 | 1 |
| 2 | A | 1 → 0 → 1 | 1 |
| 3 | B | 1 → 2 | 2 |
| 4 | C | 2 → 0 | 0 |
得到的failure function为:π = [0, 1, 1, 2, 0]
KMP匹配算法
Section titled “KMP匹配算法”算法2:KMP字符串匹配
输入:文本 T = t_0t_1...t_\{n-1\},模式串 P = p_0p_1...p_\{m-1\}输出:所有匹配位置
1. 预处理:计算failure function π2. j = 0 // 模式串中的当前匹配位置3. for i = 0 to n-1: a. while j > 0 and T[i] ≠ P[j]: j = π[j-1] // 失配时跳转 b. if T[i] == P[j]: j = j + 1 c. if j == m: // 找到完整匹配 输出匹配位置 i - m + 1 j = π[j-1] // 继续寻找下一个匹配时间复杂度:
空间复杂度:
在文本 T = "ABABABABC" 中匹配 P = "ABABC":
T: A B A B A B A B CP: A B A B C ^ 匹配 j=0→1
T: A B A B A B A B CP: A B A B C ^ 匹配 j=1→2
T: A B A B A B A B CP: A B A B C ^ 匹配 j=2→3
T: A B A B A B A B CP: A B A B C ^ 匹配 j=3→4
T: A B A B A B A B CP: A B A B C ^ 失配,j = π[3] = 2
T: A B A B A B A B CP: A B A B C ^ 失配,j = π[1] = 1
T: A B A B A B A B CP: A B A B C ^ 匹配 j=1→2
...继续...最终在位置4找到匹配:"ABABC"
- 预处理:计算failure function需要 时间
- 匹配:主循环中,文本指针
i只向前移动,模式串指针j通过failure function跳转,总的比较次数不超过 - 总体:
为什么是线性时间
Section titled “为什么是线性时间”关键在于:
- 文本指针
i在外层循环中只增不减 - 模式串指针
j通过failure function跳转,但每次跳转都会让j减小 j的总增加次数不超过 ,总减少次数也不超过- 因此总操作次数是
与其他算法的比较
Section titled “与其他算法的比较”| 算法 | 预处理时间 | 匹配时间 | 空间 | 特点 |
|---|---|---|---|---|
| 朴素扫描 | 简单但慢 | |||
| KMP | 最坏情况线性,适合多次匹配同一模式 | |||
| Boyer-Moore | $O(m+ | \Sigma | )$ | 平均 |
| Rabin-Karp | 平均 | 哈希方法,适合多模式匹配 |
在生物信息学中的位置
Section titled “在生物信息学中的位置”KMP本身在现代生物信息学工具中不常直接使用,但:
- 思想传承:Aho-Corasick算法(多模式匹配)扩展了failure function的思想
- 索引基础:suffix automaton、suffix array等高级结构都利用了类似的预处理思想
- 教学价值:是理解”如何避免重复计算”的经典案例
在实际的生物序列搜索中,更常见的是:
- 基于k-mer的索引(如BWA、minimap2)
- 基于后缀的索引(如FM-index)
- 这些方法在预处理阶段构建复杂索引,查询时达到亚线性时间
KMP算法的发表是字符串算法发展的重要里程碑。在此之前,人们认为字符串匹配的最坏时间复杂度不可能优于 。KMP证明通过适当的预处理,线性时间是可以实现的。
有趣的是,Morris 在1970年就发现了该算法,但当时只作为 IBM 内部技术报告发布。直到1974年Knuth开始研究该问题,Morris 才公开发表自己的工作。Knuth、Morris 和 Pratt 最终合作完成了经典论文。
- Knuth, D. E., Morris, J. H., & Pratt, V. R. (1977). Fast pattern matching in strings. SIAM J. Comput., 6(2), 323-350.
- Cormen, T. H., et al. (2009). Introduction to Algorithms (3rd ed.), Chapter 32. MIT Press.
- Gusfield, D. (1997). Algorithms on Strings, Trees, and Sequences. Cambridge University Press.
- Aho, A. V., & Corasick, M. J. (1975). Efficient string matching: an aid to bibliographic search. Commun. ACM, 18(6), 333-340.