开始
堆排序一种比较复杂的排序算法,它主要用到堆(Heap)这一数据结构。
堆(Heap)是二叉树的一类,完全二叉树,它的特点是:
- 除最后一层外,其他层节点都是满的
- 最后一层节点可以不满
- 最后一层节点从左向右填充
- 左节点和右节点的大小关系没有限制
如下图
堆又分为
- MaxHeap, 父节点值大于子节点值
- MinHeap, 父节点值小于子节点值
堆排序的主要过程(以MaxHeap为例):
- 将数组构造为MaxHeap,父节点为最大值
- 将父节点与末尾元素交换,
- 将剩余的元素再次构建为MaxHeap
- 重复二,三步,至只剩一个元素
构造MaxHeap的过程
从第一个非叶子结点开始,从下至上,从右至左,对每一个非叶子结点做shiftDown操作,所以shiftDown是一个关键过程。
其中,可以利用数组结构来表示Heap,已知节点索引i, 其节点关系有
- 父节点, Math.floor((i - 1) / 2)
- 左节点,2 * i + 1
- 右节点,2 * i + 2
- 第一个非叶子结点,arr.length/2 - 1
推荐实现
参考中都给出了一些实现,不过,对于heapifyDown方法,推荐如下实现,来源于参考4,个人觉得从理解和结构上看都比较好。
heapifyDown(customStartIndex = 0) {
// Compare the parent element to its children and swap parent with the appropriate
// child (smallest child for MinHeap, largest child for MaxHeap).
// Do the same for next children after swap.
// 当使用Heap排序时,顶部元素被赋值为尾元素,此时,需要从顶向下重新建堆
let currentIndex = customStartIndex;
let nextIndex = null;
// 假设在MaxHeap中,节点初始关系为P->C->C1, 首先进行堆构建,
// 继续假设子节点C大于父节点P, 则需要将子节点C与父节点P进行交换,
// 父节点P下沉做为子节点,此时新的节点关系为C->P->C1,
// 如果C1>P,则需要继续将节点P下沉,变为C->C1->P
// 以上例子说明,只要一个节点位置发生变化,就需要逐层向下递归,以确保节点关系正确
// 此处的while循环就是这个道路,虽然上面只有一个参数customStartIndex, 但考虑
// 节点可能互换,就需要进行向下递归,把当前节点的所有节点都进行一次比较
while (this.hasLeftChild(currentIndex)) {
// 与二叉树不同,Heap结构中,左右节点值的关系是不固定的
// 左节点可能小于右节点,也可能大于右节点,只能保证左右节点和父节点的值大于关系固定
// 取左右节点中,较大(For MaxHeap)或较小(For MinHeap)的值与父节点进行交换
if (
this.hasRightChild(currentIndex)
&& this.pairIsInCorrectOrder(this.rightChild(currentIndex), this.leftChild(currentIndex))
) {
nextIndex = this.getRightChildIndex(currentIndex);
} else {
nextIndex = this.getLeftChildIndex(currentIndex);
}
// 如果节点大小关系正确,说明不用进行节点互换,直接退出,
// 否则进行递归检测与修正
if (this.pairIsInCorrectOrder(
this.heapContainer[currentIndex],
this.heapContainer[nextIndex],
)) {
break;
}
this.swap(currentIndex, nextIndex);
currentIndex = nextIndex;
}
}
总结
参考1,2里图文都很详细,推荐,其中实现形式,可以自己再整理优化下
另外,我找到一个可视化工具,不仅有Heap Sort, 还有Binary Tree,可以上手测试,亲身感受,强烈推荐: btv.melezinek.cz/binary-heap…
稳定性
堆排序是一种不稳定的排序方法, 对于相同的关键字可能出现排在后面的关键字被交换到前面来的情况