堆

堆的原理实现详解以及堆的应用 文章目录堆是什么堆的存储方式插入结点删除结点堆排序代码实现 堆是什么   堆(heap)本质上就是一棵完全二叉树,只不过这棵完全二叉树有点特别,能够被称为堆的二叉树,除了它必须要是一棵完全二叉树外,它的每一个结点都必须大于或者小于其子节点。 解释一下就是对于树中的所有的结点都至少有一棵子树,对于这个结点的子树上所有的结点都必须要大于或者小于其父节点。因此,堆也是个递归的定义,通常,我们将结点都大于或者等于子树所有结点的堆称为最大堆 ;将结点都小于或者等于子树所有结点的堆称为最小堆 。如下图所示: 阅读详情

from: http://blog.youkuaiyun.com/beenlast/article/details/6707192


最近学了下堆,堆就是一坨一坨的东西,最普通的堆有两种,大根堆(也叫最大堆)和小根堆(最小堆)。

堆是一棵树,普通的二叉堆是完全二叉树,每棵子树也是一个堆,树中结点个数就是堆的大小(size)。

对于最大堆,就是上面的一定比下面的大,但同一层之间互相的关系完全不清楚,因此最大的就是根节点,也就是堆顶。

因为堆也就这些信息,一般就只对堆顶进行操作。

一颗完全二叉树,如果自顶向下,自左向右对节点编号的话,设根节点编号为1,那么对于每个节点u其左儿子是2u,右儿子是2u+1,其父亲就是u/2取整。

有这个性质,二叉堆就非常容易写了,用数组来存堆即可。



最大堆的几个主要函数:

最主要的是保持堆性质的heapify函数:

[cpp] view plain
copy

void heapify(int u){
int max,l,r;
max=u;
l=u<<1;r=l|1;
if(l<=siz&&node[max]<node[l])
max=l;
if(r<=siz&&node[max]<node[r])
max=r;
if(max!=u){
swap(node[u],node[max]);
heapify(max);
}
}
我们假设u这个节点的左子树和右子树已经是个好的堆,那么我们只要调整u节点数值的位置就能使以u这个节点为根节点的堆成为一个好的堆。


那么我们找出u,u的左儿子与u的右儿子中哪个最大,如果u最大那么已经是一个好的堆了。否则,就将u和最大的那个节点交换,u的值就是正确了。

但是这样就改变了被交换的那个节点值,可能破坏了那个子堆的性质,于是就对该子节点继续heapify,直到整个堆正确。

由于这个函数是以dfs实现的,复杂度和dfs树的层数有关,于是这个函数最坏的复杂度为O(log(n))。





有了这个函数,几乎我们就已经掌握了整个堆怎么写了,下面给出建堆函数:

[cpp] view plain
copy

void build(int &n){
siz=n;
for(int i=siz>>1;i>=1;i--)
heapify(i);
}

因为对于任意一棵完全二叉树,其叶子节点一定是满足堆的性质的 (因为叶子节点没有儿子,自己就是自己这个堆的堆顶)。


那么我们从最后一个叶子节点的父亲开始从后往前从下往上运行heapify函数就能建好整个堆。

这个过程的复杂度我不知道怎么算,但书上给出是O(n)的。



那么我们取出堆顶的函数也很简单了:

[cpp] view plain
copy

int top(){
return node[1];
}


复杂度O(1)。



怎么删除堆顶元素呢?

其实我们只要把堆顶元素和堆的最后一个元素交换,然后减小size再对堆顶运行一次heapify即可:

[cpp] view plain
copy

void pop(){
swap(node[1],node[siz]);
siz--;
heapify(1);
}

复杂度显然就是O(log(n))了。

另外堆排序的话,用这个pop函数你就直接建个最大堆然后不停pop直到siz等于1,整个数组从1开始就是升序排列的了,复杂度O(nlog(n)),是不稳定排序(因为堆除了保持顶部数值最大外其他信息一概不清晰,于是对于其他键值不能保持稳定)。



其实优先队列也就是堆,STL的优先队列我现在所用到的功能(虽然我用的功能很少。。)全都可以由最大堆实现。





上面没有给出怎么给堆添加元素(push),其实很简单,你自己稍稍动动脑子就能想到了。











下面给出我自己写的最大堆(部分功能优先队列):

[cpp] view plain
copy

