首先,你的代码存在一个问题:尝试使用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],而253-1是“奇数”[LSB = 1]),所以您减去b
得到253,乘以得到253-1,再加上b
得到253。
回答您的第二个问题:因为底层PRNG几乎总是在区间[0,2n-1]中生成随机数,即生成随机位。很容易选择一个合适的n(浮点表示中的精度位数)并除以2n以获得可预测的分布。请注意,使用此方法您将永远不会生成[0,1)
中的某些数字(使用IEEE双精度浮点数,在(0,2-53)中的任何内容都不会生成)。
这也意味着您可以执行a[Math.floor(Math.random()*a.length)]
而不必担心溢出(作业:在IEEE二进制浮点数中,证明b < 1
意味着对于正整数a
,a*b < a
)。
另一个好处是您可以将每个随机输出x视为表示间隔[x,x+2-53)的区间(不太好的是返回的平均值略小于0.5)。如果您返回[0,1],您是否应该像其他所有内容一样具有同等概率地返回端点,还是它们只应该具有一半的概率,因为它们只代表了其他所有内容的一半间隔?
为了回答返回[0,1]中的数字的简单问题,下面的方法有效地生成一个整数[0,2n](通过在[0,2n+1-1]中生成一个整数并在它太大时丢弃它),然后除以2n:
function randominclusive() {
while (Math.random() >= 0.5) {
if (Math.random() == 0) { return 1.0; }
}
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这样做)。要生成double,您需要从两个连续输出中获取53位,然后除以253,但某些输出是不可能的(您无法使用48位状态生成253个输出!)。我怀疑其中一些从不返回0(假设单线程访问),但我现在不想检查Java的实现。
- 需要获取额外的位,因此Math.random()对于每个潜在输出都是两倍,但这会对PRNG施加更多限制(要求我们推理上述LCG的四个连续输出)。
- 平均每个输出调用Math.random()约四次。有点慢。
- 它确定性地丢弃结果(假设单线程访问),因此几乎可以保证减少输出空间。