几种二叉可并堆(详细)

可并堆与二叉堆 •定义: –二叉堆满足堆特性:父结点的键值总是大于或等于(小于或等于)任何一个子结点的键值,且每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆) –完全二叉树,结点i的儿子是2*i和2*i+1,父亲是i/2; –当且仅当它满足以下两个条件之一时,才能称之为堆 •(1)a[i]i*2]且a[i]i*2+1](即小根堆,结点的值不大于两个儿子的值) •(2)a[i]>=a[i* 阅读详情

大家好,这是我在优快云上的第一篇文章,如果有什么不恰当的地方,请各位大神们指出。 

最近都在学可并堆去了,挺有感想,所以给广大朋友们分享一下。

今天就讲讲3种吧:左偏树,斜堆,随机堆

声明:

1.转载请声明出处

2.此文章中所有代码是本人亲手编写,如有雷同,纯属巧合

我们先来了解一下堆:

支持在(lg n) 时间内完成以下操作的数据结构:

1.插入一个值

2.取出其中的最小(大)值

这显然就是二叉堆的工作了,那么可并堆是干啥的?普通的二叉堆不就行了吗?

下面这个操作就是二叉堆无法完成的了:

3.将两个堆合并

这时,二叉堆就必须一个一个节点的插入,时间为O(n lg n)

所以,可并堆开始发挥它的神奇作用了!下面为大家一一讲解:

1.斜堆

先来看看它的ADT:

struct SkewHeap{
	SkewHeap* lch;
	SkewHeap* rch;
	int key;
	SkewHeap(int n=0){
		key=n;
		lch=rch=0;
	}
};

怎么好像和二叉堆没啥区别?别急,我们慢慢来。

它和二叉堆的区别:

这里用了链表的储存方式,而不是普通的堆方式,因为斜堆需要合并,所以并不是平衡的,而且有可能一边重一边轻,这样,我们就写出了第一个合并的代码(递归进行):

SkewHeap* Merge(SkewHeap*& a,SkewHeap*& b){
	if(a==NULL) return b;
	if(b==NULL) return a;
	if(a->key>b->key) Swap(a,b);
	a->rch=Merge(a->rch,b);
	return a;
}
这里的意思就是,如果a或b有一个是空的,就返回另一个

否则在a,b中选一个较小的,让后再把他的右儿子和b合并(递归),直到a的右儿子为空后,将它的右儿子设为b

思路很简单,代码很短,但是效率呢?

我们很清楚地意识到,这样会使得整个堆的右儿子非常大,左儿子却几乎为空,非常不平衡,怎么办?

有一种很好的办法:每次插入完之后就交换左右儿子

SkewHeap* Merge(SkewHeap*& a,SkewHeap*& b){
	if(!a) return b;
	if(!b) return a;
	if(a->key>b->key) Swap(a,b);
	a->rch=Merge(a->rch,b);
	Swap(a->lch,a->rch);
	return a;
}

怎么样,很巧妙吧,第一次与右边边和并,下一次就是左边了,所以这时效率会大大提升(右儿子的高度决定了合并它的效率)

而且一般来说,合并后的右儿子应该是要比左儿子要大的,所以斜堆就把他们就交换了,把较小的放在右边(虽然情况往往不是这样)

那么时间复杂度是多少呢?

最坏情况,整个堆成一条链,时间O(n)

但是别紧张,这种情况在有了那条Swap语句之后根本不会有,其实它的均摊复杂度才为O(lg n) ,最坏也不超过O(4lg n) (具体证明参见《Data Structure and Problem Solving Using Java Second Edition》(Mark Allen Weiss著,电子工业出版社出版)中的784页的Theorem 23.2。

合并讲完了,那么插入和取出最小节点呢?

插入:建立一个新堆n,只包含你要插入的那个元素,让后Merge(root,n)

void Insert(SkewHeap*& root,int k){
	SkewHeap* n=new SkewHeap(k);
	root=Merge(root,n);
}

取出最小节点:直接将根的左右儿子合并即可,Merge(root.lch,root.rch)

int DeleteMin(SkewHeap*& root){
	//if(!root) return 0x80000000;
	int k=root->key;
	root=Merge(root->lch,root->rch);
	return k;
}
贴个完整的代码:

#include<stdio.h>
struct SkewHeap{
	SkewHeap* lch;
	SkewHeap* rch;
	int key;
	SkewHeap(int n=0){
		key=n;
		lch=rch=0;
	}
};
template<typename _>
void Swap(_& a,_& b){
	_ c=a;a=b;b=c;
}
SkewHeap* Merge(SkewHeap*& a,SkewHeap*& b){
	if(!a) return b;
	if(!b) return a;
	if(a->key>b->key) Swap(a,b);
	a->rch=Merge(a->rch,b);
	Swap(a->lch,a->rch);
	return a;
}
void Insert(SkewHeap*& root,int k){
	SkewHeap* n=new SkewHeap(k);
	root=Merge(root,n);
}
int Min(SkewHeap* root){
	return root->key;
}
int DeleteMin(SkewHeap*& root){
	int k=root->key;
	root=Merge(root->lch,root->rch);
	return k;
}
int main(){
	SkewHeap *s;int n;
	scanf("%d",&n);
	while(n--){getchar();
		char c=getchar();
		if(c=='A') {
			int a;
			scanf("%d",&a);
			Insert(s,a);
		} 
		else {
			printf("%d\n",DeleteMin(s));
		}
	}
}


2.左偏树

我们再来想想这句话:
而且一般来说,合并后的右儿子应该是要比左儿子要大的,所以就交换了,把较小的放在右边(虽然情况往往不是这样)
如果左儿子特别大,右儿子很小,那么交换就不划算了,所以这时该怎么办呢?
我们记录一下哪边大哪边小不久行了吗?
所以,左偏树诞生了。
左偏树:
基本结构与斜堆一样,只是左偏树的每个结点多了一个距离值NPL(Null Path Length)即该结点一直向右儿子走,到达空结点的距离。

左偏树的ADT:

struct LeftHeap{
<span style="white-space:pre">	</span>LeftHeap* lch;
<span style="white-space:pre">	</span>LeftHeap* rch;
<span style="white-space:pre">	</span>int key,npl;
<span style="white-space:pre">	</span>LeftHeap(int n=0){
<span style="white-space:pre">		</span>key=n;npl=1;
<span style="white-space:pre">		</span>lch=rch=0;
<span style="white-space:pre">	</span>}
};

那么维护了npl之后,我们就不用再乱交换了,而且保证了左儿子大于右儿子的npl,所以效率又会提升。
那么怎么维护npl呢?我们来看看新的Merge:
LeftHeap* Merge(LeftHeap*& a,LeftHeap*& b){
	if(!a) return b;
	if(!b) return a;
	if(a->key>b->key) Swap(a,b);
	a->rch=Merge(a->rch,b);
	if(!a->lch||a->lch->npl<a->rch->npl)
		Swap(a->lch,a->rch);
	if(!a->rch) a->npl=0;else
	a->npl=a->rch->npl+1;
        return a;
}
让我来解释一下:
首先,如果左子树为空,那么把左右交换必然不会使右子树变高,所以效率提高
第二,如果左子树的npl比较小,那么交换后必然使右子树的高度变小,所以效率提高
让后维护,如果右子树为空,npl为0,否则为右子树的npl+1。

插入和取出最小节点:什么?和斜堆的一模一样啊,这个就不用讲了吧。
效率呢?
npl值的引入能保证树左偏,使之不会退化成斜堆那样的最坏情况,保证了的最坏情况下的效率
由于每次merge 时都是将一棵树的右子树和另一棵树合并,利用NPL值能保证分解的次数不会超过log(n1+1) + log(n2+1) -2,其中N1和N2分别为左偏树A和B的结点个数,因此最坏情况下的时间复杂度为O(log n1+log n2)
但是,有个问题,它的常数变大了,难道不会影响效率吗?这个后面再讲。
贴个完整代码:
#include<stdio.h>
struct LeftHeap{
	LeftHeap* lch;
	LeftHeap* rch;
	int key,npl;
	LeftHeap(int n=0){
		key=n;npl=1;
		lch=rch=0;
	}
}* root;
template<typename _>
void Swap(_& a,_& b){
	_ c=a;a=b;b=c;
}
LeftHeap* Merge(LeftHeap*& a,LeftHeap*& b){
	if(!a) return b;
	if(!b) return a;
	if(a->key>b->key) Swap(a,b);
	a->rch=Merge(a->rch,b);
	if(!a->lch||a->lch->npl<a->rch->npl)
		Swap(a->lch,a->rch);
	if(!a->rch) a->npl=0;else
	a->npl=a->rch->npl+1;return a;
}
void Insert(LeftHeap*& root,int k){
	LeftHeap* n=new LeftHeap(k);
	root=Merge(root,n);
}
int Min(LeftHeap*& root){
	return root->key;
}
int DeleteMin(LeftHeap*& root){
	int k=root->key;
	root=Merge(root->lch,root->rch);
	return k;
}
int main(){
	LeftHeap *s;int n;
	scanf("%d",&n);
	while(n--){getchar();
		char c=getchar();
		if(c=='A') {
			int a;
			scanf("%d%d",&a);
			Insert(s,a);
		} else {
			printf("%d\n",DeleteMin(s[a]));
		}
	}
}


3.随机堆

我们再来想想这一句话:
“我们很清楚地意识到,这样会使得整个堆的右儿子非常大,左儿子却几乎为空,非常不平衡,怎么办?”
我们的斜堆用的是交换儿子,那么我们是不是有另一种想法,我们不一定每次向右儿子插入,而是向左右儿子都插入,那么怎么决定向哪边呢?答案就是:随机数。
这下我们连交换都不用了,直接用随机数就好了。
先是ADT:
struct RandHeap{
	RandHeap* lch;
	RandHeap* rch;
	int key;
	RandHeap(int n=0){
		key=n;
		lch=rch=0;
	}
};

然后就是神奇的Merge了:
RandHeap* Merge(RandHeap*& a,RandHeap*& b){
	if(!a) return b;
	if(!b) return a;
	if(a->key>b->key) Swap(a,b);
	if(rand()%2)
		a->rch=Merge(a->rch,b);
	else a->lch=Merge(a->lch,b);
	return a;
}

如果觉得长,这里还有一个短的:
RandHeap* Merge(RandHeap*& a,RandHeap*& b){
	return !(a&&b)?(a?a:b):
		(a->key<b->key?
			(rand()%2?Merge(a->lch,b):Merge(a->rch,b)):
				(rand()%2?Merge(b->lch,a):Merge(b->rch,a)));
}

贴个完整的代码:
#include<stdio.h>
#include<stdlib.h>
struct RandHeap{
	RandHeap* lch;
	RandHeap* rch;
	int key;
	RandHeap(int n=0){
		key=n;
		lch=rch=0;
	}
}* root;
template<typename _>
void Swap(_& a,_& b){
	_ c=a;a=b;b=c;
}
RandHeap* Merge(RandHeap*& a,RandHeap*& b){
	if(!a) return b;
	if(!b) return a;
	if(a->key>b->key) Swap(a,b);
	if(rand()%2)
		a->rch=Merge(a->rch,b);
	else a->lch=Merge(a->lch,b);
	return a;
}
void Insert(RandHeap*& root,int k){
	RandHeap* n=new RandHeap(k);
	root=Merge(root,n);
}
int Min(RandHeap*& root){
	return root->key;
}
int DeleteMin(RandHeap*& root){
	if(!root) return 0x80000000;
	int k=root->key;
	root=Merge(root->lch,root->rch);
	return k;
}
int main(){
	RandHeap *s;int n;
	scanf("%d",&n);
	while(n--){getchar();
		char c=getchar();
		if(c=='A') {
			int a;
			scanf("%d",&a);
			Insert(s,b);
		} else {
			printf("%d\n",DeleteMin(s));
		}
	}
}

那么效率呢?
虽然说最坏情况是O(n),但是其实均摊时间依然是O(lg n) ,而且实验中也发现有时甚至快过左偏树,所以就看人品啦。

以上就是3种可并堆的讲解,最后贴个图吧

所以说,有时常数也是很重要的。看看斜堆吧。

本人第一篇文章,难免有些错误,欢迎广大读者指出。
如果觉的文章好,请点赞哦(没赞没动力),本人一定会相继写出更好的文章。
可并堆-STL-pbds详解 对于堆的合并,我们首先想到的是朴素的nlogn出堆之后nlogn入堆。实质上,有一种更为高效的数据结构——可并堆。即为在保证堆顶极值的前提下实现logn的时间合并两个堆。与普通堆相比,左偏树的每个结点多了一个距离值NPL(Null Path Length)即该结点一直向右儿子走,到达空结点的距离。基于NPL,我们便可以发现,交换变得有序了:在保证了左儿子NPL大于右儿子NPL的时候,效率有所提升。 阅读详情

相关推荐

基于Yolo11的无人机小目标检测系统的设计与性能优化改进项目实现

该项目旨在开发一个基于YOLO11的无人机目标检测系统,能够实时识别并定位无人机拍摄过程中捕捉的小目标。考虑到无人机拍摄的目标通常较小,系统将采用特定的调优策略,以提高小目标的检测精度和召回率。

m0_73723097的博客 1205

数据结构专题-学习笔记:可并堆 - 左偏树

左偏树学习笔记。

Plozia 的小窝 480

Vue3+codemirror6实现公式(规则)编辑器

vue3+codemirror6+ts实现公式编辑器,可以插入标签、公式,也有补全提示

小阮的笔记本 5043

可并堆(左偏树)

前置废话 &amp;amp;amp;nbsp; &amp;amp;amp;nbsp; &amp;amp;amp;nbsp; &amp;amp;amp;nbsp;可并堆,顾名思义,就是一些可以合并的堆。 &amp;amp;amp;nbsp; &amp;amp;amp;nbsp; &amp;amp;amp;nbsp; &amp;amp;amp;nbsp;左偏树,顾名思义,就是向左偏的树。 &amp;amp;amp;nbsp; &amp

a_forever_dream的博客 610

微信支付开发之平台给用户转账到零钱的功能

请求消息中应包含商家批次单号、转账名称、appid、转账总金额、转账总笔数、转账openid、收款用户姓名等信息。批次已受理成功,若发起批量转账的30分钟后,转账批次单仍处于该状态,可能原因是商户账户余额不足等。【转账场景ID】 该批次转账使用的转账场景,如不填写则使用商家的默认场景,如无默认场景可为空,可前往“商家转账到零钱-前往功能”中申请。【转账总笔数】 一个转账批次单最多发起一千笔转账。【商家批次单号】 商户系统内部的商家批次单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一。

程序员weixin_sodomany的博客 3449

Linux-RGMII PHY 88E1512 驱动调试记录

1. 简介 1)开发板平台:创龙 AM5728 (tl5728-easy-evm) 2)PHY芯片:Marvell 88E1512 88E1512和88E1510都是一个系列的phy,采用marvell的通用phy驱动,驱动源码路径如下:drivers/net/phy/marvell.c 2. 应用场景 1)系统框图 AM5728连接两个PHY,PHY0的工作模式是 RGMII to Copper (RGMII到电口),PHY1的工作模式是 RGMII to SGMII。 两个PHY与处理器的连接方式都是

