抽奖算法中,O(1)查表法内存随精度指数级增长(亿分位达400MB),O(logn)二分法内存与奖品数量线性相关。当奖品数远大于精度时,二分法更省内存;反之查表法占优。需根据概率范围与奖品数量的比值权衡时空开销。
| 精度 | rateRange | 数组长度 | int类型占用 | 总内存 |
|---|---|---|---|---|
| 万分位 | 10,000 | 10,000 | 4 bytes | 40 KB |
| 十万分位 | 100,000 | 100,000 | 4 bytes | 400 KB |
| 百万分位 | 1,000,000 | 1,000,000 | 4 bytes | 4 MB |
| 千万分位 | 10,000,000 | 10,000,000 | 4 bytes | 40 MB |
| 亿分位 | 100,000,000 | 100,000,000 | 4 bytes | 400 MB |
从这张表可以清晰地看到,内存从 40KB 增长到 400MB,呈现出指数级增长趋势。当精度达到亿分位时,仅这一个数组就需要占用 400MB 内存,这在大多数生产环境中都是无法接受的。
场景1:100个奖品,万分位精度
长期稳定更新的攒劲资源: >>>点此立即查看<<<
rateRange = 10000
awardCount = 100
slotsPerAward = 100 // 每个奖品100个槽位
内存占用 = 10000 × 4 bytes = 40 KB 可接受
场景2:100个奖品,十万分位精度
rateRange = 100000
awardCount = 100
slotsPerAward = 1000 // 每个奖品1000个槽位
内存占用 = 100000 × 4 bytes = 400 KB 可接受,但增长明显
场景3:100个奖品,百万分位精度
rateRange = 1000000
awardCount = 100
slotsPerAward = 10000 // 每个奖品10000个槽位
内存占用 = 1000000 × 4 bytes = 4 MB 开始需要注意
场景4:1000个奖品,十万分位精度
rateRange = 100000
awardCount = 1000
slotsPerAward = 100 // 每个奖品100个槽位
内存占用 = 100000 × 4 bytes = 400 KB 可接受
场景5:1000个奖品,百万分位精度
rateRange = 1000000
awardCount = 1000
slotsPerAward = 1000 // 每个奖品1000个槽位
内存占用 = 1000000 × 4 bytes = 4 MB 内存占用较大
几个场景走下来,规律非常明显:当奖品数量固定时,精度每提升一个量级,内存就跟着翻倍。这就引出了另一个问题——当奖品数量本身也变得很大时,又该如何处理?
// 存储随机数到奖品的映射
int[] awardMappingArray = new int[rateRange]; // 固定大小数组
// 例如:rateRange = 1000000
// 内存 = 1000000 × 4 bytes = 4 MB
特点:
// 存储奖品信息和前缀和
List awards = new ArrayList<>(); // 奖品列表
double[] prefixSums = new double[awardCount]; // 前缀和数组
// 例如:awardCount = 1000
// 内存 = 1000 × (award对象大小 + 8 bytes) ≈ 100 KB
特点:
| 对比项 | O(1) 数组算法 | O(logn) 前缀和算法 |
|---|---|---|
| 内存占用 | O(rateRange) | O(awardCount) |
| 万分位(10K奖品) | 40 KB | ~1 MB(奖品对象) |
| 十万位(1K奖品) | 400 KB | ~100 KB |
| 百万位(100奖品) | 4 MB | ~10 KB |
| 关系 | 与精度成正比 | 与奖品数量成正比 |
重要结论:
rateRange >> awardCount 时,O(logn) 更节省内存
rateRange ≈ awardCount 时,两者内存相近
典型场景:
万分位 + 100奖品: rateRange(10000) > awardCount(100) → O(1)更省内存
万分位 + 1万奖品: rateRange(10000) ≈ awardCount(10000) → 差不多
万分位 + 10万奖品: rateRange(10000) < awardCount(100000) → O(logn)更省内存
这里有一个容易被忽视的反直觉点:当奖品数量远大于精度时,O(1)算法反而可能比O(logn)更费内存。比如万分位下有10万个奖品,O(1)只有40KB,但O(logn)因为要存储10万个对象,内存可能会达到MB级别。所以,选择的关键在于对比“精度”和“奖品数量”这两个量级。
> 内存占用
> ↑
> │ O(1)数组算法
> │ /
> │ /
> │ /
> │ /
> │ /
> │ /
> │ /
> │ /
> │ /
> │ /
> │ /
> │ /
> │ /
> │ /
> │/
> └─────────────────────────────────→ 精度(rateRange)
> 低 中 高 非常高
> O(logn)算法内存几乎不变
这张图把问题说得很透彻:O(1)算法的内存成本随着精度提升一路飙升,而O(logn)算法则像一条水平线,几乎不受精度影响。这就是经典的“空间换时间”与“时间换空间”的取舍。
| 操作 | O(1) 数组算法 | O(logn) 前缀和算法 |
|---|---|---|
| 预热 | O(rateRange) | O(awardCount × log awardCount) |
| 单次抽奖 | O(1) | O(log awardCount) |
| 空间 | O(rateRange) | O(awardCount) |
从时间复杂度来看,O(1)在单次查询上具有绝对优势,但其预热开销也更大。如果系统需要频繁预热或动态调整奖品配置,那么O(1)的预热成本可能会成为瓶颈。
| 场景 | rateRange | awardCount | 推荐算法 | 原因 |
|---|---|---|---|---|
| 1 | 10,000 | 100 | O(1) | 内存小(40KB),查询快 |
| 2 | 100,000 | 100 | O(1) | 内存可接受(400KB),查询快 |
| 3 | 1,000,000 | 100 | O(1) | 内存较大(4MB),但奖品少 |
| 4 | 10,000 | 10,000 | 两者皆可 | 内存相近,看查询频率 |
| 5 | 10,000 | 100,000 | O(logn) | O(1)内存太大(40MB) |
| 6 | 100,000 | 1,000 | O(logn) | O(1)内存太大(400KB) |
| 7 | 1,000,000 | 1,000 | O(logn) | O(1)内存太大(4MB) |
这份决策矩阵可以当作一张参考清单。在实际项目中,建议先画出自己系统的“rateRange”和“awardCount”坐标点,然后直接在表里查找对应的推荐方案,效率会高很多。
/**
* O(1)抽奖算法 - 预热时生成固定大小数组
* 优点:查询时间O(1)
* 缺点:内存占用与rateRange成正比
*/
public Integer raffleStrategyO1(Long strategyId,
List strategyAwardEntities) {
// 1. 获取精度范围
BigDecimal minAwardRate = minAwardRate(strategyAwardEntities);
int rateRange = convert(minAwardRate); // 例如:10000, 100000, 1000000
// 2. 分配数组(内存占用 = rateRange × 4 bytes)
int[] strategyAwardRateRandom = new int[rateRange];
// 3. 填充数组
int currentIndex = 0;
for (StrategyAwardEntity award : strategyAwardEntities) {
// 计算该奖品应该占用的槽位数
int awardSlots = (int) (award.getAwardRate() * rateRange);
// 填充槽位
for (int i = 0; i < awardSlots; i++) {
if (currentIndex >= rateRange) break;
strategyAwardAwardRateRandom[currentIndex++] = award.getAwardId();
}
}
// 4. 随机打乱(消除初始顺序偏差)
shuffle(strategyAwardAwardRateRandom);
// 5. 抽奖(O(1)时间复杂度)
int randomIndex = ThreadLocalRandom.current().nextInt(rateRange);
return strategyAwardAwardRateRandom[randomIndex];
}
/**
* O(logn)抽奖算法 - 实时计算前缀和
* 优点:内存占用与awardCount成正比,与rateRange无关
* 缺点:查询时间O(logn),需要每次计算
*/
public Integer raffleStrategyLogn(Long strategyId,
List strategyAwardEntities) {
// 1. 计算最小精度(用于确定随机数范围)
BigDecimal minAwardRate = minAwardRate(strategyAwardEntities);
int rateRange = convert(minAwardRate); // 例如:100000, 1000000
// 2. 构建前缀和数组(内存占用 = awardCount × 8 bytes)
double[] awardRates = new double[strategyAwardEntities.size()];
double[] prefixSums = new double[strategyAwardEntities.size()];
for (int i = 0; i < strategyAwardEntities.size(); i++) {
StrategyAwardEntity award = strategyAwardEntities.get(i);
awardRates[i] = award.getAwardRate();
if (i == 0) {
prefixSums[i] = awardRates[i];
} else {
prefixSums[i] = prefixSums[i - 1] + awardRates[i];
}
}
// 3. 生成随机数
int randomValue = ThreadLocalRandom.current().nextInt(rateRange);
double randomRate = (double) randomValue / rateRange;
// 4. 二分查找(O(logn)时间复杂度)
int left = 0;
int right = prefixSums.length - 1;
while (left < right) {
int mid = (left + right) / 2;
if (prefixSums[mid] < randomRate) {
left = mid + 1;
} else {
right = mid;
}
}
return strategyAwardEntities.get(left).getAwardId();
}
/**
* 综合考虑槽位数和内存占用的算法选择
*/
public Integer raffleStrategy(Long strategyId,
List strategyAwardEntities) {
// 1. 计算精度范围
BigDecimal minAwardRate = minAwardRate(strategyAwardEntities);
int rateRange = convert(minAwardRate); // 10000, 100000, 1000000...
int awardCount = strategyAwardEntities.size();
int slotsPerAward = rateRange / awardCount;
// 2. 计算内存占用
long o1MemoryBytes = (long) rateRange * 4; // O(1)数组内存
long lognMemoryBytes = (long) awardCount * 40; // O(logn)奖品对象内存估算
// 3. 算法选择条件
// 条件1:槽位数 >= 10(保证公平性)
// 条件2:内存占用可接受(O(1)算法不超过10MB)
if (slotsPerAward >= 10 && o1MemoryBytes <= 10 * 1024 * 1024) {
// 使用O(1)算法
return raffleStrategyO1(strategyId, strategyAwardEntities);
} else {
// 使用O(logn)算法
return raffleStrategyLogn(strategyId, strategyAwardEntities);
}
}
最后的这个综合选择函数很实用。它兼顾了公平性(槽位数不少于10)和内存开销(O(1)数组小于10MB),相当于在运行时做一次动态决策。这种思路比硬编码一种方案要灵活得多。
/**
* 内存优化:如果rateRange非常大,使用压缩算法
*/
public Integer raffleStrategyCompressed(Long strategyId,
List strategyAwardEntities) {
BigDecimal minAwardRate = minAwardRate(strategyAwardEntities);
int rateRange = convert(minAwardRate);
// 如果rateRange > 1000000,使用Run-Length Encoding压缩
if (rateRange > 1000000) {
return raffleStrategyCompressed(strategyId, strategyAwardEntities);
}
// 正常O(1)算法
return raffleStrategyO1(strategyId, strategyAwardEntities);
}
/**
* Run-Length Encoding压缩存储
* 例如:[A,A,A,B,B,C,C,C,C] → [(A,3), (B,2), (C,4)]
*/
class CompressedArray {
int[] awardIds; // 奖品ID数组(去重后的)
int[] runLengths; // 每个奖品连续出现的次数
// 随机抽奖
public int raffle() {
// 1. 计算总长度
int totalLength = Arrays.stream(runLengths).sum();
// 2. 生成随机位置
int randomPosition = ThreadLocalRandom.current().nextInt(totalLength);
// 3. 查找对应的奖品
int currentPosition = 0;
for (int i = 0; i < runLengths.length; i++) {
currentPosition += runLengths[i];
if (randomPosition < currentPosition) {
return awardIds[i];
}
}
return awardIds[awardIds.length - 1];
}
}
/**
* 分级缓存策略:根据访问频率选择不同的存储方式
*/
public class TieredStrategyCache {
// 热数据:高频访问的奖品,使用O(1)数组
private int[] hotAwardsArray;
// 冷数据:低频访问的奖品,使用O(logn)列表
private List coldAwardsList;
// 访问统计
private Map accessCountMap = new ConcurrentHashMap<>();
// 定期调整热冷数据
public void rebalance() {
// 统计访问频率
Map sortedAwards = accessCountMap.entrySet().stream()
.sorted(Map.Entry.comparingByValue())
.limit(100) // 取访问最多的100个奖品
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(e1, e2) -> e1,
LinkedHashMap::new
));
// 将高频奖品放入热数据
rebuildHotAwardsArray(sortedAwards.keySet());
}
}
/**
* 懒加载策略:只有首次访问时才加载数据
*/
public class LazyStrategyCache {
private volatile int[] cachedArray = null;
private volatile List cachedAwards = null;
// 懒加载O(1)数组
public int[] getO1Array(List awards) {
if (cachedArray == null) {
synchronized (this) {
if (cachedArray == null) {
// 只在首次访问时计算
cachedArray = buildStrategyArray(awards);
}
}
}
return cachedArray;
}
// 懒加载O(logn)列表
public List getAwardsList() {
if (cachedAwards == null) {
synchronized (this) {
if (cachedAwards == null) {
cachedAwards = loadAwardsFromDB();
}
}
}
return cachedAwards;
}
}
这几类优化方案各有侧重:压缩存储解决了“数组太大”的问题,分级缓存解决了“所有奖品一视同仁”的问题,懒加载则解决了“系统启动就吃内存”的问题。在实际项目中,往往需要根据自身特点组合使用。
O(1)算法内存 = rateRange × 4 bytes
rateRange越大,内存占用越大
O(logn)算法内存 = awardCount × 奖品对象大小
与rateRange无关,只与奖品数量有关
| 因素 | O(1)算法 | O(logn)算法 |
|---|---|---|
| 内存 | 与精度成正比 | 与奖品数量成正比 |
| 时间 | O(1)快 | O(logn)稍慢 |
| 公平性 | 依赖槽位数 | 实时计算,更公平 |
| 复杂度 | 实现简单 | 需要前缀和+二分查找 |
说到底,算法选择从来不是一个纯粹的技术问题,而是一个工程权衡问题。你需要根据自己系统的精度要求、奖品规模、查询频率、硬件资源等多个维度,找到那个最合适的平衡点。数据不会说谎,把上面的决策矩阵跑一遍,答案自然就出来了。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述