包括 Pick4Us 在内,大多数在线随机数生成器使用的是伪随机数生成器(PRNG):一个固定的算法,从一个看不见的“种子”出发,算出一串看起来随机、也能通过统计检验的数字。密码学安全的生成器在此基础上保证不可预测,硬件随机数生成器则直接测量物理噪声。做游戏、课堂点名和日常决定,一个好的伪随机算法完全够用;生成密码或进行受法规约束的抽签,就不够了。
三类随机数生成器
| 类型 | 随机性从哪里来 | 常见用途 |
|---|---|---|
| 伪随机数生成器(PRNG) | 对内部状态反复套用同一个公式,从种子开始计算 | 游戏、模拟、日常抽选(如 Math.random()) |
| 密码学安全伪随机数生成器(CSPRNG) | 同样是算法,但设计成看到再多输出也推算不出下一个,并以操作系统收集的熵作为种子 | 密码、密钥、安全令牌(如 crypto.getRandomValues()) |
| 硬件随机数生成器(“真随机”) | 测量电子噪声等物理过程 | 为 CSPRNG 提供种子、专用设备 |
用大白话讲种子和周期
- 状态与种子。伪随机算法内部保存着一小块“记忆”,叫作状态。每调用一次,算法就把状态打乱一次,并从中取出一部分作为输出。种子就是最初的状态:同一个种子永远得到同一串数字,这在需要重现的模拟实验或想原样重玩的游戏关卡里很有用。
- 周期。状态能取的值是有限的,所以数列迟早会重复,开始重复之前能产生的数字个数就叫周期。主流浏览器引擎实现 Math.random() 所用的 xorshift128+,周期是 2¹²⁸ − 1,约为 3.4 × 10³⁸,任何网站都不可能用到头。Python 标准库的 random 模块使用梅森旋转算法(Mersenne Twister),周期更是长达 2¹⁹⁹³⁷ − 1。
- 可预测性。统计质量好,不等于不可预测。快速的伪随机算法可以通过严格的成套统计检验,但由于算法是公开的、状态又不大,只要观察到足够多的输出,理论上就能反推出内部状态,进而预测后面所有的数字。
CSPRNG 正是为堵上这个缺口而设计的:即使手里有大量输出,猜下一位也不会比瞎猜更准。硬件随机数生成器测量的是物理过程,但原始噪声采集得慢,还要经过处理才能变得均匀,所以操作系统通常把它混入 CSPRNG 作为种子材料,而不是每次请求都直接读取。
Math.random() 与 crypto.getRandomValues()
浏览器里的 JavaScript 自带两个随机数来源。
Math.random() 返回一个大于等于 0、小于 1 的小数,分布大致均匀。ECMAScript 规范有意不规定具体算法,交给各个引擎自行选择;MDN 也说明,种子同样由引擎选定,用户无法指定或重置。
实际上,主流引擎在 2015 至 2016 年间都换成了 xorshift128+。Chrome 和 Node.js 使用的 V8 引擎从 4.9.41 版起采用这一算法,随 Chrome 49 发布,取代了此前在多项统计检验中表现不佳的 MWC1616;Firefox 的 SpiderMonkey 和 Safari 的 JavaScriptCore 也在同一时期做了同样的更换。引擎今后仍可能调整这些细节,规范本身也不保证任何特定算法。
crypto.getRandomValues() 会用密码学强度的随机值填满一个整数数组。MDN 解释说,出于性能考虑,浏览器并不是每次都去读取硬件随机源,而是使用以足够熵作为种子的密码学安全伪随机算法。单次调用最多填充 65,536 字节。它比 Math.random() 慢一些,但在人能感知的尺度上没有区别。
把随机字节换算到某个范围时,有个常见陷阱:直接取余数会带来小偏差。一个字节有 256 种取值,而 256 = 6 × 42 + 4,所以用“字节 % 6”时,256 个取值中各有 43 个会得到 0、1、2、3,却只各有 42 个会得到 4 和 5。严谨的代码会丢弃多出来的那几个值并重新抽取。
什么时候用 Math.random() 没问题,什么时候不行
适合使用 Math.random() 的场景:
- 游戏和聚会工具:掷骰子、转盘、决定谁先开始。
- 课堂抽选:随机点名、分组、安排汇报顺序(可参考我们的课堂随机点名指南)。
- 日常决定:晚饭吃什么、看哪部电影、朋友之间打破僵局。
- 轻量的模拟和概率教学。
不适合的场景:
- 密码、PIN 码、验证码、API 密钥、会话令牌或重置密码的令牌。请使用 crypto.getRandomValues() 或密码管理器。
- 受法规约束的抽签,例如官方有奖抽奖、入学或住房摇号。这类活动往往对抽取方式、见证人和记录有法律规定,请按当地规则办理。浏览器里的小工具不是经过审计的抽签系统。
- 任何有人能通过预测结果而获利的场合。
Pick4Us 的所有工具都使用 Math.random(),这适合快速的日常抽选,网站也无法预设结果。它没有使用 crypto.getRandomValues(),也不宣称“真随机”。
如何抽出不重复的数字
如果允许重复,重复出现得比直觉要早得多。从 1–100 中抽 10 个数,至少出现一次重复的概率约为 37%。算法是把每个新数字都避开已抽数字的概率连乘起来:100/100 × 99/100 × … × 91/100 ≈ 0.628,再用 1 减去它。这和著名的“生日问题”是同一个道理。
在 Pick4Us 随机数生成工具里:
- 不重复(默认勾选)保证同一次生成的结果里没有重复。要求的个数不能超过范围内的数字总数,否则工具会提示还有多少个可用。
- 不重复号码池模式把这一保证延续到多次生成:同一范围内已经抽出的号码会被排除,直到号码池用完或点击“开始新轮”。适合分批抽取的场景。
- “生成个数”每次可设为 1–500,从“最小值”到“最大值”的范围最多可以包含 10¹² 个整数。
从一万亿个数里抽 5 个不重复的数,总不能先把一万亿个数全部列出来吧?这个工具用的是 Floyd 抽样算法,由 Jon Bentley 与 Bob Floyd 于 1987 年在《Communications of the ACM》的“Programming Pearls”专栏中发表。要从 n 个数里抽 k 个,它只需要走 k 步:每一步在 0 到某个上限之间随机挑一个数,上限从 n − k 开始,每步加 1;如果挑中的数已经被选过,就改选当前的上限本身。这样,每一种 k 个数的组合被选中的机会都相同,计算量只取决于 k,与范围大小无关。最后工具还会把选出的数字打乱,让顺序也是随机的。
如果要抽的是名字而不是数字,可以用转盘选择的“连续抽签”模式,它会在下一次旋转前移除上一次抽中的选项,效果相同。转盘还支持 1–100 的整数权重,每个选项被选中的概率等于它的权重除以转盘上剩余选项的总权重。
包含两端的范围与“差一”陷阱
Pick4Us 的范围包含两端:最小值设为 1、最大值设为 6,1 和 6 都可能出现。可能的取值个数是“最大值 − 最小值 + 1”,所以 1–100 有 100 个数,而 0–100 有 101 个。
如果自己写代码,常用的包含两端的写法(也是 MDN 上的 getRandomIntInclusive 示例)是 Math.floor(Math.random() * (max − min + 1)) + min。常见错误有两个:
- 漏掉“+ 1”,结果永远取不到最大值。
- 用 Math.round 代替 Math.floor。比如用 Math.round(Math.random() * 5) + 1 模拟骰子,1 和 6 的概率各只有 1/10,2 到 5 各为 1/5,而不是每一面都是 1/6。
自己动手检验是否公平
只看输出无法“证明”一个生成器是随机的,但足以发现明显的问题。
- 多收集一些结果。先关闭“不重复”(否则同一次生成里每个值最多只出现一次),每次最多生成 500 个数字,复制结果粘贴到表格里,重复几次。
- 算出期望次数。每个值的期望出现次数 = 抽取总次数 ÷ 可能取值的个数。
- 与正常波动比较。对于公平的生成器,某个值出现次数的标准差是 √(抽取次数 × p × (1 − p)),其中 p = 1 ÷ 可能取值的个数。落在期望值上下约两个标准差之内都属正常;取值较多时,有一两个超出范围同样正常。
| 抽取次数 | 范围 | 每个值的期望次数 | 正常范围(约 ±2 个标准差) |
|---|---|---|---|
| 60 | 1–6 | 10 | 约 4–16 |
| 600 | 1–6 | 100 | 约 82–118 |
| 1,000 | 1–10 | 100 | 约 81–119 |
| 1,000 | 1–2 | 500 | 约 468–532 |
如果想用一个数字来概括,可以计算卡方统计量:对每个值算出(实际次数 − 期望次数)² ÷ 期望次数,再全部加起来。有 6 个可能取值时,公平的生成器得到大于 11.07 的结果只有 5% 的机会;有 10 个取值时,这个 5% 门槛是 16.92。一次超过门槛并不能说明问题,因为大约每 20 次检验就有 1 次会碰巧超过;反复超过才值得深究。
小样本最容易误导人:掷 60 次骰子,某一面只出现 5 次或者出现 15 次都很平常。我们的抛硬币概率文章也展示了,在完全公平的序列里,长长的连续同面有多常见。
实用总结:在线随机数生成器大多是伪随机的。做游戏、课堂和日常抽选,Math.random() 已经足够;密码、令牌和受监管的抽签则需要密码学安全的来源和规范的流程。需要避免重复时打开“不重复”或使用号码池,记住范围包含两端,判断是否公平要看几百次抽取的统计,而不是几次的手感。