struct __heap{
#define CONT int //改变CONT可以改变堆元素类型。
#define VV 2000001 //VV是堆的最大大小。
private:
CONT node[VV];
int siz;
public:
void heapify(int u){
int max,l,r;
max=u;
l=u<<1;r=l|1;
if(l<=siz&&node[max]<node[l])
max=l;
if(r<=siz&&node[max]<node[r])
max=r;
if(max!=u){
swap(node[u],node[max]);
heapify(max);
}
}
int size(){
return siz;
}
void clr(){
siz=0;
}
bool empty(){
return !siz;
}
void push(CONT &val){ //加点函数(简单吧~)
node[++siz]=val;
int now=siz,parent=siz>>1;
while(parent>0&&node[parent]<node[now]){
swap(node[now],node[parent]);
parent=(now=parent)>>1;
}
}
CONT top(){
return node[1];
}
void pop(){
swap(node[1],node[siz]);
siz--;
heapify(1);
}
void in(CONT &val){
node[++siz]=val;
}
void cpy(CONT *a,int l,int r){
siz=0;
for(int i=l;i<=r;i++)
node[++siz]=a[i];
}
void build(int &n){
siz=n;
for(int i=siz>>1;i>=1;i--)
heapify(i);
}
};



这只是最普通的二叉堆,只有取出顶部和删除顶部的功能,对于堆的合并要O(n)的时间(把两个表示堆的数组并起来build下),效率不佳。





如果堆可以合并,那么所有操作都统一了,加点就是把单节点的堆并入原来的堆,删除就是将堆顶的左右儿子合并起来。

为了方便合并,就不用完全二叉树了,因为接下来的结构有可能根本就不是完全二叉树了,不是完全二叉树了就不好用数组写了,那就用原始的指针开始写就好了。



我们知道如果一个过程是递归的那么代码也会很好写,那我们就让合并也成为一个递归的过程。

假设我们要合并A,B两个最大堆,如果A顶的值小于B顶的值就交换A和B(保持堆的性质)。

如果A是空堆就返回B,如果B是空堆就返回A。

否则我们就把A的右儿子(不一定非得是右,但整个递归过程必须是统一方向的儿子,例如全部都是右)和B合并,然后让A的右儿子变成A的右儿子和B合并起来的堆。

大概这样:



[cpp] view plain
copy

heap* merge(heap *a,heap *b)
{
if(a==NULL) return b;
if(b==NULL) return a;
if(a->key < b->key) swap(a,b);
a->r = merge(a->r,b);
return a;
}

这样虽然很简洁,但是合并着合并着右边就会越来越多,右边越多合并速度就会越慢,这样下去合并的速度就会退化。


那该怎么办?

于是我们想办法让堆的左边尽量多然后合并后如果右边多于左边就把右边的甩到左边去。



定义一个节点如果儿子不全(没有左儿子或右儿子,或者全没有)就称其为外部节点,每个节点往下走到其最近的外部节点所走的边数叫做该节点的距离,外部节点的距离为0。

然后我们维护一棵树,使其每个节点左儿子的距离不小于其右儿子的距离,这样的树就是左偏树。

左偏树又称为左高树、左式堆……。

左偏树有个很方便的性质:每个节点的距离等于其右儿子距离+1(证明网上搜一下就有)。

另外空节点的距离为-1。



那么合并函数就变成了这样:

[cpp] view plain
copy

_node* merge(_node *a,_node *b)
{
if(a == NULL) return b;
if(b == NULL) return a;
if(a->key < b->key) swap(a,b);
a->r = merge(a->r,b);
if(dis(a->l) < dis(a->r)) swap(a->l,a->r);
a->dis = dis(a->r) + 1;
return a;
}

其最坏复杂度为O(logn1+logn2),n1是a的节点数,n2是b的节点数。




加点就成了这样:

[cpp] view plain
copy

void push(CONT val)
{
siz++;
_node *p = newnode();
p->l = p->r = NULL;
p->key = val;
head = merge(head,p);
}



删除顶部元素就是这样:

[cpp] view plain
copy

void pop()
{
if(siz == 0) return ;
siz--;
_node *p = head;
head = merge(head->l,head->r);
delnode(p);
}

虽然写起来比二叉堆麻烦了点,空间占用也比二叉堆多,但是其合并速度远远超过二叉堆。


总体来说左偏树是一个中庸的数据结构,其合并速度慢于二项堆和fibonacci堆,但是变成复杂度远小于后两者。

