之前一直保持在word文档中记录总结,最近发现C博客是个分享的好地方,记录自己学习总结的同时,也可以把总结拿出来分享,万一能帮到别人呢^_^
花了两天,把KMP算法搞懂了,此篇旨在用通俗易懂的语言讲算法实现过程,但看此篇之前最好对朴素字符串匹配动手写过代码。好了,下面进入正题。
朴素字符串匹配算法时间复杂度为O(n*m),n/m分别为主串/子串长度,而KMP算法的时间复杂度为O(n+m)。当主串或子串非常长时,这个时间提升的就非常明显了。
KMP算法思路:主串指针i不回溯,只回溯子串指针j,指针j回溯到哪,就看nextVal[i]的值了。所以该算法关键是求nextVal[]数组。其实nextVal[]是在next[]数组的基础上求得的,它改进了使用next[]的不足。下面一步步来讲。
1、next[]数组求解
这里需要先将所给子串化成字符数组char[] p,从1开始放子串字符(为什么从1开始,为了数组计算方便),所以p.length=子串长度+1,next[]长度等于p的长度。
抽象层面来说,next[i]=子串p[1,2,…,i-1]中前缀子串与后缀子串相等时的最大长度+1。具体利用代码自动求解,可以利用next[i]与next[i+1]的递推关系,如下图所示。
假设已知next[i]=j,即p[1,…,k-1]=p[i-k+1,…,i-1],这时比较p[i]和p[j]:
(1)如果p[i]=p[j]=p[next[i]],则有p[1,…,j]=p[i-j+1,…,i],前缀子串与后缀子串相等的最大长度为j+1,即next[i+1]=j+1=next[i]+1.
(2)如果p[i] != p[j],则将j回溯到next[i],即j=next[i],然后继续比较p[i]和p[j].
(3)这里注意特殊情况:j回溯到0后,即到了上图最下面j==0的情况,因为p[0]里面为非子串字符,无意义,这时就要指针i、j都加1,加过后next[i]=j,再继续比较p[i]和p[j]。
(4)初始条件next[1]=0,next[2]=1。
下面是Java版求next[]代码:
public static int[] getNext(char[] p){
int i=2,j=1;
int[] next = new int[p.length];
next[1]=0;
next[2]=1;
while(i<next.length-1){
if (j==0 || p[i]==p[j]){
i++;
j++;
next[i]=j;
}
else j=next[j];
}
return next;
}
2、nextVal[]求解
为什么要引入nextVal[]数组?因为next[]有缺陷,以子串"ababaaaba"为例。
当j=5时,如果子串p[5]!=主串s[i],那么j要回溯到j=next[5]=3,接着,比较p[3]与s[i],因为p[3]==p[5],所以p[3]!=s[i]。可见,这一步是多余的,我们可以直接j=next[3],以此类推。这就是nextVal[]的由来,对求next[]的代码稍加改动,即可求解nextVal[],代码如下。
public static int[] getNextVal(char[] p){
int i=1,j=0;
int[] next = new int[p.length];
int[] nextVal = new int[p.length];
next[2]=1;
nextVal[1]=0;
while(i<next.length-1){
if (j==0 || p[i]==p[j]){
i++;
j++;
next[i]=j;
if(p[i]==p[j]) nextVal[i]=nextVal[j];
else nextVal[i]=j;
}
else j=next[j];
}
return nextVal;
}
3、匹配主串
返回子串在主串中首次出现的位置,str是主串,pc是子串。
public static int KMP(String str,String pc) {
int[] nextVal = getNextVal(pc);
str = '0' + str; //主串
pc = '0' + pc; //子串
char[] s = str.toCharArray();
char[] p = pc.toCharArray();
int i=1,j=0;
while(i<s.length && j<p.length){
if(j==0 || s[i]==p[j]){
i++;
j++;
}
else j=nextVal[j];
}
if(j==p.length) return i-p.length+1;
else return -1; //Unfound
}