☰
字符串处理经典题:最后一个单词长度的split法与空间O(1)逆向扫描
2026/10/14 20:42:50 网站建设 项目流程

上周在一个技术交流群里,有人贴了一行 Python 代码,说是“最后一个单词的长度”这道题的优雅解法,配文“一行秒杀”。结果评论区立刻吵起来了——有人说末尾带空格会翻车,有人说空字符串会越界,还有人掏出各种边界输入一顿测。那一幕特别典型:一道标着“简单”的字符串题,恰恰是筛掉一大批细节意识薄弱选手的分水岭。

“最后一个单词的长度”是字符串处理里的经典入门题,也是很多人口中的“送分题”。它要解决的问题一句话就能说清:给定一个仅包含字母和空格的字符串,返回最后一个单词的长度。但这句话背后藏着不少工程上真实存在的边角情况,比如末尾空格、连续空格、纯空格输入、空串等。你能不能在几分钟内写对、写稳、写出能讲清原理的实现,直接反映你日常处理字符串数据的基本功。

这篇文章我会从题目约束开始,把 split 一行流的写法、空间 O(1) 的逆向扫描法、边界用例设计、工程迁移场景一次讲透。无论你是刚开始刷题的新人,还是在准备面试,或者单纯想把字符串处理写得更稳,这篇都值得耐心读完。

1. 读题先读约束:这道题真正给出的前提条件

1.1 “单词”的定义与输入范围的隐含信息

题目原文的条件其实非常克制:输入字符串只包含英文字母和空格,要求返回最后一个单词的长度。这里的“单词”指连续的不含空格的字符序列,所以“hello world”的最后一个单词是 world,长度是 5;“hello world ”末尾带个空格,最后一个单词依然是 world,长度还是 5。

这两个例子说明一个核心事实:题目并没有保证字符串末尾没有空格。它也没有保证字符串一定非空,没有保证单词之间只隔一个空格。这些“没有保证”恰恰是整道题的全部考点。

我见过很多人读完题之后直接写:

def length_of_last_word(s): return len(s.split()[-1])

这段代码在“hello world”上确实能返回 5,看起来很对。可一旦输入变成“hello world ”,split 后得到['hello', 'world', ''],[-1]取到的是空字符串,长度变成 0。这就是典型的“用例没想全,提交就翻车”。

1.2 最容易被忽略的三种输入形态

从实际提交经验来看,最容易让人翻车的输入有三种:

第一种是末尾有空格。这是最常见的脏数据形态,用户粘贴文本、文件读入、字符串拼接,都极容易在尾部留下空白字符。很多人都知道“先预处理一下”,但到底是strip()还是rstrip(),到底要不要考虑多个连续空格,心里没底。

第二种是纯空格或空字符串。比如输入是" ",此时正确输出应该是 0,因为没有单词。有些实现方式在遇到这种情况时会直接报错,比如前面那行split()[-1],如果字符串只有空格,split()的结果是空列表,取[-1]直接越界。

第三种是单词内部完全由单字符组成。比如"a"、"a "、" a",这些输入会把实现里的边界判断逼出来:你是从 0 开始扫还是从末尾开始扫?你有没有处理索引走到 -1 的情况?

这三种形态不是出题人刁难你,它们就是真实项目里字符串数据的常态。日志文件的最后一行经常带换行符,配置文件的 value 经常带尾部空格,从接口返回的文本偶尔会是空串。一个字符串处理函数连这些输入都扛不住,那它基本没有上线资格。

2. split 一行流:能用,但要清楚它的代价

2.1 “一行秒杀”的做法为什么能过,以及它的正确写法

先别急着否定 split 方案。对于绝大多数编程场景,Python 的split()确实是最直白、最不容易写错的路线。关键在于你用的是split()还是split(' '),这俩行为完全不同。

split()不带参数时,会按任意空白字符(空格、制表符、换行符等)进行分割,并且会自动忽略开头和结尾的空白,连续空白也会被当成一个分隔符处理。这就意味着:

