ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

C语言刷题实战:反向遍历解决“最后一个单词的长度”

C语言刷题实战:反向遍历解决“最后一个单词的长度” C语言刷题和用脚本语言刷题完全是两种体验。拿这道“最后一个单词的长度”来说在Python里一行s.strip().split()[-1]就收工了但在C语言里没有现成的split工具你得自己面对字符串、指针和边界条件。这也是很多初学者在LeetCode 58这道Easy题上翻车的原因——不是不会写逻辑而是不知道该怎么处理“特殊情况”。题目本身很简单给一个由英文字母和空格组成的字符串返回最后一个单词的长度。单词就是连续的非空格字符。比如Hello World返回5 fly me to the moon 返回4luffy is still joyboy返回6。这篇文章我会从思路推导、代码实现、边界测试到调试踩坑完整过一遍适合刚开始刷题、或者想巩固C字符串基本功的人参考。1. 一道Easy题的隐藏难度在哪里先说个反直觉的事实这道题通过率并不低但每年论坛上都有人发帖问“为什么我的代码提交不过”。问题几乎都出在同一个地方——大家习惯性地把字符串当成一个可以用“分割”处理的序列却没意识到C语言里没有split你只能一个字符一个字符地判断。1.1 送分题背后的语言差异LeetCode 58的官方难度是Easy题目约束也不复杂字符串长度最大10^4只包含大小写英文字母和空格字符而且保证至少有一个单词。如果你用高级语言这题的解法就是调库。但C语言里没有这种“一句话”的便利你必须自己设计遍历逻辑。这种差异正是C语言题目容易被低估的原因。我第一次做这道题时第一反应是正着遍历用一个count记录当前单词长度遇到空格就清零最后输出count不就行了吗这个思路听上去完全合理但写到一半你会发现一个尴尬的问题——如果字符串末尾有空格最后一次遇到空格已经把计数器清零了你记录的就变成了“最后一个单词之后的所有空格”的长度也就是0。必须额外用一个变量保存“上一次非空格段的长度”这逻辑一下子就拐弯了。1.2 为什么要选择“倒着找”的视角正序遍历确实能解但代码绕。换个视角会清爽得多既然要找的是“最后一个”单词为什么不直接从字符串末尾往前找从末尾往前遇到的第一段连续非空格字符就是我们要的答案。这样既不需要保存历史状态也不需要额外的数组或变量一次扫描就能解决。这种“倒着处理”的思路在真实开发中也很常见。比如解析日志文件时你往往只关心行尾的最后一个字段处理用户输入时你只想拿到末尾那段有效数据。从末尾定位目标往往比从开头一路找过去省事得多。2. 反向双指针从右往左数非空格字符把思路转换成可执行的算法其实就是两个循环第一个循环跳过末尾所有的空格第二个循环数非空格字符的数量。这两个循环配合就能精确锁定最后一个单词的长度。2.1 跳过末尾空格第一轮扫描的意义假设字符串是Hello World 末尾有三个空格。我们要找的最后一个单词是World长度为5。第一步就是把World后面的三个空格全部跳过去让指针停到d的位置。这一步很关键它把“末尾可能有空格”的干扰彻底清除了后续的数数操作才会准确。有不少人会问如果字符串末尾没有空格怎么办没有空格的话第一轮扫描直接不会执行指针停在字符串末尾的\0前一位正好是最后一个单词的最后一个字符。所以“先跳空格”这个操作对“无空格”的情况天然免疫不需要特判。2.2 数非空格字符第二轮扫描的终止条件指针停到最后一个单词的末尾后开始往前逐个字符检查只要不是空格计数器就加1继续往前一旦遇到空格说明这个单词已经完整数完立刻退出循环。这里有两个终止条件需要你非常警惕遇到空格字符说明单词左边是边界指针已经越过字符串开头索引变成-1说明这个单词就是字符串的第一个单词前面没有空格了。第二个条件是新手最容易漏掉的。举个例子字符串是Hello没有空格第二轮扫描会一直往前数数到H之后指针继续减到-1这时如果循环条件里没有“索引大于等于0”的判断代码就会访问s[-1]属于非法内存访问。虽然偶尔“碰巧”能跑出结果但这是典型的未定义行为在线判题系统里可能直接崩溃。2.3 用生活化类比理解两次扫描想象你面前有一摞文件最上面几张是空白的底下压着写有关键信息的纸。你要找最后一张写了字的纸。正常人不会从最底下开始一张一张翻开检查而是直接从上往下把空白纸抽掉然后拿起第一张有字的纸数它的字数就行。这个“先抽空白纸再数关键信息”的流程就是上面的两次扫描。3. C语言实现与代码逐行拆解下面给出一个干净、无额外依赖的C语言实现。这个版本只用到标准库里的strlen核心逻辑完全是自己写的。#include stdio.h #include string.h int lengthOfLastWord(char* s) { int len strlen(s); int i len - 1; int count 0; while (i 0 s[i] ) { i--; } while (i 0 s[i] ! ) { count; i--; } return count; }3.1 每个关键步骤在干什么第一步strlen(s)求出字符串长度并让索引i指向字符串最后一个字符的位置。这里有个细节C字符串以\0结尾s[len - 1]才是最后一个有效字符s[len]是字符串结束符不需要参与遍历。第二步第一个while循环里i 0和s[i] 是两个必须同时满足的条件。有人会问为什么先判i 0再取s[i]因为运算符有短路特性如果i已经变成-1后面的s[i]根本不会执行避免了非法访问。如果把顺序反过来遇到全空格字符串时代码会在i -1时去读s[-1]轻则结果随机重则段错误。第三步第二个while循环开始数数。每遇到一个非空格字符count加1。循环退出时要么遇到了空格说明单词左边有边界要么i变成了-1说明单词已经延伸到了字符串开头。两种情况都表示我们已经数完了整个单词。3.2 三种常见写法对比除了上面这种“索引法”还有两种写法在C语言里也很常见。第一种是纯指针版本不显式使用下标int lengthOfLastWord(char* s) { char* p s strlen(s) - 1; while (p s *p ) p--; int count 0; while (p s *p ! ) { count; p--; } return count; }这个版本的核心逻辑和索引法完全一致只是把“索引和数组名”换成了“指针和指针比较”。指针版本更贴近C语言的底层习惯但要注意p s这种比较在指针越界时同样是未定义行为。标准库的写法里更稳妥的是先判断p s再解引用和索引法的短路判断是一个道理。第二种是不依赖strlen的“完全手写”版本int lengthOfLastWord(char* s) { int len 0; while (s[len] ! \0) len; int i len - 1; while (i 0 s[i] ) i--; int count 0; while (i 0 s[i] ! ) { count; i--; } return count; }这种写法在一些禁止调库的刷题环境里更稳妥因为strlen本身也是一次完整遍历提前算出来并不会影响整体复杂度。三者的时间代价都是扫描常数量级区别只在于代码风格和可读性。我个人推荐索引版本入门逻辑直接调试时不容易在指针比较上栽跟头。3.3 关于返回值和题目约束的说明题目的约束是字符串长度至少为1且至少有一个单词所以len - 1不会变成负数。但在实际工程代码里调用者可能传空字符串、全空格字符串甚至空指针进来。你会看到很多题解不考虑这些情况因为在线判题系统的测试用例严格符合题目约束。不过把“空指针判断”写进去是一种职业习惯尤其是你准备把这段逻辑复用到生产环境时。加上防御性判断本身不会影响这道题的答案而且能让你的代码更稳健。int lengthOfLastWord(char* s) { if (s NULL) return 0; // 其余逻辑不变 }4. 边界情况才是本题的主战场6类测试用例一个算法题真正考你的往往不是主流程而是边界。这一道题我整理了6类必须手动验证的用例每一类都对应一种容易出错的输入形态。用例输入预期输出考察点基础案例Hello World5正常双单词场景末尾多空格Hello World 5第一轮跳过空格是否生效开头多空格 Hello World5开头的空格是否被正确忽略全空格 0会不会发生越界单个单词Hello5第二轮扫描到开头能否退出单个字母H1最小规模输入4.1 全空格用例为什么会卡住很多人按照题目约束输入不会全空格。但很多用户在用本地编辑器测试时会顺手敲几个空格进去然后发现程序输出一堆奇怪的结果甚至崩溃。原因前面已经说过第二个循环里如果i降到-1后仍然去判断s[i]就会越界。如果你写了while (s[i] )而忘了i 0这个用例基本上必挂。我建议所有人在本地测试时至少把全空格用例跑一遍。这不仅仅是刷题的问题而是“万一调用方给了脏数据我的程序会怎样”的意识。真实项目中用户输入末尾多几个空格、整个字段全是空格的情况太常见了这类防御性编写习惯会让你少挨很多骂。4.2 只有一个单词时第二轮循环如何正确退出当输入是Hello字符串末尾没有空格第一个循环直接退出i停在o的位置。第二个循环会依次检查o、l、l、e、H每检查一个count加1。检查完H后i变成-1此时i 0不再成立循环终止返回5。整个过程没有越界。很多人在这一步会写成while (s[i] ! )完全忘记检查索引下界。在Hello这种用例上s[-1]读取的是数组前一个地址的内容可能是垃圾数据也可能恰好不是空格导致继续循环结果彻底乱套。这个问题在本地测试中不一定稳定复现但在在线判题环境里很可能暴露出来。4.3 混合极端情况的组合测试除了上面这种单一边界还要习惯做“组合测试”比如 a 开头三个空格、一个字母、结尾三个空格。这种用例能同时考验第一轮跳过空格、第二轮正确识别单词、以及索引不越界三个能力。组合测试是刷题里最容易漏掉的因为很多人测试时习惯只改一个变量而真实系统的输入往往同时存在多种边界问题。我一般会用这样一组用例做快速回归全空格、单字母、单字母加前后空格、长串单词加末尾空格、超长字符串加混杂大小写。只要这几个都过了主流程基本稳了。5. 调试实录我在提交过程中翻过的车前面讲了很多理论这里分享一下我自己实际调试这道题时踩过的几个坑。每一个都让我多花了时间如果你正在做这题看到这些能少走点弯路。5.1 第一个坑把\0和 搞混我第一次拿这道题练手时下意识在循环里写了s[i] ! \0作为判定条件。这个错误非常隐蔽因为字符串末尾确实是\0而且程序在某些用例下能跑出正确答案。比如Hello World从末尾往前找遇到d之前没有任何空格while 循环在第一个空格就退出\0根本不会被遇到所以字符串里有没有\0参与判断根本不会影响该用例的结果。但放到Hello World 这个用例上第一轮跳过空格时会一直往前跳直到遇到W才停而\0在d后面的位置整个过程中\0同样没有被访问到。真正能暴露问题的用例是a这种最短单词从a往左一步就是越界位置这时候访问内存可能读到任意值。所以一定要记住\0是字符串终止符 是空格字符二者不一样循环里判断结束要用索引边界而不是靠\0当兜底。5.2 第二个坑strlen 的返回值类型阴了一下strlen的返回类型是size_t本质上是无符号整数。如果直接把strlen(s) - 1赋给一个int变量在大多数平台上没问题因为在int能表示的范围内无符号转到有符号没有语义变化。可一旦字符串长度为0strlen(s) - 1会变成size_t类型的最大值赋给int后变成一个负数。虽然题目约束保证了长度至少为1但在本地测试时输入空串我的第一版代码就出现了索引负数的情况走了完全错误的流程。用int len strlen(s);显式转换一下可读性和安全性都更好。5.3 第三个坑用本地编译器过了提交后却超时严格来说这道题不会超时但我曾经把正序遍历写法里的变量count用成了static局部变量。结果多次调用之间状态没有清零第二次调用拿到的是上一次遗留的计数整个程序行为错乱。类似这种“状态残留”问题在刷题时不多见但在写真实服务时很常见——一个全局状态被多个逻辑共用又忘了复位。提示刷题时优先使用局部变量不要用static或全局变量保存中间状态。C语言里static变量的初始化时机和生命周期与普通局部变量完全不同很多诡异问题都是从这里冒出来的。6. 把这道题的思路移植到真实开发场景LeetCode 58看起来像是纯粹的面试题但它背后的“从末尾截取一段有效数据”思想在真实开发里应用非常广泛。至少有三类场景能直接套用。6.1 日志文件行尾字段提取处理日志时行尾往往是时间戳或状态码而行的中间可能包含用户输入内容本身就有空格。比如[INFO] 2025-01-15 10:23:45 user login success如果我想提取最后的success直接正着切需要根据空格位置计算多个字段的起始和结束非常繁琐。用同样的倒查思路先跳过末尾的空格再数到下一个空格或行首就能拿到最后一个单词。我写过的一个小工具就是靠这个逻辑在几万行日志里提取状态码速度比逐行split快得多。6.2 处理用户输入中的多余空格用户输入带前后空格是常态。比如一个表单里的搜索关键词用户在末尾敲了一个空格直接拿去查询就会出问题。很多C语言项目里没有现成的trim函数你完全可以仿照这道题的逻辑从末尾把空格跳过再配合开头跳过空格的逻辑就得到了一个轻量级的“去首尾空格”工具。这种自己动手写的小函数比引库更可控。6.3 协议解析中的“最后一个字段”场景一些简单的自定义网络协议字段用空格分隔但最后一节不允许有空格。解析时用同样的思路从报文末尾倒着找最后一个非空字段就能快速提取结束标志或校验码。倒查的好处是天然忽略前面字段长度变化带来的影响——不管前面有多少个字段、字段内容是什么我关心的只是尾部那一段。我个人的体会是刷题的最大收获往往不是记住某道题的答案而是把题目里的思路拆出来变成自己写代码时的一种条件反射。LeetCode 58的代码量不大却把“两次扫描”“索引边界”“防御性判断”这几个C语言核心能力完整过了一遍。如果你刚开始刷题不妨把这几个点当成比正确答案更重要的收获。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表