斐波那契堆
- 斐波那契堆也是数据储存结构的一种,这种数据结构之所以产生,主要有两个优点:1.对于数据合并操作具有很高的效率;2.对于其他一般数据操作平均时间复杂度较好(注意这里是平均复杂度,不是最坏情形)。
- 具体时间复杂度如下表:
- 上表可以看出,从理论上讲斐波那契堆的性能不错,但是从实际角度出发,除了某些需要管理大量数据的应用外,对于大多数应用,斐波那契堆的常数因子和编程的复杂性相对于二叉堆的性能较差,所以对于斐波那契堆的研究只是理论的研究(其实看到这句话的时候这一章我就不想看了)。
斐波那契堆的结构
- 其实斐波那契堆就是一系列最小堆的集合,换句话说就是斐波那契堆中每棵树对满足最小堆的性质,这里我顺便介绍一下什么是最小堆和最大堆。
- 最小堆:堆中所有的节点的关键字都大于其父节点的关键字。
- 最大堆:堆中所有的节点的关键字都小于其父节点的关键字。排序中有一种排序叫做堆排序就是基于最大堆来实现的。
- 下面我给出一棵具体的斐波那契堆结构(黑色是被标记的节点),便于理解:
- 斐波拉契树的每个节点具有如下属性:(1)x.degree储存节点的孩子数目(2)bool属性x.mark指示节点x自从成为别人新的子节点之前有没有失去过子节点,初始设置为false。(这个属性的作用可以看后面。)
- 从上图可以看出斐波那契堆还有一个指向堆最小值的指针H.min,该指针指向具有最小值关键字的根节点,我们把这个节点叫做斐波那契堆的最小节点;其次还有一个属性为H.n,用来表示斐波那契堆中节点的数目。
- 在开始介绍斐波那契堆的优点时,我说过具有较好的平摊时间复杂度,怎么来评判平摊时间复杂度??需要通过平摊分析来对每个操作的时间复杂度进行平摊,这里使用常用的势函数来对其进行分析。
势函数分析
使用势函数分析斐波那契堆的操作性能,首先我们给出势函数的表达式:
式中H表示一个斐波那契堆,t(H)表示H中树的数目,m(H)表示H中树标记节点的数目。(势函数为什么是这样,我也很纳闷,往下看吧)。堆操作
- 对堆的操作主要有6种:
- 创建一个空堆
- 插入一个节点
- 寻找最小节点
- 合并堆
- 抽取最小节点
- 关键字减值
- 删除关键字
- 上述操作中,抽取最小节点和关键字减值理解起来比较难,其他操作相对简单。
创建一个空堆
- 创建一个空堆MAKE-FIB-HEAP只需要如下三个操作:(1)为对开辟空间(2)将H.n=0(3)H.min=NULL
- 有势函数可知,创建一棵新堆的节点数为0,标记节点数目为0,,故势为0。摊还代价为
。
插入一个节点
- 在斐波那契堆中插入一个节点不是在树中插入,而是在根链表中插入,换句话说就是在堆中插入了一棵树。具体情况看下图:
- 下面来看插入的伪代码:
FIB-HEAP-INSERT(H,x) x.degree=0 x.p=NIL x.child=NIL x.mark=FALSE if H.min=NIL creat a root list for H contain just x H.min=x else insert x into H's root list if(x
- 上面代码主要就做了两件事情:一件是对节点x进行初始化,另一件事将节点x插入到根链表中。插入到根链表中也分为两种情况:堆为空和堆不为空,然后根据不同情况在堆中插入元素。
- 接下来利用势函数分析插入操作的时间复杂度,插入操作堆的数目加1,标记节点数目不变,则有:t(H')=t(H)+1,m(H')=m(H)故势的增加量为1,故摊还代价为
。
寻找最小节点
- 由于堆中有一个指针指向堆中最小值,所以找到最小节点的时间复杂度为O(1)。
堆的合并
- 在合并堆H1和H2的时候,简单的将H1和H2的根链表连在一起,确定最小节点就可以了,同时销毁堆H1,H2,H1和H2将不再使用。
FIB-HEAP-UNION(H1,H2) H=MAKE-FIB-HEAP() H.min=H1.min concatenate the root list of H2 with the root list of H if(H1.min==NIL)or(H2.min!=NIL and H2.min.key
在合并堆操作中,其实相对而言堆节点的数目和堆标记节点的数目其实并没有发生改变,所以势增加0,时间复杂度为O(1).
抽取最小节点
![]()
![]()
抽取最小节点的主要过程通过上述示意图可以更加容易理解整个过程,下面通过伪代码的方式来介绍抽取最小节点的整个过程:FIB-HEAP-EXTRACT-MIN(H) z=H.min if(z!=NIL) //如果树不为空 for each child ofz add x to the root list of H // 将z每个子节点放入根链表中 x.p=NIL //给子节点的父节点赋空 remove z from the root list of H //将最小根节点从根链表中移除 if z==z.right //如果树中只有z一个节点 H.min=NIL else //如果树中有多个节点 H.min=z.right //将树中最小值暂时赋值为z的右兄弟节点 CONSOLIDATE(H) //维护树的性质 H.n=H.n-1 //树的节点数 return z
有上伪代码可知,抽取最小节点主要是分为三个部分操作:1.将最小节点的子节点放入堆的根链表中,并删除最小节点2.选取最小节点的右兄弟节点作为最小节点3.对堆的性质进行维护。而对对的性质进行维护又有如下几个原则,伪代码如下所示:CONSOLIDATE(H) let A[0,...D(H,n)]be a new array //记录每棵树的度 for i=0 to D(H,n) //数组初始化 A[i]=NIL for each node w in the root list of H //遍历每个树节点 x=w d=x.degree //d表示树x的度 while A[d]!=NIL //寻找度相同的树 y=A[d] //此时y为以前遍历节点,x为当前遍历节点,两者度相同 if(x.key>y.key) //通过比较权重来确定合并,权重大的合并到权重小的树中 exchange x with y FIB -HEAP-LINK(H,y,x)//合并树 A[d]=NIL //将其赋空 d=d+1 //合并之后度加1,继续循环向上循环 A[d]=x //储存产生的新树的度 H.min=NIL for i=0 to D(H,n) //创建新的根链表 if A[i]!=NIL if H.min==NIL //初始情况创建一条根链表 creat a root list for H containing just A[i] H.min=A[i] else //在根链表中进行插值 insert A[i] into H's root list if A[i].key
堆性质的维护主要是树度的维护,就是斐波那契堆中每棵树的度不能相同,如果有两棵树的度相同这将这两棵树进行合并,合并的原则是根节点权重大的合并入权重小的树中,并对并入的树节点进行标记。具体过程可以看伪代码和示意图,应该比较好理解。
- 下面主要的任务是证明n个节点的斐波那契堆抽取最小节点的摊还代价为O(D(n)),这部分不想看的不看,不然会晕,开始我也晕,晕多了现在习惯了!
- 首先我们分析FIB-HEAP-EXTRACT-MIN(H)的伪代码,这段伪代码是将最小节点的子节点提上根链表中,最小节点最多有D(n)个子节点,CONSOLIDATE(H)对数组A初始化操作和创造新根链表处理的元素也只有D(n)个,所以这里所需的时间代价为O(D(n)),对于树的合并这一块根链表的大小最大为D(n)+t(H)-1,所以数合并节点的时间复杂度为O(D(n)+t(H))。
- 由势函数可知,抽取最小节点之前的势函数为t(H)+2m(H),因为最多有D(n)+1个根节点留下来,且该过程没有任何节点被标记,所以抽取最小节点之后的的势最大为D(n)+1+2m(H),所以摊还代价为:
- O(D(n)+t(H))+D(n)+1+2m(H)-t(H)-2m(H)=O(D(n)),后面会得到D(n)为O(lg(n)),所以抽取最小节点的摊还代价为O(lg(n))。
关键字减值
下面介绍在O(1)时间内减小斐波那契堆中节点关键字的值。同上,我们还是在伪函数的基础上对该操作过程进行分析,伪函数代码如下:FIB-HEAP-DECREASE-KEY(H,x,k) if(k>x.key) //保证赋值树比节点x的关键字小 error "new key is greater than current key" x.key=k //对x的关键字进行赋值 y=x.p //取出x的父节点 if y!=NIL and x.key
- 总的来说关键字减值主要分为两个部分:1.将节点x的关键字进行更改,判断关键字更改后是否满足最大堆的要求,即该节点的关键字要大于其父节点。如果该条件不满足,则将该节点与父节点切断,放入根链表中。2.同时对父节点进行标记,如果父节点mark为false,则标记为true,如果父节点mark为true,则说明父节点以前可能被切断过子节点,这此时需要重复上述过程,将父节点与爷节点(名字不好听,知道意思就行)进行切断,向上回溯,知道节点没有被标记过为止。
- 对于第二步骤是不是有点迷惑,按照我的理解就是这个操作本质上没有用,唯一的作用就是建立势函数,通过势函数来说明该操作的平摊代价为O(1),所以看不懂的可以忽略。
- 下面对该操作的时间复杂度进行分析,不要看不起这部分内容,如果你仅仅想当一个码农,当然你可以忽略,知道这是啥就行,但是你真的想设计一个算法,你还是老老实实掌握这部分知识,不然不可能设计出一个好的算法,这是我的想法。闲话不多说,来点干货,首先根据伪函数可以知道,FIB-HEAP-DECREASE-KEY(H,x,k)函数的时间复杂度为O(1),CUT(H,x,y)函数的时间复杂度为O(1),假设FIB-HEAP-DECREASE-KEY(H,x,k)要调用c次,c的最大值为堆树最大的度,这是一个常数,所以CUT(H,x,y)的时间复杂度为O(c),同理CASCADING(H,y)的时间复杂度也为O(c),则该操作的时间复杂度为O(c)。
- 下面来进行势的计算,操作完成后堆中树的数目为t(H)+c,而堆中标记节点数目为m(H)-c+2(这里c-1个节点被提到根链表中,但是最后一个节点又多标记了一个)。
- 势函数方程就可以写出来了:((t(H)+c)+2*(m(H)-c+2))+O(c)-t(H)-2*m(H)=O(c)+4-c=O(1)
- 可以看出,该操作的平摊代价为O(1)
删除节点
有了上面的基础操作,删除一个节点就变得非常简单了,我们假定堆中元素关键字均不为无穷小,我们可以给删除节点的关键字赋值为无穷小,之后再用过删除最小值的方法把该元素删除。伪代码如下:到这里为止,斐波那契堆的所有操作就讲完了,里面涉及到了一些平摊分析的内容,不懂的可以查找一些资料。FIB-HEAP-DELETE(H,x) FIB-HEAP-DECREASE-KEY(H,x,-99999) //赋值为无穷小 FIB-HEAP-EXTRACT-MIN(H) //删除最小值