Распределение числа цифр случайных чисел

Я сталкиваюсь с этим любопытным явлением, пытающимся реализовать генератор UUID в JavaScript.

В принципе, в JavaScript, если я создаю большой список случайных чисел со встроенным Math.random() на Node 4.2.2:

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

Причина действительно связана с представлением с плавающей запятой. Число чисел с плавающей запятой имеет максимальное количество (двоичных) цифр, которые оно может представлять, и ограниченный диапазон значений экспоненты. Теперь, когда вы распечатываете это без использования научной нотации, вы можете в некоторых случаях иметь нулевые значения после десятичной точки, прежде чем начнут следовать знаковые цифры.

Вы можете визуализировать этот эффект, напечатав те случайные числа, которые имеют самую длинную длину при преобразовании в 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: когда они становятся слишком маленькими, научная нотация используется в представлении string. Вы можете увидеть это со следующим script (не уверен, что каждый браузер имеет одинаковую точку прерывания, поэтому, возможно, вам нужно немного поиграть с номером):

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

Это дает мне следующий результат:

0.0000012345678901234567 1.2345678901234568e-7

Таким образом, очень маленькие числа получат более фиксированную длину string в результате, не более 22 символов, в то время как в ненаучном обозначении длина 23 является общей. Это влияет и на второй script I, а длина 22 будет больше, чем 23.

Следует отметить, что javascript не переключается на научную нотацию при преобразовании в string в двоичном представлении:

var i = 0.1234567890123456789e-120;
console.log(i.toString(2));

Вышеприведённый текст выведет строку из более чем 450 двоичных цифр!

Ответ 2

Это потому, что некоторые из значений такие:

0.00012345...

И, таким образом, они длиннее.