要弄清楚堆排序,我们首先要懂得以下两点:
1)逻辑上的结构,怎么样才是一个堆。
2)存储上的结构,一个堆存储起来的结构是怎么样的。
一般来讲,堆排序中的“堆”指的是二叉堆,一种完全二叉树的结构,每个父结点最多只有两个子结点,且满足两点:
1)父结点总是大于(小于)其两个子结点(大于的,我们叫最大堆,小于的,我们叫最小堆)。
2)父结点的左右子树也都满足上面1)的规则,也就是说其左右子树也是一个最大堆或者是最小堆。
树结构,我们可以看下图:
一般来说,算法中提到的堆结构都是用数组来存储的,所以上面的这个二叉树放在数组中就是
从上面我们可以看到,按顺序从上到下,从左到右存储,比如“1”存在a[0],则其左子结点则存在a[2*0 + 1]=a[1]的位置上,其右子结点则存在a[2*0 + 2] = a[2]的位置上,而"2"的左结点是4,是存在a[2*1+1] = a[3]的位置上,其右结点5,则是在跟着的位置上,所以我们可以发现如果父结点的位置是i,则其左右子结点的位置分别2*i + 1 和 2*i + 2,这其实是因为完全二叉树就是2倍2倍往下变大的。
int lchild = 2 * i + 1;
int rchild = 2 * i + 2;
不过要注意,这只是将根节点放在a[0]的位置上,如果将根节点放在a[1]上,则左结点应该是a[2*1],而右结点则是a[2*1 + 1] = a[3]了,也就是说左右子结点的公式就变了:
int lchild = 2 * i;
int rchild = 2 * i + 1;
接下来,我们来看一下堆排序的原理。
一开始,其实数组是无序的,如下面一堆数据:
它是无序的,如果把它当成一个堆,它的结构就是下面这样,
它即不是最大堆,也不是最小堆。在这里,假设我们把它调节成最大堆(最小堆),
1)那么它的根结点一定是要比下面所有的子结点都大的,这样,我们就可以拿出这堆数据中最大的那个数,比如上图中的57。
2)我们将这个最大数从这个堆顶移走,然后再重新调整成最大堆,于是我们又跑到第1)步了,这样,我们又拿到一个最大数,把这个数拿走,我们将剩下的值继续调整成最大堆。这样,一次又一次,我们就能够顺序地将这堆数据由大到小地给拿出来了,而这其实就是一个排序过程,也就是堆排序。
而第一步,我们就要先把这个无序的数组给调整成一个最大堆的结构,调整过程如下图:
1)从第一个非叶子结点开始,也即是7开始,我们比较它跟其左右子结点的值,将三者中最大的值给放到顶上,所以就把7跟23给互换了位置,由于子结点已经没有子结点了,所以第一个就结束了。
2)那么就轮到其前面一个结点,也即是12了,同样的将其跟57互换位置,也结束了。
3)轮到9了,比较9跟其子结点的值,发现23比9还要大,这时候把23换上调,把9往下放,由于9还有子结点,所以就要继续往下比较,所以就把10给放上来,而9就沉到底下了,然后这一次结束。
4)那么就轮到根结点11了,比较发现,57比它要大,就把它跟57互换位置,而11调下来之后,其还有子结点,就要继续比较,而44比11大,所以44往上放,而11就继续往下放。
当所有非叶子结点都调整完了之后,建堆这个操作也就结束了,这个时候可以现,57,这个数组中最大的值已经给放到堆顶了。
而此时在数组中的结构也相对应地变了,如下:
这样,我们就能够把57跟9互换一下位置,然后拿数组前面8个数值再去继续调整,这是为了让要调整的数组永远是从0开始,改变的只是数组的长度,一直到最后调整的数组长度为1,就说明调整结束了,而这个时候,数组也已经就是有序的了。
利用最大堆,我们就可以把数组从小到大排,反之,利用最小堆,就可以把数组从大到小排。
下面就是根据这个原理而实现的堆排序的代码了:
package com.lms;
/**
*
* @author linmiansheng
* @date 2014-02-28
*
*/
public class HeapSort {
/**
* build a big root heap
* @param a
* @param i
* @param size
*/
public static void adjustHeap(int[] a, int i, int size){
int lchild = 2 * i + 1;
int rchild = 2 * i + 2;
int tmp = i;
// i <= size/ 2 means i has children.
if(i <= size/2 ){
if(lchild < size && a[lchild] > a[tmp]){
tmp = lchild;
}
if(rchild < size && a[rchild] > a[tmp]){
tmp = rchild;
}
if(tmp != i){
//either the left child or the right child, swap the two values.
Helper.swap(a, tmp, i);
adjustHeap(a, tmp, size);//continue to check the child and its children.
}
}
}
public static void buildHeap(int[] a,int size){
for (int i = size /2; i >= 0; i--) {
adjustHeap(a, i, size);
}
}
public static void heapSort(int[] a) {
int size = a.length;
buildHeap(a, size);
for (int i = 1; i < size; i++) {
Helper.swap(a, 0, size - i);
adjustHeap(a, 0, size - i);
}
}
public static void main(String[] args){
int[] a = { 11, 9, 12, 7, 4, 57, 44, 23, 10 };
Helper.printArray(a);
heapSort(a);
Helper.printArray(a);
}
}
同样的,我们也像前面 算法学习(二)快速排序(上)一样,来试试大数据量的时间:
int[] a = Helper.generateRandomNumbers(20000000, 100000000);
long start = System.currentTimeMillis();
heapSort(a);
long end = System.currentTimeMillis();
long duration = end - start;
Helper.print("duration = " + duration + "\n");
而所花的时间,如下:
duration = 24573
很明显,对于大数据量来说,其排序速度不如快速排序。