求数组中第k大的元素

题目描述

在一个未排序的数组中,求出其中第k大的数,需要注意的是,数组中可能存在重复元素,所以求解的是第k大的数,而不是第k个不同的元素。

这个题目对应LeetCode上的第215号题,链接:leetcode第215题, 在继续往下看具体的解法之前,可以先花一点时间,自己想一下如何解决:)

解法一

既然是求其中第k大的数,那么使其数组整体有序后,找到其中第k大的数就比较容易了,所以先对数组进行排序,然后再取第k大的数即可。

使用c++代码:

int findKthLargest(vector<int>& nums, int k) {
    if (k < 1 || k > nums.size()) {
        return 0;
    }
    
    // 从大到小排序
    sort(nums.begin(), nums.end(), greater<int>());
    
    return nums[k - 1];
}
复制代码

首先函数开始执行,先对参数k进行检查,当k小于1,或者大于数组的个数时,我的处理方式是返回零。

然后再进行排序,这里我的代码实现中,用的是从大到小的排序,因为数组的索引是从0开始计算,所以计算第k大的数时,就应该取数组的第k-1个元素。另外,这里也可以从小到大的排序,那么,第k大的数就应该是数组的第n-k个元素。

解法二

可以使用优先队列,维护前k大的元素。

代码:

int findKthLargest(vector<int>& nums, int k) {
    if(k < 1 || k > nums.size()) {
        return 0;
    }
    
    priority_queue<int,vector<int>,greater<int>> q; // 构建最小堆
    
    for (int i = 0; i < nums.size(); i++) {
        if (q.size() != k) {
            q.push(nums[i]);
        } else if (q.top() < nums[i]) {
            q.pop();
            q.push(nums[i]);
        }
    }
    return q.top();
}
复制代码

注意,这里使用优先队列创建最小堆,来维护前k大的元素。这样在堆顶的元素始终是堆中最小的,当队列中有k个元素,再入队一个新元素时,便将堆顶的元素,也就是最小的元素出队。当循环一遍数组,队列中的堆顶元素也就是第k大的数了。

解法三

可以使用二分查找结合快速排序来更高效的查找。

先简单复习下二分查找和快速排序的基本思想。

二分查找:对于一个有序的数组,搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。

快速排序:以从小到大排序为例,快速排序是选定数组中的某一个元素作为标定点,比这个元素小的全部排到这个特定元素的左边,比这个元素大的全部排到这个特定元素的右边,然后分别对这两部分继续按照上述方法进行排序,最后使整体达到有序。如果忘记快速排序的具体实现的话,可以看我之前写的关于快速排序的文章:快速排序

那如何结合二分查找和快速排序来解决当前的问题呢?按照快排的partition操作,我们可以先选定一个标定点,然后与标定点比较分成两部分区间,然后确定第k大元素所属的区间是标定点的左边部分还是右边部分,再按照二分查找的方式,只对第k大元素所属的区间继续进行partition操作,直到找到第k大的元素为止。

假设以数组[7,92,23,9,-1,0,11,6]这个数组为例,寻找其中第四大的元素。我们来对其进行具体的分析。

使用快排并不需要将整体全部排序,只是用来确定需要寻找元素的范围。

按照从小到大排序的思路使用快速排序,那么其中第k大的元素,也就是数组中第n+1-k小的元素,数组中的元素个数为8,加1, 减去k = 4,也就是求数组中第5小的元素。

开始,随机选取标定点为11,第一次partition操作后如图。为了使观察更清晰,数字下方的&代表这个数字比标定点小,#代表比标定点大。

[7,9,-1,0,6,11,92,23];
 & &  & & &     # #
复制代码

可以看到,比标定点小的个数有5个,即可以确定这5个元素是数组中前5个较小的元素,那么我们要找到第5小的元素,包含在其中,只需要在这5个元素中查找,而后面的元素,我们就完全可以抛弃,不用去管它了。如图。

[7,9,-1,0,6,*,*,*]
复制代码

然后继续对这一部分进行partition操作。假设随机选取的标定点为6。

[-1,0,6,7,9,*,*,*]
  & &   # #
复制代码

这样我们就可以进一步确定,数组中第5小的元素位于6的右边,也就是7与9之间。而标定点左边的元素就不用管了。

[*,*,*,7,9,*,*,*]
复制代码

假设随机选取的标定点为9,继续进行partition,可以发现9所处的位置正好是这个数组第5小的位置。由于是从小到大排序,我们就可以确定第5小的是9。第5小的元素,从下图从右往左看,也就是数组中第4大的元素。

[*,*,*,*,9,*,*,*]
复制代码

下面是具体的代码实现。

public:
    int findKthLargest(vector<int>& nums, int k) {
        if(k < 1 || k > nums.size()) {
            return 0;
        }
        
        srand(time(NULL));
        return findKthLargest(nums, 0, nums.size() - 1, nums.size() - k);
    }
    
private:
    // 在数组[l,r]区间内寻找k大的数排序后所处的索引s。
    int findKthLargest(vector<int>& nums,int l, int r, int s){
        if (l == r) {
            return nums[l];
        }
        
        int p = partition(nums,l,r);
        if (p == s) {
            return nums[p];
        } else if (s < p){
            return findKthLargest(nums, l, p - 1, s);
        } else { // s > p
            return findKthLargest(nums, p + 1, r, s);
        }
    }
    
    int partition(vector<int>& nums, int l, int r) {
        int p = rand()%(r-l+1) + l;
        swap(nums[l], nums[p]);
        
        int j = l;
        for (int i = l + 1; i <= r; i++) {
            if (nums[i] < nums[l]) {
                swap(nums[i], nums[j + 1]);
                j++;
            }
        }
        swap(nums[l], nums[j]);
        return j;
    }
复制代码

上面代码需要注意的几个地方

  • 使用随机数来选取标定点。
  • 对partition过程不理解的,可以查看:快速排序 中讲解partition的部分。

三种方法的复杂度分析

第一种解法: 使用了快速排序,时间复杂度为O(nlogn),空间复杂度为0(1)。

第二种解法:使用了优先队列维护前k大的元素,所以空间复杂度为0(k),在时间复杂度上,循环遍历了一遍数组,并且对遍历每个元素时,都要维护前k个元素,每进队一个元素,堆中的时间复杂度是0(logk),所以整体为O(nlogk)。

第三种解法:快排的时间复杂度是O(nlogn),这里使用了二分查找,舍弃了每次partiton后对另一部分处理的时间,所以时间复杂度为O(n),空间复杂度要考虑递归函数入栈时的空间,整体为0(logn)。

转载于:https://juejin.im/post/5d3ea1755188255d87219841

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值