KMP算法是模式串匹配算法中最为著名的一个,其他的还有BM、Horspool、Sunday等。
这篇文章,对各种算法有比较全面的介绍。但是,其中代码尚存在问题,不能照搬,重在理解各种算法思想。
KMP算法应用最多(至少在ACM竞赛中出现频率很高),因此需要深入理解,熟练掌握。其O(n+m)的时间复杂度,也是非常出色的。
在我看过的几篇材料中,这篇文章,对KMP算法的介绍比较透彻。可惜的是,有些地方略显冗长,细节和例子讲得过细了,结果是无法很快掌握KMP算法的精髓。
掌握KMP算法的精髓,抓住这几点:
1. 理解“移动”和“跳转”的区别
“移动”只不过是人们考虑问题的角度,事实上,编程时是借助“跳转”实现的,即只改变串的索引。这就是理解和编程时的思维差别,不要总在脑子里打转转。
2. KMP与暴力匹配只有一步之遥
KMP的思想非常简单:在暴力匹配失败时,采取不同的“跳转”方式,使得模式串尽可能向后“移动”。暴力匹配中,一旦失败,模式串相对于原串总是一个字符一个字符地向后“移动”,再从模式串的头部开始匹配;而KMP中,模式串可以一次向后“移动”好几步,并且不一定重新从模式串的头部开始匹配,减少了不必要的匹配次数。
3. KMP跳转的依据:next数组
Next数组可以理解为模式串的属性,与原串无关。Next[i]的值,表示模式串中索引为i的位置一旦匹配失败,则模式串的索引立即由i变为next[i],而原串的索引不变,并将原串当前索引的字符与模式串索引为next[i]的字符继续匹配下去。
4. Next数组计算的依据:已匹配子串的对称性
KMP要解决的问题,就是暴力匹配常常“功亏一篑”。暴力匹配往往已经匹配成功很长的一段,却在最后几个字符上出了问题。KMP要利用已经匹配成功的子串,假如该子串有相同的前缀和后缀,那么就将模式串向后“移动”,使得前缀部分完全覆盖原来的后缀部分,模式串接下来的匹配只需要从覆盖部分的下一个字符开始(即前缀的下一个字符开始)。如果模式串的索引是从0开始编号的,那么可以发现next[i]的值恰好是前缀长度。
5. Next数组的实现
理解了上面的部分,可以说KMP的思想已经吃透了。剩下的最大障碍,就是编程实现计算next数组,显然,如果对于每一个i,都列出之前子串的所有前缀、后缀,然后找到最长公共前缀后缀,最后将next[i]的值赋为该长度,这不是一个现实的办法。事实上,我们可以通过0,1,2……i-1中已经计算出的next值,得出next[i]值。这一点,在很多文章或者书籍中已经介绍得很详细了,这里不再赘述。值得注意的是,next[i]和i两个索引指向的模式串字符不应相等,否则即使跳转了,接下来比较的还是两个一模一样的字符,肯定重蹈覆辙。这一点便是优化KMP算法的出发点,同样在很多文章和书籍中有所提及,不再赘述。
下面附上我写的经过测试的KMP代码,供大家参考。
#include <stdio.h>
#include <string.h>
void getNext(char t[], int next[]) //建立next数组
{
int i=0, j=-1, tLen=strlen(t);
next[i]=j; //next[0]=-1
while (i<tLen)
if (j==-1 || t[i]==t[j])
{
i++;j++;
if (t[i]!=t[j]) next[i]=j; else next[i]=next[j]; //KMP算法的优化
}
else j=next[j];
}
int kmpMain(char s[], char t[], int next[], int pos) //从原串s中索引为pos处开始匹配模式串t
{
int i=pos, j=0, sLen=strlen(s), tLen=strlen(t);
while (i<sLen && j<tLen)
if (j==-1 || s[i]==t[j]){i++;j++;}
else j=next[j];
if (j==tLen) return i-j; //返回匹配成功开始的地方
else return -1; //未找到任何模式串
}
int main()
{
char s[1000],t[1000];
int next[1000];
scanf("%s",s);
scanf("%s",t);
getNext(t,next);
printf("%d",kmpMain(s,t,next,0));
return 0;
}