一.相关概念
①什么是堆?
堆排序(英语:Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值总是小于等于(或者大于等于)它的父节点。
1.父结点的键值总是不小于它的两个子结点的键值的堆称之为"大顶堆"(简称"大堆")
2.父结点的键值总是不大于它的两个子结点的键值的堆称之为"小顶堆"(简称"小堆")
②建什么堆?
首先,你要确定经过堆排序后获得的有序序列是递增还是递减?
1.如果你要获得排序后的序列是递增,则需要把无序序列建为大堆
2.如果你要获得排序后的序列是递减,则需要把无序序列建为小堆
③怎么建堆?
1.建小堆
a.先将无序序列按顺序以层序建一棵完全二叉树
b.从最后一个非叶子结点开始从后往前调整堆
假如父结点的左右孩子结点均比父节点更大,则不必调整该结点,开始调整前一个结点
假如父结点的左右孩子结点中更小的结点比父结点更小,则与父结点交换位置,原父结点交换位置后,需继续检查当前位置是否也满足是小堆,如果满足则不必再调整,反之继续调整
c.就这样从后往前依次调整堆,直到调整到堆顶,一个小堆便建成
2.建大堆
建大堆与建小堆原理是一样的
a.先将无序序列按顺序以层序建一棵完全二叉树
b.从最后一个非叶子结点开始从后往前调整堆
假如父结点的左右孩子结点均比父结点更小,则不必调整该结点,开始调整前一个结点
假如父结点的左右孩子结点中更大的结点比父结点更大,则与父结点交换位置,父结点交换位置后,需继续检查当前位置是否也满足是小堆,如果满足则不必再调整,反之继续调整
c.就这样从后往前依次调整堆,直到调整到堆顶,一个大堆便建成
注意: 上述说的建完全二叉树并不是开辟新内存以二叉链形式存储二叉树,而是在原顺序表上,以顺序表的形式存储二叉树,并不需要开辟新的内存
通常我们排序后都想得到递增序列,所以我的算法也是建的大堆,建大堆图解如下:
④如何用堆排序?
上面我们初始建堆完成,接下来我们就要利用这个初始建好的堆实现排序,这里以大堆讲解(小堆也是同样的原理)
1.我们把堆顶元素和堆尾元素(也就是顺序表中首元素和尾元素)互换位置,然后把堆尾元素去除(也就是顺序表最后一个元素不会再进行调整)
2.执行1操作后,堆的元素减少了一个,而且不是大堆了,这个时候我们就要把堆从前往后重新调整为大堆
3.循环执行1,2操作,直至堆中只有一个元素,这时顺序表中的元素便是有序递增的
过程如图:
二.思路分析
1.要以代码实现算法,首先我们要知道这几个知识点:
假如完全二叉树是以顺序表作为存储结构(0单元不用,从索引1开始存储),那么如果一个父结点在顺序表中的位置序号为s,则其左孩子的序号为2s,右孩子的序号为2s+1;最后一个非叶子结点的序号为结点总数除以2取整([L.length/2])
2.有了上面这几个知识点我们就可以操作了,首先是初始建堆从最后一个非叶子结点开始(i=L.length/2),从后往前(i–)调整堆,直至调整到堆顶(堆顶结点也要完成调整,所以调整完成后i==0)
3.调整时,因为要调整的结点未必只需调整一下,可能位置需要多次调整才能确定,所以我们用辅助单元(零单元)暂存要调整的结点元素,到了最后的安放位置才赋值过去,这样可以减少元素移动次数,提高算法效率
4.然后用堆排序时,每次交换堆顶和堆尾,并去除一个堆尾,就把结点数量减一(i–),然后重新从前往后调整为大堆,也是和3同理,直至堆中只有一个元素(i ==1),顺序表中的元素便是有序递增
三.代码实现
注意:顺序表中零单元不用,从索引1开始存储元素
//堆排序(排列为递增序列)
void HeapSort(SqList &L)
{
int i,j,s,m; //i为建大堆的起点堆顶位置,j为孩子节点位置,s为正在进行建大堆的堆顶位置,m为堆尾位置
for(i=L.length/2;i>0;i--) //初始建大堆,从最后一个非叶子节点开始,从下至上为起点堆顶
{
s=i,m=L.length;
L.elem[0]=L.elem[s]; //辅助单元暂存父结点元素
for(j=2*s;j<=m;j*=2)
{
if(j<m&&L.elem[j]<L.elem[j+1]) //如果左右孩子均存在,并且右孩子大于左孩子
j++; //使j为右孩子位置序号,使j为元素更大的那个孩子的序号
if(L.elem[0]>=L.elem[j]) //如果父结点比左右孩子结点都大,不必做任何操作
break;
L.elem[s]=L.elem[j]; //大元素往上升
s=j; //继续往下检查是否满足大堆
}
L.elem[s]=L.elem[0]; //此时s为父结点元素的安放位置
}
for(i=L.length;i>1;i--) //输出大堆堆顶的元素(放在堆尾),堆尾元素放在堆顶,然后重新调整为大堆
{
L.elem[0]=L.elem[i]; //辅助单元暂存堆尾元素
L.elem[i]=L.elem[1]; //堆顶和堆底元素交换
L.elem[1]=L.elem[0];
s=1,m=i-1; //重新调整大堆
L.elem[0]=L.elem[s]; //辅助单元暂存父结点元素
for(j=2*s;j<=m;j*=2)
{
if(j<m&&L.elem[j]<L.elem[j+1]) //如果左右孩子均存在,并且右孩子大于左孩子
j++; //使j为右孩子位置序号
if(L.elem[0]>=L.elem[j]) //如果父结点比左右孩子结点都大,不必做任何操作
break;
L.elem[s]=L.elem[j]; //大元素往上升
s=j; //继续往下检查是否满足大堆
}
L.elem[s]=L.elem[0]; //此时s为父节点元素的安放位置
}
}
操作结果: