KMP 算法(Knuth-Morris-Pratt 算法)是一个著名的字符串匹配算法,效率很高,但是确实有点复杂。
很多读者抱怨 KMP 算法无法理解,这很正常,想到大学教材上关于 KMP 算法的讲解,也不知道有多少未来的 Knuth、Morris、Pratt 被提前劝退了。有一些优秀的同学通过手推 KMP 算法的过程来辅助理解该算法,这是一种办法,不过本文要从逻辑层面帮助读者理解算法的原理。十行代码之间,KMP 灰飞烟灭。
先在开头约定,本文用 pat
表示模式串,长度为 M
,txt
表示文本串,长度为 N
。KMP 算法是在 txt
中查找子串 pat
,如果存在,返回这个子串的起始索引,否则返回 -1。
为什么我认为 KMP 算法就是个动态规划问题呢,等会再解释。对于动态规划,之前多次强调了要明确 dp
数组的含义,而且同一个问题可能有不止一种定义 dp
数组含义的方法,不同的定义会有不同的解法。
读者见过的 KMP 算法应该是,一波诡异的操作处理 pat
后形成一个一维的数组 next
,然后根据这个数组经过又一波复杂操作去匹配 txt
。时间复杂度 O(N),空间复杂度 O(M)。其实它这个 next
数组就相当于 dp
数组,其中元素的含义跟 pat
的前缀和后缀有关,判定规则比较复杂,不好理解。本文则用一个二维的 dp
数组(但空间复杂度还是 O(M)),重新定义其中元素的含义,使得代码长度大大减少,可解释性大大提高。
PS:本文的代码参考《算法4》,原代码使用的数组名称是 dfa
(确定有限状态机),因为我们的公众号之前有一系列动态规划的文章,就不说这么高大上的名词了,我对书中代码进行了一点修改,并沿用 dp
数组的名称。
一、KMP 算法概述
首先还是简单介绍一下 KMP 算法和暴力匹配算法的不同在哪里,难点在哪里,和动态规划有啥关系。
暴力的字符串匹配算法很容易写,看一下它的运行逻辑:
// 暴力匹配(伪码)
int search(String pat, String txt) {
int M = pat.length;
int N = txt.length;
for (int i = 0; i < N - M; i++) {
int j;
for (j = 0; j < M; j++) {
if (pat[j] != txt[i+j])
break;
}
// pat 全都匹配了
if (j == M) return i;
}
// txt 中不存在 pat 子串
return -1;
}
对于暴力算法,如果出现不匹配字符,同时回退 txt
和 pat
的指针,嵌套 for 循环,时间复杂度 O ( M N ) O(MN) O(MN),空间复杂度 O ( 1 ) O(1) O(1)。最主要的问题是,如果字符串中重复的字符比较多,该算法就显得很蠢。
比如 txt = “aaacaaab” pat = “aaab”:
很明显,pat
中根本没有字符 c,根本没必要回退指针 i
,暴力解法明显多做了很多不必要的操作。
KMP 算法的不同之处在于,它会花费空间来记录一些信息,在上述情况中就会显得很聪明:
再比如类似的 txt = “aaaaaaab” pat = “aaab”,暴力解法还会和上面那个例子一样蠢蠢地回退指针 i
,而 KMP 算法又会耍聪明:
因为 KMP 算法知道字符 b 之前的字符 a 都是匹配的,所以每次只需要比较字符 b 是否被匹配就行了。
KMP 算法永不回退 txt
的指针 i
,不走回头路(不会重复扫描 txt
),而是借助 dp
数组中储存的信息把 pat
移到正确的位置继续匹配,时间复杂度只需 O(N),用空间换时间,所以我认为它是一种动态规划算法。
KMP 算法的难点在于,如何计算 dp
数组中的信息?如何根据这些信息正确地移动 pat
的指针?这个就需要确定有限状态自动机来辅助了,别怕这种高大上的文学词汇,其实和动态规划的 dp
数组如出一辙,等你学会了也可以拿这个词去吓唬别人。
还有一点需要明确的是:计算这个 dp
数组,只和 pat
串有关。意思是说,只要给我个 pat
,我就能通过这个模式串计算出 dp
数组,然后你可以给我不同的 txt
,我都不怕,利用这个 dp
数组我都能在 O(N) 时间完成字符串匹配。
具体来说,比如上文举的两个例子:
txt1 = "aaacaaab"
pat = "aaab"
txt2 = "aaaaaaab"
pat = "aaab"
我们的 txt
不同,但是 pat
是一样的,所以 KMP 算法使用的 dp
数组是同一个。
只不过对于 txt1
的下面这个即将出现的未匹配情况:
dp
数组指示 pat
这样移动:
PS:这个j
不要理解为索引,它的含义更准确地说应该是状态(state),所以它会出现这个奇怪的位置,后文会详述。
而对于 txt2
的下面这个即将出现的未匹配情况:
dp
数组指示 pat
这样移动:
明白了 dp
数组只和 pat
有关,那么我们这样设计 KMP 算法就会比较漂亮:
public class</