Knuth-Morris-Pratt 字符串查找算法(常简称为 “KMP算法”)是在一个“主文本字符串”S
内查找一个“词”W
的出现,通过观察发现,在不匹配发生的时候这个词自身包含足够的信息来确定下一个匹配将在哪里开始,以此避免对以前匹配过的字符重新检查。
举个例子: leetcode中28. Implement strStr()
返回在haystack中第一次出现Needle的索引,如果Needle不是haystack的一部分,则返回-1。
Example 1:
Input: haystack = "hello", needle = "ll" Output: 2
字符串搜索算法的基本分类
设m为模式的长度,n为可搜索文本的长度,k = |Σ| 是字母表的大小。
Algorithm | Preprocessing time | Matching time[1] | Space |
---|---|---|---|
Naïve string-search algorithm | none | Θ(nm) | none |
Rabin–Karp algorithm | Θ(m) | average Θ(n + m), worst Θ((n−m)m) |
O(1) |
Knuth–Morris–Pratt algorithm | Θ(m) | Θ(n) | Θ(m) |
Boyer–Moore string-search algorithm | Θ(m + k) | best Ω(n/m), worst O(mn) |
Θ(k) |
Bitap algorithm (shift-or, shift-and, Baeza–Yates–Gonnet) | Θ(m + k) | O(mn) | |
Two-way string-matching algorithm | Θ(m) | O(n+m) | O(1) |
BNDM (Backward Non-Deterministic Dawg Matching) | O(m) | O(n) | |
BOM (Backward Oracle Matching) | O(m) | O(mn) |
在过去的几天里,我一直在阅读Knuth-Morris-Pratt字符串搜索算法的各种解释。出于某种原因,没有一个解释是为我做的。一旦我开始阅读“......前缀后缀的前缀”,我就不停地将头撞在砖墙上。
最后,在一遍又一遍地读了同一段CLRS(算法导论)大约30分钟后,我决定坐下来,做一堆例子,然后把它们弄清楚。我现在理解算法,并且可以解释它。对于那些像我一样思考的人,这是用我自己的话说的。作为旁注,我不打算解释为什么它比na“字符串匹配更有效率; 那是在一个解释的非常清楚众多的地方。我将解释它是如何工作的,正如我的大脑所理解的那样。
当然,KMP的关键是部分匹配表。我和理解KMP之间的主要障碍是我没有完全掌握部分匹配表中的值到底意味着什么。我现在试着用最简单的话来解释它们。
这是“abababca”模式的部分匹配表:
char: | a | b | a | b | a | b | c | a |
index: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
value: | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 |
如果我有一个八字符模式(在这个例子的持续时间内让我们说“abababca”),我的部分匹配表将有八个单元格。 如果我正在查看表格中的第八个和最后一个单元格,我对整个模式(“abababca”)感兴趣。 如果我正在查看表格中的第七个单元格,我只对模式中的前七个字符感兴趣(“abababc”)。第八个(“a”)是无关紧要的,可以从建筑物或其他东西上掉下来。 如果我正在看表中的第六个单元格...你明白了。请注意,我还没有谈到每个单元格的含义,而是它所指的内容。
现在,为了谈论其含义,我们需要了解正确的前缀和正确的后缀。
正确的前缀:字符串中的所有字符,其中一个或多个字符被截断。“S”,“Sn”,“Sna”和“Snap”都是“Snape”的正确前缀。
正确的后缀:字符串中的所有字符,一个或多个字符在开头处截断。“agrid”,“grid”,“rid”,“id”和“d”都是“Hagrid”的正确后缀。
考虑到这一点,我现在可以给出部分匹配表中值的一句话含义:
(子)模式中与同一(子)模式中的正确后缀匹配的最长正确前缀的长度。
让我们来看看我的意思。
假设我们正在寻找第三个细胞。从上面你会记得,这意味着我们只对前三个字符(“aba”)感兴趣。在“aba”中,有两个正确的前缀(“a”和“ab”)和两个正确的后缀(“ba”和“a”)。正确的前缀“ab”与两个正确的后缀中的任何一个都不匹配。但是,正确的前缀“a”与正确的后缀“a”匹配。因此,在这种情况下,匹配正确后缀的最长正确前缀的长度是1。
让我们尝试一下第四组。在这里,我们对前四个字符(“abab”)感兴趣。我们有三个正确的前缀(“a”,“ab”和“aba”)和三个正确的后缀(“bab”,“ab”和“b”)。这次,“ab”在两者中,并且是两个字符长,因此单元格4获得值2。
仅仅因为这是一个有趣的例子,让我们也尝试第五小组,这涉及“ababa”。我们有四个正确的前缀(“a”,“ab”,“aba”和“abab”)和四个正确的后缀(“a”,“ba”,“aba”和“baba”)。现在,我们有两个匹配:“a”和“aba”都是正确的前缀和正确的后缀。由于“aba”比“a”长,所以它获胜,而第五个小组获得值3。
让我们跳到第7单元格(倒数第二个单元格),它关注模式“abababc”。即使没有列举所有正确的前缀和后缀,显然也不会有任何匹配; 所有后缀都以字母“c”结尾,并且没有前缀。由于没有匹配,单元格7变为0。
最后,让我们看看第8单元,它关注整个模式(“abababca”)。由于它们都以“a”开头和结尾,我们知道它的值至少为1.但是,它就是它的结束点; 在长度为2或更高时,所有后缀都包含ac,而只有最后一个前缀(“abababc”)。这个七个字符的前缀与七个字符的后缀(“bababca”)不匹配,因此单元格8的前缀为1。
当我们找到部分匹配时,我们可以使用部分匹配表中的值来跳过(而不是重做不必要的旧比较)。该公式的工作方式如下:
如果找到长度为partial_match_length的部分匹配 table[partial_match_length] > 1
,我们可以跳过前面的partial_match_length - table[partial_match_length - 1]
字符。
假设我们将“abababca”模式与“bacbababaabcbab”相匹配。这里是我们的部分匹配表,以便于参考:
char: | a | b | a | b | a | b | c | a |
index: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
value: | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 |
我们第一次获得部分匹配是在这里:
bacbababaabcbab
|
abababc
这是partial_match_length为1. table[partial_match_length - 1]
(或table[0]
)的值为0,因此我们不会先跳过任何一个。我们得到的下一个部分匹配是:
bacbababaabcbab
|||||
abababca
这是partial_match_length为5. table[partial_match_length - 1]
(或table[4]
)的值为3.这意味着我们可以跳过前面partial_match_length - table[partial_match_length - 1]
(或5 - table[4]
或5 - 3
或2
)字符:
// x denotes a skip
bacbababaabcbab
xx|||
abababca
这是partial_match_length 3. table[partial_match_length - 1]
(或table[2]
)的值是1.这意味着我们可以跳过前面partial_match_length - table[partial_match_length - 1]
(或3 - table[2]
或3 - 1
或2
)字符:
// x denotes a skip
bacbababaabcbab
xx|
abababca
此时,我们的模式比文本中的其余字符长,所以我们知道没有匹配。
它不是KMP的详尽解释或正式证明; 这是我大脑中的一个散步,我发现的部分非常详细地拼写出来。 如果您有任何疑问或注意到我搞砸了,请发表评论; 也许我们都会学到一些东西。
原文地址:http://jakeboxer.com/blog/2009/12/13/the-knuth-morris-pratt-algorithm-in-my-own-words/
上面的示例包含算法的所有元素。 目前,我们假设存在“部分匹配”表T,如下所述,其表示在当前的匹配以不匹配结束的情况下我们需要在哪里寻找新匹配的开始。
构造T的条目使得如果我们具有从S [m]开始的匹配,其在比较S [m + i]和W [i]时失败,则下一个可能的匹配将从索引m + i开始 - T [ 我在S中(也就是说,T [i]是在不匹配后我们需要做的“回溯”量)。 这有两个含义:首先,T [0] = -1,表示如果W [0]不匹配,我们就不能回溯,只需检查下一个字符; 第二,尽管下一个可能的匹配将从索引m + i - T [i]开始,如上例所示,我们不需要在此之后实际检查任何T [i]字符,以便我们继续从W搜索[T[I]]。 以下是KMP搜索算法的示例伪代码实现。
algorithm kmp_search: 输入: 一个字符数组,S(要搜索的文本) 一个字符数组,W(单词搜索) 输出: 一个整数数组,P(在S中找到W的位置) 一个整数, nP(位置数) 定义变量: 一个整数,j←0(S中当前字符的位置) 一个整数,k←0(W中当前字符的位置) 一个整数数组,T(表,在其他地方计算) 让nP←0 while j < length(S) do if W[k] = S[j] then let j ← j + 1 let k ← k + 1 if k = length(W) then (occurrence found, if only first occurrence is needed, m ← j - k may be returned here) let P[nP] ← j - k, nP ← nP + 1 let k ← T[k] (T[length(W)] can't be -1) else let k ← T[k] if k < 0 then let j ← j + 1 let k ← k + 1