LeetCode 206. 反转链表详解

题目描述

206. 反转链表 是 LeetCode 上的一道经典链表操作题目,属于简单难度。题目要求反转一个单链表,即将链表的所有节点顺序完全颠倒。

题目要求:

  • 输入:一个单链表的头节点 head
  • 输出:反转后链表的头节点
  • 要求:使用迭代或递归两种方法实现

分析(不明确-qoderWork分析)

链表概念

先看链表节点长什么样

java 复制代码
class ListNode {
    int val;        // 存数据
    ListNode next;  // 存「下一个节点的地址」
}

每个节点是一个小盒子,里面装两样东西:

val:当前节点的数据(比如 3)

next:一根指向下一个盒子的箭头

画出来更直观

链表 1 → 2 → 3 → null 在内存里大致是:

java 复制代码
head
 │
 ▼
┌─────────┐    ┌─────────┐    ┌─────────┐
│ val: 1  │    │ val: 2  │    │ val: 3  │
│ next: ●─┼──▶ │ next: ●─┼──▶ │ next: ●─┼──▶ null
└─────────┘    └─────────┘    └─────────┘
   节点A          节点B          节点C
 地址 0x100     地址 0x208     地址 0x3F0

那么对于 head(指向节点 A):

表达式 是什么 在上图里的值
head 节点 A 的地址 0x100
head.val 节点 A 里的数据 1
head.next 节点 B 的地址(一根箭头) 0x208
head.next.val 顺着箭头到节点 B,再取它的数据 2
head.next.next 节点 C 的地址 0x3F0
head.next.next.val 节点 C 的数据 3
head.next.next.next null(链表结束) null

回答你的两个具体困惑

是下一个值吗?

不是。head.next 不是 2,要拿到值 2 必须再写 head.next.val。.next 只是「箭头」,.val 才是「数据」。

是下一个空间吗?

也不准确。它不是一块空着的空间,而是「指向已经存在的下一个节点的地址」。如果链表到底了(比如节点 C),那么 C.next 是 null,意思是「没有下一个节点」,而不是「指向一块空白空间」。

类比理解

把链表想成寻宝游戏的纸条:

每张纸条 = 一个节点

纸条上写的「数字」= val

纸条上写的「下一张纸条藏在哪」= next

head = 你手里的第一张纸条

head.next = 第一张纸条上写的那个「下一张藏哪」的地址,不是数字本身,也不是空地,而是一个指引

null = 纸条上写「到此为止,没有下一张了」

为什么递归反转代码里会写 head.next.next = head

回到你最早那道题,现在应该秒懂了:

head.next.next = head;

读法:「沿着 head 的箭头走到下一个节点(head.next),把那个节点的箭头(.next)改成指向 head 自己」。

也就是把后一个节点的 next 反过来指回前一个节点,这就是「反转一对指针」的核心动作。

递归

这段是用递归反转单链表,核心是「先递归到底,再回溯时一对一对反转指针」。

假设链表是 1 → 2 → 3 → 4 → 5 → null,跟着代码走一遍最直观。

第一步:终止条件

if (head == null || head.next == null) return head;

递归到最后一个节点 5 时,5.next == null,直接把 5 返回。这个返回值会一路往上传,它就是反转后的新头节点 newHead,整个递归过程中 newHead 永远指向 5,从不改变。

第二步:递归下钻

java 复制代码
ListNode newHead = reverseList(head.next);

调用栈一直压到底:

reverseList(1) → reverseList(2) → reverseList(3) → reverseList(4) → reverseList(5)

到 5 时直接返回。然后开始回溯。

第三步:回溯时反转指针(关键)

以回溯到 head = 4 这一帧为例,此时链表状态是:

1 → 2 → 3 → 4 → 5 ↑ newHead 指着它

执行:

java 复制代码
head.next.next = head;  // 4.next.next = 4,即 5.next = 4
head.next = null;       // 4.next = null

效果:

1 → 2 → 3 → 4 ← 5 (4.next=null,5.next=4) ↑ newHead

接着回溯到 head = 3,同理 3.next.next = 3 让 4 → 3,再 3.next = null:

1 → 2 → 3 ← 4 ← 5 ↑ newHead

一直回溯到 head = 1,最终得到 5 → 4 → 3 → 2 → 1 → null,返回 newHead(也就是 5)。

为什么 head.next.next = head 这一句这么妙

在当前帧里,head.next 是后一个节点(比如 head=4 时 head.next=5),让 5.next 反指回 4,就完成了这一对的反转。然后 head.next = null 是为了:

如果 head 是原来的头(最后回溯到 1),需要 1.next = null 收尾;

如果不是头,下一帧回溯时它的前驱会重新覆盖 head.next,所以置 null 也无副作用。

一句话记忆

递归走到底拿到新头 → 回溯时让「下一个节点反指回自己」,再切断自己的 next,逐对翻转。

自己总结

1、递归结束节点:到达链表尾部及回溯后 递归到头部结束

2、先递归后回溯

3、重点在回溯(将链表翻转,将下一个节点指回自己,自己的下个节点为null【递归结束用】)

4、链表的实现加强下,给出一个节点,节点的next指向下一个节点,自动进行回溯

前期误解【对链表逻辑模糊了】

为什么return head可以进行回溯,head不是一个节点吗?如果是return reverseList(head.next)感觉才是一个回溯状态

