1. 简介

在很多场景中,生成随机数是非常有用的,例如游戏开发、统计分析、密码学等领域。在这些场景中,通常每个随机数的生成都是一个独立事件,与过去或未来的结果无关。比如掷骰子,每次掷出的数字都是独立的,不受之前结果的影响。

但在某些情况下,我们希望对随机数加以限制,确保生成的数值不会重复。例如模拟一副洗牌后的扑克牌、从播放列表中随机选择歌曲等,都需要每个元素只出现一次。

本文将介绍几种常见的实现方式,帮助你在不同场景下高效生成不重复的随机数。


2. 记录历史法

最直观的方法是记录所有已生成的数字,每次生成新数时都检查是否已存在:

algorithm RememberingHistory(maximum):
    // INPUT
    //    maximum = 随机数的最大值
    //    rng = 用于生成随机数的函数
    //    history = 已生成数字的集合
    // OUTPUT
    //    返回一个未出现过的随机数,并更新 history

    number <- rng(0, maximum)
    while number in history:
        number <- rng(0, maximum)
    
    history.add(number)
    return number

优点:实现简单,逻辑清晰
缺点:性能差,尤其是当已生成数字很多时,碰撞概率高,循环次数剧增
⚠️ **时间复杂度为 O(2n)**,当生成的数字接近最大值时效率急剧下降


3. 提前洗牌法

既然我们要的是一个不重复的随机序列,那我们可以先生成一个打乱顺序的数字列表,然后依次从中取出:

algorithm PreShufflingNumbers(maximum, shuffled):
    // INPUT
    //    maximum = 数字最大值
    //    shuffled = 打乱顺序后的列表
    // OUTPUT
    //    返回下一个随机数,并更新 shuffled

    if shuffled is empty:
        shuffled <- shuffle([1, 2, ..., maximum])
    
    return pop first element from shuffled

优点:每次取数时间复杂度为 O(1),性能稳定
缺点:初始化成本高,尤其当 maximum 很大但实际只需要取少量数字时
⚠️ 如果 maximum 是 2^32,初始化几乎不可行


4. 动态洗牌法(推荐)

结合前两种方法的优点,在生成过程中动态洗牌,无需预先生成完整列表:

algorithm ShufflingDuringGeneration(maximum):
    // INPUT
    //    maximum = 数字最大值
    //    numbers = [1, 2, ..., maximum]
    //    count = 当前可用数字的上限索引(初始为 maximum)
    // OUTPUT
    //    返回一个唯一的随机数

    index <- rng(0, count)
    count <- count - 1
    swap(numbers[index], numbers[count])
    return numbers[count]

这个算法的核心思想是:

  • 每次从当前可用数字中随机选一个
  • 将它交换到“已使用”区域的末尾
  • 返回这个数字,并缩小可用区域

示例流程图:

初始状态:

1_Screenshot-2022-05-24

第一次生成:

1_Screenshot-2022-05-24

第二次生成:

1_Screenshot-2022-05-24

后续生成示例:

1_Screenshot-2022-05-24

最终状态:

1_Screenshot-2022-05-24

优点:O(1) 时间复杂度,无初始化开销,内存占用合理
⚠️ 适合大多数中等规模的随机数需求


5. 更进一步:内存优化方案

前面的方法都需要一个数组来保存所有可能的数字,当 maximum 是 2^32 时,内存占用会达到 16GB,显然不可行。

这时候我们只能放弃“动态洗牌”法,考虑以下替代方案:

✅ 方案一:使用记录法(即“记录历史法”)

  • 仅记录已生成的数字
  • 内存消耗取决于生成数量,而不是最大值
  • 缺点:性能随生成数量增加而下降

✅ 方案二:使用伪随机函数 + 哈希

  • 例如:使用一个递增计数器 + 加密哈希(如 SHA-256)生成伪随机数
  • 可以保证唯一性(只要计数器不重复)
  • 缺点:伪随机性质,不能保证真正的随机性

⚠️ 这些方法都不能保证真正的“随机”和“唯一”同时成立,需要根据业务场景权衡


6. 总结

方法 时间复杂度 内存消耗 适用场景
记录历史法 O(n^2) 中等(随生成数增长) 小规模、低性能要求
提前洗牌法 O(1) + O(n) 初始化 中小规模、需高性能
动态洗牌法 O(1) 中等 推荐方案,通用性强
哈希计数法 O(1) 极低 大规模、伪随机可接受

📌 选择建议

  • 如果你只需要从 1~100 中随机取 10 个不重复数,用“记录历史法”最简单
  • 如果你需要从 1~10000 中取 5000 个不重复数,“动态洗牌法”是首选
  • 如果你要生成 32 位随机数且不重复,“哈希 + 计数器”是唯一可行方案

📌 最后提醒

  • 不要盲目追求“完全随机”,很多场景伪随机就足够
  • 如果你发现生成速度变慢,可能是你选错了算法,而不是随机函数的问题

希望你下次需要生成不重复随机数时,能快速想到这些方法中的一种。


原始标题:Non-repeating Random Number Generator