抽奖案例一:按设置的概率实时抽奖-保证人人平等
抽奖案例一:按设置的概率实时抽奖-保证人人平等
抽奖案例一:按设置的概率实时抽奖-保证人人平等
核心:保证每一次抽奖的概率是一样的,这是一个放回问题(学过概率的朋友们都懂的)
一、奖品配置(本期不含库存,库存下期再讲)
比如我们想要这样配置奖品和概率:
奖品名称 | 中奖概率 |
---|---|
汤臣一品 | 80% |
迈巴赫 | 1% |
1元红包 | 0.01% |
用X轴描述一下我们的奖品分布:
为什么max值是10000呢?因为我们允许小数点后面最多两位,调整成整数后就是10000。
二、随机值与概率
从1-10000中间随机获取一个x值,就可以得到对应f(x)的结果。这个结果就是我们的中奖结果。
接下来就是用Java来实现这个f(x)。
入参:一个随机值x,一个奖品与概率的mapping关系
出参:f(x)结果
首先我们设置一下奖品和概率,这里直接放在Redis中:
[
{"id":1,"name":"汤臣一品","rate":"0.8"},
{"id":2,"name":"迈巴赫","rate":"0.01"},
{"id":3,"name":"1元红包","rate":"0.0001"}
]
随机数的话我们用SecureRandom,为什么不用Random?
SecureRandom secureRandom = new SecureRandom();
double v = secureRandom.nextDouble();
double ceil = Math.ceil(v * 10000); //等会就用这个随机数来抽奖
三、实现方式
把奖池数据处理成一个TreeMap,为什么要用TreeMap呢?等会儿我跟你讲
private TreeMap<Double, Integer> getTreeMap(String poolId) {
//获取奖池中奖品信息
List<Prize> prizes = getPool(poolId);
if (CollectionUtils.isEmpty(prizes)) {
return null;
}
TreeMap<Double, Integer> treeMap = new TreeMap<>();
Double js = 0.0;
for (int i = 0; i < prizes.size(); i++) {
double rate = Math.ceil((Double.valueOf(prizes.get(i).getRate()))* 10000);
treeMap.put(rate + js, prizes.get(i).getId());
js = js + rate;
}
//收个尾
treeMap.put(10000.0, 0);
return treeMap;
}
会得到-----------【手动加粗放大】
{8000.0:1,8100.0:2,8101.0:3,10000.0:0}
再使用tailMap()和firstEntry()去找到对应的value
Integer prizeId = treeMap.tailMap(ceil, false).firstEntry().getValue();
就实现了根据概率抽奖的核心流程,是不是很简单
四、插播----冷知识
插播一段:为什么使用的是TreeMap而不是FlowerMap不是GlassMap
与HashMap相比,TreeMap是一个能比较元素大小的Map集合,TreeMap是SortedMap接口的一个实现。看的出来sorted,我该怎么跟你解释这个sorted呢?sorted?这个应该很好理解吧?S-O-R-T-E-D
就是排序的(因为TreeMap的key是要实现Comparable接口的,不然的话用不了哦)。所以能很好的支持我们这个f(x)并且快速找到结果。然后他也实现了NavigableMap,这篇(https://blog.csdn.net/qq_20919883/article/details/135370278)就给大家很好的解释了一下SortedMap和NavigableMap。看完记得回来继续啊!!!
我们使用到的tailMap
public NavigableMap<K,V> tailMap(K fromKey, boolean inclusive)
其中有两个参数:
fromKey 范围为 [fromKey, +∞) / (fromKey, +∞) 的子视图
inclusive 就是要不要包含fromKey。为true的时候就是[fromKey, +∞) ,为false时是(fromKey, +∞)
这样只是获得了一个子视图,比如我们随机数ceil是8002的话,子视图是8002至+∞。这里对应还有很多value,我们想要的只是:8002往后遇到的第一个键对应的值。所以结合一下firstEntry()这个方法的作用是返回这个映射中最小键(即第一个键)所对应的键值对
所以以下代码段就实现了我们的随机并保证概率的一次抽奖。
Integer prizeId = treeMap.tailMap(ceil, false).firstEntry().getValue();
关于inclusive的取值,是这样想的👇🏻
有这样一个问题:如果随机数=8000,你希望中的是汤臣一品还是迈巴赫?
(小孩子才做选择,成年的我都要!!!)
醒醒啊!很明显啊 ,应该是迈巴赫的。0-7999有8000个整数,这样才能对应我们一开始的权重分布图。如果随机数8000算汤臣一品的话,那阿汤的概率就变成80.01%了。
—> 因此inclusive取值是false才能保证,随机到8000时,抽到的是阿迈。
五、测试结果
奖品名称 | 10人抽奖结果 | 100人抽奖结果 | 1000人抽奖结果 | 10000人抽奖结果 |
---|---|---|---|---|
汤臣一品 | 10 | 75 | 759 | 8037 |
迈巴赫 | 0 | 0 | 15 | 106 |
1元红包 | 0 | 0 | 0 | 3 |
未中奖 | 0 | 25 | 190 | 1853 |
就是这样,再见