简单高效的原地排序算法
选择排序(Selection Sort)是一种简单直观的排序算法。它的工作原理如下:
简单来说:每一轮从未排序的部分中,选择一个最小的元素,放到已排序部分的末尾。
[5, 8, 5, 2, 9],第一轮找到 2,与第一个 5 交换,变成 [2, 8, 5, 5, 9],两个 5 的相对顺序改变了。下一页将进行详细的动态可视化演示。
点击“开始排序”按钮进行可视化演示
选择排序的 C++ 实现通常使用嵌套循环来完成。外层循环确定当前要放置最小元素的位置,内层循环则负责在未排序部分中查找最小元素的索引。
i 从 0 到 n-2 (最后一个元素自然有序)。j 从 i+1 到 n-1,用于在 a[i...n-1] 范围内寻找最小值。minIdx 记录当前找到的最小元素的索引,初始设为 i。minIdx 不等于 i,则交换 a[i] 和 a[minIdx]。点击按钮查看带有详细注释的完整 C++ 代码,并支持一键复制。
j 通常从何处开始?现在可以尝试下面的编程练习了。
修改标准的升序选择排序算法,实现一个函数 void selectionSortDescending(vector,使得数组 a 中的元素按降序(从大到小)排列。
提示:主要改动在于内层循环比较逻辑,寻找最大值而不是最小值。
利用选择排序的思想(每轮找到一个最值),编写一个函数 vector,返回输入数组 a 中前 k 个最小的元素(无需排序这 k 个元素,只需找到它们即可)。请注意,不要修改原数组 a。
提示:可以运行选择排序的前 k 轮,或者修改选择排序逻辑只记录和返回前 k 个最小值。
在内层循环找到最小元素索引 minIdx 后,如果 minIdx 恰好就是当前位置 i,说明 a[i] 已经是未排序部分的最小值,此时无需执行交换操作。这可以略微优化性能,尤其是在数组部分有序的情况下,可以减少写操作。
// 在交换前添加判断
if (minIdx != i) {
swap(a[i], a[minIdx]);
}
注意:这个优化并不会改变算法的 O(n²) 时间复杂度,因为比较次数并未减少。
实现时要特别注意循环的边界条件,防止数组越界访问:
i 的范围通常是 0 到 n-2 (或 < n-1)。当 i 到达 n-1 时,只剩一个元素,无需再排序。j 的范围通常是 i+1 到 n-1 (或 < n)。确保 j 始终在数组的有效索引范围内。选择排序的主要优点是实现简单、空间复杂度低(O(1))。但其 O(n²) 的时间复杂度使其不适合处理大规模数据集。
它主要用于:
| 算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 | 特点 |
|---|---|---|---|---|---|
| 选择排序 | O(n²) | O(n²) | O(1) | 不稳定 | 比较次数固定,交换次数少 (O(n)) |
| 冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 | 实现简单,有序时可优化至 O(n) |
| 插入排序 | O(n²) | O(n²) | O(1) | 稳定 | 对部分有序数据效率较高,接近 O(n) |
选择排序每轮需要 O(n) 时间找到最小值。如果能用更高效的方式找到最小值,就能改进算法。堆排序(Heap Sort) 正是利用了堆(Heap)这种数据结构。
堆可以在 O(log n) 时间内找到并移除最值,同时维护堆的性质。通过先将数组构建成一个最大堆(或最小堆),然后重复取出堆顶元素(最值)放到数组末尾,堆排序将整体时间复杂度优化到了 O(n log n)。它同样是原地排序(O(1) 空间),但也是不稳定的。
可以认为堆排序是选择排序在选择最小值这一步上的高效优化版本。
排序算法的稳定性指的是:如果数组中有两个相等的元素,排序后它们的相对位置保持不变。
稳定性在某些场景下非常重要:
选择排序由于其交换操作的性质,通常是不稳定的。
你已经完成了选择排序的学习!🎉