LeetCode 148. 排序链表
问题描述
给定链表的头节点 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) 时间复杂度和常数级空间复杂度下完成排序。
算法思路(归并排序)
采用归并排序解决链表排序问题,分为三个关键步骤:
- 分割链表:使用
快慢指针找到链表中间节点,将链表分成两半 - 递归排序:对分割后的两个子链表递归排序
- 合并有序链表:合并两个已排序的子链表
自顶向下归并排序
/**
* 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) {
// 递归终止条件:链表为空或只有一个节点
if (head == null || head.next == null) {
return head;
}
// 使用快慢指针找到链表的中间节点
// 慢指针每次走1步,快指针每次走2步
ListNode slow = head;
ListNode fast = head.next; // 快指针从head.next开始,确保慢指针停在中间节点的前驱
// 移动快慢指针直到快指针到达链表末尾
while (fast != null && fast.next != null) {
slow = slow.next; // 慢指针移动1步
fast = fast.next.next; // 快指针移动2步
}
// 分割链表为两部分
ListNode mid = slow.next; // 后半部分链表的头节点
slow.next = null; // 断开链表(将前半部分的尾节点next置为null)
// 递归排序左右两个子链表
ListNode left = sortList(head); // 排序前半部分
ListNode right = sortList(mid); // 排序后半部分
// 合并两个有序链表
return merge(left, right);
}
// 合并两个有序链表
private ListNode merge(ListNode l1, ListNode l2) {
// 创建哑节点(dummy node)作为新链表的起始点
ListNode dummy = new ListNode(0);
ListNode tail = dummy; // 指向新链表的尾节点
// 遍历两个链表,比较节点值,将较小值接入新链表
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
tail.next = l1; // 将l1接入新链表
l1 = l1.next; // 移动l1指针
} else {
tail.next = l2; // 将l2接入新链表
l2 = l2.next; // 移动l2指针
}
tail = tail.next; // 移动新链表的尾指针
}
// 将剩余部分直接接入新链表末尾
tail.next = (l1 != null) ? l1 : l2;
return dummy.next; // 返回新链表的头节点(跳过哑节点)
}
}
自底向上归并排序 - 空间复杂度 O(1)
class Solution {
public ListNode sortList(ListNode head) {
// 边界条件处理
if (head == null || head.next == null) {
return head;
}
// 计算链表长度
int length = 0;
ListNode node = head;
while (node != null) {
length++;
node = node.next;
}
// 创建哑节点指向链表头
ListNode dummy = new ListNode(0);
dummy.next = head;
// 步长从1开始,每次倍增(1, 2, 4...)
for (int step = 1; step < length; step <<= 1) {
ListNode prev = dummy; // 记录已排序部分的尾节点
ListNode curr = dummy.next; // 当前待合并段的起始位置
while (curr != null) {
// 获取第一段链表(长度为step)
ListNode head1 = curr;
// 移动step-1步,找到第一段尾部
for (int i = 1; i < step && curr.next != null; i++) {
curr = curr.next;
}
// 获取第二段链表(长度为step)
ListNode head2 = curr.next;
curr.next = null; // 断开第一段与第二段的连接
curr = head2; // 移动curr到第二段头部
// 移动step-1步,找到第二段尾部
for (int i = 1; i < step && curr != null && curr.next != null; i++) {
curr = curr.next;
}
// 保存后续未处理链表的头节点
ListNode next = null;
if (curr != null) {
next = curr.next;
curr.next = null; // 断开第二段与后续链表的连接
}
// 合并第一段和第二段链表
ListNode merged = merge(head1, head2);
// 将合并后的链表接入已排序部分
prev.next = merged;
// 移动prev到新链表的末尾
while (prev.next != null) {
prev = prev.next;
}
// 处理下一组链表
curr = next;
}
}
return dummy.next;
}
// 合并两个有序链表(同上)
private ListNode merge(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode tail = dummy;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
tail.next = l1;
l1 = l1.next;
} else {
tail.next = l2;
l2 = l2.next;
}
tail = tail.next;
}
tail.next = l1 != null ? l1 : l2;
return dummy.next;
}
}
算法分析
-
时间复杂度:
- 两种方法均为 O(n log n),归并排序的标准时间复杂度
- 每层归并操作需要 O(n) 时间,递归深度为 O(log n)
-
空间复杂度:
- 自顶向下:O(log n),递归调用栈空间
- 自底向上:O(1),仅使用有限几个指针变量
算法过程(自顶向下)
以输入 [4,2,1,3] 为例:
-
分割阶段:
- 快慢指针:slow 从4开始,fast 从2开始
- 第一轮:slow=4→2,fast=2→1→3(fast.next=null 结束)
- 分割结果:左链表
[4,2],右链表[1,3]
-
递归排序左链表:
- 分割
[4,2]→ 左子链表[4],右子链表[2] - 合并
[4]和[2]→[2,4]
- 分割
-
递归排序右链表:
- 分割
[1,3]→ 左子链表[1],右子链表[3] - 合并
[1]和[3]→[1,3]
- 分割
-
合并结果:
- 合并
[2,4]和[1,3]:- 比较2和1 → 取1
- 比较2和3 → 取2
- 比较4和3 → 取3
- 剩余4 → 最终结果
[1,2,3,4]
- 合并
关键点
-
快慢指针分割链表:
- 快指针速度是慢指针2倍
- 当快指针到达末尾时,慢指针正好在中间
- 注意断开连接:
slow.next = null
-
递归终止条件:
- 链表为空或只有一个节点时直接返回
-
合并有序链表技巧:
- 使用哑节点(dummy)简化头节点处理
- 比较两个链表节点值,取较小者接入新链表
- 最后将剩余链表直接接入
两种方法对比
| 特性 | 自顶向下 | 自底向上 |
|---|---|---|
| 空间复杂度 | O(log n)(递归栈) | O(1)(迭代实现) |
| 代码复杂度 | 简单直观 | 较复杂(需手动控制步长) |
| 适用场景 | 面试推荐(易理解) | 满足严格空间要求 |
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1: 普通乱序
ListNode head1 = createList(new int[]{4,2,1,3});
ListNode sorted1 = solution.sortList(head1);
printList(sorted1); // 预期: [1,2,3,4]
// 测试用例2: 包含负数
ListNode head2 = createList(new int[]{-1,5,3,4,0});
ListNode sorted2 = solution.sortList(head2);
printList(sorted2); // 预期: [-1,0,3,4,5]
// 测试用例3: 空链表
ListNode head3 = createList(new int[]{});
ListNode sorted3 = solution.sortList(head3);
printList(sorted3); // 预期: []
// 测试用例4: 单节点链表
ListNode head4 = createList(new int[]{0});
ListNode sorted4 = solution.sortList(head4);
printList(sorted4); // 预期: [0]
}
// 创建链表
private static ListNode createList(int[] arr) {
ListNode dummy = new ListNode(0);
ListNode curr = dummy;
for (int val : arr) {
curr.next = new ListNode(val);
curr = curr.next;
}
return dummy.next;
}
// 打印链表
private static void printList(ListNode head) {
StringBuilder sb = new StringBuilder();
while (head != null) {
sb.append(head.val).append(head.next != null ? "->" : "");
head = head.next;
}
System.out.println("[" + sb.toString() + "]");
}
常见问题
-
为什么用快慢指针找中点?
- 链表不支持随机访问,快慢指针是唯一 O(1) 空间找中点的方法
- 快指针速度是慢指针2倍,确保精确找到中点
-
如何处理链表节点为奇数的情况?
- 自然处理:快指针结束时,慢指针正好在中点位置
- 示例:
[1,2,3]→ 分割为[1,2]和[3]
-
哑节点(dummy)的作用是什么?
- 简化链表操作:无需特殊处理头节点
- 始终通过
dummy.next访问真实头节点
-
自底向上方法中步长的作用?
- 控制归并的链表长度(从1开始,每次倍增)
- 模拟递归过程:先两两合并,再四四合并等
-
哪种方法更优?
- 面试优先用自顶向下(简洁易理解)
- 实际工程若要求严格 O(1) 空间,用自底向上
51万+

被折叠的 条评论
为什么被折叠?



