反转字符串中的单词 III 图解题解
这道题到底在问什么
- 输入
- s = "Let's take LeetCode contest"
- 输出
- "s'teL ekat edoCteeL tsetnoc"
最优解:为什么这么做
一句话答案:LeetCode 557 反转字符串中的单词 III:按空格把句子切成一个个单词,让每个单词各自倒过来,再用空格拼回,单词的先后顺序和空格都保持不变,一遍扫完时间 O(n)。
要倒的是每个单词,还是整句话
给一个字符串 s,把里面每个单词的字母顺序倒过来,但空格的位置和单词的先后顺序都不动。题面例子 s=「Let's take LeetCode contest」,输出是「s'teL ekat edoCteeL tsetnoc」——Let's 变成了 s'teL、take 变成 ekat,可四个单词谁前谁后、中间几个空格,全和原来一样。要动的只是每个单词内部的字母。
把整句直接倒过来,为什么是错的
很自然会想到把整句字符串从尾到头整个翻一遍。拿「Let's take LeetCode contest」试,倒完是「tsetnoc edoCteeL ekat s'teL」:单词内部确实倒了,可单词的先后顺序也跟着颠倒,contest 跑到了最前面。题目只要单词内部倒、顺序不许乱,这一下把两件事一起做了,答案就错。所以别对整句下手,得把每个单词单独拎出来处理。
为什么按空格切开就能各管各的
空格是天然的分界线:它把句子隔成一个个单词,题目又保证空格和单词顺序都不动。所以沿着空格把句子 split 成单词列表,每一段是一串独立的字母,倒它不牵连别人。把每一段各自倒过来,再用空格按原样拼回去,空格有几个、单词排第几,都没被碰过。
分词、逐词反转、再拼回,具体怎么接
三步接起来。第一步 s.split() 沿空格切出单词列表,「Let's take LeetCode contest」切成四段 Let's、take、LeetCode、contest。第二步把每一段的字母从后往前重排,Let's 得到 s'teL。第三步用一个空格把倒好的四段 join 回一个字符串。参考代码把三步压成一行:「 」.join(t[::-1] for t in s.split()),其中 t[::-1] 就是把单词 t 整个倒序。
题面这句话,一段段倒给你看
按空格切成四段。第一段 Let's 五个字符,从后往前排是 s、'、t、e、L,拼成 s'teL。第二段 take 倒成 ekat。第三段 LeetCode 八个字母,倒过来是 edoCteeL。第四段 contest 倒成 tsetnoc。四段之间照旧用一个空格连起来,答案落在「s'teL ekat edoCteeL tsetnoc」。
只有一个单词、或者空句子会不会出错
边界先过一遍。整句只有一个单词时,split 切出一段、倒完就是答案,没有空格要拼;空字符串时 split 得到空列表,join 出来还是空串,都不报错。真正容易写坏的地方有两个:把空格也卷进反转,单词边界和排版立刻乱套;顺手连整句顺序一起倒了,又退回上面那个把 contest 甩到句首的错答案。守住「只在单词内部动、空格和顺序不碰」,两个坑都绕开。
复杂度上,切分、反转、拼接每个字符都只经手常数次,时间 O(n),n 是字符串长度;按切分拼接要另存单词列表和结果串,空间 O(n)。换用动画那种原地双指针逐词交换,不额外开数组,空间能压到 O(1)。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记住这句口诀:每个单词左右各放一个指针,往中间夹着两两交换,空格不动、顺序不变。下面每帧都在套它。
- 4先把句子摊成一排字符格子。灰色的两格是空格,它们把句子切成三个单词 Let、us、code。空格永远不动,我们只在每个单词内部做交换。
- 5扫到第 1 个单词的开头,左指针 l 落在下标 0,也就是字符 「L」。接着要找这个单词的右端在哪。
- 6右指针 r 一路向右滑,碰到空格或句尾就停,停在下标 2(字符 「t」)。这样单词 「Let」 的左右两端 l=0、r=2 都框定了。
- 7现在 l=0 在 r=2 的左边,符合「l 在 r 左边就交换」的条件。把这两格的字母 「L」 和 「t」 对调一下。
- 8交换好了,下标 0 变成 「t」,下标 2 变成 「L」。然后 l 向右走一步、r 向左走一步,继续往中间夹。
- 9两个指针碰头了,停在同一格 1。中间这个字符 「e」 跟自己交换没意义,所以单词长度是奇数时,正中间那一格保持不动。
- 10这个单词反转完成,标成绿色,结果是 「teL」。注意它还在原来的位置上,空格也没挪,整句的单词顺序丝毫没变。
- 11扫到第 2 个单词的开头,左指针 l 落在下标 4,也就是字符 「u」。接着要找这个单词的右端在哪。
- 12右指针 r 一路向右滑,碰到空格或句尾就停,停在下标 5(字符 「s」)。这样单词 「us」 的左右两端 l=4、r=5 都框定了。
- 13现在 l=4 在 r=5 的左边,符合「l 在 r 左边就交换」的条件。把这两格的字母 「u」 和 「s」 对调一下。
- 14交换好了,下标 4 变成 「s」,下标 5 变成 「u」。然后 l 向右走一步、r 向左走一步,继续往中间夹。
- 15l 已经走到 r 的右边,「l 在 r 左边」这个条件不成立了,交换就此打住。单词长度是偶数时,正好两两配对、没有落单的字符。
- 16这个单词反转完成,标成绿色,结果是 「su」。注意它还在原来的位置上,空格也没挪,整句的单词顺序丝毫没变。
- 17扫到第 3 个单词的开头,左指针 l 落在下标 7,也就是字符 「c」。接着要找这个单词的右端在哪。
- 18右指针 r 一路向右滑,碰到空格或句尾就停,停在下标 10(字符 「e」)。这样单词 「code」 的左右两端 l=7、r=10 都框定了。
- 19现在 l=7 在 r=10 的左边,符合「l 在 r 左边就交换」的条件。把这两格的字母 「c」 和 「e」 对调一下。
- 20交换好了,下标 7 变成 「e」,下标 10 变成 「c」。然后 l 向右走一步、r 向左走一步,继续往中间夹。
- 21现在 l=8 在 r=9 的左边,符合「l 在 r 左边就交换」的条件。把这两格的字母 「o」 和 「d」 对调一下。
- 22交换好了,下标 8 变成 「d」,下标 9 变成 「o」。然后 l 向右走一步、r 向左走一步,继续往中间夹。
- 23l 已经走到 r 的右边,「l 在 r 左边」这个条件不成立了,交换就此打住。单词长度是偶数时,正好两两配对、没有落单的字符。
- 24这个单词反转完成,标成绿色,结果是 「edoc」。注意它还在原来的位置上,空格也没挪,整句的单词顺序丝毫没变。
- 25三个单词全部反转好了,拼起来正是 teL su edoc。每个单词内部倒过来,空格和单词的先后顺序原样保留,和一开始算出的目标一模一样。
⚠️ 容易写错的地方
✗ 错:把空格也卷进交换
✓ 对:只在单词内部 [l, r] 区间交换
空格参与交换会打乱单词边界和句子排版
✗ 错:循环条件写成 l ≤ r
✓ 对:应是 l 小于 r
l 等于 r 时是同一格,自己跟自己换没意义还多一步
✗ 错:误把整句单词顺序也倒过来
✓ 对:只反转每个单词内部
本题要求单词的先后顺序和空格都保持不变
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def reverseWords(self, s: str) -> str:
return " ".join(t[::-1] for t in s.split())C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
string reverseWords(string s) {
stringstream ss(s);
string t;
string ans;
while (ss >> t) {
reverse(t.begin(), t.end());
ans += t;
ans.push_back(' ');
}
ans.pop_back();
return ans;
}
};Java
import java.util.*;
class Solution {
public String reverseWords(String s) {
String[] words = s.split(" ");
for (int i = 0; i < words.length; ++i) {
words[i] = new StringBuilder(words[i]).reverse().toString();
}
return String.join(" ", words);
}
}复杂度
时间
O(n)
每个字符最多被指针看一次、交换一次
空间
O(1)
原地双指针,只用 l、r 两个下标;参考代码按切分拼接记 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 反转字符串中的单词 III 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题和「反转字符串里的单词」(LeetCode 151) 是一回事吗?+
方向正好相反。557 只把每个单词内部的字母倒过来,单词的先后顺序和空格全部保持不变;151 反过来,是把整句单词的先后顺序颠倒,单词内部的字母不动,还要清掉多余空格。一个动内部、一个动顺序,很容易记混,做题前先看清要的是哪种。
参考代码一行就写完了,动画为什么用双指针?+
两种都对,差在空间。一行版按空格 split 成单词列表、每段反转再 join,思路最直白,但要额外存单词列表和新字符串,空间 O(n)。双指针则不新开数组:在原串上给每个单词的两端各放一个指针 l、r,往中间夹着两两交换,只用两个下标,空间压到 O(1)。面试若追问「能不能不额外开空间」,答的就是这个原地双指针写法。
为什么按空格切开、单词的顺序和空格就一定不会乱?+
因为 split 只在空格处下刀,切出来的单词列表天生保留了它们的先后次序;我们只对每一段单独反转,不调换段与段的位置。最后用一个空格把这些段按原顺序 join 回去,空格的数量和落点也照旧。整个过程没移动过任何单词的相对位置,也没往里加减空格,顺序和空格自然纹丝不动。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 反转字符串中的单词 III 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。