KMP算法超详细入门

算法核心思想:

          对于常规的字符串匹配,一般都是一路穷举过去,如下图。

              (例如要在字符串T="abcabcabd"中寻找字符串S="abcabd")




    

     黄、灰色代表被检查过,黄色代表相等,灰色代表不相等,没有颜色标记代表不用检查,可以看出常规的办法,每次都要从头开始检查,一旦检查到不相等,S串只能向右移一位,然后重新从头检查,做了很多重复的工作。


       然而KMP算法的过程是这样的:


        明显的看到,检查的次数减少了(直接跳过了原来常规的第二次和第三次检查),在第一次检查完后S串直接右移三位,而且在第二次检查的时候,也并没有从开头检查。

     KMP算法的原理:

    如图,此时黄色标记为对应竖直T中字符是相等的,灰色代表不相等(和上面的表达的【是否检查】意义不同)。


   为了阐述方便,将处于不同位置的两个S串在竖直方向上连续重合、在竖直上对应的字符相等、左侧没有灰框的区域称为交集。

    铺垫结束,开始阐述。

     在一次S串匹配失败后,如果出现解,则接下来的移动出现解只有三种可能:

1.向右移动,有交集



 2.向右移动,有重合没交集


3.向右移动,没重合没交集



        针对于第一种情况,KMP算法核心就是依此产生的:如果在一次匹配失败后,假设存在移动1使解出现,由于解一定和原来S串有交集,所以这个解只能在与原来S串有交集的S串中找,没交集的肯定不行,所以KMP算法中存在S串右移多步的情况。并且,交集处的字符因为相等,所以移动后的S串已经间接和对应的T中字符比较过,所以移动后的S串不需要从头和T串比较。如果移动1无法产生解或者说不存在移动1,那只能够向右移动的更远,也就是选择移动2,移动2也没有,那只能移动的更远,选择移动3。

        在一次匹配失败后,S要右移,为了防止错漏,移动的选择必须由近到远的(下文有说原因),所以我们必须要确定是否存在移动1,即确定S串右移是否存在交集,如果存在,当前交集的最长是多少,必须要从最长的交集开始匹配(为了防止错漏,还是由近到远的原则),由交集的性质我们知道,交集肯定是从黑框左边开始的,如下图,我们发现交集中S串的字符所对应的下标,没错,上面的S串的下标正是从黑框左边的第一个数起,     而下面的则是从 S[ 0 ]开始数起,也就是说,交集本质其实是, 从S[ 0 ]开始的n个连续的字符构成的字符串和黑框前的n个字符串相等的意思。


        很明显n越大,也就是交集越长,也就是相等的字符串部分越长的时候,两个位置的S串竖直重合更多,第二个离第一个越近,我们用一个数组next来记录交集信息next[ i ]记录的是当当前位置 S[ i ]是一个灰框(也就是不和T相等的时候)、S[ i ]之前不存在灰框,S[ i ]前 next[ i ]个字符和从S[ 0 ]开始数起的next [ i ]个字符是相等的。

对于 s="avoavd",next[ 5 ]=2;

很明显 S[ 0 ] 、S[ 1 ] = “av”,S[ 3 ]、S[ 4 ]="av"

        这样一来,在匹配的过程中一旦出现黑框,就停止匹配,根据出现黑框的S的下标,利用next数组判断前面是否有交集,如果有,右移S串至最大的交集的位置(因为此时向右移动的最少,不会错漏),然后继续重复判断,如果没有,就要根据情况选择移动2或者移动3了。

这里先叉开next数组,填一下之前的由近到远的原则是怎么回事。

移动的选择是由近到远的:

三种移动的选择优先顺序是,移动1>移动2>移动3,三种移动的移动距离很明显是由近到远的,参照一开始的暴力常规匹配,S串是每次向右移动一个单位,是为了避免错漏,同理,可以选择移动1的情况下,选择了移动2或3,也会造成错漏,如:

        以下是在初始S之后的三种移动选择,可见,由于没按照移动顺序从近到远的原则,选择之后只能在该选择的基础上往右移,因此部分可能没有被检查就被忽略掉了

