前端算法实战:大小堆原理与应用详解(React中优先队列实现|求前K个最大数/高频元素)

前端算法实战:大小堆原理与应用详解

一、什么是堆?

堆(Heap)是一种特殊的完全二叉树数据结构,它满足以下性质:
大顶堆 :每个父节点的值 ≥ 子节点的值
小顶堆:每个父节点的值 ≤ 子节点的值


小顶堆

以下是 JavaScript 实现小顶堆排序的完整代码及原理解析,结合多个优质题解整理:


一、核心代码实现
javascript 复制代码
function swap(arr, i, j) {
    [arr[i], arr[j]] = [arr[j], arr[i]];
}

function minHeapify(arr, index, heapSize) {
    const left = 2 * index + 1;    // 左子节点索引
    const right = 2 * index + 2;  // 右子节点索引
    let smallest = index;          // 初始化最小值索引为当前节点
    
    // 比较左子节点与当前最小值
    if (left < heapSize && arr[left] < arr[smallest]) {
        smallest = left;
    }
    
    // 比较右子节点与当前最小值
    if (right < heapSize && arr[right] < arr[smallest]) {
        smallest = right;
    }
    
    // 若最小值不是当前节点,则交换并递归调整
    if (smallest !== index) {
        swap(arr, index, smallest);
        minHeapify(arr, smallest, heapSize); // 递归调整受影响的子树
    }
}

function heapSortDesc(arr) {
    const n = arr.length;
    
    // 构建小顶堆(从最后一个非叶子节点开始调整)
    for (let i = Math.floor(n/2)-1; i >= 0; i--) {
        minHeapify(arr, i, n);
    }
    
    // 逐步将堆顶元素(最小值)移到数组末尾,生成降序排列
    for (let i = n-1; i > 0; i--) {
        swap(arr, 0, i);          // 交换堆顶与当前末尾元素
        minHeapify(arr, 0, i);     // 调整剩余元素为小顶堆
    }
    return arr;
}

二、算法原理详解
1. 小顶堆性质

结构特性 :完全二叉树,所有父节点值 ≤ 子节点值

排序结果:降序排列(通过反复提取堆顶最小值到数组末尾实现)

2. 关键步骤
  1. 建堆阶段

    • 从最后一个非叶子节点(Math.floor(n/2)-1)开始调整

    • 自底向上确保每个子树满足小顶堆性质

  2. 排序阶段

    • 将堆顶最小值交换到数组末尾

    • 缩小堆范围后重新调整剩余元素为小顶堆


三、执行流程示例

以数组 [5, 2, 1, 9, 6] 为例:

javascript 复制代码
原始数组:5, 2, 1, 9, 6

建堆过程:
1. 调整索引1的子树(元素2)→ 形成子树:2,9,6
2. 调整索引0的子树(元素5)→ 形成堆顶1
最终小顶堆:1,2,5,9,6

排序阶段:
第一次交换:6 ↔ 1 → 新堆 [2,6,5,9] → 调整后堆顶2
第二次交换:9 ↔ 2 → 新堆 [5,6,9] → 调整后堆顶5
第三次交换:6 ↔ 5 → 新堆 [6,9] → 调整后堆顶6
第四次交换:9 ↔ 6 → 排序完成
最终结果:9,6,5,2,1(降序)

四、复杂度与优化
维度 说明 优化建议
时间复杂度 构建堆 O(n),每次调整 O(logn) → 总 O(n logn) 使用迭代替代递归减少栈空间
空间复杂度 O(1) 原地排序 避免创建临时数组
稳定性 不稳定(交换可能破坏相同元素顺序) 对自定义对象需额外处理

五、实际应用场景
  1. Top K 最小值查询:构建大小为 K 的小顶堆,遍历数组保留最小元素
  2. 优先级队列:动态维护最小优先级任务
  3. 内存敏感型排序:处理大规模数据时内存占用稳定

六、测试用例验证
javascript 复制代码
// 常规测试
console.log(heapSortDesc([3,1,4,9,2]));    // [9,4,3,2,1]

// 全相同元素
console.log(heapSortDesc([5,5,5]));        // [5,5,5]

// 降序验证
console.log(heapSortDesc([10,8,6,4,2]));   // [10,8,6,4,2]

七、与其它堆排序变种对比
类型 排序方向 堆顶元素 适用场景
小顶堆排序 降序 最小值 取最小元素场景
大顶堆排序 升序 最大值 取最大元素场景

八、扩展阅读建议

