LeetCode Java刷题笔记—148. 排序链表

148. 排序链表

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

由于需要O(nlogn) 时间复杂度,那么肯定就是归并排序、快速排序和堆排序。

实际上链表排序大部分都是用归并排序,它是一种稳定的排序。所谓归并排序,它采用了分治思想(Divide and Conquer)。分(divide)阶段将问题分成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"合并"在一起,即分而治之。

归并算法的原理是:如果初始序列含有n个记录,先将总记录拆分成相同长度两个子序列,然后再对两个子序列继续拆分(利用了递归),最终拆分成n个有序的子序列,每个子序列的长度为1;然后两两归并,得到|n/2|(|x|表示不小于x的最小整数)个长度为2或1的有序子序列;再两两归并,如此重复(利用了递归),直至得到一个长度为n的有序序列为止,这种排序方法又被称为二路归并排序。

我们这里的案例也不例外,两种方式都是采用归并方法。

第一个方法采用递归归并求解,除了考察归并思想之外,还可以考察到“求链表中点”,“合并两个有序链表”的操作,这两个操作也都是LeetCode上的题目。第一种方法需要O(logn)的空间复杂度。

第二个方法采用非递归归并求解,这种方法只需要O(1)的空间复杂度,是最好的方式,当然更难理解。使用该方法的时候,需要注意在进行了部分链表合并排序之前,需要断开链表的联系,排序之后,需要将前后节点的引用关系再关联起来。

1 方法一

/**
 * 148. 排序链表
 * 在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。
 * https://leetcode-cn.com/problems/sort-list/
 * 中等
 */
public class LeetCode148 {


    /**
     * 自顶向下递归实现
     * 空间复杂度:O(logn)
     */
    public ListNode sortList(ListNode head) {
        /*
         * 1 递归-拆分
         */
        //递归到最深处的结束的条件
        if (head == null || head.next == null) {
            return head;
        }
        //通过快慢指针找出中点,快指针每次走两步,慢指针每次走一步,当快指针走到了末尾,那么此时慢指针所在的位置就是中间位置
        //求链表中间节点,这实际上就是LeetCode 876题:https://leetcode-cn.com/problems/middle-of-the-linked-list/

        //唯一的区别就是这里我们让fast先走两步,然后fast走到终点的时候,slow就差一步到中点,下面进行递归的时候就采用
        //slow.next作为右边的起点,同时方便后续断开两个链表的连接操作
        ListNode slow = head, fast = head.next.next;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        // 对右半部分链表进行递归的分解,直到满足返回条件
        ListNode right = sortList(slow.next);
        //因为左右两边的链表现在已经独立了,所以需要将slow.next置为null,让他们真正的断开
        //因为后面会再将两个子链表组合在一起,所有这一步很重要,真正的断开原来链表左右部分的联系,防止循环链表
        slow.next = null;
        // 对左半部分链表进行递归的分解,直到满足返回条件
        ListNode left = sortList(head);
        /*
         * 2 排序-合并
         */
        ListNode dummy = new ListNode(-1), cur = dummy;
        //这里实际上就是对两个排序链表进行合并的过程,left代表左边链表的头部,right代表右边链表的头部
        //两个排序链表合并,这实际上就是LeetCode 21题:https://leetcode-cn.com/problems/merge-two-sorted-lists/
        while (left != null && right != null) {
            if (left.val < right.val) {
                cur.next = left;
                left = left.next;
            } else {
                cur.next = right;
                right = right.next;
            }
            cur = cur.next;
        }
        cur.next = left == null ? right : left;
        return dummy.next;
    }

    public class ListNode {

        int val;
        ListNode next;

        ListNode() {

        }

        ListNode(int val) {

            this.val = val;
        }

        ListNode(int val, ListNode next) {

            this.val = val;
            this.next = next;
        }
    }

}

2 方法二

/**
 * 148. 排序链表
 * 在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。
 * https://leetcode-cn.com/problems/sort-list/
 * 中等
 */
public class LeetCode148 {

    /**
     * 自底向上非递归实现
     * 空间复杂度:O(1)
     */
    public ListNode sortList1(ListNode head) {
        //直接返回的条件
        if (head == null || head.next == null) {
            return head;
        }
        //计算链表长度,并且获取尾部节点
        int length = 0;
        ListNode node = head;
        while (node != null) {
            length++;
            node = node.next;
        }
        //由于不确定head节点,因此使用哨兵节点
        ListNode dummy = new ListNode(0, head);
        //分割链表,实现自底向上的归并。subLength为分割单位,每次增长一倍。
        //每次循环对当前分割轮次的链表进行两两合并排序,由于subLength起始值为1,因此最终合并之后的链表是有序的
        for (int subLength = 1; subLength < length; subLength <<= 1) {
            //前驱节点和当前节点
            ListNode pre = dummy, cur = dummy.next;
            while (cur != null) {
                //获取当前分割单位长度的链表节点,作为左边部分
                //左子链表的起始节点
                ListNode left = cur;
                for (int i = 1; i < subLength && cur.next != null; i++) {
                    cur = cur.next;
                }
                //获取当前分割单位长度的链表节点,作为右边部分
                //右子链表的起始节点
                ListNode right = cur.next;
                /*这一步很重要,就和递归实现的方法中一样,需要断开左右两个子链表的关系*/
                cur.next = null;
                cur = right;
                for (int i = 1; i < subLength && cur != null && cur.next != null; i++) {
                    cur = cur.next;
                }
                //获取右子链表尾部节点的后继,然后断开和后面的链表节点的关系
                ListNode next = null;
                if (cur != null) {
                    next = cur.next;
                    cur.next = null;
                }
                //对这个左子链表和右子链表进行合并,并且将合并之后的链表头节点赋值给前驱节点的后继
                //这样,这一部分的链表节点就有序了,然后进行下一部分排序
                pre.next = merge(left, right);
                //然后pre指向尾部节点,为下次循环做准备,向后推进
                while (pre.next != null) {
                    pre = pre.next;
                }
                //当前节点指向之前已经断开联系的后继,向后推进
                cur = next;
            }
        }
        return dummy.next;
    }

    public ListNode merge(ListNode left, ListNode right) {
        ListNode dummy = new ListNode(-1), cur = dummy;
        //这里实际上就是对两个排序链表进行合并的过程,left代表左边链表的头部,right代表右边链表的头部
        //两个排序链表合并,这实际上就是LeetCode 21题:https://leetcode-cn.com/problems/merge-two-sorted-lists/
        while (left != null && right != null) {
            if (left.val < right.val) {
                cur.next = left;
                left = left.next;
            } else {
                cur.next = right;
                right = right.next;
            }
            cur = cur.next;
        }
        cur.next = left == null ? right : left;
        return dummy.next;
    }


    public class ListNode {

        int val;
        ListNode next;

        ListNode() {

        }

        ListNode(int val) {

            this.val = val;
        }

        ListNode(int val, ListNode next) {

            this.val = val;
            this.next = next;
        }
    }

}

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘Java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值