在 Node.js 的包含范围内生成大随机数
Generating large random numbers between an inclusive range in Node.js
所以我对老好人非常熟悉
Math.floor(Math.random() * (max - min + 1)) + min;
这对小数字非常有效,但是当数字变大时,这很快就会变得有偏差,只有 returns 数字比它低一个零(例如 0
和之间的随机数1e100
几乎总是(每次我测试,自从我使用 for 循环生成大量数字以来已经有数十亿次)return [x]e99
)。是的,我等待程序生成那么多数字的时间很长,两次。至此,可以安全地假设所有实际用途的输出始终为 [x]e99
。
接下来我尝试了这个
Math.floor(Math.pow(max - min + 1, Math.random())) + min;
虽然这对于大范围来说非常有效,但对于小范围来说就不行了。所以我的问题是如何做到这两点 - 能够在没有任何偏差的情况下生成小随机数和大随机数(或者最小偏差到不明显的程度)?
注意:我使用 Decimal.js to handle numbers in the range -1e2043
< x < 1e2043
but since it is the same algorithm I displayed the vanilla JavaScript forms above to prevent confusion. I can take a vanilla answer and convert it to Decimal.js 没有任何问题,所以请随意回答。
注意#2:我想平衡获得大数字的几率。例如 1e33
应该与我的 0-1e100
示例中的 1e90
具有相同的赔率。但同时我需要支持更小的数字和范围。
您可以以小于 Number.MAX_SAFE_INTEGER
的增量创建数字,然后将生成的数字连接成一个字符串
const r = () => Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
let N = "";
for (let i = 0; i < 10; i++) N += r();
document.body.appendChild(document.createTextNode(N));
console.log(/e/.test(N));
你的问题是精度。这就是您首先使用 Decimal.js 的原因。与 JS 中的所有其他 Number 一样,Math.random()
仅支持 53 位精度 (某些浏览器甚至用于创建仅高 32 位的随机性)。但是您的值 1e100
需要 333 位精度。因此,较低的 280 位 (100 中的小数点后约 75 位) 在您的公式中被丢弃。
但是Decimal.js提供了random()
方法。为什么不用那个?
function random(min, max){
var delta = new Decimal(max).sub(min);
return Decimal.random( +delta.log(10) ).mul(delta).add(min);
}
另一个 "problem" 使用 e+99
得到这么多值的原因是概率。对于范围 0 .. 1e100
,获得某些指数的概率是
e+99 => 90%,
e+98 => 9%,
e+97 => 0.9%,
e+96 => 0.09%,
e+95 => 0.009%,
e+94 => 0.0009%,
e+93 => 0.00009%,
e+92 => 0.000009%,
e+91 => 0.0000009%,
e+90 => 0.00000009%,
and so on
因此,如果您生成 100 亿个数字,统计上您将获得一个高达 1e+90
的值。这就是赔率。
I want to even out those odds for large numbers. 1e33 should have the same odds as 1e90 for example
OK,那我们生成一个min ... max
范围内的10随机数。
function random2(min, max){
var a = +Decimal.log10(min),
b = +Decimal.log10(max);
//trying to deal with zero-values.
if(a === -Infinity && b === -Infinity) return 0; //a random value between 0 and 0 ;)
if(a === -Infinity) a = Math.min(0, b-53);
if(b === -Infinity) b = Math.min(0, a-53);
return Decimal.pow(10, Decimal.random(Math.abs(b-a)).mul(b-a).add(a) );
}
现在指数几乎均匀分布,但值有点倾斜。因为101到101.510 .. 33
和101.5到10的概率是一样的2 34 .. 100
Math.random() * Math.pow(10, Math.floor(Math.random() * 100));
在较小数字时的问题是随机范围 [0, 1)
,这意味着在单独计算指数时需要确保前缀范围 [1, 10)
。否则你想在 [1eX, 1eX+1)
中计算一个数字但是有例如0.1
作为前缀并在 1eX-1
中结束。这是一个示例,maxExp
不是 100,而是 10,用于输出的可读性但易于调整。
let maxExp = 10;
function differentDistributionRandom() {
let exp = Math.floor(Math.random() * (maxExp + 1)) - 1;
if (exp < 0) return Math.random();
else return (Math.random() * 9 + 1) * Math.pow(10, exp);
}
let counts = new Array(maxExp + 1).fill(0).map(e => []);
for (let i = 0; i < (maxExp + 1) * 1000; i++) {
let x = differentDistributionRandom();
counts[Math.max(0, Math.floor(Math.log10(x)) + 1)].push(x);
}
counts.forEach((e, i) => {
console.log(`E: ${i - 1 < 0 ? "<0" : i - 1}, amount: ${e.length}, example: ${Number.isNaN(e[0]) ? "none" : e[0]}`);
});
您可能会在此处看到类别 <0
,希望这就是您想要的(截止点是任意的,此处 [0, 1)
与 [1, 10)
和 [=22= 的概率相同] 等等,但是 [0.01, 0.1)
的可能性再次低于 [0.1, 1)
)
如果您不坚持 base 10
,您可以将来自两个 Math.random
调用的伪随机位重新解释为 Float64
,这将给出类似的分布,base 2
:
function exponentDistribution() {
let bits = [Math.random(), Math.random()];
let buffer = new ArrayBuffer(24);
let view = new DataView(buffer);
view.setFloat64(8, bits[0]);
view.setFloat64(16, bits[1]);
//alternatively all at once with setInt32
for (let i = 0; i < 4; i++) {
view.setInt8(i, view.getInt8(12 + i));
view.setInt8(i + 4, view.getInt8(20 + i));
}
return Math.abs(view.getFloat64(0));
}
let counts = new Array(11).fill(0).map(e => []);
for (let i = 0; i < (1 << 11) * 100; i++) {
let x = exponentDistribution();
let exp = Math.floor(Math.log2(x));
if (exp >= -5 && exp <= 5) {
counts[exp + 5].push(x);
}
}
counts.forEach((e, i) => {
console.log(`E: ${i - 5}, amount: ${e.length}, example: ${Number.isNaN(e[0]) ? "none" : e[0]}`);
});
这个明显受限于Float64
的精度端,由于IEEE754的一些细节,有一些分布不均匀的部分,例如denorms/subnorms 并且我没有处理像 Infinity
这样的特殊值。它更应该被看作是一个额外的乐趣,提醒浮点值的分布。请注意,循环执行1 << 11
(2048)次迭代次数,大约是Float64
,11位,[-1022, 1023]
的指数范围。这就是为什么在示例中每个桶都获得大约所述数字 (100) 的命中。
所以我对老好人非常熟悉
Math.floor(Math.random() * (max - min + 1)) + min;
这对小数字非常有效,但是当数字变大时,这很快就会变得有偏差,只有 returns 数字比它低一个零(例如 0
和之间的随机数1e100
几乎总是(每次我测试,自从我使用 for 循环生成大量数字以来已经有数十亿次)return [x]e99
)。是的,我等待程序生成那么多数字的时间很长,两次。至此,可以安全地假设所有实际用途的输出始终为 [x]e99
。
接下来我尝试了这个
Math.floor(Math.pow(max - min + 1, Math.random())) + min;
虽然这对于大范围来说非常有效,但对于小范围来说就不行了。所以我的问题是如何做到这两点 - 能够在没有任何偏差的情况下生成小随机数和大随机数(或者最小偏差到不明显的程度)?
注意:我使用 Decimal.js to handle numbers in the range -1e2043
< x < 1e2043
but since it is the same algorithm I displayed the vanilla JavaScript forms above to prevent confusion. I can take a vanilla answer and convert it to Decimal.js 没有任何问题,所以请随意回答。
注意#2:我想平衡获得大数字的几率。例如 1e33
应该与我的 0-1e100
示例中的 1e90
具有相同的赔率。但同时我需要支持更小的数字和范围。
您可以以小于 Number.MAX_SAFE_INTEGER
的增量创建数字,然后将生成的数字连接成一个字符串
const r = () => Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
let N = "";
for (let i = 0; i < 10; i++) N += r();
document.body.appendChild(document.createTextNode(N));
console.log(/e/.test(N));
你的问题是精度。这就是您首先使用 Decimal.js 的原因。与 JS 中的所有其他 Number 一样,Math.random()
仅支持 53 位精度 (某些浏览器甚至用于创建仅高 32 位的随机性)。但是您的值 1e100
需要 333 位精度。因此,较低的 280 位 (100 中的小数点后约 75 位) 在您的公式中被丢弃。
但是Decimal.js提供了random()
方法。为什么不用那个?
function random(min, max){
var delta = new Decimal(max).sub(min);
return Decimal.random( +delta.log(10) ).mul(delta).add(min);
}
另一个 "problem" 使用 e+99
得到这么多值的原因是概率。对于范围 0 .. 1e100
,获得某些指数的概率是
e+99 => 90%,
e+98 => 9%,
e+97 => 0.9%,
e+96 => 0.09%,
e+95 => 0.009%,
e+94 => 0.0009%,
e+93 => 0.00009%,
e+92 => 0.000009%,
e+91 => 0.0000009%,
e+90 => 0.00000009%,
and so on
因此,如果您生成 100 亿个数字,统计上您将获得一个高达 1e+90
的值。这就是赔率。
I want to even out those odds for large numbers. 1e33 should have the same odds as 1e90 for example
OK,那我们生成一个min ... max
范围内的10随机数。
function random2(min, max){
var a = +Decimal.log10(min),
b = +Decimal.log10(max);
//trying to deal with zero-values.
if(a === -Infinity && b === -Infinity) return 0; //a random value between 0 and 0 ;)
if(a === -Infinity) a = Math.min(0, b-53);
if(b === -Infinity) b = Math.min(0, a-53);
return Decimal.pow(10, Decimal.random(Math.abs(b-a)).mul(b-a).add(a) );
}
现在指数几乎均匀分布,但值有点倾斜。因为101到101.510 .. 33
和101.5到10的概率是一样的2 34 .. 100
Math.random() * Math.pow(10, Math.floor(Math.random() * 100));
在较小数字时的问题是随机范围 [0, 1)
,这意味着在单独计算指数时需要确保前缀范围 [1, 10)
。否则你想在 [1eX, 1eX+1)
中计算一个数字但是有例如0.1
作为前缀并在 1eX-1
中结束。这是一个示例,maxExp
不是 100,而是 10,用于输出的可读性但易于调整。
let maxExp = 10;
function differentDistributionRandom() {
let exp = Math.floor(Math.random() * (maxExp + 1)) - 1;
if (exp < 0) return Math.random();
else return (Math.random() * 9 + 1) * Math.pow(10, exp);
}
let counts = new Array(maxExp + 1).fill(0).map(e => []);
for (let i = 0; i < (maxExp + 1) * 1000; i++) {
let x = differentDistributionRandom();
counts[Math.max(0, Math.floor(Math.log10(x)) + 1)].push(x);
}
counts.forEach((e, i) => {
console.log(`E: ${i - 1 < 0 ? "<0" : i - 1}, amount: ${e.length}, example: ${Number.isNaN(e[0]) ? "none" : e[0]}`);
});
您可能会在此处看到类别 <0
,希望这就是您想要的(截止点是任意的,此处 [0, 1)
与 [1, 10)
和 [=22= 的概率相同] 等等,但是 [0.01, 0.1)
的可能性再次低于 [0.1, 1)
)
如果您不坚持 base 10
,您可以将来自两个 Math.random
调用的伪随机位重新解释为 Float64
,这将给出类似的分布,base 2
:
function exponentDistribution() {
let bits = [Math.random(), Math.random()];
let buffer = new ArrayBuffer(24);
let view = new DataView(buffer);
view.setFloat64(8, bits[0]);
view.setFloat64(16, bits[1]);
//alternatively all at once with setInt32
for (let i = 0; i < 4; i++) {
view.setInt8(i, view.getInt8(12 + i));
view.setInt8(i + 4, view.getInt8(20 + i));
}
return Math.abs(view.getFloat64(0));
}
let counts = new Array(11).fill(0).map(e => []);
for (let i = 0; i < (1 << 11) * 100; i++) {
let x = exponentDistribution();
let exp = Math.floor(Math.log2(x));
if (exp >= -5 && exp <= 5) {
counts[exp + 5].push(x);
}
}
counts.forEach((e, i) => {
console.log(`E: ${i - 5}, amount: ${e.length}, example: ${Number.isNaN(e[0]) ? "none" : e[0]}`);
});
这个明显受限于Float64
的精度端,由于IEEE754的一些细节,有一些分布不均匀的部分,例如denorms/subnorms 并且我没有处理像 Infinity
这样的特殊值。它更应该被看作是一个额外的乐趣,提醒浮点值的分布。请注意,循环执行1 << 11
(2048)次迭代次数,大约是Float64
,11位,[-1022, 1023]
的指数范围。这就是为什么在示例中每个桶都获得大约所述数字 (100) 的命中。