一、概述
应用场景:抽奖活动
开奖,可以是手动开奖和定时开奖。
抽奖这种活动需要尽可能的公平。
算法分为三个部分:
-
Input
输出输入参数:活动参与者列表、奖品数
-
Process
处理选择出中奖人
-
Output
输出中奖名单
二、抽奖算法
想到的算法有:
- 所有数
random
- 抽样算法
shuffle
算法- 分区算法
(1)所有数 random
此为暴力方法,也是比较容易想到的:随机抽取若干人作为中奖人。
但此问题会产生冲突。
简单步骤如下:
- 加载参与活动的人于内存中
random
一人作为中奖人,加入中奖名单result
- 若中奖人有重复,则继续步骤二
代码如下:
/**
* 获取中奖名单
*
* @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
算法
不就是随机嘛,我把每个都打乱,然后取前面几个作为中奖人即可。
此方法,避免了冲突。
如图:
简单步骤如下:
- 加载参与活动的人于内存中
- 遍历,每次
random
,根据random
后的数交换位置 - 获取名单中前几个人作为中奖人
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)分区算法
在不产出冲突情况下,能再简单嘛?
思路:划分好分区,每个分区抽取一人
如图:
简单步骤如下:
- 加载参与活动的人于内存中
- 计算分区数(
range
),每个分区random
,这个分区中奖人下标 =random
+offset
偏移位 - 根据下标获取中奖人员
有个弊端,就是如果奖品数量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:相比其他几种,更喜欢这种,可操作性高又简单。