《Thinking in Algorithm》16.堆结构之斐波那契堆

2024-01-25 20:18

本文主要是介绍《Thinking in Algorithm》16.堆结构之斐波那契堆,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

堆的变体:

  • 二叉堆
  • 二项堆
  • 斐波那契堆

前面的博客中我们讲到的堆的两种变体,二叉堆和二项堆,今天我们要讲的就是著名的斐波那契堆。

依然首先列出了三种堆的时间复杂的比较。

从上面能发现斐波那契堆的时间复杂度在很多操作上有优化,如insert, minimum, union , decrease-key,而extreact-min,delete没有变化。

可能看到这很多人会说为什么有这么好的斐波那契堆,我们还要去研究其他堆呢,确实,理论上来讲,对于extreact-min和delete操作想对于其他操作少的情况下,斐波那契堆确实提高了很大的效率。

但是实际中,除了某些需要管理大量数据的应用外,对于大多数应用,斐波那契堆的常熟因子和变成复杂性使得它比起普通二项堆并不是那么使用。

二项堆和斐波那契堆对于search操作的支持均比较低效;可能花费一段时间才能找到关键字。为此,涉及给定元素的操作(如DECREASE-KEY和DELETE)均需要一个指针指向这个元素,并且指针作为输入的一部分。


Fibonacci Heap(简称F-Heap)是一种基于二项堆的非常灵活的数据结构。它与二项堆不同的地方在于:

1)root list和任何结点的child list使用双向循环链表,而且这些lists中的结点不再有先后次序(Binomial Heap中root list的根结点按degree从小到大顺序,child list的结点按degree从大到小顺序);

2)二项堆中任何一颗Binomial Tree中根结点的degree是最大的,而F-Heap中由于decrease-key操作(cut和cascading cut)的缘故,并不能保证根结点的degree最大;

3) 二项堆中任何结点(degree等于k的)为根的子树中,结点总数为2^k;F-Heap中相应的结点总数下界为F{k+2},上界为2^k(如果没有 Extract-Min和Delete两类操作的话)。其中F{k+2}表示Fibonacci数列(即0,1,1,2,3,5,8,11...)中第 k+2个Fibonacci数,第0个Fibonacci数为0,第1个Fibonacci数为1。注意不像二项堆由二项树组成那样,F-Heap的 root list中的每棵树并不是Fibonacci树(Fibonacci树属于AVL树),而F-Heap名称的由来只是因为Fibonacci数是结点个数 的一个下界值。

4)基于上面的区别,若F-Heap中结点总数为n,那么其中任何结点(包括非根结点)的degree最大值不超过 D(n) = floor(lgn/lg1.618),这里1.618表示黄金分割率(goldren ratio),即方程x^2=x+1的一个解。所以在Extract-Min的consolidate操作之后,root list中的结点最多有D(n)+1。而二项堆中degree最大值不超过floor(lgn),从而root list中最多有floor(lgn)+1颗二项树。

5)另外一个与二项堆的最大不同之处在于:F-Heap是一种具有平摊意义上的高性能 数据结构。除了Extract-Min和Delete两类操作具有平摊复杂度O(lgn),其他的操作(insert,union,find- min,decrease-key)的平摊复杂度都是常数级。因此如果有一系列的操作,其中Extract-min和delete操作个数为p,其他操作 个数为q,p<q,那么总的平摊复杂度为O(p + q.lgn)。达到这个复杂度的原因有以下几点,第一,root list和任何结点的child list中使用了双向循环链表;第二,union和insert操作的延迟合并,从而在所有的可合并堆中,F-heap的合并开销O(1)最小的;第 三,decrease-key中cut和cascading cut的巧妙处理(即任何非根结点最多失去一个孩子)。


1. 斐波那契堆的结构


typedef int Type;typedef struct _FibonacciNode
{Type   key;                     // 关键字(键值)int degree;                     // 度数struct _FibonacciNode *left;    // 左兄弟struct _FibonacciNode *right;   // 右兄弟struct _FibonacciNode *child;   // 第一个孩子节点struct _FibonacciNode *parent;  // 父节点int marked;                     //是否被删除第1个孩子(1表示删除,0表示未删除)
}FibonacciNode, FibNode;

FibNode是斐波那契堆的节点类,它包含的信息较多。key是用于比较节点大小的,degree是记录节点的度,left和right分别是指向节点的左右兄弟,child是节点的第一个孩子,parent是节点的父节点,marked是记录该节点是否自从上一次成为另一个结点的孩子后,是否失去过孩子。true表示是去过,被标记(marked在删除节点时有用)。

typedef struct _FibonacciHeap{int   keyNum;                   // 堆中节点的总数int   maxDegree;                // 最大度struct _FibonacciNode *min;     // 最小节点(某个最小堆的根节点)struct _FibonacciNode **cons;   // 最大度的内存区域
}FibonacciHeap, FibHeap;

