随机数生成器

在任意整数区间里批量取值,用拒绝采样把取模偏差压到零,并给出样本均值、标准差与以标准误为单位的偏离,方便你判断这一批是不是够均匀。

示例展示(演示值,点「生成随机数」重新生成):
27 84 3 61 95 12 47 8 73 39
100 19 56 2 88 33 70 45 16 91

提示:任意输入框里裸按 Enter 可直接重新抽一批;「结果不重复」用的是部分 Fisher–Yates 洗牌,区间跨度超过 100 万时会拒绝而不是退化成慢速循环。

使用方法

  1. 「最小值」填 1、「最大值」填 100(示例值),两端都包含在内,所以区间跨度 n = 100 − 1 + 1 = 100。
  2. 「生成个数」填 20(示例值),允许 1~1000。个数只决定一次输出几项,不改变每一项的分布。
  3. 「抽样方式」默认允许重复:同一个值可以出现两次,只有这样才能支持远超区间跨度的个数。需要一批互不相同的值时改成「结果不重复」,此时个数不能超过跨度,且跨度上限是 100 万。
  4. 点「生成随机数」(或在任意输入框里裸按 Enter)立刻输出;下方统计条给出最小值、最大值、平均值、样本标准差和以标准误为单位的偏离量 z。
  5. 「复制结果」一行一个带走;「导出 CSV」存成带 BOM 的表格,Excel 打开不乱码。想换成别的区间,直接改最小值/最大值再回车即可。

这个随机数生成器能解决什么问题

一是抽样与分组。从 1~1000 的工号里抽 20 个做质检、把 200 个样本随机分成两组,本质上都是"在区间里取若干个互不相同的整数"。切到「结果不重复」,重复风险直接归零,不用事后去重。

二是给测试与压测造输入。填入一个大区间(例如 1~1000000000)批量取值,可以快速铺满边界用例;本页的区间跨度上限是 2×1015,远超常见的业务编号范围。

三是验证自己的随机实现有没有偏差。很多同事手写的"随机"其实是 Math.random() * n 直接取整,短区间下肉眼看不出问题,长跑之后才发现某些值偏多。本页把接受上限、丢弃个数、丢弃概率都列在对照表里,可以拿来做基准对照。

四是做公平性相关的操作。分批派单、随机排序、随机点名这类场景,只要区间和个数确定,同一个方法就能覆盖,不需要为每个场景重新写一遍取整逻辑。

本页不做:不生成小数或正态分布的取值(输出一律是整数);不做加密、不做密钥派生;不保证「不重复」模式在超大区间上可用(跨度超过 100 万会被拒绝);不写浏览器存储、不发网络请求。

生成原理:拒绝采样如何消除取模偏差

在讲这个随机数生成器怎么取值之前,先把问题说清楚。crypto.getRandomValues(new Uint32Array(1)) 给你的是 [0, 232) 上的整数,而你要的是 [0, n) 上的整数。最直觉的写法 v % n 是有偏的——因为 232 = 4294967296 通常不能被 n 整除:n = 3 时 232 ÷ 3 余 1,于是余数 0 比余数 1 和 2 各多出现一次;n = 100 时余 96,前 96 个取值比后 4 个多出现一次。偏差量就是 (232 mod n) / 232。

拒绝采样的做法是先把尾部切掉。设接受上限 limit = Math.floor(232 / n) * n:以 n = 100 为例,Math.floor(4294967296 / 100) = 42949672,乘回 100 得 limit = 4294967200。抽到的随机数小于 4294967200 才接受,落在 4294967200~4294967295 这 96 个取值上就丢弃重抽。切掉尾之后,剩下的取值个数是 n 的整数倍,v % n 的分布就严格均匀了。

为什么接受上限要写成 floor(232/n)*n 而不是别的形式。这个写法等价于 232 - (232 mod n),是"小于 232 的最大 n 的倍数",正好保留了尽可能多的取值:丢弃个数就是 232 mod n,不可能更少。代价也很小——n = 100 时丢弃概率是 96 ÷ 4294967296 ≈ 2.235×10-8,接受率 99.9999978%,平均只多抽 1.0000000 次。

