返回文章列表
数据结构与算法
算法字符串双指针

字符串原地操作:整体反转与局部反转

字符串专题里有一类反复出现的模式:不给额外空间,在 char[] 上完成反转、旋转、重组。本文先把最基础的双指针反转写成模板,再讲它的两个高阶组合用法——用反转做旋转,以及翻转字符串里的单词。

反转字符串(344):一切的起点

两个指针一头一尾,交换后向中间走:

class Solution {
    public void reverseString(char[] s) {
        int left = 0, right = s.length - 1;
        while (left < right) {
            char tmp = s[left];
            s[left++] = s[right];
            s[right--] = tmp;
        }
    }
}
  • 时间复杂度 O(n):n/2 次交换。
  • 空间复杂度 O(1):直接在数组上原地交换。

这个写法建议立刻抽象成辅助函数,后面反复复用:

// 反转 arr[left..right] 闭区间
private void reverse(char[] arr, int left, int right) {
    while (left < right) {
        char tmp = arr[left];
        arr[left++] = arr[right];
        arr[right--] = tmp;
    }
}

组合技巧一:两次反转实现旋转

目标示例:k = 2,"abcdefg" → "cdefgab",即把前 k 个字符整体挪到末尾(等价说法是把后 n-k 个字符转到前面,只是 k 的语义方向不同,本质是同一个问题)。

直接环形搬移字符容易出错,换成"反转"的视角:

原串(n=7, k=2):   a b c d e f g
① 整体反转      →   g f e d c b a
② 反转前 n-k 个  →   c d e f g b a    即 "cdefgab" ✓

为什么成立?整体反转把"前 k 个"送到了末尾、"后 n-k 个"送到了开头,但两段内部顺序各是倒的;再把前 n-k 个反转回来,全部字符就位。

public String rotate(String s, int k) {
    char[] arr = s.toCharArray();
    int n = arr.length;
    k %= n;                         // k 可能大于 n
    reverse(arr, 0, n - 1);         // ① 整体反转
    reverse(arr, 0, n - k - 1);     // ② 反转前 n-k 个
    return new String(arr);
}

顺带一提:卡码网 55「右旋字符串」的定义是"末尾 k 个字符移到前面",用同样的两步反转框架也能解决——整体反转后,反转前 k 个、再反转后 n-k 个即可;或者把本例的 k 换成 n-k。遇到旋转题,先想想能不能拆成"局部反转 + 整体反转"。

组合技巧二:翻转字符串里的单词(151)

题目:把 " the sky is blue " 变成 "blue is sky the",要求去掉首尾和中间多余空格,空间 O(1)。

用 split + 拼接当然能过,但那就浪费了这道题。原地解法是三步组合:

第一步:快慢指针去多余空格

fast 负责扫描,slow 指向"下一个可写位置"。单词之间手动补一个空格,首尾空格自然被丢弃:

fast 扫:  [空][空] t h e [空][空][空] s k y
slow 写:  t h e [空] s k y

第二步:整体反转

"the sky" → "yks eht"。单词之间的顺序对了,但每个单词内部是倒的。

第三步:逐词反转

把每个单词各自反转回来:"yks eht" → "sky the"。

class Solution {
    public String reverseWords(String s) {
        char[] arr = s.toCharArray();
        int n = arr.length;
 
        // 1. 快慢指针原地去除多余空格
        int slow = 0;
        for (int fast = 0; fast < n; fast++) {
            if (arr[fast] != ' ') {               // 遇到单词开头
                if (slow != 0) arr[slow++] = ' '; // 非首个单词,先补一个空格
                while (fast < n && arr[fast] != ' ') {
                    arr[slow++] = arr[fast++];    // 复制整个单词
                }
            }
        }
 
        // 2. 整体反转
        reverse(arr, 0, slow - 1);
 
        // 3. 逐个单词反转回来
        int start = 0;
        for (int i = 0; i <= slow; i++) {
            if (i == slow || arr[i] == ' ') {     // 单词边界(含末尾)
                reverse(arr, start, i - 1);
                start = i + 1;
            }
        }
        return new String(arr, 0, slow);
    }
 
    private void reverse(char[] arr, int left, int right) {
        while (left < right) {
            char tmp = arr[left];
            arr[left++] = arr[right];
            arr[right--] = tmp;
        }
    }
}

三步全是线性扫描,总时间 O(n)、额外空间 O(1)。

Java 里 char[] 原地操作的三条习惯

  1. 先转 char[] 再动手:s.toCharArray() 返回的是副本,随便改,不影响原串。
  2. 区间约定全文统一:统一用闭区间 [left, right],边界推导不容易乱。
  3. 记录有效长度:原地压缩后(如去空格),用 slow 表示新长度,最后 new String(arr, 0, slow) 截取,不要依赖 arr.length。

复杂度与易错点

操作 时间 空间
双指针反转 O(n) O(1)
两次反转旋转 O(n) O(1)
151 三步组合 O(n) O(1)

易错点清单:

  • 旋转前忘记 k %= n,k 大于串长时下标直接越界。
  • 151 去空格时补空格的时机:应在复制单词之前判断 slow != 0,写在复制之后就晚了。
  • 逐词反转的循环要跑到 i == slow,否则最后一个单词不会被反转。
  • 原地题里混用 String 拼接,空间和时间的 O(1)/O(n) 全部作废。

题目清单

题号 题名 一句话考点
344 反转字符串 双指针原地反转模板
541 反转字符串 II 每 2k 个字符处理前 k 个,分组边界
151 翻转字符串里的单词 去空格 + 整体反转 + 逐词反转
卡码网 55 右旋字符串 局部反转与整体反转的组合运用

参考