KMP代码实现
1. 简述
KMP是经典的笔试题目之一。昨天复习了一下,主要有一下几点需要注意。
首先,原理上,利用最长的相等前端距离和后端距离(详见C语言名题精选百则6.4节)。
其次,有两类代码,一类实现fail函数,另一类实现next函数。
· fail数组的含义是,当前元素匹配失败了,那么[开始元素,当前元素]范围内,最长前端的下标。假设最长相等前端后端分别为pat[0]-pat[s],pat[i-s]-pat[i], fail[i]=s。很多情况下,没有最长前端,所以可能有很多-1出现(没有的时候,用-1代替)。在kmp主函数中,如果text[t]!=pat[p],那么p=fail[p-1]+1。
· next数组的含义是,当前元素匹配失败了,那么[开始元素,当前元素的前一元素]的范围内,最长前端的元素后面那个元素的下标。假设最长相等前端后端分别为pat[0]-pat[s],pat[i-1-s]-pat[i-1], next[i]=s+1。对于next只有next[0]=-1其余,最小的也就是0。在kmp主函数中,如果text[t]!=pat[p],那么p=next[p]
· 区别:fail[i]是[0, i]的最长前后端的前端的最后一个元素。 next[i]是[0, i-1]的最长前后端的前端的最后一个元素后面的那个元素,实际上next[i]就是说当前pat[i]匹配失败了,那么下一个可以匹配的pat下标是多少。
next[i]"指的是pat[0]到pat[i-1]的范围内,“最长相等前端后端”的前端的最后一个元素的再往后面的一个元素。假设最长相等前端后端分别为pat[0]-pat[s], pat[i-1-s]-pat[i-1],next[i]=pat[s+1]。
然后,对于next函数,是可以优化的,对于pat[p] == pat[next[p]]的情况,可以想象,如果text[t]!=pat[p],那么下一个要取p=next[p],那么由于前者相等,所以必然还是不等还给往前找next[next[p]],所以可以要求对于next[0]后面的元素,如果pat[p]=pat[next[p]], 令next[p]=next[next[p]]。这一步优化可以在前面求next[p]的过程中,处理出来,也可以在next求出来后,再来一遍进行优化。
KMP可以说的有很多,理解也有很多,就先到这里了。尤其是实现算法的细节也很多,这个比较晕,当前next函数的实现较多,一遍都是将next的,毕竟能够优化嘛,建议找算法的话,找一些权威一点的,维基百科链接的代码,等等。
2. 代码
当然上代码还是上自己写的。不一定对啊,不过通过fail和next都通过一些例子验证了,估计不会错吧。其实要记得话,记next应该就够了,网上fail的实现真的很少,看来我看C名题百则上的代码,走了弯路,不过其中关于最长相等前端后端的说明还是不错的,虽然还能说得更好点吧。。呵呵。实际上,最长相等的前端后端是有一种嵌套的前端后端的含义在,书里面没挑明这一点,所以理解起来不是那么直接。
using namespace std;
// fail 函数:经典KMP
// next 函数:经典KMP另一种定义
// next-val 函数:改进KMP,要保证pat[i] != pat[next[i]],
// 可以首先求得next数组,然后,求next-val,
// 如果pat[i] != pat[next[i]],那么next-val[i]=next[i];
// 反之next-val[i]=next-val[next[i]]; 由于这种相等的
void get_fail(const char pat[], int fail[]) {
int len = strlen(pat);
int i,j;
fail[0] = -1;
for(i=1; i<len; i++) {
for(j=fail[i-1]; j>=0 && pat[j+1]!=pat[i]; j=fail[j])
;
fail[i] = (j<0 && pat[j+1]!=pat[i]) ? -1 : j+1;
}
cout << "fail array: " << endl;
for(int i=0; i<len; i++) {
cout << i << ": " << fail[i] << endl;
}
}
int kmp_fail(const char text[], const char pat[]) {
int t_length = strlen(text);
int p_length = strlen(pat);
int t,p;
int* fail = new int[t_length];
get_fail(pat, fail);
for(t=0,p=0; t<t_length&&p<p_length; ) {
if(text[t] == pat[p]) // 相等都往后走一步
t++,p++;
else
if(p > 0) // 前面还有可能的元素没比较过
p = fail[p-1]+1;
else // 可能的元素都比较过了
t++;
}
delete []fail;
return t<t_length?(t-p_length):-1;
}
void get_next(const char pat[], int next[]) {
int len = strlen(pat);
int i,j; next[0] = -1;
for(i=1; i<len; i++) {
for(j=next[i-1]; j>=0 && pat[i-1]!=pat[j]; j=next[j])
;
if(j<0 || pat[i-1]!=pat[j])
next[i] = 0;
else
next[i] = j+1; // if (pat[i]==pat[next[i]]) next[i]=next[next[i]];
}
cout << "next array: " << endl;
for(int i=0; i<len; i++) {
cout << i << ": " << next[i] << endl;
}
for(int i=0; i<len; i++) {
if(pat[i] == pat[next[i]])
next[i] = next[next[i]];
}
cout << "new next array: " << endl;
for(int i=0; i<len; i++) {
cout << i << ": " << next[i] << endl;
}
}
int kmp_next(const char text[], const char pat[]) {
int t_length = strlen(text);
int p_length = strlen(pat);
int t,p; int* next = new int[p_length];
get_next(pat, next);
for(t=0,p=0; t<t_length,p<p_length; ) {
if(text[t] == pat[p])
t++,p++;
else
if(next[p] == -1) // 说明此时p=0,而且pat[0]都匹配不了
t++;
else
p = next[p];
}
delete []next;
return t<t_length ? (t-p_length):-1;
}
int main() {
const char *text = "aaaddddddddddddddddd";
// const char *pat = "aaaadd";
// const char *pat = "xyxyyxyxyxx";
const char* pat = "abcabcaa";
cout << "index: " << kmp_fail(text, pat) << endl;
cout << "index: " << kmp_next(text, pat) << endl;
system("PAUSE");
return 0;
}
主要说一下测试样例:对于pat="abcabcaa",答案来自网络,只有next和next-val的正确答案:-1,0,0,0,1,2,3,4和-1,0,0,-1,0,0,-1,4。对于pat="xyxyyxyxyxx" 答案来自书籍,只有fail的正确答案:-1,-1,0,1,-1,0,1,2,3,2,0。
3. 参考
C语言名题精选百则,6.4节。 其中讲了fail函数的实现。其中有fail的测试数据。
数据结构习题与解析(第三版,A级,Page114)。
手算KMP匹配的Next值和Nextval值。 http://www.slyar.com/blog/kmp-next-nextval.html 其中有next和next-val的测试数据。
Knuth–Morris–Pratt algorithm 维基百科 http://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm 没仔细看,不过链接了很多代码实现和讲解的网页,由于代码实现细节被一些中文书先入为主了,这些代码有些看不进去。
4. 备注
大致参考就这些了,国内的一些博客上的讲解我就不连接了,不能保证其肯定是对的,反正很多我在不会kmp的时候,是看不太懂,好像有一套理解方式是从自动机那边走,算法导论好像就是先讲了自动机,就接着讲了字符串匹配的kmp,我现在的理解方式就是从最长前端最长后端理解。