98. 链表排序

描述

在 O(nlogn) 时间复杂度和常数级的空间复杂度下给链表排序

样例

给出 1->3->2->null,给它排序变成 1->2->3->null.

挑战

分别用归并排序和快速排序做一遍。

PS

常数级复杂度即 O(1) 的复杂度只用一个额外变量

知识点

  1. quick sort:
    平均时间复杂度 O(nlogn),如果每次选取的切割点都是链表端点,通过 O(1) 的操作使 O(n) 的问题变为 O(n - 1),最坏情况时间复杂度是 O(n^2),空间复杂度 O(1)
  2. Merge sort
    时间复杂度一直是 O(nlogn),空间复杂度 O(n)
  3. heap sort
    时间复杂度 O(n),空间复杂度 O(1)

区别

无论是快排还是归并排序都需要用递归,但快排是先整体有序后局部有序,而归并排序是先局部有序再整体有序

代码

/**
 * Definition for ListNode.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int val) {
 *         this.val = val;
 *         this.next = null;
 *     }
 * }
 */
  1. Merge Sort
public class Solution {            
    private ListNode findMiddle(ListNode head) {
        ListNode slow = head, fast = head.next;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        return slow;
    }    

    // 合并两个链表
    private ListNode merge(ListNode head1, ListNode head2) {
        ListNode dummy = new ListNode(0);
        ListNode tail = dummy;

        while (head1 != null && head2 != null) {
            if (head1.val < head2.val) {
                tail.next = head1;
                head1 = head1.next;
            } else {
                tail.next = head2;
                head2 = head2.next;
            }
            tail = tail.next;
        }

        if (head1 != null) {
            tail.next = head1;
        } else {
            tail.next = head2;
        }

        return dummy.next;
    }

    /* 用二分的方式不断地递归细化链表,直到链表变成只有两个结点,
     * 然后用 merge 排个序,再一层层返回递归,
     * 值得说明的是每退出一层递归都有两个排好序的链表,
     * 每一层递归都要 merge 名为 left 和 right 的两个链表
     */
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }

        ListNode mid = findMiddle(head);

        ListNode right = sortList(mid.next);
        // 保证 left 是 head 到 mid 的链表,不然 left 还是从 head 开始的整个链表
        mid.next = null;
        ListNode left = sortList(head);

        return merge(left, right);
    }
}

  1. Quick Sort 1
    链表快排并不是采用两根指针,一根指针从头到尾
public class Solution {
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }

        ListNode mid = findMedian(head); // O(n)

        // 定义每一部分的头指针
        ListNode leftDummy = new ListNode(0), leftTail = leftDummy;
        ListNode rightDummy = new ListNode(0), rightTail = rightDummy;
        ListNode middleDummy = new ListNode(0), middleTail = middleDummy;

        // 快排交换结点的模板
        while (head != null) {
            if (head.val < mid.val) {
                leftTail.next = head;
                leftTail = head;
            } else if (head.val > mid.val) {
                rightTail.next = head;
                rightTail = head;
            } else {
                middleTail.next = head;
                middleTail = head;
            }
            head = head.next;
        }

        // 确保每一轮切分后,新的部分到切分点结束,这个地方若不写,则会出现超时,因为不写 null 指针,每一轮递归问题的规模不会下降
        leftTail.next = null;
        middleTail.next = null;
        rightTail.next = null;

        // 递归
        ListNode left = sortList(leftDummy.next);
        ListNode right = sortList(rightDummy.next);

        return concat(left, middleDummy.next, right);
    }

    private ListNode findMedian(ListNode head) {
        ListNode slow = head, fast = head.next;
        // 比较有意思的是,fast.next != null 写到 fast != null 前面会出现空指针溢出错误
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }

    // 把三部分链表拼起来
    private ListNode concat(ListNode left, ListNode middle, ListNode right) {
        ListNode dummy = new ListNode(0), tail = dummy;

        tail.next = left; tail = getTail(tail);
        // 下一个 tail 是 left 的最后一个非空结点,形参的 middle 对应的传入值是middleDummy.next
        tail.next = middle; tail = getTail(tail);
        // 下一个 tail 是 middle 的最后一个非空结点
        tail.next = right;
        // 可写可不写的一句
        tail = getTail(tail);

        return dummy.next;
    }

    // 一个接一个地找到链表中的非空结点,遍历它们,然后返回每一部分最后一个非空结点
    private ListNode getTail(ListNode head) {
        if (head == null) {
           return null;
        } 

        while (head.next != null) {
            head = head.next;
        }
        return head;
    }
}

