资讯动态

单链表一趟遍历找最大值结点:从课后题到面试实战

发布时间:2026/10/16 13:18:03 来源:尧图企业网站定制
1. 从一道课后题说起单链表一趟遍历找最大值到底在考什么单链表找最大值听起来像是数据结构课里最不起眼的那种小题很多人扫一眼题目就觉得自己会了——遍历一遍比一比记下最大的那个完事。但真正动手写过、并且在纸上或者白板上被追问过细节的人都知道这道题藏着的坑比想象中多。P53算法设计题第6题要求通过一趟遍历确定长度为n的单链表中值最大的结点关键词有三个一趟遍历、长度为n、值最大的结点。这三个词每一个都在限定你的解法边界。先说一趟遍历。这意味着你不能先遍历一遍求最大值再遍历一遍去找那个结点那样是两趟。你必须在一次从头到尾的扫描过程中同时完成找最大值和定位最大值所在结点两件事。这其实是一个很经典的在线算法思想——数据只能看一遍边看边维护当前最优解。类似的问题还有一趟遍历找倒数第k个结点一趟遍历判断是否有环等等都是链表操作里考察指针运用能力的经典题型。再说长度为n。这个条件看起来是废话但它其实在暗示你链表长度是已知的不需要你额外去数。有些变体题目不给n那就需要你自己在遍历中计数或者用快慢指针来处理。这里给了n说明出题人希望你专注于找最大值这个核心逻辑而不是在长度上做文章。最后说值最大的结点。注意题目要的是结点不是最大值本身。这意味着你的算法不仅要记录当前最大值是多少还要记录这个最大值对应的结点指针。很多同学第一次写的时候只返回了一个int型的最大值结果被扣分——因为题目要的是结点你得返回一个指针或者结点引用。这道题在实际面试中也经常出现只不过面试官往往会加码如果链表中有多个结点的值相同且都是最大值你要返回哪一个如果链表为空怎么办如果链表只有一个结点呢这些边界情况才是真正拉开差距的地方。我带过几个刚入门的同学做这道题大部分人能写出基本逻辑但能一次性把所有边界都处理干净的不到三成。所以这篇文章不只是给你一个标准答案而是要把这道题从头到尾拆开讲清楚每一步为什么这么写、哪里容易出错、面试或者作业里怎么写出让老师眼前一亮的代码。不管你是正在赶数据结构作业的学生还是准备面试的求职者或者只是想把链表操作重新捡起来的开发者下面的内容都能直接拿去用。2. 解题思路的整体设计与方案选型2.1 为什么选择边遍历边比较而不是其他方案拿到这道题第一反应可能有几种。最朴素的想法是先把所有结点的值存到一个数组里然后用排序或者线性扫描找最大值最后再回到链表里找对应结点。这个方案能work但空间复杂度是O(n)而且实际上遍历了不止一趟——存数组是一趟找最大值是一趟回链表定位又是一趟。题目明确要求一趟遍历这个方案直接出局。另一种想法是用递归。递归本质上也是遍历但递归的空间复杂度是O(n)调用栈而且对于找最大值结点这个问题递归的写法反而比迭代更绕——你需要在递归回溯的过程中比较并返回最大值结点代码可读性不见得更好。除非题目特别要求递归实现否则迭代是更自然的选择。所以最优方案就是迭代式一趟遍历用两个指针变量一个记录当前遇到的最大值比如叫maxVal一个记录当前最大值所在的结点比如叫maxNode。从头结点开始沿着next指针一路走到底每遇到一个结点就比较它的值和maxVal如果更大就更新maxVal和maxNode。走完之后maxNode就是答案。这个方案的时间复杂度是O(n)空间复杂度是O(1)完美符合题目要求。而且代码非常直观几行就能写完。但能写完和写对之间还有距离下面我们逐行拆解。2.2 带头结点与不带头结点的差异处理单链表有一个经典的坑带头结点还是不带头结点。这两种结构在操作上有些微差异很多教材尤其是国内教材默认使用带头结点的单链表而很多在线判题系统和实际工程代码用的是不带头结点的。这道题没有明确说但根据P53这个位置和常见教材的惯例大概率是带头结点的。带头结点意味着链表的第一个结点头结点不存储有效数据它只是一个哨兵真正的数据从head-next开始。这种情况下初始化maxNode的时候不能直接拿head来比较因为head里的data可能是垃圾值。正确的做法是先把maxNode指向head-nextmaxVal设为head-next-data然后从head-next-next开始遍历。不带头结点的话head本身就是第一个有效结点初始化时maxNode headmaxVal head-data然后从head-next开始遍历。这个差异看起来很小但如果你搞混了代码可能在测试用例上跑出莫名其妙的结果。我建议你在写作业的时候先在注释里明确写清楚本算法基于带头结点的单链表这样即使老师用的是另一种结构也能看出你的思路是对的。2.3 空链表与单结点链表的边界处理空链表的情况必须单独处理。如果链表为空head NULL或者带头结点时head-next NULL那么根本不存在值最大的结点这时候应该返回NULL或者抛出一个异常具体取决于题目的要求。很多同学写代码的时候直接假设链表非空结果遇到空链表就段错误了。单结点链表其实不需要特殊处理只要你的初始化逻辑正确遍历循环自然就不会执行直接返回那个唯一的结点。但如果你在初始化时犯了错比如把maxVal初始化成了0或者INT_MIN而结点的值恰好是负数就可能出问题。所以初始化的正确性很关键。注意初始化maxVal的时候千万不要图省事写成maxVal 0或者maxVal -1。如果链表里所有结点的值都是负数你的算法会返回一个错误的结点或者返回NULL。正确的做法是用第一个结点的值来初始化。3. 核心细节解析与代码实现要点3.1 指针操作的常见陷阱链表操作最怕的就是指针乱飞。在这道题里虽然不涉及插入删除但指针的使用仍然有几个容易翻车的地方。第一个陷阱是空指针解引用。在遍历循环里你判断循环是否结束的条件应该是p ! NULL而不是p-next ! NULL。如果你写成后者那么当p指向最后一个结点时循环体会执行然后p p-next变成NULL下一轮循环判断p-next就会崩溃。这个错误在新手里非常常见因为很多人习惯性地觉得要判断下一个结点是否存在但实际上你应该判断当前结点是否存在。第二个陷阱是比较时的类型问题。如果结点的data是int型直接比较没问题。但如果data是float或者double直接用比较可能会有精度问题。更稳妥的做法是用一个epsilon来做容差比较不过对于这道作业题来说int型是默认情况不用过度设计。第三个陷阱是返回值类型。题目要的是结点所以函数的返回类型应该是结点指针比如Node*或者LNode*而不是int。如果你返回int那就变成返回值最大的值而不是值最大的结点了。这个区别在考试里是会被扣分的。3.2 完整代码实现与逐行注释下面给出带头结点版本的完整C语言实现。如果你用的是不带头结点的版本只需要把初始化的部分改一下即可。#include stdio.h #include stdlib.h typedef struct LNode { int data; struct LNode *next; } LNode, *LinkList; /** * 在带头结点的单链表中查找值最大的结点 * param head 头指针指向头结点 * return 值最大的结点指针若链表为空则返回NULL */ LNode* findMaxNode(LinkList head) { // 边界处理头指针为空或者链表为空只有头结点 if (head NULL || head-next NULL) { return NULL; } // 初始化从第一个有效结点开始 LNode *maxNode head-next; // 记录当前最大值结点 int maxVal maxNode-data; // 记录当前最大值 // 从第二个有效结点开始遍历 LNode *p head-next-next; while (p ! NULL) { if (p-data maxVal) { maxVal p-data; maxNode p; } p p-next; } return maxNode; }这段代码看起来简单但每一行都有讲究。head NULL处理的是头指针本身为空的情况head-next NULL处理的是空链表只有头结点没有有效数据。初始化时用head-next-data而不是硬编码一个值保证了即使所有数据都是负数也能正确工作。循环从head-next-next开始因为第一个结点已经在初始化时处理过了。如果你用的是不带头结点的链表代码会变成这样LNode* findMaxNodeNoHead(LinkList head) { if (head NULL) { return NULL; } LNode *maxNode head; int maxVal head-data; LNode *p head-next; while (p ! NULL) { if (p-data maxVal) { maxVal p-data; maxNode p; } p p-next; } return maxNode; }对比一下就能发现不带头结点的版本少了一层-next初始化也更直接。两种写法没有优劣之分关键是要和你的链表结构定义保持一致。3.3 时间与空间复杂度的严格分析时间复杂度方面算法从头到尾只遍历了一次链表每个结点访问一次比较一次所以是O(n)。这里的n是链表中有效结点的个数不包括头结点。即使是最坏情况最大值在最后一个结点也只需要n-1次比较仍然是O(n)。空间复杂度方面只用了三个指针变量maxNode、p和一个整型变量maxVal都是常数级别的额外空间所以是O(1)。这个空间复杂度已经是最优了不可能再低。有些同学可能会问如果我用递归写空间复杂度是多少答案是O(n)因为递归调用栈的深度等于链表长度。所以在空间上迭代版本严格优于递归版本。这也是为什么我推荐迭代写法。提示在作业或者考试中写完代码后一定要把时间复杂度和空间复杂度分析写上。这是得分点而且能体现你对算法的理解深度。4. 实操过程与完整测试验证4.1 构建测试用例的完整流程光写函数不够你得验证它是对的。下面我给出一个完整的测试程序包括创建链表、调用函数、打印结果的完整流程。你可以直接复制到本地编译运行。#include stdio.h #include stdlib.h typedef struct LNode { int data; struct LNode *next; } LNode, *LinkList; // 尾插法创建带头结点的单链表 LinkList createList(int arr[], int n) { LinkList head (LinkList)malloc(sizeof(LNode)); head-next NULL; LNode *tail head; for (int i 0; i n; i) { LNode *newNode (LNode*)malloc(sizeof(LNode)); newNode-data arr[i]; newNode-next NULL; tail-next newNode; tail newNode; } return head; } LNode* findMaxNode(LinkList head) { if (head NULL || head-next NULL) { return NULL; } LNode *maxNode head-next; int maxVal maxNode-data; LNode *p head-next-next; while (p ! NULL) { if (p-data maxVal) { maxVal p-data; maxNode p; } p p-next; } return maxNode; } void testCase(int arr[], int n, const char *desc) { LinkList head createList(arr, n); LNode *result findMaxNode(head); printf(测试用例%s\n, desc); if (result NULL) { printf( 结果链表为空返回NULL\n); } else { printf( 最大值结点的值%d\n, result-data); } printf(\n); // 释放内存作业里可以不写但养成好习惯 LNode *p head; while (p ! NULL) { LNode *tmp p; p p-next; free(tmp); } } int main() { int arr1[] {3, 1, 4, 1, 5, 9, 2, 6}; testCase(arr1, 8, 普通情况最大值9在中间); int arr2[] {-5, -3, -8, -1, -4}; testCase(arr2, 5, 全部为负数最大值-1); int arr3[] {7}; testCase(arr3, 1, 只有一个结点); int arr4[] {2, 2, 2, 2}; testCase(arr4, 4, 所有结点值相同); int arr5[] {1, 2, 3, 4, 5}; testCase(arr5, 5, 最大值在最后一个结点); testCase(NULL, 0, 空链表); return 0; }这个测试程序覆盖了六种典型情况普通情况、全负数、单结点、全相同、最大值在末尾、空链表。跑一遍下来如果所有输出都符合预期说明你的算法基本没问题。4.2 测试结果分析与验证跑完上面的测试程序你应该看到类似这样的输出测试用例预期结果实际结果是否通过普通情况99通过全负数-1-1通过单结点77通过全相同2第一个2通过最大值在末尾55通过空链表NULLNULL通过这里有一个细节值得注意当所有结点的值相同时算法返回的是第一个最大值结点。这是因为我们用的是严格大于号只有遇到更大的值才更新。如果你想让算法返回最后一个最大值结点把改成即可。这个选择取决于题目要求如果没有特别说明返回第一个是更常见的做法。4.3 在纸上手写代码的实操建议如果是考试或者面试你需要在纸上或者白板上手写代码。这时候有几个技巧可以帮你减少错误。第一先写结构体定义。虽然题目可能已经给了但你自己写一遍能加深印象也能避免用错字段名。第二把边界处理写在最前面。空链表、单结点这些情况先处理掉后面的主逻辑就不用再操心这些了。第三初始化的时候用第一个结点的值不要用0或者INT_MIN。这个习惯能帮你避免负数场景的bug。第四循环条件写p ! NULL不要写p-next ! NULL。写完之后自己心里默念一遍当p是最后一个结点时循环体执行然后p变成NULL循环结束。确认逻辑正确。第五返回之前检查一下maxNode是否为空。虽然理论上不会为空因为前面已经处理了空链表但多一层检查不亏。实操心得我在带同学做这道题的时候发现最常见的错误不是逻辑错误而是初始化错误。很多人习惯性地把maxVal初始化成0然后遇到全负数的链表就翻车了。记住链表的第一个有效结点永远是你最好的初始化来源。5. 常见问题与排查技巧实录5.1 段错误与野指针的排查方法段错误是链表代码最常见的崩溃原因。如果你跑程序的时候遇到Segmentation fault按下面的顺序排查。首先检查空指针解引用。在每一个-操作之前问自己这个指针有没有可能是NULL特别是在初始化和循环条件这两个地方。如果你写了head-next-data而head是NULL那就直接崩了。其次检查内存分配。如果你用malloc创建结点了检查有没有分配失败的情况。虽然在小程序里malloc几乎不会失败但养成检查习惯是好的。最后检查释放逻辑。如果你在测试程序里释放了内存确保没有重复释放或者释放后继续使用。这个在作业里不常见但在实际项目中很致命。5.2 逻辑错误与边界遗漏的速查表下面这张表列出了这道题最常见的错误和对应的解决方法你可以对照自己的代码逐项检查。常见错误错误表现原因分析解决方法初始化maxVal为0全负数链表返回错误结果没有用第一个结点的值初始化改为maxVal head-next-data循环条件写错段错误或漏掉最后一个结点写成了p-next ! NULL改为p ! NULL返回类型错误返回值而不是结点函数签名写成了int改为返回LNode*空链表未处理段错误直接访问了head-next-data开头加空链表判断带头结点/不带头结点混淆结果莫名其妙初始化时多算或少算了一层明确链表结构统一处理相同最大值返回最后一个与预期不符用了而不是根据题目要求选择比较符5.3 面试中的追问与应对策略如果你在面试中遇到这道题面试官大概率会追问几个问题。提前准备好答案能让你从容应对。第一个常见追问是如果链表中有多个最大值你要返回哪一个你可以回答默认返回第一个因为用的是严格大于号。如果需要返回最后一个把比较符改成大于等于即可。具体取决于业务需求。第二个追问是如果链表长度未知你的算法还能用吗答案是能用的因为我们的算法本来就不依赖n遍历到NULL自然结束。n这个条件只是题目给的不影响算法本身。第三个追问是能不能用递归实现可以但空间复杂度会变成O(n)。你可以现场写一个递归版本展示你的代码能力。第四个追问是如果链表是双向链表呢算法完全一样只是结点结构多了个prev指针遍历方向不变。这个问题考察的是你对链表结构的理解是否灵活。避坑技巧面试的时候写完代码后主动说出时间复杂度和空间复杂度再主动提一下边界情况的处理。这样即使代码有小瑕疵面试官也能看出你的思维是完整的。6. 从这道题延伸出去的知识点6.1 一趟遍历思想的更多应用一趟遍历这个约束在链表问题里反复出现掌握了一道题一类题都会了。比如找倒数第k个结点用快慢指针快指针先走k步然后两个指针一起走快指针到末尾时慢指针就是答案。再比如判断链表是否有环也是快慢指针快指针每次走两步慢指针每次走一步如果有环它们必然相遇。还有找中间结点同样是快慢指针快指针到末尾时慢指针刚好在中间。这些问题的共同点是你只有一次扫描机会必须在扫描过程中维护足够的信息来得到答案。这种思维方式在流式数据处理、在线算法等领域非常重要。比如在一个数据流里找最大值、找中位数、找top k都是类似的思想。6.2 链表操作在工程实践中的价值有人可能会说链表在实际工程里用得越来越少数组和哈希表才是主流。这话有一定道理但链表的思维方式——指针操作、内存管理、边界处理——是通用的编程能力。你在写链表代码时培养的严谨性会迁移到其他数据结构的使用上。而且链表并没有完全退出历史舞台。在操作系统内核里链表仍然是最基础的数据结构之一。在LRU缓存、内存分配器、文件系统等场景里链表的身影随处可见。即使你平时用的是高级语言理解链表的底层原理也能帮你写出更高效的代码。6.3 如何把这道题写进简历或项目经历如果你正在准备求职这道题本身太小不足以单独写进简历。但你可以把它作为一个引子展示你对数据结构的理解。比如在简历里写熟悉链表、栈、队列等基础数据结构能够手写常见算法如单链表找最大值、反转链表、判断环等并分析时间空间复杂度。这样既展示了能力又不会显得浮夸。如果你做过更复杂的项目比如用链表实现了一个简单的内存池或者任务调度器那就可以把这道题作为项目里的一个技术点来写。重点不是题目本身而是你从中体现出的算法思维和代码能力。7. 我个人在实际操作中的几点体会这道题我前前后后写过不下十遍每次教别人的时候都会有新的感受。最开始我觉得它太简单不值得花时间。后来带同学做作业发现每个人踩的坑都不一样才意识到简单题里藏着基本功。我的第一个体会是初始化决定成败。链表题目的bug一半以上出在初始化上。用第一个有效结点的值来初始化maxVal这个习惯能帮你避开绝大多数边界问题。不要图省事用0或者INT_MIN那是在给自己挖坑。第二个体会是循环条件要背下来。while (p ! NULL)这个写法应该变成肌肉记忆。每次写链表遍历手自动就敲出这个条件不要临时去想。想多了就容易写成p-next ! NULL然后段错误。第三个体会是测试用例要覆盖边界。空链表、单结点、全负数、全相同、最大值在末尾这五种情况跑一遍基本就能确认算法正确。不要只测一个普通情况就交差那是在赌运气。第四个体会是复杂度分析要写出来。不管是作业还是面试写完代码后把O(n)时间和O(1)空间写上。这是得分点也是展示你思维完整性的机会。很多人代码写对了但忘了写复杂度白白丢分。最后一个体会是不要小看任何一道基础题。这道题看起来简单但它考察的指针操作、边界处理、算法思维是后面所有复杂算法的基础。把这题吃透了后面学树、图、动态规划都会轻松很多。基础不牢地动山摇这话在数据结构学习里是真理。如果你正在做这道作业希望上面的内容能帮你少走弯路。如果你是在准备面试希望这些追问和应对策略能让你更有底气。如果你只是路过希望这篇分享能让你对链表操作有一点新的认识。代码这东西看会了不算会写会了才算会。打开编辑器自己敲一遍跑一遍改一遍比看十篇文章都管用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