【LeetCode】148. 排序链表

148. 排序链表

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

示例 1:

输入: 4->2->1->3
输出: 1->2->3->4
示例 2:

输入: -1->5->3->4->0
输出: -1->0->3->4->5

分析

难点在于 时间复杂度限制在 O(nlogn),能达到这个复杂度的排序一般就是 快排,堆排序和归并排序,但是稳定的只有归并排序。至于常数空间复杂度,由于是链表操作,因此比较容易实现。

因此,实际上就是归并排序的问题。

解题思路收藏:

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

  • 对数组做归并排序的空间复杂度为 O(n),而根据链表可以通过修改引用来断开的特性,无需开辟额外空间,因此可以满足题目 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。

在这里插入图片描述

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;
    }
}

第一步: 递归使用二分法把链表分割成两个链表
  • 不过由于是单向链表,没法直接获得中间节点,需要循环先计算出链表的长度。
  • 通过 count/2 计算出中间的节点,注意分割后的链表的长度为count/2和(count-count/2),考虑奇数问题
  • 找到中间节点后,把链表断成两个链表,不然各种判断会很复杂
第二步: 合并有序的链表(使用递归合并)
  • 第一步分到最后是单个元素的链表,可以看成有序链表
  • 实际上,这一步就转换成了合并两个有序的链表
  • 使用递归,每次只判断链表头,代码简洁且易懂
总结
  • 把问题分解成子问题,比较容易解决(分成两步(二分法和合并有序链表))
  • 如果循环解决问题困难,考虑使用递归(合并有序链表)
class Solution {
   public ListNode sortList(ListNode head) {
        if (head == null) return head;
        ListNode countNode = head;
        //计算出链表的长度
        int count = 0;
        while (countNode != null) {
            count++;
            countNode = countNode.next;
        }
        return sortList(head, count);
    }
    /**
     * 排序链表
     * @param head
     * @param count
     * @return
     */
    private ListNode sortList(ListNode head, int count) {
        //递归结束条件
        if (count <= 1) return head;
        ListNode leftEnd = head;
        for (int i = 0; i < count / 2-1; i++) {
            leftEnd = leftEnd.next;
        }
        ListNode rightStart = leftEnd.next;
        //断链,如果不断链,各种判断让你死去活来
        leftEnd.next = null;
        //合并两个已经排完序的链表
        //第二个链表的长度为count - count / 2,不能直接是count / 2,奇数计算会错误
        return merge(
                sortList(head, count / 2),
                sortList(rightStart, count - count / 2)
        );
    }


    /**
     * 合并有序的链,使用递归更加简洁和易懂,每次只比较两个链表的链头
     *
     * @param l1
     * @param l2
     * @return
     */
    private ListNode merge(ListNode l1, ListNode l2) {
        if (l1 == null) return l2;
        if (l2 == null) return l1;
        ListNode head;
        if (l1.val <= l2.val) {
            head = l1;
            l1.next = merge(l1.next, l2);
        } else {
            head = l2;
            l2.next = merge(l1, l2.next);
        }
        return head;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值