KMP算法

什么是KMP算法?

有句话可以这么形容KMP:一个人能走的多远不在于他在顺境时能走的多快,而在于 他在逆境时多久能找到曾经的自己。

KMP算法是一个字符串匹配算法,取得是三个发明人的名字首字母。KMP算法的作用 是在一个已知字符串中查找子串的位置,也叫做串的模式匹配,后文主串和模式串匹配, 子串和模板串匹配。

暴力做法

KMP可以解决串的模式匹配,但是一般我们解决这个问题首先想到的是暴力做,什么 也不管,直接两个for循环。暴力匹配也叫朴素的模式匹配。

从主串和子串的第一个字符开始,将两字符串的字符一一比对,如果出现某个字符 不匹配,主串回溯到第二个字符,子串回溯到第一个字符再进行一一比对。如果出 现某个字符不匹配,主串回溯到第三个字符,子串回溯到第一个字符再进行一一比对… 一直到子串字符全部匹配成功。最坏的情况下时间复杂度是O(m*n),这样不停的回溯速 度会非常慢,当数据非常大工程量也会非常大。

所以这时候就要想想怎么优化了,再想一下暴力做法,一直在回溯,回溯的步骤太多了, 所以能不能找到一种规律减少回溯的次数,这就是KMP算法

KMP算法

KMP算法一种改进的模式匹配算法,是D.E.Knuth、V.R.Pratt、J.H.Morris于1977年联合 发表,KMP算法又称克努特-莫里斯-普拉特操作。

它的改进在于:每当从某个起始位置i开始一趟比较后,在匹配过程中出现失配,不 回溯到i+1,而是利用已经得到的部分匹配结果,将一种假想的位置定位“指针”在模式上 向右滑动一段距离到某个位置后,继续按规则进行下一次的比较。

比如当我们在主串下标为3匹配子串,往后继续匹配主串,在下标是10的位置主串和 子串不匹配,这个时候就要把子串往后移动到首字母相同的位置继续匹配,但其实我 们中间已经匹配了很多字符了,里面是有一些额外信息在里面的。我们利用这些额外 信息就可以帮我们少枚举一些东西。

还是以上面为例,当我们在下标10主串和子串不匹配,我们要从3往后面找和子串首 字母相同的位置然后继续匹配,如果这个位置在10前面的话,我们可以发现这个位置 到10的距离和第一次匹配的子串有重合的部分,我们假设这个位置是6

https://img-blog.csdnimg.cn/1c61cc20fac449f78624fa4b59206e0f.png

我们可以很明显的发现6-10的数组在第一次匹配的子串和第二次匹配的子串出现重合 的情况,也就是在子串中间和前面是重合的,所以我们只要将子串直接移动到下标为6 的位置就行(利用第一次比较信息3到10),这样我们就减少了枚举的次数。所以我们只要求出子串往后移动多少位 可以出现重合即可

这个问题只和我们的子串也就是模板串有关系,如果我们可以预处理出来这种子串的中 间(3到10)和开头(3到6之间的一段(包头不一定包尾))有一段距离相同,我们求出这一段的最大值就行,直接从开头的重复部位移动到中间的重复部位。 这就是一个子串重复的前缀、后缀、也就是next数组,next数组就是当前子串长度下重复前后缀的长度。

所以要给我们的子串,也就是模板串的每个点进行预处理,以某个点为终点的后缀和前 缀相等,相同的最大长度是多少,这个就是KMP算法中最难的next数组的含义。

next[i]表示的就是以i为终点的后缀和从1开始的前缀相等,且相同的部分最长,这里 我们默认子串下标从1开始。比如next[i]=j就表示在子串中p[1,j]=p[i-j+1,i],这里我们 用p数组暂时表示子串,这个就表示子串中下标从1到j这一段和i-j+1到i是相等的, 而且长度最长。所以下次从j+1再开始继续匹配。

例如:

模式串abab

是next[4]=2     因为是四位所以是next[4],下标是从1开始,最大公共相同前缀后缀是ab

所以值是2

I=4 j=2

P[1,j]=p[i-j+1,i]  -> p[1,2]=p[4-2+1.4]    枚举

