Day7. 字符串: 344.反转字符串 541.反转字符串Ⅱ 剑指offer 05.替换空格 151.翻转字符串里的单词 剑指offer 58

70 阅读4分钟

344.反转字符串

题目链接:344.反转字符串

本题也算是双指针法的一个经典应用了。

有序数组的平方,三数之和,四数之和也是两个指针逐步往中间移动。

可以自己做一个总结:双指针法的一个经典的移动过程。

我们应该头尾各有2个指针,这两个指针用来指向我们要进行两两交换的位置,然后这两个指针同时向中间移动。

07.01.gif

代码思路:

len = nums.size();

使用for循环交换字符:

你可能会想:为什么是 i < len;,而不是 i <= len; ?

A:遇到这种边界性的条件,想不明白时,代入一个具体的例子就能想懂了。

 for (int i = 0; j = len - 1; i < len / 2; i++, j--) {
     
 }

为什么没有写 j 移动的终止条件?

A:因为 ij 是同时移动的,我控制了 i ,相对来说就控制了 j

接下来就是交换2个元素,我们可以直接调用 swap()

 class Solution {
 public:
     void reverseString(vector<char>& s) {
         for (int i = 0, j = s.size() - 1; i < s.size() / 2; i++, j--) {
             swap(s[i], s[j]);
         }
     }
 };

541.反转字符串Ⅱ

题目链接:541.反转字符串II

与上一道题目是一脉相承的,只不过用到了上道题中反转字符串里面的实现的函数。

本题没有涉及到具体的算法,就是模拟在这个复杂规则下,让我们去反转字符串。

07.02.png

很多同学写这道题的代码很复杂,其实可以很简洁。

针对这道题,很多人写for循环很习惯这么写:

for (i = 0; i < s.size(); i++)

其实,你会发现这道题每次处理的时候都是以2k个、2k个、2k个……去遍历字符串,因此没必要写 i++, 而应该写 i += 2k;, 直接i 每次移动2k,然后每次操作里面的前k个。(即操作 ii + k 这样距离的一个字符串,进行反转)

代码思路:

i 每次移动2k,那么每次翻转就是 ii + k 这样的距离。

 for (i = 0; i < s.size(); i += 2k) {
     reverse(s, i, i + k);
 }

reverse() 实现的是:针对 s 这个字符串,从 i 开始,到 i + k 这个距离进行反转。

i ~ i + k,不包含 i + k ,因此是左闭右开区间。

一般来说,编程语言自己实现的库函数都是左闭右开规则。

(大家自己实现reverse函数的时候,可以左闭右开,也可以左闭右闭)

🦢🦢🦢🦢🦢🦢🦢🦢🦢🦢🦢🦢🦢🦢🦢🦢🦢🦢🦢🦢🦢🦢🦢🦢🦢🦢🦢🦢

在反转字符串的时候,有一个条件:

因为 i 每次移动2k个,那么移到最后的这一块不够 k 个该怎么办?

这个时候如果还直接对 ii + k 这个距离进行反转的话,就会操作到空的字符串了。

因此需要加一个判断条件: if (i + k < s.size()) ,条件成立,再进行反转。

要保证 i + k 在数组的范围内,不要超过这个数组的长度。

 for (i = 0; i < s.size(); i += 2k) {
     if (i + k < s.size()) {
         reverse(s, i, i + k);  //反转时不包含i + k
     }
 }

⚠️边界条件要注意!

细节拷问:应该写 if (i + k < s.size()) 还是 if (i + k <= s.size())

A:代一个例子就可以知道了 07:30

注:reverse() 里面,反转时不包含 i + k

因此:

 for (i = 0; i < s.size(); i += 2k) {
     if (i + k <= s.size()) {
         reverse(s, i, i + k);  //反转时不包含i + k
         continue;
     }
     reverse(s, i, s.size());
 }

有些同学把这道题目想复杂了,因为尾部处理的时候,还分凑足 k 个和凑不足 k 个这2种情况; 其实凑足 k 个的情况在if语句里面顺带处理了;没凑足 k 个的情况在外层 reverse() 处理了。

AC代码: (核心代码模式)

 class Solution {
 public:
     string reverseStr(string s, int k) {
         for (int i = 0; i < s.size(); i += (2 * k)) {
             //包含2种情况:
             //1.每隔2k个字符的前k个字符进行反转
             //2,剩余字符 < 2k,但>= k个,则反转前k个
             if (i + k <= s.size()) {
                 reverse(s.begin() + i, s.begin() + i + k);
             }
             else {
                 reverse(s.begin() + i, s.end());
             }
         }
         return s;
     }
 };

我们可以自己写一个 reverse()左闭右闭区间:

AC代码: (核心代码模式)

 class Solution {
 public:
     //实现自己的reverse函数
     void reverse(string& s, int start, int end) {
         for (int i = start, j = end; i < j; i++, j--) {
             swap(s[i], s[j]);
         }
     }
     string reverseStr(string s, int k) {
         for (int i = 0; i < s.size(); i += (2 * k)) {
             //包含2种情况:
             //1.每隔2k个字符的前k个字符进行反转
             //2,剩余字符 < 2k,但>= k个,则反转前k个
             if (i + k <= s.size()) {
                 reverse(s, i, i + k - 1);
                 continue;
             }
             reverse(s, i, s.size() - 1);
         }
         return s;
     }
 };

