堆是什么?
-
完全二叉树
- 除最后一层外,每层都填满
- 用数组存储非常高效,父子节点下标有规律
-
堆顶始终是最大或最小的数,便于快速取出。
应用场景
1️⃣ 堆排序(Heap Sort)
原理:构建一个最大堆,每次取堆顶元素(最大),放到数组末尾,重复堆化。
时间复杂度:O(n log k)
📌 应用:
- 在一万条数据里,取最大的10个数
- React 内部 reconciler 中有对 fiber list 的优先级调整过程(虽然更复杂)
2️⃣ React 的 scheduler 中的最小堆队列
React 17+ 中的 scheduler
模块用于调度更新任务,其核心是一个时间驱动的优先级队列。
源码地址:
arduino
js
复制编辑
const taskQueue = new HeapQueue(); // 按照 expirationTime 排序(时间优先)
const timerQueue = new HeapQueue(); // 按照 startTime 排序(定时任务)
这里使用的是 最小堆,以保证:
- 取出最早过期或最早开始的任务是 O(1)
- 插入/更新任务是 O(log n)
📌 相关概念:
expirationTime
决定任务的紧急程度(Lanes机制)startTime
控制任务延迟开始(如setTimeout
)
3️⃣ 前端动画调度器(如 requestIdleCallback polyfill)
浏览器的 requestIdleCallback
用于在浏览器空闲时间运行回调,类似的调度器内部也常用最小堆存储任务(按照预定的执行时间排序),例如:
yaml
js
复制编辑
[
{ callback: fn1, time: 158000001 },
{ callback: fn2, time: 158000003 }
]
每一帧取出堆顶执行任务(即将过期的),其余任务继续等待。
4️⃣ 优先级任务队列 / 限流工具
你实现一个并发请求池(如 5 并发),但需要支持优先级调度,可用最大堆实现优先级队列:
javascript
js
复制编辑
class Task {
constructor(priority, callback) { ... }
}
堆顶是优先级最高的任务,先执行它。
📌 应用:
- 文件上传任务队列(优先级)
- 渲染任务(长列表懒加载)
5️⃣ 前端游戏 / 动画帧控制系统
在复杂的 WebGL、canvas 动画或游戏循环中,需要按执行时间调度任务帧 ------ 最小堆非常适合做这种"时间轴排序"。
例如:
scss
js
复制编辑
scheduleAt(time, callback)
内部将任务按时间压入最小堆,当前帧时间到了就从堆顶拿任务执行。
为什么前端常用"最小堆"?
-
因为前端调度大多数以**"时间驱动"或"优先级最小者先执行"**为主:
- 比如:任务最早执行、超时最早发生、优先级最小者最紧急
-
所以最小堆正好满足这个模型:堆顶是最小值,取用只要 O(1)
如何实现堆?
peek
函数: 查看堆的顶点, 也就是优先级最高的task
或timer
.pop
函数: 将堆的顶点提取出来, 并删除顶点之后, 需要调用siftDown
函数向下调整堆.push
函数: 添加新节点, 添加之后, 需要调用siftUp
函数向上调整堆.siftDown
函数: 向下调整堆结构, 保证数组是一个最小堆.siftUp
函数: 当插入节点之后, 需要向上调整堆结构, 保证数组是一个最小堆.
总结
面试官问:堆在前端有什么应用?
在前端开发中,堆主要应用在需要快速获取"最小/最大值"的任务调度场景中。例如:
- React 中的
scheduler
模块使用最小堆管理任务队列,以 O(1) 获取优先级最高的任务- requestIdleCallback 的 polyfill 实现中,也会用最小堆安排任务执行时间
- 自定义的并发任务队列(如上传、抓取)也可用最大堆做优先级控制
- 当然,堆排序本身也可以用来实现稳定的 O(n log k) 排序
因为堆能在 log(n) 时间内插入任务,O(1) 获取最小值,所以非常适合调度类系统。