s = " hello world " parts = s.split() # 结果是 ['hello', 'world']

连续空格被自动折叠,首尾空格被自动丢弃。这太适合这道题了。而split(' ')则严格按单个空格切分,"hello world"会切出空字符串元素,行为完全不同。所以网上流传的“一行解法”其实大多写的是不带参数的split()。

一个更稳的一行流写法是:

def length_of_last_word(s: str) -> int: words = s.split() return len(words[-1]) if words else 0

先判断 words 是否为空,避免全空格输入时越界。这个版本在绝大多数测试用例上都能通过,而且短小精悍。

2.2 空间与性能的隐性消耗:为了一道题创建整个数组

但如果你深入一层,会发现split()方案有一个绕不开的问题:它为了找到最后一个单词,把整条字符串切成了单词数组,每个单词都是一个独立的新字符串对象。在 LeetCode 这种小输入上无感,但如果把场景切换到真实工程——比如处理几十 MB 的日志文本、分析一段很长的自然语言——split()的时间和空间成本就会成倍放大。

我举个例子:假设有一个字符串包含 10 万个单词,总长度大约 600 KB 左右。用split()会一次性创建 10 万个字符串对象,加上列表本身的开销,内存峰值可能达到原始字符串的 5~10 倍。只是为了知道“最后一个单词多长”,这个代价显然不太划算。

这个点也是面试官非常喜欢追问的地方:你的解法时间复杂度是 O(n),这没问题,但空间复杂度是多少?如果你回答 O(n),他接着就会问“能不能做到 O(1)”。所以 split 方案适合快速写对、适合输入规模可控的场景,但如果你追求更节省资源的实现,就得往下看逆向扫描法。

3. 空间 O(1) 的逆向扫描法:原理、实现与复杂度拆解

3.1 从右往左数数的完整思路

逆向扫描法的核心逻辑特别朴素:既然要的是最后一个单词,那就从字符串末尾往前找。先跳过所有尾部空格,找到最后一个非空格字符,然后继续往左数,直到遇到空格或者字符串开头,数出来的长度就是答案。

整个过程分两段:

第一段,从i = len(s) - 1开始往左扫,只要s[i]是空格就i -= 1。循环结束时,i要么停在最后一个单词的最后一个字母上,要么变成 -1(说明整条字符串全是空格或本身就是空串)。

第二段,让j = i,继续往左扫,只要j >= 0且s[j]不是空格就j -= 1。循环结束时,j停在最后一个单词前那个空格的位置,或者停在 -1(说明没有更早的空格了,整个字符串从头到尾只有一个单词)。

最终答案就是i - j。这里有个数学上的小巧合:如果输入全是空格,第一段结束后i变成 -1,第二段j也保持 -1,i - j = 0,返回 0,语义完全正确。所以这版实现不需要额外判断“是否全空格”,自然就覆盖了边界。

以"hello world"为例走一遍:字符串长度 11,索引 0~10。第一段从索引 10 开始,它不是空格,直接跳出,此时i = 10,指向字符'd'。第二段从j = 10开始,'d'不是空格,j 移到 9,'l'不是空格,一直移到 5,此时s[5]是空格,循环结束。i - j = 10 - 5 = 5,正确。

3.2 三种语言的落地写法与细节对比

Python 版本:

def length_of_last_word(s: str) -> int: i = len(s) - 1 while i >= 0 and s[i] == ' ': i -= 1 j = i while j >= 0 and s[j] != ' ': j -= 1 return i - j

Java 版本:

public int lengthOfLastWord(String s) { int i = s.length() - 1; while (i >= 0 && s.charAt(i) == ' ') { i--; } int j = i; while (j >= 0 && s.charAt(j) != ' ') { j--; } return i - j; }

JavaScript 版本:

