在Java开发中,加权随机选择是指根据每个候选项预设的权重值,按照权重比例随机返回对应结果的功能,广泛应用于抽奖、灰度发布、负载均衡等场景。实现这类功能的核心是建立权重与随机区间的映射关系,确保高权重项被选中的概率更大。

加权随机选择的核心原理
加权随机的本质是将所有候选项的权重累加得到总权重,然后生成一个[0, 总权重)区间的随机数,最后判断该随机数落在哪个候选项的权重区间内,对应的候选项就是选中结果。假设有三个选项A、B、C,权重分别为2、3、5,总权重为10,那么A的区间是[0,2),B的区间是[2,5),C的区间是[5,10),随机数落在哪个区间就返回对应选项。
基础实现方案
首先定义带权重的候选项实体类,用来存储选项本身和对应的权重值:
public class WeightedItem<T> {
// 候选项内容
private T item;
// 对应权重
private int weight;
public WeightedItem(T item, int weight) {
this.item = item;
this.weight = weight;
}
public T getItem() {
return item;
}
public int getWeight() {
return weight;
}
}
接下来实现基础的加权随机选择器,支持传入候选项列表并返回随机选择的结果:
import java.util.List;
import java.util.Random;
public class WeightedRandomSelector<T> {
private List<WeightedItem<T>> itemList;
private int totalWeight;
private Random random;
public WeightedRandomSelector(List<WeightedItem<T>> itemList) {
this.itemList = itemList;
this.random = new Random();
// 计算总权重
this.totalWeight = 0;
for (WeightedItem<T> weightedItem : itemList) {
if (weightedItem.getWeight() <= 0) {
throw new IllegalArgumentException("权重必须大于0");
}
totalWeight += weightedItem.getWeight();
}
}
public T select() {
// 生成[0, totalWeight)的随机数
int randomValue = random.nextInt(totalWeight);
int currentWeight = 0;
// 遍历查找随机数所在的区间
for (WeightedItem<T> weightedItem : itemList) {
currentWeight += weightedItem.getWeight();
if (randomValue < currentWeight) {
return weightedItem.getItem();
}
}
// 理论上不会走到这里,兜底返回最后一个元素
return itemList.get(itemList.size() - 1).getItem();
}
}
使用示例
下面演示如何使用上述加权随机选择器实现抽奖功能,假设有三个奖品,权重分别为1、2、3:
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
List<WeightedItem<String>> prizeList = new ArrayList<>();
prizeList.add(new WeightedItem<>("一等奖", 1));
prizeList.add(new WeightedItem<>("二等奖", 2));
prizeList.add(new WeightedItem<>("三等奖", 3));
WeightedRandomSelector<String> selector = new WeightedRandomSelector<>(prizeList);
// 模拟1000次抽奖统计概率
int firstCount = 0;
int secondCount = 0;
int thirdCount = 0;
for (int i = 0; i < 1000; i++) {
String result = selector.select();
switch (result) {
case "一等奖":
firstCount++;
break;
case "二等奖":
secondCount++;
break;
case "三等奖":
thirdCount++;
break;
}
}
System.out.println("一等奖选中次数:" + firstCount + ",概率约:" + (firstCount / 1000.0));
System.out.println("二等奖选中次数:" + secondCount + ",概率约:" + (secondCount / 1000.0));
System.out.println("三等奖选中次数:" + thirdCount + ",概率约:" + (thirdCount / 1000.0));
}
}
优化方案:预处理前缀和
上面的基础实现每次调用select()方法都需要遍历列表累加权重,如果候选项数量很多,频繁调用的场景下性能会受影响。可以提前计算好每个候选项的前缀和,后续查找时使用二分查找提升效率:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class OptimizedWeightedRandomSelector<T> {
private List<T> itemList;
private List<Integer> prefixSumList;
private Random random;
public OptimizedWeightedRandomSelector(List<WeightedItem<T>> items) {
this.itemList = new ArrayList<>();
this.prefixSumList = new ArrayList<>();
this.random = new Random();
int sum = 0;
for (WeightedItem<T> item : items) {
if (item.getWeight() <= 0) {
throw new IllegalArgumentException("权重必须大于0");
}
sum += item.getWeight();
itemList.add(item.getItem());
prefixSumList.add(sum);
}
}
public T select() {
int randomValue = random.nextInt(prefixSumList.get(prefixSumList.size() - 1));
// 二分查找前缀和数组中第一个大于randomValue的位置
int left = 0;
int right = prefixSumList.size() - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (prefixSumList.get(mid) <= randomValue) {
left = mid + 1;
} else {
right = mid;
}
}
return itemList.get(left);
}
}
注意事项
- 权重值必须为正整数,避免出现概率为0或者负数的情况,否则会导致概率计算错误。
- 如果候选项数量很少且调用频率不高,基础遍历实现就足够使用,不需要额外引入二分查找增加复杂度。
- 如果需要支持动态调整权重,需要重新计算总权重或者前缀和,避免数据和实际权重不匹配。
- 随机数生成建议使用
ThreadLocalRandom代替Random,在高并发场景下可以减少竞争,提升性能。
适用场景说明
这种通用加权随机选择器可以适配大部分需要按概率分配的场景,比如抽奖系统中不同奖品的分配、灰度发布中不同版本的用户分配、负载均衡中不同节点的请求分配等。开发者只需要根据业务需求定义好候选项和对应的权重,就可以直接复用上述实现,不需要重复编写概率计算逻辑。