1.题目描述:
请求出一组序列中的数中的前k小的数,或者前k大的数,问题很简单
2.算法思路:
该题利用顺序存储二叉堆可以很好的解决:
首先我们先解释一下“堆”这个数据结构
堆这个东西我们可以理解成是利用了二叉树的一条性质而人工构建的一组顺序存储二叉树
分类:
最小堆:每个父亲节点都比子节点小
最大堆:每个父亲节点都比子节点大
顺序存储实现的原理:
二叉树存在一条性质,(这个性质和二叉树的层序遍历的序号有关
)
n号节点的左儿子的存在的话编号为2*n,右儿子存在的话,编号为2*n+1
那么我们依据这一条就可以完全实现二叉树的线性存储,并且这种结构和性质还可以方便我们进行各种操作
下面主要来介绍二叉堆的核心的两个函数:向上浮动调整和向下浮动调整——目的是用来维护我们最小堆和最大堆的性质
《以求前k小的数来举例》
说到这里,有的人可能会问了,为什么我们用最小堆或者最大堆可以解决这个问题呢
我们来这么考虑,我们如果可以构建出只有k个元素的 最大堆
每次加入新的元素,我们将该元素和堆顶进行比较,如果比堆顶小的话,我们将其和堆顶进行替换,然后再次进行调整维护,直到所有的数据都加入完毕,那么堆里面剩下的就是所有的我们要求的元素,这时候我们再利用最小堆的维护函数进行调整进行堆排序就可以按顺序输出前k个元素了
原理讲完了,那么这个核心的向上调整函数和乡下调整函数到底怎么实现呢
3.核心代码段解析:(最小堆举例)
3.1 siftdown(向下调整)
在代码段中进行解析:
void siftdown(int i) //i代表要开始维护的节点的层序标号
{
int t,flag=1;
while(i*2<=num&&flag==1)
{
if(data[i]>data[i*2]) t=i*2; //如果比坐儿子大,t记录要交换的做儿子的标号
else t=i; //否则t只标记自己
if(i*2+1<=num&&data[i*2+1]<data[t]) t=i*2+1; //如果右儿子存在且右儿子比左儿子还小,那么更换标记为右儿子,保持更换后的最小堆性质
if(t!=i)
{
swap(i,t);
i=t; //向下调整完之后,我们要再次追踪,看看是否还能满足最小堆的性质
}
else flag=0; //满足了,调整结束
}
}
3.2 siftup(向上调整)
void siftup(int i) //在这里我们只要维护跟踪根节点就可以了
{
int t,flag=1;
while(i!=1&&fag==1)
{
if(data[i]<data[i/2])
{
swap(i,i/2);
i=i/2;
}
else flag=0; //已经满足最小堆的性质,跳出即可
}
}
3.3 buildheap(建堆操作)
1.整体操作(我们开始得到的就是一整个散乱的堆,我们维护成最小堆)
void buildheap(int num) //num代表堆的大小
{
for(int i=num/2;i>=1;i--) siftdown(1); //我们从倒数第二层最右边的根节点开始逐层向上维护
}
2.逐个插入不断维护:
void buildheap(int num)
{
for(int i=1;i<=num;i++)
{
cin>>data[i]; //加入到堆尾
siftup(i); //向上调整
}
}
4.堆排序:
基于上面的原理我们可以很好地实现查找前k个数的操作,但是至于堆排序我们还没有进行讲解
其是堆排序很简单
按照升序排列我们需要最大堆,按照降序排列我们需要最小堆
每次我们将堆尾和堆顶进行交换,然后将堆数递减,保持堆尾固定,然后在向下调整,重复过程,直到我们的堆数为1,然后这个堆就有序了
所以说根据上面的原理讲解我们很容易发现堆排序的复杂度始终是O(n*logn)的logn是指堆的二叉树深度,n是因为我们每次都要从未到头进行遍历交换
不多说了,附上代码:
void heapsort(int num) //堆的大小
{
int help=num; //先记录下来对的原始大小,不然这个数据会丢失
while(num!=1)
{
swap(1,num);
num--;
siftdown(1);
}
}