最后这个地方如果这么写,括号里写 left,middle,right 和写成 tail 实际上差了一个结点,若这么写,
比如:输入2 -> -1 -> 0 -> null,left == null,tail = getTail(left) 会返回 null 使得 tail 指向 null,空指针后面就没办法继续连接 middle 和 right 部分了;但如果是 tail = getTail(tail),tail 是 dummy 结点,沿 tail.next = left 计算时尽管 left 等于空仍旧返回 dummy (非空结点),仍可以继续进行; middle 和 right 为空的情形也是同理,这种写法保证了连接过程中不会出现连接 null 的错误

错误
  1. Quick Sort 2

相比于另一种快排省略了连接的过程,两种快排的区别主要在于右边切分部分的头结点位置,本算法在链表最结尾,另一种算法在分割好的右链表的开始结点

// 用pair来代表左右两个链表的分割点
class Pair {
    public ListNode first, second; 
    public Pair(ListNode first, ListNode second) {
        this.first = first;
        this.second = second;
    }
}

public class Solution {
    /**
     * @param head: The head of linked list.
     * @return: You should return the head of the sorted linked list,
                    using constant space complexity.
     */
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }

        ListNode mid = findMedian(head); // O(n)
        // 注意这个地方时mid.val
        Pair pair = partition(head, mid.val); // O(n)

        // 递归传入的参数一定得是个变化的值,若是固定值,问题规模不见减小,会出现超时
        ListNode left = sortList(pair.first);
        ListNode right = sortList(pair.second);
        // 把拆开的两行代码重新连起来
        getTail(left).next = right; // O(n)

        return left;
    }

    // 1->2->3 return 2
    // 1->2 return 1
    private ListNode findMedian(ListNode head) {
        ListNode slow = head, fast = head.next;
        // fast == null 代表只有一个结点,fast.next == null 代表只有两个结点,这种情况中值直接返回 slow 就可以了
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }

    // < value in the left, > value in the right
    private Pair partition(ListNode head, int value) {
        ListNode leftDummy = new ListNode(0), leftTail = leftDummy;
        ListNode rightDummy = new ListNode(0), rightTail = rightDummy;
        ListNode equalDummy = new ListNode(0), equalTail = equalDummy;

        // 遍历链表将链表分为 3 部分
        while (head != null) {
            if (head.val < value) {
                leftTail.next = head;
                leftTail = head;
            } else if (head.val > value) {
                rightTail.next = head;
                rightTail = head;
            } else {
                equalTail.next = head;
                equalTail = head;
            }
            // 忘记这句 head 不继续往下移动就会出现超时
            head = head.next;
        }

        leftTail.next = null;
        rightTail.next = null;
        equalTail.next = null;

        // 只有链表中结点值全部一样时才会出现左边没结点,右边也没结点,这时找到 equal 的中间结点,左右各一半
        if (leftDummy.next == null && rightDummy.next == null) {
            ListNode mid = findMedian(equalDummy.next);
            leftDummy.next = equalDummy.next;
            rightDummy.next = mid.next;
            mid.next = null;
        // 之前选取的分割点是链表中最小的点,左边结点少,把中间结点全分给左边
        // 尽量保证快速排序的时间平衡性
        } else if (leftDummy.next == null) {
            leftTail.next = equalDummy.next;
        // 之前选取的分割点是链表中最大的点,右边结点少,把中间结点全分给右边
        // 尽量保证快速排序的时间平衡性
        } else {
            rightTail.next = equalDummy.next;
        }

        // 尤其是在递归中,参数对应的值随时可变,要 deep copy 一下
        return new Pair(leftDummy.next, rightDummy.next);
    }

    private ListNode getTail(ListNode head) {
        if (head == null) {
           return null;
        } 
        // head.next == null 表明遍历到了链表最后一个结点,while 循环确保不返回null
        while (head.next != null) {
            head = head.next;
        }
        return head;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容

  • //leetcode中还有花样链表题,这里几个例子,冰山一角 求单链表中结点的个数----时间复杂度O(n)这是最...
    暗黑破坏球嘿哈阅读 1,509评论 0 6
  • 背景 一年多以前我在知乎上答了有关LeetCode的问题, 分享了一些自己做题目的经验。 张土汪:刷leetcod...
    土汪阅读 12,719评论 0 33
  • 第一章 绪论 什么是数据结构? 数据结构的定义:数据结构是相互之间存在一种或多种特定关系的数据元素的集合。 第二章...
    SeanCheney阅读 5,735评论 0 19
  • 网红”进化史 随着网红人气的迅速增长,以及直播平台的兴起与快速发展,让大众看到一些高质量网红的粉丝活跃度和粘性并不...
    情丶不弃阅读 321评论 0 0
  • 说好的坚持每天一篇文,可一些不大不小的事让我断了许久。有时觉得思考可供日后静坐长椅时细细品味,有时却认为生不带来死...
    夜缨阅读 216评论 0 0