:::tips 作者:LemonCat🐱
时间:2023-8-2 :::

第四章 字符串part01

:::info

  • 344.反转字符串
    1. 反转字符串II
  • 剑指Offer 05.替换空格
  • 151.翻转字符串里的单词
  • 剑指Offer58-II.左旋转字符串 :::

344.反转字符串

:::tips 建议: 本题是字符串基础题目,就是考察 reverse 函数的实现,同时也明确一下
平时刷题什么时候用 库函数,什么时候 不用库函数
题目链接:力扣 344. 反转字符串
文章讲解/视频讲解:代码随想录 :::

方法 - 经典双指针

  • 在反转链表(LC 206.反转链表)中,使用了双指针的方法。
  • 那么反转字符串依然是使用双指针的方法,只不过对于字符串的反转,其实要比链表简单一些。
  • 因为字符串也是一种数组,所以元素在内存中是连续分布,而链表在内存中不是连续分布的,这就决定了反转链表和反转字符串方式上还是有所差异的。
  • 对于字符串,我们定义两个指针(也可以说是索引下标)
    1. 一个从字符串前面 - left
    2. 一个从字符串后面 - right
    3. 两个指针同时向中间移动,并交换元素。

以字符串hello为例,过程如下:
代码随想录 | 第四章 - 字符串 - 图1

  1. /**
  2. * 方法 - 经典双指针
  3. */
  4. class Solution555 {
  5. public void reverseString(char[] s) {
  6. int left = 0; // 左指针
  7. int right = s.length-1; // 右指针
  8. int i = 0; // 索引
  9. char tempC;
  10. // 反转字符 - 当 left < right
  11. while (left < right) {
  12. tempC = s[left];
  13. s[left++] = s[right];
  14. s[right--] = tempC;
  15. }
  16. }
  17. }

优化 - 位运算交换

