链表——排序链表
- 切半:快慢指针找中点,切成左右两条链表
- 递归排序:左右分别sortList,把左右两段都排好序
- 合并:把两个有序小段拼成一个大有序链表
完整代码实现如下:
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. 找中点(快慢指针)
ListNode slow = head;
ListNode fast = head.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
ListNode mid = slow;
ListNode rightHead = mid.next;
mid.next = null; // 切断链表
// 2. 递归排序左右
ListNode left = sortList(head);
ListNode right = sortList(rightHead);
// 3. 合并两个有序链表
return merge(left, right);
}
// 合并两个有序链表
private ListNode merge(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
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 ? l2 : l1;
return dummy.next;
}
}
为什么要切成左右两条链表排序完再合并?
因为链表不支持随机访问,没法像数组那样快速交换,快速选基准,所以最适合用【分治法+归并】
而且“切两半——>分别排序——>合并”就是归并排序的核心逻辑
为什么不能直接在链表上用冒泡/选择排序?
可以用,但是太慢了:
- 冒泡、选择排序时间复杂度 O(n²)
- 链表一长,直接超时
而我们需要 **O(n log n) **的排序,只有三种:
- 快速排序
- 堆排序
- 归并排序
堆排序需要随机访问,不行;
快速排序在链表上也能写,但很难写、边界多、不稳定;
归并排序最适合链表!
- 切分:log n 层
- 每层合并都是 O(n)
- 总复杂度 O(n log n)
为什么要让fast先走一步?
让fast先走一步,就是为了:在偶数个节点时,让slow停在左半段的最后一个,方便一刀切成两段,当偶数个节点时,fast需要走奇数个节点(fast从head开始,减去头节点),fast因为是一步走两个节点,最后,要想用fast.next!=0作为条件,即fast不能停在最后一个节点上,所以必然会少走一次,从而使得slow也少走一次,停留在左侧的链表尾,
浙公网安备 33010602011771号