(接下来的颜色标记代表的意思, 黄、灰色代表被检查过,黄色代表相等,灰色代表不相等,红色代表不用检查肯定相等,没有颜色标记代表不用检查。)


另外,在进行移动1的时候,一定要选交集最长的开始移动匹配,否则违背了由近到远的原则,也会导致错漏,如:


NEXT数组:

        next数组还有一个理解,next[ i ]代表的是当S[ 0 ]...S[ i-1 ]都和T对应的字符相等时,然而S[ i ]不相等时,S串将右移,使得S[  next[ i ] ]移动到S[ i ],如

                  T="abababdccq",    S="ababd"

next中的内容如下:


模拟匹配过程:


       在第一次匹配过程中,发现S[ 4 ] ! = T [ 4 ],由于next[ 4 ] = 2,所以要右移S串使S[ 2 ]移动到S[ 4 ]所对应的位置,然后进行第二次匹配

       在第二次匹配,由于是S[ 0 ] = =S [  2 ],S [ 1 ] = = S [ 3 ],而且S[ 2 ]、S[ 3 ]已经对应T中字符比较过了,所以

S[ 0 ]、S[ 1 ]不需要比较,直接从S[ 2 ]开始和T[ 5 ]开始比较.

这里再解释下next[ i ]的内容,它就是表达,当前S[ i ]不匹配时,S[ next[ i ] ]串应该右移至S[ i ]的位置,(-1是额外规定的含有额外的意义),所以在如果要 填写next[ i ]中的内容是的时候,直接想,S[0]...S[i-1]都匹配,然而

S[ i ]不匹配,S串右移使得S[ X ]到S[ i ]的位置,这个X是什么即可。


next[ 0 ]=-1,  说明S[ 0 ]和当前的T [ m ]比较过了 (T[ m ]是当前S[ 0 ]竖直上对应的字符,下同),应该移动S串使得下次直接比较S[ 0 ]和T[ m+1 ];

next[ 1 ]=0,  说明S[ 0 ]与当前T[ m ]需要比较(T [ m ]是当前S[ 1 ]所对应的字符);

next[ 2 ]=-1, 说明S[ 0 ]和当前的T [ m ]比较过了,应该移动S串使得下次直接比较S[ 0 ]和T[ m+1 ];

next[ 3 ]=1,  说明S[ 0 ]==S[ 2 ],应该右移S串使S[ 1 ]到T[ m ]的位置,比较S[ 1 ]和T[ m ]的值

next[ 4 ]=2,  说明S[ 0 ],S[ 1 ] = = S[  2 ],S [  3 ],应该右移S串使得S[ 2 ]到T [ m ]的位置,比较 S[ 2 ]和T [ m ];


       以下是网上拿来的总结,看一看有助于想到各种各样的填NEXT [ i ]的情况:

定义:
1 next[0]= -1   意义:任何串的第一个字符的模式值规定为 -1

2 next[j]= -1   意义:模式串 T 中下标为 j 的字符,如果与首字符 相同,且 j 的前面的 1—k 个字符与开头的 1—k 个字符不等(或者相等但 T[k]==T[j] )( 1 k)。  如: T=”abCabCad”   next[6]=-1 ,因 T[3]=T[6]

3 next[j]=k     意义:模式串 T 中下标为 j 的字符,如果 j 的前面 k 字符与开头的 k 个字符相等,且 T[j] != T[k]  1 k) T[0]T[1]T[2] 。。。 T[k-1] ==  T[j-k]T[j-k+1]T[j-k+2]…T[j-1], T[j] != T[k].     1 k;

(4) next[j]=0      意义:除( 1 )( 2 )( 3 )的其他情况。


例子:


最后是 next数组的生成华丽超短代码,好好咀嚼咀嚼吧

