该随机算法可以实现权重随机也可以做一般随机抽奖。业务需求来源是有100个病人,按照1:1的比例进行随机分配到两个组里。

算法

  1. 根据proportionMap<组id,比例>分组,每个分组有最大、最小值、比例
  2. 取随机数,看随机数落到哪个范围内就是哪个分组
  3. 如果分组内的总数达到sum*weight,则进行满桶处理并且重复第二步直到成功分组。
  4. 目前用fullHandler方法进行满桶处理,getOverdueRandom方法有栈溢出风险,尽管风险很低(风险随着桶的数量和总数增加而增加)。

满桶处理算法

  1. 将weightRandomize(已经满的桶)放入overdueBarrels(过期桶数组)
  2. 重新构建随机权重桶,就是为没有满的桶重新分配最大值、最小值
import java.math.BigDecimal;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.*;

/**
 *  随机算法工具
 */
public class RandomizeUtil {

    private RandomizeUtil(){

    }

    /**
     * 获取随机列表
     * 将sum总数的(0-sum)随机数按比例分配到proportionMap的分组中
     * @param sum 总数
     * @param proportionMap<组id,比例>
     * @return
     */
    public static List<Long> getRandomizeList(int sum, Map<Long,Integer> proportionMap){
        RandomizeHandle randomizeHandle = new RandomizeHandle(sum,proportionMap,true);
        return randomizeHandle.getRandomizeList();

    }

    /**
     * 获取随机列表
     * @param sum
     * @param proportionMap
     * @return
     */
    public static Map<Long,List> getRandomizeMap(int sum, Map<Long,Integer> proportionMap){
        RandomizeHandle randomizeHandle = new RandomizeHandle(sum,proportionMap,true);
        List<Long> list = randomizeHandle.getRandomizeList();
        return toMap(list);

    }

    private static Map<Long,List> toMap(List<Long> list){
        Map<Long,List> data = new HashMap<>();
        for (int i = 0; i < list.size(); i++){
            Long key = list.get(i);
            if (data.containsKey(key)){
                List dataList = data.get(key);
                dataList.add(i);
            }else {
                List dataList = new ArrayList();
                dataList.add(i);
                data.put(key,dataList);
            }
        }
        return data;
    }

    /**
     * 获取单个随机数,可以用于类似抽奖的功能
     * @param sum
     * @param proportionMap
     * @return
     */
    public static Long getRandomize(int sum, Map<Long,Integer> proportionMap){
        RandomizeHandle randomizeHandle = new RandomizeHandle(sum,proportionMap,true);
        return randomizeHandle.weightRandomize();
    }

    /**
     * 随机处理类
     * 算法:1 根据proportionMap分组,每个分组有最大和最小值
     *      2 取随机数,看随机数落到哪个范围内就是哪个分组
     *      3 如果分组数达到sum*weight,则进行满桶处理并且重复第二步直到成功分组。
     *  目前用fullHandler方法进行满桶处理,getOverdueRandom方法有栈溢出风险,尽管风险很低(风险随着桶的数量和总数增加而增加)。
     */
    static class RandomizeHandle{
        //原始权重list
        private List<WeightRandomize> originalWeightRandomizes = new ArrayList<>();

        //权重list
        private List<WeightRandomize> weightRandomizes = new ArrayList<>();

        //原始分组比例map
        Map<Long,Integer> originalProportionMap = new HashMap<>();

        //分组比例map
        Map<Long,Integer> proportionMap = new HashMap<>();

        private int defalutValue = -1;

        //总数
        private Integer originalSum;

        //总数
        private Integer sum;

        //当前数,初始为sum,分配一个减1
        private Integer currentCount;

        //过期桶
        private List<WeightRandomize> overdueBarrels = new ArrayList<>();

        //加减法标记
        private boolean addSubflag = true;

        //余数
        private int remainder;

        public RandomizeHandle(Integer size, Map<Long,Integer> proportionMap,boolean isFilterRemainder){
            if (isFilterRemainder){
                this.remainder = filterRemainder(size,proportionMap);
                size = size - remainder;
            }
            this.originalSum = size;
            this.sum = size;
            this.currentCount = size;
            this.originalProportionMap.putAll(proportionMap);
            this.proportionMap.putAll(proportionMap);
            this.originalWeightRandomizes = buildWeightRandomizeList();
            this.weightRandomizes = buildWeightRandomizeList();
        }

        /**
         * 过滤余数
         * @param size
         * @param proportionMap
         * @return
         */
        private int filterRemainder(Integer size, Map<Long,Integer> proportionMap){
            int scaleSum = getScaleSum(proportionMap);
            if (scaleSum == 0){
                return 0;
            }

            int remainderCount = 0;
            if (size < scaleSum){
                remainderCount = size;
            } else {
                remainderCount = size % scaleSum;
            }
            return remainderCount;
        }

