应用场景:最近做一个推广活动的小程序,用户通过链接分享点击数可以抽取奖励,点击数越高,抽取到大奖的概率越高,所以这里奖品的随机概率是确定并且动态变化的。
参考了HashMap的些许设计思路,代码如下:
package com.rc.components.common.utils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
import lombok.Data;
/**
* 概率随机工具
* @author: rc
* @date: 2018年2月5日 下午4:55:48
* @version: V1.0
* @review: rc/2018年2月5日 下午4:55:48
*/
public class ProbRandom {
private final Integer randomBound; //随机边界范围
private final Random random; //随机生成器
private final HashMap<Object, Integer> originalMap; //随机元素的原始数据容器
private ArrayList<ProbDistribution> probDistributions; //元素分布概率的数组
private boolean updateFlag; //原始数据容器是否有更新
private Integer originalSum; //原始数据随机值求和
private Integer zeroOriginalCount; //原始数据随机值为0(未设置随机值)的元素个数
public ProbRandom(Integer randomBound) {
this.originalMap = new HashMap<>();
this.updateFlag = true;
this.randomBound = randomBound;
this.originalSum = 0;
this.zeroOriginalCount = 0;
this.random = new Random();
}
/**
* 生成随机数
* @return
*/
public Object next(){
distribute(); //计算元素分布的概率值
if (null == probDistributions) {
return null;
}
int nextInt = random.nextInt(randomBound);
ProbDistribution prob = probDistributions.stream().filter(c->(c.floor<=nextInt&&c.ceil>nextInt)).findFirst().get();
return prob.getKey();
}
/**
* 添加随机元素和概率值,返回添加后的概率值
* 如果prob=0 ,概率值计算为总体概率值确定的默认值
* @param key
* @param prob
* @return
*/
public void put(Object key, Integer prob){
//校验
if (null == key || prob == null ) {
throw new NullPointerException("key或者prob不能为空");
}
if (prob < 0) {
throw new RuntimeException("prob不能小于0");
}
//添加
Integer previousProb = originalMap.put(key, prob);
//更新
if (prob == 0) {
zeroOriginalCount ++ ;
} else {
originalSum += prob;
}
if (previousProb == null) {
updateFlag = true;
} else {
if (previousProb == 0) {
zeroOriginalCount -- ;
} else {
originalSum -= previousProb;
}
if (prob != previousProb) {
updateFlag = true;
}
}
}
/**
* 刪除已經添加的元素,返回元素設置的隨機值
* 如果元素不存在,返回null
* @param key
* @return
*/
public Integer remove(Object key){
//校验
if (null == key) {
throw new NullPointerException("key或者prob不能为空");
}
//刪除
Integer remove = originalMap.remove(key);
if (null != remove) {
updateFlag = true;
}
return remove;
}
/**
* 等同于put(key,0)
* @param key
*/
public void put(Object key){
put(key,0);
}
/**
* 返回随机值边界randomBound
* @return
*/
public Integer getRandomBound(){
return randomBound;
}
/**
* 返回元素的随机值prob
* @param key
* @return
*/
public Integer getProb(Object key){
return originalMap.get(key);
}
/**
* 返回元素的集合
* @return
*/
public Set<Object> getKeySet(){
return originalMap.keySet();
}
//计算概率元素的概率分布
private void distribute() {
if (randomBound < originalSum) {
throw new RuntimeException("总随机值不能大于随机值边界");
}
//如果原始数据容器有更新,就从新计算分布概率数组
if (updateFlag) {
//创建分布概率数组
probDistributions = new ArrayList<ProbRandom.ProbDistribution>();
//重组分布概率数组
Integer tempFloor = 0;
if (zeroOriginalCount == 0) {
for (Entry<Object, Integer> e : originalMap.entrySet()) {
Integer ceil = (int) (tempFloor + e.getValue()*1.00*randomBound/originalSum);
probDistributions.add(new ProbDistribution(e.getKey(), tempFloor, ceil)) ;
tempFloor = ceil;
}
} else {
Integer defProbSize = (int) ((randomBound - originalSum)*1.00/zeroOriginalCount);
for (Entry<Object, Integer> e : originalMap.entrySet()) {
Integer ceil;
if (e.getValue() == 0) {
ceil = tempFloor + defProbSize;
} else {
ceil = (int) (tempFloor + e.getValue());
}
probDistributions.add(new ProbDistribution(e.getKey(), tempFloor, ceil)) ;
tempFloor = ceil;
}
}
updateFlag = false;
}
}
//描述单个元素分布概率情况
@Data
private class ProbDistribution{
private Object key;
private Integer floor;
private Integer ceil;
ProbDistribution(Object key, Integer floor, Integer ceil){
this.key = key;
this.floor = floor;
this.ceil = ceil;
}
}
}
以上代码大致思路:
1.使用一个集合originalMap作为随机元素的容器,key是对应随机对象,value是设定的随机概率,这里的概率并不是百分数,而是以一个概率基数randomBound作为比较的。
2.在进行随机取样时,是将originalMap中的所有概率值转换计算distribute()成randomBound内的数值范围分布probDistributions,通过random生成的随机数对应的数值范围分布next()来产生一定概率的随机对象。
3.其中distribute()是一个比较关键并且损耗性能的方法,所以在这里使用一个标记updateFlag来标识originalMap中概率值是否发生过变化,只有发生过变化的时候才需要重新distribute()进行probDistributions的计算,否则使用原有的probDistributions来产生随机对象即可。
4.另外对外暴露了put,remove,get的方法来操作originalMap,但是没有直接获取originalMap的方法,因为如果可以直接获取originalMap,updateFlag就无法准确的描述概率值是否发生了变化。
简单的测试:
package com.rc.components.common.utils;
public class ProbRandoTest {
public static void main(String[] args) {
ProbRandom pr = new ProbRandom(1000);
pr.put("A", 300);
pr.put("B", 200);
pr.put("C", 300);
int countA = 0;
int countB = 0;
int countC = 0;
long start = System.currentTimeMillis();
System.out.println(start);
for (int i = 0; i < 10000; i++) {
Object next = pr.next();
if ("A".equals(next)) {
countA++;
}else if ("B".equals(next)) {
countB++;
}else if ("C".equals(next)) {
countC++;
}
}
long end = System.currentTimeMillis();
System.out.println(end-start);
System.out.println("countA" + countA);
System.out.println("countB" + countB);
System.out.println("countC" + countC);
}
}
测试结果
循环随机1000000次耗时223毫秒
countA。。。375239
countB。。。250509
countC。。。374252
结果显示概率准确性和运行速度都没有太大问题。
当然这只是一个初级的版本,没有考虑多线程情况下的数据可靠性问题,如果需要考虑到线程安全,可以模拟ConcurrentHashMap的分段锁机制,实现性能和安全性调和。