KMP算法
- 在本文中,将使用始于零的数组来表示字符串。比如,若字符串
S = "ABC"
,则S[2]
表示字符'C'
。这种表示方法与C语言一致。
在计算机科学中,Knuth-Morris-Pratt字符串查找算法(简称为KMP算法)可在一个字符串S
内查找一个词W
的出现位置。一个词在不匹配时本身就包含足够的信息来确定下一个匹配可能的开始位置,此算法利用这一特性以避免重新检查先前配对的字符。
这个算法由高德纳和沃恩·普拉特在1974年构思,同年詹姆斯·H·莫里斯也独立地设计出该算法,最终三人于1977年联合发表。
查找过程
以W
="ABCDABD"
,S
="ABC ABCDAB ABCDABCDABDE"
为例说明查找过程。查找过程同时使用两个循环变量m
和i
:
m
代表主文字符串S
内匹配字符串W
的当前查找位置,i
代表匹配字符串W
当前做比较的字符位置。
图示如下:
1 2 m: 01234567890123456789012 S: ABC ABCDAB ABCDABCDABDE W: ABCDABD i: 0123456
从W
与S
的开头比较起。比对到S[3](=' ')
时,发现W[3](='D')
与之不符。接着并不是从S[1]
比较下去。已经知道S[1]~S[3]
不与W[0]
相合。因此,略过这些字符,令m = 4
以及i = 0
。
1 2 m: 01234567890123456789012 S: ABC ABCDAB ABCDABCDABDE W: ABCDABD i: 0123456
如上所示,检核了"ABCDAB"
这个字符串。然而,下一字符便不相合。可以注意到,"AB"
在"ABCDAB"
的头尾处均有出现。这意味着尾端的"AB"
可以作为下次比较的起始点。因此,令m = 8, i = 2
,继续比较。图标如下:
1 2 m: 01234567890123456789012 S: ABC ABCDAB ABCDABCDABDE W: ABCDABD i: 0123456
于m = 10
的地方,又出现不相符的情况。类似地,令m = 11, i = 0
继续比较:
1 2 m: 01234567890123456789012 S: ABC ABCDAB ABCDABCDABDE W: ABCDABD i: 0123456
这时,S[17](='C')
不与W[6]
相同,但是已匹配部分"ABCDAB"
亦为首尾均有"AB"
,采取一贯的作法,令m = 15
和i = 2
,继续搜索。
1 2 m: 01234567890123456789012 S: ABC ABCDAB ABCDABCDABDE W: ABCDABD i: 0123456
找到完全匹配的字符串了,其起始位置于S[15]
的地方。
部分匹配表
部分匹配表,又称为失配函数,作用是让算法无需多次匹配S
中的任何字符。能够实现线性时间搜索的关键是在主串的一些字段中检查模式串的初始字段,可以确切地知道在当前位置之前的一个潜在匹配的位置。换句话说,在不错过任何潜在匹配的情况下,"预搜索"这个模式串本身并将其译成一个包含所有可能失配的位置对应可以绕过最多无效字符的列表。
对于W
中的任何位置,都希望能够查询那个位置前(不包括那个位置)有可能的W
的最长初始字段的长度,而不是重新从W[0]
开始比较整个字段,这长度就是查找下一个匹配时回退的距离。因此T[i]
是W
的可能的适当初始字段同时也是结束于W[i - 1]
的子串的最大长度。使空串长度是0。当一个失配出现在模式串的最开始,这是特殊情况(无法回退),设置T[0] = -1
,在下面讨论。
创建表算法示例
以W = "ABCDABD"
为例。以下将看到,部分匹配表的生成过程与前述查找过程大同小异,且出于类似原因是高效的。
首先,设定T[0] = -1
。为求出T[1]
,必须找到一个"A"
的真后缀(真后缀指不等于原串的后缀)兼W
的前缀。但"A"
没有真后缀,所以设定T[1] = 0
。类似地,T[2] = 0
。
继续到T[3]
,注意到检查所有后缀有一个捷径:假设存在符合条件的前后缀,两者分别为W[0..1] = W[1..2]
,则必有W[0..0] = W[1..1]
。由于W[0..0]
亦是W
的真前缀,上一步必然已经得到T[2] = 1
(而有T[2] = 0
,说明假设不成立)。一般地,遍历到每个字符时,只有上一步已经发现一个长为m的有效后缀,才需要判断有无长为m+1的后缀,而毋需考虑长为m+2、m+3等的后缀。
从而,不必考虑长为2的后缀,而唯独需要考虑的长度1亦不可行,故得到T[3]=0
。
接下来是W[4] = 'A'
。基于同样的理由,需要考虑的最大长度为1,并且在'A'
这个情况中有效,回退到寻找的当前字符之前的字段,因此T[4] = 0
。
现在考虑下一个字符W[5] = 'B'
,使用这样的逻辑:如果曾发现一个子模式在上一个字符W[4]
之前出现,继续到当前字符W[5]
,那么在它之前它本身会拥有一个结束于W[4]
合适的初始段,与事实相反的是已经找到'A'
是最早出现在结束于W[4]
的合适字段。因此为了找到W[5]
的终止串,不需要查看W[4]
。因此T[5] = 1
。
最后到W[4] = 'A'
。下一个字符是'B'
,并且这也确实是W[5]
。此外,上面的相同参数说明为了查找W[6]
的字段,不需要向前查看W[4]
,所以得出T[6] = 2
。
于是得到下面的表:
i
|
0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
W[i]
|
A | B | C | D | A | B | D |
T[i]
|
-1 | 0 | 0 | 0 | 0 | 1 | 2 |
另一个更复杂和有趣的例子:
i
|
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
W[i]
|
P | A | R | T | I | C | I | P | A | T | E | I | N | P | A | R | A | C | H | U | T | E | ||
T[i]
|
-1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 2 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 2 | 3 | 0 | 0 | 0 | 0 | 0 |
创建表算法的伪代码的解释
上面的例子以最少的复杂步骤展示了组织这个表格的一般性方法。这么做的原理是对整体的搜索:大多数工作已经在检测到当前位置的时候做完了,剩下需要做的很少。略微复杂的一点是找到一个共同前后缀。这就需要有一些初始化的代码。
algorithm kmp_table: input: an array of characters, W (the word to be analyzed) an array of integers, T (the table to be filled) output: nothing (but during operation, it populates the table) define variables: an integer, pos ← 2 (the current position we are computing in T) an integer, cnd ← 0 (the zero-based index in W of the next
character of the current candidate substring) (the first few values are fixed but different from what the algorithm
might suggest) let T[0] ← -1, T[1] ← 0 while pos < length(W) do (first case: the substring continues) if W[pos - 1] = W[cnd] then let cnd ← cnd + 1, T[pos] ← cnd, pos ← pos + 1 (second case: it doesn't, but we can fall back) else if cnd > 0 then let cnd ← T[cnd] (third case: we have run out of candidates. Note cnd = 0) else let T[pos] ← 0, pos ← pos + 1
创建表的算法的效率
创建表的算法的复杂度是 ,其中 是W
的长度。
除去一些初始化的工作,所有工作都在循环中完成。为说明循环执行用了 的时间,考虑pos
和pos - cnd
的大小。
- 在第一个分支里,
pos - cnd
不变,而pos
与cnd
同时自增,自然,pos
增加了。 - 在第二个分支里,
cnd
被更小的T[cnd]
所替代,从而增加了pos - cnd
。 - 在第三个分支里,
pos
增加了,而cnd
不变,所以pos
和pos - cnd
都增加了。
因为pos ≥ pos - cnd
,即在每一个阶段要么pos
增加,要么pos
的一个下界增加。故既然算法在pos = n
时终止,此循环必然在最多 次迭代后终止。因此创建表的算法的复杂度是 。
另见
- Boyer-Moore字符串搜索算法
外部链接
- (英文)An explanation of the algorithm (页面存档备份,存于互联网档案馆) and sample C++ code (页面存档备份,存于互联网档案馆) by David Eppstein
- (英文)Knuth-Morris-Pratt algorithm (页面存档备份,存于互联网档案馆) description and C code by Christian Charras and Thierry Lecroq
- (英文)Interactive animation for Knuth-Morris-Pratt algorithm by Mike Goodrich
- (英文)Explanation of the algorithm from scratch (页面存档备份,存于互联网档案馆) by FH Flensburg.
引用
- 高德纳; James H. Morris, Jr, Vaughan Pratt. Fast pattern matching in strings. SIAM Journal on Computing. 1977, 6 (2): 323–350 [2006-07-27]. (原始内容存档于2010-01-04).
- Thomas H. Cormen; Charles E. Leiserson, Ronald L. Rivest, Clifford Stein. Section 32.4: The Knuth-Morris-Pratt algorithm. Introduction to Algorithms Second edition. MIT Press and McGraw-Hill. 2001: 923-931. ISBN 978-0-262-03293-3.