void get_next(char *s,int *next)
{
	int j,k;
	next[0]=k=-1,j=0;
	while(s[j]!='\0')//作用:找当前断点的最长前缀
	{
		//begin
		if(k==-1 || s[j]==s[k])//前缀相同
		{
			j++,k++;
			if(s[j]==s[k])//前缀相同且断点都相同
				next[j]=next[k];
			else//前缀相同断点不同
				next[j]=k;
		}
		else //前缀不同,去找到相同的前缀,或到最后为空
			k=next[k];
	}
}

        前缀的意思:对于s="avoavd",那么对于S[ 5 ],next[ 5 ]=2它的前缀就是S[ 0 ],S[ 1 ],也就是从S[ 0 ]数起的next[ 5 ]个字符.(为了方便就不起另外一个名字了,其实前缀本来的意思就是任意前面连续的n个字符,这里特指相等的前缀).


同时,这里有一个性质:S[ k ]与 S [ next[ k ] ], S[ next[ k ] ]与S [ next[ next [ k ] ] ]......他们之间的前缀是存在公共前缀的(这里 S的下标不为 0,如果下标为 0,S[ 0 ]的前缀就是空,其实空也可以看成一个公共前缀.)


如:S="acacacad", 当K=7时,S[ 7 ]的前缀" a c a" ; 当K=3,S[ 3 ]的前缀"a"; 当K=1时,S[ 1 ]的前缀" a ";

所以他们的公共前缀是"a",这性质有什么用呢? 代码的最后一句 k=next[ k ]就是利用这个性质


不妨用S="acacacade",j=7,k=3,从循环的begin开始,大概就可以清楚k=next[ k ]的作用了.



此文参考网上众多文章,特别此篇,http://blog.chinaunix.net/uid-27164517-id-3280128.html。

另外,链接中介绍了next的第二种表示方法,虽然没这么高效,还是在此贴一下代码:

void get_next()
{
    int j, k;
    next[0]=k=-1,j=0;
    while(s[j]!='\0')
        if(k == -1 || s[j] == s[k])//有相同的前缀
            next[++j] = ++k;
        else//没有相同前缀,找相同的前缀
            k = next[k];
}


写的很仓促,冗长,要靠读者自己压缩找精华了,其实理解了关键,KMP的核心内容是很少的。

难免有写错疏漏的地方,请各位指正。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
KMP算法是一种字符串匹配算法,它的全称是Knuth-Morris-Pratt算法,由Donald Knuth、Vaughan Pratt和James H. Morris三人于1977年联合发表。KMP算法的核心思想是利用已知信息尽可能地减少匹配的次数。 KMP算法的实现需要两个步骤:预处理和匹配。预处理阶段是为了计算出模式串中每个位置的最长公共前后缀长度,匹配阶段则是利用预处理结果进行匹配。 具体来说,预处理阶段需要计算出模式串中每个位置的最长公共前后缀长度,这个长度可以用一个数组next来存储。next[i]表示模式串中以i结尾的子串的最长公共前后缀长度。计算next数组的方法是从前往后依次计算,假设已经计算出了next到next[i-1],现在要计算next[i],则需要比较模式串中以i-1结尾的子串和以0结尾的子串、以1结尾的子串……以next[i-1]结尾的子串,找到最长的公共前后缀即可。 匹配阶段则是利用预处理结果进行匹配。具体来说,假设现在要在文本串中查找模式串,首先将模式串和文本串的第一个字符进行比较,如果相等,则继续比较下一个字符,否则需要根据next数组来移动模式串的位置。具体来说,假设当前模式串的位置是j,文本串的位置是i,如果模式串中第j个字符和文本串中第i个字符不相等,则需要将模式串向右移动j-next[j]个位置,这样可以保证模式串中前next[j]个字符和文本串中前i-(j-next[j])-1个字符是相等的。如果模式串已经移动到了最后一个字符,说明匹配成功,否则继续比较下一个字符。 KMP算法的时间复杂度是O(m+n),其中m和n分别是模式串和文本串的长度。KMP算法的优点是可以在O(m+n)的时间内完成匹配,而且不需要回溯文本串中已经匹配过的字符。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值