你的浏览器不支持canvas

做你害怕做的事情,然后你会发现,不过如此。

java 抽奖代码

时间: 作者: 黄运鑫

本文章属原创文章,未经作者许可,禁止转载,复制,下载,以及用作商业用途。原作者保留所有解释权。


需求

  • 有若干个奖品,根据奖品的中奖概率抽取奖品给用户
  • 后台通过维护奖品数量来控制中奖概率
  • 本文为原创,转载请注明: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;//返回未中奖
    }
    
  • 如果随机数很小,可能只循环一两次就能得到结果,效率非常高;

对于本文内容有问题或建议的小伙伴,欢迎在文章底部留言交流讨论。