NG SWE · Super Day VO1 Coding / OOD

Bloomberg Lottery System:O(1) 增删与均匀随机抽取

完整 OOD 接口来自题目;用动态数组和 ID→下标映射实现增删及均匀取样,解释末尾交换不变量。

题目重点:完整 OOD 接口来自题目;用动态数组和 ID→下标映射实现增删及均匀取样,解释末尾交换不变量。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • ID 唯一;重复 add 返回 false、缺失 remove 返回 false;空集 pick 返回 -1,按题目接口。
  • 随机源可等概率生成 [0,n) 整数,O(1) 指期望时间与动态数组均摊追加,不保证强密码学随机。

解题路线

  1. 用 ids 动态数组存现有参与者,用 position[id] 记录其下标;保持两个结构双向一致。
  2. 新增时先查映射,若不存在将 ID 追加并记录尾下标。
  3. 删除任意 ID 时取其位置,将数组末尾 ID 换到空位并更新该末尾 ID 的映射,再 pop;若删的本来就是末尾,无需覆盖。
  4. 抽取时均匀选择 [0,len(ids)) 下标;末尾交换改变顺序,不改变每个 ID 恰有一个下标,因此每人概率 1/n。并发版需在同一锁/事务内维护数组与映射。

手动走一遍

ids=[7,9,4],删除 9:用末尾 4 覆盖下标 1,pop 后 ids=[7,4],position[4]=1 并删 position[9]。现在 pick 的两个下标分别对应 7、4,各 1/2。

Python 参考实现

下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。

代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。

from random import randrange


class LotterySystem:
    def __init__(self, pick_index=randrange):
        self.ids = []
        self.position = {}
        self.pick_index = pick_index

    def addParticipant(self, participant_id):
        if participant_id in self.position:
            return False
        self.position[participant_id] = len(self.ids)
        self.ids.append(participant_id)
        return True

    def removeParticipant(self, participant_id):
        index = self.position.pop(participant_id, None)
        if index is None:
            return False
        last = self.ids.pop()
        if index < len(self.ids):
            self.ids[index] = last
            self.position[last] = index
        return True

    def randomPick(self):
        if not self.ids:
            return -1
        return self.ids[self.pick_index(len(self.ids))]

复杂度

add/remove/pick 期望 O(1),空间 O(n);动态数组追加均摊 O(1)。若要求线程安全,锁竞争成本另算。

易错点与追问

容易失分的边界

  • 数组中间直接删除导致 O(n) 位移。
  • 末尾换入后忘记改 position。
  • 空集调用随机数。
  • 删除失败仍修改状态。
  • 把伪随机均匀抽样说成安全随机。

继续深挖

  • 若抽取后自动移除赢家怎么办?
  • 按权重抽奖还能三操作都 O(1) 吗?
  • 多线程并发增删如何保持不变量?
  • 如何做可重复测试的随机源注入?
✓已复制微信号!