markdown复制## 1. 问题背景与核心需求
快速选择算法是算法竞赛和面试中的高频考点,AcWing 786题要求在一个无序数组中找出第k小的数。常规排序后取第k个元素的方法时间复杂度为O(nlogn),而快速选择算法可以优化到平均O(n)时间复杂度,这对处理大规模数据至关重要。
我在刷题过程中发现,很多同学虽然能写出快速排序,却对快速选择算法的变体束手无策。本文将拆解这个经典问题的三种实现方案,并分享调试时发现的边界陷阱。
## 2. 算法核心思想解析
### 2.1 快速选择与快速排序的异同
快速选择算法脱胎于快速排序的partition过程:
- 相同点:都采用分治思想,通过pivot划分数组
- 不同点:快速排序递归处理两侧子数组,而快速选择只需处理包含第k元素的单侧
关键优化在于每次partition后:
1. 计算左区间元素个数sl
2. 若k ≤ sl,只在左区间递归
3. 否则在右区间寻找第(k-sl)小的数
### 2.2 时间复杂度证明
理想情况下每次partition将数组减半:
T(n) = T(n/2) + O(n)
根据主定理可得O(n)
最坏情况(如已排序数组):
T(n) = T(n-1) + O(n) → O(n²)
可通过随机化pivot避免(后文详述)
## 3. 三种实现方案对比
### 3.1 基础版(Lomuto Partition)
```cpp
int quick_select(int q[], int l, int r, int k) {
if (l >= r) return q[l];
int i = l - 1, j = r + 1, x = q[l + r >> 1];
while (i < j) {
do i++; while (q[i] < x);
do j--; while (q[j] > x);
if (i < j) swap(q[i], q[j]);
}
if (k <= j) return quick_select(q, l, j, k);
else return quick_select(q, j + 1, r, k);
}
注意:这种写法在元素全相同时会退化为O(n²),需特别处理
3.2 随机化版本(避免最坏情况)
cpp复制int partition(int q[], int l, int r) {
swap(q[l], q[l + rand() % (r - l + 1)]); // 随机选择pivot
int i = l, j = r, x = q[l];
while (i < j) {
while (i < j && q[j] >= x) j--;
q[i] = q[j];
while (i < j && q[i] <= x) i++;
q[j] = q[i];
}
q[i] = x;
return i;
}
3.3 迭代版本(节省栈空间)
cpp复制int iterative_select(int q[], int l, int r, int k) {
while (true) {
if (l == r) return q[l];
int i = l, j = r, x = q[l + r >> 1];
while (i <= j) {
while (q[i] < x) i++;
while (q[j] > x) j--;
if (i <= j) swap(q[i++], q[j--]);
}
if (k <= j) r = j;
else if (k >= i) l = i;
else return q[k];
}
}
4. 关键调试经验
4.1 边界条件陷阱
- 当k=0或k>n时的处理
- 元素全相同的情况(死循环风险)
- 递归终止条件应为
l >= r而非l == r
4.2 性能优化技巧
- 三数取中法选择pivot:
cpp复制int mid = l + (r - l) / 2;
if (q[l] > q[r]) swap(q[l], q[r]);
if (q[mid] > q[r]) swap(q[mid], q[r]);
if (q[mid] > q[l]) swap(q[mid], q[l]);
- 小数组切换插入排序:
cpp复制if (r - l < 10) {
insertion_sort(q + l, r - l + 1);
return q[l + k - 1];
}
5. 同类问题扩展
5.1 求前k小的数集合
修改算法在找到第k小数后,直接返回前k个元素
5.2 寻找中位数
当k=n/2时的特例,可结合BFPRT算法优化
5.3 流式数据中的选择
使用堆结构维护top-k,适用于数据动态到达的场景
我在实际编码中发现,当数组包含大量重复元素时,Hoare划分比Lomuto划分更高效。一个常见的错误是在交换元素后忘记移动指针,导致死循环。建议在本地用以下测试用例验证:
- [1,1,1,1,1]
- [3,2,3,1,2,4,5,5,6] k=4
- 已排序的逆序数组
code复制