LeetCode 148. 排序链表 ------ 解法一:自顶向下递归(分治 + 归并)
📌 题目链接
https://leetcode.cn/problems/sort-list/
💡 解题思路
要在链表上实现 O ( n log n ) O(n \log n) O(nlogn) 的时间复杂度,归并排序(Merge Sort)是最佳选择。本题采用的是 自顶向下(Top-Down) 的递归策略。
核心思想可以概括为"分而治之":
- 分(Divide):利用快慢指针找到链表的中点,将链表从中间断开,递归地对左右两个子链表进行排序。
- 治(Conquer):当子链表被拆分到只剩一个节点(或为空)时,它天然就是有序的,此时开始返回。
- 合(Merge):将两个排好序的子链表合并成一个新的有序链表。
🛠 关键前置知识
为了更好地理解本题解,你需要掌握以下两道经典题目:
-
- 作用:用于"分"。
- 核心技巧:快慢指针(Fast & Slow Pointers)。快指针一次走两步,慢指针一次走一步,当快指针到达终点时,慢指针正好位于中点。
- 关键点 :为了切断链表,本题解中额外引入了
pre指针记录慢指针的前驱,以便执行pre.next = null。
-
- 作用:用于"合"。
- 核心技巧:双指针遍历。创建一个虚拟头结点(Dummy Node),比较两个链表的头结点值大小,依次接入较小值,最后接上剩余的链表。
🧩 代码实现(Java)
java
/**
* Definition for singly-linked list.
*/
public class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
class Solution { // 解法一:自顶向下递归
public ListNode sortList(ListNode head) {
// 1. 递归终止条件:空节点或单节点天然有序
if (head == null || head.next == null) {
return head;
}
// 2. 找到中点,分割链表
ListNode head1 = middle(head); // head1 为右半部分的头结点
// 3. 递归排序左右两部分
head = sortList(head);
head1 = sortList(head1);
// 4. 合并两个有序链表
return merge(head, head1);
}
/**
* 876. 链表的中间结点(变体:要求断开链表)
* 使用快慢指针找到中点,并切断连接
*/
private ListNode middle(ListNode head) {
ListNode pre = head; // 记录慢指针的前一个节点
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
pre = slow;
slow = slow.next;
fast = fast.next.next;
}
// 断开链表:pre 是左半部分的尾,slow 是右半部分的头
pre.next = null;
return slow;
}
/**
* 21. 合并两个有序链表
* 标准的双指针合并逻辑
*/
private ListNode merge(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(); // 虚拟头结点
ListNode cur = dummy;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
cur.next = l1;
l1 = l1.next;
} else {
cur.next = l2;
l2 = l2.next;
}
cur = cur.next;
}
// 直接拼接剩余部分
cur.next = (l1 != null) ? l1 : l2;
return dummy.next;
}
}
⏱ 复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O ( n log n ) O(n \log n) O(nlogn) | 递归树的深度为 log n \log n logn,每一层都需要遍历 n n n 个节点进行合并。 |
| 空间复杂度 | O ( log n ) O(\log n) O(logn) | 主要由递归调用栈产生(递归深度为 log n \log n logn)。 |
✨ 总结
这种自顶向下 的解法逻辑非常清晰,完美复用了"找中点"和"合并"这两个基础操作。虽然递归栈带来了 O ( log n ) O(\log n) O(logn) 的空间开销,但在面试和日常解题中,这是最容易被理解和书写的版本。