前置知识:
注意:实现堆需要用到完全二叉树的知识,如果未学习,点击了我也没用,因为我还没写
什么是堆?
堆(heap),又叫二叉堆,是一种基于完全二叉树实现的数据结构,它可以实现在堆顶的元素是整个堆里面最大的元素(大根堆),也可以是最小的元素(小根堆),进而获取到整个仪器中的最值的一种数据结构。通过它,我们可以快速获取一组数据中的最值,它的时间复杂度只有O(logn),堆同时也支持删除操作,不过它只能删除堆顶。
图解示范
首先我们先看完全二叉树的节点序号如下,可以发现一个父节点和子节点序号的关系:
设父节点序号为x,那么左子节点的序号为2x,右子节点的序号为2x+1
我们同时也可以知道一个节点x的父节点序号为x/2,(右子节点也是,因为int类型除以2默认向下取整)
首先给出一个堆(这里我们以大根堆为例)
1 | int w[MAXN]; |
对于插入操作
假如我们插入一个5,我们将其插到数组的最后,作为叶子节点。
如果此时插入的子节点大于其父节点的值,违背了大根堆的性质,那么我们需要堆二叉堆进行修复(modify)操作
即向上比较,如果其大于父节点的值,那么便交换两者的位置.
因为5 > 1 所以与其父节点进行位置交换,然后接着进行修复操作,继续向上比较大小
5 > 4,继续与父节点进行位置交换,然后接着向上进行比较,发现小于它的父节点,所以停止修复
可见整个修复过程是相对二叉堆来讲自下而上的修复。
接着我们来讲删除堆顶的操作,首先我们无法直接删除堆顶,否则二叉堆会分裂为两棵树,修复麻烦巨大。
所以我们寻找一种合适的做法:先把堆顶和堆尾(数组的最后一个数)交换位置
然后我们将交换过去的堆顶在队尾删除。
接着我们发现堆顶的元素是一个较小(甚至最小)的数,违背了大根堆的原则,我们又需要对二叉堆进行修复(repair)
相对于前者的插入修复,这里的删除修复是一种自上而下的修复。
首先比较两个子节点的大小,接着父节点与子节点中较大的一个进行大小的比较,如果小于其中较大的节点,对二者进行交换。
选择其中较大的子节点是为了确保如果进行交换,交换上去的子节点是一个更大的数,这样满足大根堆的性质。
此处
每一步也都要对修复的过程进行判断,如果已经是叶子节点(2x > tot) 那么停止修复,这里满足的就是这种情况
如果出现父节点的值大于两个子节点的值,也停止修复。
这样,我们就完成了删除堆顶操作的修复操作。
代码示范
访问堆顶,很简单,根节点(1)即为最大值
1 | int top() |
修复到的节点序号为x,如果修复过程中x已经是根节点(x == 1
),或者大于其父节点w[x] < w[x/2]
回溯
否则,交换x与父节点的值,然后接着向上递归修复。
1 | void modify(int x) |
接下来是删除堆顶操作,详解请看备注
讲一下三目运算符 condition ? x : y
condition为布尔型判断,为真返回x,为假返回y
1 | void repair(int x) |
自此我们实现了堆的所有操作,如果想要判断是否为空和堆的节点个数,通过tot的值便可以实现,这里不再给出具体的代码。
接下来我们引入一个模板题
堆的模板题
https://www.luogu.com.cn/problem/P3378
这是一道小根堆的模板题,大家可以前往自测,这里不做讲解只给出代码示范
1 |
|
快速排序(堆排序)
题目描述
将读入的
输入格式
第一行为一个正整数
第二行包含
输出格式
将给定的
样例输入
1 | 5 |
样例输出
1 | 1 2 4 4 5 |
这是一个通过堆排序实现快排(用小根堆)的模板题,原理就是不断弹出堆顶,来从小到大输出数据。
大家可以前往洛谷进行自测 点我跳转
对比上一道题,这道题的做法就是将一组数据不断插入堆顶,然后输出一个,pop一个,进行n次操作。
下面是伪代码示范
1 |
|
接着我们给出最后一道例题
[NOIP2004 提高组] 合并果子
题目描述
在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。
每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过
因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为
例如有
输入格式
共两行。
第一行是一个整数
第二行包含
输出格式
一个整数,也就是最小的体力耗费值。输入数据保证这个值小于
样例输入
1 | 3 |
样例输出
1 | 15 |
通过题目描述我们不难看出,先合并的石子在后面会参与到后续的合并过程,这就导致它的重量会在后面反复被消耗,即先合并的果子后面其被搬运的次数越多,通过贪心策略我们不难得知,较小的体力消耗的次数越多我们最后体力消耗的总数越少,所以我们实现一个小根堆,每次连续取出两个小根堆堆顶的值,然后再将两个堆顶的和插入到堆中,直到堆为空。
接下来给出伪代码
1 | while(tot > 1) //tot为2时搬运最后一次,将最后两堆石子合在一起 |
优先队列(priority_queue)
优先队列(priority_queue)是一种由STL操作的特殊的队列,它可以实现队列的功能,在此之上,实现队首是整个队列中的最大值(或最小值)
它的底层逻辑是堆,需要包含<queue>
头文件或者万能头,但是它只能访问队首(也就是最值)。
接下里我们给出使用方法
1 | priority_queue <int> q : 新建一个内部为int类型的优先队列q,默认q为大根堆 |
这样我们再回去做合并石子,并用STL操作优先队列来完成这道题目
给出代码示范
1 |
|