好的,我们这次来详细讲解 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 指针的指向。原本指向下一个节点的指针,现在要指向上一个节点。
为了做到这一点,我们需要三个指针来协同工作:
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移动到1curr = next_temp->curr从1移动到2
- 此时状态:
None <- [1] (prev) | [2] (curr) -> 3 -> 4 -> 5。我们已经成功反转了第一个节点。
- 保存下一个节点:
-
第二轮循环:
next_temp = curr.next->next_temp = 3curr.next = prev-> 节点2的next指向节点1。链表变为None <- 1 <- [2] 3 -> 4 -> 5- 移动指针:
prev = curr->prev = 2curr = next_temp->curr = 3
- 此时状态:
None <- 1 <- [2] (prev) | [3] (curr) -> 4 -> 5
-
后续循环:
重复这个过程。每一步都是:- 保存
curr的下一个节点。 - 将
curr的next指向前一个节点prev,实现局部反转。 - 将
prev和curr共同向前移动一步。
- 保存
-
循环终止:
当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,我们可以这样想:
- 如果我已经反转了
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,断开原来的连接,防止产生环。
- 代码就是
步骤三:递归法代码实现
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):
- 调用
reverseList(1)。 reverseList(1)调用reverseList(2)。reverseList(2)调用reverseList(3)。reverseList(3)满足终止条件(3.next is None),返回3。- 回到
reverseList(2):new_head = 3head是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 = 3head是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。 |
| 优点 | 效率高,空间占用小。 | 代码简洁,逻辑清晰。 |
| 缺点 | 代码比递归稍长。 | 链表很长时可能导致栈溢出。 |
| 推荐 | 首选方法,更实用。 | 理解递归思想的好例题。 |
希望这个从直觉到实现、从迭代到递归的逐步讲解,能让你彻底理解如何反转一个链表!这是很多复杂链表问题的基础,务必掌握。