一、题目大意
给链表头结点head,将链表按升序排序,返回排序完成后的链表。 要求时间复杂度 (O(n\log n))。
链表不支持随机访问,快排实现麻烦,优先选用归并排序。
二、核心思路
归并排序分为分和合两步:
- 分(分割链表) 使用快慢指针找到链表中点,
preSlow记录慢指针前一个节点,preSlow.next=null切断链表,拆成左右两条子链表。 递归继续分割子链表,直到子链表只有 0 或 1 个节点(天然有序,递归终止)。 - 合(合并有序链表) 复用 LeetCode21 题:合并两个有序链表,用 dummy 虚拟头结点 + tail 尾指针拼接节点。
时间复杂度:(O(n\log n)) 空间复杂度:(O(\log n)),递归调用栈开销
三、完整 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) {
//递归终止:空链表 / 单节点链表本身有序
if(head == null || head.next == null) {
return head;
}
ListNode fast = head;
ListNode slow = head;
ListNode preSlow = null;
//1.快慢指针找中点
while(fast != null && fast.next != null) {
preSlow = slow;
slow = slow.next;
fast = fast.next.next;
}
preSlow.next = null; //2.切断链表,拆分成左右两段
ListNode left = sortList(head);
ListNode right = sortList(slow);
//合并两个有序链表
return mergeTwoLists(left,right);
}
//21题:合并两个有序链表
private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(-1);
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; //尾指针向后移动,高频易错点
}
//拼接剩余未遍历完的链表
if(l1 != null) {
tail.next = l1;
} else {
tail.next = l2;
}
return dummy.next;
}
}
四、实例演示(表格)
输入链表:4 → 2 → 1 → 3
| 递归层级 | 当前处理链表 | preSlow | slow | 切断后左链表 | 切断后右链表 | 操作 |
|---|---|---|---|---|---|---|
| 第 1 层(顶层) | 4→2→1→3 | 节点 2 | 节点 1 | 4→2 | 1→3 | 递归处理左、右链表 |
| 第 2 层(处理左) | 4→2 | 节点 4 | 节点 2 | 4 | null | 递归处理左右 |
| 第 3 层(最底层左) | 4 | - | - | 4 | null | 到达递归出口直接返回 4 |
| 第 3 层(最底层右) | 2 | - | - | 2 | null | 到达递归出口直接返回 2 |
| 回溯第 2 层 | — | — | — | 4 | 2 | 合并:merge(4,2) → 2→4 |
| 第 2 层(处理右) | 1→3 | 节点 1 | 节点 3 | 1 | null | 递归处理左右 |
| 第 3 层(底层左) | 1 | - | - | 1 | null | 递归出口返回 1 |
| 第 3 层(底层右) | 3 | - | - | 3 | null | 递归出口返回 3 |
| 回溯第 2 层 | — | — | — | 1 | 3 | 合并:merge(1,3) →1→3 |
| 回溯第 1 层(顶层) | — | — | — | 2→4 | 1→3 | 合并:merge(2→4,1→3) →1→2→3→4 |
执行流程:不断向下递归分割,分割到单个节点触发递归终止;之后一层层回溯,执行 merge 合并有序链表。
五、语法坑与边界条件
- 必须保存 preSlow,执行
preSlow.next=null切断链表;不切断链表,递归不会分割,死循环。 - mergeTwoLists 的 while 条件:
l1 != null && l2 != null,是&&不是||;只有两个链表都不为空,才能读取.val,避免空指针。 tail = tail.next千万不能漏写:不移动尾指针,tail 一直钉死在 dummy,会反复覆盖节点,输出结果只有少量节点。- 递归终止条件:
head == null || head.next == null,空链表、单个节点直接返回。 - 递归版本空间(O(\log n));面试如果要求(O(1))空间,要写自底向上迭代归并。
六、个人感悟
- 链表归并排序核心分为两部分:快慢指针分割 + 合并有序链表(21 题) ,是旧题组合。
- 链表排序不适合快排,链表没有数组随机访问能力。
- merge 函数坑很多:循环条件、尾指针移动、剩余链表拼接,三处全部写对才能 AC。
- 递归虽然代码简洁,会占用栈空间;面试可以主动提自底向上迭代版本思路。