FibHeap是斐波那契堆对应的类。min是保存当前堆的最小节点,keyNum用于记录堆中节点的总数,maxDegree用于记录堆中最大度,而cons在删除节点时来暂时保存堆数据的临时空间。


一个斐波那契堆是一系列具有最小堆序(min-heap ordered)的有根树的集合。也就是说,每棵树遵循最小堆性质:每个结点的关键字大于或等于它的父节点的关键字,如下图:




2. 可合并堆操作


2.1 创建一个斐波那契堆

类似于二项堆的操作,分配并返回一个Fib对象H,取H.n = 0 , H.min = NIL ,H中没有树。花费为O(1).


2.2 插入一个结点

下面的代码是插入结点x到斐波那契堆H中,假设该结点已经被分配,并且x被赋值。时间复杂度:O(1)



如上图中,插入结点x.key=21。

1-4:初始化结点x

5-7:如果斐波那契堆为空,那么就直接将x结点插入H堆中的根链表,并且H.min指向x结点

8-10:H不为空时,首先将结点x插入到H堆中的根链表中,将x与堆中最小元素做比较,如果更小,则H.min指向x,否则不做任何改变。最后n加一。


2.3 找到最小节点

不同于二项堆,斐波那契堆有一个指向最小结点的指针,所以操作复杂度为O(1).


2.4 合并两个斐波那契堆

下面的代码中表示合并两个斐波那契堆H1和H2,并且摧毁H1和H2。要做的仅仅是将他们的根链表结合起来,并且确定新的最小的元素。复杂度:O(1)



2.5 取出最小元素

这个操作是本节内最复杂的一个了。 操作复杂度为O(lgn).先看


3-6:将最小结点的孩子存入到根链表中,并且从链表中删除z

7-8:如果z的右兄弟等于z的话,说明z没有孩子,且原堆中只有z一个结点,则删除z后为空。

9-11:如果不止z一个的话,那么将H.min随意指向链表中的一个元素,不一定要是链表中最小的,然后执行CONSOLIDATE(H)


对于取出最小结点的整体操作我们知道了,但对于CONSOLIDATE(H)的具体操作还完全不知,下面我们就来介绍他。

CONSOLIDATE过程要做的工作是:使每个度数的二项树唯一,也就是使每个根都有一个不同的degree值为止。对根表的合并过程是反复执行下面的步骤:

1)在根表中找出两个具有相同度数的根x和y,且key[x] <= key[y].

2)  将y链接到x:将y从根表中移出,成为x的一个孩子。这个过程由FIB-HEAP-LINK完成。

首先看伪代码:


解释下上面的伪代码:

1-3:初始化一个数组A,其中D(H.n)=lgn,因为我们要使得每个根的度数唯一,而根最多有度数最大为lgn

4-14:使得链表中的树根度数唯一

15-23:重新建立一个树根表,保存数组A[i]中的根结点,并且找出最小的元素用H.min指向该元素。

可能单纯根据伪代码还很难理解,下面我们根据具体流程图来分析。


他的操作流程可以参照下图。



a-b:就是删除最小元素3,并且将他的孩子存入根链表中

c-k:从H.min指向的根结点开始向右遍历,并按照其度数存入对应的数组A中的位置,当所存入的位置不为空,则说明存在相同度数的根结点,于是就执行FIB-HEAP-LINK操作,将他们结合起来,然后继续遍历。

l-k:找出链表中最小的元素,H.min指向该元素。


2.6 减少元素的值

下面的伪代码是将x节点的值减少到k


5-7:当节点减少后的值小于他的父节点,那么就要进行调整,首先是执行CUT(H,x,y)操作,即将x节点放到根链表中。然后执行CASCADING-CUT(H,y)操作,即使得斐波那契堆中的树不是那么不像二项树。下面是对该操作的详细解释:

  • 由于增加了删除和关键字减值操作,所以,F堆中的最小树就不一定必须是二项树了。事实上,可能存在度为k却只有k + 1(一棵所有叶子节点的)个结点的最小树。为了保证每个度为k的最小树至少包含ck个结点(c > 1), 每次执行删除操作和关键字减值操作后,还必须进行级联剪枝操作。为此,为每个结点增加一个布尔类型的child_cut域(即本文里的marked)。child_cut域的值仅对那些不是最小树树根的结点有意义。对于不是最小树树根的结点x, x的child_cut域为TRUE,当且仅当在最近一次x成为其当前父结点的儿子之后,x的一个儿子被删除。这就意味着,在执行删除最小元素中,每次连接两棵最小树时,关键字值较大的根结点的child_cut域应该赋值为FALSE。更进一步地说,一旦删除操作或关键字减值操作将最小树的非根结点q从其所在双向链表中删除时,则调用级联剪枝操作。在执行级联剪枝操作过程中,检查从被删除结点q的父节点p开始,到被删节点的最近的child_cut域为FALSE的祖先结点的路径。对在该路径上所有child_cut域为TRUE的非根结点,将其从所在的双向链表中删除,并将其加入到F堆的最小树的根节点组成的双向链表中。如果该路径上存在child_cut域为FALSE的结点 ,则将其该域的值修改为TRUE。

