堆
首先了解一下堆的性质:堆是一个完全近似二叉树的结构,并且满足父节点的值总大于等于(或小于等于)子节点的性质。
堆通常是使用一维数组进行保存,在起始位置为0的数组中:
- 父节点
i的左子节点在(2i+1)的位置 - 父节点
i的右子节点在(2i+2)的位置 - 子节点
i的父节点在(i-1)/2向下取整的位置
堆排序
堆排序的基本步骤,以数组Arr为例:
- 首先利用Arr中的元素,建立一个大顶堆,即堆顶的根节点为Arr中的最大元素
- 将堆顶的根节点与末尾元素进行交换,此时末尾元素就是最大值。将剩余元素重新堆化成一个大顶堆。
- 重复第二步,直至数组有序排列
建堆
将给定的无序数组构造成大顶堆,如果是降序排列使用小顶堆
- 以数组
[4,8,1,5,7]为例
2. 从非叶子节点开始,即
Math.floor(len/2) - 1的节点开始从下到上的调整。父节点5,跟左右节点8和7比较,8 > 5且8 > 7,则5跟8进行交换。由于8是叶子结点,不需要再向下交换。
- 处理第二个非叶子结点
4,4跟左右节点8和1比较,8 > 4且8 > 1,则4跟8进行交换
- 这时候
4不是叶子结点,继续进行左右节点5和7比较,7 > 4且7 > 5,则4跟7交换。到达了叶子节点,不需要再向下交换。
至此,完成了大顶堆的构建。堆顶即为最大的元素,也是数组的第一个元素。
排序
- 在建好的大顶堆基础上,将堆顶元素
8与末尾元素4进行交换
由于只有堆顶元素不一定符合堆的性质,因此从堆顶元素开始,将涉及交换的元素将与其子节点进行比较,如果有不符合堆性质的元素,通过交换使其重新满足堆的性质。整个重新堆化的流程最多涉及二叉树其中一边节点的调整,也是一个自顶向下的一次调整。
- 调整堆1 - 堆顶元素
4与左右子节点7和1进行比较,有7 > 4且7 > 1,将7与4进行交换,
调整堆2 - 可以看到元素4依然不满足堆的性质: 5 > 4,由于元素8已经排好序了,不再参与流程。交换4和5。
- 此时堆已经符合堆的性质了,继续交换堆顶元素
7和末尾元素4,得到第二大元素,数组中的末尾两个元素已经有序,不再参与流程。
- 调整堆 - 堆顶元素
4与左右子节点5和1进行比较,有5 > 4且5 > 1,将4与5进行交换。
- 此时堆已经符合堆的性质了,继续交换堆顶元素
5和末尾元素1,得到第三大元素5,数组中的末尾三个元素已经有序,不再参与流程。
- 调整堆 - 堆顶元素与左节点进行比较,有
4 > 1,将4与1交换。
- 此时堆已经符合堆的性质了,继续交换堆顶元素
4和末尾元素1,得到第四大元素4,此时数组已经有序,堆排序流程结束。
代码实现
// 堆排序
function heapSort(arr){
// 建堆
function buildHeap(arr){
let heapSize = arr.length;
// 从最后的非叶子节点开始,创建大顶堆,满二叉树中最后一个非叶子节点为(n/2-1),n为二叉树的节点数
// 从下往上遍历是为了让堆顶元素为最大或最小数,
for(let i=Math.floor(heapSize / 2 - 1); i>=0; --i){
heapify(arr, i, heapSize);
}
}
function heapify(arr, index, heapSize){
while(true){
let minIndex = index;
// 先跟左叶子节点比较
if(2*index + 1 < heapSize && arr[2*index + 1] > arr[minIndex]){
minIndex = 2*index + 1;
}
// 再跟右叶子节点比较
if(2*(index + 1) < heapSize && arr[2*(index + 1)] > arr[minIndex]){
minIndex = 2*(index + 1);
}
// 如果当前节点比左右节点都大,则不需要进行操作
if(minIndex === index) return;
// 将当前节点与左右节点中较大的进行交换
swap(arr, minIndex, index);
// 将交换了的节点也进行heapify,保证节点会比左右叶子节点大
index = minIndex;
}
}
// 调用建堆函数
buildHeap(arr);
// 大顶堆排序,在建堆的时候,只保证了根节点会大于叶子节点
// 不断地将根节点移到数组的最后面,再对剩余的节点进行堆化
for(let i=arr.length-1; i>0; --i){
swap(arr, 0, i);
heapify(arr, 0, i)
}
}