堆排序是利用堆这种数据结构而设计的一种排序算法。
既然是利用堆进行排序,我们首先了解什么是完全二叉树?什么是堆?
(1)完全二叉树是一种树型结构,每一个结点的子节点数不超过2个。下图1、2、3均为完全二叉树,图4不是完全二叉树:
(2)堆是具有以下性质的完全二叉树:
每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆(最大堆);
每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆(最小堆)。如下图:
树型结构的操作很复杂,一般用将其储存到数组中进行操作。那怎么把树型结构存储到数组中呢?
(1)从树根(编号为0)开始,按照从上到下,从左到右的顺序进行编号,如上图红色编号;
(2)数组arr的下标即结点的编号,将结点存放在数组对应下标的位置上。
图1(大顶堆)对应的逻辑数组如下: 图2(小顶堆)对应的逻辑数组如下:
这样就可以把堆的性质表达出来:
结点i的左子结点为2i+1,右子结点为2i+2。
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
接下来,我们了解什么是堆排序?
堆排序的基本思想:(以大顶堆为例)
1、将待排序序列(R1,R2…Rn)建立成一棵完全二叉树,并将其构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
2、将堆顶元素R1(序列的最大值)。与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,…Rn-1)和新的有序区(Rn)。(此时的Rn为最大值)
3、由于交换后新的堆顶R[1]可能违反堆的性质(不能保证堆顶的值比它的左右子节点大),因此需要对当前无序区(R1,R2,…Rn-1)调整为新的大顶堆,然后再次将R[1]与无序区最后一个元素Rn-1交换,得到新的无序区(R1,R2…Rn-2)和新的有序区(Rn-1,Rn)。(此时的Rn-1为第二大的值)
4、不断重复步骤(3)直到有序区的元素个数为n-1(无序区的元素只有1个),则整个排序过程完成,得到一个升序的序列。
(用小顶堆实现降序排序的过程类似,这里就不再讲述)
为便于学者理解,举例说明:假定对序列 5 6 4 1 7进行降序排列(采用大顶堆)
第一步:
1、将序列5 6 4 1 7建立成一棵完全二叉树。
2、构造初始大顶堆
具体过程如下:
(1)无序区的最后一个非叶子结点是6,以6为堆顶的堆不是大顶堆。取6和它的左子结点1,以及它的右子结点7中值最大的结点7与结点6进行交换。调整后,以6为堆顶的堆为大顶堆。如下图所示:
(2)找到6前面的一个非叶结点5,以5为堆顶的堆不是大顶堆。取5和它的左子结点7,以及它的右子结点4中值最大的结点7与结点5进行交换。如下图所示:
(3)由于7和5交换后,导致{5,1,6}这个堆不满足大顶堆的结构。取5和它的左子结点1,以及它的右子结点6中值最大的结点6与结点5进行交换。如下图:
到此,我们就将无序序列{7,6,4,1,5}构造成一个大顶堆。将堆顶元素7与堆中最后一个元素5交换,如图所示:
此时得到新的无序区(5,6,4,1)和新的有序区(7),有序区用红色的背景标注。
第二步:把无序区(5,6,4,1)构造成对应的大顶堆,为了更好的展示,把有序区用矩形圈起来,代表它不参与排序。
从当前无序区{5,6,4,1}中的堆顶节点5开始,取5和它的左子结点6,以及它的右子结点4中值最大的结点6与结点5进行交换。如图所示:
到此,我们就将无序序列{6,5,4,1}构造成一个大顶堆,将堆顶元素6与堆中最后一个元素1交换,如图所示:
此时得到新的无序区(1,5,4)和新的有序区(6,7),有序区用红色的背景标注。
第三步:调整无序区(1,5,4),构造对应的大顶堆,为了更好的展示,把有序去用矩形圈起来,代表它不参与排序。
(1)从当前无序区{1,5,4}的堆顶元素1开始,取1和它的左子结点5,以及它的右子结点4中值最大的结点5与结点1进行交换。如图所示:
到此,我们就将无序序列{5,1,4}构造成一个大顶堆,将堆顶元素5与堆中最后一个元素4交换,如图所示:
此时得到新的无序区(4,1)和新的有序区(5,6,7),有序区用红色的背景标注。
第四步:调整无序区(4,1),构造对应的大顶堆,为了更好的展示,把有序去用矩形圈起来,代表它不参与排序。由于以4为堆顶的{4,1}满足大堆的结构,不需要调整。
此时把堆顶元素4和堆中最后一个元素1进行交换,如图所示
此时得到新的无序区(1)和新的有序区(5,6,7,9),有序区用红色的背景标注。
第五步:此时的无序区只有1个数(有序区有4个),终止堆排序。
最终得到的升序序列为:1 4 5 6 7
堆排序中最关键的是构建大顶堆,我们来梳理一下构建的过程:
1、构建原则:保证每个小堆都是大顶堆,那么整个堆就是大顶堆。
2、构建顺序:从最后一个非叶结点开始,下一个非叶结点编号为当前非叶结点编号减1。(相当于从最后一个非叶结点所在数组的位置往左到数组的第一个位置)
3、构建过程:
(1)每次调整,保证以当前结点为堆顶的小堆满足大顶堆的结构;调整之后还需检查这次调整是否导致以该结点的左子结点(或者右子结点)为堆顶的小堆是否满足大顶堆结构,若不满足就需要对它们进行相应的调整;调整后,再检查左子节点(或右子结点)的子节点的小堆是否满足大顶堆结构,若不满足就调整。就这样逐层检查下去,直到叶结点为止!到此,以该结点为堆顶的堆就满足大顶堆的结构。这个过程可以用递归实现。
…
(2)对每一个非叶结点都执行步骤(1)的操作,保证以每一个非叶结点为堆顶的堆都满足大顶堆的结构。这样,整个堆就是一个大顶堆!
到此,是不是觉得堆排序也不是想象中的那么难了吧!!!
但是,在用代码实现之前,我们需要弄清楚以下关键点:
1、如何找到待排序列的最后一个非叶结点?
根据以下两点:
(1)数组的第一个元素是堆顶元素,最后一个元素是堆的最后一个叶子结点。
(2)堆结点i的左子结点为2i+1,右子结点为2i+2;(i从0开始的)
得出:最后一个非叶结点的编号不会超过size/2-1(size为无序序列的长度)。
因此,从编号为size/2的结点开始,如果该结点是叶子结点,就不需要调整(单个叶子结点是一种特殊的堆,它没有左右子结点);如果该结点是非叶子结点,根据情况调整。
2、判断结点i是否是叶子结点的方法:
判断它的左子结点2i+1或右子结点2i+2是否存在,即判断2i+1<=size和2i+2<=size是否成立。
3、无序区长度size的变化过程
每当堆顶元素与最后一个元素交换之后,堆顶元素“下沉“,size减1。
突破这些关键点后,我们就可以写程序实现堆排序!!!
)
堆排序算法分析:
(1)时间复杂度为O(NlogN)
(2)不稳定排序算法:因为相同的数字有可能在不同的小堆里,由于我们是递归到每一个小堆进行构造大顶堆的,所以相同数字的位置的先后顺序可能会发生变化。
最后,附上堆排序的代码:`
#include <iostream>
#include<algorithm>
using namespace std;
void HeapAdjust(int *a,int i,int size) { //调整堆,参数为:数组、开始结点编号、数组的长度
int lchild=2*i; //i的左子节点
int rchild=2*i+1; //i的右子节点
int max=i; //临时变量
if(i<=size/2) { //如果结点i是非叶结点
if(lchild<=size&&a[lchild]>a[max]) {//如果i的左子结点存在,且左子节点值比i的值大
max=lchild;
}
if(rchild<=size&&a[rchild]>a[max]) {//如果i的右子结点存在,且右子节点值比i的值大
max=rchild;
}
if(max!=i) { //如果结点i不是值最大的结点,将结点i的值和值最大的结点交换;否则,就不用调整
swap(a[i],a[max]);
HeapAdjust(a,max,size); //递归调整,避免调整之后以max为根的子树不是堆
}
}
}
void BuildHeap(int *a,int size) { //建立堆
int i;
for(i=size/2-1; i>=0; i--) { //非叶节点最大序号值为size/2
HeapAdjust(a,i,size); //从当前结点开始调整
}
}
void HeapSort(int *a,int size) { //堆排序
int i;
BuildHeap(a,size); //构建堆
for(i=size-1; i>=0; i--) {
swap(a[0],a[i]); //交换堆顶和最后一个元素,即每次将剩余元素中的最大者放到最后面
HeapAdjust(a,0,i-1); //重新调整堆顶节点成为大顶堆,此时新的堆长度位原来的长度减1
}
}
int main() {
int a[100];
int size;
scanf("%d",&size);//输入待排序列的长度size
for(int i=0; i<size; i++) //输入待排数字序列
scanf("%d",&a[i]);
HeapSort(a,size); //堆排序
for(int i=0; i<size; i++) //输出排序后的序列
printf("%d ",a[i]);
printf("\n");
return 0;
}
还可以学学其他排序算法:
sort函数排序 https://blog.youkuaiyun.com/weixin_43956598/article/details/90241551
冒泡排序 https://blog.youkuaiyun.com/weixin_43956598/article/details/90176251
选择排序 https://blog.youkuaiyun.com/weixin_43956598/article/details/90178197
插入排序 https://blog.youkuaiyun.com/weixin_43956598/article/details/90181567
快速排序 https://blog.youkuaiyun.com/weixin_43956598/article/details/90215135
希尔排序 https://blog.youkuaiyun.com/weixin_43956598/articledetails/90234480
为了便于道友们向我咨询问题,特意开设了一个免费的知识星球——CaptianXue,星球提供学习、理财、生活、职场等各类文章和免费答疑!!