在朴素字符串匹配算法中,当匹配失败时,位移加一,也就是模式向后滑动一位,效率较低。我们能否在匹配失败时,利用已有的匹配信息(如当前文本的匹配位置,模式已经匹配的长度等)将模式向后滑动尽可能远的距离呢?
受有限自动机字符串匹配算法启示,利用前缀后缀原理,假设当前字符匹配长度为q(P[1..q]),在匹配P[q+1]时失败了,这时只要找到一个满足Pk为Pq的真后缀的最大k值,我们就可以把模式向后滑动q-k个长度,即位移s’=s+(q-k)。所谓真后缀是因为当前在匹配P[q+1]时已经失败了,模式至少要向后滑动1个长度,也就是说0<=k<q。但是我们注意到s’也不一定是个有效的位移,因为如果此时P[k+1]!=P[q+1],这也是一个无效的位移。再注意到Pk的真后缀P的最长前缀,也是Pq的真后缀,所以我们可以进行递推,直到找到一个合适的k,满足P[k+1]==P[q+1]或k==0。k==0也就是将模式向后移动q个长度,此时匹配长度为0,又开始重新匹配。
模式P的前缀函数next函数的定义如下:
next[q] = max{k|k<q,且Pk为Pq的真后缀},显然next[1]==0。
计算前缀函数的伪代码如下:
Compute-Prefix-Function(P)
{
π[1] == 0//P1的真后缀为空串P0
k=0
//递归求π[q],2<=q<=m
for q=2 to m
{
//进入循环说明已经匹配了q-1个字符
//且进入循环时有k==π[q-1]
while(k>0 and P[k+1] ≠P[q])
{
k =π(k)//找下一个真后缀
}
if(P[k+1] ==P[q])//说明找到了合适的k值
then k++
π[q] = k
}
return π;
}
利用计算好的前缀函数,仅扫描一遍文本进行字符串匹配的算法伪代码如下:
KMP-Matcher(T,P)
{
q = 0 //已经匹配的字符数
for 1=1 to n
{
while(q>0 and P[q+1] ≠T[i])
{
q =π[q]
}
if(P[q+1]==T[i])
then q++
if(q==m)
{
print ”pattern occurs with shift ”i-m
q =π[q]
}
}
}
算法的C++实现及测试代码如下:
#include <cstdlib>
#include <iostream>
#include <cstring>
using namespace std;
//next数组范围为[0..m-1]
void Compute_Prefix_Function(const char* P, int* next, int m)
{
next[0] = -1;
int k = -1;
for(int q=1; q<m; q++)
{
//此时已经匹配了q个字符,P[0,q-1],并且 k=next[q-1];
//即P[0,k]为P[0,q-1]的后缀 ,k=-1时,Pk为空串
while(k>-1 && P[k+1]!=P[q])
{
k = next[k];
}
if(P[k+1]==P[q])
{
k++;
}
next[q] = k;
}
}
void KMP_Matcher(const char* T, const char*P)
{
int n = strlen(T);
int m = strlen(P);
int next[m];
Compute_Prefix_Function(P,next,m);
int q = -1;//已匹配0个字符
for(int i=0; i<n; i++)
{
while(q>-1 && P[q+1]!=T[i])//进入循环说明P[0..q-1]已经匹配
{
q = next[q];
}
if(P[q+1]==T[i])
{
q++;
}
if(q==m-1)
{
cout<<"Pattern occurs with shift "<<i-m+1<<endl;
q = next[q];
}
}
}
int main(int argc, char *argv[])
{
const int Max_Length = 1000;
char T[Max_Length];
char P[Max_Length];
while(gets(T))
{
gets(P);
KMP_Matcher(T,P);
cout<<"next case:"<<endl;
}
system("PAUSE");
return EXIT_SUCCESS;
}
补充说明:
(1)前面叙述的时候,我们假设数组下标是从1开始的,P0表示空串,在我们实际实现中,数组下标是从0开始的,我们只需简单地用-1表示空串即可,但是我们应该注意到当q==m-1是就表示找到了一个完整的匹配。
(2)如果你觉得上述代码不是很好理解,可以参考算法导论(第二版)P568-P573,多琢磨琢磨,你也可以参考严蔚敏的数据结构(C语言版)的第四章,那里提供了另外一种讲解思路,但是原理是一致的。
(3)算法的预处理时间为O(m),匹配时间为O(n)。