图解堆排序原理

1,460 阅读4分钟

首先了解一下堆的性质:是一个完全近似二叉树的结构,并且满足父节点的值总大于等于(或小于等于)子节点的性质。

堆通常是使用一维数组进行保存,在起始位置为0的数组中:

  • 父节点i的左子节点在(2i+1)的位置
  • 父节点i的右子节点在(2i+2)的位置
  • 子节点i的父节点在(i-1)/2向下取整的位置

堆排序

堆排序的基本步骤,以数组Arr为例:

  • 首先利用Arr中的元素,建立一个大顶堆,即堆顶的根节点为Arr中的最大元素
  • 将堆顶的根节点与末尾元素进行交换,此时末尾元素就是最大值。将剩余元素重新堆化成一个大顶堆。
  • 重复第二步,直至数组有序排列

建堆

将给定的无序数组构造成大顶堆,如果是降序排列使用小顶堆

  1. 以数组[4,8,1,5,7]为例

image.png 2. 从非叶子节点开始,即Math.floor(len/2) - 1的节点开始从下到上的调整。父节点5,跟左右节点87比较,8 > 58 > 7,则58进行交换。由于8是叶子结点,不需要再向下交换。

image.png

  1. 处理第二个非叶子结点44跟左右节点81比较,8 > 48 > 1,则48进行交换

image.png

  1. 这时候4不是叶子结点,继续进行左右节点57比较,7 > 47 > 5,则47交换。到达了叶子节点,不需要再向下交换。

image.png

至此,完成了大顶堆的构建。堆顶即为最大的元素,也是数组的第一个元素。

image.png

排序

  1. 在建好的大顶堆基础上,将堆顶元素8与末尾元素4进行交换

image.png

由于只有堆顶元素不一定符合堆的性质,因此从堆顶元素开始,将涉及交换的元素将与其子节点进行比较,如果有不符合堆性质的元素,通过交换使其重新满足堆的性质。整个重新堆化的流程最多涉及二叉树其中一边节点的调整,也是一个自顶向下的一次调整。

  1. 调整堆1 - 堆顶元素4与左右子节点71进行比较,有7 > 47 > 1,将74进行交换,

image.png

调整堆2 - 可以看到元素4依然不满足堆的性质: 5 > 4,由于元素8已经排好序了,不再参与流程。交换45

image.png

  1. 此时堆已经符合堆的性质了,继续交换堆顶元素7和末尾元素4,得到第二大元素,数组中的末尾两个元素已经有序,不再参与流程。

image.png

  1. 调整堆 - 堆顶元素4与左右子节点51进行比较,有5 > 45 > 1,将45进行交换。

image.png

  1. 此时堆已经符合堆的性质了,继续交换堆顶元素5和末尾元素1,得到第三大元素5,数组中的末尾三个元素已经有序,不再参与流程。

image.png

  1. 调整堆 - 堆顶元素与左节点进行比较,有4 > 1,将41交换。

image.png

  1. 此时堆已经符合堆的性质了,继续交换堆顶元素4和末尾元素1,得到第四大元素4,此时数组已经有序,堆排序流程结束。

image.png

代码实现

// 堆排序
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)  
    }
}