代表性面试主题

编程面试:如何实现 Fibonacci Heap 并解释 decrease-key 的摊销复杂度?

编程题困难
Offer.cc 编辑团队发布 更新

题干

请实现 Fibonacci Heap 的 insert、meld、find-min、extract-min、decrease-key 和 delete,并证明主要操作的摊销复杂度。

题目

请实现一个支持 insert、meld、find-min、extract-min、decrease-key 和 delete 的 Fibonacci Heap。解释根链表、父子关系、degree、mark 标记和级联切断如何协作,并用势能函数说明为什么 insert、meld、find-min、decrease-key 可以达到 O(1) 摊销时间,而 extract-min 为 O(log n) 摊销时间。

面试官考察点

  • 能否区分实际单次成本与摊销成本,不把 O(1) 摊销说成每次都 O(1)。
  • 能否维护循环双向链表、最小根指针、节点句柄和父指针。
  • 能否正确实现 decrease-key 的切断、标记和级联切断。
  • 能否说明 Fibonacci Heap 的理论优势、工程常数和与 pairing heap、binary heap 的取舍。

参考答案

Fibonacci Heap 是一组满足堆序的树。根节点组成循环双向链表,每个节点保存 parent、child、degree 和 mark。结构允许先延迟合并,extract-min 时再把根链表中的树按 degree 合并。

insert 只需把新节点加入根链表并更新最小值;meld 把两个根链表拼接。decrease-key 若破坏堆序,就把节点从父节点的 child 链表切下并放入根链表;如果父节点已经被切过,再递归级联切断。mark 表示节点是否已经失去一个孩子,用于限制级联损失。

extract-min 将最小根的孩子提升到根链表,删除该根,再按 degree 反复链接同阶树。势能通常定义为根数加上两倍的标记节点数。插入和 meld 增加根数但只支付常数,decrease-key 的级联切断减少标记节点并由势能抵消;extract-min 的实际链接次数被树高限制在 O(log n)。

实现示例

下面的伪代码只展示 decrease-key 的关键路径,节点句柄由调用方保存。

text
decreaseKey(x, newKey):
  if newKey > x.key: error
  x.key = newKey
  p = x.parent
  if p is not empty and x.key < p.key:
    cut(x, p)
    cascadingCut(p)
  if x.key < min.key:
    min = x

cut(x, p):
  removeFromChildList(p, x)
  p.degree -= 1
  addToRootList(x)
  x.parent = empty
  x.mark = false

cascadingCut(y):
  p = y.parent
  if p is empty: return
  if y.mark is false:
    y.mark = true
  else:
    cut(y, p)
    cascadingCut(p)

实现 extract-min 时要先安全遍历被提升的孩子,再删除最小根。链接同阶根节点时必须更新 parent、child、degree 和 mark,并在最后重新扫描根链表找到新的 min。

常见误区

  • 只写出二叉堆式数组实现,却声称支持 Fibonacci Heap 的 O(1) 摊销 decrease-key。
  • 切断节点后忘记清空 parent 或 mark,导致下一次级联切断错误。
  • 在循环双向链表遍历时边删除边使用失效的 next 指针。
  • extract-min 后只比较根节点,不把新提升的孩子纳入根链表和最小值扫描。
  • 只比较渐进复杂度,忽略指针跳转、缓存局部性、内存分配和实现复杂度。

复杂度取舍

在 decrease-key 很频繁、需要 meld 且分析模型允许摊销时,Fibonacci Heap 的理论优势明显,经典应用是改进 Prim 或 Dijkstra 的边界分析。实际工程中 pairing heap、rank-pairing heap 或 binary heap 常因更简单、更好的缓存行为而更有竞争力。

Fibonacci Heap 的复杂度依赖节点句柄;如果调用方只能按键查找节点,额外索引会改变设计。多线程环境还需要明确根链表和句柄的所有权,不能把无锁安全性从摊销分析中推导出来。

先验证单节点、重复键、meld 空堆、连续 decrease-key 和删除最后一个节点。再生成随机操作序列,与一个参考优先队列比较最小值和 extract-min 顺序。针对级联切断构造一个节点连续失去两个孩子的测试,确认第一次只标记、第二次才切断。

参考资料

  • MIT OpenCourseWare Fibonacci heaps 讲义:势能分析与 decrease-key、extract-min 复杂度。
  • Fibonacci Heaps Revisited:级联切断和摊销边界的再分析。
  • Fredman 与 Tarjan 的原始论文:Fibonacci Heap 及其网络优化算法应用。

追问

为什么 mark 节点要在势能中计两倍?

级联切断会减少一个标记节点并增加一个根节点。给标记节点两单位势能,可以支付清除标记和加入根链表的成本,使整段级联的摊销代价保持常数。

extract-min 为什么是 O(log n) 摊销?

删除最小根并提升孩子后,链接过程让每个 degree 至多保留一棵树。堆序保证节点的 degree 与子树规模相关,因此最大 degree 是 O(log n),根链表整理的链接次数也受此限制。

meld 为什么可以 O(1) 摊销?

两个循环根链表可以直接拼接,并比较两个 min 指针。没有立即合并同阶树,代价被留给未来的 extract-min。

什么时候 binary heap 更合适?

当 decrease-key 不频繁、需要数组局部性、节点句柄不方便维护或团队更看重可读性时,binary heap 的 O(log n) 操作和简单实现通常更合适。

如何处理 delete?

常见做法是把节点的键降到负无穷,再执行 extract-min;生产实现必须定义键域、哨兵值和句柄失效规则,避免把正常业务键误当成哨兵。

公开来源

同类题目

相关面试工具

用 Screenshot 处理算法题

截图题目后,按顺序看约束、解法、代码、边界条件和复杂度。

查看工具