KMP(Knuth-Morris-Pratt)算法是一种改进的字符串搜索算法,用于在一个文本中找到一个模式的出现位置。它的名字来自于三位发明者:Donald Knuth、Vaughan Pratt 和 James H. Morris。
用于解决字符串的匹配问题。
基于朴素的字符串匹配算法,当出现字符比对不匹配时,我们需要重新开始比对。但实际上,当字符不匹配时,我们已经知道了一些字符匹配的信息。这可以充分利用这些信息来避免不必要的比对。
KMP 算法通过使用一个叫做“部分匹配表”(partial match table,又叫做"失效函数"或"next 数组")的结构,记录了模式串中前后缀的最长公共元素长度,从而实现当匹配失败时,模式串向右移动的位数。这个表格会在模式串自身进行一次匹配计算得出。
KMP算法在字符串搜索的算法中是很高效的,它的时间复杂度是 O(n),其中 n 是文本串的长度。因为无论何时,每一个字符(无论是文本串的还是模式串的)都会被访问一次且仅一次。
我们可以通过一个例子来理解KMP算法。比如我们在字符串 "BBC ABCDAB ABCDABCDABDE" 中查找 "ABCDABD"。
在朴素的字符串匹配算法中,我们会从左到右逐个比较,当发现不匹配的字符时,我们会将模式串向右移动一位,然后重新开始比对。这就意味着,我们可能需要进行大量的不必要的比对。
但是在KMP算法中,当我们发现不匹配的字符时,我们可以将模式串向右移动更多的位数。
在上面的例子中,当我们比对到"ABCDAB "与 "ABCDABD"的最后一个字符时发现不匹配,我们可以直接将模式串向右移动6位(也就是 "ABCDAB"的长度减1),然后开始比对"ABCDABD"与 "ABCDABD"。
为什么可以这么做呢?因为在不匹配发生时,我们已经知道文本串中 "ABCDAB "前面没有 "ABCDABD",我们可以直接跳过这部分开始新一轮的比对。
这就是KMP算法的基本思想,通过一个"部分匹配表"(在这个例子中就是针对 "ABCDABD"的部分匹配表)来确定在不匹配发生时,模式串应该向右移动多少位。
部分匹配表是如何得到的呢?它的每一个位置表示的是,模式串中到当前位置为止的子串的前缀和后缀的最长公共部分的长度。
比如对于模式串 "ABCDABD",它的部分匹配表如下:
模式串的子串 | 前缀 | 后缀 | 最长公共部分 | 部分匹配值 |
---|---|---|---|---|
A | [] | [] | [] | 0 |
AB | [A] | [B] | [] | 0 |
ABC | [A, AB] | [BC, C] | [] | 0 |
ABCD | [A, AB, ABC],[BCD, CD, D] | [] | 0 | |
ABCDA | [A, AB, ABC, ABCD] | [A, DA, CDA, BCDA] | A | 1 |
ABCDAB | [A, AB, ABC, ABCD, ABCDA] | [B, AB, DAB, CDAB, BCDAB] | AB | 2 |
ABCDABD | [A, AB, ABC, ABCD, ABCDA, ABCDAB] | [D, BD, ABD, DABD, CDABD, BCDABD] | [] | 0 |
因此,我们的部分匹配表就是 [0, 0, 0, 0, 1, 2, 0],这个表告诉我们在发生不匹配的情况下,模式串应该向右移动多少位。
KMP算法具体的实现细节和优化可能会有些复杂,但这就是它的基本思想。希望这个例子能帮助你理解KMP算法。
28. 找出字符串中第一个匹配项的下标
给你两个字符串
haystack
和needle
,请你在haystack
字符串中找出needle
字符串的第一个匹配项的下标(下标从 0 开始)。如果needle
不是haystack
的一部分,则返回-1
。示例 1:
输入:haystack = "sadbutsad", needle = "sad" 输出:0 解释:"sad" 在下标 0 和 6 处匹配。 第一个匹配项的下标是 0 ,所以返回 0 。示例 2:
输入:haystack = "leetcode", needle = "leeto" 输出:-1 解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。提示:
1 <= haystack.length, needle.length <= 104
haystack
和needle
仅由小写英文字符组成
方法一:暴力解法
def strStr(self, haystack, needle):
"""
:type haystack: str
:type needle: str
:rtype: int
"""
# 方法一: 暴力解法 time: O(n*m). space: O(1)
haystack_len = len(haystack)
needle_len = len(needle)
for i in range(haystack_len):
if haystack[i:i+needle_len] == needle:
return i
return -1
方法二:KMP解法
class Solution(object):
def compute_prefix_func(self, pattern):
"""
i 是当前正在处理的 pattern 字符串中的位置,它从 1 开始,
到 len(pattern) 结束。i 是主循环变量,用于遍历整个 pattern。
j 是当前已经找到的与 pattern 开头相同的前缀的长度。
当在 i 位置上的字符和在 j 位置上的字符不同时,我们需要在 pattern 前缀中
找到一个更短的相同前缀。这是通过查找 prefix_len[j-1] 实现的,
因为 prefix_len[j-1] 存储的是前 j 个字符的最长相同前后缀的长度。
"""
prefix_len = [0] * len(pattern)
j = 0 # 当前已经找到的与 pattern 开头相同的前缀的长度。
for i in range(1, len(pattern)):
while j > 0 and pattern[i] != pattern[j]:
j = prefix_len[j-1]
if pattern[i] == pattern[j]:
j += 1
prefix_len[i] = j
return prefix_len
def strStr(self, haystack, needle):
"""
:type haystack: str
:type needle: str
:rtype: int
"""
# # 方法一: 暴力解法 time: O(n*m). space: O(1)
# haystack_len = len(haystack)
# needle_len = len(needle)
# for i in range(haystack_len):
# if haystack[i:i+needle_len] == needle:
# return i
# return -1
# 方法二: KMP 算法 time: O(m + n). space: O(n)
if not needle:
return 0
prefix_len = self.compute_prefix_func(needle)
j = 0
for i in range(len(haystack)):
while j > 0 and haystack[i] != needle[j]:
j = prefix_len[j-1]
if haystack[i] == needle[j]:
j += 1
if j == len(needle):
return i - (j - 1) # i - len(needle) + 1
return -1 # pattern not found
459. 重复的子字符串
给定一个非空的字符串
s
,检查是否可以通过由它的一个子串重复多次构成。示例 1:
输入: s = "abab" 输出: true 解释: 可由子串 "ab" 重复两次构成。示例 2:
输入: s = "aba" 输出: false示例 3:
输入: s = "abcabcabcabc" 输出: true 解释: 可由子串 "abc" 重复四次构成。 (或子串 "abcabc" 重复两次构成。)提示:
1 <= s.length <= 104
s
由小写英文字母组成
class Solution(object):
def repeatedSubstringPattern(self, s):
"""
:type s: str
:rtype: bool
"""
return s in (s+s)[1:-1]