可指定概率的随机对象生成工具-java

应用场景:最近做一个推广活动的小程序,用户通过链接分享点击数可以抽取奖励,点击数越高,抽取到大奖的概率越高,所以这里奖品的随机概率是确定并且动态变化的。

参考了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 originalMap; //随机元素的原始数据容器
    
    private ArrayList 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 getKeySet(){
        return originalMap.keySet();
    }
        
    //计算概率元素的概率分布
    private void distribute() {
        if (randomBound < originalSum) {
            throw new RuntimeException("总随机值不能大于随机值边界");
        }
        //如果原始数据容器有更新,就从新计算分布概率数组
        if (updateFlag) {
            //创建分布概率数组
            probDistributions = new ArrayList();
            
            //重组分布概率数组
            Integer tempFloor = 0;
            if (zeroOriginalCount == 0) {
                for (Entry 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 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的分段锁机制,实现性能和安全性调和。

你可能感兴趣的:(可指定概率的随机对象生成工具-java)