← 返回题目列表

如何翻转字符串里的单词?(LeetCode 151)

高频 中等 第 6 / 25 题 更新于 2026/07/28
字符串算法翻转单词双指针原地操作

简化版

给一个字符串(单词间可能有多个空格、首尾也可能有空格),把单词顺序翻转,返回结果(单词内部字母不变,单词间只保留一个空格,去掉首尾空格)。如 " the sky is blue ""blue is sky the"。最简单:用语言内置的 split 按空格分词、过滤空串、反转拼接。进阶的原地 O(1) 空间做法(针对字符数组):先整体翻转整个字符串,再逐个翻转每个单词,最后清理多余空格。

详细版

解法一:库函数(简洁)

String reverseWords(String s) {
    String[] words = s.trim().split("\\s+");   // 去首尾空格,按连续空格分词
    Collections.reverse(Arrays.asList(words));
    return String.join(" ", words);
}

解法二:原地翻转(O(1) 额外空间,面试考点)

// 思路(针对可变字符数组):
// 1. 去掉多余空格(首尾 + 单词间只留一个)
// 2. 整体翻转整个字符数组:"the sky" -> "yks eht"
// 3. 再翻转每个单词:"yks eht" -> "sky the"
  • 解法一trim() 去首尾,split("\\s+") 按一个或多个空格切分,反转后用单空格拼接。O(n)。
  • 解法二:「整体翻转 + 逐词翻转」两次翻转抵消单词内部顺序、只翻转单词间顺序。经典技巧。
  • 复杂度:O(n) 时间;解法一 O(n) 空间,解法二可 O(1)(原地)。

完整版教学

一、题目的坑:空格处理

这题看似简单,坑在空格

  • 首尾可能有空格,要去掉
  • 单词之间可能有多个连续空格,结果只保留一个

所以核心是「提取出所有非空单词,反转它们的顺序,用单个空格连接」。空格处理不干净是最常见的失分点。

二、解法一:split + reverse

利用库函数最省事:

  • s.trim():去掉首尾空格。
  • split("\\s+"):按「一个或多个空白字符」切分,自动合并多空格、跳过空串(trim 后不会有前导空串)。得到纯单词数组。
  • 反转数组 + String.join(" ", ...) 用单空格拼回。

简洁可靠,面试若不特别要求 O(1) 空间,这就够。注意 split(" ")(单空格)会在多空格处产生空串,必须用 "\\s+"

三、解法二:两次翻转(O(1) 空间经典技巧)

面试常追问「能否 O(1) 额外空间」(针对 C++/字符数组可变的语言)。经典做法是两次翻转

  1. 先整体翻转整个字符串"the sky is blue""eulb si yks eht"。此时单词的顺序已经反过来了,但每个单词内部的字母也被翻转了(拼写倒了)。
  2. 再逐个翻转每个单词:把每个单词内部再翻转一次,字母顺序恢复正常:"eulb"→"blue""yks"→"sky"……得到 "blue is sky the"

两次翻转的精髓:整体翻转搞定「单词间顺序」,逐词翻转「抵消」掉单词内部被翻转的副作用。加上原地清理空格(用双指针把多余空格压掉),可实现 O(1) 额外空间。

这个「整体翻转 + 局部翻转」是字符串/数组翻转的通用套路,轮转数组(189)也用它(翻转整体 + 分段翻转实现 O(1) 空间轮转)。

四、原地清理空格的双指针

原地做法里,去多余空格用快慢指针:慢指针 slow 指向写入位置,快指针 fast 扫描;跳过多余空格,单词之间只写一个空格,首尾不写。这样把清理、翻转都在原数组上完成,不额外开数组。细节较多,面试能讲清「两次翻转 + 双指针清空格」的思路即可,未必要写全。

五、易错点

  • split(" ") vs split("\\s+"):前者遇多空格会切出空串,必须用后者(或先 trim + 后者)。
  • 忘记 trim 首尾:结果会多出首尾空格。
  • 拼接用单空格String.join(" ", ...),别把原来的多空格带回来。
  • 语言差异:Java 的 String 不可变,「O(1) 空间原地」严格说要转成 char[]StringBuilder 操作;面试说清思路即可。

