首先,您的代码存在问题:尝试randomInRange(0,5e-324)
或直接Math.random()*5e-324
在浏览器的 JavaScript 控制台中输入。
即使没有上溢/下溢/规范,也很难可靠地推断浮点运算。经过一番挖掘,我可以找到一个反例:
>>> 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=2 53和 b=0.5会发生这种情况:2 53 -1 是下一个可表示的数字。默认舍入模式(“舍入到最接近的偶数”)将 2 53 -0.5 向上舍入(因为 2 53是“偶数”[LSB = 0],而 2 53 -1 是“奇数”[LSB = 1]),因此您减去b
并得到 2 53,乘以得到 2 53 -1,再相加b
得到 2 53。
回答你的第二个问题:因为底层 PRNG 几乎总是在区间 [0,2 n -1] 中生成一个随机数,即它生成随机位。选择合适的 n(浮点表示中的精度位)并除以 2 n并获得可预测的分布非常容易。请注意,有一些数字中[0,1)
,你将将永远不会产生使用这种方法(在任何(0,2 -53)与IEEE双打)。
这也意味着您可以这样做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,2 n ](通过在 [0,2 n+1 -1] 中生成一个整数并将其丢弃,如果它太大了)并除以 2 n:
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();
}
评论暗示基数为 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 位并除以 2 53,但有些输出是不可能的(您不能生成 2 53具有 48 位状态的输出!)。我怀疑其中一些永远不会返回 0(假设是单线程访问),但我现在不想检查 Java 的实现。
- 由于需要获得额外的位,Math.random() 对于每个潜在输出是两次,但这对 PRNG 施加了更多限制(需要我们推理上述 LCG 的四个连续输出)。
- Math.random()每个输出平均被调用四次。有点慢。
- 它确定性地丢弃结果(假设单线程访问),因此几乎可以保证减少输出空间。