解决字符串匹配问题,暴力的解法非常简单,只要枚举文本串的起始位置i,然后逐位匹配,失配时,i+1,即可。但是暴力法的时间复杂度为O(nm),当n,m比较大时,难以接受。
下面介绍的KMP算法,时间复杂度O(n+m)。它是由Knuth、Morris、Pratt这3位科学家共同发现的,这也是KMP名字的由来。
next数组
在正式进入KMP算法之前,先来学习一个重要的数组。
现在定义一个int型数组next,其中next[i]表示使子串s[0...i]的前缀s[0...k]等于后缀s[i-k...i]的最大k(注意前缀和后缀可以部分重叠,但不能是s[0...i]本身);如果找不到相等的前后缀,那么就令next[i]=-1。显然,next[i]就是所求前后缀中前缀最后一位的下标。
next数组的求解过程如下:
1、初始化next数组,令j=next[0]=-1。
2、让i在1-len-1范围遍历,对每个i,执行3、4,以求解next[i]。
3、不断令j=next[j],直至j回退为-1,或是s[i]==s[j+1]。
4、如果s[i]==s[j+1],则next[i]=j+1;否则next[i]=j。
##输入样例
ababaab
## next数组的获取
def getNext(s):
global Next
## 初始化j=next[0]=-1
j=-1
Next[0]=-1
## 求解next[1]-next[len-1]
for i in range(1,len(s)):
while j!=-1 and s[i]!=s[j+1]:
## 反复令j=Next[j]
## 直至j退回-1,或是s[i]==s[j+1]
j=Next[j]
if s[i]==s[j+1]:
j+=1
Next[i]=j
s=input()
Next=[-1]*len(s)
getNext(s)
print(Next)
## 输出样例
[-1, -1, 0, 1, 2, 0, 1]
KMP算法
在此前的基础上,下面正式进入KMP算法的讲解。读者会发现,有了上面的基础,KMP算法就是在照葫芦画瓢。从某种角度讲,next数组的含义就是当j+1位失配时,j应该回退到的位置。
由此可以总结出KMP算法的一般思路:
1、初始化j=-1,表示pattern当前已被匹配的最后位。
2、让i遍历文本串text,对每个i,执行3、4来试图匹配text[i]和pattern[j+1]。
3、不断令j=next[j],直到j回退为-1,或是text[i]==pattern[j+1]成立。
4、如果text[i]==pattern[j+1],则令j++。如果j达到m-1,说明pattern是text的子串,返回ture。m=len(pattern)
## 输入样例
abababaac
ababaab
## next数组的获取
def getNext(s):
global Next
## 初始化j=next[0]=-1
j=-1
Next[0]=-1
## 求解next[1]-next[len-1]
for i in range(1,len(s)):
while j!=-1 and s[i]!=s[j+1]:
## 反复令j=Next[j]
## 直至j退回-1,或是s[i]==s[j+1]
j=Next[j]
if s[i]==s[j+1]:
j+=1
Next[i]=j
## KMP算法
def KMP(text,pattern):
## 计算pattern的next数组
getNext(pattern)
##初始化j为-1,表示当前还没有任意一位被匹配
j=-1
for i in range(len(text)):
## 不断回退,直到j==-1或text[i]==pattern[j+1]
while j!=-1 and text[i]!=pattern[j+1]:
j=Next[j]
## text[i]与pattern[j+1]匹配成功,令j加1
if text[i]==pattern[j+1]:
j+=1
## 完全适配
if j==len(pattern)-1:
return True
## 匹配失败
return False
text=input()
pattern=input()
Next=[-1]*len(pattern)
print(KMP(text,pattern))
## 输出样例
False
读者会发现这段代码和实现next求解的代码惊人的相似。事实上稍加思考就会发现,求解next数组的过程其实就是模式串pattern进行自我匹配的过程。
接着考虑如何统计文本串text中模式串pattern出现的次数。主要是适配成功时,ans++,同时j如何后退,模式串pattern在哪个位置开始下一次匹配的问题。
## 输入样例
abababab
abab
## next数组的获取
def getNext(s):
global Next
## 初始化j=next[0]=-1
j=-1
Next[0]=-1
## 求解next[1]-next[len-1]
for i in range(1,len(s)):
while j!=-1 and s[i]!=s[j+1]:
## 反复令j=Next[j]
## 直至j退回-1,或是s[i]==s[j+1]
j=Next[j]
if s[i]==s[j+1]:
j+=1
Next[i]=j
## KMP算法
def KMP(text,pattern):
ans=0
## 计算pattern的next数组
getNext(pattern)
##初始化j为-1,表示当前还没有任意一位被匹配
j=-1
for i in range(len(text)):
## 不断回退,直到j==-1或text[i]==pattern[j+1]
while j!=-1 and text[i]!=pattern[j+1]:
j=Next[j]
## text[i]与pattern[j+1]匹配成功,令j加1
if text[i]==pattern[j+1]:
j+=1
## 完全适配
if j==len(pattern)-1:
ans+=1
j=Next[j]
## 匹配失败
return ans
text=input()
pattern=input()
Next=[-1]*len(pattern)
print(KMP(text,pattern))
## 输出样例
3
至此,大家已经可以理解next数组的求解过程和KMP算法的流程了,一般来说,上面的内容已经够日常使用了。但是,真的没有优化空间了吗?当然不是。优化过程即在求解next数组过程的基础上稍作修改即可得到。(优化,可跳过无意义的回退部分)
优化后的next数组被称为nextval数组,它丢失了next数组的最长相等前后缀含义,却让适配时的处理达到最优,因此nextval数组的含义应该理解为当模式串pattern的i+1位发生失配时,i应该回退到的最佳位置。
## next数组优化部分
def getNextval(s):
j=-1
Nextval[0]=-1
for i in range(1,len(s)):
while j!=-1 and s[i]!=s[j+1]:
j=Nextval[j]
if s[i]==s[j+1]:
j+=1
## 与优化前的区别所在
if j==-1 or s[i+1]!=s[j+1]:
Nextval[i]=j
else:
Nextval[i]=Nextval[j]