目录

题目描述

58. 最后一个单词的长度

题意分析

输入是一个只含英文字母和空格的字符串,要返回其中最后一个单词的长度。这里「单词」的定义是极大的、不含空格的字符序列。

关键的约束信号有两个:一是答案只依赖末尾那一段连续字母,前面的内容与结果无关;二是题目保证字符串中至少存在一个单词,所以不存在「无解」分支,返回值必然为正。

边界主要来自空格的位置。字符串尾部可能挂着若干空格(如 "a "),头部也可能有前导空格(如 " day"),单词之间还可能有连续多个空格(如 "a b")。这三种情况都不能让计数逻辑提前停止或多数一位。

另一个容易忽略的边界是整串只有一个单词、且不带任何空格(如 "hello"),此时向左扫描会一直走到下标 -1,越界判断必须写在循环条件里。

解法:从后向前扫描

核心思路

最朴素的做法是按空格把整串切开,得到所有片段后取最后一个非空片段求长度。它能算对,但要把整串扫完,还要额外开一个数组存放全部片段。

瓶颈很清楚:除最后一个单词以外的所有片段,切出来之后立刻被丢弃,这部分工作完全是浪费的,额外空间也随单词数量线性增长。

换个方向观察就能绕开这个浪费。既然答案在字符串的最右端,那就从最右端开始往左走:先把尾部空格剥掉,再一路数字母,第一次撞到空格或走出左边界时,刚数过的这一段恰好就是最后一个单词。

整个过程维持的不变量是:指针 i 右边的字符已经全部处理完毕,且 len 恰好等于区间 $[i+1,\ n-1]$ 中属于最后一个单词的字母个数。第一阶段结束时 len 仍为 0(剥掉的都是空格),第二阶段结束时 len 就是答案。

解题步骤

  • 把指针 i 初始化为 n - 1,从最右端切入。因为答案只在尾部,从右往左是唯一不做无用功的方向。
  • i >= 0s[i] 是空格时持续左移。这一步专门负责剥掉尾部空格,让 i 停在最后一个单词的末字母上;如果不先剥,形如 "a " 的输入会一开始就撞上空格。
  • len = 0,当 i >= 0s[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 = 1i = 23s[23] = 'o'len = 2i = 22s[22] = 'o'len = 3i = 21s[21] = 'm'len = 4i = 20s[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)$,全程只维护 ilen 两个整型变量,没有切分出任何中间数组或子串。

关键点总结

  • 「只需要结果的一小部分」是一个很强的信号:不要先把整体加工好再取局部,而应该直接从答案所在的那一端切入,把复杂度和空间一起省下来。
  • 反向扫描时要把「跳过分隔符」和「统计有效字符」拆成两个独立循环。每个循环只维护一件事,语义单一,边界才不会互相污染。
  • 两个循环的守卫都要带上 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 = 3n = 4,返回 0,正确答案是 3。
  • 错误写法:先 trim() 再取 lastIndexOf(' '),长度写成 t.length() - idx。用例 "hello world"idx = 5t.length() = 11,返回 6,把那个空格也算进去了,正确答案是 5。
  • 错误写法:计数循环里只写 len++ 而忘了 i--。用例 "ab" → 指针停在同一个字母上反复计数,直接死循环。
  • 错误写法:把停在单词左侧的下标 i 当成长度返回。用例 " moon" → 扫描结束时 i = 1,返回 1,正确答案是 4。
  • 错误写法:循环条件漏掉 i >= 0,只判断字符内容。用例 "hello" → 数完全部字母后 i 变成 -1,下一轮取字符时下标越界抛异常。

相似题目

题目 难度 考察点
151. 反转字符串中的单词 中等 需要清洗前导、尾随与中间多余空格后整体反序,不能只看尾部
186. 反转字符串中的单词 II 中等 常数空间限制下先整体反转再逐词反转,考察原地双指针
557. 反转字符串中的单词 III 简单 单词顺序保持不变,只在每个单词边界内部做反转
387. 字符串中的第一个唯一字符 简单 答案在最左端,需要先统计频次再正向扫描定位