KMP刷题笔记

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

这代码有三个地方要解释:

  1. 在匹配过程中的 w h i l e ( i − j < n ) while (i - j < n) while(ij<n),为什么循环条件是这个?

如果b为a重复叠加若干次时的子串,则b的第一个字符 ( j = 0 ) (j = 0) (j=0)与a中最差匹配上的情况为 i i i指向a的最后一个元素,即 i = n − 1 i = n - 1 i=n1,所以只要能匹配上, i i i j j j的差值肯定要小于 n n n,反之则跳出循环。

  1. 为什么 i f ( n − i n d e x > = m ) if (n - index >= m) if(nindex>=m),返回 − 1 -1 1

在该种情况下,说明 n − i n d e x n - index nindex包含了字符b,则说明b是没重复叠加的a本身的子串。
推导

  1. 为什么最后 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(nindex)1)//n+2?

减一的目的在上面已经说了,其他的看下图
推导

推导一下a在重复叠加后与b的重叠情况,就可以知道怎么来的了。

  • 26
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值