六、先规范空格,再证明两次翻转

原地方案先把多个空格压成单空格并去掉首尾空格,这一步确定了单词边界。整体翻转改变单词顺序但也反转每个单词内部;随后逐个翻转单词,内部字符恢复,而单词的全局逆序保持不变。

原串:"  the   sky is blue  "
清理空格:"the sky is blue"
整体翻转:"eulb si yks eht"
逐词翻转:"blue is sky the"
单词间恰好一个空格
没有前导或尾随空格
字符数组方案可原地完成核心变换
校验维度本题必须保持的结论
循环/递推不变量清理后单词由单个空格分隔;整体翻转后单词块顺序已逆转,局部翻转不改变块顺序。
边界条件全为空格的输入清理后为空;Java String 不可变,所谓 O(1) 额外空间通常针对可变字符数组。
复杂度与代价每个字符参与常数次移动,时间 O(n);split 方案通常需要 O(n) 辅助数组。
自检方式用最小输入、典型输入和触及边界的输入分别手推,结果应与定义一致。

这组推演的作用不是替代证明,而是把抽象规则落到每一步状态变化上。面试写完代码后,应主动指出不变量如何保证不漏解,以及边界为何不会越界或死循环。若手推结果与定义不一致,应先修正状态语义,而不是继续给代码打补丁。

七、面试现场如何验证这道题

字符串题首先要固定“处理的是字符、UTF-16 代码单元还是 Unicode 码点”,再讨论下标与窗口。本题应先复述这条不变量:清理后单词由单个空格分隔;整体翻转后单词块顺序已逆转,局部翻转不改变块顺序。只有代码的初始化、更新顺序和终止条件都维持它,样例通过才具有证明力。

  • 典型路径从“原串:” the sky is blue “”开始手推,最后应得到“字符数组方案可原地完成核心变换”。
  • 边界复核:全为空格的输入清理后为空;Java String 不可变,所谓 O(1) 额外空间通常针对可变字符数组。
  • 代价复核:每个字符参与常数次移动,时间 O(n);split 方案通常需要 O(n) 辅助数组。
  • 用空串、单字符、全相同字符和首尾命中检查下标边界。
  • 涉及窗口或双指针时,明确区间是闭区间还是左闭右开,并在移动后再判断长度。
  • 涉及计数数组时,先确认题目字符集;超出小写英文字母就不能硬套 26 个桶。

最后要主动构造一个“少写一次循环、改变一个不等号或交换两行更新就会失败”的反例。能说清反例破坏了哪条不变量,比只报出复杂度更能证明真正掌握了算法。

记忆钩子:本题的代码可以压缩,但“清理后单词由单个空格分隔;整体翻转后单词块顺序已逆转,局部翻转不改变块顺序。”这条正确性主线不能省。

八、常见误区与追问

  • 误区:直接整体翻转字符串就是答案。 它也会把每个单词内部字符反转,还需逐词恢复。
  • 误区:按单个空格 split 就天然处理连续空格。 不同语言的 split 语义不同,可能产生空 token,应明确过滤或用正则。
  • 误区:Java 字符串方案可以真正 O(1) 空间。 String 不可变,转换字符数组和构造结果都会占用 O(n) 空间。
  • 追问:为何先清理空格更容易? 统一分隔符后,单词边界扫描和最终格式都不再需要特殊分支。
  • 追问:若只需保留原空格布局怎么办? 题意改变后不能压缩空格,需要记录每段空白或按 token 原样重排。
  • 追问:Unicode 代理对会受 char 反转影响吗? Java char 按 UTF-16 代码单元处理,直接反转可能拆开代理对,需按码点处理。

九、加强记忆

翻转字符串里的单词 = 提取单词、反转顺序、单空格连接,坑在空格处理(去首尾、合并中间多空格)。解法一trim() + split("\\s+") + 反转 + join(" "),简洁。解法二(O(1) 空间经典)整体翻转整个串 + 逐个翻转每个单词——整体翻转定「单词间顺序」,逐词翻转「抵消」单词内字母被翻转的副作用,再用双指针原地清理多余空格。这个「整体翻转+局部翻转」套路也用于轮转数组。核心:两次翻转 + 空格要清干净