算法导论 第20章 斐波那契堆
斐波那契堆的定义
参看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;//标志在上次清除标记后是否失去过一个孩子,初始时均为falsesize_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;//标志是否曾经失去过一个孩子,初始时均为falsesize_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);//首先移除biglinkNode(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);//从根表中移除headif (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){//扫描tempif (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))//测试是否需要重新设置headhead = 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);但是若是只是删除一部分节点,高效算法目前还不得,求指导。
算法导论 第20章 斐波那契堆相关推荐
- 算法导论读书笔记-第十九章-斐波那契堆
算法导论第19章--斐波那契堆 可合并(最小)堆(mergeable min-heap) : 支持以下5种操作的一种数据结构, 其中每一个元素都有一个关键字: MAKE-HEAP(): 创建和返回一个 ...
- 《算法导论》第19章-斐波那契堆 引入 19.1 斐波那契堆结构
引入 1.可合并堆 可合并堆(mergeable heap)是支持以下5种操作: MAKE-HEAP():创建和返回一个新的不含任何元素的堆. INSERT(H,x):将一个已填人关键字的元素x插人堆 ...
- 《算法导论3rd第十九章》斐波那契堆
前言 第六章堆排序使用了普通的二叉堆性质.其基本操作性能相当好,但union性能相当差. 对于一些图算法问题,EXTRACT-MIN 和DELETE操作次数远远小于DECREASE-KEY.因此有了斐 ...
- 算法导论之斐波那契堆
斐波那契堆,和二项堆类似,也是由一组最小堆有序的树构成.注意区别,不是二项树,是有根而无序的树.导论中,斐波那契堆只是具有理论上的意义,是以平摊分析为指导思想来设计的数据结构,主要是渐进时间界比二项堆 ...
- 算法导论--斐波那契堆
斐波那契堆 斐波那契堆也是数据储存结构的一种,这种数据结构之所以产生,主要有两个优点:1.对于数据合并操作具有很高的效率:2.对于其他一般数据操作平均时间复杂度较好(注意这里是平均复杂度,不是最坏情形 ...
- c语言 兔子数列螺线图,经典算法大全51例——2.斐波那契数列(兔子数列)
经典算法大全51例--2.斐波那契数列 算法目录合集 地址 说明 题目 原理分析 代码实现--Java 相关题目其他变形: 1.爬楼梯(来源:力扣LeetCode) 2.兔子成熟期拉长 官方题解 分析 ...
- 斐波那契堆(Fibonacci heaps)
一:斐波那契堆 1:特性 斐波那契堆同二项堆一样,也是一种可合并堆.斐波那契堆的优势是:不涉及删除元素的操作仅需要O(1)的平摊运行时间(关于平摊分析的知识建议看<算法导论>第17章).和 ...
- 优先队列——斐波那契堆(without source code)
[0]README 0.1) 本文部分内容转自 数据结构与算法分析,旨在理解 斐波那契堆 的基础知识: 0.2) 文本旨在理清 斐波那契堆的 核心idea,还没有写出源代码实现,表遗憾: 0.3)从实 ...
- 斐波那契堆(不太详尽)
总结:这一章讲了斐波那契堆,它是一种比二项堆更松散的堆,它由一组无序的二项树组成,对不涉及删除元素的操作,它仅需O(1)的平摊运行时间.本章介绍斐波那契堆的插入.合并.删除等操作. 1. 斐波那 ...
最新文章
- 基于thinkphp的省略图便捷函数
- Java的基础方法Java的对象_java基础之 创建对象的几种方式
- html文件本质上是一个,html文件是什么
- 怎么样才能写出出色的代码
- js中类型识别的方法
- 对于随机过程方面书籍的评论(转贴)
- Visual Studio下Qt调用IDL
- linux进入运行exe命令,在Deepin V20系统中打开运行exe文件的两种方法
- 与其埋头啃文献不如关注这些公众号
- CentOS更改root密码
- 清明出行之高德路况思考
- 为什么许多器件的片选信号低电平有效,而不是高电平有效?
- STM32名字含义以及其与ARM公司的关系
- HyperX旋火无线游戏鼠标,摆脱“线”制,黑白双煞争分夺秒
- 声音能否内嵌在PPT中
- 泛目录如何实现日收?
- python爬取前程无忧scrapy存mogondb案例
- Axure9 自建元件库+编辑
- mplus的java_Mplus的二阶五因子语法
- html网页制作期末大作业成品_网页设计期末作业-简洁源码-我的学校
热门文章
- 冼牛:即构实时网络调度系统如何应对跨国场景挑战
- Yunxion资产监测设备帮助外贸企业实时把握国际货物运输状态
- ViewPager 实现纵向翻页切换、仿抖音视频垂直切换
- Private Set Intersection(PSI)简介和资料分享
- windows下编译xv6
- html5 toggle,JQuerytoggle使用分析
- opera浏览器怎么打开html的控制台,Opera浏览器怎么开启预读功能
- 用 Vue 实现学生信息管理系统的增删改查操作,模拟数据库操作(但并没有连接数据库)
- 在充满不确定性的职场中,她只做了这一件事
- 奇数阶幻方法C语言运用指针,奇数幻方的构造方法(转载)