代码随想录算法训练营第九天 | 字符串 part 2 | KMP算法

KMP算法

思路

当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。

细节

前缀表

  • 作用:前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。

这个动画演示了在文本串aabaabaafa中寻找模式串aabaaf
动图

  • 前缀表是什么:记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。

字符串(eg. “abc”)的前缀(prefix)是指不包含最后一个字符的所有以第一个字符开头的连续子串。(eg. “a”, “ab”)

后缀(postfix)是指不包含第一个字符的所有以最后一个字符结尾的连续子串。(eg. “c”, “bc”)

找到相同前缀后缀之所以重要,是因为它允许我们匹配字符串失败之后,能够快速在模式串找到下一个应该与文本串匹配的位置。比如说下图,在文本串匹配到模式串Index 5的时候'f' != 'b',匹配失败。那这个时候,我们看到index 5之前的模式串(模式串[0:5])里面有最大相同前缀后缀:"aa",那此时模式串的指针只需要回到index 2再继续和文本串比对即可(因为已知模式串[0:2] == 模式串[3:5] == "aa")。

在这里插入图片描述

计算前缀表

(统一不减一的前缀表)参考代码:

    def getNext(self, s):
        next = [0] * len(s)
        i = 0
		
		# i是前缀末尾,j是后缀末尾
        for j in range(1, len(next)):
        	# 当s[i] != s[j],回退,注意要使用while loop,因为可以回退多次
            while i > 0 and s[i] != s[j]:
                i = next[i-1]
			
			# 当s[i] == s[j],前缀指针前进
            if s[i] == s[j]:
                i += 1
			
			# i所在位置是最长相同前缀后缀的长度
            next[j] = i
        
        return next

一段使用前缀表(Next数组)来匹配的动画:

动画2

复杂度分析

其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。

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

Leetcode

在这里插入图片描述

思路

使用KMP算法。也可以用python里面的find(),index().

代码

KMP

class Solution:
    def strStr(self, haystack: str, needle: str) -> int:

        if len(needle) > len(haystack):
            return -1

        next = self.getNext(needle)
        i = 0

        for j in range(len(haystack)):
            while i > 0 and haystack[j] != needle[i]:
                i = next[i-1]

            if haystack[j] == needle[i]:
                i += 1
            
            if i == len(needle):
                return j - len(needle) + 1
 
        return -1

    def getNext(self, s):
        next = [0] * len(s)
        i = 0

        for j in range(1, len(next)):
            while i > 0 and s[i] != s[j]:
                i = next[i-1]

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

            next[j] = i
        
        return next

Find

class Solution:
    def strStr(self, haystack: str, needle: str) -> int:
        return haystack.find(needle)

Index

class Solution:
    def strStr(self, haystack: str, needle: str) -> int:
        try:
            return haystack.index(needle)
        except ValueError:
            return -1

459.重复的子字符串

Leetcode

给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。

思路

移动匹配

具体的做法:将s_fold定义为s[1: ] + s[:-1],然后检测s_fold是否含有s就行了。如果s是由重复的子字符串组成的,那

关于移动匹配这篇发在leetcode的solution写得很好。我将这个proof复制粘贴在下方。

brianchiang_tw — simple proof

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这个proof里面,如果k = 1,那说明s是一个不由重复子字串组成的字符串(因为s = p,所以s只能被视为一个pattern。),这个时候,s_fold不含有s

如果是k >= 2, 那么说明s是由重复子字串组成。通过推导我们可以得出s_fold含有s

代码
class Solution:
    def repeatedSubstringPattern(self, s: str) -> bool:
        return s in s[1:] + s[:-1]
复杂度分析

时间复杂度:关键还是在判断s是否在s_fold里面,用KMP算法实现这一步骤可以达到O(m+n)
空间复杂度:O(1)

暴力法

s[1]s[n//2]之间遍历,判断形成的子字符串能否重复组合成s

代码
class Solution:
    def repeatedSubstringPattern(self, s: str) -> bool:
        n=len(s)
        for i in range(1,(n//2)+1):
            if s==s[:i]*(n//i):
                return True
        return False
复杂度分析

时间复杂度:O(n^2)
空间复杂度:O(1)

KMP算法

具体思路参考卡哥写的文章:

链接

具体操作为,(我使用的是不减一的前缀表)选取next数组的最后一个元素,该元素意味着最长的相同前缀和后缀的长度。只需要判断len(s) % (len(s) - next[-1]) == 0即可。

代码
class Solution:
    def repeatedSubstringPattern(self, s: str) -> bool:
        next = self.getNext(s)

        if next[-1] != 0 and len(s) % (len(s) - next[-1]) == 0:
            return True

        else:
            return False

    def getNext(self, s):
        next = [0] * len(s)
        i = 0

        for j in range(1, len(s)):
            while i > 0 and s[i] != s[j]:
                i = next[i-1]

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

            next[j] = i
        
        return next
复杂度分析

时间复杂度:O(n)
空间复杂度:O(n)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值