28. 实现 strStr() (本题可以跳过)因为KMP算法很难,大家别奢求 一次就把kmp全理解了,大家刚学KMP一定会有各种各样的疑问,先留着,别期望立刻啃明白,第一遍了解大概思路,二刷的时候,再看KMP会 好懂很多。
或者说大家可以放弃一刷可以不看KMP,今天来回顾一下之前的算法题目就可以。
因为大家 算法能力还没到,细扣 很难的算法,会把自己绕进去,就算别人给解释,只会激发出更多的问题和疑惑。所以大家先了解大体过程,知道这么回事, 等自己有 算法基础和思维了,在看多看几遍视频,慢慢就理解了。
题目链接/文章讲解/视频讲解:代码随想录
KMP算法解决的是字符串匹配的问题
文本串中是否出现过模式串
暴力法:两层for循环,一层遍历文本串(长度n),一层遍历模式串(长度m),一一匹配,不匹配就将模式串整体向后移一位,继续匹配...直到完全匹配或移动到文本串结尾,不存在。O(m*n)
KMP算法:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。
前缀表(prefix table):前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。
前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,再重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。
找到与后缀相等的前缀的后面,重新开始匹配(因此,需要知道一个字符串中,它的最长相等前后缀/最长公共前后缀,这样在遇到不匹配的位置时,就找前面那个子串的最长相等前后缀)
前缀是指不包含最后一个字符的所有以第一个字符开头的所有连续子串。
后缀是指不包含第一个字符的所有以最后一个字符结尾的所有连续子串。
前缀表要求的就是最长的相同前后缀的长度。
因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面重新匹配就可以了。
所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。
找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。
因为要找前面字符串的最长相同的前缀和后缀。所以要看前一位的前缀表的数值。
前一个字符的前缀表的数值是2, 所以把下标移动到下标2的位置继续比配。(跳到前缀的后面,它的下标正好是前缀的长度)
前缀表与next数组
next/prefix,遇到冲突后,next数组告诉我们要回退到哪里,下一个位置,所以叫next。
具体实现KMP算法过程中,会对前缀表进行右移操作,或统一减一操作(初始位置变为-1)这并不涉及到KMP的原理,而是两种不同的具体实现
原封不动地使用前缀表作为next数组,依然可以实现KMP算法。
时间复杂度分析
其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。
暴力的解法显而易见是O(n × m),所以KMP在字符串匹配中极大地提高了搜索的效率。
构造next数组
构造next数组其实就是计算模式串s,前缀表的过程。 主要有如下三步:
- 初始化(j指向前缀末尾位置(还代表i之前的子串的最长相等前后缀长度),i指向后缀末尾位置)
- 处理前后缀不相同的情况
- 处理前后缀相同的情况
// 前缀表(不减一)
class Solution {
public int strStr(String haystack, String needle) {
if (needle.length() == 0) return 0;
int[] next = new int[needle.length()];
getNext(next,needle);
int j = 0; // 因为next数组里记录的起始位置为0
for(int i = 0; i < haystack.length(); i++){
while( j > 0 && needle.charAt(j) != haystack.charAt(i)){// 注意i就从0开始,j要保证大于0,因为下面有取j-1作为数组下标的操作
j = next[j - 1]; //j 寻找之前匹配的位置,是要找前一位的对应的回退位置了
}
if (needle.charAt(j) == haystack.charAt(i)){ // 找到相同的前后缀,匹配,j和i同时向后移动
j++; // i的增加在for循环里
}
if(j == needle.length()){ // 文本串s里出现了模式串t
return i - needle.length() + 1;
}
}
return -1;
}
// 构造next数组(前缀表)
public void getNext(int[] next, String s){
// 定义两个指针i和j,j指向前缀末尾位置,i指向后缀末尾位置。
int j = 0;
next[0] = 0;
for(int i = 1; i < s.length(); i++){ // 注意i从1开始
while( j > 0 && s.charAt(j) != s.charAt(i)){ // 前后缀不相同了
j = next[j-1]; // 向前回退
}
if (s.charAt(j) == s.charAt(i)){ // 找到相同的前后缀
j++;
}
next[i] = j; // 将j(前缀的长度)赋给next[i]
}
}
}
459.重复的子字符串 (本题可以跳过)本题算是KMP算法的一个应用,不过 对KMP了解不够熟练的话,理解本题就难很多。
我的建议是 KMP和本题,一刷的时候 ,可以适当放过,了解怎么回事就行,二刷的时候再来硬啃
题目链接/文章讲解/视频讲解: 代码随想录
一开始想到暴力法:
class Solution {
public boolean repeatedSubstringPattern(String s) {
StringBuilder sb = new StringBuilder("");
for(int i = 0; i<s.length(); i++){
sb.append(s.charAt(i));
if(s.length() % (i+1) != 0) continue;
StringBuilder temp = new StringBuilder("");
int num = s.length() / (i+1);
for(int j = 0; j< num; j++){
temp.append(sb);
}
if(s.equals(temp.toString()) && s.length() != sb.length()){
return true;
}
}
return false;
}
}
移动匹配:
任何一个内部由重复的子串组成的字符串,它的前半部分和后半部分,相等(不一定对半分)
那么既然前面有相同的子串,后面有相同的子串,用 s + s,这样组成的字符串中,后面的子串做前串,前后的子串做后串,就一定还能组成一个s。所以判断字符串s是否由重复子串组成,只要两个s拼接在一起,里面还出现一个s的话,就说明是由重复子串组成。
我们在判断 s + s 拼接的字符串里是否出现一个s的的时候,要刨除 s + s 的首字符和尾字符,这样避免在s+s中搜索出原来的s,我们要搜索的是中间拼接出来的s。
搜索(find/contains)实际上应该也需要用到KMP算法。时间复杂度应该是O(m+n)。
class Solution {
public boolean repeatedSubstringPattern(String s) {
String newstring = s+s;
String newnewstring = newstring.substring(1, newstring.length()-1);
if(newnewstring.contains(s)) return true;
return false;
}
}
KMP法:
给定字符串,判断另一个字符串是否在这个字符串中出现过,
KMP算法在查找过程中,遇到不匹配的地方,能直接跳到上次匹配的地方然后继续匹配(通过计算好的next数组)
next数组是以各个字符串为结尾的子串的最长相等前后缀的集合
- 前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;
- 后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串
在由重复子串组成的字符串中,最长相等前后缀不包含的子串就是最小重复子串。
原字符串如果能整除 最长相等前后缀不包含的子串(最小重复子串),说明该字符串由重复的子串组成。
数组长度减去最长相同前后缀的长度相当于是第一个周期的长度,也就是一个周期的长度,如果这个周期可以被整除,就说明整个数组就是这个周期的循环。
class Solution {
public boolean repeatedSubstringPattern(String s) {
if(s.equals("")) return false;
int len = s.length();
int[] next = new int[len];
// 定义两个指针i和j,j指向前缀末尾位置,i指向后缀末尾位置。
int j = 0;
next[0] = 0;
for(int i = 1; i < s.length(); i++){ // 注意i从1开始*****
while( j > 0 && s.charAt(j) != s.charAt(i)){ // 前后缀不相同了
j = next[j-1]; // 向前回退
}
if (s.charAt(j) == s.charAt(i)){ // 找到相同的前后缀
j++;
}
next[i] = j; // 将j(前缀的长度)赋给next[i]
}
if(next[len-1] > 0 && len % (len - next[len-1]) == 0){
return true;
}
return false;
}
}