KMP算法总结

LeetCode题目

  • 28.找出字符串中第一个匹配项的下标

简介

  • 要在文本串 text 中找出第一次出现模式串 pattern 的下标,即文本查找,可以用KMP算法
  • 先看一个例子:
   text: aabaabaafa
pattern: aabaaf
  • 如果要进行暴力匹配,可以用双指针法
class Solution:
    def strStr(self, text: str, pattern: str) -> int:
        """
        双指针法, t_idx=0, p_idx=0
        - 当 t_idx<len(text), 且 p_idx<len(pattern)
            - 若 text[t_idx] 不等于 pattern[p_idx]
                - t_idx-=p_idx, p_idx=0 (从头开始匹配)
            - 否则
                - p_idx+=1 (从下一位继续匹配)
            - t_idx+=1
        - 若 p_idx==len(pattern)
            - 返回 t_idx-len(pattern)
        - 返回 -1
        """
        t_idx = 0
        p_idx = 0
        while t_idx < len(text) and p_idx < len(pattern):
            if text[t_idx] != pattern[p_idx]:
                t_idx -= p_idx
                p_idx = 0
            else:
                p_idx += 1

            t_idx += 1

        if len(pattern) == p_idx:
            return t_idx - len(pattern)

        return -1
  • 暴力匹配的最坏时间复杂度是 O(n*m),主要问题是在于每次text[t_idx] 不等于 pattern[p_idx](失配)时,text 和 pattern 都要从头开始匹配
  • KMP算法可以在每次失配时,利用前面已经匹配好的部分,不必从头开始

KMP算法

  • 观察例子,从头开始,当匹配到 pattern 的最后一位时,f!=b,但是 f 前面的 aa 被匹配上,所以下一次可以从 b 开始匹配
   text: aabaacaabaafa
pattern: aabaaf
  • text 也不需要回退,直接从第 6 位的 c 开始,和 pattern 的第 3 位 b 继续匹配
  • 上述操作中,在匹配 pattern 的最后一位时,发生失配,因为前面已经匹配的 aabaa,以 aa 开头,aa 结尾,所以从第 3 位 b 继续匹配,如果还是失配,继续查看前面已经匹配的 aa,以 a 开头,a 结尾,所以从第 2 位 a 继续匹配,如果还是失配,则只能从头开始
  • 如何决定当前位失配后,应该从第几位继续匹配呢,其实这是在寻找已匹配部分的“最大相等前后缀”,如果得到 pattern 每一位的最大相等前后缀(即从头开始,到该位前一位的子串的最大相等前后缀),则可以在每次失配后,快速决定从哪一位继续匹配

最大相等前后缀

  • 字符串的前缀:不包括最后一位的连续子串,记为 prefix
  • 字符串的后缀:不包括第一位的连续子串,记为 suffix
  • 将最大相等前后缀保存为一个数组,记为 next
  • 对 pattern 的每一位
    • a: 显然 next[0]=0
    • aa: 以 a 开头,以 a 结尾,next[1]=1
    • aab: 以 a 开头,以 b 结尾,不存在相等前后缀,next[2]=0
    • aaba: 最大相等前后缀为 a,next[3]=1
    • aabaa: 最大相等前后缀为 aa,next[4]=2
    • aabaaf: 不存在相等前后缀,next[5]=0
  • 接下来可以利用 next 数组和双指针法,再做一次
class Solution:
    def strStr(self, text: str, pattern: str) -> int:
        """
        KMP 算法
        - 先计算前缀表 next
        双指针法, t_idx=0, p_idx=0
        - 当 t_idx<len(text), 且 p_idx<len(pattern)
            - 当 p_idx>0, 且 text[t_idx]!=pattern[p_idx]
                - p_idx=next[p_idx-1]
            - 若 text[t_idx]==pattern[p_idx]
                - p_idx+=1
            - t_idx+=1
        - 若 p_idx==len(pattern)
            - 返回 t_idx-len(pattern)
        - 返回 -1
        """
        next_ = self.get_next(pattern)
        t_idx = 0
        p_idx = 0
        while t_idx < len(text) and p_idx < len(pattern):
            while p_idx > 0 and text[t_idx] != pattern[p_idx]:
                p_idx = next_[p_idx - 1]

            if text[t_idx] == pattern[p_idx]:
                p_idx += 1

            t_idx += 1

        if len(pattern) == p_idx:
            return t_idx - len(pattern)

        return -1
  • next 数组的计算,类似自己对自己进行 KMP
def get_next(self, pattern):
    """
    类似自己对自己进行 KMP
    """
    next_ = [0]
    j = next_[0]
    for i in range(1, len(pattern)):
        while j > 0 and pattern[i] != pattern[j]:
            j = next_[j - 1]

        if pattern[i] == pattern[j]:
            j += 1

        next_.append(j)

    return next_
  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值