堆排序是一种常用的高效排序方法,他的时间复杂度是O(lgn),是一种原地排序的算法。
堆是一种数组对象,它被视为一颗完全二叉树,如图 1所示,树的每个节点与数组中存放该节点值的那个元素对应,除最后一层外树的每一层都是满的,最后一层元素从左到右依次填入,。树的根为第1个元素,对于给定的下标i,其父节点parent(i)为i/2,左孩子left(i)为2i,右孩子right(i)为2i+1。
图 1
堆分为大根堆和小根堆;大根堆是指除根节点以外每个节点i都有A[parent(i)]>=A[i],即每个节点的值最多和父节点的值一样大,这样堆中的最大值就在根节点中,且以某个节点为根的子树中,各个节点的值都不大于该子树根节点的值,图 1就是一个大根堆。小根堆的刚好相反,每个节点的值都不小于其父节点。
堆的常用操作有三个
一、调整堆以使其保持大根堆的性质maxHeapify
maxHeapify是对大根堆进行操作的重要子程序,其输入为一个数组A和一个下标i,maxHeapify被调用时假定left(i)和right(i)都满足大根堆的性质,但是A[i]有可能小于其子女而违反了大根堆性质,maxHeapify使A[i]下降,使以A[i]为根的堆成为大根堆。其过程如图 2所示:
图二
二、建立大根堆buildMaxHeap
buildMaxHeap算法是利用maxHeapify算法来进行的。用数组存储一个有n个元素堆时,叶节点的下标是n/2+1、n/2+2……n(这里就不做证明了,有兴趣可以自己证明),建立大根堆就是利用这个性质。叶节点可以看做只有一个元素的堆,只有一个元素也就自然满足大根堆的性质,所以以叶节点为根的堆都是大根堆。但是以其它节点为根的堆就不一定是大根堆,为了使他们满足大根堆的性质,就在节点上调用maxHeapify(叶节点是大根堆就满足了函数输入时的条件)。所以建立大根堆的过程就是在除叶节点以为的节点上调用maxHeapify。
三、利用大根堆排序heapSort
heapSort也是利用maxHeapify算法来进行的。在一个大根堆中,最大元素就是堆的根。堆排序就是利用的这个性质。在一个含有n个元素的数组上调用buildMaxHeap,就能得到最大的元素,然后将最大的元素和数组的尾部,再将最大元素从堆中除去,此时堆的元素为n-1个,但是不满足大根堆的性质,就在根节点上调用maxHeapify使其成为大根堆。不断重复这个过程直到堆中只剩下一个元素,就完成了排序。
下边是三个程序的java实现,数组是从0开始计数的,而上述理论从1开始,在设计数据结构的时候堆的长度本应该是堆的固有属性,为了简便我把size单独提出来了,所以程序看起来有些怪异。
maxHeapify:
/*
* @param A 待调整的堆
*
* @param node 待调整的节点
*
* @param size 堆的长度
*/
public void maxHeapify(int[] A, int node, int size) {
int left = (node << 1) + 1;
int right = left + 1;
int max = 0;
if (left <= size && A[left] > A[node]) {
max = left;
} else {
max = node;
}
if (right <= size && A[right] > A[max]) {
max = right;
}
if (max != node) {
int temp = A[max];
A[max] = A[node];
A[node] = temp;
maxHeapify(A, max, size);
}
System.out.println(Arrays.toString(A));
}
buildMaxHeap:
* @param A 待建成大根堆的堆
* @param size 堆的长度
*/
public void buildMaxHeap(int[] A, int size) {
for (int i = size / 2; i >= 0; i--) {
maxHeapify(A, i, size);
}
}
heapSort:
/*
* @param A 待排序的堆
* @param size 堆的长队
*/
public void heapSort(int[] A, int size) {
int temp = 0;
buildMaxHeap(A, size);
for (int i = size; i > 0; i--) {
temp = A[0];
A[0] = A[size];
A[size] = temp;
size -= 1;
maxHeapify(A, 0, size);
}
}