微信紅包的核心演算法業界公認採用**「二倍均值法」**。本文深度解析其數學原理、Java 完整實作、邊界條件處理、公平性驗證與擴展優化方案。
設剩餘總金額為 M,剩餘紅包數為 N,則**當前平均值** avg = M / N。
下一個紅包金額 X 在區間 (0, 2 * avg) 內**均勻隨機**取值:
X ~ Uniform(0, 2 * avg)
E[X] = avg,長期期望下每人分得金額相等。σ = avg / √3 ≈ 0.577 * avg,適度波動製造「手氣最佳」樂趣。2*avg 確保單包不會佔據過多餘額,留給後續參與者機會。M 和 N 更新,avg 自動重算,適應剩餘狀態。當 N = 1 時,直接分配剩餘所有金額 M,確保總額精確吻合 (解決浮點誤差累積)。
package com.example.redpacket;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
public final class RedPacketAlgorithm {
private RedPacketAlgorithm() {}
/**
* 核心演算法:二倍均值法 (使用 BigDecimal 避免浮點誤差)
* @param totalAmount 總金額 (元,保留兩位小數)
* @param count 紅包個數
* @return 每個紅包金額列表 (元,BigDecimal)
*/
public static List divide(BigDecimal totalAmount, int count) {
// 參數驗證
if (totalAmount == null || totalAmount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("總金額必須大於 0");
}
if (count <= 0) {
throw new IllegalArgumentException("紅包個數必須大於 0");
}
// 最小金額限制:0.01 元
BigDecimal minAmount = new BigDecimal("0.01");
if (totalAmount.compareTo(minAmount.multiply(BigDecimal.valueOf(count))) < 0) {
throw new IllegalArgumentException("總金額不足以分配 " + count + " 個最小紅包 (0.01元)");
}
List packets = new ArrayList<>(count);
BigDecimal remainingAmount = totalAmount;
int remainingCount = count;
Random random = ThreadLocalRandom.current(); // 執行緒安全、高效能
for (int i = 0; i < count; i++) {
remainingCount = count - i;
// 最後一包:直接分配剩餘
if (remainingCount == 1) {
packets.add(remainingAmount.stripTrailingZeros());
break;
}
// 計算當前平均值
BigDecimal avg = remainingAmount.divide(BigDecimal.valueOf(remainingCount), 10, RoundingMode.HALF_UP);
// 上限 = 2 * 平均值
BigDecimal maxAmount = avg.multiply(BigDecimal.valueOf(2));
// 隨機生成 [0.01, maxAmount] 區間金額
// 為了避免生成 0,最小設為 0.01
BigDecimal min = minAmount;
// 確保上限不超過剩餘金額 - (剩餘包數-1)*0.01 (保留後續最小額度)
BigDecimal maxPossible = remainingAmount.subtract(minAmount.multiply(BigDecimal.valueOf(remainingCount - 1)));
if (maxAmount.compareTo(maxPossible) > 0) {
maxAmount = maxPossible;
}
// 生成隨機金額:min + random * (max - min)
BigDecimal range = maxAmount.subtract(min);
// 使用 scale=2 的隨機數 (分為單位)
int minCents = min.movePointRight(2).intValueExact();
int maxCents = maxAmount.movePointRight(2).intValueExact();
if (minCents > maxCents) { // 邊界保護
packets.add(min);
remainingAmount = remainingAmount.subtract(min);
continue;
}
int randomCents = random.nextInt(maxCents - minCents + 1) + minCents;
BigDecimal packet = new BigDecimal(randomCents).movePointLeft(2);
packets.add(packet.stripTrailingZeros());
remainingAmount = remainingAmount.subtract(packet);
}
return packets;
}
/** 便利方法:double 版本 (內部轉 BigDecimal) */
public static List divide(double totalAmount, int count) {
return divide(BigDecimal.valueOf(totalAmount), count)
.stream()
.map(BigDecimal::doubleValue)
.toList();
}
/** 驗證總和精確等於原金額 */
public static boolean validateSum(List packets, BigDecimal expected) {
BigDecimal sum = packets.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
return sum.stripTrailingZeros().compareTo(expected.stripTrailingZeros()) == 0;
}
}
import org.junit.Test;
import java.math.BigDecimal;
import java.util.List;
import static org.junit.Assert.*;
public class RedPacketAlgorithmTest {
@Test
public void testBasicDivision() {
List packets = RedPacketAlgorithm.divide(new BigDecimal("100.00"), 10);
assertEquals(10, packets.size());
assertTrue(RedPacketAlgorithm.validateSum(packets, new BigDecimal("100.00")));
for (BigDecimal p : packets) {
assertTrue(p.compareTo(new BigDecimal("0.01")) >= 0);
}
}
@Test
public void testEdgeCases() {
// 最小金額剛好
List packets = RedPacketAlgorithm.divide(new BigDecimal("0.10"), 10);
assertEquals(10, packets.size());
for (BigDecimal p : packets) {
assertEquals(new BigDecimal("0.01"), p);
}
// 單個紅包
packets = RedPacketAlgorithm.divide(new BigDecimal("50.00"), 1);
assertEquals(1, packets.size());
assertEquals(new BigDecimal("50.00"), packets.get(0));
}
@Test
public void testStatisticalFairness() {
// 大樣本驗證期望值接近均值
int trials = 10000;
int count = 5;
BigDecimal total = new BigDecimal("100.00");
BigDecimal expectedAvg = total.divide(BigDecimal.valueOf(count), 2, RoundingMode.HALF_UP); // 20.00
double[] sums = new double[count];
for (int t = 0; t < trials; t++) {
List packets = RedPacketAlgorithm.divide(total, count);
for (int i = 0; i < count; i++) {
sums[i] += packets.get(i).doubleValue();
}
}
for (int i = 0; i < count; i++) {
double actualAvg = sums[i] / trials;
// 允許 5% 誤差範圍
assertEquals(expectedAvg.doubleValue(), actualAvg, expectedAvg.doubleValue() * 0.05);
}
}
}
| 問題 | 原因 | 解決方案 |
|---|---|---|
| 浮點數誤差導致總和不等 | double 二進位無法精確表示 0.01 | **全程使用 BigDecimal**,最後驗證總和 |
| 生成 0 元紅包 | random.nextDouble() 可能極小 | 設定最小值 0.01 (分為單位整數隨機) |
| 最後幾包金額異常大/小 | 未保留後續最小額度 | 上限取 min(2*avg, 剩餘 - (剩餘包數-1)*0.01) |
| 高併發下 Random 競爭 | java.util.Random 使用 CAS 更新種子 | 使用 **ThreadLocalRandom** (Java 7+) |
| 金額精度超過兩位小數 | 除法產生無限循環小數 | BigDecimal 設定 scale=2, RoundingMode.HALF_UP |
二倍均值法產生均勻分佈,實際微信紅包傾斜度更高 (少數大額、多數小額)。可引入 LogNormal 分佈:
// 簡化版:生成對數正態隨機數 double logNormal = Math.exp(ThreadLocalRandom.current().nextGaussian() * sigma + mu); // 再歸一化到總金額
LPOP 直接取出,無運算延遲。public class RedPacketDemo {
public static void main(String[] args) {
BigDecimal total = new BigDecimal("200.00");
int count = 20;
System.out.println("=== 微信紅包模擬: " + total + " 元, " + count + " 個 ===");
List packets = RedPacketAlgorithm.divide(total, count);
BigDecimal sum = BigDecimal.ZERO;
for (int i = 0; i < packets.size(); i++) {
BigDecimal p = packets.get(i);
sum = sum.add(p);
System.out.printf("第 %2d 包: %6.2f 元 (剩餘: %.2f)%n",
i+1, p, total.subtract(sum));
}
System.out.println("總計驗證: " + (sum.stripTrailingZeros().compareTo(total.stripTrailingZeros()) == 0 ? "✅ 通過" : "❌ 失敗"));
// 統計
BigDecimal max = packets.stream().max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
BigDecimal min = packets.stream().min(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
BigDecimal avg = total.divide(BigDecimal.valueOf(count), 2, RoundingMode.HALF_UP);
System.out.printf("統計: 最大=%.2f, 最小=%.2f, 平均=%.2f, 理論平均=%.2f%n", max, min,
sum.divide(BigDecimal.valueOf(count), 2, RoundingMode.HALF_UP), avg);
}
}
二倍均值法以簡潔的數學期望保證公平性,配合動態上限控制極端值,是紅包演算法的經典實作。**生產環境必須使用 BigDecimal 處理金額、ThreadLocalRandom 保證併發效能、完善邊界條件防護**。理解其統計特性,可進一步擴展為更貼近真實分佈的進階演算法。