常用数据结构--二叉堆

c++ 专栏收录该内容
7 篇文章 0 订阅

二叉堆

*在信息学奥赛中常会遇到给你一个数列,让你求最大值或最小值的问题,这个问题很简单,但是一旦遇到频繁查找那就很复杂了,这里就要涉及到一个新的数据结构,二叉堆.

一、堆的定义
堆的定义:n个元素的序列K={a1,a2,a3,…an},当且仅当满足如下条件时,称之为堆。Ki<=K2i且Ki<=K2i+1或者Ki>=K2i且Ki>=K2i+1,(i=1,2,… 可以看出,若将此序列看成完全二叉树.则堆的定义表明,完全二叉树中每个非结点的关键字均小于等于 (或大于等于)其左右子结点.这样根就成了最小或最大值.堆也因此称为小根堆或大根堆.由此可见,从堆中获取最小 (大)值是很方便的.堆的子树也满足堆得性质.

二、堆的存储方式
[存储方式]最小堆的元素保存在a[1…n]内
– 根在a[1]
– K的左儿子是2k, K的右儿子是2k+1,
– K的父亲是[k/2]

在这里插入图片描述
三、堆的基本操作
1.删除最小值
• 三步法
①直接删除根
②用最后一个元素代替根上元素
③向下调整
在这里插入图片描述

void heapdown()//向下堆化
{
	int t,i,j;
	i=1;
	while(i*2<=a[0])//a[0]表示堆的大小
	{
		if(i*2==a[0]||a[i*2]<a[i*2]+1)
			j=i*2;
		else
			j=i*2+1;
		if(a[i]>a[j])
		{
			swap(a[i],a[j]);
			i=j;		
		}	
		else
			break;
	} 
	return;
} 
int deleteMin()//删除堆顶元素
{
	int r=a[1];
	a[1]=a[a[0]--];
	heapdown(1);
	return r;	
} 

2.插入元素和向上调整
• 插入元素是先添加到末尾, 再向上调整
• 向上调整:比较当前结点p和父亲, 如果父亲比p小,停止; 否则交换父亲和p, 继续调整

void heapup()//向上堆化
{
	int i,t;
	i=a[0];
	while(i>1&&a[i]<a[i/2])
	{
		swap(a[i],a[i/2]);
		i=i/2;
	}	
	return;
} 
void setheap(int c)//建堆 
{
	++a[0];
	a[a[0]]=c;
	heapup();
}

3.删除任意元素(delete)
删除任意元素类似于删除最小元素,先用最后一个元素代替被删除元素,再进行调整。不过值得注意的是,最后一个元素被交换后可能要向下调整,也有可能要向上调整。具体实现可参照以上两段代码。
4.堆的建立(build)
给n个整数,如何构造一个二叉堆?可以一个一个插入,但是有更好的方法:从下往上一层一层向下调整。由于叶子无需调整,因此只需要从n/2开始递减到1。由数学归纳法可以证明循环变量为i时,第i+1,i+2,…,n均为最小堆的根。代码如下:

void build()
{  
	int i;
  	for(i=a[0]/2;i>0;i--)
	  	down(i);
}

5、时间复杂度
向上调整/向下调整:每层是常数级别, 共logn层, 因此O(logn);
插入/删除:只调用一次向上或向下调整, 因此都是O(logn);

接下来我们用一些例题来讲解:

1560 – 【堆练习】堆排序
Description
输入N个整数,进行二叉堆排序.输出结果.
Input
第1行:一个整数N(N≤100000),表示个数。
第2行:N个空格分开的整数。
Output
第1行:N个空格分开的整数,以不降序排列。
Sample Input
5
2 1 0 3 4
Sample Output
0 1 2 3 4

//hanyiyang c++ code
#include <bits/stdc++.h>
#define MAXN 100005
using namespace std;
int n,num;
int a[MAXN]={};
void heapup()//向上堆化
{
	int i;
	i=a[0];
	while(i>1&&a[i]<a[i/2])
		swap(a[i],a[i/2]),i=i/2;
	return;
}
void heapdown()
{
	int i,j;
	i=1;
	while(i*2<=a[0])//a[0]表示堆的大小
	{
		if(i*2==a[0]||a[i*2]<a[i*2+1])//最小堆(从小到大排列,堆顶元素最大),如果是从大到小排列就是a[i*2]>a[i*2+1]
			j=i*2;
		else
			j=i*2+1;
		if(a[i]>a[j])
			swap(a[i],a[j]),i=j;//注意i和j也要交换
		else
			break;
	}
	return;
}
void setheap(int x)//建堆
{
	a[++a[0]]=x;
	heapup();
	return;
}
void print_del()
{
	printf("%d ",a[1]);//输出堆顶元素
	a[1]=a[a[0]],--a[0];
	heapdown();
	return;
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;++i)
		scanf("%d",&num),setheap(num);
	for(int i=1;i<=n;++i)
		print_del();
	return 0;
}

