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

Posted 刘Java

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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;
        
    



以上是关于LeetCode Java刷题笔记—148. 排序链表的主要内容,如果未能解决你的问题,请参考以下文章

LeetCode Java刷题笔记—82. 删除排序链表中的重复元素 II

Leetcode 148. Sort List 归并排序 in Java

LeetCode Java刷题笔记—328. 奇偶链表

LeetCode刷题模版:141 - 150

LeetCode刷题模版:141 - 150

LeetCode刷题笔记-数据结构-day21