目录
引言
这部分内容涉及排序算法,包括快速排序和堆排序算法。
下面给出几篇博客,可以去了解一下,这个知识:
【STL中堆的相关概念】
【堆排序原理】
【快排原理】
数组中的第K个最大元素
- 🎈 题目链接:
- 🎈 做题状态:
我的解题
使用了 快速选择(Quick Select) 的思想,这是解决「数组中第K个最大元素」问题中非常高效的一种方法,平均时间复杂度为 O(n),最坏为 O(n^2)(与快速排序类似)。
下面我将详细解析思路并添加注释版代码:
✅ 思路解析
我们希望在一个无序数组中找到第 K 大的元素。
快速选择的思路是:
-
每次从数组中选一个“基准值”(pivot),并把数组划分成三部分:
big
: 大于 pivot 的元素equal
: 等于 pivot 的元素small
: 小于 pivot 的元素
-
接着判断目标位置
K
在哪一段:- 如果 K 小于等于
big
的个数 → 目标在big
中,递归找第 K 大 - 如果 K 落在
equal
的范围 → 目标是 pivot,直接返回 - 如果 K 大于
big + equal
→ 目标在small
中,更新目标为k - big.size() - equal.size()
,递归查找
- 如果 K 小于等于
这种“每次排除一部分数据”的方式是快速选择的本质。
✅ 详细注释版代码
class Solution {
private:
// 快速选择函数,用于找到 nums 中的第 k 大元素
int quickSelect(vector<int>& nums, int k)
{
// 1. 选择一个基准值(pivot),这里选取第一个元素
int pivot = nums[0];
// 2. 创建三个分区:大于、等于、小于 pivot
vector<int> big, equal, small;
for (int num : nums)
{
if (num > pivot)
{
big.push_back(num); // 放入大于 pivot 的部分
}
else if (num < pivot)
{
small.push_back(num); // 放入小于 pivot 的部分
}
else
{
equal.push_back(num); // 放入等于 pivot 的部分
}
}
// 3. 判断目标第 k 大元素在哪一部分:
// 情况一:第 k 大在 big 部分中 → 递归查找
if (k <= big.size())
{
return quickSelect(big, k);
}
// 情况二:第 k 大在 small 部分中
// 剩下的数量:big.size() + equal.size()
// 如果 k 超过这些,就在 small 里继续找第 (k - 前面两个数量) 大
if (k > big.size() + equal.size())
{
return quickSelect(small, k - big.size() - equal.size());
}
// 情况三:第 k 大在 equal 中,说明 pivot 就是我们要找的值
return pivot;
}
public:
// 主函数接口
int findKthLargest(vector<int>& nums, int k) {
return quickSelect(nums, k);
}
};
✅ 举个例子说明流程:
假设:
nums = [3, 2, 1, 5, 6, 4], k = 2
目标是第2大的元素(即是整个数组排序后第2大的那个数)。
-
选定
pivot = 3
-
划分:
big = [5, 6, 4]
equal = [3]
small = [2, 1]
-
因为
k = 2
,且big.size() = 3 >= k
,所以第2大在big
中 -
递归继续处理
[5, 6, 4]
,重复上述逻辑…
最终会返回 5
。
✅ 时间复杂度分析
-
平均时间复杂度: $O(n)$
每次划分都能有效地减少问题规模 -
最坏时间复杂度: $O(n^2)$
如果 pivot 每次都选得不好,例如每次最小/最大值,会退化为排序 -
空间复杂度: $O(n)$
因为每次都会重新构造新的子数组(可用partition + in-place swap
改成 $O(1)$)
划分为两个区间
完全可以只划分两个区间来实现快速选择(Quick Select)——这是经典的、空间更优、执行效率更高的做法,也是许多面试中建议写法。
✅ 两区间快选版本思想
我们不需要将数组划分成「大于 / 等于 / 小于 pivot」三份,而是直接采用**原地 partition(快排的 Lomuto 或 Hoare 分区方案)**将数组划分成两部分:
- 左边区间:≥ pivot 的元素
- 右边区间:< pivot 的元素
然后只需要关注当前 pivot 的**下标 pivotIndex
**和目标下标 k_index = k - 1
(从大到小第 k 个,就是从小到大排好后第 n-k
个)。
✅ 原地快选代码(只划分两区间)
class Solution {
private:
// 快速选择:寻找第k大的元素(第k大 == 第n-k小)
int quickSelect(vector<int>& nums, int left, int right, int k_smallest)
{
if (left == right) return nums[left]; // 只有一个元素时直接返回
// 选择最后一个元素作为基准
int pivot = nums[right];
int store_index = left;
// partition过程:把所有小于pivot的元素放在左边
for (int i = left; i < right; ++i)
{
if (nums[i] < pivot) // 注意:这里是升序排列,小的放左边
{
swap(nums[store_index], nums[i]);
store_index++;
}
}
// 把 pivot 放到最终的位置(中间)
swap(nums[store_index], nums[right]);
// 判断 pivot 的下标位置和目标位置的关系
if (store_index == k_smallest)
{
return nums[store_index]; // 找到了
}
else if (store_index < k_smallest)
{
return quickSelect(nums, store_index + 1, right, k_smallest);
}
else
{
return quickSelect(nums, left, store_index - 1, k_smallest);
}
}
public:
int findKthLargest(vector<int>& nums, int k)
{
int n = nums.size();
// 找第k大的元素 = 找第(n-k)小的元素
return quickSelect(nums, 0, n - 1, n - k);
}
};
✅ 时间复杂度分析(和三段式一样)
- 平均时间复杂度: $O(n)$
- 最坏时间复杂度: $O(n^2)$(例如 pivot 每次都是最大或最小)
- 空间复杂度: $O(1)$(原地 partition,无需额外数组)
✅ 总结:两段式 vs 三段式
比较项 | 三段式划分 | 两段式划分(原地 partition) |
---|---|---|
空间复杂度 | $O(n)$(使用3个临时数组) | $O(1)$ 原地操作 |
实现复杂度 | 简单清晰,逻辑更直观 | 更接近标准快排、效率更高 |
工程实用性 | 教学友好,适合理解 quick select | 实战常用(如 LeetCode 高频解法) |