大家好,这是我在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.左偏树
左偏树的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,所以效率又会提升。
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; }让我来解释一下:
#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.随机堆
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)); } } }
以上就是3种可并堆的讲解,最后贴个图吧