在介绍堆排序之前,先来介绍一下有关堆的概念。
假设有n个数据元素的序列 K0,K1……Kn-1,当且仅当满足如下关系时,可以将这组数据称为小顶堆(小根堆):Ki <= K2i+1 且 Ki <= K2i+2 (其中i=0,2……(n-1)/2 )。
当且仅当满足如下关系时,可以将这组数据称为大顶堆(大根堆):Ki >= K2i+1 且 Ki>= K2i+2 (其中i=0,2……(n-1)/2 )。
对于满足小顶堆的数据序列K0,K1……Kn-1,如果将他们顺序排成一个完全二叉树,则此树的特点是:树中所有节点的值都小于其左、右子节点的值,此树的根节点的值必然最小。反之,对于满足大顶堆的数据序列K0,K1……Kn-1,如果将他们顺序排成一颗完全二叉树,则此树的特点是:树中所有节点的值都大于其左、右两个子节点的值,此树的根节点的值必然最大。
普及:1.小顶堆的任意子树也是小顶堆,大顶堆的子树也是大顶堆。
2.对于索引为 K 的节点,它的两个子节点的索引分别为 2K+1 和 2K+2;反过来索引为 K 的节点,其父节点的索引为 (k-1)/2。
堆排序的关键在于建堆,它安如下步骤完成排序:
第一趟:将索引 0~n-1 处的全部数据建成大顶堆(小顶堆),就可以选择出这组数据中的最大值(最小值)。
将上一步所建的大顶堆(小顶堆)的根节点与这组数据的最后一个节点交换,就使得这组数据中的最大值(最小值)排在了最后。
第二趟:将索引 0~n-2 处的全部数据建成大顶堆(小顶堆),就可以选择出这组数据中的最大值(最小值)。
将上一步所建的大顶堆(小顶堆)的根节点与这组数据的倒数第 2 个节点交换,就使得这组数据中的最大值(最小值)排在了倒数第 2 位。
……
第 K 趟:将索引 0~n-k 处的全部数据建成大顶堆(小顶堆),就可以选择出这组数据中的最大值(最小值)。
将上一步所建的大顶堆(小顶堆)的根节点与这组数据的倒数第 k 个节点交换,就使得这组数据中的最大值(最小值)排在了倒数第 k 位。
通过上面的介绍不难发现,堆排序的步骤就是重复执行以下两部:
① 建堆。
② 拿堆的根节点和最后一个节点交换。
模拟数据操作如下:
数据为:21,30,49,30*,21*,16,9
public class HeapSort {
public static void heapSort(DataWrap[] data){
System.out.println( "-开始排序-");
int arraylength = data.length;
for(int i = 0; i < arraylength - 1; i ++){
builMaxHeap(data,arraylength - 1 - i);
swap(data,0,arraylength - 1 - i);
//没排完一次输出一下结果
System.out.println( java.util.Arrays.toString(data));
}
}
//21,,30,49,30*,21*,16,9
public static void main(String[] args){
DataWrap[] data = {new DataWrap(21,""),
new DataWrap(30,""),
new DataWrap(49,""),
new DataWrap(30,"*"),
new DataWrap(21,"*"),
new DataWrap(16,""),
new DataWrap(9,""),};
System.out.println( "-排序前-"+java.util.Arrays.toString(data));
heapSort(data);
System.out.println( "-排序后-"+java.util.Arrays.toString(data));
}
/**
* 对data数组从 0 到 lastIndex 建大顶堆
* @param data
* @param lastIndex
*/
private static void builMaxHeap(DataWrap[] data, int lastIndex){
//从lastIndex处节点(最后一个节点)的父节点开始
for(int i = (lastIndex - 1)/2; i >= 0; i --){
//保存当前正在判断的的节点
int k = i;
//如果当前节点 k 的子节点存在
while( (k*2+1) <= lastIndex){
int biggerIndex = k*2+1;
//说明有右子节点
if(biggerIndex < lastIndex){
//右子节点 大于 左子节点
if(data[biggerIndex].compareTo(data[biggerIndex+1]) < 0){
biggerIndex ++;
}
}
//选出三个节点中最大的值放在父节点索引
if(data[k].compareTo(data[biggerIndex]) < 0){
swap(data,k,biggerIndex);
k = biggerIndex; //可能子节点下面还有节点,这里与while循环对应
}else{
break;
}
}
}
}
/**
* 交换data数组中索引为 i 和 j 处的元素
* @param data
* @param i
* @param j
*/
public static void swap(DataWrap[] data, int i, int j){
DataWrap dw = data[i];
data[i] = data[j];
data[j] = dw;
}
}
堆排序与直接选择排序的差别就在于堆排序可以通过树形结构保存部分的比较结果(其中红线部分),可以减少比较次数。
对于堆排序算法而言,假设有n个数据,需要进行n-1次建堆,每次建堆本身消耗时为log2N,则其时间效率为O(n*log2N)。
其空间效率为 O(1),从上面的输出结果来看堆排序是不稳定的。
参考书籍:《疯狂Java程序员的基本修养》 --李刚
《Java算法》(第三版 第一卷) --清华大学出版社