链表中点查找、链表排序、归并排序、分治与递归、递归、非递归。

题目

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。

进阶:

你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?

示例1

输入:head = [4,2,1,3]
输出:[1,2,3,4]

示例2

输入:head = [-1,5,3,4,0]
输出:[-1,0,3,4,5]

示例3

输入:head = []
输出:[]

题解

方法一:字典

  • 将结点地址和值存入字典,使用sorted函数按照值排序,重新构造成链表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def createLink_rearinsert(lista):
head = ListNode(-1)
rear = head
for i in lista:
new_node=ListNode(i)
rear.next=new_node
rear=new_node
return head.next

class Solution:
def sortList(self, head: ListNode) -> ListNode:
if not head:
return head
dic=dict()
while head:
dic[head]=head.val
head=head.next
#print(dic)
new_dic=sorted(dic.items(),key=lambda item:item[1])
head= ListNode(-1)
rear = head
for d in new_dic:
rear.next = d[0]
rear = rear.next
rear.next=None
return head.next

if __name__=="__main__":
head = [-1,5,3,4,0]
new_a=createLink_rearinsert(head)
a1 = Solution()
p=a1.sortList(new_a)
list_head=[]
while p!=None:
list_head.append(p.val)
p=p.next
print(list_head)
[-1, 0, 3, 4, 5]

题目进阶分析

这道题考虑时间复杂度更低的排序算法。题目的进阶问题要求达到O(nlogn) 的时间复杂度和 O(1) 的空间复杂度,时间复杂度是 O(nlogn) 的排序算法包括归并排序、堆排序和快速排序(快速排序的最差时间复杂度是 $O(n^2)$) 其中最适合链表的排序算法是归并排序

归并排序基于分治算法。最容易想到的实现方式是自顶向下的递归实现,考虑到递归调用的栈空间,自顶向下归并排序的空间复杂度是O(logn)。如果要达到 O(1)的空间复杂度,则需要使用自底向上的实现方式。

方法二:自顶向下归并排序

对链表自顶向下归并排序的过程如下。

  • 找到链表的中点,以中点为分界,将链表拆分成两个子链表。寻找链表的中点可以使用快慢指针的做法,快指针每次移动 2 步,慢指针每次移动 1 步,当快指针到达链表末尾时,慢指针指向的链表节点即为链表的中点。

  • 对两个子链表分别排序。

  • 将两个排序后的子链表合并,得到完整的排序后的链表。

上述过程可以通过递归实现。递归的终止条件是链表的节点个数小于或等于 1,即当链表为空或者链表只包含 1 个节点时,不需要对链表进行拆分和排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Solution:
def sortList(self, head: ListNode) -> ListNode:
def sortFunc(head: ListNode, tail: ListNode) -> ListNode:
if not head:
return head
if head.next == tail:
head.next = None
return head
slow = fast = head
while fast != tail:
slow = slow.next
fast = fast.next
if fast != tail:
fast = fast.next
mid = slow
return merge(sortFunc(head, mid), sortFunc(mid, tail))

def merge(head1: ListNode, head2: ListNode) -> ListNode:
dummyHead = ListNode(0)
temp, temp1, temp2 = dummyHead, head1, head2
while temp1 and temp2:
if temp1.val <= temp2.val:
temp.next = temp1
temp1 = temp1.next
else:
temp.next = temp2
temp2 = temp2.next
temp = temp.next
if temp1:
temp.next = temp1
elif temp2:
temp.next = temp2
return dummyHead.next

return sortFunc(head, None)


if __name__=="__main__":
head = [-1,5,3,4,0,2]
new_a=createLink_rearinsert(head)
a1 = Solution()
p=a1.sortList(new_a)
list_head=[]
while p!=None:
list_head.append(p.val)
p=p.next
print(list_head)
[-1, 0, 2, 3, 4, 5]

自顶至底归并2

通过递归实现链表归并排序,有以下两个环节:

  • 分割 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 分别代表两个链表长度。
  • 当题目输入的 head == None 时,直接返回None。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution:
def sortList(self, head: ListNode) -> ListNode:
if not head or not head.next: return head # termination.
# cut the LinkedList at the mid index.
slow, fast = head, head.next
while fast and fast.next:
fast, slow = fast.next.next, slow.next
mid, slow.next = slow.next, None # save and cut.
# recursive for cutting.
left, right = self.sortList(head), self.sortList(mid)
# merge `left` and `right` linked list and return it.
h = res = ListNode(0)
while left and right:
if left.val < right.val: h.next, left = left, left.next
else: h.next, right = right, right.next
h = h.next
h.next = left if left else right
return res.next

if __name__=="__main__":
head = [-1,5,3,4,0,6,2]
new_a=createLink_rearinsert(head)
a1 = Solution()
p=a1.sortList(new_a)
list_head=[]
while p!=None:
list_head.append(p.val)
p=p.next
print(list_head)
[-1, 0, 2, 3, 4, 5, 6]

方法三: 自底至顶归并排序

  • 对于非递归的归并排序,需要使用迭代的方式替换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(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。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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

if __name__=="__main__":
head = [-1,5,3,4,0,6,7,2]
new_a=createLink_rearinsert(head)
a1 = Solution()
p=a1.sortList(new_a)
list_head=[]
while p!=None:
list_head.append(p.val)
p=p.next
print(list_head)
[-1, 0, 2, 3, 4, 5, 6, 7]