Data structures 斐波那契堆数据结构背后的直觉是什么?

Data structures 斐波那契堆数据结构背后的直觉是什么?,data-structures,fibonacci-heap,Data Structures,Fibonacci Heap,我已经阅读了和CLRS对数据结构的描述,但它们对这种数据结构的工作原理没有什么直观的解释。为什么斐波那契堆是这样设计的?它们是如何工作的 谢谢 这个答案会很长,但我希望它能帮助我们了解斐波那契堆的来源。我假设你已经熟悉和 动机:为什么斐波那契堆? 在跳入斐波那契堆之前,最好先探究一下我们为什么需要它们。还有很多其他类型的堆(例如,还有),那么为什么我们需要另一个呢 主要原因出现在和。这两种图算法都通过维护一个优先级队列来工作,该队列包含具有相关优先级的节点。有趣的是,这些算法依赖于一个名为red

我已经阅读了和CLRS对数据结构的描述,但它们对这种数据结构的工作原理没有什么直观的解释。为什么斐波那契堆是这样设计的?它们是如何工作的


谢谢

这个答案会很长,但我希望它能帮助我们了解斐波那契堆的来源。我假设你已经熟悉和

动机:为什么斐波那契堆? 在跳入斐波那契堆之前,最好先探究一下我们为什么需要它们。还有很多其他类型的堆(例如,还有),那么为什么我们需要另一个呢

主要原因出现在和。这两种图算法都通过维护一个优先级队列来工作,该队列包含具有相关优先级的节点。有趣的是,这些算法依赖于一个名为reduce key的堆操作,该操作获取优先级队列中已有的条目,然后降低其密钥(即增加其优先级)。事实上,这些算法的很多运行时都是通过调用reduce key的次数来解释的。如果我们能够构建一个优化reducekey的数据结构,我们就可以优化这些算法的性能。对于二进制堆和二项式堆,reduce key需要时间O(logn),其中n是优先级队列中的节点数。如果我们可以把它降到O(1),那么Dijkstra算法和Prim算法的时间复杂度将从O(m logn)降到(m+n logn),这比以前快得多。因此,尝试构建一个支持有效减少密钥的数据结构是有意义的

还有另一个理由考虑设计一个更好的堆结构。将元素添加到空二进制堆时,每次插入都需要时间O(logn)。如果我们事先知道所有的n个元素,这是可能的,但是如果元素到达一个流中,这是不可能的。在二项式堆的情况下,插入n个连续元素每个都需要摊销时间O(1),但如果插入与删除交错,则插入可能最终每个都需要Ω(logn)时间。因此,我们可能希望搜索一个优先级队列实现,该实现优化插入,使每个插入花费时间为O(1)

第一步:懒惰的二项式堆 为了开始构建斐波那契堆,我们将从一个二项式堆开始,并对其进行修改,使插入花费时间为O(1)。尝试这一点并不是不合理的——毕竟,如果我们要做大量的插入而不是排那么多的队列,那么优化插入是有意义的

如果您还记得的话,二项式堆的工作原理是将堆中的所有元素存储在一个集合中。n阶二叉树中有2n个节点,堆结构是所有服从堆属性的二叉树的集合。通常,二项式堆中的插入算法工作如下:

  • 创建一个新的单例节点(这是一个顺序为0的树)
  • 如果存在顺序为0的树:
    • 将两棵0阶树合并为一棵1阶树
    • 如果有一个顺序为1的树:
      • 将顺序为1的两棵树合并为顺序为2的树
      • 如果有一棵2级树:
此过程确保在每个时间点,每个订单最多有一棵树。由于每棵树的节点数都以指数形式超过其顺序,这就保证了树的总数很小,这使得出列可以快速运行(因为在执行出列最小步骤后,我们不必查看太多不同的树)

然而,这也意味着将节点插入二项式堆的最坏运行时是Θ(logn),因为我们可能有需要合并在一起的Θ(logn)树。这些树需要合并在一起,只是因为我们需要在执行出列步骤时保持较低的树数,而且在将来的插入中保持较低的树数绝对没有好处

这引入了对二项式堆的第一个偏离:

修改1:将节点插入堆时,只需创建一个0级树并将其添加到现有树集合中。不要把树捆在一起

我们还可以做另一个改变。通常,当我们将两个二项式堆合并在一起时,我们会执行一个合并步骤,以确保结果树中每个顺序最多有一棵树。同样,我们进行这种压缩是为了让出列速度更快,没有真正的理由说明合并操作应该为此付费。因此,我们将进行第二次更改:

修改2:将两个堆合并在一起时,只需将它们的所有树合并在一起,而不进行任何合并。不要将任何树木合并在一起

如果我们进行此更改,我们很容易在排队操作中获得O(1)性能,因为我们所做的只是创建一个新节点并将其添加到树集合中。但是,如果我们只是做了这个更改而不做任何其他事情,那么我们将完全破坏dequeue min操作的性能。回想一下,在删除最小值后,dequeue min需要扫描堆中所有树的根,以便找到最小值。如果我们通过在路径中插入节点来添加Θ(n)节点,那么我们的出列操作将不得不花费Θ(n)时间查看所有这些树。这是一个巨大的性能打击。。。我们能避免吗

如果我们的插入真的只是添加了更多的树,那么第一次出列肯定要花费Ω(n)的时间。然而,这并不意味着每次出列都要花费高昂的费用。如果在执行出列后,我们将堆中的所有树合并在一起,从而每个顺序只得到一棵树,会发生什么?最初这需要很长时间,但如果我们开始这样做