1 问题
给定一个单链表,随机返回一个节点的值,保证每个节点被选择的概率相同。
链表的长度未知,可能会非常大。
2 测试方法
一个节点数为7的单链表,返回1000000次值,统计每个节点被选择的频率。看每个节点被选择的频率是不是接近1/7。
3 java随机数生成类java.util.Random
Random(),创建一个新的随机数生成器。
int nextInt(int bound),生成的整数在[0, bound)之间,生成这个区间里面的每个int的概率相同。
nextInt(10),生成{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}这10个数中的一个,生成这10个数中的每个数的概率相同。
4 reservoir sampling,水塘抽样
这是一个在大数据流中随机抽样的问题,即在内存无法加载全部数据时,如何从未知大小的数据流中随机选取k个数据,保证每个数据被选择到的概率相同。
水塘抽样的具体操作:
第一,保留前k个数据;
第二,当遍历到第i个数据的时候,以k/i的概率保留该数据,同时随机删除前k个数据中的一个。
用归纳法证明:
当数据总数为k的时候,每个数据被抽到的概率为1,显然是相同的。
当数据总数为k+1的时候,当新加的第k+1的数据被以k/(k+1)的概率被选中的时候。原来的k个数据被选中的概率为:
1/(k+1) + (k/(k+1))*(1-1/k))=k/(k+1)
当数据总数为k+2的时候,当新加的第k+2的数据被以k/(k+2)的概率被选中的时候。原来的k+1个数据被选中的概率为:
(k/(k+1)) * (2/(k+2) + k/(k+2) * (1-1/k))=k/(k+2)
假设当数据总数为N的时候,按照水塘操作每个元素被选中的概率为k/N,那么当新来了一个新的元素N+1的时候,该元素被选中的概率为k/(N+1),其它元素被选中的概率为
(k/N)*(1 - k/(N+1) + k/(N+1)*(1 - 1/k))=k/(N+1)
所以,当数据总数为N+1的时候,所有数据被选中的概率也是一样的。
因此,水塘操作可以保证数据流中每个元素被选中的概率相同。
5 当k=1的时候就是Linked List Random Node问题
第一,保留第一个数据;
第二,当遍历到第i个数据的时候,以1/i的概率保留该数据用来替换原来的数据。
比如来了第4个数据,前三个数据被选中的概率是
在上个数据到来时被选中的概率 * 第4个数据没有被选中的概率 = (1/3) * (3/4) = 1/4
6 编码实现
private static int getRandomNode(ListNode head) {
int i = 1;
ListNode listNode = head;
int x = 0;
Random random = new Random();
while (listNode != null) {
if (random.nextInt(i) == (i - 1)) {
x = listNode.get();
}
listNode = listNode.next;
i++;
}
return x;
}
7 测试
HashMap<Integer, Integer> choosenCntMap = new HashMap<Integer, Integer>();
for (int j = 0; j < 1000000; j++) {
int x = getRandomNode(head);
if (choosenCntMap.containsKey(x)) {
int cnt = choosenCntMap.get(x);
cnt++;
choosenCntMap.put(x, cnt);
} else {
choosenCntMap.put(x, 1);
}
}
for (Integer x : choosenCntMap.keySet()) {
int cnt = choosenCntMap.get(x);
System.out.println("x:" + x + "----" + "cnt:" + cnt);
}
x:2----cnt:142675
x:3----cnt:142428
x:5----cnt:142322
x:7----cnt:143076
x:8----cnt:143200
x:10----cnt:143340
x:11----cnt:142959
每个节点被选择的概率都接近1/7。