当前位置: 首页 > news >正文

【Hot 100】 148. 排序链表

目录

  • 引言
  • 十大排序算法
    • 1. 冒泡排序 (Bubble Sort)
    • 2. 选择排序 (Selection Sort)
    • 3. 插入排序 (Insertion Sort)
    • 4. 希尔排序 (Shell Sort)
      • 简单代码说明
      • 关键特点
    • 5. 归并排序 (Merge Sort)
    • 6. 快速排序 (Quick Sort)
    • 7. 堆排序 (Heap Sort)
    • 8. 计数排序 (Counting Sort)
    • 9. 桶排序 (Bucket Sort)
    • 10. 基数排序 (Radix Sort)
    • 总结
  • 排序链表
    • 我的解题
    • 代码优化

请添加图片描述

  • 🙋‍♂️ 作者:海码007
  • 📜 专栏:算法专栏
  • 💥 标题:【Hot 100】 148. 排序链表
  • ❣️ 寄语:书到用时方恨少,事非经过不知难!

引言

今天的题目是对链表进行排序,所以我先简单回顾一下十大排序算法的基本思想以及C++代码实现。

十大排序算法

1. 冒泡排序 (Bubble Sort)

基本思想:重复地遍历要排序的数列,一次比较两个元素,如果顺序错误就交换它们。每次遍历后,最大的元素会"冒泡"到最后。

void bubbleSort(int arr[], int n) {for (int i = 0; i < n-1; i++) {for (int j = 0; j < n-i-1; j++) {if (arr[j] > arr[j+1]) {swap(arr[j], arr[j+1]);}}}
}

2. 选择排序 (Selection Sort)

基本思想:每次从未排序部分选择最小(或最大)的元素,放到已排序部分的末尾。

void selectionSort(int arr[], int n) {for (int i = 0; i < n-1; i++) {int min_idx = i;for (int j = i+1; j < n; j++) {if (arr[j] < arr[min_idx]) {min_idx = j;}}swap(arr[i], arr[min_idx]);}
}

3. 插入排序 (Insertion Sort)

基本思想:将未排序部分的第一个元素插入到已排序部分的适当位置。

void insertionSort(int arr[], int n) {for (int i = 1; i < n; i++) {int key = arr[i];int j = i - 1;while (j >= 0 && arr[j] > key) {arr[j+1] = arr[j];j--;}arr[j+1] = key;}
}

4. 希尔排序 (Shell Sort)

讲解视频(这个视频讲的非常好)

希尔排序就是使用分治思想的插入排序,他使用了插入排序的两个优点:

  1. 如果数组基本有序的话,插入排序的时间复杂度为 O(n)
  2. 数据量较小时,排序效率较高

在这里插入图片描述

用班级排队来理解希尔排序

想象你是一个体育老师,要帮全班同学按身高从矮到高排队。如果直接用插入排序(让每个同学一个个找到自己的位置),对于乱序的队伍会非常耗时。希尔排序的做法更聪明:

  1. 先把同学分成几组:比如先让每间隔4个同学为一组(第1、5、9…个同学一组,第2、6、10…个同学一组,以此类推)

  2. 在组内排好序:让每个小组内部先按身高排好

  3. 缩小分组范围:然后让每间隔2个同学为一组,再次排序

  4. 最后全体一起排:最后取消分组,全体一起做一次完整的插入排序

这时候队伍已经基本有序了,最后这次完整的排序会非常快!

为什么这样更快?

  • 前期分组排序:就像先把大问题拆成小问题解决
  • 逐步细化:每次排序后队伍都更有序一点
  • 最后一步轻松:当队伍基本有序时,插入排序效率最高

简单代码说明

void shellSort(int arr[], int n) {// 初始间隔是数组长度的一半for (int gap = n/2; gap > 0; gap /= 2) {// 对每个分组进行插入排序for (int i = gap; i < n; i++) {int temp = arr[i]; // 记住当前这张"牌"int j;// 把比当前"牌"大的往后移for (j = i; j >= gap && arr[j-gap] > temp; j -= gap) {arr[j] = arr[j-gap];}// 把当前"牌"放到正确位置arr[j] = temp;}}
}

关键特点

  • 不是相邻比较:普通插入排序是相邻元素比较,希尔排序是"跳着比较"
  • 越来越精确:比较的间隔从大到小,最后变成1(就是普通插入排序)
  • 前期工作不白费:每次分组排序都为下一次创造了更好的基础

记住这个算法就像"先粗排,再细排,最后精排"的过程,这样就能既快又好地完成排序任务!

