react调度源码基础-最小堆

469 阅读5分钟

什么是最小堆

最小堆,是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于其左子节点和右子节点的值。

堆是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于(或不小于)其左子节点和右子节点的值。

最大堆和最小堆是二叉堆的两种形式。

最大堆:根结点的键值是所有堆结点键值中最大者。

最小堆:根结点的键值是所有堆结点键值中最小者。

而最大-最小堆集结了最大堆和最小堆的优点,这也是其名字的由来。

最大-最小堆是最大层和最小层交替出现的二叉树,即最大层结点的儿子属于最小层,最小层结点的儿子属于最大层。

以最大(小)层结点为根结点的子树保有最大(小)堆性质:根结点的键值为该子树结点键值中最大(小)项。

如图所示,需要满足的条件就是其父节点要小于两个子节点,而两个子节点的大小我们并不关注。

我们用数组来保存最小堆,根据图示结构判断父节点和子节点的索引关系:

  • 父节点索引 = (子节点 - 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调度源码基础-最小堆