首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
快慢指针完全解读:链表中间节点与倒数第K个节点
📅 2026/10/22 5:46:58
✍️ 爱科研究院
👁 阅读 3,247
链表题在面试里的地位有点像数学里的“数列题”看着不难但考的是手稳不稳、边界意识强不强还有一个很重要的点——你到底理解不了解指针的移动节奏。很多人平时刷题能AC一到白板写链表就从“双指针常规操作”开始翻车十有八九都是对快慢指针只学了模板没吃透原理。这篇就借两道高频题——找中间节点和倒数第k个节点——把快慢指针彻底讲明白从为什么有效、怎么写不会错到面试官延伸追问怎么接招一次说透。这两道题适合所有准备算法面试的人不管你是刚学链表还是刷了三百题想补盲区都可以把这篇当跳板它不解决“会不会写”而是解决“为什么这么写”。毕竟面试官真正想看的不是你把LeetCode某道题背下来而是你能不能在三分钟里解释清楚双指针为什么是最优解以及为什么你的代码在边界条件下不会崩。1. 快慢指针思路拆解为什么“一次遍历”能成为题眼1.1 核心思想两步走与一步走的节奏差快慢指针本质上就是让两个指针以不同速度在链表上移动。最简单的形式就是慢指针每次走一步快指针每次走两步。因为速度差是1所以当快指针到达终点时慢指针刚好走了一半。这个“1:2”的节奏差是解题的命门。你可能会问为什么不先遍历一次数出链表长度n再遍历一次走到n/2的位置功能上没问题复杂度同样是O(n)但多了一次完整遍历。链表题在面试里特别讲究“一次遍历”因为有的链表场景根本没给你第二次机会比如一个数据流只给你从头到尾的访问权限更常见的原因是你要是能用一个指针既探到底又精确停在中间这种“资源复用”的思路会让面试官觉得你有空间和时间上的综合直觉而不是只知道套API。做个生活化类比两个人跑步你跑得快的那个先到达终点另一个人此时在跑道中点。快慢指针就是这个逻辑的代码化。这里有个很关键的点快指针一次走两步慢指针一次走一步这个“一步”和“两步”的语义在实现时要格外小心。很多人写while循环条件时写成fast.next.next ! null结果慢指针多走了一步也有人写成fast ! null fast.next ! null又在奇数长度链表上得到不同的中点。这不只是细节问题它直接决定你返回的是“偏左中点”还是“偏右中点”必须想清楚。1.2 为什么是“倒数第k个”而不是“第k个”找“正数第k个节点”很简单从head往后走k-1步就到了。但“倒数第k个”麻烦在未知总长度你没法直接算偏移量。常规思路是先遍历一遍拿长度n再从头走n-k步这样要两次遍历。快慢指针的解法是让一个指针先走k步然后两个指针同步走先走的那个到达尾部时后走的正好停在倒数第k个节点上。这个思路的精髓在于“预先拉开距离”而不是“让谁追谁”。先走k步相当于把“倒数”转换成“顺数”——你不需要知道链表长度只需要让两个指针之间始终保持着一段长度恰好为k的窗口。面试时如果你能把这句话说出来“我制造一个长度为k的滑动窗口当窗口右端到达链表尾部时左端就指向倒数第k个节点。”面试官通常会很满意因为你不是背模板而是理解了机制。为什么快慢指针在“倒数第k个节点”里的“快指针”其实不快它只是先出发的那个指针。严格来说这里不是速度差异而是起始位置差异。这也是很多初学者最困惑的地方明明两个指针速度相同为什么叫快慢指针因为这类题的统称就叫快慢指针但具体的“快”可以理解为快指针提前消耗掉了k步的领先优势。1.3 快慢指针与空间和时间复杂度的关系两道题的时间复杂度都是O(n)空间复杂度都是O(1)。链表这种结构没法索引注定只能靠指针在节点之间移动。能在一个循环里完成探测和目标定位的快慢指针已经是极致。这也是为什么面试题会反复考——它检验的不只是链表操作基本功还有对复杂度本质的理解。更进阶一点你可以在面试时主动补充如果链表存储在外存/磁盘上一次遍历的意义不仅是时间常数还可能减少寻址次数这时快慢指针的工程意义就更实际。提示面试手写时先把复杂度分析写在代码后面。很多候选人代码跑通但忘了说复杂度面试官会默认你没想清楚。2. 中间节点写法、边界与正确性验证2.1 标准代码框架先给一个最常用的版本语言用C风格写大家都能看懂ListNode* findMiddle(ListNode* head) { if (head nullptr) return nullptr; ListNode* slow head; ListNode* fast head; while (fast ! nullptr fast-next ! nullptr) { slow slow-next; fast fast-next-next; } return slow; }循环停止时slow指向的位置取决于链表长度。奇数长度比如5个节点slow最终指向第3个节点这正是正中间。偶数长度比如6个节点slow会指向第4个节点也就是偏右的那个中点。你需要问我一个问题凭什么循环条件是fast ! nullptr fast-next ! nullptr因为fast一次走两步为了防止下一步访问空指针必须确保fast本身不是空、fast-next也不是空。一旦fast-next为空说明fast已经站在最后一个节点上再走一步就越界了若fast为空说明链表已经走完。这个条件的写法直接决定了能不能Cover偶数长度的边界。我见过有人写成while (fast-next ! nullptr fast-next-next ! nullptr)也能跑但慢指针会停在偏左中点这在有些题比如回文链表里会导致没法正确分成两半。所以不是随便写都能过要根据题目语义选择中点的偏向。2.2 奇数/偶数长度的边界分析用长度为5和6的链表分别模拟一下链表 1-2-3-4-5fast和slow都从1出发。第1轮slow到2fast到3第2轮slow到3fast到5fast-next为空循环停止返回3正确。链表 1-2-3-4-5-6第1轮slow到2fast到3第2轮slow到3fast到5第3轮slow到4fast到nullfast为空循环停止返回4这是偶数长度下的右中点。在很多场景下右中点是你要的答案。比如你需要在链表中点切分右中点作为后半段的起点比较自然。但如果你在做“寻找中间节点”这道题本身LeetCode标准答案给的是偶数取右中点。所以这个版本是安全的。如果你需要左中点也就是偶数长度时返回第3个节点怎么改把循环条件换成fast-next ! nullptr fast-next-next ! nullptr或者让slow起始往后退一步。这个变体在回文链表拆分时需要用到稍后会讲。2.3 常见变体偏左中间节点的处理实际面试中面试官可能会改口“如果偶数长度我取偏左的那个你怎么处理”最快的方式不是改循环条件而是在循环结束后判断如果fast为空且slow存在说明是偶数长度此时slow偏右若要偏左则返回slow的前一个节点。但链表是单向的找前一个节点得从头遍历这就麻烦了。所以正确做法是直接改循环条件用while (fast-next ! nullptr fast-next-next ! nullptr)模拟一下偶数6初始 slow1, fast1第1轮slow到2fast到3第2轮slow到3fast到5第3轮fast-next-next不为空fast在55-next-next7-next为空不成立停止slow停在3正好是左中点。这个变体价值在于它潜移默化告诉你快慢指针的边界条件不是死记的而是根据题目需求推出来的。面试时你如果能主动说明“我为了取左中点所以改了循环条件”这是加分的。注意不要在原题没要求的情况下擅自改成偏左中点除非你明确和面试官确认了语义。算法的正确性定义不清是手写代码的大忌。3. 倒数第k个节点双指针保持距离的两种姿势3.1 写法一先走k步的经典解法这是最直观的版本先用一个指针走k步然后两个指针一起动ListNode* findKthFromEnd(ListNode* head, int k) { ListNode* fast head; ListNode* slow head; // 快指针先走 k 步 for (int i 0; i k; i) { if (fast nullptr) return nullptr; // k 大于链表长度 fast fast-next; } // 同步前进 while (fast ! nullptr) { slow slow-next; fast fast-next; } return slow; }逻辑很清晰fast先走了k步于是fast和slow之间的距离是k。当fast到达尾部null时slow恰好指向倒数第k个节点。这里有个细节先走k步是指从head开始移动k次而不是k-1次。很多人在这个上面翻车。例如k1倒数第1个节点是最后一个节点fast走1步后正好指向第二个节点然后循环里slow同步走最终fast从第二个走到nullslow从第一个走到最后一个正确。如果写成k-1k1时fast一步不走距离没有拉开结果就不对。3.2 写法二固定间距的哨兵解法另一种等价写法是让两个指针的间距从一开始就固定。你可以给链表加一个哑节点dummy node作为起点fast和slow都从dummy出发先让fast向前移动k步然后两个指针再同步走。当fast走到真正的null时slow正好指向倒数第k个节点。dummy node的引入有一个好处如果题目要求删除倒数第k个节点你需要找到它的前驱节点dummy就直接解决了头节点可能被删除的边界问题。这在链表题里非常常见属于“以空间换代码简洁度”的典型。ListNode* removeKthFromEnd(ListNode* head, int k) { ListNode dummy(0); dummy.next head; ListNode* fast dummy; ListNode* slow dummy; for (int i 0; i k; i) { fast fast-next; } while (fast-next ! nullptr) { slow slow-next; fast fast-next; } // slow 是倒数第 k1 个节点删除 slow-next ListNode* toDelete slow-next; slow-next slow-next-next; delete toDelete; return dummy.next; }注意这里循环条件变成了fast-next ! nullptr而不是fast ! nullptr因为我们需要slow停在倒数第k1个即待删节点的前一个而不是停在待删节点上。3.3 k等于链表长度时的边界行为假设链表长度正好是k。例如5个节点k5倒数第5个节点就是头节点。先走k步的写法fast从head开始走5步最终变成null。然后while循环条件fast ! nullptr不成立直接返回slow此时slow还在head正确。删除版本fast从dummy走5步走到null的前一个节点。循环条件fast-next ! nullptr此时fast-next正好是null的next也就是空循环不执行。slow停在dummy位置slow-next就是真正的头节点删除头节点并返回dummy.next。逻辑依然正确。那k大于链表长度怎么办经典写法里有一个if (fast nullptr) return nullptr的保护但如果你忘了这个判断fast在for循环里会变成访问空指针的next程序直接崩溃。这个case面试官几乎必考你必须在代码里显式处理。3.4 倒数第k个和中间节点的统一视角如果你仔细想中间节点其实就是倒数第n/2个节点。快慢指针在“倒数第k个”里先走k步在“中间节点”里先走0步然后两个指针速度不同本质上都是通过“探测链表尾部位置”来反推目标节点。统一视角会让你在面试时迁移更快——考完倒数第k个再顺带说一句“其实中间节点只是klength/2的特例”面试观感会非常好。4. 实战中的坑与高频追问4.1 空链表、单节点、双节点的边界清单链表的坑多半集中在几个极端输入上。我列一张表你可以按这个顺序自查输入场景中间节点期望结果倒数第k个注意点空链表返回null直接返回null或视题目要求报错只有1个节点返回该节点k1时返回自身k1时无解只有2个节点偶数长度返回第2个右中点k2时返回头节点k恰好等于链表长度——倒数第k个就是头节点k1——返回最后一个节点k大于链表长度——需要提前返回null还有一个小坑当链表很长时如果你用递归做会爆栈。所以面试时优先使用迭代双指针方案稳定性更高。4.2 面试官最爱追问的三个问题问题一为什么快指针要走两步不能走三步走三步不是不行但慢指针的位移关系会变成1:3找中间点时你需要知道快指针到达终点的“位置语义”而链表的长度可能是奇数或偶数三步的速度无法保证慢指针刚好停在正中间非偏位。两步是最小的偶数速度能保证在快指针探测到边界时慢指针的步数恰好等于总步数一半。更快不是更优而是更难分析。我见过有人走三步还配了计数器取整代码复杂度直线上升没有必要。面试时你可以直接回答“两步是方便与慢指针形成1:2的位移关系且步数最小、最易判断边界。”问题二这个算法能处理单向链表吗能但快慢指针之所以可用前提是链表不带头节点环路。如果有环快指针可能永远走不到null两者会陷入死循环。遇到“判断链表是否有环”这类题时你不能把快慢指针当成找边界工具而要把它当成“追击问题”来处理——这是另一个变体后面扩展部分会提。问题三倒数第k个解法里为什么要先走k步而不是k-1步因为你的快指针要把“窗口长度”定义为k而两个实体节点之间的距离是k个指针移动步数。从head走到第k个节点需要k-1步但从head走k步可以走到null这样后续循环条件最干净。如果先走k-1步当k1时窗口长度为0边界处理反而别扭。4.3 现场手写时怎么组织代码面试手写书写顺序和思考顺序应该是反的。先把边界条件写好再写主体循环最后补复杂度。很多人上来就写while写到一半发现忘记判空又把代码涂改得乱七八糟观感很差。我建议的书写顺序是先写函数签名明确返回类型是ListNode*。立刻处理空链表if (head nullptr) return nullptr;然后定义slow和fast并把循环条件写在while后面用注释标出循环不变式// fast is k steps ahead of slow。写完循环体再回头看for循环里是否需要对k非法值做保护。最后留一行注释写复杂度。这样做的好处是即使代码最后没写完面试官也能看到你的思路是完整的。手写代码本来就是交流过程不是ACM提交现场。小技巧写倒数第k个时可以先写“fast先走k步”的for循环然后在for循环里顺手判空。这样就算后面while写错了面试官也会因为你在关键位置做了防御而给高分。5. 扩展变体回文链表、环形链表、删除中间节点5.1 回文链表判断这类题在面试中的出现率极高。思路是先用快慢指针找到链表的中点然后把后半段链表反转再和前半段逐一比较。关键点在于找到中点后需要从slow位置开始反转后半段。如果中点取的是右中点偶数长度从slow开始反转反转后链表段的起点就是前半段的下一个节点比较逻辑很自然。如果中点取左中点你得让slow停在中间偏左然后从slow-next开始反转比较时注意尾部对齐。我之前提过偏左中点的获取方式是修改循环条件。在回文链表里更常见的做法是快慢指针同时走找中点。同时把前半段原地反转。慢指针继续走后半段一边走一边和前半段的反转结果比较。这种方式能省掉一次反转后半段的遍历属于优化版本。但面试时如果你没把握先用“找中点反转后半段”写清楚再提优化方案比一上来就写复杂版更稳妥。5.2 环入口检测这道题出道即巅峰给定一个可能带环的链表要求判断是否有环并找到环入口。快慢指针在这里的作用完全变了个身份——从“边界探测”变成“追击”。快指针每次走两步慢指针走一步。如果链表有环两者一定会在环内相遇因为快指针终会“追上”慢指针。但“追上”这个说法并不准确因为环内路程有限快慢指针的移动是循环的所以相遇是必然的。相遇后怎么找环入口经典结论是让一个指针从头开始走另一个从相遇点继续走两者每次都走一步再相遇的位置就是环入口。这个结论可以通过数学推导出来面试时能把这个推理讲清楚的候选人极少属于加分项。具体的推导网上很多这里不展开但你在准备时需要能够从头推导一遍因为面试官很可能会让你现场证明。5.3 删除中间节点题目描述可能是“给你一个单链表删除它的中间节点。如果中间节点有两个删除第二个。”这题其实就是“找到中点”加上“删除节点”的复合题。核心思路和删除倒数第k个一样找到目标节点的前驱。取左中点时前驱就是中点前一个取右中点时你需要在循环里记录prev或者在找到中点后从头遍历到其前驱。用dummy node会简单很多因为你不用单独处理“删除头节点”这种特殊情况。这里提醒一个误区很多人会把“删除中间节点”和“删除当前节点”搞混。后者是google面试的经典题传给你一个非空节点让你O(1)时间内删除它。解法是把下一个节点的值拷贝到当前节点然后删除下一个节点而不是真正删这个节点。这已经超出本文范畴但它和快慢指针没有任何关系别在面试时混为一谈。5.4 快慢指针的工程直觉从纯面试角度看快慢指针覆盖的题型包括中间节点、倒数第k个、回文链表、环形链表、交点检测。但工程里你什么时候真的需要这种技巧举个实际的场景某个高并发服务需要对内存链表进行热点统计你不想遍历两次链表再去计算阈值一次遍历拿到中间位置的节点做采样快慢指针就有用武之地了。虽然不是天天遇到但能理解这种“一次遍历内完成探测和目标定位”的思维模式对你的工程直觉是有正向帮助的。我在带新人做代码评审时经常发现知道这个概念的人不少能在运行时边界条件下不炸的人不多。链表题尤其能暴露一个人的“空指针焦虑”——有经验的人会不自觉地在每个next访问前判断一下这不是胆小而是对运行环境的敬畏。6. 一套模板覆盖N道题的总结思路我用下面这套“快慢指针五步法”来组织所有链表题的思路你可以试试看能不能套用到别的题目上明确两个指针的起始位置都从head开始还是从dummy开始明确两个指针的速度差1:1还是1:2谁先走明确循环的终止条件fast走到null还是fast-next走到null明确返回指针的位置停在目标节点还是停在目标节点的前驱明确边界处理空链表、单节点、k非法、k等于长度。你去看LeetCode上所有快慢指针题几乎都逃不过这五个问题。所谓“秒杀解法”不是背一个模板走天下而是把五个变量按题意配置好代码自然就出来了。这篇文章写到这里我想分享一个实际经验很多人在面试前疯狂刷题追求量但拿到快慢指针题时依然会犯同一个错误——上来就写代码而不是先把上述五个问题想清楚。我自己面试别人时最欣赏的不是秒写正确代码的候选人而是写之前花三十秒自言自语“这里取右中点所以循环条件应该是这样k非法的情况用这个分支处理”的人。这三十秒的思考比三十分钟的刷题更能证明一个人的算法素养。链表题最迷人的地方就是它把“简单”藏在“边界”后面。看懂节点、next、null只是开始真正理解指针如何在链路上以不同速度移动才是在面试里干掉大部分竞争者的关键。把它练熟你会发现不仅这两道题后续的回文、环检测、链表排序都会轻松很多。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/22 5:46:58
链表面试题拆解:LeetCode 203删除与206反转的4种核心解法
2026/10/22 5:46:57
Windows线程局部存储TLS深度解析:静态与动态TLS原理、实战与避坑指南
2026/10/22 5:46:57
Windows多线程TLS实战:四种实现方式与踩坑指南
2026/10/22 6:47:01
PJ85718DM+RA6M4温控方案:高精度模拟前端与工业MCU协同设计
2026/10/22 6:47:01
基于PIC32和PJ85718DM的本地远程温度监测系统设计与实现
2026/10/22 6:47:01
PJ85718DM+PIC18F96J65工业温控设计:高精度热电偶采集与抗扰以太网实现
2026/10/22 6:47:01
STM32F303ZE搭配PJ85718DM:双通道本地与远程温度监测实践
2026/10/22 6:47:01
STM32+模拟温度传感器PJ85718DM实现本地与远程测温上报
2026/10/22 6:42:01
PJ85718DM与PIC32MZ2048EFH144的HVAC双通道温度监测方案解析
2026/10/22 0:01:24
电商智能运营体系从0到1:架构设计、落地步骤与避坑指南
2026/10/22 0:01:24
压缩空气储能与热网热惯性联合优化调度:热电联产系统建模与求解
2026/10/22 0:01:24
若依框架AI代码生成不稳定?用Agent Skill三层结构+五步法搞定
2026/10/21 19:05:16
OpenResearch实战:Claude Code、Codex、OpenCode、Cursor 工作流搭建与故障排查
2026/10/21 14:47:26
知识管理核弹:用Obsidian构建第二大脑的工程化方案(TaoToken 统一 Key 接入版)
2026/10/21 19:05:04
金融AI落地指南:从调研报告到技术选型与工程实践
2026/10/22 5:06:15
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/22 2:07:18
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/21 19:01:08
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)