
方法一:快速选择算法(最优解,O (n) 时间复杂度)
这是唯一满足题目 O (n) 时间复杂度要求的解法,也是面试首选。
1. 核心思想:基于快速排序的改进
快速排序的核心是划分(partition):
- 选一个元素作为主元(pivot)
- 把数组分成两部分:左边所有元素 ≤ 主元,右边所有元素 ≥ 主元
- 主元的最终位置就确定了,不会再变
快速选择的精髓是:我们不需要把整个数组排序,只需要找到目标位置的元素即可。
- 我们要找的是第 k 大的元素,也就是升序排序后下标为
n-k的元素(因为升序数组的倒数第 k 个就是第 n-k 个) - 每次划分后,看主元的位置 q 和目标位置
n-k的关系:- 如果
q == n-k:主元就是我们要找的元素,直接返回 - 如果
q < n-k:目标元素在右半部分,递归右半区间 - 如果
q > n-k:目标元素在左半部分,递归左半区间
- 如果
这样我们每次只需要递归一个区间,而不是两个,时间复杂度从快速排序的 O (nlogn) 降到了期望 O (n)。
2. 完整解题步骤
- 计算数组长度 n,目标下标为
target = n - k - 调用快速选择函数,在区间
[0, n-1]中找下标为 target 的元素 - 快速选择函数:
-
如果区间只有一个元素(l==r),返回该元素
-
对区间进行划分,得到主元的位置 q
-
根据 q 和 target 的关系,递归左半或右半区间
cppclass Solution { public: int quickselect(vector<int> &nums,int l,int r,int k){ //参数1:输入 //2:左指针的初始位置 //3:右指针的初始位置 //4:从左往右数第k个 //左指针等于右指针 返回 if(l == r){ return nums[k]; } //选择一个基准数 要求这个数的左边的值都小于他 右边的数都大于他 int p = nums[l]; int i = l-1;//出格 避免下面的循环不启动 int j = r+1; while(i<j){ do i++; while(nums[i]<p);//要满足左边的数小于基准值,循环结束的时候 i就是大于基准值的位置 do j--; while(nums[j]>p);//右边的数要大于基准值,循环结束,j就是小于基准值的位置。 //小的需要在左边 大的在右边 //所以如果i在j左边 就得交换 if(i<j){ swap(nums[i],nums[j]); } } //循环结束j的位置就是基准的位置 //接下来看看k目标位置和基准位置的关系 如果等于直接返回j。如果小于j就在左半部分 直接递归左半部分 反之亦然 if(k <= j ) return quickselect(nums,l,j,k); else return quickselect(nums,j+1,r,k); } int findKthLargest(vector<int>& nums, int k) { int n = nums.size(); //n-k是相当于升序后的倒数第K个 也就是第k个最大元素 return quickselect(nums,0,n-1,n-k); } };
-
大根堆法:
大根堆 = 堆顶永远是整个堆里最大的数
我们要找第 K 大,思路超级简单:
- 把数组建成大根堆
- 把最大的扔掉(1 次)
- 把第二大的扔掉(2 次)
- ......
- 扔完 K-1 次以后,堆顶就是第 K 大!
完美解决!
堆长啥样?(一句话)
堆就是一个数组 ,但逻辑上是完全二叉树:
- 下标
i的左孩子 =i*2 + 1 - 下标
i的右孩子 =i*2 + 2 - 父节点永远比孩子大(大根堆)
三、堆法只有 3 个核心动作
1. 调整堆(heapify)
让一个位置重新变成大根堆: 把当前节点和左右孩子比,谁大谁放上面,不对就交换,再递归调整。
2. 建堆
从最后一个非叶子节点开始,从下往上全部调整一遍。
3. 删除堆顶(拿最大值)
- 堆顶和最后一个元素交换
- 堆大小 -1
- 重新调整堆顶
cpp
class Solution {
public:
void heapify(vector<int>& a, int i, int size) {
// 让i这个位置变成大根堆
int left = i * 2 + 1; // 左孩子
int right = i * 2 + 2; // 右孩子
int maxIdx = i; // 先假设自己最大
// 如果左孩子或者右孩子更大 就最大值变成他 最后maxIdx存的就是最大值
// 左孩子更大
if (left < size && a[left] > a[maxIdx])
maxIdx = left;
// 右孩子更大
if (right < size && a[right] > a[maxIdx])
maxIdx = right;
// 如果最大的不是自己 那就交换 让自己是最大的
if (maxIdx != i) {
swap(a[i], a[maxIdx]);
// 换完后调整
heapify(a, maxIdx, size);
}
}
// 核心2:建堆 → 把整个数组变成大根堆
void buildHeap(vector<int>& a, int size) {
// 从最后一个非叶子节点往上建
for (int i = size / 2 - 1; i >= 0; i--) {
heapify(a, i, size);
}
}
int findKthLargest(vector<int>& nums, int k) {
int n = nums.size();
// 建堆
buildHeap(nums, n); // 堆顶是最大数
int h = n;
// 把k-1次最大值弄出来
for (int i = n - 1; i >= n - k + 1; i--) {
swap(nums[0], nums[i]); // 堆顶最大的数 放到最后
// 然后重新调整新堆顶
h--;
heapify(nums, 0, h);
}
return nums[0];
}
};