《算法导论》第6章介绍堆排序(heapsort)像插入排序而不像合并排序,是一种原地(in place)排序算法:任何时候,数组中只有常数个元素存储在输入数组之外。并且时间复杂度为O(nlgn)。
堆数据结构不只在堆排序中有用,还可以构成一个有效的优先队列。
堆介绍
(二叉)堆数据结构是一种数组对象。可以被视为一棵完全二叉树。每个节点都有两个子结点,每一层都是填满的最后一层除外,因此可以用数组表示即可。(《算法导论》中指出,length为此数组中的元素个数,heap_size为此数组中堆的元素个数。就是说,heap_size以后的数组元素不属于堆内,不符合堆的要求。)
树的根为A[1],给定一个数组位置i,它的父节点为i/2」,左儿子为2i,右儿子为2i+1。
堆实现
堆的高度为O(lgn),一些基本操作可以在O(lgn)内完成。
注:以下算法假设数组a下标从1开始,a[0]不参与计算,heap_size为数组中堆的大小。
MAX-HEAPIFY过程,保持最大堆性质的关键
输入一个数组A和下标i,并且假定以i为父节点的所有子结点满足堆性质。
输出使得i结点保持堆性质。
/**
* 输入:默认A数组中i后面的位置都满足最大堆性质
* 输出:使得i也满足最大堆性质
* 时间复杂度:O(lgn)
*/
private static void MaxHeapify(int[] a, int i, int heap_size) {
int left = 2 * i; //左儿子
int right = 2 * i+1; //右儿子
int largest = i; //i和左儿子、右儿子中最大值的位置
//如果左儿子比i大,则把左儿子做为最大值
if( left <= heap_size && a[left] > a[i]) {
largest = left;
}
//如果右儿子比最大值大,则把右儿子做为最大值位置
if( right <= heap_size && a[right] > a[largest]) {
largest = right;
}
//如果最大值位置不是i,则将最大值位置的值与i位置的值交换,交换后需要被交换的子结点继续保持堆性质
if ( largest != i) {
int tmp = a[i];
a[i] = a[largest];
a[largest] = tmp;
MaxHeapify(a, largest, heap_size);
}
}
BUILD-MAX-HEAP过程,可以在无序的数组上构造堆
/**
* 自底向上的用MaxHeapify将一个数组变成最大堆
* 时间复杂度O(nlgn)
*/
private static void BuildMaxHeap(int[] a) {
//堆底为叶结点肯定都满足性质,从非叶结点开始构建。
int heap_size = a.length - 1;
int i = heap_size/2;
for (; i>0; i--)
MaxHeapify(a, i, heap_size);
}
HEAPSORT过程,运行时间为O(nlgn)
排序算法每次从堆顶取出最大值,与数组末尾值交换即可,并缩小heap的大小,将末尾值排除在堆外。当堆缩小为1时,数组已完成从小到大排序。
private static void HeapSort(int[] a) {
BuildMaxHeap(a);
for(int i=0;i<a.length;i++) {
System.out.println(a[i]);
}
int heap_size = a.length-1; //构造后的堆的heap_size为数组大小
for(; heap_size > 0; heap_size--){//每次从堆顶取出最大值与堆末尾交换,并缩减堆大小
int tmp = a[heap_size];
a[heap_size] = a[1];
a[1] = tmp;
MaxHeapify(a, 1, heap_size-1);
}
}
优先级队列
《算法导论》第6章第5节优先级队列中指出,“实际中快速排序的一个好的实现往往由于堆排序。尽管这样,堆数据结构还是有着很大的用处:作为高效的优先级队列(priority queue)。”【查看JAVA8源码可知,Arrays.sort也使用的快速排序(小于47个数的时候用的插入排序)。】
“最大优先级队列的一个应用是在一台分时计算机上进行作业调度。当一个作业做完或被中断时,用EXTRACT-MAX操作从所有等待的作业中,选择出具有最高优先级的作业”