堆排序指利用堆这种数据结构的一种排序算法,是选择排序的一种。 如果是非降序排序,需要使用大根堆(是完全二叉树),即每个节点的值都不大于父节点的值。将二叉树的节点按照层级顺序放入数组,用长度为N+1的私有pq[]来表示一个大小为N的堆(为后面计数方便,不使用pq[0],堆元素放在pq[1]至pq[N]之间),根节点在位置1,它的子节点在位置2、3,以此类推。位置k的节点的两个子节点位置分别为2k和2k+1,它的父节点为k/2。
堆的有序化:
- 由下至上的堆有序化(上浮):如果堆的有序状态因为某个节点变得比它的父节点更大而被打破,就需要交换它和它的父节点来修复堆,以此类推,直到遇到一个更大的父节点。
private void swim(int k){
while(k > 1 && pq[k] > pq[k / 2]){//k位置的节点比父节点大则交换,注意k位置的节点不能是根节点
int temp = pq[k / 2];
pq[k / 2] = pq[k];
pq[k] = temp;
k /= 2;
}
}
- 由上至下的堆有序化(下沉):如果堆的有序状态因为某个结点变得比它的两个子节点或者其中之一更小而被打破,需要将它和它的两个子节点中的较大者交换来恢复堆,以此类推,直到它的子节点都比它更小或者达到了堆的底部。
private void sink(int k){
while(2 * k <= N){//k至少有子节点才能下沉
int j = 2 * k;//左子节点
if(j < N && pq[j] < pq[j + 1])
j++;//找到子节点中较大的那个
if(pq[k] >= pq[j])
break;//k所在节点不小于子节点时结束下沉
int temp = pq[k];
pq[k] = pq[j];
pq[j] = temp;
k = j;
}
}
堆排序分为两个阶段:
- 堆的构造阶段,将原始数组重新组织安排进一个堆中;
- 下沉排序阶段,从堆中按递减顺序取出所有元素并得到排序结果。
从左至右遍历数组,用swim()保证下标左侧的所有元素已经是一颗堆有序的完全树即可,更高效的办法是从右至左用sink()构造子堆。
时间复杂度为O(NlogN),不稳定的排序算法。
Java实现代码如下:
private static void exchange(int[] a, int i, int j){
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
private static void sink(int[] a, int k, int N){
while(2 * k <= N){
int i = 2 * k;
if(i < N && a[i] < a[i+1]){
i++;
}
if(a[k] >= a[i]){
break;
}
exchange(a, k, i);
k = i;
}
}
public static void sort(int[] a){
int N = a.length - 1;
for(int k = N / 2; k > 0; k--){
sink(a, k, N);//从最后一个子堆开始构造有序堆
}
while(N > 1){
exchange(a, 1, N--);//将最大的元素a[1]和a[N]交换
sink(a, 1, N);//修复堆:将交换上来的a[1]下沉
}
}
注意此处代码实现未对a[0]进行操作,可随意赋值一个,但不参与排序。