力扣-215. 数组中的第K个最大元素
1.题目
题目地址(215. 数组中的第K个最大元素 - 力扣(LeetCode))
https://leetcode.cn/problems/kth-largest-element-in-an-array/
题目描述
给定整数数组 nums
和整数 k
,请返回数组中第 k
个最大的元素。
请注意,你需要找的是数组排序后的第 k
个最大的元素,而不是第 k
个不同的元素。
你必须设计并实现时间复杂度为 O(n)
的算法解决此问题。
示例 1:
输入:[3,2,1,5,6,4],
k = 2 输出: 5
示例 2:
输入:[3,2,3,1,2,4,5,5,6],
k = 4 输出: 4
提示:
1 <= k <= nums.length <= 105
-104 <= nums[i] <= 104
2.题解
2.1 nth_element的使用
思路
在强大的STL库中存在一个神奇的函数,那就是nth_element,这个函数主要用来将数组元素中第k小的整数排出来并在数组中就位,随时调用,可谓十分实用。
他的时间复杂的为O(n), 原因是他不要像排序那样确定每个元素都在自己应该在的位置上,它只要保证指定的k位置左边元素均小于它,右边元素均大于它即可。
故实际实现使用分治算法思想,只需要首尾指针对于数组进行一次递推遍历即可,这里先讲nth_element函数的使用方法,在2.2中详细介绍实现。
函数语句:nth_element(数组名,数组名+第k小元素,数组名+元素个数)
代码
- 语言支持:C++
C++ Code:
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
int n = nums.size();
nth_element(nums.begin(), nums.begin() + n - k, nums.end());
return nums[n - k];
}
};
2.2 分治算法
思路
我们使用分治思想,每次将整个区间分为左右两个区域,选取一个基准点后,通过首尾指针不断逼近,中间通过交换保证r左侧的区间全部小于等于currNum, l右侧的区间全部大于等于currNim
直到最后l > r结束循环,此时已经可以保证[begin, r] 和 [l, end]两个区域都是"有序的",也就是左边均小于等于currNum,右边均大于等于currNum
我们再来判断目标位置k所处的位置,分三种情况,每次都可以二分缩小一半查找范围:
1.k <= r, 去左半区间[begin, r]继续寻找
2.k >= l, 去右半区间[l, end]继续寻找
3.r < k < l. 这种情况比较特殊,具体可以见情况二图片,必然是 l - r = 2, k = r + 1 = l - 1的情况,
否则如果像情况一,l - r = 1, 那么必然有 k <= r 或者 k >= l, 而不可能存在r < k < l
大致的情况有以下两种:
情况一:
情况二:
代码
class Solution {
public:
int ans = 0;
void sortNum(vector<int>& nums, int begin, int end, int k){
if(begin == end){
ans = nums[begin];
return;
}
int currNum = nums[begin + (end - begin) / 2];
int l = begin, r = end;
while(l <= r){
while(nums[l] < currNum) l++;
while(nums[r] > currNum) r--;
if(l <= r) swap(nums[l++], nums[r--]);
}
if(k <= r) sortNum(nums, begin, r, k);
else if(k >= l) sortNum(nums, l, end, k);
else sortNum(nums, r + 1, l - 1, k);
}
int findKthLargest(vector<int>& nums, int k) {
int n = nums.size();
sortNum(nums, 0, n - 1, n - k);
return nums[n - k];
}
};
2.3 小根堆
思路
由于建堆和更新堆时间复杂度都是O(n),我们考虑使用小根堆
思路很简单,我们总是维护一个大小为k的小根堆,遍历数组的同时不断更新堆,保证小根堆中是当前遍历到的元素中最大的k个
而因为是小根堆,所以根节点值是这k个元素中最小的,也就是说,在遍历完数组所有元素后,小根堆的根节点值就是我们要求的第K大的元素值!
代码
class Solution {
public:
void heapadjust(vector<int>& heap, int curindex, int len) {
// 这里下标从0开始,所以是要+1
int child = curindex * 2 + 1;
int curValue = heap[curindex];
while (child < len) {
// 判断是否有右子节点,且选择更小的那个
if (child + 1 < len && heap[child] > heap[child + 1]) {
child++;
}
// 如果父节点比左右子节点较小的那个更大的话,进行交换
// if (heap[curindex] > heap[child]) {
if (curValue > heap[child]) {
heap[curindex] = heap[child];
curindex = child;
child = curindex * 2 + 1;
}
// 否则说明排序已经正确,退出循环
else {
break;
}
}
heap[curindex] = curValue;
}
int findKthLargest(vector<int>& nums, int k) {
if (nums.size() < k)
return -1;
// 开始初始化参数(这里其实实际分配[0, k-1]共k个元素,end()是到达不了的!!!!)
vector<int> heap(nums.begin(), nums.begin() + k);
int len = nums.size(), sublen = heap.size();
// 自底向上初始化小根堆(小根堆这里从下标0开始,所以注意-1)
// sublen/2-1代表的是最后一个有叶子结点的父节点(完全二叉树)
for (int i = sublen / 2 - 1; i >= 0; i--) {
heapadjust(heap, i, sublen);
}
// 开始更新维护小根堆
for (int j = k; j < len; j++) {
if (nums[j] <= heap[0])
continue;
else {
heap[0] = nums[j];
heapadjust(heap, 0, sublen);
}
}
return heap[0];
}
};
2.4 优先级队列
思路
对于2.3,如果不想手动建立一个小根堆,我们可以考虑使用一个一小根堆为基础的优先级队列
代码
#include <vector>
#include <queue>
#include <functional>
class Solution {
public:
int findKthLargest(std::vector<int>& nums, int k) {
if (nums.size() < k) {
return -1;
}
// 创建一个小根堆(greater<int》,父元素 > 子元素 下沉,即小根堆)
std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;
// 构建初始大小为k的小根堆
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]);
}
}
// 堆顶元素就是第k大的元素
return minHeap.top();
}
};