二叉树为基础的数据结构,一般用数组存储。查找和更新快。时间复杂度为O(nlog n)
最大堆
定义
父节点大于子节点的二叉树。
节点位置关系
根据父节点获得左子节点:parentIndex * 2 + 1
根据父节点获得右子节点:parentIndex * 2 + 2
根据子节点获得父节点:Math.floor((childIndex - 1) / 2)
操作
插入上浮
- 在堆数组尾部插入需要新增的节点。
- 根据新增节点位置获取其父节点。
- 比较父子节点的大小,如果父节点比子节点值小,交换父子节点在堆数组中的位置。
- 如果位置交换了,把前一步中交换得到的父节点,作为比较值,递归2,3步操作,向上比较,调整堆的各节点位置。
例如:
依次插入5,3,10,1,1进入最大堆(最新插入的节点为红色),实现思路如下:
第一步: 插入第一个节点5。
第二步: 插入新节点3。
第三步: 根据新插入的节点3的位置找到其父节点的值,判断父子大小,也就是5和3的大小,这里5大于3,父大于子,符合最大堆定义,不需要调换位置。
第四步: 插入新节点10。
第五步: 根据新插入的节点10的位置找到其父节点的值,判断父子大小,也就是5和10的大小,这里10大于5,父小于子,需要调换父子位置。
第六步: 判断新插入的节点的新位置,是否还有父节点,如果有,需要再根据插入节点的新位置获取父节点,再比较新节点和父节点的值,这里没有父节点了,调整结束,新增节点位置确定。
第七步: 插入节点1。
第八步: 根据新插入的节点1的位置找到其父节点的值,判断父子大小,也就是1和5的大小,这里5大于1,父大于子,无需调换父子位置,这里新节点1位置确定。
第九步: 插入新节点1。
第十步: 根据新插入的节点1的位置找到其父节点的值,判断父子大小,也就是1和5的大小,这里5大于1,父大于子,无需调换父子位置,这里新节点1位置确定。
删除下沉
删除顶元素
-
把堆数组的最后一个元素设置替换到数组的首元素的位置。完成顶元素的删除,后面再调整堆数组中各元素的位置。
-
根据第一个节点,获取其左右子树的值,
A. 如果没有左右子节点,返回,完成调整。
B. 如果有左右子节点,获取其中大的那一个子节点。
-
父节点与较大的子节点比较,如果父节点小于子节点,交换父子节点在数组中的位置,反之返回,完成调整。
-
如果位置交换了,把前一步中交换得到的子节点,作为比较值,递归2,3步操作,向下比较,调整堆的各节点位置。
例如:
现存在堆数组11,10,5,3,1,删除顶节点11。
第一步: 把堆数组最后一位的1移动到第一位,把之前第一位的11替换掉。
第二步: 1已经移位到第一位,所以把堆数组中的最后一位pop掉,堆中剩下1,10,5,3。
第三步: 目前堆数组元素位置不符合最大堆的定义,需要下沉调整堆中的节点。
第四步: 找出下沉节点的最大子节点,与下沉节点比较,如果最大子节点大于下沉节点,调换下沉节点和其最大子节点的位置。
第五步: 位置替换后,再进行下沉比较,发现下沉节点仍然小于子节点,继续调整位置。
第六步: 位置替换后,再查找子节点进行比较,发现没有子节点了,无需再调整,本轮调整完毕。
删除任意元素
- 查找到要删除的节点在数组中的坐标位置,把数组的最后一个节点替换到所要删除的节点的位置,完成节点删除。
- 把替换后的节点做下沉比较,步骤同删除顶元素的2,3,4步。
- 如果需要删除的节点,在数组中存在多处,那么需要先获取要删除的节点个数,然后有几个,就执行几遍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。
以上是堆数组。
第一步: 删除节点4(叶子节点),先通过值查找到要删除的4在堆数组里的坐标位置。
第二步: 把堆数组的最后一个元素,替换到要删除的4的位置。然后元素pop掉最后一个元素1。
第三步: 下沉比较,但是这个更换后的节点1没有子节点,所以位置确定。堆数组是10,8,6,7,6,1,5,3,2
第四步: 删除节点10(删除顶节点)。先通过值查找到要删除的10在堆数组里的坐标位置。
第五步: 把堆数组的最后一个元素,替换到要删除的10的位置。然后元素pop掉最后一个元素2。
第六步: 根据2的新位置,获取到其两个子节点中较大的子节点,这里是8,2小于8,需要交换父子节点位置。
第七步: 根据2的新位置,获取到其两个子节点中较大的子节点,这里是7,2小于7,需要交换父子节点位置。
第八步: 根据2的新位置,获取到子节点,这里只有3,2小于3,需要交换父子节点位置。
第九步: 2的新位置再没有子节点,这轮调整结束。
第十步: 删除6(中间节点,并且是多个)。先通过值查找到要删除的6在堆数组里的坐标位置,这里返回的是两个值。
第十一步: 我们对它们一个一个的删除,无非是删除两遍。先删除位置靠前的一个。
第十二步: 把堆数组的最后一个元素,替换到要删除的6的位置。然后元素pop掉最后一个元素2。
第十三步: 根据2的新位置,获取到其两个子节点中较大的子节点,这里是5,2小于5,需要交换父子节点位置。
第十四步: 调整到新位置的2已经没有子节点,这轮调整结束。
第十五步: 删除另一个节点6。
第十六步: 把堆数组的最后一个元素,替换到要删除的6的位置。然后元素pop掉最后一个元素2。
第十七步: 新位置的2是叶子节点,不需要再调整。
整个实例结束,结果为8,7,5,3,2,1
最小堆
定义
父节点大于子节点的二叉树。
节点位置关系
同最大堆
操作
插入上浮和删除下沉与最大堆的思路完全一致,只是最小堆是父子节点判断小的上浮,大的下沉;父节点也是跟小的子节点比较。
复盘
在实现最大最小堆时,当编写获取左右子节点的逻辑时,漏掉了判断下沉后的节点的左右子节点是否都为空,或是是否有一个子节点为空的场景。
这里体现了,在思考问题的解决方案时,只考虑了主体逻辑,对边界值的考虑不够全面。比如第一个节点、最后一个节点、没有子节点的节点、只有一个子节点的节点、没有父节点的节点等等。
如果没有子节点,终止下沉;如果节点位置是0,终止上浮。