也可以说左偏树是兼顾了合并速度和编程复杂度低的两个优点。



其实左偏树还有个变种,叫做斜堆,斜堆没有了距离这个概念,它在合并的时候不管左儿子多还是右儿子多总是交换一次:

[cpp] view plain
copy

_node* merge(_node *a,_node *b)
{
if(a == NULL) return b;
if(b == NULL) return a;
if(a->key < b->key) swap(a,b);
a->r = merge(a->r,b);
swap(a->l,a->r);
return a;
}
虽然复杂度有可能会变成O(n),但总体上和左偏树是相差无几的。





附上我自己写的左偏树代码:

[cpp] view plain
copy

struct leftist_tree{
#define CONT int
private:
struct _node{
CONT key;
int dis;
_node *l,*r;
};
_node *head;
_node* newnode()
{
_node *p = (_node*)malloc(sizeof(_node));
return p;
}
void delnode(_node *p)
{
free(p);
}
public:
int siz;
leftist_tree(){siz=0;head=NULL;}
void clr()
{
head=NULL;
siz=0;
}
int dis(_node *p)
{
if(p==NULL) return -1;
return p->dis;
}
_node* merge(_node *a,_node *b)
{
if(a == NULL) return b;
if(b == NULL) return a;
if(a->key < b->key) swap(a,b);
a->r = merge(a->r,b);
if(dis(a->l) < dis(a->r)) swap(a->l,a->r);
a->dis = dis(a->r) + 1;
return a;
}
void merge(leftist_tree &b)
{
head = merge(head,b.head);
siz += b.siz;
}
CONT top()
{
if(head == NULL) return NULL;
return head->key;
}
void push(CONT val)
{
siz++;
_node *p = newnode();
p->l = p->r = NULL;
p->key = val;
head = merge(head,p);
}
void pop()
{
if(siz == 0) return ;
siz--;
_node *p = head;
head = merge(head->l,head->r);
delnode(p);
}
};






据说二项堆、fibonacci堆编程复杂度高得那啥,过一阵子学吧。。。。


二项堆与斐波那契堆 二项堆 二项堆是符合最小堆性质的二项树链接成的森林二项树是形如下图的树 阅读详情

相关推荐

acm-【堆】学习笔记(二叉堆、左偏树、配对堆、斐波拉契堆、斜堆、STL)

引言 本文主要针对作者本人复习使用,相关基础知识点不会讲述。 目录引言二叉堆左偏树斜堆配对堆二项堆斐波拉契堆STL中的堆堆的应用 二叉堆 二叉堆是最普通的堆,能够实现push,poppush,poppush,pop等基本操作,下面以小根堆为例给出一种常见的实现。 首先设置一个足够大的数组,我们将树的节点按照层次遍历的顺序从小到大依次放置在数组中,数组中的位置也对应是该节点的编号,如下图所示: 上图是一个典型的二叉树,每个节点上标注的数字即是该节点的编号,容易发现对于一个节点uuu而言,它的儿子分别是2u,

qq_30361651的博客 1476

堆 (带图详解)

如果有一个关键码的集合K = { , , ,…, },把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足: = ) i = 0,1,所以要从倒数第二层开始调整。

qq_62939852的博客 3405

四旋翼无人机控制算法实战:从PID到MPC的路径跟踪

本文深入探讨了四旋翼无人机控制算法的演进路径,从基础的PID控制到高级的MPC算法,详细解析了各类算法的原理、实现技巧及适用场景。通过实战案例对比分析,为开发者提供从算法选型到参数调优的全方位指导,助力提升无人机飞行控制的精度与稳定性。

weixin_29090917的博客 242

数据结构——堆(C语言实现)

本篇文章主要介绍了堆的概念、堆的实现、堆排序的实现、建堆和堆排序时间复杂度的讲解、以及简单介绍了TOPK问题。

sinzz的博客 3179

Vienna整流器双PI闭环仿真:从SVPWM调制到中点电位平衡的实践解析

本文深入解析Vienna整流器双PI闭环仿真技术,涵盖SVPWM调制和中点电位平衡的实践细节。通过详细的MATLAB/Simulink仿真搭建流程和常见问题排查方法,帮助工程师掌握高效整流器控制策略,提升系统稳定性和动态响应性能。

weixin_29168153的博客 327

