在算法题中,"O(1) 时间复杂度"是高频要求,尤其涉及插入、删除、随机访问三类操作时,单一数据结构往往难以满足所有需求。本文将拆解 LeetCode 380 题,讲解如何通过"数组+哈希表"的组合结构,实现一个高效的 RandomizedSet 类,并解析核心代码的设计逻辑与优化方向。
一、题目分析
题目要求
实现 RandomizedSet 类,支持以下三个操作,且每个操作的平均时间复杂度必须为 O(1):
-
insert(int val):元素 val 不存在时插入,返回 true;存在则返回 false。 -
remove(int val):元素 val 存在时删除,返回 true;不存在则返回 false。 -
getRandom():随机返回集合中的一个元素,每个元素被返回的概率相同(测试用例保证调用时集合非空)。
核心矛盾
单一数据结构的局限性:
-
数组:随机访问(
arr[index])是 O(1),但插入/删除中间元素需移动后续元素,时间复杂度 O(n)。 -
哈希表(对象/Map):插入/删除/查找是 O(1),但无法通过索引随机访问元素,无法满足
getRandom()需求。
解决方案:组合两者优势------用数组保证随机访问,用哈希表存储"元素→索引"映射,快速定位元素位置,规避数组插入/删除的 O(n) 操作。
二、代码实现与逐行解析
以下是基于 TypeScript 的实现代码,核心逻辑为"数组存元素,对象存索引映射",完全满足 O(1) 平均时间复杂度要求。
typescript
class RandomizedSet {
// 数组:存储元素,保证getRandom()的O(1)随机访问
private arr: Array<number>;
// 对象:存储"元素→索引"映射,实现O(1)查找元素位置
private comObj: Record<number, number>;
constructor() {
this.arr = [];
this.comObj = {};
}
insert(val: number): boolean {
// 先判断元素是否存在(O(1):对象属性查找)
if (val in this.comObj) {
return false;
}
// 存储元素索引:数组长度即为新元素的索引(push前长度不变)
this.comObj[val] = this.arr.length;
// 数组尾部插入元素(O(1):无需移动其他元素)
this.arr.push(val);
return true;
}
remove(val: number): boolean {
// 元素不存在则返回false(O(1)查找)
if (!(val in this.comObj)) {
return false;
}
// 获取待删除元素的索引
const deleteIndex = this.comObj[val];
// 获取数组最后一个元素(用于交换)
const lastVal = this.arr[this.arr.length - 1];
// 关键操作:将待删除元素与最后一个元素交换
this.arr[deleteIndex] = lastVal;
// 更新最后一个元素的索引映射(O(1)赋值)
this.comObj[lastVal] = deleteIndex;
// 数组尾部删除元素(O(1):无需移动其他元素)
this.arr.pop();
// 从对象中删除待删除元素的映射(O(1)删除)
delete this.comObj[val];
return true;
}
getRandom(): number {
// 生成[0, arr.length)的随机整数(O(1)数学运算)
const randomIndex = Math.floor(Math.random() * this.arr.length);
// 数组索引访问元素(O(1))
return this.arr[randomIndex];
}
}
核心逻辑拆解
1. insert 方法(O(1))
步骤:先通过 val in this.comObj 判断元素是否存在(对象属性查找为 O(1));若不存在,先记录元素的索引(数组当前长度即为新元素插入后的索引),再将元素插入数组尾部(push 操作 O(1)),最后更新对象映射。
注意:必须先记录索引再 push,因为 push 后数组长度会增加,索引会对应变化。
2. remove 方法(O(1))
这是最关键的优化点,核心是"交换+尾部删除"规避 O(n) 遍历:
-
获取待删除元素的索引
deleteIndex和数组最后一个元素lastVal。 -
将
lastVal赋值到deleteIndex位置,覆盖待删除元素(O(1) 赋值)。 -
更新
lastVal在对象中的索引为deleteIndex(保持映射一致性)。 -
删除数组最后一个元素(pop 操作 O(1)),再删除对象中待删除元素的映射。
通过这种方式,无需遍历数组删除元素,将时间复杂度从 O(n) 降至 O(1)。
3. getRandom 方法(O(1))
利用数组索引访问的优势:生成一个 [0, 数组长度) 的随机整数作为索引,直接通过 arr[randomIndex] 获取元素,全程无遍历,时间复杂度 O(1)。
三、关键注意事项与避坑点
1. 元素存在性判断:用 in 而非直接判断值
若写成 if (this.comObj[val]) 会出现误判:当元素索引为 0 时,this.comObj[val] = 0,转为布尔值为 false,会误判元素不存在。
val in this.comObj 直接判断"键是否存在",与值无关,是最严谨的方式。
2. 索引一致性维护
删除元素时,必须同步更新最后一个元素的索引映射,否则后续查找该元素会得到错误索引,导致功能异常。
3. 空数组边界处理
题目虽保证 getRandom() 调用时集合非空,但实际开发中可添加边界判断,避免数组为空时返回 undefined:
typescript
getRandom(): number {
if (this.arr.length === 0) {
throw new Error("集合为空,无法获取随机元素");
}
const randomIndex = Math.floor(Math.random() * this.arr.length);
return this.arr[randomIndex];
}
四、优化方向:用 Map 替代 Object
当前代码用 Record<number, number> 作为哈希表,存在一个隐式问题:JavaScript 对象的键本质是字符串/符号,数字键会被自动转为字符串(如 comObj[5] 实际存储为 comObj["5"])。虽不影响功能,但类型不够精准。
优化方案:用Map<number, number> 替代对象,原生支持数字键,语义更清晰,且 has()、set()、delete() 方法更贴合键值对操作:
typescript
class RandomizedSet {
private arr: number[];
private comMap: Map<number, number>;
constructor() {
this.arr = [];
this.comMap = new Map();
}
insert(val: number): boolean {
if (this.comMap.has(val)) {
return false;
}
this.comMap.set(val, this.arr.length);
this.arr.push(val);
return true;
}
remove(val: number): boolean {
if (!this.comMap.has(val)) {
return false;
}
const deleteIndex = this.comMap.get(val)!; // 非空断言,已判断存在
const lastVal = this.arr[this.arr.length - 1];
if (deleteIndex !== this.arr.length - 1) { // 优化:若为最后一个元素,跳过交换
this.arr[deleteIndex] = lastVal;
this.comMap.set(lastVal, deleteIndex);
}
this.arr.pop();
this.comMap.delete(val);
return true;
}
getRandom(): number {
const randomIndex = Math.floor(Math.random() * this.arr.length);
return this.arr[randomIndex];
}
}
额外优化:当待删除元素就是数组最后一个元素时,可跳过交换逻辑,减少无效操作,小幅提升性能。
五、复杂度证明
-
insert:对象/Map 查找(O(1))+ 数组 push(O(1))+ 映射更新(O(1)),整体 O(1)。 -
remove:对象/Map 查找(O(1))+ 元素交换(O(1))+ 数组 pop(O(1))+ 映射删除(O(1)),整体 O(1)。 -
getRandom:随机数生成(O(1))+ 数组索引访问(O(1)),整体 O(1)。
所有操作均为常数级,满足题目"平均 O(1) 时间复杂度"的要求。
六、总结
本题的核心思路是"组合数据结构弥补单一结构的不足":数组保证随机访问,哈希表(对象/Map)保证高效的插入/删除/查找。其中,删除操作的"交换+尾部删除"是实现 O(1) 的关键技巧,也是算法题中高频的优化思路。
无论是用对象还是 Map 作为哈希表,核心逻辑一致,可根据场景选择:对象更简洁,Map 类型更精准、性能更稳定(适合大量键值对操作)。掌握这种"1+1>2"的数据结构组合思路,能解决更多复杂的算法优化问题。