← 返回题目列表

什么是可合并堆?左偏堆、斜堆和二项堆解决了什么问题?

困难 第 28 / 28 题 更新于 2026/07/30
可合并堆左偏堆二项堆

简化版

可合并堆重点解决的是「两个堆如何高效合并」。

普通二叉堆适合插入和删除堆顶,但合并两个堆通常需要把元素重新建堆,成本较高。左偏堆、斜堆、二项堆、斐波那契堆等结构,会把 merge 设计成核心操作。

它们常出现在算法竞赛、图算法理论和高级数据结构面试追问中,业务开发里不如普通堆常见。

详细版

普通二叉堆用数组表示完全二叉树,这让它缓存友好,但也让「合并」不方便。两个数组堆直接拼起来后,堆序未必成立,需要重新 heapify。

可合并堆一般采用指针结构,把合并两个堆设计成基础操作。

结构merge 特点常见用途
左偏堆利用零路径长保持右路径短支持较快合并
斜堆自调节,不显式维护距离实现相对简单
二项堆由多棵二项树组成类似二进制进位合并
斐波那契堆摊还复杂度优秀理论分析常见

面试中不一定要求手写,但要知道它们解决的问题不是替代所有堆,而是优化 merge、decrease-key 等操作。

完整版教学

1. 普通二叉堆为什么不擅长合并

普通二叉堆最大的优势是数组紧凑存储。

但它的结构要求是完全二叉树,所以两个堆合并时不能简单把根连起来。最直接的做法是:

newArray = heapA + heapB
heapify(newArray)

这个过程是 O(n + m)

如果系统里经常要合并优先队列,比如合并多个事件集合、合并多个分支状态、合并多个集合的最小值,那么普通二叉堆就不够优雅。

2. 可合并堆的核心思想

可合并堆把 merge 当作核心操作。

一旦有了高效 merge:

  • 插入一个元素可以看成「当前堆 merge 一个单节点堆」;
  • 删除堆顶可以看成「merge 根节点的两个子堆」;
  • 合并两个优先队列可以直接调用 merge。

可合并堆的设计哲学是:先把合并做好,其他操作自然变成合并的变体。

3. 左偏堆如何保持合并效率

左偏堆维护一个概念:零路径长,也就是到空节点的最短距离。

它要求左子树的零路径长不小于右子树,直觉上就是让右路径尽量短。

合并两个左偏堆时,通常沿着右路径递归合并:

merge(a, b):
  if a is null: return b
  if b is null: return a
  if b.key < a.key: swap(a, b)
  a.right = merge(a.right, b)
  if npl(a.left) < npl(a.right): swap(a.left, a.right)
  update npl(a)
  return a

因为右路径较短,所以合并效率较好。

4. 斜堆为什么说是自调节结构

斜堆和左偏堆很像,但它不显式维护零路径长。

它在每次 merge 时直接交换左右子树,依靠长期操作的摊还效果避免结构退化。

结构是否维护额外信息实现特点
左偏堆维护零路径长更有结构约束
斜堆不维护距离更简单,自调节

面试里可以把它类比成「类似 splay tree 的自调节思想」,不追求每一步绝对平衡,而追求整体摊还效率。

5. 二项堆为什么像二进制加法

二项堆由一组二项树组成,每个阶数最多一棵。

这和二进制数字很像:某一位要么有,要么没有。合并两个二项堆时,如果同阶树相遇,就把其中根较大的树挂到根较小的树下面,产生更高一阶的树。

B0 + B0 -> B1
B1 + B1 -> B2

这种结构让二项堆的合并可以做到对数级。

6. 斐波那契堆为什么常出现在理论题

斐波那契堆的 decrease-key 摊还复杂度可以做到 O(1),删除最小值是 O(log n)

这让它在 Dijkstra、Prim 等算法理论复杂度分析里很漂亮。

但工程上它很少作为首选,因为:

  1. 指针结构复杂;
  2. 常数开销大;
  3. 缓存局部性差;
  4. 实现维护成本高;
  5. 标准库通常不提供。

所以面试回答要区分「理论复杂度」和「工程实践」。

7. 可合并堆和普通堆怎么选

可以按场景判断:

场景更适合
只需要 push / pop普通二叉堆
需要频繁合并两个堆可合并堆
追求标准库和稳定实现普通优先队列
算法竞赛高级结构左偏堆、斜堆、二项堆
理论复杂度分析斐波那契堆

如果面试官问「为什么实际业务不用斐波那契堆」,重点回答常数、实现复杂度和缓存局部性。

8. 常见误区与追问

  • 误区:可合并堆一定比二叉堆高级,所以更应该用。 它优化的是合并等特定操作,不是所有操作都更适合。
  • 误区:普通二叉堆不能合并。 可以合并,只是通常要重新建堆,成本是线性级。
  • 误区:斐波那契堆工程上一定最快。 它理论摊还复杂度好,但常数和实现复杂度很高。
  • 追问:插入为什么可以看成 merge? 因为插入一个元素等价于把当前堆和单节点堆合并。
  • 追问:删除堆顶为什么也依赖 merge? 删除根后,需要把根的子堆重新合成一个堆。