转码刷 LeetCode 笔记[3]:151. 反转字符串中的单词(Python)
摘要
反转字符串中的单词是经典的字符串操作问题,常规解法(如" ".join(reversed(s.split())))虽能通过测试,但额外空间复杂度为 O (N),无法满足 “O (1) 额外空间复杂度下原地操作” 的进阶要求。
本文将拆解 “原地反转单词” 的核心思路 —— 先去除字符串中多余空格,再整体反转字符串,最后逐个反转单词内部字符,并通过快慢指针、双指针等核心技巧,详细分析实现过程中易踩坑的细节,最终给出符合进阶要求的完整原地解法。
关键词: 原地操作、快慢指针、双指针、字符串反转
题目描述
原题链接:151. 反转字符串中的单词
给你一个字符串 s ,请你反转字符串中 单词 的顺序。
单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。
返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。
注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。
示例 1:
输入:s = "the sky is blue"
输出:"blue is sky the"
示例 2:
输入:s = " hello world "
输出:"world hello"
解释:反转后的字符串中不能存在前导空格和尾随空格。
示例 3:
输入:s = "a good example"
输出:"example good a"
解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。
提示:
1 <= s.length <= 104
s 包含英文大小写字母、数字和空格 ' '
s 中 至少存在一个 单词
进阶:如果字符串在你使用的编程语言中是一种可变数据类型,请尝试使用 O(1) 额外空间复杂度的 原地 解法。
额外空间复杂度
这道题如果用s.split()把字符串转成字符串列表,然后用reverse()反转,看似很简单。但其实split()开辟了内存空间存储新字符串,有O(N)的额外空间复杂度。并不能满足进阶要求的“原地”操作。
正确思路总览
核心三步:
- 去掉多余空格 :通过快慢指针的 “快指针读取 - 慢指针写入” 的方式,仅保留单词间单个空格,去除前导 / 尾随空格;
- 整体反转字符串 :将整个字符列表反转(如hello world→dlrow olleh);
- 逐个单词再反转 :遍历字符列表,以空格为分隔,逐个反转单词内部字符(如dlrow→world,olleh→hello)。
上述思路如下图所示:

顺序反了,单词内部又恢复了。
这是一种经典技巧:整体反转 + 局部反转 = 顺序翻转
如何原地去除多余空格?
我们使用快慢指针来去除多余空格,fast负责读取,slow负责写入。
我们先来手动模拟一下
假设字符串为s=" hello world "。这个字符串中前导空格、单词间空格和尾随空格各有 2 个。
我们知道,在很多语言中(如Python)字符串是无法被修改的。为了方便修改,我们需要把字符串转为字符列表(数组)。该字符列表的长度为16。
为了方便演示和理解,我将以表格的形式呈现去除多余空格的过程。
初始状态如下图所示,快慢指针都在索引0。

fast读取->索引0是空格->右移(到索引1)->索引1是空格->右移(到索引2)->索引2不是空格->把s[fast]赋值给s[slow]

后面以此类推,如下图所示,图中标黄处表示被覆盖上的、我们期望得到的字符。
此时,fast==6, slow==4, s_list==['h', 'e', 'l', 'l', 'o', 'l', 'o', ' ', ' ', 'w', 'o', 'r', 'l', 'd', ' ', ' ']
我们通过 覆盖 的方式“原地”去掉了前导空格。此时列表的长度没有变化。
想一想,到目前位置,是什么样的逻辑呢?是不是fast只要读到空格,就跳过(右移,即fast+=1),只要fast读到的不是空格就覆盖(赋值写入,即s[slow]=s[fast]),然后fast和slow各向右移一位。

接下来又出现空格了,如果还按照刚才的逻辑走,fast就会跳过中间两个空格,指向'w'(此时slow==5, fast==9)。如果现在覆盖的话,就是直接在hello后面接'w',如下图所示,这并不是我们想要的。那么怎么才能在'o'和'w'之间加一个空格呢?

我们需要加一个逻辑,什么样的逻辑?在fast跳过所有空格后,在已经写入的单词结尾,通过slow写入一个空格,如下图所示,这样slow==5处写入的就是空格,而不是'w'了。记得写入空格后slow+=1。

注意,这一步,我们只对负责写入的slow指针进行了操作。写入了一个空格,然后让slow右移一位(此时)。并没有动负责读取的fast指针,如下图所示,此时fast==9,依然指向'w'。

下面,可以依照之前的逻辑覆盖了,逐个字符写入即可。下图演示了这一过程。

下图是fast==11, s[fast]=='r', slow==8

fast读到'd'的时候其实已经可以了,因为是最后一个单词了,后面是什么都不要了,fast读到空格也会走那个遇到空格就跳过的逻辑,而且slow一直停在我们要的结尾字母上。
此时s_list==['h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', 'r', 'l', 'd', ' ', ' ']

