1. 问题背景与需求分析
链表重排是数据结构中的经典问题,LeetCode第143题要求将给定单链表L0→L1→...→Ln-1→Ln重新排列为L0→Ln→L1→Ln-1→L2→Ln-2→...的形式。这种操作在实际开发中常用于优化数据访问模式,例如:
- 音乐播放器的"随机+顺序"混合播放模式
- 数据库查询结果的交替分页展示
- 游戏场景中动态加载资源的顺序优化
关键提示:链表操作必须注意节点引用变更顺序,否则会导致指针丢失或循环引用。我在实际面试中遇到过候选人因忽略这一点导致整个链表断裂的情况。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解决方案设计与复杂度分析
2.1 三步分解法(最优解)
java复制public void reorderList(ListNode head) {
if (head == null || head.next == null) return;
// 1. 找中点
ListNode slow = head, fast = head;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 2. 反转后半部分
ListNode prev = null, curr = slow.next;
slow.next = null; // 切断前后两部分
while (curr != null) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
// 3. 合并两个链表
ListNode first = head, second = prev;
while (second != null) {
ListNode tmp1 = first.next, tmp2 = second.next;
first.next = second;
second.next = tmp1;
first = tmp1;
second = tmp2;
}
}
时间复杂度分析:
- 找中点:O(n/2)
- 反转链表:O(n/2)
- 合并链表:O(n/2)
总时间复杂度:O(n),空间复杂度O(1)
2.2 线性表辅助法(新手友好)
java复制public void reorderList(ListNode head) {
List<ListNode> list = new ArrayList<>();
ListNode curr = head;
while (curr != null) {
list.add(curr);
curr = curr.next;
}
int i = 0, j = list.size() - 1;
while (i < j) {
list.get(i).next = list.get(j);
i++;
if (i == j) break;
list.get(j).next = list.get(i);
j--;
}
list.get(i).next = null;
}
时间复杂度:O(n),空间复杂度O(n)
3. 关键技术与实现细节
3.1 快慢指针找中点
快指针每次走两步,慢指针每次走一步。当快指针到达末尾时,慢指针正好在中点。这里有两个易错点:
- 循环条件应该是
fast.next != null && fast.next.next != null而非fast != null && fast.next != null,否则对于偶数长度链表会找到偏右的中点 - 找到中点后需要执行
slow.next = null切断前后两部分,否则会产生循环链表
3.2 链表反转的四种写法对比
- 迭代法(推荐):
java复制ListNode reverse(ListNode head) {
ListNode prev = null;
while (head != null) {
ListNode next = head.next;
head.next = prev;
prev = head;
head = next;
}
return prev;
}
- 递归法(栈空间O(n)):
java复制ListNode reverse(ListNode head) {
if (head == null || head.next == null) return head;
ListNode newHead = reverse(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
- 头插法:
java复制ListNode reverse(ListNode head) {
ListNode dummy = new ListNode(-1);
while (head != null) {
ListNode next = head.next;
head.next = dummy.next;
dummy.next = head;
head = next;
}
return dummy.next;
}
- 利用栈结构(空间O(n)):
java复制ListNode reverse(ListNode head) {
Deque<ListNode> stack = new ArrayDeque<>();
while (head != null) {
stack.push(head);
head = head.next;
}
ListNode dummy = new ListNode(-1);
ListNode curr = dummy;
while (!stack.isEmpty()) {
curr.next = stack.pop();
curr = curr.next;
}
curr.next = null;
return dummy.next;
}
实测发现:迭代法在LeetCode上运行时间最短(0ms),递归法由于函数调用开销通常多出1ms
4. 边界条件与异常处理
4.1 特殊输入情况
- 空链表:直接返回
- 单节点链表:直接返回
- 双节点链表:无需处理(已经是正确顺序)
- 超长链表(测试用例可达5*10^4个节点):必须保证O(n)时间复杂度
4.2 内存管理要点
- Java的垃圾回收机制虽然会自动处理未被引用的节点,但良好的实践应该:
- 在切断节点引用前保存必要的临时引用
- 避免在循环中创建不必要的临时对象
- 对于大型链表,考虑使用对象池复用节点
5. 测试用例设计
完整测试应包含以下场景:
java复制@Test
public void testReorderList() {
// 空链表
ListNode head1 = null;
reorderList(head1);
assertNull(head1);
// 单节点
ListNode head2 = new ListNode(1);
reorderList(head2);
assertEquals("1", printList(head2));
// 双节点
ListNode head3 = buildList(new int[]{1,2});
reorderList(head3);
assertEquals("1->2", printList(head3));
// 奇数长度
ListNode head4 = buildList(new int[]{1,2,3,4,5});
reorderList(head4);
assertEquals("1->5->2->4->3", printList(head4));
// 偶数长度
ListNode head5 = buildList(new int[]{1,2,3,4,5,6});
reorderList(head5);
assertEquals("1->6->2->5->3->4", printList(head5));
// 超长链表(性能测试)
ListNode head6 = buildLargeList(50000);
long start = System.currentTimeMillis();
reorderList(head6);
long duration = System.currentTimeMillis() - start;
assertTrue(duration < 100); // 应在100ms内完成
}
6. 同类问题扩展
6.1 变种问题
- 重排双向链表:需要额外处理prev指针
- 环形链表重排:先检测环,再断开环处理
- 多级链表扁平化:类似DFS与BFS的结合
6.2 相关LeetCode题目
- 206.反转链表
- 234.回文链表
- 328.奇偶链表
- 21.合并两个有序链表
- 148.排序链表
7. 工程实践中的优化技巧
- 内存局部性优化:对于频繁操作的链表,可以改用静态链表(数组模拟)提升缓存命中率
java复制class StaticListNode {
int val;
int next; // 数组下标
public StaticListNode(int val, int next) {
this.val = val;
this.next = next;
}
}
- 多线程安全版本:如果需要线程安全,可以使用AtomicReference包装节点引用
java复制class ConcurrentListNode {
int val;
AtomicReference<ConcurrentListNode> next;
// 使用CAS操作更新指针
boolean atomicNextUpdate(ConcurrentListNode expect, ConcurrentListNode update) {
return next.compareAndSet(expect, update);
}
}
- 可视化调试技巧:在开发过程中可以添加链表打印方法
java复制String printList(ListNode head) {
StringBuilder sb = new StringBuilder();
Set<ListNode> visited = new HashSet<>(); // 检测环
while (head != null) {
if (visited.contains(head)) {
sb.append("(loop)");
break;
}
visited.add(head);
sb.append(head.val);
if (head.next != null) sb.append("->");
head = head.next;
}
return sb.toString();
}
8. 面试考察点分析
这道题在技术面试中通常考察:
- 对指针操作的熟练程度(60%)
- 边界条件处理能力(20%)
- 时间/空间复杂度分析能力(15%)
- 代码整洁度(5%)
常见follow-up问题:
- 如何检测链表是否有环?
- 如果不允许修改原链表怎么做?
- 如何优化内存访问模式?
- 如果链表特别长但内存有限怎么办?
我在实际面试中遇到过候选人写出完美算法但忽略slow.next = null导致死循环的情况。这提醒我们:写完代码后一定要用简单的测试用例(如3个节点)手动走查指针变化。
