参考:数据结构与算法分析——Java语言描述 (美)Mark Allen Weis
堆排序的时间复杂度是O(N log N) 。堆排序是不稳定的。
首先介绍一下堆吧。堆是一颗被完全填满的二叉树,可能的例外是在底层,底层上的元素从左到右填入,这样的树被称为完全二叉树。下图所示就是一个例子:
一颗高为h的完全二叉树有2h 到 2h+1-1个结点。这意味着完全二叉树的高是 [logN] 。因为完全二叉树很有规律,所以可以用一个数组来表示而不需使用链。如果有一个数组用它来表示二叉树,从下标1开始存放元素的话,那么对于数组任一位置i上的元素,其左儿子在位置2i上,右儿子在2i+1上,它的父亲则在[i/2]上;若从下标0开始存放元素的话,那么对于数组任一位置i上的元素,其左儿子在位置2i+1上,右儿子在2i+2上,它的父亲则在[(i-1)/2]上。
一颗完全二叉树要想成为堆,还要满足所谓的堆序性质(使操作被快速地执行的性质,heap-order property)。比如我们想要快速地找出最小元,因此最小元应该在根上。那么对于所有的父节点,它的元素值要小于它的左右儿子。很显然,根据这个性质,堆中最小元素值在根节点上。
在介绍堆排序的原理时还要介绍一下堆的一个操作:删除最小元素。堆的最小元素很容易找到,因为它就是根节点的值,难的是删除它。当删除最小元时,根节点就为空了。由于现在堆中少了一个元素,因此堆中最后一个元素X移到该堆的某个地方。如果X直接就可以放到空穴中,那么操作完成。不过这一般不太可能,因此我们将空穴的两个儿子中的较小者移入空穴,这样就把空穴向下推了一层,重复该步骤直到X可以被放入空穴中。
下面用一个简单的例子来说明这一过程。如下图:
→→→→
图1 所示的是一个含有5个元素的堆,现在我们要删除最小元13,那么根节点现在相当于是个空穴了。此时最后一个元素21能直接放入这个空穴中吗?显然不能,因为这将破坏堆序性质。那么我们在这个空穴的两个儿子中取较小值放入这个空穴,也就是将 14放入空穴中如图2,此时,原来元素14所在的位置及成了空穴了,21能放进去吗?还是不能!于是重复上面的那个步骤,最后的结果如图5所示。对了,以上的策略称为“下滤”(percolate down)。
那么现在有一个数组a,用堆排序法是怎样对它排序的呢?它大致的思想是这样的:首先,把这个数组结构转化成符合堆序性质的堆结构。什么意思呢?我们慢慢解释。假设现在a={3,9,0,1,4};如果我们把它看成是一颗二叉树的话,它的结构如图6所示:
显然,这不是一个堆。然而我们通过某种方式可以把它转换成一个堆,不过我们这里转换成的堆是让父节点的元素的值大于它的左右儿子结点的值,至于为什么要这样待会儿你就明白了。这种方式就是对下标为[a.length/2]-1一直到0为止 的位置(实际上也就是成为父节点的位置)执行上面所说的“下滤” 操作。不信的话你试试,当然,记住是父节点的元素的值大于它的左右儿子结点的值。以此处为例的话,依次对下标或者说序号为 1 、0 的位置(就是图6中元素9 、元素3所处的位置)执行“下滤”操作。结果如图7所示。
然后,我们要做的事情就是把根节点的元素和最后一个元素交换,那么交换过后,最后一个位置上的元素就是最大的元素9了。我们想象一下,把这个结点抹去,只剩下4个结点了,剩下的这4个结点构成的二叉树又不符合堆序性质了,对根节点执行“下滤”操作,这时候这4个结点又构成了一个堆。以这个堆为对象,重复以上的步骤,可以想象这个堆中的最后一位位置上就是这个堆的最大的元素4了。那么不断重复以上步骤吧,最后这个数组有序了!而且是从小到大,这下你知道为什么我们要让让父节点的元素的值大于它的左右儿子结点的值了吧。
代码:
public class Sort<AnyType extends Comparable<? super AnyType>> {
public static void main(String[] args) {
Sort<Integer> sort= new Sort<Integer>();
Integer[] a={3,9,0,1,4};
sort.heapSort(a);
for(int i=0;i<a.length;i++)
System.out.println(a[i]);
}
//堆排序
/**
* Internal method for heapsort.
* @param i the index of an item in the heap.
* @return the index of the left child.
*/
public int leftChild(int i){
return 2*i+1;
}
/**
* Internal method for heapsort that is used in deleteMax and buildHeap.
* @param a an array of Comparable items.
* @index i the position from which to percolate down.
* @int n the logical size of the binary heap.
*/
public void percDown(AnyType[] a,int i,int n){
int child;
AnyType tmp= a[ i ];
for(; leftChild( i ) < n; i = child )
{
child = leftChild( i );
if(child != n - 1 && a[ child ].compareTo( a[ child + 1 ] ) < 0 )
child++;
if( tmp.compareTo( a[ child ] ) < 0 )
a[ i ] = a[ child ];
else
break;
}
a[ i ] = tmp;
}
public void heapSort(AnyType[] a){
for(int i=a.length/2-1;i>=0;i--)
percDown(a, i, a.length);
for(int i=a.length-1;i>0;i--)
{
AnyType s=a[0];
a[0]=a[i];
a[i]=s;
percDown(a, 0, i);
}
}
}