LeetCode 58 · 最后一个单词的长度:从右向左跳过尾部空格

这是一道很多人会做错"复杂度"的题。s.split()[-1] 一行搞定,但 Python 的 split 会复制整个字符串数组------O(n) 空间。如果面试官追问"能不能用 O(1) 空间",正确答案是从右向左扫描:跳过尾部空格,再数到下一个空格为止。这道题的本质考的是"识别题目里的反向扫描机会"。


题目长什么样

给定一个字符串 s,由若干单词组成,单词之间用空格分隔。返回最后一个单词 的长度。单词定义为仅由字母组成、不含空格的最大子字符串

输入s = " fly me to the moon "

输出4

解释 :最后一个单词是 "moon",长度为 4。注意尾部有两个空格。

关键陷阱:字符串尾部可能有空格,从右向左扫描时必须先把它们跳过,否则会误以为 0 是答案。


第一反应:split 一把梭

最 Pythonic 的写法:

python 复制代码
class SolutionSplit:
    def lengthOfLastWord(self, s: str) -> int:
        return len(s.split()[-1])
维度 说明
时间 O(n) split 遍历整个字符串
空间 O(n) split 生成所有单词的列表

这个解法在工程代码里完全够用------清晰、可读、不容易出 bug。但面试官会追问:能不能不用 O(n) 空间? 因为题目只需要"最后一个",前面的单词都是无用信息,没必要存。


最优解:从右向左扫描

关键观察:答案只依赖字符串尾部。从右往左走两步即可:

text 复制代码
1. 跳过尾部所有空格
2. 数连续的非空格字符,数到下一个空格或越界为止
python 复制代码
class Solution:
    def lengthOfLastWord(self, s: str) -> int:
        i = len(s) - 1
        # 第一步:跳过尾部空格
        while i >= 0 and s[i] == " ":
            i -= 1
        # 第二步:数单词长度
        length = 0
        while i >= 0 and s[i] != " ":
            length += 1
            i -= 1
        return length

为什么这样做是对的?

这道题的语义是"找最后一个非空格连续段"。从左向右扫描需要把所有单词都看一遍;从右向左扫描只需要看尾部------前面的内容全部跳过。两个 while 循环分别对应"跳过尾部空格"和"数单词长度"。

循环条件的顺序很重要i >= 0 and s[i] == " " 必须先判断 i >= 0,否则 s[i] 会越界。这是短路求值的标准用法。

跑一遍示例 2

text 复制代码
s = "   fly me   to   the moon  "
索引: 0123456789...                    ...28,29
长度 = 30

初始: i = 29

第一步: 跳过尾部空格
  s[29] = ' ' → i=28
  s[28] = ' ' → i=27
  s[27] = 'n' → 停止, i=27

第二步: 数单词
  s[27] = 'n', length=1, i=26
  s[26] = 'o', length=2, i=25
  s[25] = 'o', length=3, i=24
  s[24] = 'm', length=4, i=23
  s[23] = ' ' → 停止

返回 length = 4 ✓

跑一遍示例 1(无尾部空格)

text 复制代码
s = "Hello World", 长度 = 11

初始: i = 10

第一步: 跳过尾部空格
  s[10] = 'd' → 立即停止, i=10

第二步: 数单词
  s[10..6] = "World", length=5, i=5
  s[5] = ' ' → 停止

返回 length = 5 ✓

跑一遍示例 3(无尾部空格 + 单词直接到末尾)

text 复制代码
s = "luffy is still joyboy", 长度 = 22

初始: i = 21

第一步: s[21] = 'y' → 立即停止, i=21

第二步: 数 "joyboy" 6 个字符 → length=6 ✓
维度 说明
时间 O(n) 最坏情况扫描整个字符串(尾部第一个就是答案)
空间 O(1) 只用了 i 和 length 两个变量

两种解法放在一起看

