对前端开发者而言,学习算法绝非为了"炫技"。它是你从"页面构建者"迈向"复杂系统设计者"的关键阶梯。它将你的编码能力从"实现功能"提升到"设计优雅、高效解决方案"的层面。从现在开始,每天投入一小段时间,结合前端场景去理解和练习,你将会感受到自身技术视野和问题解决能力的质的飞跃。------ 算法:资深前端开发者的进阶引擎
LeetCode215. 数组中的第K个最大元素
1. 题目描述
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
示例 1:
输入: [3,2,1,5,6,4], k = 2
输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6], k = 4
输出: 4
2. 问题分析
从前端开发的视角看这个问题:
- 业务场景:排行榜系统中取第K名用户、性能监控中取响应时间第K长的请求、大数据可视化中筛选关键数据点
- 关键理解 :第K个最大元素 = 排序后从后往前数的第K个 = 排序后下标为
n-k的元素 - 挑战点:当数据量巨大时(如前端的海量日志数据),不能简单排序所有数据
- 前端关联:类似React中Virtual DOM的diff算法需要快速找到关键节点
3. 解题思路
3.1 直接排序法
最简单直观的方法:排序后直接取第K个元素。
- 时间复杂度:O(n log n)
- 空间复杂度:O(log n) ~ O(n)(取决于排序算法)
3.2 最小堆法
维护一个大小为K的最小堆,遍历数组,堆中始终保持最大的K个元素。
- 时间复杂度:O(n log k)
- 空间复杂度:O(k)
3.3 快速选择法(最优解)
基于快速排序的分区思想,每次只处理一半数据,类似二分查找。
- 平均时间复杂度:O(n)
- 最坏时间复杂度:O(n²)(可通过随机化避免)
- 空间复杂度:O(1)(原地分区)
最优解:快速选择法,特别是经过优化的随机化快速选择。
4. 各思路代码实现
4.1 直接排序法
javascript
/**
* 方法1:直接排序法
* 思路:排序后直接取第K个最大元素
* 时间复杂度:O(n log n)
* 空间复杂度:取决于排序算法实现
*/
function findKthLargestSort(nums, k) {
// 1. 降序排序
nums.sort((a, b) => b - a);
// 2. 返回第k-1个元素(数组从0开始索引)
return nums[k - 1];
}
// 示例说明
const nums1 = [3, 2, 1, 5, 6, 4];
const k1 = 2;
console.log(findKthLargestSort(nums1, k1)); // 输出: 5
// 步骤分解:
// 1. 排序后: [6, 5, 4, 3, 2, 1]
// 2. 取第2个元素: 5
4.2 最小堆法
javascript
/**
* 方法2:最小堆法
* 思路:维护一个大小为K的最小堆,堆顶是当前K个最大元素中最小的
* 适合处理数据流或海量数据(只需存储K个元素)
*/
class MinHeap {
constructor() {
this.heap = [];
}
// 获取父节点索引
parent(i) { return Math.floor((i - 1) / 2); }
// 获取左子节点索引
left(i) { return 2 * i + 1; }
// 获取右子节点索引
right(i) { return 2 * i + 2; }
// 插入元素
insert(val) {
this.heap.push(val);
this._siftUp(this.heap.length - 1);
}
// 删除堆顶元素
extractMin() {
if (this.heap.length === 0) return null;
if (this.heap.length === 1) return this.heap.pop();
const min = this.heap[0];
this.heap[0] = this.heap.pop();
this._siftDown(0);
return min;
}
// 获取堆顶元素
peek() {
return this.heap.length > 0 ? this.heap[0] : null;
}
// 获取堆大小
size() {
return this.heap.length;
}
// 上浮操作
_siftUp(i) {
while (i > 0 && this.heap[this.parent(i)] > this.heap[i]) {
[this.heap[this.parent(i)], this.heap[i]] =
[this.heap[i], this.heap[this.parent(i)]];
i = this.parent(i);
}
}
// 下沉操作
_siftDown(i) {
let minIndex = i;
const n = this.heap.length;
const left = this.left(i);
if (left < n && this.heap[left] < this.heap[minIndex]) {
minIndex = left;
}
const right = this.right(i);
if (right < n && this.heap[right] < this.heap[minIndex]) {
minIndex = right;
}
if (i !== minIndex) {
[this.heap[i], this.heap[minIndex]] = [this.heap[minIndex], this.heap[i]];
this._siftDown(minIndex);
}
}
}
function findKthLargestHeap(nums, k) {
const minHeap = new MinHeap();
// 遍历数组
for (let i = 0; i < nums.length; i++) {
if (minHeap.size() < k) {
// 堆未满,直接插入
minHeap.insert(nums[i]);
} else if (nums[i] > minHeap.peek()) {
// 当前元素比堆顶大,替换堆顶
minHeap.extractMin();
minHeap.insert(nums[i]);
}
// 否则忽略该元素(它比当前K个最大元素中的最小者还小)
}
// 堆顶就是第K个最大元素
return minHeap.peek();
}
// 示例说明
const nums2 = [3, 2, 3, 1, 2, 4, 5, 5, 6];
const k2 = 4;
console.log(findKthLargestHeap(nums2, k2)); // 输出: 4
// 步骤分解(k=4):
// 遍历过程堆的变化(显示堆顶元素):
// 1. 插入3 -> [3]
// 2. 插入2 -> [2,3]
// 3. 插入3 -> [2,3,3]
// 4. 插入1 -> [1,2,3,3]
// 5. 2>1,替换 -> [2,2,3,3]
// 6. 4>2,替换 -> [2,3,3,4]
// 7. 5>2,替换 -> [3,4,3,5]
// 8. 5>3,替换 -> [3,4,5,5]
// 9. 6>3,替换 -> [4,5,5,6]
// 最终堆顶为4
4.3 快速选择法(最优解)
javascript
/**
* 方法3:快速选择法(优化版)
* 思路:基于快速排序的分区思想,每次只处理一半数据
* 关键优化:随机选择pivot避免最坏情况
*/
function findKthLargestQuickSelect(nums, k) {
// 转换为寻找第(n-k+1)小的元素
return quickSelect(nums, 0, nums.length - 1, nums.length - k);
/**
* 快速选择递归函数
* @param {number[]} arr - 数组
* @param {number} left - 左边界
* @param {number} right - 右边界
* @param {number} k_smallest - 第k小的元素索引
*/
function quickSelect(arr, left, right, k_smallest) {
// 如果只有一个元素,直接返回
if (left === right) return arr[left];
// 随机选择pivot,避免最坏情况
const pivotIndex = partition(arr, left, right);
// 如果pivot正好是第k小的元素
if (k_smallest === pivotIndex) {
return arr[k_smallest];
}
// 如果k_smallest在pivot左边,只处理左半部分
else if (k_smallest < pivotIndex) {
return quickSelect(arr, left, pivotIndex - 1, k_smallest);
}
// 否则处理右半部分
else {
return quickSelect(arr, pivotIndex + 1, right, k_smallest);
}
}
/**
* 分区函数:将数组分为两部分,左边小于pivot,右边大于pivot
* 返回pivot的最终位置
*/
function partition(arr, left, right) {
// 随机选择pivot并交换到最右端
const randomIndex = left + Math.floor(Math.random() * (right - left + 1));
[arr[randomIndex], arr[right]] = [arr[right], arr[randomIndex]];
const pivot = arr[right];
let i = left; // i指向小于pivot的区域的末尾
for (let j = left; j < right; j++) {
// 如果当前元素小于等于pivot,交换到前面
if (arr[j] <= pivot) {
[arr[i], arr[j]] = [arr[j], arr[i]];
i++;
}
}
// 将pivot放到正确位置
[arr[i], arr[right]] = [arr[right], arr[i]];
return i;
}
}
// 示例说明
const nums3 = [3, 2, 1, 5, 6, 4];
const k3 = 2;
console.log(findKthLargestQuickSelect(nums3, k3)); // 输出: 5
// 步骤分解(寻找第2大元素 = 第5小元素,n=6, k=2, n-k=4):
// 初始: [3,2,1,5,6,4], left=0, right=5, k_smallest=4
// 1. 随机选择pivot,假设选到3,分区后: [2,1,3,5,6,4], pivotIndex=2
// 2. k_smallest(4) > pivotIndex(2),处理右半部分[5,6,4]
// 3. 在[5,6,4]中随机选pivot,假设选到6,分区后: [5,4,6], pivotIndex=4
// 4. k_smallest(4) == pivotIndex(4),返回arr[4]=5
4.4 使用内置API的简洁写法
javascript
/**
* 方法4:使用JavaScript内置API(面试时需说明原理)
* 实际工作中可根据数据量选择合适方法
*/
function findKthLargestBuiltIn(nums, k) {
// 使用快速选择的原理,但依赖语言内置实现
// 注意:不同JavaScript引擎实现可能不同
nums.sort((a, b) => b - a);
return nums[k - 1];
}
// 或使用nth_element思想(如果环境支持)
function findKthLargestPartialSort(nums, k) {
// 部分排序:只确保前k个元素是最大的
// 这在实际前端性能优化中很有用
return nums.sort((a, b) => b - a)[k - 1];
}
5. 各实现思路的复杂度、优缺点对比
| 方法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|---|
| 直接排序法 | O(n log n) | O(log n) ~ O(n) | 实现简单,代码可读性强 | 当n很大时效率低,排序了整个数组 | 数据量小(n < 1000),对性能要求不高 |
| 最小堆法 | O(n log k) | O(k) | 适合处理数据流,内存消耗固定 | 实现相对复杂,常数因子较大 | 数据流处理,海量数据(n > 10^6),k较小 |
| 快速选择法 | 平均O(n),最坏O(n²) | O(log n)递归栈 | 平均性能最优,原地操作 | 最坏情况性能差,需随机化优化 | 通用场景,特别适合中等规模数据 |
| 内置API法 | 取决于引擎实现 | 取决于引擎实现 | 代码极其简洁 | 不可控,不同环境表现不同 | 快速原型开发,非性能关键路径 |
6. 总结
6.1 通用解题模板
对于"第K个元素"这类问题,通用思路如下:
javascript
/**
* 第K个元素问题通用解决框架
*/
function findKthElement(nums, k, comparator = (a, b) => a - b) {
// 方法选择策略:
// 1. 如果k接近n(如k>n/2),考虑转化为找第(n-k+1)小元素
// 2. 根据数据规模选择算法:
// - 小数据(n<1000):直接排序
// - 大数据流:堆方法
// - 中等数据,内存有限:快速选择
// 示例:基于数据规模的自适应选择
const n = nums.length;
if (n < 1000) {
// 小数据直接排序
return nums.sort(comparator)[k - 1];
} else if (k < 100 && n > 10000) {
// 大数据且k小,用堆
return heapMethod(nums, k, comparator);
} else {
// 通用情况用快速选择
return quickSelectMethod(nums, k, comparator);
}
}
6.2 前端实际应用场景
- 性能监控:从大量性能数据中找出最慢的K个请求
- 数据分析可视化:在图表中显示Top K数据点
- 排行榜系统:显示前K名用户
- 资源调度:选择性能最差的K个节点进行优化
6.3 LeetCode类似题目
-
简单难度:
-
- 数据流中的第K大元素(最小堆的典型应用)
- 剑指 Offer 40. 最小的k个数(思路完全相同)
-
-
中等难度:
-
- 前K个高频元素(结合哈希表和堆)
-
- 最接近原点的K个点(二维空间的距离计算)
-
- 找到K个最接近的元素(在有序数组中查找)
-
-
困难难度:
-
- 寻找两个正序数组的中位数(可看作K=(m+n)/2的特殊情况)
-
- 数据流的中位数(需要维护两个堆)
-