题目:给你链表的头结点 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;
}
}

总结:

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

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

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

【LeetCode 148】算法进阶:排序链表 ( 归并排序、快速排序、计数排序 )的更多相关文章

  1. 八大排序方法汇总(选择排序,插入排序-简单插入排序、shell排序,交换排序-冒泡排序、快速排序、堆排序,归并排序,计数排序)

    2013-08-22 14:55:33 八大排序方法汇总(选择排序-简单选择排序.堆排序,插入排序-简单插入排序.shell排序,交换排序-冒泡排序.快速排序,归并排序,计数排序). 插入排序还可以和 ...

  2. C# 插入排序 冒泡排序 选择排序 高速排序 堆排序 归并排序 基数排序 希尔排序

    C# 插入排序 冒泡排序 选择排序 高速排序 堆排序 归并排序 基数排序 希尔排序 以下列出了数据结构与算法的八种基本排序:插入排序 冒泡排序 选择排序 高速排序 堆排序 归并排序 基数排序 希尔排序 ...

  3. [Leetcode]148. 排序链表(归并排序)

    题目 在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序. 示例 1: 输入: 4->2->1->3 输出: 1->2->3->4 示例 2: ...

  4. [LeetCode] 148. 排序链表 ☆☆☆(归并排序)

    148.排序链表 描述 在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序. 示例 1: 输入: 4->2->1->3输出: 1->2->3-> ...

  5. C#版 - LeetCode 148. Sort List 解题报告(归并排序小结)

    leetcode 148. Sort List 提交网址: https://leetcode.com/problems/sort-list/  Total Accepted: 68702 Total ...

  6. 数据结构与算法-排序(八)计数排序(Counting Sort)

    摘要 计数排序本质就是统计不同元素出现的次数,然后将元素依次从小到大放置,每个元素看统计的次数,就紧挨着放置几个同样的元素. 看似简单的处理,在算法中,会依据统计的元素次数推算出每个元素的索引位置,这 ...

  7. Python线性时间排序——桶排序、基数排序与计数排序

    1. 桶排序 1.1 范围为1-M的桶排序 如果有一个数组A,包含N个整数,值从1到M,我们可以得到一种非常快速的排序,桶排序(bucket sort).留置一个数组S,里面含有M个桶,初始化为0.然 ...

  8. 第2章 排序 | 第10节 计数排序练习题 && 基数排序

    对于一个int数组,请编写一个计数排序算法,对数组元素排序. 给定一个int数组A及数组的大小n,请返回排序后的数组. 测试样例: [1,2,3,5,2,3],6 [1,2,2,3,3,5] 计数排序 ...

  9. 【程序员笔试面试必会——排序②】Python实现 计数排序、基数排序

    一.计数排序 概要: 时间复杂度O(n),空间复杂度O(k),k是输入序列的值的范围(最大值-最小值),是稳定的.计数排序一般用于已知输入值的范围相对较小,比如给公司员工的身高体重信息排序. 思路: ...

  10. php基础排序算法 冒泡排序 选择排序 插入排序 归并排序 快速排序

    <?php$arr=array(12,25,56,1,75,13,58,99,22);//冒泡排序function sortnum($arr){    $num=count($arr);    ...

随机推荐

  1. HSRP、GLBP、VRRP、NSRP 协议对比与配置指南

    HSRP.GLBP.VRRP.NSRP 协议对比与配置指南 一.协议对比表 特性 HSRP (Cisco) GLBP (Cisco) VRRP (标准协议) NSRP (Juniper) 协议类型 思 ...

  2. 【uni-app】在windows10系统中HBuliderX用iPhone苹果手机进行调试运行详细说明

    测试准备: 1)iphone13  ios18.4.1  和一根可以读取数据的苹果线 2)HBuliderX打开uni-app项目文件(项目图标是正方形内一个U) 3)windows10系统 测试目标 ...

  3. jupyter的使用 -- 快捷键

    jupyter的使用 1.快捷键的使用 插入cell:a,b 删除cell:x 执行cell:shift+enter 切换cell的模式:m,y cell执行后,在cell的左侧双击就可以回到cell ...

  4. 「Log」2023.8.29 小记

    序幕 早上下雨了,七点到校,还是先整理博客. 今天是生日,发条犇犇纪念一下,16 岁了! 学长进行杂题选讲,一些 KD-Tree.根号分治.生成树题,大部分是图相关,高低胡上两道. 补一些题 \(\c ...

  5. 阿里云数据库Inventory Hint技术分析

    秒杀场景是电商系统中最具挑战性的场景之一,其核心痛点在于超高并发请求(百万级甚至千万级QPS) 与 有限库存 之间的矛盾,以及由此引发的 系统崩溃.超卖.不公平 等问题.阿里通过一套精密的架构和算法组 ...

  6. vue.js+vuetify学习开发排坑:一个古怪的代码 v-slot:activator="{ on, attrs }"

    由于需要全栈开发一个售票系统项目,时隔一年后重新捡回了我的前端技术~ 开发习惯是边看文档边做,然后再vuetify这个MD设计的UI元件库翻来翻去,再涉及到元件交互的时候有几段代码不是很能理解 < ...

  7. 实战干货|Spark 在袋鼠云数栈的深度探索与实践

    Spark 是一个快速.通用.可扩展的大数据计算引擎,具有高性能.易用.容错.可以与 Hadoop 生态无缝集成.社区活跃度高等优点.在实际使用中,具有广泛的应用场景: · 数据清洗和预处理:在大数据 ...

  8. 性能、安全和稳定,袋鼠云数据服务平台 DataAPI 为企业 API 保驾护航

    通过API 对外提供数据服务是大部分企业中比较常见的数据应用方式,对于 API 平台管理者.开发者和调用者来说,API 的调用性能.安全性和稳定性是在平台选型时最需要考虑的三个因素. 袋鼠云API开发 ...

  9. 数栈V6.0全新产品矩阵发布,数据底座 EasyMR 焕新升级

    4月20日,袋鼠云成功举行了以"数实融合,韧性生长"为主题的2023春季生长大会.会上,袋鼠云自主研发的一站式大数据基础软件--数栈V6.0产品矩阵全新发布.对旗下大数据基础平台. ...

  10. 速看!新版SpringAI的2个致命问题

    无论是使用最新正式版的 Spring AI,还是最新正式版 Spring AI Alibaba,在实现自定义 MCP 服务器端和客户端的时候,一定要注意这两个问题,不然你会发现你的 MCP 服务器端能 ...