215. 数组中的第K个最大元素

215. 数组中的第K个最大元素

[此处请插入:快速选择(Quick Select)算法分区过程示意图]

✨核心逻辑

本题要求在时间复杂度为 O(n) 的约束下解决,因此采用 快速选择(Quick Select) 算法,其基于快速排序的分治思想:

  1. 目标索引映射:求第 k 大的元素,实际上等同于求排序后数组中下标为 n - k 的元素。
  2. 分区操作(Hoare 分区法):在区间 [l, r] 内选取基准值 x,利用双指针 ij 分别从左右两端向中间扫描。将小于基准值的元素放在左侧,大于基准值的放在右侧。
  3. 分治递归
    • 分区结束后,基准值会处于正确的排序位置,记下此时右指针 j 的位置。
    • 如果目标索引 k <= j,说明目标元素在左半区,递归处理左半区 [l, j]
    • 否则,目标元素在右半区,递归处理右半区 [j + 1, r]
  4. 快速收敛:与快排不同,我们不需要对两边都进行递归,而是根据 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 的位置 递归重新定位区间

image-Kbhx.png