kmp算法next计算方法_KMP算法详解

KMP算法作为一个效率比较高的模式匹配算法被广泛使用,但是KMP算法的效率究竟高在哪呢?

首先我们观察朴素的模式匹配算法:

我们使用text代指源字符串,pattern代指模式串

若text=aaaacdeaaab pattern=aaab

匹配过程如下:

很明显在失配后i需要回到开始点重新进行匹配,因此总的匹配时间复杂度为O(nm)


我们再观察另一个例子:

可以观察到,按照朴素方法,当i=6且j=6时会发生失配(此时text中的匹配开始点为0),此时按照一般思路,就会让i变成1,j变成0重新开始匹配

当前匹配状态:

但是我们可以观察到一点:pattern以索引5为结尾的后缀“abc”与pattern的前缀“abc”一致,同时由于在pattern中到6的位置才适配,所以3~5肯定已经和text中的i指示的当前位置的前3个字符匹配,因此我们没必要从头开始匹配,显然可以调整j为3,从pattern索引为3的字符处开始比较,也就是如下图所示:

在这种情况下,通过判断前缀后缀匹配关系,从而节省了很多次无用的比较,因此相对于朴素算法而言,这种情况匹配更快一些


我们再看另外一种情况:

此时,a与b不一致从而失配(就是无法匹配)

此时pattern没有重复前缀,那么该怎么办呢?

很简单,直接拿pattern[0]和text[1]比较,此时也可以看做拥有空的重复前缀

从而变成下面这样:

由于匹配成功,紧接着i和j后移,可以得到最后的结果(即匹配成功):


我们再来看一种情况:

这个例子,很明显,根据上面那个例子可以知道,此时,pattern将滑动成这样:

显然这时候text[1]和pattern[0]依然不一致,那么接下来又该如何做呢?

显然,由于pattern[0]之前已经没有可用于比对的字符了,也就是text[1]无法作为开始点,所以应该将i加一,从而变成如下的情况:

最终匹配完成


我们归纳一下子串匹配的方法:

若当前两个字符相同,则推进i,j

若当前两个字符不同,且j>0,则i保持不变,j跳回pattern前缀的下一个位置

若当前两个字符不同,且j=0,则推进i

上面的方法依赖于前缀的查找,那么,我们该如何快速的找到前缀呢?

答案是构建一个叫做next的数组,数组第i项表示匹配到pattern第i+1个字符时如果失配时的pattern后缀对应pattern前缀的下一个字符的索引

例如对于

对应next数组如下:

显然,若匹配到索引为1的字符时失配,此时的后缀只有“a”和ε(希腊字母epsilon,这里表示空串),这里要注意,前缀不能是后缀(也就是说,两个字符串不能内容相等的同时在pattern中的起始点也相同),否则j就无法向前跳(对于上述pattern来说,next[0] = 1,这样会形成一个死循环,j没有发生移动),所以在pattern中不存在前缀“a”,仅存在前缀ε,因此next[0]=0(ε的后一个字符的索引当然是0)

若匹配到索引为2的字符时失配,与上面同理,所以next[1] = 0

若匹配到索引为3的字符时失配,显然,此时存在四个后缀“a”“ba”“aba” ε,而pattern在字符3之前存在前缀“a”“ab”“aba” ε,排除掉一样的"aba",就剩"a"和ε了(事实上,上述两个ε的位置并不一样,一个在索引0之前,另一个在索引2与3之间)

本着前缀最长的原则(因为这时候可以保持最大的已匹配状态,减少匹配次数),显然选择前缀"a",那么,下一个匹配字符就必定是pattern[1]了,因此next[2] = 1

那么如果索引为0的字符失配怎么办?这个问题在讲子串匹配的方法的时候就提到了:若当前两个字符不同,且j=0,则推进i


next数组的定义讲完了,那么该如何求出next数组呢?

事实上可以通过pattern与自身匹配的方法来实现:

此次失配,同时由于失配的并不是pattern的第一个字符,此时相同的前缀和后缀仅为 ε,因此若j+1个字符失配,必须跳到j=0时进行比较,即next[j] = 0

此时我们相当于找到了一个相同的前缀和后缀(“a”与“a”),根据next数组定义,若第j+1个字符失配,则应跳回相同前缀的后面一个字符,因此,显然next[i] = j + 1(后缀由i-j开始由i结束,前缀由j结束,此时i=2,j=0)

此时我们相当于找到了一个相同的前缀和后缀(“ab”与“ab”),根据next数组定义,若第j+1个字符失配,则应跳回相同前缀的后面一个字符,因此,显然next[i] = j + 1(后缀由i-j开始由i结束,前缀由j结束,此时i=3,j=1)


我们依据上述的方法可以获得next数组,时间复杂度为O(n),同时利用next数组进行查找时间复杂度为O(m),因此总共的子串查找时间复杂度为O(n + m),额外的空间复杂度为O(n)

那么,进行查找的时间复杂度是否真的是O(m)?

我们观察下面一个pattern:

那么,在匹配时,若在任意一个字符处失配,都会回退一次(回退到j=0的情况,除了j=0时),假若在text中除了最后四个字符是abcd,前面都是ax的形式,那么总回退次数显然是

,总比较次数显然是
(源字符串每个字符都要比较一次,回退额外比较一次)

我们再观察下面一个pattern:

如果匹配时每次都在pattern的最后一个字符失配,每次失配都会产生9次回退,那么会不会让KMP算法的查找时间复杂度从O(m)降到O(mn)呢?

我们拥有这样一个事实:pattern可以回退正是因为pattern曾经推进,而pattern的推进是由于text的推进,因此总pattern的回退次数不会超过m

即总比较次数不会超过2m,该结论也可以应用到next数组的构建算法,即构建算法次数不会超过2n,因此KMP的总比较次数(包括next构建和子串比较)不会超过2n+2m次

因此KMP的时间复杂度确实是O(n+m)

实际上由上述说明还可以获知:字符串比较次数的增多正是由于pattern中存在较多的重复子串,因此对于同样的text而言,无重复字符的pattern在KMP算法中是最快的,这种子串相对于朴素算法的提速是最明显的

这里提供到leetcode上的相关题目以及我的实现代码:

力扣​leetcode-cn.com
class 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值