【排序】堆排序详解 附代码

按照国际惯例,开篇前先简单介绍(吹一波)堆排序(Heapsort)。Heapsort是一种优秀的排序算法(个人感觉基本排序算法中仅次于快速排序),时间复杂度为O(nlgn),同时,Heapsort具有空间原址性:任何时候都只需要常数个额外的元素空间存储临时数据。

首先需要明白何谓堆,堆也称为二叉堆,本质上是一个数组,可以被看成一个近似的完全二叉树(完全二叉树是指除了最底层外,该树是完全充满的,而且是从左到右填充)。表示堆的数组A有两个属性:

  • A.length:表征数组元素的个数
  • A.heap-size:表征有多少个数组元素存储在该数组中。
    在这里插入图片描述
    堆一般分为两种形式:最大堆和最小堆。最大堆某个结点的值至多与其父结点一样大,最小堆则恰好相反,最小堆的最小元素存放在根结点中。
    如果把堆看成一棵树,那么堆的高度就可以定义为结点的高度:即该结点到叶结点最长简单路径上边的数目。
    堆排序算法中将会涉及以下三个基本过程:
  • max-heapify过程:时间复杂度O(lgn),维护最大堆性质的关键
  • build-max-heap过程:线性时间复杂度,可以从无序的输入数组中构造一个最大堆
  • heapsort过程:时间复杂度为O(nlgn),功能是对一个数组进行原址排序。

维护堆的性质

维护堆的性质,本质上就是要保证任何一个结点值都要严格大于等于其子结点的值。它的输入是一个数组A和索引i,我们假定根结点为left(i)和right(i)的二叉树都是最大堆,但这时A[i]可能小于其孩子,这就破坏了最大堆的性质。Max-heapify通过让A[i]的值在最大堆中“逐级下降”从而使得以下标i为根结点的子树重新遵循最大堆的性质。
在程序的每一步,从A[i]、A[left(i)]、A[right(i)]中选出最大的,并将其下标存储在largest中。如果A[i]最大,那么以i为根结点的子树已经是最大堆;否则最大元素是i的某个子结点,交换A[i]和A[largest]的值,交换后,下标为largest的结点的值是原来的A[i]。对于每个结点的处理都可以用以下流程图表示:
在这里插入图片描述
但是以该结点为根的子树又可能违反最大堆性质,所以需要按照上述流程堆该子树递归的调用。执行过程如下图所示:
在这里插入图片描述
附维护最大堆性质的代码,Java语言实现:

/***
    * @description 维持最大堆的性质。说明:二叉堆的数组下标从1开始,而此处保留Java的语言习惯,数组从0开始计数
    * @method maxHeapify
    * @params [array, i]
    * @returns void
    * @author Carson Chu
    */
   public static <T extends Comparable<? super T>> void maxHeapify(T[] array, int index) {
       int length = array.length;
       maxHeapify(array, index, length);
   }

   private static <T extends Comparable<? super T>> void maxHeapify(T[] array, int index, int length) {
       int childIndex;
       T curValue;
       for (curValue = array[index]; leftChildIndex(index) < length; index = childIndex) {
           childIndex = leftChildIndex(index);
           if (childIndex != length - 1 && array[childIndex].compareTo(array[childIndex + 1]) < 0) {
               childIndex++;
           }
           if (curValue.compareTo(array[childIndex]) < 0) {
               array[index] = array[childIndex];
           } else {
               break;
           }
       }
       array[index] = curValue;
   }

   /***
    * @description 获取索引为i的作子结点的索引
    * @method leftChildIndex
    * @params [i]
    * @returns int
    * @author Chu Xianglu
    */
   private static int leftChildIndex(int i) {
       return i << 1 + 1;
   }

建堆

读到这里,想必聪明的你就会懂得,为何笔者会先介绍维持堆的性质。没错,建堆的关键就在于保持堆的性质。实际上,我们可以通过自底向上的方法利用过程max-heapify建立最大堆。理论证明,我们永远可以在线性的时间内把一个无序数组构造成一个最大堆。

堆排序

堆排序的重点就在于已经建成的最大堆,我们可以每次删除根结点的值,对剩下的n-1个元素通过max-heapify调整使其重新组成新的最大堆,再删除根结点的值……如此循环往复,直到堆的大小降到2。堆排序的运行过程如下所示:
在这里插入图片描述
堆排序的时间复杂度是O(nlgn),因为每次调用build-max-heap的时间复杂度是O(n),而n-1次调用max-heapify每次的时间复杂度都为O(lgn)

代码(Java版实现)

/**
 * @author Carson Chu
 * @date 2020/1/5 15:28
 * @description
 */
public class Heapsort<T> {

    /**
     * 标准的堆排序
     *
     * @param a an array of Comparable items.
     */
    public static <T extends Comparable<? super T>>
    void heapsort(T[] a) {
        for (int i = a.length / 2 - 1; i >= 0; i--) {
            /* 建立最大堆 */
            maxHeapify(a, i, a.length);
        }
        for (int i = a.length - 1; i > 0; i--) {
            /* 删除根结点的元素 */
            swapReferences(a, 0, i);
            maxHeapify(a, 0, i);
        }
    }

    /***
     * @description 维持最大堆的性质。说明:二叉堆的数组下标从1开始,而此处保留Java的语言习惯,数组从0开始计数
     * @method maxHeapify
     * @params [array, i]
     * @returns void
     * @author Chu Xianglu
     */
    public static <T extends Comparable<? super T>> void maxHeapify(T[] array, int index) {
        int length = array.length;
        maxHeapify(array, index, length);
    }

    private static <T extends Comparable<? super T>> void maxHeapify(T[] array, int index, int length) {
        int childIndex;
        T curValue;
        for (curValue = array[index]; leftChildIndex(index) < length; index = childIndex) {
            childIndex = leftChildIndex(index);
            if (childIndex != length - 1 && array[childIndex].compareTo(array[childIndex + 1]) < 0) {
                childIndex++;
            }
            if (curValue.compareTo(array[childIndex]) < 0) {
                array[index] = array[childIndex];
            } else {
                break;
            }
        }
        array[index] = curValue;
    }

    /***
     * @description 获取索引为i的作子结点的索引
     * @method leftChildIndex
     * @params [i]
     * @returns int
     * @author Chu Xianglu
     */
    private static int leftChildIndex(int i) {
        return i << 1 + 1;
    }

    /**
     * 交换数组中两个元素的值
     *
     * @param a      an array of objects.
     * @param index1 the index of the first object.
     * @param index2 the index of the second object.
     */
    private static <T> void swapReferences(T[] a, int index1, int index2) {
        T tmp = a[index1];
        a[index1] = a[index2];
        a[index2] = tmp;
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值