weixin_40407893的博客 2万+

可并堆的实现

我要写三种可并堆,然后讲解它们. 1. 二项堆2. 配对堆3. RP堆 这三种堆都有一个共同特点,就是用一棵半满的树表示子堆,用一系列子堆表示整个堆(当然子堆是有限制的).那么我们可以很方便的进行合并两个堆等操作. 1.二项堆 这三种堆里最简单的就是二项堆了(Binomial Heap).为什么称为二项堆呢?二项堆这个小婊杂特别有趣,它的每个子堆大小都为2的幂.因此,它每次合并两个大小相等...

weixin_30826095的博客 308

浅谈几种常用二叉堆合并

可合并的堆有二叉堆、二项堆、配对堆、斐波那契堆等,光是二叉堆的合并就有无脑启发式合并、左偏树、斜堆以及随机化堆合并等做法。这里主要讲的是二叉堆合并。

偶耶的博客 2840

SAP LTMC实战指南:物料主数据高效导入全解析

本文详细解析了SAP LTMC工具在物料主数据高效导入中的实战应用,包括工具选择、项目搭建、模板定制、数据导入全流程及常见问题解决方案。重点介绍了LTMC的预置模板、LTMOM灵活配置、数据校验机制等核心功能,帮助用户快速掌握SAP数据迁移技巧,提升工作效率。

