本文摘抄总结于《算法》
我们可以把任意优先队列变成一种排序方法。而优先队列有多种实现方式,如无序数组实现的最小优先队列,当反复查找并删除最小(最大)元素时,相当于进行一次选择排序。而二叉堆实现的优先队列,当反复查找并删除最小(最大)元素时,则对应了一种新的排序方法——堆排序。
对于堆排序,分为两个阶段:
-
构造阶段,使原始数组堆有序
构造有两种方式,从左至右或从右至左。下面描述这两种方法的实现原理和优劣。从左至右,用一个指针从左至右扫描整个数组,扫描过程中,用swim()保证指针左侧数组中所有元素将都将按照堆有序的完全树的顺序排列,即左侧已经是一个优先队列了,而右侧的元素将不断的插入到这个优先队列中,然后再上浮,即调用swim()。
从右至左 ,该方法使用sink()函数从右至左构造子堆,比从左至右更加聪明高效。原因是,把原始数组想象成一个堆的有序状态被破坏的完全二叉树,树的叶子结点可以看做是一个个大小为1的堆有序的子树,也可称为子堆,如果一个结点的两个子节点已经是堆了,那么在该结点上调用sink()进行下沉可以将其变成一个堆,由此我们可以递归的建立起堆的秩序。且由于叶子结点占树结点总数的一半,它们本身已经是堆有序了,扫描时可以跳过它们,即跳过大小为1的子堆,不断自底向上用sink()构造堆,直到到达位置1. 扫描结束。通过这个过程可以看到,这种方式使扫描的规模减少了接近一半。
通过代码对比查看:
从左至右构造堆:
//exch,less函数,见名知意,不编写了
void constructHeap(int[] a, int len)
{
for(int i = 1; i < len; i++)
{
swim(a,i);
}
}
void swim(int[] a, int k)
{
while(k > 1 && less(k, k/2))
{
exch(a, k, k/2);
k = k/2;
}
}
从右至左构造堆:
void constructHeap(int[] a, int len)
{
for(int k = n/2; k >= 1; k--)
{
sink(a, k ,len);
}
}
void sink(int[] a, int k, int len)
{
while(2k <= N)
{
int j = 2k;
if(j < len && less(j, j+1)) j++;
if(!less(k, j)) break;
exch(a, k, j);
k = j;
}
}
- 下沉排序阶段
下沉排序,即将堆的最大元素删除,然后放入堆缩小后空出的位置。之所以会空出位置,是因为将堆中最大元素删除后,堆的最末尾元素将被替换到最大元素所在位置,那么最末尾元素所在位置就空了出来。
//len为结点个数,len+1才为数组大小,因为[0]位置没存储元素
while(len > 1)
{
exch(a, 1, len--);
sink(a, 1, len);
}
堆排序最终代码:
void sort(int[] a, int len)
{
for(int k = n/2; k >= 1; k--)
{
sink(a, k ,len);
}
while(len > 1)
{
exch(a, 1, len--);
sink(a, 1, len);
}
}