单向链表反转:从迭代到递归的深度解析

157 阅读4分钟

在算法与数据结构的学习中,链表反转是一个经典且基础的问题。它不仅是面试常客,更是理解指针操作、递归思维的重要切入点。本文将带你深入剖析单向链表反转的两种核心解法——迭代法递归法,并通过图示和代码详解每一步的逻辑变化,帮助你真正掌握这一基础但关键的算法素养。


📌 问题描述

给定一个单向链表的头节点 head,要求将其反转,并返回新的头节点。

例如:

输入: 1 -> 2 -> 3 -> 4 -> null
输出: 4 -> 3 -> 2 -> 1 -> null

✅ 方法一:迭代法(推荐,空间最优)

🔍 核心思想

使用三个指针:

  • prev:当前节点的前一个节点(初始为 null
  • curr:当前正在处理的节点(初始为 head
  • next:用于临时保存 curr.next,防止断链

在遍历过程中,逐步将每个节点的 next 指针指向前一个节点,从而实现整个链表方向的“倒转”。

💡 指针变化过程详解

我们以 1 -> 2 -> 3 -> null 为例:

步骤currnextcurr.next = prevprevcurr(更新后)
初始1--null1
11curr.next=21 → null12
22curr.next=32 → 123
33curr.next=null3 → 23null

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) 时:

  1. 先递归地反转 2 -> 3 -> null 这一部分;
  2. 假设这部分已经成功反转为 3 -> 2 -> null
  3. 我们只需要让 2 指向 1,然后把 1next 设为 null 即可。

🔄 递归三要素

  1. 终止条件

    if (!head || !head.next) return head;
    

    当链表为空或只有一个节点时,无需反转,直接返回。

  2. 递归调用

    const newHead = reverseListRecursive(head.next);
    

    让后面的链表完成反转,并返回新的头节点(即原链表尾部)。

  3. 回溯处理

    head.next.next = head;     // 让下一个节点指向自己
    head.next = null;          // 断开原链接,防止成环
    

🎯 关键点解释:head.next.next = head

这行代码是递归反转的核心!

  • head.next 是下一个节点。
  • head.next.next = head 相当于让下一个节点的 next 指向当前节点,形成反向连接。

比如当前是节点 12 已经反转好并指向 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) ❌
是否易理解中等(需理清指针顺序)高(需理解递归模型)
是否有栈溢出风险有(长链表时)
推荐场景生产环境、性能敏感场景学习递归思想、代码简洁需求

🧠 算法素养提升建议

  1. 画图辅助理解
    对于指针类问题,动手画出每一步的指针变化,能极大提升理解效率。

  2. 理解“子问题”思想
    递归的本质是分治。学会问自己:“如果剩下的部分已经被解决了,我该怎么合并结果?”

  3. 注意边界情况
    如空链表、单节点、两个节点等情况,都要考虑是否兼容。

  4. 练习变形题

    • 反转链表前 k 个节点
    • 反转第 m 到第 n 个节点
    • K 个一组反转链表(LeetCode 25)

✅ 结语

链表反转看似简单,却是检验编程基本功的一块试金石。无论是迭代中的指针迁移,还是递归中的“信任子问题”,都体现了算法设计的精妙之处。

掌握它,不是为了做题,而是为了培养一种严谨、清晰的程序逻辑思维。

当你能够不靠记忆、而是凭理解写出这两种解法时,你就已经迈出了通往高级算法之路的第一步。


📌 小挑战:你能尝试用 TypeScript 写一个类型安全的链表反转函数吗?欢迎留言讨论!

👨‍💻 编程路上,每一个基础知识点,都是未来的基石。共勉!