leetcode:215. 数组中的第K个最大元素
题目
给定整数数组 nums
和整数 k
,请返回数组中第 k
个最大的元素。
请注意,你需要找的是数组排序后的第 k
个最大的元素,而不是第 k
个不同的元素。
示例:
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4
解答 & 代码
解法一:快速选择 O(N)
rand
头文件:#include <stdlib.h>
class Solution {
private:
int partition(vector<int>& nums, int left, int right)
{
int randomIdx = rand() % (right - left + 1) + left;
swap(nums[left], nums[randomIdx]);
int pivot = nums[left];
while(left < right)
{
while(left < right && nums[right] <= pivot)
--right;
nums[left] = nums[right];
while(left < right && nums[left] >= pivot)
++left;
nums[right] = nums[left];
}
nums[left] = pivot;
return left;
}
void quickSelect(vector<int>& nums, int left, int right, int k)
{
if(left >= right)
return;
int pivot = partition(nums, left, right);
if(pivot == k - 1)
return;
else if(pivot > k - 1)
quickSelect(nums, left, pivot - 1, k);
else
quickSelect(nums, pivot + 1, right, k);
}
public:
int findKthLargest(vector<int>& nums, int k) {
quickSelect(nums, 0, nums.size() - 1, k);
return nums[k - 1];
}
};
复杂度分析:设数组长为 N
- 时间复杂度 O(N):粗略估计,假设每次扔掉一半,则
,其中
,即
,所以
- 等比数列求和公式:
- 因此
- 空间复杂度 O(log N):递归栈空间
执行结果:
执行结果:通过
执行用时:9 ms, 在所有 C++ 提交中击败了 72.53% 的用户
内存消耗:9.6 MB, 在所有 C++ 提交中击败了 89.72% 的用户
解法二:小根堆(优先队列) O(Nlogk)
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<int>> minHeap;
for(int i = 0; i < k; ++i)
minHeap.push(nums[i]);
for(int i = k; i < nums.size(); ++i)
{
if(nums[i] > minHeap.top())
{
minHeap.pop();
minHeap.push(nums[i]);
}
}
return minHeap.top();
}
};
复杂度分析:设数组长为 N
- 时间复杂度 O(N logk):小根堆最多存储 k 个元素,因此每次查找、删除的时间复杂度 O(log k),共 N 次
- 空间复杂度 O(k):小根堆最多存储 k 个元素
执行结果:
执行结果:通过
执行用时:16 ms, 在所有 C++ 提交中击败了 33.98% 的用户
内存消耗:9.9 MB, 在所有 C++ 提交中击败了 13.52% 的用户
解法三:小根堆(手造)
class Solution {
private:
void adjustHeap(vector<int>& nums, int start, int end)
{
int dadIdx = start; // 父节点序号
int sonIdx = dadIdx * 2 + 1; // 左子节点序号
while(sonIdx <= end)
{
if(sonIdx + 1 <= end && nums[sonIdx + 1] < nums[sonIdx])
sonIdx += 1;
if(nums[dadIdx] < nums[sonIdx])
return;
else
{
swap(nums[dadIdx], nums[sonIdx]);
dadIdx = sonIdx;
sonIdx = dadIdx * 2 + 1;
}
}
}
void buildMinHeap(vector<int>& nums)
{
// 从最后一个非叶子节点开始调整
for(int i = nums.size() / 2 - 1; i >= 0; --i)
adjustHeap(nums, i, nums.size() - 1);
}
public:
int findKthLargest(vector<int>& nums, int k) {
vector<int> minHeap;
for(int i = 0; i < k; ++i)
minHeap.push_back(nums[i]);
buildMinHeap(minHeap);
for(int i = k; i < nums.size(); ++i)
{
if(nums[i] > minHeap[0])
{
minHeap[0] = nums[i];
adjustHeap(minHeap, 0, k - 1);
}
}
return minHeap[0];
}
};
复杂度分析:设数组长为 N
- 时间复杂度 O(N logk):小根堆最多存储 k 个元素,因此每次查找、删除的时间复杂度 O(log k),共 N 次
- 空间复杂度 O(k):小根堆最多存储 k 个元素
执行结果:
执行结果:通过
执行用时:8 ms, 在所有 C++ 提交中击败了 72.53% 的用户
内存消耗:9.7 MB, 在所有 C++ 提交中击败了 79.68% 的用户
扩展:求 const top k:二分查找
如果传入的数组是 const vector<int> nums
,也就是不能修改原数组的元素值,那么如何在 O(1) 的空间复杂度内实现查找第 k 大的元素?
class Solution {
private:
/* 统计数组 nums 中大于等于 x 的元素个数,并返回 */
int countGreaterThanX(vector<int> nums, int x)
{
int cnt = 0;
for(int i = 0; i < nums.size(); ++i)
{
if(nums[i] >= x)
++cnt;
}
return cnt;
}
public:
int findKthLargest(vector<int>& nums, int k) {
// 先统计数组中的最小值和最大值,
int minNum = INT_MAX;
int maxNum = INT_MIN;
for(int i = 0; i < nums.size(); ++i)
{
minNum = min(minNum, nums[i]);
maxNum = max(maxNum, nums[i]);
}
// 二分查找:minNum 就相当于二分查找的 left,maxNum 就相当于二分查找的 right
while(minNum <= maxNum)
{
int midNum = minNum + (maxNum - minNum) / 2;
// 统计数组中大于等于 midNum 的元素个数 cnt
int cnt = countGreaterThanX(nums, midNum);
// 若 cnt >= k,说明需要增大 midNum,使得大于等于 midNum 的元素个数减少
// 因此,收缩左边界
if(cnt >= k)
minNum = midNum + 1;
// 否则,说明需要减小 midNum,使得大于等于 midNum 的元素个数增大
// 因此,收缩右边界
else
maxNum = midNum - 1;
}
return maxNum;
}
};
复杂度分析:设数组长为 N
- 时间复杂度 O(N logN):二分查找的时间复杂度是
O(logN)
,而对于每次二分的midNum
,调用countGreaterThanX
查找数组nums
中大于等于midNum
的元素个数,时间复杂度为O(N)
- 空间复杂度 O(1):
执行结果:
执行结果:通过
执行用时:8 ms, 在所有 C++ 提交中击败了 72.53% 的用户
内存消耗:11.8 MB, 在所有 C++ 提交中击败了 5.09% 的用户