148. 排序链表

§ 解答一:归并排序(递归法)
  • 题目要求时间空间复杂度分别为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 分别代表两个链表长度。
    • 当题目输入的 head == None 时,直接返回None。

Picture2.png

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)

Picture1.png

  • 模拟上述的多轮排序合并:
    • 统计链表长度length,用于通过判断intv < length判定是否完成排序;
    • 额外声明一个节点res,作为头部后面接整个链表,用于:
      • intv *= 2即切换到下一轮合并时,可通过res.next找到链表头部h
      • 执行排序合并时,需要一个辅助节点作为头部,而res则作为链表头部排序合并时的辅助头部pre;后面的合并排序可以将上次合并排序的尾部tail用做辅助节点。
    • 在每轮intv下的合并流程:
      1. 根据intv找到合并单元1和单元2的头部h1, h2。由于链表长度可能不是2^n,需要考虑边界条件:
        • 在找h2过程中,如果链表剩余元素个数少于intv,则无需合并环节,直接break,执行下一轮合并;
        • h2存在,但以h2为头部的剩余元素个数少于intv,也执行合并环节,h2单元的长度为c2 = intv - i
      2. 合并长度为c1, c2h1, h2链表,其中:
        • 合并完后,需要修改新的合并单元的尾部pre指针指向下一个合并单元头部h。(在寻找h1, h2环节中,h指针已经被移动到下一个单元头部)
        • 合并单元尾部同时也作为下次合并的辅助头部pre
      3. 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值