n 很大时必须换算法。上面的公式只对 n ≤ 232 成立。当区间跨度超过 232,一次 32 位取不够,本页改用两次 32 位拼出 53 位:(hi >>> 5) * 67108864 + (lo >>> 6)。这里 hi >>> 5 保留高 27 位、lo >>> 6 保留高 26 位,27 + 26 = 53 位,乘数 67108864 就是 226,拼出的值落在 [0, 253) 上,然后对 253 做同样形状的拒绝采样。之所以停在 53 位而不是 64 位,是因为 JavaScript 的 Number 遵循 IEEE 754 binary64,能精确表示的整数到 253 = 9007199254740992 为止,再往上整数之间就会出现空隙,拼出来的是假随机。

去重抽样用的是部分洗牌。切到「结果不重复」时,本页把 [min, max] 上的整数铺成一个数组,再用 Fisher–Yates 从前往后遍历,只做前 k 次交换(第 i 次把 pool[i] 与 pool[i + rand(n - i)] 互换),取前 k 个元素。这样每个组合出现的概率完全相同,而且不需要把整个数组都洗一遍。代价是要先分配 length = n 的数组,所以跨度上限设成 100 万;超过就明确报错,而不是退化成"抽到了就重抽"的慢循环。

依据的规范。随机源 crypto.getRandomValues 由 W3C Web Cryptography API 定义,规范明确写了单次请求的字节数不得超过 65536(也就是一次最多填 16384 个 uint32),超了会抛 QuotaExceededError。Math.random() 的行为由 ECMAScript 规范约束,只保证返回 [0, 1) 且分布大致均匀,不承诺密码学安全,所以本页一次都没用它来产生抽样值。整数精度边界来自 IEEE 754 binary64 的 53 位有效位。这三处都是长期稳定的公开规范。

完整示例(数值仅作演示)

这个随机数生成器的示例条件与页面打开时的默认值完全一致:最小值 1、最大值 100、生成 20 个、允许重复。区间跨度 n = 100,落在 [1, 100] 的整数共 100 个。

27 84 3 61 95 12 47 8 73 39
100 19 56 2 88 33 70 45 16 91

这 20 个值的实测结果:

把区间固定为 [1, 100]、个数固定为 20,允许重复时每次的输出组合数是 10020 = 1.000×1040 种;切成「结果不重复」后组合数变成 C(100, 20) = 5.360×1020 种,每个组合的概率完全相同。这也解释了为什么"不重复"模式下个数必须小于等于跨度:从 100 个数里抽 101 个互不相同的整数,不存在这样的组合。

区间跨度与拒绝采样代价对照表

下表是同一套公式在不同 n 下的实际代价。注意「丢弃个数」与「朴素 % 偏差」两栏数字完全相同——这不是巧合:被拒绝采丢弃的那些取值,正是朴素取模下多出现一次的那批值,两者是同一个量。

区间跨度 n接受上限 limit丢弃个数丢弃概率接受率平均抽签次数
2429496729600100%1.0000000
3429496729512.328×10⁻¹⁰99.99999998%1.0000000
4429496729600100%1.0000000
5429496729512.328×10⁻¹⁰99.99999998%1.0000000
6429496729249.313×10⁻¹⁰99.99999991%1.0000000
7429496729249.313×10⁻¹⁰99.99999991%1.0000000
10429496729061.397×10⁻⁹99.99999986%1.0000000
13429496728792.095×10⁻⁹99.99999979%1.0000000
524294967248481.118×10⁻⁸99.99999888%1.0000000
100(示例)4294967200962.235×10⁻⁸99.99999776%1.0000000
100042949670002966.892×10⁻⁸99.99999311%1.0000001
1000004294900000672961.567×10⁻⁵99.99843314%1.0000157
100000042940000009672962.252×10⁻⁴99.97747838%1.0002253
21474836492147483649214748364749.99999998%50.00000002%2.0000000
30000000003000000000129496729630.147%69.84919310%1.4316558
4294967295429496729512.328×10⁻¹⁰99.99999998%1.0000000

