KMP刷题笔记
用python3实现
题都来源于LeetCode
做到需要用KMP的还会继续更新,欢迎收藏
如果有错误,欢迎评论或私信告诉我
214.最短回文串
给定一个字符串 s,你可以通过在字符串前面添加字符将其转换为回文串。找到并返回可以用这种方式转换的最短回文串。
先使用暴力解法,看看如何做。
暴力解法
首先分析,如果s本身就是一个回文串,则直接return即可,反之如果s本身不是回文串,则将s反转后在加到s的头部,一定能形成一个回文串。
但题目要求的是最短的回文串,所以肯定不能直接拼接后返回。在s中,如果含有回文的部分,则在反转后,该部分仍然不变,如果该部分在s的头部,则在反转s后,拼接到s的头部的部分就会产生重复,如下:
为了最短,则要去掉反转后的尾部重复的部分,即反转后的尾部与s的头部的字符对比,相同则去掉,最后的结果追加到s的头部。下面是代码
Code
class Solution:
def shortestPalindrome(self, s: str) -> str:
n = len(s)
if n <= 1:
return s
rev = s[::-1]
if rev == s:
return s
for i in range(n-1, -1, -1):
# s的头对比rev的尾,去掉重复的部分
if s[0: i] == rev[n-i:n]:
return rev[0:n-i] + s
通过暴力解法,我们发现,只是为了去除s的头部和rev的尾部相同的字符,那不就相当于公共前后缀吗?即将rev尾部作为后缀,s头部作为前缀,两个之间相互匹配,所以使用KMP算法求最长公共前后缀。
KMP
Code
class Solution:
def shortestPalindrome(self, s: str) -> str:
n = len(s)
if n <= 1:
return s
if s[::-1] == s:
return s
# ne的大小为反转字符串加主串与一个分割字符
N = n + n + 1
ne = [0 for i in range(N)]
# 将主串与反转后的字符串组合为一个字符串,用#分割,说明用#前的去匹配#后的,求#后的next数组
string = (s + '#' + s[::-1])
# KMP中求解next数组其实也就是自己和自己匹配
# 下面也可以作为KMP算法的模版
j = 0
for i in range(1, n + 1 + n):
while j > 0 and string[i] != string[j]:
j = ne[j-1]
if string[i] == string[j]:
j += 1
ne[i] = j
# 最后返回 反转后的去除对应前缀的s 与 s 的和
return (s[ne[len(string)-1]: n])[::-1] + s
28. 找出字符串中第一个匹配项的下标
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。
暴力解法
直接一个个遍历haystack每个字符与后面 l e n ( n e e d l e ) len(needle) len(needle)个,判断是否与needle相同,相同直接return当前下标i即可,反之遍历完return -1.
Code
class Solution:
def strStr(self, haystack: str, needle: str) -> int:
n = len(haystack)
m = len(needle)
# 因为是
for i in range(n - m + 1):
if haystack[i:i+m] == needle:
return i
return -1
KMP
直接套模板就好,先求needle的next数组,再用next数组去遍历needle,与haystack匹配。
Code
class Solution:
def strStr(self, haystack: str, needle: str) -> int:
n = len(haystack)
m = len(needle)
ne = [0] * 1010 # next数组
# 求解next数组
j = 0
for i in range(1, m):
while j > 0 and needle[i] != needle[j]:
j = ne[j - 1]
if needle[i] == needle[j]:
j += 1
ne[i] = j
# 匹配
j = 0
for i in range(n):
while j > 0 and haystack[i] != needle[j]:
j = ne[j - 1]
if haystack[i] == needle[j]:
j += 1
if j == m:
return i - m + 1
return -1
100186. 匹配模式数组的子数组数目 I
给你一个下标从 0 开始长度为 n 的整数数组 nums ,和一个下标从 0 开始长度为 m 的整数数组 pattern ,pattern 数组只包含整数 -1 ,0 和 1 。
大小为 m + 1 的子数组nums[i…j] 如果对于每个元素 pattern[k] 都满足以下条件,那么我们说这个子数组匹配模式数组 pattern :
如果 pattern[k] == 1 ,那么 nums[i + k + 1] > nums[i + k]
如果 pattern[k] == 0 ,那么 nums[i + k + 1] == nums[i + k]
如果 pattern[k] == -1 ,那么 nums[i + k + 1] < nums[i + k]
请你返回匹配 pattern 的 nums 子数组的 数目 。
KMP
将数组nums按照数对,转为与pattern数组形式相同的数组,即前一个元素大于后一个元素记为-1,前一个元素小于后一个元素记为1,前一个元素等于后一个元素记为0,得到大小为len(nums)-1的数组a,再将该数组与pattern数组进行KMP匹配(组合为一个数组,即
数组
p
a
t
t
e
r
n
+
[
不在
p
a
t
t
e
r
n
中的数,用于分割
]
+
a
数组pattern+[不在pattern中的数,用于分割]+a
数组pattern+[不在pattern中的数,用于分割]+a,传给匹配函数)。
最后计算传回的next数组中与pattern长度相同的数的个数,因为满足条件的子数组的长度都为len(pattern)+1。
Code
# 匹配
def KMP(p):
ne = [0] * len(p)
j = 0
for i in range(1, len(p)):
while j > 0 and p[j] != p[i]:
j = ne[j - 1]
if p[j] == p[i]:
j += 1
ne[i] = j
return ne
# 将nums转为与pattern数组相同类型的数组
def f(x):
return 1 if x > 0 else (0 if x == 0 else -1)
class Solution:
def countMatchingSubarrays(self, nums: List[int], pattern: List[int]) -> int:
add = pattern + [-2] + [f(y-x) for x, y in pairwise(nums)]
return KMP(add).count(len(pattern))
1455. 检查单词是否为句中其他单词的前缀
给你一个字符串 sentence 作为句子并指定检索词为 searchWord ,其中句子由若干用 单个空格 分隔的单词组成。请你检查检索词 searchWord 是否为句子 sentence 中任意单词的前缀。
如果 searchWord 是某一个单词的前缀,则返回句子 sentence 中该单词所对应的下标(下标从 1 开始)。如果 searchWord 是多个单词的前缀,则返回匹配的第一个单词的下标(最小下标)。如果 searchWord 不是任何单词的前缀,则返回 -1 。
字符串 s 的 前缀 是 s 的任何前导连续子字符串。
暴力解法
和28. 找出字符串中第一个匹配项的下标这题其实差不多,只不过多了统计空格的数量,最后依据空格数量返回答案。
Code
class Solution:
def isPrefixOfWord(self, sentence: str, searchWord: str) -> int:
n = len(sentence)
m = len(searchWord)
count = 0
for i in range(n - m + 1):
if sentence[i] == ' ':
# 记录遇到了几个括号,如果匹配上了,则答案就是count + 1
count += 1
if sentence[i: i+m] == searchWord:
# 如果左边为括号,或在开头位置匹配上了,则直接return count + 1
if sentence[i-1] == ' ' or i == 0:
return count + 1
# 其余情况就是虽然匹配上了,但是是在单词的中间,不能作为前缀
return -1
KMP
也是直接套模板匹配,多了统计空格的数量。
Code
class Solution:
def isPrefixOfWord(self, sentence: str, searchWord: str) -> int:
n = len(sentence)
m = len(searchWord)
ne = [0] * m
# 求解next数组
j = 0
for i in range(1, m):
while j > 0 and searchWord[i] != searchWord[j]:
j = ne[j-1]
if searchWord[i] == searchWord[j]:
j += 1
ne[i] = j
# 匹配
j = 0
count = 0
for i in range(n):
# 统计空格的数量,作为返回的依据
if sentence[i] == ' ':
count += 1
while j > 0 and sentence[i] != searchWord[j]:
j = ne[j-1]
if sentence[i] == searchWord[j]:
j += 1
if j == m:
# 当完全匹配上后的sentence中第一个匹配项的下标的左边为括号时
# 或者在开头后一部分完全匹配上时,return count + 1
if sentence[i - m] == ' ' or i - m + 1 == 0:
return count + 1
# 可能出现匹配上,但不满足条件的情况,所以将j回退,重新匹配,直到满足条件
else:
j = ne[j-1]
return -1
796. 旋转字符串
给定两个字符串, s 和 goal。如果在若干次旋转操作之后,s 能变成 goal ,那么返回 true 。
s 的 旋转操作 就是将 s 最左边的字符移动到最右边。
例如, 若 s = ‘abcde’,在旋转一次之后结果就是’bcdea’ 。
队列解法
旋转就是将字符串头部的字符放到尾部,队列就支持这种操作,队头出,队尾入。
Code
class Solution:
def rotateString(self, s: str, goal: str) -> bool:
queue = list(s)
for i in range(len(s)):
# "".join(map(str, queue))就是将queue中的内容转为字符串,如果旋转后与goal相同,则return True
# 有疑惑的可以了解一下join函数和map函数
if "".join(map(str, queue)) == goal:
return True
# 队头字符放到队尾
queue.append(queue[0])
# 队头元素弹出
queue.pop(0)
return False
KMP
因为s + s就包含了所有旋转后的结果(随便举例一下也就晓得了),所以只要和goal进行字符串匹配,观察goal是否为s + s的子串。要注意如果s和goal的长度不相同,则无论s怎么旋转,都不可能得到goal。
Code
class Solution:
def rotateString(self, s: str, goal: str) -> bool:
# 当s与goal长度不相同时,s无论怎么旋转都不能得到goal
if len(s) != len(goal):
return False
string = s + s # s + s中包含了所有的旋转操作后的字符串
# 求解goal的next数组
ne = [0] * len(goal)
j = 0
for i in range(1, len(goal)):
while j > 0 and goal[i] != goal[j]:
j = ne[j-1]
if goal[i] == goal[j]:
j += 1
ne[i] = j
# 将goal与string进行匹配
j = 0
for i in range(len(string)):
while j > 0 and string[i] != goal[j]:
j = ne[j-1]
if string[i] == goal[j]:
j += 1
if j == len(goal):
return True
return False
686. 重复叠加字符串匹配
给定两个字符串 a 和 b,寻找重复叠加字符串 a 的最小次数,使得字符串 b 成为叠加后的字符串 a 的子串,如果不存在则返回 -1。
注意:字符串 “abc” 重复叠加 0 次是 “”,重复叠加 1 次是 “abc”,重复叠加 2 次是 “abcabc”。
KMP
如果在a重复叠加若干次后b为a的子串,则说明在a未重复叠加时其中的一个字符(下标记为index)等于b的第一个字符,从index开始,重复遍历a(即为重复叠加),直到匹配完b,说明b为重复叠加若干次的a的子串,这样很直观的就能想到使用KMP。
Code
class Solution:
def repeatedStringMatch(self, a: str, b: str) -> int:
n, m = len(a), len(b)
if m == 0:
return -1
# 对b数组构建next数组
ne = [0 for i in range(m)]
j = 0
for i in range(1, m):
while j > 0 and b[j] != b[i]:
j = ne[j-1]
if b[j] == b[i]:
j += 1
ne[i] = j
# 匹配
j = 0
i = 0
# index记录匹配上的第一个字符的下标
index = -1
while i - j < n:
while j > 0 and a[i % n] != b[j]:
j = ne[j-1]
if a[i % n] == b[j]:
j += 1
if j == m:
index = i - m + 1
break
i += 1
if index == -1:
return -1
if n - index >= m:
return 1
# 多减1的目的是
# 在a重复叠加后与b相同时,防止多加一,即在m-(n-index)为1的情况下,让分子为0,2为答案。
# 其他情况因为整除是向下取整,所以1 // n不影响结果
return (m - (n - index) - 1) // n + 2
这代码有三个地方要解释:
- 在匹配过程中的 w h i l e ( i − j < n ) while (i - j < n) while(i−j<n),为什么循环条件是这个?
如果b为a重复叠加若干次时的子串,则b的第一个字符 ( j = 0 ) (j = 0) (j=0)与a中最差匹配上的情况为 i i i指向a的最后一个元素,即 i = n − 1 i = n - 1 i=n−1,所以只要能匹配上, i i i和 j j j的差值肯定要小于 n n n,反之则跳出循环。
- 为什么 i f ( n − i n d e x > = m ) if (n - index >= m) if(n−index>=m),返回 − 1 -1 −1?
在该种情况下,说明 n − i n d e x n - index n−index包含了字符b,则说明b是没重复叠加的a本身的子串。
- 为什么最后 r e t u r n ( m − ( n − i n d e x ) − 1 ) / / n + 2 return (m - (n - index) - 1) // n + 2 return(m−(n−index)−1)//n+2?
减一的目的在上面已经说了,其他的看下图
推导一下a在重复叠加后与b的重叠情况,就可以知道怎么来的了。