数组题目总结 -- 随机数问题

本文介绍了如何在O(1)时间复杂度下实现插入、删除和获取随机元素,以及在黑名单和按权重随机选择的场景下解决问题的方法。主要涉及使用哈希表和数组的组合,以及利用前缀和和二分搜索等技巧。此外,还讨论了水塘抽样算法在链表随机节点选择中的应用。
摘要由CSDN通过智能技术生成

一. 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,多占了一位;
      • 如果不在里面,就找比它大并且离它最近的数字。当然这个数列是有序的,就用二分搜索来做,找他的最左边界
        在这里插入图片描述
        在这里插入图片描述
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)。我们只需要常数的空间保存若干变量。

参考:https://mp.weixin.qq.com/s?__biz=MzAxODQxMDM0Mw==&mid=2247484974&idx=1&sn=795a33c338d4a5bd8d265bc7f9f63c03&scene=21#wechat_redirect

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值