KMP算法
很早以前就了解到有KMP算法的存在,当时就是知道其可以高效的匹配字符串,但是没敢细看(其实看了一眼,又吓得赶紧退出来。。),最近突然又看到这个算法,就想着学习一下,鉴于自己理解力不太够,花了好久才明白个大概,因此防止过两天就忘,在这里做个笔记。
字符串前缀与后缀
- 前缀:除了最后一个字符以外,一个字符串的全部头部组合。
- 后缀:除了第一个字符以外,一个字符串的全部尾部组合。
- 最大前后缀:前缀集与后缀集交集中的最大长度的子串
如下图所示:
KMP算法思想
在匹配的过程中,如文本串S匹配到i,模式串匹配P到位置j时,两者不匹配,即S[i]!=P[j]时,保持i不变,j移动到next[j]=k位置处,k等于P[j]之前子串的最大前后缀的长度。相当于将模式串P向后移动j-next[j]个位置。
与暴力破解法相比,其文本串S指针不会回溯。
暴力匹配代码(python):
def baoli(s,t):
s_len=len(s) #目标字符串
t_len=len(t) #匹配字符串
i=0
j=0
while i<s_len and j <t_len:
if s[i]==t[j]:
i+=1
j+=1
else:
j=0
i=i-j+1
if j==t_len:
return i-j
else:
return -1
if __name__=='__main__':
s='sdfwowjeofijowejifeowji'
t='feo'
print(baoli(s,t))
KMP算法复杂度
KMP算法是指在一个文本串S内查找一个模式串P 的出现位置,假设S的长度为n,P的长度为m,传统的暴力匹配法时间复杂度为O(m*n),而KMP算法由两个部分构成:
- 求模式串P的next数组:时间复杂度为O(m)
- 根据next数组在文本串S中匹配P:时间复杂度为O(n)
因此,KMP算法的时间复杂度为O(m+n)
KMP算法过程
前面提到,KMP算法由两个过程组成,首先第二个过程比较好理解。
- 根据next数组在文本串S中匹配P,这里先给出python代码:
def kmp(s,t):
s_len=len(s) #目标字符串
t_len=len(t) #匹配字符串
i=0
j=0
next_array=get_nextArray(t)
while i<s_len and j<t_len:
if j==-1 or s[i]==t[j]: #j==-1存在于开头第一个字符就没匹配上,此时next[j]=-1,因此i+=1后目标字符串向右移一位等于0,
j+=1 #表示匹配字符串继续从第一个开始匹配
i+=1
else:
j=next_array[j]
if j==t_len:
return i-j
else:
return -1
KMP算法核心在于第一部分:求next数组。它是通过递推的方式来解决:已知next [0, …, j],如何求出next [j + 1]??分为两种情况:
- 若p[k] == p[j],则next[j+1] = next [j]+1 = k+1;
- 若p[k] ≠ p[j],如果此时p[next[k]] == p[j],则next[j+1] = next[k]+1,否则继续递归前缀索引k = next[k],而后重复此过程。
关键:为什么递归k=next[k],当p[k]=p[j]时就可以得到next[j+1]=k+1
我在这个地方纠结了很久,不知其所以然心里总是不得劲。。相信很多人跟我一样,因此就自己举例子,通过例子来理解会更清楚。其实这就是next数组的特性,这里的递归:k = next[k]与第一部分kmp算法匹配中的:j=next_array[j]原理是一样的,前者是与模式串P的p[j]比较来决定是否递归,后者是与文本串S中的S[i]对比
如上图所示,求取next[j+1]时,已知next[:j],且next[j]=4=k,此时根据递推式,因为p[j]=’c’,p[k]=p[4]=’a’,因此令k=next[k]=1,此时p[k]=p[1]=’c’=p[j],因此next[j+1]=k+1=2。
通过上述例子可以看到,当p[k]=p[1]=’c’与p[j]相等时候,其前面的k个字符必然等于p[4]=’a’前面的k个字符,同样等于p[j]前面k个字符,因此得到next[j+1]=k+1
第二部分j=next_array[j]原理也是一样,只是它不是跟p[j]比较,而是与文本串S[i]比较。都是在最长前后缀子串中寻找最长子串,直到匹配为止。
next数组求取过程(python):
def get_nextArray(t):
next_array=[-1]
t_len=len(t)
k=-1 #因为采用的是从前往后递推的方法,因此需要知道初始状态:j=0时,next[0]=k=-1
j=0
while j<t_len-1: #之所以是len-1:next[0]是固定值为-1
if k==-1 or t[j]==t[k]:
k+=1
j+=1
next_array.append(k)
else:
k=next_array[k]
return next_array
然而上述next数组存在一定的问题,以下图为例,若在i=5时匹配失败,按照3.2的代码,此时应该把i=1处的字符拿过来继续比较,但是这两个位置的字符是一样的,都是B,既然一样,拿过来比较不就是无用功了么?之所以会这样是因为KMP不够完美。因此需要对代码进行一定的修改,如下所示:
- 如果a位字符与它的next值(即next[a])指向的b位字符相等(即p[a] == p[next[a]]),则a位的next值就指向b位的next值即(next[ next[a] ])。
next数组(优化后)求取过程(python):
def get_nextArray_inp(t): #改进的next数组,当k=next[j],且p[j]=p[k]时,即next[j]=next[next[j]]=next[k]
next_array=[-1]
t_len=len(t)
k=-1
j=0
while j<t_len-1:
if k==-1 or t[j]==t[k]:
k+=1
j+=1
if t[j]==t[k]:
next_array.append(next_array[k])
else:
next_array.append(k)
else:
k=next_array[k]
return next_array
KMP算法优化版与未优化版在某些情况下区别很大,如下所示:
KMP算法(未优化版): next数组表示最长的相同前后缀的长度,我们不仅可以利用next来解决模式串的匹配问题,也可以用来解决类似字符串重复问题等等,这类问题大家可以在各大OJ找到,这里不作过多表述。
KMP算法(优化版): 根据代码很容易知道,优化后的next仅仅表示相同前后缀的长度,但不一定是最长(我个人称之为“最优相同前后缀”)。此时我们利用优化后的next可以在模式串匹配问题中以更快的速度得到我们的答案(相较于未优化版),但是上述所说的字符串重复问题,优化版本则束手无策。
所以,该采用哪个版本,取决于你在现实中遇到的实际问题。
关于KMP算法的博客介绍很多,写的都特别详细,本文就不重复介绍了,主要插入图片和排版太麻烦了。。详细见下面的参考部分