zoukankan      html  css  js  c++  java
  • 几种二叉可并堆(详细)

    大家好,这是我在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种可并堆的讲解,最后贴个图吧

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

    本人第一篇文章,难免有些错误,欢迎广大读者指出。
    如果觉的文章好,请点赞哦(没赞没动力),本人一定会相继写出更好的文章。
  • 相关阅读:
    【2020-08-30】盼头这事,还是得有一点好
    【一句日历】2020年9月
    【2020-08-29】边走边想吧,少年
    【2020-08-28】欲望与能力的矛盾假象
    【2020-08-27】人生十三信条
    【2020-08-26】日复一日,年复一年
    【2020-08-25】今天七夕,男人有话要说
    【2020-08-24】处处较真,其实就是自己虚荣
    【2020-08-23】人生十三信条
    【2020-08-22】人生十三信条
  • 原文地址:https://www.cnblogs.com/Extended-Ash/p/9477384.html
Copyright © 2011-2022 走看看