排序之选择排序:简单选择排序与堆排序
选择排序,说白了就是「每次挑一个最值出来」。我刚开始学排序时,觉得这思路太直白了——不就是找最小嘛。但后来发现,同样是「选」,简单选择和堆排序的效率差距,能差出几个数量级。今天咱们就把这两种选择排序掰开揉碎,看看它们到底差在哪。
一、简单选择排序
简单选择排序的思路,就像你在一堆扑克牌里找最小的那张。找到后放到最左边,然后从剩下的牌里继续找。嗯,就这么简单。
1. 算法原理
每次从待排序序列中选出最小(或最大)的元素,放到已排序序列的末尾。重复这个过程,直到所有元素排好。
我举个例子:数组 [5, 3, 8, 6, 4]。
- 第一趟:找最小元素 3,与第一个元素 5 交换 → [3, 5, 8, 6, 4]
- 第二趟:从 [5, 8, 6, 4] 中找最小 4,与第二个元素 5 交换 → [3, 4, 8, 6, 5]
- 第三趟:从 [8, 6, 5] 中找最小 5,与第三个元素 8 交换 → [3, 4, 5, 6, 8]
- 第四趟:从 [6, 8] 中找最小 6,位置不变 → [3, 4, 5, 6, 8]
2. 代码实现
void selectSort(int arr[], int n) {
int i, j, minIdx, tmp;
for (i = 0; i < n - 1; i++) {
minIdx = i;
for (j = i + 1; j < n; j++) {
if (arr[j] < arr[minIdx]) {
minIdx = j;
}
}
if (minIdx != i) {
tmp = arr[i];
arr[i] = arr[minIdx];
arr[minIdx] = tmp;
}
}
}
3. 性能分析
| 指标 | 值 |
|---|---|
| 最好时间复杂度 | O(n²) |
| 最坏时间复杂度 | O(n²) |
| 平均时间复杂度 | O(n²) |
| 空间复杂度 | O(1) |
| 稳定性 | 不稳定 |
你可能会问:为什么最好和最坏都是 O(n²)?因为不管数据原本是什么顺序,它都得老老实实把每个元素都比一遍。说白了,它不关心数据是否已经有序,每次都从头找到尾。
二、堆排序
堆排序,说白了就是「用堆这种数据结构来加速选择」。简单选择排序每次找最小元素都要遍历整个数组,太慢了。堆排序通过维护一个堆,每次取堆顶元素,然后调整堆,把找最小的时间从 O(n) 降到了 O(log n)。
1. 堆的概念
堆是一棵完全二叉树,分为大顶堆和小顶堆:
- 大顶堆:每个节点的值都大于等于其子节点的值。堆顶是最大值。
- 小顶堆:每个节点的值都小于等于其子节点的值。堆顶是最小值。
堆排序通常用大顶堆来实现升序排序。为什么?因为每次把堆顶(最大值)放到数组末尾,剩下的元素再调整成大顶堆,重复下去就得到升序序列了。
2. 堆的调整(下沉操作)
堆调整是堆排序的核心操作。它的作用是把一个「除了根节点外,左右子树都已经是堆」的二叉树,调整成一个完整的堆。
void heapify(int arr[], int n, int i) {
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < n && arr[left] > arr[largest])
largest = left;
if (right < n && arr[right] > arr[largest])
largest = right;
if (largest != i) {
int tmp = arr[i];
arr[i] = arr[largest];
arr[largest] = tmp;
heapify(arr, n, largest);
}
}
这个函数做的事情很简单:找到当前节点和它的左右子节点中最大的那个,如果最大的不是当前节点,就交换,然后递归调整被交换下去的那个子树。
3. 建堆
建堆就是从最后一个非叶子节点开始,从下往上依次做堆调整。
void buildHeap(int arr[], int n) {
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}
}
为什么从 n/2 - 1 开始?因为最后一个非叶子节点的索引就是 n/2 - 1。叶子节点不需要调整,它们本身就是「堆」。
4. 堆排序的完整实现
void heapSort(int arr[], int n) {
// 第一步:建堆
buildHeap(arr, n);
// 第二步:逐个取出堆顶元素
for (int i = n - 1; i > 0; i--) {
// 将堆顶(最大值)与末尾元素交换
int tmp = arr[0];
arr[0] = arr[i];
arr[i] = tmp;
// 调整剩余元素为大顶堆
heapify(arr, i, 0);
}
}
整个过程分两步:先建堆,然后反复取堆顶、调整堆。每次取出的最大值都放到数组末尾,最终数组就是升序的。
5. 性能分析
| 指标 | 值 |
|---|---|
| 最好时间复杂度 | O(n log n) |
| 最坏时间复杂度 | O(n log n) |
| 平均时间复杂度 | O(n log n) |
| 空间复杂度 | O(1) |
| 稳定性 | 不稳定 |
堆排序最大的优点是:不管数据怎么分布,它都能稳定在 O(n log n)。不像快速排序,最坏情况会退化到 O(n²)。我在一些对实时性要求高的嵌入式系统里,就喜欢用堆排序——它的性能是可预测的。
三、两种选择排序的对比
| 对比项 | 简单选择排序 | 堆排序 |
|---|---|---|
| 时间复杂度 | O(n²) | O(n log n) |
| 空间复杂度 | O(1) | O(1) |
| 交换次数 | 最多 n-1 次 | 约 n log n 次 |
| 适用场景 | 数据量小(<1000),交换成本高 | 数据量大,需要稳定性能 |
| 实现复杂度 | 简单 | 中等 |
简单选择排序虽然慢,但代码简单,不容易出错。堆排序虽然快,但实现起来要注意边界条件。我个人建议:如果你在写一个快速原型,数据量不大,用简单选择排序就够了。但如果是正式产品,尤其是数据量不确定的情况下,堆排序是更稳妥的选择。
四、知识体系图
这张图把选择排序的两种实现方式梳理得很清楚了。左边是简单选择排序,右边是堆排序。它们的核心区别就在于「如何找最小/最大元素」——简单选择是暴力遍历,堆排序是用堆来加速。
好了,选择排序就讲到这里。记住一句话:选择排序的核心是「每次选一个最值」,而堆排序就是给这个「选」的过程装上了加速器。
公众号:蓝海资料掘金营,微信deep3321