zoukankan      html  css  js  c++  java
  • LeetCode——链表随机节点/随机数索引:蓄水池算法

    蓄水池算法

    引用:蓄水池采样算法(Reservoir Sampling)
    采样问题经常会被遇到,比如:

    • 从 100000 份调查报告中抽取 1000 份进行统计。
    • 从一本很厚的电话簿中抽取 1000 人进行姓氏统计。
    • 从 Google 搜索 "Ken Thompson",从中抽取 100 个结果查看哪些是今年的。

    这些都是很基本的采用问题。既然说到采样问题,最重要的就是做到公平,也就是保证每个元素被采样到的概率是相同的。所以可以想到要想实现这样的算法,就需要掷骰子,也就是随机数算法。
    对于第一个问题,还是比较简单,通过算法生成([0, 100000 - 1))间的随机数 1000 个,并且保证不重复即可。再取出对应的元素即可。但是对于第二和第三个问题,就有些不同了,我们不知道数据的整体规模有多大。可能有人会想到,我可以先对数据进行一次遍历,计算出数据的数量 N,然后再按照上述的方法进行采样即可。这当然可以,但是并不好,毕竟这可能需要花上很多时间。也可以尝试估算数据的规模,但是这样得到的采样数据分布可能并不平均。

    算法过程

    假设数据序列的规模为 (n),需要采样的数量的为 (k)
    首先构建一个可容纳 (k) 个元素的数组,将序列的前 (k) 个元素放入数组中。
    然后从第 (k+1) 个元素开始,以 (frac{k}{n}) 的概率来决定该元素是否被替换到数组中(数组中的元素被替换的概率是相同的)。 当遍历完所有元素之后,数组中剩下的元素即为所需采取的样本。

    证明过程:
    对于第(i)个数((i le k))。在 (k) 步之前,被选中的概率为 (1)。当走到第 (k+1) 步时,被 (k+1) 个元素替换的概率 (= k+1) 个元素被选中的概率 * (i) 被选中替换的概率,即为(frac{k}{k + 1} imes frac{1}{k} = frac{1}{k + 1})。则被保留的概率为(1 - frac{1}{k + 1} = frac{k}{k + 1})。依次类推,不被 (k+2) 个元素替换的概率为(1 - frac{k}{k + 2} imes frac{1}{k} = frac{k + 1}{k + 2})。则运行到第 n 步时,被保留的概率 = 被选中的概率 * 不被替换的概率,即:

    [1 imes frac{k}{k + 1} imes frac{k + 1}{k + 2} imes frac{k + 2}{k + 3} imes … imes frac{n - 1}{n} = frac{k}{n} ]

    对于第 (j) 个数((j>k))。在第 (j) 步被选中的概率为 (frac{k}{j})。不被 (j+1) 个元素替换的概率为(1 - frac{k}{j + 1} imes frac{1}{k} = frac{j}{j + 1})。则运行到第 (n) 步时,被保留的概率 = 被选中的概率 * 不被替换的概率,即:

    [frac{k}{j} imes frac{j}{j + 1} imes frac{j + 1}{j + 2} imes frac{j + 2}{j + 3} imes ... imes frac{n - 1}{n} = frac{k}{n} ]

    所以对于其中每个元素,被保留的概率都为(frac{k}{n}).

    代码示例

    public class ReservoirSamplingTest {
    
        private int[] pool; // 所有数据
        private final int N = 100000; // 数据规模
        private Random random = new Random();
    
        @Before
        public void setUp() throws Exception {
            // 初始化
            pool = new int[N];
            for (int i = 0; i < N; i++) {
                pool[i] = i;
            }
        }
    
        private int[] sampling(int K) {
            int[] result = new int[K];
            for (int i = 0; i < K; i++) { // 前 K 个元素直接放入数组中
                result[i] = pool[i];
            }
    
            for (int i = K; i < N; i++) { // K + 1 个元素开始进行概率采样
                int r = random.nextInt(i + 1);
                if (r < K) {
                    result[r] = pool[i];
                }
            }
    
            return result;
        }
    
        @Test
        public void test() throws Exception {
            for (int i : sampling(100)) {
                System.out.println(i);
            }
        }
    }
    

    两个蓄水池算法题目

    Q:给定一个单链表,随机选择链表的一个节点,并返回相应的节点值。保证每个节点被选的概率一样。

    进阶:
    如果链表十分大且长度未知,如何解决这个问题?你能否使用常数级空间复杂度实现?

    示例:

    // 初始化一个单链表 [1,2,3].
    ListNode head = new ListNode(1);
    head.next = new ListNode(2);
    head.next.next = new ListNode(3);
    Solution solution = new Solution(head);
    
    // getRandom()方法应随机返回1,2,3中的一个,保证每个元素被返回的概率相等。
    solution.getRandom();
    

    A:
    蓄水池算法:

    class Solution {
    
        private ListNode node;
        
        public Solution(ListNode head) {
            node = head;
        }
        
        public int getRandom() {
            ListNode res = node;
            ListNode cur = node.next;
            int i = 2;
            //从第二个节点开始,每次循环替换res的概率都是1/i
            while(cur != null){
                Random random = new Random();
                int ran = random.nextInt(i);
                if(ran == 0){
                    res = cur;
                }
                cur = cur.next;
                i++;
            }
            return res.val;
        }
    }
    

    Q:给定一个可能含有重复元素的整数数组,要求随机输出给定的数字的索引。 您可以假设给定的数字一定存在于数组中。

    注意:
    数组大小可能非常大。 使用太多额外空间的解决方案将不会通过测试。

    示例:

    int[] nums = new int[] {1,2,3,3,3};
    Solution solution = new Solution(nums);
    
    // pick(3) 应该返回索引 2,3 或者 4。每个索引的返回概率应该相等。
    solution.pick(3);
    
    // pick(1) 应该返回 0。因为只有nums[0]等于1。
    solution.pick(1);
    

    A:
    代码:

        private int[] nums;
        public Solution(int[] nums) {
           this.nums = nums;
        }
        
        public int pick(int target) {
            Random r = new Random();
            int n = 0;
            int index = 0;
            for(int i = 0;i < nums.length;i++)
                if(nums[i] == target){
                //我们的目标对象中选取。
                    n++;
                    //我们以1/n的概率留下该数据
                    if(r.nextInt() % n == 0) index = i;
                }
            return index;
        }
    
  • 相关阅读:
    [chrome]click事件会触发mouseleave
    鼠标的指针状态 以及 事件禁用
    CSS3 线性渐变(linear-gradient)
    css 的函数 calc() 、linear-gradient()、、、
    1.闰年的计算方法。 2.某一月的周数
    moment.js 使用方法总结
    Echarts 版本查看
    如何使用 onscroll / scrollTo() / scrollBy()
    水平居中、垂直居中
    【LeetCode】22. Generate Parentheses (I thought I know Python...)
  • 原文地址:https://www.cnblogs.com/xym4869/p/12835335.html
Copyright © 2011-2022 走看看