目录
- 一. O(1) 时间插入、删除和获取随机元素
- 思路和代码:
- I. 博主的做法
- II. 东哥的做法
- III. 其他做法
- 二. 黑名单中的随机数
- 思路和代码:
- I. 博主的做法
- II. 东哥的做法
一. O(1) 时间插入、删除和获取随机元素
- 题目链接:https://leetcode.cn/problems/insert-delete-getrandom-o1/
思路和代码:
I. 博主的做法
- 博主只会写个输入输出,,,,等概率随机这个,不会。。
II. 东哥的做法
-
这个问题可以分解一下:
- 加入元素,时间复杂度O(1)
- 删除元素,时间复杂度O(1)
- 随机返回集合里的元素,使得每一个元素返回的可能性相同,时间复杂度也为O(1)
-
也就是说存数据时间复杂度O(1),删除数据时间复杂度O(1),拿出任意元素时间复杂度也为O(1)
- 拿出任意元素,复杂度为O(1),那么数据结构只可能是数组
- 加入元素,就从数据末尾加入,时间复杂度也为O(1),这都好说,关键是删除元素,怎么能保证时间复杂度为O(1)呢?
- 数组删除元素,时间复杂度为O(1)的情况,只能是删除末尾元素。那既然如此,那我们每次删除的时候,将要删除的元素和末尾元素交换,再删除,就OK了。当然交换的时间复杂度也是O(1),因为数组是用下标进行交换的。
- 再想,插入,和删除的时候要判断数组中存在不存在这个元素,而数组检验这个操作,时间复杂度为O(n),不行,但哈希表可以!,我们用哈希表来存每一个元素的下标。
- 当然在本题中,哈希表还将用来提取一个元素的下标,时间复杂度也是O(1)
-
底层用数组作为数据结构,用哈希表存储每一个元素的下标。
class RandomizedSet {
private List<Integer> arrayList;
private Map<Integer, Integer> map;
public RandomizedSet() {
arrayList = new ArrayList<>();
map = new HashMap<>();
}
public boolean insert(int val) {
if(valToIndex.containsKey(val))
//不能用if(arrayList.contains(val))
return false;
else{
map.put(val, arrayList.size());
arrayList.add(val);
return true;
}
}
public boolean remove(int val) {
if(!valToIndex.containsKey(val))
//不能用if(!arrayList.contains(val))
return false;
else{
int index = map.get(val);
//下面两句顺序不能换
map.put(arrayList.get(arrayList.size()-1), index);
Collections.swap(arrayList, index, arrayList.size()-1);
// arrayList.remove(arrayList.size()-1);
//不能用arrayList.remove(val);
arrayList.remove((Integer)val);
map.remove(val);
return true;
}
}
public int getRandom() {
//不能用 return arrayList.get((int)Math.random() * arrayList.size());
return arrayList.get((int)(Math.random() * arrayList.size()));
}
}
/**
* Your RandomizedSet object will be instantiated and called as such:
* RandomizedSet obj = new RandomizedSet();
* boolean param_1 = obj.insert(val);
* boolean param_2 = obj.remove(val);
* int param_3 = obj.getRandom();
*/
- ArrayList有两种remove()的方法,如下图:
- 但在本题到当中,不能用
arrayList.remove(val);
因为这样会默认val是下标。而将它转换成Integer对象:arrayList.remove((Integer)val);
,那么调用的就是图中第二个方法了。当然这里也可以用下标进行删除:arrayList.remove(arrayList.size()-1);
- 但在本题到当中,不能用
- 博主一开始想将下面两句换位置,结果报错,因为 如果先执行交换函数,那么arrayList当中的元素就已经交换了,而此时在进行对末尾元素的下标更新,那么现在ArrayList的末尾元素其实是原来index对应的元素。(相当于原来下标为index的元素,现在又更新到了index,发生错误)
- 这里我们需要将arrayList的末尾元素下标更新为index,而val不需要更新,因为,马上就要删除val这个映射。
map.put(arrayList.get(arrayList.size()-1), index); Collections.swap(arrayList, index, arrayList.size()-1);
return arrayList.get((int)(Math.random() * arrayList.size()));
括号一定要加对位置,如果对Math.random()进行强转,那么结果只可能是0,也就是:return arrayList.get((int)Math.random() * arrayList.size());
Math.random()
函数生成的是0 ~ 1
之间的随机小数,再乘以arrayList的大小,就会生成0 ~ arrayList.size()
之间的随机小数。我们此时对这个数强转int,结果作为下标,用来随机抽取元素。
- 此时在执行
map.containsKey(val);
时,Java 会使用哈希函数将 val 映射到哈希表中的一个位置上,然后查找键为 val 的元素是否存在于哈希表中。 - 哈希表的查找操作时间复杂度为 O(1),在某些特殊情况下,可能会导致哈希函数的冲突,从而使得哈希表的查找操作时间复杂度变高,甚至退化到 O(n)。
- 时间复杂度:O(1)
- 空间复杂度:O(n)
III. 其他做法
- 主要区别体现在remove方法上,就是让arrayList末尾的元素替换val(也就是index对应的元素),此时,再删除末尾元素,就相当于删除了val。
public boolean remove(int val) {
if (!indices.containsKey(val)) {
return false;
}
int index = indices.get(val);
int last = nums.get(nums.size() - 1);
//将last元素替换index对应的元素(val)
nums.set(index, last);
indices.put(last, index);
nums.remove(nums.size() - 1);
indices.remove(val);
return true;
}
- 需要注意的是,arrayList.get()是下标 -> 元素;而map.get()是元素 -> 下标。
二. 黑名单中的随机数
- 题目链接:https://leetcode.cn/problems/random-pick-with-blacklist/
思路和代码:
I. 博主的做法
- 先创建一个0 - n的动态数组,再遍历blacklist数组,如果动态数组中有这个数,那么将它删掉。
- 代码没有问题,但是超出内存限制,无语。。
class Solution {
private List<Integer> list;
public Solution(int n, int[] blacklist) {
list = new ArrayList<>();
for(int i = 0; i < n; i++)
list.add(i);
for(int num : blacklist){
if(list.contains(num))
list.remove((Integer)num);
}
}
public int pick() {
return list.get((int)(Math.random() * list.size()));
}
}
/**
* Your Solution object will be instantiated and called as such:
* Solution obj = new Solution(n, blacklist);
* int param_1 = obj.pick();
*/
II. 东哥的做法
-
类似于上一个做法,将数组变成[0,size)为非黑名单数,[size,n)为黑名单数,这样我们就可以在[0,size)上取随机数进行提取了。
- 创建一个hashmap,加入所有黑名单数。
- 如果黑名单数本来就在size之后,那么就不需要再交换;
- 如果在size之前,
- 用last当做指针,从后往前,找到在size之后的非黑名单数字的索引。
- 将size前黑名单的索引,替换成size之后非黑名单数字的索引。
※ eg:这个图,4 显然在size的右边,不需要映射,跳过就好。此处需要的是将 1 的索引映射为 3 的索引。
-
pick()方法时,如果随机抽取,命中黑名单数,那么,返回map当中映射的非黑名单数的索引;如果是正常数字,那么返回它自己的索引就可以。
※ 最终的结果就是上面这个图,0,1为size前黑名单数,将它的索引映射到size之后的非黑名单数3,4的索引。然后,我们直接再[0,size)上随机进行取值就可以了。
class Solution {
private int size;
private Map<Integer, Integer> map;
public Solution(int n, int[] blacklist) {
map = new HashMap<>();
size = n - blacklist.length;
//将黑名单数组存入map,后面什么数字都行,目的是将黑名单数字加入map当中
for(int i : blacklist)
map.put(i, -1);
int last = n - 1;
//索引交换
for(int i : blacklist){
if(i >= size)
continue;
//找到一个size之后的非黑名单数字
while(map.containsKey(last))
last--;
//将size前黑名单数字映射到size后非黑名单数字上(相当于两个数字进行了交换)
map.put(i, last);
last--;
}
}
public int pick() {
int index = (int)(Math.random() * size);
//如果存在,get(index),否则,返回index
return map.getOrDefault(index, index);
}
}
/**
* Your Solution object will be instantiated and called as such:
* Solution obj = new Solution(n, blacklist);
* int param_1 = obj.pick();
*/
- 当时博主一直没有理解if(i >= size) 这个啥意思,其实可以带个值,例如第一个图当中,
N = 5,blacklist.length = 2
,那么最后我们随机提取数组的长度就是N - blacklist.length = 3
。因为原来默认的数组是[ 0,N - 1 ],也就是递增的,所以,只要是 >=size的黑名单数就不用管了。
- 时间复杂度:O(m),m 是 blacklist 的长度。初始化hashmap为O(m),替换的时候,while遍历,最坏的结果就是 blacklist 全部都在 size 之前,那么那个last- -;这条语句,执行完整个循环,走了m,所以为O(m)。(size之后的每个数字要么是黑名单数,要么被一个黑名单数所映射,因此while循环增加了 m 次)
- 空间复杂度:O(m),构建哈希表要 m 的空间。
参考:
https://leetcode.cn/problems/random-pick-with-blacklist/solution/hei-ming-dan-zhong-de-sui-ji-shu-by-leet-cyrx/
https://labuladong.github.io/algo/di-yi-zhan-da78c/shou-ba-sh-48c1d/chang-shu–6b296/