js 数据结构之堆

72 阅读4分钟

堆的基本概念,原理分析,和它的 js 代码的实现……


一 基本概念

堆(Heap),可以理解为一个实现二叉树的数组,本质上是一种二叉树,但存储的形式是数组,与普通数组的区别在于,它有特定的排序方式。

堆可以分为两种:

  • 最大堆:每个根节点的值都大于(含等于)子节点。
  • 最小堆:每个根节点的值都小于(含等于)子节点。

堆的根节点中存放的是最大或者最小元素,但是其他节点的排序顺序是未知的。

堆化, 维护最大/小堆性质的过程,和子节点不断比较,将最大/小值和父节点交换。

最大堆的例子:

最大堆.webp

如上图,存储为数组:

[10, 7, 2, 5, 1]

数组元素的顺序,按照二叉树的每一排依次进行排序。

堆排序.webp

二 索引的计算

如果单看数组,如何知道哪一个节点是父节点,哪一个节点是它的子节点?

对于索引为 n 的节点,它的:

  • 父节点索引为:(n - 1) / 2。
  • 子左节点索引为:2n + 1。
  • 右子节点索引为:2n + 2。

如果子节点的索引超过了数组的大小,则说明子节点不存在。

其他计算公式

树的高度,是指从树的根节点到最低叶节点所需要的步数,即一个高度为 h 的堆有 h + 1 层。

如果一个堆有 n 个节点,那么它的高为 h = floor(log2(n))。(floor 舍小数取整方法)

已知树高 h ,则整个堆的节点数目为:2^(h + 1) - 1

叶节点总是位于数组的 floor(n/2) 与 n - 1 之间。

三 堆的常用操作及增删原理

操作描述
shiftUp()一个节点需要将它同父节点交换位置以实现‘有效的堆’,这个节点在数组中的位置上升
shiftDown()一个节点需要将它同子节点交换位置以实现‘有效的堆’,这个节点在数组中的位置后移。这个操作也叫堆化
insert(value)在堆尾添加一个元素,然后使用 shiftUp 修复
removeRoot()移除最大/小值,移除后要将最后一个元素补过来,然后用 shiftDown 修复
removeAtIndex(index)移除 index 位置的节点,移除后要将最后一个元素补过来,然后用 shiftUp/shiftDown 修复
replace(index, value)替换节点,然后用 shiftUp 修复
search(value)寻找 index
buildHeap(array)反复调用 insert() 方法将一个(无序)数组转换成一个堆
peek()返回最大/小值

insert 示例(最大堆)

insert1.webp

将数字 16 插入到上图中的堆中,即在堆尾添加 16。数组为 [10, 7, 2, 5, 1, 16]。

相应的树变成了:

insert2.webp

此时,子节点大于根节点,不满足堆属性,为了满足堆属性,需要将数字 2 和数字 16 交换位置:

insert3.webp

这时,父节点 10 仍小于 子节点 16,仍不满足堆属性,需要继续交换子父节点。

insert4.webp

以上操作就是 shiftUp

removeRoot 示例

insert1.webp

删除 root 元素,即删掉图中的数字 10。

删除一个元素,要将数组最后一个元素和被删的元素互换位置。

[ 10, 7, 2, 5, 1 ]  ==> [ 1, 7, 2, 5, 10 ]

这时最后一个元素就是要返回的值,之后要将该元素从数组中彻底移除。

remove3.webp

然后,shiftDown堆修复:

remove4.webp

remove5.webp

四 代码实现

// 最小堆的实现
class MinHeap {
  constructor(initArr = []) {
    this.heap = [];
    this.buildHeap(initArr);
  }
  /**
  * 交换父子节点位置
  */
  swap(i1, i2) {
    [this.heap[i1], this.heap[i2]] = [this.heap[i2], this.heap[i1]];
  }
  /**
  * 父节点索引
  */
  getParentIndex(i) {
    return Math.floor((i - 1) / 2);
  }
  /*
  * 左子节点索引
  */
  getLeftIndex(i) {
    return i * 2 + 1
  }
  /*
  * 右子节点索引
  */
  getRightIndex(i) {
    return i * 2 + 2
  }
  /*
  * 向上修复堆
  */
  shiftUp(index) {
    if (index === 0) {
      return
    }
    const parentIndex = this.getParentIndex(index)
    if (this.heap[parentIndex] > this.heap[index]) {
      this.swap(parentIndex, index);
      this.shiftUp(parentIndex)
    }
  }
  /*
  * 向下修复堆
  */
  shiftDown(index) {
    const leftIndex = this.getLeftIndex(index)
    const rightIndex = this.getRightIndex(index)
    if (this.heap[leftIndex] < this.heap[index]) {
      this.swap(leftIndex, index)
      this.shiftDown(leftIndex)
    }
    if (this.heap[rightIndex] < this.heap[index]) {
      this.swap(rightIndex, index)
      this.shiftDown(rightIndex)
    }
  }
  /*
  * 添加元素
  */
  insert(value) {
    this.heap.push(value)
    this.shiftUp(this.heap.length - 1)
  }
  /*
  * 删除 root 元素
  */
  removeRoot() {
    this.heap[0] = this.heap.pop() // 将最后一位pop并且赋值给堆顶
    this.shiftDown(0)
  }
  buildHeap(arr) {
    arr.forEach((item) => this.insert(item));
  }
  /*
  * root 元素
  */
  peek() {
    return this.heap[0]
  }
  size() {
    return this.heap.length
  }
}

参考:raywenderlichDaftJayee