今天,重拾《数据结构》中学到的 KMP 算法,印象中 KMP 是《数据结构》中最难的一个点。
KMP 解决的是否 字符串的匹配问题:如果给出两个字符串 text 和 pattern,需要判断字符串 pattern 是否是 text 的子串。
暴力解法时间复杂度为 ,而 KMP 的时间复杂度为 。
KMP 由 Knuth、Morris、Pratt 三位科学家共同发现,也是名字的由来。
next
对字符串 s 而言,它以 i 号位为结尾的子串为 s[0...i]
。对该子串而言,长度为 k + 1 的前缀 s[0...k]
,后缀 s[i - k...i]
。
next[i]
表示使 s[0...i]
的前缀 s[0...k]
等于后缀 s[i - k...i]
的 最大 的 k(k 不能为 i)。 如果找不到相等的前后缀,则 next[i] = -1
。
- 显然,
next[i]
就是所求最长相等前后缀中前缀的最后一位的下标
求解 next 的过程:
- 初始化
next[]
,令j = next[0] = -1
- 让 i 在
1 ~ len - 1
范围遍历,对每个 i,执行 3、4 步骤,求解next[i]
- 不断让
j = next[j]
,直到j == -1
,或是s[i] == s[j + 1]
成立 - 如果
s[i] == s[j + 1]
,则next[i] = j + 1
,否则next[i] = j
1 | void getNext(char s[], int len) { |
KMP
KMP 算法是在 next 数组的基础上进行匹配的。
next 数组的作用:匹配过程中,若 pattern 的 j + 1 位失配,j 应该回退到的位置。
KMP 算法的一般思路:
- 初始化
j = -1
,表示 pattern 当前已被匹配的最后位 - 让 i 遍历 text,对每个 i,执行 3、4 步骤来试图匹配
text[i]
和pattern[j + 1]
- 不断令
j = next[j]
,直到 j 回退到 -1,或者是text[i] == pattern[j + 1]
- 若
text[i] == pattern[j + 1]
,则令 j++。如果j == strlen(pattern) - 1
,则说明匹配成功
1 | bool KMP(char text[], char pattern[]) { |
可以看出 KMP算法与求解 next 数组十分的相似,其实 求解 next 数组就是 pattern 自我匹配的过程。
统计模式串
如何统计文本串 text 中模式串 pattern 出现的次数?稍微修改一下 KMP 的算法即可:
1 | int KMP(char text[], char pattern[]) { |
KMP 优化
复旦大学朱洪教授对 KMP 算法进行了改进,他主要是修改了next函数,跳过了无意义的匹配。
1 | void getNextval(char s[], int len) { |
自动机
其实对 KMP 算法来说,实际上是对模式串 pattern 构造了一个 有限状态自动机(Finite State Machine,FSM),然后将文本串 text 送入自动机,能达到终止状态就成功匹配。
如果把自动机推广为树形,就会产生 字典树(Trie 树、前缀树),此时可以解决 多维字符串匹配问题。通常把解决多维字符串匹配问题的算法称为 AC 自动机(Aho-Corasick automaton)。