在前面,我们讲了二叉堆的实现,堆顶元素始终是最大或最小的,那么堆排序是怎么一回事呢?
- 根据数列生成一个最大堆(需要从小到大排序的时候)或最小堆;
- 交换堆顶和最后一个元素(假删除,自我调节中不需要处理交换到最后的栈顶元素),进行自我调节(downAdjust / upAdjust);
-
循环数列每个元素执行第2步;
最终形成了一个层序遍历为从小到大(从大到小)数列的二叉树。
构建堆
把一个无序数列构建成一个二叉堆,最大最小而异,本质都是将其所有非叶子节点依次下沉,为什么这样就可以构建一个二叉堆呢?
二叉堆的定义,父元素的值始终大于等于或者小于等于子节点的值,那么我们只需要将所有父节点都移动至满足该条件即可,所以所有的父节点都是非叶子节点。
我们在构建堆的时候,该如何找到所有非叶子节点呢?
我们都知道查找一个右子节点的父节点的索引为parentIndex = (childIndex - 2)/ 2
,那么索引值最大的非叶子节点是多少?很明显,最后一个节点的父节点就是,即maxNotLeafNodeIndex = Math.floor((list.length - 1) / 2)
。此时我们只需要从这个节点往前,遍历剩余的非叶子节点,将其依次下沉调整,即可得到一个二叉堆。
function buildHeap(list) {
const maxNotLeafNodeIndex = Math.floor((list.length - 1) / 2);
for (let i = maxNotLeafNodeIndex; i >= 0; i--) {
downAdjust(list, i, list.length);
}
return list;
}
通过构建最大堆来排序
构建完堆以后,执行上面排序步骤的第2步第3步:
function downAdjust(list, targetIndex, realLength) {
let parentIndex = targetIndex;
const target = list[parentIndex];
let childIndex = Math.floor(2 * parentIndex + 1);
while (childIndex <= realLength - 1) {
if (childIndex + 1 <= realLength - 1 && list[childIndex+1] > list[childIndex]) {
childIndex++;
}
if (list[childIndex] <= target) {
break;
}
list[parentIndex] = list[childIndex];
parentIndex = childIndex;
childIndex = Math.floor(2 * parentIndex + 1);
}
list[parentIndex] = target;
}
function headSort(list) {
// 生成最大堆
const maxNotLeafNodeIndex = Math.floor((list.length - 1) / 2);
for (let i = maxNotLeafNodeIndex; i >= 0; i--) {
downAdjust(list, i, list.length);
}
// 遍历
for (let i = 0; i < list.length; i++) {
const max = list[0];
// 最大的元素到数列末尾,不再参与堆的自我调整过程
const usefulLen = list.length - 1 - i;
list[0] = list[usefulLen];
list[usefulLen] = max;
downAdjust(list, 0, usefulLen);
}
return list;
}
堆排序的总结
- 找到所有非叶子节点,构建二叉堆;
- 从堆顶开始遍历,放置到堆底,再次进行自我调整,形成有序数列;
- 时间复杂度稳定为O(nlogn),空间复杂度为O(1),不需要任何多余的空间来存储额外值。
网友评论