5. 归并排序 (Merge Sort)

基本思想:分治法,将列表分成两半,分别排序,然后合并两个有序列表。

void merge(int arr[], int l, int m, int r) {int n1 = m - l + 1;int n2 = r - m;int L[n1], R[n2];for (int i = 0; i < n1; i++) L[i] = arr[l + i];for (int j = 0; j < n2; j++) R[j] = arr[m + 1 + j];int i = 0, j = 0, k = l;while (i < n1 && j < n2) {if (L[i] <= R[j]) arr[k++] = L[i++];else arr[k++] = R[j++];}while (i < n1) arr[k++] = L[i++];while (j < n2) arr[k++] = R[j++];
}void mergeSort(int arr[], int l, int r) {if (l < r) {int m = l + (r - l) / 2;mergeSort(arr, l, m);mergeSort(arr, m + 1, r);merge(arr, l, m, r);}
}

6. 快速排序 (Quick Sort)

基本思想:分治法,选择一个基准元素,将数组分为两部分,一部分小于基准,一部分大于基准,然后递归排序。

int partition(int arr[], int low, int high) {int pivot = arr[high];int i = low - 1;for (int j = low; j < high; j++) {if (arr[j] < pivot) {i++;swap(arr[i], arr[j]);}}swap(arr[i + 1], arr[high]);return i + 1;
}void quickSort(int arr[], int low, int high) {if (low < high) {int pi = partition(arr, low, high);quickSort(arr, low, pi - 1);quickSort(arr, pi + 1, high);}
}

7. 堆排序 (Heap Sort)

基本思想:利用堆这种数据结构进行排序,首先构建最大堆,然后逐个取出堆顶元素。

void heapify(int arr[], int n, int i) {int largest = i;int l = 2 * i + 1;int r = 2 * i + 2;if (l < n && arr[l] > arr[largest]) largest = l;if (r < n && arr[r] > arr[largest]) largest = r;if (largest != i) {swap(arr[i], arr[largest]);heapify(arr, n, largest);}
}void heapSort(int arr[], int n) {for (int i = n / 2 - 1; i >= 0; i--) {heapify(arr, n, i);}for (int i = n - 1; i > 0; i--) {swap(arr[0], arr[i]);heapify(arr, i, 0);}
}

8. 计数排序 (Counting Sort)

基本思想:适用于整数排序,统计每个元素出现的次数,然后计算每个元素在输出数组中的位置。

void countingSort(int arr[], int n) {int max_val = *max_element(arr, arr + n);int min_val = *min_element(arr, arr + n);int range = max_val - min_val + 1;vector<int> count(range), output(n);for (int i = 0; i < n; i++) count[arr[i] - min_val]++;for (int i = 1; i < range; i++) count[i] += count[i - 1];for (int i = n - 1; i >= 0; i--) {output[count[arr[i] - min_val] - 1] = arr[i];count[arr[i] - min_val]--;}for (int i = 0; i < n; i++) arr[i] = output[i];
}

9. 桶排序 (Bucket Sort)

基本思想:将数组分到有限数量的桶里,每个桶再分别排序(可以使用其他排序算法)。

void bucketSort(float arr[], int n) {vector<vector<float>> buckets(n);for (int i = 0; i < n; i++) {int bi = n * arr[i];buckets[bi].push_back(arr[i]);}for (int i = 0; i < n; i++) {sort(buckets[i].begin(), buckets[i].end());}int index = 0;for (int i = 0; i < n; i++) {for (int j = 0; j < buckets[i].size(); j++) {arr[index++] = buckets[i][j];}}
}

10. 基数排序 (Radix Sort)

基本思想:按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。

void countingSortForRadix(int arr[], int n, int exp) {int output[n];int count[10] = {0};for (int i = 0; i < n; i++) count[(arr[i] / exp) % 10]++;for (int i = 1; i < 10; i++) count[i] += count[i - 1];for (int i = n - 1; i >= 0; i--) {output[count[(arr[i] / exp) % 10] - 1] = arr[i];count[(arr[i] / exp) % 10]--;}for (int i = 0; i < n; i++) arr[i] = output[i];
}void radixSort(int arr[], int n) {int max_val = *max_element(arr, arr + n);for (int exp = 1; max_val / exp > 0; exp *= 10) {countingSortForRadix(arr, n, exp);}
}

总结

1. 冒泡排序 (Bubble Sort)

优点

  • 实现简单,代码容易理解
  • 对于小规模数据效率尚可
  • 稳定排序(相同元素相对位置不变)

