D

当前:LC151 · 反转字符串中的单词 · 首次出现于 Day 15 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC151

反转字符串中的单词

字符串双指针空格压缩整体反转 + 局部反转

给定字符串 s,反转其中单词的顺序,并删除首尾空格、把多个连续空格压缩成一个空格。不是简单 reverse 所有字符。

样例 1
输入:" hello world "
输出:"world hello"
样例 2
输入:"the sky is blue"
输出:"blue is sky the"
当前用例
" hello world "
先压缩空格,再整体反转 + 逐词反转
最终答案
"world hello"
复杂度
时间 O(n)
空间 O(n) 工程版 · O(1) follow-up 额外空间

两个测试用例 · 多空格 vs 干净输入

题目压缩空格整体反转逐词反转结论
题目Step 1/8
Step 1 · 题目输入
当前发生了什么

Step 1 · 题目输入

给定原始字符串 " hello world "。注意首尾有多余空格,中间 "hello" 和 "world" 之间也有连续空格。目标不是反转所有字符,而是反转单词顺序,并把空格规范成「无首尾空格、单词间仅一个空格」。

机器状态

阶段
输入
output
" hello world "

为什么正确

LC151 考的是词序反转 + 空格规范化,不是简单 reverse 整串。

面试该怎么说 · 工程写法:使用 strings.Fields(s) 得到单词数组,再双指针反转数组,最后 strings.Join(words, " ")。时间 O(n),空间 O(n)。

字符串可视化 · 输入

先压缩空格,再整体反转 + 逐词反转
0
1
h
2
e
3
l
4
l
5
o
6
7
8
9
w
10
o
11
r
12
l
13
d
14
15
16
当前字符串
"␣␣hello␣␣␣world␣␣"
多余/首尾空格fastslow当前单词

执行轨迹

  • raw = " hello world "
  • 目标 → "world hello"

Go 代码 · 工程可读版

题目
版本 A · 工程可读版
1// 版本 A · 工程可读版 — strings.Fields + 双指针反转数组
2func reverseWords(s string) string {
3 words := strings.Fields(s) // 自动 trim + 过滤空串
4 for l, r := 0, len(words)-1; l < r; l, r = l+1, r-1 {
5 words[l], words[r] = words[r], words[l]
6 }
7 return strings.Join(words, " ")
8}
版本 B · follow-up 原地 O(1)
10// 版本 B · follow-up — []byte 原地:压缩空格 → 整体反转 → 逐词反转
11func reverseWordsFollowUp(s string) string {
12 b := []byte(s)
13 n := cleanSpaces(b) // fast/slow 压缩空格
14 reverseRange(b, 0, n-1) // 整体反转
15 reverseEachWord(b, n) // wordStart/wordEnd 逐词反转
16 return string(b[:n])
17}
18
19func cleanSpaces(b []byte) int {
20 slow, needSpace := 0, false
21 for fast := 0; fast < len(b); fast++ {
22 if b[fast] != ' ' {
23 if needSpace {
24 b[slow] = ' '; slow++; needSpace = false
25 }
26 b[slow] = b[fast]; slow++
27 } else if slow > 0 {
28 needSpace = true // 单词之间才写入一个空格
29 }
30 }
31 return slow
32}
33
34func reverseRange(b []byte, left, right int) {
35 for left < right {
36 b[left], b[right] = b[right], b[left]
37 left++; right--
38 }
39}
40
41func reverseEachWord(b []byte, n int) {
42 start := 0
43 for i := 0; i <= n; i++ {
44 if i == n || b[i] == ' ' {
45 reverseRange(b, start, i-1) // 反转 [wordStart, wordEnd]
46 start = i + 1
47 }
48 }
49}

不变量

  • a. 压缩空格后,字符串中没有首尾空格,单词之间只有一个空格。
  • b. 整体反转会改变单词顺序。
  • c. 逐词反转会恢复每个单词内部顺序。
  • d. 两次反转组合后,得到「单词顺序反转、单词内部不变」的结果。

8 步剧本 · Step 1/8

为什么「整体反转 + 逐词反转」能得到答案?

压缩空格保证输出格式合法。整体反转把最后一个单词移动到最前面。逐词反转只修复每个单词内部字符顺序,不改变单词之间的相对位置。因此最终结果满足:单词顺序反转,单词内部顺序不变,空格格式合法。

"hello world"
→ 整体反转 → "dlrow olleh"(词序反 + 词内反)
→ 逐词反转 → "world hello"(词内恢复,词序保持)

面试怎么答

工程写法

工程写法:使用 strings.Fields(s) 得到单词数组,再双指针反转数组,最后 strings.Join(words, " ")。时间 O(n),空间 O(n)。

follow-up 写法

follow-up 写法:如果字符串是可变字符数组(Go 里转 []byte),可以先原地压缩空格,再整体反转,再逐词反转。时间 O(n),额外空间 O(1)。

常见误区

  • 只 reverse 整个字符串,忘记逐词反转 —— 会得到字符全反而不是词序反。
  • 忘记处理首尾空格 —— 输出可能带多余空格。
  • 忘记把多个连续空格压缩成一个 —— 输出格式不合法。
  • 在 Go 里说「原地修改 string」,但 Go string 不可变,应转 []byte 或 []rune 讨论。
  • 直接 strings.Split(s, " ") 后没有过滤空字符串 —— 会产生大量 "" 元素。