题目描述
思路解析
一句话答案: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)。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住这句口诀:每个单词左右各放一个指针,往中间夹着两两交换,空格不动、顺序不变。下面每帧都在套它。
先把句子摊成一排字符格子。灰色的两格是空格,它们把句子切成三个单词 Let、us、code。空格永远不动,我们只在每个单词内部做交换。
扫到第 1 个单词的开头,左指针 l 落在下标 0,也就是字符 「L」。接着要找这个单词的右端在哪。
右指针 r 一路向右滑,碰到空格或句尾就停,停在下标 2(字符 「t」)。这样单词 「Let」 的左右两端 l=0、r=2 都框定了。
现在 l=0 在 r=2 的左边,符合「l 在 r 左边就交换」的条件。把这两格的字母 「L」 和 「t」 对调一下。
交换好了,下标 0 变成 「t」,下标 2 变成 「L」。然后 l 向右走一步、r 向左走一步,继续往中间夹。
两个指针碰头了,停在同一格 1。中间这个字符 「e」 跟自己交换没意义,所以单词长度是奇数时,正中间那一格保持不动。
这个单词反转完成,标成绿色,结果是 「teL」。注意它还在原来的位置上,空格也没挪,整句的单词顺序丝毫没变。
扫到第 2 个单词的开头,左指针 l 落在下标 4,也就是字符 「u」。接着要找这个单词的右端在哪。
右指针 r 一路向右滑,碰到空格或句尾就停,停在下标 5(字符 「s」)。这样单词 「us」 的左右两端 l=4、r=5 都框定了。
现在 l=4 在 r=5 的左边,符合「l 在 r 左边就交换」的条件。把这两格的字母 「u」 和 「s」 对调一下。
交换好了,下标 4 变成 「s」,下标 5 变成 「u」。然后 l 向右走一步、r 向左走一步,继续往中间夹。
l 已经走到 r 的右边,「l 在 r 左边」这个条件不成立了,交换就此打住。单词长度是偶数时,正好两两配对、没有落单的字符。
这个单词反转完成,标成绿色,结果是 「su」。注意它还在原来的位置上,空格也没挪,整句的单词顺序丝毫没变。
扫到第 3 个单词的开头,左指针 l 落在下标 7,也就是字符 「c」。接着要找这个单词的右端在哪。
右指针 r 一路向右滑,碰到空格或句尾就停,停在下标 10(字符 「e」)。这样单词 「code」 的左右两端 l=7、r=10 都框定了。
现在 l=7 在 r=10 的左边,符合「l 在 r 左边就交换」的条件。把这两格的字母 「c」 和 「e」 对调一下。
交换好了,下标 7 变成 「e」,下标 10 变成 「c」。然后 l 向右走一步、r 向左走一步,继续往中间夹。
现在 l=8 在 r=9 的左边,符合「l 在 r 左边就交换」的条件。把这两格的字母 「o」 和 「d」 对调一下。
交换好了,下标 8 变成 「d」,下标 9 变成 「o」。然后 l 向右走一步、r 向左走一步,继续往中间夹。
l 已经走到 r 的右边,「l 在 r 左边」这个条件不成立了,交换就此打住。单词长度是偶数时,正好两两配对、没有落单的字符。
这个单词反转完成,标成绿色,结果是 「edoc」。注意它还在原来的位置上,空格也没挪,整句的单词顺序丝毫没变。
三个单词全部反转好了,拼起来正是 teL su edoc。每个单词内部倒过来,空格和单词的先后顺序原样保留,和一开始算出的目标一模一样。
边界先想清:整句一个词、偶数长度、单字符。
面试重点:和 151 区分开,再答出原地与切分两种写法。
参考代码
from __future__ import annotationsfrom 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())复杂度
- 时间:O(n),每个字符最多被指针看一次、交换一次
- 空间:O(1),原地双指针,只用 l、r 两个下标;参考代码按切分拼接记 O(n)
易错点
面试追问把动画讲成自己的话
追问这题和「反转字符串里的单词」(LeetCode 151) 有什么区别?
追问能不能一行解决?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
最长连续递增序列
LeetCode 674 · 简单 · 沿着 双指针套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题