斐波那契堆 — 从零精通算法与数据结构——Google 面试系统备战 第7篇

第7章:斐波那契堆

本章目标

读完本章你会:

  • 理解斐波那契堆的"懒惰哲学"——推迟工作到真正需要时
  • 解释合并(union)为何是 O(1),而二叉堆只能 O(n)
  • 掌握度约束和标记机制如何保证摊还效率
  • 理解 ExtractMin 中的"合并相同度树"步骤
  • 明白斐波那契堆为什么在理论上优于二叉堆,但实践中很少使用

知识讲解

从一个生活例子开始

两个公司合并。 如果两家都有精细的组织架构图,合并时需要逐一比对、重组——代价高。

斐波那契堆的做法是:先不管,把两张图钉在一起就行。 等到需要 CEO 汇报时,才从上到下梳理合并。这就是"懒惰合并"——把工作推迟到不得已时。

二叉堆像一个时刻保持整洁的房间——每次放东西都要放在正确位置。斐波那契堆像一个允许暂时凌乱的工作台——工具散落着,但你知道到截止日期时整理一次就能全搞定。

关键洞察:大多数插入的元素很快就会被删掉,花时间立刻整理它们浪费了

工作原理

7.1 从二叉堆到斐波那契堆:问题在哪里?

二叉堆的合并需要 O(n)——把一个堆的所有元素插入到另一个堆。这在 Dijkstra 最短路径算法中是真正的瓶颈:我们频繁地 DecreaseKey + ExtractMin,而二叉堆的 DecreaseKey 需要 O(log n)。

斐波那契堆的核心承诺:

操作 二叉堆 斐波那契堆(摊还)
Insert O(log n) O(1)
FindMin O(1) O(1)
ExtractMin O(log n) O(log n)
DecreaseKey O(log n) O(1)
Merge O(n) O(1)
Delete O(log n) O(log n)

这些摊还 O(1) 的操作,使得基于斐波那契堆的 Dijkstra 达到 O(V log V + E),在稠密图上优于二叉堆的 O((V+E) log V)。

7.2 结构:根链表 + 标记机制

斐波那契堆不是一棵树,而是一个根链表的森林

min ─→ [3] ⇄ [7] ⇄ [12] ⇄ [5]
        |     |           |
       [8]  [9,4]      [6,11]
             |
            [5]
  • 每棵树满足最小堆性质(父 ≤ 子)
  • 所有树根节点通过双向循环链表连接
  • min 指针指向最小的根节点
  • 节点可以丢失子节点——用 marked 标记跟踪

关键规则: 一个节点最多丢失一个子节点。丢失第二个子节点时,该节点自己也被"切掉"移到根链表。这保证了每个节点的度不会太大。

7.3 操作详解

Insert(x): 创建一个单节点树,插入到根链表。如果 x < min,更新 min。→ O(1)。

Union(H1, H2): 把两个根链表拼起来。→ O(1)。

ExtractMin():

  1. 从根链表移除 min 节点
  2. 把 min 的所有子节点提升到根链表
  3. 合并(Consolidate): 扫描根链表,合并相同度的树——这是唯一"不懒惰"的步骤
合并步骤——关键思想:
- 创建一个度数组 degree[0..D_max]
- 遍历根链表的每棵树:
  - 如果 degree[d] 为空 → 放进去
  - 如果 degree[d] 已有树 → 两棵合并(大的根成为小的根的子节点),度 d+1
- 合并后,剩余的树构成新的根链表

合并保证结果中任意两棵树的度互不相同——这是"斐波那契"名字的来源:最大度被斐波那契数约束。

DecreaseKey(x, k):

  1. 将 x 的 key 减小为 k
  2. 如果违反堆性质(x < x.parent):
    • 切掉 x 并移到根链表
    • 设置 x.marked = false
    • 如果 x.parent 已经被标记过(marked = true),递归切掉 x.parent
    • 这个"级联切割"(cascading cut)是平摊 O(1) 的关键

7.4 为什么摊还是 O(log n) 和 O(1)?

势能函数: Φ = t + 2m,其中 t 是根链表中树的数量,m 是已标记节点数。

  • Insert: 增加一棵树 → ΔΦ = 1 → 摊还代价 = O(1) + 1 = O(1)
  • DecreaseKey: 切割可能增加树并清除标记 → 但 m 的增加被 t 的有限增加平衡 → O(1)
  • ExtractMin: 合并后的树数 ≤ D_max = O(log n) → 势能大幅下降 → 摊还 O(log n)

