<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">KMP字符串匹配算法是经典的字符串匹配算法,具有O(n)的时间复杂度,最早由Knuth,Morris及Pratt三人提出,而KMP算法本身并不是特别容易理解,在浏览了一些介绍KMP的经典文章(</span><a target=_blank href="http://www.matrix67.com/blog/archives/115" style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">http://www.matrix67.com/blog/archives/115</a><span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">)后,搞懂了KMP的工作原理,现总结如下:</span>
对于两个字符串a和b:
a: 12121232
b: 12123
可以看出在前4项匹配之后,第5项两个串并不匹配,这是如果是最直接的字符串匹配算法,姑且称它为native算法,就会直接放弃当前的匹配,然后从a[2]开始,重新开始和b[1]进行匹配检测。但其实这样就浪费了我们之前匹配4项后,所蕴含的信息。之前a[4]和b[4]匹配成功,假设我们从a[2]开始可以找到一个匹配,那么现在a[4]和b[3]也应该匹配成功,那么也就说b[3]==b[4],b[3]需要等于2,同理可知,如果从a[3]或a[4]开始找到一个匹配,那么b[2]或b[1]需要等于2。而实际上b串只有b[2]==2,也就是说只有可能从a[3]开始找到一个匹配。所以,native算法对于从a[2]和a[4]开始寻找匹配的尝试,都是徒劳,只是浪费时间而已。
因此我们在第五项匹配失败时,从b[4]开始往前找与b[4]相同的字符,我们找到b[2],然后用b[2]和a[4]匹配
a: 12121232
b: 12123
我们发现b[3]可以与a[5]匹配,我们进一步向后检测,发现当前得到一个匹配。那么是否每次出现不匹配时,都往前找与当前最后匹配字符相同的字符就可以了呢?实际上不然,要找到的这个字符,必须满足以下性质,假设我们找到的字符为b[j],当前最后匹配位置为b[t],那么b[1]b[2]...b[j-1]b[j] == b[t-j+1]b[t-j+2]...b[t-1]b[t]。也就是说:这个j要使得b串的钱j个字符等于b串的后j个字符,这其实不难理解,当不匹配在b[t+1]发生时,我们要将b串的前j位移到之前b串后j位的位置来进行匹配检测。例如:
a: 12121212
b: 121213
当b[5]出现不匹配时,我们找到的新的位置是b[3],而b[1]~b[3]等于b[3]到b[5],我们下一步就要把b[1]~b[3]移动到之前b[3]到b[5]的位置来进行进一步的检测:
a: 12121212
b: 121213
我们注意到其实j==1也是可以的,即b[1]~b[1]等于b[5]~b[5], 但我们的原则是选择符合条件的最长串,例如:当j==3时,有可能出现匹配,如果我们直接选择j==1,那么就把b[1]移动到之前b[5]的位置,如下:
a: 12121212
b: 121213
那么就跳过了j==3的情况,从而可能产生错误的结果。那么如果j==3匹配也不成功,进而我们就继续往前再寻找这样一个j,而对于我们这个例子,j==1。如果连j==1都不行,如下:
j==5
a: 1212132
b: 121212
j==3
a: 1212132
b: 121212
j==1
a: 1212132
b: 121212
那么我们已经无法再往前寻找了,也就说明我们无法从A串的前五位中的某一位开始找到找到一个匹配,这是我们的j取0,表示b串从a[6]开始重新寻找可能的匹配。假设我们通过预处理,将每次b串在j+1位匹配不成功之后,b串需要移动到的位置存储在P[j]中,以我们上面的例子为例:P[5]==3, P[3]==1, P[1]==0, 同理,我们不难求出P[6]==4, P[4]==2, P[2]==0。那我们的算法就描述为:a串和b串同时从头开始检测匹配,如果匹配成功就都往后移一位继续检测,如果不成功,假设在b串的j+1位和a串的i位不成功,那么将P[j]的值赋给j,然后继续检测匹配,如果仍不成功,继续重复这个过程,直到找到匹配,或者完全找不到匹配,那么就放弃a的前i位,从a[i+1]从头开始与b[1]检测匹配。
下面是这段算法的具体实现,其中略有不同的是:算法描述过程中,每个串从1开始计数,而程序中从0开始计数。
void KMP(char* A , char* B, int *P)
{
int j=-1;
for(int i=0; i<strlen(A);i++)
{
while (j>-1&&A[i]!=B[j+1])
{
j = P[j];
}
if(A[i]==B[j+1])
{
j = j+1;
}
if(j==strlen(B)-1)
{
printf("B is the substring of A\n");
}
}
}
要了解KMP算法的时间复杂度,要用到摊还分析。具体的过程如下:strlen(A)是求A串的长度,假设为n,那么显然两个if判断里的语句最多执行n次,现在的关键就是要了解while里的语句执行了多少次,我们注意到j = P[j]实际上是一个j值减少的过程,而j值必须为非负,而这个减少的次数必然小于等于n,这不难理解,因为要j = j + 1这条语句把j加到一个非负数,j值才有可能通过j = P[j]往下减。而j = j + 1总共最多执行n次,那么j = P[j]显然最多执行n次,因为每次j = P[j],j至少减少1。因此KMP算法的每部分都是线性时间,那么这几部分加起来也是线性时间O(n)。
知道了KMP算法,还有一个重要的问题没有解决,就是怎样通过预处理得到这个P数组。其实这个过程与KMP算法本身非常类似,可以看做是b串自己与自己的匹配过程。如下:
b1: abababa
b2: abababa
b串自己和自己当然是完全匹配,但为了求P[i],我们假设在i出现了不匹配,我们以i==6为例,来说明这个求值的过程,此时P[1]~P[5]都已经求出来了,下面要来求P[6]。j==5,因为前5位都匹配,所以j+1==i,这点通过前面的KMP算法就可以理解。那么在第6位,即j+1出现不匹配时,我们同样使用与KMP()相同的方法,将P[j]赋给j,然后再检测b2[j+1和b1[i],如果不匹配,那么继续将P[j]赋给j。这样的好处就是:如果b2[j+1]和b1[i]相同,那么将b2移到新的位置后,至少b1和b2有两位匹配,即b1[i-1]和把b2[j], b1[i]和b2[j+1]。其实,任何移动b2后,b2与b1有大于等于两位匹配的情况,必然通过j == P[j]的方式产生,例如上面的例子:j == P[5]==3, 假设不行,进一步j==P[3]==1, 如下:
j==P[5]==3
b1: abababa
b2: abababa
j==P[3]==1
b1: abababa
b2: abababa
如果匹配成功,P[i]==j+1;如果j==P[3]==1仍然不行,那么j==P[1]==0,也就是说在b2中找不到一个与b1前6位任一后缀,有至少两位匹配的前缀。那么,此时j==0,我们下一步检测b2[j+1]和b1[i],如果相等,那么就找到一个与b1中最后1位后缀匹配的b2的前缀,不难理解,如果只有一位匹配,那么必然是b2[1]和b1[i]匹配,P[i]==1,如果仍然不相等,那么就是说找不出一个b2的前缀可以与b1前6位的某一后缀匹配,P[i]==0。从这个过程可以看出,其实预处理检测了所有情况:大于等于两位匹配,1位匹配,及没有匹配,因此能够保证得到的结果符合我们对P[j]的要求,即满足b[1]~b[P[j]] == b[i-P[j]+1]~b[i]情况下的最大值。
下面是具体实现的代码:同样,算法描述过程中,每个串从1开始计数,而程序中从0开始计数。
void PreProcess(char *B, int *P)
{
P[0]=-1;
int j=-1;
for(int i=1; i<M; i++)
{
while(B[j+1]!=B[i] && j>=0)
{
j = P[j];
}
if(B[j+1]==B[i])
{
j=j+1;
}
P[i] = j;
}
}
PreProcess()的时间复杂度分析方法同KMP(),可以得出结论PreProcess()同样是线性时间,因此整个KMP算法的时间复杂度为O(n)。
以下是完整代码:
#include <stdio.h>
#include <string.h>
#define M 150
#define N 300
int m;
int n;
void PreProcess(char *B, int *P)
{
P[0]=-1;
int j=-1;
for(int i=1; i<M; i++)
{
while(B[j+1]!=B[i] && j>=0)
{
j = P[j];
}
if(B[j+1]==B[i])
{
j=j+1;
}
P[i] = j;
}
}
void KMP(char* A , char* B, int *P)
{
int j=-1;
for(int i=0; i<strlen(A);i++)
{
while (j>-1&&A[i]!=B[j+1])
{
j = P[j];
}
if(A[i]==B[j+1])
{
j = j+1;
}
if(j==strlen(B)-1)
{
printf("B is the substring of A\n");
}
}
}
int main()
{
char A[N],B[M];
int P[M];
scanf("%s",A);
scanf("%s", B);
n=strlen(A);
m=strlen(B);
printf("%s\n",A);
printf("%s\n",B);
PreProcess(B,P);
KMP(A, B, P);
for(int i=0;i<m;i++)
{
printf("%d ",P[i]);
}
}