Mac用户必看:PD虚拟机网络初始化失败的3分钟修复指南(附详细截图)

本文针对Mac用户在Parallels Desktop(PD虚拟机)启动时遇到的“网络初始化失败”问题,提供了快速诊断与修复指南。核心原因是macOS系统更新后权限未正确授予。文章通过详细的三步修复法(关闭进程、修改配置文件、重新授权重启),帮助用户在3分钟内恢复虚拟机网络连接,并附有排查清单与预防建议。

weixin_29231027的博客 538

堆(Heap)

什么是堆?如何写一个自己的堆?如何实现大顶堆?时间复杂度如何?

2301_77345975的博客 500

发票OFD格式转换成PDF

发票OFD格式转换成PDF

Be_insighted的博客 934

堆(二叉树,带图详解)

向下调整建堆,向上调整建堆,大堆小堆带图详解

qq_74732628的博客 753

告别商业软件?实测用gnina(CNN打分版)做分子对接:从安装到跑通第一个案例

本文详细介绍了基于卷积神经网络(CNN)的开源分子对接工具gnina的安装与实战应用。从环境配置、编译安装到运行第一个COVID-19主蛋白酶抑制剂对接案例,全面解析了这款替代商业软件的强大功能,特别适合预算有限的研究团队进行药物发现和虚拟筛选。

weixin_42522320的博客 256

AFSim仿真系统中的图像处理器:从静态图像到视频流的目标识别技术解析

本文深入解析了AFSim仿真系统中WSF_IMAGE_PROCESSOR图像处理器的核心功能与技术实现。它详细阐述了该处理器如何从静态图像与视频流中完成目标检测、跟踪与识别,将原始像素数据转化为可供指挥系统使用的轨迹情报,并重点介绍了其基于约翰逊准则的目标识别进阶逻辑、关键参数配置以及脚本扩展能力,为构建高逼真度仿真应用提供了实践指导。

cola5的博客 214

【数据结构】堆的概念 | 从零开始实现数组堆

我们之前似乎确凿在C语言教学里讲过堆,但是那是操作系统中的堆,我们今天将要讲的堆是数据结构里的堆。数据结构中也有栈和堆,它跟操作系统对内存划分中的栈和堆没有关系。我横竖卷不动其他人,于是就打算再更亿篇博客罢。

有道无术, 术尚可求。有术无道, 止于术。 3219

手把手教你从入门到精通C# MES通信

本文是C#与MES通信通信的目录,使用C#与各种MES系统通信的方法我都会在专栏中讲到,比如WebService、Http、Ftp、共享文件夹、串口通信等,绝对是上位机软件工程师实现与MES系统通信的必备教程!

qq_34059233的博客 8751

堆(什么是堆以及怎样自己创建堆)

一篇文章教你认识什么是堆以及自己如何创建堆

不能再留遗憾了的博客 2450

lammps多晶正弦粗糙界面建模

其中,30可以理解为正弦曲线的幅度,150为正弦曲线的基准坐标值,80控制周期,改变参数多试试就能找到规律。很久之前介绍过lammps粗糙界面建模,主要是在lammps中进行一维和二维的正弦粗糙面建模。运行完成后,会生成final.data,这个模型即为上表面为正弦波动的粗糙界面。新建1.py文件,粘贴下面内容后,运行:python 1.py。多晶需要在atomsk中进行,因此上述的方法就不再适用。最近有学员咨询多晶模型的正弦粗糙界面建模应该如何实现。在cmd窗口按顺序运行以下命令创建多晶模型。

lammps_jiayou的博客 390

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

堆的实现以及利用堆进行排序

堆是一种特殊的完全二叉树,分为最大堆和最小堆。在最小堆中,父节点的值总是小于或等于其子节点的值。这种特性使得堆的根节点始终是所有节点中的最小值,非常适合实现优先队列。堆中某个结点的值总是不⼤于或不⼩于其⽗结点的值;堆总是⼀棵完全⼆叉树。⼆叉树性质对于具有 n 个结点的完全⼆叉树,如果按照从上⾄下从左⾄右的数组顺序对所有结点从0 开始编号,则对于序号为 i 的结点有:若 i>0 , i 位置结点的双亲序号: (i-1)/2;i=0 , i 为根结点编号,⽆双亲结点。

zzh_Zao的博客 1838

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

Java——堆

