数据结构与算法
算法字符串双指针
字符串原地操作:整体反转与局部反转
字符串专题里有一类反复出现的模式:不给额外空间,在 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[] 原地操作的三条习惯
- 先转 char[] 再动手:
s.toCharArray()返回的是副本,随便改,不影响原串。 - 区间约定全文统一:统一用闭区间
[left, right],边界推导不容易乱。 - 记录有效长度:原地压缩后(如去空格),用 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 | 右旋字符串 | 局部反转与整体反转的组合运用 |