斐波那契堆有两种用途。第一种,斐波那契堆支持一系列操作,这些操作构成了“可合并堆”。第二种,斐波那契堆的一些操作可以在常数时间(摊还)内完成,使得这些数据结构非常适合用于频繁调用这些操作。
可合并堆可以实现以下操作:
- MAKE-HEAP ( ) \text {MAKE-HEAP}() MAKE-HEAP():创建一个新的可合并堆,堆中不含任何元素。
- INSERT ( H , x ) \text {INSERT}(H, x) INSERT(H,x):在可合并堆 H H H 中插入 x x x。
- MININUM ( H ) \text{MININUM}(H) MININUM(H):返回堆 H H H 中的最小元素。
- EXTRACT-MIN ( H ) \text{EXTRACT-MIN}(H) EXTRACT-MIN(H):删除堆 H H H 中的最小元素。
- UNION ( H 1 , H 2 ) \text{UNION}(H_1,H_2) UNION(H1,H2):合并堆 H 1 H_1 H1 和 H 2 H_2 H2,并销毁堆 H 1 , H 2 H_1,H_2 H1,H2。
斐波那契堆还支持以下两种操作:
- DECREASE-KEY ( H , x , k ) \text{DECREASE-KEY}(H,x,k) DECREASE-KEY(H,x,k):在斐波那契堆 H H H 中,修改 x x x 的关键字为 k k k,其中 k k k 需要小于 x x x 的原关键字。
- DELETE ( H , x ) \text{DELETE}(H,x) DELETE(H,x) 从堆 H H H 中删除元素 x x x。
斐波那契堆和二叉堆的时间复杂度如下表所示:
操作 | 斐波那契堆(摊还) | 二叉堆(最坏) |
---|---|---|
MAKE-HEAP | O ( 1 ) O(1) O(1) | O ( 1 ) O(1) O(1) |
INSERT | O ( 1 ) O(1) O(1) | O ( lg n ) O(\lg n) O(lgn) |
MININUM | O ( 1 ) O(1) O(1) | O ( 1 ) O(1) O(1) |
EXTRACT-MIN | O ( lg n ) O(\lg n) O(lgn) | O ( lg n ) O(\lg n) O(lgn) |
UNION | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) |
DECREASE-KEY | O ( 1 ) O(1) O(1) | O ( lg n ) O(\lg n) O(lgn) |
DELETE | O ( lg n ) O(\lg n) O(lgn) | O ( lg n ) O(\lg n) O(lgn) |
当 EXTRACT-MIN \text{EXTRACT-MIN} EXTRACT-MIN 和 DELETE \text{DELETE} DELETE 操作数量比其他操作操作数量要小很多的时候,斐波那契堆尤其适用。
很多算法(例如最短路,最小生成树)中,对于很多边稠密图,每条边都调用 DECREASE-KEY \text{DECREASE-KEY} DECREASE-KEY,从二叉堆的 O ( lg n ) O(\lg n) O(lgn),到 O ( 1 ) O(1) O(1) 是一个很大的改进。
所以像最短路这些算法,有些是可以用斐波那契堆加速的。
斐波那契堆
斐波那契堆是一些具有最小堆序的有根树的集合。(一大堆最小堆的集合)
每个节点 x x x 包含一个指向父亲的指针(可以用数组 f a t h e r [ x ] father[x] father[x],但指针可能好理解点) x . p x.p x.p 和指向他某个孩子(以后会知道有什么用) x . c h i l d x.child x.child。
x x x 的所有孩子被链接的成一个环形双向链表,称这个链表为 x x x 的孩子链表, x x x 的一个孩子 y y y 有两个指针 y . l e f t y.left y.left, y . r i g h t y.right y.right 分别指向左右兄弟。当 y y y 是仅有的孩子的时候, y . l e f t = y . r i g h t = y y.left = y.right = y y.left=y.right=y。
环形双向链表应用在斐波那契堆中,组要有两个原因:
- 可以在 O ( 1 ) O(1) O(1) 的时间内从任意位置插入或删除一个节点
- 可以在 O ( 1 ) O(1) O(1) 时间内合并两个环形双向链表。
斐波那契堆中每个节点 x x x, x . d e g r e e x.degree x.degree( x x x 的度)表示节点 x x x 的孩子链表的大小(孩子的数量)。布尔值 x . m a r k x.mark x.mark 表示节点 x x x 在最近一次成为另一个节点的孩子后,有没有失去过孩子(还是一样的,后面就知道用途了)。
斐波那契堆 H H H 中, H . m i n H.min H.min 是一个指向斐波那契堆的具有最小关键字的节点的指针,我们将这个节点称为最小节点,如果斐波那契堆中没有节点, H . m i n = NIL H.min=\text{NIL} H.min=NIL, H . n H.n H.n 表示当前斐波那契堆中节点个数。
斐波那契堆中,所有树的根都用 l e f t left left, r i g h t right right 链成环形双向链表,这个链表被称为斐波那契堆的根链表。因此,指针 H . m i n H.min H.min 指向根链表中关键字最小的节点,根链表中的树次序可以任意。
势函数
我们将用势函数的方法来分析斐波那契堆的操作性能,对于一个斐波那契堆 H H H, t ( H ) t(H) t(H) 表示 H H H 的根链表的元素个数,用 m ( H ) m(H) m(H) 表示 H H H 中已经标记的节点( x . m a r k x.mark x.mark 为 t r u e true true 的节点)的数目。
则斐波那契堆
H
H
H 的势函数
Φ
(
H
)
\Phi(H)
Φ(H) 如下:
Φ
(
H
)
=
t
(
H
)
+
2
m
(
H
)
\Phi(H) = t(H) + 2m(H)
Φ(H)=t(H)+2m(H)
最大度数
可合并堆操作
斐波那契堆上的一些可合并堆操作要尽可能长的延后执行。不同的操作可以进行性能平衡。
例如:如果从空的斐波那契堆开始,连续插入 k k k 个节点,这时候斐波那契堆是一个包含 k k k 个节点的环形双向链表。我们在斐波那契堆 H H H 上执行 EXTRACT-MIN \text{EXTRACT-MIN} EXTRACT-MIN 操作,移除 H . m i n H.min H.min 后,我们还需要 O ( k − 1 ) O(k-1) O(k−1) 的时间去遍历根链表寻找 H . m i n H.min H.min。下面将看到,我们可以在执行 EXTRACT-MIN \text{EXTRACT-MIN} EXTRACT-MIN 之后,合并一些树以减小根链表规模。
创建一个新斐波那契堆
MAKE-FIB-NEAP \text {MAKE-FIB-NEAP} MAKE-FIB-NEAP 过程分配并返回一个斐波那契堆 H H H,其中 H . n = 0 H.n=0 H.n=0 和 H . m i n = HIL H.min=\text{HIL} H.min=HIL, H H H 中不存在树, t ( H ) = m ( H ) = Φ ( H ) = 0 t(H)=m(H)=\Phi(H)=0 t(H)=m(H)=Φ(H)=0。
插入一个节点
下面伪代码将节点 x 插入斐波那契堆 H H H 中,假设该节点已经被分配, x . k e y x.key x.key 已经被赋值。
FIB_HEAP_INSERT(H,x)
x.degree = 0//初始化x属性
x.p = NIL
x.child = NIL
x.mark = FALSE
if H.min == NIL//检测H是否为空
create a root list for H containing just x
H.min = x//使x称为H根链表中唯一节点,将H.min指向x
else insert x into H's root list
if x.key < H.min.key
H.min = x//更新H.min
H.n = H.n + 1
为确定 FIB-HEAP-INSERT \text{FIB-HEAP-INSERT} FIB-HEAP-INSERT 的摊还代价,设 H H H 是输入的斐波那契堆, H ′ H' H′ 是结果斐波那契堆。那么 t ( H ′ ) = t ( H ) + 1 t(H') = t(H) + 1 t(H′)=t(H)+1 和 m ( H ′ ) = m ( H ) m(H') = m(H) m(H′)=m(H),并且势的增加量为
( ( t ( H ) + 1 ) + 2 m ( H ) ) − ( t ( H ) + 2 m ( H ) ) = 1 ((t(H)+1)+2m(H))-(t(H)+2m(H)) = 1 ((t(H)+1)+2m(H))−(t(H)+2m(H))=1
由于实际代价为 O ( 1 ) O(1) O(1),因此摊还代价为 O ( 1 ) + 1 = O ( 1 ) O(1) + 1 = O(1) O(1)+1=O(1)。
寻找最小节点
斐波那契堆的最小节点可以直接通过指针 H . m i n H.min H.min 得到。由于 Φ ( H ) \Phi(H) Φ(H) 没有变化,所有时间复杂度为 O ( 1 ) O(1) O(1)
合并斐波那契堆
下面伪代码合并斐波那契堆 H 1 H_1 H1 和 H 2 H_2 H2,并在该过程中销毁 H 1 H_1 H1 和 H 2 H_2 H2。它简单的连接 H 1 H_1 H1 和 H 2 H_2 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 //链接H1,H2根链表
if(H1.min == NIL) or (H2.min != NIL ans H2.min.key < H1.min.key)
H.min = H2.min //设定最小节点
H.n = H1.n + H2.n //计算H.n
return H//返回斐波那契堆H
势函数变化为:
Φ
(
H
)
−
(
Φ
(
H
1
)
+
Φ
(
H
2
)
)
=
(
t
(
H
)
+
2
m
(
H
)
)
−
(
(
t
(
H
1
)
+
2
m
(
H
1
)
)
+
(
t
(
H
2
)
+
2
m
(
H
2
)
)
=
0
\Phi(H)-(\Phi(H_1)+\Phi(H_2))=(t(H)+2m(H))-((t(H_1)+2m(H_1))+(t(H_2)+2m(H_2)) = 0
Φ(H)−(Φ(H1)+Φ(H2))=(t(H)+2m(H))−((t(H1)+2m(H1))+(t(H2)+2m(H2))=0
所有
FIB-HEAP-UNION
\text{FIB-HEAP-UNION}
FIB-HEAP-UNION 的摊还代价等于实际代价
O
(
1
)
O(1)
O(1)。
部分参考于:
《算法导论》第19章 - 斐波那契堆