总结: 关键的思路:我们要想清楚在操作字符串或操作数组的时候,如果发现让我们一段一段地(2k)去处理,

那么这里面的i是可以成段成段地跳的。(没有必要老选择 i++ ,摒弃固性思维)

剑指offer 05.替换空格

题目链接:剑指offer 05.替换空格

07.03.gif

07.03.gif

AC代码: (核心代码模式)

 class Solution {
 public:
     string replaceSpace(string s) {
         int count = 0;
         int sOldSize = s.size();
         //统计空格个数
         for (int i = 0; i < s.size(); i++) {
             if (s[i] == ' ') {
                 count++;
             }
         }
         //扩充字符串s的大小,也就是每个空格替换成"%20"之后的大小
         s.resize(s.size() + count * 2);
         int sNewSize = s.size();
         //从后向前将空格替换成"%20"
         for (int i = sNewSize - 1, j = sOldSize - 1; j < i; i--, j--) {
             if (s[j] != ' ') {
                 s[i] = s[j];
             }
             else {
                 s[i] = '0';
                 s[i - 1] = '2';
                 s[i - 2] = '%';
                 i -= 2;
             }
         }
         return s;
     }
 };

151.翻转字符串里的单词

题目链接:151.反转字符串中的单词

难度指数:😀😕🙁

动画:

07.03.gif

整体思路: 将原字符串进行整体反转,反转之后,再对每个单词进行一次反转,这样就可以得到目标字符串。

复杂的地方在于:需要把多余的空格都删掉。

在数组章节里,"移除元素"那道题讲了在数组中如何移除元素,

那么本题要求是移除多余的空格,空格也属于元素,因此思路是一样的。

移除空格,可能很多同学会这么写:

 for () {
     if (空格) {
         erase();  //删除
     }
 }

erase() 的时间复杂度是O(n)

那么这整个移除操作的时间复杂度是O(n^2)

正常来说,对这个字符串进行移除元素,只需要O(n)就可以达到效果。

因为很多同学调用 erase() 只是删除一个元素,而这里相当于批量删除。

思路都是使用双指针:快指针,慢指针

  • 快指针指向我们想要获取的元素
  • 慢指针指向我们获取快指针获取的元素所指向的新的位置。

本题的空间复杂度控制在O(1),不申请一个新的数组。

快指针用来获取符合题目要求的字母;慢指针告诉我们获取这些字母之后,更新在哪里

代码思路:

slow一开始也是指向起始位置,

fast一定是要把字符串全遍历一遍。

找到不为空的字符,把它收集起来,然后赋给慢指针 slow 所指向的地方。 (慢指针告诉我们更新在哪里)

⚠️注意:把多余的空格移除的时候,需要保留单词之间的空格。(第一个单词前面不需要留空格) 这个逻辑需要特殊处理:

if (slow != 0) { //slow不指向第一个位置的时候 s[slow] = ' '; slow++; }

当慢指针不是起始位置的时候,慢指针所指向的位置要多留一个空格。

 slow = 0;
 for (fast = 0; fast < s.size(); fast++) {
     if (s[fast] != ' ') {  //如果这里的字符不为空,就把它收集起来
         if (slow != 0) {  //slow不指向第一个位置的时候
             s[slow] = ' ';  
             slow++;
         }
     }
 }

接下来,如果快指针不为空,应该把快指针指向的单词直接赋给慢指针:

 slow = 0;
 for (fast = 0; fast < s.size(); fast++) {
     if (s[fast] != ' ') {  //如果这里的字符不为空,就把它收集起来
         if (slow != 0) {  //slow不指向第一个位置的时候
             s[slow] = ' ';  
             slow++;
         }
         while (fast < s.size() && s[fast] != ' ') {
             s[slow] = s[fast];
             fast++;
             slow++;
         }
     }
     s.resize(slow);
 }

s.resize(slow);

因为我们是原地修改字符串,那么此时新的字符串就是 slow 所指向的位置。

AC代码: (核心代码模式)

 class Solution {
 public:
     //实现自己的reverse函数
     void reverse(string& s, int start, int end) {
         for (int i = start, j = end; i < j; i++, j--) {
             swap(s[i], s[j]);
         }
     }
 ​
     //去除所有空格并在相邻单词之间添加空格
     void removeExtraSpaces(string& s) {
         int slow = 0;
         for (int i = 0; i < s.size(); i++) {
             if (s[i] != ' ') {
                 if (slow != 0) {  //slow不指向第一个位置的时候
                     s[slow++] = ' ';
                 }
             }
             while (i < s.size() && s[i] != ' ') {
                 s[slow++] = s[i++];
             }
         }
         s.resize(slow);
     }
 ​
     string reverseWords(string s) {
         removeExtraSpaces(s);  //去除多余的空格,保证单词之间只有一个空格
         reverse(s, 0, s.size() - 1);
         int start = 0;
         for (int i = 0; i <= s.size(); i++) {
             if (i == s.size() || s[i] == ' ') {
                 reverse(s, start, i - 1);
                 start = i + 1;
             }
         }
         return s;
     }
 };

剑指offer 58.Ⅱ 左旋转字符串

AC代码: (核心代码模式)

 class Solution {
 public:
     string reverseLeftWords(string s, int n) {
         reverse(s.begin(), s.begin() + n);
         reverse(s.begin() + n, s.end());
         reverse(s.begin(), s.end());
         return s;
     }
 };