【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)
讲解视频(这个视频讲的非常好)
希尔排序就是使用分治思想的插入排序,他使用了插入排序的两个优点:
- 如果数组基本有序的话,插入排序的时间复杂度为 O(n)
- 数据量较小时,排序效率较高
用班级排队来理解希尔排序
想象你是一个体育老师,要帮全班同学按身高从矮到高排队。如果直接用插入排序(让每个同学一个个找到自己的位置),对于乱序的队伍会非常耗时。希尔排序的做法更聪明:
-
先把同学分成几组:比如先让每间隔4个同学为一组(第1、5、9…个同学一组,第2、6、10…个同学一组,以此类推)
-
在组内排好序:让每个小组内部先按身高排好
-
缩小分组范围:然后让每间隔2个同学为一组,再次排序
-
最后全体一起排:最后取消分组,全体一起做一次完整的插入排序
这时候队伍已经基本有序了,最后这次完整的排序会非常快!
为什么这样更快?
- 前期分组排序:就像先把大问题拆成小问题解决
- 逐步细化:每次排序后队伍都更有序一点
- 最后一步轻松:当队伍基本有序时,插入排序效率最高
简单代码说明
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)
- 希尔排序 (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通常采用快速排序的优化版本(如内省排序,结合了快速排序、堆排序和插入排序的优点)。
排序链表
- 🎈 题目链接:
- 🎈 做题状态:
我的解题
使用了冒泡排序的思想,在排序链表时和排序数组完全是不一样的。因为链表的访问都是从前往后,不能随机访问。所以在实现的时候需要额外处理。
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)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。