单次遍历,等概率随机选取问题

又是一道概率问题,不过跟之前的题目一样,这也是一道非常简单的题目。

问题描述:假设我们有一堆数据(可能在一个链表里,也可能在文件里),数量未知。要求只遍历一次这些数据,随机选取其中的一个元素,任何一个元素被选到的概率相等。O(n)时间,O(1)辅助空间(n是数据总数,但事先不知道)。

如果元素总数为n,那么每个元素被选到的概率应该是1/n。然而n只有在遍历结束的时候才能知道,在遍历的过程中,n的值还不知道,可以利用乘法规则来逐渐凑出这个概率值。在《利用等概率Rand5产生等概率Rand3》中提到过,如果要通过有限步概率的加法和乘法运算,最终得到分子为1、分母为n的概率,那必须在某一次运算中引入一个n在分母上,而分母和分子上其他的因数则通过加法、乘法、约分等规则去除。

很容易能够想到这样一个简单的式子来凑出1/n:

pi=1i×ii+1×i+1i+2××n1n=1n

下面用一段Python程序来实现这个过程,这里设计了一个类,叫做RandomSelector,提供方法AddItem,在遍历数据的时候把每个元素通过这个函数传进来,最后通过SelectedItem获取随机选择的元素。这么做主要是为了强调事先不知道元素的总数。

 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from  random  import Random

class RandomSelector:
   def  __init__ ( self , rand = None ):
     self._selectedItem  =  None
     self._count  =  0
     self._rand  = rand
     if  self._rand  is  None:
       self._rand  = Random ( )

   def SelectedItem ( self ):
     return  self._selectedItem

   def Count ( self ):
     return  self._count

   def AddItem ( self , item ):
     if  self._rand. randint ( 0 ,  self._count )  ==  0:
       self._selectedItem  = item
     self._count + =  1

在Python 2.5中,yield不仅是个语句,更是一个表达式(详见PEP 342 -- Coroutines via Enhanced Generators,查阅Generator和Coroutine,中文叫做“生成器”和“协程”),利用yield可以把程序写的更简洁一些:

 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
from  random  import Random

def RandomSelect (rand = None ):
  selection  =  None
  count  =  0
   if rand  is  None:
    rand  = Random ( )
   while  True:
     # Outputs the current selection and gets next item
    item  =  yield selection
     if rand. randint ( 0 , count )  ==  0:
      selection  = item
    count + =  1

下面这段程序示意了如何调用RandomSelect函数来测验其随机效果:

 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
# Sample code to use RandomSelect function
=  10
repeat  =  100000
occurrences  =  [ 0  for i  in  xrange (n ) ]
rand  = Random ( )
for i  in  xrange (repeat ):
  selector  = RandomSelect (rand )
  selector. next ( )
  selection  =  None
   for item  in  xrange (n ):
    selection  = selector. send (item )
  occurrences [selection ] + =  1
print occurrences

十个元素,重复十万次,理论上每个元素会被选中恰好一万次。某次实验结果如下:

 
 
[10020, 10084, 10003, 10008, 9985, 10145, 9987, 9925, 9955, 9888]

可见每个元素被选中的次数相差不大,是等概率的。

如果用C#,就可以利用IEnumerable来实现,比如:

 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static bool RandomSelect <tsource > (
    IEnumerable <tsource > source,
    Random random,
     out TSource selectedItem )
{
     if  (source  ==  null )
     {
         throw  new ArgumentNullException ( "source" ) ;
     }
     if  (random  ==  null )
     {
        random  =  new Random ( ) ;
     }

    selectedItem  =  default (TSource ) ;
     int count  =  0 ;
     foreach  (TSource item  in source )
     {
         if  (random . Next ( ++count )  ==  0 )
         {
            selectedItem  = item ;
         }
     }

     return  (count  >  0 ) ;
}

核心代码也就那么两三行而已,时间复杂度为O(n)(并且只遍历一次),空间复杂度为O(1)。其中Python的random.randint(x, y)返回[x, y]之间的随机整数;C#的Random.Next(x)返回[0, x)之间的随机整数。

看一下概率,如果最终被选取的是第i个元素(1 <= i <= n),那就必须是遍历到它的时候,恰好被选中(random.randint(0, i - 1) == 0或者Random.Next(i) == 0),并且从此之后都恰好再也没有被其他元素替换掉。这些事件彼此独立,计算概率的方法正好是上面提到的式子,最终的概率就是1/n。

OK,问题解决了。结束之前再做个简单的扩展,改成等概率随机选取m个元素(可知每个元素被选中的概率都是m/n)。

解决办法也非常简单,只要在上面的代码中,把selectedItem(selection)改成一个长度为m的数组,稍作调整就可以了。

这里就给出Python的程序片段:

 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from  random  import Random

def RandomSample (m = 1 , rand = None ):
  selection  =  [ ]
  count  =  0
   if rand  is  None:
    rand  = Random ( )
   while  True:
     # Outputs the current selection and gets next item
    item  =  yield selection
     if  len (selection )  < m:
      selection. append (item )
     else:
      idx  = rand. randint ( 0 , count )
       if idx  < m:
        selection [idx ]  = item
    count + =  1

时间复杂度O(n),空间复杂度O(m)(不可能是O(1)的)。概率的计算方法为:

pi=mi×ii+1×i+1i+2××n1n=mn1×mm+1×m+1m+2××n1n=mni>mim

等概率问题通常都是比较简单的。下一次将会对这个问题做进一步的扩展,变成每个元素都有一个权重,要求任何一个元素被选取的概率正比于其权重。

地址:http://www.gocalf.com/blog/random-selection.html

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值