代表性面试主题

编程面试:如何实现支持 O(1) 操作的稀疏集合?

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

题干

给定整数范围 [0, U),请实现支持 insert、remove、contains、clear 和 iterate 的集合,所有更新与查询都要求 O(1)。

题目与范围

给定固定整数宇宙 [0, U),实现一个集合,支持 insert(x)remove(x)contains(x)clear() 和遍历当前元素。要求前四项为最坏 O(1),遍历为当前元素数 O(k)。集合不允许重复值,删除不存在的值不报错。

公开面经记录了 Pure Storage 的同型题目;题目真正考察的是 dense/sparse 数组不变量,而不是记忆某个库的类名。

面试官考察什么

  • 是否明确固定宇宙是前提,不能把 O(1) 结论无条件推广到任意整数。
  • 是否同时维护 dense[sparse[x]] == x,并用它避免删除后的误判。
  • 删除是否采用末元素交换,从而保持 dense 前缀连续且遍历为 O(k)
  • 是否说明空间为 O(U),当集合很稠密或宇宙未知时应考虑其他结构。

作答前澄清

  1. U 是否已知且可接受两条长度为 U 的数组?这是稀疏集合成立的资源前提。
  2. iterate() 是否要求排序?本方案只保证遍历全部成员,不保证顺序。
  3. 是否需要稳定迭代器或并发访问?若需要,交换删除和同步策略都要重新定义。
  4. clear() 是否必须避免扫描 U?本题要求常数时间,因此只重置 size

30 秒回答

“我维护长度为 U 的 sparse 索引和长度为 U 的 dense 数组,再保存当前 size。元素 x 存在,当且仅当 sparse[x] < sizedense[sparse[x]] == x。插入把 x 写到 dense[size] 并记录索引;删除时用最后一个元素覆盖被删位置并修正它的索引;清空只把 size 置零。四个核心操作都是最坏 O(1),遍历 dense 的前 size 个位置是 O(k),空间是 O(U)。”

分步深答

第一步:定义不变量。

dense[0..size) 保存全部成员且没有重复;如果 x 在集合中,则 sparse[x] 是它在 dense 中的位置,且 dense[sparse[x]] == x。对不在集合中的值,sparse 内容可以是旧值,所以 contains 不能只检查索引范围。

第二步:查询与插入。

contains(x) 先检查 0 <= x < U,再验证 sparse[x] < size 和反向链接。插入先调用 contains;若不存在,把它写入 dense[size],设置 sparse[x] = size,最后递增 size。

第三步:交换删除。

x 在位置 i,令 last = dense[size - 1],把 last 写入 dense[i],更新 sparse[last] = i,再递减 size。无需清理 sparse[x],因为 size 变化后反向链接校验会使它失效;删除最后一个元素也遵循同一流程。

第四步:常数时间清空与线性遍历。

clear() 只设置 size = 0,旧数组内容不会被读取为有效成员。遍历只扫描 dense[0]dense[size - 1],因此耗时是 O(k),而不是 O(U)

第五步:复杂度与适用边界。

containsinsertremoveclear 都是最坏 O(1);遍历是 O(k);空间是 O(U)。GCC 的实现还指出,稀疏集合适合固定宇宙且需要密集遍历的场景;宇宙未知、需要扩容或 U 远大于可用内存时,哈希集合或位图可能更合适。

第六步:测试不变量。

用参考 Set 逐次对照随机操作。覆盖空集合、重复插入、删除不存在值、删除中间元素、删除最后元素、清空后复用、0U-1,并在每次操作后验证 dense 前缀无重复、每个成员的反向链接成立。

高质量示范回答

“固定宇宙 [0, U) 让我可以用两个数组换取确定的常数时间。dense 保存当前成员的紧凑前缀,sparse 把值映射回 dense 下标;成员判断必须同时检查边界、下标小于 size 和反向链接,不能只看 sparse 数值。删除用最后元素覆盖目标位置并更新其 sparse 下标,clear 只重置 size。这样更新和查询是最坏 O(1),遍历是 O(k),空间是 O(U);当宇宙不可控时我会改用哈希表或位图。”

常见错误

  • 只判断 sparse[x] < size 未加入的值可能残留合法下标 → 补上 dense[sparse[x]] == x
  • 删除后整体搬移 → 删除退化为 O(U)O(k)用最后元素交换。
  • 把 clear 写成填零循环 → 清空变成 O(U)只重置 size。
  • 忽略固定宇宙 → 数组索引越界或内存不可接受 → 先确认 [0, U) 与容量约束。
  • 声称遍历是 O(1) → 取到视图是常数,处理全部元素仍需 O(k)区分返回视图与消费元素。

追问与回答

追问 1:如何支持任意整数?

先做坐标压缩,把实际值映射到 [0, U);若值域持续增长或无法预扫描,哈希集合更自然,但其常数时间是均摊或期望意义,不能复用本题的最坏 O(1) 结论。

追问 2:如何保证遍历顺序?

当前 dense 的顺序会被交换删除改变。若要求插入顺序,需要额外链表或稳定数组,删除和空间开销都会变化;应先确认顺序是否属于接口合同。

追问 3:什么时候选择位图?

若只需要成员判断、宇宙规模适中且希望每个值占一位,位图更省空间。若还需要快速枚举成员,稀疏集合的 dense 前缀通常更适合;最终选择取决于 U、成员数和访问模式。

公开来源

同类题目

相关面试工具

用 Screenshot 处理算法题

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

查看工具