29

力扣148——排序链表

 5 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzU0NTk3NDMyMQ%3D%3D&%3Bmid=2247483982&%3Bidx=1&%3Bsn=2e915580cd5cc1af4b4bd0881d633600
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

原题

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

示例 1:

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

示例 2:

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

原题url:https://leetcode-cn.com/problems/sort-list/

解决

题目很明确,排序,对于时间复杂度和空间复杂度有要求,针对 O(n log n) ,让我想到了 归并排序快速排序 ,接下来我们各自来看看。

对了,这里先统一放一下节点类,单向链表中的节点,存储当前节点的值和后一个节点的引用。

Definition for singly-linked list.
public class ListNode {
    int val;
    ListNode next;
    ListNode(int x) { val = x; }
}

归并排序

归并排序,说白了,就是先分解到最小单元,然后逐个进行合并并排序,这样在合并的时候,其实两个链表本身就是有序的,那么当有一个全部取完后,另一个可以直接拼接在最后面。

让我们看一下代码:

public class Solution {
    public ListNode sortList(ListNode head) {
        // 归并排序

        if (head == null || head.next == null) {
            return head;
        }

        // 先分隔,利用快慢指针分隔。
        // 快指针先走,因为只有当空节点或1个节点才是终止条件,2个节点的时候,如果不让快指针先走,而是也指向head,那么2个节点永远不会被分隔,会陷入死循环
        ListNode fast = head.next.next;
        ListNode slow = head;
        while (true) {
            if (fast == null || fast.next == null) {
                break;
            }

            fast = fast.next.next;
            slow = slow.next;
        }
        // 后半部分的开头
        ListNode second = slow.next;
        second = sortList(second);
        // 前半部分的开头
        slow.next = null;
        ListNode first = head;
        first = sortList(first);

        // 合并
        ListNode result = new ListNode(0);
        head = result;
        while (first != null && second != null) {
            if (first.val < second.val) {
                result.next = first;
                first = first.next;
            } else {
                result.next = second;
                second = second.next;
            }
            result = result.next;
        }
        if (first != null) {
            result.next = first;
        } else {
            result.next = second;
        }

        return head.next;
    }
}

提交OK,执行用时: 5 ms ,内存消耗: 39.6 MB ,执行用时只战胜了 59.07% 的 java 提交记录,应该还有优化的空间。

归并排序——优化

针对上面的代码,在分隔的时候,设置 fast = head.next.next ,这是因为我们设置的递归终止条件是针对 null 或者单个节点的。其实当只剩下两个节点的时候,就可以进行排序了,这样应该可以节省近一半的时间,当然了,从时间复杂度上来说并没有改变。

我们看一下代码:

public class Solution {
    public ListNode sortList(ListNode head) {
        // 归并排序
        if (head == null || head.next == null) {
            return head;
        }

        // 说明只有两个节点
        if (head.next.next == null) {
            ListNode second = head.next;
            if (head.val > second.val) {
                return head;
            } else {
                second.next = head;
                head.next = null;
                return second;
            }
        }

        // 先分隔,利用快慢指针分隔。
        ListNode fast = head;
        ListNode slow = head;
        while (true) {
            if (fast == null || fast.next == null) {
                break;
            }

            fast = fast.next.next;
            slow = slow.next;
        }
        // 后半部分的开头
        ListNode second = slow.next;
        second = sortList(second);
        // 前半部分的开头
        slow.next = null;
        ListNode first = head;
        first = sortList(first);

        // 合并
        ListNode result = new ListNode(0);
        head = result;
        while (first != null && second != null) {
            if (first.val < second.val) {
                result.next = first;
                first = first.next;
            } else {
                result.next = second;
                second = second.next;
            }
            result = result.next;
        }
        if (first != null) {
            result.next = first;
        } else {
            result.next = second;
        }

        return head.next;
    }
}

执行用时,有的时候是 4 ms ,有的时候是 3 ms ,看来归并排序这条路差不多就是这样了。

快速排序

快速排序的思想就是选择一个标准值,将比它大的和比它的小的,做交换。针对链表这种结构,就是将比它大的放在一个链表中,比它小的放在一个链表中,和它一样大的,放在另一个链表中。然后针对小的和大的链表,继续排序。最终将三个链表按照小、相等、大进行连接。

