按照国际惯例,开篇前先简单介绍(吹一波)堆排序(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;
}
}