什么是最小堆
最小堆,是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于其左子节点和右子节点的值。
堆是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于(或不小于)其左子节点和右子节点的值。
最大堆:根结点的键值是所有堆结点键值中最大者。
最小堆:根结点的键值是所有堆结点键值中最小者。
而最大-最小堆集结了最大堆和最小堆的优点,这也是其名字的由来。
最大-最小堆是最大层和最小层交替出现的二叉树,即最大层结点的儿子属于最小层,最小层结点的儿子属于最大层。
以最大(小)层结点为根结点的子树保有最大(小)堆性质:根结点的键值为该子树结点键值中最大(小)项。
如图所示,需要满足的条件就是其父节点要小于两个子节点,而两个子节点的大小我们并不关注。
我们用数组来保存最小堆,根据图示结构判断父节点和子节点的索引关系:
- 父节点索引 = (子节点 - 1) / 2,向下取整;
- 左子节点索引 = 2 * 父节点索引 - 1;
- 右子节点索引 = 左子节点 + 1。
react中的应用
我们知道在react的调度中会有一个优先级的问题,而这些任务就可以看成一个最小堆,我们会依次取到优先级最高的任务执行,即最小堆的顶点。这里也是我们要学习的目的。
实现最小堆
const heap = [];
给定一个数组heap,我们按照最小堆的设计理念,我们一一实现其中方法。
添加节点push
实现一个最小堆,我们最先想到的应该是向里面添加到元素,即我们应该需要实现一个push方法。
/**
* 向最小堆里添加一个节点
* @param {*} heap 最小堆
* @param {*} node 节点
*/
function push(heap, node){
heap.push(node);
}
就这么简单嘛,当然不是!我们加入的新的元素,是有可能比之前的元素小的,为了维持最小堆的结构,我们需要向上调整我们加入的元素,所以完整的应该是:
/**
* 向最小堆里添加一个节点
* @param {*} heap 最小堆
* @param {*} node 节点
*/
function push(heap, node) {
//获取元素的数量
const index = heap.length;
//先把添加的元素放在数组的尾部
heap.push(node);
//把尾部这个元素向上调整到合适的位置
siftUp(heap, node, index);
}
下面我们看一下向上调整方法的实现
向上调整siftUp
/**
* 向上调整某个节点,使其位于正确的位置
* @param {*} heap 最小堆
* @param {*} node 节点
* @param {*} i 节点所在的索引
*/
function siftUp(heap, node, i) {
let index = i;
while (true) {
// 拿到父节点的索引
const parentIndex = (index - 1) >>> 1; // (子节点索引 - 1) / 2
// 获取父节点
const parent = node[parentIndex];
// 如果父节点存在,并且父节点比子节点要大
if (parent !== undefined && compare(parent, node) > 0) {
// 把儿子的值给父索引
heap[parentIndex] = node;
// 把父亲的值给子索引
heap[index] = parent;
// 让index等于父亲的索引
index = parentIndex;
} else {
// 如果子节点比父节点要大,不需要交换位置,结束循环
return;
}
}
}
这里需要注意的是根据子节点索引获取父节点索引的方法,这里我们上一节已经交代过,拿到索引交换位置,直到数值比父节点大。
另外还需要主要的是compare方法的实现,这里也比较简单。
function compare(a, b) {
const diff = a.sortIndex - b.sortIndex;
return diff !== 0 ? diff : (a.id - b.id);
}
查看顶点元素peek
瞄一眼顶点的元素,就是看数组第一个元素,这个实现比较简单。
/**
* 查看最小堆顶的元素
* @param {*} heap
*/
function peek(heap) {
const first = heap[0];
return first === undefined ? null : first;
}
弹出最小堆的元素pop
/**
* 弹出最小堆的堆顶元素
* @param {*} heap 最小堆
*/
function pop(heap) {
// 取出数组中第一个也就是堆顶元素
const first = heap[0];
if (first !== undefined) {
// 弹出数组中的最后一个元素
const last = heap.pop();
if (last !== first) {
heap[0] = last;
// 向下调整
siftDown(heap, last, 0);
}
// 如果等于first我们就不用操作了 直接返回first
return first;
} else {
return null;
}
}
这里做出的优化就是,并不是直接弹出第一个元素,而是先把第一个元素和最后一个元素交换位置,然后弹出最后一个元素;比较第一个元素和最后是否相同,相同则返回第一个元素,不能则向下调整,因为第一个需要满足最小项条件。
下面就看看我们这里最复杂的向下调整方法。
向下调整siftDown
/**
* 向下调整某个节点,使其位于正确的位置
* @param {*} heap 最小堆
* @param {*} node 节点
* @param {*} i 节点所在的索引
*/
function siftDown(heap, node, i) {
let index = i;
const length = heap.length;
while (index < length) {
// 左子节点的索引
const leftIndex = index * 2 + 1;
const left = heap[leftIndex];
const rightIndex = leftIndex + 1;
const right = heap[rightIndex];
// 如果左子节点存在,并且左子节点比父节点要小
if (left !== undefined && compare(left, node) < 0) {
// 如果右节点存在,并且右节点比左节点还要小
if (right !== undefined && compare(right, left) < 0) {
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
heap[index] = left;
heap[leftIndex] = node;
index = leftIndex;
}
} else if (right !== undefined && compare(right, node) < 0) {
// 如果右节点存在,并且比右节点要小
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
return;
}
}
}
说是复杂,其实逻辑还是比较清晰的, 就是和左右节点的比价,然后交换位置即可。
测试
let heap = [];
let id = 1;
push(heap, { sortIndex: 1, id: id++ });
push(heap, { sortIndex: 2, id: id++ });
push(heap, { sortIndex: 3, id: id++ });
console.log(peek(heap)); // { sortIndex: 1, id: 1 }
push(heap, { sortIndex: 4, id: id++ });
push(heap, { sortIndex: 5, id: id++ });
push(heap, { sortIndex: 6, id: id++ });
push(heap, { sortIndex: 7, id: id++ });
console.log(peek(heap)); // { sortIndex: 1, id: 1 }
pop(heap);
console.log(peek(heap)); // { sortIndex: 2, id: 2 }
🎉 这里根据结果不难看出我们已经实现了最小堆。react调度源码基础-最小堆