KMP算法及python实现

KMP算法及python实现

1. 整体思路

​ KMP算法是一种在字符串匹配中应用十分广泛、也十分高效的算法,就是查找模式串(子串)在目标串(主串)中出现的位置,具体的问题可参考leetcode “28.实现strStr()”,题面如下图所示。

在这里插入图片描述

​ 最暴力的算法就是:模式串的第0位与目标串的第0位进行比较,如果匹配,则比较模式串的第1位与目标串的第1位;如果不匹配,则将模式串整体后移1位,比较模式串的第0位与目标串的第1位。以此类推,此处便不再赘述。这种算法的时间复杂度为O(n * m),其中n、m分别为目标串和模式串的长度。

​ 而KMP算法的改进之处是:匹配过程出现字符必相等的情况时,模式串不是向后移1位,而是后移一段距离再进行比较,也许这里第一次接触KMP算法的旁友还比较疑惑,且听我细细道来~

​ 假设目标串为"GTGTGAGCTGGTGTGTGCFAA",模式串为”GTGTGCF“,如下图所示。

在这里插入图片描述

​ 首先,进行逐一比较,当匹配到第6位时,出现了“坏字符”,目标串与模式串不匹配,如下图所示。

在这里插入图片描述

​ 按照暴力做法,我们应该将模式串整体往后移1位,再进行逐一字符的比较;而KMP算法,则是直接将模式串整体往后移2位,再继续目标串当前位(仍为目标串的第5位A)与模式串相应位(模式串后移2位后,目标串的第5位对应模式串的3位T)的比较,如下图所示。

在这里插入图片描述

​ 那为什么KMP算法可以使模式串直接后移一段距离呢?因为这里面存在一个简单的逻辑:出现坏字符(即目标串的当前位)时,想要使坏字符匹配成功,则必须要求目标串当前位的前k位模式串的前k位相同时,才可能出现匹配的情况,所以可以直接移动一段距离使得这两个片段对齐。而移动的距离取决于目标串的最长可匹配后缀子串最长可匹配前缀子串的长度,如下图所示。

在这里插入图片描述

​ 那如何去找到一个字符串的最长可匹配前缀子串和最长可匹配后缀子串呢?需要每一轮都重新遍历吗?非也,用next数组就可以确定模式串移动的距离。

2. next数组

​ 在使用KMP算法进行目标串(主串)s和模式串t的匹配时,next数组是该算法的关键。依然用之前的例子,目标串s为“GTGTGAGCTGGTGTGTGCFAA",模式串t为“GTGTGCF”。

​ next[i]的值是模式串第0位到第i-1位 最长的相等的后缀子串与前缀子串的长度,这样说可能比较拗口。其实在这个案例中,当i=3时,模式串第0位到第i-1位为GTG,则最长的相等的后缀子串与前缀子串为G,next[i]=1;当i=4时,模式串第0位到第i-1位为GTGT,则最长的相等的后缀子串与前缀子串为GT,next[i]=2;当i=5时,模式串第0位到第i-1位为GTGTG,则最长的相等的后缀子串与前缀子串为GTG,next[i]=3;当i=5时,模式串第0位到第i-1位为GTGTGC,不存在相等的后缀子串与前缀子串,next[i]=0。

模式串字符 G T G T G C F
next数组下标i 0 1 2 3 4 5 6
数组元素值next[i] 0 0 0 1 2 3 0

​ next数组本质上可以理解当模式串的第i位目标串的当前位不匹配时,可以直接用模式串第next[i]位去和目标串的当前位进行匹配。以下图为例:

在这里插入图片描述

​ 模式串的第5位C与目标串的当前位A不匹配时,直接用模式串的第next[5]位(即第3位)T与目标串的当前位A进行匹配,相当于模式串整体后移了2位,如下图所示。

在这里插入图片描述

​ 之后同理,模式串的第3位T与目标串的当前位A任然不匹配,直接用模式串的第next[3]位(即第1位)T与目标串的当前位A进行匹配,相当于模式串整体又后移了2位,如下图所示。

在这里插入图片描述

​ 之后的步骤依次类推。那next数组又该如何去生成呢?

​ 可以采用类似“动态规划”的方法,用i表示待填充的next数组下标,用j表示将要填入next[i]的值,即最长可匹配前缀子串的下一个位置。

​ 当i=0,j=0时,next[0]=j=0。

​ 然后让i加1,当i=1时,不存在最长可匹配前缀子串,则next[1]=j=0。

​ 继续让i加1,当i=2时,模式串中pattern[i-1] != pattern[j],说明最长可匹配前缀子串仍然不存在,next[2]=j=0。

​ 继续让i加1,当i=3时,模式串中pattern[i-1] == pattern[j],则j=j+1=1,next[3]=j=1。

​ 继续让i加1,当i=4时,模式串中pattern[i-1] == pattern[j],则j=j+1=2,next[4]=j=2。

​ 继续让i加1,当i=5时,模式串中pattern[i-1] == pattern[j],则j=j+1=3,next[5]=j=3。

​ 继续让i加1,当i=6时,模式串中pattern[i-1] != pattern[j],这里要详细分析:pattern[5]和pattern[3]不相等了,即最长可匹配前缀子串被中断了,j应该要回溯到之前的某一个值,那么要返回到什么值呢?根据之前所说的匹配要符合的原则,j就应该返回到模式串第j位前的最长可匹配前缀子串的下一位,即j=next[j]。在本案例中,当i=6,j=3时,模式串的第0到j=3位“GTGT”和模式串的第i-j-1=2到i-1=5位“GTGC”不匹配了,那么应该使j=next[j]=1,用模式串的第0到j=1位“GT”尝试去和模式串的第i-j-1=4到i-1=5位“GC”进行匹配。但是回溯后,pattern[j] != pattern[i-1],则应该再次回溯使j=next[j]=0,但pattern[j] != pattern[i-1],可是无法回溯了,就使得next[6]=j=0。(这一块的确比较烧脑,如果没看懂,可以多看几遍)

3. python代码实现

​ 以下是实现leetcode 28.实现strStr()的代码,需要注意的是当模式串为空字符串时,应该输出0。该算法的时间复杂度为O(n + m),其中n、m分别为目标串和模式串的长度。

class Solution:
    def strStr(self, hayback: str, needle: str) -> int:
        if needle == "":
            return 0
        next = self.getNext(needle)
        i, n, j, m = 0, len(hayback), 0, len(needle)
        while i < n and j < m:
            while j > 0 and hayback[i] != needle[j]:
                j = next[j]
            if hayback[i] == needle[j]:
                j += 1
            i += 1
            if j == m:
                return i - j
        return -1

    def getNext(self, needle):
        i, j, l = 2, 0, len(needle)
        next = [0] * l
        while i < l:
            while j > 0 and needle[i-1] != needle[j]:
                j = next[j]
            if needle[i-1] == needle[j]:
                j += 1
            next[i] = j
            i += 1
        return next

以上部分内容引自https://baijiahao.baidu.com/s?id=1659735837100760934&wfr=spider&for=pc

猜你喜欢

转载自blog.csdn.net/NickHan_cs/article/details/107663880
今日推荐