【LeetCode 148】算法进阶:排序链表 ( 归并排序、快速排序、计数排序 )

题目:给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。

提示:

  • 链表中节点的数目在范围 [0, 5 * 104] 内
  • -105 <= Node.val <= 105

进阶:你可以在 O (n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?


要对链表进行排序,可以采用归并排序算法,这是因为归并排序特别适用于链表排序,因为它不需要额外的空间来存储数组索引,而且它的分治策略可以很好地应用于链表的分割和合并。除了归并排序,还有其他几种方法可以用来对链表进行排序。比如快速排序和计数排序。

快速排序是一种分治算法,它通过选择一个"基准"元素并将链表分为两个子链表(一个包含小于基准的元素,另一个包含大于基准的元素)来工作。然后递归地对这两个子链表进行快速排序。

计数排序是一种非比较排序算法,适用于元素范围有限的情况。对于链表排序,我们可以先遍历链表,使用一个数组(或哈希表)来统计每个值出现的次数,然后根据这些计数来重建链表。

在选择排序方法时,需要考虑链表的特点(如是否允许修改原链表、元素值的范围等)以及算法的性能(如时间复杂度和空间复杂度)。

下面分别介绍 归并排序、快速排序、计数排序 这三种方法的算法步骤和 Java 代码实现 及各自优缺点。

一、归并排序

算法步骤:

  1. 找到中点:首先找到链表的中点,将链表分成两半。

  2. 递归排序:递归地对前半部分和后半部分进行排序。

  3. 合并两个有序链表:将两个已排序的链表合并成一个有序链表。

复杂度分析:

  • 时间复杂度:O (n log n),其中 n 是链表的长度。这是因为归并排序的时间复杂度为 O(n log n)。

  • 空间复杂度:O (1),只使用了常数级别的额外空间。

归并排序的 Java 代码:

复制代码
class Solution {
    public ListNode sortList(ListNode head) {
        if(head==null || head.next==null){
            return head;
        }
        // 步骤1: 找到中点并分割链表
        ListNode mid = getMid(head);   // 找到中点
        ListNode midNext = mid.next;   // 记录后半段链表的头结点
        mid.next = null;               // 从中点断开链表      
        // 步骤2: 递归排序
        ListNode left = sortList(head);
        ListNode right = sortList(midNext);       
        // 步骤2: 递归排序
        return merge(left,right);
    }

    // 方法:找到链表中点
    private ListNode getMid(ListNode head){
        if(head==null || head.next==null){
            return head;
        }
        // 慢指针走一步,快指针走两步
        ListNode slow = head;
        ListNode fast = head;
        while(fast.next != null && fast.next.next != null){ //单数和双数两种情况
            slow = slow.next;
            fast = fast.next.next;
        }
            return slow; // 慢指针就是中点 
    }
    
    // 方法:合并两个有序链表
    private ListNode merge(ListNode l1, ListNode l2){
        ListNode dummyHead = new ListNode(0);
        ListNode cur = dummyHead;
        //遍历两个链表直到其中一个结束
        while(l1 != null && l2 != null){  
            if(l1.val < l2.val){  //每次比较结点大小
                cur.next = l1;
                l1 = l1.next;
            }else{
                cur.next = l2;
                l2 = l2.next;
            }
            cur = cur.next;
        }
        //连接剩下的部分链表
        cur.next = (l1 != null)? l1 : l2;
        return dummyHead.next;
    }

}

二、快速排序

快速排序是一种分治算法,它通过选择一个"基准"元素并将链表分为两个子链表(一个包含小于基准的元素,另一个包含大于基准的元素)来工作。然后递归地对这两个子链表进行快速排序。

算法步骤:

  1. 选择基准:从数列中挑出一个元素,称为"基准"(pivot)。

  2. 分区操作:重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。

  3. 递归排序:递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

  4. 重复步骤:重复步骤1-3,直到整个数列排序完成。

复杂度分析:快速排序的时间复杂度平均为 O(n log n),但在最坏情况下会退化到 O(n^2),不过这种情况比较少见,可以通过随机选择基准来避免。

快速排序的 Java 代码:

复制代码
class Solution {
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }

        ListNode slow = head, fast = head, prev = null;
        // 使用快慢指针找到中点
        while (fast != null && fast.next != null) {
            prev = slow;
            slow = slow.next;
            fast = fast.next.next;
        }
        prev.next = null; // 分割链表

        // 递归排序
        ListNode left = sortList(head);
        ListNode right = sortList(slow);
        
        // 合并两个有序链表
        return merge(left, right);
    }

    private ListNode merge(ListNode l1, ListNode l2) {
        ListNode dummyHead = new ListNode(0);
        ListNode curr = dummyHead;
        while (l1 != null && l2 != null) {
            if (l1.val < l2.val) {
                curr.next = l1;
                l1 = l1.next;
            } else {
                curr.next = l2;
                l2 = l2.next;
            }
            curr = curr.next;
        }
        curr.next = (l1 != null) ? l1 : l2;
        return dummyHead.next;
    }
}