当然这都是最基本的,现在来一个具有特殊思想的问题

1421 – 【堆练习】最小函数值
Description
  有n个函数,分别为F1,F2,…,Fn。定义Fi(x)=Aix^2+Bix+Ci(x∈N∗)。给定这些Ai、Bi和Ci,请求出所有函数的所有函数值中最小的m个(如有重复的要输出多个)。
Input
  第一行输入两个正整数n和m。
  以下n行每行三个正整数,其中第i行的三个数分别位Ai、Bi和Ci。输入数据保证Ai<=10,Bi<=100,Ci<=10000。
Output
  将这n个函数所有可以生成的函数值排序后的前m个元素。这m个数应该输出到一行,用空格隔开。
Sample Input
3 10
4 5 3
3 4 5
1 7 1
Sample Output
9 12 12 19 25 29 31 44 45 54

这道题看似很简单,似乎只需要把每一个函数的值算出来,在进行一个堆排序就AC了,但是当看到(x∈N∗)的时候 (意思是x是任意自然数)可能就有些懵了,x并没有限制.应该怎么计算? 通过分析此题,我们不难发现,如果将每一个函数的所有函数值看作一个序列,那么这个序列一定是单调递增子序列. 那么又有人会说:如果这样,那就只需要每一个函数都算m个值或者其他能保证所有函数算出来的数能又m个,再进行一个堆排序就行了,但是时间复杂度太高容易TLE,所以,我们可以采取如下方法:
二叉堆方法:
①对于每一个给的a[i],b[i],c[i],把此事函数的最小值也就是a[i]+b[i]+c[i]入堆.并用Fn[i]记录此值属于的函数位置 ,Fx[i]记录此值是这个函数的第几个值.
②输出堆顶元素.
③将堆顶元素所在的函数的下一个值计算出来,并放入堆顶,向下堆化.
④重复执行②,③m次
代码如下:

//hanyiyang c++ code
#include <bits/stdc++.h>
#define MAXN 10005
using namespace std;
int n,m;
int dui[MAXN]={};
int a[MAXN],b[MAXN],c[MAXN];
int Fn[MAXN],Fx[MAXN];//Fn[i]记录此值属于的函数位置 ,Fx[i]记录此值是这个函数的第几个值. 
void heapup()//向上堆化 
{
	int i;
	i=dui[0];
	while(i>1&&dui[i]<dui[i/2])
	{
		swap(Fn[i],Fn[i/2]);
		swap(Fx[i],Fx[i/2]);
		swap(dui[i],dui[i/2]);
		i=i/2;
	}	
	return;
}
void heapdown()//向下堆化 
{
	int i,j;
	i=1;
	while(i*2<=dui[0])
	{
		if(i*2==dui[0]||dui[i*2]<dui[i*2+1])
			j=i*2;
		else
			j=i*2+1;
		if(dui[i]>dui[j])
		{
			swap(Fn[i],Fn[j]);//交换位置 
			swap(Fx[i],Fx[j]); 
			swap(dui[i],dui[j]);//交换值 
			i=j;
		}
		else
			break;
	}
	return;
}
void setheap(int num)//建堆 
{
	dui[++dui[0]]=num;
	heapup();
	return;
}
int main()
{
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;++i)
	{
		scanf("%d %d %d",&a[i],&b[i],&c[i]);
		Fn[i]=i;//记录此值属于的函数位置 
		Fx[i]=1;//记录此值是属于函数的第几个值 
		setheap(a[i]+b[i]+c[i]);//建堆 
	}
	for(int i=1;i<=m;++i)
	{
		printf("%d ",dui[1]);
		int s;
		++Fx[1]; 
		s=a[Fn[1]]*Fx[1]*Fx[1]+b[Fn[1]]*Fx[1]+c[Fn[1]];//计算此函数所在函数的下一个值 
		dui[1]=s;
		heapdown();
		//printf("%d ",s);
	}
	return 0;
}

这就是二叉堆的大致概念,有能力的读者可以去尝试以下几个问题:
1.用堆优化Prime算法
2.用堆优化Dijkstra算法
3.描述二叉堆的数组表示法,证明树的高度为logn;
4.为什么插入和删除只需要调整一条从根到叶子的路径而不会影响到其他?
5.如果减小了一个结点的值,需要对此结点进行怎样的调整,才能重新得到一个堆?

感谢大家的观看,如果觉得满意请点赞,关注一下我,给我这个初一的蒟蒻一点点鼓励,谢谢😀

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2020 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值