一、题目描述
给定链表的头结点 head,请将其按升序排列并返回排序后的链表。
示例 1:
输入:head = [4,2,1,3]
输出:[1,2,3,4]
示例 2:
输入:head = [-1,5,3,4,0]
输出:[-1,0,3,4,5]
示例 3:
输入:head = []
输出:[]
二、解题思路总览
核心思想:归并排序(自顶向下)
| 步骤 | 说明 |
|---|---|
| 分割 | 用快慢指针找到链表中点,将链表分成两半 |
| 递归 | 对左右两半分别递归排序 |
| 合并 | 将两个有序链表合并成一个有序链表 |
时间复杂度:O(n log n)
空间复杂度:O(log n)(递归栈)
三、完整代码
cpp
class Solution {
public:
// 1. 找到链表的中间节点,并断开
ListNode* middleNode(ListNode* head) {
ListNode* pre = head;
ListNode* fast = head;
ListNode* slow = head;
while (fast && fast->next) {
pre = slow;
slow = slow->next;
fast = fast->next->next;
}
pre->next = NULL; // 从中间断开
return slow; // 返回后半段的头节点
}
// 2. 合并两个有序链表
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode* dummy = new ListNode(0);
ListNode* cur = dummy;
while (list1 && list2) {
if (list1->val > list2->val) {
cur->next = list2;
list2 = list2->next;
} else {
cur->next = list1;
list1 = list1->next;
}
cur = cur->next;
}
cur->next = list1 ? list1 : list2;
return dummy->next;
}
// 3. 归并排序主函数
ListNode* sortList(ListNode* head) {
if (head == NULL || head->next == NULL) return head;
// 找到中点,分割成两段
ListNode* head2 = middleNode(head);
// 递归排序左右两段
head = sortList(head);
head2 = sortList(head2);
// 合并两个有序链表
return mergeTwoLists(head, head2);
}
};
四、算法流程图
4.1 sortList 主函数流程
输入:链表头节点 head
[Step 1] 判断边界
|
v
head == NULL 或 head->next == NULL ?
|是 |否
v v
返回 head [Step 2] 继续
| |
v v
【返回】 [Step 2] 找中点
|
v
head2 = middleNode(head)
|
v
链表被分成两段:
前半段 head,后半段 head2
|
v
【递归排序前半段】
|
v
head = sortList(head)
|
v
【递归排序后半段】
|
v
head2 = sortList(head2)
|
v
【合并两段有序链表】
|
v
return mergeTwoLists(head, head2)
|
v
【返回】
4.2 middleNode 找中点流程
输入:链表头节点 head
[Step 1] 初始化指针
pre = head, fast = head, slow = head
|
v
[Step 2] 快慢指针循环
|
v
fast && fast->next 成立?
|否 |是
v v
【返回 slow】 [Step 3] 移动指针
| pre = slow
slow = slow->next
fast = fast->next->next
| |
v v
回到 Step 2
4.3 mergeTwoLists 合并流程
输入:两个有序链表 list1, list2
[Step 1] 创建哑节点
dummy = new Node(0)
cur = dummy
|
v
[Step 2] 比较接入循环
|
v
list1 && list2 成立?
|否 |是
v v
[Step 4] [Step 3] 比较大小
| |
v v
list1->val > list2->val ?
|是 |否
v v
cur->next = list2 cur->next = list1
list2 = list2->next list1 = list1->next
| |
v v
cur = cur->next
|
v
回到 Step 2
[Step 4] 处理剩余节点
|
v
cur->next = list1 ? list1 : list2
|
v
return dummy->next
|
v
【返回合并后的链表】
4.4 整体递归展开流程
sortList([4,2,1,3])
|
+-- middleNode --> 分割为 [4,2] 和 [1,3]
|
+-- sortList([4,2])
| |
| +-- middleNode --> 分割为 [4] 和 [2]
| |
| +-- sortList([4]) --> 返回 [4]
| |
| +-- sortList([2]) --> 返回 [2]
| |
| +-- merge([4], [2]) --> [2,4]
| |
| 返回 [2,4]
|
+-- sortList([1,3])
| |
| +-- middleNode --> 分割为 [1] 和 [3]
| |
| +-- sortList([1]) --> 返回 [1]
| |
| +-- sortList([3]) --> 返回 [3]
| |
| +-- merge([1], [3]) --> [1,3]
| |
| 返回 [1,3]
|
+-- merge([2,4], [1,3]) --> [1,2,3,4]
|
返回 [1,2,3,4]
五、逐行解析
5.1 middleNode:找中点并分割
原理:
- slow 每次走一步
- fast 每次走两步
- 当 fast 到达末尾时,slow 正好在中间
循环条件: while (fast && fast->next)
| 变量 | 作用 |
|---|---|
| slow | 记录中点位置 |
| fast | 快指针,用来判断是否到末尾 |
| pre | 记录 slow 的前一个节点,用于断开 |
关键: pre->next = NULL 将链表从中点断开,分成两段。
举例:
链表: 1 -> 2 -> 3 -> 4 -> 5 -> NULL
slow 走到: 3
pre 走到: 2
pre->next = NULL
前半段: 1 -> 2 -> NULL
后半段: 3 -> 4 -> 5 -> NULL
5.2 mergeTwoLists:合并两个有序链表
原理:
- 创建 dummy 哑节点,简化边界处理
- 用 cur 遍历,比较两个链表的当前节点
- 小的节点接入新链表
- 最后把剩余的部分直接接上去
循环逻辑:
while (list1 && list2):
if list1->val > list2->val:
cur->next = list2
list2 = list2->next
else:
cur->next = list1
list1 = list1->next
cur = cur->next
收尾: cur->next = list1 ? list1 : list2
如果 list1 还有剩余,就接 list1;否则接 list2(两者必居其一)
5.3 sortList:归并排序主逻辑
递归终止条件:
if (head == NULL || head->next == NULL) return head
空链表或单节点,直接返回(已经有序)
六、复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n log n) | 递归树高度 log n,每层共 O(n) |
| 空间复杂度 | O(log n) | 递归栈深度 log n |
时间复杂度推导:
递归层数 = log n(每次对半分割)
每层总工作量 = O(n)(遍历所有节点)
总时间 = O(n log n)
七、面试追问
| 问题 | 回答要点 |
|---|---|
| 归并排序的时间复杂度是多少? | O(n log n),每层分割都要遍历所有节点,递归深度 log n |
| 为什么用快慢指针找中点? | fast 速度是 slow 的 2 倍,fast 到末尾时 slow 正好在中点 |
| pre->next = NULL 为什么要断开? | 不断开的话,左右两段仍然是连在一起的,无法独立排序 |
| 合并时为什么要用 dummy 哑节点? | 省去对头节点的特殊判断,统一用 cur->next 接入 |
| 递归栈会爆吗? | n 不超过 10^5,递归深度 log n 不超过 17,完全安全 |
| 能用迭代实现吗? | 可以用自底向上的归并排序,但代码更复杂 |
| 链表排序和数组排序有什么区别? | 数组可以随机访问,可以用原地分割;链表必须靠指针遍历,分割需要找中点 |
八、相关题目
| 题号 | 题目 | 关键点 |
|---|---|---|
| 21 | 合并两个有序链表 | mergeTwoLists 基础版 |
| 23 | 合并 K 个有序链表 | 堆优化或分治 |
| 148 | 排序链表 | 本题 |
| 143 | 重排链表 | 先找中点再合并 |
| 234 | 回文链表 | 快慢指针找中点 |
九、总结
| 要点 | 内容 |
|---|---|
| 核心算法 | 归并排序(自顶向下) |
| 找中点 | 快慢指针,pre->next = NULL 断开 |
| 合并 | 双指针遍历,dummy 哑节点简化 |
| 复杂度 | 时间 O(n log n),空间 O(log n) |
| 记忆口诀 | 分割中点、递归排序、合并链表三步走 |