344.反转字符串
思路
题目要求就是查找子字符串,这样第一反应就可以是暴力查找法和今天最重要的KMP算法(上难度!)
-
KMP算法思想:
KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配,而next数组就是关于公共前后缀的回退表。 -
什么是前缀后缀
例如 aabaa 中,a,aa,aab,aaba就是前缀,而a,aa,baa,abaa就是后缀。前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。 -
怎么回退
当 aabaaf 和 aabaabaaf 匹配时,当子串指向5时,两字符串开始不匹配,这时候寻找i=5之前子串的相同前后缀的最长长度,如下图,得出最长相同前后缀长度为2。
这个时候i跳转到下标为2的地方。就达到了回退效果,这就是KMP的精髓处:寻找最长相同前后缀。 -
求KMP的next数组
-
长度为前1个字符的子串a,最长相同前后缀的长度为0。(注意字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。)
-
长度为前2个字符的子串aa,最长相同前后缀的长度为1。
-
长度为前3个字符的子串aab,最长相同前后缀的长度为0。
-
以此类推: 长度为前4个字符的子串aaba,最长相同前后缀的长度为1。 长度为前5个字符的子串aabaa,最长相同前后缀的长度为2。 长度为前6个字符的子串aabaaf,最长相同前后缀的长度为0。
那么把求得的最长相同前后缀的长度就是对应前缀表的元素,如图:
代码实现
class Solution {
public:
void getNext(int* next, const string& s) {
int j = 0;
next[0] = 0;
for(int i = 1; i < s.size(); i++) {
while (j > 0 && s[i] != s[j]) {
j = next[j - 1];
}
if (s[i] == s[j]) {
j++;
}
\ next[i] = j;
}
}
int strStr(string haystack, string needle) {
if (needle.size() == 0) {
return 0;
}
int next[needle.size()];
getNext(next, needle);
int j = 0;
for (int i = 0; i < haystack.size(); i++) {
while(j > 0 && haystack[i] != needle[j]) {
j = next[j - 1];
}
if (haystack[i] == needle[j]) {
j++;
}
if (j == needle.size() ) {
return (i - needle.size() + 1);
}
}
return -1;
}
};
总结
一刷:在这里我犯了个错误,就是没有给next[0]赋初值0,这样会造成超时。同时给next分配空间时应该是needle.size( ),不然会访问到非法空间。
二刷:加深理解了一下kmp的操作方法,我用的next是next[i]为0-i长度内公共前后缀最长长度,当next[i]=2,最长公共前后缀长度为2,则从下标为2的位置继续比较,这样比较时当j > 0 && haystack[i] != needle[j],j = next[j - 1]即为直接从next[j - 1]开始比较,理解才更不会出错。
459.重复的子字符串
思路
这道题有三个方法:
-
暴力法
暴力的解法, 就是一个for循环获取 子串的终止位置, 然后判断子串是否能重复构成字符串,又嵌套一个for循环,所以是O(n^2)的时间复杂度。
其实我们只需要判断,以第一个字母为开始的子串就可以,所以一个for循环获取子串的终止位置就行了。 而且遍历的时候 都不用遍历结束,只需要遍历到中间位置,因为子串结束位置大于中间位置的话,一定不能重复组成字符串。 -
移动匹配
当一个字符串s:abcabc,内部由重复的子串组成,那么这个字符串的结构一定是这样的:(也就是由前后相同的子串组成)
那么既然前面有相同的子串,后面有相同的子串,用 s + s,这样组成的字符串中,后面的子串做前串,前面的子串做后串,就一定还能组成一个s,如图:
所以判断字符串s是否由重复子串组成,只要两个s拼接在一起,里面还出现一个s的话,就说明是由重复子串组成。
- KMP
-
找到最小重复子串
-
数组长度减去最长相同前后缀的长度相当于是第一个周期的长度,也就是一个周期的长度,如果这个周期可以被整除,就说明整个数组就是这个周期的循环。
-
代码实现
- 移动匹配
class Solution {
public:
bool repeatedSubstringPattern(string s) {
string t = s + s;
t.erase(t.begin()); t.erase(t.end() - 1); // 掐头去尾
if (t.find(s) != std::string::npos) return true; // r
return false;
}
};
时间复杂度: O(n)
空间复杂度: O(1)
- KMP
class Solution {
public:
void getNext (int* next, const string& s){
next[0] = 0;
int j = 0;
for(int i = 1;i < s.size(); i++){
while(j > 0 && s[i] != s[j]) {
j = next[j - 1];
}
if(s[i] == s[j]) {
j++;
}
next[i] = j;
}
}
bool repeatedSubstringPattern (string s) {
if (s.size() == 0) {
return false;
}
int next[s.size()];
getNext(next, s);
int len = s.size();
if (next[len - 1] != 0 && len % (len - (next[len - 1] )) == 0) {
return true;
}
return false;
}
};
时间复杂度: O(n)
空间复杂度: O(n)
总结
一刷:好难好难呜呜呜,算法尽头还是数学(悲)
二刷:已经完全没有印象了,不过看了思路发现还是很好理解的,因为next[len-1]就已经记录了从0-(len-1)内下标最长公共前后缀的长度了,所以减去最长公共前后缀剩下的就是无法匹配的部分了,那么如果总长度可以整除落单的部分长度,那整个字符串都可以由这一部分组成。