这个算法之前已经看过两遍了,但是发现这东西实在太容易忘记,每次复习的时候总要费好大功夫才能想起来。这一次参考了别人的博客http://www.cnblogs.com/wangguchangqing/archive/2012/09/09/2677701.html。
首先,须知KMP匹配算法包括两部分,第二部分自然是在主串S中寻找模式串P,而第一部分是模式串P的自匹配。为什么需要第一步呢?因为第二 步在主串S中寻找模式串P需要一个叫做next的辅助数组,这个数组是KMP算法的核心所在,也是联系该算法两部分的中介。而这个数组从哪里来的?当然不是从石头里蹦出来的,它是第一步模式串P的自匹配过程计算出来的。
next数组的具体含义是什么?这个要从算法本身的匹配过程来说。如果不知道KMP算法,你会怎么寻找字符串?一般都是傻瓜式判断,就如同下方表示的一样,它叫朴素模式匹配算法。
看粗来了吧,这个算法就是将主串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算法的匹配过程就如同下面表示的那样(灰色部分表示不需要判断)。
原理清楚了,程序也就容易理解了。代码贴在下面,每一句基本上也注释了,可以和上面的原理部分对照着看。其中唯一让人有些费解的就是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算法就说完了,希望下次来看的时候不要又懵逼了。