LeetCode 380. O(1) 时间插入、删除和获取随机元素
问题描述
实现 RandomizedSet 类:
RandomizedSet()初始化RandomizedSet对象bool insert(int val)当元素val不存在时,向集合中插入该项,并返回true;否则,返回falsebool remove(int val)当元素val存在时,从集合中移除该项,并返回true;否则,返回falseint getRandom()随机返回现有集合中的一项(测试用例保证调用此方法时集合中至少存在一项)
要求:每个函数都必须在 平均时间复杂度为 O(1) 的情况下完成。
示例:
// 初始化一个空的集合
RandomizedSet randomSet = new RandomizedSet();
// 向集合中插入 1 ,返回 true 表示插入成功
randomSet.insert(1);
// 向集合中插入 2 ,返回 true 表示插入成功
randomSet.insert(2);
// getRandom 应随机返回 1 或 2
randomSet.getRandom();
// 从集合中移除 1 ,返回 true 表示删除成功
randomSet.remove(1);
// 2 已在集合中,所以返回 false 表示插入失败
randomSet.insert(2);
// getRandom 应该返回 2
randomSet.getRandom();
算法思路
核心思想:哈希表 + 动态数组
-
哈希表(HashMap):
- 提供 O(1) 的查找、插入、删除操作
- 存储
值 -> 索引的映射关系
-
动态数组(ArrayList):
- 支持 O(1) 的随机访问(通过索引)
- 可以通过
Math.random()生成随机索引
关键:数组的删除操作通常是 O(n) 的,需要移动后面的元素。
- 交换删除:
- 当需要删除某个元素时,将其与数组最后一个元素交换
- 然后删除最后一个元素(O(1) 操作)
- 同时更新哈希表中的索引映射
步骤:
插入操作:
- 检查元素是否已存在(通过哈希表)
- 如果不存在,将元素添加到数组末尾
- 在哈希表中记录
元素 -> 数组索引的映射
删除操作:
- 检查元素是否存在
- 如果存在:
- 获取要删除元素的索引
idx - 获取数组最后一个元素
lastElement - 将
lastElement移动到idx位置 - 更新哈希表中
lastElement的索引为idx - 从数组末尾删除元素
- 从哈希表中删除
val的映射
- 获取要删除元素的索引
随机获取:
- 生成
[0, size)范围内的随机索引 - 返回数组中对应位置的元素
代码实现
import java.util.*;
class RandomizedSet {
/**
* 动态数组:存储所有元素,支持O(1)随机访问
*/
private List<Integer> nums;
/**
* 哈希表:存储元素到数组索引的映射,支持O(1)查找
*/
private Map<Integer, Integer> valToIndex;
/**
* 随机数生成器:用于生成随机索引
*/
private Random random;
/**
* 构造函数:初始化数据结构
*/
public RandomizedSet() {
nums = new ArrayList<>();
valToIndex = new HashMap<>();
random = new Random();
}
/**
* 插入元素
*
* @param val 要插入的元素
* @return 如果插入成功返回true,如果元素已存在返回false
*
* 时间复杂度: O(1)
* 空间复杂度: O(1)
*/
public boolean insert(int val) {
// 如果元素已存在,返回false
if (valToIndex.containsKey(val)) {
return false;
}
// 将元素添加到数组末尾
nums.add(val);
// 在哈希表中记录元素到索引的映射
// 新元素的索引就是数组的最后一个位置(size-1)
valToIndex.put(val, nums.size() - 1);
return true;
}
/**
* 删除元素
*
* @param val 要删除的元素
* @return 如果删除成功返回true,如果元素不存在返回false
*
* 时间复杂度: O(1)
* 空间复杂度: O(1)
*/
public boolean remove(int val) {
// 如果元素不存在,返回false
if (!valToIndex.containsKey(val)) {
return false;
}
// 获取要删除元素的索引
int idx = valToIndex.get(val);
// 获取数组最后一个元素
int lastElement = nums.get(nums.size() - 1);
// 将最后一个元素移动到要删除元素的位置
nums.set(idx, lastElement);
// 更新哈希表中最后一个元素的索引
valToIndex.put(lastElement, idx);
// 从数组末尾删除最后一个元素
nums.remove(nums.size() - 1);
// 从哈希表中删除要删除元素的映射
valToIndex.remove(val);
return true;
}
/**
* 随机获取一个元素
*
* @return 集合中的随机元素
*
* 时间复杂度: O(1)
* 空间复杂度: O(1)
*/
public int getRandom() {
// 生成 [0, size) 范围内的随机索引
int randomIndex = random.nextInt(nums.size());
// 返回对应位置的元素
return nums.get(randomIndex);
}
}
算法分析
-
时间复杂度:O(1)(平均)
insert:哈希表查找 O(1) + 数组添加 O(1)remove:哈希表查找 O(1) + 数组交换 O(1) + 数组删除末尾 O(1)getRandom:随机数生成 O(1) + 数组随机访问 O(1)
-
空间复杂度:O(n)
- 数组存储 n 个元素
- 哈希表存储 n 个键值对
-
关键:
- 结合了哈希表的快速查找和数组的随机访问
- 通过交换删除将删除操作优化到 O(1)
算法过程
插入操作:
初始状态: nums = [], valToIndex = {}
insert(1):
- 1 不在哈希表中
- nums = [1]
- valToIndex = {1: 0}
- 返回 true
insert(2):
- 2 不在哈希表中
- nums = [1, 2]
- valToIndex = {1: 0, 2: 1}
- 返回 true
删除操作:
当前状态: nums = [1, 2, 3, 4], valToIndex = {1:0, 2:1, 3:2, 4:3}
remove(2):
- 2 存在,索引 idx = 1
- lastElement = 4
- 将 4 移动到索引 1: nums = [1, 4, 3, 4]
- 更新 valToIndex: {1:0, 4:1, 3:2, 4:3}
- 删除末尾元素: nums = [1, 4, 3]
- 删除 2 的映射: valToIndex = {1:0, 4:1, 3:2}
- 返回 true
随机获取:
当前状态: nums = [1, 4, 3]
getRandom():
- 生成随机索引: 0, 1, 或 2
- 返回 nums[randomIndex]: 1, 4, 或 3
- 每个元素被选中的概率都是 1/3
测试用例
public static void main(String[] args) {
// 测试用例1:标准示例
RandomizedSet randomSet = new RandomizedSet();
System.out.println("Test 1 - insert(1): " + randomSet.insert(1)); // true
System.out.println("Test 2 - insert(2): " + randomSet.insert(2)); // true
System.out.println("Test 3 - getRandom(): " + randomSet.getRandom()); // 1 or 2
System.out.println("Test 4 - remove(1): " + randomSet.remove(1)); // true
System.out.println("Test 5 - insert(2): " + randomSet.insert(2)); // false
System.out.println("Test 6 - getRandom(): " + randomSet.getRandom()); // 2
// 测试用例2:边界情况 - 单元素
RandomizedSet set2 = new RandomizedSet();
System.out.println("Test 7 - insert(5): " + set2.insert(5)); // true
System.out.println("Test 8 - remove(5): " + set2.remove(5)); // true
System.out.println("Test 9 - insert(5): " + set2.insert(5)); // true
System.out.println("Test 10 - getRandom(): " + set2.getRandom()); // 5
// 测试用例3:重复插入和删除
RandomizedSet set3 = new RandomizedSet();
System.out.println("Test 11 - insert(10): " + set3.insert(10)); // true
System.out.println("Test 12 - insert(20): " + set3.insert(20)); // true
System.out.println("Test 13 - insert(30): " + set3.insert(30)); // true
System.out.println("Test 14 - remove(20): " + set3.remove(20)); // true
System.out.println("Test 15 - remove(20): " + set3.remove(20)); // false
System.out.println("Test 16 - insert(20): " + set3.insert(20)); // true
// 测试用例4:负数
RandomizedSet set4 = new RandomizedSet();
System.out.println("Test 17 - insert(-1): " + set4.insert(-1)); // true
System.out.println("Test 18 - insert(-2): " + set4.insert(-2)); // true
System.out.println("Test 19 - getRandom(): " + set4.getRandom()); // -1 or -2
System.out.println("Test 20 - remove(-1): " + set4.remove(-1)); // true
// 测试用例5:大量操作验证随机性
RandomizedSet set5 = new RandomizedSet();
for (int i = 1; i <= 100; i++) {
set5.insert(i);
}
// 随机获取10次,验证不会抛异常
for (int i = 0; i < 10; i++) {
int randomVal = set5.getRandom();
System.out.println("Test 21 - getRandom(): " + randomVal);
assert randomVal >= 1 && randomVal <= 100;
}
// 测试用例6:删除后再插入
RandomizedSet set6 = new RandomizedSet();
set6.insert(100);
set6.remove(100);
set6.insert(100);
System.out.println("Test 22 - getRandom(): " + set6.getRandom()); // 100
}
关键点
-
数据结构:
- ArrayList:支持 O(1) 随机访问和末尾操作
- HashMap:支持 O(1) 查找、插入、删除
-
交换删除:
- 将要删除的元素与最后一个元素交换
- 删除末尾元素(O(1))
- 更新哈希表中的索引映射
-
索引维护:
- 插入时:新元素索引 =
size - 1 - 删除时:更新被移动元素的索引
- 插入时:新元素索引 =
-
随机性:
- 使用
Random.nextInt(size)生成均匀分布的随机索引 - 每个元素被选中的概率相等
- 使用
-
边界情况处理:
- 空集合的插入
- 重复插入/删除
- 单元素集合的操作
常见问题
-
为什么不能直接用 HashSet?
- HashSet 支持 O(1) 插入和删除,但不支持 O(1) 随机获取
- 需要遍历才能随机获取,时间复杂度 O(n)
-
交换删除会影响随机性吗?
- 只是改变了元素在数组中的位置
- 随机获取时仍然均匀选择所有索引
-
如果删除最后一个元素?
lastElement就是要删除的元素- 交换后删除末尾,逻辑一致
-
空间复杂度能优化?
- 当前实现已经是最优的 O(n) 空间复杂度
- 需要同时存储元素和索引映射,无法进一步优化
1608

被折叠的 条评论
为什么被折叠?