weixin_33738555的博客 434

TI单芯片毫米波雷达1642代码走读(〇)——总纲

前言 近年来,自动驾驶行业发展如火如荼,雷达技术也逐渐从军工封闭圈走向了开放的市场。 毫米波雷达具有全天候探测能力,特别是在雨雪雾天气以及夜间都能可靠工作,并且探测距离相对其他车载传感器非常远,对运动目标非常敏感,价格还低廉,这些特性都使其成为自动驾驶感知层面不可或缺的一环。 现如今的雷达已经不仅仅局限于测距测速测角等传统的功能,雷达技术正在从感知走向认知、从宽带走向超宽带、从二维走向三维四维、从相控阵走向数字阵列、从单通道走向MIMO、从独立工作走向分布式协同、从分立组件走向射频数字一体化。MIMO

3万+

堆

堆是一颗特殊的树。常用于优先队列的实现 符合以下定义,即为堆: 1.是一颗树 2.树中的每个节点全都大于它的子节点(最大堆),或者每个节点全都小于它的子节点(最小堆) 堆的合并:(最小堆为例) 方法1,随意从任意一个堆叶子节点中取下一个节点,将两个堆的根结点作为这个节点的左右子节点,这个节点就是新的根结点,然后,对堆进行一次维护 方法2: //将堆顶更小的堆的右节点(堆)和另...

