【讲●解】KMP算法

术语与规定#

为了待会方便,所以不得不做一些看起来很拖沓的术语,但这些规定能让我们更好地理解KMP甚至AC自动机。

字符串匹配形式化定义如下:
假设文本是一个长度为n的数组T[1...n],而模式是一个长度为m的数组P[1...m],其中m<=n,进一步假设构成PT的元素都是来自一个有限字母集Σ的字符。例如:Σ={0,1}或者Σ={a,b,...,z}。字符数组通常称为字符串。

我们用Σ表示所有有限长度字符串的集合,该字符串由字母表Σ中的字符组成。特别地,长度为0的空字符用ε表示,也属于Σ。一个字符串x的长度用|x|表示。两个字符串xy连结xy来表示,长度为|x|+|y|,由x的字符串后接y的字符串构成。

图一

如果0<=s<=nm,并且T[s+1...s+m]=P[1...m](即如果T[s+j]=P[j],其中1<=j<=m),那么称模式P在文本T中出现,且偏移为s。如果PT中以偏移s存在,那么称s是有效偏移;否则,称它为无效偏移。如图一,s=3就是一个有效位移。根据此约定,字符串匹配问题就可以变成:对于模式串P找出所有文本串T的有效偏移

如果对于某个字符串yΣx=wy,则称字符串w是字符串x前缀,记作wx。类似的,我们也可以定义后缀符号:若x=yw,则称字符串w是字符串x后缀,记作wx。可以看出:如果wx,则|w|<=|x|。特别地,空字符串ε同时是任何一个字符串的前缀和后缀。

(想一想我们为什么要引入符号。)

如果不做特殊说明,我们约定认为a为一个字符,即长度为1的字符串。

为了使符号简洁,我们把模式P的由前k个字符组成的前缀P[1...k]记作PkPm=P,采用这种记号,我们又能够把字符串匹配问题表述成:
找到所有偏移s(0<=s<=nm),使得PTs+m

首先,我们考虑朴素算法的操作过程#

图二

图一展示了一个针对文本串T模式串P的的一个特定位移s。它已经匹配到了Pq,在q+1的位置与文本串T失配

按照朴素算法的操作,我们这时应进行s=s+1q=1的操作,把文本串的匹配指针左移到s+1位,模式串P匹配指针移回1位,从头开始匹配。可这样时间开销是个很大的问题。

那怎么办呢?

我们能不能不把文本串的指针向左移,而直接把模式串的匹配指针对准下一个可能的匹配位置上,即只移动模式串P呢?

答案是可以的。

别忘了我们已经匹配好了Pq,这意味着我们已经知道了T[s+1...s+q]=P[1...q],如果能把这东西给利用起来那该多好啊!

怎么用呢?

于是,KMP算法就来了。

KMP主体#

还是用图二。

图二

q=5个字符已经匹配成功的信息确定了相应的文本字符。已知的这p个文本字符使我们能够立即确认某些偏移一定是无效的。就比如上面所说的s=s+1

KMP算法思想便是利用已经匹配上的字符信息,使得模式串的指针回退的字符位置能将主串与模式串已经匹配上的字符结构重新对齐

什么意思?

假设我们存在这样一个映射函数,先把它理解成一个小黑盒。当我们在模式串q+1的位置上失配时,它能跳到P串的某一位置k(注意是P串),即k=next[q]使得Pk与先前已匹配的q个字符的文本串不发生冲突,然后再比较k+1的位置是否与当前文本串指针匹配,如果不能,那继续找next[k];如果能,那就成功匹配一位,进行下一位的匹配。这样,文本串的指针只会向右移而不会向左移。那么这个匹配程序就很好实现了。

这里直接给出伪代码:

Copy
KMP-MATCHER(T,P) n = T.length m = P.length next = COMPUTE-PREFIX-FUNCTION(P)//这里的next就是那个小黑盒 q = 0 for i=1 to n while q>0 and P[q+1]!=T[i] q = next[q] if P[q+1] == T[i] q = q+1 if q == m print "Pattern occurs with shift" i-m q = next[q]//匹配成功后肯定要往回走啊

这就是KMP算法的主体!

(仔细回味下)

那我们怎么求这个next[q]呢?

我们来观察它的性质。

图三

图四

如图三,根据q=5个字符已经完全匹配,那么图中的PkT[s+1...s+q],且k是满足此条件的最大值,我们直接可以从P[k+1]开始与文本串匹配。也就是说,这里的k就是我们要找的next[q]
在图四中,我们把PqPk单独拿了出来,你发现了什么?

PkPq

可以看出k是满足条件的最大值,也就是说:

next[q]=max{k:k<qPkPq}

为什么会是这样呢?

我们想要直接在k+1位开始匹配,就得保证PkT[s+1...s+q],虽然我们在q+1位失配,但我们已经知道了Pq=T[s+1...s+q],所以即有PkPq

那为什么我们会要求k为满足条件的最大值呢?
这里先简单理解,k为最大值也就包含了k为更小值的情况

那么,这个next我们就把它视为P前缀函数

那么,怎么来求这个next呢?

首先,我们肯定能想到一种朴素算法,这里就不细说了,因为用朴素算法还不如敲个O((nm+1)m)的匹配算法呢。。。

那我们怎么来优化求法呢?

同样假设,对于一个模式串P,我们已经知道了next[1...q1],现在,我们来计算next[q]。其中,next[q1]=k

引理1:当P[k+1]=P[q]时,可得next[q]=k+1。(前缀函数延续性引理)#

证明:
因为:next[q1]=kPkPq1
若字符P[k+1]=P[q],则Pk+1Pq
所以:next[q]=k+1
证毕。

引理2:若next[q1]的最大候选项为k,即next[q1]=k,则它的次大候选项为next[k],次次大为next[next[k]]......(前缀函数迭代引理)#

证明:(反证法)
若存在k0使得Pk0Pq1next[k]<k0<k
因为:next[q1]=k,即PkPq1
又因为:k0<k
所以:Pk0Pk
即:next[k]=k0
这与next[k]<k0矛盾。
所以假设不成立。
证毕。
后面的依次类推。

由前两个引理可以看出,next[q]可能的候选项为:next[q1]+1next[next[q1]]+1......而易知,next[1]=0

图五:模式串P为ababaca时前缀函数的值

于是,我们便可以高效计算next数组。

Copy
COMPUTE-PREFIX-FUNCTION(P) m = P.length pi[1] = 0 k = 0 for q=2 to m while k>0 and P[k+1]!=P[q] k = pi[k] if P[k+1] == P[q] k = k+1 pi[q] = k

是不是和KMP-MATCHER很像?

其实,实质上,KMP算法求前缀函数的过程就是模式串的自我匹配

为什么我们先说KMP算法的主体再谈next的计算?其实这是从两种角度出发认识KMP。讲解主体的时候我们采用了假设法,这是一种十分感性的认知,比较好懂。在讲解next的计算时,我们引用了一些数学思维来帮助我们更加理解KMP。大家可以看出,实质上,KMP主体和next的计算是几乎一样的逻辑。

至此,KMP算法原理的讲解到此结束。

参考文献#

posted @   SilentEAG  阅读(712)  评论(2编辑  收藏  举报
编辑推荐:
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示
CONTENTS