最大度的界: D_max ≤ log_φ(n),其中 φ = (1+√5)/2 ≈ 1.618 是黄金比例。因为合并后每棵树的度互不相同,最小可能度序列就是斐波那契数列 —— 度 d 的节点至少包含 F_{d+2} 个后代。

这就是为什么叫斐波那契堆。

7.5 实践中的诚实结论

斐波那契堆在教科书中风光无限,但实际工程中极少使用。 原因:

  1. 常数因子巨大: 双向链表四指针操作、级联切割的递归、合并步骤的复杂度——每条指令的开销是二叉堆的 5-10 倍
  2. 大多数图不够大: Dijstra 用斐波那契堆的理论优势只在 |E| ≈ |V| log |V| 时显著——实际图往往更稀疏
  3. 配对堆(Pairing Heap)更实用: 思路类似,结构简单得多,实践中表现更好

面试结论: 理解斐波那契堆的设计哲学(懒惰求值、摊还势能分析、级联切割)比手写代码重要得多。面试中能说清楚"为什么 DecreaseKey 是摊还 O(1)"已是加分项。


代码实战

斐波那契堆的完整实现约 200+ 行。本章给出核心结构的骨架,完整实现作为 ⭐⭐⭐ 挑战练习。

include/algo/fibonacci_heap.h(骨架)

#ifndef ALGO_FIBONACCI_HEAP_H_
#define ALGO_FIBONACCI_HEAP_H_

#include <cmath>
#include <cstddef>
#include <unordered_map>
#include <vector>

namespace algo {

template <typename T>
class FibonacciHeap {
 public:
  FibonacciHeap() = default;

  // 禁止拷贝——节点间指针关系复杂
  FibonacciHeap(const FibonacciHeap&) = delete;
  FibonacciHeap& operator=(const FibonacciHeap&) = delete;

  FibonacciHeap(FibonacciHeap&& other) noexcept;
  FibonacciHeap& operator=(FibonacciHeap&& other) noexcept;

  ~FibonacciHeap();

  // 插入新元素,返回节点指针(用于 DecreaseKey)
  struct Node;
  Node* Insert(const T& value);

  // 查找最小值
  const T& FindMin() const;

  // 取出最小值
  T ExtractMin();

  // 将 key 减小到 new_value(new_value 必须 < 当前值)
  void DecreaseKey(Node* node, const T& new_value);

  // 删除指定节点
  void Delete(Node* node);

  // 合并另一个堆(另一个堆之后变空)
  void Merge(FibonacciHeap& other);

  bool empty() const { return min_node_ == nullptr; }
  std::size_t size() const { return size_; }

  struct Node {
    T value;
    Node* parent = nullptr;
    Node* child = nullptr;
    Node* left = this;    // 双向循环链表——新建时指向自己
    Node* right = this;
    int degree = 0;
    bool marked = false;  // 是否丢失过子节点
  };

 private:
  void Consolidate();
  void Link(Node* y, Node* x);  // 将 y 链接为 x 的子节点
  void Cut(Node* x, Node* y);   // 将 x 从 y 的子节点中切出
  void CascadingCut(Node* y);   // 级联切割

  Node* min_node_ = nullptr;
  std::size_t size_ = 0;
};

}  // namespace algo

#endif  // ALGO_FIBONACCI_HEAP_H_

本章小结

  1. 斐波那契堆的核心思想:懒惰求值——推迟合并不急的工作,在 ExtractMin 时集中处理
  2. Insert 和 DecreaseKey 的摊还 O(1) 来自势能函数 Φ = t + 2m
  3. ExtractMin 的合并(Consolidate)保证之后每棵树的度唯一 → 最大度 ≤ log_φ(n)
  4. 级联切割保证"一个节点最多丢失一个子节点",约束了度的大小
  5. 实践中配对堆更常用,但斐波那契堆是摊还分析最美的案例

关键术语

术语 释义
懒惰求值 推迟计算到结果真正需要时才执行——合并不立刻重组树
Consolidate ExtractMin 后合并所有同度树的步骤
级联切割 当一个已标记节点被切掉第二个子节点时,递归切掉它自己
势能函数 定义数据结构"状态能量"的函数,用于摊还分析
posted @ 2026-06-22 01:09  Yobeeo  阅读(6)  评论(0)    收藏  举报