理解:p[从1到最大公共前缀后缀的长度(就是前缀部分)]=p[模式串的长度-最大公共前缀后缀长度+1(加一是为了得到后缀起始下标,i(模式串长即为后缀结束下标)]

验证:模式串abcab  最大公共前缀后缀为ab

p[从1到最大公共前缀后缀的长度(就是前缀部分)(1,2)]=p[模式串的长度(5)-最大公共前缀后缀长度(2)+1(加一是为了得到后缀起始下标,i(模式串长即为后缀结束下标)(5)]

next数组(子串)

next 数组的值是除当前字符外(注意不包括当前字符)的公共前后缀最长长度

求next数组最重要的一点是找最长公共前后缀,什么是前后缀呢

前缀是除了最后一个字符的所有子串。

后缀是除了第一个字符的所有子串。

如图

https://img-blog.csdnimg.cn/41bb9a5c79cd4add9dde17c4eeb47e39.png

举个栗子

比如子串是ababababab,我们求他的next数组,子串下标从1开始

很明显next[1]=0,因为第一个默认是0

next[2]=0,因为没有公共前后缀

next[3]=1,最长公共前后缀是a

next[4]=2,最长公共前后缀是ab

Next[5]=3,最长公共前后缀是aba,依次类推next[6]=4.....

我们可以发现next数组的值就是子串退回时的下标

动画演示

我们可以看一下暴力做法和KMP做法的一个区别

https://img-blog.csdnimg.cn/92f4d32bb78040cbaa6b01320890209f.gif

Kmp算法的基本思路:

当我们发现某一个字符不匹配的时候,由于已经知道之前遍历过的字符(ABAB)

那能不能利用这些信息来避免暴力算法中”回溯”的步骤呢

换句话说我们不希望递减上面的这个指针(主串A)

这时候我们知道前面都遍历过哪些字符(主串ABAB、子串ABAB,就是用子串的后缀去对应主串的前缀)

就可以将子串移动到下图这个位置

由于这里的AB和主串中的AB是相同的

我们完全可以跳过它们,避免重复对比,接下来只需要继续测试后面的字符就好了

这里就要用到KMP中定义的next数组了

我们先不管next数组是怎么生成的

先来看一下它的功能和用途

KMP算法在匹配失败的时候

会去看最后一个匹配的字符它所对应的next数值

比如这里是2

于是我们移动子串(注意我们这里跳过的是子串的AB不是主串)

这里的2代表子串中我们可以”跳过匹配”的字符个数

也就是说前面的这两个AB就不需要看了

直接从下一个字符接着匹配

很显然这样是没有问题的

因为跳过的这两个AB确实能够与主串中的AB匹配上

所以我们只需要继续测试后面的字符就好了

由于不再需要回退主串中的指针

只需要一次主串的遍历就可以完成匹配

效率自然比之前的暴力算法高高很多

接下來我们来看一下KMP算法的程序实现

def kmp_search(string,patt):
    next=build_next(patt)         #假设我们已经1算出了next数组(马上讲到)

    i=0   # 主串中的指针
    j=0   # 子串中的指针

    while i<len(string):
        if string[i]==patt[j]:    # 字段匹配、指针后移
            i+=1
            j+=1
        elif j==0:
            i+=1
        else:
            j=next[j-1]

        if j==len(patt):     # 匹配成功
            return i-j

下面我们还是以ABABC为例来说明一下next数组的计算

首先对于第一个字符

显然不存在比它短的前后缀

所以next直接为0

接着对于前两个字符

同样没有相同的前后缀

所以next为0

所以对于前三个字符

由于A是共同的前后缀

所以next为1

对于前四个字符

由于AB是共同的前后缀

Next为2

对于前五个字符

同样找不到共同的前后缀

所以next为0

这样我们就可以计算得到了整个next数组

但计算应该怎么写呢

我们可以当然for循环暴力求解

但效率极低

其实这里可以采用一种递推的方式来快速求解next数组

它的巧妙之处在于不断利用已经掌握的信息来避免重复的运算

假设我们已经知道当前的共同前后缀了

接下來分两种情况讨论

如果下一个字符依然相同的话

不就是直接构成一个更长的前后缀吗

很明显它的长度等于之前的加上一

但如果下一个字符不同的话

又应该怎么办呢

既然ABA无法与下一个字符构成更长的前后缀

我们就看看其中存不存在更短的(第7个对应的A是为对应到第一个A、串ABACABAB看前缀A和后缀A)

比如这里的A

它其实是有可能与下一个字符构成共同前后缀的

这一步难道要暴力求解吗

根据之前的计算我们掌握了一个重要信息

哪就是子串前后的这俩部分是完全相同的

也就是说右面这部分的后缀其实等同于左边这部分的后缀

哪我们直接在左边寻找共同的前后缀不就好了吗

而左边的前后缀我们之前已经计算过了

直接查表就可以得到它的长度是1

于是我们又回到了最开始的步骤

检查下一个字符是否相同

如果相同,则可以构成一个更长的前后缀

长度加一即可

在我们掌握逻辑原理之后,代码的实现就很简单了

def build_next(patt):     #计算 Next 数组
    next=[0]              #next数组(初值元素一个0)
    prefix_len=0          #当前共同前后缀的长度
    i=1

    while i<len(patt):
        if patt[prefix_len]==patt[i]:
            prefix_len+=1
            i+=1
            next.append(prefix_len)
        elif prefix_len>0:
            prefix_len=next[prefix_len-1]
        else:                   #没有公共前后缀
            prefix_len=0
            next.append(prefix_len)
            i+=1
    return next

整个kmp的算法代码如下:

def kmp_search(string,patt):
    next=build_next(patt)         #假设我们已经1算出了next数组(马上讲到)

    i=0   # 主串中的指针
    j=0   # 子串中的指针

    while i<len(string):
        if string[i]==patt[j]:    # 字段匹配、指针后移
            i+=1
            j+=1
        elif j==0:
            i+=1
        else:
            j=next[j-1]

        if j==len(patt):     # 匹配成功
            return i-j


def build_next(patt):     #计算 Next 数组
    next=[0]              #next数组(初值元素一个0)
    prefix_len=0          #当前共同前后缀的长度
    i=1

    while i<len(patt):
        if patt[prefix_len]==patt[i]:
            prefix_len+=1
            i+=1
            next.append(prefix_len)
        elif prefix_len>0:
            prefix_len=next[prefix_len-1]
        else:                   #没有公共前后缀
            prefix_len=0
            next.append(prefix_len)
            i+=1
    return next




string=["A","B","A","B","A","B","C","A","A"]
patt=["A","B","A","B","C"]
print(kmp_search(string,patt))
print(build_next(patt))

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值