215. 数组中的第K个最大元素
[此处请插入:快速选择(Quick Select)算法分区过程示意图]
✨核心逻辑
本题要求在时间复杂度为 O(n) 的约束下解决,因此采用 快速选择(Quick Select) 算法,其基于快速排序的分治思想:
- 目标索引映射:求第
k大的元素,实际上等同于求排序后数组中下标为n - k的元素。 - 分区操作(Hoare 分区法):在区间
[l, r]内选取基准值x,利用双指针i和j分别从左右两端向中间扫描。将小于基准值的元素放在左侧,大于基准值的放在右侧。 - 分治递归:
- 分区结束后,基准值会处于正确的排序位置,记下此时右指针
j的位置。 - 如果目标索引
k <= j,说明目标元素在左半区,递归处理左半区[l, j]。 - 否则,目标元素在右半区,递归处理右半区
[j + 1, r]。
- 分区结束后,基准值会处于正确的排序位置,记下此时右指针
- 快速收敛:与快排不同,我们不需要对两边都进行递归,而是根据
k的位置,只选择可能存在目标元素的一侧继续递归,以此将平均时间复杂度优化至O(n)。
🔥代码实现(含详细变量注释)
class Solution {
public int findKthLargest(int[] nums, int k) {
// n:记录数组的总长度
int n = nums.length;
// 第 k 大的元素,在升序排序后的数组中,对应的索引实际上是 n - k
return help(nums, 0, n - 1, n - k);
}
// help:快速选择算法的辅助递归函数
// nums:原数组;l:当前分区的左边界;r:当前分区的右边界;k:全局升序索引目标
public int help(int[] nums, int l, int r, int k) {
// 递归终止条件:如果区间内只有一个元素,那么这个元素就是我们要找的答案
if (l == r) return nums[r];
// x:选取当前区间最左侧的元素作为基准值(Pivot)
int x = nums[l];
// i:左侧扫描指针,初始化为 l - 1,用于配合 do-while 循环
int i = l - 1;
// j:右侧扫描指针,初始化为 r + 1,用于配合 do-while 循环
int j = r + 1;
// 开始 Hoare 分区过程,只要 i < j 就继续扫描
while (i < j) {
// 左指针向右移动,直到找到一个大于等于基准值 x 的元素
do {
i++;
} while (nums[i] < x);
// 右指针向左移动,直到找到一个小于等于基准值 x 的元素
do {
j--;
} while (nums[j] > x);
// 如果左右指针还没有相遇,则交换这两个不满足分区条件的元素
if (i < j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
// 分区结束后,右指针 j 及其左侧的元素都是小于等于基准值的部分。
// 判断目标索引 k 落在基准值 j 的左侧还是右侧
if (k <= j) {
// 如果 k 在 j 的左侧(包含 j),递归搜索左半区 [l, j]
return help(nums, l, j, k);
} else {
// 如果 k 在 j 的右侧,递归搜索右半区 [j + 1, r]
return help(nums, j + 1, r, k);
}
}
}
- ⏱️复杂度分析
时间复杂度:平均 O(N),最坏 O(N^2)。在基准值选择最差时(例如每次都是最大或最小值),复杂度退化为 O(N^2);但在大多数情况下,期望时间复杂度为线性 O(N)
空间复杂度:O(log N) 或 O(N)(取决于递归深度)。主要消耗在递归调用时系统隐式维护的函数调用栈空间。最坏情况下(如数组本就有序且总是选择端点作基准值),递归深度为 N。
🔍总结一下:
这是一道标准 Hoare 解法,其思想是定位基准元素 x ,遍历区间进行互换,然后根据j 和 k 的位置 递归重新定位区间