缺点

  • 效率低下,时间复杂度高
  • 不适用于大规模数据

复杂度

  • 时间复杂度:
    • 最好:O(n)(已排序情况)
    • 平均:O(n²)
    • 最坏:O(n²)
  • 空间复杂度:O(1)(原地排序)

2. 选择排序 (Selection Sort)

优点

  • 实现简单
  • 不占用额外内存空间
  • 交换次数少(最多n-1次交换)

缺点

  • 时间复杂度高
  • 不稳定排序
  • 对已排序数组仍需同样时间

复杂度

  • 时间复杂度:
    • 最好/平均/最坏:O(n²)
  • 空间复杂度:O(1)

3. 插入排序 (Insertion Sort)

优点

  • 对小规模或基本有序数据效率高
  • 稳定排序
  • 实现简单
  • 原地排序

缺点

  • 大规模乱序数据效率低
  • 最坏情况性能差

复杂度

  • 时间复杂度:
    • 最好:O(n)(已排序)
    • 平均:O(n²)
    • 最坏:O(n²)
  • 空间复杂度:O(1)
  1. 希尔排序 (Shell Sort)

优点

  • 插入排序的改进版,效率更高
  • 适用于中等规模数据
  • 原地排序

缺点

  • 复杂度分析复杂
  • 不稳定排序
  • 增量序列选择影响性能

复杂度

  • 时间复杂度:
    • 最好:O(n)
    • 平均:O(n^1.3)(取决于增量序列)
    • 最坏:O(n²)
  • 空间复杂度:O(1)
  1. 归并排序 (Merge Sort)

优点

  • 稳定排序
  • 时间复杂度稳定为O(nlogn)
  • 适合链表排序
  • 适合外部排序

缺点

  • 需要额外O(n)空间
  • 对小规模数据可能不如插入排序

复杂度

  • 时间复杂度:
    • 最好/平均/最坏:O(nlogn)
  • 空间复杂度:O(n)
  1. 快速排序 (Quick Sort)

优点

  • 平均情况下最快的排序算法
  • 原地排序(优化版本)
  • 缓存友好

缺点

  • 最坏情况O(n²)
  • 不稳定排序
  • 递归实现需要栈空间

复杂度

  • 时间复杂度:
    • 最好/平均:O(nlogn)
    • 最坏:O(n²)(已排序或所有元素相同)
  • 空间复杂度:
    • 平均:O(logn)(递归栈)
    • 最坏:O(n)
  1. 堆排序 (Heap Sort)

优点

  • 时间复杂度稳定
  • 原地排序
  • 适合获取前k个元素

缺点

  • 不稳定排序
  • 缓存不友好(跳跃访问)
  • 实际效率常不如快速排序

复杂度

  • 时间复杂度:
    • 最好/平均/最坏:O(nlogn)
  • 空间复杂度:O(1)
  1. 计数排序 (Counting Sort)

优点

  • 线性时间复杂度
  • 稳定排序(实现得当)

缺点

  • 仅适用于整数且范围不大的情况
  • 需要额外空间

复杂度

  • 时间复杂度:O(n+k)(k是数值范围)
  • 空间复杂度:O(n+k)
  1. 桶排序 (Bucket Sort)

优点

  • 当分布均匀时效率高
  • 稳定排序(实现得当)

缺点

  • 需要知道数据分布情况
  • 最坏情况退化为O(n²)
  • 需要额外空间

复杂度

  • 时间复杂度:
    • 最好:O(n)
    • 平均:O(n+k)(k是桶数量)
    • 最坏:O(n²)
  • 空间复杂度:O(n+k)
  1. 基数排序 (Radix Sort)

优点

  • 线性时间复杂度
  • 稳定排序(实现得当)

缺点

  • 仅适用于整数或特定格式数据
  • 需要额外空间
  • 常数因子较大

复杂度

  • 时间复杂度:O(d(n+k))(d是位数,k是基数)
  • 空间复杂度:O(n+k)
排序算法平均时间复杂度最坏时间复杂度空间复杂度稳定性适用场景
冒泡排序O(n²)O(n²)O(1)稳定小规模数据教学用途
选择排序O(n²)O(n²)O(1)不稳定交换成本高的情况
插入排序O(n²)O(n²)O(1)稳定小规模或基本有序数据
希尔排序O(n^1.3)O(n²)O(1)不稳定中等规模数据
归并排序O(nlogn)O(nlogn)O(n)稳定大规模数据、外部排序
快速排序O(nlogn)O(n²)O(logn)不稳定通用排序、大规模随机数据
堆排序O(nlogn)O(nlogn)O(1)不稳定需要稳定时间复杂度
计数排序O(n+k)O(n+k)O(n+k)稳定小范围整数
桶排序O(n+k)O(n²)O(n+k)稳定均匀分布的数据
基数排序O(d(n+k))O(d(n+k))O(n+k)稳定多位数整数或字符串