参考资料:位运算(&、|、^、~、>>、 | 菜鸟教程

  • 我们采用位运算来完成交换
    • ^ -> 两个位相同为0,相异为1
    • ^ -> 自反性: a^b^b=a^0=a; ```java /**
  • 双指针 - 位运算交换优化 */ class Solution { public void reverseString(char[] s) {

    1. int left = 0; // 左指针
    2. int right = s.length - 1; // 右指针
    3. // 位运算
    4. while (left < right) {
    5. s[left] ^= s[right]; //构造 a ^ b 的结果,并放在 a 中
    6. s[right] ^= s[left]; //将 a ^ b 这一结果再 ^ b ,存入b中,此时 b = a, a = a ^ b
    7. s[left] ^= s[right]; //a ^ b 的结果再 ^ a ,存入 a 中,此时 b = a, a = b 完成交换
    8. left++;
    9. right--;
    10. }

    } } ```

    541. 反转字符串II

    :::tips 建议:本题又进阶了,自己先去独立做一做,然后在看题解,对代码技巧会有很深的体会。
    题目链接:力扣 541. 反转字符串 II
    文章讲解/视频讲解:代码随想录 :::

方法 - 双指针

  1. 思路基本与 344.反转字符串 保持一致 -> 核心都是双指针
  2. 每隔2k个反转前k个
    1. 尾数 大于等于 k 时只反转前 k 个
    2. 尾数不够 k 个时候全部反转
  3. 其实只需在遍历字符串的过程中

    1. 让 i += (2 k),i 每次移动 2 k 就可以了 -> for (int i = 0; i < sChar.length; i += 2 * k)
    2. 然后判断是否需要有反转的区间
    3. 这样就会使得代码更加简洁

      1. class Solution {
      2. public String reverseStr(String s, int k) {
      3. char[] sChar = s.toCharArray();
      4. for (int i = 0; i < sChar.length; i += 2 * k) {
      5. int left; // 左指针
      6. int right; // 右指针
      7. // 进行判断结尾剩余情况
      8. // 给出合适的 左右指针
      9. if (sChar.length - i < k) { // 当结尾个数小于 k 时
      10. left = i;
      11. right = sChar.length - 1;
      12. } else { // 当结尾个数大于等于 k 时
      13. left = i;
      14. right = left + k - 1;
      15. }
      16. // 位运算进行交换
      17. while (left < right) {
      18. sChar[left] ^= sChar[right]; //构造 a ^ b 的结果,并放在 a 中
      19. sChar[right] ^= sChar[left]; //将 a ^ b 这一结果再 ^ b ,存入b中,此时 b = a, a = a ^ b
      20. sChar[left] ^= sChar[right]; //a ^ b 的结果再 ^ a ,存入 a 中,此时 b = a, a = b 完成交换
      21. left++;
      22. right--;
      23. }
      24. }
      25. return new String(sChar);
      26. }
      27. }
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

    小优化

  • 判断尾数够不够 k 个来取决右指针的位置

原来写法:

  1. // 方法一
  2. if (sChar.length - i < k) { // 当结尾个数小于 k 时
  3. left = i;
  4. right = sChar.length - 1;
  5. } else { // 当结尾个数大于等于 k 时
  6. left = i;
  7. right = left + k - 1;
  8. }

更加简洁的写法:

  1. // 方法二
  2. left = i;
  3. right = Math.min(sChar.length - 1,left + k - 1);

剑指Offer 05.替换空格

:::tips 建议:对于线性数据结构,填充或者删除,后序处理会高效的多。好好体会一下。
题目链接:力扣 剑指 Offer 05. 替换空格
文章讲解:代码随想录 :::

方法一 - 扩容 + 双指针法:

极致处理就是 不使用额外的辅助空间了:

  1. 首先扩充数组到每个空格替换成”%20”之后的大小。 ```java StringBuilder sb = new StringBuilder();

// 扩充空间,空格数量2倍 for (int i = 0; i < s.length(); i++) { if (‘ ‘ == s.charAt(i)) { sb.append(“ “); // 两个字符就是字符串了,所以用双引号 } }

  1. 2. 然后从后向前替换空格,也就是双指针法
  2. 1. 过程如下:left 指向新长度的末尾,right 指向旧长度的末尾。
  3. ![](https://cdn.nlark.com/yuque/0/2023/gif/25477887/1691040770308-1e8ebed1-5593-4a07-8d45-8485e24b8d3c.gif#averageHue=%23fcfcfc&clientId=u07fac3f7-56dc-4&from=paste&id=u70c5edaa&originHeight=346&originWidth=498&originalType=url&ratio=1.5&rotation=0&showTitle=false&status=done&style=none&taskId=u23f86e9f-313d-4c1a-8873-298de0bfa8a&title=)
  4. - 为什么要从后向前填充,从前向后填充不行么?
  5. - 从前向后填充就是 O(n2) 的算法了,因为每次添加元素都要将添加元素之后的所有元素向后移动。
  6. **其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。**<br />这么做有两个好处:
  7. 1. 不用申请新数组。
  8. 2. 从后向前填充元素,避免了从前向后填充元素时,每次添加元素都要将添加元素之后的所有元素向后移动的问题。
  9. ```java
  10. /**
  11. * 扩容 + 双指针
  12. */
  13. class Solution {
  14. public String replaceSpace(String s) {
  15. // 提前返回
  16. if (s == null) {
  17. return s;
  18. }
  19. StringBuilder sb = new StringBuilder();
  20. // 扩充空间,空格数量2倍
  21. for (int i = 0; i < s.length(); i++) {
  22. if (' ' == s.charAt(i)) {
  23. sb.append(" "); // 两个字符就是字符串了,所以用双引号
  24. }
  25. }
  26. // 若是没有空格直接返回
  27. if (sb.length() == 0) {
  28. return s;
  29. }
  30. s += sb.toString();// 将原来的字符串扩容到预订大小
  31. int right = s.length() - 1; // 左指针:指向原始字符串最后一个位置
  32. int left = s.length() - sb.length() - 1; // 右指针:指向扩展字符串的最后一个位置
  33. char[] sChars = s.toCharArray();
  34. while (left >= 0) {
  35. if (sChars[left] == ' ') {
  36. sChars[right--] = '0';
  37. sChars[right--] = '2';
  38. sChars[right--] = '%';
  39. } else {
  40. sChars[right--] = sChars[left];
  41. }
  42. left--;
  43. }
  44. return new String(sChars); // 字符数组转化成字符串
  45. }
  46. }

方法二 - StringBuilder

  • 用 StringBuilder 遇到 ‘ ‘ 直接进行替换 ```java /**
  • StringBuilder */ class Solution {

    public String replaceSpace(String s) {

    1. // 提前返回
    2. if (s == null) {
    3. return s;
    4. }
    5. // 选用 StringBuilder 单线程使用,比较快,
    6. StringBuilder sb = new StringBuilder();
    7. char[] sChar = s.toCharArray();
    8. // 使用 sb 逐个复制 s ,碰到空格则替换,否则直接复制
    9. for (int i = 0; i < sChar.length; i++) {
    10. if (sChar[i] == ' ') {
    11. sb.append("%20");
    12. } else {
    13. sb.append(sChar[i]);
    14. }
    15. }
    16. return sb.toString();

    } } ```

151.翻转字符串里的单词

:::danger

  • 这道题目综合考察了字符串的多种操作 重点学习!!! ::: :::tips 建议:这道题目基本把 刚刚做过的字符串操作 都覆盖了,不过就算知道解题思路,本题代码并不容易写,要多练一练。
    题目链接:力扣 151. 反转字符串中的单词
    文章讲解/视频讲解:代码随想录 :::

    方法一 - 不使用Java内置方法实现

  1. 去除首尾以及中间多余空格
  2. 反转整个字符串
  3. 反转各个单词 ```java /**

    • 方法一 - 不使用Java内置方法实现
      1. 去除首尾以及中间多余空格
      1. 反转整个字符串
      1. 反转各个单词 */ class Solution1 { public String reverseWords(String s) {

        // 1.去除首尾以及中间多余空格 StringBuilder sb = removeSpace(s); // 2.反转整个字符串 reverseString(sb, 0, sb.length() - 1); // 3.反转各个单词 reverseEachWord(sb); return sb.toString(); }

      /**

        1. 去除首尾以及中间多余空格 *
      • @param s
      • @return */ private StringBuilder removeSpace(String s) {

        int start = 0; // 起始指针 int end = s.length() - 1; // 结束指针

        while (s.charAt(start) == ‘ ‘) start++; // 将起始指针移动到 非空格字符 while (s.charAt(end) == ‘ ‘) end—; // 将结束指针移动到 非空格字符

        StringBuilder sb = new StringBuilder();

        // 从开始指针 start 逐个遍历到 end while (start <= end) {

        1. char c = s.charAt(start);
        2. // 妙!即如果不是空格 或者 拼接的字符串结尾字符不是空格 -> 都进行拼接
        3. if (c != ' ' || sb.charAt(sb.length() - 1) != ' ') {
        4. sb.append(c);
        5. }
        6. start++;

        } return sb; }

      /**

        1. 反转整个字符串 - 指定区间[start, end]的字符 *
      • @param sb
      • @param start
      • @param end */ private void reverseString(StringBuilder sb, int start, int end) {

        char tempC; // 临时字符

        while (start < end) {

        1. tempC = sb.charAt(start);
        2. sb.setCharAt(start, sb.charAt(end)); // setCharAt() -> 设置指定索引的数值
        3. sb.setCharAt(end, tempC);
        4. start++;
        5. end--;

        } }

      /**

        1. 反转各个单词 *
      • @param sb */ private void reverseEachWord(StringBuilder sb) { int start = 0; int end = 1; int length = sb.length(); while (start < length) {

        1. while (end < length && sb.charAt(end) != ' ') {
        2. end++;
        3. }
        4. reverseString(sb, start, end - 1); // 单个单词的反转
        5. start = end + 1;
        6. end = start + 1;

        } }

}

  1. <a name="fwljh"></a>
  2. ### 方法二 - 创建新字符数组填充法
  3. :::danger
  4. - 整不动了 有时间再细细研究~
  5. :::
  6. ```java
  7. /**
  8. * 方法二 - 创建新字符数组填充
  9. */
  10. class Solution2 {
  11. public String reverseWords(String s) {
  12. //源字符数组
  13. char[] initialArr = s.toCharArray();
  14. //新字符数组
  15. char[] newArr = new char[initialArr.length + 1];//下面循环添加"单词 ",最终末尾的空格不会返回
  16. int newArrPos = 0;
  17. //i来进行整体对源字符数组从后往前遍历
  18. int i = initialArr.length - 1;
  19. while (i >= 0) {
  20. // 跳过空格
  21. while (i >= 0 && initialArr[i] == ' ') {
  22. i--;
  23. }
  24. // 此时i位置是边界或!=空格,先记录当前索引,之后的while用来确定单词的首字母的位置
  25. int right = i;
  26. while (i >= 0 && initialArr[i] != ' ') {
  27. i--;
  28. }
  29. // 指定区间单词取出(由于i为首字母的前一位,所以这里+1,),取出的每组末尾都带有一个空格
  30. for (int j = i + 1; j <= right; j++) {
  31. newArr[newArrPos++] = initialArr[j];
  32. if (j == right) {
  33. newArr[newArrPos++] = ' ';//空格
  34. }
  35. }
  36. }
  37. //若是原始字符串没有单词,直接返回空字符串;若是有单词,返回0-末尾空格索引前范围的字符数组(转成String返回)
  38. if (newArrPos == 0) {
  39. return "";
  40. } else {
  41. return new String(newArr, 0, newArrPos - 1);
  42. }
  43. }
  44. }
  • 时间复杂度O(n)

    方法 —- 其他方法

  • 还有两种方式 以后有时间再补充~

剑指Offer58-II.左旋转字符串

:::tips 建议:题解中的解法如果没接触过的话,应该会想不到
题目链接:力扣 剑指 Offer 58 - II. 左旋转字符串
文章讲解:代码随想录 :::

方法一 - 局部反转 + 整体反转

用原始数组来进行反转操作:

  1. 反转区间为前n的子串
  2. 反转区间为n到末尾的子串
  3. 反转整个字符串

例如 :示例1中 输入:字符串abcdefg,n=2
如图:
image.png

  • 先整个字符串反转,再反转前面的,最后反转后面 n 个 ```java /**
  • 方法一 - 局部反转 + 整体反转
  • 不开辟新数组 空间复杂度O(1) */ class Solution { public String reverseLeftWords(String s, int n) {

    1. char[] chars = s.toCharArray();
    2. reverse(chars, 0, chars.length - 1);
    3. reverse(chars, 0, chars.length - 1 - n);
    4. reverse(chars, chars.length - n, chars.length - 1);
    5. return new String(chars);

    }

    private void reverse(char[] sChar, int start, int end) {

    1. while (start < end) {
    2. sChar[start] ^= sChar[end];
    3. sChar[end] ^= sChar[start];
    4. sChar[start] ^= sChar[end];
    5. start++;
    6. end--;
    7. }

    } } ```

  • 空间复杂度 O(1)

第四章 字符串part02

:::info

双指针回顾

:::tips 文章讲解:代码随想录 :::