LeetCode 381 - O(1) 时间插入、删除和获取随机元素(允许重复)
文章目录
- 摘要
- 描述
- 题解答案
- 题解代码分析
- 代码拆解
- 示例测试及结果
- 时间复杂度
- 空间复杂度
- 总结
摘要
这道题有点意思,它让我们实现一个特殊的数据结构 RandomizedCollection
,要求能在 平均 O(1) 时间 内完成插入、删除和随机获取元素,而且集合里是允许有重复值的。
听起来像是 HashSet
+ Array
的结合体,但允许重复元素就让问题变得更 tricky。接下来我会详细展开解决方案、代码解析,以及跟实际场景结合,让你彻底搞懂这道题。
描述
我们需要实现一个 RandomizedCollection
,支持以下操作:
- 初始化:创建一个空集合
- insert(val):把元素插入集合。即使该元素已经存在也要插进去。如果元素之前不存在,返回
true
,否则返回false
。 - remove(val):删除集合里的一个
val
,如果存在返回true
,否则返回false
。注意如果有多个相同元素,只删除其中一个。 - getRandom():随机返回集合中的一个元素,返回的概率跟该元素出现的次数成正比。
举个例子:
let collection = RandomizedCollection()
collection.insert(1) // true, 集合 = [1]
collection.insert(1) // false, 集合 = [1, 1]
collection.insert(2) // true, 集合 = [1, 1, 2]
collection.getRandom() // 2/3 概率返回 1, 1/3 概率返回 2
collection.remove(1) // true, 集合 = [1, 2]
collection.getRandom() // 50% 概率返回 1, 50% 概率返回 2
题解答案
核心思路是结合 动态数组 + 哈希表:
- 用一个数组
nums
存储所有元素,这样我们可以 O(1) 时间随机访问。 - 用一个字典
indices
存储每个值对应的 下标集合(因为有重复,所以要存多个下标)。 - 插入时:直接追加到数组,并把下标存进字典。
- 删除时:为了保持 O(1),我们把要删的元素跟最后一个元素交换,再 pop 掉数组末尾,这样就不会有“挪动一大段数组”的开销。字典里的下标集合也要同步更新。
- 随机获取:直接从数组里
randomElement()
即可。
题解代码分析
下面是 Swift 实现:
import Foundationclass RandomizedCollection {private var nums: [Int]private var indices: [Int: Set<Int>]init() {nums = []indices = [:]}// 插入元素func insert(_ val: Int) -> Bool {let existed = indices[val] != nilnums.append(val)indices[val, default: []].insert(nums.count - 1)return !existed}// 删除元素func remove(_ val: Int) -> Bool {guard var valSet = indices[val], !valSet.isEmpty else {return false}// 从 val 的下标集合里取出一个位置let removeIndex = valSet.removeFirst()// 如果删除的不是最后一个元素,需要做交换if let lastVal = nums.last, removeIndex < nums.count - 1 {nums[removeIndex] = lastVal// 更新 lastVal 的索引集合indices[lastVal]!.remove(nums.count - 1)indices[lastVal]!.insert(removeIndex)}nums.removeLast()if valSet.isEmpty {indices[val] = nil} else {indices[val] = valSet}return true}// 随机返回一个元素func getRandom() -> Int {return nums.randomElement()!}
}
代码拆解
-
nums: [Int]
存储所有插入的元素,可以重复。 -
indices: [Int: Set<Int>]
用一个字典来记录每个值对应的下标集合,比如{1: {0,1}, 2: {2}}
。 -
insert
直接把值 append 到nums
,并把下标放进indices
。 -
remove
- 先拿到该值的一个下标
removeIndex
- 如果要删除的位置不是最后一个元素,就跟
nums.last
交换位置 - 更新哈希表里对应的索引集合
- 删除数组最后一个元素(O(1))
- 先拿到该值的一个下标
-
getRandom
直接调用randomElement()
,时间复杂度 O(1)。
示例测试及结果
我们来运行一下 Demo:
let collection = RandomizedCollection()print(collection.insert(1)) // true, 集合 = [1]
print(collection.insert(1)) // false, 集合 = [1,1]
print(collection.insert(2)) // true, 集合 = [1,1,2]print("随机结果:", collection.getRandom()) // 可能是 1 或 2print(collection.remove(1)) // true, 集合 = [1,2]print("随机结果:", collection.getRandom()) // 可能是 1 或 2
可能输出:
true
false
true
随机结果: 1
true
随机结果: 2
多次运行,你会发现返回的结果是随机的,但概率分布符合要求。
时间复杂度
insert
: O(1)remove
: O(1)(通过交换和字典更新实现)getRandom
: O(1)
即便有重复元素,整体仍能保证平均 O(1) 的时间复杂度。
空间复杂度
nums
存储所有元素,占用 O(n)。indices
存储每个值对应的下标集合,也占用 O(n)。
整体空间复杂度:O(n)。
总结
这道题的关键点在于:
- 数组提供 O(1) 随机访问
- 哈希表提供 O(1) 元素索引定位
- 交换删除保证 O(1) 删除操作
实际场景里,这个结构特别适合做 随机抽奖池 或 随机推荐系统:比如你有一个商品池,用户可以不断加商品、删商品,每次要随机推荐一个商品给用户,而且还要考虑重复的概率。这个题的思路就是一个很实用的底层实现。