三、计数排序

计数排序是一种非比较排序算法,适用于元素范围有限的情况。对于链表排序,我们可以先遍历链表,使用一个数组(或哈希表)来统计每个值出现的次数,然后根据这些计数来重建链表。

算法步骤:

  1. 确定范围:找出待排序数组中最大和最小的元素,确定元素的数值范围。

  2. 创建计数数组:创建一个大小为数值范围的数组(计数数组),初始化计数数组的所有元素为0。

  3. 计数:遍历待排序数组,对每个元素,在计数数组中对应的索引加1。

  4. 计算累计和:将计数数组中的每个元素累加,得到每个元素在排序后数组中的位置。

  5. 放置元素:从后向前遍历待排序数组(这样可以保证相同元素的相对顺序),根据计数数组中的累计和,将元素放到排序后数组的正确位置上,并更新计数数组中的值。

  6. 构建排序数组:构建一个新的数组,按计算出的索引将元素放到新数组中。

复杂度分析:

  • 时间复杂度为 O (n + k),其中 n 是数组长度,k 是数值范围。

  • 空间复杂度为 O (k),因为需要一个额外的数组来存储计数信息。

计数排序的 Java 代码:

复制代码
class Solution {
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }

        // 找到链表的最大值以确定计数数组的大小
        int maxVal = 0;
        ListNode curr = head;
        while (curr != null) {
            maxVal = Math.max(maxVal, curr.val);
            curr = curr.next;
        }

        // 初始化计数数组
        int[] count = new int[maxVal + 1];
        curr = head;
        while (curr != null) {
            count[curr.val]++;
            curr = curr.next;
        }

        // 重建链表
        ListNode dummyHead = new ListNode(0);
        ListNode tail = dummyHead;
        for (int i = 0; i <= maxVal; i++) {
            for (int j = 0; j < count[i]; j++) {
                tail.next = new ListNode(i);
                tail = tail.next;
            }
        }

        return dummyHead.next;
    }
}

总结:

  • 归并排序:适合链表排序,因为它是稳定的,且不需要额外的空间。

  • 快速排序:在链表上实现稍微复杂,但性能通常很好。

  • 计数排序:适用于链表中元素值范围不大的情况,实现简单。

相关推荐
Alfred king4 小时前
Leetcode 四数之和
算法·leetcode·职场和发展·数组·排序·双指针
艾莉丝努力练剑19 小时前
【数据结构与算法】数据结构初阶:详解排序(三)——归并排序:递归版本和非递归版本
c语言·开发语言·数据结构·学习·算法·链表·排序算法
朝朝又沐沐2 天前
算法竞赛阶段二-数据结构(36)数据结构双向链表模拟实现
开发语言·数据结构·c++·算法·链表
艾莉丝努力练剑3 天前
【数据结构与算法】数据结构初阶:详解排序(二)——交换排序中的快速排序
c语言·开发语言·数据结构·学习·算法·链表·排序算法
艾莉丝努力练剑3 天前
【LeetCode&数据结构】二叉树的应用(二)——二叉树的前序遍历问题、二叉树的中序遍历问题、二叉树的后序遍历问题详解
c语言·开发语言·数据结构·学习·算法·leetcode·链表
KarrySmile3 天前
Day04–链表–24. 两两交换链表中的节点,19. 删除链表的倒数第 N 个结点,面试题 02.07. 链表相交,142. 环形链表 II
算法·链表·面试·双指针法·虚拟头结点·环形链表
Codeking__3 天前
链表算法综合——重排链表
网络·算法·链表
此心安处是吾乡10244 天前
数据结构 双向链表
数据结构·链表
Alfred king4 天前
面试150 IPO
面试·职场和发展·贪心·数组··排序