前两天花了一天的时间琢磨KMP算法,刚开始的时候真的是一头雾水,感觉理解算法的思想,却不明白其原理(就是许多博客中提到的next表的实现过程)。最后看了一些讲解的视频,小有所得,在此分享给大家。
KMP算法是由Knuth,Morris,Pratt共同提出的模式匹配算法,其对于任何模式和目标序列,都可以在线性时间内完成匹配查找,而不会发生退化,是一个非常优秀的模式匹配算法。最典型的应用是给出一个目标串(text_str)和模式串(pattern_str),求模式串在目标串出现的位置,暴力求解法是使用两个下标i,j分别指向目标串和模式串,比较text_str[i+j]和pattern_str[j],如果相等,j加1,如果不相等i加1,j赋值为0,时间复杂度为O(m*n),m,n为目标串和模式串的长度。这种方法的不足是存在回溯,即当失配的时候,我们无需移动i,只需要让j往前移即可。那么问题来了,j向前移多少呢。我们从简单的情况开始考虑,我们假设模式串没有相同的字符,现在解决问题的过程变成:i,j指向目标串和模式串的起始位置,如果text_str[i] == pattern_str[j],++i,++j;如果不相等,那么因为前面比较过得字符两两不相等,那么只要把j向前移到起始位置并且把i加1,然后重新比较即可,这样就避免了回溯i的过程,时间复杂度变成了O(m),m为目标串长度。
更进一步,如果模式串有相同的字符呢,很简单,思路可以不变,只是失配的时候,我们可以把j向前移x步,而不是移到起始位置即可。失配时移动多少步只和模式有关和目标串无关。我们引进一个数组,数组的值保存的时模式串对应位置字符的部分匹配值,这个数组的求法如下:
首先要介绍一下字符串的前缀和后缀,"前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。比如字符串“abcd”,它的前缀为“a”,"ab","abc"。它的后缀为“d”,"cd","bcd"。在KMP算法中,模式串下标i对应的部分匹配值就是模式串[0,i]字串中前缀和后缀最长的共有元素的长度,如模式串为”abcdabd“,则对应的部分匹配值为0000120,即
模式串 : a b c d a b d
部分匹配值 : 0 0 0 0 1 2 0
解释如下:
"a"的前缀和后缀都为空集,共有元素的长度为0;
"ab"的前缀为[a],后缀为[b],共有元素的长度为0;
"abc"的前缀为[a ab],后缀为[bc, c],共有元素的长度0;
"abcd"的前缀为[a, ab, abc],后缀为[bcd, cd, d],共有元素的长度为0;
"abcda"的前缀为[a, ab, abc, abcd],后缀为[bcda, cda, da, a],共有元素为"a",长度为1;
"abcdab"的前缀为[a, ab, abc, abcd, abcda],后缀为[bcdab, cdab, dab, ab, b],共有元素为"ab",长度为2;
"abcdabd"的前缀为[a, ab, abc, abcd, abcda, abcdab],后缀为[bcdabd, cdabd, dabd, abd, bd, d],共有元素的长度为0。
那么失配时,向前移的部数为:移动位数 = 已匹配的字符数 - 对应的部分匹配值,这样我们就解决了模式有重复字符的情况了。
以上是KMP算法的基本思想,即避免回溯。思想并不难理解,难点是KMP在实现的过程中的技巧。
我们先介绍如果求部分匹配值的数组part_match,我们定义一个变量max_part_match保存最大的部分匹配值,初始值为0,并初始化part_match[0] = 0。如果max_part_match等于0且pattern_str[max_part_match] != pattern_str[i](i为循环变量,从1开始),说明模式串中还没出现重复的字符,此时,part_match[i]=max_part_match。如果max_part_match等于0且pattern_str[max_part_match] == pattern_str[i](i为循环变量,从1开始),说明开始出现前缀和后缀相同的情况,此时++max_part_match,并把part_match[i]=max_part_match。如果max_part_match大于0且pattern_str[max_part_match] == pattern_str[i](i为循环变量,从1开始),说明出现前缀和后缀相同的长度在增长,此时++max_part_match,并把part_match[i]=max_part_match。如果如果max_part_match大于0且pattern_str[max_part_match] != pattern_str[i](i为循环变量,从1开始),
max_part_match = part_match[max_part_match];至于为什么是这样,详细的证明见《算法导论》,我的理解是part_match[max_part_match]的值为刚好为当前模式串前缀和后缀相同的位置的下一个位置,这些把max_part_match赋值成它的值,就行重新开始了一轮比较,也可以从递归的角度理解。
完整代码如下(实现在写博客之前,变量命名不太一致):
void patternPrefixEx(std::string pattern_str,std::vector<int>& prefix_offset)
{
if (pattern_str.size() == 0)
return ;
int pat_len = pattern_str.size();
int max_prefix = 0;//最大部分匹配值,max_part_match
prefix_offset[0] = 0;//部分匹配值表,part_match
for (int pattern_index = 1;pattern_index < pat_len;++pattern_index)
{
while (max_prefix > 0 && pattern_str[max_prefix] !=
pattern_str[pattern_index])
max_prefix = prefix_offset[max_prefix];
if (pattern_str[max_prefix] == pattern_str[pattern_index])
++max_prefix;
prefix_offset[pattern_index] = max_prefix;
}
return ;
}
下面的任务就是实现KMP算法了,先求得部分匹配值数组,定义一个变量max_matched表示已经匹配的字符个数,如果等于模式串的长度就返回,思路求部分匹配值相似,需要注意的是,失配时,max_matched值得改变,安装上面的理论,失配时:移动位数 = 已匹配的字符数 - 对应的部分匹配值,注意这是向前移的位数,max_matched不仅代表已匹配的值还代表模式串的下标,此时上面的公式就编程,max_matched(下标) = max_matched(下标) - 移动位数,展开就是:max_matched(下标) = max_matched(下标) - (max_matched(已匹配的数)- 部分匹配值);即max_matched = 部分匹配值,注意这个部分匹配值得位置是max_matched-1的,即max_matched = prefix_offset[max_matched - 1];
完整代码如下:
int kmp(std::string text_str,std::string pattern_str)
{
if (text_str.size() == 0 || pattern_str.size() == 0)
return -1;
int text_len = text_str.size();
int pattern_len = pattern_str.size();
std::vector<int> prefix_offset(pattern_len,0);
patternPrefixEx(pattern_str,prefix_offset);
int max_matched = 0;
for (int match_index = 0; match_index != text_len;++match_index)
{
while (max_matched > 0 && pattern_str[max_matched] !=
text_str[match_index])
max_matched = prefix_offset[max_matched - 1];
if (pattern_str[max_matched] == text_str[match_index])
++max_matched;
if (max_matched == pattern_len)
return (match_index - pattern_len + 1);
}
return -1;
}
参考文章:
http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html