KMP算法中的KMP分别是指三个人名:Knuth、Morris、Pratt,其本质也是前缀匹配算法,对比前缀蛮力匹配算法,区别在于它会动态调整每次模式串的移动距离,而不仅仅是加一,从而加快匹配过程。下图通过一个直观的例子展示前缀蛮力匹配算法和KMP算法的区别,前文提过,这二者唯一的不同在于模式串移动距离。
上图中,前缀蛮力匹配算法发现匹配不上,就向右移动距离1,而KMP算法根据已经比较过的前缀信息,了解到应该移动距离为2;换句话说针对母串的下一个匹配字符,KMP算法了解它下回应该匹配模式串的哪个位置,比如上图中,针对母串的第i+1个字符,KMP算法了解它应该匹配模式串的第k+1个字符。为什么会是这样,这是因为母串的子串T[i-k, i]=aba,而模式串的子串P[0,k]=aba,这二者正好相等。所以模式串应该移动到这个位置,从而让母串的第i+1个字符和模式串的第k+1个字符继续比较。
那k值又是如何寻找?请注意上图中,模式串位置j已经匹配上母串的位置i,也就是T[i-k, i] = P[j-k, j]=aba;根据前文的T[i-k, i] = P[0, k] = aba, 从而得出P[0, k] = P[j-k, j] = aba。通过观察发现,就是在模式的子串[0, j]中寻找一个最长前缀[0,k],从而使得[j-k, j] = [0,k];
于是可以定义一个jump数组,jump[j]=k,表示满足P[0, k] ==P[j-k, j] 的最大k值,或者表述为:如果模式串j+1匹配不上母串的i+1,那跳转到模式串k+1继续比较。有了这个jump数组,就很容易写出kmp算法的伪代码:
j:=0;
for i:=1 to n do
Begin
while (j>0) and (P[j+1]<>T[i]) do j:=jump[j];[
if P[j+1]=T[i] then j:=j+1;
if j=m then
Begin
writeln('Pattern occurs with shift ',i-m);
end;
end;
KMP算法中jump数组的构建可以通过归纳法来解决,首先确定jump[1]=0;假设jump[j]=k,也就是P[0, k] == P[j-k, k],如果P[j+1] == P[k+1],那么得出[0,k+1] = P[j-k, j+1],从而更加定义得出jump[j+1] = k+1;
如果P[j+1] != P[k+1],那就接着比较P[j+1] ?= P[k1+1],其中(jump[k] = k1),根据(jump[k]=k1)的定义,P[0,k1] == P[k-k1, k],根据(jump[j]=k)的定义,P[0, k] == P[j-k, j],根据这两个等式,推出P[0, k1] == P[j-k1, j],如果此时P[j+1] == P[k1+1],则得出:jump[j+1] = K1 +1 = jump[k] +1。
如果P[j+1] != P[K1+1],继续递归比较P[j+1] 和P[ jump[ jump[k ] ] +1] …. P[1];
int KMP_Match(const string &target, const string &pattern) {
const int length = pattern.size();
// calculate jump
vector<int> jump(length);
jump[0] = -1;
for (int i = 1; i < length; i++) {
int index = jump[i-1];
while (index >= 0 && pattern[i] != pattern[index+1]) {
index = jump[index];
}
if (pattern[i] == pattern[index+1]) {
jump[i] = index + 1;
} else {
jump[i] = -1;
}
}
//match algorithm start
int pattern_index = 0, target_index = 0;
while (pattern_index < length && target_index < target.length()) {
if (target[target_index] == pattern[pattern_index]) {
++target_index;
++pattern_index;
} else if (patter_index == 0) {
++target_index;
} else {
pattern_index = jump[pattern_index-1] + 1;
}
}
if (pattern_index == length) {
return target_index - length;
} else {
return -1;
}
}
考虑模式串匹配不上母串的最坏情况,前缀蛮力匹配算法的时间复杂度最差是O(n×m),最好是O(n),其中n为母串的长度,m为模式串的长度。KMP算法最差的时间复杂度是O(n);最好的时间复杂度是O(n/m)。