在 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) 的命中。