基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

如何设计微信红包的金额分配算法?

知识点图片

设计微信红包的金额分配算法是一个非常经典的面试题,也是一个有趣的数学与工程结合的问题。

设计这个算法的核心目标有以下几点:

  1. 随机性:每个人抢到的金额需要随机。
  2. 总额匹配:所有人的金额加起来必须严格等于总金额。
  3. 最小限制:每个人至少要抢到 0.01 元。
  4. 公平性(心理上):虽然是随机,但不能让先抢的人把钱全抢光了,也不能让大部分人都只拿 0.01 元。
  5. 性能与并发:微信红包的并发量极大,计算必须简单高效。

目前业界公认微信采用的是 “二倍均值法” (Double Mean Method)


1. 核心算法:二倍均值法

基本思路
每次分配金额时,剩余金额为 MM,剩余人数为 NN
那么,当前剩余的人均金额M/NM/N
算法规定,生成的随机金额范围是 [0.01,MN×2)[0.01, \frac{M}{N} \times 2)

公式
本次抢到的金额=Random(0.01,剩余金额剩余人数×2)\text{本次抢到的金额} = \text{Random}(0.01, \frac{\text{剩余金额}}{\text{剩余人数}} \times 2)

为什么要乘 2?
在数学上,在 [0,K][0, K] 之间均匀分布的随机数,其数学期望(平均值)是 K/2K/2
如果我们希望生成的随机数期望值等于“当前的人均金额” (M/NM/N),那么随机范围的上限 KK 就应该是人均金额的 2 倍。

这样设计的好处是:无论你是第几个来抢红包的,从统计学期望上来看,你抢到的钱都差不多是人均金额。

举例推演

假设总金额 100 元,分给 10 个人。

  1. 第1人
    • 剩余金额 100,剩余人数 10。
    • 均值 = 10。
    • 随机范围 = [0.01,20)[0.01, 20)
    • 假设随机到了 12 元
  2. 第2人
    • 剩余金额 88,剩余人数 9。
    • 均值 = 9.77。
    • 随机范围 = [0.01,19.55)[0.01, 19.55)
    • 假设随机到了 4 元
  3. 第3人
    • 剩余金额 84,剩余人数 8。
    • 均值 = 10.5。
    • 随机范围 = [0.01,21)[0.01, 21)
    • ...以此类推。

注意:最后一个人不需要计算,直接拿走剩下的所有钱。


2. 代码实现 (Python 示例)

在工程实现中,绝对不能使用浮点数 (float/double) 进行金额计算,因为会有精度丢失问题(例如 0.1 + 0.2 != 0.3)。
必须将金额转换为“分” (整数) 进行计算。

python
import random

def allocate_red_packet(total_amount, total_people):
    """
    :param total_amount: 总金额(单位:元)
    :param total_people: 总人数
    :return: 分配金额列表(单位:元)
    """
    # 1. 转换为“分”进行整数计算
    rest_amount = int(total_amount * 100)
    rest_people = total_people
    result = []

    # 2. 异常检查
    if rest_amount < rest_people * 1:
        raise ValueError("总金额不足以支付每人1分钱")

    # 3. 循环分配(前 N-1 个人)
    for i in range(total_people - 1):
        # 核心算法:二倍均值法
        # 随机范围:[1, 剩余人均 * 2 - 1]
        # 为什么要 -1?因为要保证范围是开区间或者留余地,这里简化逻辑
        
        # 计算最大可用金额:
        # 必须给后面剩下的人留至少每人1分钱
        # max_val = 剩余金额 - (剩余人数-1) * 1
        # 算法限制上限 = (剩余金额 / 剩余人数) * 2
        
        avg = rest_amount / rest_people
        upper_bound = int(avg * 2)
        
        # 保证随机上限不低于1分,且不超过剩余总额(虽然二倍均值通常不会超,但要防止极端情况)
        # 关键点:如果 upper_bound 很小(比如剩下钱很少),至少要能随到1
        upper_bound = max(1, upper_bound)

        # 生成随机金额
        # random.randint(a, b) 是包含 b 的,所以这里要注意边界
        amount = random.randint(1, upper_bound)
        
        # 修正:必须保证剩下的钱够后面的人分
        # 如果这次随多了,导致剩下的钱不够每人1分,就要截断
        min_remaining = (rest_people - 1) * 1
        if rest_amount - amount < min_remaining:
            amount = rest_amount - min_remaining

        result.append(amount)
        rest_amount -= amount
        rest_people -= 1

    # 4. 最后一个人拿走剩下的
    result.append(rest_amount)

    # 5. 将“分”转回“元”
    return [x / 100.0 for x in result]

# 测试
amounts = allocate_red_packet(100, 10)
print(f"分配结果: {amounts}")
print(f"总金额验证: {sum(amounts)}")

3. 另一种思路:线段切割法 (Line Segment Cutting)

虽然二倍均值法是主流,但它有一个特点:方差随着人数减少而变小。也就是说,第一个人抢到的金额波动很大(可能极多也可能极少),而最后几个人抢到的金额通常会在平均值附近。

如果你想要一种“完全随机、每个人概率分布完全一致”的算法,可以使用线段切割法

思路

  1. 把总金额 MM 想象成一条长为 MM 的线段。
  2. 要在 NN 个人中分,就需要在绳子上切 N1N-1 刀。
  3. [0,M][0, M] 之间随机产生 N1N-1 个不重复的切割点。
  4. 对切割点进行排序。
  5. 计算相邻两个点之间的距离,即为每个人的金额。

缺点
这种算法虽然数学上分布更均匀,但在工程上处理“每人至少 0.01 元”这个约束比较麻烦,往往需要反复生成或事后修正,效率不如二倍均值法高。


4. 系统架构层面的设计 (高并发处理)

在微信这种亿级流量的场景下,算法只是冰山一角,真正的难点在于数据一致性并发性能

实际流程通常是“预分配” (Pre-allocation):

  1. 发红包时

    • 用户发出 100 元红包给 10 个人。
    • 后端服务器立刻运行上述算法,计算出 10 个金额:[12.3, 5.4, ..., 8.1]
    • 将这 10 个数字打乱顺序(Shuffle),存入 Redis 的 List (队列) 中。
    • 同时在数据库记录一条红包记录。
  2. 抢红包时

    • 大量用户并发点击“开”。
    • 请求到达服务器,服务器直接对 Redis 的 List 执行 LPOP (左弹出) 操作。
    • 因为 Redis 是单线程原子操作,所以天然线程安全,不会超发。
    • LPOP 拿到多少钱,用户就得多少钱。
    • 如果 LPOP 返回空,说明红包抢完了。
  3. 入账

    • 抢到红包后,异步写入数据库,给用户账户加钱。

这种架构的优点

  • 速度极快:抢红包时不需要进行复杂的计算,只是读内存队列。
  • 无锁:利用 Redis 原子性避免了数据库行锁。
  • 体验好:用户点击时几乎没有延迟。

总结

设计微信红包算法的三个层次:

  1. 数学层:使用 二倍均值法 保证期望值平等,兼顾趣味性。
  2. 数据层:使用 整数 (分) 运算,避免浮点数精度灾难。
  3. 架构层:使用 预生成 + Redis 队列 应对高并发,将计算压力转移到发红包阶段,让抢红包阶段极其轻量。
00:00
00:00