不容易解的题10.4(kmp算法的应用和讲解)

151.反转字符串中的单词

151. 反转字符串中的单词 - 力扣(LeetCode)https://leetcode.cn/problems/reverse-words-in-a-string/?envType=list&envId=ZCa7r67M题目描述中给出的字符串可能含有前导空格,两个单词之间存在多个单词和末尾含有多个空格的情况,让我们删除多余的空格的同时返回一个该字符串各个单词的反转字符串。

思路:先删除空格再整体反转字符串,再将每个单词反转。

为什么要先删除空格?先反转字符串行不行?先反转字符串不行,因为我们整体反转字符串后,再反转各个单词依据的是每两个单词间只存在一个空格的规律,来正确的找到单词进行反转,如果我们先进行反转,空格多少不一定,不具有规律,所以没办法反转。

那如何删除空格?我之前写的思路是分别判断是哪一种形式,如果是前导和末尾空格则全部删除,如果是单词间则删到只剩下一个。这种思想肯定是可行的,但是很容易写错,需要注意的事情多

class Solution {
public:
void reverse(string& s,int start,int end){for(int i=start,j=end;i<j;i++,j--)swap(s[i],s[j]);
}string reverseWords(string s) {int slow=0,fast=0;while(fast<s.size()&&s[fast]==' ')fast++;for(;fast<s.size();fast++){if(fast>0&&s[fast]==s[fast-1]&&s[fast]==' ')//不仅起到了让字符复制的效果,//也保证了中间空格只能取一次continue;elses[slow++]=s[fast];}if(s[slow-1]==' ')//有slow++的缘故,最终slow会停在字符串的下一个位置s.resize(slow-1);elses.resize(slow);int start=0;reverse(s,0,s.size()-1);//整体的反转for(int i=0;i<=s.size();i++){//局部的单词反转if(s[i]==' '||i==s.size()){reverse(s,start,i-1);start=i+1;}}return s;}
};

这段代码我标上了注释,删除空格的过程中你不仅要判断此时是否是连续空格出现的情况,删除完毕后,还要判断是否末尾出现了空格,如果末尾出现了连续空格删除空格时fast会跳过它,但是请注意这里的循环代码仅仅能判断连续空格的删除,也就是只出现一个空格应该被认为是正常现象,根据slow的运动规律我们可以知道,slow总是指向最后一个字符的下一个位置,所以判断一下如果slow-1位置出现空格则说明含有末尾空格,此时resize减少一个长度就可以了。

然后整体反转字符串,还有部分反转单词就显得十分的简单了,就是遇到空格了就说明已经遍历完一个单词了。

我们来看另一种删除空格的方法:是只要遇见空格就删除,然后等到需要用slow写字符时候,判断如果此时不是从0开始,那么手动添加一个空格就可以了

注意:我们说的删除空格并不是真正删除它,而是用快慢指针的方法,慢指针是覆写数据的,快指针遍历数据,到我们想保留的数据时,再用slow指针写,而覆写的过程会把不要的数据覆盖就完成了所谓的删除,其实很多字符串或者数组的删除操作,都是可以用这种方法实现。

class Solution {
public:
void del(string&s){int i=0;for(i=0;i<s.size();++i)if(s[i]!=' ')break;int slow=0,fast=i;while(fast<s.size()){if(slow&&s[fast]!=' ')s[slow++]=' ';while(fast<s.size()&&s[fast]!=' ')s[slow++]=s[fast++];fast++;}s.resize(slow);
}string reverseWords(string s) {del(s);reverse(s.begin(),s.end());int left=0;for(int i=0;i<s.size();++i){if(s[i]==' '){reverse(s.begin()+left,s.begin()+i);left=i+1;}}reverse(s.begin()+left,s.end());return s;}
};

这种方法,很明显要简洁的多,需要想的事情也变少了

关于反转字符串再说一嘴:题目求的是反转单词的顺序,我们反转整个字符串就可以做到把顺序全部倒置,也就是说把后面单词拿到前面,而这时字符也是倒过来的,我们再部分反转就达到了目的


28.找出字符串中第一个匹配项的下标

28. 找出字符串中第一个匹配项的下标 - 力扣(LeetCode)https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/?envType=list&envId=ZCa7r67M典型的字符串查找问题,而以高效率处理这种问题的有一种专门的方法是kmp算法。

暴力解法应该也是可以解决的,这里官方也给出了详细的代码,这里我只阐述代码思想不写暴力解法,暴力解法思想就是双for循环,这是很常见的,就是外层循环一个字符串里层循环一个字符串,拿外层的字符串和里层的挨个比较,用一个标志flag标记此次判断是否成功,如果遇见两字符对应位置不相等字符,直接break,判断标志flag为真,返回此时外层下标,否则继续从外层循环下一个字符起遍历。

思路太简单了,但是也有需要注意的细节,怎么在内层循环一步步的同时比较外层字符串和内层字符串位置,并且还能保证不出界?用i+s2.size()<=s1.size()判断就可以了,这也是一步剪枝

好了不说这个了,重点是kmp算法。这是一道kmp模板题

我们这里假设在字符串s1里查找字符串s2,第一次出现的下标

首先最重要的是我们要清楚什么题需要使用kmp

一个字符串中查找另一个字符串就可以使用

那这个算法可以起到什么作用?

在我们匹配完一定的数据时,如果此时发生了两字符串该位置不匹配的情况,根据一个前缀表使s2字符串查找发生回退,而不用从头开始匹配!这就是kmp比暴力解法快的原因。虽然也是s1的每个字符做开头查找但它不是用双for循环,由于有前缀表查表的缘故,可以在On时间内很快匹配完成。

前缀表介绍及如何书写:

前缀表的作用我们在上面已经说过了,现在来说说实现机制。

前缀表是用s2也就是被找的那个字符串编写的

前缀表是用最长相等前后缀的原则去编写的,前缀是指包含第一个字符不包含最后一个字符的子串,后缀指包含最后一个字符但不包含第一个字符的子串,我们要找的是字符串s2中各个字符为终点时,最长相等前后缀的数值是多少,该数值代表回退的下标,判断到该字符如果和s1不相等,就回退到该数值对应的下标。这是利用了回文子串对称的原理

在aabaabaafa字符串中查找aabaaf字符串

我们用一个变量记录当前前后缀最长等于几,然后一个循环遍历s2,这个循环此时对应的位置如果和当前以最长前后缀的值为下标的对应的s2位置字符相等,则最长前后缀值增加1。如果不等,判断当前前后缀值是否大于0,如果大于0,根据前缀表回退,最后操作完之后,把当前前后缀长度值填到当前字符为结尾的前缀表对应值中。

这样说可能有些过于抽象了,我们举个例子,用i表示当前前后缀相等最长为多少,一开始应该是0

然后初始化前缀表第一个位置把它制成0,因为一个字符组成字符串既没有前缀也没有后缀,不知道为什么去看前后缀定义。进入循环从j=1开始,当前字符串是“aa”s2【i】==s2【j】

所以i++,当前位置前缀表arr【1】应该是1,再往后s2【i】!=s2【j】,此时i=arr【i-1】完成前缀表回退,而此时的arr【i-1】是0,当前前缀表arr【j】=0,代表“aaf”最长相等前后缀长度是0,请大家自己完成接下来的前缀表填写。就可以知道为什么前缀表能够准确地跳到要匹配的地方了。

给出前缀表实现代码

void getnext(string& s2,vector<int>& next){int i=0;//前后缀长度next[0]=0;for(int j=1;j<s2.size();++j){while(i>0&&s2[i]!=s2[j])i=next[i-1]if(s2[i]==s2[j])i++;next[j]=i;
}
}

请注意回退应该是一个连续的过程,这是容易出错的点,一直找不到对应的下标等于该次以j结尾的下标字符的话,只能说明一个问题,即当前子串前后缀最长相等部分为0,所以一直回退它最后会回到next【0】当前前缀表就为0了,当然除了回退到0之外还有一种可能也会跳出while,那就是某时候找到字符与当前字符相等了,那就用那个时候的i作为当前前缀表值,那个时候那个字符对应的前后缀情况和当前的情况完全一致,不懂多模拟一下就懂了,不模拟光是看文字肯定是理解不了的。

前缀表写完了已经完成90%了,接下来就是用循环匹配字符串了,用一个变量代表字符串s2当前遍历的位置,如果遇到不相等字符用前缀表回退,不停判断如果此时i如果已经增加到s2字符串的长度了,说明已经匹配完了,返回值即可。

疑问:i在这里代表什么?还是最长相等前后缀吗?

并不是,这里代表着是s2此时应该从哪个字符开始遍历,前缀表已经编写完成了,匹配i++,不匹配i从前缀表找下一次匹配时需要从哪里继续匹配即可,如果i走到s2末尾,说明找到了

class Solution {
public:
void getnext(string&s,vector<int>&next){int j=0;for(int i=1;i<s.size();++i){while(j&&s[j]!=s[i])j=next[j-1];if(s[j]==s[i])j++;next[i]=j;}
}int strStr(string haystack, string needle) {vector<int>next(needle.size(),0);getnext(needle,next);int i=0;for(int j=0;j<haystack.size();++j){while(i>0&&haystack[j]!=needle[i])i=next[i-1];if(haystack[j]==needle[i])i++;if(needle.size()==i)return j-i+1;}return -1;}
};

记住三件事,1、如何编写前缀表2、在比较两个字符串时,出现两字符不匹配情况,像编写前缀表那样去回退。3、多练,练多了自然就能理解,多画图和模拟,永远记住前缀表是由被查找字符串编写。


459.重复的子字符串

459. 重复的子字符串 - 力扣(LeetCode)https://leetcode.cn/problems/repeated-substring-pattern/description/?envType=list&envId=ZCa7r67M这道题是一个好题,这里提供三种解法,暴力解法也是可以实现对,而且很有细节

暴力思路好想,就是用子串去和源字符串反复比较,重要的是取什么样的子串?怎么反复比较?

这道题的意思就是判断当前字符串是否可以由某字符串重复组成,那该字符串长度一定得是我们取的这个子串长度的倍数,不然怎么可能构成呢?

我们每次判断字符串长度模上子串长度==0,我们还知道如果一个子串能重复构成该字符串,那么它一定是从该字符串的头部开始能在某处找到一个子串的终结,我们就循环让它从0开始走,把n%i==0每次都判断一次就ok了。并且如果该子串真的能组成该字符串,那么一定是呈周期性的对应字符相等,具体就是让一个j=i,j每次自增,并判断s【j】==s【j-i】

也就是s【j】==s【0】、s【j+1】==s【1】,一直到末尾这些字符判断都得相等,这应该很好理解,下面看代码。

class Solution {
public:bool repeatedSubstringPattern(string s) {int n=s.size();int flag=0;for(int i=1;i<=s.size()/2;i++){if(n%i==0){flag=1;for(int j=i;j<s.size();j++){//假设找到由某子串组成,那么//该子串后面位置减去子串长度的位置字符应该与现在位置字符相等if(s[j]!=s[j-i]){flag=0;break;}}if(flag)return true;}}return false;}
};

虽然是暴力题解,但是也有技巧,可以好好理解一下。

第二种解法:将该字符串两个拼接在一起,然后删减去第一个和最后一个字符,判断能否在新字符串中找到原字符串。

这是一种很好的方法,也是和题解学习的,自己想不出来,由于重复子字符串构成的字符串轴对称,所以源字符串的后半部分和另一个源字符串的前半部分拼接在一起,一定能组成一个原来的字符串,这样想或许更好理解。代码很简单。

class Solution {
public:bool repeatedSubstringPattern(string s) {string ss=s+s;ss.erase(ss.begin());ss.erase(ss.end()-1);if(ss.find(s)!=-1)return true;else return false;}
};

直接拼接删减后判断即可,如果没有就返回false。

第三种方法:自然是我们今天的主角,kmp算法。

kmp的算法简洁和为什么使用kmp算法?步骤是怎样的?在前面一道题已经说的十分详细了,大家可以再仔细阅读一遍,再来看本题。

本题为什么可以用kmp算法?因为其中应用了next数组的规律,next数组的创建我们知道,是根据每个下标为终止的子串,判断到此位置,最长相等前后缀是多少!

而如果一个字符串是由重复子串构成,那么它的next数组一定是越来越大的!呈递增趋势

next数组最后一个下标便是最长相等前后缀的峰值。那我们应该怎么做?

应该用总长度减去最后一个值吗?这样想就是错误的!很典型的错误

我们不要忘记前后缀的定义分别是什么

前后缀指的是包含首字母/尾字母的部分,画图看一看最长前缀和最长后缀,我们可以看得出最长前缀和最长后缀错开的那部分,正是我们要找的答案。

你也可以用测试用例来模拟一下,abcabcabcabc这串字符串。next数组的前三个位置都是0,而后面才是逐渐递增,而错开的这部分不正是我们要找的答案吗?

知道这些就好办了,就是用总长度n-next【n-1】这就是子串长度,然后用总长度取模也就是

n%(n-next【n-1】)==0如果为true,则说明该字符串由子串连续构成。

class Solution {
public:
void getnext(vector<int>&next,string&s){int i=0;for(int j=1;j<s.size();j++){while(i>0&&s[j]!=s[i])i=next[i-1];if(s[j]==s[i])i++;next[j]=i;}
}bool repeatedSubstringPattern(string s) {vector<int>next(s.size(),0);getnext(next,s);int n=s.size();if(next[n-1]&&n%(n-next[n-1])==0)return true;return false;}
};

但我们不要忘记检查一下,next最后一个下标是否为0,如果为0说明肯定不是重复字串构成的字符串,而且如果不检查,那么n-0再被n模,那么一定为true,这是容易出错的点。


以上便是本期的全部内容。

都看到这里了如果有用的话别忘了一键三连哦,如果是互粉回访我也会做的!

大家有什么想看的题解,或者想看的算法专栏、数据结构专栏,可以去看看往期的文章,有想看的新题目或者专栏也可以评论区写出来,讨论一番,本账号将持续更新。
期待您的关注

本文链接:https://my.lmcjl.com/post/12010.html

展开阅读全文

4 评论

留下您的评论.