0%

斐波那契堆的C++实现

本文改编自《算法导论》第三版第19章:斐波那契堆。文中代码为书中伪代码的C++实现,如有疏漏还请指出。

斐波那契堆的结构

斐波那契堆是一种可并堆,它支持以下操作:
$(1)$在$O(1)$时间内插入元素、获取最小值、合并两个斐波那契堆、减小元素键值;
$(2)$在$O(logn)$时间内删除元素。
斐波那契堆的平摊复杂度比其他可并堆(左偏树/二项队列)要优,但时空常数大且实现复杂度高,在实际中并不非常适用。
斐波那契堆有下图所示的结构(图示为小根堆):
在这里插入图片描述
可以看到斐波那契堆仍然满足小根堆性质,与二叉堆不同的是,斐波那契堆的第一层由双向循环链表组成(23和24的连接没有画出),并且每一个结点的孩子集合也由双向循环链表维护。(如:3的孩子为{18,53,38},这三个结点在一个双向循环链表中,3只存指向其中一个的指针)
每个结点有如下属性(成员变量):

1
2
3
4
5
6
7
8
9
template<typename T>
struct node
{
T data;
bool mark;
int degree;
node *left,*right;
node *child,*parent;
};

$data:$数据域;
$mark:$标记,$mark == true$当且仅当某个结点成为某个节点的子结点后还失去了某儿子结点,黑色节点表示$mark = true$;
$degree:$孩子个数;
$left,right:$左右兄弟;
$child:$任意的一个该结点孩子;
$parent:$该结点的父节点。
每一层结点由双向循环链表构成,为了实现方便在程序中引入表头。

斐波那契堆的成员变量:

1
2
3
int size;//结点个数
node *min;//最小元素指针
node *head;//第一层元素的链表表头

斐波那契堆操作及实现

插入节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void LeftInsert(node *base, node *tmp)
{//LeftInsert:在双向循环链表中把tmp插入base左边
tmp->left = base->left;
tmp->right = base;
base->left->right = tmp;
base->left = tmp;
}
void Insert(T data)
{
//初始化
node *tmp = new node;
tmp->degree = 0;
tmp->data = data;
tmp->parent = tmp->child = nullptr;
tmp->left = tmp->right = tmp;//双向循环链表需要把左右指针指向自己
tmp->mark = false;
if(min == nullptr)//空堆,min指针指向该结点
{
min = tmp;
LeftInsert(head, min);//LeftInsert定义见上方
}
else
{
LeftInsert(min, tmp);
if(data < min->data)//插入的结点更小,min指向新结点
min = tmp;
}
size++;
}

显然,斐波那契堆的插入操作为常数复杂度(虽然该常数较大)。

合并两个斐波那契堆

1
2
3
4
5
6
7
8
9
10
11
12
FibHeap<T> Merge(FibHeap<T> H2)
{
node *ptr = head->right;
do
{
H2.Insert(ptr->data);
ptr = ptr->right;
}while(ptr != head);
if(H2.min == nullptr || (min != nullptr && min->data < H2.GetMin()))
H2.min = min;
return H2;
}

对于每个第一层结点$x$,把$x$放入$H_2$的第一层结点中,同时在有必要时改变min指针。

删除最小结点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
T DeleteMin()
{
T ret = min->data;//保存返回的最小值
node *ptr = min;
if(ptr != nullptr)
{
node *tmp = min->child,*nxt;
for(int i = 0; i < min->degree; i++)
{
nxt = tmp->right;
LeftInsert(head, tmp);
tmp = nxt;
}//由于在LeftInsert之后tmp的链会断,所以需要提前记录下一个孩子
ptr->left->right = ptr->right;
ptr->right->left = ptr->left;//在第一层链表中把min去掉

if(ptr == ptr->right)//此时堆中仅有一个元素
min = nullptr;
else
{
min = min->right;//先随意指向一个结点
if(min == head) min = min->right;//指向表头,无意义,再指向下一个
Consolidate();//调整堆结构
}
size--;
}
return ret;
}

要删除一个最小结点,首先将min指针指向的结点的所有孩子结点加入堆的第一层结点链表中,然后删除min结点(但保留其left,right结构),随后调整min指针的位置,但不要求其一定指向新的最小结点,这一步在$Consolidate$(合并)函数中完成。

Consolidate函数

Consolidate()负责调整堆的结构,使第一层结点数减小,同时确定新的最小值位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
void link(node *y,node *x)//把y插入到x下面
{
y->left->right = y->right;
y->right->left = y->left;
y->parent = x;
if(x->child == nullptr)//当x没有儿子
{
x->child = y;
y->left = y->right = y;
}
else LeftInsert(x->child, y);
x->degree++;
y->mark = false;//把标记清空,由于它刚刚成为子结点,仍需失去儿子结点mark才会标为true
}
void Consolidate()
{
int Dn = (int)log2(size)+1;
node **nodes = new node*[Dn];
for(int i = 0; i < Dn; i++)
nodes[i] = nullptr;
node *ptr = head->right,*nxt;
do
{
int d;
node *x,*y;
x = ptr;
if(x == head) continue;
d = x->degree;
nxt = ptr->right;
while(nodes[d] != nullptr)
{
y = nodes[d];
if(x->data > y->data)
std::swap(x, y);
link(y,x);
nodes[d] = nullptr;
d++;
}
nodes[d] = x;
ptr = nxt;
}while(ptr != head);
min = nullptr;
head->left = head->right = head;
for(int i = 0; i < Dn; i++)
{
if(nodes[i] != nullptr)
{
if(min == nullptr)
{
min = nodes[i];
LeftInsert(head,min);
}
else
{
LeftInsert(head, nodes[i]);
if(nodes[i]->data < min->data)
min = nodes[i];
}
}
}
}

