转码刷 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)的额外空间复杂度。并不能满足进阶要求的“原地”操作。

正确思路总览

核心三步:

  1. 去掉多余空格 :通过快慢指针的 “快指针读取 - 慢指针写入” 的方式,仅保留单词间单个空格,去除前导 / 尾随空格;
  2. 整体反转字符串 :将整个字符列表反转(如hello world→dlrow olleh);
  3. 逐个单词再反转 :遍历字符列表,以空格为分隔,逐个反转单词内部字符(如dlrow→world,olleh→hello)。

上述思路如下图所示:
image
顺序反了,单词内部又恢复了。
这是一种经典技巧:整体反转 + 局部反转 = 顺序翻转

如何原地去除多余空格?

我们使用快慢指针来去除多余空格,fast负责读取,slow负责写入。

我们先来手动模拟一下

假设字符串为s=" hello world "。这个字符串中前导空格、单词间空格和尾随空格各有 2 个。
我们知道,在很多语言中(如Python)字符串是无法被修改的。为了方便修改,我们需要把字符串转为字符列表(数组)。该字符列表的长度为16。
为了方便演示和理解,我将以表格的形式呈现去除多余空格的过程。
初始状态如下图所示,快慢指针都在索引0。
image
fast读取->索引0是空格->右移(到索引1)->索引1是空格->右移(到索引2)->索引2不是空格->把s[fast]赋值给s[slow]
image
后面以此类推,如下图所示,图中标黄处表示被覆盖上的、我们期望得到的字符。
此时,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]),然后fastslow各向右移一位。
image
接下来又出现空格了,如果还按照刚才的逻辑走,fast就会跳过中间两个空格,指向'w'(此时slow==5, fast==9)。如果现在覆盖的话,就是直接在hello后面接'w',如下图所示,这并不是我们想要的。那么怎么才能在'o''w'之间加一个空格呢?
image
我们需要加一个逻辑,什么样的逻辑?在fast跳过所有空格后,在已经写入的单词结尾,通过slow写入一个空格,如下图所示,这样slow==5处写入的就是空格,而不是'w'了。记得写入空格后slow+=1
image
注意,这一步,我们只对负责写入的slow指针进行了操作。写入了一个空格,然后让slow右移一位(此时)。并没有动负责读取的fast指针,如下图所示,此时fast==9,依然指向'w'
image

下面,可以依照之前的逻辑覆盖了,逐个字符写入即可。下图演示了这一过程。
image
下图是fast==11, s[fast]=='r', slow==8
image
fast读到'd'的时候其实已经可以了,因为是最后一个单词了,后面是什么都不要了,fast读到空格也会走那个遇到空格就跳过的逻辑,而且slow一直停在我们要的结尾字母上。
此时s_list==['h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', 'r', 'l', 'd', ' ', ' ']
image

后面不要的字符通过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)
posted @ 2026-03-01 15:20  茴香豆的茴  阅读(0)  评论(0)    收藏  举报