简单高效的原地排序算法
选择排序(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) 空间),但也是不稳定的。
可以认为堆排序是选择排序在选择最小值这一步上的高效优化版本。
排序算法的稳定性指的是:如果数组中有两个相等的元素,排序后它们的相对位置保持不变。
稳定性在某些场景下非常重要:
选择排序由于其交换操作的性质,通常是不稳定的。
你已经完成了选择排序的学习!🎉