1. 问题背景与需求分析
链表重排是数据结构中的经典问题,LeetCode第143题要求将给定单链表重新排列为L0→Ln→L1→Ln-1→L2→Ln-2→...的形式。这个问题考察了对链表操作的熟练掌握程度,也是面试中常见的算法题。
在实际开发中,类似的重排操作可能出现在需要交替处理数据流的场景,比如音频采样处理、日志交替存储等。理解这个问题的解法不仅能帮助我们通过算法面试,更能培养处理复杂链表操作的能力。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解题思路分析
2.1 暴力解法与复杂度分析
最直观的解法是使用数组存储所有节点,然后按照题目要求的顺序重新连接链表。这种方法时间复杂度O(n),空间复杂度O(n),虽然能通过测试,但不符合题目要求的O(1)额外空间限制。
java复制// 暴力解法示例
public void reorderList(ListNode head) {
List<ListNode> nodes = new ArrayList<>();
ListNode curr = head;
while (curr != null) {
nodes.add(curr);
curr = curr.next;
}
int i = 0, j = nodes.size() - 1;
while (i < j) {
nodes.get(i).next = nodes.get(j);
i++;
if (i == j) break;
nodes.get(j).next = nodes.get(i);
j--;
}
nodes.get(i).next = null;
}
2.2 最优解法思路
更优的解法包含三个关键步骤:
- 找到链表中点(快慢指针法)
- 反转后半部分链表
- 合并两个链表
这种方法只需要O(1)的额外空间,时间复杂度仍然是O(n),完全符合题目要求。
3. 详细实现步骤
3.1 寻找链表中点
使用快慢指针技巧,快指针每次走两步,慢指针每次走一步。当快指针到达末尾时,慢指针正好在中点。
java复制private ListNode findMiddle(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
注意:对于偶数长度链表,这个实现会返回中间偏右的节点。如果需要中间偏左的节点,可以调整循环条件。
3.2 反转后半部分链表
使用经典的链表反转方法,维护prev、curr和next三个指针。
java复制private ListNode reverseList(ListNode head) {
ListNode prev = null, curr = head;
while (curr != null) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
3.3 合并两个链表
将前半部分和反转后的后半部分交替合并。
java复制private void mergeLists(ListNode first, ListNode second) {
while (second != null) {
ListNode firstNext = first.next;
ListNode secondNext = second.next;
first.next = second;
second.next = firstNext;
first = firstNext;
second = secondNext;
}
}
4. 完整代码实现
将上述步骤组合起来,得到完整的解决方案:
java复制public void reorderList(ListNode head) {
if (head == null || head.next == null) return;
// 1. 找到中点
ListNode mid = findMiddle(head);
// 2. 反转后半部分
ListNode secondHalf = reverseList(mid.next);
mid.next = null; // 切断前后两部分
// 3. 合并两个链表
mergeLists(head, secondHalf);
}
// 前面定义的工具方法...
5. 边界条件与测试用例
5.1 常见边界情况
- 空链表:直接返回
- 单节点链表:直接返回
- 双节点链表:保持不变
- 奇数长度链表
- 偶数长度链表
5.2 测试用例示例
java复制@Test
public void testReorderList() {
Solution solution = new Solution();
// 测试空链表
ListNode empty = null;
solution.reorderList(empty);
assertNull(empty);
// 测试单节点链表
ListNode single = new ListNode(1);
solution.reorderList(single);
assertEquals(1, single.val);
assertNull(single.next);
// 测试双节点链表
ListNode twoNodes = new ListNode(1, new ListNode(2));
solution.reorderList(twoNodes);
assertEquals(1, twoNodes.val);
assertEquals(2, twoNodes.next.val);
assertNull(twoNodes.next.next);
// 测试奇数长度链表
ListNode odd = new ListNode(1, new ListNode(2, new ListNode(3)));
solution.reorderList(odd);
assertEquals(1, odd.val);
assertEquals(3, odd.next.val);
assertEquals(2, odd.next.next.val);
assertNull(odd.next.next.next);
// 测试偶数长度链表
ListNode even = new ListNode(1, new ListNode(2, new ListNode(3, new ListNode(4))));
solution.reorderList(even);
assertEquals(1, even.val);
assertEquals(4, even.next.val);
assertEquals(2, even.next.next.val);
assertEquals(3, even.next.next.next.val);
assertNull(even.next.next.next.next);
}
6. 常见问题与调试技巧
6.1 常见错误
- 忘记切断中间节点的next指针,导致链表成环
- 反转链表时指针操作顺序错误
- 合并链表时没有正确处理最后一个节点
- 没有处理空链表或单节点链表的特殊情况
6.2 调试建议
- 打印链表辅助调试:
java复制void printList(ListNode head) {
while (head != null) {
System.out.print(head.val + " ");
head = head.next;
}
System.out.println();
}
-
使用小规模测试用例逐步验证每个步骤:
- 先验证findMiddle是否正确
- 再单独测试reverseList
- 最后测试mergeLists
-
对于循环链表问题,可以限制最大打印节点数防止无限循环:
java复制void printList(ListNode head, int maxNodes) {
int count = 0;
while (head != null && count < maxNodes) {
System.out.print(head.val + " ");
head = head.next;
count++;
}
System.out.println();
}
7. 性能优化与变种问题
7.1 性能考虑
虽然我们的解法已经是O(n)时间复杂度和O(1)空间复杂度,但在实际面试中还可以讨论:
- 链表很长时的处理(内存分页、缓存命中率)
- 多线程环境下的安全性考虑
- 是否可以并行处理(如并行反转后半部分)
7.2 相关问题变种
- 重排链表为L0→L1→Ln→L2→Ln-1→...顺序
- 重排双向链表
- 重排链表并保持相对顺序稳定(稳定排序)
- 重排环形链表
8. 实际应用场景
虽然这是一个算法题,但类似的链表操作技术在实际开发中有广泛应用:
- 内存池管理中的块重组
- 文件系统中的块链重组
- 游戏开发中的对象链表优化
- 网络数据包的重组与排序
理解链表的基本操作和常见算法模式,能够帮助我们在面对复杂数据结构问题时快速找到解决方案。
