第20章 斐波那契堆
最后更新于:2022-04-01 07:35:58
# 一、综述
### 1.斐波那契堆
斐波那契堆是可合并堆
在不涉及删除的操作(除去EXTRACT和DELETE)中,操作仅需O(1)的平摊运行时间
当EXTRACT和DELETE的操作数目较小时斐波那契堆能得到较好的运行效率。
斐波那契堆不能有效地支持SEARCH操作
用于解决诸如最小生成树和寻找单源最短路径等问题的快速算法都要用到斐波那契堆。
### 2.斐波那契堆的结构
斐波那契堆由一组最小堆构成,这些最小堆是有根的无序树。
结点结构:
key: 关键字,作为排序、判断结点大小的标准
left, right:用于维护双链表,所有的根结点形成一个双链表,每个结点的孩子们形成双链表
parent, child : 维护父子关系
mark : 这个域与维护堆结构无关,只与具体的算法策略有关,不在这里讲
degree: 记录该结点有几个孩子
斐波那契堆
n:堆中结点的个数
min:批向最小的结点
### 3.可合并堆
引理19.1中给出的二项树的性质对无序二项树仍然成立P278
有n个结点FibHeap,结点的最大度数D(n) = lgn(向下取整)
将合并堆的操作尽可能地推后
### 4.最大度数的界
在一个包含n个结点的斐波那契堆中,结点的最大度数D(n)为O(lgn)
# 二、理解
### 1.延迟合并操作
FIB-HEAP-INSERT和FIB-HEAP-UNION只是最基础的链表合入操作,因为合并操作要尽可能地拖后
FIB-HEAP-EXTRACT-MIN除了要完成本职工作外,还要作合并调整
### 2.合并调整操作
CONSOLIDATE是作合并调整的函数
它将度数相同的根链接起来,直到对应每个度数至多只有一个根
遍历每一个根结点去判断,如果两个根结点的度是一样的,让大的结点作为小的结点的孩子
### 3.mark的作用
为了防止堆太宽,需要策略来调整堆,使根结点成为别的根结点的孩子,该策略就是CONSOLIDATE
同理,为了防止堆太深,也需要有相应的策略去调整,在适当的时候,把某个结点的孩子变成根
这一策略就是CUT和CASCADING-CUT,mark在实现这一策略的过程中起到辅助作用。
原理:当一个非根结点被切掉了2个孩子,就把它升为根结点
在删除一个结点时,怎么区分是第一个被删除的孩子,还是第二个?此时需要用mark来标记
### 4.P300那句话
因为翻译不好,严重影响理解
一旦第二个孩子也失掉后,x与其父结点之间的联系就被切断了,并成为一个新根。
原文:As soon as the second child has been lost, we cut x from its parent, making it a new root.
# 三、改进
### 1.命名
mark的命名不能体现它的作用,影响理解,如果换一个好一点的名字,就不用那么大段的文字去说明
外部函数不需要FIB-HEAP-这样的前缀,因为本来就是为它写的接口
内部函数的名字要说明函数的作用,因为内部函数是被自己调用的,不要给自己添麻烦
### 2.分解函数
提取了一些对双链表的常用操作
### 3.合并函数
CUT和CASCADING-CUT合并成一个函数,因为它们其实是一个功能,就是根据策略把孩子结点升为根结点
### 4.参数和返回值
CUT和CASCADING-CUT中的x和y是父子关系,而且重点是子,父是只为了方便处理,不需要作为参数传进来,在函数里面重新获取一个就可以了。多传一个函数,就一个出错源
对于带参数的函数,增加一返回值。用于告知调用者是否成功,或什么原因导致失败
### 5.功能
FIB-HEAP-DECREASE-KEY和FIB-HEAP-DELETE这两个函数作用不大。
因为它们的入参是node*。要想调用这两个函数,就必须先获取目标结点的指针。
可是没有一个接口返回指向结点的指针,怎么找到我的目标结点的指针呢?
调用者必须自己在创建结点后保持这个结点,这样不合理
# 四、代码
### 1.[FibHeap.h](https://code.csdn.net/mishifangxiangdefeng/exerciseforalgorithmsecond/tree/master/include/chapter20/FibHeap.h)
### 2.[FibHeap.cpp](https://code.csdn.net/mishifangxiangdefeng/exerciseforalgorithmsecond/tree/master/src/chapter20/FibHeap.cpp)
### 3.[测试用例](https://code.csdn.net/mishifangxiangdefeng/exerciseforalgorithmsecond/tree/master/tst/chapter20/FibHeapTest.cpp)
# 五、习题
### 20.1斐波那契堆
### 20.2可合并堆
### 20.2-1
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-02_56b02bd2a1255.gif)
### 20.2-4
McGee和FibHeap的区别在于合并的时机。
Fibheap认为合并调整应该尽量地推迟,而McGee则在每次堆中结点有增加的时候就作合并调整。
个人认为,合并调整操作的意义是防止堆过宽而影响性能。但是从算法过程上看,根结点的个数多少不会影响INSERT和UNION的性能,因此没有必要。
比较认可FibHeap的做法。
### 20.3减小一个关键字与删除一个结点
### 20.3-1
根据P300的描述,只有非根结点才可能被打上标记,如果根结点有标记,一定是它是非根结点的时候打上标记,然后被移到根结点的位置。
把结点移至根结点是通过上面代码中的函数addNodeToRootList和addListToRootList完成的,目标缩小至这两个函数周围
让根结点成为有标记结点,须满足以下两个条件
(1)调用这两个函数前,该结点是非根结点
(2)调用后没有清标记
结论:x是pMinData的孩子,根据P300的步骤被打上标记后,执行extract()时又成为根结点
### 20.4最大度的界
# 四、思考题
### 20-1删除的另一种实现
把FIB-HEAP-DELETE中的两个函数展开,再和PISANO-DELETE对比,并附上x不是min[H]的假设,可以发现这两个函数执行的操作基本上是一样的,区别在于
(1)PISANO-DELETE中去掉了FIB-HEAP-DELETE中多余的判断,不影响效率
(2)FIB-HEAP-DELETE在删掉结点之后有合并调整的动作
a)add x's child list to the root list of H的时间不是O(1),因为每个child都有一个pParent指针,必须依次修改每个child的指针
### 20-2其它斐波那契堆的操作
a)
(1)k < key[x]的情况,直接调用FIB-HEAP-DECREASE-KEY
(2)k = key[x]的情况,不用处理
(3)k > key[x]的情况,交换它与它孩子的内容,但是指针保持不变,直到符合最小堆的情况,时间与堆的深度有关
b)
以我有限的智商,只能想到执行min(r, n[H])次EXTRACT