KMP字符串模式匹配算法

        这个算法之前已经看过两遍了,但是发现这东西实在太容易忘记,每次复习的时候总要费好大功夫才能想起来。这一次参考了别人的博客http://www.cnblogs.com/wangguchangqing/archive/2012/09/09/2677701.html。

        首先,须知KMP匹配算法包括两部分,第二部分自然是在主串S中寻找模式串P,而第一部分是模式串P的自匹配。为什么需要第一步呢?因为第二 步在主串S中寻找模式串P需要一个叫做next的辅助数组,这个数组是KMP算法的核心所在,也是联系该算法两部分的中介。而这个数组从哪里来的?当然不是从石头里蹦出来的,它是第一步模式串P的自匹配过程计算出来的。

        next数组的具体含义是什么?这个要从算法本身的匹配过程来说。如果不知道KMP算法,你会怎么寻找字符串?一般都是傻瓜式判断,就如同下方表示的一样,它叫朴素模式匹配算法。

KMP字符串模式匹配算法_第1张图片

        看粗来了吧,这个算法就是将主串S与模式串P从头到尾进行判断,只要模式串P结尾之前发现不匹配,则主串与模式串全部回头重新来过,只不过主串往前走了一步而已。

        KMP算法与朴素算法的区别是:

        第一,主串S从不回头,要么停,要么往前走。

        第二,模式串会回头,但不是回到最开始,而是往前渐进回头,至于回到哪里,那么就由之前所说的next数组决定。怎么说?意思就是当开始匹配后,模式串P发现在索引i处和主串S索引i处不匹配,模式串P就直接回头到索引next[i]处,继续和主串S在索引i处比较。所以,next数组的长度与模式串P的长度一致。

        然后就有人想问了,为什么模式串P匹配失败就可以回到next数组给的索引上呢?难道这个数组能保证它给的新位置next[i]之前的所有字符都和主串S索引i之前的对应字符全部相同吗?没错,还真是这样,它就是这样一个傲娇的数组,不但可以保证这一点,还能保证next[i]是有必要比较的最近索引。所以模式串P回头之后,next[i]之前的字符都不用比较了,因为必定都是相同的,它只要比较当前以及之后的字符就行。那么它是怎么算出来的?最后说!

        现在可以说一下KMP第二部分主串S与模式串P匹配的过程了。首先,主串S与模式串P都从头开始匹配,一旦发现在索引i处不匹配,则主串不动,模式串回到next[i]处,然后S[i]与P[next[i]]进行比较。如果还是不相同,则主串还是不动,模式串回到索引next[next[i]]处,然后S[i]与P[next[next[i]]]进行比较。如此循环往复,直到最后会出现两种情况:

        第一种是主串S与模式串P终于匹配了,此时主串S索引依旧是i,而模式串P经过多次回头假设其索引变成了j。那么,主串S索引i与模式串P索引j都加1 ,进行下一个字符的比较。

        第二种就是模式串P经过多次回头,已经回到了开头,即索引0处,并且发现S[i] != P[0],那么主串S索引加1,而模式串P的索引不变,依旧为0,继续比较。

        那么这个匹配过程啥时候结束呢?很容易理解,两种情况:

        第一种就是模式串P的索引到了结尾并且匹配成功,那就是在主串S中找到匹配结果了,自然可以结束。当然了,如果你想找出主串S中所有和模式串P一样的字符串,你就将主串S的索引加1,模式串P的索引设为0 ,继续往后匹配呗。

        第二种就是主串S的索引已经到了结尾并且匹配失败,那就是说主串S中没有和模式串P一样的字符串。要知道伤心总是难免的,你又何苦一往情深!

        假设模式串P = "WWWWQ",则next数组就是{-1,0,1,2,3},至于原因,最后再说。那么KMP算法的匹配过程就如同下面表示的那样(灰色部分表示不需要判断)。

