目录
一. O(1) 时间插入、删除和获取随机元素
思路和代码:
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()是元素 -> 下标。
二. 黑名单中的随机数
思路和代码:
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/
三 . 按权重随机选择
思路和代码:
前缀和 + 二分搜索(搜索左边界)
- 平常要想等概率的生成随机数,那我们直接塞给 Random 类就可以,现在不一样了,要求按权重来生成随机数。假定权重数组w[] = {1,3,2,1},也就是第一个元素的出场的概率为1 / (1+3+2+1); 第二个元素出场的概率为3 / (1+3+2+1)…,相当于一根绳子切了很多段,随机将target扔到绳子上,扔到哪一段,就是对应的数字,如下面的图:
- 现在的难题就是w[] 中数字是不相同的,并且是乱序的,这就很不好处理,我们由此想到了前缀和,前缀和数组能将乱序的数字变得有序起来!!!
- 构建前缀和数组preSum[] = {0,1,4,6,7},很巧妙,除了0以外,w[]中每一个权重都在preSum中精准的通过数字表示了出来,
- 问题就转换成了在[1,2,3,4,5,6,7]这几个数中随机取一个整数
- 如果这个数在数组里,那就返回这个preSum对应数字的索引 - 1,因为preSum比w多一个数字0,多占了一位;
- 如果不在里面,就找比它大并且离它最近的数字。当然这个数列是有序的,就用二分搜索来做,找他的最左边界
- 问题就转换成了在[1,2,3,4,5,6,7]这几个数中随机取一个整数
class Solution {
private int[] preSum;
private Random rand = new Random();
public Solution(int[] w) {
int n = w.length;
preSum = new int[n+1];
preSum[0] = 0;
//注意这里的<=,由于preSum比w大一个单位,所以1~n
for(int i = 1; i <= n; i++)
preSum[i] = preSum[i-1] + w[i-1];
}
public int pickIndex() {
int n = preSum.length;
//不能写成preSum(n-1),因为这其实是我们人工分的,里面还是那几个数字
//就拿上面的举例子preSum.length = 5,而要从[1,7]当中来取随机数
//rand.nextInt():取得是左闭右开也就是[0,7),加上1之后就是[1,8),刚刚好
int target = rand.nextInt(preSum[n-1]) + 1;
int left = 0; int right = n;
while(left < right){
int mid = left + (right - left) / 2;
if(target > preSum[mid])
left = mid + 1;
else
right = mid;
}
//前缀和数组比原数组多一个, 用来存0
return left - 1;
}
}
/**
* Your Solution object will be instantiated and called as such:
* Solution obj = new Solution(w);
* int param_1 = obj.pickIndex();
*/
参考:
https://mp.weixin.qq.com/s/_5t0RSqUzErWUYYb-w0MMw
四 . 按权重随机选择
思路和代码:
法一:暴力法
思路:遍历整个链表,然后将值存入list中,然后随机提取一项,返回它的val。
class Solution {
private List<Integer> list;
private ListNode head;
private Random rand;
public Solution(ListNode head) {
this.head = head;
list = new ArrayList<>();
rand = new Random();
}
public int getRandom() {
for(ListNode node = head; node != null; node = node.next){
list.add(node.val);
}
return list.get(rand.nextInt(list.size()));
}
}
-
时间复杂度:初始化为O(n),随机选择为 O(1),其中 n 是链表的元素个数。
-
空间复杂度:O(n)。我们需要 O(n)O(n) 的空间存储链表中的所有元素。
法二:水塘抽样算法
思路:当你遇到第i个元素时,应该有1/i的概率选择该元素,1 - 1/i的概率保持原有的选择。推荐东哥的讲义:https://mp.weixin.qq.com/s?__biz=MzAxODQxMDM0Mw==&mid=2247484974&idx=1&sn=795a33c338d4a5bd8d265bc7f9f63c03&scene=21#wechat_redirect
class Solution {
private ListNode head;
private Random rand;
public Solution(ListNode head) {
this.head = head;
this.rand = new Random();
}
public int getRandom() {
int i = 0, res = 0;
for(ListNode node = head; node != null; node = node.next){
/*
这里需要注意rand.nextInt( i )方法的结果是左闭右开的,
也就是说 i 必须大于0。eg:i == 1时,结果为[0,1),还是0,所以在for循环中,
第一个值head.val 也会考虑在内。
下面等于0,是因为在长度为0 ~ n-1的列表中,取到0的概率是1/n,满足题意
*/
if(rand.nextInt(++i) == 0)
res = node.val;
}
return res;
}
}
-
时间复杂度:初始化为O(1),随机选择为 O(n),其中 n 是链表的元素个数。
-
空间复杂度:O(1)。我们只需要常数的空间保存若干变量。