【C++】寻找数组第k大元素

一、基本问题

题目:给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

注意:你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

要求:时间复杂度为 O(N)

        看到这个问题,我们脑袋里第一个想到的就是先把整个数组排个序,然后取第 k 大就可以了。但是一看要求时间复杂度要求为 O(N) ,众所周知,基于比较的排序的时间复杂度下限为 O(NlogN) ,不基于比较的排序又不适用于 数据范围很大的情况,所以本题目要选择其他思路。

        其实只要算法题刷到一定数目,看到第 k 大和前 k 大这样的字眼,应该条件反射出快速排序和堆排序这两种方法,但是上述两个排序的时间复杂度均为 O(NlogN) ,所以需要改进一下,下面给出改进方法和时间复杂度分析。

(一)快速选择

  • 算法原理

        快速排序的原理就是可以一次把一个元素放到数组它对应的位置上,利用这个特性我们可以进行左右边界的更迭达到类似二分的效果,即一次甩掉一半的元素可以不必再纠结它们的顺序和大小。具体是否可以完成二分效果,取决于 base 元素的选择,如果 base 选择较差,那么会使算法退化到 O(N^2) 的时间复杂度;反之可以把问题规模每次都缩小一半。具体流程如下:

    //快速选择方法-迭代
    int findKthLargest(vector<int>& nums, int k) {
        int n=nums.size();
        int l=0,r=n-1;//初始化左右边界
        while(true)//直至找到结果返回,因为一定有结果
        {
            int i=l,j=r;//相当于迭代的那个子过程
            int randPos=rand()%(r-l+1)+l;//随机化寻找base元素,以免时间复杂度退化到 O(N^2)
            swap(nums[l],nums[randPos]);
            int base=nums[l];//确定base元素,为最左
            while(i<j)
            {
                while(i<j && nums[j]<=base) j--;//选择最左元素为base,就先遍历右边,注意递增递减顺序
                while(i<j && nums[i]>=base) i++;
                if(i<j) swap(nums[i],nums[j]);
            }
            swap(nums[l],nums[i]);
            if(i==k-1) return nums[i];//找到第 k 大元素
            else if(i>k-1) r=i-1;// i 过大,更新右边界
            else l=i+1;// i 过小,更新左边界
        }
        return -1;//无实际意义,因为一定会在 while 里返回
    }
  • 复杂度分析

        上述代码中,我们对 base 元素进行了一个随机化选择,这样可以保证二分的效果,那么问题就会变成 T(N)=T(N/2)+O(N) ,根据主定理时间复杂度为 O(N)。这里插一句题外话,分治问题需要判断时间复杂度又没什么思路的时候,可以多用下主定理。

(二)优先队列(堆方法)

1、利用C++的 priority_queue 数据结构

  • 算法原理

        建立一个小顶堆,用于存储数组中 k 个最大的数,那么堆顶元素就是第 k 大的数。

    //优先队列方法
    int findKthLargest(vector<int>& nums, int k) {
        priority_queue<int,vector<int>,greater<int>> tmp;//小顶堆
        for(int i=0;i<k;++i) tmp.push(nums[i]);
        for(int i=k;i<nums.size();++i)
        {
            if(nums[i]>tmp.top())
            {
                tmp.pop();
                tmp.push(nums[i]);
            }
        }
        return tmp.top();
    }
  • 复杂度分析

        C++中优先队列的 push 和 pop 都是 O(logN) 级别的,遍历整个数组是 O(N) 级别的,故总体复杂度为 O(NlogN) 级别,哎呀,超过了题目要求。不过这种方法写起来很简单,思想也很容易,大家可以留作备选。

2、自建堆

        面试过程中,直接用现成的优先队列,可能没办法满足你的面试官,而且它的时间复杂度也没有达标,那么只能手动撕堆了。

  • 算法原理

        先对数组的非叶节点进行建堆操作,然后遍历叶子节点去跟堆头节点交换,然后在进行堆调整,堆末元素会逐渐有序,第 k 个堆顶就是所求元素。

    void adjustHeap(vector<int> &nums,int father,int heapSize)//建堆的数组,需调整的堆头,堆大小
    {
        int left=2*father+1;
        int right=2*father+2;
        int maxPos=father;
        if(left<heapSize && nums[left]>nums[maxPos]) maxPos=left;
        if(right<heapSize && nums[right]>nums[maxPos]) maxPos=right;
        if(maxPos!=father)
        {
            swap(nums[father],nums[maxPos]);
            adjustHeap(nums,maxPos,heapSize);//交换可能破坏堆性质,调整堆
        }
    }
    //堆排序方法
    int findKthLargest(vector<int>& nums, int k) {
        int heapSize=nums.size();
        for(int i=heapSize/2;i>=0;--i)//利用非叶节点建堆
            adjustHeap(nums,i,heapSize);
        for(int i=nums.size()-1;i>=nums.size()-k+1;--i)//调整 k 次,数组头部(堆头)为第 k 大
        {
            swap(nums[i],nums[0]);//将堆尾元素与堆头元素交换,那么数组尾部就有序了
            heapSize--;//数组尾部有序,将堆 size--
            adjustHeap(nums,0,heapSize);//交换了头尾元素,可能破坏堆性质,调整堆
        }
        return nums[0];
    }
  • 复杂度分析

        建堆的时间复杂度为 O(N) ,调整堆的过程中所用时间复杂度为 O(klogN) ,故总体时间复杂度为 O(N) 。        

二、算法推广

题目:给定一个 int 型 的无序数组,寻找该数组的 中位数

要求:时间复杂度为 O(N)

        求完了前 k 大,我相信前 k 小大家也一定可以如鱼得水。那么我们再来看这个数组中位数的题目,求数组的中位数,我们一般的做法就是先把整个数组排序,然后分情况讨论:当数组长度 N 为奇数时,取第 \frac{N}{2} 位元素为中位数;当数组长度 N 为偶数时,取第 \frac{N}{2}\, and \, \frac{N}{2}-1 位的平均数为中位数。但是这样的时间复杂度还是最少为 O(NlogN)。

        看完一般思路,再结合一下第 k 大问题的解决方案,是不是瞬间醍醐灌顶,本题只不过是给第 k 大问题套了个外壳。下面我仅给出伪码,供大家参考。

double findMid(vector<int> &a)
{
    int n=a.size();
    //奇数
    if(n%2==1) return (double)findKthLargest(a,n/2);
    //偶数
    else
    {
        int m1=findKthLargest(a,n/2-1);
        int m2=findKthLargest(a,n/2);
        return (double)(m1+m2)/2;
    }
}

        当然,本题也可以用最大堆(lessHeap)结合最小堆(greaterHeap)的方法来做,具体实现可以参考:寻找一个数组的中位数C++版本(使用priority_queue)_wxtRelax的博客-CSDN博客_queue 中位数维护一个最小堆和一个最大堆,平衡两个堆的个数,两种情况:最大堆比最小堆个数多 1最大堆和最小堆个数相同如果个数相等,返回两个数的平均值。如果最大堆比最小堆个数多1,那么返回最大堆的堆顶元素即为中位数。通过下面代码就可以实现://寻找中位数double findMedian(vector<int>& data){if (data.size() == 0){return -1;}priority_queue<int, vector<ihttps://blog.csdn.net/w_weixiaotao/article/details/111999743

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

棱角码农

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值