一、基本问题
题目:给定整数数组
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 元素进行了一个随机化选择,这样可以保证二分的效果,那么问题就会变成 ,根据主定理时间复杂度为 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 为奇数时,取第 位元素为中位数;当数组长度 N 为偶数时,取第
位的平均数为中位数。但是这样的时间复杂度还是最少为 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