KMP是一种字符串匹配算法,用于在字符串a中找出字串b出现的最早位置。
最简单的方法就是逐个比对,两层循环,时间复杂度是两个字符串长度的积。具体方法是先用字串b的第一个字符与a的第一个去比较,相同则用两个的第二个字符比较,如果是一直到b匹配结束,则返回结果;如果是不匹配,则从a的第二个字符开始,b的第一个开始,重新开始匹配。这种方法不多解释。
KMP算法算是上述算法的优化。怎么优化?先看一下怎么做吧。
首先有一个next数组,长度与字串b相同,定义是如果存在k(0<k<j)使b[0]到b[k-1]的字串与b[j-k]到b[j-1]的串相同,则next[j]为最大的那个k;如果k不存在,则next[j]为零,特例next[0]是-1(自己设定的,-2什么的都行,就是后面要做相应的调整)。举例说明,如有字串qwqwrqw,b[0]是'q',b[1]是'w'等等。那么,next[1]是0(看定义,k不存在);因为b[0]不等于b[1],所以j为2时,k也不存在,next[2]为0;j为3时,由于b[0]和b[2]相同,且没有b[0]==b[1]、b[1]==b[2],所以最大的k为1,所以next[3]为1;同理,可以分析出next[4]为2。
知道了next数组的存在,接下来看怎么优化,记得上面的简单做法是在不匹配的时候,字串b从头开始匹配a的下一个字符,KMP算法不这样做,而是让字串b从当前下标的next值开始,a的下标不移动,继续匹配,这样a的下标没有回溯,也就节约了时间。那么为什么可以这样做,我们来分析一下。如果是a[0] == b[0],a[1] == b[1],...,一直到a[x]与b[x]不匹配,假设next[x]值为3(x>3),按照以上说法,接下来比较a[x]与b[next[x]](即b[3])。首先,b[0]到b[next[x]-1](即b[2])与a[x-next[x]](即a[x-3])到a[x-1]相同,不用多解释,因为刚刚b[0]到b[next[x]-1](即b[2])与a[0]到a[next[x]-1](即a[2])已匹配,而b[0]到b[next[x]-1](即b[2])与b[x-next[x]](即b[x-3])到b[x-1]相同,这个是next数组的定义。设有m=x-next[x],如果m>1,则跳过了b[0]与a[1]、a[2]、...、a[m-1]等的比较,那么重点在于为什么可以跳过,这么想的原因是觉得有a[1]到a[x]与b[0]到b[x-1]相同的可能性,那么我们来用反证法否定它:
假设有条件a[1]到a[x]与b[0]到b[x-1]相同且x-next[x]>1。已知的有a[0]到a[x-1]与b[0]到b[x-1]相同,假设意味着a[1]到a[x-1]与b[0]到b[x-2]相同,即有b[1]到b[x-1]与b[0]到b[x-2]相同。这时,在回顾next的定义,这里存在的k是x-1,而next[x]是最大的k,也就是说x-1<=next[x],而x的意义又与next定义中的j相同,而0<k<j,就有next[x]<x,这里得出next[x]等于x-1(由x-1<=next[x]与next[x]<x得出),即x-next[x]==1,与x-next[x]>1冲突,这时否定假设的条件,即a[1]到a[x]与b[0]到b[x-1]的可能性不存在(x-next[x]>1)。
(当然,如果x-next[x]==1,也就是没跳过,没有优化,不需要考虑)
如果有人觉得要是字符串a不是从0开始(进行到中间一段)怎么办,这个证明还正确吗?那么此后,其实每次把x-next[x]当做是a的0下标开始,也就可以像以上一样了。(有点类似数学归纳法,前面的已否定,从当前开始就行了)
那么代码如下:
int KMP( char * p, char * s ){
int lenP = strlen(p);
int lenS = strlen(s);
if ( lenP == 0 || lenS == 0 ){
return -1;
}
int * next = new int[lenS];
initNext( next, s, lenS );
int i = 0, j = 0;
while ( i < lenP ){
if ( p[i] == s[j] ){
i ++;
j ++;
}else{
j = next[j];
if ( j == -1 ){
j = 0;
i ++;
}
}
if ( j == lenS ){
delete next;
next = NULL;
return i-lenS;
}
}
delete next;
next = NULL;
return -1;
}
next[0]为-1,就意味一个没匹配上,直接下一步了。
接下来就是重点,求next数组了
我们可以递归来求,next[0]=-1,next[1]=0,这是一定的(next[0]=-1自己定义也行)。定义两个下标i、j,i代表前面已匹配的长度,j代表当前next数组的下标,自然是从2增到子串b的长度减1。如果是next[2],自然是考虑b[0]与b[1]。接下来考虑next[3]时,如果前面考虑next[2]时b[0]与b[1]已匹配,此时只需要考虑b[1]与b[2],也就是说如果一直匹配下去的话,每次在前一个基础上加一就行了。代码:
if ( s[i] == s[j-1] ){
i ++;
next[j] = i;
j ++;
}else{
当然,也有不匹配的时候。
最蠢的方法无疑是从头搜一遍,k值从j-1递减1去试出最大的。这里有更好的方法。
不匹配时,i直接取next[i],以下是说明:
a[i]与a[j-1]不匹配,设m=next[i](m<i),那么a[0]到a[m-1]与a[i-m]到a[i-1]相同,i已经到这里说明之前a[0]到a[i-1]与a[j-1-i]到a[j-2]相同,即有a[i-m]到a[i-1]与a[j-m-1]到a[j-2]相同(因为m<i,也有j-m-1>j-1-i),那么也就是说a[0]到a[m-1]与a[j-m-1]到a[j-2]相同,所以如果a[j-1]与a[m]相同,就存在k=m+1符合next定义。那么问题是为什么不先考虑更大的k的存在,那么再次使用反证法。
先列出已知(上面有(1)、(2)、(3)推出(4)):
(1)m=next[i](m<i)
(2)a[0]到a[m-1]与a[i-m]到a[i-1]相同
(3)a[0]到a[i-1]与a[j-1-i]到a[j-2]相同
(4)a[0]到a[m-1]与a[j-m-1]到a[j-2]相同
假设存在有k=m+2(大于m+1),即a[0]到a[m+1]与a[j-m-2]到a[j-1],即有a[0]到a[m]与a[j-m-2]到a[j-2],且因j-m-2=j-1-i+i-(m+1),而i-(m+1)>=0,即有j-m-2>=j-1-i,符合条件(3),所以a[0]到a[m]与a[i-1-m]到a[i-1]相同,这里有k=m+1,但是next[i]=m,矛盾,所以假设不成立,所以不存在k=m+2。同样也可以换成k=m+1+n(n>0)来证明,说明k不可能大于m+1。
所以,证明完毕。
代码:
void initNext( int * next, char * s, int lenS ){
next[0] = -1;
next[1] = 0;
int i = 0;
int j = 2;
while ( j < lenS ){
//printf("i:%d s[i]:%c\n", i, s[i]);
if ( s[i] == s[j-1] ){
i ++;
next[j] = i;
j ++;
}else{
i = next[i];
if ( i == -1 ){
i = 0;
next[j] = 0;
j ++;
}
}
}
}
以上证明都是个人见解,有误请指出。
上面的证明都掺杂很多举例,所以不算是很正规、严密,不过稍稍改一下,把常量改成字母,基本就行了,这里只是说明原理,所以感觉太抽象反而不利于理解。自然,还有些细节地方,如next=-1等等,自己去想应该很简单了,所以就这样了,有时间就补点图吧。