快速选择算法

快速选择算法

算法简介

快速选择算法是一种用于在未排序列表中找到第k小(或第k大)元素的高效算法。它由计算机科学家托尼·霍尔在1961年提出,是快速排序算法的一个变种。如不了解快速排序算法,可参考——八大排序算法

与快速排序不同,快速选择只递归处理包含目标元素的那部分数组,而不是对整个数组进行完全排序,这使得它在平均情况下具有更好的时间复杂度

算法思想

快速选择基于分治策略分区操作,核心思想可以概括为:

  1. 选择一个基准元素(pivot)
  2. 将数组分为三部分:小于基准的元素,等于基准值元素,大于等于基准的元素
  3. 根据分区结果决定递归处理哪一部分

算法步骤

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)

时间复杂度证明

直观理解

快速选择算法的时间复杂度分析可以通过以下直观方式理解:

  1. 第一次分区:处理n个元素,时间复杂度O(n)
  2. 第二次分区:处理约n/2个元素,时间复杂度O(n/2)
  3. 第三次分区:处理约n/4个元素,时间复杂度O(n/4)
  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)(递归栈深度)

优化策略

  1. 随机化基准选择:避免最坏情况发生
  2. 三数取中法:选择左端、中间、右端三个元素的中位数作为基准
  3. 迭代实现:避免递归调用栈溢出
  4. 小数组使用插入排序:当子数组较小时切换到插入排序

使用场景

适用情况

  • 查找中位数:快速找到数组的中位数
  • Top K 问题:查找前K大或前K小的元素
  • 顺序统计:需要获取特定顺序位置的元素
  • 大数据处理:当数据量太大无法完全排序时

实际应用

  1. 成绩排名系统:快速找到第K名学生的成绩
  2. 数据分析:计算数据的百分位数
  3. 资源调度:选择优先级第K高的任务
  4. 机器学习: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个最大元素

数组中的第K个最大元素

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

第k大的数
代码实现:

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算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值