KMP算法

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等等,自己去想应该很简单了,所以就这样了,有时间就补点图吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值