前言
堆排序算法平均时间复杂度近似为O(nlog2(n)),2为下标,与快速排序、希尔排序一样为不稳定算法。堆排序有大根堆小根堆的排序形式,这里讲大根堆形式,小根堆实现方式思路一样。小根堆排序的代码在大根堆代码的基础上,更改数的比较部分即可。
实现思路
大根堆小根堆
首先来了解两个概念,什么是大根堆
和小根堆
?
顾名思义,大根堆就是:所形成的二叉树,每一个双亲结点的数据域都大于孩子结点的数据域,例如下图,可以发现每一个双亲结点数据域都大于孩子数据域,所以叫“大根堆”。小根堆就是:所形成的二叉树,每一个双亲结点数据域都小于孩子结点的数据域,如下第二张图。
那如果这棵树只有一个结点到底是大根堆还是小根堆呢?
可以发现一个结点时(或叶子结点),其左右子树都为空,我们规定此时既可以认定它为大根堆,也可以认定它为小根堆。
实现过程
【堆排序大致分为两个过程】堆排序只对左右子树均为堆的二叉树进行排序
①堆排序形成初始堆;
②对数组内非叶子结点都进行一次堆排序,对每次排完序形成的大(小)根堆,此时形成的堆中,根结点数据域为当前序列最大(小)值。将根节点与底部最靠右的叶子结点进行数据域交换,即将数组中最后一个数据与数组第一个数据进行交换;
首先我们得到一串整型序列[9 4 2 6 1 8 3 5 7]
(这里存储数据均从下标1开始,方便后面的堆排序)
按顺序按层序放入树中,得到
此时的数组为
可以发现,这棵二叉树既不是大根堆也不是小根堆,这里我们需要先形成一个初始堆,可是要怎么形成堆呢?
形成初始堆
前面我们讲到,堆排序是对根结点左右子树均为堆的二叉树来排序的,按照这个规则,我们就必须满足当前作为根结点的左右子树为大根堆。显然,叶子结点是可以作为大根堆的,所以我们可以大胆构建,每次从数组下标为n/2
的结点一直往前考察至下标为0
的结点是否满足堆的条件。这里n
代表数组内元素个数。
在该图中,n/2=4
,所以从结点6
开始查看它的左右子树是否满足都小于 6 的情况。这里有个结论,按照层序从左到右标号的话,很容易得出序号为i
的双亲结点的左孩子对应的下标为2*i
,相应地,右孩子下标为2*i+1
,有这条规则,我们就能轻松找到当前结点的左右孩子结点。
好了,那么我们现在仅观察结点6
和它的左右子树能否形成一个大根堆,6 对应的下标i=4
,看图我们可以发现它右子树数据域 7是大于6 的,为了形成大根堆,我们需要将 7 跟 6 进行交换。细心的小伙伴肯定会问了,如果左右子树都大于双亲结点呢?其实这个时候我们只要选择左右子树较大的那一方跟双亲结点数据交换就行啦!
我们完成了第一次交换,此时树变为下图。然后我们再往前一个结点看,即结点2
,它的下标为i=3
,由图可以看出以2为根结点的二叉树依然不满足大根堆的条件,所以依然要进行交换,那么这里就遇到了上面所说的左右子树都大于双亲结点的情况啦,这个时候选择8 3
中最大的那个跟结点2
交换位置。
更新后的树为下图。同理我们再来观察以4
为根节点的子树。由于是自底向上来形成大根堆,可以发现 4 的左子树已经是堆了,满足堆排序的前提“二叉树根结点的左右子树均为堆”的条件,所以能够放心进行堆排序。但是这里需要进行两次交换,第一次是 4和7 交换,交换完后发现 5和6 依然比4大,所以再让 4和6 进行交换
交换后如图:
同理再观察结点9,发现 7 8均小于9 ,所以一个初始大根堆已经构建完成。
此时数组为
每轮循环将根结点放到数组最后一个位置
由初始大根堆,其根结点就为当前序列的最大值,所以我们就直接拿出来,把它放到数组尾部,就不需要再对它进行排序了,接着选出剩余序列中的最大值,以此类推,经过n-1
次循环,就能把整个序列排好序了。
那么我们来看初始大根堆进行交换后的树(由于9已经是当前序列最大值,这里我们不用再管结点9,所以图中删除了,接着就是需要在下面的这个图里找最大值,往后的循环也是这个过程)
存9的位置锁死,不再参与堆排序,数组为
继续堆排序,形成
根结点8和5交换后再删掉结点8,变成
再按此步骤一直进行堆排序,一直到整棵树只剩根结点结束。
交换+删除后👉
交换+删除后👉
。
。
。
。
。
。【最终数组 】
实现代码
//这里的排序用的是大根堆
#include <bits/stdc++.h>
using namespace std;
void sift(int *E, int left, int border) { //border为数组maxsize
int i = left, j = 2 * i; //i为当前结点,j为i的左孩子
int tmp = E[i]; //将当前结点临时保存
while (j <= border) {
if (j < border && E[j] < E[j + 1]) //比较当前结点的两个孩子大小,选大的来进行交换,较小的不用考虑
j++;
if (tmp < E[j]) { //如果孩子结点小于最初的结点
E[i] = E[j]; //孩子节点变为双亲结点
i = j; //向下搜索结点
j = 2 * i; //j依然为i的左孩子结点
} else
break;
}
E[i] = tmp; //将最初的结点存到最终数据域小于它的结点的位置上
}
void Heapsort(int *E, int n) {
for (int i = n / 2; i > 0; i--) //先生成一个初始堆
sift(E, i, n);
//输出
puts("初始堆");
for (int i = 1; i <= n; i++)
printf("%d ", E[i]);
puts("\n变换");
for (int i = n; i > 1; i--) { //接着每交换一次数据就要进行一次大根堆的调整
int t = E[1];
E[1] = E[i];
E[i] = t;
sift(E, 1, i - 1);
//输出
for (int j = 1; j <= n; j++)
printf("%d ", E[j]);
puts("");
}
}
int main() {
//数据输入
int n;
scanf("%d", &n);
int a[n + 1];
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
//堆排序
Heapsort(a, n);
}