使用方法
- 「最小值」填 1、「最大值」填 100(示例值),两端都包含在内,所以区间跨度 n = 100 − 1 + 1 = 100。
- 「生成个数」填 20(示例值),允许 1~1000。个数只决定一次输出几项,不改变每一项的分布。
- 「抽样方式」默认允许重复:同一个值可以出现两次,只有这样才能支持远超区间跨度的个数。需要一批互不相同的值时改成「结果不重复」,此时个数不能超过跨度,且跨度上限是 100 万。
- 点「生成随机数」(或在任意输入框里裸按 Enter)立刻输出;下方统计条给出最小值、最大值、平均值、样本标准差和以标准误为单位的偏离量 z。
- 「复制结果」一行一个带走;「导出 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 个值的实测结果:
- 最小值 2、最大值 100,两端都落在闭区间内,说明边界处理是"取得到"的;
- 总和 969,平均值 48.45,理论均值是 (1 + 100) ÷ 2 = 50.50;
- 样本标准差 32.1364(除以 n,不是 n − 1),理论标准差是 √((100² − 1) ÷ 12) = 28.8661;
- 平均值的标准误是 28.8661 ÷ √20 = 6.4547,所以这次偏离 z = −0.3176——绝对值不到 1 个标准误,属于完全正常的波动,不需要"再抽一次";
- 20 个值里没有重复,但这是巧合:允许重复时两两不同的概率约 13.04%,重复才是常态。
把区间固定为 [1, 100]、个数固定为 20,允许重复时每次的输出组合数是 10020 = 1.000×1040 种;切成「结果不重复」后组合数变成 C(100, 20) = 5.360×1020 种,每个组合的概率完全相同。这也解释了为什么"不重复"模式下个数必须小于等于跨度:从 100 个数里抽 101 个互不相同的整数,不存在这样的组合。
区间跨度与拒绝采样代价对照表
下表是同一套公式在不同 n 下的实际代价。注意「丢弃个数」与「朴素 % 偏差」两栏数字完全相同——这不是巧合:被拒绝采丢弃的那些取值,正是朴素取模下多出现一次的那批值,两者是同一个量。
| 区间跨度 n | 接受上限 limit | 丢弃个数 | 丢弃概率 | 接受率 | 平均抽签次数 |
|---|---|---|---|---|---|
| 2 | 4294967296 | 0 | 0 | 100% | 1.0000000 |
| 3 | 4294967295 | 1 | 2.328×10⁻¹⁰ | 99.99999998% | 1.0000000 |
| 4 | 4294967296 | 0 | 0 | 100% | 1.0000000 |
| 5 | 4294967295 | 1 | 2.328×10⁻¹⁰ | 99.99999998% | 1.0000000 |
| 6 | 4294967292 | 4 | 9.313×10⁻¹⁰ | 99.99999991% | 1.0000000 |
| 7 | 4294967292 | 4 | 9.313×10⁻¹⁰ | 99.99999991% | 1.0000000 |
| 10 | 4294967290 | 6 | 1.397×10⁻⁹ | 99.99999986% | 1.0000000 |
| 13 | 4294967287 | 9 | 2.095×10⁻⁹ | 99.99999979% | 1.0000000 |
| 52 | 4294967248 | 48 | 1.118×10⁻⁸ | 99.99999888% | 1.0000000 |
| 100(示例) | 4294967200 | 96 | 2.235×10⁻⁸ | 99.99999776% | 1.0000000 |
| 1000 | 4294967000 | 296 | 6.892×10⁻⁸ | 99.99999311% | 1.0000001 |
| 100000 | 4294900000 | 67296 | 1.567×10⁻⁵ | 99.99843314% | 1.0000157 |
| 1000000 | 4294000000 | 967296 | 2.252×10⁻⁴ | 99.97747838% | 1.0002253 |
| 2147483649 | 2147483649 | 2147483647 | 49.99999998% | 50.00000002% | 2.0000000 |
| 3000000000 | 3000000000 | 1294967296 | 30.147% | 69.84919310% | 1.4316558 |
| 4294967295 | 4294967295 | 1 | 2.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.floor(rand * n) + min这类写法时,n = 100 的两个端点概率只有 0.5051%,而中间每个值是 1.0101%——正好差一倍。本页所有取值都用min + r(r ∈ [0, n))得出,所有值概率严格相等,都是 1%。 - 区间跨度上限 2×1015。最小值与最大值各自限制在 ±1015 内,所以最大跨度是 1015 − (−1015) + 1 = 2000000000000001,仍小于 253 = 9007199254740992,拼出来的 53 位随机值可以逐一遍历区间里的每个整数。
- 个数上限 1000。超过会被拒绝。个数只影响一次输出多少项,"允许重复"模式下再多也不会增加页面渲染时间以外的风险。
- 「结果不重复」的跨度上限是 100 万。超出时页面直接报错并说明原因,而不是偷偷退化成慢速重抽。100 万以内的跨度要先生成一个 length = n 的数组,这是该模式唯一的内存开销。
- 「结果不重复」时个数不能超过跨度。从 100 个数里抽 101 个互不相同的整数不存在这样的组合,页面会算出差值并提示当前跨度是多少。
- 不要用
Math.random()代替这里的抽样。它只有 52 位尾数,粒度是 2-52;更麻烦的是可预测——ECMAScript 不要求它做密码学安全。用Math.floor(Math.random() * n)时区间跨度一旦超过 252 = 4503599627370496,就有一部分整数永远抽不到,例如 [0, 1016] 里的整数个数是 252 的 2.22 倍。 - z 值不是"随机性好坏"的判据。它衡量的是这一批样本的平均值偏离理论均值多少个标准误。条数少于 30 时波动本来就大,|z| 大于 2 也很常见;只有反复生成、z 的绝对值持续偏大,才值得怀疑抽样环节。
- 本页的随机源不可复现,也不可复现地"固定种子"。取的是操作系统熵池,刷新页面就是全新的一批。需要可复现序列请用带种子的伪随机算法自己实现,那属于另一类工具。
常见问题
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 之间的小数,都不在本页范围内,请用底部的相关工具。