要想了解KMP算法,首先我们来看这样的两个比较过程。这两个过程是基于最长公共前后缀的概念衍生出的串的模式匹配算法,读者可以先了解最长公共前后缀的概念,然后去看具体的匹配过程。(第二张图可以先不看实际上……那段话)
可以看出,KMP算法主要是以下过程的循环:
1、从比较指针开始,逐一进行元素的比较、
2、找到失配位置
3、在子串中找到失配位置的最长公共前后缀
4、将最长前缀的位置移动到最长后缀的位置;实际上根据相对运动的思想,也就是让当前主串的失配位置与字串序号为“最长公共前后缀”的位置进行比较。(这时候可以看第二张图实际上……后面的内容啦,对照前面的匹配过程加深一下理解)
5、从新的比较指针开始,在进行逐一的元素比较,也就是回到了步骤1进行循环
有些聪明的小伙伴就要问了,这样大幅度的跳过一些字符的比较是不是可能遗漏了一些可能匹配成功的字符串?实际上根据数学的反证法是不可能的,详情请见up主宇智波藤原的文章。(我把相关的证明放在下面,感谢up主宇智波藤原的辛苦付出!)
在上述过程中,很显然,找到每个失配位置的最长公共前后缀是重新开始比较的关键。(如果大家感兴趣的话这个过程叫回溯)。
然而,在实际目标串s与模式串t的比较过程中,每个位置都可能成为失配位置,而且在真正的搜索引擎当中,会有无数个目标串,但是搜索的目标只有1个模式串。同时,最长公共前后缀也只与模式串本身有关。所以,我们可以先假设每个位置都有可能成为失配位置,从而找出每个位置作为失配位置的最长公共前后缀,把结果存在一个next列表里,准备随时被调用。
那么如何用代码实现找出每个位置作为失配位置的最长公共前后缀呢?请看下图。(可以大致理解一下,不必深究,后面会讲)
根据最长公共前后缀的规定,next[0]=-1,因为0号位置没有前面的元素,没有意义,所以规定为-1。next[1]=0,因为1号位置前面只有一个元素,而最长公共前后缀的规定要求相同序列本身不能最为最长公共前后缀,所以规定为0。
由上面这个求next[8]的例子我们可以得出,只要知道了0,1号元素的next值和每一个序号的next值,就可以通过比较的方法得到每一个序号的next值。
其实所有的next值都是由next[0]、next[1]、每个序号的值衍生出来的,给大家举个例子就动啦。请看下图。
求next[i]的值的过程如下:
1、找到next[i-1]的值,然后比较t[i-1]和t[next[i-1]] 的值
2、若相等,next[i-1]=t[next[i-1]] +1
3、若不等,则继续比较t[i-1]与t[next{next[i-1]}],若要循环的话,next依次往外面加(这里有点像二分法)
这里建议大家把求next序列的手写过程自己写一遍,这个对KMP算法的实现过程非常重要,一定要理解透彻!
下面分析一下next数组构建的代码:
#这个代码非常简洁,非常美观
#获取模式串t的next数组
#这个代码建议背诵,因为里面实在是太精妙了
def GetNext(t,next):
j,k=0,-1#j用来遍历串的每一个字符,k用来标记用来比较t[k]与t[j],但是在while循环里比较的话j加过1了,所以还是后一位用前一位比较
next[0]=-1#next[0]没有意义,设置为-1,用来标记遍历完没有结果的情况,没有最长公共前后缀则next为0
while j<t.getsize()-1:#一旦j=t.getsize()-1,后面j再+1正好求得的是最后一位的next值
if k==-1 or t[j]==t[k]:#k为-1应对一开始的情况
j,k=j+1,k+1
next[j]=k#前一个语句已经加过1了,k也加过1了
else:
k=next[k]#重置k的话,再次进入循环就可以两个值再次进入比较了,如果不相等,j就不变
下面讲一下KMP算法的代码:
根据上面一开始的两张图我们可以得知,KMP算法的核心在于将元素一一比较、找到不匹配的点、讲t串模式串的比较指针移动到一个合适位置,然后通过继续比较移动s串目标串的比较指针。
具体的代码展示如下:
def KMP(s,t):
next=[None]*MaxSize#创建next存储空间
GetNext(t,next)#创建next数组
i,j=0,0#i是目标串s的指针,j是t模式串的指针,KMP算法的特点在于指针i不回溯,指针j回溯到一个合适的位置,再一一比较的过程中实现指针i的移动
while i<s.getsize() and j<t.getsize():
if j==-1 or s[i]==t[j]:#相等的话再比较下一个元素,直到找到不相等的元素;j=--1的话说明next数组此时为0,再进行下一次循环时就进行了赋值
i,j=i+1,j+1
else:
j=next[j]#j回溯到合适位置
if j>=t.getsize():#说明遍历完了(之前j加过1了)
return(i-t.getsize())#和BF算法一样,返回头位置,如果不理解看我的BF算法
else:
return -1