LeetCode 第 206 题:反转链表(Reverse Linked List)
字数 3348 2025-10-25 17:22:19

好的,我们这次来详细讲解 LeetCode 第 206 题:反转链表(Reverse Linked List)

这是一道非常经典且基础的链表题目,理解它对于掌握链表操作至关重要。


1. 题目描述

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

示例 1

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

链表 1 -> 2 -> 3 -> 4 -> 5 反转为 5 -> 4 -> 3 -> 2 -> 1

示例 2

输入:head = [1,2]
输出:[2,1]

示例 3

输入:head = []
输出:[]

链表节点的定义(题目已给出):

# Definition for singly-linked list.
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

2. 循序渐进的理解过程

我们的目标是反转整个链表。想象一下,你有一条链条,每个环只连接着下一个环。现在你要把所有的连接方向反过来。

步骤一:理解我们需要做什么(迭代思路)

假设我们有链表:1 -> 2 -> 3 -> 4 -> 5 -> None

反转后,它应该变成:5 -> 4 -> 3 -> 2 -> 1 -> None

关键观察
反转的本质是改变每个节点的 next 指针的指向。原本指向下一个节点的指针,现在要指向上一个节点。

为了做到这一点,我们需要三个指针来协同工作:

  1. prev:指向当前节点之前已经反转好的部分的头节点。
  2. curr:指向我们当前正在处理的节点。
  3. next_temp:临时保存当前节点的下一个节点,因为当我们改变 curr.next 的指向后,我们就丢失了原本链表的后续部分。

步骤二:一步步拆解迭代过程

