目录
- 引言
- 十大排序算法
-
- [1. 冒泡排序 (Bubble Sort)](#1. 冒泡排序 (Bubble Sort))
- [2. 选择排序 (Selection Sort)](#2. 选择排序 (Selection Sort))
- [3. 插入排序 (Insertion Sort)](#3. 插入排序 (Insertion Sort))
- [4. 希尔排序 (Shell Sort)](#4. 希尔排序 (Shell Sort))
- [5. 归并排序 (Merge Sort)](#5. 归并排序 (Merge Sort))
- [6. 快速排序 (Quick Sort)](#6. 快速排序 (Quick Sort))
- [7. 堆排序 (Heap Sort)](#7. 堆排序 (Heap Sort))
- [8. 计数排序 (Counting Sort)](#8. 计数排序 (Counting Sort))
- [9. 桶排序 (Bucket Sort)](#9. 桶排序 (Bucket Sort))
- [10. 基数排序 (Radix Sort)](#10. 基数排序 (Radix Sort))
- 总结
- 排序链表

引言
今天的题目是对链表进行排序,所以我先简单回顾一下十大排序算法的基本思想以及C++代码实现。
十大排序算法
1. 冒泡排序 (Bubble Sort)
基本思想:重复地遍历要排序的数列,一次比较两个元素,如果顺序错误就交换它们。每次遍历后,最大的元素会"冒泡"到最后。
cpp
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)
基本思想:每次从未排序部分选择最小(或最大)的元素,放到已排序部分的末尾。
cpp
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)
基本思想:将未排序部分的第一个元素插入到已排序部分的适当位置。
cpp
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)
讲解视频(这个视频讲的非常好)
希尔排序就是使用分治思想的插入排序,他使用了插入排序的两个优点:
- 如果数组基本有序的话,插入排序的时间复杂度为 O(n)
- 数据量较小时,排序效率较高

用班级排队来理解希尔排序
想象你是一个体育老师,要帮全班同学按身高从矮到高排队。如果直接用插入排序(让每个同学一个个找到自己的位置),对于乱序的队伍会非常耗时。希尔排序的做法更聪明:
-
先把同学分成几组:比如先让每间隔4个同学为一组(第1、5、9...个同学一组,第2、6、10...个同学一组,以此类推)
-
在组内排好序:让每个小组内部先按身高排好
-
缩小分组范围:然后让每间隔2个同学为一组,再次排序
-
最后全体一起排:最后取消分组,全体一起做一次完整的插入排序
这时候队伍已经基本有序了,最后这次完整的排序会非常快!
为什么这样更快?
- 前期分组排序:就像先把大问题拆成小问题解决
- 逐步细化:每次排序后队伍都更有序一点
- 最后一步轻松:当队伍基本有序时,插入排序效率最高
简单代码说明
cpp
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)
基本思想:分治法,将列表分成两半,分别排序,然后合并两个有序列表。
cpp
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)
基本思想:分治法,选择一个基准元素,将数组分为两部分,一部分小于基准,一部分大于基准,然后递归排序。
cpp
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)
基本思想:利用堆这种数据结构进行排序,首先构建最大堆,然后逐个取出堆顶元素。
cpp
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)
基本思想:适用于整数排序,统计每个元素出现的次数,然后计算每个元素在输出数组中的位置。
cpp
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)
基本思想:将数组分到有限数量的桶里,每个桶再分别排序(可以使用其他排序算法)。
cpp
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)
基本思想:按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。
cpp
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)
- 希尔排序 (Shell Sort)
优点:
- 插入排序的改进版,效率更高
- 适用于中等规模数据
- 原地排序
缺点:
- 复杂度分析复杂
- 不稳定排序
- 增量序列选择影响性能
复杂度:
- 时间复杂度:
- 最好:O(n)
- 平均:O(n^1.3)(取决于增量序列)
- 最坏:O(n²)
- 空间复杂度:O(1)
- 归并排序 (Merge Sort)
优点:
- 稳定排序
- 时间复杂度稳定为O(nlogn)
- 适合链表排序
- 适合外部排序
缺点:
- 需要额外O(n)空间
- 对小规模数据可能不如插入排序
复杂度:
- 时间复杂度:
- 最好/平均/最坏:O(nlogn)
- 空间复杂度:O(n)
- 快速排序 (Quick Sort)
优点:
- 平均情况下最快的排序算法
- 原地排序(优化版本)
- 缓存友好
缺点:
- 最坏情况O(n²)
- 不稳定排序
- 递归实现需要栈空间
复杂度:
- 时间复杂度:
- 最好/平均:O(nlogn)
- 最坏:O(n²)(已排序或所有元素相同)
- 空间复杂度:
- 平均:O(logn)(递归栈)
- 最坏:O(n)
- 堆排序 (Heap Sort)
优点:
- 时间复杂度稳定
- 原地排序
- 适合获取前k个元素
缺点:
- 不稳定排序
- 缓存不友好(跳跃访问)
- 实际效率常不如快速排序
复杂度:
- 时间复杂度:
- 最好/平均/最坏:O(nlogn)
- 空间复杂度:O(1)
- 计数排序 (Counting Sort)
优点:
- 线性时间复杂度
- 稳定排序(实现得当)
缺点:
- 仅适用于整数且范围不大的情况
- 需要额外空间
复杂度:
- 时间复杂度:O(n+k)(k是数值范围)
- 空间复杂度:O(n+k)
- 桶排序 (Bucket Sort)
优点:
- 当分布均匀时效率高
- 稳定排序(实现得当)
缺点:
- 需要知道数据分布情况
- 最坏情况退化为O(n²)
- 需要额外空间
复杂度:
- 时间复杂度:
- 最好:O(n)
- 平均:O(n+k)(k是桶数量)
- 最坏:O(n²)
- 空间复杂度:O(n+k)
- 基数排序 (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通常采用快速排序的优化版本(如内省排序,结合了快速排序、堆排序和插入排序的优点)。
排序链表
- 🎈 题目链接:
- 🎈 做题状态:
我的解题
使用了冒泡排序的思想,在排序链表时和排序数组完全是不一样的。因为链表的访问都是从前往后,不能随机访问。所以在实现的时候需要额外处理。
cpp
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;
}
};
代码优化
最适合链表的排序是归并排序,这个时间复杂度还低一点
cpp
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)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。