一、什么是堆
1.堆是一种特殊的完全二叉树,就像下边这棵树一样:
2.发现:
我们发现这棵树的所有父节点都比子节点要小,符合这样的完全二叉树我们称为最小堆。
最大堆:如果二叉树的所有父节点都比子节点要大,就称为最大堆。
二、堆可以用来干什么
1.问题描述
加入有14个数,分别是99,5,36,7,22,17,46,12,2,19,25,28,1,92.
请找出这14个数中最小的数,请问应该怎么办呢?
最简单的方法就是将14个数从小扫描到尾,用一个循环就可以解决,但是这种方法的时间复杂度是O(14),也就是O(N)。
如果我们先删除最小的数,再需要增加一个新数23,并且再次求这14个数的最小数,那么这个时候时间复杂度就是O(N的平方)。
2.思路解析
上边的问题可以用堆很简单的解决。
首先我们将14个数按照最小堆的要求(就是所有的父节点都要比子节点小)放入一棵完全二叉树,就像下边的这棵树一样:
很显然最小的数就在堆顶,假设存储这个堆的数组叫做h的话,最小数就是h[1].
接下来我们将堆顶部的数删除,将新增加的23放在对顶,再次调节为最小堆。
如何调整为最小堆呢,向下调整,将23与两个儿子做对比,选择较小的一个与它交换,交换后如下:
我们发现此时还不符合最小堆的特性,继续讲下调整。
于是继续将23与它的两个儿子交换,结果如下:
这样我们就实现了最小堆,知道了最小数。
3.代码复现
向下调整的代码如下:
void siftdown(int i)//传入一个需要向下调整的结点编号i,这里传入1,即从堆顶开始向下调整
{
int t,flag=0;//flag用来标记是否继续向下调整
//当i结点有儿子(其实就是有左儿子的情况下)并且需要继续调整的时候,循环执行
while( i*2<=n && flag==0)
{
//首先判断它和左儿子的关系,并用t来记录值比较小的结点编号
if( h[i]>h[i*2])
t=i*2;
else
t=i;
//如果有右儿子,再对右儿子进行讨论
if( i*2+1<=n)
{
//如果右儿子值更小,更新较小的结点编号
if( h[t]>h[i*2+1])
t=i*2+1;
}
//如果发小最小的结点编号不是自己,说明子节点中有比父节点更小的
if(t!=i)
{
swap(t,i);//交换它们,注意swap函数要自己来写
i=t;//更新i为刚才与它交换的儿子的结点,便于接下来继续向下调整。
}
else
flag=1;//否则说明当前的父节点已经比两个子节点都小了,不需要进行调整了
}
}
三、堆排序
与快速排序一样,堆排序的时间复杂度也是O(NlogN)
.
如果我们现在要进行从小到大的排序,首先建立最小堆,然后每次删除顶部元素并将顶部元素输出或者放在一个新的数组中,直到堆空为止。最终输出的就是一个排序的序列。
1.建立最小堆,从小到大排序。
代码实现
//建立堆以及堆排序的完整代码
#include<stdio.h>
int h[101];//用来存放堆的数组
int n;//用来存储堆中元素的个数,也就是堆的大小
//交换函数,用来交换堆中两个元素的值
void swap(int x,int y)
{
int t;
t=h[x];
h[x]=h[y];
h[y]=t;
}
//向下调整函数
void siftdown(int i)//传入一个需要向下调整的结点编号i,这里传入1,即从堆顶开始向下调整
{
int t,flag=0;//flag用来标记是否继续向下调整
//当i结点有儿子(其实就是有左儿子的情况下)并且需要继续调整的时候,循环执行
while( i*2<=n && flag==0)
{
//首先判断它和左儿子的关系,并用t来记录值比较小的结点编号
if( h[i]>h[i*2])
t=i*2;
else
t=i;
//如果有右儿子,再对右儿子进行讨论
if( i*2+1<=n)
{
//如果右儿子值更小,更新较小的结点编号
if( h[t]>h[i*2+1])
t=i*2+1;
}
//如果发小最小的结点编号不是自己,说明子节点中有比父节点更小的
if(t!=i)
{
swap(t,i);//交换它们,注意swap函数要自己来写
i=t;//更新i为刚才与它交换的儿子的结点,便于接下来继续向下调整。
}
else
flag=1;//否则说明当前的父节点已经比两个子节点都小了,不需要进行调整了
}
}
//建立堆的函数
void creat()
{
int i;
//从最后一个非叶节点到第1个节点依次进行向上调整
for(i=n/2;i>=1;i--)
{
siftdown(i);
}
}
//删除最大的元素
int deletemax()
{
int t;
t=h[1];//用一个临时变量记录顶点的值
h[1]=h[n];
n--;
siftdown(1);//向下调整
return t;//返回之前记录的堆的顶点的最大值
}
int main()
{
int i,num;
//读入要排序的数字的个数
scanf("%d",&num);
for(i=1;i<=num;i++)
{
scanf("%d",&h[i]);
}
n=num;
//建堆
creat();
//删除顶部元素,连续删除n次,其实就是从小到大排列出来
for(i=1;i<=num;i++)
printf("%d ",deletemax());
getchar();
getchar();
return 0;
}
2.建立最大堆,从小到大排序。
代码实现
//从小到大排序,建立最大堆
#include<stdio.h>
int h[101];//用来存放堆的数组
int n;//用来存储堆中元素的个数,也就是堆的大小
//交换函数,用来交换堆中两个元素的值
void swap(int x,int y)
{
int t;
t=h[x];
h[x]=h[y];
h[y]=t;
}
//向下调整函数
void siftdown(int i)//传入一个需要向下调整的结点编号i,这里传入1,即从堆顶开始向下调整
{
int t,flag=0;//flag用来标记是否继续向下调整
//当i结点有儿子(其实就是有左儿子的情况下)并且需要继续调整的时候,循环执行
while( i*2<=n && flag==0)
{
//首先判断它和左儿子的关系,并用t来记录值比较大的-结点编号
if( h[i]<h[i*2])
t=i*2;
else
t=i;
//如果有右儿子,再对右儿子进行讨论
if( i*2+1<=n)
{
//如果右儿子值更大,更新较小的结点编号
if( h[t]<h[i*2+1])
t=i*2+1;
}
//如果发小最大的结点编号不是自己,说明子节点中有比父节点更大的
if(t!=i)
{
swap(t,i);//交换它们,注意swap函数要自己来写
i=t;//更新i为刚才与它交换的儿子的结点,便于接下来继续向下调整。
}
else
flag=1;//否则说明当前的父节点已经比两个子节点都小了,不需要进行调整了
}
}
//建立堆的函数
void creat()
{
int i;
//从最后一个非叶节点到第1个节点依次进行向上调整
for(i=n/2;i>=1;i--)
{
siftdown(i);
}
}
//堆排序
void heapsort()
{
while(n>1)
{
swap(1,n);
n--;
siftdown(1);
}
}
int main()
{
int i,num;
//读入要排序的数字的个数
scanf("%d",&num);
for(i=1;i<=num;i++)
{
scanf("%d",&h[i]);
}
n=num;
//建堆
creat();
//堆排序
heapsort();
//输出
for(i=1;i<=num;i++)
printf("%d ",h[i]);
getchar();
getchar();
return 0;
}