        /**
         * 构建权重对象列表
         * @return
         */
        private List<WeightRandomize> buildWeightRandomizeList(){
            List<WeightRandomize> list = new ArrayList<>();
            Iterator<Long> iterator = proportionMap.keySet().iterator();
            int min = 0;
            int max = 0;
            while (iterator.hasNext()){
                Long id = iterator.next();
                //比例
                Integer scale = proportionMap.get(id);
                BigDecimal scaleDecimal = BigDecimal.valueOf(scale);

                //比例总数
                Integer scaleSum = getScaleSum(proportionMap);
                BigDecimal scaleSumDecimal = BigDecimal.valueOf(scaleSum);

                //总数
                BigDecimal sumDecimal = BigDecimal.valueOf(currentCount);

                //权重
                BigDecimal weightDecimal = scaleDecimal.divide(scaleSumDecimal,2,BigDecimal.ROUND_UP);

                //权重*总数
                int weightSum = sumDecimal.multiply(weightDecimal).setScale(0,BigDecimal.ROUND_UP).intValue();

                //下一个桶的最小值是上个桶的最大值
                min = max;
                //下一个桶的最大值是上个桶最大值+权重间距
                max = max + weightSum;

                WeightRandomize weightRandomize = new WeightRandomize(id,weightDecimal.doubleValue(),min,max,weightSum);
                list.add(weightRandomize);
            }
            return list;
        }

        /**
         * 重置权重集合
         * @return
         */
        public List<WeightRandomize> reBuildWeightRandomizeList(){
            Iterator<WeightRandomize> iterator = weightRandomizes.iterator();
            int min = 0;
            int max = 0;
            //比例总数
            Integer scaleSum = getScaleSum(proportionMap);
            BigDecimal scaleSumDecimal = BigDecimal.valueOf(scaleSum);

            while (iterator.hasNext()){
                WeightRandomize weightRandomize = iterator.next();
                //比例
                Integer scale = proportionMap.get(weightRandomize.getId());
                BigDecimal scaleDecimal = BigDecimal.valueOf(scale);

                // 权重
                BigDecimal weightDecimal = scaleDecimal.divide(scaleSumDecimal,2,BigDecimal.ROUND_UP);

                // 下一个桶的最小值是上个桶的最大值
                min = max;
                // 下一个桶的最大值是上个桶最大值+权重间距
                max = max + (weightRandomize.sum - weightRandomize.currentCount);
                // 更新最大最小值
                weightRandomize.setMinValue(min);
                weightRandomize.setMaxValue(max);
                weightRandomize.setWeight(weightDecimal.doubleValue());

            }
            return weightRandomizes;
        }

        /**
         * 获取比例总数 1:1 就是2
         * @param map
         * @return
         */
        private int getScaleSum(Map<Long,Integer> map){
            Iterator<Long> iterator = map.keySet().iterator();
            int scaleSum = 0;
            while (iterator.hasNext()){
                Long key = iterator.next();
                Integer val = map.get(key);
                scaleSum +=val;
            }
            return scaleSum;
        }

        /**
         * 获取随机列表
         * @return
         */
        public List<Long> getRandomizeList(){
            List<Long> list = new ArrayList<>();
            for (int i = 0; i<originalSum; i++){
                Long id = weightRandomize();
                list.add(id);
                currentCount--;
            }
            //添加余数
            for (int i = 0; i < remainder; i++ ){
                list.add(0L);
            }
            return list;
        }

        private Map<Integer,List> toMap(List<Integer> list){
            Map<Integer,List> data = new HashMap<>();
            for (int i = 0; i < list.size(); i++){
                Integer key = list.get(i);
                if (data.containsKey(key)){
                    List dataList = data.get(key);
                    dataList.add(i);
                }else {
                    List dataList = new ArrayList();
                    dataList.add(i);
                    data.put(key,dataList);
                }
            }
            return data;
        }

        /**
         * 权重随机
         * @return
         */
        private Long weightRandomize(){
            int rvalue = getSimpleRandom(sum);

            for (WeightRandomize weightRandomize : weightRandomizes){
                boolean flag = weightRandomize.randomize(rvalue);
                if (flag){
                    Long id = weightRandomize.getId();
                    //满桶
                    if (weightRandomize.isFull()){
                        fullHandler(weightRandomize);
                    }
                    return id;
                }
            }
            return 0L;
        }

        /**
         * 满桶处理 将weightRandomize放入overdueBarrels,重新构建随机权重桶
         * @param weightRandomize
         */
        private void fullHandler(WeightRandomize weightRandomize){
            overdueBarrels.add(weightRandomize);
            proportionMap.remove(weightRandomize.getId());
            weightRandomizes.remove(weightRandomize);
            sum = currentCount - 1;
            //放最后
            reBuildWeightRandomizeList();
        }

        /**
         * 获取简单随机数
         * @param randomCount
         * @return
         */
        public int getSimpleRandom(int randomCount){
            return getRandom(randomCount,2);
        }