var lengthOfLastWord = function(s) { let i = s.length - 1; while (i >= 0 && s.charAt(i) === ' ') { i--; } let j = i; while (j >= 0 && s.charAt(j) !== ' ') { j--; } return i - j; };

三种语言写出来的形态几乎一样,因为这道题的逻辑不依赖任何语言特性。要注意的是:这里判断空格用的是字符' ',如果你从真实文件里读数据,行尾很可能是\r\n或\n,那就需要额外处理换行符。这个问题我后面会专门展开。

3.3 两种方案的性能对比与选型建议

方案时间复杂度空间复杂度额外内存分配适用场景
split 法O(n)O(n)创建单词数组与多个字符串对象输入规模小、追求代码简洁、快速提交
逆向扫描法O(n)O(1)无对内存敏感、输入规模大、面试追问优化

我的建议是:刷题阶段两种都要会写。先用 split 法把题意理解对,再手写一遍逆向扫描法,把边界情况重新梳理一遍。实际开发中如果只是临时取一下字符串末尾信息,数据量不大,split 法完全够用,不用为了“最优”而过度设计。但如果你在处理日志文件、大文本,或者处在内存受限环境,逆向扫描几乎是唯一合理的选择。

4. 这道题的坑位清单:边界条件设计的完整思路

4.1 我把能想到的输入全测了一遍

我在本地写了一个测试脚本,把能想到的输入形态全跑了一遍,下面是我的用例和预期结果:

输入预期输出说明
""0空字符串
" "0纯空格,多个
"hello"5单单词无空格
"hello "5末尾一个空格
"hello "5末尾多个空格
" hello"5前导空格
"hello world"5常规两个单词
"hello world "5末尾带空格的常规输入
"a"1单个字符
"a "1单字符加末尾空格
" day"3前导空格加短单词

跑完有一件事让我印象很深:很多看起来“没问题”的实现,栽就栽在"a "这种单字符加空格的组合上。有些人的思路是先split()再判断,这没问题;但有些人的思路是“从左往右扫,遇到空格就重置计数器”,这种思路如果不加“跳过末尾空格”的预处理,必然会在这类用例上出错。

4.2 面试官常拿这些变体来加难度

这道题的变体比原题本身更能说明问题。我在不同场合见过以下追问方式:

第一种是改分隔符。比如“给你一个字符串,只包含字母和逗号,返回最后一个逗号分隔块的长度”。这时候逆向扫描法的思想完全复用,只是把判断s[i] == ' '换成s[i] == ','。这种追问考察的是你有没有真正理解算法本质,而不是背答案。

第二种是改返回值。比如“返回最后一个单词本身,而不是长度”。思路依然是从末尾往前找到起始位置,但最后一步不是i - j,而是切片s[j+1:i+1]。这里尤其要注意切片的下标关系,j+1是单词起点,i+1才是终点的“后一位”,写错一个索引就全错了。

第三种是加空白类型。比如问“如果字符串里除了空格还有制表符和换行符,怎么做”。这时候split()依然是省心的,因为它默认按所有空白字符切分。但对手写的逆向扫描法来说,判断条件就要从s[i] == ' '扩展成s[i].isspace()或正则匹配,稍微多两行,却也是真实文本处理中必然会遇到的问题。

我之前见过一位候选人,原题写得又快又对,但面试官把末尾空格换成\t,他当场卡住。原因就是他只记住了“判断空格”,没有理解“跳过任意空白字符”这个语义。这道题真正的价值,就是逼你把“空白字符”这个概念理解完整。

5. 从字符串尾部处理看工程思维:这题真的不止是刷题

5.1 日志文件末尾读取场景中的内存取舍

讲完刷题套路,我想重点说说这题和真实工程的关联。最直接的一个场景就是读取日志文件的最后一段内容。

假设你有一个运行中的服务,日志文件不断追加内容,你想只读取最后一行或者最后一句话,而不是把整个文件载入内存。最简单粗暴的做法是read()全部读进来再处理,但文件一大,内存就吃紧。更合理的做法其实和逆向扫描法的思路一脉相承:先从文件末尾定位,往前找最近的换行符,只读那一段字节,再做后续解析。

