Knuth-Morris-Pratt算法(简称KMP算法)以三个发明者命名,K就是著名的科学家Donald Knuth。这个算法不好理解,我试着参考网上的资料讲清楚。考虑下面这张图片描述的情况。
在字符串BBC ABCDAB ABCDABCDABDE中匹配ABCDABD,当匹配到如图所示的位置时,前六个字符ABCDAB都匹配成功了,空格和D没有匹配上。如果按照朴素的匹配算法,就会将ABCDABD向后移动一位开始新的匹配。其实这个时候我们已经知道了前六个字符匹配成功的事实,但是匹配的朴素算法并没有对此加以利用,所以效率很低。如果我们能提前计算一些信息,就可以多向前移动几位。下图描述了在字符串O中寻找f,当匹配到位置i时两个字符串不相等,向前移动了k位的情况。
观察这幅图片我们发现,在向前移动了k位之后A和B必然要是相等的。令f在位置i之前(不包括位置i)的子串为f1,很显然,A是f1的前缀,B是f1的后缀。所以kmp算法的核心即是计算字符串f每一个位置之前的子串的前缀和后缀的最大公共长度(不包括字符串本身)。我们用next[i]表示前i个字符的最大公共长度并且规定next[1]=next[0]=0。比如说,对于字符串cacca,next[2]=0,next[3]=1(c),next[4]=1(c),next[5]=2(ca)。获得f每一个位置的最大公共长度之后,就可以利用该最大公共长度快速和字符串O比较。当每次比较到两个字符串的字符不同时,我们就可以根据最大公共长度将字符串f向前移动(已匹配长度-最大公共长度)位,接着继续比较下一个位置。事实上,字符串f的前移只是概念上的前移,只要我们在比较的时候从最大公共长度之后比较f和O即可达到字符串f前移的目的。对应于上图所示的例子,我们应该向后移动四位。
现在的问题是如何计算next[i]。如果仔细观察,可以发现构造next[i]的时候,可以利用next[i-1]的结果。假设已求得next[10]=3。
000#xxx000
字符串的前十个字符
000
末尾3个字符
这意味着末尾3个字符和前3个字符是一样的。为求next[11],可以直接比较第4个字符和第11个字符。
000#xxx000#
字符串的前十一个字符
000#
末尾4个字符
如果它们相等,next[11]=next[10]+1=4,这是因为next[10]=3保证了前3个字符和末尾4个字符的前3个字符是一样的。如果它们不相等,我们可以将长度为next[i]的字符串继续分割,获得其最大公共长度next[next[i]],然后再和位置i的字符比较。这是因为长度为next[i]前缀和后缀都可以分割成上部的构造,如果位置next[next[i]]和位置i的字符相同,则next[i+1]就等于next[next[i]]加1。如果不相等,就可以继续分割长度为next[next[i]]的字符串,直到字符串长度为0为止。
以hihoCoder1015的AC代码为例给出KMP算法的示例代码:
#include<cstdio> #include<cstring> #include<iostream> using namespace std; int Next[10010]; char Pattern[10010],Text[1000010]; int m,n; void compute_next(int *Next,char *Pattern) { int k=0; Next[0]=Next[1]=0; //Next[i]表示Pattern[0]到[i-1]的最大公共长度,k是Next[i-1]的值 for(int i=2;i<=m;i++) { for(;k!=0&&Pattern[k]!=Pattern[i-1];k=Next[k]); if(Pattern[k]==Pattern[i-1]) k++; Next[i]=k; } } int kmp_match(char *Text,char *Pattern,int *Next) { int i=0,j=0,ans=0; //i表示上一次迭代匹配了多少个字符 //j表示这次迭代从Text的哪个字符开始比较 //ans表示匹配成功的次数 while(j<n) { for(i=Next[i];i<m&&Pattern[i]==Text[j];i++,j++); //直接从Next[q]的位置开始匹配 if(i==0) j++; //如果一个也没有匹配j向后移动一位 else if(i==m) ans++; //如果匹配成功ans++ } return ans; } int main() { int N; scanf("%d",&N); while(N--) { scanf("%s %s",Pattern,Text); m=strlen(Pattern),n=strlen(Text); compute_next(Next,Pattern); cout<<kmp_match(Text,Pattern,Next)<<endl; } }参考资料:
KMP算法详解
kmp算法的理解与实现
字符串匹配的KMP算法