快速选择算法
算法简介
快速选择算法是一种用于在未排序列表中找到第k小(或第k大)元素的高效算法。它由计算机科学家托尼·霍尔在1961年提出,是快速排序算法的一个变种。如不了解快速排序算法,可参考——八大排序算法
与快速排序不同,快速选择只递归处理包含目标元素的那部分数组,而不是对整个数组进行完全排序,这使得它在平均情况下具有更好的时间复杂度。
算法思想
快速选择基于分治策略和分区操作,核心思想可以概括为:
- 选择一个基准元素(pivot)
- 将数组分为三部分:小于基准的元素,等于基准值元素,大于等于基准的元素
- 根据分区结果决定递归处理哪一部分
算法步骤
function quickSelect(arr, k):
if arr.length == 1:
return arr[0]
pivot = 选择基准元素(arr)//一般使用随机选取基准值优化效率
left = 所有小于pivot的元素
right = 所有大于等于pivot的元素
if k <= left.length:
return quickSelect(left, k)
else if k == left.length + 1:
return pivot
else:
return quickSelect(right, k - left.length - 1)
时间复杂度证明
直观理解
快速选择算法的时间复杂度分析可以通过以下直观方式理解:
- 第一次分区:处理n个元素,时间复杂度O(n)
- 第二次分区:处理约n/2个元素,时间复杂度O(n/2)
- 第三次分区:处理约n/4个元素,时间复杂度O(n/4)
- 依此类推…
总时间复杂度 = O(n + n/2 + n/4 + n/8 + …) ≈ O(2n) = O(n)
数学证明
设T(n)为快速选择算法处理n个元素的期望时间复杂度。
在随机选择基准的情况下,分区操作大致将数组分成两个部分,大小分别为αn和(1-α)n,其中0<α<1。
递归关系为:T(n) = T(αn) + O(n)
使用主定理(Master Theorem)或展开递归树:
T(n) = T(αn) + cn
= T(α²n) + cαn + cn
= T(α³n) + cα²n + cαn + cn
= ...
= T(1) + cn(1 + α + α² + α³ + ...)
由于0<α<1,几何级数1 + α + α² + α³ + …收敛到1/(1-α)
因此,T(n) = O(n × 1/(1-α)) = O(n)
最坏情况分析
最坏情况发生在每次分区都极不均衡时(如已排序数组且选择端点作为基准):
- 第一次分区:处理n个元素
- 第二次分区:处理n-1个元素
- 第三次分区:处理n-2个元素
- …
- 第n次分区:处理1个元素
总时间复杂度 = O(n + (n-1) + (n-2) + … + 1) = O(n²)
代码实现
void sortSelect(vector<int>& nums,int l,int r)
{
if(l>=r)return;
int key=getRandom(nums,l,r);
int left=l-1,cur=l,right=r+1;
while(cur<right)
{
if(nums[cur]<key)swap(nums[++left],nums[cur++]);
else if(nums[cur]==key)++cur;
else if(nums[cur]>key)swap(nums[cur],nums[--right]);
}
//经过一次划分之后,分为三个区间,<key,==key,>key
//对应的区间范围为,key[l,left] ==key[left+1,right-1] >key[rihgt,r]
sortSelect(nums,l,left);
sortSelect(nums,right,r);
}
//随机选者基准值优化
int getRandom(vector<int>& nums,int left,int right)
{
int r=rand();
return nums[left+r%(right-left+1)];
}
使用前记得种下随机数种子
时间复杂度分析
| 情况 | 时间复杂度 | 说明 |
|---|---|---|
| 平均情况 | O(n) | 每次分区大致将问题规模减半 |
| 最坏情况 | O(n²) | 每次选择最差基准,如已排序数组 |
| 最佳情况 | O(n) | 每次都能均匀分区 |
空间复杂度:
- 迭代版本: O(1)
- 递归版本: O(log n)(递归栈深度)
优化策略
- 随机化基准选择:避免最坏情况发生
- 三数取中法:选择左端、中间、右端三个元素的中位数作为基准
- 迭代实现:避免递归调用栈溢出
- 小数组使用插入排序:当子数组较小时切换到插入排序
使用场景
适用情况
- 查找中位数:快速找到数组的中位数
- Top K 问题:查找前K大或前K小的元素
- 顺序统计:需要获取特定顺序位置的元素
- 大数据处理:当数据量太大无法完全排序时
实际应用
- 成绩排名系统:快速找到第K名学生的成绩
- 数据分析:计算数据的百分位数
- 资源调度:选择优先级第K高的任务
- 机器学习:K近邻算法中的距离计算优化
与其他算法对比
| 算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|---|
| 快速选择 | O(n) | O(n²) | O(1) | 单次查询,大数据量 |
| 堆排序 | O(n log k) | O(n log k) | O(k) | 多次查询,动态数据 |
| 完全排序 | O(n log n) | O(n log n) | O(1) | 需要完全有序结果 |
| BFPRT算法 | O(n) | O(n) | O(1) | 保证线性时间,复杂实现 |
题目
颜色分类

