几种二叉可并堆(详细)

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

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

今天就讲讲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
",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
",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
",DeleteMin(s));
		}
	}
}

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

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

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

本人第一篇文章,难免有些错误,欢迎广大读者指出。
如果觉的文章好,请点赞哦(没赞没动力),本人一定会相继写出更好的文章。
原文地址:https://www.cnblogs.com/Extended-Ash/p/9477384.html