把表横着读一遍能看出三件事。第一,n 是 2 的幂时丢弃个数为 0(n = 2、4 两行),因为 232 能被它们整除,此时拒绝采样和朴素取模完全等价。第二,n 在 231 附近时代价最惨:n = 2147483649 时接受上限只保留了一半取值,一半的抽样要被丢掉重来,平均要抽 2 次,而朴素取模的偏差同样是 49.99999998%。第三,常见的 n(100、1000、100 万)代价都可以忽略,丢弃概率从 2.235×10⁻⁸ 到 2.252×10⁻⁴,平均尝试次数都在 1.0002 次以内。

边界与精度说明

常见问题

Math.random() * n 在短区间上看起来挺均匀的,为什么要这么麻烦?

短区间上误差确实看不出来。n = 3 时 Math.random() 的取模偏差只有 2.220×10-16,肉眼和统计都发现不了。真正的问题不在偏差而在可预测性:ECMAScript 不要求它密码学安全,实现里内部状态有限,观察到连续输出后可以预测后面的值。本页用拒绝采样是为了同时解决偏差和随机源两件事。

为什么 n 是 2 的幂时丢弃个数是 0?

因为 232 能被 2 的整数次幂整除,232 mod n = 0,接受上限就等于 232 本身,没有任何取值需要丢弃。此时拒绝采样和朴素 % n 的结果完全一样——这也是为什么很多旧代码在取 2 的幂时"看起来很对",换个 n 就出问题。

为什么 n 接近 231 时偏差能到 50%?

因为那时 Math.floor(232 / n) 掉到了 1。以 n = 2147483649 为例,接受上限是 2147483649,后面 2147483647 个取值全被丢弃,接受率正好 50%。剩下的一半里,取值 0 与其余取值各出现一次,而朴素取模下取值 0 出现两次——一半的取值概率是另一半的两倍。n 越接近 232,余数越小,偏差反而迅速回落。

「结果不重复」是怎么保证不重的?

先把 [min, max] 上的整数按顺序铺成数组,再用部分 Fisher–Yates:第 i 轮把下标 i 与从 i 到末尾之间随机选出的一个位置交换,只做前 k 轮。洗完之后前 k 个元素就是抽样结果,每个组合的出现概率完全相同。不是"抽到了就重抽",因此不会在区间快被抽满时卡住。

为什么「结果不重复」的跨度上限只有 100 万?

因为要先分配一个 length = n 的数组。跨度 100 万时这个数组约占 8 MB,生成和部分洗牌都在毫秒级;再往上内存和耗时都不划算,而且"从十亿个数里抽 20 个互不相同的数"本身用不着先把十亿个数铺开。需要这种场景请用允许重复模式,或自己实现基于哈希的抽样。

生成的这批数会上传或保存吗?

不会。随机数来自浏览器接口,拒绝采样、洗牌和统计都在当前页面完成,没有网络请求,也不写 localStorage。你可以打开开发者工具的 Network 面板,连点几次「生成随机数」自行确认。

平均值算出来离理论均值差好几个数,是不是有 bug?

先看 z 值再下结论。20 个样本的标准误是 6.4547,平均值偏两三个数只相当于零点几个标准误,非常常见。把个数调到 1000 再抽几次,你会看到平均值收紧到理论均值附近——这正是"多次抽样让随机误差按 √k 缩小"的直接体现,不是抽样器坏了。

这里的随机数和别的取值工具有什么区别?

本页的落点是区间抽样:任意整数区间、任意个数、可切换是否重复,并且把接受上限、丢弃概率、平均抽签次数逐档列出来。需要自定义字符池拼字符串、需要按固定位布局改写字节、或者只想要一个 0~1 之间的小数,都不在本页范围内,请用底部的相关工具。

相关工具