思路解析:
题目说得很清楚,就是要我们对该数组进行排序;因此可以使用各种排序算法解决;但仔细观察就能发现,要排序的数只有0,1,2这三种类型,所以该数组中的数会大量重复,所以此时使用快速选择算法是十分合适的

代码实现:
class Solution
{
public:
void sortColors(vector<int>& nums)
{
int left=-1,cur=0,right=nums.size();
while(cur<right)
{
//小于基准值
if(nums[cur]<1)swap(nums[++left],nums[cur++]);
//等于基准值
else if(nums[cur]==1)++cur;
//大于基准值
else if(nums[cur]>1)swap(nums[cur],nums[--right]);
}
}
};
排序数组

思路解析:
依旧是对数组进行排序,但是有时间复杂度的要求
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn),所以可以直接使用时间复杂度为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)的排序算法解答。但是,可能快排的名声太大了,该题中直接使用快排是无法通过的,测试用例中专门给快排留了一手,基础的快排无法通过;所以这里使用快速选择算法。

class Solution
{
public:
vector<int> sortArray(vector<int>& nums)
{
//快排必须优化否则x
srand(time(nullptr));
qsort(nums,0,nums.size()-1);
return nums;
}
void qsort(vector<int>& nums,int l,int r)
{
if(l>=r)return;
int key=getRandom(nums,l,r);//随机选取
int left=l-1,cur=l,right=r+1;
while(cur<right)
{
if(nums[cur]<key)swap(nums[++left],nums[cur++]);
else if(nums[cur]==key)++cur;
else if(nums[cur]>key)swap(nums[cur],nums[--right]);
}
//经过一次划分之后,分为三个区间,<key,==key,>key
//对应的区间范围为,key[l,left] ==key[left+1,right-1] >key[rihgt,r]
qsort(nums,l,left);
qsort(nums,right,r);
}
int getRandom(vector<int>& nums,int left,int right)
{
int r=rand();
return nums[left+r%(right-left+1)];
}
};
数组中的第K个最大元素

思路解析: 在快排中,当我们把数组分成三块之后: [l, left] [left + 1, right - 1] [right, r] ,我们可以通过计算每⼀个区间内元素的个数,进而推断出我们要找的元素是在哪一个区间里面;那么我们可以直接去相应的区间去寻找最终结果就好了。并且时间复杂度要求为
O
(
N
)
O(N)
O(N),也符合快速选择排序。

代码实现:
class Solution
{
public:
int findKthLargest(vector<int>& nums, int k)
{
return qsort(nums,0,nums.size()-1,k);
}
int qsort(vector<int>& nums,int l,int r,int k)
{
if(l>=r) return nums[l];//一个值,直接返回;
int key=getRandom(nums,l,r);
int left=l-1,cur=l,right=r+1;
while(cur<right)
{
int x=nums[cur];
if(x<key)swap(nums[++left],nums[cur++]);
else if(x==key)++cur;
else swap(nums[--right],nums[cur]);
}
//三个区间的个数
int c=r-right+1,b=right-left-1;
//看看在哪个区间以便确认要不要子再次缩小范围
//需要继续缩小区间(一般转化情况需要由:1->3->2)
if(c>=k) return qsort(nums,right,r,k);//1
else if(b+c>=k)return key;//2;到这里才真正返回
//
else return qsort(nums,l,left,k-b-c);//2;不能用size,区间会缩小
}
int getRandom(vector<int>& nums,int left,int right)
{
int r=rand();
return nums[r%(right-left+1)+left];
}
};
详细步骤已在注释中,不清楚可参考。
总结
快速选择算法是一种高效的选择算法,特别适用于在未排序数组中查找第K小(或第K大)元素的场景。通过巧妙的分区策略和随机化技术,它在平均情况下能够达到线性时间复杂度,是处理大数据量选择问题的理想选择。
虽然存在最坏情况下的O(n²)时间复杂度,但通过合理的优化策略(如随机化基准选择、三数取中法),在实际应用中通常能够获得很好的性能表现。对于需要保证线性时间复杂度的场景,可以考虑使用更复杂的BFPRT算法。
352

被折叠的 条评论
为什么被折叠?



