堆(Heap)

135 阅读7分钟

二叉树为基础的数据结构,一般用数组存储。查找和更新快。时间复杂度为O(nlog n)

最大堆

定义

节点大于子节点的二叉树。

节点位置关系

根据节点获得左子节点:parentIndex * 2 + 1

根据节点获得右子节点:parentIndex * 2 + 2

根据节点获得父节点:Math.floor((childIndex - 1) / 2)

操作

插入上浮

  1. 在堆数组尾部插入需要新增的节点。
  2. 根据新增节点位置获取其父节点。
  3. 比较父子节点的大小,如果父节点比子节点值小,交换父子节点在堆数组中的位置。
  4. 如果位置交换了,把前一步中交换得到的父节点,作为比较值,递归2,3步操作,向上比较,调整堆的各节点位置。

例如:

依次插入5,3,10,1,1进入最大堆(最新插入的节点为红色),实现思路如下:

maxHeap_add_first.png

第一步: 插入第一个节点5。

屏幕快照 2021-12-17 上午11.39.14.png

第二步: 插入新节点3。

第三步: 根据新插入的节点3的位置找到其父节点的值,判断父子大小,也就是5和3的大小,这里5大于3,父大于子,符合最大堆定义,不需要调换位置。

屏幕快照 2021-12-17 上午11.18.48.png

第四步: 插入新节点10。

屏幕快照 2021-12-17 上午11.21.02.png

第五步: 根据新插入的节点10的位置找到其父节点的值,判断父子大小,也就是5和10的大小,这里10大于5,父小于子,需要调换父子位置。

第六步: 判断新插入的节点的新位置,是否还有父节点,如果有,需要再根据插入节点的新位置获取父节点,再比较新节点和父节点的值,这里没有父节点了,调整结束,新增节点位置确定。

屏幕快照 2021-12-17 下午3.48.14.png

第七步: 插入节点1。

第八步: 根据新插入的节点1的位置找到其父节点的值,判断父子大小,也就是1和5的大小,这里5大于1,父大于子,无需调换父子位置,这里新节点1位置确定。

屏幕快照 2021-12-17 下午4.10.36.png

第九步: 插入新节点1。

第十步: 根据新插入的节点1的位置找到其父节点的值,判断父子大小,也就是1和5的大小,这里5大于1,父大于子,无需调换父子位置,这里新节点1位置确定。

删除下沉

删除顶元素

  1. 把堆数组的最后一个元素设置替换到数组的首元素的位置。完成顶元素的删除,后面再调整堆数组中各元素的位置。

  2. 根据第一个节点,获取其左右子树的值,

    A. 如果没有左右子节点,返回,完成调整。

    B. 如果有左右子节点,获取其中大的那一个子节点。

  3. 父节点与较大的子节点比较,如果父节点小于子节点,交换父子节点在数组中的位置,反之返回,完成调整。

  4. 如果位置交换了,把前一步中交换得到的子节点,作为比较值,递归2,3步操作,向下比较,调整堆的各节点位置。

例如:

现存在堆数组11,10,5,3,1,删除顶节点11。

屏幕快照 2021-12-17 下午5.44.53.png

第一步: 把堆数组最后一位的1移动到第一位,把之前第一位的11替换掉。

屏幕快照 2021-12-17 下午5.59.24.png

第二步: 1已经移位到第一位,所以把堆数组中的最后一位pop掉,堆中剩下1,10,5,3

第三步: 目前堆数组元素位置不符合最大堆的定义,需要下沉调整堆中的节点。

屏幕快照 2021-12-17 下午6.08.41.png

第四步: 找出下沉节点的最大子节点,与下沉节点比较,如果最大子节点大于下沉节点,调换下沉节点和其最大子节点的位置。

屏幕快照 2021-12-17 下午6.15.36.png

第五步: 位置替换后,再进行下沉比较,发现下沉节点仍然小于子节点,继续调整位置。

第六步: 位置替换后,再查找子节点进行比较,发现没有子节点了,无需再调整,本轮调整完毕。

