( h e a p ) (heap) (heap)数据结构详解

堆是一种特殊的完全二叉树,常用于实现优先队列和堆排序算法。堆分为两种主要类型:最大堆和最小堆。

堆的基本特性

最大堆 ( M a x (Max (Max H e a p ) Heap) Heap)

  • 每个节点的值都大于或等于其子节点的值
  • 根节点是堆中的最大值

最小堆 ( M i n (Min (Min H e a p ) Heap) Heap)

  • 每个节点的值都小于或等于其子节点的值
  • 根节点是堆中的最小值

堆的表示

堆通常使用数组来表示,利用完全二叉树的特性:

  • 对于索引为 i 的节点:
    • 父节点索引: ( i − 1 ) / 2 (i-1)/2 (i1)/2
    • 左子节点索引: 2 ∗ i + 1 2*i+1 2i+1
    • 右子节点索引: 2 ∗ i + 2 2*i+2 2i+2

时间复杂度

操作 时间复杂度
插入 O ( l o g O(log O(log n ) n) n)
删除 O ( l o g O(log O(log n ) n) n)
获取最大/最小值 O ( 1 ) O(1) O(1)
构建堆 O ( n ) O(n) O(n)

堆是一种高效的数据结构,特别适合需要频繁访问最大或最小元素的场景

模板展示

#include <bits/stdc++.h>
using namespace std;
priority_queue<int,vector<int>>q;//大根堆
priority_queue<int,vector<int>,greater<int>>Q;//小根堆
//加了greater就是小根堆
int main(){
	//增加元素操作:
	q.push(1);
	//删除堆顶
	q.pop();
	//堆的长度
	int a=q.size();
	cout <<a<<endl;
	//堆顶
	q.push(1);
	q.push(2);
	q.push(3);
	cout <<q.top();//堆顶
	return 0;
}

代码展示了大根堆与小根堆的定义和操作
结果将会输出

0
3

若是用Q完成操作,将会输出

0
1

大根堆的堆顶是最大值,小根堆的堆顶是最小
每次使用 p o p pop pop会弹出堆顶

这里给出手写堆的示例:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e6+5;
int h[MAXN],len=0;//len记录当前二叉树的长度
void push_s(int x){//上浮,插入新元素
	h[++len]=x;
	int i=len;
	while(i>1&&h[i]<h[i/2]){
		swap(h[i],h[i/2]);
		i=i/2;
	}
}
void pop_s(){//下沉,删除堆头,调整堆
	h[1]=h[len--];//根结点替换为最后一个结点,然后结点数量减1
	int i=1;
	while(2*i<=len){//至少有左儿子
		int son=2*i;//左儿子
		if(son<len&&h[son+1]<h[son])son++;//son<len表示有右儿子,选儿子中较小的
		if(h[son]<h[i]){//与小的儿子交换
			swap(h[son],h[i]);
			i=son;//下沉到儿子处
		}
		else break;//如果不比儿子小,就停止下沉
	}
}//_s小根堆
void push_b(int x){//下沉,插入新元素
	h[++len]=x;
	int i=len;
	while(i>1&&h[i]>h[i/2]){//改为大于号
		swap(h[i],h[i/2]);
		i=i/2;
	}
}
void pop_b(){//上浮,删除堆头,调整堆
	h[1]=h[len--];//根结点替换为最后一个结点,然后结点数量减1
	int i=1;
	while(2*i<=len){//至少有左儿子
		int son=2*i;//左儿子
		if(son<len&&h[son+1]>h[son])son++;//改为大于号,选较大的儿子
		if(h[son]>h[i]){//改为大于号,与大的儿子交换
			swap(h[son],h[i]);
			i=son;//下沉到儿子处
		}
		else break;//如果不比儿子大,就停止下沉
	}
}//_b大根堆
int main(){
	return 0;
}

调用部分自己参考

主要功能介绍 : \large\color{FFC500}{主要功能介绍:} 主要功能介绍:

  • p u s h push push_ s s s
    小根堆的 p u s h push push,塞入一个元素 x x x并上浮
  • p o p pop pop_ s s s
    小根堆的 p o p pop pop,弹出堆头并下沉
  • p u s h push push_ b b b
    大根堆的 p u s h push push,塞入一个元素 x x x并下沉
  • p u s h push push_ b b b
    大根堆的 p o p pop pop,弹出堆头并上浮

