为什么要学最小堆?
想象一下,你是一个超级忙碌的项目经理,手上有无数个任务等待处理。有些任务紧急程度高,有些相对不那么着急。你会怎么安排处理顺序呢?
这就是 React 在进行任务调度时面临的问题!每当页面需要更新时,React 需要决定哪些任务优先级最高,应该先处理。而最小堆(Min Heap)就是 React Scheduler 用来解决这个问题的核心数据结构。
今天,我们就来揭开最小堆的神秘面纱,看看它是如何帮助 React 高效管理任务优先级的!
什么是最小堆?一个生动的比喻 🏆
想象最小堆就像一个"自动排序的金字塔":
- 塔顶永远是最小的数(就像最高优先级的任务总在最前面)
- 每个父节点都比子节点小(上司的优先级总是比下属高)
- 插入新元素时,金字塔会自动重新排列(新任务来了会自动找到合适位置)
这种特性让我们可以在 O(log n) 的时间复杂度内插入任务,并在 O(1) 的时间内获取最高优先级的任务!
最小堆的结构:从抽象到具体
1. 树形结构视图
让我们先看看最小堆长什么样:
graph TD
A[1] --> B[3]
A --> C[4]
B --> D[7]
B --> E[10]
C --> F[9]
C --> G[6]
D --> H[15]
D --> I[14]
E --> J[12]
style A fill:#ff9999
style B fill:#ffcc99
style C fill:#ffcc99
style D fill:#ffffcc
style E fill:#ffffcc
style F fill:#ffffcc
style G fill:#ffffcc
style H fill:#ccffcc
style I fill:#ccffcc
看出规律了吗?
- 根节点 1 是最小的
- 每个父节点都比它的子节点小
- 这就保证了堆顶永远是最小值!
2. 数组存储:化繁为简
虽然最小堆在逻辑上是树结构,但在实际存储时,我们使用数组!这是个巧妙的设计:
graph LR
subgraph "数组索引"
idx0[0]
idx1[1]
idx2[2]
idx3[3]
idx4[4]
idx5[5]
idx6[6]
idx7[7]
idx8[8]
idx9[9]
end
subgraph "数组值"
val1[1]
val3[3]
val4[4]
val7[7]
val10[10]
val9[9]
val6[6]
val15[15]
val14[14]
val12[12]
end
idx0 --- val1
idx1 --- val3
idx2 --- val4
idx3 --- val7
idx4 --- val10
idx5 --- val9
idx6 --- val6
idx7 --- val15
idx8 --- val14
idx9 --- val12
数组 = [1, 3, 4, 7, 10, 9, 6, 15, 14, 12]
3. 索引关系:数学的魅力
这里有个超酷的数学规律:
graph TD
subgraph "索引关系"
A["父节点索引 i"]
B["左子节点: 2*i + 1"]
C["右子节点: 2*i + 2"]
D["子节点 j 的父节点: (j-1)÷2"]
A --> B
A --> C
D --> A
end
举个例子:
- 索引 0 的左子节点:2×0+1 = 1
- 索引 0 的右子节点:2×0+2 = 2
- 索引 3 的父节点:(3-1)÷2 = 1
这个规律让我们可以仅用数组就完美模拟树结构!
代码实现:从理论到实践
现在让我们动手实现一个最小堆类:
/**
* 最小堆的实现 - React Scheduler 的核心数据结构
* 让任务按优先级自动排序!
*/
class MiniHeap {
heap = [];
constructor() {
console.log("🎯 最小堆初始化完成!");
}
/**
* 插入新任务 - 就像给金字塔添加新砖块
* @param {number} val 任务的优先级值(越小优先级越高)
*/
insert(val) {
console.log(`📥 插入新任务,优先级: ${val}`);
this.heap.push(val);
// 如果不是第一个元素,需要向上调整位置
if (this.heap.length > 1) {
this.heapifyUp();
}
console.log(`当前堆状态: [${this.heap.join(', ')}]`);
}
/**
* 获取并移除最高优先级任务 - 取走金字塔顶端
* @returns {number|undefined} 最高优先级的任务
*/
pop() {
if (this.heap.length === 0) {
console.log("⚠️ 没有任务可以执行了!");
return undefined;
}
if (this.heap.length === 1) {
const task = this.heap.pop();
console.log(`🎯 执行最后一个任务: ${task}`);
return task;
}
// 保存最小值(堆顶)
let min = this.heap[0];
console.log(`🎯 执行最高优先级任务: ${min}`);
// 将最后一个元素移到顶部,然后向下调整
this.heap[0] = this.heap.pop();
this.heapifyDown();
console.log(`执行后堆状态: [${this.heap.join(', ')}]`);
return min;
}
/**
* 获取父节点索引 - 找到上级
*/
getParentIndex(i) {
return Math.floor((i - 1) / 2);
}
/**
* 获取左子节点索引 - 找到左下属
*/
getLeft(i) {
return 2 * i + 1;
}
/**
* 获取右子节点索引 - 找到右下属
*/
getRight(i) {
return 2 * i + 2;
}
/**
* 向上调整 - 新员工可能比老板还优秀,需要晋升!
*/
heapifyUp() {
let currentIndex = this.heap.length - 1;
while (currentIndex > 0) {
let parentIndex = this.getParentIndex(currentIndex);
// 如果当前节点比父节点小,就交换位置(向上晋升)
if (this.heap[currentIndex] < this.heap[parentIndex]) {
console.log(`🔄 交换位置: ${this.heap[currentIndex]} 与 ${this.heap[parentIndex]}`);
[this.heap[currentIndex], this.heap[parentIndex]] = [
this.heap[parentIndex],
this.heap[currentIndex],
];
currentIndex = parentIndex;
} else {
break; // 已经找到合适位置
}
}
}
/**
* 向下调整 - 新来的老板可能不如下属,需要降级!
*/
heapifyDown() {
let currentIndex = 0;
while (true) {
let leftIndex = this.getLeft(currentIndex);
let rightIndex = this.getRight(currentIndex);
let smallestIndex = currentIndex;
// 找到当前节点和其子节点中最小的
if (
leftIndex < this.heap.length &&
this.heap[leftIndex] < this.heap[smallestIndex]
) {
smallestIndex = leftIndex;
}
if (
rightIndex < this.heap.length &&
this.heap[rightIndex] < this.heap[smallestIndex]
) {
smallestIndex = rightIndex;
}
// 如果当前节点已经是最小的,说明位置合适
if (smallestIndex === currentIndex) {
break;
}
console.log(`🔄 向下调整: ${this.heap[currentIndex]} 与 ${this.heap[smallestIndex]} 交换`);
[this.heap[currentIndex], this.heap[smallestIndex]] = [
this.heap[smallestIndex],
this.heap[currentIndex],
];
currentIndex = smallestIndex;
}
}
/**
* 获取堆大小
*/
size() {
return this.heap.length;
}
/**
* 查看最高优先级任务(不移除)
*/
peek() {
return this.heap.length > 0 ? this.heap[0] : undefined;
}
}
// 🎮 让我们来测试一下!
console.log("=== 最小堆演示开始 ===");
const heap = new MiniHeap();
console.log("\n📋 模拟 React 任务调度场景:");
console.log("优先级越小越重要(1=超高优先级,100=低优先级)");
// 插入一些"任务"
heap.insert(100); // 低优先级任务
heap.insert(10); // 中等优先级
heap.insert(9); // 较高优先级
heap.insert(7); // 高优先级任务
console.log("\n🎯 开始执行任务(按优先级从高到低):");
while (heap.size() > 0) {
const nextTask = heap.pop();
console.log(`执行任务,优先级: ${nextTask}`);
}
console.log("\n=== 演示结束 ===");
实战应用:React 中的任务调度
在 React 的 Scheduler 中,最小堆被用来管理任务队列:
- 插入任务:当组件需要更新时,React 会根据优先级插入任务
- 执行任务:Scheduler 总是取出优先级最高的任务执行
- 时间片管理:如果时间片用完,当前任务会被重新插入堆中
这种设计让 React 可以:
- ⚡ 响应用户交互(高优先级)
- 🎨 处理动画更新(中等优先级)
- 📊 执行后台数据同步(低优先级)
性能分析:为什么选择最小堆?
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 插入 | O(log n) | 最多需要向上调整 log n 层 |
| 删除最小值 | O(log n) | 最多需要向下调整 log n 层 |
| 查看最小值 | O(1) | 直接访问数组第一个元素 |
| 构建堆 | O(n) | 批量构建时的优化算法 |
相比普通数组:
- ❌ 插入后排序:O(n log n)
- ❌ 查找最小值:O(n)
- ✅ 最小堆插入:O(log n)
- ✅ 最小堆取最小值:O(1)