后面不要的字符通过s[:slow]截断即可。
综上所述,去掉多余空格的代码可以写为:
class Solution:
def reverseWords(self, s: str) -> str:
s = list(s)
n = len(s)
slow = 0
fast = 0
while fast < n:
# 写入字符
while fast<n and s[fast]!=" ":
s[slow] = s[fast]
fast+=1
slow+=1
# 跳过空格
while fast<n and s[fast]==" ":
fast+=1
# 单词结尾加上空格
if fast<n and slow!=0:
s[slow]=" "
slow+=1
s = s[:slow]
小结
- 快指针fast:遍历所有字符,跳过连续空格,只读取有效字符(单词);
- 慢指针slow:只写入有效字符,且仅在 “非开头、非结尾” 时,在单词间写入单个空格;
- 最终通过s[:slow]截断无效字符,完成 “原地” 去空格(无额外空间开辟)。
下一步是整体反转字符串,使用s.reverse()即可。
反转完之后是['d', 'l', 'r', 'o', 'w', ' ', 'o', 'l', 'l', 'e', 'h']
下一步该把每个单词反转字符顺序。
如何逐个单词反转?
要原地反转字符串,肯定要用双指针法,和力扣 344. 反转字符串 中用的思路一样。
那怎么分隔各个单词,只在单词内部反转呢?肯定要找一个特征。那就是空格。
start 指针初始化为 0 ,遍历字符列表,如果第 i 个字符是空格,那么 start ~ i-1 的字符需要反转。反转完把 start 更新为 i + 1 然后继续遍历。
最后一个单词需要特殊处理,因为最后一个单词后面没有空格了,把 start ~ len(s)-1 的字符反转就好了。
这部分的代码可以写成下面这样:
class Solution:
def reverseWords(self, s: str) -> str:
# ...去除多余空格的代码省略...
s = s[:slow]
s.reverse()
start = 0
for i in range(len(s)):
if s[i]==" ":
# 翻转从 start 开始到 a 的字符
a = i-1
while start<a:
s[start], s[a] = s[a], s[start]
start += 1
a -= 1
start = i+1
# 翻转最后一个单词
b = len(s) - 1
while start < b:
s[start], s[b] = s[b], s[start]
start += 1
b -= 1
# print(s)
return "".join(s)
反转字符也可以单独写一个函数去实现(见 完整代码 部分)。
小节
- 定义辅助函数reverse_sub:用双指针反转列表指定区间的字符;
- 遍历字符列表,以空格为分隔符,定位每个单词的起始(start)和结束(i-1)索引,调用辅助函数反转;
- 最后单独反转最后一个单词(无后续空格,需特殊处理)。
完整代码
class Solution:
def reverseWords(self, s: str) -> str:
# 步骤1:将字符串转为列表(Python字符串不可变,列表支持原地修改)
s = list(s)
n = len(s)
slow = 0 # 慢指针:负责写入有效字符
fast = 0 # 快指针:负责遍历读取所有字符
# 子步骤1:快慢指针去除多余空格(前导、尾随、单词间多空格)
while fast < n:
# 1. 读取并写入非空格字符(核心:保留单词本身)
while fast < n and s[fast] != " ":
s[slow] = s[fast]
fast += 1
slow += 1
# 2. 跳过所有连续空格(核心:去除多余空格)
while fast < n and s[fast] == " ":
fast += 1
# 3. 单词间添加单个空格(仅当非开头、非结尾时添加)
if fast < n and slow != 0:
s[slow] = " "
slow += 1
# 截断尾随的无效字符(此时slow指向最后一个有效字符的下一位)
s = s[:slow]
# 步骤2:整体反转整个字符列表
s.reverse()
# 步骤3:双指针逐个反转单词内部字符
def reverse_sub(s, start, end):
"""辅助函数:反转列表s中[start, end]区间的字符"""
while start < end:
s[start], s[end] = s[end], s[start]
start += 1
end -= 1
start = 0 # 记录每个单词的起始索引
for i in range(len(s)):
# 遇到空格时,反转当前单词(start ~ i-1)
if s[i] == " ":
reverse_sub(s, start, i - 1)
start = i + 1 # 更新下一个单词的起始索引
# 反转最后一个单词(无后续空格,需单独处理)
reverse_sub(s, start, len(s) - 1)
# 转回字符串返回
return "".join(s)

反转字符串中的单词是经典的字符串操作问题,常规解法(如" ".join(reversed(s.split())))虽能通过测试,但额外空间复杂度为 O (N),无法满足 “O (1) 额外空间复杂度下原地操作” 的进阶要求。
本文将拆解 “原地反转单词” 的核心思路 —— 先去除字符串中多余空格,再整体反转字符串,最后逐个反转单词内部字符,并通过快慢指针、双指针等核心技巧,详细分析实现过程中易踩坑的细节,最终给出符合进阶要求的完整原地解法。
浙公网安备 33010602011771号