一、题目核心解析
1. 题目要求
给定单链表的头节点head,将链表按升序排列并返回排序后的链表。
- 输入示例:
head = [4,2,1,3]→ 输出:[1,2,3,4] - 数据范围:链表节点数∈(0, 5×10⁴),节点值∈[-10⁵, 10⁵]
- 进阶挑战:在O (n log n) 时间 、O (1) 空间内完成排序
2. 算法选型分析
链表的非连续存储特性,直接排除了数组中高效的随机访问类排序。结合复杂度要求,算法选型如下:
表格
| 排序算法 | 时间复杂度 | 空间复杂度 | 适配性 |
|---|---|---|---|
| 冒泡 / 插入排序 | O(n²) | O(1) | ❌ 超时,无法处理大数据量 |
| 快速排序 | 平均 O (n log n) | O(log n) | ❌ 链表随机访问开销大,递归栈空间不满足进阶要求 |
| 堆排序 | O(n log n) | O(1) | ❌ 链表实现复杂,无数组下标优势 |
| 归并排序 | O(n log n) | O(log n)/O(1) | ✅ 最优解,完美适配链表分治与指针操作 |
归并排序的核心优势:分治思想天然适配链表,分割仅需快慢指针,合并仅调整指针无需额外空间,是唯一能同时满足时间与空间进阶要求的算法。
二、核心前置知识:链表操作基石
解决这道题前,需掌握两个核心基础操作,也是解题的 "工具包":
1. 快慢指针找中点(分割核心)
通过快慢指针将链表从中间拆分为左右两部分,为分治做准备。
- 原理:慢指针
slow每次走 1 步,快指针fast每次走 2 步,当fast到达链表尾时,slow指向中点。 - 关键细节:快指针初始指向
head->next,确保偶数长度链表时,slow指向前半段尾节点,便于精准分割。
2. 合并两个有序链表(合并核心)
将两个已升序排列的链表合并为一个新的升序链表,核心是双指针 + 虚拟头节点,无需创建新节点,仅调整指针指向。
- 虚拟头节点
dummy:简化边界处理,无需单独判断空链表,统一拼接逻辑。
三、解法一:自顶向下递归归并(清晰易上手)
1. 算法思路
遵循分治 "分割 - 治理 - 合并" 的核心逻辑,递归实现:
- 分割:用快慢指针找到链表中点,将链表拆分为左、右两部分;
- 治理:递归排序左、右两个子链表,直到子链表长度为 1(天然有序);
- 合并:将两个有序子链表合并为一个有序链表,回溯得到最终结果。
2. 完整 C++ 代码
cpp
运行
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
// 主函数:递归归并排序
ListNode* sortList(ListNode* head) {
// 递归终止条件:空链表或单节点链表(天然有序)
if (!head || !head->next) {
return head;
}
// 1. 快慢指针找中点,分割链表
ListNode* mid = findMid(head);
ListNode* rightHead = mid->next;
mid->next = nullptr; // 切断左、右子链表,避免循环引用
// 2. 递归排序左、右子链表
ListNode* leftSorted = sortList(head);
ListNode* rightSorted = sortList(rightHead);
// 3. 合并两个有序链表
return mergeTwoLists(leftSorted, rightSorted);
}
private:
// 快慢指针找链表中点(前半段尾节点)
ListNode* findMid(ListNode* head) {
ListNode* slow = head;
ListNode* fast = head->next; // 关键:偶数长度时指向中点前一位
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
// 合并两个有序链表(核心工具函数)
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
// 虚拟头节点:简化边界处理
ListNode dummy(0);
ListNode* cur = &dummy;
// 双指针遍历,按值拼接
while (l1 && l2) {
if (l1->val <= l2->val) {
cur->next = l1;
l1 = l1->next;
} else {
cur->next = l2;
l2 = l2->next;
}
cur = cur->next;
}
// 拼接剩余节点
cur->next = l1 ? l1 : l2;
return dummy.next;
}
};
3. 复杂度分析
- 时间复杂度:O (n log n)。分割递归深度为 O (log n),每层合并操作遍历 O (n) 个节点,总复杂度为 O (n log n)。
- 空间复杂度:O (log n)。递归调用栈的深度为链表长度的对数级,不满足进阶 O (1) 要求,但代码清晰易理解,适合入门与面试口述。
四、解法二:自底向上迭代归并(O (1) 空间最优解)
1. 算法思路
为满足进阶 O (1) 空间要求,用迭代替代递归,核心是 "从小到大逐步合并有序子链表":
- 初始化 :计算链表长度,定义子链表长度
subLen = 1(初始仅单节点有序); - 迭代合并 :按
subLen切分链表为多个有序子链表,两两合并,每次合并后subLen翻倍(1→2→4→...); - 终止条件 :当
subLen >= 链表长度时,所有子链表合并完成,得到最终有序链表。
2. 完整 C++ 代码
cpp
运行
class Solution {
public:
ListNode* sortList(ListNode* head) {
if (!head || !head->next) return head;
// 1. 计算链表长度
int len = 0;
ListNode* cur = head;
while (cur) {
len++;
cur = cur->next;
}
// 2. 初始化虚拟头节点(统一处理头节点变化)
ListNode dummy(0);
dummy.next = head;
ListNode* prev = &dummy; // 记录已合并部分的尾节点
ListNode* curr = dummy.next; // 待处理的链表头
// 3. 自底向上迭代合并
for (int subLen = 1; subLen < len; subLen *= 2) {
prev = &dummy;
curr = dummy.next;
while (curr) {
// 切分第一个长度为subLen的子链表
ListNode* l1 = curr;
ListNode* l2 = cut(l1, subLen); // 切分后返回第二个子链表头
// 切分第二个长度为subLen的子链表,curr指向剩余链表头
curr = cut(l2, subLen);
// 合并两个有序子链表,拼接至已合并部分尾部
prev->next = mergeTwoLists(l1, l2);
// 移动prev到合并后链表的尾部
while (prev->next) {
prev = prev->next;
}
}
}
return dummy.next;
}
private:
// 核心工具:切分链表,返回剩余链表头,同时截断前n个节点
ListNode* cut(ListNode* head, int n) {
ListNode* p = head;
// 移动n-1步,到达第n个节点
while (--n > 0 && p) {
p = p->next;
}
if (!p) return nullptr; // 不足n个节点,返回空
ListNode* nextHead = p->next;
p->next = nullptr; // 截断前n个节点
return nextHead;
}
// 合并两个有序链表(与递归解法通用)
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode dummy(0);
ListNode* cur = &dummy;
while (l1 && l2) {
if (l1->val <= l2->val) {
cur->next = l1;
l1 = l1->next;
} else {
cur->next = l2;
l2 = l2->next;
}
cur = cur->next;
}
cur->next = l1 ? l1 : l2;
return dummy.next;
}
};
3. 复杂度分析
- 时间复杂度:O (n log n)。外层循环控制子链表长度翻倍(O (log n) 次),内层循环每次遍历整个链表(O (n)),总复杂度 O (n log n)。
- 空间复杂度:O (1)。仅使用常数个指针变量(
dummy/prev/curr等),无递归栈开销,完美满足进阶要求,是面试最优解。
五、解题避坑与关键细节
- 截断链表必须置空 :分割子链表时,务必将截断位置的
next置为nullptr,否则会形成循环引用,导致递归 / 迭代死循环。 - 快慢指针初始位置 :找中点时,快指针初始指向
head->next而非head,否则偶数长度链表会分割不均(如4→2→1→3会分割为4→2和1→3,若快指针初始为head则分割为4和2→1→3)。 - 虚拟头节点的必要性:合并链表时使用虚拟头节点,可避免单独处理空链表、头节点值较小等边界情况,统一拼接逻辑。
- 迭代法的 subLen 翻倍:子链表长度必须按 2 的幂次递增(1→2→4→...),否则无法保证所有子链表有序,最终合并结果错误。
六、总结与拓展
LeetCode 148「排序链表」的核心是归并排序在链表上的适配,两种解法各有侧重:
- 递归归并:代码逻辑清晰,契合分治思想,适合理解算法本质,面试中可快速口述思路;
- 迭代归并:空间复杂度最优,是进阶要求的标准解法,适合追求极致性能的实战场景。