斐波那契堆(Fibonacci heaps)
斐波那契堆同二项堆一样,也是一种可合并堆。斐波那契堆的优势是:不涉及删除元素的操作仅需要O(1)的平摊运行时间(关于平摊分析的知识建议看《算法导论》第17章)。和二项堆一样,斐波那契堆由一组树构成。这种堆松散地基于二项堆,说松散是因为:如果不对斐波那契堆做任何DECREASE-KEY 或 DELETE 操作,则堆中每棵树就和二项树一样;但是如果执行这两种操作,在一些状态下必须要破坏二项树的特征,比如DECREASE-KEY或DELETE 后,有的树高为k,但是结点个数却少于2k。这种情况下,堆中的树不是二项树。
与二项堆相比,斐波那契堆同样是由一组最小堆有序树构成,但是斐波那契堆中的树都是有根而无序的,也就是说,单独的树满足最小堆特性,但是堆内树与树之间是无序的,如下图。
对于斐波那契堆上的各种可合并操作,关键思想是尽可能久地将工作推后。例如,当向斐波那契堆中插入新结点或合并两个斐波那契堆时,并不去合并树,而是将这个工作留给EXTRACT-MIN操作。
一、每个结点x的域:
1) 父节点p[x]
2) 指向任一子女的指针child[x]——结点x的子女被链接成一个环形双链表,称为x的子女表
3) 左兄弟left[x]
4) 右兄弟right[x]——当left[x] = right[x] = x时,说明x是独子。
5) 子女的个数degree[x]
6) 布尔值域mark[x]——标记是否失去了一个孩子
结点ADT:
二、堆结构ADT:
对于一个给定的斐波那契堆H,可以通过指向包含最小关键字的树根的指针min[H]来访问,这个结点被称为斐波那契堆中的最小结点。如果一个斐波那契堆H是空的,则min[H] = NIL. 在一个斐波那契堆中,所有树的根都通过left和right指针链接成一个环形的双链表,称为堆的根表。于是,指针min[H]就指向根表中具有最小关键字的结点。
堆结构ADT:
1
2
3
4
5
6
7
8
9
|
//斐波那契堆ADT
struct
FibonacciHeap {
int
keyNum;
//堆中结点个数
FibonacciHeapNode * min;
//最小堆,根结点
int
maxNumOfDegree;
//最大度
FibonacciHeapNode * * cons;
//指向最大度的内存区域
};
typedef
FibonacciHeap FibHeap;
|
三、创建一个新的斐波那契堆
创建一个空的斐波那契堆,过程MAKE-FIB-HEAP 分配并返回一个斐波那契堆对象H;
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
//初始化一个空的Fibonacci Heap
FibHeap * FibHeapMake() {
FibHeap * heap = NULL;
heap = (FibHeap *)
malloc
(
sizeof
(FibHeap));
if
(NULL == heap) {
puts
(
"Out of Space!!"
);
exit
(1);
}
memset
(heap, 0,
sizeof
(FibHeap));
return
heap;
}
//初始化结点x
FibNode * FibHeapNodeMake() {
FibNode * x = NULL;
x = (FibNode *)
malloc
(
sizeof
(FibNode));
if
(NULL == x) {
puts
(
"Out of Space!!"
);
exit
(1);
}
memset
(x, 0,
sizeof
(FibNode));
x->left = x->right = x;
return
x;
}
|
四、插入一个结点
简单说就是生成一个结点x,对结点的各域初始化,赋值,然后构造自身的环形双向链表后,将x加入H的根表中。 也就是说,结点x 成为一棵单结点的最小堆有序树,同时就是斐波那契堆中一棵无序二项树。 伪代码:
FIB-HEAP-INSERT(H, x)
1 degree[x] ← 0
2 p[x] ← NIL
3 child[x] ← NIL
4 left[x] ← x
5 right[x] ← x
6 mark[x] ← FALSE
7 concatenate the root list containing x with root list H
8 if min[H] = NIL or key[x] < key[min[H]]
9 then min[H] ← x
10 n[H] ← n[H] + 1
如图是将关键字为21的结点插入斐波那契堆。该结点自成一棵最小堆有序树,从而被加入到根表中,成为根的左兄弟。
五、合并两个斐波那契堆
不同于二项堆,这个操作在斐波那契堆里非常简单。仅仅简单地将H1和H2的两根表并置,然后确定一个新的最小结点。
伪代码:
FIB-HEAP-UNION(H1, H2)
1 H ← MAKE-FIB-HEAP()
2 min[H] ← min[H1]
3 concatenate the root list of H2 with the root list of H
4 if (min[H1] = NIL) or (min[H2] ≠ NIL and min[H2] < min[H1])
5 then min[H] ← min[H2]
6 n[H] ← n[H1] + n[H2]
7 free the objects H1 and H2
8 return H
六、抽取最小结点
前边说过,对根表中的树合并是推后到EXTRACT-MIN中的,所以抽取最小这个操作比较麻烦。该过程还用到一个辅助过程CONSOLIDATE。
伪代码:
FIB-HEAP-EXTRACT-MIN(H)
1 z ← min[H]
2 if z ≠ NIL
3 then for each child x of z
4 do add x to the root list of H
5 p[x] ← NIL
6 remove z from the root list of H
7 if z = right[z]
8 then min[H] ← NIL
9 else min[H] ← right[z]
10 CONSOLIDATE(H)
11 n[H] ← n[H] – 1
12 return z
这个过程先使最小结点的每个子女都成为一个根,并将最小结点从根表中取出。然后,通过将度数相同的根链接起来,直至对应每个度数至多只有一个根来调整根表。FIB-HEAP-EXTRACT-MIN中,3~5行中使z的所有子女成为根(将他们放入根表)来从H中删除结点z,并在第6行中将z从根表中去掉。如果z为根节点中唯一的结点且没有子女,则第8行返回空即可;否则,让指针min[H]指向根表中的一个非z的结点(伪代码中为right[z])。这个min[H]只是临时值,并不是真正的最小结点。第9行之前程序执行过程如图a)~b)(一图胜千言)。
CONSOLIDATE过程要做的工作是:使每个度数的二项树唯一,也就是使每个根都有一个不同的degree值为止。对根表的合并过程是反复执行下面的步骤:
1)在根表中找出两个具有相同度数的根x和y,且key[x] <= key[y].
2)将y链接到x:将y从根表中移出,成为x的一个孩子。这个过程由FIB-HEAP-LINK完成。
伪代码:
CONSOLIDATE(H)
1 for i ← 0 to D(n[H])
2 do A[i] ← NIL
3 for each node w in the root list of H
4 do x ← w
5 d ← degree[x]
6 while A[d] ≠ NIL
7 do y ← A[d] ▹ Another node with the same degree as x.
8 if key[x] > key[y]
9 then exchange x ↔ y
10 FIB-HEAP-LINK(H, y, x)
11 A[d] ← NIL
12 d ← d + 1
13 A[d] ← x
14 min[H] ← NIL
15 for i ← 0 to D(n[H])
16 do if A[i] ≠ NIL
17 then add A[i] to the root list of H
18 if min[H] = NIL or key[A[i]] < key[min[H]]
19 then min[H] ← A[i]
FIB-HEAP-LINK(H, y, x)
1 remove y from the root list of H
2 make y a child of x, incrementing degree[x]
3 mark[y] ← FALSE
这个过程中用到的数组是哈希辅助数组A[],如果度数为i的树不存在,则A[i]为空。这个伪代码3~13行的工作就是使每个度数的二项树唯一,里边的while循环反复地将包含结点w的数的根x链接到与其相同度数的其他树根上,直到没有其他度数相同的根为止。14行清空旧的根表,第15~19行根据数组A重新构造根表,最终结果如图m)。
七、减小一个关键字
减小关键字操作最大的难点是,如果减小后的结点破坏了最小堆的性质,如何维护斐波那契堆的性质。这里用到一个操作:级联剪枝(Cascading Cut)。减小关键字的代码流程基本就是:如果减小后的结点破坏了最小堆性质,则把它切下来(cut),即从所在双向链表中删除,并将其插入到由最小树根节点形成的双向链表中,然后再从parent[x]到所在树根节点递归执行级联剪枝。
关于级联剪枝,《数据结构》中的解释:
由于增加了删除和关键字减值操作,所以,F堆中的最小树就不一定必须是二项树了。事实上,可能存在度为k却只有k + 1(原书是k + 1,应该是k – 1吧)个结点的最小树。为了保证每个度为k的最小树至少包含ck个结点(c > 1), 每次执行删除操作和关键字减值操作后,还必须进行级联剪枝操作。为此,为每个结点增加一个布尔类型的child_cut域(即本文里的marked)。child_cut域的值仅对那些不是最小树树根的结点有意义。对于不是最小树树根的结点x, x的child_cut域为TRUE,当且仅当在最近一次x成为其当前父结点的儿子之后,x的一个儿子被删除。这就意味着,在执行删除最小元素中,每次连接两棵最小树时,关键字值较大的根结点的child_cut域应该赋值为FALSE。更进一步地说,一旦删除操作或关键字减值操作将最小树的非根结点q从其所在双向链表中删除时,则调用级联剪枝操作。在执行级联剪枝操作过程中,检查从被删除结点q的父节点p开始,到被删节点的最近的child_cut域为FALSE的祖先结点的路径。对在该路径上所有child_cut域为TRUE的非根结点,将其从所在的双向链表中删除,并将其加入到F堆的最小树的根节点组成的双向链表中。如果该路径上存在child_cut域为FALSE的结点 ,则将其该域的值修改为TRUE。
伪代码:
FIB-HEAP-DECREASE-KEY(H, x, k)
1 if k > key[x]
2 then error "new key is greater than current key"
3 key[x] ← k
4 y ← p[x]
5 if y ≠ NIL and key[x] < key[y]
6 then CUT(H, x, y)
7 CASCADING-CUT(H, y)
8 if key[x] < key[min[H]]
9 then min[H] ← x
CUT(H, x, y)
1 remove x from the child list of y, decrementing degree[y]
2 add x to the root list of H
3 p[x] ← NIL
4 mark[x] ← FALSE
CASCADING-CUT(H, y)
1 z ← p[y]
2 if z ≠ NIL
3 then if mark[y] = FALSE
4 then mark[y] ← TRUE
5 else CUT(H, y, z)
6 CASCADING-CUT(H, z)
图中:a),b)46减小为5; c),d),e)35减小为5
级联剪切的过程很明了,我当时看的时候最烦的问题是,为什么要进行级联剪切,级联剪切丫的要干什么?
如果仅仅要切除父结点y的一个结点x,则仅仅需要把结点x加入到根结点所在双向链表中,再检测y是否marked == true即可,这是因为斐波那契中的树并不一定是二项树,近似二项树也可以。当删除y的第二个结点时,对在该路径上所有marked域为TRUE的非根结点,将其从所在的双向链表中删除,并将其加入到F堆的最小树的根节点组成的双向链表中,即只有在删除同一个结点偶数个孩子时,才要进行级联剪枝,来维护二项树性质,奇数个时(即一个),对树影响不大,莫管它,只标记一下即可。
为什么偶数个的时候要递归往上删除?
二项树中在深度为i处恰有Cik个结点(I = 0, 1, 2, ……, k)。试着如果不进行级联剪枝,就可以发现,稍微删得结点超过两三个,最后的树就会不成样子,毫无章法。但是如果进行了级联剪枝,在偶数个结点时进行级联剪切时,原来是C30 = 1, C31 = 3, C32 = 3, C33 = 1, 减少两个结点关键字后,变为:C20 = 0,C21 = 2, C22 = 1;二项式是对称的,所以,偶数个结点时进行级联剪枝可以保证类似上边的正好使二项式减少一个数量级,怎么也要保证最高次项是存在的。