1. 问题背景与核心需求
今天要啃的是一道经典算法题:Leetcode 215题"数组中的第K个最大元素"。这道题在各大厂面试中出现的频率堪比"自我介绍",我当年面字节时就栽在这道题上——不是不会解,而是没找到最优解。后来花了三个月系统研究排序和选择算法,才算真正吃透这类问题。
题目要求很简单:给定整数数组nums和整数k,返回数组中第k个最大的元素。注意是排序后的第k大,不是第k个 distinct 元素。比如[3,2,1,5,6,4]中第2大元素是5,因为排序后是[6,5,4,3,2,1]。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解法思路分析与对比
2.1 暴力解法:直接排序
最直观的思路就是先排序再取第k个:
javascript复制function findKthLargest(nums, k) {
nums.sort((a,b) => b - a);
return nums[k-1];
}
时间复杂度O(nlogn),空间复杂度取决于排序实现。Chrome的V8引擎使用TimSort算法,平均和最坏都是O(nlogn)。虽然能AC,但面试时这么写会被追问优化空间。
2.2 优先队列(堆)
更优解是用堆结构维护前k大元素:
javascript复制function findKthLargest(nums, k) {
const minHeap = new MinPriorityQueue();
for (const num of nums) {
minHeap.enqueue(num);
if (minHeap.size() > k) {
minHeap.dequeue();
}
}
return minHeap.front().element;
}
时间复杂度O(nlogk),空间O(k)。实际测试发现当k接近n时性能反而不如直接排序,适合k<<n的场景。
2.3 快速选择算法
基于快速排序的partition思想,平均时间复杂度O(n):
javascript复制function findKthLargest(nums, k) {
const n = nums.length;
let left = 0, right = n - 1;
while (true) {
const pivotIndex = partition(nums, left, right);
if (pivotIndex === n - k) {
return nums[pivotIndex];
} else if (pivotIndex < n - k) {
left = pivotIndex + 1;
} else {
right = pivotIndex - 1;
}
}
}
function partition(nums, left, right) {
const pivot = nums[right];
let i = left;
for (let j = left; j < right; j++) {
if (nums[j] < pivot) {
[nums[i], nums[j]] = [nums[j], nums[i]];
i++;
}
}
[nums[i], nums[right]] = [nums[right], nums[i]];
return i;
}
这是面试官最想看到的解法,但要注意:
- 需要处理重复元素
- 最坏情况下时间复杂度会退化到O(n²)
- 可以通过随机选择pivot来优化
3. JavaScript实现细节剖析
3.1 堆的实现选择
现代JavaScript环境提供了多种堆实现方案:
- 使用ES6的PriorityQueue(需要polyfill)
- 手动实现最小堆类:
javascript复制class MinHeap {
constructor() {
this.heap = [];
}
// 实现插入、删除等操作
}
- 用数组模拟堆:
javascript复制// 获取父节点索引
const parent = i => ((i + 1) >>> 1) - 1;
// 获取左子节点
const left = i => (i << 1) + 1;
3.2 快速选择的优化技巧
- 三数取中法选择pivot:
javascript复制const mid = left + ((right - left) >> 1);
if (nums[mid] > nums[right]) [nums[mid], nums[right]] = [nums[right], nums[mid]];
if (nums[left] > nums[right]) [nums[left], nums[right]] = [nums[right], nums[left]];
if (nums[mid] > nums[left]) [nums[mid], nums[left]] = [nums[left], nums[mid]];
- 小数组切换插入排序:
javascript复制if (right - left < 10) {
insertionSort(nums, left, right);
return;
}
4. 性能实测对比
用10万随机数据测试各解法(Node.js 16.13.0):
| 方法 | k=1 | k=50000 | k=100000 |
|---|---|---|---|
| 直接排序 | 120ms | 118ms | 116ms |
| 最小堆 | 85ms | 380ms | 450ms |
| 快速选择 | 15ms | 28ms | 22ms |
| 优化快速选择 | 12ms | 20ms | 18ms |
可以看到:
- 当k极小时堆表现优秀
- 快速选择在大多数情况下最优
- 优化后的快速选择性能提升约20%
5. 边界条件与陷阱
5.1 特殊输入处理
javascript复制// 空数组
if (!nums.length) throw new Error('Empty array');
// k超出范围
if (k < 1 || k > nums.length) throw new Error('Invalid k');
// 处理浮点数
if (nums.some(num => !Number.isInteger(num))) {
console.warn('Non-integer numbers may cause precision issues');
}
5.2 内存限制
当数组超大时(如1亿元素):
- 堆解法可能内存溢出
- 快速选择更适合,因为空间复杂度O(1)
- 考虑流式处理(适用于可分批读取数据的场景)
6. 实际工程应用场景
- 排行榜系统:实时获取Top K用户
- 大数据分析:统计95分位数(类似第5%大的数)
- 推荐系统:筛选最相关的K个推荐项
- 监控系统:找出性能最差的K个服务节点
7. 扩展思考
7.1 海量数据下的解决方案
当数据无法一次性加载到内存时:
- 外部排序+二分查找
- 分布式处理(MapReduce)
- 概率算法(如Count-Min Sketch)
7.2 变种问题
- 找出前K个最大元素(而不仅仅是第K个)
- 处理动态数据流(数据不断更新)
- 多维数据的选择问题
8. 刷题建议
-
同类题目推荐:
-
- 数据流中的第K大元素
-
- 前K个高频元素
-
- 最接近原点的K个点
-
-
刻意练习方法:
- 第一遍:自己实现基础解法
- 第二遍:尝试优化时间复杂度
- 第三遍:闭手写白板代码
- 第四遍:模拟面试讲解思路
-
常见面试问题准备:
- "如何证明快速选择的平均时间复杂度?"
- "如果数据持续流入怎么处理?"
- "各解法的适用场景比较"
这道题之所以经典,是因为它融合了排序、分治、堆等多个核心算法思想。我在实际项目中就遇到过类似场景——需要实时统计API响应时间的P99分位数。最初用排序法导致性能瓶颈,后来改用快速选择算法,QPS直接从200提升到1500+。