实际应用中,快速排序通常是最佳选择,但当需要稳定性或特定数据特性时,其他算法可能更合适。C++中的std::sort通常采用快速排序的优化版本(如内省排序,结合了快速排序、堆排序和插入排序的优点)。

排序链表

  • 🎈 题目链接:
  • 🎈 做题状态:

我的解题

使用了冒泡排序的思想,在排序链表时和排序数组完全是不一样的。因为链表的访问都是从前往后,不能随机访问。所以在实现的时候需要额外处理。

class Solution {
public:ListNode* sortList(ListNode* head) {// 我感觉这道题用插入排序还是比较难写出代码,相比冒泡排序思路代码就好写一点,因为每次都是比较相邻的节点// 边界情况if (head == nullptr || head->next == nullptr) return head;ListNode* tail = nullptr;while (head != tail){ListNode* left = head;ListNode* right = head->next;while(right != tail){if (left->val > right->val){swap(left->val, right->val);}left = left->next;right = right->next;}tail = left;    // 更新结束点位}return head;}
};

代码优化

最适合链表的排序是归并排序,这个时间复杂度还低一点

class Solution {
public:ListNode* sortList(ListNode* head) {return sortList(head, nullptr);}ListNode* sortList(ListNode* head, ListNode* tail) {if (head == nullptr) {return head;}if (head->next == tail) {head->next = nullptr;return head;}ListNode* slow = head, *fast = head;while (fast != tail) {slow = slow->next;fast = fast->next;if (fast != tail) {fast = fast->next;}}ListNode* mid = slow;return merge(sortList(head, mid), sortList(mid, tail));}ListNode* merge(ListNode* head1, ListNode* head2) {ListNode* dummyHead = new ListNode(0);ListNode* temp = dummyHead, *temp1 = head1, *temp2 = head2;while (temp1 != nullptr && temp2 != nullptr) {if (temp1->val <= temp2->val) {temp->next = temp1;temp1 = temp1->next;} else {temp->next = temp2;temp2 = temp2->next;}temp = temp->next;}if (temp1 != nullptr) {temp->next = temp1;} else if (temp2 != nullptr) {temp->next = temp2;}return dummyHead->next;}
};作者:力扣官方题解
链接:https://leetcode.cn/problems/sort-list/solutions/492301/pai-xu-lian-biao-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

相关文章:

  • AI Engine Kernel and GraphProgramming--知识分享1
  • 从有线到无线:冶炼工厂的高效转型
  • 视觉问答论文解析:《Skywork R1V2: Multimodal Hybrid Reinforcement Learning for Reasoning》
  • 数电发票整理:免费实用工具如何高效解析 XML 发票数据
  • 数据采集,埋点模型
  • 论文公式根据章节自动编号教程
  • 阿里云服务迁移实战: 06-切换DNS
  • 10.idea中创建springboot项目_jdk17
  • 大连理工大学选修课——机器学习笔记(1):概述
  • 【Agent】MCP协议 | 用高德MCP Server制作旅游攻略
  • Java 表达式及运算符的优先级与结合性入门
  • Windows 10 环境二进制方式安装 MySQL 8.0.41
  • 异步协程中基于 aiomysql 的异步数据库操作
  • 第五部分:进阶项目实战
  • 2025平航杯—团队赛
  • c#确定按钮5秒自动确定
  • 涨薪技术|0到1学会性能测试第44课-apachetop模块监控
  • iview内存泄漏
  • 【Android】轻松实现实时FPS功能
  • 开源协议全解析:类型、选择与法律风险规避指南
  • 微软上财季净利增长18%:云业务增速环比提高,业绩指引高于预期
  • 荣盛发展股东所持1.17亿股将被司法拍卖,起拍价约1.788亿元
  • 中方拟解除对5名欧洲议会议员制裁?外交部:望中欧立法机构相向而行
  • 解密62个“千亿县”:强者恒强,新兴产业助新晋县崛起
  • 海量数据处于“原矿”状态,数据价值释放如何破局?
  • 五一假期上海地铁部分线路将延时运营,这些调整请查收