举例:在ABCDABABCDABD中匹配模式ABCDABD
最简单的解决方式是暴力匹配(红色加粗表示匹配到):
第一步:
ABCDABABCDABD
ABCDABD
第二步:
ABCDABABCDABD
ABCDABD
第三步:
ABCDABABCDABD
ABCDABD
第四步:
……
第七步:
ABCDABABCDABD
AAAAAAABCDABD
终于匹配上了,但是如果采用KMP算法的话,在第一步匹配失败之后,可以在第二步直接跳到:
KMP第二步:
ABCDABABCDABD
AAAAABCDABD
KMP第三步:
ABCDABABCDABD
AAAAAAABCDABD
只需要三步。为何这么神奇呢?其实是因为KMP算法采取了“最大程度利用已经匹配过的片段”的思想。在上面这个例子中,因为模式ABCDABD本身隐含了两个AB的重复结构,所以如果像第一步那样,在前面匹配了AB的话,就可以直接跳到下一个匹配AB的地方。如果是像ABCDEF这样的模式,因为其自身不含任何重复结构,所以下次匹配直接跳过之前匹配的一大片区域,因为我们知道前面那些不可能再被匹配上,这样说有点抽象,还是举个例子:
第一步:
ABCDEFABCDEFG
ABCDEFG
第二步:
ABCDEFABCDEFG
AAAAAAABCDEFG
两步就匹配上了,如果用暴力方法需要七步。
那么,跳转多长的距离是由什么决定的呢?KMP算法告诉我们,是由匹配模式本身的结构决定的。
设想一个匹配模式长度为n,我们且用p[0]p[1]p[2]p[3]......p[n-1]来表示它。然后用i和j分别表示我们指在待匹配目标和匹配模式上的下表(从0算起)。
如果假设在某一步里,模式和待匹配目标匹配到了下标m处(j=m,m<n-1),如下:
第N步:
@%#^*&!p[0]p[1]p[2]p[3]...p[m-1]p[m]#$%&^*......
aaaaaaaa!p[0]p[1]p[2]p[3]...p[m-1]p[m]p[m+1]......p[n-1]
此时我们考虑如果片段p[0:k]与p[m-k:m]相同,那么我们可以保持i不变,而令j=k,这样做相当于变成下面这种情形:
第N+1步:
@%#^*&!p[0]p[1]p[2]p[3]...p[m-k]p[m-k+1]p[m-k+2]......p[m-1]p[m]#$%&^*......
aaaaaaaaaaaaaaaaaaaaaaap[0] p[1] p[2] ......p[k-1] p[k]...p[k+1]...p[n-1]
之所以能这么做,是因为在第N步中t[i-k:i]与p[j-k:j]相同,而j=m时,又有p[m-k:m]与p[0:k]相同,所以t[i-k:i]与p[0:k]相同,所以可以令j=k来跳转
其实这种现象本质上是因为已匹配片段(长度为m+1而不是n的那段)的长为k+1的前缀和长为k+1的后缀相同。如果有多个k就是说有多个相同的前缀后缀对,那么KMP算法取最大的那个,这也是显然的,取最大的k可以得到最大的跳转长度。
于是当已匹配片段为m+1或者说最大匹配下标j为m时,下一次匹配的j会变为k,那么当j分别为0123...时呢?也都可以计算出来,于是就得到一个jump数组来记录上一次匹配到哪里时,下一次需要跳转到哪里。代码如下:
int* get_jump(char *pattern){
int i, j;
int len_pattern = strlen(pattern);
int *jump = new int[len_pattern];
jump[0] = -1;
j = jump[0];
for(i = 1; i < len_pattern; i++)
{
while(j > -1 && pattern[j+1] != pattern[i])
j = jump[j];
if(pattern[j+1] == pattern[i])
j ++;
//else j == -1
jump[i] = j;
}
return jump;
}
有了jump之后,就可以利用它来判断下一次的跳转了,完整的代码如下:
#include <iostream>
using namespace std;
int strlen(char *s){
int i = 0;
while(s[i] != '\0')
i ++;
return i;
}
int* get_jump(char *pattern){
int i, j;
int len_pattern = strlen(pattern);
int *jump = new int[len_pattern];
jump[0] = -1;
j = jump[0];
for(i = 1; i < len_pattern; i++)
{
while(j > -1 && pattern[j+1] != pattern[i])
j = jump[j];
if(pattern[j+1] == pattern[i])
j ++;
//else j == -1
jump[i] = j;
}
return jump;
}
void kmp_match(char *target, char *pattern){
int *jump = get_jump(pattern);
int i, j;
j = -1;
int len_pattern = strlen(pattern);
int len_target = strlen(target);
for(i = 0; i < len_target; i++){
while(j > -1 && pattern[j+1] != target[i])
j = jump[j];
if(pattern[j+1] == target[i])
j ++;
//else j == -1
if(j == len_pattern - 1) //pattern index reach the end
cout<<"Pattern occurs with shift "<<i - len_pattern + 1<<endl;
}
}
int main(){
int i, j;
char target[] = "ABCDABCDABCDABC";
char pattern[] = "ABCDABC";
kmp_match(target, pattern);
return 0;
}