一、题目打卡
1.1 实现strStr()
题目链接:. - 力扣(LeetCode)
这个题目就是典型的 KMP 问题的一个范式,一开始拿到这个题目,我首先想到了双指针,然后写了这样的一个代码:
class Solution {
public:
int strStr(string haystack, string needle) {
int i = 0, j = 0;
for(;i < haystack.size(); i++){
if(haystack[i] == needle[j]){
int tmp = i; // 记录 i 的位置
while(j != needle.size()){
if(needle[j] != haystack[i]){
j = 0;
i = tmp;
break;
}else{
i++;
j++;
}
if(j == needle.size()) return tmp;
}
}
}
return -1;
}
};
本身的逻辑不是很复杂,本质上也是暴力法的一种延伸,关键的一个步骤是在双指针循环的过程中如果遇到了不相等的情况,那么需要对两个指针进行怎样的操作,我这里是先把 needle 的指针归零,并通过 tmp 暂存了 i ,然后如果遇到了不相等的情况,就把 i 回溯到 tmp 的位置,然后从下一个 i + 1 再进行判断,这样其实在极端情况下,其时间复杂度是 O(n^2),回溯的时候也采用的是最简单粗暴的办法,然后我看了答案,学习了 KMP 的思路。
建议直接看视频比较简单明了,看文字容易把自己绕进去: 帮你把KMP算法学个通透!(理论篇)_哔哩哔哩_bilibili
学习完了以后,自己对 KMP 算法的认识,最关键的就是维护一个 next 列表,其中存的是每次第二个数组如果需要回溯时,那么它的回溯的依据是什么,而不是简单粗暴地直接回溯到最开始的位置,这也就是我一个直观的理解和认知,然后按照视频的思路,重新写了代码:
class Solution {
private:
// void buildNext(string s, vector<int> &next){
// int i = 1, j = 0;
// for(; i < s.size() ; i++){
// while(j > 0 && s[i] != s[j]){
// j = next[j - 1];
// }
// if(s[i] == s[j]){
// j++;
// }
// next[i] = j;
// }
// }
// 使用传入数组的方式写kmp
void getNext(string &s, int *next){
int i = 1, j = 0;
next[0] = 0;
for(;i < s.size();i++){
while(j > 0 && s[i] != s[j]){
j = next[j - 1];
}
if(s[i] == s[j]) j++;
next[i] = j;
}
}
public:
int strStr(string haystack, string needle) {
// 测试KMP算法的
// string input = "aabaaf";
// vector<int> next(input.size(),0);
// buildNext(input,next);
// for(auto& it : next){
// cout << it << " ";
// }
// cout <<endl;
// int next[input.size()];
// getNext(input,next);
// for(int i = 0 ; i < input.size() ; i++){
// cout << next[i] << " ";
// }
// cout <<endl;
int next[needle.size()];
getNext(needle,next);
int i = 0, j = 0;
for(; i < haystack.size();i++){
// 这一步想错了,其实如果不相等,那么i在for循环结束一直加的,这样的话会使得第二个回溯一直不会进行
// while(haystack[i] != needle[j]){
// i++;
// }
while(j > 0 && needle[j] != haystack[i]){
j = next[j - 1];
}
if(needle[j] == haystack[i]){
j++;
}
if(j == needle.size()) return i - needle.size() + 1;
}
return -1;
}
};
最后发现确实是在运行效率上会有很大的提升。
1.2 重复的子字符串(借助答案思路)
这个题我看和 KMP 算法有关,然后自己尝试通过输出观察规律的方式来解,然后我发现我整体的猜想是正确的,最后确实是通过整体的长度和在后面递增序列位数取余后判断是否为0来返回 bool 值,但是我的思路是正向的,答案的思路是逆向求前面的位数(确实是应该这样,相对也会更简单),至于背后的原理由于时间原因我想等后面二刷再看吧,有一个细节需要注意的是在最后的判断条件里面,需要保证 next 数组的最后一位是不等于 0 的,否则 0 向任何取余都是 0 ,那么就影响判断了。
class Solution {
private:
void getNext(string &s, int *next){
int i, j = 0;
next[0] = 0;
for(i = 1; i < s.size(); i++){
while(j > 0 && s[j] != s[i]){
j = next[j-1];
}
if(s[i] == s[j]) j++;
next[i] = j;
}
}
public:
bool repeatedSubstringPattern(string s) {
// if(s.size() == 1) return false;
int next[s.size()];
getNext(s,next);
// for(int i = 0 ; i < s.size(); i++){
// cout << next[i] << " ";
// }
// cout << endl;
// int start = 0,tmp = 0;
// for(;start < s.size();start++){
// if(next[start] == 0) tmp = start + 1;
// }
// while(start < s.size() && next[start] == 0 ) start++;
// cout << tmp <<endl;
// int it = tmp;
// while(it < s.size() - 1){
// if(next[it + 1] <= next[it] && next[it] == 1) tmp = it + 1;
// it++;
// }
// cout << tmp <<endl;
// if(next[s.size() - 1] == 0) return false;
// return next[s.size() - 1] % tmp == 0 ? true : false;
// return false;
if(next[s.size() - 1] != 0 && (s.size() % (s.size() - (next[s.size() - 1])) == 0)) return true;
return false;
}
};
二、总结篇
要不要使用库函数:
对于这种问题,主要是看使用库函数的部分是否是题目过程的一个小部分, 并且在熟悉库函数的时间复杂度和原理的时候,可以使用。
双指针法:
双指针在字符串中,最经典的是反转字符串,然后在替换空格的问题中,也学习到了一种新型处理字符串的方法:对于很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作,移除元素和翻转字符串里的单词,同样用到了双指针的办法简化时间复杂度,在这个过程中也学习到,对于典型的 erase 操作,其时间复杂度是 O(n) .
反转系列:
反转系列主要考查的是对字符串处理的逻辑掌握,这里学习到当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章,而通过对整体反转加局部反转的应用,还可以实现类似翻转字符串中的单词和左右旋等经典的应用。
KMP:
KMP主要当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。
而KMP的关键在于理解前缀和后缀,最主要的在于理解 j = next[j - 1] 这一步,认识到 j 是如何进行回溯的,同时 j 的意义也需要理解。