网上有很多KMP算法的资料,但是我还是通过这种形式对我的想法做一个梳理同时分享给大家。有不妥之处还请指教。
KMP匹配算法其实是在朴素匹配的基础上进行了一点变化,主要特点就是通过一个前缀函数对模式串进行预处理,从而得到一个前缀数组P[],利用这个它来减少不必要的比较操作。
比如,目标串A = "ababaababcad",模式串B = "ababca"。分别用指针 i 和 j 来操作。
随着匹配的进行,i 和 j 同时增加,但是当发生匹配失败时,我们就需要用到预处理得到的前缀数组了。
发生匹配失败时,目标串指针i = 4不变,模式串指针 j 被赋值为P[ j ],并且继续与目标指针匹配。
0 1 2 3 4 5 6 7 8 9 10 11
A: a b a b a a b a b c a d
B: a b a b c a
0 1 2 3 4 5
P[j]存储的值k 是指[0, j-1] 之间的最长自匹配子串的长度。从上图我们知道,j = 4时,[]0, 3]间的最长自匹配字串为“ab”,P[4] = 2。(预处理函数后文会讲到)
0 1 2 3 4 5 6 7 8 9 10 11
A: a b a b a a b a b c a d
B: a b a b c a
0 1 2 3 4 5
由于A[5] <> P[3] ,i = 5保持不变,j = (P[3] = 1)。
0 1 2 3 4 5 6 7 8 9 10 11
A: a b a b a a b a b c a d
B: a b a b c a
0 1 2 3 4 5
A[5]<>P[1],i = 5不变,j = (P[1] = 0)。
0 1 2 3 4 5 6 7 8 9 10 11
A: a b a b a a b a b c a d
B: a b a b c a
0 1 2 3 4 5
最终匹配成功。
0 1 2 3 4 5 6 7 8 9 10 11
A: a b a b a a b a b c a d
B: a b a b c a
0 1 2 3 4 5
我的这个例子找到不是特别好,但是最后3步应该也可以看出一点儿迭代的架势来。而这就是KMP匹配算法的精华所在,通过对前缀数组P[]进行迭代缩小范围来确认已经成功匹配的前缀,从而节省了不断回溯的时间,因为已确认成功匹配的前缀无需再比较。
若匹配成功,该函数返回该匹配成功串的头位置,否则返回-1。
template<class T>//Find only one match, return the start of the match
int KMPMatch(const T* target, int tl, const T* pattern, int pl)
{
int *P, posT = 0, posP = 0;
next = new int[pl];
setPrefix(pattern, pl);
while(posT < tl && posP < pl)//算法时间复杂度是线性的,为O(tl + pl)
{
if(target[posT] == pattern[posP] || posP == -1) { posT++; posP++;}
else posP = P[posP];
}
if(posP < pl) return -1;
return posT - pl;
}
下面是对前缀函数的说明。
首先我们定义P[0] = -1,这样方便在自匹配的时候同加一。以模式串B为例,红色表示自匹配指针 k,蓝色表示遍历指针 i,函数具体执行过程如下。
0 1 2 3 4 5
B: -1 a b a b c a
B: -1 a b a b c a P[1] = (k= 0);不匹配,k = P[k]
B: -1 a b a b c a
B: -1 a b a b c a P[2] = (k= 0);
B: -1 a b a b c a P[3] = (k= 1);
B: -1 a b a b c a P[4] =(k = 2);不匹配,k = P[k]
B: -1 a b a b c a不匹配,k = P[k]
B: -1 a b a b c a
B: -1 a b a b c a P[5] =(k = 0);OVER
该函数的作用就是在每一个位置记录下前面最长自匹配子序列的尾位置。这样在目标串与模式串的某一元素匹配失败时,模式串指针就可以利用这种这个记录快速迭代到前缀都以匹配好的位置。
template<class T>
void setPrefix(const T* pattern, int* P, int length)
{
int i = 0, k = -1;
P[0] = -1;
while(i < length - 1) //注意这里的限制是length - 1而不是length。如果建立数组时严格按照模式串长度来的话,在循环到i = length时会产生P[length]的溢出
{
if(k == -1 || pattern[i] == pattern[k])
{
++i;
++k;
P[i] = k;
}
else k = P[k];
}
}
本文中给出的算法只是最基本的寻找一个匹配的算法,如有多个匹配的需要请自行改进。