「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。
今天学习排序链表,记录一波优秀题解
【题解】
方法一:归并排序 (递归法)
-
通过递归实现链表归并排序,有以下两个环节:
- 分割 cut 环节:
- 找到当前链表中点,并从中点将链表断开(以便在下次递归 cut 时,链表片段拥有正确边界);
- 我们使用
fast
,slow
快慢双指针法,奇数个节点找到中点,偶数个节点找到中心左边的节点。 - 找到中点
slow
后,执行slow.next = null
将链表切断。 - 递归分割时,输入当前链表左端点
head
和中心节点slow
的下一个节点mid
(因为链表是从slow
切断的)。
- cut 递归终止条件: 时间复杂度
O(l + r)
,l
,r
分别代表两个链表长度。- 当
head.next == null
时,说明只有一个节点了,直接返回此节点。 - 合并 merge 环节: 将两个排序链表合并,转化为一个排序链表。
- 双指针法合并,建立辅助
ListNode newHead
作为头部。 - 设置两指针
left
,right
分别指向两链表头部,比较两指针处节点值大小,由小到大加入合并链表头部,指针交替前进,直至添加完两个链表。 - 返回辅助
ListNode newHead
作为头部的下个节点newHead.next
。
- 当
- 分割 cut 环节:
【代码】
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var sortList = function (head) {
if (head === null || head.next === null) return head
// 找到中点 midNode 后,执行 midNode.next = null 将链表切断。
// 递归分割时,输入当前链表左端点 head 和中心节点 midNode 的下一个节点 rightHead(因为链表是从 midNode 切断的)。
let midNode = getMiddleNode(head)
let rightHead = midNode.next
midNode.next = null
// 合并 merge 环节: 将两个排序链表合并,转化为一个排序链表。
// 时间复杂度 O(l + r),l, r 分别代表两个链表长度。
// 设置两指针 left, right 分别指向两链表头部
let left = sortList(head)
let right = sortList(rightHead)
return mergeTwoLists(left, right)
};
// 找到链表中间节点
function getMiddleNode(head) {
// 分割 cut 环节: 找到当前链表中点,并从中点将链表断开(以便在下次递归 cut 时,链表片段拥有正确边界);
// 我们使用 fast,slow 快慢双指针法
let fast = head.next, slow = head
// 奇数个节点找到中点,偶数个节点找到中心左边的节点。
while (fast !== null && fast.next !== null) {
fast = fast.next.next
slow = slow.next
}
// fast 走两步,slow走一步,fast走完时,slow在中点
return slow
}
// 合并两个有序链表
function mergeTwoLists(left, right) {
// 双指针法合并,建立辅助ListNode newHead 作为头部。
let newHead = new ListNode()
let ret = newHead
// 比较两指针处节点值大小,由小到大加入合并链表头部,指针交替前进,直至添加完两个链表。
while (left !== null && right !== null) {
if (left.val < right.val) {
newHead.next = left
left = left.next
} else {
newHead.next = right
right = right.next
}
newHead = newHead.next
}
newHead.next = left !== null ? left : right
// 返回辅助ListNode newHead 作为头部的下个节点 newHead.next。
return ret.next
}
方法二:归并排序(从底至顶直接合并)
【题解】
首先求得链表的长度 length
,然后将链表拆分成子链表进行合并。
具体做法如下。
-
用
subLength
表示每次需要排序的子链表的长度,初始时subLength=1
。 -
将
subLength
的值加倍,重复第 2 步,对更长的有序子链表进行合并操作,直到有序子链表的长度大于或等于length
,整个链表排序完毕。
如何保证每次合并之后得到的子链表都是有序的呢?可以通过数学归纳法证明。
-
初始时
subLength=1
,每个长度为 1 的子链表都是有序的。 -
如果每个长度为
subLength
的子链表已经有序,合并两个长度为subLength
的有序子链表,得到长度为subLength×2
的子链表,一定也是有序的。 -
当最后一个子链表的长度小于
subLength
时,该子链表也是有序的,合并两个有序子链表之后得到的子链表一定也是有序的。
【代码】
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var sortList = function(head) {
if (head === null) {
return head;
}
let length = 0;
let node = head;
while (node !== null) {
length++;
node = node.next;
}
const dummyHead = new ListNode(0, head);
for (let subLength = 1; subLength < length; subLength <<= 1) {
let prev = dummyHead, curr = dummyHead.next;
while (curr !== null) {
let head1 = curr;
for (let i = 1; i < subLength && curr.next !== null; i++) {
curr = curr.next;
}
let head2 = curr.next;
curr.next = null;
curr = head2;
for (let i = 1; i < subLength && curr != null && curr.next !== null; i++) {
curr = curr.next;
}
let next = null;
if (curr !== null) {
next = curr.next;
curr.next = null;
}
const merged = mergeTwoLists(head1, head2);
prev.next = merged;
while (prev.next !== null) {
prev = prev.next;
}
curr = next;
}
}
return dummyHead.next;
};
// 合并两个有序链表
function mergeTwoLists(left, right) {
// 双指针法合并,建立辅助ListNode newHead 作为头部。
let newHead = new ListNode()
let ret = newHead
// 比较两指针处节点值大小,由小到大加入合并链表头部,指针交替前进,直至添加完两个链表。
while (left !== null && right !== null) {
if (left.val < right.val) {
newHead.next = left
left = left.next
} else {
newHead.next = right
right = right.next
}
newHead = newHead.next
}
newHead.next = left !== null ? left : right
// 返回辅助ListNode newHead 作为头部的下个节点 newHead.next。
return ret.next
}