KMP算法
KMP算法由D.E.Knuth,J.H.Morris和V.R.Pratt提出,故得名KMP,用于模式字符串和原始字符串的匹配;
匹配的描述:给定文本text和模式字符串pattern,从text中找出pattern第一次出现的位置;
首先想到暴力求解(Brute Force),假设text的字符数量是n,pattern的字符数量是m,规定i是pattern的首字符在text中的匹配位置(i属于0到n),j是目前检测到模式字符串的匹配位置(j属于0到m);
n个字符都与m个字符分别比较一次是否相等,即如果
t
e
x
t
[
i
+
j
]
=
p
a
t
t
e
r
n
[
j
]
text\left [ i+j \right ]=pattern\left [ j \right ]
text[i+j]=pattern[j],则j向后移动一位进行两字符串的下一字符比较,如果
t
e
x
t
[
i
+
j
]
≠
p
a
t
t
e
r
n
[
j
]
text\left [ i+j \right ]\neq pattern\left [ j \right ]
text[i+j]=pattern[j],则i先后移动一位,j复位到0,进行下一个text字符的比较;
可见时间复杂度是O(mn)的;
暴力求解的实现如下:
#暴力求解
def bruteforce(text,pattern):
tsize=len(text)
psize=len(pattern)
i=0#当前模式首字符在text中的匹配位置
j=0#模式字符串的匹配位置
while i<tsize and j<psize:
if text[i+j]==pattern[j]:
j+=1
else:
i+=1
j=0
if j>=psize:
return i
return None
bruteforce('python','th')
显然,这是很耗费时间的,因为每次匹配失败,pattern的j立刻就复位到pattern的首位(即j=0),如果pattern的开头几个连续字符和中间的几个连续字符相同,明显可以不用直接复位到j=0,这样就能减少多余的判断;
重点
可以想一下,pattern和text比较时,pattern的第j位之前有三个子串构成[str1,str2,str3],而str1和str3是一样的,既然已经比较到第j个字符,意味着[str1,str2,str3]都已经匹配对了,而在遇到第j个字符时,偏偏不相等,j可以不用回到pattern[0]处,由于str1和str3是一样的,此时text的i可以不变,j回到str1的下一位置处,即当前text的i与pattern的str2首位比较,因为在text的i前已经确定str3是匹配对的,即str1也是匹配对的;所以j只需要回溯到pattern里str1的下一位置;
计算next数组
KMP的要点就在于计算了一个数组,该数组可以确定j应该回到pattern哪个位置,在KMP里,这个数组叫做next数组;
计算next数组:
在pattern[j]前的子串(pattern的j-1前缀):
p
a
t
t
e
r
n
j
−
1
=
p
0
p
1
.
.
.
p
j
−
1
pattern_{j-1}=p_{0}p_{1}...p_{j-1}
patternj−1=p0p1...pj−1
在其中找最长前缀和后缀,假设最长为k,则前缀为
p
0
p
1
.
.
.
p
k
−
1
p_{0}p_{1}...p_{k-1}
p0p1...pk−1,后缀为
p
j
−
k
p
j
−
k
+
1
.
.
.
p
j
−
1
p_{j-k}p_{j-k+1}...p_{j-1}
pj−kpj−k+1...pj−1;它们关系应该是:
p
0
p
1
.
.
.
p
k
−
1
=
p
j
−
k
p
j
−
k
+
1
.
.
.
p
j
−
1
p_{0}p_{1}...p_{k-1}=p_{j-k}p_{j-k+1}...p_{j-1}
p0p1...pk−1=pj−kpj−k+1...pj−1
从而计入next[j]=k,可见,next数组只与pattern有关,给出pattern就能计算出对应的next数组;
举例:对于pattern:“abaabcaba”,当j=5,即在c位置,最大前缀和后缀是"ab",所以next[5]=2,当pattern与text比较到c发现不匹配时(发现text[i]不是c),则根据next[5]=2,回溯到pattern[2]的a与text[i]再次比较;
这样人为的求next是不现实的,所以引出了next的递推关系:
对于
j
j
j,
n
e
x
t
[
j
]
=
k
next[j]=k
next[j]=k,即:
p
0
p
1
.
.
.
p
k
−
1
=
p
j
−
k
p
j
−
k
+
1
.
.
.
p
j
−
1
p_{0}p_{1}...p_{k-1}=p_{j-k}p_{j-k+1}...p_{j-1}
p0p1...pk−1=pj−kpj−k+1...pj−1,对于
j
+
1
j+1
j+1,需要看pattern的j前缀:
若
p
[
k
]
=
p
[
j
]
p[k]=p[j]
p[k]=p[j],则
n
e
x
t
[
j
+
1
]
=
n
e
x
t
[
j
]
+
1
next[j+1]=next[j]+1
next[j+1]=next[j]+1;
而如果
p
[
k
]
≠
p
[
j
]
p[k] \neq p[j]
p[k]=p[j],则记录
k
=
n
e
x
t
[
k
]
k=next[k]
k=next[k],若
p
[
k
]
=
p
[
j
]
p[k]=p[j]
p[k]=p[j],则
n
e
x
t
[
j
+
1
]
=
k
+
1
next[j+1]=k+1
next[j+1]=k+1;不相等的话,就再重复该过程;
计算方法的实现如下:
"""
next是以首个字符为研究对象
递推关系:
next[0]=-1
next[j]=k,即p[0到k-1]=p[j-k到j-1]
对于j+1:
若p[k]=p[j],则next[j+1]=next[j]+1
若p[k]!=p[j],记录next[k],若p[next[k]]=p[j]
"""
def calcnext(pattern):
"""
该函数的实现与理论差异在于next[j]的计算对象是pattern[0->j]
"""
#k=0代表无匹配前缀后缀
k, psize = 0, len(pattern)
pnext = [0]*psize
j = 1
while j < psize:
if (pattern[j] == pattern[k]):
pnext[j] = k + 1
k += 1
j += 1
elif (k!=0):
k = pnext[k-1]
else:
pnext[j] = 0
j += 1
#现在pnext[0, 0, 1, 1, 2, 0, 1, 2, 3]
#处理一下变回理论习惯:next[j]的计算对象是pattern[0->j-1]
pnext.insert(0,-1)
pnext.pop()
#[-1, 0, 0, 1, 1, 2, 0, 1, 2]
return pnext
nextarr=calcnext('abaabcaba')
nextarr
#[-1, 0, 0, 1, 1, 2, 0, 1, 2]
KMP实现
基于next数组,实现KMP就是一个简单的过程:
首先注意,在索引i上,与暴力求解时相比,做一点改动,BF中是
t
e
x
t
[
i
+
j
]
text[i+j]
text[i+j]与
p
a
t
t
e
r
n
[
j
]
pattern[j]
pattern[j]比较,现在让
i
i
i代表BF中的
i
+
j
i+j
i+j,即
i
i
i就是text的字符下标,这样改动利于计算出第一次匹配的位置;
如果
t
e
x
t
[
i
]
=
p
a
t
t
e
r
n
[
j
]
text[i]=pattern[j]
text[i]=pattern[j],则i和j都向后移动一位;
如果
t
e
x
t
[
i
]
≠
p
a
t
t
e
r
n
[
j
]
text[i] \neq pattern[j]
text[i]=pattern[j],则i不变,
j
=
n
e
x
t
[
j
]
j=next[j]
j=next[j],再比较,当
j
=
p
a
t
t
e
r
n
s
i
z
e
j=pattern size
j=patternsize时,代表匹配结束,则首次匹配正确的首个字符位于
i
−
p
a
t
t
e
r
n
s
i
z
e
i-patternsize
i−patternsize,实现如下:
def kmp(text,pattern,nextarr):
ans=-1
i=0
j=0
psize=len(pattern)
textsize=len(text)
while i<textsize:
#j=-1代表已回溯到pattern[0],text移位,pattern复位至pattern[0]
if j==-1 or text[i]==pattern[j]:
i+=1
j+=1
else:
j=nextarr[j]
#看是否匹配结束
if j==psize:
ans=i-psize
break
return ans
nextarr=calcnext('aabc')
#从0开始计数,aabc在text[7]首次出现
kmp('pythonaaabcdpython','aabc',nextarr)
#7