【链表】LeetCode 148. 排序链表【中等】

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

示例1:

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

 

 示例2:

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

 

示例3:

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

提示:

  • 链表中节点的数目在范围 [0, 5 * 104] 内
  • -105 <= Node.val <= 105

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

【分析】

方法一:归并排序(递归法)

题目要求时间空间复杂度分别为O(nlogn)和O(1),根据时间复杂度我们自然想到二分法,从而联想到归并排序;

对数组做归并排序的空间复杂度为O(n),分别由新开辟数组O(n)和递归函数调用O(logn)组成,而根据链表特性:

  数组额外空间:链表可以通过修改引用来更改节点顺序,无需像数组一样开辟额外空间;

  递归额外空间:递归调用函数将带来O(logn)的空间复杂度,因此若希望达到O(1)空间复杂度,则不能使用递归。

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

(1)分割cut环节:找到当前链表中点,并从中点将链表断开(以便在下次递归cut时,链表片段拥有正确边界)

  我们使用fast,slow快慢指针法,奇数个节点找到中点,偶数个节点找到中心左边的节点;

  找到中点slow后,执行slow.next = None将链表切断;

  递归分割时,输入当前链表左端点head和中心点slow的下一个节点tmp(因为链表是从slow切断的);

  cut递归终止条件:当head.next == None时,说明只有一个节点了,直接返回此节点。

(2)合并merge环节:将两个排序链表合并,转化为一个排序链表。

  双指针法合并,建立辅助ListNode h作为头部

  设置两指针left,right分别指向两个链表头部,比较两个指针处节点值大小,由小到大加入合并链表头部,指针交替前进,直至添加完两个链表;

  返回辅助ListNode h作为头部的下个节点h.next;

  时间复杂度:O(l+r),l,r 分别代表两个链表的长度。

当题目输入的head == None时,直接返回None。

 

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next

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

方法二:归并排序(从底至顶直接合并)

对于非递归的归并排序,需要使用迭代的方式替换cut环节:

  我们知道,cut环节本质上是通过二分法得到链表最小节点单元,再通过多轮合并得到排序结果。

  每一轮合并merge操作针对的单元都固定长度intv,例如:

  (1)第一轮合并时intv = 1,即将整个链表切分为多个长度为1的单元,并按顺序两两排序合并,合并完成的已排序单元长度为2;

  (2)第二轮合并时,intv = 2,即将整个链表分为多个长度为2的单元,并按顺序两两排序合并,合并完成的已排序单元长度为4;

  (3)以此类推,直到单元长度intv >= 链表长度,代表已经排序完成。

  根据以上理论,我们可以仅根据intv计算每个单元的边界,并完成链表的每轮排序合并,例如:

  (1)当intv = 1时,将链表第1和第2节点排序合并,第3和第4节点排序合并,……。

  (2)当intv = 2时,将链表第1-2和第3-4节点排序合并,第5-6和第7-8节点排序合并,……。

  (3)当intv = 3时,将链表第1-4和第5-8节点排序合并,第9-12和第13-16节点排序合并,……。

此方法时间复杂度O(nlogn),空间复杂度O(1)。

 

模拟上述的多轮排序合并:

(1)统计链表长度length,用于通过判断intv < length判定是否完成排序;

(2)额外声明一个节点res,作为头部后面接整个链表,用于:

intv *= 2即切换到下一轮合并时,可通过res.next找到链表头部h;

执行排序合并时,需要一个辅助节点作为头部,而res则作为链表头部排序合并时的辅助头部pre;后面的合并排序可以将上次合并排序的尾部tail用做辅助节点。

(3)在每一轮intv下的合并流程:

  a)根据intv找到合并单元1和单元2的头部h1,h2。由于链表长度可能不是2n,需要考虑边界条件:

    在找h2的过程中,如果链表剩余元素个数少于intv,则无需合并环节,直接break,执行下一轮合并;

    若h2存在,但以h2为头部的剩余元素个数少于intv,也执行合并环节,h2单元的长度为c2 = intv - i。

  b)合并长度为c1, c2的h1, h2链表,其中:

    合并完成后,需要修改新的合并单元的尾部pre指针指向下一个合并单元头部h(在寻找h1, h2环节中,h指针已经被移动到下一个单元头部)。

    合并单元尾部同时也作为下次合并的辅助头部pre。

  c)当h等于None时,代表此轮intv合并完成,跳出。

(4)每轮合并完成后,将单元长度*2,切换到下一轮合并:intv *= 2。

代码看: https://leetcode-cn.com/problems/sort-list/solution/sort-list-gui-bing-pai-xu-lian-biao-by-jyd/

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章