首页 > 代码库 > KMP算法精读
KMP算法精读
看了网上很多KMP算法的讲解,总感到云里雾里,最后翻了翻严蔚敏的《数据结构》终于了解了KMP算法。这里给大家分享一下。
对于很久没碰算法的同学,首先要了解一下最原始的模式匹配算法BF算法。如果是由一定了解的可以直接转到KMP算法。
1、BF算法
算法基本思想就是从主串S开头起,与模式串T的第一个字符比较。若相等,则继续逐个比较字符;否则从主串的开头下一个字符开始重新比较。相当于主串S我要循环S.len长度,比较过程中与模式串T比较也是一个循环,这个循环的次数最多是S.len-1,且每次循环长度最多T.len。也就是说此算法复杂度为O(S.len*T.len)。
下图展示了BF算法匹配的过程
图一
代码如下:
int Index(String S,String T,int pos)
{
//返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数值为0。
//其中,T非空,1<=pos<=S.len。
i=pos;j=1;
whie(i<=S.len && j<=T.len)
{
if(S[i] == T[j]){++i;++j;}//继续比较后继字符
else{i=i-j+2;j=1;}//指针后退重新开始匹配
}
if(j>T.len)return i-T.len;
else return 0;
}
2、KMP算法
事实上,算法可以在O(S.len+T.len)数量级上完成。其改进在于:每当一趟匹配过程中出现字符比较不相等时,不需要回溯i指针,而是利用已经得到的“部分匹配”的结果将模式向右“滑动”尽可能远的记录后,在进行比较。如下面这个例子:
图二
回顾图一的匹配过程,当i=7,j=5字符匹配不等是,又从i=4,j=1重新开始比较。然后经自己观察可发现,在i=4和j=1,i=5和j=1以及i=6和j=1这3次比较都是不必进行的。因为从第三趟部分匹配结果就可得知,主串中第4、5和6字符必然都是b、c和a。因为模式T中第一个字符是a,因此无需要在和这三个字符进行比较。而仅需将模式向右滑动3个字符的位置继续进行i=7、j=2时的字符比较即可。同理,在第一趟匹配中出现字符不等时,仅需将模式T向右移动两个字符的位置继续进行i=3、j=1时的字符比较。由此可以得到图二指针i没有回溯。
原理就是这样,但如何知道当下不匹配后下一步移动模式串T,这是next函数解决的事情。next函数定义为:
由此定义退出下列模式串的next函数值:
网上对next的解释真是五花八门,后面还会讲到一个改进的next函数,但是我们还是要从最基础的next讲起,真正的理解一个东西,最好的方式我认为就是从无到有的去了解它。
next[j]表明当模式串中第j个字符与主串中相应字符“失配”时,在模式中需重新和主串中该字符进行比较的字符的位置。如当j=4的模式串T[4]与主串S[4]失配时,这时候就应该用j=next[j]=2位置的字符与主串S[4]进行对比。为什么呢?可以看看j=4前面的字符串是什么(网上都用“后缀”这个术语),这里前面的字为T[3]=a,而T[1]=a且T[3]=S[3]的,所以没必要比较T[1]与S[3]了,我们直接比较T[2]和S[4]即可,这样就让模式串实现了滑动。next的值也渐渐清晰,next[j]的值其实就是由T[1~j-1]与next[j-1]决定的。next[j]的值其实就是模式串T[1~j-1]正向从头开始数和模式串T[1~j-1]逆向从尾开始数(只是去数,而不是将字符串倒序排)得到最长相同字符串的长度+1。最终了解到next[j]的值跟T[j]的值没有关系。有了next值就可以实现KMP算法了。
int Index_KMP(String S,String T,int pos)
{
//在研究算法的时候,这里的pos其实可以不要的。严老师只是拓展了一下,可以让这个函数对主串S从第pos位起开始做模式匹配的工作。
i=pos;j=1;
while(i<=S.len && j<= T.len)
{
if(j==0 || S[i]==T[j]){++i;++j}
else j=next[j];
}
if(j>T.len) return i-T.len;
else return 0;
}
手工算next的值是会了,但是代码实现确实看得我心里憔悴。这是一种递归的思想,还跟我们常见的递归不一样,真实呀灭爹啊!
这里需要用递推的方法来分析求解next函数值。
有定义知
next[1]=0
设next[j] = k,这表明模式串存在下列关系T[1~k-1] = T[j-k+1~j-1]。此时next[j+1]可能有两种情况:
(1)若T[k] = T[j],则表明模式串中T[1~k]=T[j-k+1~j],next[j+1]=next[j]+1
(2)若T[k]!=T[j],则表明在模式串中T[1~k]!=T[j-k+1~j]。此时把这个模式串即当成主串也当成模式串考虑。移动模式串,以模式中的第next[k]个字符和主串的第j个字符相比较。用T‘表示主串,k‘=next[k],若T‘[j]=T[k‘],则说明主串中第j+1个字符之前存在一个长度为k‘的最长子串,和模式串中从字符起长度为k‘的子串相等。
即T[1~k‘]=T[j-k‘+1~j],也就得到next[j+1]=next[k‘]+1。
同理,若T[1~k‘]!=T[j-k‘+1~j],则继续将模式串向右滑动至next[k‘]……依此类推,直至某个字符匹配成功或者不存在任何k‘满足next[j+1] = 1。
算法实现如下(仿照KMP算法):
void get_next(String T,int next[])
{
i=1;next[1]=0;j=0;
while(i<T.len)
{
if(j==0 || T[i] == T[j]){++i;++j;next[i]=j;}
else j=next[j];
}
}
对next函数算法还可以进一步优化,优化当然也是鉴于问题的提出。比如这样的一个主串S——‘aaaaaaaab’和模式串T——‘aaaab’。
当S[4]=a与T[4]=b进行比较不相等时,next[4]=3指向T[3],而next[3]=2指向T[2]……,这样就导致做了很多重复事情。事实可以直接滑到模式串首直接进行S[4]与T[1]进行比较。根据上面的思路定义可得,当S[i]<>T[j]时,且T[j]=T[k],这里其实就是k=next[j],那么我们可以不用在让S[i]和T[k]进行比较,而是跟T[next[k]]进行比较,依此类推直到T[i]<>T[next[k‘]]或者k‘=1,就是要跳转的位置。实现代码如下:
void get_nextval(string T,int next[])
{
i=1;nextval[1]=0;j=0;
while(i<T.len)
{
if(j==0 || T[i] == T[j])
{
++i;++j
if(T[i] != T[j]) next[i] = j;
else nextval[i] = nextval[j];//这里作为不同之处,就是判断是否相同,不同则+1,相同则用前一个next值作为当前位置的值
}
else j=nextval[j];
}
}
如有错误还请指正~