下图中是上面代码中的操作流程。




2.7 删除节点

跟二项堆一样,也是利用到前面的两个操作。直接上代码了。




reference:

           http://www.cs.princeton.edu/~wayne/teaching/fibonacci-heap.pdf


这篇关于《Thinking in Algorithm》16.堆结构之斐波那契堆的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/644486

相关文章

使用Java实现通用树形结构构建工具类

《使用Java实现通用树形结构构建工具类》这篇文章主要为大家详细介绍了如何使用Java实现通用树形结构构建工具类,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录完整代码一、设计思想与核心功能二、核心实现原理1. 数据结构准备阶段2. 循环依赖检测算法3. 树形结构构建4. 搜索子

利用Python开发Markdown表格结构转换为Excel工具

《利用Python开发Markdown表格结构转换为Excel工具》在数据管理和文档编写过程中,我们经常使用Markdown来记录表格数据,但它没有Excel使用方便,所以本文将使用Python编写一... 目录1.完整代码2. 项目概述3. 代码解析3.1 依赖库3.2 GUI 设计3.3 解析 Mark

mysql通过frm和ibd文件恢复表_mysql5.7根据.frm和.ibd文件恢复表结构和数据

《mysql通过frm和ibd文件恢复表_mysql5.7根据.frm和.ibd文件恢复表结构和数据》文章主要介绍了如何从.frm和.ibd文件恢复MySQLInnoDB表结构和数据,需要的朋友可以参... 目录一、恢复表结构二、恢复表数据补充方法一、恢复表结构(从 .frm 文件)方法 1:使用 mysq

Python中顺序结构和循环结构示例代码

《Python中顺序结构和循环结构示例代码》:本文主要介绍Python中的条件语句和循环语句,条件语句用于根据条件执行不同的代码块,循环语句用于重复执行一段代码,文章还详细说明了range函数的使... 目录一、条件语句(1)条件语句的定义(2)条件语句的语法(a)单分支 if(b)双分支 if-else(

使用Navicat工具比对两个数据库所有表结构的差异案例详解

《使用Navicat工具比对两个数据库所有表结构的差异案例详解》:本文主要介绍如何使用Navicat工具对比两个数据库test_old和test_new,并生成相应的DDLSQL语句,以便将te... 目录概要案例一、如图两个数据库test_old和test_new进行比较:二、开始比较总结概要公司存在多

Java中switch-case结构的使用方法举例详解

《Java中switch-case结构的使用方法举例详解》:本文主要介绍Java中switch-case结构使用的相关资料,switch-case结构是Java中处理多个分支条件的一种有效方式,它... 目录前言一、switch-case结构的基本语法二、使用示例三、注意事项四、总结前言对于Java初学者

结构体和联合体的区别及说明

《结构体和联合体的区别及说明》文章主要介绍了C语言中的结构体和联合体,结构体是一种自定义的复合数据类型,可以包含多个成员,每个成员可以是不同的数据类型,联合体是一种特殊的数据结构,可以在内存中共享同一... 目录结构体和联合体的区别1. 结构体(Struct)2. 联合体(Union)3. 联合体与结构体的

PostgreSQL如何查询表结构和索引信息

《PostgreSQL如何查询表结构和索引信息》文章介绍了在PostgreSQL中查询表结构和索引信息的几种方法,包括使用`d`元命令、系统数据字典查询以及使用可视化工具DBeaver... 目录前言使用\d元命令查看表字段信息和索引信息通过系统数据字典查询表结构通过系统数据字典查询索引信息查询所有的表名可

usaco 1.3 Mixing Milk (结构体排序 qsort) and hdu 2020(sort)

到了这题学会了结构体排序 于是回去修改了 1.2 milking cows 的算法~ 结构体排序核心: 1.结构体定义 struct Milk{int price;int milks;}milk[5000]; 2.自定义的比较函数,若返回值为正,qsort 函数判定a>b ;为负,a<b;为0,a==b; int milkcmp(const void *va,c

自定义类型:结构体(续)

目录 一. 结构体的内存对齐 1.1 为什么存在内存对齐? 1.2 修改默认对齐数 二. 结构体传参 三. 结构体实现位段 一. 结构体的内存对齐 在前面的文章里我们已经讲过一部分的内存对齐的知识,并举出了两个例子,我们再举出两个例子继续说明: struct S3{double a;int b;char c;};int mian(){printf("%zd\n",s