需求
- 有若干个奖品,根据奖品的中奖概率抽取奖品给用户
- 后台通过维护奖品数量来控制中奖概率
- 本文为原创,转载请注明:https://blog.xinpapa.com/2018/03/21/lottory/
代码
第一种代码(待优化)
- 同事随手写的代码
//奖品实体类 public class Prize { //奖品id(数据库主键) private Long id; //奖品名称 private String name; //剩余数量 private Integer num; public Prize(Long id, String name, Integer num) { this.id = id; this.name = name; this.num = num; } //TODO GET和SET方法略 }
//抽奖类 public class Lottery { //奖品集合 private List<Prize> prizeList; //所有奖品总数 private Integer totalNum = 0; //加载奖品信息和奖品数量 public Lottery() { //正式环境从数据库获取 this.prizeList = new ArrayList<>(); this.prizeList.add(new Prize(1L, "一等奖", 1)); this.prizeList.add(new Prize(2L, "二等奖", 2)); this.prizeList.add(new Prize(3L, "未中奖", 100)); //计算剩余奖品总数量 for (Prize prize : prizeList) { this.totalNum += prize.getNum(); } } //抽奖的方法 public Long lottery() { //中奖率map集合 HashMap<Long, Integer> map = new HashMap<>(); for (Prize prize : prizeList) { //中奖率取整 Double percentage = new Double(Math.ceil(new Double(prize.getNum()) / new Double(this.totalNum) * 100)); map.put(prize.getId(), percentage.intValue()); } //根据中奖率的大小,把奖品id放入list中 List<Long> list = new ArrayList<>(); for (Map.Entry<Long, Integer> entry : map.entrySet()) { for (int i = 0; i < entry.getValue(); i++) { list.add(entry.getKey()); } } if (list.size() == 0) { return 3L;//没有奖品,返回未中奖 } //抽中的奖品id Long resultId = list.get(new Random().nextInt(list.size())); //TODO 奖品剩余数量-1(略) return resultId; } public static void main(String[] args) { Lottery lottery = new Lottery(); System.out.println("抽中的奖品id=" + lottery.lottery()); } }
- 虽然代码能正常使用,但是抽奖的方法
lottery()
很臃肿,不便于理解,循环多效率低,而且对中奖率取整使得中奖率不是很精确,存在优化的空间;
第二种(对方法优化)
-
原抽奖方法中,是先计算各奖品的中奖率,再根据中奖率创建一个奖品池
list
,最后再抽奖;
看似方法和思路没问题,但是细想后发现其实没有必要计算中奖率,只需要根据各奖品的数量,将奖品放到list
中,即可创建一个一样的奖品池,并且中奖率更准确; - 不改变原代码中的思路,优化后的
lottery()
方法如下:public Long lottery() { List<Long> list = new ArrayList<>(); for (Prize prize : prizeList) { for (int i = 0; i < prize.getNum(); i++) { list.add(prize.getId()); } } if (list.size() == 0) { return 3L;//没有奖品,返回未中奖 } //抽中的奖品id Long resultId = list.get(new Random().nextInt(list.size())); //TODO 奖品剩余数量-1(略) return resultId; }
- 优化后的方法虽然省去了一个
for
循环和计算中奖率的过程,但是优化的程度并不大,嵌套的for
循环还是很影响效率;
假如有10个奖品,每个奖品有10000个,那么每次抽奖就要循环100000次;
由其对于抽奖这种调用频率很高的方法,还是显得不尽人意;
第三种(最终:对思路的优化)
-
原抽奖思路:根据各奖品的剩余数量,将奖品放到一个
list
中,这个list
相当于奖品池;
然后在奖品池的大小范围内,获取一个随机数resultId
;
最后用resultId
找到奖品池中对应的奖品; -
优化思路:可以发现,无论各奖品数量是多少,最后得到的随机数
resultId
范围是0 <= resultId < list.size()
,而list.size()
的值我们一开始就知道,就是奖品总数totalNum
;
所以在方法的一开始就可以得到resultId
,没必要将所有奖品都放到list
中再抽奖;
最后根据各奖品的数量即可得到resultId
对应的奖品; - 最终优化的
lottery()
方法如下:public Long lottery() { //先抽奖 Integer resultId = new Random().nextInt(totalNum + 1);//这里有改动,得到的resultId为 1<= resultId <= totalNum //找到找对应的奖品 Integer temp = 0;//奖品数量累计值 for (Prize prize : prizeList) { temp += prize.getNum(); if (resultId <= temp) { //TODO 奖品剩余数量-1(略) return prize.getId(); } } return 3L;//返回未中奖 }
- 如果随机数很小,可能只循环一两次就能得到结果,效率非常高;