常用排序算法总结:堆排序

92 阅读4分钟

首先,我们来看看什么是堆(heap):

  1. 堆中某个节点的值总是不大于或不小于其父节点的值;
  2. 堆总是一棵完全二叉树(Complete Binary Tree)。

完全二叉树是由满二叉树(Full Binary Tree)而引出来的。除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树称为满二叉树。 如果除最后一层外,每一层上的节点数均达到最大值;在最后一层上只缺少右边的若干结点,这样的二叉树被称为完全二叉树。 一棵完全二叉树,如果某个节点的值总是不小于其父节点的值,则根节点的关键字是所有节点关键字中最小的,称为小根堆(小顶堆); 如果某个节点的值总是不大于其父节点的值,则根节点的关键字是所有节点关键字中最大的,称为大根堆(大顶堆)。

☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆

需要注意的是,堆只对父子节点做了约束,并没有对兄弟节点做任何约束,左子节点与右子节点没有必然的大小关系。 将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根节点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值), 然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次最大值。如此反复执行,就能得到一个有序序列了。

  1. 最坏时间复杂度 O(nlog n)
  2. 最优时间复杂度 O(nlog n)
  3. 平均时间复杂度 (nlog n)
  4. 空间复杂度 O(1)

☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆

/**
 * 堆排序的实现需要解决的两个关键问题:
 * (1)将一个无序序列构成一个堆。
 * (2)输出堆顶元素后,调整剩余元素成为一个新堆。
 * 时间复杂度:平均:O(nlogn)、最坏:O(nlogn)、最优:O(nlogn)。
 * 空间复杂度:最坏:O(n)。
 *
 * @param array 待排序的数组
 */
public void heapSort(int[] array) {
    for (int i = 0; i < array.length; i++) {
        // 每次建堆就可以排除一个元素了,每次排序完成之后,最大值总之在末尾。
        maxHeapify(array, array.length - i);
        // 交换,将第一个元素和当前已经排完序的元素进行交换
        int temp = array[0];
        array[0] = array[(array.length - 1) - i];
        array[(array.length - 1) - i] = temp;
    }
    for (int i = 0; i < array.length; i++) {
        System.out.print(array[i] + " ");
    }
}

/**
 * 完成一次建堆,最大值在堆的顶部(根节点)
 */
public void maxHeapify(int[] array, int size) {
    // 从数组的尾部开始,直到第一个元素(角标为0)
    for (int i = size - 1; i >= 0; i--) {
        heapify(array, i, size);
    }
}

/**
 * 建堆(为什么要从最后一个值开始建堆,为什么?)
 * 这是为了保障顶点一定是最大的元素,不信的话你就断点试试,或者自己用草稿本试一下,从0开始是不能保证根节点是最大的值。
 *
 * @param array                   看作是完全二叉树
 * @param currentRootNodePosition 当前父节点位置
 * @param size                    节点总数
 */
public void heapify(int[] array, int currentRootNodePosition, int size) {
    if (currentRootNodePosition < size) {
        // 左子树和右字数的位置
        int left = 2 * currentRootNodePosition + 1;
        int right = 2 * currentRootNodePosition + 2;
        // 把当前父节点位置看成是最大的
        int max = currentRootNodePosition;
        if (left < size) {
            // 如果比当前根元素要大,记录它的位置
            if (array[max] < array[left]) {
                max = left;
            }
        }
        if (right < size) {
            // 如果比当前根元素要大,记录它的位置
            if (array[max] < array[right]) {
                max = right;
            }
        }
        // 如果最大的不是根元素位置,那么就交换
        if (max != currentRootNodePosition) {
            int temp = array[max];
            array[max] = array[currentRootNodePosition];
            array[currentRootNodePosition] = temp;
            //继续比较,直到完成一次建堆
            heapify(array, max, size);
        }
    }
}

/**
 * 非递归方式的建堆。(二叉树的前序遍历)
 *
 * @param array                   待排序的数组
 * @param currentRootNodePosition 当前根节点的位置
 * @param size                    节点总数
 */
public void heapifyNotRecursive(int[] array, int currentRootNodePosition, int size) {
    if (currentRootNodePosition < size) {
        //左右子树的位置
        int left = 2 * currentRootNodePosition + 1;
        int right = 2 * currentRootNodePosition + 2;
        //把当前父节点位置看成是最大的
        int max = currentRootNodePosition;
        if (left < size) {
            if (array[max] < array[left]) {
                max = left;
            }
        }
        if (right < size) {
            if (array[max] < array[right]) {
                max = right;
            }
        }
        if (max != currentRootNodePosition) {
            int sum = array[max] + array[currentRootNodePosition];
            array[max] = sum - array[max];
            array[currentRootNodePosition] = sum - array[currentRootNodePosition];
        }
    }
}