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

难度:🟡 中等

标签数组分治快速选择排序

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

题目描述

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

解题思路

核心思想

本题的目标是在一个未排序的数组中,找到第 k 大的元素。一个直观的方法是对数组完全排序然后取倒数第 k 个元素,但其时间复杂度为 O(n log n),不符合题目要求。要实现 O(n) 的时间复杂度,我们需要更高效的算法。

思路选择

快速选择算法 (Quickselect) 是解决此问题的标准最优思路。

  • 快速选择算法是快速排序算法的一个变种。快速排序的核心是“分区”操作,它会选择一个“枢轴”(pivot) 并将数组分为两部分:小于枢轴的元素和大于枢轴的元素。分区操作结束后,枢轴元素就位于其最终排序后的正确位置上。

  • 我们利用这个特性。每次分区后,我们检查枢轴的索引 j

    • 如果 j 恰好等于我们想找的索引(第 k 大的元素,其索引为 n-k),那么我们就找到了答案。

    • 如果 j 大于目标索引,说明第 k 大的元素在枢轴的左边,我们只需在左半部分递归地继续查找。

    • 如果 j 小于目标索引,说明第 k 大的元素在枢轴的右边,我们只需在右半部分递归地继续查找。

  • 由于每次都只处理其中一半的数组,其平均时间复杂度可以达到 O(n)。

关键步骤

  1. 问题转换:将寻找“第 k 大”的元素,转换为寻找“第 n-k 小”的元素(按索引从 0 开始算)。

  2. 定义快速选择函数 quickselect(nums, left, right, targetIndex): a. 在 [left, right] 区间内,选择一个枢轴(例如 nums[left])。 b. 执行一次分区操作,使得所有小于枢轴的元素在左边,大于枢轴的元素在右边,返回枢轴最终的索引 j。 c. 判断与递归: i. 如果 targetIndex === j,返回 nums[j]。 ii. 如果 targetIndex < j,在左半部分 [left, j-1] 递归调用 quickselect。 iii. 如果 targetIndex > j,在右半部分 [j+1, right] 递归调用 quickselect

代码实现

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var findKthLargest = function(nums, k) {
    const n = nums.length;
    // 寻找第 k 大的元素,等价于寻找第 n-k 小的元素(索引从0开始)
    return quickselect(nums, 0, n - 1, n - k);
};

/**
 * 快速选择辅助函数
 * @param {number[]} nums - 输入数组
 * @param {number} left - 当前搜索范围的左边界
 * @param {number} right - 当前搜索范围的右边界
 * @param {number} targetIndex - 目标元素的索引
 * @return {number}
 */
function quickselect(nums, left, right, targetIndex) {
    // 递归终止条件:当分区大小为1时,直接返回该元素
    if (left === right) return nums[left];

    // 选择第一个元素作为枢轴
    const pivotValue = nums[left];
    let i = left - 1, j = right + 1;

    // 快速排序的划分过程
    while (i < j) {
        // 从左向右找到第一个大于等于枢轴的元素
        do i++; while (nums[i] < pivotValue);
        // 从右向左找到第一个小于等于枢轴的元素
        do j--; while (nums[j] > pivotValue);

        // 交换这两个元素
        if (i < j) {
            [nums[i], nums[j]] = [nums[j], nums[i]];
        }
    }

    // 递归查找:根据枢轴的位置 j,决定在哪一半区间继续搜索
    if (targetIndex <= j) {
        return quickselect(nums, left, j, targetIndex);
    } else {
        return quickselect(nums, j + 1, right, targetIndex);
    }
}

复杂度分析

  • 时间复杂度平均 O(n)最坏 O(n²) 平均情况下,每次分区操作都能将问题规模减半,总时间复杂度为 n + n/2 + n/4 + ... = 2n,即 O(n)。最坏情况下(每次都选到最大或最小的元素作为枢轴),算法退化为 O(n²)。

  • 空间复杂度平均 O(log n)最坏 O(n) 空间消耗主要来自递归调用栈。

相关题目

总结

本题是“Top K”问题的经典范例。虽然排序后取值或使用堆(优先队列)也能解决,但快速选择算法提供了一个平均时间复杂度为 O(n) 的高效解法。它充分利用了快排的分区思想,通过“减治法”不断缩小问题规模,是面试中考察算法基本功和优化能力的高频题。

Copyright © Jun 2025 all right reserved,powered by Gitbook该文件修订时间: 2025-07-03 17:35:08

results matching ""

    No results matching ""

    results matching ""

      No results matching ""