代表性面试主题

编码面试:如何实现一个随机可合并堆?

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

题干

请实现支持 meld、insert、find-min 和 extract-min 的随机可合并最小堆,并解释随机化如何影响复杂度、如何处理重复键、节点别名和深递归。

题干与适用场景

你要为任务调度器实现一个可合并的最小优先队列。调用方频繁把两个队列合并,再插入任务并取出最小优先级。请实现 meldinsertfind-minextract-min,说明随机化、空堆、重复键和节点所有权。

随机可合并堆用二叉树表达堆序,不维护左偏堆的秩等额外元数据。随机选择递归合并进入左子树或右子树,重点考察不变量、概率分析和可测试性。

面试官考察点

重点包括根键最小不变量、meld 的交换与结合语义、随机数生成器边界、重复键、节点归属、递归深度、销毁策略,以及期望复杂度和最坏情况的区分。候选人还应能比较二叉堆、左偏堆和配对堆的适用条件。

回答前需要澄清的问题

  • meld 是否允许消耗输入堆,还是必须保留两个原堆?
  • 随机种子能否注入,以便重放失败测试?
  • 最大节点数和递归栈预算是多少?
  • 是否需要稳定句柄、删除任意节点或 decrease-key
  • 目标是教学实现、工程吞吐,还是需要严格最坏界?

30 秒回答框架

“每个节点保存键、值、左右子树。meld(a,b) 先处理空树,再让较小根成为结果根;随机决定把另一棵树与左子树或右子树递归合并。insert 用单节点与根 meld,extract-min 用根的左右子树 meld。这样根始终最小,操作通常为期望对数时间,但递归深度和随机种子必须测试与约束。”

分步骤深入解答

第一步:定义节点和所有权

节点至少保存 keyvalueleftright。堆对象保存根和节点数。若 meld 采用可变实现,输入堆的根会被重新连接,接口必须明确输入是否失效;若要求持久化,则需要路径复制,复杂度和空间都会改变。

text
meld(a, b):
  if a is empty: return b
  if b is empty: return a
  if b.key < a.key: swap(a, b)
  if randomBit() == 0:
    a.left = meld(a.left, b)
  else:
    a.right = meld(a.right, b)
  return a

第二步:维护 meld 不变量

先比较两个根,较小键保留为根;相等键可按固定的平局规则或随机规则处理,但不能破坏堆序。递归返回后,结果子树的所有键仍不小于当前根,因此不变量沿路径保持。

实现中要避免把同一节点同时挂到两个父节点下。可变 meld 应记录输入所有权,调试版本可检查节点计数和无环;持久化版本则不能直接复用会被修改的子树。

第三步:实现 insert 与 find-min

insert 创建单节点树并与当前根 meld,节点数加一。find-min 读取根键;空堆按接口返回空值或错误。重复键不应被去重,值相同也不代表节点相同。

如果随机源是全局状态,测试难以重放。更稳妥的做法是把随机源作为依赖注入,并在测试中使用固定种子;生产环境仍需避免低质量或偏置的随机位实现。

第四步:实现 extract-min

取出根后,新的根是原根左右子树的 meld。先断开旧根的两个指针,再减少节点数;若堆拥有节点内存,最后释放旧根。输入堆被消耗时,旧根句柄必须标记失效,避免再次参与合并。

若需要保留旧版本,就采用持久化路径复制,不能在共享节点上原地改指针。该选择应在接口层写清楚,否则调用方会遇到别名导致的隐蔽数据损坏。

第五步:说明复杂度边界

随机可合并堆的 meldinsertextract-min 通常可给出期望对数时间或高概率对数时间的分析,具体表述取决于随机模型和实现细节。find-min 是常数时间。空间为节点数的线性级别。

不要把期望界说成每次操作的最坏界;恶劣随机序列可能形成较深树。生产实现要限制递归风险,必要时改成显式栈或设置深度监控,并用基准和随机测试验证分布。

第六步:测试与对照实现

把堆与标准优先队列对拍,覆盖空堆、重复键、交替 meld、连续 extract-min、固定随机种子和极端深度。每次操作后检查根最小、节点数正确、无环和所有权约束。

与配对堆相比,本题使用二叉树和随机分支,不需要兄弟链表、两遍合并或句柄切断;与左偏堆相比,不维护秩元数据,换取概率化分析。比较时要同时说明缓存局部性、可变语义和证明要求。

高质量示范回答

我会把最小键留在根,meld 对空树直接返回,非空时交换根使 a 更小,再随机把 ba.lefta.right 合并。insertextract-min 都复用 meldfind-min 读根。实现前确认 meld 是否消耗输入;测试注入固定随机源,对拍标准优先队列,检查无环、计数和根最小。复杂度表述为随机模型下的期望或高概率对数时间,并单独处理递归深度风险。

常见错误

  • 随机分支放在比较根之前 → 根可能不是最小键 → 先交换根,再随机选择子树。
  • 可变 meld 后继续使用旧堆 → 节点同时拥有两个父节点 → 明确消耗语义或实现持久化。
  • 把期望界写成最坏 O(log n) → 概率假设被遗漏 → 注明随机模型和高概率边界。
  • 随机源不可注入 → 失败用例无法重放 → 依赖注入并固定种子测试。
  • 递归无深度策略 → 极端树可能耗尽调用栈 → 显式栈、深度监控或文档化限制。

追问及应对

追问一:如何让测试完全确定?

把随机位生成器作为堆实例依赖,测试传入固定序列或种子;生产再使用独立实例,避免全局随机状态让多个测试相互影响。

追问二:meld 必须保留输入时怎么办?

采用持久化实现,对沿递归路径的节点做路径复制,未修改的子树共享。需要同步更新空间复杂度和垃圾回收策略,不能继续宣称原地常数额外空间。

追问三:如何处理两个堆都引用同一节点?

可变 API 应禁止跨堆共享并在调试模式记录归属;持久化 API 允许结构共享,但所有节点必须不可变。发现归属冲突时返回错误,不要静默修复。

追问四:为什么不用配对堆?

配对堆适合需要 decrease-key 的场景,但要维护多叉子链和删除后的重组;随机可合并堆的二叉 meld 更短,适合只需要合并、插入和取最小值且能接受概率化保证的接口。

公开来源

同类题目

相关面试工具

用 Screenshot 处理算法题

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

查看工具