题目难度: 困难
今天我们来做一道包含随机因素的设计题, 这道题是 380. 常数时间插入、删除和获取随机元素的进阶版, 感兴趣的同学可以先看看那道题如何解决
大家有什么想法建议和反馈的话欢迎随时交流, 包括但不限于公众号聊天框/知乎私信评论等等~
题目描述
设计一个支持在平均时间复杂度 O(1) 下,执行以下操作的数据结构。
- insert(val):当元素 val 不存在时,向集合中插入该项。
- remove(val):元素 val 存在时,从集合中移除该项。
- getRandom:随机返回现有集合中的一项。每个元素应该有相同的概率被返回。
题目样例
示例
// 初始化一个空的集合。 RandomizedSet randomSet = new RandomizedSet(); // 向集合中插入 1 。返回 true 表示 1 被成功地插入。 randomSet.insert(1); // 返回 false ,表示集合中不存在 2 。 randomSet.remove(2); // 向集合中插入 2 。返回 true 。集合现在包含 [1,2] 。 randomSet.insert(2); // getRandom 应随机返回 1 或 2 。 randomSet.getRandom(); // 从集合中移除 1 ,返回 true 。集合现在包含 [2] 。 randomSet.remove(1); // 2 已在集合中,所以返回 false 。 randomSet.insert(2); // 由于 2 是集合中唯一的数字,getRandom 总是返回 2 。 randomSet.getRandom();
题目思考
- 注意题目要求, 要使得每个元素返回的概率都相同, 但又存在重复, 你想到了哪些方法可以保证这种平均性?
- 要使得三种操作平均复杂度都是 O(1), 需要哪些数据结构的组合?
解决方案
思路
- 先设计需要使用的数据结构
- 考虑随机元素取值要平均, 自然想到可以使用一个列表存下来所有的元素, 然后每次直接取一个长度范围内的下标对应的元素即可
- 如何保证插入和删除也要 O(1), 我们需要额外引入一个字典/hash, 因为删除的是元素值, 那么字典中存的 key 就需要是元素值, 这样才能在 O(1)时间内定位
- 而为了关联起来字典和列表, 字典的值需要是列表下标的集合. 为什么不能是下标列表呢, 这是为了删除的时候也能做到 O(1)
- 接下来就是写具体的逻辑了, 这里一共有 3 种操作:
- 获取随机值: 只需要得到一个在长度范围内的随机下标即可, 只需要 O(1)时间, 而且这样保证了每个元素都有相同的概率被取到
- 插入值: 列表直接追加一个元素, 并更新对应的值和下标组合到字典中, 只需要 O(1)时间
- 删除值: 删除的操作比较复杂, 因为为了保证删除列表元素也只使用 O(1)时间, 我们需要额外的处理
- 首先定位到字典对应值的下标集合, 如果列表最后一个下标恰好在该集合, 直接把列表 pop 一下, 然后字典中移除最后的下标即可, 都只需要 O(1)时间
- 否则可以先从集合 pop 一个下标出来, 然后交换该下标和列表最后一个下标的值, 并更新字典中对应键值对的结果, 最后采用上一步的操作即可, 也只需要 O(1)时间
- 最后就是具体的代码部分了, 下面代码对每步操作都有详细的注释, 希望可以帮助大家更好理解
复杂度
- 时间复杂度 O(1): 每种操作都只需要 O(1)时间, 不管是列表 pop, 集合 remove 还是字典更新
- 空间复杂度 O(N): 需要存 N 个元素
代码
Python 3
from collections import defaultdict import random class RandomizedCollection: def __init__(self): """ Initialize your data structure here. """ # 使用字典+列表的组合 # 列表存每个元素值 # 字典存{元素值:{对应的列表所有下标}} self.dict = defaultdict(set) self.list = [] def insert(self, val: int) -> bool: """ Inserts a value to the collection. Returns true if the collection did not already contain the specified element. """ notExist = True if val in self.dict: notExist = False # 列表追加值 self.list.append(val) # 字典更新val和对应的下标 self.dict[val].add(len(self.list) - 1) return notExist def remove(self, val: int) -> bool: """ Removes a value from the collection. Returns true if the collection contained the specified element. """ exist = False if val in self.dict and self.dict[val]: exist = True lastIndex = len(self.list) - 1 if lastIndex in self.dict[val]: # 只需要pop移除列表的最后一个元素 self.dict[val].remove(lastIndex) self.list.pop() else: # 列表最后的下标不在集合中, 需要下标交换 index = self.dict[val].pop() lastVal = self.list[lastIndex] # 交换当前下标和最后下标的值, 并将列表pop, (O(1)) self.list[index], self.list[lastIndex] = self.list[ lastIndex], self.list[index] self.list.pop() # 注意, 也需要更新对应的字典, 由于最后的下标已经移除了, 所以这时候不需要在dict[val]中加入lastIndex self.dict[lastVal].add(index) self.dict[lastVal].remove(lastIndex) return exist def getRandom(self) -> int: """ Get a random element from the collection. """ index = random.randint(0, len(self.list) - 1) return self.list[index]
C++
class RandomizedCollection { public: /** Inserts a value to the collection. Returns true if the collection did not already contain the specified element. */ bool insert(int val) { bool ret = notExist(val); list.push_back(val); dict[val].insert(list.size() - 1); return ret; } /** Removes a value from the collection. Returns true if the collection contained the specified element. */ bool remove(int val) { if (notExist(val) || dict[val].empty()) { return false; } int lastIndex = list.size() - 1; if (dict[val].find(lastIndex) != dict[val].end()) { dict[val].erase(lastIndex); list.pop_back(); } else { int index = *dict[val].begin(); dict[val].erase(index); int lastValue = list[lastIndex]; list[index] = lastValue; list.pop_back(); dict[lastValue].insert(index); dict[lastValue].erase(lastIndex); } return true; } /** Get a random element from the collection. */ int getRandom() { return list[rand() % list.size()]; } private: bool notExist(int val) { return dict.find(val) == dict.end(); } private: vector<int> list; unordered_map<int, unordered_set<int>> dict; };
大家可以在下面这些地方找到我~😊
我的公众号: 每日精选算法题, 欢迎大家扫码关注~😊