Published on

Understanding Min Heaps

Understanding Min Heaps

Scheduler 中,使用最小堆的数据结构在对任务进行排序。

// 两个任务队列
var taskQueue: Array<Task> = []
var timerQueue: Array<Task> = []

push(timerQueue, newTask) // 像数组中推入一个任务
pop(timerQueue) // 从数组中弹出一个任务
timer = peek(timerQueue) // 从数组中获取第一个任务

Basic knwoledge of Binary Heap

Binary Tree

所谓二叉树,指的是一个父节点只能有 1 个或者 2 个子节点,例如下图:

Understanding-Min-Heaps-1

总之就是不能多余两个节点。

Complete Binary Tree

A Complete Binary Tree (CBT) is a binary tree in which every level, except possibly the last, is completely filled, and all nodes are as far left as possible.

完全二叉树从根结点到倒数第二层满足完美二叉树,最后一层可以不完全填充,其叶子结点都靠左对齐。

Understanding-Min-Heaps-2

再例如,下面的这些树,就不是完全树:

Understanding-Min-Heaps-3

Heap Properties in a Complete Tree

  • 最大堆:父节点的数值大于或者等于所有的子节点
  • 最小堆:刚好相反,父节点的数值小于或者等于所有的子节点

最大堆示例:

Understanding-Min-Heaps-4
Understanding-Min-Heaps-5
  • 无论是最大堆还是最小堆,第一个节点一定是这个堆中最大的或者最小的
  • 每一层并非是按照一定顺序来排列的,比如下面的例子,6 可以在左分支,3 可以在右分支
Understanding-Min-Heaps-6
  • 每一层的所有元素并非一定比下一层(非自己的子节点)大或者小(例如上图,4 在第二层,但大于在第三层的 3)

Heap Implementation

堆一般来讲,可以使用数组来实现

Understanding-Min-Heaps-7

通过数组,我们可以非常方便的找到一个节点的所有亲属

  • 父节点:Math.floor((currentNodeIndex - 1) / 2)
子节点父节点
10
31
41
52
  • 左分支节点:currentNodeIndex \* 2 + 1
父节点左分支节点
01
13
25
  • 右分支节点:currentNodeIndex \* 2 + 2
父节点右分支节点
02
14
26

Application of Min Heap in React

在 React 中,最小堆对应的源码在 SchedulerMinHeap.js 文件中,总共有 6 个方法,其中向外暴露了 3 个方法

  • push:向最小堆推入一个元素
  • pop:弹出一个
  • peek:取出第一个

没有暴露的是:

  • siftUp:向上调整
  • siftDown:向下调整
  • compare:这是一个辅助方法,就是两个元素做比较的

所谓向上调整,就是指将一个元素和它的父节点做比较,如果比父节点小,那么就应该和父节点做交换,交换完了之后继续和上一层的父节点做比较,依此类推,直到该元素放置到了正确的位置

Understanding-Min-Heaps-8

向下调整,就刚好相反,元素往下走,先和左分支进行比较,如果比左分支小,那就交换。

peek

取出堆顶的任务,堆顶一定是最小的

这个方法极其的简单,如下:

peek(timerQueue)
export function peek(heap) {
  // 返回这个数组的第一个元素
  return heap.length === 0 ? null : heap[0]
}

push

向最小堆推入一个新任务,因为使用的是数组,所以在推入任务的时候,首先该任务是被推入到数组的最后一项,但是这个时候,推入到末尾的数值可能会打破最小堆保持的规则,所以可能会涉及到一个调整,我们需要向上调整,把这个任务调整到合适的位置

push(timerQueue, newTask)
export function push(heap, node) {
  const index = heap.length
  // 推入到数组的最后一位
  heap.push(node)
  // 向上调整,调整到合适的位置
  siftUp(heap, node, index)
}

pop

pop 是从任务堆里面弹出第一个任务,也就是意味着该任务已经没有在队列里面了

peek 操作用于查看堆顶元素,即获取堆中具有最高(或最低)优先级的元素,而不移除它。这意味着 peek 操作不会改变堆的结构或大小,仅仅返回堆顶的元素。它可以用于获取当前堆的最大(或最小)值,而不会影响堆中的其他元素。

pop 操作用于移除堆顶元素,即删除堆中具有最高(或最低)优先级的元素,并返回该元素的值。这会导致堆的结构发生变化,因为删除堆顶元素后,需要重新调整堆的顺序,以确保堆的性质得到维护。通常,pop 操作会将下一个具有次高优先级的元素移动到堆顶位置,并通过对堆进行适当的堆化操作,使其满足堆的性质。

pop(taskQueue)
export function pop(heap) {
  if (heap.length === 0) {
    return null
  }
  // 获取数组的第一个任务(一定是最小的)
  const first = heap[0]
  // 拿到数组的最后一个
  const last = heap.pop()
  if (last !== first) {
    // 将最后一个任务放到第一个
    heap[0] = last
    // 接下来向下调整
    siftDown(heap, last, 0)
  }
  return first
}

具体的调整示意图如下:

Understanding-Min-Heaps-9