380. O(1) 时间插入、删除和获取随机元素
✨核心逻辑
本题要求所有操作的时间复杂度均为 O(1),因此采用了 变长数组 + 哈希表 的组合数据结构:
- 数组(
List)负责常数级随机访问:数组支持通过下标O(1)的访问,正好可以满足getRandom()返回随机元素的需求。 - 哈希表(
Map)负责常数级查找:哈希表存储了元素值val到它在数组中对应下标index的映射关系,使得insert和remove操作可以快速定位元素。 - 巧妙处理删除(交换法):常规删除数组中间元素会导致后续元素平移,时间复杂度为
O(N)。为了做到O(1)删除,我们将待删除元素与数组末尾元素进行交换,然后直接删除末尾元素,并同步更新哈希表内的索引映射,完美避开了数组平移的开销。
🔥代码实现(含详细变量注释)
class RandomizedSet {
// list:变长数组,用于存储集合中的元素,保证 getRandom() 能够在 O(1) 时间内随机访问
List<Integer> list = new ArrayList<>();
// map:哈希表,用于存储元素值 val 到其在 list 中对应下标 index 的映射,保证查找和删除是 O(1)
Map<Integer, Integer> map = new HashMap<>();
// rand:随机数生成器,用于在 getRandom() 时生成随机的数组下标
Random rand = new Random();
public RandomizedSet() {
}
public boolean insert(int val) {
// 如果 map 中已经存在该元素,说明插入失败,直接返回 false
if (map.containsKey(val)) {
return false;
}
// 如果不存在,获取当前 list 的长度(即新元素应该存放的索引)
int size = list.size();
// 将新元素追加到数组的末尾
list.add(val);
// 在哈希表中记录元素和它的索引位置
map.put(val, size);
return true;
}
public boolean remove(int val) {
if (map.containsKey(val)) {
// 核心技巧:不能直接对 list 按索引 remove,否则后面的元素会向前平移,导致时间复杂度变为 O(N)
// 获取数组末尾的那个元素值
int last = list.get(list.size() - 1);
// 获取当前要删除的元素 val 在数组中的索引
int index = map.get(val);
// 1. 在哈希表中,将末尾元素 last 的索引更新为当前要删除元素的索引 index
map.put(last, index);
// 2. 在数组中,将末尾元素 last 直接覆盖到当前要删除元素的索引位置
list.set(index, last);
// 3. 删除数组的最后一个元素(原本的 val 已经被覆盖,现在移走的是 原本 last 的冗余副本)
list.remove(list.size() - 1);
// 4. 从哈希表中移除元素 val 的记录
map.remove(val);
return true;
} else {
// 如果 map 中不存在该元素,说明删除失败,返回 false
return false;
}
}
public int getRandom() {
// rand.nextInt(list.size()) 生成一个 [0, list.size()) 范围内的随机下标
// 然后直接从数组中取出该下标的元素返回
return list.get(rand.nextInt(list.size()));
}
}
- ⏱️复杂度分析
时间复杂度:O(1)。insert() 操作是数组末尾追加和哈希表更新;remove() 操作是数组元素交换和哈希表更新;getRandom() 操作直接通过随机下标访问数组。所有方法均只有常数次操作。
空间复杂度:O(N),其中 N 是集合中元素的数量。底层数据结构使用了一个变长数组和一个哈希表,用于存储所有插入的元素。
🔍总结一下:
这类实现类的题:都是基于底层的数据结构来实现的,选对数据结构,进行操作,实现数据的一致性就行了