这种“从尾部向前找边界”的思想,本质上就是这道题的工程化版本。只是工程里把“空格”换成了“换行符”,把“字符串末尾”换成了“文件末尾”,核心的定位逻辑没有任何改变。

5.2 数据清洗:尾部脏字符和结束标记的处理

另一个高频场景是数据清洗。从外部接口拿回来的字符串,经常带着各种尾部脏数据:空格、制表符、换行符,甚至 BOM 头、结束标记。常规方案确实就是rstrip(),但如果这段字符串没有读入内存,而是流式接收的,你就得自己维护“最后一个有效段”的状态。

我自己在处理流式分词时踩过类似的坑:数据按块到达,每个块末尾可能被截断在单词中间,也可能正好落在空格后面。当时用了一个简单状态机,每次处理一个块时,先记下“当前是否有未闭合的单词”,再等下一个块到达时继续拼接。这其实就是把“找最后一个单词”的逻辑拆到了时间维度上:这道题在连续输入的工程场景里,答案是动态更新的。

从这个角度看,刷题不只是为了应付面试。字符串边界处理、尾部空白忽略、索引越界防范,这些能力在日志监控、数据清洗、接口参数校验里全都是实打实的日常需求。

6. 给新人的练习路径:从“写对”到“讲明白”

6.1 三个阶段的递进练习方法

如果你刚开始刷题,我不建议直接背这道题的答案。更有效的路径是分三步走:

第一步,用 split 法写一个能过的版本,把题目里“空格”的含义弄清楚,理解split()和split(' ')的区别。很多人在这一步就已经能体会到“API 默认行为也是一种隐式约定”。

第二步,在不看题解的前提下,手写逆向扫描法,把 4.1 节那张测试用例表全部跑一遍。这一步练的是循环边界和 index 的敏感度,特别是i >= 0这种条件,写漏一个就会在某些输入上越界或死循环。

第三步,尝试回答“为什么你的解法空间复杂度是 O(1)”“如果分隔符换成逗号怎么改”“如果字符串非常大怎么办”这类追问。能做到这一步,才算真正吃透了这道题。

6.2 面试中被追问时,哪些回答能加分

面试官问这种简单题,通常不是考察你能不能写出来,而是考察你思考的完整度。我在实际交流中见过两类回答:

低分回答是:写完代码,问“还有问题吗”,回答“没问题了”。这基本等于告诉对方你没想过边界。

高分回答是:写完代码,主动说“这个解法在最坏情况下要遍历整个字符串,时间复杂度 O(n),空间 O(1);如果输入末尾没有空格,第一段可以少扫几次;如果输入包含大量单词且只关心最后一个,没必要把整个字符串存成数组,所以选逆向扫描比较合适”。

这两者的差距不在于代码,而在于有没有把“为什么这么写”想透。

另外,如果你在面试中提前向面试官确认输入范围,比如“这个字符串可能包含换行符吗”“单词里会有数字吗”,这本身就是一个加分行为。因为真实开发里,需求永远比题目描述的更模糊,先问清楚约束条件,是两个成熟工程师之间沟通的正常方式,也是把简单题做出工程感的秘诀。

最后说点个人经验。我刷到这道题时已经工作了一段时间,当时第一反应也是写 split 一行流,但后来处理一次线上日志解析时,发现内存占用涨得太快,才回头认真研究逆向扫描。从那以后,我遇到任何“取最后一个 X”的问题,都会条件反射地想一遍:能不能从尾巴往前找?能不能不创建额外数组?这已经变成一种肌肉记忆了。

如果你把这道题当作纯粹的算法题,它确实是小菜一碟。但如果你把它当作一次字符串边界处理的基本功训练,反复打磨自己的实现和表达,它能带给你的东西远比那 5 行代码多得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询