算法导论 第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;//标志在上次清除标记后是否失去过一个孩子,初始时均为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);但是若是只是删除一部分节点,高效算法目前还不得,求指导。







  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值