381.O(1) 时间插入、删除和获取随机元素 - 允许重复
设计一个支持在平均 时间复杂度 O(1) 下, 执行以下操作的数据结构。
注意: 允许出现重复元素。
- insert(val):向集合中插入元素 val。
- remove(val):当 val 存在时,从集合中移除一个 val。
- getRandom:从现有集合中随机获取一个元素。每个元素被返回的概率应该与其在集合中的数量呈线性相关。
示例:
// 初始化一个空的集合。
RandomizedCollection collection = new RandomizedCollection();
// 向集合中插入 1 。返回 true 表示集合不包含 1 。
collection.insert(1);
// 向集合中插入另一个 1 。返回 false 表示集合包含 1 。集合现在包含 [1,1] 。
collection.insert(1);
// 向集合中插入 2 ,返回 true 。集合现在包含 [1,1,2] 。
collection.insert(2);
// getRandom 应当有 2/3 的概率返回 1 ,1/3 的概率返回 2 。
collection.getRandom();
// 从集合中删除 1 ,返回 true 。集合现在包含 [1,2] 。
collection.remove(1);
// getRandom 应有相同概率返回 1 和 2 。
collection.getRandom();
解题思路:
首先,应该思考用来存数据的「数据结构」。根据题目要求:O(1)时间复杂度的插入、删除、随机获取以及允许重复,首先想到数组。
然后,分析数组是否满足题目要求:
- 插入:数组是实打实的O(1)
- 随机获取:生成 数组长度 以内随机数做为下标,返回下标对应的元素。满足O(1)且人人平等
- 删除:数组不支持元素删除,仅支持角标删除,且删除元素会进行数组拷贝
- 允许重复:自带属性
针对删除的弊端思考解决方案:
- 角标删除问题:如果我们能通过「元素」能找到「元素对应的角标」,就能达成目的。使用Hash表来存,key 为当前元素,value 为元素对应的下标,因为允许重复,所以 value 应该是一个集合。Hash表的 put 和 get 时间复杂度均为O(1)。
- 数组拷贝问题:数组在删除元素时,会进行「数组拷贝」,把删除索引之后的数据往前移一位,导致时间复杂度变为O(N)。如果我们每次删除都是删除数组最后一位,就不会发生「数组拷贝」了。所以在删除时,我们取到数组最后一位的值,并set到需要删除的索引位置,再删除数组最后一位。就能保证时间复杂度为O(1)。
这样数据结构就出来了:一个用于存放元素的数组,一个用于存放元素索引的Hash表
方法一:
数据结构:
// 数字集合
private List<Integer> nums;
// 数字索引的集合
private Map<Integer, Set<Integer>> idx;
构造函数:
public RandomizedCollection() {
// 初始化
nums = new ArrayList<>();
idx = new HashMap<>();
}
insert方法:
// 需要做2件事:1.添加元素 2.更新该值对应的索引集合
public boolean insert(int val) {
// 添加元素
nums.add(val);
// 更新该值对应的索引集合
Set<Integer> set = idx.getOrDefault(val, new HashSet<>());
set.add(nums.size() - 1);
idx.put(val, set);
return set.size() == 1;
}
remove方法:需要注意的地方参考注释
public boolean remove(int val) {
if (!idx.containsKey(val)) {
return false;
}
// 取出该数字的索引,从索引中取出第一个(题目要求仅删除一个)并移除
Set<Integer> set = idx.get(val);
int delIndex = set.iterator().next();
set.remove(delIndex);
// 若只有一个索引,删除该集合
if (set.size() == 0) {
idx.remove(val);
}
// 删除的索引不在末尾
if (delIndex < nums.size() - 1) {
// 获取末尾的值,放到当前被删除索引位置
int lastNum = nums.get(nums.size() - 1);
nums.set(delIndex, lastNum);
// 更新末尾的值的索引
Set<Integer> lastNumIdx = idx.get(lastNum);
lastNumIdx.remove(nums.size() - 1);
lastNumIdx.add(delIndex);
}
// 删除末尾元素
nums.remove(nums.size() - 1);
return true;
}
getRandom方法:
public int getRandom() {
// 题有个小问题,并未提示没有元素时返回什么,经测试需要返回-1
if (nums.size() == 0) {
return -1;
}
int index = (int) (Math.random() * nums.size());
return nums.get(index);
}
执行结果:
附:ArrayList. remove 方法源码
当 size - index - 1 > 0 ,即 index < size - 1 时,才会执行 System.arraycopy。
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}