众所周知, KMP是个神奇的算法,不少人都会使用,但是大部分人只是只知其然不知其所以然(像今天之前的我,模板记得溜溜的,但是却不知道是怎样一步一步推导而来的),下面我就来详细探讨一下;
首先学习一个算法,要首先知道该算的是什么,是用来做什么的;
KMP算法,就是一个用来匹配字符串的优秀算法,由D.E.Knuth、J.H.Morris和V.R.Pratt三位大牛同时发现的,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。其时间复杂度为O(n+m),(n为主串的长度,m为模式串的长度);
解释一下主串与模式串:
现有字符串S1, S2, 问S2是否包含于S1中,则我们把S1视为主串,把S2视为模式串(通俗地讲,长度长的为主串,反之为模式串);
那么KMP算法是如何来匹配字符串的呢?
先把KMP放到一边,我们先来手动匹配一下下面两个串:
S1: a b c a b c a b d e
S2: a b c a b d e
大家想要怎样匹配呢?
首先S1[0]与S2[0] 匹配,匹配成功,然后S1[1]与S2[1]匹配…, 直到S1[5]与S2[5]匹配失败,此时,我们再开始由S1[1]与S2[0]匹配…, 直到S1[3]成功匹配到S2[0],接下来,就把后边全部匹配成功了;
但是大家有没有发现,这样匹配有点麻烦,当最初S1[5]与S2[5]匹配失败时,我们可以直接用S1[5]与S2[2]匹配,为什么呢? 因为S1[3, 4]是和S2[0, 1]相匹配的,直接由S1[5]与S2[2]即可;之前的不需再去匹配,这样效率就提高了;
当红色匹配失败时,我们知道蓝色是必定会匹配成功的
所以直接由绿色部分开始匹配,以提高效率;
那么,我们又是如何知道上边图中的蓝色部分是已经匹配成功了的呢?
我们来观察一下模式串S2:
a b c a b d e;
前五个字符构成的串是不是有一个长度为2的相同前后缀ab呢?
那么,既然之前S1与S2的前五个字符是匹配成功的,那么,S1的前五个字符构成的串,也必定有这样一个相同的前后缀ab,那么,第二个ab不就直接与S2的第一个ab相匹配了吗?
到了这里,大家也许就有点恍然,我们可以由模式串S2中得到一些信息,是的在将S2与S1匹配时不做无用功,提高效率;这样就引出了next数组;
next数组定义如下:
next[0]=-1;
next[i]=j;表示前i个字符构成的串中有一个长度为j的相同前后缀;
下面来推一下next数组的计算过程;
初始为next[0]=-1;
next[1]=0;
假设已知next[j]=k;
有已知:前j个字符构成的串,有一个长度为k的相同前后缀,即S[0 ~ k-1] = S[j-k ~ j-1];
(下图红色串s1);
下面求next[j+1];
(一): 若S[k] == S[j], 则next[j+1]=k+1;
(二):若S[k] != S[j];
先看前半部分,必有绿色串s2位相同前后缀;那么后边的s1串中也必有一个后缀s2;
而s2的长度为next[k],即S[0 ~ next[k] ];
此时将S[j]与S[ nenxt[k] ]匹配,若S[j] == S[ nenxt[k] ],则next[j+1]=next[k]+1;
若S[j] != S[ nenxt[k] ],那么继续将S[j]与S[ next[ next[k] ] ] 匹配,直到next[ next […[k] ] ]=-1,此时不存在相同前后缀,next[j+1]记为0;
可以看出将k重置为next[k], 就是一个递归过程,这样就可以求出next数组了;
next数组计算代码:
int next[maxn];
void cal_next(char *s){
int len=strlen(s);
int j=0, k=-1;
next[0]=-1;//next数组初始值;
while(j<len){
if(k==-1 || s[j]==s[k]) j++, k++, next[j]=k;
else k=next[k];
}
}
好,上面就是next数组的计算,一定记住,next数组是根据模式串S2计算的!
那么下边就可以进行字符串的匹配了,但是在那之前,我想先说一下next数组的应用——求字符串的循环节;
WTF!!!
还有这种操作???
下面是结论:
若串S是由若干个循环节s构成的(s的个数大于1),则对于S串的next数组有: strlen(S) % (strlen(S) - next[strlen(S)]) = 0;
此时最小循环节长度(即s的长度)为:strlen(S) - next[ strlen(S) ]; 注: next[ strlen(S) ] != 0;
接下来就是字符串的匹配了;
还是以之前的字符串匹配为例:
S1: a b c a b c a b d e
S2: a b c a b d e
当匹配到红色区域时,匹配失败;
由next数组可以得知S2中 d 前方的字符构成的串中,有一个长度为2的相同前后缀,也就是说,S1中 c 前方的字符构成的字符串中也有同样的前后缀,此时S1中的后缀和S2中的前缀是匹配的,即ab不需再匹配了,直接cc匹配;
S1的指针不需要移动,只需把S2的指针挪到已经匹配好的串后边开始匹配即可;
bool KMP(char *s1, char *s2){
int len1=strlen(s1), len2=strlen(s2);
int i=0, j=0;
for(int i=0; i<len1; i++){
while(j&&s1[i]!=s2[j]) j=next[j];
if(s1[i]==s2[j]) j++;
if(j==len2){
return true;
}
}
return false;
}