§ 解答一:归并排序(递归法)
-
题目要求时间空间复杂度分别为O(nlogn)O(nlogn)O(nlogn)和O(1)O(1)O(1),根据时间复杂度我们自然想到二分法,从而联想到归并排序;
-
对数组做归并排序的空间复杂度为 O(n)O(n)O(n),分别由新开辟数组O(n)O(n)O(n)和递归函数调用O(logn)O(logn)O(logn)组成,而根据链表特性:
- 数组额外空间:链表可以通过修改引用来更改节点顺序,无需像数组一样开辟额外空间;
- 递归额外空间:递归调用函数将带来O(logn)O(logn)O(logn)的空间复杂度,因此若希望达到O(1)O(1)O(1)空间复杂度,则不能使用递归。
-
通过递归实现链表归并排序,有以下两个环节:
- 分割 cut 环节: 找到当前链表
中点
,并从中点
将链表断开(以便在下次递归cut
时,链表片段拥有正确边界);- 我们使用
fast,slow
快慢双指针法,奇数个节点找到中点,偶数个节点找到中心左边的节点。 - 找到中点
slow
后,执行slow.next = None
将链表切断。 - 递归分割时,输入当前链表左端点
head
和中心节点slow
的下一个节点tmp
(因为链表是从slow
切断的)。 - cut 递归终止条件: 当
head.next == None
时,说明只有一个节点了,直接返回此节点。
- 我们使用
- 合并 merge 环节: 将两个排序链表合并,转化为一个排序链表。
- 双指针法合并,建立辅助ListNode
h
作为头部。 - 设置两指针
left
,right
分别指向两链表头部,比较两指针处节点值大小,由小到大加入合并链表头部,指针交替前进,直至添加完两个链表。 - 返回辅助ListNode
h
作为头部的下个节点h.next
。 - 时间复杂度
O(l + r)
,l, r
分别代表两个链表长度。
- 双指针法合并,建立辅助ListNode
- 当题目输入的
head == None
时,直接返回None。
- 分割 cut 环节: 找到当前链表
class Solution {
public ListNode sortList(ListNode head) {
if (head == null || head.next == null)
return head;
ListNode fast = head.next, slow = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
ListNode tmp = slow.next;
slow.next = null;
ListNode left = sortList(head);
ListNode right = sortList(tmp);
ListNode h = new ListNode(0);
ListNode res = h;
while (left != null && right != null) {
if (left.val < right.val) {
h.next = left;
left = left.next;
} else {
h.next = right;
right = right.next;
}
h = h.next;
}
h.next = left != null ? left : right;
return res.next;
}
}
§ 解答二:归并排序(从底至顶直接合并)
- 对于非递归的归并排序,需要使用迭代的方式替换
cut
环节:- 我们知道,
cut
环节本质上是通过二分法得到链表最小节点单元,再通过多轮合并得到排序结果。 - 每一轮合并
merge
操作针对的单元都有固定长度intv
,例如:- 第一轮合并时
intv = 1
,即将整个链表切分为多个长度为1的单元,并按顺序两两排序合并,合并完成的已排序单元长度为2。 - 第二轮合并时
intv = 2
,即将整个链表切分为多个长度为2的单元,并按顺序两两排序合并,合并完成已排序单元长度为4。 - 以此类推,直到单元长度
intv >= 链表长度
,代表已经排序完成。
- 第一轮合并时
- 根据以上推论,我们可以仅根据
intv
计算每个单元边界,并完成链表的每轮排序合并,例如:- 当
intv = 1
时,将链表第1
和第2
节点排序合并,第3
和第4
节点排序合并,……。 - 当
intv = 2
时,将链表第1-2
和第3-4
节点排序合并,第5-6
和第7-8
节点排序合并,……。 - 当
intv = 4
时,将链表第1-4
和第5-8
节点排序合并,第9-12
和第13-16
节点排序合并,……。
- 当
- 我们知道,
- 此方法时间复杂度O(nlogn)O(nlogn)O(nlogn),空间复杂度O(1)O(1)O(1)。
- 模拟上述的多轮排序合并:
- 统计链表长度
length
,用于通过判断intv < length
判定是否完成排序; - 额外声明一个节点
res
,作为头部后面接整个链表,用于:intv *= 2
即切换到下一轮合并时,可通过res.next
找到链表头部h
;- 执行排序合并时,需要一个辅助节点作为头部,而
res
则作为链表头部排序合并时的辅助头部pre
;后面的合并排序可以将上次合并排序的尾部tail
用做辅助节点。
- 在每轮
intv
下的合并流程:- 根据
intv
找到合并单元1和单元2的头部h1
,h2
。由于链表长度可能不是2^n
,需要考虑边界条件:- 在找
h2
过程中,如果链表剩余元素个数少于intv
,则无需合并环节,直接break
,执行下一轮合并; - 若
h2
存在,但以h2
为头部的剩余元素个数少于intv
,也执行合并环节,h2
单元的长度为c2 = intv - i
。
- 在找
- 合并长度为
c1, c2
的h1, h2
链表,其中:- 合并完后,需要修改新的合并单元的尾部
pre
指针指向下一个合并单元头部h
。(在寻找h1, h2
环节中,h指针已经被移动到下一个单元头部) - 合并单元尾部同时也作为下次合并的辅助头部
pre
。
- 合并完后,需要修改新的合并单元的尾部
- 当
h == None
,代表此轮intv
合并完成,跳出。
- 根据
- 每轮合并完成后将单元长度×2,切换到下轮合并:
intv *= 2
。
- 统计链表长度
class Solution:
def sortList(self, head: ListNode) -> ListNode:
h, length, intv = head, 0, 1
while h: h, length = h.next, length + 1
res = ListNode(0)
res.next = head
# merge the list in different intv.
while intv < length:
pre, h = res, res.next
while h:
# get the two merge head `h1`, `h2`
h1, i = h, intv
while i and h: h, i = h.next, i - 1
if i: break # no need to merge because the `h2` is None.
h2, i = h, intv
while i and h: h, i = h.next, i - 1
c1, c2 = intv, intv - i # the `c2`: length of `h2` can be small than the `intv`.
# merge the `h1` and `h2`.
while c1 and c2:
if h1.val < h2.val: pre.next, h1, c1 = h1, h1.next, c1 - 1
else: pre.next, h2, c2 = h2, h2.next, c2 - 1
pre = pre.next
pre.next = h1 if c1 else h2
while c1 > 0 or c2 > 0: pre, c1, c2 = pre.next, c1 - 1, c2 - 1
pre.next = h
intv *= 2
return res.next