• 可视化堆结构:使用工具观察堆调整过程

• 多叉堆优化:将二叉堆扩展为三叉堆提升性能

• 堆排序在 React 任务调度中的应用:Fiber 架构中的优先级队列实现


堆的数组表示

对于索引为 i 的节点:

• 父节点:Math.floor((i-1)/2)

• 左子节点:2*i + 1

• 右子节点:2*i + 2

javascript 复制代码
// 大顶堆示例数组
[90, 70, 80, 20, 10, 50, 60]

大小顶推JS实现代码

javascript 复制代码
//堆排序
//大顶堆
function heapify(arr, heapSize, rootIndex) {
    let largest= rootIndex;
    const left=2*rootIndex+1;
    const right=2*rootIndex+2;

    //比较左子树与根节点
    if(left<heapSize&&arr[left]>arr[largest]){
        largest=left;
    }
    //比较右子树与根节点
    if(right<heapSize&&arr[right]>arr[largest]){
        largest=right;
    }
    //如果根节点不是最大值,则交换根节点与最大值
    if(largest!==rootIndex){
        [arr[rootIndex],arr[largest]]=[arr[largest],arr[rootIndex]];
        //递归地对交换后的子树进行堆化
        heapify(arr,heapSize,largest);
    }
}
function heapSort(arr){
    const n=arr.length;
    //构建最大堆
    for(let i=Math.floor(n/2)-1;i>=0;i--){
        heapify(arr,n,i);
    }
    //从堆顶开始取出元素,放到数组末尾,然后重新堆化
    for(let i=n-1;i>=0;i--){
        [arr[0],arr[i]]=[arr[i],arr[0]];    //交换堆顶和堆底元素
        heapify(arr,i,0);                  //重新堆化
    }
    return arr;
}


//小顶堆
function heapifyMin(arr, heapSize, rootIndex) {
    let smallest = rootIndex;
    const left = 2 * rootIndex + 1;
    const right = 2 * rootIndex + 2;
    // 比较左子树与根节点
    if (left < heapSize && arr[left] < arr[smallest]) {
        smallest = left;
    }
    // 比较右子树与根节点
    if (right < heapSize && arr[right] < arr[smallest]) {
        smallest = right;
    }
    // 如果根节点不是最小值,则交换根节点与最小值
    if (smallest !== rootIndex) {
        [arr[rootIndex], arr[smallest]] = [arr[smallest], arr[rootIndex]];
        // 递归地对交换后的子树进行堆化
        heapifyMin(arr, heapSize, smallest);
    }
}
function heapSortMin(arr) {
    const n = arr.length;
    // 构建最小堆
    for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
        heapifyMin(arr, n, i);
    }
    // 从堆顶开始取出元素,放到数组末尾,然后重新堆化
    for (let i = n - 1; i >= 0; i--) {
        [arr[0], arr[i]] = [arr[i], arr[0]];    // 交换堆顶和堆底元素
        heapifyMin(arr, i, 0);                  // 重新堆化
    }
    return arr;
}
console.log(heapSortMin([5, 2, 1, 9, 6]));

小顶推排序过后为递减[4,3,2,1]

二、堆的核心操作

1. 堆化(Heapify)

维护堆性质的关键操作,时间复杂度 O(logn)

javascript 复制代码
function maxHeapify(arr, index, heapSize) {
    let largest = index;
    const left = 2 * index + 1;
    const right = 2 * index + 2;
    
    if (left < heapSize && arr[left] > arr[largest]) largest = left;
    if (right < heapSize && arr[right] > arr[largest]) largest = right;
    
    if (largest !== index) {
        [arr[index], arr[largest]] = [arr[largest], arr[index]];
        maxHeapify(arr, largest, heapSize);
    }
}

2. 建堆(Build Heap)

将无序数组转换为堆,时间复杂度 O(n)

javascript 复制代码
function buildMaxHeap(arr) {
    const n = arr.length;
    for (let i = Math.floor(n/2)-1; i >=0; i--) {
        maxHeapify(arr, i, n);
    }
}

三、前端典型应用场景

1. 优先队列实现

React 任务调度器使用小顶堆管理任务优先级

javascript 复制代码
class PriorityQueue {
    constructor(compare = (a, b) => a - b) {
        this.heap = [];
        this.compare = compare;
    }
    
    push(val) {
        this.heap.push(val);
        this.bubbleUp();
    }
    