接下来让我们看看代码:

class Solution {
        public ListNode sortList(ListNode head) {
            // 利用快排

            // 单个节点是终止节点
            if (head == null || head.next == null) {
                return head;
            }
            // 比标准值小的节点
            ListNode lowHead = new ListNode(0);
            ListNode low = lowHead;
            // 和标准值一样的节点
            ListNode midHead = new ListNode(0);
            ListNode mid = midHead;
            // 比标准值大的节点
            ListNode highHead = new ListNode(0);
            ListNode high = highHead;

            // 标准值
            int val = head.val;
            ListNode node = head;
            // 遍历
            while (node != null) {
                // 比标准值大的节点
                if (node.val > val) {
                    high.next = node;
                    high = high.next;
                }
                // 比标准值小的节点
                else if (node.val < val) {
                    low.next = node;
                    low = low.next;
                }
                // 和标准值一样的节点
                else {
                    mid.next = node;
                    mid = mid.next;
                }
                node = node.next;
            }
            // 终止,避免造成环
            low.next = null;
            high.next = null;

            lowHead.next = sortList(lowHead.next);
            highHead.next = sortList(highHead.next);

            // 找出小节点链表的末尾
            low = lowHead;
            while (low.next != null) {
                low = low.next;
            }
            // 拼接
            low.next = midHead.next;
            mid.next = highHead.next;

            return lowHead.next;
        }
    }

提交OK,执行用时: 2 ms ,内存消耗: 40.01 MB

和归并排序相比,时间更短,至于原因,我确实是没有想明白,因为都需要比较,然后重新构造新链表。我猜测是测试数据离散程度更高,这样归并排序的话,并没有充分利用其特性:

当两个链表合并时,如果一个链表已经全部结束,另一个链表剩余的部分可以直接拼接。

总结

以上就是这道题目我的解答过程了,不知道大家是否理解了。针对它的时间复杂度要求,利用归并排序或者快速排序解决。

有兴趣的话可以访问我的博客或者关注我的公众号,说不定会有意外的惊喜。

https://death00.github.io/

公众号:健程之道

raEzmm3.jpg!web

点击此处留言


Recommend

  • 31
    • 微信 mp.weixin.qq.com 5 years ago
    • Cache

    力扣80——删除排序数组中的重复项 II

    原题 给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素最多出现两次,返回移除后数组的新长度。 不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。 ...

  • 51
    • 微信 mp.weixin.qq.com 5 years ago
    • Cache

    力扣86——分隔链表

    原题 给定一个链表和一个特定值 x,对链表进行分隔,使得所有小于 x 的节点都在大于或等于 x 的节点之前。 你应当保留两个分区中每个节点的初始相对位置。 示例: 输入: head = 1->4->3->...

  • 16

    力扣-1508 子数组和排序后的区间和

  • 8

    几乎刷完了力扣所有的链表题,我发现了这些东西。。。 | lucifer的网络博客 先上下本文的提纲,这个是我用 mindmap 画的...

  • 9

    本文首发于公众号「五分钟学算法」,是图解 LeetCode 系列文章之一。 个人网站:https://www.cxyxiaowu.com

  • 5
    • segmentfault.com 3 years ago
    • Cache

    JZ-016-合并两个排序的链表

    JZ-016-合并两个排序的链表发布于 12 月 5 日合并两个排序的链表输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链...

  • 7
    • allenwind.github.io 3 years ago
    • Cache

    二叉搜索树转换成排序双向链表

    Mr.Feng BlogNLP、深度学习、机器学习、Python、Go二叉搜索树转换成排序双向链表遇到一道有趣的问题:把二叉搜索树转换成排序的双向链表。分享下思路,感受转换过...

  • 7

    Mr.Feng BlogNLP、深度学习、机器学习、Python、Go合并两(多)个排序的链表(算法17)问题:输入两个递增排序的链表,合并这两个链表并使新链表中的结点仍然是按...

  • 4

  • 2
    • senitco.github.io 3 years ago
    • Cache

    链表排序

    链表排序问题总结归纳。 Merge Sorted ArrayDescription: Given two sorted integer arrays nums1 and nums2, merge nums2...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK