【每日算法】LeetCode215. 数组中的第K个最大元素

对前端开发者而言,学习算法绝非为了"炫技"。它是你从"页面构建者"迈向"复杂系统设计者"的关键阶梯。它将你的编码能力从"实现功能"提升到"设计优雅、高效解决方案"的层面。从现在开始,每天投入一小段时间,结合前端场景去理解和练习,你将会感受到自身技术视野和问题解决能力的质的飞跃。------ 算法:资深前端开发者的进阶引擎

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. 问题分析

从前端开发的视角看这个问题:

  1. 业务场景:排行榜系统中取第K名用户、性能监控中取响应时间第K长的请求、大数据可视化中筛选关键数据点
  2. 关键理解 :第K个最大元素 = 排序后从后往前数的第K个 = 排序后下标为 n-k 的元素
  3. 挑战点:当数据量巨大时(如前端的海量日志数据),不能简单排序所有数据
  4. 前端关联:类似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 前端实际应用场景

  1. 性能监控:从大量性能数据中找出最慢的K个请求
  2. 数据分析可视化:在图表中显示Top K数据点
  3. 排行榜系统:显示前K名用户
  4. 资源调度:选择性能最差的K个节点进行优化

6.3 LeetCode类似题目

  1. 简单难度

      1. 数据流中的第K大元素(最小堆的典型应用)
    • 剑指 Offer 40. 最小的k个数(思路完全相同)
  2. 中等难度

      1. 前K个高频元素(结合哈希表和堆)
      1. 最接近原点的K个点(二维空间的距离计算)
      1. 找到K个最接近的元素(在有序数组中查找)
  3. 困难难度

      1. 寻找两个正序数组的中位数(可看作K=(m+n)/2的特殊情况)
      1. 数据流的中位数(需要维护两个堆)
相关推荐
im_AMBER几秒前
Leetcode 97 移除链表元素
c++·笔记·学习·算法·leetcode·链表
代码猎人2 分钟前
substring和substr有什么区别
前端
pimkle2 分钟前
visactor vTable 在移动端支持 ellipsis 气泡
前端
donecoding2 分钟前
告别 scrollIntoView 的“越级滚动”:一行代码解决横向滚动问题
前端·javascript
0__O2 分钟前
如何在 monaco 中实现自定义语言的高亮
前端·javascript·编程语言
海奥华23 分钟前
Golang Channel 原理深度解析
服务器·开发语言·网络·数据结构·算法·golang
Jasmine_llq4 分钟前
《P3200 [HNOI2009] 有趣的数列》
java·前端·算法·线性筛法(欧拉筛)·快速幂算法(二进制幂)·勒让德定理(质因子次数统计)·组合数的质因子分解取模法
呆头鸭L5 分钟前
快速上手Electron
前端·javascript·electron
Aliex_git9 分钟前
性能指标笔记
前端·笔记·性能优化
秋天的一阵风9 分钟前
🌟 藏在 Vue3 源码里的 “二进制艺术”:位运算如何让代码又快又省内存?
前端·vue.js·面试