什么是 KMP 算法
KMP 算法是一种用于字符串模式匹配的高效算法,由 Knuth、Morris、Pratt 三位大佬于 1977 年提出,算法名称来源于这几个人的名字首字母。可以将时间复杂度从傻瓜式的暴力匹配的 O(m x n) 降低到 O(m + n) 级别,效率提升非常大。
暴力匹配
傻瓜式的暴力匹配原理很简单,对于 text 的第 i 个字符,分别用 p、t 指针分别指向 text 的第 i 个字符、pattern 的起始字符,然后判断两个下标指针位置的字符是否相等;如果相等,则两个下标指针后移一位;如果不相等,则对 text 字符串的第 (i + 1) 个字符循环上述过程,直到 pattern 中的字符全部匹配成功或者到达 text 的最后一个字符。
第一轮:

第二轮:

第三轮:

代码实现如下。
1 | def match(text, pattern): |
这种匹配方式实现起来非常简单,时间复杂度为 O(m x n)。 但是这样的匹配方式存在回溯问题,每次遇到不匹配的字符时需要回退到 text 的第 i + 1 个字符。
KMP 算法的匹配过程
KMP 算法消除了暴力匹配的回溯问题。
观察匹配过程:

直到第 5 个字符: A、C 不匹配时,前面的 4 个字符都是匹配的,并且观察可以发现前 4 个字符中存在重复的序列 AB,因为前 4 个字符是匹配的,所有同样重复的序列在 pattern 中同样存在。于是可以想有没有可能不回溯 text,而是将 pattern 向右移动,移动时跳过重复的 AB 字符?如下图,这样就能保证在当前匹配到的第 5 个位置之前的字符都是匹配的,然后就可以继续往后匹配了,从而节省回溯 text 之后的几次重复无用匹配。

这就是 KMP 算法的思想,利用 pattern 中子串重复的信息来指示当遇到不匹配字符时 pattern 串应该跳过多少次匹配(也就是跳过多少个存在重复的最长子串),确保 text 不回溯,从而提高匹配的效率。
next 数组就是用来保存 pattern 串中截止到每个字符最长的重复子串信息的。
next 数组
上面提到,next 数组是用来保存 pattern 串中截止到当前字符最长重复子串长度信息的。
要计算 next 数组,需要几个概念:最长前缀、最长后缀和最长公共前后缀。
最长前缀
前缀是指所有以第二个字符开始的连续子串,最长前缀就是这些连续子串中最长的一个。
比如 ABAB ,前缀有:A、AB、ABA,最长前缀为 ABA。
最长后缀
后缀是指所有以倒数第二个字符结尾的连续子串,最长后缀就是这些连续子串中最长的一个。
比如 ABAB 中,后缀有:A、BA、ABA。
最长公共前后缀
就是字符串对应的最长前缀和最长后缀的交集中长度最长的一个。
比如 ABAB 中,公共前缀和后缀有:A、ABA,最长的公共前后缀就是 ABA。
next 数组
next 数组是指字符串中以第一个字符开始,以每个字符结尾的子串对应的最长公共前缀的长度为元素的数组。
比如 ABABC 中:
A:不存在公共前后缀,所以长度为 0。
B:子串为 AB,不存在公共前后缀,所以长度为 0。
A:子串为 ABA,最长公共前后缀为 B,长度为 1。
B:子串为 ABAB,最长公共前后缀为 BA,长度为 2。
C:字串为 ABABC,不存在公共前后缀,所以长度为 0。
于是得到 ABABC 的 next 数组如下。

代码实现 next 数组的计算
可以使用暴力求解,遍历 pattern 字符串,对于每个字符,向前找出所有的前缀、向后找出所有的后缀,求两个集合的交集,如果交集为空,则返回 0,否则取出交集中最长的子串的长度即为结果。
在网上看到有效率更高的方法,采用动态规划,从左到右来求最长公共前后缀。大致过程如下:
- 用 next 数组记录结果,用 i 记录当前字符的下标位置,用 lenth 来记录两个指针已经连续匹配上多少个字符,即 text 中对应字符的 next 值。
- 初始化时,next[0] = 0 且 i = 1(即默认第一个字符 next 值为 0,从第 2 个字符开始进行计算),length = 0,lenth 用来记录前一个字符的 next 值,并且可以用作第 2 个指针。
- 对于 text 中的每个字符:
- 如果 text[i] == text[len],说明当前字符在前面出现过,于是 length + 1,并且两个指针都往后移动一位,即 i + 1,lenth 已经也加过 1。
- 如果 text[i] != text[length],说明重复序列结束了,即 text[0] 至 text[length - 1] 这个子串和 text[i - length] 至 text[i - 1] 这个字串相同,但是 text[0] 至 text[length] 这个子串和 text[i - length] 至 text[i] 这个子串最后一位不相同,相同序列结束了,让 length 指针退回到重复序列之前,即 lenth = next[length - 1],再重复上面的步骤,如果 length 退回到了起始位置,说明当前字符对应的最长公共子串为 0。
代码实现如下。
1 | def next(pattern): |
代码实现 KMP 算法
求出 next 数组之后,text 中的指针只管往后走,每当遇到不匹配的字符时,只需要将 pattern 中的 p 指针移动到 p - next[p - 1] 的位置处,跳过 next[p - 1] 次比较即可。循环上面的过程,如果 p 指针走到了最后,说明匹配到结果;如果 i 指针走到了最后并且 p 指针没走到最后,则匹配失败。
Python 代码如下。
1 | def kmp_match(self, text, pattern): |
参考资料
这个算法比较难理解,这次花了很长时间才算是勉强搞明白其中的原理。
可以参考一下这些讲得很不错资料:
B 站上也有一些视频,结合视频动画理解匹配过程会更容易理解。