在算法与数据结构的学习中,链表反转是一个经典且基础的问题。它不仅是面试常客,更是理解指针操作、递归思维的重要切入点。本文将带你深入剖析单向链表反转的两种核心解法——迭代法与递归法,并通过图示和代码详解每一步的逻辑变化,帮助你真正掌握这一基础但关键的算法素养。
📌 问题描述
给定一个单向链表的头节点 head,要求将其反转,并返回新的头节点。
例如:
输入: 1 -> 2 -> 3 -> 4 -> null
输出: 4 -> 3 -> 2 -> 1 -> null
✅ 方法一:迭代法(推荐,空间最优)
🔍 核心思想
使用三个指针:
prev:当前节点的前一个节点(初始为null)curr:当前正在处理的节点(初始为head)next:用于临时保存curr.next,防止断链
在遍历过程中,逐步将每个节点的 next 指针指向前一个节点,从而实现整个链表方向的“倒转”。
💡 指针变化过程详解
我们以 1 -> 2 -> 3 -> null 为例:
| 步骤 | curr | next | curr.next = prev | prev | curr(更新后) |
|---|---|---|---|---|---|
| 初始 | 1 | - | - | null | 1 |
| 1 | 1 | curr.next=2 | 1 → null | 1 | 2 |
| 2 | 2 | curr.next=3 | 2 → 1 | 2 | 3 |
| 3 | 3 | curr.next=null | 3 → 2 | 3 | null |
当 curr === null 时循环结束,此时 prev 指向原链表最后一个节点,也就是新链表的头节点。
✅ 时间 & 空间复杂度
- 时间复杂度:O(n) —— 遍历一次链表
- 空间复杂度:O(1) —— 只用了常量级额外空间
🧩 JavaScript 实现
function reverseList(head) {
let prev = null;
let curr = head;
while (curr) {
const next = curr.next; // 保存下一个节点
curr.next = prev; // 当前节点指向前一个
prev = curr; // prev 向前移动
curr = next; // curr 向后移动
}
return prev; // 最终 prev 是新的头节点
}
✅ 方法二:递归法(理解递归本质)
🔍 核心思想
递归的关键在于相信“子问题已经解决”。
对于链表 1 -> 2 -> 3 -> null,当我们调用 reverseListRecursive(1) 时:
- 先递归地反转
2 -> 3 -> null这一部分; - 假设这部分已经成功反转为
3 -> 2 -> null; - 我们只需要让
2指向1,然后把1的next设为null即可。
🔄 递归三要素
-
终止条件:
if (!head || !head.next) return head;当链表为空或只有一个节点时,无需反转,直接返回。
-
递归调用:
const newHead = reverseListRecursive(head.next);让后面的链表完成反转,并返回新的头节点(即原链表尾部)。
-
回溯处理:
head.next.next = head; // 让下一个节点指向自己 head.next = null; // 断开原链接,防止成环
🎯 关键点解释:head.next.next = head
这行代码是递归反转的核心!
head.next是下一个节点。head.next.next = head相当于让下一个节点的next指向当前节点,形成反向连接。
比如当前是节点 1,2 已经反转好并指向 3,现在我们要让 2 → 1,所以设置 2.next = 1,而 2 = 1.next,因此就是 1.next.next = 1。
最后记得 head.next = null,避免出现环。
✅ 时间 & 空间复杂度
- 时间复杂度:O(n)
- 空间复杂度:O(n) —— 由于递归调用栈的深度为 n
⚠️ 虽然递归写法更简洁优美,但在极端情况下可能引发栈溢出,生产环境建议使用迭代法。
🧩 JavaScript 实现
function reverseListRecursive(head) {
// 递归结束条件:空节点或只有一个节点
if (!head || !head.next) {
return head;
}
// 递归反转后续链表,得到新的头节点
const newHead = reverseListRecursive(head.next);
// 回溯时调整指针:让下一个节点指向当前节点
head.next.next = head;
head.next = null; // 断开原连接
return newHead; // 始终返回最终的头节点(原链表尾部)
}
📊 对比总结
| 特性 | 迭代法 | 递归法 |
|---|---|---|
| 时间复杂度 | O(n) | O(n) |
| 空间复杂度 | O(1) ✅ | O(n) ❌ |
| 是否易理解 | 中等(需理清指针顺序) | 高(需理解递归模型) |
| 是否有栈溢出风险 | 无 | 有(长链表时) |
| 推荐场景 | 生产环境、性能敏感场景 | 学习递归思想、代码简洁需求 |
🧠 算法素养提升建议
-
画图辅助理解
对于指针类问题,动手画出每一步的指针变化,能极大提升理解效率。 -
理解“子问题”思想
递归的本质是分治。学会问自己:“如果剩下的部分已经被解决了,我该怎么合并结果?” -
注意边界情况
如空链表、单节点、两个节点等情况,都要考虑是否兼容。 -
练习变形题
- 反转链表前 k 个节点
- 反转第 m 到第 n 个节点
- K 个一组反转链表(LeetCode 25)
✅ 结语
链表反转看似简单,却是检验编程基本功的一块试金石。无论是迭代中的指针迁移,还是递归中的“信任子问题”,都体现了算法设计的精妙之处。
掌握它,不是为了做题,而是为了培养一种严谨、清晰的程序逻辑思维。
当你能够不靠记忆、而是凭理解写出这两种解法时,你就已经迈出了通往高级算法之路的第一步。
📌 小挑战:你能尝试用 TypeScript 写一个类型安全的链表反转函数吗?欢迎留言讨论!
👨💻 编程路上,每一个基础知识点,都是未来的基石。共勉!