[数据结构·排序] 快速选择算法
针对从乱序数组中找到第 $k$ 小元素的问题,最经典且高效的算法是 快速选择算法 (Quick Select)。
虽然普通的排序(如快速排序、归并排序)可以将数组全排好后取第 $k$ 个,时间复杂度是 $O(N \log N)$,但如果只要第 $k$ 个,其实没必要把所有人都有序排列。
以下是几种针对该场景的方法,按推荐程度排序:
1. 快速选择算法 (Quick Select) —— 最推荐
这是基于快速排序 (Quick Sort) 思想的变种算法。
核心思想:
在快排中,我们每次选取一个基准值 (Pivot),执行 Partition 操作,将小于 Pivot 的放左边,大于 Pivot 的放右边。
快排:需要递归处理左右两边。
快选:Partition 后,我们可以知道 Pivot 最终所在的下标
index。如果
index == k-1,那 Pivot 就是我们要找的目标,直接返回。如果
index > k-1,说明目标在左半区,我们只递归左边。如果
index < k-1,说明目标在右半区,我们只递归右边。
时间复杂度:
平均情况:$O(N)$。因为每次大约甩掉一半的数据($N + N/2 + N/4 + ... = 2N$)。
最坏情况:$O(N^2)$(极少出现,除非数组本身有序且每次选最差 Pivot)。
空间复杂度:$O(1)$ (迭代写法)或 $O(\log N)$ (递归栈)。
C++ 代码实现 (结合你的 C++ 背景)
C++
1 |
|
2. 堆排序 (Heap Select) —— 适合处理海量数据流
如果你不能一次性把数据都读进内存(比如数据是源源不断过来的流),或者 $k$ 远小于 $N$ 时,堆是很好的选择。
核心思想:
维护一个容量为 $k$ 的大顶堆 (Max Heap)。
先把前 $k$ 个元素入堆。
从第 $k+1$ 个元素开始遍历:
如果当前元素 < 堆顶元素,说明当前元素比堆里最大的那个更小,更有可能是“前 $k$ 小”的一员。
弹出堆顶,压入当前元素。
遍历结束后,堆顶元素就是第 $k$ 小的数(因为堆里存的是最小的 $k$ 个数,而堆顶是这 $k$ 个里最大的,即第 $k$ 小)。
时间复杂度:$O(N \log k)$。
空间复杂度:$O(k)$。
3. C++ STL 的“作弊”方法 (std::nth_element)
作为 C++ 学生,在实际工程或比赛中(非手写算法题),你应该直接使用标准库。
C++ STL 提供了一个专门针对这个问题的函数 std::nth_element。它底层通常使用的是 Introselect 算法(混合了快速选择,当递归太深时转为堆排序,保证最坏也是 $O(N)$)。
C++
1 |
|
总结建议
面试/考试手写算法:首选 快速选择算法 (Quick Select),因为它的平均时间复杂度是线性的 $O(N)$。
实际开发/工程:直接使用
std::nth_element,它经过了极致优化。海量数据/数据流:使用 容量为 k 的大顶堆。
