[小设计] 抽奖算法

2,911 阅读2分钟

一、概述

应用场景:抽奖活动

开奖,可以是手动开奖和定时开奖。

抽奖这种活动需要尽可能的公平。

算法分为三个部分:

  1. Input 输出

    输入参数:活动参与者列表、奖品数

  2. Process 处理

    选择出中奖人

  3. Output 输出

    中奖名单


二、抽奖算法

想到的算法有:

  1. 所有数 random
  2. 抽样算法
  3. shuffle 算法
  4. 分区算法

(1)所有数 random

此为暴力方法,也是比较容易想到的:随机抽取若干人作为中奖人。

但此问题会产生冲突。

简单步骤如下:

  1. 加载参与活动的人于内存中
  2. random 一人作为中奖人,加入中奖名单 result
  3. 若中奖人有重复,则继续步骤二

代码如下:

    /**
     * 获取中奖名单
     *
     * @param num 奖品数量
     * @param participantIds 活动参与者
     * @return 中奖名单
     */
    private List<String> getWinnerIds(final Integer num, final List<String> participantIds) {

        if (num >= participantIds.size()) {

            return participantIds;
        }

        Random random = new Random();
        
        Set<String> userIdSet = new HashSet<>(num);
        
        while (userIdSet.size() < num) {
            
            int index = random.nextInt(participantIds.size());
            
            String userId = participantIds.get(index);
            
            userIdSet.add(userId);
        }

        return participantIds.subList(0, num);
    }

(2)抽样算法

想到以往学过的蓄水池抽样算法,场景乍看一看差不多,都是从一批数据中挑选几个。

但抽样算法主要应用于大数据场景下,所以这边只是简单提一下,有兴趣可以看下下面的链接: https://crunch.apache.org/apidocs/0.15.0/org/apache/crunch/lib/Sample.html#weightedReservoirSample-org.apache.crunch.PCollection-int-java.lang.Long-


(3)shuffle 算法

不就是随机嘛,我把每个都打乱,然后取前面几个作为中奖人即可。

此方法,避免了冲突。

如图:

简单步骤如下:

  1. 加载参与活动的人于内存中
  2. 遍历,每次 random,根据 random 后的数交换位置
  3. 获取名单中前几个人作为中奖人

shuffle 可以直接使用 JDK 提供的 java.util.Collections

代码如下:

    /**
     * 获取中奖名单
     *
     * @param num 奖品数量
     * @param participantIds 活动参与者
     * @return 中奖名单
     */
    private List<String> getWinnerIds(final Integer num, final List<String> participantIds) {

        if (num >= participantIds.size()) {

            return participantIds;
        }

        Collections.shuffle(participantIds);

        return participantIds.subList(0, num);
    }

(4)分区算法

在不产出冲突情况下,能再简单嘛?

思路:划分好分区,每个分区抽取一人

如图:

简单步骤如下:

  1. 加载参与活动的人于内存中
  2. 计算分区数(range),每个分区 random,这个分区中奖人下标 = random + offset 偏移位
  3. 根据下标获取中奖人员

有个弊端,就是如果奖品数量num 和 活动参与者participantIds 不能整除的话,就会出现其中一个分区的中奖率提升。

代码如下:

    /**
     * 获取中奖名单
     * 
     * @param num 奖品数量
     * @param participantIds 活动参与者
     * @return 中奖名单
     */
    private List<String> getWinnerIds(final Integer num, final List<String> participantIds) {

        if (num >= participantIds.size()) {

            return participantIds;
        }

        int offset = 0;

        int range = participantIds.size() / num;

        List<Integer> indexList = new ArrayList<>(num);

        Random rand = new Random();

        for (int i = 0; i < num; ++i) {

            int random = rand.nextInt(range);

            indexList.add(offset + random);

            offset += range;
        }

        return indexList.stream().map(participantIds::get).collect(Collectors.toList());
    }

PS:相比其他几种,更喜欢这种,可操作性高又简单。