数据结构——堆以及堆排序
堆
上一篇介绍了二叉树,这一片我们来看堆。首先什么是堆?其实堆也是一种二叉树,只不过是一种特殊的二叉树。堆分为大根堆和小根堆。大根堆就是根节点的值是最大的,且他的两个孩子节点也是一个大根堆,以此类推。比如下图
根节点是100,比他的两个孩子90和85大,而他的两个孩子作为根节点72同样比他们的孩子值大,这样的结构我们称之为大根堆,并且这个堆的根节点的值是这个堆里面所有值的最大值,正是这一特点,为堆排序埋下伏笔。同样的小根堆也是类似,只不过是根节点为最小值,而他的两个孩子作为根节点的话也是一个小根堆。
大根堆的创建
首先要明确一点,堆并不是我们直接写好的,是需要创建堆,即根据一种算法,把不是堆的一组数据变成大根堆或者小根堆,还是上述的图我们可以知道这个堆的顺序是100,90,85,72,71,65,79,52,56,45,58。当然,这个堆的顺序存在多个,只要满足根节点和孩子节点之间的关系就行,所以堆不是唯一的。
接下来,我们看一下创建一个大根堆需要哪些数据类型以及函数接口
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef int hpdatatype;//数据类型
typedef struct heap //堆这个结构体
{
hpdatatype* array;//堆数据
int size; //堆的大小
int capacity;//堆的容量
}heap;
//交换函数
void swap(hpdatatype* array,int a,int b)
{
int tmp = array[a];
array[a] = array[b];
array[b] = tmp;
}
void shiftdownbig(hpdatatype* array,int size,int parent);//大堆向下调整
void shiftupbig(hpdatatype* array,int child)//大堆向上调整
void heapcreat(heap* hp,hpdatatype* arry,int size) //创建堆
void heappush(heap* hp,hpdatatype data)//向堆中插入一个数据
void heappopbig(heap* hp)//堆中删除最值
hpdatatype heaptop(heap* hp)//获取堆顶元素
int heapisempty(heap* hp)//判断堆是否为空
在创建堆之前,我们需要了解一下什么是大堆向下调整。
顾名思义,每一次向下调整成一个大堆,那么我们就需要找到这组数据的最后一个非叶子结点,来调整。我们默认叶子节点是有序的,则需要从这个叶子节点的父节点开始向下判断,如果不是大堆,则需要与两个孩子的最大值进行交换,依次向上判断,直至变成一个大根堆。
创建堆
hp是指向这个堆的根节点的指针,hpdatatype* array则是给你的那组数据,你要将它变成一个大根堆,即要将这个数据复制到hp所指向的那个内存空间
void heapcreat(heap* hp,hpdatatype* array,int size)
{
hp->array = (hpdatatype*)malloc(sizeof(hpdatatype)*size);//给这个堆空间初始化
hp->array = memcpy(hp->array,array,sizeof(hpdatatype)*size);//将所需要创建成堆的数据复制到该空间
hp->size = size;//当前大小为size
hp->capacity = size;//初始堆容量为size
//现在需要找到对后一个非叶子结点
for(int parent = (size-2)/2;parent>=0;parent--)
//这里需要大家记住一个公式,(size-2)/2就是该二叉树的最后一个非叶子节点,从这结点开始依次向上判断,所以这里需要一个循环,直到判断到根节点是否满足大根堆的要求为止
{
//循环判断并调整大根堆
shiftdownbig(hp,hp->size,parent)
}
}
大堆向下调整
上面已经说过什么是大堆向下调整,这里我们仔细的来看。如下图
很明显这不是一个大根堆,那么我们是如何调整呢?
首先我们找到最后一个非叶子结点,发现是90,那么从这里向下判断是否为一个大根堆呢?显而易见这是一个大根堆,因为90大于45和71,像上一个走,到了56,此时发现这不是一个大根堆,因为72是大于56的,所以这里调整一下,让72与56交换得下图
此时72这个结点向下看为一个大根堆,调整好后继续向上走,走到85,发现该结点向下看去是一个大根堆,则继续走,走到58,发现这不是一个大根堆,则从两个孩子中选出最大的和58交换,显然90是最大的,交换得下图
可是这里发现,虽然90的确比他的两个孩子大,但是一交换后,58这个结点又不是他的孩子中最大的,这里怎么办呢?很简单,当90这个结点交换后,我们只需要继续向下走,判断它的两个孩子是否还满足大根堆的要求,如果不满足,则继续交换。交换后就得到下图
到这里我们发现所有结点都满足大根堆的条件了,也相当于这个大根堆创建好了
代码实现如下
void shiftdownbig(hpdatatype* arry,int size,int parent)
{
//当找到最后一个非叶子结点parent后,需要找到他的左孩子
int child = parent*2+1;
//找到左孩子后,则开始循环向下判断
while(child<size) //如果当前孩子结点小于这个堆的大小则向下继续调整
{
//需要判断是否有右孩子且右孩子的值是否大于左孩子
if(child+1<size&&array[child+1]>array[child])//如果有右孩子,且右孩子值大于左孩子
child++;//则取右孩子
//当取到孩子结点中最大值后,需要和父结点比较大小,若大于则交换,小于则不交换
if(array[child]>array[parent])
{
//交换父结点与最大孩子结点的值
swap(array,parent,child);
//交换完后,有可能会出现上述图片中下面不是一个大根堆的情况
//所以我们只需要继续向下判断即可
parent = child; //让父结点等于当前孩子节点
child = parent*2+1;//寻找父结点的左孩子
}
else
break;//不需要调整跳出循环
}
}
大堆向上调整
如果向已经建好的一个大根堆中插入一个数据,则需要通过大根向上调整来重新调整好一个新的堆
与大堆向下调整思路类似,但值得注意的是大堆向上调整是从最后一个孩子结点判断是否满足大根堆的条件,依次类推
//参数传入的是孩子
void shiftupbig(hpdatatype* arry,int child)
{
int parent = (child-1)/2;//找到当前孩子结点的父结点
//只要孩子节点>0则继续调整
while(child>0)
{
if(arry[child]>arry[parent])
{
swap(arry,child,parent);//交换当前父结点和最大孩子结点的值
child = parent;//将父结点置为孩子结点
parent = (child-1)/2;//寻找当前孩子结点的父结点
}
else
break;//不需要调整跳出循环
}
}
堆中插入数据
在已建好的堆中插入一个数据,最好办法就是尾插,即插入到堆的最后一个为止,并且使size++,然后来一次大根堆向上调整即可
void heappush(heap* hp,hpdatatype data)
{
if(hp->size == hp->capacity)//若当前堆为满堆则需要扩容
{
hp->capacity*=2;//新的堆的大小为原来的2倍
hp->array = (hpdatatype*)realloc(hp->array,sizeof(hpdatatype)*hp->capacity);//扩容该堆
}
hp->array[hp->size++] = data;
//做一次向上调整,这里直接传入该堆的最后一个结点下标即可
shifupbig(hp,hp->size - 1);
}
堆中删除最值
我们创建好堆,删除什么值意义最大呢?很显然,删除堆中的最值意义最大,那么我们能直接删掉吗?如果直接删掉,那么这个堆便没有根节点,什么也不是了,所以我们需要换一种删除方法。我们可以将根节点与最后一个结点交换,再通过尾删,将最值删除后,同时走一遍向上调整构成一个新的堆,这时我们的删除就结束了。
void heappopbig(heap* hp)
{
//如果该堆为空则直接返回
if(hp->size == 0)
return ;
swap(array,0,hp->size-1);//交换根节点和最后一个结点
hp->size--;//尾删操作
shiftdownbig(array,hp->size,0);//从根节点开始做一次向下调整
}
获取堆顶元素
hpdatatype heaptop(heap* hp)
{
return hp->array[0];
}
判断堆是否为空
int heapisempty(heap* hp)
{
if(hp->size == 0)
return 1;//为空则返回1
return 0;//非空返回0
}
堆排序
上面已经说过创建堆后,那么堆排序便已经呼之欲出了。堆排序,即我们每次从大根堆或者小根堆中取出一个最后,做一次堆的调整,直到该堆为空后,就会得到一组有序的数值,即排序成功。也不难想到通过尾删的方法来实现堆排序,当然这里的尾删是假尾删。代码如下
首先要想堆排序,我们先得建堆,具体怎么建堆,上面已经说过。
void heapsort(int* array,int size)
{
//建堆
for(int parent = (size-2)/2;parent>=0;parent--)
{
shiftdownbig(array,size,parent);
}
//排序
int end = size - 1;//取最后一个元素
while(end>0)
{
swap(array,0,end);//交换根结点和最后一个结点
end--;//长度假-1
shiftdownbig(array,end,0);//调整当前为止到根结点成一个新的堆
}
//该循环每结束一次,都会将最值放到最后,依次形成一个有序序列
}
值得注意的是,如果想要升序排序则需要建立大根堆,如果是降序排列则需要建立小根堆,小根堆的建立与大根堆建立类似,只是每个根结点都是最小值。大家可以下去自己写。
今天的分享就到这里,如果大家对创建堆以及堆排序有什么不懂的地方,欢迎留言讨论
上一篇: 堆以及php实现堆排序
下一篇: Java版-数据结构-队列(循环队列)