
反转链表这道题在LeetCode上是第206题在各大公司的算法面试里是当之无愧的“钉子户”。每次到了面试季我整理高频题单它一定排在最前面每次帮人做模拟面试我也几乎必考它。但有意思的是这道题标的是Easy实际通过率却不算高——很多候选人一听是链表题就觉得“太简单了”上手一写却漏洞百出或者干脆卡在“为什么递归能反转链表”上。这篇文章就针对这道必刷题做一次彻底的拆解先聊清楚它为什么值得出现在面试里再把迭代和递归两种解法的每个指针动作掰开揉碎接着延伸到面试官真正想考察的几个变体题最后结合真实面试场景讲一讲那些最容易翻车的细节。无论你是刚开始刷题的候选人还是想系统补链表基本功的工程师这篇文章都适合你。1. 为什么“必刷”的是这道题一道简单题背后的面试逻辑1.1 面试官爱出它的三个理由高频、低门槛、高区分度反转链表能成为算法面试的常青树核心原因有三个。第一个原因是高频。几乎所有主流大厂的高频题单里它都稳居前二十名。原因很简单链表是最基础的数据结构之一而反转链表又是链表题里最经典的代表用它开场既不会让候选人觉得被刁难又能快速摸清对方的代码功底。第二个原因是低门槛。这道题对前置知识的要求极低只需要理解单链表的next指针指向就够了不涉及哈希表、动态规划、二叉树这些进阶概念。所以面试官可以在面试前十分钟临时选它也可以用它作为一道“热身题”之后顺势展开更难的内容。第三个原因才是关键——高区分度。一道看似简单的题能把候选人分成好几个层次。背过答案的人能默写出来但追问一句“为什么最后返回prev而不是curr”就卡壳理解不深的人写递归会漏掉head.next None导致链表成环真正掌握的人不仅能写出两种解法还能把指针移动的每一步讲得清清楚楚。面试官要考察的恰恰就是这背后的“理解深度”。1.2 链表反转和数组反转的本质差异你改的是方向不是数据很多人刚开始刷题时会有一个错觉反转链表和反转数组差不多都是把顺序倒过来嘛。这个理解恰恰是最需要纠正的。数组反转很简单。它有下标可以随机访问所以只需要首尾交换值中间用一个临时变量整个数组就反转完成了。比如[1,2,3,4]变成[4,3,2,1]你操作的是“值”。链表反转则完全不同。单链表的每个节点只知道自己的值和自己下一个节点是谁没有下标不能随机访问。反转一条1-2-3-4的链表最终要得到4-3-2-1但你会发现从头到尾2、3、4这几个数字本身一个都没有动真正变的是每个节点里的next指针方向。原来的1.next指向2反转后变成2.next指向1原来的2.next指向3反转后变成3.next指向2。也就是说链表反转的每一步操作的都是“引用关系”而不是“数据值”。这个差异背后是两种完全不同的遍历模型数组可以按下标跳转链表只能沿着next一个接一个走。而所有链表题的核心能力就是在“指针移动”中安全地管理这些引用关系。反转链表正是培养这种能力最短的路径。这道题吃透了后面做环形链表、合并有序链表、删除倒数第N个节点思路都会顺畅很多。1.3 前端视角的“链表直觉”虚拟DOM和diff算法为什么也跟它有关我接触过不少前端工程师一看到链表题就想跳过觉得“这是后端才需要的东西”。但我得说一句公道话面试里问虚拟DOM和diff算法的时候底层同样大量涉及链式结构的操作。举个典型的例子。React的Fiber架构本身就是用链表组织起来的每个Fiber节点通过child、sibling指针串联成树形链表结构diff算法在对比新旧节点时本质上就是在两条链上做遍历、标记、复用和移动。你会在源码里看到很多维护prev、current、next这类指针的代码这和你写反转链表时维护prev、curr、next_temp的思维方式完全是一个套路。所以现在一些前端团队在面试时也喜欢拿链表题来验证候选人面对链式结构时的直觉。理解了这一点你就明白为什么反转链表被放在“必刷”的位置它不只是一道数据结构题更是一道通用的逻辑思维题。无论你投后端还是前端岗它都有可能在你的面试中出现。2. 迭代反转三个指针如何完成一次漂亮的“掉头”2.1 为什么必须准备三个指针断链瞬间会发生什么先说结论迭代反转的核心就是维护三个指针——prev前一个节点、curr当前节点、next_temp下一个节点。为什么要三个你可以自己试着只用两个指针想一遍。假设只有prev和curr第一步操作是curr.next prev把当前节点的指针掉头。可问题就在于当你执行这一步时curr.next原本指向的那个节点就彻底找不到了。因为链表是单向的每个节点只有一条next边你把这条边改了方向原来的后继就断了后半段链表直接丢干净。所以第三个指针next_temp的作用就是在改变curr.next方向之前先把curr原来的后继节点保存下来。这三者的关系其实就是一句话先保存再翻转后推进。这里有个很贴切的类比。把链表想成一串火车车厢每一节都连着下一节。你要让整列火车掉头不可能把每节车厢搬起来换个位置能做的只是在每一节车厢的连接处换钩子。换钩子的顺序必须是先用新钩子挂住下一节车厢再解开旧钩子。这样无论何时断链其他车厢始终都是有东西牵着的。反转链表里的next_temp就是那个“先挂上去的新钩子”。2.2 一次完整的循环走查1 - 2 - 3 - 4 - NULL先把迭代版的完整代码贴出来这是面试里最稳妥的写法。class ListNode: def __init__(self, val0, nextNone): self.val val self.next next def reverseList(head: ListNode) - ListNode: prev None curr head while curr: # 1. 保存当前节点的下一个节点防止断链后丢失 next_temp curr.next # 2. 把当前节点的指针掉头指向prev curr.next prev # 3. prev和curr整体向后推进 prev curr curr next_temp return prev代码只有几行但每一个字段为什么这样变值得走一遍。以1 - 2 - 3 - 4 - NULL为例四轮循环的完整变化如下表所示。轮次执行前currnext_temp操作后curr.next指向轮次结束prev轮次结束curr初始1--None1第1轮12None12第2轮23123第3轮34234第4轮4None34None第1轮curr指向节点1先把next_temp保存为节点2然后把1.next改为NULL。此时节点1从原来的头变成了新链表的尾。prev推进为节点1curr推进为节点2。第2轮curr指向节点2保存next_temp为节点3把2.next改为节点1。此时节点1和节点2已经完成了掉头。prev推进为节点2curr推进为节点3。第3轮和第4轮依此类推。等curr走到NULL循环结束prev正好停在原链表的最后一个节点4上而4-3-2-1-NULL就是一个完全反转后的链表。2.3 为什么返回prev而不是curr出口设计里的关键细节这是面试官最爱追问的一个细节。循环结束的条件是curr变成None此时prev恰好走到原链表的最后一个节点也就是新链表的头节点。如果这时候你鬼使神差地return curr返回的就是一个空指针整个反转功亏一篑。深一层说这里体现的是“循环不变量”思想。在整个循环过程中不变的是什么是这样一个事实prev始终指向“已经反转好的那部分链表的头节点”而curr始终指向“接下来要处理的那个节点”。每轮循环结束时这个关系都会重新成立。所以当循环无法继续时curr为Noneprev自然就是完整反转链表的头。能把这一层讲清楚面试官会觉得你不只是在套模板而是真的理解了指针的移动规律。2.4 时间复杂度和空间复杂度为什么它能抗住长链表迭代版的效率是相当优秀的。时间上每个节点只被访问一次整体时间复杂度是O(n)其中n是链表长度。空间上额外只用了prev、curr、next_temp三个指针变量不随链表长度增长而增长所以空间复杂度是O(1)。这也是工程代码里默认选择迭代版的原因。真实业务中链表可能很长如果选择递归写法调用栈深度会等于链表长度链表到达一定规模后容易栈溢出。迭代版就没有这个问题稳得很。另外如果你用的是Java写法几乎一一对应。class Solution { public ListNode reverseList(ListNode head) { ListNode prev null; ListNode curr head; while (curr ! null) { ListNode nextTemp curr.next; curr.next prev; prev curr; curr nextTemp; } return prev; } }前后端语言的写法没有本质区别核心都是那三步保存、翻转、推进。3. 递归反转一次“信任”的传播3.1 递归函数的“信任契约”你只需要想清楚当前节点很多人在递归解法上栽跟头不是因为代码看不懂而是因为思路没转过来。他们总想用大脑完整展开整个递归调用栈一层层模拟下去结果很快就晕了。递归的正确打开方式是建立“信任契约”。先给函数定义一个清晰的语义reverseList(head)表示“反转以head为头节点的链表并返回反转后的新头节点”。然后你不需要关心它内部是怎么完成的你只需要相信如果调用reverseList(head.next)它一定能把head后面的整条链表反转好并返回新的头节点。在这个信任基础上你手里的局面就变得非常简单。当后面那段已经被反转好之后head.next这个引用指向的是谁注意原来的head.next指向的是节点2但这会儿以节点2为头的后半段已经被反转了所以节点2变成了后半段的尾节点。也就是说head.next仍然指向节点2但节点2现在处于反转后链表的末尾。你要做的只有两件事第一把head接到这个尾巴的后面第二让head变成新的尾节点。第一件事只需要让head.next.next head第二件事只需要让head.next None。3.2 两句关键代码为什么缺一不可递归版的完整代码同样简短。def reverseList(head: ListNode) - ListNode: if head is None or head.next is None: return head new_head reverseList(head.next) head.next.next head head.next None return new_head先看递归基。head为空链表返回Nonehead是单节点返回head自身这两种情况反转结果都是它自己不需要额外处理。再看两句关键代码。head.next.next head的作用是把当前节点拼接到已经反转好的链表末尾。head.next None的作用是切断当前节点原来向后的引用。这两句缺一不可漏掉第一句整个反转根本不会发生因为每个节点都没有被接到新链表上漏掉第二句链表会形成环——以1-2-3-4为例当节点1执行完head.next.next head后如果不切断1.next最终会有一个循环引用让遍历永远走不完测试时直接超时。面试现场漏掉head.next None是我见过最典型的递归翻车点。3.3 从调用栈视角完整走查四个节点的执行过程虽然我建议你不要在脑子里展开完整调用栈但为了彻底搞懂原理这里还是从栈视角完整走一遍。以1-2-3-4-NULL为例。第一步调用reverseList(1)。因为1.next不是None所以进入递归调用reverseList(2)。 第二步调用reverseList(2)。因为2.next不是None继续调用reverseList(3)。 第三步调用reverseList(3)。因为3.next不是None继续调用reverseList(4)。 第四步调用reverseList(4)。此时4.next是None命中递归基直接把节点4返回。这一步是整个递归的“转折点”。现在开始逐层返回。回到reverseList(3)这一层new_head拿到节点4。执行3.next.next 3也就是4.next 3然后执行3.next None。此时局部链表是4-3返回new_head节点4。回到reverseList(2)这一层new_head仍然是节点4。执行2.next.next 2此时2.next是节点3所以实际上是3.next 2然后2.next None。局部链表变成4-3-2返回节点4。回到reverseList(1)这一层new_head还是节点4。执行1.next.next 1此时1.next是节点2所以实际上是2.next 1然后1.next None。最终链表变成4-3-2-1-NULL返回节点4。可以看到每一层做的事情完全一样只是“当前节点”在不断变化。递归之所以能这么简洁是因为它把“逐节点掉头”这件事交给了调用栈一层层自动完成。3.4 面试官问“迭代和递归你选哪个”标准回答的思路这道题几乎必被追问“两种解法你更喜欢哪一种”别急着二选一面试官想听的是你对两者差异的分析。我建议这样回答先说递归。它的代码最贴合“反转”这个定义的数学语义写起来最简洁思维负担最小。但代价是空间复杂度O(n)因为递归栈深度等于链表长度链表特别长的时候有栈溢出的风险。再说迭代。它在时间上和递归一样是O(n)但空间上只有O(1)不依赖调用栈工程上更稳妥。最后补一句在面试场景下我会把递归思路讲给面试官听然后写迭代版本如果时间允许两种都写展示的是你对两种思维模式的掌握。这样回答既讲清了原理又体现了工程判断力。面试官对你的印象分会比单纯报一个答案高不少。4. 从206延伸出去面试官真正想考的变体题4.1 反转前N个节点学会“保留后继”很多面试官不会只考一道原题他们会把206稍加变形变成“反转链表的前N个节点”。这个变体的关键区别在于你不能再像整条反转那样把尾巴一刀切因为前N个节点反转完后第N个节点必须接回原链表的第N1个节点。解决方法是增加一个成员变量successor专门用来保存原链表第N1个节点。递归到底的时候先把它记下来再逐层往回归。class Solution: def __init__(self): self.successor None def reverseN(self, head: ListNode, n: int) - ListNode: if n 1: # 记录第 n1 个节点这是反转后的尾部要接的地方 self.successor head.next return head new_head self.reverseN(head.next, n - 1) head.next.next head head.next self.successor return new_head注意这里和206的差异。206里每一层的head.next都置为None因为整条链表反转后头部就是新的尾部后面不需要再接任何东西。但在reverseN里head.next必须指向successor而不是None否则前半段反转完就和原链表后半段脱节了。4.2 区间反转LeetCode 92定位起点复用reverseN再进一步面试官可能会让你反转从第left个节点到第right个节点这一区间这就是LeetCode 92题。它的解法层次非常清晰如果left等于1问题就退化成反转前right个节点直接调用reverseN。如果left大于1就递归处理head.next并把left和right同时减1直到left变成1。核心代码如下。def reverseBetween(head: ListNode, left: int, right: int) - ListNode: if left 1: return reverseN(head, right) head.next reverseBetween(head.next, left - 1, right - 1) return head这个解法的巧妙之处在于区间反转最终被拆成了“从头部开始反转前几个节点”的子问题而复用的正是前面那套递归逻辑。如果你能现场写出这段代码面试官对你的评价会明显上台阶。4.3 K个一组翻转LeetCode 25分组反转的工程化思维还有一道更硬核的变体LeetCode 25题K个一组翻转链表。每K个节点一组进行反转最后一组如果不足K个保持不变。这道题的核心思想是把大问题切成一连串等长的子问题每个子问题内部用的还是206那三个指针的循环。具体思路是先数出K个节点找到这一组的边界递归处理后面剩余的链表然后反转当前这一组并把反转后的结果与后续结果接上。需要一个辅助函数reverse(a, b)用来反转[a, b)区间内的节点。代码量比206多不少但如果你已经把206吃透这道题思路上的核心障碍其实并不大。它考察的更多是“把一个复杂任务拆成可复用子任务”的工程化思维这也是为什么面试官喜欢在链表问题上层层递进的原因。4.4 回文链表LeetCode 234快慢指针与半段反转的经典组合最后一个高频变体是LeetCode 234判断一个链表是否为回文链表。主流解法是用快慢指针找到链表的中点把后半段链表反转然后从头部和中点同时往后遍历逐一比较节点值是否相等。这里的后半段反转用的就是206的迭代版。你可以理解为反转链表不再只是一道独立的题而是很多中等题里的一个标准子步骤。这也是它被放在“必刷”位置的最重要原因——它是地基上面可以盖出很多楼。学会206之后我建议按这个顺序继续刷92区间反转、25K个一组翻转、234回文链表、61旋转链表。这几道题刷完链表翻转相关的面试题基本都能覆盖。5. 现场容易翻车的细节NULL处理、断链顺序与测试用例5.1 三个最常见的翻车瞬间我在模拟面试里见过太多人在这道题上翻车翻车点高度集中在三处。第一处迭代版忘了保存next_temp。这个错误太典型了有人一紧张把curr.next prev直接写在保存之前结果curr原来的后继节点彻底丢失。如果面试官让你跑一个长度为3的用例你会发现第二个节点还没有被处理整个链表就断了。这种错误一旦发生往往很难在短时间内发现因为代码在逻辑上看起来是“通顺”的。第二处递归版漏了head.next None。这个问题我在前面提过它会导致链表成环。最可怕的是一开始跑小用例可能没事只有链表长度拉长后才会在遍历时死循环。面试现场一旦测试超时很多人的心态会直接崩掉。第三处没有处理好空链表和单节点。有人一上来就写head.next.next空链表直接崩有人忘记判断单节点情况导致代码在逻辑上多做一轮无意义的操作。这类边界问题的处理方式其实很简单进入递归或循环前统一判断链表是否为空或只有一个节点。这三处翻车点本质上都是对指针行为没有建立清晰的预判。我的建议是写代码之前先在纸上把三个节点画出来走一遍指针的移动路线再动手写。花三十秒画图能省下五分钟的调试时间。5.2 面试时自测的顺序从空链表到长链表面试现场写完代码面试官通常会让你“跑个例子验证一下”。这时候怎么选用例非常能体现工程素养。我建议按这个顺序自测空链表head为None验证代码不会崩。单节点链表验证递归基和循环边界。两个节点的链表这是最简单的真实反转场景。奇数长度链表比如三个节点。偶数长度链表比如四个节点。原本就已经反转好的链表比如5-4-3-2-1验证反转后变成1-2-3-4-5。带重复值的链表防止你把值比较误当成指针比较。每跑一个用例重点检查三件事返回的头节点是否正确、遍历链表时是否会成环、链表尾部的next是否为None。在面试现场主动说出“我先用空链表和单节点验证边界再用四节点验证完整反转”这种条理清晰的自测过程本身就是加分项。5.3 讲思路的正确姿势先画图、再写码、最后自测最后聊一个很多人忽略的软技能在面试里沟通比代码更值钱。面对反转链表这道题面试官想看到的不是你在白板上默写代码而是你能把指针怎么移动这件事讲清楚。我的经验是三步走。第一步先在白板上画三个节点把prev、curr、next_temp三个指针标出来口头讲一遍循环体保存、翻转、推进。第二步确认面试官理解了你的思路再开始写代码。第三步写完之后主动说“我跑一个例子验证一下”然后用长度为3的链表完整走一遍。这一套流程走下来哪怕最后代码里有一两个小瑕疵面试官也会觉得你思路清晰、有工程意识。反过来很多人一上来就闷头写代码写错了才开始解释这时候印象分已经扣掉了。我做算法面试辅导这些年最深的感受是反转链表这道题几乎没有难度难的是你能不能在高压环境下把指针的变化讲清楚、把边界条件想全。如果你能在一分钟内画清三个指针的移动这道题就是送分题。最后再分享一个小技巧准备这道题的时候别只刷206把92和25连着一起刷你会发现后续面试里遇到任何链表翻转的变形题思路都能自然迁移。我每次模拟面试都会先考这道题能把迭代和递归两种版本都写清楚、还能答出“为什么返回prev”的候选人后面的表现基本都不会差。