删除任意元素

  1. 查找到要删除的节点在数组中的坐标位置,把数组的最后一个节点替换到所要删除的节点的位置,完成节点删除。
  2. 把替换后的节点做下沉比较,步骤同删除顶元素的2,3,4步。
  3. 如果需要删除的节点,在数组中存在多处,那么需要先获取要删除的节点个数,然后有几个,就执行几遍1,2步,最终把需删除的节点都删除。

查找是某个值的节点位置

数组遍历查找,可以用reduce 方法把查到的所有节点位置放的一个数组中。

function find (heapContainer,value) {
  return heapContainer.reduce((arr, node, index) => {
    if (node === value) {
      arr.push(index)
      return arr
    }
    return arr
  }, [])
}

例如:

先存在堆数组10,8,6,7,6,4,5,3,2,1,先删除4,再删除10,再删除6。

屏幕快照 2021-12-20 下午4.48.20.png

以上是堆数组。

屏幕快照 2021-12-20 下午4.53.32.png

第一步: 删除节点4(叶子节点),先通过值查找到要删除的4在堆数组里的坐标位置。

屏幕快照 2021-12-20 下午4.56.44.png

第二步: 把堆数组的最后一个元素,替换到要删除的4的位置。然后元素pop掉最后一个元素1。

第三步: 下沉比较,但是这个更换后的节点1没有子节点,所以位置确定。堆数组是10,8,6,7,6,1,5,3,2

屏幕快照 2021-12-20 下午5.01.03.png

第四步: 删除节点10(删除顶节点)。先通过值查找到要删除的10在堆数组里的坐标位置。

屏幕快照 2021-12-20 下午5.01.48.png

第五步: 把堆数组的最后一个元素,替换到要删除的10的位置。然后元素pop掉最后一个元素2。

屏幕快照 2021-12-20 下午5.04.15.png

第六步: 根据2的新位置,获取到其两个子节点中较大的子节点,这里是8,2小于8,需要交换父子节点位置。

屏幕快照 2021-12-20 下午5.05.25.png

第七步: 根据2的新位置,获取到其两个子节点中较大的子节点,这里是7,2小于7,需要交换父子节点位置。

屏幕快照 2021-12-20 下午5.06.36.png

第八步: 根据2的新位置,获取到子节点,这里只有3,2小于3,需要交换父子节点位置。

第九步: 2的新位置再没有子节点,这轮调整结束。

屏幕快照 2021-12-20 下午5.10.58.png

第十步: 删除6(中间节点,并且是多个)。先通过值查找到要删除的6在堆数组里的坐标位置,这里返回的是两个值。

屏幕快照 2021-12-20 下午5.15.39.png

第十一步: 我们对它们一个一个的删除,无非是删除两遍。先删除位置靠前的一个。

屏幕快照 2021-12-20 下午5.16.24.png

第十二步: 把堆数组的最后一个元素,替换到要删除的6的位置。然后元素pop掉最后一个元素2。

屏幕快照 2021-12-20 下午5.19.16.png

第十三步: 根据2的新位置,获取到其两个子节点中较大的子节点,这里是5,2小于5,需要交换父子节点位置。

第十四步: 调整到新位置的2已经没有子节点,这轮调整结束。

屏幕快照 2021-12-20 下午5.32.04.png

第十五步: 删除另一个节点6。

屏幕快照 2021-12-20 下午5.34.21.png

第十六步: 把堆数组的最后一个元素,替换到要删除的6的位置。然后元素pop掉最后一个元素2。

第十七步: 新位置的2是叶子节点,不需要再调整。

整个实例结束,结果为8,7,5,3,2,1

最小堆

定义

节点大于子节点的二叉树。

节点位置关系

同最大堆

操作

插入上浮和删除下沉与最大堆的思路完全一致,只是最小堆是父子节点判断小的上浮,大的下沉;父节点也是跟小的子节点比较。

复盘

在实现最大最小堆时,当编写获取左右子节点的逻辑时,漏掉了判断下沉后的节点的左右子节点是否都为空,或是是否有一个子节点为空的场景。

这里体现了,在思考问题的解决方案时,只考虑了主体逻辑,对边界值的考虑不够全面。比如第一个节点、最后一个节点、没有子节点的节点、只有一个子节点的节点、没有父节点的节点等等。

如果没有子节点,终止下沉;如果节点位置是0,终止上浮。