堆的变体:
前面的博客中我们讲到的堆的两种变体,二叉堆和二项堆,今天我们要讲的就是著名的斐波那契堆。
依然首先列出了三种堆的时间复杂的比较。
从上面能发现斐波那契堆的时间复杂度在很多操作上有优化,如insert, minimum, union , decrease-key,而extreact-min,delete没有变化。
可能看到这很多人会说为什么有这么好的斐波那契堆,我们还要去研究其他堆呢,确实,理论上来讲,对于extreact-min和delete操作想对于其他操作少的情况下,斐波那契堆确实提高了很大的效率。
但是实际中,除了某些需要管理大量数据的应用外,对于大多数应用,斐波那契堆的常熟因子和变成复杂性使得它比起普通二项堆并不是那么使用。
二项堆和斐波那契堆对于search操作的支持均比较低效;可能花费一段时间才能找到关键字。为此,涉及给定元素的操作(如DECREASE-KEY和DELETE)均需要一个指针指向这个元素,并且指针作为输入的一部分。
Fibonacci Heap(简称F-Heap)是一种基于二项堆的非常灵活的数据结构。它与二项堆不同的地方在于:
1)root list和任何结点的child list使用双向循环链表,而且这些lists中的结点不再有先后次序(Binomial Heap中root list的根结点按degree从小到大顺序,child list的结点按degree从大到小顺序);
2)二项堆中任何一颗Binomial Tree中根结点的degree是最大的,而F-Heap中由于decrease-key操作(cut和cascading cut)的缘故,并不能保证根结点的degree最大;
3) 二项堆中任何结点(degree等于k的)为根的子树中,结点总数为2^k;F-Heap中相应的结点总数下界为F{k+2},上界为2^k(如果没有 Extract-Min和Delete两类操作的话)。其中F{k+2}表示Fibonacci数列(即0,1,1,2,3,5,8,11...)中第 k+2个Fibonacci数,第0个Fibonacci数为0,第1个Fibonacci数为1。注意不像二项堆由二项树组成那样,F-Heap的 root list中的每棵树并不是Fibonacci树(Fibonacci树属于AVL树),而F-Heap名称的由来只是因为Fibonacci数是结点个数 的一个下界值。
4)基于上面的区别,若F-Heap中结点总数为n,那么其中任何结点(包括非根结点)的degree最大值不超过 D(n) = floor(lgn/lg1.618),这里1.618表示黄金分割率(goldren ratio),即方程x^2=x+1的一个解。所以在Extract-Min的consolidate操作之后,root list中的结点最多有D(n)+1。而二项堆中degree最大值不超过floor(lgn),从而root list中最多有floor(lgn)+1颗二项树。
5)另外一个与二项堆的最大不同之处在于:F-Heap是一种具有平摊意义上的高性能 数据结构。除了Extract-Min和Delete两类操作具有平摊复杂度O(lgn),其他的操作(insert,union,find- min,decrease-key)的平摊复杂度都是常数级。因此如果有一系列的操作,其中Extract-min和delete操作个数为p,其他操作 个数为q,p<q,那么总的平摊复杂度为O(p + q.lgn)。达到这个复杂度的原因有以下几点,第一,root list和任何结点的child list中使用了双向循环链表;第二,union和insert操作的延迟合并,从而在所有的可合并堆中,F-heap的合并开销O(1)最小的;第 三,decrease-key中cut和cascading cut的巧妙处理(即任何非根结点最多失去一个孩子)。
typedef int Type; typedef struct _FibonacciNode { Type key; // 关键字(键值) int degree; // 度数 struct _FibonacciNode *left; // 左兄弟 struct _FibonacciNode *right; // 右兄弟 struct _FibonacciNode *child; // 第一个孩子节点 struct _FibonacciNode *parent; // 父节点 int marked; //是否被删除第1个孩子(1表示删除,0表示未删除) }FibonacciNode, FibNode;
FibNode是斐波那契堆的节点类,它包含的信息较多。key是用于比较节点大小的,degree是记录节点的度,left和right分别是指向节点的左右兄弟,child是节点的第一个孩子,parent是节点的父节点,marked是记录该节点是否自从上一次成为另一个结点的孩子后,是否失去过孩子。true表示是去过,被标记(marked在删除节点时有用)。
typedef struct _FibonacciHeap{ int keyNum; // 堆中节点的总数 int maxDegree; // 最大度 struct _FibonacciNode *min; // 最小节点(某个最小堆的根节点) struct _FibonacciNode **cons; // 最大度的内存区域 }FibonacciHeap, FibHeap;
FibHeap是斐波那契堆对应的类。min是保存当前堆的最小节点,keyNum用于记录堆中节点的总数,maxDegree用于记录堆中最大度,而cons在删除节点时来暂时保存堆数据的临时空间。
一个斐波那契堆是一系列具有最小堆序(min-heap ordered)的有根树的集合。也就是说,每棵树遵循最小堆性质:每个结点的关键字大于或等于它的父节点的关键字,如下图:
类似于二项堆的操作,分配并返回一个Fib对象H,取H.n = 0 , H.min = NIL ,H中没有树。花费为O(1).
下面的代码是插入结点x到斐波那契堆H中,假设该结点已经被分配,并且x被赋值。时间复杂度:O(1)
如上图中,插入结点x.key=21。
1-4:初始化结点x
5-7:如果斐波那契堆为空,那么就直接将x结点插入H堆中的根链表,并且H.min指向x结点
8-10:H不为空时,首先将结点x插入到H堆中的根链表中,将x与堆中最小元素做比较,如果更小,则H.min指向x,否则不做任何改变。最后n加一。
不同于二项堆,斐波那契堆有一个指向最小结点的指针,所以操作复杂度为O(1).
下面的代码中表示合并两个斐波那契堆H1和H2,并且摧毁H1和H2。要做的仅仅是将他们的根链表结合起来,并且确定新的最小的元素。复杂度:O(1)
这个操作是本节内最复杂的一个了。 操作复杂度为O(lgn).先看
3-6:将最小结点的孩子存入到根链表中,并且从链表中删除z
7-8:如果z的右兄弟等于z的话,说明z没有孩子,且原堆中只有z一个结点,则删除z后为空。
9-11:如果不止z一个的话,那么将H.min随意指向链表中的一个元素,不一定要是链表中最小的,然后执行CONSOLIDATE(H)
对于取出最小结点的整体操作我们知道了,但对于CONSOLIDATE(H)的具体操作还完全不知,下面我们就来介绍他。
CONSOLIDATE过程要做的工作是:使每个度数的二项树唯一,也就是使每个根都有一个不同的degree值为止。对根表的合并过程是反复执行下面的步骤:
1)在根表中找出两个具有相同度数的根x和y,且key[x] <= key[y].
2) 将y链接到x:将y从根表中移出,成为x的一个孩子。这个过程由FIB-HEAP-LINK完成。
首先看伪代码:
解释下上面的伪代码:
1-3:初始化一个数组A,其中D(H.n)=lgn,因为我们要使得每个根的度数唯一,而根最多有度数最大为lgn
4-14:使得链表中的树根度数唯一
15-23:重新建立一个树根表,保存数组A[i]中的根结点,并且找出最小的元素用H.min指向该元素。
可能单纯根据伪代码还很难理解,下面我们根据具体流程图来分析。
他的操作流程可以参照下图。
a-b:就是删除最小元素3,并且将他的孩子存入根链表中
c-k:从H.min指向的根结点开始向右遍历,并按照其度数存入对应的数组A中的位置,当所存入的位置不为空,则说明存在相同度数的根结点,于是就执行FIB-HEAP-LINK操作,将他们结合起来,然后继续遍历。
l-k:找出链表中最小的元素,H.min指向该元素。
下面的伪代码是将x节点的值减少到k
5-7:当节点减少后的值小于他的父节点,那么就要进行调整,首先是执行CUT(H,x,y)操作,即将x节点放到根链表中。然后执行CASCADING-CUT(H,y)操作,即使得斐波那契堆中的树不是那么不像二项树。下面是对该操作的详细解释:
下图中是上面代码中的操作流程。
跟二项堆一样,也是利用到前面的两个操作。直接上代码了。
reference:
http://www.cs.princeton.edu/~wayne/teaching/fibonacci-heap.pdf
《Thinking in Algorithm》16.堆结构之斐波那契堆
原文地址:http://blog.csdn.net/speedme/article/details/24596671