解法 时间 空间 思路
split 一把梭 O(n) O(n) 切出所有单词,取最后一个
从右向左扫描 O(n) O(1) 只关心尾部,跳过空格数单词

两种解法时间复杂度都是 O(n),看起来没差。但有一个实际性能差异容易被忽略:

  • split 一定要扫描整个字符串,并复制所有单词到列表。
  • 从右向左扫描在尾部单词结束的瞬间就停止了,前面的大部分内容根本不会被访问。

" fly me to the moon " 这种字符串,split 扫描全部 30 个字符并创建 4 个字符串对象;从右向左扫描只访问了 6 个字符(2 个空格 + 4 个字母)。在 s 很长、最后一个单词很短的极端情况下,差异显著。


这道题教会我什么

"答案只依赖一端" → 反向扫描

这是反向扫描的典型信号。识别它只要问一句:"答案是否只依赖字符串的某一端?"

  • 最后一个单词长度(本题):依赖右端 → 从右扫
  • 第一个不重复字符(LeetCode 387):依赖整体频次 → 正向扫
  • 回文验证(LeetCode 125):依赖两端 → 双指针相向
  • 验证回文串:双指针

类似的反向扫描题:

  • LeetCode 917 · 仅仅反转字母:双指针,但移动逻辑可以从任一端触发
  • LeetCode 844 · 比较含退格的字符串 :从右向左处理 # 退格
  • LeetCode 28 · 找出第一个匹配项:正向匹配,但 KMP 的失配跳转是"反向利用已匹配信息"

Pythonic 与最优不总是矛盾,但要分清场合

s.split()[-1] 在工程代码里是首选------可读性、维护性、防 bug 能力都更强。但在以下场合应该选 O(1) 空间版本:

  • 内存敏感场景s 极长(GB 级日志文件),不能复制。
  • 面试被追问:面试官用这道题就是想看你能不能识别"反向扫描"机会。
  • 流式数据s 是一个迭代器/生成器,只能消费一次,那就只能用"消费到末尾"的思路。

工程里先 Pythonic,遇到瓶颈再优化;面试里直接出最优解,并解释为什么选它------两种场合的策略不同。

边界条件:尾部全是空格

如果题目允许 s = "abc "(尾部多个空格但单词在前面),从右向左扫描仍然正确------第一个 while 跳过所有尾部空格。但如果 s = "abc"(无尾部空格),第一个 while 直接不进入,立即开始数单词。代码无需为任何特殊边界加判断,循环条件天然处理。

题目保证 s 中至少存在一个单词,所以不需要担心"全是空格"的退化情形。如果生产代码里没有这个保证,就要在第一步 while 之后加一个 if i < 0: return 0 的兜底。


完整测试代码

python 复制代码
class Solution:
    def lengthOfLastWord(self, s: str) -> int:
        i = len(s) - 1
        while i >= 0 and s[i] == " ":
            i -= 1
        length = 0
        while i >= 0 and s[i] != " ":
            length += 1
            i -= 1
        return length


if __name__ == "__main__":
    s = Solution()

    cases = [
        ("Hello World", 5),
        ("   fly me   to   the moon  ", 4),
        ("luffy is still joyboy", 6),
        ("a", 1),                              # 单字符
        ("a ", 1),                              # 单字符 + 尾部一个空格
        ("   a", 1),                           # 头部多空格
        ("day", 3),                            # 只有一个单词,无空格
        ("  Hello   World  ", 5),              # 头尾都有多空格
        ("a b c d", 1),                        # 最后一个单词只有 1 字符
        ("tonight is the night", 5),
    ]
    for s_in, expected in cases:
        got = s.lengthOfLastWord(s_in)
        ok = "OK" if got == expected else "FAIL"
        show = s_in.replace(" ", "·")  # 把空格可视化便于检查
        print(f'输入: "{show}", 输出: {got} (期望 {expected}) [{ok}]')

相关题目推荐