LeetCode 148. 排序链表 ------ 解法二:自底向上归并(迭代,O(1) 空间)
📌 题目链接
解法一,自顶向下
💡 解题思路
本题要求在 O(n log n) 时间内排序链表,且尽可能少用额外空间。
解法一使用了递归,空间复杂度为 O(log n) 。
本解法采用 自底向上(Bottom-Up)归并排序 ,完全消除递归栈,实现 O(1) 额外空间。
核心思想
不递归拆链,而是按固定步长合并子链表。
- 先统计链表长度
len - 从
step = 1开始:- 每次合并长度为
step的两个子链表 - 合并完成后,
step *= 2
- 每次合并长度为
- 当
step >= len时,链表已完全有序
🛠 关键技术点
1️⃣ 获取链表长度
java
int getLen(ListNode node)
- 用于确定归并的上界
- 时间复杂度 O(n)
2️⃣ 按长度切割链表
java
ListNode splitList(ListNode head, int size)
功能:
- 从
head向后走size - 1步 - 将链表切断
- 返回下一段的头结点
📌 若剩余不足 size,直接返回 null
3️⃣ 合并两个有序链表(返回头 + 尾)
java
ListNode[] merge(ListNode l1, ListNode l2)
不同于传统只返回头结点,这里返回:
text
[合并后链表的头, 合并后链表的尾]
原因:
- 自底向上需要不断接在 newListTail 后面
- 知道尾指针可以避免再次遍历
🧩 完整代码(Java)
java
class Solution { // 解法二:自底向上归并排序(迭代)
public ListNode sortList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
int len = getLen(head);
ListNode dummy = new ListNode(0, head);
// step: 当前每个子链表的长度
for (int step = 1; step < len; step *= 2) {
ListNode newListTail = dummy;
ListNode cur = dummy.next;
while (cur != null) {
ListNode head1 = cur;
ListNode head2 = splitList(head1, step);
cur = splitList(head2, step);
ListNode[] merged = merge(head1, head2);
newListTail.next = merged[0];
newListTail = merged[1];
}
}
return dummy.next;
}
// 获取链表长度
int getLen(ListNode node) {
int cnt = 0;
while (node != null) {
cnt++;
node = node.next;
}
return cnt;
}
// 按长度切分链表
ListNode splitList(ListNode head, int size) {
if (head == null) return null;
ListNode cur = head;
for (int i = 0; i < size - 1 && cur != null; i++) {
cur = cur.next;
}
if (cur == null || cur.next == null) {
return null;
}
ListNode nextHead = cur.next;
cur.next = null;
return nextHead;
}
// 合并两个有序链表,返回 [头, 尾]
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;
while (cur.next != null) {
cur = cur.next;
}
return new ListNode[]{dummy.next, cur};
}
}
⏱ 复杂度分析
| 指标 | 复杂度 |
|---|---|
| 时间复杂度 | O(n log n) |
| 空间复杂度 | O(1)(无递归栈) |
🔍 解法一 vs 解法二 对比总结
| 维度 | 解法一:自顶向下递归 | 解法二:自底向上迭代 |
|---|---|---|
| 思想 | 递归拆链 | 固定步长合并 |
| 是否递归 | ✅ 是 | ❌ 否 |
| 空间复杂度 | O(log n) | ✅ O(1) |
| 实现难度 | ⭐⭐⭐(易理解) | ⭐⭐⭐⭐(细节多) |
| 切链方式 | 快慢指针 | 按长度切割 |
| 合并方式 | 返回头结点 | 返回头 + 尾 |
| 面试推荐 | ✅ 高频 | ✅ 进阶 / 面试官追问 |
✅ 总结
- 解法一适合:快速写出、逻辑清晰、面试首选
- 解法二 适合:
- 要求 O(1) 空间
- 考察对链表操作的熟练度
- 进阶 / 高级面试
一句话总结:
递归写法优雅,迭代写法极致。