本文将以特殊的方式来让人们更好地理解kmp算法,不包括kmp算法的推导,接下来,我们将从朴素算法出发。
在这之前,我们先设主串为S,模式串为T,我们要解决的询问是主串中是否包含模式串(即T是否为S的子串)。
版权声明:本文为原创文章,转载请标明出处。
朴素算法
朴素算法说白了就是暴力,简单地讲就是先从主串的第一个位置开始逐个对模式串进行匹配,若匹配失败,则从主串的第二个位置继续进行匹配,以此类推,直到匹配成功或主串的结尾。
举个例子1
主串S:aabaaced
模式串T:aac
首先我们会进行这样的匹配
aabaaced
aac
发现T[0]和S[0]匹配,T[1]和S[1]匹配,而T[2]==c和S[2]==b匹配失败,接着我们会这样
aabaaced
aac
发现T[1]和S[1]匹配,而T[2]==c和S[3]==b匹配失败,接着
aabaaced
aac
发现T[2]和S[2]不匹配,继续
aabaaced
aac
这次终于成功匹配。
以上所述就是朴素算法,然而我们再来看一个例子
举个例子2
主串S:aaaaaaaaaaaaaaaaaaaaab
模式串T:aaaaab
如果这个例子我们还用朴素算法去匹配,很显而易见,每次我们都要从头开始匹配,做法如下
aaaaaaaaaaaaaaaaaaaaab
aaaaab
从T[0]到T[5],对S[0]和S[5]依次进行匹配,发现末尾(T[5]和S[5])没有匹配,继续
aaaaaaaaaaaaaaaaaaaaab
aaaaab
从T[0]到T[5],对S[1]和S[6]依次进行匹配,发现末尾(T[5]和S[6])没有匹配,继续
……(此处省略大量的中间过程)
aaaaaaaaaaaaaaaaaaaaab
aaaaab
终于匹配成功。
如果用kmp算法,则过程如下:
aaaaaaaaaaaaaaaaaaaaab
aaaaab
从T[0]到T[5],对S[0]和S[5]依次进行匹配,发现末尾(T[5]和S[5])没有匹配,继续
aaaaaaaaaaaaaaaaaaaaab
aaaaab
直接匹配T[5]和S[6]发现匹配失败,继续
……(此处省略大量的中间过程)
aaaaaaaaaaaaaaaaaaaaab
aaaaab
我们发现kmp算法从第二次匹配开始省略了T[0]到T[4]对S的匹配,因为由kmp算法我们知道T[0]到T[4]一定已经匹配了,不需要再判断,那么kmp算法是怎么知道并利用这些信息的呢,
接下来我们进入正题。
kmp算法的理解
首先我们从朴素算法出发,一步一步去引出kmp算法
主串S:S[1]S[2]S[3]S[4]S[5]S[6]S[7]S[8]S[9]
模式串T:T[1]T[2]T[3]T[4]T[5]T[6]
一开始,我们先用朴素算法进行匹配,得到
S[1]S[2]S[3]S[4]S[5]S[6]S[7]S[8]S[9]
T[1]T[2]T[3]T[4]T[5]T[6]
这时候,我们假设前四个匹配成功了,然而S[5]与T[5]匹配失败,即有
T[1]==S[1]
T[2]==S[2]
T[3]==S[3]
T[4]==S[4]
T[5]!=S[5]
按照朴素算法的做法,我们应该把T串往右移,得到这样的式子进行匹配
S[1]S[2]S[3]S[4]S[5]S[6]S[7]S[8]S[9]
T[1]T[2]T[3]T[4]T[5]T[6]
但是这时候我们思考这样一个问题,将模式串右移一位是否有可能成功匹配??
显而易见,这样匹配成功的充要条件是:
T[1]==S[2]
T[2]==S[3]
T[3]==S[4]
T[4]==S[5]
T[5]==S[6]
T[6]==S[7]
结合上次匹配的结果,我们可以把这次匹配成功的充要条件进行变化:
T[1]==S[2]==T[2]
T[2]==S[3]==T[3]
T[3]==S[4]==T[4]
T[4]==S[5]
T[5]==S[6]
T[6]==S[7]
由此我们可以得出一个上次匹配失败后将模式串T右移一位能够匹配成功的充要条件:
T[1]==T[2]
T[2]==T[3]
T[3]==T[4]
T[4]==S[5]
T[5]==S[6]
T[6]==S[7]
进而得到上次匹配失败后将模式串T右移一位能够过匹配成功的必要条件:
T[1]==T[2]
T[2]==T[3]
T[3]==T[4]
注意,这个必要条件只和模式串T有关!
接着我们讨论将模式串右移两位是否能匹配成功:
S[1]S[2]S[3]S[4]S[5]S[6]S[7]S[8]S[9]
T[1]T[2]T[3]T[4]T[5]T[6]
显而易见,这样匹配成功的充要条件是:
T[1]==S[3]
T[2]==S[4]
T[3]==S[5]
T[4]==S[6]
T[5]==S[7]
T[6]==S[8]
结合上次匹配的结果,我们可以把这次匹配成功的充要条件进行变化:
T[1]==S[3]==T[3]
T[2]==S[4]==T[4]
T[3]==S[5]
T[4]==S[6]
T[5]==S[7]
T[6]==S[8]
进而得到上次匹配失败后将模式串T右移两位能够过匹配成功的必要条件:
T[1]==T[3]
T[2]==T[4]
注意,这个必要条件只和模式串T有关!
最后我们讨论将模式串右移三位是否能匹配成功:
S[1]S[2]S[3]S[4]S[5]S[6]S[7]S[8]S[9]
T[1]T[2]T[3]T[4]T[5]T[6]
显而易见,这样匹配成功的充要条件是:
T[1]==S[4]
T[2]==S[5]
T[3]==S[6]
T[4]==S[7]
T[5]==S[8]
T[6]==S[9]
结合上次匹配的结果,我们可以把这次匹配成功的充要条件进行变化:
T[1]==S[4]==T[4]
T[2]==S[5]
T[3]==S[6]
T[4]==S[7]
T[5]==S[8]
T[6]==S[9]
进而得到上次匹配失败后将模式串T右移三位能够过匹配成功的必要条件:
T[1]==T[4]
上面讨论了三种情况,在第一次匹配到T[5]的时候匹配失败了,将模式串分别右移动一位,右移动两位,右移动三位
是否有可能成功
我们这里设Q为T[1]T[2]T[3]T[4]
可以发现:
右移动一位成功的必要条件是T[1]==T[2],T[2]==T[3],T[3]==T[4],即Q的三个前缀等于三个后缀(T[1]T[2]T[3]==T[2]T[3]T[4])
右移动两位成功的必要条件是T[1]==T[3],T[2]==T[4],即Q的两个前缀等于两个后缀!(T[1]T[2]==T[3]T[4])
右移动三位成功的必要条件是T[1]==T[4],即Q的一个前缀等于一个后缀!
注意,这些移动都只和模式串有关!
这时候,我们可以得出一个结论:
上面这个例子,T[5]是匹配失败的位置,我们把匹配失败的位置的前面的所有字符看作一个新的串Q,想要知道右移几位有可能匹配成功,我们需要讨论T[5]前面的字符组成的串Q,如果不满足Q的三个前缀等于三个后缀,我们可以直接跳过右移一位的情况,如果不满足Q的两个前缀等于两个后缀,我们可以直接跳过右移两位的情况,等等,而且,如果一旦满足,我们在右移后,不需要从模式串的头部开始匹配,因为如果满足,前面几个就已经匹配好了。就比如上面这个例子,若满足:
T[1]==T[2]
T[2]==T[3]
T[3]==T[4]
我们可以得到右移一位有可能匹配成功,而且因为有上次匹配失败后留下的信息
T[2]==S[2]
T[3]==S[3]
T[4]==S[4]
我们可以直接得到
T[1]==T[2]==S[2]
T[2]==T[3]==S[3]
T[3]==T[4]==S[4]
所以直接匹配T[4]和S[5]即可,这么一来,就是固定主串不动,从匹配失败的位置开始,判断模式串需要右移几位,然后从匹配失败的位置开始匹配即可,上面那个例子就是T[5]与S[5]匹配失败,由T[1]T[2]T[3]==T[2]T[3]T[4]可知接下来需要模式串右移一位并匹配T[4]和S[5]。
kmp算法的使用
在实际使用中,我们不可能匹配失败一次就去判断失败字符前面所有字符组成的串的最长相等的前缀和后缀,这样时间复杂度会很高,所以我们需要在匹配之前对模式串进行预处理,对每个字符如果匹配失败,要右移几位进行保存,在匹配中一旦失败,直接跳到那个位置就可以了,我们用next数组进行保存,比如上面的那个例子,T[5]匹配失败了,这时候就要让模式串的指针指向next[5],next[5]是我们在匹配之前就已经预处理过的。
至于如何处理,本文不给予证明,靠下面的几串代码可以实现,读者自行思考或阅读书籍或其它文章即可。
获得next数组的代码如下,T为模式串:
void get_next() {
next[0] = -1;
int i = 0, j = -1;
int len = strlen(T);
while(i < len) {
if(j == -1 || T[i] == T[j])
next[++i] = ++j;
else
j = next[j];
}
}
代码很短,其中next[i]代表的是如果在i位置匹配失败,应该从哪个位置继续匹配,跟i前面所有字符组成的串Q的前缀与后缀有关。注意,这个next数组是kmp算法的核心。
接下来给出匹配的过程代码:
bool KMP() {
get_next();
int len1 = strlen(T);
int len2 = strlen(S);
int i = 0, j = 0; //i指向模式串T,j指向主串S
while(j < len2) {
if(T[i] == S[j]) {
i++;
j++;
if(i == len1) {
return true;
}
} else {
i = next[i];
if(i == -1) {
j++;i++;
}
}
}
return false;
}
kmp算法的练习建议
理解kmp算法:poj2752 poj2406 poj1961
常规kmp算法练习:poj3461 poj2185
如有错误或不妥之处,欢迎指正~