首先看一下堆的定义:
对于n个元素的序列{k1,k2,k3,……,kn},当且仅当满足下列关系时,称之为堆:
K(i) <= K(2*i) && K(i) <= K(2*i+1) 此时的堆为小顶堆
K(i) >= K(2*i) && K(i) >= K(2*i+1) 此时的堆为大顶堆
(i = 1,2,……,n/2(下取整))
注意:堆得存储是用一维数组来存储的。
若将堆对应的序列看成是一个完全二叉树,则堆得含义表明:
完全二叉树中所有非终端结点的值均不大于(或不小于)其左右孩子结点的值。
因此,若序列{K1,K2,……,Kn} 是大顶堆,则堆顶元素必为序列中n个元素的最大值;反之,若序列是小顶堆,则堆顶元素必为序列中n个元素的最小值。
堆排序就是利用的这个性质。
堆排序的过程如下:
假设要从小到大排序,我们构建一个大顶堆,则堆顶元素是最大值。将堆顶元素和最后一个元素互换,则最后一个元素变成了n个元素中的最大值。之后再将剩下的 n-1 个元素调整成为大顶堆,将堆顶元素和第n-1 个元素互换,则第n-1 个元素变成了n个元素中的次大值……循环这个过程,不断调整堆,最后得到一个有序的序列。
在上面堆排序的过程中,有两个问题需要解决:
(1)如何将一个初始的序列构建成一个大顶堆?
(2)再得到最大元素后,剩下的n-1个元素如何再次调整成为一个大顶堆?
实际上,初始序列构建大顶堆也是一个不断调整堆得过程。因此,只要解决第二个问题就可以。
下图是一个大顶堆:
当把堆顶元素20和最后一个元素互换之后,最后一个元素变成了序列中的最大值。如下图:
但是,此时堆顶元素违反了大顶堆的性质,堆顶元素的左右孩子仍旧满足大顶堆的性质。因此,此时需要对堆进行调整。因为左子树的值大于右子树的值,所以将3和17互换,如下图:
此时,左子树又违反了大顶堆得性质,所以需要调整左子树,如下图:
至此,一次调整完毕,堆顶元素成为了次大元素。
实际上,调整堆就是这样一个不断筛选比较的过程,不断的和左右子树比较,一直到不需要交换为止。
将一个无序序列构建成一个大顶堆的过程就是一个反复筛选的过程。将此序列看成是一个完全二叉树,则最后一个非叶子节点是第 n/2(下取整)个元素,因此,筛选只需从第 n/2(下取整)个元素开始。
假设有序列:{49,38,65,97,76,13,27},初始二叉树是:
从第3个元素,也就是65开始调整堆,65大于左右子树的值,因此不需要调整。
然后是第2个元素,也就是从38开始调整堆,38和左右子树比较,将97和38互换,调整后如下图:
然后是第1个元素,也就是从49开始调整堆,49和左右子树比较,将97和49互换,互换之后,因为49破坏了左子树大顶堆的性质,因此需要继续调整,将49和左右子树比较,然后将49和76互换,调整过程如下图:
至此,将一个无序的序列调整成为了一个大顶堆。
同理,堆排序也分为两个过程:(1)将初始化序列调整成为一个大顶堆(2)用最后一个元素和堆顶元素交换,然后不断调整剩下的元素成为一个新的大顶堆。
代码如下:
const int N = 8;
int num[N] = {-1,49,38,65,97,76,13,27}; //从第一个元素开始存储
//调整堆的函数
void heapAdjust(int pos,int total){
int temp = num[pos];
for(int j = 2 * pos; j <= total; j *= 2){
if(j < total){ //说明还有右子树
if(num[j] < num[j + 1]){ //筛选出左右子树中较大的值
j += 1;
}
}
if(temp >= num[j]) //不需要再继续向下调整了
break;
num[pos] = num[j];
pos = j;
}
num[pos] = temp;
}
void heapSort(){
//首先将数组构建成一个大顶堆
for(int i = (N-1) / 2;i >=1;--i){
heapAdjust(i,N - 1);
}
//开始堆排序
for(int i = N-1; i > 1; --i){
int temp = num[i]; //交换第一个元素和最后一个元素
num[i] = num[1];
num[1] = temp;
heapAdjust(1,i - 1); //交换完之后,重新调整堆
}
}