Python实现一个带权无回置随机抽选函数的方法

  


  

  

有一个抽奖应用,从所有参与的用户抽出K位中奖用户(K=奖品数量),且要根据每位用户拥有的抽奖码数量作为权重。

  

如假设有三个用户及他们的权重是:(1)、B (1), C(2)。希望抽到一个的概率为25%,抽到B的概率为25%,抽到C的概率为50%。

  


  

  

比较直观的做法是把两个C放到列表中抽选,如[A, B, C, C],使用Python内置的函数随机的。选择[A, B, C, C],这样C抽到的概率即为50%。

  

这个办法的问题是权重比较大的时候,浪费内存空间。

  

更一般的方法,是将所有权重加和4,然后从[0,4)区间里随机挑选一个值,将A, B, C占用不同大小的区间。(0,1)是,(1,2)是B,(2、4)是C .

  

使用Python的函数随机的。ranint(0, 3)或者int (random.random() * 4)均可产生0 - 3的随机整数R .判断R在哪个区间即选择哪个用户。

  

接下来是寻找随机数在哪个区间的方法,

  

一种方法是按顺序遍历列表并保存已遍历的元素权重综合年代,一旦年代大于R,就返回当前元素。

        从运营商进口itemgetter      用户=[(' A ', 1), (' B ', 1), (' C ', 2)]      总=总和(地图(itemgetter(1),用户))      rnd=int (random.random () *) # 0 ~ 3      s=0   u, w用户:   s +=w   如果s比;研发:   回报你      之前      

不过这种方法的复杂度是O (N),因为要遍历所有的用户。

  

可以想到另外一种方法,先按顺序把累积加的权重排成列的表,然后对它使用二分法搜索,二分法复杂度降到O (logN)(除去其他的处理)

        用户=[(' A ', 1), (' B ', 1), (' C ', 2)]      cum_weights=列表(itertools.accumulate(地图(itemgetter(1),用户)))# (1、2、4)      总=cum_weights [1]      rnd=int (random.random () *) # 0 ~ 3      你好=len (cum_weights) - 1   指数=平分。二等分(cum_weights rnd 0,嗨)      返回用户(指数)[0]      之前      

Python内置库随机的选择函数(3.6版本后有)即是如此实现,随机的。选择函数签名为随机的。选择(人口、重量=None, *, cum_weights=None, k=1)人口是待选列表,权重是各自的权重,cum_weights是可选的计算好的累加权重(两者选一),k是抽选数量(有回置抽选)。源码如下:

        def选项(自我、人口、重量=None, *, cum_weights=None, k=1):   ”“人口“返回一个k大小的列表元素与替代选择。   如果相对权重或累积重量不指定,   选择是用概率相等。   ”“”   随机=self.random   如果cum_weights没有:   如果重量没有:   _int=int   总=len(人口)   返回(人口(_int(随机()*总)]我在范围(k))   cum_weights=列表(_itertools.accumulate(权重)   elif权重不是没有:   提高TypeError(无法指定权重和累积重量)   如果len (cum_weights) !=len(人口):   提高ValueError('权重的数量人口的不匹配)   平分=_bisect.bisect   总=cum_weights [1]   你好=len (cum_weights) - 1   返回[人口[平分(cum_weights随机()*总,0,你好))   因为我在范围(k))   之前      


  

  

因为Python内置的random.choices是有回置抽选,无回置抽选函数是random.sample,但该函数不能根据权重抽选(随机的。示例(人口、k))。

  

原生的随机的。样品可以抽选个多个元素但不影响原有的列表,其使用了两种算法实现,保证了各种情况均有良好的性能。(源码地址:random.sample)

  

第一种是部分洗牌,得到K个元素就返回。时间复杂度是O (N),不过需要复制原有的序列,增加内存使用。

        结果=[所有]* k   n=len(人口)   池=列表(人口)#不改变原有的序列   因为我在范围(k):   j=int (random.random () * (n))   结果池[k]=[j]   池[j]=[n-i-1] #池已选中的元素移走,后面未选中元素填上   返回结果   之前      

而第二种是设置一个已选择的设置,多次随机抽选,如果抽中的元素在组内,就重新再抽,无需复制新的序列。当k相对n较小时,random.sample使用该算法,重复选择元素的概率较小。

        选择=组()   selected_add=选中。添加#加速方法访问   因为我在范围(k):   j=int (random.random () * n)   j在选择:   j=int (random.random () * n)   selected_add (j)   结果[j]=[j]人口   返回结果   

Python实现一个带权无回置随机抽选函数的方法