LeetCode 151. 反转字符串中的单词
题目描述


题意分析
将字符串中的单词按相反顺序排列,但每个单词内部的字符顺序保持不变。单词是连续的非空格字符,题目中的字符只有英文字母、数字和普通空格,并保证至少包含一个单词。
原字符串可能有前导空格、尾随空格,以及单词之间的多个空格。结果必须去掉首尾空格,并且相邻单词之间恰好保留一个空格,所以不能简单反转整串字符或原样保留分隔符。
题目的常数额外空间进阶以可变字符串为前提。本篇使用的 Java
String和 Gostring都不可变,下面用输出缓冲区构造新字符串,空间复杂度为 $O(n)$。
解法:从右向左提取单词
核心思路
[!blue]
原来的最后一个单词应最先输出,因此从右向左扫描,就能直接按照目标顺序找到各个单词。反向的是寻找单词的顺序,找到一个单词后仍按它在原字符串中的从左到右顺序复制,这样单词内部不会被反转。
用
i指向尚未处理部分的最右侧。每轮先跳过连续空格;如果此时i < 0,说明已经没有单词,立即结束。否则用end = i保存当前单词的右端点,再继续左移i,直到遇到空格或越过开头。此时
i停在单词左侧,单词实际范围是闭区间[i + 1, end]。Java 追加子串和 Go 切片都使用右端不包含的范围,因此应读取[i + 1, end + 1)。当前单词已经完整复制后,下一轮从它左侧继续扫描,不会漏掉或重复读取单词。不复制原来的空格,而是在已经输出过单词时,先追加一个空格,再追加当前单词。第一个单词前不加空格,最后一个单词后也不主动加空格;任意两个单词之间只在追加后者时加入一个空格,就同时满足了去除首尾空格和压缩连续空格的要求。
指针只向左移动,输出缓冲区只在末尾追加。每个字符被扫描和复制的次数都有固定上界,因此无需拆分出全部单词,也无需对结果再次反转。
解题步骤
- 创建输出缓冲区,将
i放在字符串最后一个字符处。- 向左跳过空格;若
i < 0,结束扫描。- 保存
end = i,继续向左越过本单词的全部非空格字符。- 如果结果非空,先补一个分隔空格,再追加原字符串的
[i + 1, end + 1)。- 重复上述过程,最后把缓冲区转为字符串返回。
代码实现
class Solution {
public String reverseWords(String s) {
StringBuilder ans = new StringBuilder();
int i = s.length() - 1;
while (i >= 0) {
while (i >= 0 && s.charAt(i) == ' ') {
i--;
}
// 跳完空格可能已经越过开头,不能继续读取单词。
if (i < 0) {
break;
}
int end = i;
while (i >= 0 && s.charAt(i) != ' ') {
i--;
}
// 仅在已经输出过单词时补一个分隔空格,避免首尾多余空格。
if (ans.length() > 0) {
ans.append(' ');
}
// 扫描停在单词之前,截取区间需要同时包含两端字符。
ans.append(s, i + 1, end + 1);
}
return ans.toString();
}
}
func reverseWords(s string) string {
ans := make([]byte, 0, len(s))
for i := len(s) - 1; i >= 0; {
for i >= 0 && s[i] == ' ' {
i--
}
// 跳完空格可能已经越过开头,不能继续读取单词。
if i < 0 {
break
}
end := i
for i >= 0 && s[i] != ' ' {
i--
}
// 仅在已经输出过单词时补一个分隔空格,避免首尾多余空格。
if len(ans) > 0 {
ans = append(ans, ' ')
}
// 扫描停在单词之前,截取区间需要同时包含两端字符。
ans = append(ans, s[i+1:end+1]...)
}
return string(ans)
}
复杂度分析
- 时间复杂度:$O(n)$,指针只从右向左扫描一次,每个非空格字符只追加一次,最终转换字符串也为线性时间。
- 空间复杂度:$O(n)$,用于输出缓冲区和返回字符串。
关键点总结
[!green]
- 反向扫描可直接得到反转后的单词顺序,无需额外翻转。
- 每轮先跳空格,再提取单词,并在两个阶段都检查边界。
- 空格由结果主动添加,保证单词之间恰好一个空格。
- 若输入本身是可修改的字符数组,可先原地压缩空格,再反转全部有效字符,最后逐个反转单词:第一次改变单词顺序,第二次恢复单词内部顺序,额外只需常数个指针。把不可变字符串复制成数组仍会占用线性空间。
易错点总结
[!yellow]
- 跳过空格后未检查指针是否越界,会在前导空格处访问非法下标。
- 单词左端点应为
i + 1,右端点截取时应包含end。- 每个单词后都追加空格,会留下尾随空格。
- Java 使用字符串
+=反复拼接,最坏会退化为 $O(n^2)$。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 186. 反转字符串中的单词 II | 中等 | 单词顺序翻转目标相同,原题以可修改字符数组输入,要求原地完成。 |
| 58. 最后一个单词的长度 | 简单 | 同样需要先处理空格并定位单词边界,原题只关心最后一个词的长度。 |
| 557. 反转字符串中的单词 III | 简单 | 反转单词系列。I 改变单词顺序并保留词内顺序,III 保留词序并反转词内字符,均需识别单词边界。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!