你把「回溯」和「递归调用」搞混了,这是初学者最常见的卡点。澄清两件事:

  1. 回溯不是某条 return 语句的功劳,而是「函数调用结束后自动发生」的事

只要一个函数 return 了------不管 return 的是 head、null、42、还是 reverseList(...) 的结果------当前这一帧就从调用栈弹出,控制权自动回到调用它的那一帧继续执行。这就是「回溯」。

所以 return head; 也会回溯,return newHead; 也会回溯,只要函数结束就一定回溯。

  1. return reverseList(head.next) 不是「回溯状态」,那是「继续往下递归」

如果代码改成:

java 复制代码
public ListNode reverseList(ListNode head) {
    if (head == null || head.next == null) return head;
    return reverseList(head.next);   // ← 只有这一行
}

它的意思是:别管当前的 head,把后面的结果直接透传上去。最后效果只是返回链表最后一个节点,根本没有反转任何指针。这不是回溯,是「把球往下传,传到底再原样传回来」。

  1. 用调用栈走一遍你就明白了
java 复制代码
调用 reverseList(1)
  └ 调用 reverseList(2)
      └ 调用 reverseList(3)
          └ 调用 reverseList(4)
              └ 调用 reverseList(5)
                  ↑ 命中终止条件,return head(也就是 return 5)
              ← 回到 reverseList(4) 这一帧,newHead 拿到 5
                执行 head.next.next = head; head.next = null;
                return newHead;
          ← 回到 reverseList(3) 这一帧,newHead 拿到 5
            执行 head.next.next = head; head.next = null;
            return newHead;
        ... 一路弹栈 ...

注意看 reverseList(5) 那一行 return head 之后,箭头是 ←,这就是回溯。它把 5 这个节点作为返回值交给上一帧的 newHead 变量。

  1. 那为什么 newHead 在每一帧都等于 5?

因为每一帧的 newHead = reverseList(head.next),而 reverseList(head.next) 这次调用最终 return 的就是最深处那一帧 return 的 head(也就是 5)。中间帧的 return 值会被它的调用者继续往上 return,于是 5 这个节点一路传到最外层。

一句话总结

return head 返回的是「新链表的头节点这个值」,回溯是 Java 调用栈自带的机制,跟你 return 什么内容无关;而 return reverseList(head.next) 是发起新的递归调用,方向是「往下钻」,不是「往上回」。

CSDN官方AI分析

核心思路

1. 迭代法(双指针法)

迭代法是反转链表最直观的方法,使用两个指针 prevcurr 遍历链表:

python 复制代码
def reverseList(head):
    prev = None
    curr = head
    
    while curr:
        # 保存下一个节点
        next_node = curr.next
        # 反转当前节点的指针
        curr.next = prev
        # 移动指针
        prev = curr
        curr = next_node
    
    return prev

算法步骤:

  1. 初始化 prev = Nonecurr = head
  2. 遍历链表,每次循环:
    • 保存 curr.next 到临时变量
    • curr.next 指向 prev
    • 移动 prevcurr 指针
  3. currNone 时,prev 就是新的头节点

时间复杂度: O(n),需要遍历一次链表

空间复杂度: O(1),只使用了常数级别的额外空间

2. 递归法

递归法从链表尾部开始反转,思路更加简洁:

python 复制代码
def reverseList(head):
    # 递归终止条件
    if not head or not head.next:
        return head
    
    # 递归反转后续链表
    new_head = reverseList(head.next)
    
    # 将当前节点连接到反转后的链表
    head.next.next = head
    head.next = None
    
    return new_head

递归过程:

  1. 递归到链表末尾,找到新的头节点
  2. 从后往前逐个反转节点指针
  3. 将当前节点的下一个节点的 next 指向当前节点
  4. 断开当前节点的原始连接

时间复杂度: O(n),需要递归 n 次

空间复杂度: O(n),递归调用栈的深度为 n

关键细节与注意事项

边界情况处理

  1. 空链表 :输入 headNone 时,直接返回 None
  2. 单节点链表:直接返回该节点本身
  3. 双节点链表:需要正确处理两个节点的指针反转

指针操作要点

  • 在迭代法中,必须先保存 curr.next,再修改 curr.next 的指向
  • 递归法中,要注意断开原始连接,避免形成环
  • 两种方法都要确保不会丢失对后续节点的引用

实际应用场景

反转链表不仅是算法面试的常见题目,在实际开发中也有广泛应用:

  1. 浏览器历史记录:实现前进后退功能
  2. 撤销操作栈:某些编辑器中的撤销/重做功能
  3. 链表相关算法:如判断回文链表、链表排序等的基础操作
  4. 内存管理:某些内存分配算法中需要反转空闲块链表

扩展练习

掌握了基础的反转链表后,可以尝试以下变体题目:

  1. 反转链表 II:反转链表的一部分(指定区间)
  2. K 个一组反转链表:每 k 个节点进行反转
  3. 反转链表中的相邻节点:两两交换链表中的节点
  4. 回文链表:判断链表是否为回文结构

总结

反转链表是链表操作的基础,掌握迭代和递归两种方法对于理解链表数据结构至关重要。建议初学者先掌握迭代法,再理解递归法,最后能够灵活运用两种方法解决相关问题。

通过这道题目,可以深入理解:

  • 链表的指针操作
  • 递归与迭代的转换
  • 边界条件的处理
  • 算法的时间空间复杂度分析