28. strStr()
KMP
前缀表:
前缀: 包含首字母不包含尾字母的所有子串
后缀: 只包含尾字母不包含首字母的所有子串
求 -- 最长相等前后缀
下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面重新匹配就可以了
所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。
如何计算前缀表
aabaaf
a: 0 aa: 1 aab: 0 aaba: 1 aabaa: 2 aabaaf: 0 前缀表:01012
前缀表与next数组
很多KMP算法的时间都是使用next数组来做回退操作,那么next数组与前缀表有什么关系呢?
next数组就可以是前缀表,但是很多实现都是把前缀表统一减一(右移一位,初始位置为-1)之后作为next数组。
为什么这么做呢,其实也是很多文章视频没有解释清楚的地方。
其实这并不涉及到KMP的原理,而是具体实现,next数组既可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1)。
后面我会提供两种不同的实现代码,大家就明白了
使用next数组来匹配
以下我们以前缀表统一减一之后的next数组来做演示。
有了next数组,就可以根据next数组来 匹配文本串s,和模式串t了。
注意next数组是新前缀表(旧前缀表统一减一了)。
时间复杂度分析
其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。
暴力的解法显而易见是O(n × m),所以KMP在字符串匹配中极大地提高了搜索的效率。
为了和力扣题目28.实现strStr保持一致,方便大家理解,以下文章统称haystack为文本串, needle为模式串。
都知道使用KMP算法,一定要构造next数组
构造next数组(我更习惯next不减1的,所以按照不减1来写)
文本串s中找模式串t, j 指向模式串起始位置, i 指向文本串起始
void getNext(int* next, const string& s) {
int j = 0;
next[0] = j;
//注意 i 从 1 开始
for(int i = 1; i < s.size(); i++) {
// 前后缀不同了
while (j > 0 && s[i] != s[j]) { // j要保证大于0,因为下面有取j-1作为数组下标的操作
j = next[j - 1]; // 注意这里,是要找前一位的对应的回退位置了
}
// 如果相同
if (s[i] == s[j]) {
j++;
}
// 将前缀的长度赋给next[i]
next[i] = j;
}
}
strStr()
def strStr(self, haystack: str, needle: str) -> int:
if len(needle) == 0:
return 0
# get the nextList for needle
nextL = [0]* len(needle)
self.getNext(nextL, needle)
j = 0
for i in range(len(haystack)):
# haystack != needle
while j >0 and haystack[i] != needle[j]:
j = nextL[j - 1]
# ==
if haystack[i] == needle[j]:
j += 1
# j reach the end of the needle
if j == len(needle):
return i - len(needle) + 1
return -1
def getNext(self, nextL: List[int], s: str) -> None:
j = 0
nextL[0] = j
for i in range(1, len(s)):
while j > 0 and s[i] != s[j]:
j = nextL[j-1]
if s[i] == s[j]:
j += 1
nextL[i] = j
459. Repeated Substring Pattern (KMP)
解法1:
def repeatedSubstringPattern(self, s: str) -> bool:
# 如果是一个字符重复出现,那么把double里面肯定还有一个跟他一样的字符
new_s = s + s
new_s = new_s[1: len(new_s) - 1]
if s in new_s:
return True
else:
return False
解法2:
KMP: 在由重复子串组成的字符串中,最长相等前后缀不包含的子串就是最小重复子串,这里拿字符串s:abababab 来举例,ab就是最小重复单位,如图所示:
数学推导
假设字符串s使用多个重复子串构成(这个子串是最小重复单位),重复出现的子字符串长度是x,所以s是由n * x组成。
因为字符串s的最长相同前后缀的长度一定是不包含s本身,所以 最长相同前后缀长度必然是m * x,而且 n - m = 1,(这里如果不懂,看上面的推理)
所以如果 nx % (n - m)x = 0,就可以判定有重复出现的子字符串。
def repeatedSubstringPattern(self, s: str) -> bool:
if len(s) == 0:
return False
nxt = [0] * len(s)
self.getNext(nxt, s)
if nxt[-1] != 0 and len(s) % (len(s) - nxt[-1]) == 0:
return True
return False
def getNext(self, nextL: List[int], s: str) -> None:
j = 0
nextL[0] = j
for i in range(1, len(s)):
while j > 0 and s[i] != s[j]:
j = nextL[j-1]
if s[i] == s[j]:
j += 1
nextL[i] = j
字符串
双指针
数组篇
通过两个指针在一个for循环下完成两个for循环的工作 ( slow/fast)
字符串
定义两个指针(也可以说是索引下标),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素
很多数组(字符串)填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。
链表
reverse/cycle
N数之和篇
通过前后两个指针不算向中间逼近,在一个for循环下完成两个for循环的工作
总结
今天花了很多时间 搞懂KMP这个算法,大概掌握了70%,二刷的时候可以回顾一下