【Leetcode hot 100】215.数组中的第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
提示:
1 <= k <= nums.length <= 10^5-10^4 <= nums[i] <= 10^4
问题解答
解法 1:快速选择算法(最优平均复杂度)
算法原理
快速选择基于 快速排序 的核心思想(分治):
- 分区(Partition):随机选择一个“基准值”,将数组分为两部分:左半部分元素 ≥ 基准值(降序分区,方便直接找“第 k 大”),右半部分元素 < 基准值。
- 定位目标:
- 若基准值的索引
pivotIndex恰好等于k-1(数组下标从 0 开始),则基准值就是第k大元素。 - 若
pivotIndex > k-1:目标在左半部分,递归处理左区间。 - 若
pivotIndex < k-1:目标在右半部分,递归处理右区间。
- 若基准值的索引
关键优化
- 随机基准:避免数组有序时(如 [1,2,3,4,5])分区失衡,将时间复杂度从最坏 O(n²) 优化到 平均 O(n)。
- 原地分区:无需额外空间,空间复杂度 O(log n)(递归栈深度)。
Java 代码实现
import java.util.Random;class Solution {private static final Random RANDOM = new Random();public int findKthLargest(int[] nums, int k) {// 快速选择的核心:在 [left, right] 区间找第 k 大元素return quickSelect(nums, 0, nums.length - 1, k - 1); // k-1 是目标的下标}/*** 在 nums[left..right] 中找到下标为 targetIndex 的元素(即第 targetIndex+1 大元素)*/private int quickSelect(int[] nums, int left, int right, int targetIndex) {// 分区:返回基准值的最终下标int pivotIndex = partition(nums, left, right);if (pivotIndex == targetIndex) {// 基准值就是目标元素,直接返回return nums[pivotIndex];} else if (pivotIndex > targetIndex) {// 目标在左半区,递归处理左区间return quickSelect(nums, left, pivotIndex - 1, targetIndex);} else {// 目标在右半区,递归处理右区间return quickSelect(nums, pivotIndex + 1, right, targetIndex);}}/*** 分区函数:随机选基准,将数组分为 [≥基准, 基准, <基准] 三部分,返回基准下标*/private int partition(int[] nums, int left, int right) {// 1. 随机选择基准(避免有序数组分区失衡)int randomPivot = left + RANDOM.nextInt(right - left + 1);// 2. 基准值交换到区间末尾(方便后续遍历比较)swap(nums, randomPivot, right);int pivot = nums[right]; // 基准值int i = left - 1; // i 是“左半区(≥基准)”的最后一个元素下标// 3. 遍历区间 [left, right-1],将 ≥ 基准的元素归入左半区for (int j = left; j < right; j++) {if (nums[j] >= pivot) {i++; // 左半区扩大swap(nums, i, j); // 将当前元素加入左半区}}// 4. 将基准值交换到左半区的末尾(基准值的最终位置)swap(nums, i + 1, right);return i + 1; // 返回基准值的下标}// 交换数组中两个元素的位置private void swap(int[] nums, int i, int j) {int temp = nums[i];nums[i] = nums[j];nums[j] = temp;}
}
解法 2:计数排序(适用于数值范围有限的场景)
算法原理
利用题目中 数值范围固定 的特点(-10⁴ ≤ nums[i] ≤ 10⁴):
- 偏移量处理:由于存在负数,用
offset = 10⁴将所有数值映射到非负区间(0 ≤ nums[i]+offset ≤ 20000)。 - 计数数组:创建长度为
20001的计数数组count,统计每个数值出现的次数(count[num+offset]++)。 - 逆序遍历计数数组:从最大数值(对应
count[20000])开始累加次数,当累加和 ≥k时,当前数值即为第k大元素。
Java 代码实现
class Solution {public int findKthLargest(int[] nums, int k) {final int OFFSET = 10000; // 偏移量,将负数映射为非负数int[] count = new int[20001]; // 覆盖 [-10000, 10000] 的所有可能值// 1. 统计每个数值的出现次数for (int num : nums) {count[num + OFFSET]++;}// 2. 逆序遍历计数数组,找第 k 大元素int remaining = k; // 剩余需要找的“大元素”个数for (int i = 20000; i >= 0; i--) {if (count[i] > 0) {if (count[i] >= remaining) {// 当前数值的出现次数足够覆盖剩余需求,返回当前数值(减去偏移量)return i - OFFSET;}// 否则,减去当前数值的出现次数,继续找更小的数值remaining -= count[i];}}// 理论上不会走到这里(题目保证 k 有效)return -1;}
}
两种解法对比
| 维度 | 快速选择算法 | 计数排序 |
|---|---|---|
| 时间复杂度 | 平均 O(n),最坏 O(n²) | 严格 O(n + M)(M=20001) |
| 空间复杂度 | O(log n)(递归栈) | O(M)(固定空间,可视为 O(1)) |
| 适用场景 | 数值范围大、数据量极大的场景 | 数值范围固定且较小的场景 |
| 稳定性 | 不稳定(分区会打乱顺序) | 稳定 |
