ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

反转链表算法详解:双指针迭代与递归实现,含边界处理

反转链表算法详解:双指针迭代与递归实现,含边界处理 我第一次认真刷算法题时印象最深的就是206反转链表。代码量不大却把链表最核心的指针操作和边界意识全考了一遍所以它常年挂在算法题库前几页的必刷位置。如果你正在准备技术面试或者说想从数组思维切换到链表思维这道题是绕不开的入口。它要解决的事情一句话就能讲清给定一个单链表的头节点head把它原地反转返回新链表的头节点。看上去简单真正上手写很多人才发现自己对next指针的理解是模糊的。1. 206反转链表的核心考点你真的看懂题目了吗1.1 看似简单却考了三层能力先说题目本身单链表只能从head出发沿着next指针一个个往后走天然没有“前驱指针”。要反转链表本质是把所有next方向调头同时让原来的末尾节点变成新head。这个动作一点都不复杂但面试官真正想要观察的是你对指针操作的掌控力。三层能力很重要。第一层是理解“链表节点”和“数组下标”完全不同数组翻转可以借助下标直接交换链表不行第二层是能够设计循环不变量在整个遍历过程中让每个节点都被处理一次并且不丢失后续节点第三层是能口头讲清楚复杂度为什么迭代法是O(N)时间、O(1)空间为什么递归法代码短却要O(N)空间。很多人只背代码到了“讲思路”环节就卡壳这就是没有把这题吃透。1.2 边界条件才是放大镜面试题里越简单的题目越喜欢靠边界条件挖坑。206反转链表常见的边界有四种空链表、只有一个节点、只有两个节点、长到上千上万个节点。空链表返回空单节点返回它本身这都好办两个节点考验的是你有没有把原链表“剪断”后成功连回来长链表考验的是循环退出条件到底写没写对。我之前面试别人时经常看到候选人写完代码后被一句“如果head是null呢”问住然后慌忙在函数开头加一个if判断。这说明他对算法流程没有形成天然反射。真正熟练的人会在设计循环条件时就把空链表和单节点纳入考虑而不是事后补丁。2. 迭代法为什么先写最顺手的方案2.1 双指针翻转的核心原理迭代法也叫双指针法用两个指针prev和curr分别表示“已反转部分的前一个节点”和“当前正在处理的节点”。整个过程中有几个临时变量不是关键关键的是对当前节点的处理先把当前节点的next存下来再把当前节点的next指向前一个节点然后整体往后移动。为什么必须先存next因为curr.next一旦被改写成prev原来指向的那个“后续节点”就暂时丢了。如果没提前用temp变量记住它循环就无法继续迭代下去。生活里有个类比你要在一排人中间调整队伍方向必须先记住后面那个人的位置才能松开当前这个人的手否则整条队伍就断了。2.2 手把手推演1到2到3到4到空我拿一个具体例子来说明。初始链表是1-2-3-4-NULLprev指向NULLcurr指向1。第一步temp记住curr.next也就是2然后把curr.next指向prev于是1-NULL接着prev移动到1curr移动到2。这时链表从视觉上分成了两段1-NULL以及2-3-4-NULL但curr保证我们还能继续处理2。第二步temp记住3把2.next指向1得到2-1-NULLprev移到2curr移到3。第三步temp记住4把3.next指向2得到3-2-1-NULLprev移到3curr移到4。第四步temp记住NULL把4.next指向3得到4-3-2-1-NULLprev移到4curr变成NULL。循环结束返回prev也就是新链表的头。每走一步都有一个“已反转段”和一个“未处理段”两段之间没有交叉也没有丢节点。这就是链表题里常说的循环不变量。2.3 代码落地与易错点用Python写迭代法标准答案几乎长这样class ListNode: def __init__(self, val0, nextNone): self.val val self.next next class Solution: def reverseList(self, head: ListNode) - ListNode: prev None curr head while curr: temp curr.next curr.next prev prev curr curr temp return prevC版本同理class Solution { public: ListNode* reverseList(ListNode* head) { ListNode* prev nullptr; ListNode* curr head; while (curr) { ListNode* nextTemp curr-next; curr-next prev; prev curr; curr nextTemp; } return prev; } };易错点有三个一是忘记temp导致断链二是循环条件写成while (curr.next)这样最后一个节点没有被处理返回结果会少一截三是最后返回curr而不是prev。我见过有人把prev和curr写反返回了NULL整道题直接零分。提示如果面试官要求“不能新增节点”迭代法就是标准答案因为只申请了有限几个指针变量。它既不new ListNode也不借助外部容器全程原地改动next方向。2.4 为什么空间复杂度是O(1)有些候选人会疑惑不是用了prev、curr、temp三个变量吗为什么还能叫O(1)空间。这里需要区分清楚空间复杂度看的是“随输入规模增长而增长”的部分。链表有N个节点但我们的额外变量永远是3个不会因为N变大就变多。所以额外空间固定记作O(1)也有面试官叫“原地算法”。时间上每个节点只被访问一次每次操作是常数时间所以总时间是O(N)。代码虽然循环了N次但没有任何两层循环也不会回溯复杂度非常干净。3. 递归法理解head.next.next才是关键3.1 递归的思维前提相信函数已经搞定后面迭代法掌握了以后面试官常常会追问一句“还有没有其他解法”。这时候递归法可以作为一个加分项出现。递归版的代码极其精简甚至有点炫技class Solution: def reverseList(self, head: ListNode) - ListNode: if not head or not head.next: return head newHead self.reverseList(head.next) head.next.next head head.next None return newHead理解这段代码最忌讳的是一个节点一个节点去“跟栈”而是要有一种“函数契约”思维调用reverseList(head.next)我不用关心它内部是怎么循环怎么跳的我只需要知道它把从head.next开始的整条后半段链表反转好了并且返回了新的头节点。这句话听着抽象却是所有递归题的通解思路。我打个比方你想拧一整串螺丝先让后面的同事把后半截都拧好你只需要处理最初那一颗螺丝和下一颗螺丝之间的连接关系。递归法就是把“后半截”交给同一个函数自己处理。3.2 执行轨迹head.next.next head 究竟做了什么假设原链表是1-2-3-4-NULL。调用reverseList(1)后它会去调用reverseList(2)一直递归到节点4。节点4的next是NULL满足基准条件直接返回4。回到节点3这一层head是3head.next是4此时执行head.next.next head也就是把4.next指向3再执行head.next None也就是把3.next断开。返回的newHead是4。这一层结束时局部状态是4-3-NULL。回到节点2这一层head是2head.next是3。这里要注意3.next已经在上一层被置空了所以head.next.next也就是3.next目前是NULL。执行head.next.next head把3.next指向2再执行head.next None把2.next断开。返回的newHead还是4。这一层结束时局部状态是4-3-2-NULL。回到节点1这一层同样操作最终得到4-3-2-1-NULL返回newHead节点4。仔细看真正把方向调转的是那一行head.next.next head它让“下一个节点的下一个指针”回指到当前节点。而head.next None是为了避免两个节点互相指形成环。如果没有这一步1和2会互相指成环遍历时会死循环。3.3 递归版本的适用边界与面试亮点递归法的确代码短但它有两个不是缺点的缺点。第一空间复杂度不再是O(1)因为每层递归都会产生新的调用栈帧N个节点就要压N层栈第二在链表特别长时递归深度等于链表长度在Python或JS这类语言里容易触发最大递归深度错误。所以刷题时可以拿它练思路但线上工程里处理长链表我更建议大家用迭代法。面试时主动说出“递归虽然简洁但空间复杂度会到O(N)如果链表很长可能栈溢出”这句话会显得你理解得比背答案的人深。面试官很可能顺着问“那能不能再优化一下”你自然就能切回迭代法。4. 备选方案与面试节奏控制4.1 栈辅助法能说但别作为最终答案除了迭代和递归很多新人第一反应是“用栈”。思路非常直观链表不是只能从前往后走吗我先遍历一遍把所有节点依次压入栈然后逐个弹出重新连接next指针最终弹出顺序正好是反转后的顺序。这个方案能跑通但有两个明显问题。一是额外用了一个栈空间复杂度变成O(N)二是弹出重建链表时需要重新构建节点之间的next关系代码反而比迭代法更容易出错。面试时可以提一句“如果不限制空间可以借助栈来做”然后立刻补一句“但最优解应该用双指针原地反转”。这样展示出你具备多种思路同时知道如何取舍。4.2 头插法思路虽然绕对付链表题很通用头插法也是一个常见备选维护一个dummyNode作为哨兵节点然后不断从原链表头部摘除节点再插入到dummyNode之后。整个过程其实也在原地反转只是插的位置变成了“哨兵之后”。头插法代码写起来稍绕但它对很多链表排序题、区间反转题很通用。比如后面遇到“反转链表的一部分”或“K个一组反转”核心逻辑往往就是头插。如果你只准备206这一题迭代双指针已经enough但如果你想为后续题做铺垫可以顺手把头插法也练熟。4.3 面试沟通顺序为什么先讲迭代而不是先讲递归我自己的面试习惯是先大大方方说“这道题最直接的想法是双指针原地反转”然后画一个简单示意图不要一上来就写递归。为什么迭代法信息量少面试官能轻松跟上你的节奏而递归法代码短但概念跨度大万一你语言描述不到位反而显得你没讲清楚。流程可以这样走先抛迭代思路用一两句话说清楚prev和curr怎么移动写完代码以后主动分析时间和空间复杂度如果面试官追问再补充递归法并点出空间复杂度的区别。这套顺序既安全又显深度。5. 最容易翻车的几个瞬间实测错误清单5.1 断链的悲剧没有暂存next这是新手翻车率最高的问题。有人会写成while curr: curr.next prev prev curr curr curr.next # 这里已经拿不到下一个节点了问题在于curr.next在上一行已经被改成了prev再取curr.next取到的是前一个节点而不是原链表的下一个节点。轻则死循环重则逻辑错乱。正确做法一定是先把原next保存到一个临时变量里。5.2 成环的误区末尾没有置空递归版里head.next None很容易被忽略。如果没有这一步当递归回到最外层时原来的第一个节点的next仍然指向第二个节点而第二个节点的next又被改回了第一个节点于是形成一个环。链表题最怕环一旦成环遍历终止条件永远无法满足。迭代版倒是天然避免了这个问题curr走到NULL就停止反转后的末尾节点是在第一步被处理的它的next被设置成prev即NULL所以迭代版不容易成环。这也是我偏爱迭代法的原因之一。5.3 循环条件、返回节点傻傻分不清第二个高频错误是把循环条件写成while curr.next然后返回curr。表面看也输出了一段反转链表实际最后一个节点没有被处理返回的节点也会因缺少尾部连接而残缺。正确条件是while curr返回prev。记住一个口诀处理完当前节点再移动循环结束时prev指向最后一个被处理的节点它就是新头。5.4 边界测试用例清单在面试或自己练习时建议至少跑这几个用例跑完基本不会出大问题用例预期输出重点观察NULLNULL函数是否直接返回1-NULL1-NULL单节点不被破坏1-2-NULL2-1-NULL两个节点的next方向是否正确1-2-3-4-NULL4-3-2-1-NULL是否漏节点或成环不要觉得这些用例太简单很多实际线上bug就是忽略空链表导致的。刷题时把边界检查练成习惯面试时才不会心虚。6. 把206当杠杆后续链表题的通用套路6.1 从反转链表衍生出的高频题206反转链表几乎是链表类题目的“前置技能”。掌握之后再刷25. K个一组翻转链表、92. 反转链表II、234. 回文链表会发现核心都是“局部反转”加“区间连接”。比如反转链表II让你只反转从left到right这段你只要先定位到区间前一个节点然后用双指针法把区间内节点反转最后把边界接好即可。如果连最基本的206都没吃透那些题会显得异常混乱。回文链表题也会用到反转先通过快慢指针找到中点再反转后半段然后一个从头走、一个从中点走逐个比较。这个解法里最常见的函数其实就是“反转链表”。所以刷206不是只为了这一题而是给后续一堆题打地基。6.2 我的刷题经验一道题怎么写进简历、聊进面试很多同学刷题只是为了过笔试其实面试问答环节更考验你是否真正理解。我建议拿到206这类经典题不仅要在编辑器里通过还要能脱离代码说清楚prev和curr各自代表什么循环结束后为什么prev是头递归的空间复杂度为什么是O(N)。如果你能做到不看代码口述完整流程这道题才算真正背进了脑子里。另外简历里如果写了“熟悉常见链表操作”面试官很大概率会现场让你写反转链表或合并有序链表。这种题本身不难可一旦写崩会直接质疑你的基础功底。类似206这种题宁可多花时间彻底弄懂也不要靠浅尝辄止的记忆去碰运气。6.3 最后分享一个练习技巧我自己刷这道题时用的方法是三遍法。第一遍直接看题尝试写迭代法不参考任何答案第二遍合上答案但允许看我自己之前写过的代码在纸上画出链表每一步的指向变化第三遍对着镜子或者朋友把迭代和递归各讲一遍讲到接不上话为止。这个训练花不了多少时间但能把“看着会写”变成“闭着眼也能聊”。如果你刚开始刷链表题别急着追求刷题数量。先把206反转链表做到滚瓜烂熟你会发现后面很多题目里的指针操作都似曾相识。它就像链表世界里的“直拳”动作简单但练得越扎实遇到复杂套路时越不容易慌。
返回列表