KMP字符串模式匹配算法_第2张图片

        原理清楚了,程序也就容易理解了。代码贴在下面,每一句基本上也注释了,可以和上面的原理部分对照着看。其中唯一让人有些费解的就是next[0] = -1,它的意思其实就是之前所述的模式串回头的第二种情况(即回到了索引0处).

	//定义主串S与模式串P
	std::string S = "WWWWWWQER", P = "WWWWQ";
	//定义next数组
	int next[] = { -1, 0, 1, 2, 3 };
	//定义主串索引i与模式串索引j
	int i = 0, j = 0;
	//循环比较直到匹配结束,当j达到P末尾时找到匹配项,当i达到末尾时未找到匹配项
	while (i < S.length() && j < P.length()){
		//如果j成为了-1(模式串回头到了开始处)则主串S索引加1,模式串P索引设为0(-1 + 1 = 0),继续匹配
		//如果匹配成功则向后继续匹配
		if (j == -1 || S[i] == P[j]){
			i++;
			j++;
		}
		//不匹配则令模式串P回头
		else{
			j = next[j];
		}
		//如果j已经达到模式串尾部则找到目标,返回主串S中匹配项开始字符的索引
		if (j == P.length()){
			return i - j;
		}
	}
        至此,算法的第二部分也就结束了,下面说算法的第一部分,也就是自匹配过程。其实这一部分在实现上完全套用了第二部分的方法,只是不太好理解罢了。这一部分只做一件事,那就是求取next数组。

        这一部分的实现与第二部分几乎一致,在这儿主串是P串,模式串也是P串,所以叫自匹配。这一部分先看代码:

	//定义模式串P
	std::string P = "WWWWQ";
	//定义next数组
	int* next = (int*)malloc(P.length() * sizeof(int));
	next[0] = -1;
	//定义主串索引i与模式串索引j
	int i = 0, j = -1;
	//循环比较直到匹配结束,当i达到末尾时自匹配结束,而j不可能到达末尾
	while (i < P.length() - 1){
		//如果j成为了-1(模式串回头到了开始处)则主串索引加1,模式串索引设为0(-1 + 1 = 0),继续匹配
		//如果匹配成功则向后继续匹配
		if (j == -1 || P[i] == P[j]){
			i++;
			j++;
			//设置next数组
			next[i] = j;
		}
		//不匹配则令模式串回头
		else{
			j = next[j];
		}
	}
        与第二部分的代码相比,有三点区别:

        首先不用判断索引j是否会达到末尾了,因为模式串和主串一样长,并且一开始是拿模式串的索引0的值与主串在索引1处的值进行比较,而且模式串还可能回头主串却不能回头,这就注定了主串一定会首先到达末尾。

        然后不用在结束时返回匹配项其实索引了,因为这不是寻找字符串的过程,而是求取next数组的过程。

        最后就是程序第16行"next[i] = j"这一句,其实整个代码里计算next数组就是靠这一句的,这一句也是最难理解的。理解了这一句,整个代码和KMP算法第一部分也就全明白了。

        我们不妨分析一下S="WWWWWWQER",P="WWWWQ"这个例子。一开始j=-1,所以设置next[1]=0。这个不用说了,第二个字符不匹配自然比较第一个,朴素就是这么干的。那么一开始next[0]=-1啥意思?前面已经说过了,翻一翻。然后下一次循环发现P[1]==P[0],设置next[2]=1。这前后是什么因果关系?根据朴素算法来,当S[2]!=P[2]时,应该首先比较S[1]与P[0]、S[2]与P[1]才对。但是由于P[0]==P[1],而之前S[1]==P[1]肯定成立(否则就没必要比较S[2]与P[2]了),所以S[1]肯定等于P[0],根本就不需要比较,我们直接比较S[2]与P[1]就行,这就是next[2]=1想表达的意思。接下来一个循环,发现P[2]==P[1],于是设置next[3]=2。这又表达了什么意思?同样,那一边S[1]==P[1],S[2]==P[2],这一边P[0]==P[1],P[1]==P[2],所以S[1]==P[0],S[2]==P[1],所以只需要直接比较S[3]与P[2]就行了。接下来一个循环,发现P[3]==P[2],于是设置next[4]=3,其道理也是一样的。next数组就计算出来了。

        发现了没有?程序中一边在匹配失败时读取next数组的值进行回头,一边设置next数组的值确定最近且正确的回头位置,这样会不会出现还没设置就读取的情况呢?放心吧,不会的,因为一开始就设置i=0,j=-1,而读取next数组只会在回头时进行,回头就意味着j一定比i要小(i不会回头)。所以next数组每一个索引处一定是先写入再读取的。

        上面解释了一箩筐,应该发现了KMP算法与朴素算法的联系。其实,KMP算法就是在朴素算法的基础上省略了一些不必要的判断步骤罢了。具体是哪些不必要的判断步骤呢?一个就是上面说的已经next[j]前面完全相同不需要判断的情形。还有一个自然是反面情形,就是next[j]与j之间已知不相同故不需要判断的情形。这又怎么说呢?在这儿我把P改一下,设为"WWWWQQ",计算next数组发现前面都一样,但在计算next[5]时,发现P[4]!=P[3],所以j=next[3]=2,然后P[4]!=P[2],以此类推,发现j=next[2]=next[1]=next[0]= -1,所以最后得到next[5]=0。这儿表达的就是这个意思,那一边S[4]=P[4],而这边P[4]!=P[3],所以S[4]!=P[3],所以S[5]与P[4]根本就没有比较的必要,前面都不相同,你相同顶个鸟用?S[5]与P[3]、P[2]、P[1]不需要比较也是这个道理。

        感觉说的已经挺详细了,但是由于全是文字描述,所以感觉抽象的可以看看参考博客的图。

        讲道理,KMP这三个人实在是太欠扁了,嫌这个算法不够烧脑,后来又推出来什么威力加强版,日了狗了!

        不过还好,加强部分并不难理解,就是将代码16行"next[i] = j"改成了如下代码:

			//如果新位置字符不相同设置next数组
			if (P[i] != P[j]){
				next[i] = j;
			}
			//否则j就取next[j]避免一次或者多次不必要的判断
			else{
				next[i] = next[j];
			}
        改进之处就在于"next[i] = next[j]"这一句,使用next数组是在字符串匹配时不相同的时候发生的,所以next[j]处的字符和j处的字符相同的话,那么也必然和主串对应的字符不相同,所以还是需要继续调用next[next[j]],这还不如在计算next数组的时候就给考虑进去,到时候保证next[j]处字符与j处字符一定不相同,才有与主串对应字符相同的可能性。

        到这里,KMP算法就说完了,希望下次来看的时候不要又懵逼了。

你可能感兴趣的:(数据结构)