在字符串系列的算法中,KMP算法属于较难的一个。实际上它的代码并不多,主要一些细节的地方难以理解,再加上书上,网上实现KMP算法的方式各有不同,造成混淆也再所难免,我自己也看了不止一遍,这篇博客我会力求将我所理解的KMP算法讲清楚。不足之处,欢迎指正。
KMP算法是字符串匹配算法中比比较高效的一个,相比于常规的一个一个字符比较的方法,KMP提供了一个失配函数,依赖于模式串和字符串之前比较过的信息,得到下一次比较的位置,使得失配时不再是只移动一位,而是可以跳过多位。
算法思想:
首先,我们需要明确,KMP算法主要移动模式串来尽快找到匹配。
将模式的第j位与主串的第i位比较,j的范围是[0,np)(np模式长),i的范围是[0,ns)(ns是主串的长度),如果匹配,i,j都后移一位;如果不匹配,判断j是否为0(j=0说明第0位就不匹配),这时,模式串右滑一位,与主串的第1位比较;如果以上条件都不满足,说明失配了。失配如何处理呢,借助于失配函数。
失配函数是KMP算法的关键,它是针对模式串的,由失配函数计算得到失配数组,有的失配数组中failure[i]表示模式串中当前第i个字符与前面的第failure[i]个字符相同,且满足pat[0...failure[i]]同pat[(j-failure[i])...j]是到j为止的最长公共前后缀,我觉得这种方法很难理解,参考了网上的一些博客之后,我选了另一种理解方法:失配数组failure[i]表示,模式串pat的子串pat[0...i]的最长公共前后缀的长度。
举个栗子,前后缀的概念看了这个栗子也就懂了。
字符串p:abcabd
前缀:a,ab,abc,abca,abcab
后缀:d,bd,abd,cabd,bcabd
那么,第0个字符串肯定不会有公共前后缀,到第1个字符为止,前面没有和它相同的字符,所以也没有公共前后缀,一直到第3个字符为止,存在p[0]=[3],所以此时它的公共前后缀为a,长度=1;到第4个字符时,存在p[0...1]=p[3...4],所以此时的最长公共前后缀长度是2;到第5个字符时,不存在公共前后缀了。由这个规律怎么得到失配数组的计算方法呢?现在我们先理解前后缀了最长相同前后缀,后面再结合代码来理解失配函数。
现在通过图理解KMP算法是如何工作的:
1.先计算得到失配数组:
2.第一次失配
上图中主串和模式发生了失配,此时主串下标i=7,模式下标j=7。根据上面的失配数组可以得到failure[6]=4,此时模式右滑,置j=4,i不变,重新比较上次失配的地方。
3.第二次失配
此时不必再比较红线框里的,因为根据之前比较的结果,我们可以从失配函数中知道它们一定是匹配的,这次直接比较pat[4]和s[7],又一次失配了,这次根据failure[4]=2,将j置为2.
以上就是KMP算法的运算过程,根据失配数据来滑动模式,匹配主串。
代码
1.失配函数
首先我们来看如何计算失配数组,有以下几个需要注意的地方:
fialure保存到当前位置为止的子串的最长公共前后缀。从模式的起始位置开始,我们首先要确定,第0个字符不存在最长公共前后缀,所以failure[0]=0。
函数中的i有两层含义。首先,i表示pat的下标,从0开始,用来和j比较当前第i位和第j为是否相等;其次i也表示到第j位为止,pat的最长公共前后缀,也就是failure[j]的值。
如果pat[j]=pat[i],说明字符串中多了一位和之前字符匹配的字符,最长相同前后缀应该在上次的基础上+1。
如果pat[j]!=pat[i],这是最难理解的地方。首先,我们要知道这样一个关 系,到第j-1为为止的最长相同前后缀如果为3,说明pat[0...2]这一子串是最长相同前后缀,那么可以得到pat[0..2]=pat[(j-1-2)...(j-1)]是相等的,我们下一次就要比较pat[3]和pat[j],如果这两个位置的字符不相等,那么我们要从上一次的最长相同前后缀得到这一次比较的的值的下标,这个值就是failure[2]。由此可知,比较不相等时,我们就应该找上一次比较的最长相同前后缀的长度,来确定这次要比较的下标。如果这里实在难以理解,建议自己画一下,这样会比较清晰一些。
/*计算失配函数*/
void fail(char *pat){
int n = strlen(pat);
int i, j;
failure[0] = 0;
i = failure[0];/*i初始化为0,表示第0个字符,又表示在模式中,到0的最长公共元素个数为0*/
for (j = 1; j < n; j++){
while ((pat[j] != pat[i]) && i>0){
i = failure[i - 1];
}
if (pat[j] == pat[i])
i++;
else
i = 0;
failure[j] = i;
}
}
2.KMP函数
接下来是KMP函数,这个理解就简单多了。
如果模式和主串匹配失败,需要根据失配值来滑动模式,充分利用了之前比较过的信息。
/*返回的是模式在字符串中匹配时的起始位置*/
int kmp(char *string, char *pat){
int i = 0, j = 0;/*分别表示string,pat的下标*/
int lens = strlen(string);
int lenp = strlen(pat);
while (i < lens && j < lenp){
printf("\ti=%d j=%d\n", i, j);/*可以打印出比较的情况自己看一下*/
if (string[i] == pat[j]){/*相等时比较后一位*/
i++;
j++;
}
else if (j == 0)
i++;/*string中第一位和pat的第一位不等*/
else
j = failure[j - 1];/*第j位失配时,先由失配数组得到在模式中到j为止的最长相同
前后缀的长度k,此时右滑模式,跳过k位。*/
}
return ((j == lenp) ? i - lenp : -1);
}
总结:
写了这么多,其实KMP算法最重要的就是理解两个部分。第一,和常规的模式匹配算法相比,KMP选择移动模式来进行匹配,因为模式要比主串短的多,遍历周期也更短;第二,对失配函数的理解,求模式当前位置的最长公共前后缀,发生失配时,根据失配值来滑动模式。其实最长公共前后缀也就是找模式中的相同子串,只是这个子串是有要求的,该子串必须最长,以保证模式的滑动距离最长,该子串必须是从模式的第一位开始,也就是“前缀”,保证前缀和后缀相同,那么模式和主串失配,我们就可以将模式移动到后缀上次的位置,因为前面我们已经比较过后缀之前了部分是匹配的,且前缀和后缀是相等的。