题目描述:
给定整数数组
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
解题思路:
方法一:排序法
我的思路:
-
用
sort把数组升序排列 -
倒数第 k 个元素就是第 k 大元素
-
下标 =
nums.size() - k
代码实现:
cpp
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
sort(nums.begin(), nums.end());
return nums[nums.size() - k];
}
};
复杂度分析:
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n log n) | sort 的复杂度 |
| 空间复杂度 | O(log n) | 递归栈(内省排序) |
优缺点:
| 优点 | 缺点 |
|---|---|
| ✅ 代码极简,一行搞定 | ❌ 时间复杂度 O(n log n),不是最优 |
| ✅ 不容易出错 | ❌ 面试官会追问"能否优化到 O(n)" |
| ✅ 适合快速写出答案 | ❌ 对数组做了完全排序,浪费了信息 |
关键问题 :题目只要求找第 k 大,不需要完全排序。排序反而做了很多额外工作。
方法二:快速选择
解题思路:
基于快速排序的 partition:
-
随机选一个 pivot,把数组分成两部分:
-
左边 ≤ pivot
-
右边 ≥ pivot
-
-
判断第 k 大在左边还是右边
-
只递归包含第 k 大的那一半,另一半不用管
核心优化:每次排除一半,平均时间复杂度 O(n)。
具体过程示例:
nums = [3,2,1,5,6,4], k = 2(找第 2 大,即第 5 小)
目标下标: n - k = 6 - 2 = 4(升序后下标4)
第1次 partition: pivot=4
[3,2,1,4,5,6]
↑
pivot在位置3
目标下标4 > 3 → 在右半部分 [5,6] 找
第2次 partition: pivot=5
[5,6]
↑
pivot在位置4
目标下标4 == 4 → 找到!返回 5 ✅
代码实现:
cpp
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
int target = nums.size() - k; // 第 k 大 = 升序后下标 target
return quickSelect(nums, 0, nums.size() - 1, target);
}
private:
int quickSelect(vector<int>& nums, int left, int right, int target) {
if (left == right) return nums[left];
// 随机选 pivot,避免最坏情况
int pivotIndex = left + rand() % (right - left + 1);
swap(nums[left], nums[pivotIndex]);
int pivot = nums[left];
// partition
int i = left, j = right;
while (i < j) {
while (i < j && nums[j] >= pivot) j--;
while (i < j && nums[i] <= pivot) i++;
if (i < j) swap(nums[i], nums[j]);
}
swap(nums[left], nums[i]);
// 判断 target 在哪边
if (i == target) return nums[i];
else if (i < target) return quickSelect(nums, i + 1, right, target);
else return quickSelect(nums, left, i - 1, target);
}
};
复杂度分析:
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | 平均 O(n),最坏 O(n²) | 随机 pivot 避免最坏 |
| 空间复杂度 | O(log n) | 递归栈 |
优缺点:
| 优点 | 缺点 |
|---|---|
| ✅ 平均时间 O(n),最优 | ❌ 最坏情况 O(n²)(随机化后概率极低) |
| ✅ 原地修改,空间 O(1)(不算递归栈) | ❌ 代码比排序法复杂 |
| ✅ 面试首选 | ❌ 需要理解 partition |
方法三:小根堆
解题思路:
维护一个大小为 k 的小根堆:
-
遍历数组,把元素加入堆
-
堆大小超过 k 时,弹出堆顶(最小值)
-
遍历结束后,堆顶就是第 k 大
代码实现:
cpp
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<int>> pq; // 小根堆
for (int num : nums) {
pq.push(num);
if (pq.size() > k) {
pq.pop(); // 弹出最小的
}
}
return pq.top();
}
};
复杂度分析:
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n log k) | 每个元素入堆 O(log k) |
| 空间复杂度 | O(k) | 堆的大小 |
优缺点:
| 优点 | 缺点 |
|---|---|
| ✅ 适合海量数据流 | ❌ 比快速选择慢 |
| ✅ 不需要修改原数组 | ❌ 需要额外 O(k) 空间 |
| ✅ 代码简洁 |
三种方法对比:
| 方法 | 时间复杂度 | 空间复杂度 | 是否修改原数组 | 推荐度 |
|---|---|---|---|---|
| 排序法 | O(n log n) | O(log n) | ✅ 修改 | ⭐⭐⭐ |
| 快速选择 | 平均 O(n) | O(log n) | ✅ 修改 | ⭐⭐⭐⭐⭐ |
| 小根堆 | O(n log k) | O(k) | ❌ 不修改 | ⭐⭐⭐⭐ |