首先(程序的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
2
3
4
5
6
7
8
9
10
11
12
13
14
void DecreaseKey(node *x, T key)
{
if(key >= x->data) return;//非减小
x->data = key;

node *y = x->parent;
if(y != nullptr && x->data < y->data)
{
Cut(x,y);
CascadingCut(y);
}
if(x->data < min->data)
min = x;
}

若减值结点尚未破坏堆结构(即仍大于等于其父结点键值),则无需进行调整;否则,堆通过Cut和CascadingCut(级联切断)调整。首先,对调整节点进行切断:把它从其父亲上切断,加入第一层结点中(对应Cut函数)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Cut(node *x, node *y)//x为子结点,y为父结点
{
if(y->degree == 1)
y->child = nullptr;
else
{
x->left->right = x->right;
x->right->left = x->left;
}
y->degree--;
LeftInsert(head, x);
x->parent = nullptr;
x->mark = false;//刚成为独立的结点,标记也要清空
}

之后,对父结点$y$尝试进行级联切断,它是一种使堆保持较为平衡的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void CascadingCut(node *y)
{
node *z = y->parent;
if(z != nullptr)
{
if(y->mark == false)
y->mark = true;
else
{
Cut(y,z);
CascadingCut(z);
}
}
}

若$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
2
3
4
5
void Delete(node *x)
{
DecreaseKey(x,MIN_VALUE);
DeleteMin();
}

时间复杂度分析

对各种操作进复杂度分析要用到平摊分析的势能法。

势函数

定义斐波那契堆的势函数
$\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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
template<typename T>
class FibHeap
{
private:
struct node
{
T data;
bool mark;
int degree;
node *left,*right;
node *child,*parent;
node()
{
mark = false;
degree = 0;
child = parent = nullptr;
left = right = this;
}
};
int size;
node *min;
node *head;
T MIN_VALUE;
void LeftInsert(node *base, node *tmp)
{
tmp->left = base->left;
tmp->right = base;
base->left->right = tmp;
base->left = tmp;
}
void link(node *y,node *x)
{
y->left->right = y->right;
y->right->left = y->left;
y->parent = x;
if(x->child == nullptr)
{
x->child = y;
y->left = y->right = y;
}
else LeftInsert(x->child, y);
x->degree++;
y->mark = false;
}
void Consolidate()
{
int Dn = (int)log2(size)+1;
node **nodes = new node*[Dn];
for(int i = 0; i < Dn; i++)
nodes[i] = nullptr;
node *ptr = head->right,*nxt;
do
{
int d;
node *x,*y;
x = ptr;
if(x == head) continue;
d = x->degree;
nxt = ptr->right;
while(nodes[d] != nullptr)
{
y = nodes[d];
if(x->data > y->data)
std::swap(x, y);
link(y,x);
nodes[d] = nullptr;
d++;
}
nodes[d] = x;
ptr = nxt;
}while(ptr != head);
min = nullptr;
head->left = head->right = head;
for(int i = 0; i < Dn; i++)
{
if(nodes[i] != nullptr)
{
if(min == nullptr)
{
min = nodes[i];
LeftInsert(head,min);
}
else
{
LeftInsert(head, nodes[i]);
if(nodes[i]->data < min->data)
min = nodes[i];
}
}
}
}
public:
FibHeap()
{
size = 0;
min = nullptr;
head = new node;
}
FibHeap(T MIN_VALUE)
{
size = 0;
min = nullptr;
head = new node;
this->MIN_VALUE = MIN_VALUE;
}
T GetMin()
{
return min->data;
}
int Size()
{
return size;
}
void Insert(T data)
{
node *tmp = new node;
tmp->degree = 0;
tmp->data = data;
tmp->parent = tmp->child = nullptr;
tmp->left = tmp->right = tmp;
tmp->mark = false;
if(min == nullptr)
{
min = tmp;
LeftInsert(head, min);
}
else
{
LeftInsert(min, tmp);
if(data < min->data)
min = tmp;
}
size++;
}
FibHeap<T> Merge(FibHeap<T> H2)//TOCHECK
{
node *ptr = head->right;
do
{
H2.Insert(ptr->data);
ptr = ptr->right;
}while(ptr != head);
if(H2.min == nullptr || (min != nullptr && min->data < H2.GetMin()))
H2.min = min;
return H2;
}
T DeleteMin()
{
T ret = min->data;
node *ptr = min;
if(ptr != nullptr)
{
node *tmp = min->child,*nxt;
for(int i = 0; i < min->degree; i++)
{
nxt = tmp->right;
LeftInsert(head, tmp);
tmp = nxt;
}
ptr->left->right = ptr->right;
ptr->right->left = ptr->left;

if(ptr == ptr->right)
min = nullptr;
else
{
min = min->right;
if(min == head) min = min->right;
Consolidate();
}
size--;
}
return ret;
}
void Cut(node *x, node *y)
{
if(y->degree == 1)
y->child = nullptr;
else
{
x->left->right = x->right;
x->right->left = x->left;
}
y->degree--;
LeftInsert(head, x);
x->parent = nullptr;
x->mark = false;
}
void CascadingCut(node *y)
{
node *z = y->parent;
if(z != nullptr)
{
if(y->mark == false)
y->mark = true;
else
{
Cut(y,z);
CascadingCut(z);
}
}
}
void DecreaseKey(node *x, T key)
{
if(key >= x->data) return;
x->data = key;

node *y = x->parent;
if(y != nullptr && x->data < y->data)
{
Cut(x,y);
CascadingCut(y);
}
if(x->data < min->data)
min = x;
}
void Delete(node *x)
{
DecreaseKey(x,MIN_VALUE);
DeleteMin();
}
};