首先,您的代码有问题:尝试randomInRange(0,5e-324) 或在浏览器的JavaScript 控制台中输入Math.random()*5e-324。
即使没有上溢/下溢/反规范,也很难可靠地推断浮点操作。经过一番挖掘,我可以找到一个反例:
>>> a=1.0
>>> b=2**-54
>>> rand=a-2*b
>>> a
1.0
>>> b
5.551115123125783e-17
>>> rand
0.9999999999999999
>>> (a-b)*rand+b
1.0
更容易解释为什么 a=253 和 b=0.5 会发生这种情况:253-1 是下一个可表示的数字。默认舍入模式(“舍入到最接近的偶数”)向上舍入 253-0.5(因为 253 是“偶数” [LSB = 0] 和 2 53-1 是“奇数”[LSB = 1]),所以减去 b 得到 253,乘以得到 253-1 ,并添加b 再次获得253。
回答你的第二个问题:因为底层 PRNG 几乎总是在区间 [0,2n-1] 内生成一个随机数,即它生成随机位。选择一个合适的 n(浮点表示中的精度位)并除以 2n 并获得可预测的分布非常容易。请注意,[0,1) 中有一些数字,您将永远使用此方法生成(IEEE doubles 中的 (0,2-53) 中的任何数字)。 p>
这也意味着你可以做到a[Math.floor(Math.random()*a.length)]而不用担心溢出(作业:在IEEE二进制浮点中,证明b < 1意味着a*b < a代表正整数a)。
另一个好处是您可以将每个随机输出 x 视为表示一个区间 [x,x+2-53)(不太好的事情是平均值返回略小于 0.5)。如果返回 [0,1],返回端点的概率是否与其他所有端点相同,或者它们是否应该只有一半的概率,因为它们只代表一半的区间?
为了回答在 [0,1] 中返回一个数字的简单问题,下面的方法有效地生成一个整数 [0,2n](通过在 [0,2n+1-1],如果太大就扔掉)除以2n:
function randominclusive() {
// Generate a random "top bit". Is it set?
while (Math.random() >= 0.5) {
// Generate the rest of the random bits. Are they zero?
// If so, then we've generated 2^n, and dividing by 2^n gives us 1.
if (Math.random() == 0) { return 1.0; }
// If not, generate a new random number.
}
// If the top bits are not set, just divide by 2^n.
return Math.random();
}
cmets 暗示基数为 2,但我认为假设如下:
- 0 和 1 应该以相等的概率返回(即 Math.random() 不使用接近 0 的浮点数的更近间距)。
- Math.random() >= 0.5,概率为 1/2(对于偶数基数应该为真)
- 底层 PRNG 足够好,我们可以做到这一点。
请注意,随机数总是成对生成:while (a) 中的一个总是跟在 if 中的一个或末尾的一个 (b) 之后。通过考虑返回 0 或 0.5 的 PRNG,很容易验证它是否合理:
-
a=0 b=0 :返回0
-
a=0 b=0.5: 返回 0.5
-
a=0.5 b=0 :返回1
-
a=0.5 b=0.5: 循环
问题:
- 假设可能不正确。特别是,常见的 PRNG 是采用 48 位 LCG 的前 32 位(Firefox 和 Java 这样做)。要生成双精度数,您需要从两个连续输出中取 53 位并除以 253,但有些输出是不可能的(您无法生成 253 48 位输出国家!)。我怀疑其中一些永远不会返回 0(假设是单线程访问),但我现在不想检查 Java 的实现。
- Math.random() 对于每个 潜在 输出都是两次,因为需要获得额外的位,但这对 PRNG 施加了更多限制(需要我们推理四个连续输出上述 LCG)。
- Math.random() 每个输出平均被调用 四次 次。有点慢。
- 它会确定性地丢弃结果(假设单线程访问),因此几乎可以保证减少输出空间。