我第一次认真刷算法题时,印象最深的就是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->NULL,prev指向NULL,curr指向1。
第一步,temp记住curr.next也就是2;然后把curr.next指向prev,于是1->NULL;接着prev移动到1,curr移动到2。这时链表从视觉上分成了两段:1->NULL,以及2->3->4->NULL,但curr保证我们还能继续处理2。
第二步,temp记住3;把2.next指向1,得到2->1->NULL;prev移到2,curr移到3。
第三步,temp记住4;把3.next指向2,得到3->2->1->NULL;prev移到3,curr移到4。
第四步,temp记住NULL;把4.next指向3,得到4->3->2->1->NULL;prev移到4,curr变成NULL。循环结束,返回prev,也就是新链表的头。
每走一步,都有一个“已反转段”和一个“未处理段”,两段之间没有交叉,也没有丢节点。这就是链表题里常说的循环不变量。
2.3 代码落地与易错点
用Python写迭代法,标准答案几乎长这样:
class ListNode: def __init__(self, val=0, next=None): 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是3,head.next是4,此时执行head.next.next = head,也就是把4.next指向3;再执行head.next = None,也就是把3.next断开。返回的newHead是4。这一层结束时,局部状态是4->3->NULL。
回到节点2这一层,head是2,head.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 边界测试用例清单
在面试或自己练习时,建议至少跑这几个用例,跑完基本不会出大问题:
| 用例 | 预期输出 | 重点观察 |
|---|---|---|
| NULL | NULL | 函数是否直接返回 |
| 1->NULL | 1->NULL | 单节点不被破坏 |
| 1->2->NULL | 2->1->NULL | 两个节点的next方向是否正确 |
| 1->2->3->4->NULL | 4->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反转链表做到滚瓜烂熟,你会发现后面很多题目里的指针操作都似曾相识。它就像链表世界里的“直拳”,动作简单,但练得越扎实,遇到复杂套路时越不容易慌。