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数组)来匹配的动画:
复杂度分析
其中
n
为文本串长度,m
为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n)
,之前还要单独生成next
数组,时间复杂度是O(m)
。所以整个KMP算法的时间复杂度是O(n+m)
的。
28. 找出字符串中第一个匹配项的下标
思路
使用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.重复的子字符串
给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。
思路
移动匹配
具体的做法:将s_fold
定义为s[1: ] + s[:-1]
,然后检测s_fold
是否含有s
就行了。如果s
是由重复的子字符串组成的,那
关于移动匹配这篇发在leetcode的solution写得很好。我将这个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)