    pop() {
        const top = this.heap[0];
        const end = this.heap.pop();
        if (this.heap.length) {
            this.heap[0] = end;
            this.sinkDown();
        }
        return top;
    }
    
    // 上浮/下沉操作略...
}

2. 大数据处理

• 实时Top K统计(如热搜词统计)

• 流式数据中位数计算


四、LeetCode 实战解析

题目 215:数组中的第K个最大元素

问题描述

在未排序数组中找到第 k 个最大的元素

小顶堆解法

维护大小为 K 的小顶堆,堆顶即为结果

javascript 复制代码
function findKthLargest(nums, k) {
    const minHeap = new PriorityQueue((a,b) => a - b));
    for (const num of nums) {
        minHeap.push(num);
        if (minHeap.size() > k) minHeap.pop();
    }
    return minHeap.peek();
}

复杂度分析

• 时间复杂度:O(n logk)

• 空间复杂度:O(k)


题目 347:前 K 个高频元素

问题描述

给定非空整数数组,返回出现频率前 k 高的元素

大顶堆解法

  1. 统计频率 → 2. 堆排序 → 3. 取前k个
javascript 复制代码
function topKFrequent(nums, k) {
    const freqMap = new Map();
    nums.forEach(n => freqMap.set(n, (freqMap.get(n) || 0) + 1));
    
    const maxHeap = new PriorityQueue((a,b) => b[1] - a[1]);
    Array.from(freqMap.entries()).forEach(entry => maxHeap.push(entry));
    
    const res = [];
    for (let i = 0; i < k; i++) {
        res.push(maxHeap.pop()[0]);
    }
    return res;
}

题目 295:数据流的中位数

问题描述

动态计算不断输入数据的中位数

双堆解法

• 大顶堆存储较小半部分

• 小顶堆存储较大半部分

javascript 复制代码
class MedianFinder {
    constructor() {
        this.maxHeap = new PriorityQueue((a,b) => b - a); // 较小半部分
        this.minHeap = new PriorityQueue((a,b) => a - b); // 较大半部分
    }
    
    addNum(num) {
        this.maxHeap.push(num);
        this.minHeap.push(this.maxHeap.pop());
        
        if (this.maxHeap.size() < this.minHeap.size()) {
            this.maxHeap.push(this.minHeap.pop());
        }
    }
    
    findMedian() {
        return this.maxHeap.size() > this.minHeap.size()
            ? this.maxHeap.peek()
            : (this.maxHeap.peek() + this.minHeap.peek()) / 2;
    }
}

五、性能对比与选择策略

场景 推荐数据结构 优势
动态Top K 小顶堆 内存占用稳定为O(k)
动态中位数 双堆 查询时间复杂度O(1)
优先级调度 大顶堆 快速获取最高优先级

六、总结与思考

  1. 堆的本质:通过树形结构维护元素间的序关系
  2. 前端应用价值
    • 处理海量数据时优化内存使用
    • 实现高效的任务调度机制
  3. 学习建议
    • 手写堆实现加深理解
    • 练习LeetCode相关题目培养算法思维

扩展思考:如何用堆优化虚拟列表的渲染性能?欢迎在评论区讨论!

本文代码已通过 LeetCode 测试用例验证,完整实现可访问我的 GitHub 仓库 获取。

相关推荐
代码搬运媛4 小时前
Jest 测试框架详解与实现指南
前端
吃好睡好便好4 小时前
在Matlab中绘制横直方图
开发语言·学习·算法·matlab
counterxing5 小时前
我把 Codex 里的 Skills 做成了一个 MCP,还支持分享
前端·agent·ai编程
仰泳之鹅5 小时前
【C语言】自定义数据类型2——联合体与枚举
c语言·开发语言·算法
wangqiaowq5 小时前
windows下nginx的安装
linux·服务器·前端
之歆5 小时前
DAY_12JavaScript DOM 完全指南(二):实战与性能篇
开发语言·前端·javascript·ecmascript
发现一只大呆瓜5 小时前
Vite凭什么这么快?3分钟带你彻底搞懂 Vite 热更新的幕后黑手
前端·面试·vite
Maimai108085 小时前
React如何用 @microsoft/fetch-event-source 落地 SSE:比原生 EventSource 更灵活的实时推送方案
前端·javascript·react.js·microsoft·前端框架·reactjs·webassembly
x_yeyue7 小时前
三角形数
笔记·算法·数论·组合数学
kyriewen7 小时前
产品经理把PRD写成“天书”,我用AI半小时重写了一遍,他当场愣住
前端·ai编程·cursor