LeetCode 206. 反转链表(迭代 + 递归详解)

题目描述

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例 1:

复制代码
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

示例 2:

复制代码
输入:head = [1,2]
输出:[2,1]

示例 3:

复制代码
输入:head = []
输出:[]

提示:

  • 链表中节点的数目范围是 [0, 5000]

  • -5000 <= Node.val <= 5000

进阶: 链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?


解题思路

反转链表的核心思想是:改变每个节点的 next 指针方向,让它指向前一个节点

我们依次用两种方法来实现,并重点剖析递归的执行过程。


方法一:迭代法

思路

  1. 初始化三个指针:

    • pre:指向已经反转好的链表头(初始为 null

    • cur:指向当前待反转的节点(初始为 head

    • temp:暂存 cur.next,防止链表断开后丢失后续节点

  2. 遍历链表,每一步:

    • 保存 cur.nexttemp

    • cur.next 指向 pre(完成当前节点的反转)

    • 移动 precur

    • 移动 curtemp

  3. curnull 时,遍历结束,pre 就是反转后链表的新头节点。

代码

javascript 复制代码
var reverseList = function(head) {
    let pre = null;
    let cur = head;
    let temp;
    while (cur !== null) {
        temp = cur.next;   // 保存下一个节点
        cur.next = pre;    // 当前节点指向前一个节点
        pre = cur;         // pre 前移
        cur = temp;        // cur 前移
    }
    return pre;
};

复杂度分析

  • 时间复杂度:O(n),每个节点遍历一次

  • 空间复杂度:O(1),只使用了常数个指针


方法二:递归法

递归法相比迭代法更抽象,但代码极其简洁。理解递归的关键是明确递归函数的定义

递归函数定义

我们定义递归函数 reverse(cur, pre)

  • 功能 :反转以 cur 为当前节点,pre 为前一个节点的链表

  • 返回值:反转后的链表的新头节点

递归过程详解

假设链表为 1 -> 2 -> 3 -> 4 -> null,初始调用 reverse(head, null)

步骤拆解

  1. 第一层调用 reverse(1, null)

    • cur = 1, pre = null

    • cur !== null,执行:

      • temp = cur.nexttemp = 2

      • cur.next = pre → 将节点 1next 指向 null(完成反转)

      • 返回 reverse(temp, cur)reverse(2, 1)

    • 此时链表状态:1 -> null,节点 2 暂由 temp 保留。

  2. 第二层调用 reverse(2, 1)

    • cur = 2, pre = 1

    • 保存 temp = cur.next = 3

    • cur.next = pre → 节点 2next 指向 1

    • 返回 reverse(3, 2)

  3. 第三层调用 reverse(3, 2)

    • cur = 3, pre = 2

    • temp = 4

    • cur.next = pre → 节点 3next 指向 2

    • 返回 reverse(4, 3)

  4. 第四层调用 reverse(4, 3)

    • cur = 4, pre = 3

    • temp = null

    • cur.next = pre → 节点 4next 指向 3

    • 返回 reverse(null, 4)

  5. 第五层调用 reverse(null, 4)

    • cur === null,递归终止,返回 pre = 4

回溯返回

  • 第五层返回 4 给第四层,第四层直接返回 4 给第三层......最终第一层返回 4

此时链表已经变成了 4 -> 3 -> 2 -> 1 -> null,头节点是 4

递归调用栈示意图

javascript 复制代码
reverse(1, null)
│   temp = 2, 1.next = null
│   return reverse(2, 1)
│
└── reverse(2, 1)
    │   temp = 3, 2.next = 1
    │   return reverse(3, 2)
    │
    └── reverse(3, 2)
        │   temp = 4, 3.next = 2
        │   return reverse(4, 3)
        │
        └── reverse(4, 3)
            │   temp = null, 4.next = 3
            │   return reverse(null, 4)
            │
            └── reverse(null, 4) → 返回 4
        ← 返回 4
    ← 返回 4
← 返回 4

代码实现

javascript 复制代码
var reverseList = function(head) {
    function reverse(cur, pre) {
        if (cur === null) {
            return pre;   // 递归到末尾,pre 就是反转后的头节点
        }
        let temp = cur.next;   // 保存下一个节点
        cur.next = pre;        // 当前节点指向前一个节点
        return reverse(temp, cur);  // 继续反转剩余部分
    }
    return reverse(head, null);
};

递归法注意点

  • 递归终止条件cur === null 时,说明已经处理完所有节点,返回 pre 作为新头。

  • 原地修改 :每次递归只改变当前节点的 next 指向,没有额外创建节点。

  • 栈深度:递归深度等于链表长度,当链表过长(如 5000 节点)时,可能导致栈溢出。但在题目范围内是安全的。

复杂度分析

  • 时间复杂度:O(n),每个节点递归处理一次

  • 空间复杂度:O(n),递归调用栈需要 O(n) 的额外空间


总结

方法 优点 缺点
迭代 空间复杂度 O(1),效率高 代码稍显繁琐
递归 代码简洁,逻辑清晰 空间复杂度 O(n),链表过长有栈溢出风险

本题是链表操作的入门题,掌握迭代和递归两种写法,能帮助理解指针操作和递归思想。建议初学者先理解迭代法,再逐步消化递归法,重点体会递归函数"不关心中间过程,只相信子调用能完成剩余部分"的思维方式。

相关推荐
凡人叶枫2 小时前
Effective C++ 条款17:以独立语句将 newed 对象置入智能指针
java·linux·开发语言·c++·算法
菜鸟‍3 小时前
LeetCode 1 27 和 704 || 两数之和 移除元素 二分查找
算法·leetcode·职场和发展
退休倒计时4 小时前
【每日一题】LeetCode 142. 环形链表 II TypeScript
算法·leetcode·链表·typescript
popcorn_min5 小时前
Digits 手写数字识别:随机森林多分类 + 像素级特征热力图
算法·随机森林·分类
liulilittle5 小时前
拥塞控制:排水终止的两种决策:OR 与 AND
网络·tcp/ip·计算机网络·算法·信息与通信·tcp·通信
weixin_307779136 小时前
从脚本执行到智能体协作:AI辅助测试能力的范式重构
运维·开发语言·人工智能·算法·测试用例
量化君也6 小时前
从回测到全自动实盘交易,全天候策略需要经历哪些改造?
大数据·人工智能·python·算法·金融
fox_lht6 小时前
第十五章 函数式语言:迭代器和闭包
开发语言·后端·学习·算法·rust
zhengzhouliuhaha7 小时前
智能医疗设备控费系统:以全院一体化管控,筑牢医疗资源“安全阀”
大数据·数据结构·人工智能·算法·安全·机器学习·软件需求