1.堆的定义
堆(heap):一种有特殊用途的数据结构——用来在一组变化频繁(发生增删查改的频率较高)的数据集中查找最值。
堆是一颗完全二叉树(除了最后一层节点外,树上面的节点都是满的,不存在空节点,最后一层从左到右排列)
堆:满足一个性质,这里以小根堆为例,它的每一个节点都是小于对于它的左右儿子的。依次类推,就会发现每个点都会小于等于对于分支两边的所有点,所以根节点就是最小值。
2.如何手写一个堆?(//后面的看不懂可先跳过,继续往下看)
①.插入一个数 // 每次在堆的最后面插入,然后再up操作
②.求集合当中的最小值 //heap[1]
③.删除最小值 //删除就用最后一个元素覆盖到堆顶元素,size--,然后再down操作
④.删除任意一个元素 //假如是k,heap[k] = heap[size] ; size --; down(k) ;/ up(k) ;
⑤.修改任意一个元素 //heap[k]=x; down(k);/ up(k);
这里不同于C++中stl里的堆(优先队列),stl里的堆不支持后面两个操作
3.堆的存储
不同于链表的存储,堆是一种全新的存储方式:
用一维数组存,初始位置存根节点,每个节点x的左儿子是2x,右儿子2x+1. (这里的是指的数组下标)
4.堆有两个基本操作(这里都以小根堆为例)
①.down(x)向下调整,把节点向下移(每次操作时就把要移动的节点和其子节点的最小值交换)
②.up(x)向上调整,把节点向上移(每次操作只需要交换父节点和要移动的节点)
下面将以具体题为例实战一下
这道题我们直接把数据存放在一维数组h中,看成一个完全二叉树,每个节点的左儿子是其2倍,右儿子是2倍+1;然后开始建堆,从倒数第二层最后一个元素n/2(向下取整)开始向上遍历,不断的down。
为什么从倒数第二层的最后一个元素遍历呢?
因为最后一层往下就没有元素了,而且还会浪费大量时间。
在每次取出堆顶元素后,用最后一个元素填充堆顶,然后再进行一次 down(1)
操作,即可保持堆的性质。
#include<iostream>
using namespace std;
const int N=100010;
int n,m;
int h[N],size_;
void down(int u)
{
int t=u; //我们用t来表示每一个down的三个元素中的最小值下标
if(u*2<=size_&&h[u*2]<h[t]) t=u*2; //成立就更新下标
if(u*2+1<=size_&&h[u*2+1]<h[t]) t=u*2+1;
if(u!=t) //说明有比传入的这个节点还小的点
{
swap(h[u],h[t]);
down(t);
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&h[i]);
size_=n;
//这里不一个一个遍历了(时间复杂度O(nlog(n))),下面这样插入可以将时间复杂度降到O(n).
for(int i=n/2;i;i--) down(i); //这里的n/2是向下取整,i=n/2,即完全二叉树的倒数第二层的最后一个元素
while(m--)
{
printf("%d ",h[1]); //输出堆顶元素
h[1]=h[size_]; //删除堆顶元素,即用最后一个元素覆盖堆顶元素
size_--; //size减减
down(1); //然后再从堆顶开始往下down
}
return 0;
}
这里补一下up的操作
void up(int u)
{
while(u/2&&h[u/2]>h[u]) //这里的u/2代表u的父节点下标,u/2不为0,就一直往上up
{
swap(h[u/2],h[u]);
u/=2;
}
}
ps:完全二叉树:
完全二叉树是一种特殊的二叉树,其中除了最后一层可能不满外,其余各层的节点都达到了最大个数。
具体地说,对于一个完全二叉树:
- 最后一层或者是满的,或者是从左边开始连续缺少一些节点。
- 如果某个节点有右子节点,那么它一定有左子节点。