【经典排序算法】07. 堆排序

93 阅读4分钟

堆排序是利用最大堆性质和维护最大堆的操作来进行的排序。

还记得堆的一些性质:

堆的索引与数组索引具有特定对应的关系

如果堆是以 1 为起始结点索引的话,数组索引从左到右0,1,2...,对应堆中每个节点的索引顺序,就是对二叉堆层序遍历每个结点的顺序。对于二叉堆的某个结点索引ii来说,ii的父结点索引为 i/2,ii的左子结点索引为 i*2 ,那ii的右子结点索引就是 i*2+1(左子节点索引+1)。 在这里插入图片描述

但如果堆是以 0 为起始结点索引的话,数组索引从左到右0,1,2...,对于二叉堆的某个结点索引ii来说,ii的父结点索引为 i/2(左子结点) i/2 - 1(右子结点),ii的左子结点索引为 i*2 + 1 ,那ii的右子结点索引就是 i*2+2(左子节点索引+1)。

这同时也说明为什么习惯上用1作为堆的起始结点索引,因为这样对左右子结点来说,父结点索引的计算就可以统一为 i/2,比较方便计算。

在这里插入图片描述

代码如下:

public class Main {

    public static void main(String[] args) {
        int[] arr = {3, 5, 6, 2, 1, 7, 4};
        System.out.print("排序前:");
        arrPrint(arr);
        HeapSort(arr);
        System.out.print("排序后:");
        arrPrint(arr);
    }

    // 堆排序
    //
    // 第一个for循环,目的是将数组转化为最大堆数组(在本实例中转为[7, 5, 6, 2, 1, 3, 4])。
    // 从最后一个非叶结点索引i=len/2 - 1开始遍历,倒叙遍历,直到堆顶结点0。
    // heapify函数是一种不完全的堆化,可以将遍历结点i及其部分子二叉堆转化为最大二叉堆,
    // 我们用for循环从最后的非叶结点开始倒序遍历,可以覆盖到整个二叉堆。
    // (不知道为什么是len/2-1,可以假想最后一个结点len-1的后面还有一个虚拟结点len,
    // 当结点len是左子结点时,len的父结点len/2的上一个结点len/2 - 1 一定是最后一个非叶结点。
    // 当结点len是右子结点时,len的父结点len/2-1同样一定是最后一个非叶结点。
    // 严格证明可以看看 https://blog.csdn.net/qq_34629352/article/details/105591415 )
    //
    // 第二个for循环,从尾到头倒序遍历二叉堆所有结点,遍历索引为j,
    // 循环中,交换0和j元素,即把最大值交换到堆(数组)末端的j位置,
    // 之后调用heapify(以0为函数遍历结点,长度边界为j),可将0到j-1的子二叉堆进行堆维护,
    // 即0到j-1中的最大值上升到堆顶0的位置处。如此循环,j在倒序遍历的过程中,子二叉堆堆顶
    // 的最大值会不断地交换到j遍历位置,交换完,子二叉堆进行维护,新的最大值重新上升到堆顶,
    // 待循环结束,排序就完成了。
    private static void HeapSort(int[] arr) {
        int len = arr.length;
        for (int i = len / 2 - 1; i >= 0; i--) {
            heapify(arr, i, len);
        }
        for (int j = len - 1; j > 0; j--) {
            swap(arr, 0, j);
            heapify(arr, 0, j);
        }
    }

    // 堆化函数/堆维护函数
    // 输入为:最大堆数组arr,遍历的结点索引i,长度边界len,堆维护的操作范围是i到len-1
    // heapify实际是将遍历结点i,及遍历结点i的左右子结点,及左右子结点中较大结点的左右子结点
    // (以此顺延下去),这些结点构成的子二叉堆进行堆化操作。
    //
    // 将当前遍历结点值arr[i]记为temp。
    // for循环:起始为遍历结点i的左子节点k=i*2+1,在k超过len-1时终止,
    // 变化条件为k继续取k的左子节点,即k=k*2+1。
    //      第一个if:保证i的右子结点k+1不越界,如果i的右子结点大于左子结点值,
    //      则k指向右结点。
    //      第二个if,如果k的结点值arr[k]比父结点i的结点值arr[i]=temp要大,
    //      此时不满足堆的性质,则将子结点k赋给父结点i,arr[i]值赋为arr[k]
    //      i修改为k。if不满足,循环直接结束。
    // 最后将temp给i所在的结点(此时i是原来的k),完成i和k结点的交换。
    private static void heapify(int[] arr, int i, int len) {
        int temp = arr[i];
        for (int k = i*2 + 1; k < len; k = k*2 + 1) {
            if (k + 1 < len && arr[k] < arr[k + 1]) {
                k++;
            }
            if (arr[k] > temp) {
                arr[i] = arr[k];
                i = k;
            }
            else {
                break;
            }
        }
        arr[i] = temp;
    }

    // 交换位置函数
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    // 辅助函数:将int[] 打印出来
    private static void arrPrint(int[] arr) {
        StringBuilder str = new StringBuilder();
        str.append("[");
        for (int v : arr) {
            str.append(v + ", ");
        }
        str.delete(str.length() - 2, str.length());
        str.append("]");
        System.out.println(str.toString());
    }
}

我们对堆排序的实例的动画演示如下:

在这里插入图片描述

heapify函数的演示如下:

在这里插入图片描述

可以看到,只使用一次heapify函数无法对堆化整个数组。