LeetCode 58. 最后一个单词的长度
题目描述
题意分析
输入是一个只含英文字母和空格的字符串,要返回其中最后一个单词的长度。这里「单词」的定义是极大的、不含空格的字符序列。
关键的约束信号有两个:一是答案只依赖末尾那一段连续字母,前面的内容与结果无关;二是题目保证字符串中至少存在一个单词,所以不存在「无解」分支,返回值必然为正。
边界主要来自空格的位置。字符串尾部可能挂着若干空格(如
"a "),头部也可能有前导空格(如" day"),单词之间还可能有连续多个空格(如"a b")。这三种情况都不能让计数逻辑提前停止或多数一位。另一个容易忽略的边界是整串只有一个单词、且不带任何空格(如
"hello"),此时向左扫描会一直走到下标-1,越界判断必须写在循环条件里。
解法:从后向前扫描
核心思路
最朴素的做法是按空格把整串切开,得到所有片段后取最后一个非空片段求长度。它能算对,但要把整串扫完,还要额外开一个数组存放全部片段。
瓶颈很清楚:除最后一个单词以外的所有片段,切出来之后立刻被丢弃,这部分工作完全是浪费的,额外空间也随单词数量线性增长。
换个方向观察就能绕开这个浪费。既然答案在字符串的最右端,那就从最右端开始往左走:先把尾部空格剥掉,再一路数字母,第一次撞到空格或走出左边界时,刚数过的这一段恰好就是最后一个单词。
整个过程维持的不变量是:指针
i右边的字符已经全部处理完毕,且len恰好等于区间 $[i+1,\ n-1]$ 中属于最后一个单词的字母个数。第一阶段结束时len仍为 0(剥掉的都是空格),第二阶段结束时len就是答案。
解题步骤
- 把指针
i初始化为n - 1,从最右端切入。因为答案只在尾部,从右往左是唯一不做无用功的方向。- 当
i >= 0且s[i]是空格时持续左移。这一步专门负责剥掉尾部空格,让i停在最后一个单词的末字母上;如果不先剥,形如"a "的输入会一开始就撞上空格。- 令
len = 0,当i >= 0且s[i]不是空格时,len加一并把i左移。计数和移动必须成对出现,少一个就会死循环或漏数。- 循环因为遇到空格或
i越过左边界而停止,此时返回len。题目保证至少存在一个单词,所以len一定大于 0,不需要额外兜底。以
" fly me to the moon "走一遍:串长 27,下标 21 至 24 是moon,下标 25、26 是两个尾部空格。第一阶段:i = 26是空格,i减到 25;s[25]仍是空格,i减到 24;s[24] = 'n',第一阶段结束。第二阶段:s[24] = 'n',len = 1,i = 23;s[23] = 'o',len = 2,i = 22;s[22] = 'o',len = 3,i = 21;s[21] = 'm',len = 4,i = 20;s[20]是空格,循环终止。返回 4。
代码实现
class Solution {
// 末尾可能有空格,必须先从后往前跳过这些空格。
public int lengthOfLastWord(String s) {
int i = s.length() - 1;
while (i >= 0 && s.charAt(i) == ' ') {
i--;
}
int len = 0;
while (i >= 0 && s.charAt(i) != ' ') {
len++;
i--;
}
return len;
}
}
func lengthOfLastWord(s string) int {
// 末尾可能有空格,必须先从后往前跳过这些空格。
i := len(s) - 1
for i >= 0 && s[i] == ' ' {
i--
}
length := 0
for i >= 0 && s[i] != ' ' {
length++
i--
}
return length
}
复杂度分析
- 时间复杂度:$O(n)$,其中 $n$ 为字符串长度。两个循环共用同一个指针且只会单向左移,最坏情况(整串是一个单词)下每个字符恰好被访问一次。
- 空间复杂度:$O(1)$,全程只维护
i和len两个整型变量,没有切分出任何中间数组或子串。
关键点总结
- 「只需要结果的一小部分」是一个很强的信号:不要先把整体加工好再取局部,而应该直接从答案所在的那一端切入,把复杂度和空间一起省下来。
- 反向扫描时要把「跳过分隔符」和「统计有效字符」拆成两个独立循环。每个循环只维护一件事,语义单一,边界才不会互相污染。
- 两个循环的守卫都要带上
i >= 0。把「整串都是待跳过字符」这种极端输入交给循环条件自然收敛,比在开头堆特判更稳。- 指针的最终位置和计数值是两个不同的量,返回前想清楚要的是哪一个,不要用下标去凑长度。
- 面试视角:这题面试官真正想看的是你会不会张口就是
split(" ")。主动说出「切分要 $O(n)$ 额外空间,而且绝大部分工作会被丢弃,我改成从尾部倒扫,$O(1)$ 空间」,比把代码写对更加分。- 面试视角:简单题的差异化空间在于收尾。写完后主动报出
"a"、"a "、" a b "三组边界并口头走查一遍,是成本最低的加分动作。
易错点总结
- 错误写法:不先剥尾部空格,直接从
n - 1开始数字母。用例"a "→ 起手就撞上空格,计数循环一次都不进,返回 0,正确答案是 1。- 错误写法:把两个循环合并成一个,用「遇空格就跳过、遇字母就计数」的单循环。用例
"a b"→ 跳过中间空格后继续把a计入,返回 2,正确答案是 1。- 错误写法:跳空格阶段也让
len自增,两个阶段共用同一个计数器。用例"a "→ 两个尾部空格各贡献 1,返回 3,正确答案是 1。- 错误写法:用
s.lastIndexOf(' ')拿到最后一个空格下标idx,返回n - 1 - idx。用例"day "→idx = 3,n = 4,返回 0,正确答案是 3。- 错误写法:先
trim()再取lastIndexOf(' '),长度写成t.length() - idx。用例"hello world"→idx = 5,t.length() = 11,返回 6,把那个空格也算进去了,正确答案是 5。- 错误写法:计数循环里只写
len++而忘了i--。用例"ab"→ 指针停在同一个字母上反复计数,直接死循环。- 错误写法:把停在单词左侧的下标
i当成长度返回。用例" moon"→ 扫描结束时i = 1,返回 1,正确答案是 4。- 错误写法:循环条件漏掉
i >= 0,只判断字符内容。用例"hello"→ 数完全部字母后i变成-1,下一轮取字符时下标越界抛异常。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 151. 反转字符串中的单词 | 中等 | 需要清洗前导、尾随与中间多余空格后整体反序,不能只看尾部 |
| 186. 反转字符串中的单词 II | 中等 | 常数空间限制下先整体反转再逐词反转,考察原地双指针 |
| 557. 反转字符串中的单词 III | 简单 | 单词顺序保持不变,只在每个单词边界内部做反转 |
| 387. 字符串中的第一个唯一字符 | 简单 | 答案在最左端,需要先统计频次再正向扫描定位 |