题目描述
在一个未排序的数组中,求出其中第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)。