首页 > 代码库 > kmp算法
kmp算法
关于kmp算法,相信大家都不会陌生。但是,对于我自己而言,总感觉自己没有彻底地透彻理解它的内涵。最近看了一些资料,将自己的一些心得写下来,供自己和大家参考。
写在前面,这里为了方便与程序中使用所有的字符数组下标,均从0开始。
1、问题定义
kmp算法解决的问题是模式匹配。简单的说,就是给定两个字符串A和B,现在问字符串A是不是包含字符串B,或者说字符串B是否是字符串A的字串。一般我们称要查找的串为模式串(Pattern),简记为P,而被查找的串,也就是可能包含模式串的串称为目标串(Target),简记为T。
严谨的定义就是:对于长度为n的目标串T[0,1,...,n-1]和长度为m的模式串P[0,1,...,m-1](显而易见,n>=m),问题是,是否存在pos使得T[pos,pos+1,...,pos+m-1]=P[0,1,...,m-1](0=<pos<=n-m)。
2、普通的暴力搜索算法
2.1 介绍
为了解决上面的问题很容易想到的一种方法就是暴力搜索,用i和j分别标记模式串和目标串中的匹配位置,从头开始比较,当出现不匹配时,要回溯到目标串中本轮匹配开始位置的下一个位置进行下一轮的匹配。比如当出现T[i]<>t[j]时,下一步要回溯到本次匹配开始的位置(也就是T[i-j])的下一个位置(也就是t[i-j+1])进行下一轮的匹配,也就是要比较T[i-j+1]和P[0]是否相等,下面是一个例子。
图1 暴力搜索算法失配时的处理
2.2 缺点
直观上说,这个方法的不科学之处是很明显的,比如上面的例子中,失配之前T[0]=P[0],而T[0]!=T[1],这里再去比较T[1]是否能与P[0]不是很逗比吗?如果你觉得这个理由不具有说服力,不妨这样想,上面的例子中,当在目标串中下标为5的位置发生失配时,实际上,我们已经知道了目标串坐标为5的位置前面的字符(因为它和模式串下标为5的位置之前的字符完全相同,如果不相同早就应该发生了失配)。这样,我们再去回到一开始的地方去开始下一轮的匹配是不科学的,因为实际上能不能匹配应该已经知道了,回到该轮匹配开始位置的下一个位置去开始下一轮匹配是最笨的方法。
3、启发
3.1 启发
图2 暴力搜索算法的启发
如上图,对于已经匹配的字符(上面的绿色阴影部分),只有找到它的相等的一对前缀和后缀,才能比较T[5]是否和模式串P中相应位置的字符相等。同时,为了避免漏掉匹配,必须找到已匹配字符的最长相等前后缀。而已经匹配的字符,既出现在目标串T中,也出现在模式串P中。这样,实际上,我们只根据模式串P就能求出当前已经匹配字符最长的相等前后缀。这样,我们将模式串右移,使相等的前缀对齐到相等的后缀,就能够接着匹配T[5]和对应位置的模式串中的字符了。
根据上面的启发,可以得出通用的规律,对于模式串P,当在j位置和目标串中的i位置发生失配时(此时,j之前的字符都能够被匹配),可以求得当前已匹配字符的最长相等前后缀,这样就能确定出模式串右移的距离,也就能确定出下一步T[i]应该和P中的第几个字符比较。这样,我们先对模式串P进行预处理,对其中的每个位置(0,...,m-1),求出当在该位置发生失配时,目标串中的字符应该和模式串中的第几个字符进行匹配,并将这些值放在数组中,这个数组通常称为next数组。
3.2 next数组
数组next[0,...,j,...,m-1]的含义是当模式串P在j位置和目标串T中的i位置发生失配时,下一步T[i]应该和模式串P中的next[j]位置处的字符进行匹配。要求next[j]的值,首先应该求出模式串P中j位置之前字符的最大相等前后缀的长度,假设求得为x,那么next[j]=x。(实际上,应该是把T[i]和最长前缀后面的那个字符进行比较,但是由于这里求出的是最大相等前后缀的长度,而字符数组P下标从零开始,所以就正好不用+1。)
为了规范并明确,下面明确一下前后缀的定义:"前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。
下面说两种特殊情况:
(1)如果P中的第一个字符P[0]就和T中的对应字符(比如说T[i])不匹配,这个时候,应该比较P[0]和T中的下一个字符T[i+1]是否匹配。这种情况正好对应next[0],这里将其记为-1。
(2)如果P中失配位置之前字符的最长相等前后缀长度为0,也就是说,根本找不到相等的前后缀,这时候,应该将模式串P中的第一个字符P[0]和目标串T中对应位置的字符进行比较。这种情况对应的相应位置next数组的值为0。
4、kmp算法
4.1 介绍
kmp算法正式基于上面的启发,当P[j]和T[i]发生失配时,不用回溯到本轮匹配开始位置的下一个位置,而可以直接比较T[i]和P[(next[i])]的值,并依次往下比较即可。如果匹配成功进行到模式串的最后,说明成功找到匹配,返回模式串所在的位置即可。
4.2 kmp算法描述
j:=0; for i:=0 to n-1 do begin while (j>=0) and (P[j]<>T[i]) do j:=next[j]; if P[j]=T[i] then j:=j+1; if j=m then begin writeln('Pattern occurs with shift ',i-m+1); j:=P[j-1]; end; end;上面最后的j:=P[j-1]是为了让程序接着进行下去,因为可能会找到多个匹配。
4.3 next数组的求解
如上面所提到的除了两种特殊的情况(实际上第二种情况算不得特殊),next[j]的含义就是模式串中j位置之前的字符的最长相等前后缀。实际上,它的求解可以采用递推的方式。比如已知next[j]的值,next[j+1]的值有两种情况,如下:
(1)如果P[ next[j]-1 +1 ] == P[ j-1 +1 ],那么很显然,next[j+1] = next[j]+1。
(2)P[ next[j]-1 +1 ] != P[ j-1 +1]的情况稍微复杂。因为next[j+1]的含义是模式串P中j+1位置之前的字符的最长相等前后缀,如果P[ next[j]-1 +1 ] != P[ j-1 +1],那么只能在模式串P的前next[j]个字符中寻找最长相等前后缀,如果找到的最长相等前后缀的长度为y,而且P[y-1 +1]==P[j-1+1],那么next[j+1]=h+1,否则,接着往前找,如果都找不到那么next[j+1]=0。
如下图:
图3 next数组的计算
next数据求解的算法描述:
P[0]:=-1; j:=0; for i:=1 to m-1 do begin while (j>=0) and (P[j]<>P[i]) do j:=P[j]; if T[j+1]=T[i] then j:=j+1; P[i]:=j; end;实际上,可以看出kmp算法和上面求next数组的算法实际是很类似的,细想一下求next数组的过程也是一个对模式串进行自我匹配的过程。
4.4 时间复杂度
为什么这个程序是O(n)的?其实,主要的争议在于,while循环使得执行次数出现了不确定因素。我们将用到时间复杂度的摊还分析中的主要策略,简单地说就是通过观察某一个变量或函数值的变化来对零散的、杂乱的、不规则的执行次数进行累计。KMP的时间复杂度分析可谓摊还分析的典型。我们从上述程序的j值入手。每一次执行while循环都会使j减小(但不能减成负的),而另外的改变j值的地方只有第五行。每次执行了这一行,j都只能加1;因此,整个过程中j最多加了n个1。于是,j最多只有n次减小的机会(j值减小的次数当然不能超过n,因为j永远是非负整数)。这告诉我们,while循环总共最多执行了n次。按照摊还分析的说法,平摊到每次for循环中后,一次for循环的复杂度为O(1)。整个过程显然是O(n)的。这样的分析对于后面next数组预处理的过程同样有效,同样可以得到预处理过程的复杂度为O(m)。
4.5 kmp的一个例子
下面图4展示了一个完整的kmp算法的过程。
图4 kmp例子
参考链接:
http://www.matrix67.com/blog/archives/115(注:这篇文章分析得非常好,这里是将其中kmp的内容按照自己的理解又写了一遍。)