简述
堆排序(英语:Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
所以在了解堆排序之前,就不得不了解一下二叉树
在计算机科学中,二叉树(英语:Binary tree)是每个节点最多只有两个分支(即不存在分支度大于2的节点)的树结构。通常分支被称作“左子树”(left subtree)或“右子树”(right subtree)。二叉树的分支具有左右次序,不能随意颠倒。
二叉树是一个有根树,并且每个节点最多有2个子节点。非空的二叉树,若树叶总数为 n0,分支度为2的总数为 n2,则 n0 = n2 + 1。
一棵深度为k,且有2^k-1个节点的二叉树,称为满二叉树(如图b)。这种树的特点是每一层上的节点数都是最大节点数。而在一棵二叉树中,除最后一层外,若其余层都是满的,并且最后一层或者是满的,或者是在右边缺少连续若干节点,则此二叉树为完全二叉树(如图a)。具有n个节点的完全二叉树的深度为log2(n+1)。深度为k的完全二叉树,至少有2k-1个节点,至多有2k-1个节点。
与普通树不同,普通树的节点个数至少为1,而二叉树的节点个数可以为0;普通树节点的最大分支度没有限制,而二叉树节点的最大分支度为2;普通树的节点无左、右次序之分,而二叉树的节点有左、右次序之分。
二叉树通常作为数据结构应用,典型用法是对节点定义一个标记函数,将一些值与每个节点相关系。这样标记的二叉树就可以实现二叉搜索树和二叉堆,并应用于高效率的搜索和排序。
那么在了解二叉树后,我们如何利用完全二叉树来建立一个堆呢?
我们首先来看一下堆的定义,堆(英语:Heap)是计算机中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。在队列中,调度程序反复提取队列中第一个作业并运行,因为实际情况中某些时间较短的任务将等待很长时间才能结束,或者某些不短小,但具有重要性的作业,同样应当具有优先权。堆即为解决此类问题设计的一种数据结构。
如上图,这就是一组大根堆,及根结点元素为最大元素,并且不只是这样哦,有没有发现所有的父结点都要比子结点要大(圆圈里面的数是值,圆圈上面的数是结点编号),所以我们把符合这样特点的完全二叉树称为大根堆,类似的我们可以得出小根堆的定义(其实细心的同学能够发现在二叉树中的两个例子就是小根堆哦)。
那么这样一个堆要怎么用数组储存呢?我们通过观察结点编号容易发现,每一个父结点的左子结点的编号总是父结点的二倍,右子结点总是二倍加一,所以我们就可以通过这一特点将一颗完全二叉树用数组进行存储了(如图)。
那么讲了那么多,如何实现一个大根堆(或小根堆)呢,我们可以通过堆的下移操作来进行实现。
即类似于这种操作,下面是代码实现(代码以小根堆为例,当时敲的是从小到大排序,懒得改了嘿嘿)
void siftdown(int m)
{
int t,flag=0;
while(2*m<=n&&flag==0){ //如果该结点有左子结点,那么比较是否需要下移;
if(h[m]>h[2*m]){
t=2*m;
}else{
t=m;
}
if(2*m+1<=n){ //如果该结点有右子结点,比较一下左右子结点大小,选择更小的与之交换;
if(h[t]>h[2*m+1]){
t=2*m+1;
}
}
if(t==m){
flag=1; //此时没有发生交换,说明该元素已经满足堆,跳出循环;
}else{
swap(m,t);
m=t;
}
}
return;
}
void creat()
{
int i,j,x,y;
for(i=1;i<=n;i++){
cin>>h[i];
}
cout<<endl;
for(i=n/2;i>=1;i--){ //这里只需要从第n/2个元素进行下移,直到第一个元素即可
siftdown(i);
}
for(i=1,j=1,x=1,y=1;i<=n;i++,j++){ // 输出一下已建好的小根堆
cout<<h[i]<<" ";
if(j%y==0){
cout<<endl;
x*=2;
y+=x;
}
}
cout<<endl<<endl;
return;
}
堆排序实现
终于到正题啦~所以现在我们就可以利用完全二叉树来进行堆排序了。首先来一张堆排序的模拟图
堆排序原理
所谓的堆排序,其实就是每次将最大元素(最小元素)从大根(小根)堆顶提取出来,然后将堆的最后一个元素移至堆顶,继续进行大根堆(小根堆)的维护,直到堆内元素为空,排序停止,依次输出从堆顶取出的元素就好了
#include<iostream>
using namespace std;
int h[101];
int n;
void swap(int a,int b)
{
int x;
x=h[a];
h[a]=h[b];
h[b]=x;
return;
}
void siftdown(int m)
{
int t,flag=0;
while(2*m<=n&&flag==0){ //如果该结点有左子结点,那么比较是否需要下移;
if(h[m]>h[2*m]){
t=2*m;
}else{
t=m;
}
if(2*m+1<=n){ //如果该结点有右子结点,比较一下左右子结点大小,选择更小的与之交换;
if(h[t]>h[2*m+1]){
t=2*m+1;
}
}
if(t==m){ //此时没有发生交换,说明该元素已经满足堆,跳出循环;
flag=1;
}else{
swap(m,t);
m=t;
}
}
return;
}
void creat()
{
int i,j,x,y;
for(i=1;i<=n;i++){
cin>>h[i];
}
cout<<endl;
for(i=n/2;i>=1;i--){ //这里只需要从第n/2个元素进行下移,直到第一个元素即可
siftdown(i);
}
for(i=1,j=1,x=1,y=1;i<=n;i++,j++){ // 输出一下已建好的小根堆
cout<<h[i]<<" ";
if(j%y==0){
cout<<endl;
x*=2;
y+=x;
}
}
cout<<endl<<endl;
return;
}
int deletemax() //移除堆顶元素,保存到 p[]数组中
{
int t;
t=h[1];
h[1]=h[n];
n--; //记得将堆的元素个数减一哦
siftdown(1);
return t;
}
int main()
{
cin>>n;
int p[101],i,num;
num=n;
creat();
for(i=1;i<=num;i++){
p[i]=deletemax();
}
for(i=1;i<=num;i++){ // 最后 p[]数组中就是已经排好序的元素了
cout<<p[i]<<" ";
}
return 0;
}