斐波那契堆的定义
参看19章 二项堆我们可以看到对于可合并堆操作,二项堆均有O(lgn)的时间,对于本章将要讨论的斐波那契堆也支持这些操作,而且它有着更好的渐进时间界,对于不涉及元素删除的操作,它有着O(1)的时间界。
和二项堆一样,在仅支持可合并对操作(这些操作请参考19章)的情况下,每个斐波那契堆也是由一组二项树组成,只不过这些树均是无序的。无序二项树的定义和二项树类似:无序二项树U0只包含一个节点,一棵Uk的无序二项树包含两棵Uk-1的无序二项树,且其中一棵是另外一棵的任意一个孩子节点。
在19章列出的四个性质中,无序二项树依然满足,只是第四个性质稍作改变,如下:对无序二项树Uk,根的度数为k,它大于任何子节点的度数,根的子女以某种顺序分别成为子树U0,U1...Uk-1的根。并不是按度数递减排列的。
当然,在支持decreaKey和delete操作的斐波那契堆中,在某些时刻,堆中的树并不是无序二项树。
斐波那契堆的结构
我们先抛出一张图,直观地看看斐波那契堆是啥样子的。很显然,下图中的堆中的树就不全是无序二项树。
可以看出,整个根表是一个双向循环链表,对于每个根,是一棵无序二项树(可能),子节点也被链成双向循环链表,可以看作一个子堆。C++代码的斐波那契堆节点的结构如下:
template <typename Key,typename Value>
struct fibonacci_heap_node
{//斐波那契堆节点类型
Key key;
Value value;
bool mark = false;//标志在上次清除标记后是否失去过一个孩子,初始时均为false
size_t degree = 0;
fibonacci_heap_node *parent = nullptr;
fibonacci_heap_node *prev = nullptr;
fibonacci_heap_node *next = nullptr;
fibonacci_heap_node *child = nullptr;
fibonacci_heap_node(const Key &k, const Value &v) :key(k), value(v){}
void print(){printf("key: %-6d value: %-6d degree: %-6d\n", key, value, degree);}
};
斐波那契堆的操作
insert,minimum,delete操作很简单,就不细说了。下面只是粗略地讨论各操作流程,操作的代码将在后面给出,含有更详细的注释。
Union操作。相对于二项堆的合并,斐波那契堆的合并操作很简单,就是两个双向循环链表——双向环——的链接,下面的图解表明了一种链接方式,时间O(1)。
extractMin操作。流程如下:
1、记录下最小节点,即head所指节点;然后将其每一个孩子链接到根表,期间设置parent域为空;
2、重置head域。若整个堆中仅剩下一个元素,则设置其为空;否则将其随意指向一个节点,然后开始堆修正操作consolidate,合并那些度相同的节点。
3、返回head,结束。
consolidate子操作,目的是防止堆过宽。流程如下:
1、扫描每一个根表中的节点,将其地址存入一个数组中,按节点的度索引;
2、若碰到某节点的度所对应的数组项不为空,且不是同一节点,则说明存在两棵度相等的不同的树,开始合并;
3、将关键字大的节点链为关键字小的孩子,然后自增度,扫描下一个索引项,继续合并;
4、重复1~3,直至每一个根表节点均扫描过;
5、合并结束后,所有存在于根表的节点的指针按度索引存储于数组中,扫描数组,确定head的最终位置,结束。
该操作的终止条件从步骤2即可看出,若为同一节点,表明已经循环整个链表一周,应当终止了。
decreaseKey操作,其中的剪枝是为了防止堆过深,mark的作用就在于此,辅助剪枝。流程如下:
1、减小该节点的关键字大小,若新关键字较大,则退出;
2、若更改后节点的关键字比父节点大,则需要剪枝了,将其从父节点孩子位置剪下来,链到根表中;
3、级联判断父节点是否也该被剪枝,被剪条件是——算上这个孩子,它已经失去过两个孩子,那么应当剪掉;
4、重复上述过程,直至遇到根表节点或者不满足步骤3条件。
下面本章整个斐波那契堆的实现代码,注释详细,经过一些测试,运行正确,欢迎讨论。
//斐波那契堆,默认最小堆
#include<iostream>
#include<cmath>
#include<vector>
using namespace std;
template <typename Key, typename Value>
struct fibonacci_heap_node
{//斐波那契堆节点类型
Key key;
Value value;
bool mark = false;//标志是否曾经失去过一个孩子,初始时均为false
size_t degree = 0;
fibonacci_heap_node *parent = nullptr;
fibonacci_heap_node *prev = nullptr;
fibonacci_heap_node *next = nullptr;
fibonacci_heap_node *child = nullptr;
fibonacci_heap_node(const Key &k, const Value &v) :key(k), value(v){}
void print(){ printf("key: %-6d value: %-6d degree: %-6d\n", key, value, degree); }
};
template <typename Key, typename Value, typename Compare = less<Key>>
class fibonacci_heap
{//斐波那契堆
public:
typedef fibonacci_heap_node<Key, Value> node;
typedef Key key_type;
typedef Value value_type;
private:
node *head;//永远指向最小值节点
Compare compare;//比较器
size_t n = 0;//节点总数
private:
void linkNode(node *&lhs, node *&rhs)
{//将lhs所指节点链接为rhs所指节点的前驱
if (rhs == nullptr)
{//若rhs为空
rhs = lhs;
lhs->next = lhs;
lhs->prev = lhs;
}
else
{//否则
lhs->next = rhs;
rhs->prev->next = lhs;
lhs->prev = rhs->prev;
rhs->prev = lhs;
}
}
void removeNode(node *p)
{//将节点p从所属的双链表中移除掉
if (p->next == p)
{//若该双链表仅有一个节点
p->next = nullptr;
p->prev = nullptr;
}
else
{//否则
p->prev->next = p->next;
p->next->prev = p->prev;
}
}
void heapLink(node *big, node *small)
{//将根节点关键字较大的树链为根关键字较小的树的孩子
removeNode(big);//首先移除big
linkNode(big, small->child);//再链为其孩子
++small->degree;
big->parent = small;
big->mark = false;//1、成为一个节点的孩子时,清除标记位
}
void prune(node *p, node *par)
{//剪枝,将树根为p的子树从父节点par剪掉
if (par->degree == 1) par->child = nullptr;//可能需要重新设置孩子
else if (par->child == p) par->child = p->next;
removeNode(p);
linkNode(p, head);//链到根表中
p->parent = nullptr;
p->mark = false;//2、成为一棵新树时,清除标记位
--par->degree;
}
void removeChildsToRoot(node *p)
{//将树根p下的孩子们全部移到根表中
if (p->child != nullptr)
{
node *first = p->child, *last = p->child->prev;
while (true)
{//迭代,将p的每个孩子链到根表中
node *curr = first;
first = first->next;
removeNode(curr);
linkNode(curr, head);
curr->parent = nullptr;
if (curr == last) break;//若已经处理完最后一个,跳出循环
}
}
}
void cascadingPrune(node*);//级联剪枝
void consolidate();//合并根表
void print_aux(node*)const;
void destroy(node*);
public:
fibonacci_heap(node *h = nullptr, const Compare &c = Compare()) :head(h), compare(c){}
fibonacci_heap(const Compare &c) :head(nullptr), compare(c){}
node* insert(const Key&, const Value&);
node* minimum()const { return head; }
void FibHeapUnion(fibonacci_heap&);
pair<Key, Value> extractMin();
void decreaseKey(node*, const Key&);
void erase(node*);
bool empty()const { return head == nullptr; }
size_t size()const { return n; }
void print()const { print_aux(head); }
~fibonacci_heap(){ destroy(head); }
};
template <typename Key, typename Value, typename Compare>
inline fibonacci_heap_node<Key, Value>*
fibonacci_heap<Key, Value, Compare>::insert(const Key &k, const Value &v)
{//插入一个节点
node *p = new node(k, v);
linkNode(p, head);//直接插到根表中
if (compare(p->key, head->key))
head = p;
++n;
return p;
}
template <typename Key, typename Value, typename Compare>
inline void fibonacci_heap<Key, Value, Compare>::FibHeapUnion(fibonacci_heap &rhs)
{//合并两个斐波那契堆
if (rhs.empty())return;//若被合并堆空
if (empty())
{//若本堆空
swap(head, rhs.head);
swap(n, rhs.n);
swap(compare, rhs.compare);
return;
}
//链接两个双链表
node *head_prev = head->prev;
head_prev->next = rhs.head->prev;
rhs.head->prev->prev->next = head;
head->prev = rhs.head->prev->prev;
rhs.head->prev->prev = head_prev;
if (compare(rhs.head->key, head->key))
head = rhs.head;
n += rhs.n;
rhs.head = nullptr; rhs.n = 0;
}
template <typename Key, typename Value, typename Compare>
pair<Key, Value> fibonacci_heap<Key, Value, Compare>::extractMin()
{//抽取堆最小值
node *p = head;//记下最小值节点
removeChildsToRoot(head);
head->child = nullptr;
removeNode(head);//从根表中移除head
if (head->next == nullptr) head = nullptr;
else//重新设置head
{//且摘除该最小值节点后,对根表中的树进行合并
head = head->next;
consolidate();
}
--n;
pair<Key, Value> tmp = pair<Key, Value>(p->key, p->value);//返回值
delete p;
return tmp;
}
template <typename Key, typename Value, typename Compare>
void fibonacci_heap<Key, Value, Compare>::consolidate()
{//合并堆中,即根表中,度数相同的树
size_t max_degree = static_cast<size_t>(log(n) / log(2));
vector<node*> temp(max_degree + 1);//存储各度数的树的根
node *first = head;
while (true)
{//不断迭代,扫描每一棵树
node *small = first;
first = first->next;
if (small->degree > max_degree)
{//max_degree是指在合并后堆中的树的最大度数。但是在合并之前,可能出现某树的度数超过的情况。
//这是因为发生一系列剪枝(但没有剪掉某树根的孩子),抽取(也没有抽取该树的节点)操作后,
//节点数目减少,使得计算出来的最大度数较之前小,而此时该树的度依然维持在之前的水平,此时
//就会发生这种情况,这在第23章实现prim算法时出现过,代码后附上当时的堆结构图。因而在此需要
//将其所有孩子移到根表中,减少该树的度。
removeChildsToRoot(small);
small->degree = 0;
}
size_t d = small->degree;
if (small == temp[d]) break;//当前将要处理的树已存在于temp中,则说明已经合并完毕,退出
while (temp[d] != nullptr)
{//若temp中有另外一棵和当前树的度数相同的树
node *big = temp[d];
if (!compare(small->key, big->key))//若当前树的根关键字较大(最小堆时)
swap(small, big);//则交换
//这个错误调试了好久,将要被合并的树恰巧是first所指向的,而且要放在swap之后,第一次排除
//错误放在swap前面,后来在MST算法中,又出错,应该要放在这里,因为big与small可能会发生交换
if (big == first) first = first->next;
heapLink(big, small);//将big树链为small的孩子
temp[d] = nullptr;
++d;//生成了度增1的树,继续合并
}
temp[d] = small;//若不存在,则设置temp的相应槽位
}
head = nullptr;
for (size_t i = 0; i != temp.size(); ++i)
{//扫描temp
if (temp[i] != nullptr && (head == nullptr || compare(temp[i]->key, head->key)))
head = temp[i];//设置新的head,即最小值节点
}
}
template <typename Key, typename Value, typename Compare>
void fibonacci_heap<Key, Value, Compare>::decreaseKey(node *p, const Key &k)
{//减小某一节点关键字
if (!compare(k, p->key))
{//若新关键字较大
cerr << "greater key" << endl;
return;
}
p->key = k;
node *par = p->parent;
if (par != nullptr && compare(p->key, par->key))
{//若新关键字比父节点关键字小
prune(p, par);//则剪掉以p为根的树,使其成为根表中一员
cascadingPrune(par);//并级联剪枝父节点
}
if (compare(p->key, head->key))//测试是否需要重新设置head
head = p;
}
template <typename Key, typename Value, typename Compare>
void fibonacci_heap<Key, Value, Compare>::cascadingPrune(node *p)
{//级联剪枝
node *par = p->parent;
if (par != nullptr)
{//若p的父节点存在
//若在此之前p没有失去孩子,言下之意是p从上次清除标记后到现在仅失去过一个孩子
if (p->mark == false) p->mark = true;//则将其标为true,表明失去一个孩子
else
{//若现在失去的使其第二个孩子
prune(p, par);//则将其剪枝
p->mark = false;//3、清除标记位
cascadingPrune(par);
}
}
}
template <typename Key, typename Value, typename Compare>
void fibonacci_heap<Key, Value, Compare>::erase(node *p)
{//删除某一节点
node *p_min = minimum();
decreaseKey(p, p_min->key - 1);
extractMin();
}
template <typename Key, typename Value, typename Compare>
void fibonacci_heap<Key, Value, Compare>::print_aux(node *p)const
{//递归打印堆
if (p == nullptr) return;
node *first = p->next;
while (true)
{
node *curr = first;
first = first->next;
print_aux(curr->child);
curr->print();
if (curr == p) break;//表明已经绕地球一圈,该结束了
}
}
template <typename Key, typename Value, typename Compare>
void fibonacci_heap<Key, Value, Compare>::destroy(node *p)
{//销毁堆
if (p == nullptr) return;
node *first = p->next;
while (true)
{
node *curr = first;
first = first->next;
destroy(curr->child);
--n;
if (curr == p)
{
delete curr;
break;
}
else delete curr;
}
}
int main()
{
fibonacci_heap<int, int> fh1,fh2;
for (int i = 0; i != 10; ++i)
{
if (i % 2 == 0) fh1.insert(i, 2 * i);
else fh2.insert(i, 2 * i);
}
cout << "fh1" << endl;
fh1.print(); cout << fh1.size() << endl;
cout << "fh2" << endl;
fh2.print(); cout << fh2.size() << endl;
fh1.FibHeapUnion(fh2);
cout << "union" << endl;
fh1.print(); cout << fh1.size() << endl;
while (!fh1.empty())
{
cout << "-------------" << endl;
fh1.minimum()->print();
cout << endl;
fh1.extractMin();
fh1.print();
}
getchar();
return 0;
}
关于上述代码需要注意的几点:
1、consolidate函数中,first可能绕过双向循环链表,指向了已存在于数组中的根,而且正好是将要被合并的树(big所指向的)的根,此时需要将first继续向前移动;
2、consolidate函数第一层while循环的终止条件,即small和temp[small->degree]指向了同一棵树,表明该合并的树已经合并完毕;
3、三种情况下将会清除标记位:a.成为一个节点的孩子;b.成为根表一员,即成为一棵树;c.失去两个孩子后,被剪枝成为根表一员;
4、级联剪枝发生在该节点已经失去过两个孩子时发生;
5、为什么不直接向上调整呢?我的理解是,这样的话decreaseKey平摊时间将不再是O(1),而是O(lgn)。
6、consolidate函数中出现过的某树度数超过最大度数的情况下堆的结构图,在第23章 最小生成树算法 斐波那契堆实现中出现的。可以看出,max_degree应当为2,可是此时这棵树的度却为3,节点中的数字是顶点的编号,节点的键是该顶点和MST的距离。
思考题 20-1
1、将x的孩子双向链表摘除链接到根表在O(1)时间是可以实现的,但是每个孩子都有一个父指针parent,对它们的修改只能迭代,股时间应该是O(degree[x]);
2、O(c + degree[x]);
3、分析两者的代码,最好展开fib-delete,可以看出,两者基本一样,没有太大的区别;在x != min[H]的前提下,fib-delete只是多了一点无关紧要的判断语句,并不影响渐进时间,故pisano-delete并不具有优越的渐进运行时间。
思考题 20-2
1、k = key[x]时,不变;
k < key[x]时,就是decreaseKey;
k > key[x]时,将其与孩子节点内容不断交换,直到满足最小堆性质为止,和对深度有关,一个不紧确的上界O(lgn)。
2、若是n[H]较大还好说,就是调用destroy,时间O(n);但是若是只是删除一部分节点,高效算法目前还不得,求指导。