在算法面试与日常开发中,“Top K”问题犹如一座必翻的山峰,而LeetCode 215题“数组中的第K个最大元素”正是其中最经典的代表。它不仅要求你找出这个元素,更迫使你在时间效率、空间开销与算法稳定性之间做出精明的权衡。掌握LeetCode 215 数组中的第K个最大元素的核心价值在于,它是一道绝佳的“一题多解”案例,系统性地串联起快速排序、堆数据结构以及基于分治的快速选择算法等核心知识,是检验你算法基本功与问题分析能力的试金石。
一、问题重述与本质分析

给定整数数组 `nums` 和整数 `k`,请返回数组中第 `k` 个最大的元素。请注意,你需要找的是数组排序后的第 `k` 个最大的元素,而不是第 `k` 个不同的元素。
示例: 输入: [3,2,1,5,6,4], k = 2 输出: 5 解释: 排序后为 [1,2,3,4,5,6],第二大的元素是5。
问题核心: 这本质上是一个选择问题,而非完全的排序问题。我们不需要让整个数组有序,只需要定位到排序后特定位置(第 `n-k+1` 小,或按降序排的第 `k` 个)的元素。理解这一点是抛弃低效方案、选择高效算法的起点。
二、解法一:排序法(暴力但直观)
最直接的思路是将数组排序,然后通过索引直接访问。
// 降序排序后取第k-1个索引
public int findKthLargest(int[] nums, int k) {
Arrays.sort(nums); // Java默认升序排序
return nums[nums.length - k];
}
复杂度分析: - **时间复杂度 O(n log n)**:Java的 `Arrays.sort()` 对于基本类型使用双轴快速排序,平均复杂度为 O(n log n)。 - **空间复杂度 O(log n)**:排序算法递归调用栈的深度。
评价: 代码极其简洁,在实际工程中,如果数据量不大或对性能不敏感,这常常是可接受的方案。但在面试中,仅给出此解法通常只能获得基础分,因为它没有体现出对问题特殊性的优化思考。
三、解法二:基于堆(优先队列)的选择
这是处理Top K问题的标准数据结构解法。我们可以维护一个大小为 `k` 的最小堆。
算法思想: - 堆顶始终是堆内最小的元素。 - 遍历数组,当堆大小小于 `k` 时,直接加入元素。 - 当堆大小等于 `k` 时,如果当前元素大于堆顶(即它比当前堆里最小的元素大),则弹出堆顶(移除最小的),将当前元素入堆。 - 遍历完成后,堆顶就是整个数组中第 `k` 大的元素(因为堆里保存了最大的 `k` 个元素,其中最小的那个就是第 `k` 大)。
public int findKthLargest(int[] nums, int k) {
// 创建大小为k的最小堆
PriorityQueue minHeap = new PriorityQueue<>(k);
for (int num : nums) {
if (minHeap.size() < k) {
minHeap.offer(num);
} else if (num > minHeap.peek()) {
// 当前元素比堆顶大,替换堆顶
minHeap.poll();
minHeap.offer(num);
}
// 否则,当前元素小于等于堆顶,忽略
}
return minHeap.peek();
}
复杂度分析: - **时间复杂度 O(n log k)**:遍历 n 个元素,每次堆操作(插入或删除)的复杂度为 O(log k)。 - **空间复杂度 O(k)**:需要维护一个大小为 k 的堆。
评价: 该方法非常适用于海量数据流的场景(数据无法一次性装入内存),或者当 `k` 远小于 `n` 时,效率比全局排序高。它也是面试中非常受欢迎的解法,体现了对数据结构的熟练运用。
四、解法三:快速选择算法(期望最优解)
这是本题的算法核心考点。它基于快速排序的分区思想,但每次只递归处理目标元素所在的那一侧,从而将平均时间复杂度降至 O(n)。
算法步骤: 1. **随机选择枢轴(Pivot)**:从当前子数组中随机选择一个元素作为枢轴。这是为了避免在有序数组情况下陷入最坏 O(n²) 情况,至关重要。 2. **分区操作**:将数组重新排列,使得所有比枢轴大的元素移到其左侧,比枢轴小的元素移到其右侧(降序分区)。分区结束后,枢轴位于其最终的正确位置,设其索引为 `pivotIndex`。 3. **判断与递归**: - 如果 `pivotIndex == k - 1`(注意,我们这里可以按降序找第k个),那么枢轴就是我们要找的元素。 - 如果 `pivotIndex > k - 1`,说明第k大的元素在枢轴的左侧,我们递归地在左子数组进行快速选择。 - 如果 `pivotIndex < k - 1`,说明第k大的元素在枢轴的右侧,我们递归地在右子数组进行快速选择,但注意在右子数组中寻找的是第 `k - (pivotIndex + 1)` 大的元素。
代码实现(经典版):
public int findKthLargest(int[] nums, int k) {
return quickSelect(nums, 0, nums.length - 1, k);
}
private int quickSelect(int[] nums, int left, int right, int k) {
if (left == right) return nums[left]; // 只有一个元素
// 随机选择枢轴,并将其交换到最右端(或最左端)
int pivotIndex = left + (int)(Math.random() * (right - left + 1));
swap(nums, pivotIndex, right);
// 分区操作:目标是降序,大于枢轴的放左边
int storeIndex = left;
for (int i = left; i < right; i++) {
if (nums[i] > nums[right]) { // 注意:比较的是最右端的枢轴值
swap(nums, storeIndex, i);
storeIndex++;
}
}
// 将枢轴交换到最终位置
swap(nums, storeIndex, right);
pivotIndex = storeIndex;
// 判断枢轴位置与k的关系
if (pivotIndex == k - 1) {
return nums[pivotIndex];
} else if (pivotIndex > k - 1) {
return quickSelect(nums, left, pivotIndex - 1, k);
} else {
return quickSelect(nums, pivotIndex + 1, right, k);
}
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
复杂度分析: - **平均时间复杂度 O(n)**:由递推式 T(n) = T(n/2) + O(n) 推导得出。 - **最坏时间复杂度 O(n²)**:当每次分区都极不平衡时(如数组已有序且枢轴选择不佳),但随机化枢轴使这种情况在现实中概率极低。 - **空间复杂度 O(log n)**:递归调用栈的深度。
在“鳄鱼java”网站的算法攻坚系列中,快速选择算法被反复强调为必须掌握的“内功”,因为它体现了分治和随机化思想的威力。
五、三种解法的深度对比与应用场景
理解LeetCode 215 数组中的第K个最大元素的不同解法,关键是要知道何时用何种方法。
1. 排序法: - **场景**:数据量小、对代码简洁性要求高、或需要多次查询不同K值(预处理一次,多次查询)。 - **优点**:实现简单,不易出错。 - **缺点**:当 `n` 很大而 `k` 很小时,做了大量无用排序。
2. 堆(优先队列)法: - **场景**:`k` 远小于 `n`、处理数据流、或需要动态维护Top K列表。 - **优点**:时间复杂度稳定,空间可控,适合海量数据。 - **缺点**:平均性能不如随机化的快速选择。
3. 快速选择算法: - **场景**:一次性静态数组、要求平均时间复杂度最优、内存空间相对受限。 - **优点**:平均O(n)时间,原地操作。 - **缺点**:最坏情况理论上是O(n²)(尽管可通过随机化避免),会修改原数组,且递归有栈溢出风险(对于极大n)。
六、常见误区、边界条件与面试精讲
编码误区: 1. **索引混淆**:第k大元素在升序排序数组中的索引是 `n-k`,在降序分区中则是 `k-1`。 2. **堆的类型选错**:求第k大要用最小堆(保留最大的k个),求第k小要用最大堆。 3. **快速选择未随机化枢轴**:这是面试官考察你是否知道算法缺陷和优化方法的关键点。 4. **分区逻辑错误**:确保分区操作的方向(大于还是小于)与目标一致。
边界条件: - `k` 等于 1 或等于 `nums.length`。 - 数组所有元素相同。 - 数组长度为1。
面试讲述策略: 1. **从简单到复杂**:先提排序法,指出其不足。 2. **重点阐述堆解法**,说明其适用场景(数据流、k小)。 3. **引出并详细讲解快速选择**,强调随机化枢轴的重要性,推导平均复杂度。 4. **主动对比三种方法**,展示你的全面分析能力。
七、变体与进阶:从一道题到一类题
彻底掌握本题后,你便有能力解决一系列衍生问题:
1. 求第K个最小元素: 逻辑完全对称,快速选择时分区条件相反;使用最大堆。
2. 求前K个最大/最小的元素集合(LeetCode 347前K个高频元素的思想): 堆解法是天然选择,快速选择需要稍作修改以收集分区左侧所有元素。
3. 在数据流中实时获取中位数(LeetCode 295): 需要使用两个堆(一个大顶堆,一个小顶堆)来动态维护。
4. 快速选择的迭代实现: 可以避免递归栈开销,使用循环实现,是应对超大n的优化手段。
八、总结:选择的力量
LeetCode 215 数组中的第K个最大元素之所以经典,在于它清晰地展示了算法设计的层次感:从直接的排序,到基于数据结构的优化,再到利用问题特性(只需选择,无需全排序)的分治随机化算法。每一种解法都有其存在的土壤。
在面试和工程中,没有绝对的“最佳解法”,只有“最适合当前场景的解法”。你的价值在于,能够清晰分析数据特征(n的大小、k的大小、数据是否静态、内存限制等),并做出合理的技术选型。
现在,请你思考:如果题目条件变为——数组大到无法一次性装入内存,但 `k` 的值也非常大(例如 `n/2`),你该如何调整策略?这会将你的思维引向外部排序和更复杂的选择算法,而这正是深度掌握本题后自然打开的下一扇门。
版权声明
本文仅代表作者观点,不代表百度立场。
本文系作者授权百度百家发表,未经许可,不得转载。





