代表性面试主题

如何实现一个有容量上限且稳定出队的优先队列?

编程题中等
Offer.cc 编辑团队发布 更新

题干

请实现一个容量为 C 的优先队列:优先级数值越小越先出队;同优先级按进入顺序 FIFO;满载时拒绝最低优先级的新任务。说明堆不变量、稳定排序、淘汰策略、边界和复杂度。

1. 题目

实现 StableBoundedPriorityQueue。每项包含 prioritysequencevalue,优先比较 priority,再比较 sequence。容量为 C 时,push 必须保持最多 C 项;新项比当前最差项更优才替换,否则返回拒绝。pop 返回最优项。

2. 约束与澄清

  • C 必须是正整数;C=0 时所有插入都拒绝,不能让数组越界。
  • 优先级越小越重要;同优先级严格按进入顺序出队。
  • “满载拒绝最低优先级”要求能找到最差项;单个最小堆无法在 O(log C) 内直接找最差项,因此可维护最小堆和最差项索引,或使用双堆并处理惰性删除。
  • 本题先实现单线程版本;并发访问需要外部锁或专门的并发队列。

3. 核心思路

用最小堆保存“下一项”:比较 (priority, sequence),堆顶就是应出队项。为了在满载时判断最差项,维护一个按 (priority, sequence) 的最大堆;两堆共享条目 ID,删除时采用惰性失效。每个条目只保留一次真实记录,另一堆里的旧节点通过 alive 标记跳过。

如果希望实现更短,可在容量较小或 push 不频繁的场景中线性扫描最差项:出队仍为 O(log C),插入满载时为 O(C)。面试时先说明这个取舍,再给出双堆优化。

4. 参考实现

text
record Entry(priority, sequence, value, alive=true)

push(priority, value):
  if capacity == 0: return false
  candidate = Entry(priority, nextSequence(), value)
  if size < capacity:
    add candidate to minHeap and maxHeap
    size += 1
    return true
  discard dead nodes from maxHeap
  worst = maxHeap.peek()
  if (priority, candidate.sequence) >= (worst.priority, worst.sequence):
    return false
  worst.alive = false
  pop maxHeap
  add candidate to both heaps
  return true

pop():
  discard dead nodes from minHeap
  if minHeap is empty: return EMPTY
  entry = pop minHeap
  entry.alive = false
  size -= 1
  return entry.value

maxHeap 的键是“越差越大”:优先级越大越差;同优先级时序号越大越晚进入,也越差。若语言没有最大堆,可把键取负,或实现一个比较器。nextSequence 使用单调递增整数,溢出时应采用足够宽的整数或在队列为空时重置。

5. 复杂度与取舍

非满载插入向两个堆各插入一次,复杂度 O(log C);出队为 O(log C)。满载替换仍为 O(log C),但惰性删除可能让堆暂时包含失效节点;每个失效节点只会被弹出一次,因此摊销复杂度仍为 O(log C),空间为 O(C) 的常数倍。线性扫描版本空间更小、代码更短,但满载插入是 O(C)

6. 验证与观测

  • C=0C=1、空队列、连续拒绝和连续替换都要覆盖。
  • 插入相同优先级的多项,确认 pop 顺序与 sequence 一致。
  • 插入优先级更差、相同、更加优的项目,分别检查拒绝、拒绝和替换。
  • 随机操作后,将结果与“保存全部元素后按 (priority, sequence) 排序并截断 C”的模型比较。
  • 记录队列长度、拒绝计数和惰性节点清理次数;拒绝率持续升高时应触发上游限流或负载卸载策略。

7. 常见误区

  • 只用 priority 排序,导致同优先级项目顺序不稳定。
  • 把数值更大的优先级当成更重要,却没有先和面试官确认方向。
  • 满载时直接弹出堆顶再插入新项,误删了最优任务。
  • 双堆中的失效节点没有清理,peek 读到已被替换的条目。
  • sequence 使用时间戳,时钟回拨或同一毫秒多次插入会破坏 FIFO。

8. 追问及应对

如果要求按租户配额,怎样防止一个租户占满队列?

为每个租户维护计数和上限,插入前同时检查全局容量与租户配额;配额拒绝和全局拒绝分开计数,便于识别热点租户。

如果任务可以取消或更新优先级怎么办?

使用条目 ID 和惰性删除:取消只把 alive 设为假;更新创建新条目并让旧条目失效。出队或查看最差项时清理失效节点,避免在堆中做任意位置删除。

什么时候应改用现成的并发优先队列?

当多个线程或进程同时生产消费、需要阻塞等待或严格内存上限时,优先使用经过验证的并发实现;自制双堆只适合明确的单线程边界和可测试的生命周期。

公开来源

同类题目

相关面试工具

用 Screenshot 处理算法题

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

查看工具