Во-первых, в вашем коде есть проблема: попробуйте randomInRange(0,5e-324)
или просто введите Math.random()*5e-324
в консоли JavaScript вашего браузера.
Даже без переполнения / недостаточного / денормса трудно надежно рассуждать о плавающемPoint Ops.Немного покопавшись, я могу найти контрпример:
>>> 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).Достаточно легко убедиться, что это разумно, рассмотрев PRNG, который возвращает 0 или 0,5:
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 состоит в том, чтобы брать верхние 32 бита 48-битного LCG (Firefox и Java делают это).Чтобы сгенерировать двойное число, вы берете 53 бита с двух последовательных выходов и делите их на 2 53 , но некоторые выходы невозможны (вы не можете сгенерировать 2 53 выходов с 48 битами состояния!).Я подозреваю, что некоторые из них никогда не возвращают 0 (при условии однопоточного доступа), но мне не хочется проверять реализацию Java прямо сейчас.
- Math.random () дважды для каждого потенциала вывод как следствие необходимости получить дополнительный бит, но это накладывает больше ограничений на PRNG (что требует от нас рассуждать о четырех последовательных выходах вышеупомянутого LCG).
- Math.random () вызывается нав среднем около четыре раз на выход.Немного медленный.
- Он отбрасывает результаты детерминистически (при условии однопоточного доступа), поэтому в значительной степени гарантированно уменьшает пространство вывода.