例题

∣ 题目传送门 \mid{题目传送门} 题目传送门

P1090 [NOIP 2004 提高组] 合并果子

题目背景

P6033 为本题加强版。

题目描述

在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。

每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过 n − 1 n-1 n1 次合并之后, 就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。

因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为 1 1 1 ,并且已知果子的种类 数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。

例如有 3 3 3 种果子,数目依次为 1 1 1 2 2 2 9 9 9 。可以先将 1 1 1 2 2 2 堆合并,新堆数目为 3 3 3 ,耗费体力为 3 3 3 。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为 12 12 12 ,耗费体力为 12 12 12 。所以多多总共耗费体力 = 3 + 12 = 15 =3+12=15 =3+12=15 。可以证明 15 15 15 为最小的体力耗费值。

输入格式

共两行。
第一行是一个整数 n ( 1 ≤ n ≤ 10000 ) n(1\leq n\leq 10000) n(1n10000) ,表示果子的种类数。

第二行包含 n n n 个整数,用空格分隔,第 i i i 个整数 a i ( 1 ≤ a i ≤ 20000 ) a_i(1\leq a_i\leq 20000) ai(1ai20000) 是第 i i i 种果子的数目。

输出格式

一个整数,也就是最小的体力耗费值。输入数据保证这个值小于 2 31 2^{31} 231

输入输出样例 #1

输入 #1

3 
1 2 9

输出 #1

15

说明/提示

对于 30 % 30\% 30% 的数据,保证有 n ≤ 1000 n \le 1000 n1000

对于 50 % 50\% 50% 的数据,保证有 n ≤ 5000 n \le 5000 n5000

对于全部的数据,保证有 n ≤ 10000 n \le 10000 n10000

例题分析

可以分析
假如有 a , b , c a,b,c a,b,c三堆果子
即合并 a a a b b b后,还需合并 a + b a+b a+b c c c
则尽量使 a + b a+b a+b最小
即满足 a ≤ b ≤ c a\leq{b}\leq{c} abc
那么联想到小根堆(即把堆顶2个数合并)
代码实现如下

AC代码1

直接调用函数
这里注意 x , y x,y x,y要分开,先弹出堆顶再得到 y y y再弹出
注意: s . s i z e ( ) s.size() s.size() ≥ 2 ≥2 2
因为需要弹出两次

#include <bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;
priority_queue<int,vector<int>,greater<int>>s;
int a[MAXN];
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        s.push(a[i]);
    }
    int ans=0;
    while(s.size()>1){
        int x=s.top();
        s.pop();
        int y=s.top();
        s.pop();
        ans+=x+y;
        s.push(x+y);
    }cout <<ans;
    return 0;
}

AC代码2

用手写堆
注意是从 h [ 1 ] h[1] h[1]开始计算的

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e6+5;
int h[MAXN],len=0;
int a[MAXN],sum=0;
void push(int x){
	h[++len]=x;
	int i=len;
	while(i>1&&h[i]<h[i/2]){
		swap(h[i],h[i/2]);
		i=i/2;
	}
}
void pop(){
	h[1]=h[len--];
	int i=1;
	while(2*i<=len){
		int son=2*i;
		if(son<len&&h[son+1]<h[son])son++;
		if(h[son]<h[i]){
			swap(h[son],h[i]);
			i=son;
		}
		else break;
	}
}
int main(){
	int n;
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		push(a[i]);
	}
	while(len>1){
		int x=h[1];
		pop();
		int y=h[1];
		pop();
		sum+=x+y;
		push(x+y);
	}cout <<sum;
	return 0;
}

题单推荐

∣ 堆的题单 \mid{堆的题单} 堆的题单

题单和例题来自 洛谷 洛谷 洛谷

~ 完结撒花 完结撒花 完结撒花 ~

附:仅展示模板,习惯使用 M A X N MAXN MAXN作为数组最大空间
推荐 洛谷 洛谷 洛谷作为你的刷题区域
下一篇预告:奇妙的代码实现?或者其他数据结构或算法

更多推荐