题目描述
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)感觉才是一个回溯状态
你把「回溯」和「递归调用」搞混了,这是初学者最常见的卡点。澄清两件事:
- 回溯不是某条 return 语句的功劳,而是「函数调用结束后自动发生」的事
只要一个函数 return 了------不管 return 的是 head、null、42、还是 reverseList(...) 的结果------当前这一帧就从调用栈弹出,控制权自动回到调用它的那一帧继续执行。这就是「回溯」。
所以 return head; 也会回溯,return newHead; 也会回溯,只要函数结束就一定回溯。
- return reverseList(head.next) 不是「回溯状态」,那是「继续往下递归」
如果代码改成:
java
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) return head;
return reverseList(head.next); // ← 只有这一行
}
它的意思是:别管当前的 head,把后面的结果直接透传上去。最后效果只是返回链表最后一个节点,根本没有反转任何指针。这不是回溯,是「把球往下传,传到底再原样传回来」。
- 用调用栈走一遍你就明白了
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 变量。
- 那为什么 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. 迭代法(双指针法)
迭代法是反转链表最直观的方法,使用两个指针 prev 和 curr 遍历链表:
python
def reverseList(head):
prev = None
curr = head
while curr:
# 保存下一个节点
next_node = curr.next
# 反转当前节点的指针
curr.next = prev
# 移动指针
prev = curr
curr = next_node
return prev
算法步骤:
- 初始化
prev = None,curr = head - 遍历链表,每次循环:
- 保存
curr.next到临时变量 - 将
curr.next指向prev - 移动
prev和curr指针
- 保存
- 当
curr为None时,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
递归过程:
- 递归到链表末尾,找到新的头节点
- 从后往前逐个反转节点指针
- 将当前节点的下一个节点的
next指向当前节点 - 断开当前节点的原始连接
时间复杂度: O(n),需要递归 n 次
空间复杂度: O(n),递归调用栈的深度为 n
关键细节与注意事项
边界情况处理
- 空链表 :输入
head为None时,直接返回None - 单节点链表:直接返回该节点本身
- 双节点链表:需要正确处理两个节点的指针反转
指针操作要点
- 在迭代法中,必须先保存
curr.next,再修改curr.next的指向 - 递归法中,要注意断开原始连接,避免形成环
- 两种方法都要确保不会丢失对后续节点的引用
实际应用场景
反转链表不仅是算法面试的常见题目,在实际开发中也有广泛应用:
- 浏览器历史记录:实现前进后退功能
- 撤销操作栈:某些编辑器中的撤销/重做功能
- 链表相关算法:如判断回文链表、链表排序等的基础操作
- 内存管理:某些内存分配算法中需要反转空闲块链表
扩展练习
掌握了基础的反转链表后,可以尝试以下变体题目:
- 反转链表 II:反转链表的一部分(指定区间)
- K 个一组反转链表:每 k 个节点进行反转
- 反转链表中的相邻节点:两两交换链表中的节点
- 回文链表:判断链表是否为回文结构
总结
反转链表是链表操作的基础,掌握迭代和递归两种方法对于理解链表数据结构至关重要。建议初学者先掌握迭代法,再理解递归法,最后能够灵活运用两种方法解决相关问题。
通过这道题目,可以深入理解:
- 链表的指针操作
- 递归与迭代的转换
- 边界条件的处理
- 算法的时间空间复杂度分析