链表反转是数据结构与算法中的经典问题,也是面试中的高频考点。这个问题看似简单,却能考察程序员对指针操作、递归思维和边界条件的处理能力。在本文中,我们将深入探讨链表反转的两种经典解法:迭代法和递归法,并提供详细的代码实现和优化技巧。
问题描述
给定一个单链表的头节点 head,要求返回反转后的链表。
示例:
text
输入: 1 → 2 → 3 → 4 → 5 → null
输出: 5 → 4 → 3 → 2 → 1 → null
方法一:迭代法(推荐)
核心思想
通过两个指针 pre 和 cur 遍历链表,在遍历过程中逐个反转节点间的指针方向。
算法步骤
-
初始化两个指针:
pre = null,cur = head -
遍历链表,直到
cur为 null:-
保存
cur的下一个节点:temp = cur.next -
反转指针:
cur.next = pre -
移动指针:
pre = cur,cur = temp
-
-
返回
pre(新的头节点)
代码实现
java
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while (cur != null) {
// 保存下一个节点,防止断链
ListNode temp = cur.next;
// 反转指针
cur.next = pre;
// 移动指针
pre = cur;
cur = temp;
}
return pre;
}
图解过程
text
初始状态:
pre = null, cur = 1 → 2 → 3 → 4 → null
第1次循环:
temp = 2
1.next = null
pre = 1, cur = 2
结果:null ← 1 2 → 3 → 4 → null
第2次循环:
temp = 3
2.next = 1
pre = 2, cur = 3
结果:null ← 1 ← 2 3 → 4 → null
第3次循环:
temp = 4
3.next = 2
pre = 3, cur = 4
结果:null ← 1 ← 2 ← 3 4 → null
第4次循环:
temp = null
4.next = 3
pre = 4, cur = null
结果:null ← 1 ← 2 ← 3 ← 4
结束循环,返回pre=4
复杂度分析
-
时间复杂度:O(n),每个节点只遍历一次
-
空间复杂度:O(1),只使用了固定的几个指针变量
方法二:递归法
核心思想
利用递归的栈结构,从链表的尾部开始反转,先反转后面的部分,再处理当前节点。
算法步骤
-
递归终止条件:链表为空或只有一个节点
-
递归反转以
head.next为头节点的子链表 -
将当前节点连接到已反转的子链表的尾部
-
返回新的头节点
代码实现
java
public ListNode reverseList(ListNode head) {
// 递归终止条件
if (head == null || head.next == null) {
return head;
}
// 递归反转后续链表
ListNode newHead = reverseList(head.next);
// 将当前节点连接到反转后的链表尾部
head.next.next = head;
head.next = null;
return newHead;
}
图解递归过程
text
初始链表:1 → 2 → 3 → 4 → null
递归调用栈:
reverse(1)调用reverse(2)
reverse(2)调用reverse(3)
reverse(3)调用reverse(4)
reverse(4)返回4(base case)
开始回溯:
reverse(3)中:3.next.next = 3,即4.next = 3
3.next = null
返回newHead=4(链表:4 → 3 → null)
reverse(2)中:2.next.next = 2,即3.next = 2
2.next = null
返回newHead=4(链表:4 → 3 → 2 → null)
reverse(1)中:1.next.next = 1,即2.next = 1
1.next = null
返回newHead=4(链表:4 → 3 → 2 → 1 → null)
复杂度分析
-
时间复杂度:O(n),每个节点处理一次
-
空间复杂度:O(n),递归调用栈的深度为n
两种方法的对比
| 特性 | 迭代法 | 递归法 |
|---|---|---|
| 时间复杂度 | O(n) | O(n) |
| 空间复杂度 | O(1) | O(n)(递归栈) |
| 代码简洁性 | 中等 | 简洁优雅 |
| 理解难度 | 较易 | 较难(需要理解递归) |
| 适用场景 | 链表较长时 | 链表较短或需要简洁实现时 |
常见错误与陷阱
错误1:指针丢失
java
// 错误的写法
while (cur != null) {
cur.next = pre; // 直接修改cur.next,丢失了下一个节点
pre = cur;
cur = cur.next; // cur.next已经是pre,导致循环异常
}
解决方法:在修改指针前,先用临时变量保存下一个节点。
错误2:空指针异常
java
// 错误的写法
ListNode temp = cur.next; // 如果cur为null,这里会抛异常
while (cur != null) {
// ...
}
解决方法:确保在访问节点属性前进行空值检查。
错误3:忘记处理原头节点的next指针
在递归法中,如果不将原头节点的next设为null,会导致链表成环。
变体问题
1. 反转链表的一部分(比如反转中间到最后,那要先找到中间位置:快慢指针)
反转链表中从位置m到n的部分。
解题思路:
-
找到第
m-1个节点(连接点) -
反转
m到n的节点 -
重新连接三部分
2. K个一组反转链表
每k个节点一组进行反转,最后不足k个的保持原样。
解题思路:
-
统计链表长度
-
按组进行反转
-
递归或迭代处理剩余部分
3. 反转相邻节点
交换相邻的两个节点。
解题思路 :
使用虚拟头节点,每次处理两个节点。
面试技巧
-
先画图:在面试中,先用简单的例子画图说明思路
-
考虑边界:空链表、单节点链表、双节点链表
-
测试用例:
-
null(空链表) -
1 → null(单节点) -
1 → 2 → null(双节点) -
1 → 2 → 3 → 4 → 5 → null(多节点)
-
-
复杂度分析:主动分析时间和空间复杂度
-
代码优化:写出代码后,思考是否有优化空间
实际应用场景
-
浏览器历史记录:前进后退功能需要类似的反转操作
-
撤销操作:文本编辑器中的撤销功能
-
内存管理:某些垃圾回收算法中
-
数据同步:双向同步的数据结构
扩展学习
-
双向链表反转 :需要同时处理
prev和next指针 -
环形链表反转:需要特别注意环的断开与连接
-
原地反转数组:类似的思想可以应用于数组
总结
链表反转是掌握指针操作的基础,通过这个问题可以深入理解:
-
指针的移动与修改
-
递归思维的应用
-
边界条件的处理
-
时间与空间复杂度的权衡
建议初学者先从迭代法开始理解,掌握指针的操作技巧,再尝试理解递归法的精妙之处。在实际工程中,迭代法通常更受欢迎,因为它没有递归的栈溢出风险,且空间效率更高。
记住:理解一个算法的最好方式是多画图、多写代码、多思考边界情况。希望这篇文章能帮助你彻底掌握链表反转问题!