在计算机科学中,Knuth-Morris-Pratt 字符串搜索算法(或KMP算法)通过观察当发生不匹配时,单词本身包含足够的信息来搜索W
主“文本字符串”中“单词”的S
出现。确定下一场比赛的开始位置,从而绕过对先前匹配的角色的重新检查。
该算法由Donald Knuth和Vaughan Pratt于1970年构思,并由James H. Morris独立构思。三者于1977年联合出版。[1] 独立于1969年,Matiyasevich [2] [3]发现了一种类似的算法,由二维图灵机编码,同时研究二进制文件中的字符串模式匹配识别问题字母。这是第一个用于字符串匹配的线性时间算法。
字符串匹配算法想要m
在字符串S[]
中找到与搜索词匹配的起始索引W[]
。
最简单的算法,称为“ 暴力 ”或“朴素”算法,是在每个索引处查找单词匹配m
,即搜索字符串中的位置,即S[m]
。在每个位置m
,算法首先检查被搜索的单词中第一个字符的相等性,即S[m] =? W[0]
。如果找到匹配,则算法通过检查单词位置索引的连续值来测试被搜索单词中的其他字符i
。该算法检索W[i]
被搜索单词中的字符并检查表达式的相等性S[m+i] =? W[i]
。如果所有连续字符W
在位置匹配m
,则在搜索字符串中的该位置处找到匹配。如果索引m
到达字符串的末尾然后没有匹配,在这种情况下,搜索被称为“失败”。
通常,试用检查会很快拒绝试用比赛。如果字符串是均匀分布的随机字母,那么字符匹配的机会是26中的1。在大多数情况下,试验检查将拒绝首字母的匹配。前两个字母匹配的可能性是26 2中的 1(676 中为1 )。因此,如果字符是随机的,那么搜索S[]
长度为k的字符串的预期复杂度是k个比较或O(k)的顺序。预期的表现非常好。如果S[]
是100万个字符并且W[]
是1000个字符,那么字符串搜索应该在大约104万个字符比较之后完成。
预期的表现无法保证。如果字符串不是随机的,那么检查试验m
可能需要进行许多字符比较。最糟糕的情况是,除了最后一个字母之外,两个字符串都匹配。试想一下,该字符串S[]
由1个万字是所有的一个,和这个词W[]
是999个一个字符在最后终止乙字。现在,简单的字符串匹配算法将在每个试验位置检查1000个字符,然后拒绝匹配并推进试验位置。简单的字符串搜索示例现在将进行大约1000个字符比较,时间为100万个位置,进行10亿个字符比较。如果长度W[]
是n那么最坏情况的表现是O(k · n)。
与直接算法相比,KMP算法具有更好的最坏情况性能。KMP花一点时间预计算表(大小的顺序W[]
,Ø(ñ)),然后它使用表做有效率的搜索字符串的Ô(ķ)。
不同之处在于KMP利用了直接算法所没有的先前匹配信息。在上面的示例中,当KMP在第1000个字符(i
= 999)上看到试验匹配失败时,因为S[m+999] ≠ W[999]
它将增加m
1,但它将知道新位置的前998个字符已经匹配。KMP匹配999点甲第1000次字符(位置999)发现不匹配前的字符。推进试验比赛的位置m
会抛弃第一个A,所以KMP知道有998个A字符匹配W[]
且不重新测试; 也就是说,KMP设定i
KMP在预先计算的表和两个状态变量中保持其知识。当KMP发现不匹配时,该表确定KMP将增加多少(变量m
)以及它将在何处恢复测试(变量i
)。
为了说明算法的细节,考虑算法的(相对人为的)运行,其中W
=“ABCDABD”和S
=“ABC ABCDAB ABCDABCDABDE”。在任何给定时间,算法处于由两个整数确定的状态:
m
,表示S
预期匹配W
开始的位置,i
,表示当前考虑的字符的索引W
。每个步骤中的算法进行比较S[m+i]
与W[i]
和增量i
如果它们是相等的。这在运行开始时被描述为
1 2
m:01234567890123456789012
S:ABC ABCDAB ABCDABCDABDE
W:ABC D ABD
i:012 3 456
该算法比较W
“并行”字符的连续字符,如果它们匹配则S
通过递增从一个字符移动到下一个字符i
。但是,在第四步S[3] = ' '
中不匹配W[3] = 'D'
。S[1]
我们注意到,'A'
在第1和第2位之间没有发生,而不是再次开始搜索S
; 因此,在检查了所有这些字符之后(并且知道它们与相应的字符匹配W
),没有机会找到匹配的开始。因此,算法集m = 3
和i = 0
。
1 2
m:01234567890123456789012
S:ABC ABCDAB ABCDABCDABDE
W: A BCDABD
i: 0 123456
此匹配在初始字符处失败,因此算法设置m = 4
和i = 0
1 2
m:01234567890123456789012
S:ABC ABCDAB ABCDABCDABDE
W: ABCDAB D
i: 012345 6
在这里,i
通过几乎完全匹配递增,"ABCDAB"
直到i = 6
给出不匹配W[6]
和S[10]
。但是,就在当前部分匹配结束之前,有一个子字符串"AB"
可能是新匹配的开始,因此算法必须考虑到这一点。由于这些字符与当前位置之前的两个字符匹配,因此无需再次检查这些字符; 算法设置m = 8
(初始前缀的开始)和i = 2
(发信号通知前两个字符匹配)并继续匹配。因此,该算法不仅省略了先前匹配的S
("AB"
)的字符,而且省略了先前匹配的W
(前缀"AB"
)字符。
1 2
m:01234567890123456789012
S:ABC ABCD AB ABCDABCDABDE
W: AB C DABD
i: 01 2 3456
由于W[2]
(a 'C'
)与S[10]
(a ' '
)不匹配,因此在新位置的搜索会立即失败。与在第一次试验中一样,不匹配导致算法返回到开头W
并开始在不匹配的字符位置搜索S
:m = 10
,reset i = 0
。
1 2
m:01234567890123456789012
S:ABC ABCDAB ABCDABCDABDE
W: A BCDABD
i: 0 123456
匹配m=10
失败,因此算法接下来尝试m = 11
和i = 0
。
1 2
m:01234567890123456789012
S:ABC ABCDAB ABCDAB C DABDE
W: ABCDAB D
i: 012345 6
再次,算法匹配"ABCDAB"
,但下一个字符,与单词'C'
的最后一个字符不匹配。像以前一样推理,算法设置,从导致当前位置的双字符串开始,设置并从当前位置继续匹配。 'D'
W
m = 15
"AB"
i = 2
1 2
m:01234567890123456789012
S:ABC ABCDAB ABCD ABCDABD E
W: ABCDABD
i: 0123456
这次比赛完成,比赛的第一个字符是S[15]
。
上面的示例包含算法的所有元素。目前,我们假设存在一个“部分匹配”表T
,如下所述,它表示在当前的匹配以不匹配结束的情况下我们需要寻找新匹配的开始位置。的条目T
是这样构成,如果我们有一个比赛开始S[m]
是比较失败时S[m + i]
要W[i]
,那么下一个可能的比赛将在指数开始m + i - T[i]
在S
(即,T[i]
是“回溯”我们需要的不匹配后做的量)。这有两个含义:第一,T[0] = -1
表示ifW[0]
是不匹配的,我们不能回溯,必须简单地检查下一个字符; 第二,虽然下一个可能的匹配将从索引开始m + i - T[i]
,如上例所示,我们不需要在此T[i]
之后实际检查任何字符,以便我们继续搜索W[T[i]]
。以下是KMP搜索算法的示例伪代码实现。
算法kmp_search:
输入:
一个字符数组,S(要搜索的文本)
一个字符数组,W(寻求的单词)
输出:
一个整数数组,P(找到W的S中的位置)
整数,nP(位数)
定义变量:
一个整数,j←0(S中当前字符的位置)
整数,k←0(W中当前字符的位置)
一个整数数组,T(表,在其他地方计算)
让 nP←0
而 Ĵ<长度(S)做
如果 W [k]的= S [j]的然后
让 Ĵ←J + 1个
设 ķ←K + 1
,如果 K =长度(W)然后
(发现的情况,如果只需要第一次出现,可以在这里返回m←j - k)
令 P [nP]←j - k,nP←nP + 1
令 k←T [k](T [长度(W)]不能为-1)
否则
让 k←T [k]
如果 k <0 则
设 j←j + 1
让 k←k + 1
假设表的先前存在,T
Knuth-Morris-Pratt算法的搜索部分具有复杂度 O(n),其中n是长度,S
并且O是大O符号。除了进入和退出函数时产生的固定开销,所有计算都在while
循环中执行。限制此循环的迭代次数; 观察T
构造成,如果其已在开始比赛S[m]
,而比较失败S[m + i]
到W[i]
,那么下一个可能的匹配必须在开始S[m + (i - T[i])]
。特别是,下一个可能的匹配必须发生在比更高的指数m
,以使T[i] < i
。
这个事实意味着循环最多可以执行2 n次,因为在每次迭代时它都会执行循环中的两个分支之一。第一个分支总是增加i
而不会改变m
,因此m + i
当前检查的字符的索引S
会增加。第二个分支增加i - T[i]
了m
,正如我们所看到的,这总是一个正数。因此,m
增加了当前潜在匹配开始的位置。与此同时,第二支叶m + i
不变,对于m
被i - T[i]
添加进去,紧接着又T[i]
被指定为新的价值i
,因此new_m + new_i = old_m + old_i - T[old_i] + T[old_i] = old_m + old_i
。现在,如果m + i
= n,循环结束; 因此,循环的每个分支,最多可以达到Ñ倍,因为它们分别增加任一m + i
或m
,并且m ≤ m + i
:如果m
= Ñ,那么肯定m + i
≥ Ñ,使得因为它至多增加了由单位增量,我们必须有m + i
= Ñ在过去的某个时刻,因此我们将采取任何一种方式。
因此,循环最多执行2 n次,表明搜索算法的时间复杂度为O(n)。
这是考虑运行时的另一种方式:让我们说我们开始匹配W
并S
在位置i
和p
。如果W
作为S
p 的子字符串存在,那么W[0..m] = S[p..p+m]
。一旦成功,即在position(W[i] = S[p+i]
)处匹配的单词和文本,我们增加i
1.失败时,即单词和文本在位置(W[i] ≠ S[p+i]
)处不匹配,文本指针保持不变,而字指针回滚一定量(i = T[i]
其中T
为跳转表),我们尝试匹配W[T[i]]
用S[p+i]
。回滚的最大数量i
受限于i
也就是说,对于任何失败,我们只能尽可能多地回滚到失败。然后很明显运行时是2 n。
该表的目标是允许算法不匹配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]
,我们看到substring W[0]
- W[1]
("AB"
)有一个正确的后缀"B"
。但是“B”不是模式的前缀W
。因此,我们设定T[2] = 0
。
继续T[3]
,我们首先检查长度为1的正确后缀,并且与之前的情况一样,它失败了。我们还应该检查更长的后缀吗?不,我们现在注意到检查所有后缀的快捷方式:让我们说我们发现了一个正确的后缀,它是一个正确的前缀(一个字符串的正确前缀不等于字符串本身),并W[2]
以长度为2 结尾(最大可能); 那么它的第一个字符也是一个正确的前缀W
,因此它本身就是一个正确的前缀,它结束于W[1]
,我们已经确定它不会发生,T[2] = 0
而不是T[2] = 1
。因此,在每个阶段,快捷规则是只有在前一阶段(即T[x] = m
)找到大小为m的有效后缀并且不应该检查m + 2时,才需要考虑检查给定大小m + 1的后缀。m + 3等
因此,我们甚至不需要关注长度为2的子串,并且在前一种情况下,长度为1的唯一的子串也会失效,因此T[3] = 0
。
我们传递给后来的W[4]
,'A'
。相同的逻辑表明我们需要考虑的最长子字符串长度为1,并且与之前的情况一样,它失败,因为“D”不是前缀W
。但是,不是设置T[4] = 0
,我们可以通过注意到这一点做得更好W[4] = W[0]
,而且查找T[4]
相应S
字符的查找也是S[m+4]
不匹配的S[m+4] ≠ 'A'
。因此重新开始搜索没有意义S[m+4]
; 我们应该提前1点开始。这意味着我们可以W
通过匹配长度加一个字符来移动模式,所以T[4] = -1
。
现在考虑下一个字符,W[5]
即'B'
:通过检查看起来最长的子字符串'A'
,我们仍然设置T[5] = 0
。推理类似于原因T[4] = -1
。W[5]
本身扩展了前缀匹配开始W[4]
,我们可以假设相应的字符S
,S[m+5] ≠ 'B'
。所以回溯之前W[5]
是没有意义的,但S[m+5]
可能是'A'
因此T[5] = 0
。
最后,我们看到正在进行的片段中的下一个角色W[4] = 'A'
将会开始'B'
,实际上也是如此W[5]
。此外,与上面相同的论点表明我们不需要在之前W[4]
找到一个段W[6]
,因此这就是它,我们采取T[6] = 2
。
因此,我们编译下表:
i |
0 | 1 | 2 | 3 | 4 | 五 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
W[i] |
一个 | 乙 | C | d | 一个 | 乙 | d | |
T[i] |
-1 | 0 | 0 | 0 | -1 | 0 | 2 | 0 |
另一个例子:
i |
0 | 1 | 2 | 3 | 4 | 五 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
W[i] |
一个 | 乙 | 一个 | C | 一个 | 乙 | 一个 | 乙 | C | |
T[i] |
-1 | 0 | -1 | 1 | -1 | 0 | -1 | 3 | 2 | 0 |
另一个例子(与前一个例子略有不同):
i |
0 | 1 | 2 | 3 | 4 | 五 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
W[i] |
一个 | 乙 | 一个 | C | 一个 | 乙 | 一个 | 乙 | 一个 | |
T[i] |
-1 | 0 | -1 | 1 | -1 | 0 | -1 | 3 | -1 | 3 |
另一个更复杂的例子:
i |
00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
W[i] |
P | 一个 | [R | Ť | 一世 | C | 一世 | P | 一个 | Ť | Ë | 一世 | ñ | P | 一个 | [R | 一个 | C | H | ü | Ť | Ë | |||
T[i] |
-1 | 0 | 0 | 0 | 0 | 0 | 0 | -1 | 0 | 2 | 0 | 0 | 0 | 0 | 0 | -1 | 0 | 0 | 3 | 0 | 0 | 0 | 0 | 0 | 0 |
上面的例子说明了以最小的忙乱组装桌子的一般技术。原则是整体搜索的原则:大部分工作已经完成到达当前位置,因此离开它时几乎不需要做。唯一的小复杂因素是字符串后期正确的逻辑在开头错误地给出了不正确的子字符串。这需要一些初始化代码。
algorithm kmp_table:
输入:
一个字符数组,W(要分析的单词)
一个整数数组,T(要填充的表)
输出:
什么都没有(但在操作过程中,它会填充表格)
定义变量:
一个整数,pos←1(我们在T中计算的当前位置)
一个整数,cnd←0(当前候选子字符串的下一个字符的W中从零开始的索引)
设 T [0]←-1
而 POS <长度(W)做
如果 W [POS] = W [CND] 然后
让 T [POS]←T [CND]
否则
让 T [POS]←CND
让 CND←T [CND](提高性能)
,同时 cnd> = 0 和 W [pos] <> W [cnd] 确实
让 cnd←T [cnd]
让 pos←pos + 1,cnd←cnd + 1
设 T [pos]←cnd(仅在搜索所有单词出现时才需要)
表算法的复杂性是O(k)
,k
长度在哪里W
。至于除了一些初始化所有的工作都是在做while
循环,这足以表明,这种循环在执行O(k)
的时间,这将通过同时检查的数量来完成pos
和pos - cnd
。在第一分支,pos - cnd
被保留,因为两者pos
和cnd
同时递增,但是自然,pos
被增加。在第二个分支中,cnd
被替换为T[cnd]
,我们在上面看到的总是严格小于cnd
,因此增加pos - cnd
。因为pos ≥ pos - cnd
,这意味着在每个阶段或者pos
下限pos
增加; 因此,由于算法终止一次pos = k
,它必须在2k
循环的大多数迭代之后终止,因为pos - cnd
从开始算起1
。因此,表算法的复杂性是O(k)
。
由于算法的两个部分分别具有O(k)
和O(n)
的复杂度,因此整个算法的复杂性是O(n + k)
。
这些复杂性都是一样的,不管有多少重复模式都在W
或S
。
一个实时 KMP的版本可以使用单独的故障函数表字母表中的每个字符来实现。如果文本中的字符出现不匹配,则会针对发生不匹配的模式中的索引查询字符的失败函数表。这将返回在匹配模式前缀时结束的最长子字符串的长度,以及前缀后面的字符所添加的条件。有了这个限制,文本中的字符不需要在下一个阶段再次检查,因此在处理文本的每个索引之间只执行一定数量的操作[ 需要引证 ]。这满足了实时计算限制。
该展位算法使用KMP预处理功能的修改版本,找到字典序最小的串旋转。随着字符串的旋转,逐渐计算失败函数
import random
import datetime
def BF_Match(s, t):
slen = len(s)
tlen = len(t)
if slen >= tlen:
for k in range(slen - tlen + 1):
i = k
j = 0
while i < slen and j < tlen and s[i] == t[j]:
i = i + 1
j = j + 1
if j == tlen:
return k
else:
continue
return -1
def KMP_Match_1(s, t):
slen = len(s)
tlen = len(t)
if slen >= tlen:
i = 0
j = 0
next_list = [-2 for i in range(len(t))]
getNext_1(t, next_list)
#print next_list
while i < slen:
if j == -1 or s[i] == t[j]:
i = i + 1
j = j + 1
else:
j = next_list[j]
if(j == tlen):
return i - tlen
return -1
def KMP_Match_2(s, t):
slen = len(s)
tlen = len(t)
if slen >= tlen:
i = 0
j = 0
next_list = [-2 for i in range(len(t))]
getNext_2(t, next_list)
#print next_list
while i < slen:
if j == -1 or s[i] == t[j]:
i = i + 1
j = j + 1
else:
j = next_list[j]
if j == tlen:
return i - tlen
return -1
def getNext_1(t, next_list):
next_list[0] = -1
j = 0
k = -1
while j < len(t) - 1:
if k == -1 or t[j] == t[k]:
j = j + 1
k = k + 1
next_list[j] = k
else:
k = next_list[k]
def getNext_2(t, next_list):
next_list[0] = -1
next_list[1] = 0
for i in range(2, len(t)):
tmp = i -1
for j in range(tmp, 0, -1):
if equals(t, i, j):
next_list[i] = j
break
next_list[i] = 0
def equals(s, i, j):
k = 0
m = i - j
while k <= j - 1 and m <= i - 1:
if s[k] == s[m]:
k = k + 1
m = m + 1
else:
return False
return True
def rand_str(length):
str_0 = []
for i in range(length):
str_0.append(random.choice("abcdefghijklmnopqrstuvwxyz"))
return str_0
def main():
x = rand_str(20000)
y = rand_str(5)
print ("The String X Length is : ", len(x), " String is :")
for i in range(len(x)):
print (x[i])
print ("")
print ("The String Y Length is : ", len(y), " String is :")
for i in range(len(y)):
print (y[i])
print ("")
time_1 = datetime.datetime.now()
pos_1 = BF_Match(x, y)
time_2 = datetime.datetime.now()
print ("pos_1 = ", pos_1)
time_3 = datetime.datetime.now()
pos_2 = KMP_Match_1(x, y)
time_4 = datetime.datetime.now()
print ("pos_2 = ", pos_2)
time_5 = datetime.datetime.now()
pos_3 = KMP_Match_2(x, y)
time_6 = datetime.datetime.now()
print ("pos_3 = ", pos_3)
print ("Function 1 spend ", time_2 - time_1)
print ("Function 2 spend ", time_4 - time_3)
print ("Function 3 spend ", time_6 - time_5)
main()