LeetCode 206. 反转链表 - 完整解法详解
一、问题理解
问题描述
给你单链表的头节点 head,请你反转链表,并返回反转后的链表。
示例
text
输入: head = [1,2,3,4,5]
输出: [5,4,3,2,1]
输入: head = [1,2]
输出: [2,1]
输入: head = []
输出: []
要求
-
时间复杂度:O(n)
-
空间复杂度:递归解法 O(n),迭代解法 O(1)
-
链表可以修改
二、核心思路:节点指针反转
基本思想
反转链表的核心是将每个节点的 next 指针指向前一个节点。我们需要三个指针:
-
prev: 指向已反转部分的头节点
-
curr: 指向当前要处理的节点
-
next: 保存当前节点的下一个节点,防止链表断裂
递归与迭代
-
迭代法:通过循环逐个反转节点指针
-
递归法:通过递归到达链表末尾,然后在返回过程中反转指针
三、代码逐行解析
方法一:迭代法(最优解)
Python 解法
python
# Definition for singly-linked list.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
# 初始化三个指针
prev = None # 已反转部分的头节点
curr = head # 当前要处理的节点
# 遍历链表
while curr:
# 保存当前节点的下一个节点
next_node = curr.next
# 反转指针:当前节点指向前一个节点
curr.next = prev
# 移动prev和curr指针
prev = curr
curr = next_node
# 返回新的头节点
return prev
Java 解法
java
class Solution {
public ListNode reverseList(ListNode head) {
// 初始化指针
ListNode prev = null; // 已反转部分的头节点
ListNode curr = head; // 当前要处理的节点
// 遍历链表
while (curr != null) {
// 保存下一个节点
ListNode next = curr.next;
// 反转指针
curr.next = prev;
// 移动指针
prev = curr;
curr = next;
}
// 返回新的头节点
return prev;
}
}
方法二:递归法(简洁但空间复杂度高)
Python 解法
python
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
# 递归终止条件:空链表或只有一个节点
if head is None or head.next is None:
return head # 链表末尾,即反转后的头节点
# 递归反转剩余部分
rev_head = self.reverseList(head.next) # 递:到达链表末尾,拿到新链表的头节点
# 归:在返回过程中反转指针
tail = head.next # head.next是反转后链表的尾节点
tail.next = head # 将当前节点接到反转链表的末尾
head.next = None # 断开原连接,防止成环
return rev_head # 返回反转后的头节点
Java 解法
java
class Solution {
public ListNode reverseList(ListNode head) {
// 递归终止条件
if (head == null || head.next == null) {
return head;
}
// 递归反转剩余部分
ListNode revHead = reverseList(head.next);
// 在返回过程中反转指针
head.next.next = head; // 将当前节点接到反转链表的末尾
head.next = null; // 断开原连接,防止成环
return revHead;
}
}
四、Java 与 Python 语法对比
1. 链表节点定义
| 操作 | Java | Python |
|---|---|---|
| 节点类 | 需要预定义 | 需要预定义 |
| 创建节点 | new ListNode(val) |
ListNode(val) |
2. 空值判断
| 操作 | Java | Python |
|---|---|---|
| 检查null | node == null |
node is None |
3. 指针操作
| 操作 | Java | Python |
|---|---|---|
| 访问属性 | node.next |
node.next |
| 修改属性 | node.next = value |
node.next = value |
五、实例演示
示例:head = [1,2,3,4,5]
迭代法步骤:
-
初始状态:
-
prev = None -
curr = 1 → 2 → 3 → 4 → 5
-
-
第1次循环:
-
next_node = curr.next = 2 -
curr.next = prev→1 → None -
prev = curr = 1 -
curr = next_node = 2 -
状态:
1 → None,2 → 3 → 4 → 5
-
-
第2次循环:
-
next_node = curr.next = 3 -
curr.next = prev→2 → 1 → None -
prev = curr = 2 -
curr = next_node = 3 -
状态:
2 → 1 → None,3 → 4 → 5
-
-
第3次循环:
-
next_node = curr.next = 4 -
curr.next = prev→3 → 2 → 1 → None -
prev = curr = 3 -
curr = next_node = 4 -
状态:
3 → 2 → 1 → None,4 → 5
-
-
第4次循环:
-
next_node = curr.next = 5 -
curr.next = prev→4 → 3 → 2 → 1 → None -
prev = curr = 4 -
curr = next_node = 5 -
状态:
4 → 3 → 2 → 1 → None,5
-
-
第5次循环:
-
next_node = curr.next = None -
curr.next = prev→5 → 4 → 3 → 2 → 1 → None -
prev = curr = 5 -
curr = next_node = None -
状态:
5 → 4 → 3 → 2 → 1 → None
-
-
循环结束 ,返回
prev = 5
递归法步骤:
text
初始调用:reverseList(1)
调用:reverseList(2)
调用:reverseList(3)
调用:reverseList(4)
调用:reverseList(5)
终止条件满足,返回5
返回:5,此时head=4
tail = head.next = 5
tail.next = head → 5 → 4
head.next = None → 4 → None
返回5 → 4
返回:5,此时head=3
tail = head.next = 4
tail.next = head → 4 → 3
head.next = None → 3 → None
返回5 → 4 → 3
返回:5,此时head=2
tail = head.next = 3
tail.next = head → 3 → 2
head.next = None → 2 → None
返回5 → 4 → 3 → 2
返回:5,此时head=1
tail = head.next = 2
tail.next = head → 2 → 1
head.next = None → 1 → None
返回5 → 4 → 3 → 2 → 1
最终结果:5 → 4 → 3 → 2 → 1
六、关键细节解析
1. 为什么迭代法需要保存next_node?
-
当我们修改
curr.next时,会丢失原链表的下一个节点 -
必须提前保存
curr.next,否则无法继续遍历链表
2. 递归法的终止条件为什么是 head is None or head.next is None?
-
head is None:处理空链表的情况 -
head.next is None:链表只有一个节点时,反转后还是它自己 -
这两个条件都表示不需要继续反转
3. 为什么递归法中要设置 head.next = None?
-
如果不设置,原链表的头节点会指向第二个节点,而第二个节点又指向头节点,形成环
-
例如:原始链表
1 → 2 → 3,反转后如果不断开,会是1 ↔ 2 ← 3
4. 递归法的空间复杂度为什么是 O(n)?
-
递归调用会使用系统栈空间
-
对于长度为 n 的链表,递归深度为 n
-
空间复杂度为 O(n)
5. 如何处理空链表?
-
迭代法:
while curr条件会跳过空链表,直接返回prev = None -
递归法:终止条件
head is None会直接返回None
七、复杂度分析
迭代法
-
时间复杂度:O(n),每个节点访问一次
-
空间复杂度:O(1),只使用了常数个指针变量
递归法
-
时间复杂度:O(n),每个节点访问一次
-
空间复杂度:O(n),递归调用栈深度为 n
八、其他解法
解法一:头插法
python
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
# 创建一个虚拟头节点
dummy = ListNode()
# 遍历原链表,将每个节点插入到虚拟头节点之后
curr = head
while curr:
# 保存下一个节点
next_node = curr.next
# 将当前节点插入到dummy之后
curr.next = dummy.next
dummy.next = curr
# 继续处理下一个节点
curr = next_node
return dummy.next
解法二:栈方法(使用额外空间)
python
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
if not head:
return None
# 使用栈存储所有节点
stack = []
curr = head
while curr:
stack.append(curr)
curr = curr.next
# 从栈中弹出节点构建反转链表
new_head = stack.pop()
curr = new_head
while stack:
curr.next = stack.pop()
curr = curr.next
# 最后一个节点的next置为None
curr.next = None
return new_head
解法三:双指针简化版
python
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
# 使用两个指针
prev = None
curr = head
while curr:
# Python的并行赋值,不需要临时变量
curr.next, prev, curr = prev, curr, curr.next
return prev
九、常见问题与解答
Q1: 反转链表时如何处理环?
A1: 题目假设链表无环。如果有环,反转操作会变得复杂,需要先检测环。
Q2: 如何测试反转链表的正确性?
A2: 可以测试以下情况:
-
空链表
-
单节点链表
-
两个节点的链表
-
多个节点的链表
-
验证反转后头尾节点是否正确
Q3: 递归法和迭代法哪个更好?
A3:
-
迭代法:空间复杂度 O(1),更优
-
递归法:代码简洁,但空间复杂度 O(n),可能栈溢出
-
在实际应用中,迭代法通常更受青睐
Q4: 如果需要反转部分链表怎么办?
A4: 可以使用类似的方法,但要记录:
-
反转部分的前一个节点(用于连接)
-
反转部分的第一个节点(反转后变为尾节点)
-
反转部分的最后一个节点(反转后变为头节点)
Q5: 如何同时反转链表和打印节点值?
A5: 可以在反转过程中或反转后遍历链表:
python
def reverse_and_print(head):
# 反转链表
prev = None
curr = head
while curr:
next_node = curr.next
curr.next = prev
prev = curr
curr = next_node
# 打印反转后的链表
curr = prev
while curr:
print(curr.val, end=" ")
curr = curr.next
print()
十、相关题目
1. LeetCode 92. 反转链表 II
python
class Solution:
def reverseBetween(self, head: ListNode, left: int, right: int) -> ListNode:
# 创建虚拟头节点,简化边界处理
dummy = ListNode(0, head)
# 找到left位置的前一个节点
pre = dummy
for _ in range(left - 1):
pre = pre.next
# 反转从left到right的部分
curr = pre.next
prev = None
for _ in range(right - left + 1):
next_node = curr.next
curr.next = prev
prev = curr
curr = next_node
# 连接反转部分的前后
pre.next.next = curr # 反转部分的尾节点连接right后面的节点
pre.next = prev # left前面的节点连接反转部分的头节点
return dummy.next
2. LeetCode 25. K 个一组翻转链表
python
class Solution:
def reverseKGroup(self, head: ListNode, k: int) -> ListNode:
# 辅助函数:反转链表的一部分
def reverse(head, tail):
prev = tail.next # 注意这里连接到下一组的头节点
curr = head
while prev != tail:
next_node = curr.next
curr.next = prev
prev = curr
curr = next_node
return tail, head # 返回新的头尾节点
# 创建虚拟头节点
dummy = ListNode(0, head)
pre = dummy
while head:
# 检查是否有k个节点
tail = pre
for _ in range(k):
tail = tail.next
if not tail:
return dummy.next
# 记录下一组的头节点
next_group = tail.next
# 反转当前组
head, tail = reverse(head, tail)
# 连接反转后的组
pre.next = head
tail.next = next_group
# 移动指针到下一组
pre = tail
head = next_group
return dummy.next
3. LeetCode 234. 回文链表
python
class Solution:
def isPalindrome(self, head: ListNode) -> bool:
# 找到链表中间节点
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# 反转后半部分链表
prev = None
curr = slow
while curr:
next_node = curr.next
curr.next = prev
prev = curr
curr = next_node
# 比较前半部分和反转后的后半部分
left, right = head, prev
while right: # 只需要比较后半部分长度
if left.val != right.val:
return False
left = left.next
right = right.next
return True
十一、总结
核心要点
-
指针操作 :反转链表的核心是改变节点的
next指针指向 -
三指针法 :使用
prev、curr、next三个指针可以高效地反转链表 -
边界处理:注意处理空链表和单个节点的情况
算法步骤(迭代法)
-
初始化
prev = None,curr = head -
循环直到
curr为空:-
保存
next_node = curr.next -
反转指针:
curr.next = prev -
移动指针:
prev = curr,curr = next_node
-
-
返回
prev(新的头节点)
时间复杂度与空间复杂度
-
迭代法:
-
时间复杂度:O(n),每个节点访问一次
-
空间复杂度:O(1),只使用常数个指针
-
-
递归法:
-
时间复杂度:O(n),每个节点访问一次
-
空间复杂度:O(n),递归调用栈深度为 n
-
适用场景
-
需要反转链表顺序
-
链表操作的基础算法
-
许多其他链表问题的基础(如回文链表、分组反转等)
扩展思考
反转链表是链表操作中最基础也最重要的算法之一,掌握它可以帮助解决:
-
链表的部分反转
-
链表的回文判断
-
链表的成对或分组反转
-
链表与栈、队列的转换
无论是面试还是实际开发,反转链表都是必须掌握的基本功。通过理解指针操作的本质,可以更好地处理各种链表相关问题。