学习目标
- 完成KMP相关题目
这里强烈推荐Carl哥的:帮你把KMP算法学个通透!(理论篇)和帮你把KMP算法学个通透!(求next数组代码篇),可以说讲得十分通透了
学习内容
KMP算法
首先了解最长相等前后缀及其作用:
求 next 数组:
那么对 aabaaf
来说其 next 数组就是[0, 1, 0, 1, 2, 0]
。当然在使用过程中,为了方便使用,会做统一右移或整体-1的操作,是KMP算法的具体实现,不涉及前后缀,最终使用效果一致。统一右移得到-1, 0, 1, 0, 1, 2
,整体-1得到[-1, 0, -1, 0, 1, -1]
。
对于求解的图片,这里引用 Carl 在 28. 实现 strStr()中的动图:
j
(或者说 j+1,因为这里求的是整体-1的 next 数组)指向的是前置的最末尾
,同时也是前缀的长度
。
i
指向后缀的末尾,其实就是双指针中用来迭代的指针。
可以写(抄)出其这个动图的代码,确实很抽象:
private int[] getNext(String s) {
int j = -1;
int[] next = new int[s.length()];
next[0] = j;
//i从1开始才能比较最长相等前后缀
for(int i = 1; i < s.length(); i++) {
//前缀末尾和后缀末尾不匹配时候,看 next 数组前一位,即前一个子串前缀末尾和后缀末尾匹配时候的位置
//这是因为,例如aabcaaf,前一个子串是aabcaa,f不匹配必然要从前缀的aa后开始找f是否匹配前缀aa后的字符
//tip:这是一个循环,因为可能不能一次找到,如上面例子
while(j >= 0 && s.charAt(j+1) != s.charAt(i)) {
j = next[j];
}
//匹配了,那么前缀的长度 + 1,或者说是最长前缀的末尾的位置向右移动一格
if (s.charAt(j+1) == s.charAt(i)) {
j ++;
}
//记录next[i]对应的字符串的最长相等前后缀的长度
next[i] = j;
}
return next;
}
28. 找出字符串中第一个匹配项的下标(Middle)
题目链接: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 仅由小写英文字符组成
思路:KMP
时间复杂度:O(n+m)
空间复杂度:O(n)
解决方案:
class Solution {
public int strStr(String haystack, String needle) {
int[] next = getNext(needle);
int j = -1;
for (int i = 0; i < haystack.length(); i++) {
while (j >= 0 && haystack.charAt(i) != needle.charAt(j+1)) {
j = next[j];
}
if (haystack.charAt(i) == needle.charAt(j + 1)) {
j ++;
}
if (j == needle.length() - 1) {
return i - j;
}
}
return -1;
}
public int[] getNext(String s) {
int[] next = new int[s.length()];
int j = -1;
next[0] = -1;
for (int i = 1; i < s.length(); i++) {
while (j >= 0 && s.charAt(i) != s.charAt(j + 1)) {
j = next[j];
}
if (s.charAt(i) == s.charAt(j + 1)) {
j ++;
}
next[i] = j;
}
return next;
}
}
小结:KMP 比较抽象,一是要理解,二是要多复习。
459. 重复的子字符串(Easy)
题目链接:重复的子字符串
题目:给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。
示例 1:
输入: s = “abab”
输出: true
解释: 可由子串 “ab” 重复两次构成。
示例 2:
输入: s = “aba”
输出: false
示例 3:
输入: s = “abcabcabcabc”
输出: true
解释: 可由子串 “abc” 重复四次构成。 (或子串 “abcabc” 重复两次构成。)
提示:
1 <= s.length <= 104
s 由小写英文字母组成
思路:依稀记得这题目好像不是 Easy 难度的,但这种解法出来后就变成了 Easy 难度。当成看到也是亮瞎眼。
时间复杂度:O(n*m)
空间复杂度:O(n)
解决方案:
class Solution {
public boolean repeatedSubstringPattern(String s) {
String sa = s.substring(1, s.length()) + s.substring(0, s.length() - 1);
return sa.contains(s);
}
}
小结:上面的算法中String.contains(String s)
用的不是 KMP 算法。这里为了练习 KMP,参照题解写,主要思想:
- 获取 next 数组 (这里获取各个元素 -1 的 next 数组)
- 字符串长度 % (字符串长度 - (next[字符串长度 - 1] + 1)) == 0,且 next[字符串长度 - 1] 不能是 -1
也就是字符串长度能整除字符串减去其最大公共前缀长度后的长度,next[字符串长度 - 1] 是 -1 说明没有相同前后缀,那比不会由子串重复构成。next[字符串长度 - 1] + 1
是指 next 数组的最后一位,且由于是获取各个元素 -1 的 next 数组,因此要 +1.
class Solution {
public boolean repeatedSubstringPattern(String s) {
int[] next = getNext(s);
if (next[s.length() - 1] > -1 && s.length() % (s.length() - (next[s.length() - 1] + 1)) == 0) {
return true;
}
return false;
}
private int[] getNext(String s) {
int[] next = new int[s.length()];
int j = -1;
next[0] = j;
for(int i = 1; i < s.length(); i++) {
while (j >= 0 && s.charAt(i) != s.charAt(j+1)) {
j = next[j];
}
if (s.charAt(i) == s.charAt(j + 1)) {
j ++;
}
next[i] = j;
}
return next;
}
}
总结
- KMP 比较抽象,要理解,多复习。