今天学习了一下KMP算法,整理一下思路。各位路人如果发现此文有错误之处,望千万留言指正,不胜感激。
先引用前辈的一篇博文,给您做个简单的介绍。
看来严蔚敏和吴伟民的《(C语言版)数据结构》,其中讲到了KMP算法。初看一塌糊涂,根本不知道怎么回事。后在网上查阅相关资料后,明白了KMP算法的本质。记录如下:
主串A与匹配串B,在A中查找与B匹配的子串,在进行比较后,设A中的第i个元素与B中的第j个元素不匹配,在i不回溯的情况下,应该将j回溯到多少。这其实是一个B串自匹配的问题,回溯的多少与A并无关系,当然B串的自身匹配必须是从头开始的。j回溯的值实际上就是B与A匹配的串(即B的子串)自身最大匹配数加1。
例如下:
A串:ababcdefgababa
B串:ababa
在第一次匹配计算后,A中的第5位置与B中的第5位置并不相同,即i=5,j=5不匹配,在i不变的情况下,j所回溯的位置应该是3。因为B与A匹配的子串为abab,设为C串,C串中自匹配的值长为2(即ab是匹配的),所以j所回溯的位置为2+1=3即从第二个a开始,接着与A串的的第5个位置进行匹配计算。如下:
ababcdefgababa
ababa
计算结果当然不匹配,此时C串为a不存在自匹配的情况,按定义则j需要回溯到1的位置重新开始,自然也是不匹配,i++,j=1继续,以此类推,直到A串的最后,或找到匹配,或找不到。
如果您需要一个动态演示:http://ds.fzu.edu.cn/fine/resources/FlashContent.asp?id=40
如果您还是不清楚:(我再试试给您另外一种思维方式的解释)
A串:ababcdefgababa
B串:ababa
(同上)
普通方法,我们拿B串的第一个字符a去和A串的第一个字符a匹配,能匹配再拿B串的第二个字符b去和A串的第二个字符去匹配。。。如果B串的第N个字符和A串的第N个字符没有匹配成功,则从头开始,拿B串的第一个字符a去和A串的第N+1个字符匹配。(就是说某一次匹配失败后,B串都要从头开始匹配,即回溯到第一个字符的位置)
kmp算法不是每次匹配失败都把B串回溯到第一个字符的位置,而是根据自身的特点和当前的情况决定回溯到那个位置。比如
当匹配到A串的第5个字符c 时匹配失败。此时不需要急于把B回溯到第一个字符的位置,因为B串中有abab的情况,即便是最后一个字符(即第三个a)匹配不上,起码在A串中能和B串的第二组ab(**ab*)匹配的内容,能和B串的第一个ab(ab***)匹配。所以,可以假设B串的第一组ab已经在A串中成功匹配了,直接从B串的第三个字符开始即可。(这段只是说明一个概念 ,不明白也不必深究,我们继续。)
上段中我们提到当匹配失败时需要回溯,那具体需要回溯多少个字符的距离呢?这个我们需要对B串进行一个计算,计算一下B串自身的特点,主要是重复的特点。因为我们需要根据它重复的特点决定回溯的距离。
我们说对B串进行计算:先来看B串的内容
B:ababa
很显然B串的第三个字符和第一个字符是重复的,第四个字符和第二个字符是重复的。第五个字符和第三个字符是重复的。
我们用几个整形数组来表示这个这个重复的状态。
int a[5] = {0, 0, 1, 2, 3}
(这里提到的重复是指从第二个字符开始的每一个字符,与第一个字符开始的串的重复情况)
例:
abcdef-abc-ef-abef
这个串中,第一个 - 后边a与第一个字符重复,b与第一个字符后边的第二个字符重复,c与第一个字符后边的第二个字符后边的第三个字符重复。至此重复情况停止。然后,- 、e、f 都不能与第一个字符开始匹配。知道后边的ab又开始重复。
到现在为止,应该知道怎么计算B串的重复情况了吧。下面我们开始学习查找过程
从A串、B串的第一个字符开始匹配,如果当前字符能正确匹配,AB串都向后移动一个字符(下标移动,不是两个串的内容变动)。
如果当前字符不能正确匹配,看B串的位置
如果B串的位置是开头第一个字符,只移动A串的位置
如果B串的位置不是第一个字符,B串回溯,回溯多少呢?看当前字符的重复情况,我们之前提到的那个整形数组,该字符的位置对应在数组中应该有一个值,这个值代表B串中当前字符和前面一个字符是重复的,前面那个字符的位置就是这个值。如果这个值是0,表示没有重复的字符,直接回到开头的位置。
现在明白了吗?下面来看看实现吧。如果还不明白,就留言吧!
int my_kmp(const char *target, const char *pattern){
int tar_len = strlen(target);
int par_len = strlen(pattern);
int *pattern_value = new int[par_len];
memset((void*)pattern_value, 0, par_len*sizeof(int));
//计算B串自身的重复情况
for(int i = 1; i < par_len; ++i){
int value = 0;
int index = 0;
int j = i;
while(pattern[j] == pattern[index++]){
pattern_value[j++] = ++value;
i = j;//while循环中已经遍历过的字符不要在外层再遍历
}
}
//查看模式串(子串)与模式数组(回溯距离记录的数组)的对应关系
for(int i = 0; i < par_len; ++i){
printf("%c ", pattern[i]);
}
printf("/n");
for(int i = 0; i < par_len; ++i){
printf("%d ", pattern_value[i]);
}
printf("/n");
//查找
int tar_index = 0;
int par_index = 0;
while(tar_index < tar_len && par_index < par_len){
if(target[tar_index] == pattern[par_index]){//如果当前字符能匹配-两个串都向后移动
++tar_index;
++par_index;
}
else if(par_index == 0){//如果当前字符不能匹配,且模式串在第一个字符的位置-只移动目标串的位置
++tar_index;
}
else{//如果当前字符不匹配,且模式串不在第一个字符的位置-拿当前字符对应的下标回溯到与它重复的字符的位置
par_index = pattern_value[par_index]-1;//当前字符对应的数组里的数字是需要回溯到得字符的位置,-1表示那个字符在串中的下标
}
}
//释放内存
if(pattern_value){
delete [] pattern_value;
pattern_value = NULL;
}
//如果匹配成功,返回第一次匹配到模式串的位置
if(par_index == par_len){
return tar_index - par_len+1;
}
return -1;
}