LeetCode LCR 030. O(1) 时间插入、删除和获取随机元素
题目描述


题意分析
设计一个不含重复值的集合,支持插入、删除和等概率随机取值,三个操作都要求平均 $O(1)$ 时间。插入已有值、删除不存在的值都返回
false,成功改变集合时返回true。数组方便按随机下标访问,哈希表方便按值定位。把两者组合起来,再利用“集合不要求保持元素顺序”的条件,就能同时满足这些操作。题目保证调用
getRandom时集合非空。
解法:数组与下标映射
核心思路
[!blue]
用动态数组
nums保存全部元素,用indexMap保存“元素值 → 数组下标”。始终保持每个元素只出现一次,且nums[indexMap[v]] == v,这样既能按下标取值,也能按值找到位置。插入时先查哈希表,确认不存在后,把当前数组长度记为新元素下标,再将元素追加到末尾。数组可能偶尔扩容,但追加的均摊成本仍为常数。
删除时先找到下标
idx。不必把后续元素整体前移,而是用末尾元素last覆盖idx,将last的下标改为idx,再删除数组末尾和目标值的哈希记录。集合顺序可以变化,因此一次覆盖就能保持数组紧凑,其他元素无需移动。要先更新末尾元素的映射,最后删除目标键。若删的恰好就是末尾元素,
last与目标值相同,最后这次删除才能确保它不再留在哈希表中;唯一元素也能由同一过程处理。随机取值时,在
[0, nums.size())中均匀选择一个下标。数组没有空位,每个现存值恰好占一个位置,所以每个值被返回的概率都是1 / size。
解题步骤
- 构造时初始化空数组、空下标表;Java 同时创建供后续调用复用的随机数对象。
insert先查重,已存在则返回false;否则记录追加前的数组长度,追加元素,返回true。remove先查下标,不存在则返回false;否则取末尾值覆盖目标位置,并同步更新末尾值的映射。- 删除数组末尾,再删除目标值的哈希记录,返回
true。getRandom生成合法范围内的随机下标,直接返回数组中对应的值。
代码实现
class RandomizedSet {
private final List<Integer> nums;
private final Map<Integer, Integer> indexMap;
private final Random random;
public RandomizedSet() {
nums = new ArrayList<>();
indexMap = new HashMap<>();
random = new Random();
}
public boolean insert(int val) {
if (indexMap.containsKey(val)) {
return false;
}
// 取的是追加之前的长度,正好是新元素落位后的下标。
indexMap.put(val, nums.size());
nums.add(val);
return true;
}
public boolean remove(int val) {
if (!indexMap.containsKey(val)) {
return false;
}
int idx = indexMap.get(val);
// 用末尾元素覆盖待删位置,把 O(n) 的搬移变成 O(1) 的覆盖。
int last = nums.get(nums.size() - 1);
nums.set(idx, last);
indexMap.put(last, idx);
nums.remove(nums.size() - 1);
// 最后删除目标键,避免末尾元素就是目标值时仍留下映射。
indexMap.remove(val);
return true;
}
public int getRandom() {
return nums.get(random.nextInt(nums.size()));
}
}
import (
"math/rand"
)
type RandomizedSet struct {
nums []int
indexMap map[int]int
}
func Constructor() RandomizedSet {
return RandomizedSet{
nums: []int{},
indexMap: make(map[int]int),
}
}
func (rs *RandomizedSet) Insert(val int) bool {
if _, ok := rs.indexMap[val]; ok {
return false
}
// 取的是追加之前的长度,正好是新元素落位后的下标。
rs.indexMap[val] = len(rs.nums)
rs.nums = append(rs.nums, val)
return true
}
func (rs *RandomizedSet) Remove(val int) bool {
idx, ok := rs.indexMap[val]
if !ok {
return false
}
// 用末尾元素覆盖待删位置,把 O(n) 的搬移变成 O(1) 的覆盖。
last := rs.nums[len(rs.nums)-1]
rs.nums[idx] = last
rs.indexMap[last] = idx
rs.nums = rs.nums[:len(rs.nums)-1]
// 最后删除目标键,避免末尾元素就是目标值时仍留下映射。
delete(rs.indexMap, val)
return true
}
func (rs *RandomizedSet) GetRandom() int {
return rs.nums[rand.Intn(len(rs.nums))]
}
复杂度分析
- 时间复杂度:三个操作均为平均 $O(1)$。哈希操作按平均成本计算,动态数组追加按扩容后的均摊成本计算,覆盖与尾删只修改固定数量的位置。
- 空间复杂度:$O(n)$,其中
n为操作过程中集合的最大规模;数组和下标表各保存一份元素信息,底层容量可能在删除后保留。
关键点总结
[!green]
- 数组提供随机访问,下标表提供按值定位,两者通过同一条下标关系保持同步。
- 删除可以改变元素顺序,因此可用末尾覆盖代替整体搬移。
- 每个值只对应一个紧凑数组位置,均匀选择下标就等价于均匀选择元素。
易错点总结
[!yellow]
- 插入必须先查重,否则同一值占据多个位置,会同时破坏集合语义和随机概率。
- 移动末尾元素后必须更新它的下标,不能只改数组而留下旧映射。
- 更新搬移映射要先于删除目标键,尤其需要覆盖删除末尾或唯一元素的情况。
- 随机下标的上界不能包含数组长度;题目已保证集合非空,无需增加空集合返回值。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 381. O(1) 时间插入、删除和获取随机元素 - 允许重复 | 困难 | 同样用数组末尾覆盖被删位置,原题允许重复值,需要值到多个下标的映射。 |
| 710. 黑名单中的随机数 | 困难 | 同样把随机选择转成均匀整数下标抽样,原题用重映射避开黑名单,本题维护动态紧凑数组。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!