分治(快速选择算法)(3)

https://blog.csdn.net/2601_95366422/article/details/159008129

上节课链接

一.题目

215. 数组中的第K个最大元素 - 力扣(LeetCode)

二.思路讲解

2.1 堆思路

对于TopK问题 ,通常我们会想到使用堆排序 :维护一个大小为 k 的小根堆,遍历数组,最后堆顶就是第 k 大的元素。这种方法的时间复杂度是 O(n log k) ,虽然也能通过,但题目要求 O(n) 的时间复杂度,而堆排序无法达到线性。因此,我们需要一种更高效的算法------快速选择算法 ,它基于快速排序的划分思想,平均时间复杂度可达 O(n)

2.2 快速选择算法

要理解快速选择,必须先掌握三路划分 (即上一章的颜色排序思想)。快速选择的核心在于:每次选择一个基准值 ,将数组划分为三个区域------小于基准等于基准大于基准 。然后根据第 k 大的元素落在哪个区域,只递归处理那个区域,从而大幅减少计算量。

具体来说,假设当前区间为 [l, r],经过三路划分后,我们得到:

  • 小于区域[l, left]

  • 等于区域[left+1, right-1]

  • 大于区域[right, r]

假设我们想要第 k 大的元素,即降序排列后的第 k 个。那么我们可以比较 k 与各个区域的大小:

  • 如果 k 落在大于区域(即大于区域的长度 ≥ k),那么答案就在大于区域中,递归处理大于区域。

  • 如果 k 落在等于区域(即大于区域长度 < k ≤ 大于区域长度 + 等于区域长度),那么基准值就是答案。

  • 否则k 落在小于区域 ,需要更新 k 的值(减去前面两个区域的长度)后递归处理小于区域。

三.代码演示

cpp 复制代码
class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) 
    {
        int n = nums.size();
        srand(time(NULL));
        return qsort(nums,0,n-1,k);

    }
    int qsort(vector<int>& nums,int l,int r,int k)
    {
        //划分三块思想,不过改成降序
        int key = getRandom(nums,l,r);
        int i = l,left = l - 1,right = r + 1;
        while(i < right)
        {
            if(nums[i] > key)
            {
                swap(nums[++left],nums[i++]);
            }
            else if(nums[i] == key)
            {
                i++;
            }
            else
            {
                swap(nums[--right],nums[i]);
            }
        }
        //判断k在那个区域,采取从左------中------右的方式,因为数组现在是降序
        //[l,left]左
        //[left+1,right-1]中
        //[right,r]右
        int leftSize = left - l + 1;//左区间长度
        int midSize = right - left - 1;//右区间长度

        //在左区间
        if(leftSize >= k)
        {
            return qsort(nums,l,left,k);
        }
        //在左中区间,但是不存在左区间,那么就是在中区间
        else if(leftSize + midSize >= k)
        {
            return key;
        }  
        //把左中区间减去,就在右区间
        else
        {
            return qsort(nums,right,r,k - leftSize - midSize);
        }
    }
    int getRandom(vector<int>& nums,int l,int r)
    {
        int q = rand();
        return nums[q % (r - l + 1) + l];
    }
};

四.代码讲解

一、随机基准值的选择

为了避免快速选择算法在特定输入下退化为 O(n²) ,我们采用随机化策略 。在 getRandom 函数中,通过 rand() % (r - l + 1) + l 生成一个位于当前区间 [l, r] 内的随机下标,并将该位置的元素作为基准值 key。这样,每次划分的基准都是随机的,使得算法在期望时间复杂度上达到 O(n) 。在排序前调用 srand(time(NULL)) 设置随机种子,确保每次运行产生的随机数不同。

二、三路降序划分

