大家好,今天我们来拆解一道链表的经典题目 ------力扣 206. 反转链表,这道题是理解链表指针操作的绝佳入门题,也是面试中高频出现的基础题。本文会从题目本身出发,一步步拆解双指针迭代和递归两种实现方式,帮你彻底搞懂链表反转的底层逻辑。
一、题目描述
题目链接: 206. 反转链表
题意: 反转一个单链表。
示例: 输入: 1->2->3->4->5->NULL输出: 5->4->3->2->1->NULL
链表反转的核心思想非常简单:不需要额外开辟新的链表空间,只需要修改链表节点的next指针指向,就能完成反转。原链表的头节点会变成反转后的尾节点,原链表的尾节点会变成反转后的头节点,整个过程只改变指针方向,不新增或删除节点。
二、核心思路:双指针迭代法(最推荐)
1. 思路解析
双指针迭代法是链表反转的最优解,时间复杂度为(O(n))(遍历一次链表),空间复杂度为(O(1))(仅使用常数级额外空间)。
核心逻辑是用两个指针pre和cur,配合一个临时变量temp完成遍历和反转:
cur:当前正在处理的节点,初始指向原链表的头节点headpre:cur的前一个节点,初始为null(因为反转后,原链表的第一个节点会变成尾节点,尾节点的next为null)temp:临时变量,用于保存cur的下一个节点,防止修改cur.next后链表断裂
反转的核心流程可以拆解为四步循环执行:
- 保存下一个节点 :
temp = cur.next,先把cur的下一个节点存起来,避免后续修改指针时丢失后续节点 - 反转当前节点的指向 :
cur.next = pre,让当前节点指向前一个节点,完成当前节点的反转 - 移动前指针 :
pre = cur,pre前进到当前节点,作为下一轮的 "前一个节点" - 移动当前指针 :
cur = temp,cur前进到之前保存的下一个节点,继续处理后续节点
2. 完整代码实现
javascript
运行
ini
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function(head) {
let pre = null, temp = null, cur = head;
while(cur) {
temp = cur.next; // 步骤1:保存下一个节点
cur.next = pre; // 步骤2:反转当前节点的指向
pre = cur; // 步骤3:pre 前进到当前节点
cur = temp; // 步骤4:cur 前进到下一个节点
}
return pre;
};
3. 流程模拟(以1->2->3->4->5为例)
- 初始状态:
pre = null,cur = 1,temp = null - 第一次循环:
temp = 2→1.next = null→pre = 1→cur = 2 - 第二次循环:
temp = 3→2.next = 1→pre = 2→cur = 3 - 第三次循环:
temp = 4→3.next = 2→pre = 3→cur = 4 - 第四次循环:
temp = 5→4.next = 3→pre = 4→cur = 5 - 第五次循环:
temp = null→5.next = 4→pre = 5→cur = null - 循环结束,
pre指向原链表的尾节点5,也就是反转后的新头节点,直接返回pre即可。
三、进阶实现:递归版(和迭代逻辑完全等价)
很多同学会觉得递归很难,但其实这段递归代码,就是双指针迭代法的 "递归改写版",核心逻辑和迭代版完全一致,只是把while循环换成了递归调用。
1. 思路解析
递归版的核心是尾递归 ,每次递归处理一个节点,把pre和cur作为参数传递,和迭代版的变量更新一一对应:
- 递归终止条件:
if(!head) return pre;,当head为null时,说明已经遍历完链表,此时的pre就是反转后的新头节点,直接返回即可 - 递归逻辑:和迭代版的四步完全一致,先保存下一个节点,再反转当前节点,最后递归处理下一个节点
2. 完整代码实现
javascript
运行
php
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverse = function(pre, head) {
if(!head) return pre; // 递归终止条件
const temp = head.next; // 步骤1:保存下一个节点
head.next = pre; // 步骤2:反转当前节点的指向
pre = head; // 步骤3:pre 前进到当前节点
return reverse(pre, temp); // 步骤4:递归处理下一个节点
}
var reverseList = function(head) {
return reverse(null, head); // 初始调用:pre=null,head=原链表头节点
};
3. 递归流程模拟(以1->2->3->4->5为例)
- 初始调用:
reverseList(head)→reverse(null, 1) - 第一次递归:
temp = 2→1.next = null→pre = 1→ 调用reverse(1, 2) - 第二次递归:
temp = 3→2.next = 1→pre = 2→ 调用reverse(2, 3) - 第三次递归:
temp = 4→3.next = 2→pre = 3→ 调用reverse(3, 4) - 第四次递归:
temp = 5→4.next = 3→pre = 4→ 调用reverse(4, 5) - 第五次递归:
temp = null→5.next = 4→pre = 5→ 调用reverse(5, null) - 终止条件触发:
!head为true,返回pre=5,递归逐层返回,最终得到反转后的链表头节点。
四、两种实现方式对比
表格
| 对比维度 | 双指针迭代法 | 尾递归版 |
|---|---|---|
| 时间复杂度 | (O(n)),每个节点只处理一次 | (O(n)),每个节点只处理一次 |
| 空间复杂度 | (O(1)),仅使用常数级额外空间 | (O(n)),递归栈深度等于链表长度 |
| 优缺点 | 空间效率高,无栈溢出风险,工程上推荐 | 代码更简洁,和迭代逻辑一一对应,适合理解递归思想 |
需要注意的是,JavaScript 大部分环境不支持尾递归优化,当链表很长时,递归版可能会出现栈溢出错误,所以实际工程和面试中,优先推荐双指针迭代法。
五、常见易错点总结
- 忘记保存下一个节点 :如果不提前用
temp保存cur.next,修改cur.next = pre后,就会丢失后续节点,导致链表断裂 - 递归版遗漏终止条件 :如果没有
if(!head) return pre;,当head为null时,执行head.next会抛出Cannot read property 'next' of null错误 - 指针移动顺序错误 :必须先修改
cur.next,再移动pre和cur,顺序颠倒会导致逻辑混乱 - 初始值设置错误 :
pre必须初始为null,否则原链表的第一个节点反转后,next会指向错误的节点
这道题的核心就是理解指针的指向修改,无论是迭代还是递归,本质上都是用同样的逻辑遍历链表、修改指针。把这道题吃透,后续很多链表题(比如链表的区间反转、两两交换节点)都能轻松理解。
如果觉得这篇文章对你有帮助,欢迎点赞收藏,后续会更新更多链表、数组等基础算法题的拆解~