跨世纪逃兵的专栏 272

定位分析RCU stall问题

在编译内核时打开或者在启动参数中增加这样统计信息是在采样周期中收集的。其中number行分别是发生stalled的cpu上的硬中断,软中断,和上下文的切换次数cputime行的前三列分别是硬中断,软中断和任务消耗的cpu时间,单位为ms.最后一个数字是统计的周期时间(ms).因为用户态的任务通常不会引起RCU CPU stalls,所以任务通常是内核任务,这也是为什么只统计system cpu的原因.

happyAnger6的专栏 1993

堆python_将字典插入堆Python

我正在尝试使用(键,值)构建一个堆,因此键是一个数字,值是一个字典.import heapqheap = []dic = {'val_1': 'number_1', 'val_2': 'number_2', 'val_3': 'number_3'}insetToHeap = (2,dic)heapq.heappush(heap, insetToHeap)代码在heappush上崩溃了.元素可能不是...

weixin_39902870的博客 717

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

python设置堆大小_Python中的堆问题

Heap in python堆(英语:Heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。在队列中,调度程序反复提取队列中第一个作业并运行,因为实际情况中某些时间较短的任务将等待很长时间才能结束,或者某些不短小,但具有重要性的作业,同样应当具有优先权。堆即为解决此类问题设计的一种数据结构。逻辑定义:n个元素序列{k1,k2...ki...kn},当且仅当满足下...

weixin_39521520的博客 736

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

Heap(堆)的基础知识入门

堆 逻辑结构:                   1                  / \                 1 3                / \ / \               4  5 6 null 物理结构; 1.首先堆是一个完全二叉查找书(C...

weixin_30627341的博客 248

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

数据结构--堆

堆是一棵完全二叉树,可以O(1)O(1)取最值,O(logn)O(logn)删除最值,或插入元素。 堆的性质:每个非叶子节点比它的孩子都大(大根堆/大顶堆)/每个非叶子节点比它的孩子都小(小根堆/小顶堆)。 从上面的性质可以推出堆的根就是最值。 堆的常用操作:top返回堆顶,pop弹出堆顶,push压入新元素。 后两个操作时间是O(logn)O(logn),因为弹出或压入后都要调整堆的结构,

玫葵之蝶 309

Java JVM栈、堆、方法区的区别

栈:方法只要执行,会压栈。(存放局部变量) 堆:new出来的对象都在堆中。垃圾回收器主要针对堆。(存放实例变量/对象变量/成员变量) 方法区:类的信息,字节码信息,代码片段。(存放静态变量) 方法的代码片段放在方法区,但是方法执行过程当中需要的内存在栈中。 变量: 成员变量 局部变量 静态变量 方法: 成员方法(对象相关的方法,对象级别的方法,应该是一个对象级别的行为。) 静态方法 构造方法 实例相关的有: 实例变量 实例方法 实例变量是对象变量。 实例方法是对象方法。 ...

一只小菜鸡 413

堆,栈,队列,链表,数组

堆和栈从操作系统和数据结构中加以区别: c

YJF@HZ的博客 1770

JDK5.0中JVM堆模型、GC垃圾收集详细解析 .

前段时间在一个项目的性能测试中又发生了一次OOM(Out of swap sapce),情形和以前网店版的那次差不多,比上次更奇怪的是,此次搞了几天之后啥都没调整系统就自动好了,死活没法再重现之前的OOM了!问题虽然蹊跷,但也趁此机会再次对JVM堆模型、GC垃圾算法等进行了一次系统梳理; 基本概念 堆/Heap JVM管理的内存叫堆;在32Bit操作系统上有4G的限...

weixin_33859665的博客 92

堆和优先队列

优先队列-普通队列:先进先出 -出队顺序和入队顺序无关,和优先级相关 -操作系统划分时间片执行任务采用的就是动态优先队列优先队列的实现 - 入队 出队 普通数组 O(1) O(n) 顺序数组 O(n) O(1) 堆 O(lgn) O(lgn) -使用堆实现优先队列 对于总共N个请求 使用普通数组或者顺序数组,最差的情况:O(n^2)

我曾想浪迹天涯,却为你舍了心。 290
下一篇: poj1035
扩展的灰
博客等级 码龄12年 60粉丝 342原创
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值