最近在看些模式匹配的算法,算法很多,各种各样的。在看到KMP算法时,感觉有些吃力,查了许多这方面的资料,基本上都是国人写的技术博客,奈何绝大部分人自己都没搞懂,写出来的东西只是让俺更加的迷茫,总之一个字:不懂。后来索性拿着算法导论那本书仔细的看了下这个算法的具体思想和实现,自我感觉看得算是懂了,这里简单的说下我对KMP算法的理解。
在说KMP算法之前,我们先简单介绍另外一个算法,BF算法,这个算法也是我们在模式匹配中最容易想到的一个算法,代码大致如下:
void BF(char *x, int m, char *y, int n)
{
int i, j;
for (j = 0; j <= n - m; ++j)
{
for (i = 0; i < m && x[i] == y[i + j]; ++i);
if (i >= m)
printf(j);
}
}
简单来说,BF算法就是将模式串pattern与目的串text中的字符逐个进行比较,某一轮比较的中途发现有不匹配字符或者在该轮比较中最后发现了模式串时(此时输出该轮比较的初始位置),将目的串的初始比较位置下移一个字符,从而开始下一轮的比较,直到目的串的初始比较位置到达了n-m时整个算法就结束了。BF算法相当于一个枚举算法,有点暴力破解的含义在里面,其时间复杂度为O(n*m)。
KMP算法也是一个从左向右进行匹配的算法,当在将模式串pattern与目的串text匹配中的字符逐个进行比较的过程中发现有mismatch的时候,目的串text的下一个参与比较的字符并不回退,仍保持不变,只是将模式串向右移动几个位置继续进行字符的逐个匹配。接下来我们讨论两个问题:为什么此时只是将模式串向右移动几个位置就能继续进行字符的逐个匹配?具体应该将模式串移动几个位置?
对第一个问题的解释:
假定T为目的串,P为模式串。如图(a)所示,当我们对比到T[10]与P[6]时(字符串的起始位置从1开始)发现不匹配,此时若按照BF算法会将T[10]回退到T[6]进行下一轮的匹配,但在KMP算法中我们并不回退,因为通过前面的T[5...9]与P[1...5]相匹配(T[5...9]=P[1...5])我们已经知道T[5...9]的具体内容。此时若只是简单的将T[10]回退到T[6]再进行下一轮比较,因为T[6]=P[2]=b,而P[1]=a,T[6]肯定不等于P[1],从而导致这种回退的比较显得多余,最终导致的结果就是这种回退的比较会影响算法的效率。那么有没有一种方式可以让T[10]不回退,即下一次T与P的字符比较还是在T[10]出进行?有的,此时我们可以让P向右移动几个位置,使得移动位置后的P其某一前缀仍能与T[10]前面的某些字符相匹配,如图(b)所示,当将P向右移动两个位置后T[7...9]=P[1...3]成立,此时我们就可以安心的将T[10]与P[4]进行比较了,因为T[7...9]与P[1...3]已经相匹配了。这种情况下因为T不回退,所以其效率更高。
对第二个问题的解释:
上面对第一个问题进行了解释,说得不太好,但大致思想就这样。接下来我们解释下P具体应该向右一定几个位置呢。我们先假设T[i...i+q-1]=P[1...q],其中i是字符匹配到了的初始位置,相当于图(a)中的5,即i=5,q是匹配到了的字符的个数,相当于图(a)中的5,即q=5,综合一下就是T[5...9]=P[1...5],这个等式在图(a)中是成立的。然后我们假设P向右移动几个位置后有P[1...k]=T[i+q-k...i+q-1],因为P是向右移动的,所以肯定有0<k<q,又由前面的T[i...i+q-1]=P[1...q]可知P[1...k]=P[q-k+1...q],即模式串P中前k个字符和后k个字符相互匹配(模式串P中可能有多个这样的前缀和后缀对,我们去最长的那一对,这样可以避免在目的串中某些匹配到了的串被遗漏掉),如图(b)所示有P[1...3]=P[3...5],此时的k就是我们想要的。此处我们定义一个next[m]数组,m的值为模式串P的长度,对于0<i<m+1有next[i]=k,比如对于i=5,有k=3,所以next[5]=3。那么对于这个next数组我们应该如何计算呢,代码如下:
getNext(char *P)
m<-length[P];
next[1]<-0;
k<-0;
for q<-2 to m
do while k>0 and P[k+1]!=P[q]
do k<-next[k]
if P[k+1]=P[q]
then k<-k+1
next[q]<-k
return next
至于代码的意图,可以自己去揣摩揣摩,还是挺有意思的。
KMP匹配算法的代码如下:
KMP-MATCH(char *P,char *T)
n<-length[T]
m<-length[P]
next<-getNext(char *P)
q<-0
for i<-1 to n
do while q>0 and P[q+1]!=T[i]
do q<-next[q]
if P[q+1]=T[i]
then q-<q+1
if q==m
then print i-m
q<-next[q]
由以上两段代码我们可以看出里面的大致结构很相似,这是因为在getNext代码段本质上也是一个字符串模式匹配的过程。