该随机算法可以实现权重随机也可以做一般随机抽奖。业务需求来源是有100个病人,按照1:1的比例进行随机分配到两个组里。
算法
- 根据proportionMap<组id,比例>分组,每个分组有最大、最小值、比例
- 取随机数,看随机数落到哪个范围内就是哪个分组
- 如果分组内的总数达到sum*weight,则进行满桶处理并且重复第二步直到成功分组。
- 目前用fullHandler方法进行满桶处理,getOverdueRandom方法有栈溢出风险,尽管风险很低(风险随着桶的数量和总数增加而增加)。
满桶处理算法
- 将weightRandomize(已经满的桶)放入overdueBarrels(过期桶数组)
- 重新构建随机权重桶,就是为没有满的桶重新分配最大值、最小值
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;
}
}