我们以 1 -> 2 -> 3 -> 4 -> 5 -> None 为例。

  1. 初始化

    • prev = None (因为第一个节点反转后将是最后一个节点,它的 next 应该是 None
    • curr = 1 (从头节点开始)
    • 此时链表状态:(None) <- ? [1] -> 2 -> 3 -> 4 -> 5
  2. 第一轮循环

    • 保存下一个节点:next_temp = curr.next -> next_temp = 2
    • 反转指针curr.next = prev -> 节点 1next 指向了 None。现在链表是 (None) <- [1] 2 -> 3 -> 4 -> 5。节点 1 和节点 2 之间的连接断了,但我们用 next_temp 记住了节点 2
    • 移动指针,为处理下一个节点做准备:
      • prev = curr -> prevNone 移动到 1
      • curr = next_temp -> curr1 移动到 2
    • 此时状态:None <- [1] (prev) | [2] (curr) -> 3 -> 4 -> 5。我们已经成功反转了第一个节点。
  3. 第二轮循环

    • next_temp = curr.next -> next_temp = 3
    • curr.next = prev -> 节点 2next 指向节点 1。链表变为 None <- 1 <- [2] 3 -> 4 -> 5
    • 移动指针:
      • prev = curr -> prev = 2
      • curr = next_temp -> curr = 3
    • 此时状态:None <- 1 <- [2] (prev) | [3] (curr) -> 4 -> 5
  4. 后续循环
    重复这个过程。每一步都是:

    • 保存 curr 的下一个节点。
    • currnext 指向前一个节点 prev,实现局部反转。
    • prevcurr 共同向前移动一步。
  5. 循环终止
    curr 移动到原链表的末尾(即 curr 变为 None)时,循环结束。
    此时,prev 指针正好指向的是原链表的最后一个节点,也就是新链表的头节点。

步骤三:迭代法代码实现

class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        # 初始化两个指针
        prev = None
        curr = head

        # 遍历链表
        while curr:
            # 1. 临时保存下一个节点,防止“失联”
            next_temp = curr.next
            # 2. 核心操作:反转指针
            curr.next = prev
            # 3. 移动双指针,准备处理下一个节点
            prev = curr
            curr = next_temp

        # 循环结束时,curr 为 None,prev 是新的头节点
        return prev

3. 另一种思路:递归解法

递归的代码更简洁,但理解起来需要一些思考。其核心思想是:假设我们已经成功反转了除了头节点之外的剩余链表,那么最后只需要将剩余链表的末尾指向头节点,并将头节点指向 None

步骤一:递归的思想

对于链表 1 -> 2 -> 3 -> 4 -> 5 -> None,我们可以这样想:

  1. 如果我已经反转了 2 -> 3 -> 4 -> 5,得到了 5 -> 4 -> 3 -> 2 -> None。我们称这个新链表的头为 new_head
  2. 现在,原来的节点 1 还指向节点 2(即 1 -> 2)。
  3. 那么,我们只需要让节点 2(现在是新链表的尾部)的 next 指向节点 1,即 2.next = 1
  4. 然后再让节点 1next 指向 None

这样,整个链表就反转完成了。新的头节点是 new_head(也就是 5)。

步骤二:递归的步骤拆解

  1. 递归基(终止条件):如果链表为空(head is None)或者只有一个节点(head.next is None),那么不需要反转,直接返回 head
  2. 递归反转:调用函数反转 head.next 之后的链表。我们相信这个递归调用能返回已经反转好的新链表的头节点 new_head
    • 当前状态:head -> ... -> new_head
    • 更准确地说,此时链表结构是:head -> (已反转部分的尾部,即原来的 head.next) <- ... <- new_head
  3. 处理当前节点:我们现在要做的就是让“已反转部分的尾部”(即 head.next)指向 head
    • 代码就是 head.next.next = head
    • 然后,将 head 本身的 next 置为 None,断开原来的连接,防止产生环。

步骤三:递归法代码实现

class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        # 递归基:空链表或只有一个节点,直接返回
        if not head or not head.next:
            return head

        # 递归反转以 head.next 开头的子链表
        # new_head 是反转后子链表的头节点,也是最终整个反转链表的头节点
        new_head = self.reverseList(head.next)

        # 核心操作:
        # 1. 让“已反转子链表”的最后一个节点(即 head.next)指向当前节点 head
        head.next.next = head
        # 2. 断开当前节点原来的指向,防止成环
        head.next = None

        # 返回新的头节点
        return new_head

递归过程可视化(对于 1->2->3->None)

  1. 调用 reverseList(1)
  2. reverseList(1) 调用 reverseList(2)
  3. reverseList(2) 调用 reverseList(3)
  4. reverseList(3) 满足终止条件(3.next is None),返回 3
  5. 回到 reverseList(2)
    • new_head = 3
    • head2head.next3
    • 执行 2.next.next = 2,即 3.next = 2。现在链表是 3 -> 2
    • 执行 2.next = None,断开 2->3 的旧连接。现在链表是 3 -> 2 -> None
    • 返回 new_head(即 3)。
  6. 回到 reverseList(1)
    • new_head = 3
    • head1head.next2(但此时 2.next 已经是 None)。
    • 执行 1.next.next = 1,即 2.next = 1。现在链表是 3 -> 2 -> 1
    • 执行 1.next = None。最终链表是 3 -> 2 -> 1 -> None
    • 返回 new_head(即 3)。

4. 总结与对比

特性 迭代法 递归法
时间复杂度 O(n),n 是链表长度,遍历一次。 O(n),递归深度为 n。
空间复杂度 O(1),只用了几个指针变量。 O(n),递归调用栈的深度为 n。
优点 效率高,空间占用小。 代码简洁,逻辑清晰。
缺点 代码比递归稍长。 链表很长时可能导致栈溢出。
推荐 首选方法,更实用。 理解递归思想的好例题。

希望这个从直觉到实现、从迭代到递归的逐步讲解,能让你彻底理解如何反转一个链表!这是很多复杂链表问题的基础,务必掌握。

好的,我们这次来详细讲解 LeetCode 第 206 题:反转链表(Reverse Linked List) 。 这是一道非常经典且基础的链表题目,理解它对于掌握链表操作至关重要。 1. 题目描述 题意 : 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表的头节点。 示例 1 : 链表 1 -> 2 -> 3 -> 4 -> 5 反转为 5 -> 4 -> 3 -> 2 -> 1 。 示例 2 : 示例 3 : 链表节点的定义 (题目已给出): 2. 循序渐进的理解过程 我们的目标是反转整个链表。想象一下,你有一条链条,每个环只连接着下一个环。现在你要把所有的连接方向反过来。 步骤一:理解我们需要做什么(迭代思路) 假设我们有链表: 1 -> 2 -> 3 -> 4 -> 5 -> None 反转后,它应该变成: 5 -> 4 -> 3 -> 2 -> 1 -> None 关键观察 : 反转的本质是改变每个节点的 next 指针的指向。原本指向下一个节点的指针,现在要指向上一个节点。 为了做到这一点,我们需要三个指针来协同工作: prev :指向当前节点 之前 已经反转好的部分的头节点。 curr :指向我们当前正在处理的节点。 next_temp :临时保存当前节点的下一个节点,因为当我们改变 curr.next 的指向后,我们就丢失了原本链表的后续部分。 步骤二:一步步拆解迭代过程 我们以 1 -> 2 -> 3 -> 4 -> 5 -> None 为例。 初始化 : prev = None (因为第一个节点反转后将是最后一个节点,它的 next 应该是 None ) curr = 1 (从头节点开始) 此时链表状态: (None) <- ? [1] -> 2 -> 3 -> 4 -> 5 第一轮循环 : 保存下一个节点: next_temp = curr.next -> next_temp = 2 反转指针 : curr.next = prev -> 节点 1 的 next 指向了 None 。现在链表是 (None) <- [1] 2 -> 3 -> 4 -> 5 。节点 1 和节点 2 之间的连接断了,但我们用 next_temp 记住了节点 2 。 移动指针,为处理下一个节点做准备: prev = curr -> prev 从 None 移动到 1 curr = next_temp -> curr 从 1 移动到 2 此时状态: None <- [1] (prev) | [2] (curr) -> 3 -> 4 -> 5 。我们已经成功反转了第一个节点。 第二轮循环 : next_temp = curr.next -> next_temp = 3 curr.next = prev -> 节点 2 的 next 指向节点 1 。链表变为 None <- 1 <- [2] 3 -> 4 -> 5 移动指针: prev = curr -> prev = 2 curr = next_temp -> curr = 3 此时状态: None <- 1 <- [2] (prev) | [3] (curr) -> 4 -> 5 后续循环 : 重复这个过程。每一步都是: 保存 curr 的下一个节点。 将 curr 的 next 指向前一个节点 prev ,实现局部反转。 将 prev 和 curr 共同向前移动一步。 循环终止 : 当 curr 移动到原链表的末尾(即 curr 变为 None )时,循环结束。 此时, prev 指针正好指向的是原链表的最后一个节点,也就是新链表的头节点。 步骤三:迭代法代码实现 3. 另一种思路:递归解法 递归的代码更简洁,但理解起来需要一些思考。其核心思想是: 假设我们已经成功反转了除了头节点之外的剩余链表,那么最后只需要将剩余链表的末尾指向头节点,并将头节点指向 None 。 步骤一:递归的思想 对于链表 1 -> 2 -> 3 -> 4 -> 5 -> None ,我们可以这样想: 如果我 已经 反转了 2 -> 3 -> 4 -> 5 ,得到了 5 -> 4 -> 3 -> 2 -> None 。我们称这个新链表的头为 new_head 。 现在,原来的节点 1 还指向节点 2 (即 1 -> 2 )。 那么,我们只需要让节点 2 (现在是新链表的尾部)的 next 指向节点 1 ,即 2.next = 1 。 然后再让节点 1 的 next 指向 None 。 这样,整个链表就反转完成了。新的头节点是 new_head (也就是 5 )。 步骤二:递归的步骤拆解 递归基(终止条件) :如果链表为空( head is None )或者只有一个节点( head.next is None ),那么不需要反转,直接返回 head 。 递归反转 :调用函数反转 head.next 之后的链表。我们相信这个递归调用能返回已经反转好的新链表的头节点 new_head 。 当前状态: head -> ... -> new_head 更准确地说,此时链表结构是: head -> (已反转部分的尾部,即原来的 head.next ) <- ... <- new_head 处理当前节点 :我们现在要做的就是让“已反转部分的尾部”(即 head.next )指向 head 。 代码就是 head.next.next = head 。 然后,将 head 本身的 next 置为 None ,断开原来的连接,防止产生环。 步骤三:递归法代码实现 递归过程可视化(对于 1->2->3->None) : 调用 reverseList(1) 。 reverseList(1) 调用 reverseList(2) 。 reverseList(2) 调用 reverseList(3) 。 reverseList(3) 满足终止条件( 3.next is None ),返回 3 。 回到 reverseList(2) : new_head = 3 head 是 2 , head.next 是 3 。 执行 2.next.next = 2 ,即 3.next = 2 。现在链表是 3 -> 2 。 执行 2.next = None ,断开 2->3 的旧连接。现在链表是 3 -> 2 -> None 。 返回 new_head (即 3 )。 回到 reverseList(1) : new_head = 3 head 是 1 , head.next 是 2 (但此时 2.next 已经是 None )。 执行 1.next.next = 1 ,即 2.next = 1 。现在链表是 3 -> 2 -> 1 。 执行 1.next = None 。最终链表是 3 -> 2 -> 1 -> None 。 返回 new_head (即 3 )。 4. 总结与对比 | 特性 | 迭代法 | 递归法 | | :--- | :--- | :--- | | 时间复杂度 | O(n),n 是链表长度,遍历一次。 | O(n),递归深度为 n。 | | 空间复杂度 | O(1),只用了几个指针变量。 | O(n),递归调用栈的深度为 n。 | | 优点 | 效率高,空间占用小。 | 代码简洁,逻辑清晰。 | | 缺点 | 代码比递归稍长。 | 链表很长时可能导致栈溢出。 | | 推荐 | 首选方法 ,更实用。 | 理解递归思想的好例题。 | 希望这个从直觉到实现、从迭代到递归的逐步讲解,能让你彻底理解如何反转一个链表!这是很多复杂链表问题的基础,务必掌握。