在问 · 算法题解

381. O(1) 时间插入、删除和获取随机元素 - 允许重复

题目描述

实现一个允许重复元素的多重集合类 RandomizedCollection:

RandomizedCollection():创建空集合。insert(val):无论是否已有,都加入一个 val;如果加入之前集合里没有 val,返回 true,否则返回 false。

remove(val):如果集合里有 val,只删掉其中一个并返回 true;没有则返回 false。

getRandom():随机返回集合中的一个元素。某个值被返回的概率与它在集合中的个数成正比。调用时集合保证非空。

每个方法的平均时间复杂度都必须是 O(1)。示例中第一行为方法名,第二行为参数,构造函数的返回记为 null。

示例 1
输入:操作 = ["RandomizedCollection","insert","insert","insert","getRandom","remove","getRandom"]
参数 = [[],[1],[1],[2],[],[1],[]]
输出:[null,true,false,true,1,true,2]

解释:插入后集合为 [1,1,2],第一次随机返回了 1;删掉一个 1 后为 [1,2],第二次随机返回了 2。随机结果每次运行可能不同。

数据范围

  • -2^31 <= val <= 2^31 - 1
  • insert、remove、getRandom 总共最多调用 2 * 10^5 次
  • 调用 getRandom 时集合中至少有一个元素。

解题思路

在 380 的基础上,pos 改为“值 → 下标集合”。删除时从 pos[val] 取任意一个下标 i,把末尾元素 last 搬到 i:先把 i 加入 pos[last],再从 pos[last] 删去末尾下标,最后 pop。

为什么正确

arr 紧凑,且对每个下标 i,i ∈ pos[arr[i]]。

复杂度分析

三个操作均为平均 O(1);空间 O(n)。

易错点

如果先删末尾下标再加 i,当 i 恰好等于末尾下标时会把一个已弹出的下标留在 pos 里,之后越界。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

class RandomizedCollection:
    def __init__(self):
        self.arr = []
        self.pos = defaultdict(set)

    def insert(self, val: int) -> bool:
        self.pos[val].add(len(self.arr))
        self.arr.append(val)
        return len(self.pos[val]) == 1

    def remove(self, val: int) -> bool:
        if not self.pos[val]:
            return False
        i, last = self.pos[val].pop(), self.arr[-1]
        self.arr[i] = last
        self.pos[last].add(i)
        self.pos[last].discard(len(self.arr) - 1)
        self.arr.pop()
        return True

    def getRandom(self) -> int:
        return random.choice(self.arr)

载入编辑器,自己跑一遍 →

设计 专题的其他题目

查看 设计 全部题目 →