实现思路
1- 归并排序- 自顶向下思维技巧:
1 归并排序-普遍性:
- 把数据分成两半:找到中点并分割==> 中点分割
- 对每一半 递归排序==> 中序递归- 子序列递归排序
- 合并两个有序序列==> 合并两个有序子序列
2.1 链表归并排序特殊性1- 链表中点切割==> 快慢指针
- 不完全平均的分割 并不影响算法的正确性,因为 递归会持续分割 直到不能再分
- 即 从正确性角度:任意位置切割都可以
- 但是从性能角度:中点附近切割→ O(n log n),随机位置切割→ 最坏是 O(n^2)
2- 自底向上排序
1 自底向上sort = step * (cut + merge):
- 分别以 1/2/4/8/k个节点为 1组车组 + 对车组(a, b)两两配对 排序合并
2.1 获取 第1组车厢 头节点a1
2.2 通过cut: 斩断a1所有尾结点 + 返回第2组车厢 头节点b1
2.3 通过cut: 斩断b1所有尾结点 + 返回下一队车厢(a2, b2)中 a2
2.4 更新cur, 让它指向 下一队待处理车厢的头节点a2
- 由于 后续merge 需要斩断尾结点才能正确处理节点,后续还要能 处理下一队车厢
- 所以只能在这里 更新cur的指向
2.5 merge: 合并+排序(a1 + b1) 这一队车厢,返回排好序的 这对车厢的头节点x
2.6 更新pre,以保证pre指向为 下一队车厢(a2, b2)中 a2的前一个节点
3- 快排中序实现
1 快排-普遍性:
- 确定pivot/x: 找到随机基准点
- partition-1: 创建/划分确定 左右分区
- partition-2: 向 左右分区内 放入对应成员
- partition-3: 放置pivot/x 到正确位置
- 中序递归左右子区间
- 返回排序后的 结果
2 链表快排-特殊性:
- 无法随机访问元素,选择pivot通常使用 头节点/链表中点
- 通过next指针操作而不是交换,来正确排序 链表节点
参考文档
代码实现
1 方法1: 自顶向下归并排序,时间复杂度 O(n * logn) 空间复杂度O(logn)
function sortList(head) {
if (!head || !head.next) return head;
// 寻找中点并分割
// 易错点1: fast要是head.next,而不能是head, 否则会死循环
// 原因: fast如果是head, 在偶数长度的链表中(如1->2->null),slow会指向2导致死循环
let slow = head, fast = head.next, mid = null;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
}
mid = slow.next;
slow.next = null;
// 中序递归- 子序列递归排序
const l1 = sortList(head);
const l2 = sortList(mid);
// 合并有序子序列
return merge(l1, l2);
}
function merge(l1, l2) {
let dummy = new ListNode(-1);
let pre = dummy;
while (l1 && l2) {
if (l1.val <= l2.val) {
pre.next = l1;
l1 = l1.next;
} else {
pre.next = l2;
l2 = l2.next;
}
pre = pre.next;
}
pre.next = l1 ? l1 : l2;
return dummy.next;
}
2 方法2: 自底向上归并排序,时间复杂度 O(n * logn) 空间复杂度O(1)
function sortList(head: ListNode | null): ListNode | null {
// 处理特殊情况
if (!head || !head.next) return head
//S1 设置虚拟头节点
let dummy = new ListNode(-1, head)
//S2 获取链表长度,用于明确 归并的截止长度
let len = 0;
let cur = head;
while (cur) {
len++;
cur = cur.next;
}
//S3 分别以 1/2/4/8/k个节点为 1节车厢 + 对车厢(a, b)两两配对,进行排序
for (let step = 1; step < len; step *= 2) {
let pre = dummy; // pre固定指向 每轮循环中的 每对配对车厢(a,b)中的 a1的前一个节点
// 易错点1: cur不能指向head
let cur = dummy.next; // cur固定指向每轮循环中的 每对配对车厢(a,b)中的 a头节点
// 易错点2: 每节车厢需要保证cur有值
while (cur) {
let a1 = cur; // 获取a车厢的 头节点a1
let b1 = cut(a1, step); // 根据a1 + step, 得到b车厢的 头节点b1
cur = cut(b1, step); // 更新cur为下一对车厢的头结点a2, 同时隐式切断了b1的尾节点
// 更新pre, 以保证pre指向为 下一对车厢(a2, b2)中 a2的前一个节点
pre.next = merge(a1, b1);
while (pre.next) {
pre = pre.next;
}
}
}
return dummy.next;
};
// 对以head为头结点的链表,截断前n个节点,返回截断后的新头节点
function cut(head: ListNode, n: number) {
while (head && --n > 0) head = head.next;
const bHead = head ? head.next : null;
if (head) head.next = null;
return bHead;
}
// 合并 l1和l2 2个有序链表
function merge(l1: ListNode, l2: ListNode) {
let dummy = new ListNode(-1)
// 易错点: 为了返回头节点,需要要有新对象pre,用于后移指针
let pre = dummy
while (l1 && l2) {
if (l1.val < l2.val) {
pre.next = l1
l1 = l1.next
} else {
pre.next = l2
l2 = l2.next
}
pre = pre.next
}
pre.next = l1 ? l1 : l2
return dummy.next
}
3 方法3: 快排中序递归,时间复杂度 O(n * logn) 空间复杂度O(logn)
function sortList(head) {
if (!head || !head.next) return head
// 1 确定pivot
let x = head.val
// 2.1 partition- 设定左右区间
let ltH = new ListNode(-1), gtH = new ListNode(-1)
let ltP = ltH, gtP = gtH, cur = head.next
// 2.2 partiton- 分区左右区间
while (cur) {
if (cur.val < x) {
ltP.next = cur
ltP = ltP.next
} else {
// 这里有一个注意点: 等于x的节点,需要放在右边区
// 如果放在左边:如果都是等于x的节点,那么在递归sortList左右子区间时,会导致死循环
gtP.next = cur
gtP = gtP.next
}
cur = cur.next
}
// 2.3 partiton- 放置pivot到正确位置
ltP.next = null
gtP.next = null
head.next = null
ltP.next = head
// 3 递归partition 左右区间
ltH = sortList(ltH.next)
gtH = sortList(gtH.next)
// 4 返回排序后的链表头节点
head.next = gtH
return ltH
}