☰
反转链表详解:迭代与递归两种解法彻底拆解
2026/10/11 2:31:05 网站建设 项目流程

1. 为什么是这道题:题面、考点与前置知识

1.1 题面与核心考点

LeetCode 热题100里面的“反转链表”,很多人第一眼看到觉得简单,不就是把1->2->3->4->5变成5->4->3->2->1嘛。但真正动手写的时候,迭代写不顺畅、递归想不明白的大有人在。面试里这道题出现的频率极高,因为它能同时考察你三件东西:对链表这种数据结构本质的理解、对指针/引用操作的熟练度、以及对递归这种抽象思维方式的掌握。

题面本身不长,给一个单链表的头节点head,返回反转后的链表头节点。要求时间复杂度 O(n),空间复杂度上迭代是 O(1),递归是 O(n)(栈空间)。很多人在意的是“能不能写出来”,但面试官真正在意的是“你能不能讲清楚为什么这么写”。这也是我今天想把迭代和递归两种解法放在一起彻底拆一遍的原因。

1.2 前置知识:链表结构与指针操作

在 Java 里,链表节点通常长这样:

public class ListNode { int val; ListNode next; ListNode() {} ListNode(int val) { this.val = val; } ListNode(int val, ListNode next) { this.val = val; this.next = next; } }

这里要特别强调一件事:Java 里没有 C/C++ 那种“指针”语法,但引用类型本质上就是指针的封装。ListNode next存的不是下一个节点本身,而是下一个节点的地址引用。搞懂这一点,后续所有操作都顺了。

反转链表的核心操作就是“断链”和“接链”。把当前节点的next指向前一个节点,原来的后续节点会暂时丢失,所以必须先用一个变量把后一个节点保存下来。这个“先保存、再改指向”的模式,是整个反转算法的灵魂,也是新手最容易漏掉的地方。

2. 思路拆解与方案选型:迭代和递归到底在解决什么

2.1 迭代思路:三指针翻转的完整推导

迭代解法最核心的三个指针是prev(前驱)、curr(当前节点)、next(后继)。整个过程就像排队转身:队首的人先转过去,然后第二个人转过去,依次传递,最后队尾的人变成新的队首。

具体到每一步,逻辑是:

  1. prev初始化为null,curr初始化为head
  2. 在循环里先保存curr.next到next,防止改完指向后找不到后续节点
  3. 把curr.next指向prev
  4. prev移到curr,curr移到next
  5. 当curr为null时,循环结束,返回prev作为新头

为什么返回prev?因为当curr走到链表末尾的null时,prev恰好停在新链表的头节点上,也就是原链表的最后一个节点。这一步很多人想当然返回curr,结果返回了个空,属于典型踩坑现场。

2.2 递归思路:先想清楚“一个节点要做什么”

递归解法的难点在于很多人一开始就试图“沿着链表走一遍”,然后用循环的思路去套递归,结果越套越乱。正确的打开方式是:不要管整个链表怎么反转,先想清楚“对当前节点head来说,我需要做什么”。

递归的信任链是这样的:假设head.next之后的那段链表已经反转好了,返回的新头节点叫newHead。现在head.next指向的是原链表的下一个节点,而反转之后,这个节点应该指向head,所以执行head.next.next = head。同时,head变成了新链表的尾节点,它的next要置为null,否则会形成环。

边界条件是:head == null || head.next == null,直接返回head。这里尤其要记得判断head == null,很多人只写了head.next == null,传入空链表直接空指针异常。

递归的理解难点在于“信任”:你先相信子递归能搞定后面那一整段,然后专注做好当前节点的两行操作。写多了你会发现,递归其实是“自底向上”的视角,和迭代的“从前往后”正好相反。

2.3 方案对比与选型建议

维度迭代解法递归解法
时间复杂度O(n)O(n)
空间复杂度O(1)O(n)(调用栈)
实现难度低,但指针顺序容易错中,需要理解递归范式
面试推荐度必会,首选必会,作为加分项
适用场景长链表、生产代码链表较短时的优雅写法

我的建议是:面试时先写迭代,因为空间复杂度更好,也不容易爆栈;写完迭代之后再主动说“我还能用递归实现”,并给出递归版本,这会让面试官看到你对两种思维模式的掌握程度。但要注意,如果链表很长(比如上万节点),递归可能导致栈溢出,需要提前说明这一点。

3. 完整实操:Java 实现与逐行拆解

3.1 迭代版 Java 实现与逐行注释

直接上代码,这是我最常用的版本,也是实测下来最不容易写错的顺序:

class Solution { public ListNode reverseList(ListNode head) { ListNode prev = null; ListNode curr = head; while (curr != null) { ListNode next = curr.next; // 先保存后继,避免断链后丢失 curr.next = prev; // 当前节点指向前驱,完成翻转 prev = curr; // 前驱指针后移 curr = next; // 当前指针后移 } return prev; } }

这里每一行都有讲究。ListNode next = curr.next必须放在最前面,而且是每次循环的第一步。如果把curr.next修改了之后才保存next,那么拿到的就是上一轮的prev,整个链表就断了,还会形成环。这个顺序问题是我见过最多的报错原因。

另外注意循环结束后的返回。curr == null说明已经越过了原链表的最后一个节点,此时prev指向的就是反转后的头节点。如果传入的是空链表,循环一次都不执行,直接返回null,这个行为正好符合预期。

3.2 递归版 Java 实现与逐行注释

递归版代码更短,但每一行都需要仔细品:

class Solution { public ListNode reverseList(ListNode head) { // 空链表或只有一个节点时,直接返回 if (head == null || head.next == null) { return head; } // 递归反转后半段,newHead 是反转后链表的头 ListNode newHead = reverseList(head.next); // 让当前节点的下一个节点指向当前节点 head.next.next = head; // 当前节点变成尾节点,next 置空 head.next = null; return newHead; } }

我建议你在调试时把递归调用过程打印出来看。比如链表是1->2->3->4,最深一层处理的是节点 4,它直接返回自己;回到节点 3 时,执行4.next = 3,3.next = null;回到节点 2,执行3.next = 2,2.next = null;回到节点 1,执行2.next = 1,1.next = null。整个链表从尾部开始逐步翻转,最后返回的newHead一直是最初的节点 4。

一个很容易犯的错是忘记head.next = null。如果漏了这行,原头节点(反转后的尾节点)仍然指向原来的第二个节点,最终会导致链表带环,遍历时会死循环。这个问题在 LeetCode 上执行报错时会提示“Cycle detected”,非常典型。

3.3 测试用例与调试技巧

写算法题,很多人过一遍样例就提交,然后被隐藏用例打败。我在本地一般会准备几类固定的测试数据:

  • 空链表:reverseList(null),预期返回null
  • 单节点链表:reverseList(new ListNode(1)),预期返回原节点
  • 两个节点:1->2,预期返回2->1
  • 普通多节点:1->2->3->4->5,预期返回5->4->3->2->1
  • 带重复值的链表:1->1->2->1,用来排除“靠值判断”的错误写法

打印链表的工具方法我建议写一个,方便观察每一步的结果:

public static void printList(ListNode head) { List<Integer> values = new ArrayList<>(); while (head != null) { values.add(head.val); head = head.next; } System.out.println(values); }

用断点调试时,重点观察prev、curr、next三个变量在每次循环后的变化。你会发现它们的移动就像一段“接力”,next先跑,curr紧跟,prev最后。这个节奏一旦熟了,写起来就不会卡壳。

4. 变种与扩展:从反转链表到一类问题的解法

4.1 反转局部区间:从整个链表到一部分链表

掌握了全链表反转后,下一个常见变种是反转left到right区间内的节点。LeetCode 上这道题的编号是 92,我在模拟项目X里也给学员讲过。思路可以复用迭代反转:先找到left的前驱节点,记录为pre;然后从left节点开始,对区间内的节点执行标准的迭代反转;最后把pre.next指向反转后的头节点,把反转区间的尾节点指向right的后续节点。

实现时最需要注意的是边界条件。如果left == 1,就没有前驱节点,这时需要用到虚拟头节点(dummy node)技巧。我们在链表操作里经常创建dummy节点,让dummy.next = head,这样即使操作头节点,也能通过pre统一处理,不需要单独写分支。

4.2 两两交换节点与K个一组反转

两两交换链表中的节点是另一个经典变种。核心逻辑是每两个节点一组进行交换,交换完成后跨越到下一组。这里的关键是搞清楚三个引用之间的关系:prev(组前的节点)、first(组内第一个节点)、second(组内第二个节点)。交换操作是prev.next = second,first.next = second.next,second.next = first,然后prev移到first。

再往上走一层,K个一组反转链表就更有意思了。先数一数剩余节点是否够 K 个,够的话就反转这一段,然后递归处理后面的部分。这个题综合了三个能力:区间反转、链长计数、递归处理。我当时在实际练习时,是先自己实现一个reverseRange(start, end)方法,再在 K 个一组反转里循环调用,代码结构会清晰很多。

4.3 回文链表与环形链表排查

回文链表问题也经常被拿来一起复习。判断一个链表是否为回文,有一个经典做法是:先用快慢指针找到中间节点,反转后半段链表,然后从两头往中间遍历比较值。这里就用到了我们刚学的反转链表能力,相当于一个综合应用题。

环形链表问题是另一个相关考点。用快慢指针判断是否有环,如果有环,快慢指针会在环内相遇。之后再用“从相遇点到环入口的距离等于从头节点到环入口的距离”这个数学性质来找出入环点。我在面试辅导中常建议把这些链表题放到一起复习,因为它们的底层都是引用操作和指针移动,一通百通。

5. 常见问题与排查技巧实录

5.1 高频问题速查表

问题现象可能原因解决方案
返回结果是 null循环结束返回了 curr 而不是 prev注意循环退出时 prev 指向新头
栈溢出(StackOverflowError)递归深度过大,或递归终止条件写错先检查head == null条件;长链表改用迭代
死循环/遍历超时忘记设置head.next = null,链表成环递归版检查尾节点是否置空
空指针异常传入空链表时直接访问head.next终止条件写 `head == null
只反转了部分节点循环内next保存时机不对确保每次改curr.next之前先保存后继
输出结果少了头节点初始prev没有置为nullprev必须从null开始

这张表是我在实际刷题和辅导过程中沉淀下来的,几乎覆盖了新手所有的典型报错。很多人遇到问题就急着看答案,其实更有效的做法是在 IDE 里打上断点,一步步看变量的变化。比如输出少节点的情况,你一调试就能发现prev初始值不对。

5.2 面试官会追问什么

除了让你默写代码,面试官大概率会追加这些问题:

  • “递归的空间复杂度是多少?”——O(n),因为递归栈深度等于链表长度。
  • “如果链表有一万个节点,你会选哪种实现?”——迭代,因为递归会栈溢出。
  • “能否原地反转,不新建节点?”——能,上面的实现都是原地操作。
  • “如果链表有环,反转会怎样?”——会死循环,生产代码里要先判环。

我在真实面试中遇到过一道追问:“递归改成迭代,本质区别是什么?”现在回头看,这个问题的答案其实很通透:迭代是维护一组变量,在循环中逐步改变状态;递归是把问题拆成同构子问题,用调用栈保存中间状态。两者的核心差异不在于“性能”,而在于“思考方式”。

还有一个隐藏考点容易被忽略:输入链表可能带有环。LeetCode 原题的测试数据里没有环,但面试官可能会追问。如果直接对带环链表做反转,遍历永远走不完。所以严谨的回答应该是:先判断是否有环,有环则先处理环或直接拒绝反转。

6. 个人实操心得与经验分享

最后分享几个我自己的体会。

第一个体会是:反转链表这道题,光看代码是学不会的。我见过不少开发者把代码背得滚瓜烂熟,但一换到“反转链表 II”(区间反转)就完全懵了。原因就是他们没有真正理解“三指针接力”的含义,只是在背模板。我的建议是拿一张纸,画一个三节点的链表,用箭头模拟每一步的指针变化。如果能把这个过程画清楚,迭代解法就永远不会忘。

第二个体会是:递归的写法虽然简短,但真正内化需要“信任”这个思维跃迁。很多人在递归时总想跟踪每一层的调用,导致越分析越乱。正确的做法是:只关注当前节点要做的事,剩下的交给递归去完成。这不是玄学,而是函数式思考的基本功,它不仅在链表题里有价值,在树、图、动态规划里同样重要。

第三个体会是关于测试的。我自己早期刷题时喜欢“写完就提交”,结果压测崩溃。后来改成在本地先跑若干组边界数据,再手推每一步的结果,提交一次通过的几率大幅上升。比如反转链表,在本地把null、单节点、双节点、多节点、重复值这些用例都跑一遍,基本可以杜绝低级错误。

第四个补充小技巧:在 IDEA 里调试递归时,可以给reverseList方法加一个参数,记录当前深度,打印缩进后的调试信息。这样你能直观地看到每一层递归的输入和输出,理解起来会顺畅很多。我自己带人的时候经常用这个办法,效果非常明显。

如果后续还想深挖,我建议按这个顺序练习:暴力反转字符串数组(练基础)-> 反转链表(练指针)-> 区间反转(练边界)-> K个一组反转(练综合)-> 回文链表(练组合应用)。把这一条线打通,链表面试题基本就不怵了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询