1. LeetCode 143. Reorder List 题目解析
这道题目要求我们对一个单链表进行重新排序,具体规则是将链表从原来的顺序 L0 → L1 → ... → Ln-1 → Ln,重新排列为 L0 → Ln → L1 → Ln-1 → L2 → Ln-2 → ... 的形式。题目明确要求不能修改节点的值,只能改变节点之间的连接关系。
1.1 题目核心要求
- 输入输出要求:输入是一个单链表的头节点,输出是重新排序后的链表头节点(实际上是在原链表上直接修改)
- 操作限制:只能改变节点的next指针,不能修改节点的val值
- 时间复杂度:理想情况下应该达到O(n)时间复杂度和O(1)空间复杂度
1.2 解题思路分析
这道题的核心解法可以分为三个主要步骤:
- 找到链表的中点:将链表分成前后两半
- 反转后半部分链表:将后半部分链表进行反转
- 合并两个链表:将前半部分链表和反转后的后半部分链表交替合并
这种解法之所以有效,是因为它巧妙地利用了链表操作的基本技巧,通过分治和反转的结合,实现了题目要求的特殊排序方式。
2. 详细解题步骤与代码实现
2.1 步骤一:找到链表的中点
使用快慢指针法是找到链表中点的经典方法。快指针每次移动两步,慢指针每次移动一步,当快指针到达链表末尾时,慢指针正好位于中点。
java复制ListNode prev = null, slow = head, fast = head;
while (fast != null && fast.next != null) {
prev = slow;
slow = slow.next;
fast = fast.next.next;
}
prev.next = null; // 切断前后两半的连接
注意:这里prev指针的作用是记录慢指针的前一个节点,用于在中点处切断链表。如果不切断,后续操作会出现循环链表的问题。
2.2 步骤二:反转后半部分链表
反转链表是链表操作中的基础算法,这里我们使用迭代法实现:
java复制ListNode reverse(ListNode head) {
ListNode prev = null, curr = head, next = null;
while (curr != null) {
next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
反转链表的关键在于:
- 维护三个指针:prev、curr和next
- 每次迭代将curr.next指向prev
- 然后三个指针都向前移动一步
- 最后prev会成为新链表的头节点
2.3 步骤三:合并两个链表
合并两个链表时,我们需要交替取两个链表的节点进行连接:
java复制void merge(ListNode l1, ListNode l2) {
while (l1 != null) {
ListNode n1 = l1.next, n2 = l2.next;
l1.next = l2;
if (n1 == null)
break;
l2.next = n1;
l1 = n1;
l2 = n2;
}
}
合并时的注意事项:
- 需要提前保存两个链表的下一个节点
- 交替连接两个链表的当前节点
- 处理链表长度不等的情况(前半部分可能比后半部分多一个节点)
3. 完整代码实现与测试用例
3.1 完整解决方案代码
java复制public class Solution {
public void reorderList(ListNode head) {
if (head == null || head.next == null)
return;
// Step 1: Find the middle of the list
ListNode prev = null, slow = head, fast = head;
while (fast != null && fast.next != null) {
prev = slow;
slow = slow.next;
fast = fast.next.next;
}
prev.next = null; // Split the list into two halves
// Step 2: Reverse the second half
ListNode l2 = reverse(slow);
// Step 3: Merge the two halves
merge(head, l2);
}
private ListNode reverse(ListNode head) {
ListNode prev = null, curr = head, next = null;
while (curr != null) {
next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
private void merge(ListNode l1, ListNode l2) {
while (l1 != null) {
ListNode n1 = l1.next, n2 = l2.next;
l1.next = l2;
if (n1 == null)
break;
l2.next = n1;
l1 = n1;
l2 = n2;
}
}
}
3.2 测试用例设计
为了验证代码的正确性,应该设计多种测试用例:
-
基本测试用例:
- 输入:1->2->3->4
- 输出:1->4->2->3
-
奇数长度链表:
- 输入:1->2->3->4->5
- 输出:1->5->2->4->3
-
边界情况:
- 空链表:输入null,输出null
- 单节点链表:输入1,输出1
- 双节点链表:输入1->2,输出1->2
-
长链表测试:
- 输入:1->2->3->4->5->6->7->8
- 输出:1->8->2->7->3->6->4->5
4. 算法复杂度分析与优化
4.1 时间复杂度分析
- 找到中点:O(n) - 快指针遍历整个链表
- 反转后半部分:O(n/2) ≈ O(n)
- 合并两个链表:O(n/2) ≈ O(n)
总体时间复杂度为O(n),满足题目要求。
4.2 空间复杂度分析
算法只使用了固定数量的指针变量,没有使用额外的数据结构,因此空间复杂度为O(1)。
4.3 可能的优化方向
虽然这个解法已经相当高效,但还可以考虑以下优化:
- 减少指针变量:可以尝试复用一些指针变量,减少变量数量
- 递归实现:虽然递归的空间复杂度会变为O(n),但代码可能更简洁
- 并行处理:理论上找到中点和反转后半部分可以并行执行,但实际实现中意义不大
5. 常见错误与调试技巧
5.1 常见错误类型
-
无限循环:
- 原因:没有正确切断前后两半链表的连接
- 现象:程序陷入死循环
- 解决:确保在找到中点后执行prev.next = null
-
空指针异常:
- 原因:没有处理空链表或单节点链表的边界情况
- 现象:NullPointerException
- 解决:在函数开始处添加边界条件检查
-
合并顺序错误:
- 原因:合并时节点连接顺序错误
- 现象:输出结果不符合预期
- 解决:仔细检查merge函数中的指针操作
5.2 调试技巧
-
可视化调试:
- 在关键步骤后打印链表状态
- 例如在找到中点后、反转后、合并前打印两个链表
-
小规模测试:
- 先用2-4个节点的小链表测试
- 确认基本逻辑正确后再测试更复杂的情况
-
指针跟踪:
- 在IDE中调试时,重点关注各个指针的变化
- 特别是prev、slow、fast指针在找中点时的移动
6. 类似题目与扩展思考
6.1 LeetCode类似题目推荐
- 206. Reverse Linked List:基础的反转链表问题
- 234. Palindrome Linked List:判断链表是否为回文,也用到找中点和反转技巧
- 328. Odd Even Linked List:另一种链表重排问题
- 21. Merge Two Sorted Lists:合并两个链表的基础问题
6.2 扩展思考
-
双向链表版本:如果链表是双向链表,解法会有何不同?
- 答案:基本思路相同,但反转和合并操作可以更简单
-
环形链表检测:如果链表可能有环,如何修改解法?
- 答案:在找中点时需要先检测是否有环
-
K-way重排:如果不是两两交错,而是每K个节点插入一个反向节点,如何解决?
- 思路:将链表分成K段,反转偶数段,然后合并
在实际面试中,面试官可能会基于这道题目进行各种变体提问,因此深入理解这个问题的解法原理非常重要。掌握链表的基本操作技巧,如快慢指针、反转链表、合并链表等,是解决这类问题的关键。