随机数位数分布

15

我在尝试在JavaScript中实现UUID生成器时遇到了一个有趣的现象。

基本上,在Node 4.2.2上,如果使用内置的Math.random()生成大量随机数:

var records = {};
var l;
for (var i=0; i < 1e6; i += 1) {
  l = String(Math.random()).length;
  if (records[l]) {
    records[l] += 1;
  } else {
    records[l] = 1;
  }
}
console.log(records);

数字的位数有一个奇怪的模式:

{ '12': 1,
  '13': 11,
  '14': 65,
  '15': 663,
  '16': 6619,
  '17': 66378,
  '18': 611441,
  '19': 281175,
  '20': 30379,
  '21': 2939,
  '22': 282,
  '23': 44,
  '24': 3 }

我本以为这是 V8 的随机数生成器的一个怪癖,但类似的模式也出现在 Python 3.4.3 中:

12 : 2
13 : 5
14 : 64
15 : 672
16 : 6736
17 : 66861
18 : 610907
19 : 280945
20 : 30455
21 : 3129
22 : 224

而Python代码如下:

import random
random.seed()
records = {}
for i in range(0, 1000000):
    n = random.random()
    l = len(str(n))
    try:
        records[l] += 1
    except KeyError:
        records[l] = 1;

for i in sorted(records):
    print(i, ':', records[i])

这是预期的18位数及以下的模式:例如随机数应该有20位数字,如果一个数的最后一位是0,那么它实际上只有19位数字。如果随机数生成器很好,那么它发生的概率大约是1/10。

但是为什么19位及以上的模式相反呢?

我猜这与浮点数的二进制表示有关,但我无法确定原因。


1
一些离题的事情:获取该字典的另一种方法:__import__('collections').Counter(len(str(__import__('random').random())) for i in range(0, 1000000)) - Remi Guan
1
如果你查看 Math.random().toString(2).length,你会看到一个不同的模式,可能更符合你的期望。 - Jaromanda X
1
@KevinGuan 而 JavaScript ES6 的对应方法(有点类似):Array.apply(null, Array(1e5)).map(() => Math.random()).reduce((records, n) => records[String(n).length] ? (records[String(n).length] += 1, records) : (records[String(n).length] = 1, records), {}) - Andreas Blaesus
@Jaromanda X,二进制表示法也会显示类似的效果。这与有效数字的数量和表示微小数字所需的位数之间的差异有关,而不使用指数。 - trincot
您可能会喜欢这个谜题,它基于有关随机数字符串表示的相关事实。http://blog.coverity.com/2014/09/17/spot-defect-randomness - Eric Lippert
显示剩余3条评论
2个回答

8
原因确实与浮点数表示有关。浮点数表示法具有它可以表示的最大(二进制)数字位数和有限的指数值范围。现在,如果您在不使用科学计数法的情况下将其打印出来,您可能需要在小数点后面有一些零,然后才会出现有效数字。
您可以通过打印那些转换为string时长度最长的随机数来可视化此效果:
var records = {};
var l, r;
for (var i=0; i < 1e6; i += 1) {
    r = Math.random();
    l = String(r).length;
    if (l === 23) {
        console.log(r);
    }
    if (records[l]) {
        records[l] += 1;
    } else {
        records[l] = 1;
    }
}

这将仅打印长度为23的字符串,您将获得像这样的数字:

0.000007411070483631654
0.000053944830052166104
0.000018188989763578967
0.000029525788901141325
0.000009613635131744402
0.000005937417234758158
0.000021099748521158368

请注意第一个非零数字前面的零。这些实际上并没有存储在浮点表示的数字部分中,而是由其指数部分隐含。

如果您删除前导零,然后进行计数:

var records = {};
var l, r, s;
for (var i=0; i < 1e6; i += 1) {
    r = Math.random();
    s = String(r).replace(/^[0\.]+/, '');
    l = s.length;

    if (records[l]) {
        records[l] += 1;
    } else {
        records[l] = 1;
    }
}

如果你使用更少奇怪的结果,可以尝试以下代码:

然而,你会发现一些不规则性,这归功于javascript将小数字转换为string时使用科学计数法的表示方法。可以通过以下脚本看到这一点(不确定每个浏览器的分界点是否相同,因此可能需要稍微调整数字):

var i = 0.00000123456789012345678;
console.log(String(i), String(i/10));

这给我以下输出:
0.0000012345678901234567 1.2345678901234568e-7

非常小的数字会得到一个更固定的字符串长度,通常为22个字符,而在非科学记数法中,长度为23是常见的。这也影响了我提供的第二个脚本,长度为22的命中率比23高。
需要注意的是,在二进制表示中,JavaScript在转换为字符串时不会切换到科学计数法:
var i = 0.1234567890123456789e-120;
console.log(i.toString(2));

上述代码将输出一个超过450个二进制数字的字符串!

需要注意的是,JavaScript 有一个 Number.toFixed() 方法,当需要将数字的字符串表示用于数学运算时可以使用它。 - Blackhole
是的,那是一个有趣的替代方案。对于OP的统计分析来说,这可能不太有用——所有字符串的长度都将相同。 - trincot

2

这是因为一些值是这样的:

0.00012345...

因此它们更长。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接