本文改编自《算法导论》第三版第19章:斐波那契堆。文中代码为书中伪代码的C++实现,如有疏漏还请指出。
斐波那契堆的结构
斐波那契堆是一种可并堆,它支持以下操作:
$(1)$在$O(1)$时间内插入元素、获取最小值、合并两个斐波那契堆、减小元素键值;
$(2)$在$O(logn)$时间内删除元素。
斐波那契堆的平摊复杂度比其他可并堆(左偏树/二项队列)要优,但时空常数大且实现复杂度高,在实际中并不非常适用。
斐波那契堆有下图所示的结构(图示为小根堆):
可以看到斐波那契堆仍然满足小根堆性质,与二叉堆不同的是,斐波那契堆的第一层由双向循环链表组成(23和24的连接没有画出),并且每一个结点的孩子集合也由双向循环链表维护。(如:3的孩子为{18,53,38},这三个结点在一个双向循环链表中,3只存指向其中一个的指针)
每个结点有如下属性(成员变量):
1 | template<typename T> |
$data:$数据域;
$mark:$标记,$mark == true$当且仅当某个结点成为某个节点的子结点后还失去了某儿子结点,黑色节点表示$mark = true$;
$degree:$孩子个数;
$left,right:$左右兄弟;
$child:$任意的一个该结点孩子;
$parent:$该结点的父节点。
每一层结点由双向循环链表构成,为了实现方便在程序中引入表头。
斐波那契堆的成员变量:
1 | int size;//结点个数 |
斐波那契堆操作及实现
插入节点:
1 | void LeftInsert(node *base, node *tmp) |
显然,斐波那契堆的插入操作为常数复杂度(虽然该常数较大)。
合并两个斐波那契堆
1 | FibHeap<T> Merge(FibHeap<T> H2) |
对于每个第一层结点$x$,把$x$放入$H_2$的第一层结点中,同时在有必要时改变min指针。
删除最小结点
1 | T DeleteMin() |
要删除一个最小结点,首先将min指针指向的结点的所有孩子结点加入堆的第一层结点链表中,然后删除min结点(但保留其left,right结构),随后调整min指针的位置,但不要求其一定指向新的最小结点,这一步在$Consolidate$(合并)函数中完成。
Consolidate函数
Consolidate()负责调整堆的结构,使第一层结点数减小,同时确定新的最小值位置。
1 | void link(node *y,node *x)//把y插入到x下面 |
首先(程序的do-while循环以及之前)建立一个指针数组$nodes[]$,其大小为$D(n)+1$,$D(n)$为当前斐波那契堆结点的最大度数。最后(第4部分)会证明,$D(n)=$$O(logn)$,更具体地有$D(n)\le \lfloor log_2x \rfloor$。$nodes[i]$指向一个度数为$i$的结点。要完成整理,对于每个结点,设其度为$d.$若当前$nodes[d]$已经指向另一个结点,那么比较这两个结点,让根元素小的结点成为根元素大结点的父亲(即link操作),同时根小的结点的度数由于获得了一个儿子$+1(d++)$.重复操作直到找到一个$nodes[d]$为空,可以安放当前处理的结点,将$nodes[d]$指向当前节点。
在程序实现方面,注意(1)跳过表头:引入表头是为了确定一个基准,由于在调整过程中有的结点会成为其他结点的儿子,导致无法判断循环结束条件,表头可以提供一个基准;(2)在处理link时会导致一个结点断链,从而丢失要遍历的下一个结点,这时要用一个next指针在link之前标记出下一个要遍历的结点。
在堆的重构完成后,需要确立新的最小值结点。与插入类似,只需要将$nodes[]$下挂着的所有结点插入到堆中即完成整理。(同时不断更新最小值指针)
下图展示了删除最小值的过程:
(1)原斐波那契堆:
(2)执行DeleteMin()中,到Consolidate()之前的语句:(把儿子插入到第一层链表)
(3)开始执行Consolidate():
(i)从17开始向右遍历,由于$nodes[0,1,2]$均为空,分别指向23,17,24。这里用到了$Dn \le \lfloor logn \rfloor$的结论。注意数组大小应为$D(n)+1.$
(ii)遇到7,与$nodes[0]$指向的23比较,7应该成为父亲结点,成为一个度为1的结点;于是继续与$nodes[1]$指向的17进行比较,7成为父亲结点;最后与$nodes[2]$指向的24比较,7仍为父亲结点。这一系列操作后的斐波那契堆如图:
(iii)遇到18、53,直接让$nodes[1],nodes[0]$指向二者。
(iv)遇到38,和$nodes[1]$指向的18进行比较,18成为父结点被$nodes[2]$指向。
(v)最后进行整理(Consolidate()的do-while结束后),确定新的min指针。
关键字减值
关键字减值给定一个结点指针,并把它的值减小,重新调整堆的结构。
1 | void DecreaseKey(node *x, T key) |
若减值结点尚未破坏堆结构(即仍大于等于其父结点键值),则无需进行调整;否则,堆通过Cut和CascadingCut(级联切断)调整。首先,对调整节点进行切断:把它从其父亲上切断,加入第一层结点中(对应Cut函数)。
1 | void Cut(node *x, node *y)//x为子结点,y为父结点 |
之后,对父结点$y$尝试进行级联切断,它是一种使堆保持较为平衡的操作:
1 | void CascadingCut(node *y) |
若$y$有父亲结点$z$,当$y$标记为$false$时,将其标为$true$;否则,将$y$从$z$上切割下来,同时检查$z$是否需要执行该操作。(因为$z$刚失去一个孩子$y$,若这是它失去的第二个孩子,它也要被切割下来)
如下图,现在依次将堆中的46键值减少为15、将35减小为5:
(i)原堆:
(ii)将46切割下来改为15,由于24失去了某个孩子,它被标记,第一条语句执行完毕:
(iii)将35减小为5,把它从26上切断下来,加入第一层链表:
(iv)对26执行级联切断,由于它已被标记,切割并加入第一层链表;接着检查24,它也被标记,也要被切割;检查7,它没有父结点,停止。操作结果即26、24依次被加入第一层链表(标记也要被清空)。
(v)最后修改min指针,第二条语句执行完成。
删除关键字
有了减小键值和删除最小结点操作,很容易实现删除关键字操作(前提是有指向它的指针)。MIN_VALUE需要在初始化类时给出,如int型设为INT_MIN等。
1 | void Delete(node *x) |
时间复杂度分析
对各种操作进复杂度分析要用到平摊分析的势能法。
势函数
定义斐波那契堆的势函数
$\phi(H) =t(H)+2m(H)$,其中$t(H)$为第一层链表中结点个数,$m(H)$为标记结点个数。初始条件下对于空堆显然有$\phi(H_0)=0,$对任意状态下有$\phi(H)>\phi(H_0).$
插入操作
有$c_i=\alpha_i+\phi(H_i)-\phi(H_{i-1})$,其中$\alpha_i$为实际代价,后面的项为势能变化量。$\alpha_i$在插入部分已经说明为$O(1)$,而$\phi(H_i)-\phi(H_{i-1})=[t(H_{i-1})+1+2m(H_{i-1})]-[t(H_{i-1})+2m(H_{i-1})]=1$,故均摊代价$c_i=O(1)+1=O(1).$
最小结点
显然为$O(1)$.
删除最小结点
首先计算实际代价:
对于DeleteMin函数的前半部分(不包括Consolidate),有一个循环开销,大小为$O(D(n))$(这里尚未给出$D(n)$的界);
再考虑Consolidate中do-while循环的开销:在把删除结点的孩子放入第一层链表后,$t(H’)$最大为$t(H)+D(n)-1$(删去一个,至多引入$D(n)$个)。在每次do-while循环中,总有一个第一层结点被连接到另一个上,因此至多循环$O(D(n)+t(H)+1)=O(D(n)+t(H))$次。这两部分加到一起,实际开销为$O(D(n)+t(H))$.
再计算势能的变化:删除后,势能增大最大的情况是没有标记结点被删除,且$t(H)$最大变为$D(n)+1$.因此
$\Delta\phi\le[D(n)+1+2m(H)]-[t(H)+2m(H)]=D(n)+1-t(H)$;
$c_i=\alpha_i+\Delta\phi\le O(D(n)+t(H))+(D(n)+1-t(H))=O(D(n)).$
故均摊复杂度为$O(D(n))$.
关键字减值
先计算实际代价:只需计算级联切断的消耗(显然,减值和切断都是$O(1)$的)。设级联切断共调用$c$次,那么实际代价为$O(c).$
再计算势能变化量:
先考虑第一层结点个数:首先调用第一次Cut操作,为第一层链表增加了一个结点;之后$c$次级联切断中的$c-1$次(除了最后一次)都会为第一层链表增加一个结点,故$t(H’)=t(H)+c$;
再考虑使标记最多的情况:在$c-1$次前面的级联切断操作中每次固定会清除一个已有的标记,使得标记数减少$c-1$;最后一次级联切断又有可能增加一个标记,故
$m(H’)\le m(H)-(c-1)+1=m(H)-c+2.$
因此,均摊复杂度
$c_i\le O(c)+[t(H)+c+2(m(H)-c+2)]-[t(H)+2m(H)]=O(c)+(4-c)=O(1).$
删除关键字
由于该操作由前面两个操作叠加而成,易知其平摊复杂度$O(D(n)).$
最大度数的界
现在还有一个遗留问题:$Dn$的界。下面几条引理给出其证明:
引理1:设$y_i$是$x$的第$i$个被链入的孩子,$x$共有$k$个孩子($x.degree=k$),那么有$y_1.degree\ge 0,y_i.degree\ge i-2.$
<证明>显然有$y_1.degree\ge0.$在$y_i$链入$x$之前,$x.degree=i-1,$由于只有在Consolidate操作时,$y_i.degree=x.degree$,$y_i$才会被链接到$x$,因此此时$y_i.degree=i-1.$在这之后$y$至多失去一个孩子,否则它会被级联切断剪掉。故$y_i.degree\ge i-2.$
引理2:设斐波那契数列为$F_0=0,F_1=1,F_k=F_{k-1}+F_{k-2}(k\ge2),$那么有$F_{k+2}=1+\sum_{i=0}^{k}F_i.$
<证明>采用数学归纳法。
$(1)$当$k=0$时,等式成立;
$(2)$假设当$k=k_0$时,等式成立,往证$k=k_0+1$时,等式成立。
即有
$F_{k_0+2}=1+\sum_{i=0}^{k_0}F_i$,
等式两边同时加$F_{k_0+1},$有
$F_{k_0+3}=1+F_{k_0+1}+\sum_{i=0}^{k_0}F_i=1+\sum_{i=0}^{k_0+1}F_i$,得证。
引理3:$\forall k\in N,F_{k+2}\ge \phi^k,$其中$\phi$为黄金分割比$(1+\sqrt 5)/2.$
<证明>施归纳于$k$:
$(1)$当$k=0,1$时,等式显然成立;
$(2)$假设当$k \le k_0$时,有$,F_{k_0+2}\ge \phi^{k_0}.$
那么$F_{k_0+3} = F_{k_0+2}+F_{k_0+1}\ge \phi^{k_0}+\phi^{k_0-1}
=\phi^{k_0-1}(1+\phi)=\phi_{k_0-1}\phi^2=F_{k_0+3}\ge \phi^{k_0+1},$得证。
($1+\phi=1+(1+\sqrt5)/2=(3+\sqrt5)/2=$
$\phi^2=(6+2\sqrt5)/4=(3+\sqrt5)/2,$所以$\phi^2=1+\phi.$)
引理4:对于任意度为$k$的斐波那契堆结点$x$,有$size(x)\ge F_{k+2} \ge \phi^k,$其中$size(x)$为以$x$为根的子树结点总个数。
<证明>设$s_k$表示斐波那契堆中度数为$k$的最小可能$size$值,那么显然有
$(1)s_0=1,s_1=2;$
$(2)s_k$是一个单调递增数列;
$(3)$任意结点$x$,$size(x)\ge s_k.$
那么有
$size(x)\ge s_k$
$\ge2+\sum_{i=2}^{k}s_{y_i.degree}$($2$指的是$x$自身和它的第一个儿子)
$\ge2+\sum_{i=2}^{k}s_{i-2}$(由引理1和$s_k$的单调性)
于是有$s_k\ge2+\sum_{i=2}^{k}s_{i-2}.$
现证明$s_k\ge F_{k+2}:$
施归纳于$k:$
$(1)$当$k=0,1$时显然成立。
$(2)$假设当$k\le k_0$时不等式成立,那么有$s_{i}\ge F_{i+2}.(i\le k_0)$
$s_k\ge2+\sum_{i=2}^{k}s_{i-2}\ge2+\sum_{i=2}^{k}F_i=1+\sum_{i=0}^{k}F_i$
$=F_{k+2}$(引理2)
$\ge \phi^k$(引理3)
因此有$size(x) \ge s_k \ge F_{k+2} \ge \phi^k$
引理5:一个$n$个结点的斐波那契堆中任意结点的最大度数$Dn$为$O(logn).$
<证明>设$x$为斐波那契堆中任意结点,设$k=x.degree.$有$n\ge size(x) \ge \phi^k.$因此$k\le log_\phi n.$因此$D(n)=max{k}\le log_\phi n.$
至此,证明了删除操作的时间复杂度是$O(D(n))=O(logn).$但在Consolidate过程中,我们用到了更为确切的结论来确定所开数组的大小:$D(n)\le \lfloor logn \rfloor.$下面证明这一结论。(该结论在原书中作为无答案习题给出,如证明有错误还请指出)
定理:$D(n)\le \lfloor logn \rfloor.($$log$以$2$为底$)$
<证明>现在证明$s_k>2^{k-1}.$施归纳于$k$:
$(1)$$k=0,1$时,不等式显然成立。
$(2)$假设不等式在$k$时成立,即$s_k>2^{k-1},$对于$k+1$个度的结点,它显然至少需要两个大小为$s_k$的结点来构成,因此$s_{k+1}\ge 2s_k >2^k,$结论成立。
那么对于任意结点$x,n\ge size(x) \ge s_k>2^k,k<logn.$由于$k$是整数,$k\le \lfloor logn \rfloor.$因此$D(n)=max{k}\le \lfloor logn \rfloor.$
完整代码
1 | template<typename T> |