九月在老家是收割水稻的月份,每次打完水稻,农民伯伯就会拿稻杆累成一堆。我觉得这个稻杆堆和数据结构堆外形有点相似哈。**堆是一棵完全二叉树,**但是这个完全二叉树要满足条件:其中任意一个结点要 >= 其左孩子结点和有孩子结点,这叫大根堆;其中任意一个结点要

m0_74100417的博客 1804

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

什么是堆

堆是一种特殊的树,只要满足下面两个条件,它就是一个堆: (1)堆是一颗完全二叉树; (2)堆中某个节点的值总是不大于(或不小于)其父节点的值。 其中,我们把根节点最大的堆叫做大顶堆,根节点最小的堆叫做小顶堆。 堆详解 满二叉树 满二叉树是指所有层都达到最大节点数的二叉树。比如,下面这颗树: 完全二叉树 完全二叉树是指除了最后一层其它层都达到最大节点数,且最后一层节点都靠左排列。比如,下面这颗树: 可见,其实满二叉树是一种特殊的完全二叉树。 那么,使用什么结构存储完全二叉树最节省空

每天叫醒自己的不是闹铃 是娟 是梦 是理想。 8204

【堆】堆的基本概念及操作,堆与优先队列

Heap;

2401_87820834的博客 2215

【数据结构】【堆】堆的建立、插入和删除

目录堆的介绍用数组表示堆,如何区分父节点和子节点?堆的种类?堆的插入、删除操作几个基本操作堆的插入堆的删除 感谢简书[唐先僧]的博文数据结构:堆(Heap),本博文有部分借鉴内容。 堆的介绍 堆是一个有固定顺序的完全二叉树,通常用数组来表示。 用数组表示堆,如何区分父节点和子节点? 下图是一个堆常用的编号方式示意图:根节点编号为0,根节点的左子节点编号为1、右子节点编号为2,再往下是3、4、5、6……即按照从上往下、从左往右的顺序编号。 在数组中,就按照如上所述的下标进行保存,如上图保存为数组就是{10,

怕走崎岖路,莫想登高峰 8991

数据结构中的堆--堆的定义、调整堆、建堆、自定义堆

一、堆的定义 1.结构特点 堆(数据结构)和Java语言中提到的堆没有一点关系。 逻辑上:完全二叉树 物理上:数组 堆是一种顺序存储结构(采用数组方式存储),仅仅是利用完全二叉树的顺序结构的特点进行分析。 2.结点下标计算公式(根节点从0开始) 已知二叉树根结点的下标是root,那么它左孩子的下标left=2*root+1,右孩子的下标right=...

smell201611010513的博客 1万+

【堆】堆排

相信看到这篇文章的柚柚们对于堆这个数据结构有啦一定的了解,至少清除堆是什么啦,当然可能有些柚柚们还不了解堆,可以去看博主之前写的关于堆(Heap)的博客喔链接在这里点击即可~堆排:顾名思义是利用堆这种数据结构所设计的排序算法。

2301_80085602的博客 1049

堆的性质、堆的实现、堆排序

目录 一、堆的性质 二、堆的实现 1. 存储方式 2. 向堆中插入元素 3. 删除堆顶元素 4. 其他操作 5. 完整代码 三、堆排序 1. 原地建堆 2. 排序 3. 完整代码 4. 建堆的复杂度:O(n) 5. 堆排序的复杂度:O(nlogn) 6. 和快速排序的比较 一、堆的性质 堆是一种特殊的树。 只要满足以下两点,它就是一个堆: 堆是一个完全二叉树; 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。 第一点,堆必须是一个完全二叉树。完全二.

马小超 5631

【堆】二分堆的实现以及STL中的堆

堆的概念binary_heap就是一种完全二叉树,也就是说整棵二叉树除了最底层的叶节点之外,是填满的,而最底层的叶节点由左至右又不得有空隙。底层实现就是一个数组 vector。 如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki <= K2*i+1 且 Ki<= K2*i+2(Ki >= K2*i+1 且

菜鸟成长记 2323

斐波那契堆

斐波那契堆 自《算法导论》 一、斐波那契堆结构 二、可合并堆操作 三、关键字减值和删除一个结点

大树先生的博客 2396
上一篇: Avoiding tar's (or any other commandline's) argument list too long error
下一篇: Linux下推荐应用程序列表
nomad2
博客等级 码龄21年 163粉丝 420原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值