这是一道很多人会做错"复杂度"的题。
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}]')
相关题目推荐:
- LeetCode 844 · 比较含退格的字符串(反向扫描的经典应用)
- LeetCode 387 · 字符串中的第一个唯一字符(正向扫描 + 频次表)
- LeetCode 125 · 验证回文串(双指针相向扫描的入门题)