数据结构与算法---字符串(中)
KMP模式匹配算法
我们的前辈D.E.Knuth、J.H.Morris和V.R.Pratt发表了一个模式匹配算法,可以大大避免重复遍历的情况,我们把它称为克努特--莫里斯----普拉特算法,简称KMP算法。我们不直接看算法,我们先通过实例来研究它的原理,懂了原理,算法学起来就easy了。
假设主串S=”abcdefgab“,子串T=”abcdex“。如果是按朴素的模式匹配算法,需要经过以下步骤:
我们通过①可以知道:1.子串T的首位字符a与其它字符互不相等.2.子串T的前5位字符,与主串前五位字符相等.由已知条件,我们可以知道子串T的首字符a一定与主串2,3,4,5位置的字符不相等。因此,②③④⑤判断是可以省略掉的。这样,是不是效率提高了很多。(注意,字串首字符a与子串其中字符不相等,是KMP的前提。)有朋友会问:”如果子串首字符a有子串中其他字符有相同,那么还可以这么高效么?”我们一起来检验一下:
S=“abcabcabc”,T=“abcabcbx”。分析一下处理经过:
因为子串首字符a与子串第二个字符b、第三个字符c不相等。又因为字符b、c与主串的第二位,第三位字符相等。那么可以省略掉②③步,又因为子串第一个字符a、第二个字符b,分别与子串第五个字符a、第六个字符b相等,又因为字符a、字符b分别与主串第五位、第六位字符相等,所以④⑤可以省略。这样开来,即使子串出现了与首字符相等的字符,也是可以省略掉一部分不必要的判断步骤的。对比这两个例子,我们发现,①中我的i值也就是主串的下标为6,②③④⑤分别为2,3,4,5。通过观察,我们②③④⑤是可以省略的,也就是说i值不需要回溯(朴素的模式匹配算法,是通过i判断字符是否相等,i值回溯)。KMP的原理就是控制i 的值,使其不能变小,从而减除了很多不必要的重复判断。既然i值不回溯了,那么就让j回溯。通过观察,我们发现j值得变化与主串没有关系。我们屡屡提到子串的首字符与自身后面的字符比较,我们发现,当首字符与自身后面的字符不同时,j值的变化就会不同。j值的变化取决字串T的结构中是否有重复的问题。例如上面T=“abcdex”,当中没有重复的,所以j值就由6变为1。T=“abcabx”,由于前缀ab与后缀ab是相等的,所以j由6变为了3。因此,我们得出结论:”j值得多少,取决于当前字符之前的串的前后缀的相似度”。我们把T串 各个位置j值得变化,定义为一个数组next。那么next的长度就是T串的长度。我们可以得出下面的函数定义:
next数组推导:
T=”abcdex”
当j=1时,next[1]=0;
当j=2时,T串2位置之前只有a,属于其他情况。next[2]=1;
当j=3时,T串3位置之前有a、b,不相等,属于其他情况。next[3]=1;
当j=4时,T串的4位置之前有a、b、c,不相等,属于其他情况。next[4]=1;
当j=5时,T串的5位置之前有a、b、c、d,不相等,属于其他情况。next[5]=1;
当j=6时,T串的6位置之前有a、b、c、d、e,不相等,属于其他情况。next[6]=1;
所以子串T各个位置j值的变化数组为next[6]=011111
T=”abcabx”
当j=1时,T串的一位置之前没有字符,所以next[1]=0;
当j=2时,T串2位置之前只有一个字符a,属于其他情况.next[2]=1;
当j=3时,T串3位置之前有字符a、b,a b不相等,所以属于其他情况.next[3]=1;
当j=4时,T串4位置之前有abc,互不相等,所以属于其他情况next[4]=1;
当j=5时,T串5位置之前有abca,其中位置1与位置4相等。根据’p1…pk-1’=pj-k+1…..pj-1,p1=p4,得K=2,next[5]=2;
当j=6时, T串6位置之前有abcab,其中位置1、2与位置4、5相等。根据’p1….pk-1=pj-k+1….pj-1,得k=3,next[6]=3;
通过next[]数组推导,我们可以发现,当前位置字符之前的串的,如果前后缀相等字符的个数为n,那么当前位置的j值为n+1.
KMP模式匹配算法实现
/* 功能:获取子串各个位置的j值变化数组 */ void get_next(string t, int *next) { int i=1, j=0; next[1] = 0; while (i<t[0]) //t[0]存放的是串t的长度 { if (j == 0 || t[i] == t[j]) { ++i; ++j; next[i] = j; } else { j = next[j]; //若字符不符,j值回溯。 } } }
这段代码是为了,获得子串各个位置j值的变化。
/* 功能:返回子串T在主串S中pos后,出现的位置。如果没有,返回0 */ int index_kmp(string s, string t, int pos) { int i = pos; int j = 0; int next[255]; //用来获取子串next数组 get_next(t, next); while (i <= s[0] && j <= t[0]) { if (j == 0 || s[i] == s[j]) { ++i; ++j; } else { j = next[j]; //获取j位置j的变化 (朴素算法,是将i回溯,j回溯到子串的第一位。kmp算法的不同之处,就是i保持不变,而将j回溯到合适的位置) } } if (j > t[0]) return i - t[0]; else return 0; }
这段代码是kmp模式匹配的完整算法。
KMP模式匹配算法改进
我们发现这个KMP模式匹配算法还是有缺陷的。例如:主串S=”aaaabcde”,子串=”aaaaax”.next数组值分别为012345。看图:
当i=5,j=5,主串b与子串的a不相等。j回溯到4位置。此时同样是字符a,还是与主串的5位置b不等。j回溯到3位置,此时还是字符a,与主串的5位置b不等。依次类推,直到i改变位置。那么②③④⑤步骤,都是可以省略的。我们可以使用next[1]来代替相等字符的next[j]。例如,在①中,此时i=5,j=5,字符不相等,j应该回溯到4位置。此时我们判断4位置的字符是否与4位置相等,直接回溯到4位置对应的next数组的值3.我们看一下改进的算法代码:
void get_nextval(string t, int *nextval) { nextval[1] = 0; int i = 1; int j = 0; while (i < t[0]) //i应该小于子串的长度 { if (j == 0 || t[i] == t[j]) { ++i; ++j; if (t[i] != t[j]) //当前字符与前缀字符不同 nextval[i] = j; //则将j给当前j为nextval数组的i位置的值 else nextval[i] = nextval[j]; //当前字符与前缀字符相同,则将前缀字符的nextval值给nextval在i位置的值 } else { j = nextval[j]; } } }