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

0 阅读3分钟

给定整数数组 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

提示:

  • 1 <= k <= nums.length <= 105
  • -104 <= nums[i] <= 104

1. 快速选择算法 (Quick Select) —— “寻找接班人的选拔赛”

这是图一中的核心解法,也是面试官最想看到的 O(n)O(n) 平均时间复杂度的算法。

  • 生活案例:选拔全校身高第 K 名的学生

    • 划分 (Partition) :你随机挑一个学生(Pivot),让比他高的站左边,比他矮的站右边。

    • 判断

      • 如果“高个子组”的人数正好是 k1k-1 个,那么这个 Pivot 就是第 KK 名,直接带走。
      • 如果“高个子组”人太多了,说明第 KK 名还在左边,你只需要去左边继续这个过程,右边的人可以回教室了。
      • 反之,去右边找。
  • 代码逻辑

    JavaScript

    const quickSelect = (left, right) => {
        if (left >= right) return;
        // 随机挑一个基准值,避免最坏情况
        let pivot = nums[Math.floor(Math.random() * (right - left + 1)) + left];
        let lt = left, gt = right, i = left;
        // 三路划分:小于、等于、大于基准值
        while (i <= gt) {
            if (nums[i] < pivot) [nums[i], nums[lt]] = [nums[lt], nums[i]], i++, lt++;
            else if (nums[i] > pivot) [nums[i], nums[gt]] = [nums[gt], nums[i]], gt--;
            else i++;
        }
        // 只去目标索引 target 所在的区间递归
        if (target >= lt && target <= gt) return;
        else if (target < lt) quickSelect(left, lt - 1);
        else quickSelect(gt + 1, right);
    };
    

2. 桶排序 (Bucket Sort) —— “超市理货员的分类筐”

这是图二中的解法。它极快,但前提是数据范围不能太离谱。

  • 生活案例:整理 100 分制下的试卷

    • 你面前有 101 个筐(从 0 分到 100 分)。
    • 你把试卷往对应的筐里一扔。
    • 找第 KK 名时,你从 100 分的筐开始往回数,数到第 KK 张试卷,那个筐的标签就是分数。
  • 代码逻辑

    JavaScript

    var findKthLargest = function(nums, k) {
        let offset = 10000; // 处理负数
        let bucket = new Array(20001).fill(0); // 准备 20001 个“筐”
        for (let num of nums) {
            bucket[num + offset]++; // 往对应分数的筐里加 1
        }
        let count = 0;
        // 从大分数的筐开始往回找
        for (let i = bucket.length - 1; i >= 0; i--) {
            count += bucket[i];
            if (count >= k) return i - offset; // 数够了 K 个,收工
        }
    };
    

3. 直接排序 (Library Sort) —— “交给专业物流公司”

这是图三中的解法。这是最直观、代码量最少的保底方案。

  • 生活案例:快递公司自动化分拣

    • 你懒得分类,也懒得选拔,直接把所有包裹推上全自动分拣流水线。
    • 机器会帮你把所有东西排好序,你直接去传送带的第 KK 个位置拿就行了。
    • 代价:这需要消耗更多的能源(时间复杂度 O(nlogn)O(n \log n))。
  • 代码逻辑

    JavaScript

    var findKthLargest = function(nums, k) {
        // 直接调用系统内置排序,降序排列
        let arr = nums.sort((a, b) => b - a);
        return arr[k - 1]; // 取第 K 个
    };
    

总结对比

方法生活比喻优点缺点
快速选择精准选拔赛平均 O(n)O(n) ,面试首选代码实现相对复杂
桶排序分筐理货稳定 O(n)O(n) ,数据集中时极快数字范围太大时浪费内存(空间换时间)
直接排序自动化分拣代码极简,易于理解时间复杂度 O(nlogn)O(n \log n),效率不如前两者