前段时间写过一个堆排序的小程序,现在来整理一下。堆排序是一种基于堆积树这种数据结构的排序算法,堆分为大根堆和小根堆,是完全二叉树。
我们以升序排序为例,先简单说一下堆排序的原理吧,就是不断维护一个大根堆,让每个结点都不小于其子节点,此时根节点必然是所有数字中最大的。然后,我们只需要将第一个与最后一个数字交换位置,就可将最大的数移动到数组的最后,然后再将数组前n-1个元素继续维护成大根堆,再将根节点与第n-1个元素交换位置,此时第n-1与第n个元素都已经按照升序排好了。我们只需要不断重复上述步骤,直到所有元素按照升序排好。先不看具体是怎样维护的,简单画个示意图
接下来,我们再来看看具体的维护过程,先看一下完全二叉树的一些特点,假设一共有n个结点,那么分枝结点的个数就为n/2,我们只需要对这些分枝结点进行维护就可以了。某个结点的下标为i,如果根节点的下标为1,那么他的左右子结点的下标分别为2i和2i+1;如果根节点的下标为0,那么他的左右子结点2i+1和2i+2。
进行排序前,我们需要先构建大根堆,然后再不断对其进行维护,而构建大根堆,其实也可以看做是维护大根堆的过程,所以我们只需要将大根堆的维护写成一个函数,然后不断的调用就可以了。那么我们就来看一下是怎样调整、维护的。对某个结点进行维护,其实只需要将该结点以及他的左右结点中最大的元素放在该父结点的位置即可,但需要注意的是,一旦发生了交换,被交换的结点对应的子树可能不再满足大根堆了,所以需要对被交换的结点再进行一次维护,比如下图这种情况
以根节点下标为1为例,来看一下C++下的代码实现
void adjust(int a[],int i,int length)//参数分别为待排序的数组,要调整的结点的下标,还有数组中元素的个数
{
int left = i*2; //左子树结点序号
int right = i*2+1; //右子树结点序号
int max = i; //父节点与左右子节点中最大元素的序号
if (i <= length/2) //一半之后的都是叶子节点,不需要调整
{
if(a[left] > a[max] && left <= length)
max = left;
if(a[right] > a[max] && right <= length)
max = right;
if(max != i)
{
int temp = a[i];
a[i] = a[max];
a[max] = temp;
adjust(a,max,length); //将最大的交换之后,被换下来的数,可能比他的子节点小,因此要对这个结点进行维护
}
}
}
有了这个调整大根堆的函数之后,我们就可以开始进行堆排序了
首先,我们要先进行建堆,也即把待排序的数组构建成大根堆。从最后一个非叶子节点开始进行调整,直到调整到根节点,这样就完成了建堆的操作,代码实现非常简单,只需要再循环中不断调用刚才写好的调整函数即可,直接看代码。
for(int i = length/2;i >= 1 ;i--) //构造大根堆,从最后一个非叶子节点开始调整
{
adjust(Array,i,length);
}
其中,Array为待排序的数组,length为数组中元素的个数。
建好堆之后,就可以开始排序了,按照文章开头讲的思路,将根节点移动到第n位,然后维护前n-1个结点,再将根节点移到第n-1位,维护前n-2个结点,以此类推知道数组有序,来看代码实现。
for(int i = length;i >= 1;i--)
{
temp = Array[1];
Array[1] = Array[i];
Array[i] = temp;
adjust(Array,1,i-1); //将第一个元素与最后元素交换后,前n-1个元素,除刚交换的第一个外都符合大根堆,因此只需要调整第一个结点即可完成对堆的维护
}