为了高效处理重复元素,我们使用三路划分 ,但这里将数组划分为大于基准等于基准小于基准三个区域(即降序排列)。定义三个指针:

  • left :初始为 l - 1,指向大于区域的最后一个元素(即大于区域为空)。

  • right :初始为 r + 1,指向小于区域的第一个元素(即小于区域为空)。

  • i :初始为 l,用于遍历当前元素。

循环条件为 i < right,遍历过程中根据 nums[i]key 的关系分三种情况:

  1. nums[i] > key :该元素应放入大于区域 。先将 left 右移一位(++left),然后交换 nums[left]nums[i],接着 i++。此时 left 指向新放入的大于元素,而 i 继续向后。

  2. nums[i] == key :该元素属于等于区域 ,无需移动,直接 i++

  3. nums[i] < key :该元素应放入小于区域 。先将 right 左移一位(--right),然后交换 nums[right]nums[i]。注意,此时 i 不能增加 ,因为交换过来的元素来自 right 位置,尚未被处理,需要在下一次循环中重新判断。

循环结束后,数组被划分为:

  • [l, left] :全部大于 key 的元素

  • [left+1, right-1] :全部等于 key 的元素

  • [right, r] :全部小于 key 的元素

三、根据区域大小确定第k大的元素所在区间

由于我们要找的是第 k 大的元素,而当前数组是降序划分,因此大于区域 在前,等于区域 居中,小于区域在后。我们需要计算各个区域的长度:

  • 左区间(大于区域)长度leftSize = left - l + 1

  • 中间区间(等于区域)长度midSize = right - left - 1

接下来,根据 k 与这些长度的关系,决定下一步操作:

  • 如果 k <= leftSize :说明第 k 大的元素在大于区域 中,递归处理左区间 [l, left],且 k 保持不变

  • 如果 leftSize < k <= leftSize + midSize :说明第 k 大的元素就是基准值 key,直接返回 key

  • 如果 k > leftSize + midSize :说明第 k 大的元素在小于区域 中,递归处理右区间 [right, r],但此时需要更新 k ,减去前面两个区域的长度,即新的 k = k - leftSize - midSize

四、递归处理对应区间

根据上述判断,只对包含目标元素的区间进行递归,其他区间被舍弃。这样每次递归的规模平均减半,保证了线性期望时间复杂度。至于为什么时间复杂度是O(n)则需要去看书,还是很难的!

五、关键细节
  • 随机化:随机选择基准值是避免最坏情况的关键,使得算法在概率上高效。

  • 三路划分:将等于基准值的元素集中在一起,避免了重复元素带来的冗余递归,同时简化了区间判断。

  • 降序划分:代码采用大于在左、小于在右的方式,直接对应第 k 大的概念,无需转换。

  • k 的更新:当递归进入小于区域时,需要减去前面区域的大小,因为第 k 大的位置在全局中发生了变化。

相关推荐
_日拱一卒5 小时前
LeetCode:合并区间
算法·leetcode·职场和发展
xiaoye-duck5 小时前
【C++:哈希表封装】哈希表封装 myunordered_map/myunordered_set 实战:底层原理 + 完整实现
数据结构·c++·散列表
汀、人工智能5 小时前
[特殊字符] 第3课:最长连续序列
数据结构·算法·数据库架构·图论·bfs·最长连续序列
少许极端5 小时前
算法奇妙屋(四十一)-贪心算法学习之路 8
学习·算法·贪心算法
A.A呐5 小时前
【C++第二十三章】C++11
开发语言·c++
Kethy__5 小时前
计算机中级-数据库系统工程师-数据结构-图
数据结构·算法·软考··数据库系统工程师·计算机中级
亿秒签到6 小时前
L2-007 家庭房产
数据结构·c++·算法
2401_892070986 小时前
【Linux C++ 日志系统实战】日志消息对象 LogMessage 完整实现:流式拼装 + 标准化输出
linux·c++·日志系统·流式日志
paeamecium6 小时前
【PAT甲级真题】- Longest Symmetric String (25)
数据结构·c++·算法·pat考试