努力的小Z从资料上找到了几篇KMP算法讲解,这篇博客拣其精华来继续讲解KMP。
假设文本是一个长度为n的字符串T,模板是一个长度为m的字符串P,且m<=n。需要求出模板在文本中的所有匹配点i,即满足T【i】=P【0】,T【i+1】=P【1】,... ,T【m-1]=P【m-1]的非负整数i(注意字符串下标从0开始)。如下图所示,P在T中有且只有一个匹配点,即位置3。
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
T | a | b | a | a | b | a | b | a | b | b | a |
P | b | a | b | a | a |
最朴素的方法是依次判断每个位置s是不是一个匹配点。检查匹配点需要O(m)时间(每一个字符注意比较),而可能的匹配点有O(n-m)个,所以最坏的情况时间复杂度为O(m(n-1))。
有一个简单的优化: 在检查匹配点的合法性时只要有一个字符不同,立刻停止比较,换下一个匹配点。但最坏的情况仍然需要O(m(n-1))。
和朴素算法相比,KMP算法的时间效率就强多了。它首先用O(m)的时间对模板进行预处理,然后用O(n)时间完成匹配。
KMP算法的精髓蕴含在下图中
!! | * | ||||||||||||||||||
a | b | b | a | a | b | a(错) | |||||||||||||
a(错) | b | b | a | a | b | a | |||||||||||||
a(错) | b | b | a | a | b | a | |||||||||||||
a | b(错) | b | a | a | b | a | |||||||||||||
a | b | b(需要和*比较) | a | a | b | a |
假定在匹配的过程中正在比较文串*位置的字符和模板串abbaaba的最后一个字符,发现二者不同(称为失配),这时,朴素算法会把模板串右移一位,重新比较abbaaba的第一个字符和文本串!!位置的字符。
KMP算法认为,既然!!位置已经比较过一次,就不应该在比一次了。事实上,我们已经知道灰色部分就是abbaab,应该可以直接利用模板串本身的特性判断右移一位一定不匹配。同理,右移两位或者三位也不行,但是右移四位是有可能的。这个时候,需要比较*处的字符和abbaaba的第三个字符。
有了失配函数后,KMP算法不难写出,代码如下:
void find(char *T,char *P,int *f){
int n=strlen(T),m=strlen(P);
getFail(P,f);
int j=0;//当前结点编号,初始为0号结点
for(int i=0;i<n;i++){ //文本串当前指针
while(j&&P[j]!=T[i]) //顺着失配边走,直到可以匹配
j=f[j];
if(P[j]==T[i])
j++;
if(j==m)
printf("%d\n",i-m+1); //找到了
}
}
状态转移图的构造是KMP算法的关键,也是它最巧妙的地方。算法的思想是“自己匹配自己”,根据f[0],f[1],...,f[i-1]递推f[i],代码和匹配部分非常相似,如下所示:
void getFail(char *P,int *f){
int m=strlen(P);
f[0]=0;f[1]=0; //递推边界初值
for(int i=1;i<m;i++){
int j=f[i];
while(j&&P[i]!=P[j])
j=f[j;
f[i+1]=P[i]==P[j]?j+1:0;
}
}
下一篇小Z会具体展示一道例题,帮助大家更容易理解透彻KMP算法QAQ