前言:
我们知道研究一个数据结构自然要研究它的逻辑结构和物理结构,其中逻辑结构是我们真正关注的,因为一个数据结构是否优秀,它的各类操作的核心思想都是由逻辑结构决定的,而物理结构仅仅是去适应逻辑结构。而堆排序用到了堆这个数据结构,所以我们就先来研究一下堆的这两个结构。
堆的逻辑结构:
1、树结构的一种变形,是完全二叉树。树结构同样也是逻辑结构下的一种数据结构
2、满足“每个节点都大于等于其父节点(根节点除外)”的堆称为最小堆
3、满足“每个节点都小于等于其父节点(根节点除外)”的堆称为最大堆
具体图见下:
左边为大根堆,右边为小根堆
堆的物理结构:
物理结构探究的就是如何把这个逻辑结构在真实的计算机内存空间中存储下来。这个存储必须要能体现逻辑结构所有的一切逻辑特征。
常见的物理结构就是链表(随机存储)、数组(顺序存储)。对于堆和树,我们采取的物理结构往往是数组存储。然后通过下标去体现树的逻辑结构。
例如上面的小根堆和大根堆的对应的数组存储方式如下:
总的来说就是利用树的层次遍历的顺序一一把结点放到数组当中 。
堆排序用到树的一些性质(树堆相同):
1、令x为根节点下标,则x*2为其左子树下标,x*2+1为其右子树下标
2、最后一排叶子节点的个数为总节点数的一半
3、存储从数组下标1开始,若是0开始上面两条性质会改变。
堆结构基础操作方法:
1、heap数组就是存储树结构各节点的数组
2、size就是整个数组的长度,也是堆的节点数
3、当一个堆因加入、修改、删除等操作出现一个节点发生改变,小于原本的节点时,整体堆可能不再满足其堆的性质,这个时候就要通过down操作,把该节点通过逐级下沉(若是小根堆则要逐级上浮,用up函数),放到其应该在的位置,使整体结构仍满足堆的性质。
所以在堆的问题中最难的地方在于down和up两个操作
down操作:
down顾名思义就是把一个元素下沉到它应该在的地方。对应环境:小根堆中,一个元素值变大了,就要考虑下沉。或者大根堆中,一个元素值变小了,就要考虑下沉。
具体的步骤如下:(小根堆为例子)
1、定义一个变量t表示待调整数x应该在的位置,开始时赋值为x的初始位置,即t=x。
2、先将x与其左子树比较大小(即num[t] 比较 num[x/2]),如果比左子树要大,则将t赋值为左子树的位置(即t=x/2)。表示目前x应该在左子树这个地方。
3、再将t的值与右子树比较(即num[t] 比较 num[x/2+1]),如果目前t位置(x目前应该在的位置)的值大于右子树的值,那么说明x有更优的解,故将t=x/2+1。
(2,3步骤的本质就是找到x左右子树中较小的那一个,因为x就是要和较小值交换)
4、最后如果t和x不相等,则交换两者的位置。
图解如下:(小根堆为例子)
说明:1、图中27先于15、19中较小的15交换,完成第一轮下沉
2、再与18、28中的18交换,完成第二轮下沉。
3、之后,再不停重复这两个操作。直到27比下面两者都小或到达叶子结点为止。
up操作
up操作顾名思义就是把一个元素不停上浮到它应该在的地方。对应环境是:小根堆中,一个根节点元素值比原本要小了,就要考虑是否需要上浮。或者大根堆中,一个根节点变大了,就要考虑上浮
具体步骤如下:(选取小根堆为例子,大根堆的话要相反)
1、与根节点比较,如果小于根节点,则上浮。
2、再与下一层根节点比较,如果仍然小于则继续上浮
3、直到到达最上方,或不再小于根节点为止
图解如下:(大根堆为例子,让大家两种情况都看看)
堆排序:
了解完堆的基本操作就让我们来看看,这个利用堆结构特点来操作的排序算法。
对于小根堆:
1、将元素随意放到一个堆中,再对堆进行初始化,使其从杂乱状态变为小根堆
2、取出其顶部元素,自然就是整个堆中的最小值。
3、然后再把最尾部的元素上移到根节点,这个尾部元素自然很大,所以在对这个元素执行down操作,从而使堆重新变为小根堆的结构。
4、不停重复1 2操作直到全部取出元素。自然形成由小到大的序列
对于大根堆:
1、将元素随意放到一个堆中,再对堆进行初始化,使其从杂乱状态变为大根堆
2、取出其顶部元素,自然就是整个堆中的最大值。
3、然后再把最尾部的元素上移到根节点,这个尾部元素自然很小,所以在对这个元素执行down操作,从而使堆重新变为大根堆的结构。
4、不停重复1 2操作直到全部取出元素。自然形成由大到小的序列
注意:堆排序算法并没有用到up操作
图解堆排序:
上图就体现了从取出元素到调整新的有序堆的全过程 ,大家好好琢磨肯定能明白的!
画图不易,大家给个免费的赞呗。如果觉得写的不错,也可以按下可爱的五角星收藏
堆排序完整代码如下:
#include<iostream> using namespace std; const int n=100001; void down(int x,int a[],int num){ int k=x; if(x*2<=num&&a[x*2]<a[k])k=x*2; if(x*2+1<=num&&a[x*2+1]<a[k])k=x*2+1;//首先判断是否存在左右子树再进行判断,否则会造成非法空间访问 //交换并进入下一层递归 if(a[k]<a[x]){ swap(a[k],a[x]);//交换值 down(k,a,num);//下次从a[x]所在的新的位置继续向下down }//如果a【x】的值此时已经小于左右子树中的最小值,那么直接放置并且退出循环即可 } int main(){ int num,k;//num表示总共有多少待排序的数,k表示要输出前k个数 int a[n]; cin>>num>>k; for(int i=1;i<=num;i++){//从1开始,为了方便下标计算 cin>>a[i]; } //初始化小根堆。从小到大就是小根堆,反之就是大根堆.时间复杂度为O(n) for(int i=num/2;i>0;i--){ down(i,a,num); } //初始化后开始堆排序 for(int i=1;i<=k;i++){ cout<<a[1]<<" ";//这里第一次错了,debug40分钟 a[1]=a[num--]; down(1,a,num); } return 0; }