        /**
         * 获取权重随机数,过滤已经满的权重桶overdueList
         * 加法: 该桶最大值+1
         * 减法:SecureRandom().nextInt(minValue)
         * 1 如果桶满了,根据addSubflag来进行加法或者减法跳过该桶,
         * 2 默认使用加法,超过上标改成使用减法。超过下标则重新获取
         *
         * 备注:极端情况下有可能栈溢出
         * @param randomCount
         * @return
         */
        public int getOverdueRandom(int randomCount){
            int value = getRandom(randomCount,1);

            for (WeightRandomize weightRandomize : overdueBarrels){
                //最大值
                int maxValue = weightRandomize.getMaxValue();
                //最小值
                int minValue = weightRandomize.getMinValue();
                //不在范围内
                if (!weightRandomize.isRange(value)){
                    continue;
                }
                //加法
                if (addSubflag){
                    value = maxValue+1;
                    //超出上限,改用减法
                    if (value > sum){
                        addSubflag = false;
                    }
                }
                //减法
                if (!addSubflag){
                    //超出下限,重新获取
                    if (minValue <= 0){
                        addSubflag = true;
                        return getOverdueRandom(0);
                    }
                    value = getRandom(minValue,1);
                }
            }
            return value;
        }

        /**
         * 获取随机数
         * @param value
         * @param model 1: value为0取随机数,不为0直接返回  2: 直接取随机数
         * @return
         */
        public int getRandom(int value,int model){
            SecureRandom random= null;
            try {
                random = SecureRandom.getInstance("SHA1PRNG");
            } catch (NoSuchAlgorithmException e) {
                throw new RuntimeException("生成随机数失败--getRandom");
            }

            if (model == 1){
                if (value == 0){
                    //获取随机数,排除0
                    int randomCount =  random.nextInt(sum);
                    return randomCount == 0 ? 1: randomCount;
                }else {
                    return value;
                }
            }

            if (model == 2){
                //获取随机数,排除0
                int randomCount =  random.nextInt(value);
                return randomCount == 0 ? 1: randomCount;
            }
            return value;
        }

        public List<WeightRandomize> getWeightRandomizes() {
            return weightRandomizes;
        }

        public void setWeightRandomizes(List<WeightRandomize> weightRandomizes) {
            this.weightRandomizes = weightRandomizes;
        }

        public Map<Long, Integer> getProportionMap() {
            return proportionMap;
        }

        public void setProportionMap(Map<Long, Integer> proportionMap) {
            this.proportionMap = proportionMap;
        }

        public Integer getSum() {
            return sum;
        }

        public void setSum(Integer sum) {
            this.sum = sum;
        }

        public List<WeightRandomize> getOverdueBarrels() {
            return overdueBarrels;
        }

        public void setOverdueBarrels(List<WeightRandomize> overdueBarrels) {
            this.overdueBarrels = overdueBarrels;
        }

        public boolean isAddSubflag() {
            return addSubflag;
        }

        public void setAddSubflag(boolean addSubflag) {
            this.addSubflag = addSubflag;
        }

        public int getDefalutValue() {
            return defalutValue;
        }

        public void setDefalutValue(int defalutValue) {
            this.defalutValue = defalutValue;
        }
    }

    static class WeightRandomize{
        //唯一标识
        private Long id;
        //比例
        private Integer scale;
        //权重
        private Double weight;
        //最小值
        private Integer minValue;
        //最大值
        private Integer maxValue;
        //总数
        private int sum;
        //当前数
        private int currentCount = 0;

        WeightRandomize(Long id){
            this.id = id;
        }

        WeightRandomize(Long id,Double weight,Integer minValue,Integer maxValue,Integer sum){
            this.id = id;
            this.weight = weight;
            this.minValue = minValue;
            this.maxValue = maxValue;
            this.sum = sum;
        }

        public boolean randomize(Integer value){
            if (isFull()){
                return false;
            }

            if (isRange(value)){
                currentCount++;
                return true;
            }
            return false;
        }

        public boolean isRange(int value){
            return value > minValue && value <= maxValue;
        }

        public boolean isFull(){
            return currentCount >= sum;
        }

        public Long getId() {
            return id;
        }

        public void setId(Long id) {
            this.id = id;
        }

        public Double getWeight() {
            return weight;
        }

        public void setWeight(Double weight) {
            this.weight = weight;
        }

        public Integer getMinValue() {
            return minValue;
        }

        public void setMinValue(Integer minValue) {
            this.minValue = minValue;
        }

        public Integer getMaxValue() {
            return maxValue;
        }

        public void setMaxValue(Integer maxValue) {
            this.maxValue = maxValue;
        }

        public Integer getSum() {
            return sum;
        }

        public void setSum(Integer sum) {
            this.sum = sum;
        }

        public Integer getCurrentCount() {
            return currentCount;
        }

        public void setCurrentCount(Integer currentCount) {
            this.currentCount = currentCount;
        }
    }