第四章 字符串part02
今日任务
- 151.翻转字符串里的单词
- 卡码网:55.右旋转字符串
- 28. 实现 strStr()
- 459.重复的子字符串
- 字符串总结
- 双指针回顾
详细布置
151.翻转字符串里的单词
建议:这道题目基本把 刚刚做过的字符串操作 都覆盖了,不过就算知道解题思路,本题代码并不容易写,要多练一练。
题目链接/文章讲解/视频讲解:代码随想录
所以解题思路如下:
- 移除多余空格
- 将整个字符串反转
- 将每个单词反转
举个例子,源字符串为:"the sky is blue "
- 移除多余空格 : "the sky is blue"
- 字符串反转:"eulb si yks eht"
- 单词反转:"blue is sky the"
这样我们就完成了翻转字符串里的单词。
感觉算法的思路怎么这么巧妙呢
由于不知道开头中间结尾的空格究竟有多少个,所以去除空格确实有点麻烦,针对快慢指针的题目,完全可以把它想象成两个空间,一个输入字符串,一个输出字符串。快指针就一直遍历,慢指针用来誊抄。先看 if(s[i]==" ") if (slow != 0) s[slow++] = ' ';这行代码。这行代码是处理假如开头就空格的情况,slow是0,不满足条件,i++,直到快指针没遇到空格。然后if (slow != 0) 这行代码就没必要看了,因为已经解决开头是空格的情况,slow不为0。快指针遇到了字母后,慢指针抄下来。遇到空格以后,慢指针就不抄了,快指针i继续不断++,直到遇到单词,s[slow++] = ' ';即:s[slow] = ' ';slow++;slow抄写空格,然后继续++。这样就把全部抄完了。假如最后几个空格,i一直++遍历完,都是空格结束,slow就不抄了。结束。最后别忘了重新设置下字符串大小呢。
class Solution {
public:
void deleteSpace(string &s)
{
int slow =0;
for(int i=0;i<s.size();i++)
{
if(s[i]!=' ')
{
if (slow != 0) s[slow++] = ' ';
while(s[i]!=' '&&i<s.size())
s[slow++]=s[i++];
}
}
s.resize(slow);
}
void reverseWord (string &s,int start,int end)
{
while(start<end)
{
s[start]^=s[end];
s[end]^=s[start];
s[start]^=s[end];
start++;
end--;
}
}
void reverseAll(string &s)
{
reverseWord (s,0,s.size()-1);
}
string reverseWords(string s) {
deleteSpace(s);
reverseAll(s);
int start = 0, end = 0;
for(int i=0;i<s.size();i++)
{
if(s[i]==' ')
{
end=i-1;
reverseWord (s,start,end);
start=i+1;
}
}
reverseWord (s,start,s.size()-1);
return s;
}
};
s[i] == " "
在这行代码中,s[i]
是一个字符 (char
),而 " "
是一个 C 风格的字符串(实际上是一个指向字符串常量的指针 const char*
)。因此,你是在比较一个字符和一个字符串指针,这是不合法的。
卡码网:55.右旋转字符串
建议:题解中的解法如果没接触过的话,应该会想不到
题目链接/文章讲解:
为了让本题更有意义,提升一下本题难度:不能申请额外空间,只能在本串上操作。 (Java不能在字符串上修改,所以使用java一定要开辟新空间)
其实
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ||
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ||
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ||
9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | ||
8 | 9 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
其实讲了那么多,还是有点华丽呼哨,还是先看以上表格吧,假设“123456789”字符串移动两个位置,第二行,就是移动的结果,此时,就可以根据第三行,把字符分成两部分,part1和part2,咱们可以先来个全局转换,part1和part2调换下顺序,但是此时有个副作用,按理说part1和part2内部也调换了位置,因此再把内部负负得正就解决问题了。
整体代码如下:
#include<iostream>
#include<algorithm>
using namespace std;
int main() {
int n;
string s;
cin >> n;
cin >> s;
int len = s.size(); //获取长度
reverse(s.begin(), s.end()); // 整体反转
reverse(s.begin(), s.begin() + n); // 先反转前一段,长度n
reverse(s.begin() + n, s.end()); // 再反转后一段
cout << s << endl;
}
28. 实现 strStr() (本题可以跳过)
KMP算法确实太难了,当时花了两三天才搞明白,果然在这种复杂题上不要过分钻牛角尖,当时还不知道双指针,前缀后缀的思想,确实很头秃,现在才试着研究下。
给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。
示例 1: 输入: haystack = "hello", needle = "ll" 输出: 2
示例 2: 输入: haystack = "aaaaa", needle = "bba" 输出: -1
题目链接/文章讲解/视频讲解:代码随想录
首先我们要知道说明是前缀,什么是后缀。比如说"abcab","a","ab","abc","abca"是"abab"的前缀,注意"abcab"不是前缀,同理"b","ab"“cab”"bcab"是"abcab"的后缀,注意“abcab”不是后缀。通过这个例子就懂什么是前后缀。
然后要求对于一个字符串,公共最大前后缀。我们依次看“a”,由于前后缀不能是自身,所以根本没有前后缀,为0;“ab,前缀只有“a”,后缀只有b,没有公共前后缀,所以也为0;“abc”,没有公共前后缀,所以也为0;“abca”,有公共前后缀"a",所以为1;abcab有公共前后缀“ab”,所以为2
为什么求公共最大前后缀呢。大家想想,如果暴力法,在比对的时候,前面几个字符都匹配成功,但是最后的b和一个匹配失败的文本“X”对比,匹配失败。按照正常流程,就要从needle[0]重新开始匹配,a和b匹配失败,下一个,a和c匹配失败,再下一个,a和a匹配成功,很好,继续,但是问题是,我们知道匹配不成功的X前面是abca,我完全可以一步到位,直接跳到第三步。继续比较b和X。当一段文本大量存在“abcaX”的时候,这种方法就很方便。公共最大子前后缀,就是确定我应该跳到哪一步。所以我们的第一步就是求前后缀。
1 | 2 | 3 | 4 | 5 | a | b | c | a | X | ? | ? | ? | ? | ? |
a | b | c | a | b | ||||||||||
a | b | c | a | b | ||||||||||
a | b | c | a | b | ||||||||||
a | b | c | a | |||||||||||
a | b | c | a | |||||||||||
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
a | b | a | b | c | d | a | b | a | b | a | |
0 | 0 | 1 | 2 | 0 | 0 | 1 | 2 | 3 | 4 | 3 | |
-1 | -1 | 0 | 1 | -1 | -1 | 0 | 1 | 2 | 3 | 2 | |
a | b | a | b | c | d |
为什么要减一,因为为了和数组对应上,而且当为-1时也好进行判断,确定进入下一个循环。
j = next[j];毫无疑问是最难理解的,具体可以看上表,假设字符串"ababcdababa";只考虑最长子字符串。到了s[9]时,"abab"和"abab"前后缀相同,所以最大公共前后缀为4,再减去1为3。然后到s[10],第十位没有匹配上,j=next[j];j=next[3]=1,然后进行判断 if (s[i] == s[j ]+1),'a'=='a',j++,j=next[10]=2。
我们可以考虑,s[10]没有匹配上,当前9个字符的子字符串最大公共前后缀子字符串为3,说明只可能在"abab"中找最大前缀,我们要回退到"abab"中找新的前10个字符的子字符串。这时候我们知道前面s[0]-s[3]和s[6]-s[9]匹配上,那么我们就考虑可不可以,把它们剪切掉,对比小份的,这时候就要回退到next[j],s[3]对应的前缀"ab"和后缀"ab"相同,而ab也是s[8]的后缀,又一个相同,所以ab是原本第二小的公共前后缀,这时候我们再进行判断,if (s[i] == s[j + 1]),此时s[2]和s[10]相同,此时公共最大子字符串为3,next[10]=3-1=2;
通过上面的例子,我们发现代码还可以继续优化,因为我们发现我们可以求nextVal[],
在数值传递的时候&next[0]对应着int* next,这是一个指向next的指针,vector<int>& next对应着next,这是一个引用。
感觉是自己做过最难的题,花了两三天真™心烦,至少明白了为什么要回退,为什么最大前后缀子字符串为什么要减一,为什么比较的时候要+1,因为是最大前后缀子字符串之后的一个字符进行比较。另外,一定要先while后if,因为判断是最本质的需求,所以放在最后,否则前面可能运行了,影响后面的判断。
class Solution {
public:
void getNext(int* next, const string& s) {
int j=-1;
next[0]=j;
for(int i=1;i<s.size();i++)
{
while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
j = next[j]; // 向前回退
}
if(s[j+1]==s[i])
j++;
next[i]=j;
}
}
int strStr(string haystack, string needle)
{
vector<int> next(needle.size());
getNext(&next[0],needle);
int j=-1;
for(int i=0;i<haystack.size();i++)
{
while (haystack[i]!=needle[j+1]&&j>=0)//j+1是因为最大公共前后缀子字符串,然后要继续加1
{
j=next[j];
}
if(haystack[i]==needle[j+1])
{
j++;
if(j==needle.size()-1)
{
return i-j;
}
continue;
}
}
return -1;
}
};
字符串总结
比较简单,大家读一遍就行
题目链接/文章讲解:代码随想录
双指针回顾
此时我们已经做过10道双指针的题目了,来一起回顾一下,大家自己也总结一下双指针的心得
文章讲解:代码随想录