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} \times \frac{1}{k} = \frac{1}{k + 1}\)。则被保留的概率为\(1 - \frac{1}{k + 1} = \frac{k}{k + 1}\)。依次类推,不被 \(k+2\) 个元素替换的概率为\(1 - \frac{k}{k + 2} \times \frac{1}{k} = \frac{k + 1}{k + 2}\)。则运行到第 n 步时,被保留的概率 = 被选中的概率 * 不被替换的概率,即:

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

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

\[\frac{k}{j} \times \frac{j}{j + 1} \times \frac{j + 1}{j + 2} \times \frac{j + 2}{j + 3} \times ... \times \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;
    }
posted @ 2020-05-06 11:56  Shaw_喆宇  阅读(348)  评论(0编辑  收藏  举报