JavaScript, Node.JS
Рекомендация: подборка платных и бесплатных курсов таргетированной рекламе — https://katalog-kursov.ru/
Вы когда-нибудь задумывались, как работает Math.random()? Что такое случайное число и как оно получается? А представьте вопрос на собеседовании?—?напишите свой генератор случайных чисел в пару строк кода. И так, что же это такое, случайность и возможно ли ее предсказать?
Меня очень увлекают различные IT головоломки и задачки и генератор случайных чисел — одна из таких задачек. Обычно в своем телеграм канале я разбираю всякие головоломки и разные задачи с собеседований. Задача про генератор случайных чисел набрала большую популярность и мне захотелось увековечить ее в недрах одного из авторитетных источников информации — то бишь здесь, на Хабре.
Данный материал будет полезен всем тем фронтендерам и Node.js разработчикам, кто на острие технологий и хочет попасть в блокчейн проект/стартап, где вопросы про безопасность и криптографию, хотя бы на базовом уровне, спрашивают даже у фронтендеров.
Генератор псевдослучайных чисел и генератор случайных чисел
Для того, чтобы получить что-то случайное, нам нужен источник энтропии, источник некого хаоса из который мы будем использовать для генерации случайности.
Этот источник используется для накопления энтропии с последующим получением из неё начального значения (initial value, seed), которое необходимо генераторам случайных чисел (ГСЧ) для формирования случайных чисел.
Генератор ПсевдоСлучайных Чисел использует единственное начальное значение, откуда и следует его псевдослучайность, в то время как Генератор Случайных Чисел всегда формирует случайное число, имея в начале высококачественную случайную величину, которая берется из различных источников энтропии.
Энтропия?—?это мера беспорядка. Информационная энтропия?—?мера неопределённости или непредсказуемости информации.
Выходит, что чтобы создать псевдослучайную последовательность нам нужен алгоритм, который будет генерить некоторую последовательность на основании определенной формулы. Но такую последовательность можно будет предсказать. Тем не менее, давайте пофантазируем, как бы могли написать свой генератор случайных чисел, если бы у нас не было Math.random()
ГПСЧ имеет некоторый алгоритм, который можно воспроизвести.
ГСЧ?—?это получение чисел полностью из какого либо шума, возможность просчитать который стремится к нулю. При этом в ГСЧ есть определенные алгоритмы для выравнивания распределения.
Придумываем свой алгоритм ГПСЧ
Генератор псевдослучайных чисел (ГПСЧ, англ. pseudorandom number generator, PRNG)?—?алгоритм, порождающий последовательность чисел, элементы которой почти независимы друг от друга и подчиняются заданному распределению (обычно равномерному).
Мы можем взять последовательность каких-то чисел и брать от них модуль числа. Самый простой пример, который приходит в голову. Нам нужно подумать, какую последовательность взять и модуль от чего. Если просто в лоб от 0 до N и модуль 2, то получится генератор 1 и 0:
function* rand() {
const n = 100;
const mod = 2;
let i = 0;
while (true) {
yield i % mod;
if (i++ > n) i = 0;
}
}
let i = 0;
for (let x of rand()) {
if (i++ > 100) break;
console.log(x);
}
Эта функция генерит нам последовательность 01010101010101… и назвать ее даже псевдослучайной никак нельзя. Чтобы генератор был случайным, он должен проходить тест на следующий бит. Но у нас не стоит такой задачи. Тем не менее даже без всяких тестов мы можем предсказать следующую последовательность, значит такой алгоритм в лоб не подходит, но мы в нужном направлении.
А что если взять какую-то известную, но нелинейную последовательность, например число PI. А в качестве значения для модуля будем брать не 2, а что-то другое. Можно даже подумать на тему меняющегося значения модуля. Последовательность цифр в числе Pi считается случайной. Генератор может работать, используя числа Пи, начиная с какой-то неизвестной точки. Пример такого алгоритма, с последовательностью на базе PI и с изменяемым модулем:
const vector = [...Math.PI.toFixed(48).replace('.','')];
function* rand() {
for (let i=3; i<1000; i++) {
if (i > 99) i = 2;
for (let n=0; n<vector.length; n++) {
yield (vector[n] % i);
}
}
}
Но в JS число PI можно вывести только до 48 знака и не более. Поэтому предсказать такую последовательность все так же легко и каждый запуск такого генератора будет выдавать всегда одни и те же числа. Но наш генератор уже стал показывать числа от 0 до 9.
Мы получили генератор чисел от 0 до 9, но распределение очень неравномерное и каждый раз он будет генерировать одну и ту же последовательность.
Мы можем взять не число Pi, а время в числовом представлении и это число рассматривать как последовательность цифр, причем для того, чтобы каждый раз последовательность не повторялась, мы будем считывать ее с конца. Итого наш алгоритм нашего ГПСЧ будет выглядеть так:
function* rand() {
let newNumVector = () => [...(+new Date)+''].reverse();
let vector = newNumVector();
let i=2;
while (true) {
if (i++ > 99) i = 2;
let n=-1;
while (++n < vector.length) yield (vector[n] % i);
vector = newNumVector();
}
}
// TEST:
let i = 0;
for (let x of rand()) {
if (i++ > 100) break;
console.log(x)
}
Вот это уже похоже на генератор псевдослучайных чисел. И тот же Math.random()?—?это ГПСЧ, про него мы поговорим чуть позже. При этом у нас каждый раз первое число получается разным.
Собственно на этих простых примерах можно понять как работают более сложные генераторы случайных числе. И есть даже готовые алгоритмы. Для примера разберем один из них?—?это Линейный конгруэнтный ГПСЧ(LCPRNG).
Линейный конгруэнтный ГПСЧ
Линейный конгруэнтный ГПСЧ(LCPRNG)?—?это распространённый метод для генерации псевдослучайных чисел. Он не обладает криптографической стойкостью. Этот метод заключается в вычислении членов линейной рекуррентной последовательности по модулю некоторого натурального числа m, задаваемой формулой. Получаемая последовательность зависит от выбора стартового числа?—?т.е. seed. При разных значениях seed получаются различные последовательности случайных чисел. Пример реализации такого алгоритма на JavaScript:
const a = 45;
const c = 21;
const m = 67;
var seed = 2;
const rand = () => seed = (a * seed + c) % m;
for(let i=0; i<30; i++)
console.log( rand() )
Многие языки программирования используют LСPRNG (но не именно такой алгоритм(!)).
Как говорилось выше, такую последовательность можно предсказать. Так зачем нам ГПСЧ? Если говорить про безопасность, то ГПСЧ?—?это проблема. Если говорить про другие задачи, то эти свойства?—?могут сыграть в плюс. Например для различных спец эффектов и анимаций графики может понадобиться частый вызов random. И вот тут важны распределение значений и перформанс! Секурные алгоритмы не могут похвастать скоростью работы.
Еще одно свойство?—?воспроизводимость. Некоторые реализации позволяют задать seed, и это очень полезно, если последовательность должна повторяться. Воспроизведение нужно в тестах, например. И еще много других вещей существует, для которых не нужен безопасный ГСЧ.
Как устроен Math.random()
Метод Math.random() возвращает псевдослучайное число с плавающей запятой из диапазона [0, 1), то есть, от 0 (включительно) до 1 (но не включая 1), которое затем можно отмасштабировать до нужного диапазона. Реализация сама выбирает начальное зерно для алгоритма генерации случайных чисел; оно не может быть выбрано или сброшено пользователем.
Как устроен алгоритм Math.random()?—?интересный вопрос. До недавнего времени, а именно до 49 Chrome использовался алгоритм MWC1616:
uint32_t state0 = 1;
uint32_t state1 = 2;
uint32_t mwc1616() {
state0 = 18030 * (state0 & 0xffff) + (state0 >> 16);
state1 = 30903 * (state1 & 0xffff) + (state1 >> 16);
return (state0 << 16) + (state1 & 0xffff);
}
Именно этот алгоритм генерит нам последовательность псевдослучайных чисел в промежутке между 0 и 1.
Предсказываем Math.random()
Чем это было чревато? Есть такой квест: alf.nu/ReturnTrue
В нем есть задача:
{
let rand = Math.random();
function random4(x) {
return rand === x;
}
}
random4(???)
Что нужно вписать вместо вопросов, чтобы функция вернула true? Кажется что это невозможно. Но, это возможно, если вы заглядывали в спеку и видели алгоритм ГПСЧ V8. Решение этой задачи в свое время мне показал Роман Дворнов:
random4(function(){var A=18030,B=36969,F=65535,Z=16,M=Math,I=M.imul,c=M.random,M=M.pow(2,32),k,d,g=c()*M,h=c()*M;for(k=0;F^k&&(c=I(A,g>>>Z)+k++)&F^h>>>Z;);for(k=0;F^k&&(d=I(B,g&F)+k++)&F^h&F;);for(k=2;k—;g=c<<Z|d&F)c=c/A|c%A<<Z,d=d/B|d%B<<Z;return(g<0?g+M:g)/M}())
Этот код работал в 70% случаев для Chrome < 49 и Node.js < 5. Рома Дворнов, как всегда, показал чудеса магии, которая не что иное, как глубокое понимание внутренних механизмов браузеров. Я все жду, когда Роман сделает доклад на основе этих событий или напишет более подробную статью.
Что здесь происходит? Все дело в том, что алгоритм можно предсказать. Чтобы это было нагляднее, можно сгенерировать картинку случайных пикселей. На сайте есть такой генератор. Вот что было, когда в браузере был алгоритм MWC1616:
Видите эти равномерности на левом слайде? Изображение показывает проблему с распределением значений. На картинке слева видно, что значения местами сильно группируются, а местами выпадают большие фрагменты. Как следствие?—?числа можно предсказать.
Выходит что мы можем отреверсить Math.random() и предсказать, какое было загадано число на основе того, что получили в данный момент времени. Для этого получаем два значения через Math.random(). Затем вычисляем внутреннее состояние по этим значениям. Имея внутреннее состояние можем предсказывать следующие значения Math.random() при этом не меняя внутреннее состояние. Меняем код так так, чтобы вместо следующего возвращалось предыдущее значение. Собственно все это и описано в коде-решении для задачи random4. Но потом алгоритм изменили (подробности читайте в спеке). Его можно будет сломать, как только у нас в JS появится нормальная работа с 64 битными числами. Но это уже будет другая история.
Новый алгоритм выглядит так:
uint64_t state0 = 1;
uint64_t state1 = 2;
uint64_t xorshift128plus() {
uint64_t s1 = state0;
uint64_t s0 = state1;
state0 = s0;
s1 ^= s1 << 23;
s1 ^= s1 >> 17;
s1 ^= s0;
s1 ^= s0 >> 26;
state1 = s1;
return state0 + state1;
}
Его все так же можно будет просчитать и предсказать. Но пока у нас нет “длинной математики” в JS. Можно попробовать через TypedArray сделать или использовать специальные библиотеки. Возможно кто-то однажды снова напишет предсказатель. Возможно это будешь ты, читатель. Кто знает
Сrypto Random Values
Метод Math.random() не предоставляет криптографически стойкие случайные числа. Не используйте его ни для чего, связанного с безопасностью. Вместо него используйте Web Crypto API (API криптографии в вебе) и более точный метод window.crypto.getRandomValues().
Пример генерации случайного числа:
let [rvalue] = crypto.getRandomValues(new Uint8Array(1));
console.log( rvalue )
Но, в отличие от ГПСЧ Math.random(), этот метод очень ресурсоемкий. Дело в том, что данный генератор использует системные вызовы в ОС, чтобы получить доступ к источникам энтропии (мак адрес, цпу, температуре, etc…).
Добавлено 3 июня 2021 в 21:55
Возможность генерировать случайные числа может быть полезна в определенных видах программ, особенно в играх, программах статистического моделирования и научных симуляторах, которые должны моделировать случайные события. Возьмем, к примеру, игры – без случайных событий монстры всегда будут атаковать вас одинаково, вы всегда найдете одно и то же сокровище, расположение подземелий никогда не изменится и т.д. И это не сделает игру очень хорошей.
Так как же нам генерировать случайные числа? В реальной жизни мы часто получаем случайные результаты, например, подбрасывая монету, бросая кости или тасуя колоду карт. Эти события включают в себя так много физических переменных (например, гравитацию, трение, сопротивление воздуха, импульс и т.д.), что их почти невозможно предсказать или контролировать, и они дают результаты, которые во всех смыслах случайны.
Однако компьютеры не предназначены для использования преимуществ физических переменных – ваш компьютер не может подбрасывать монету, бросать кости или тасовать реальные карты. Компьютеры живут в управляемом электрическом мире, где всё двоично (ложь или истина) и нет промежуточного состояния. По самой своей природе компьютеры предназначены для получения максимально предсказуемых результатов. Когда вы говорите компьютеру вычислить 2 + 2, вы всегда хотите, чтобы ответ был 4. А не 3 или 5 в отдельных случаях.
Следовательно, компьютеры обычно не способны генерировать случайные числа. Вместо этого они должны моделировать случайность, что чаще всего делается с помощью генераторов псевдослучайных чисел.
Генератор псевдослучайных чисел (ГПСЧ или PRNG, pseudo-random number generator) – это программа, которая принимает начальное число (англ. seed) и выполняет над ним математические операции, чтобы преобразовать его в какое-то другое число, которое, кажется, не связано с начальным числом. Затем он берет это сгенерированное число и выполняет с ним ту же математическую операцию, чтобы преобразовать его в новое число, не связанное с числом, из которого оно было сгенерировано. Постоянно применяя алгоритм к последнему сгенерированному числу, он может генерировать последовательность новых чисел, которые будут казаться случайными, если алгоритм достаточно сложен.
Лучшая практика
Предоставить начальное значение своему генератору случайных чисел вы должны только один раз. Если ввести его более одного раза, результаты будут менее случайными или совсем не случайными.
На самом деле написать ГПСЧ довольно просто. Вот короткая программа, которая генерирует 100 псевдослучайных чисел:
#include <iostream>
unsigned int PRNG()
{
// наше начальное число - 5323
static unsigned int seed{ 5323 };
// Берём текущее порождающее значение и генерируем из него новое значение
// Из-за того, что мы используем большие константы и переполнение,
// будет сложно случайно предсказать, какое получится следующее число
// из предыдущего.
seed = 8253729 * seed + 2396403;
// Берём число и возвращаем значение от 0 до 32767
return seed % 32768;
}
int main()
{
// Напечатать 100 случайных чисел
for (int count{ 1 }; count <= 100; ++count)
{
std::cout << PRNG() << 't';
// Если мы напечатали 5 чисел, начинаем новую строку
if (count % 5 == 0)
std::cout << 'n';
}
return 0;
}
Результат этой программы:
23070 27857 22756 10839 27946
11613 30448 21987 22070 1001
27388 5999 5442 28789 13576
28411 10830 29441 21780 23687
5466 2957 19232 24595 22118
14873 5932 31135 28018 32421
14648 10539 23166 22833 12612
28343 7562 18877 32592 19011
13974 20553 9052 15311 9634
27861 7528 17243 27310 8033
28020 24807 1466 26605 4992
5235 30406 18041 3980 24063
15826 15109 24984 15755 23262
17809 2468 13079 19946 26141
1968 16035 5878 7337 23484
24623 13826 26933 1480 6075
11022 19393 1492 25927 30234
17485 23520 18643 5926 21209
2028 16991 3634 30565 2552
20971 23358 12785 25092 30583
Каждое число кажется довольно случайным по сравнению с предыдущим. Как оказалось, наш алгоритм на самом деле не очень хорош по причинам, которые мы обсудим позже. Но он эффективно иллюстрирует принцип генерации значений генератором псевдослучайных чисел.
Генерация случайных чисел в C++
C (и, как следствие, C++) поставляется со встроенным генератором псевдослучайных чисел. Он реализован как две отдельные функции, которые находятся в заголовке cstdlib:
std::srand()
устанавливает начальное порождающее значение в значение, которое передается вызывающей функцией. std::srand()
необходимо вызывать только один раз, в начале вашей программы. Обычно это делается в верхней части main()
.
std::rand()
генерирует следующее случайное число в последовательности. Это число будет псевдослучайным целым числом от 0 до RAND_MAX
, значение константы в cstdlib, которое обычно установлена на 32767.
Вот пример программы, использующей эти функции:
#include <iostream>
#include <cstdlib> // для std::rand() и std::srand()
int main()
{
std::srand(5323); // устанавливаем начальное порождающее значение 5323
// Из-за недостатков некоторых компиляторов нам нужно вызвать std::rand()
// один раз здесь, чтобы получить "лучшие" случайные числа.
std::rand();
// Вывести 100 случайных чисел
for (int count{ 1 }; count <= 100; ++count)
{
std::cout << std::rand() << 't';
// Если мы напечатали 5 чисел, начинаем новую строку
if (count % 5 == 0)
std::cout << 'n';
}
return 0;
}
Вот результат этой программы:
17421 8558 19487 1344 26934
7796 28102 15201 17869 6911
4981 417 12650 28759 20778
31890 23714 29127 15819 29971
1069 25403 24427 9087 24392
15886 11466 15140 19801 14365
18458 18935 1746 16672 22281
16517 21847 27194 7163 13869
5923 27598 13463 15757 4520
15765 8582 23866 22389 29933
31607 180 17757 23924 31079
30105 23254 32726 11295 18712
29087 2787 4862 6569 6310
21221 28152 12539 5672 23344
28895 31278 21786 7674 15329
10307 16840 1645 15699 8401
22972 20731 24749 32505 29409
17906 11989 17051 32232 592
17312 32714 18411 17112 15510
8830 32592 25957 1269 6793
Последовательности ГПСЧ и заполнение
Если вы запустите приведенный выше пример программы с std::rand()
несколько раз, вы заметите, что она каждый раз выводит один и тот же результат! Это означает, что хотя каждое число в последовательности кажется случайным по сравнению с предыдущими, вся последовательность вовсе не случайна! А это означает, что наша программа оказывается полностью предсказуемой (одни и те же входные данные всегда приводят к одним и тем же выходным данным). Бывают случаи, когда это может быть полезно или даже желательно (например, вы хотите, чтобы научное моделирование повторялось, или вы пытаетесь выяснить, почему ваш генератор случайных подземелий дает сбой).
Но часто это не то, что нужно. Если вы пишете игру «hi-lo» (высоко-низко) (где у пользователя есть 10 попыток угадать число, а компьютер сообщает ему, является ли его предположение слишком высоким или слишком низким), вы не хотите, чтобы программа каждый раз выбирала одни и те же числа. Итак, давайте подробнее посмотрим, почему это происходит, и как мы можем это исправить.
Помните, что каждое число в последовательности ГПСЧ определенным образом генерируется из предыдущего числа. Таким образом, при любом одном и том же начальном числе ГПСЧ всегда будут генерировать одну и ту же последовательность чисел! Мы получаем ту же последовательность, потому что наше начальное число всегда 5323.
Чтобы сделать всю нашу последовательность случайной, нам нужен способ выбрать начальное число, которое не является фиксированным значением. Первый ответ, который, вероятно, приходит в голову, – нам нужно случайное число! Это хорошая мысль, но если нам нужно случайное число для генерации случайных чисел, тогда мы попадаем в уловку-22. Оказывается, нам на самом деле не нужно, чтобы наше начальное число было случайным – нам просто нужно выбирать что-то, что меняется при каждом запуске программы. Затем мы можем использовать наш ГПСЧ для генерации уникальной последовательности псевдослучайных чисел из этого начального числа.
Общепринятым методом для этого является использование системных часов. Каждый раз, когда пользователь запускает программу, время будет другим. Если мы используем это значение времени в качестве начального числа, тогда наша программа при каждом запуске будет генерировать другую последовательность чисел!
C поставляется с функцией std::time()
, которая возвращает количество секунд с полуночи 1 января 1970 года. Чтобы использовать ее, нам просто нужно включить заголовок ctime, а затем инициализировать std::srand()
с помощью вызов std::time(nullptr)
. Мы еще не рассмотрели nullptr
, но в данном контексте это эквивалент 0.
Вот та же программа, что и выше, но с вызовом time()
в качестве начального числа:
#include <iostream>
#include <cstdlib> // для std::rand() и std::srand()
#include <ctime> // для std::time()
int main()
{
// устанавливаем начальное значение в значение системных часов
std::srand(static_cast<unsigned int>(std::time(nullptr)));
for (int count{ 1 }; count <= 100; ++count)
{
std::cout << std::rand() << 't';
// Если мы напечатали 5 чисел, начинаем новую строку
if (count % 5 == 0)
std::cout << 'n';
}
return 0;
}
Теперь наша программа будет каждый раз генерировать новую последовательность случайных чисел! Запустите ее пару раз и убедитесь сами.
Генерация случайных чисел между двумя произвольными значениями
Как правило, нам не нужны случайные числа от 0 до RAND_MAX
– нам нужны числа между двумя другими значениями, которые мы назовем min
и max
. Например, если мы пытаемся имитировать, как пользователь бросает кубик, нам нужны случайные числа от 1 до 6.
Вот короткая функция, которая преобразует результат rand()
в нужный диапазон:
// Генерируем случайное число от min до max (включительно)
// Предполагается, что std::srand() уже был вызван
// Предполагается, что max - min <= RAND_MAX
int getRandomNumber(int min, int max)
{
// static используется для повышения эффективности, поэтому
// мы вычисляем это значение только один раз
static constexpr double fraction { 1.0 / (RAND_MAX + 1.0) };
// равномерно распределяем случайные числа по нашему диапазону
return min + static_cast<int>((max - min + 1) * (std::rand() * fraction));
}
Чтобы смоделировать бросок кубика, мы должны вызвать getRandomNumber(1, 6)
. Чтобы выбрать случайную цифру, мы вызываем getRandomNumber(0, 9)
.
Дополнительный материал: как работает предыдущая функция?
Функция getRandomNumber()
может показаться немного сложной, но всё не так уж плохо.
Вернемся к нашей цели. Функция rand()
возвращает число от 0 до RAND_MAX
(включительно). Мы хотим каким-то образом преобразовать результат rand()
в число от min
до max
(включительно). Это означает, что когда мы выполняем преобразование, 0 должен стать min
, а RAND_MAX
должен стать max
с равномерным распределением чисел между ними.
Мы делаем это в пять этапов:
- Умножаем наш результат от
std::rand()
на дробьfraction
. Это преобразует результатrand()
в число с плавающей запятой от 0 (включительно) до 1 (не включая). - Если
rand()
возвращает 0, тогда0 * fraction
по-прежнему равно 0. Еслиrand()
возвращаетRAND_MAX
, тогдаRAND_MAX * fraction
равноRAND_MAX / (RAND_MAX + 1)
, что немного меньше 1. Любые другие числа, возвращаемые функциейrand()
будут равномерно распределены между этими двумя точками. - Затем нам нужно знать, сколько чисел мы можем вернуть. Другими словами, сколько чисел находится между
min
(включительно) иmax
(включительно)? - Это просто (
max - min + 1
). Например, еслиmax
= 8 иmin
= 5, (max
—min
+ 1) = (8 — 5 + 1) = 4. Между 5 и 8 есть 4 числа (то есть 5, 6, 7 и 8). - Умножаем два предыдущих результата вместе. Если у нас было число с плавающей запятой от 0 (включительно) до 1 (не включая), а затем мы умножили его на (
max - min + 1
), теперь у нас есть число с плавающей запятой между 0 (включительно) и (max - min + 1
) (не включая). - Преобразуем предыдущий результат в целое число. Это удаляет любую дробную составляющую, оставляя нам целочисленный результат от 0 (включительно) до (
max
—min
) (включительно). - Наконец, мы добавляем
min
, что переводит наш результат в целое число отmin
(включительно) доmax
(включительно).
Дополнительный материал: почему в предыдущей функции мы не используем оператор остатка от деления (%
)?
Один из наиболее частых вопросов, которые задают читатели, – почему мы используем деление в приведенной выше функции вместо взятия остатка от деления (%
). Короткий ответ заключается в том, что метод с остатком от деления имеет тенденцию смещаться в пользу малых чисел.
Давайте посмотрим, что бы произошло, если бы вместо этого приведенная выше функция выглядела так:
return min + (std::rand() % (max-min+1));
Похожа, правда? Давайте разберемся, где всё идет не по плану. Чтобы упростить пример, скажем, что rand()
всегда возвращает случайное число от 0 до 9 (включительно). В нашем примере мы выберем min
= 0 и max
= 6. Таким образом, max - min + 1
равно 7.
Теперь давайте посчитаем все возможные исходы:
0 + (0 % 7) = 0
0 + (1 % 7) = 1
0 + (2 % 7) = 2
0 + (3 % 7) = 3
0 + (4 % 7) = 4
0 + (5 % 7) = 5
0 + (6 % 7) = 6
0 + (7 % 7) = 0
0 + (8 % 7) = 1
0 + (9 % 7) = 2
Посмотрите на распределение результатов. Результаты с 0 по 2 появляются дважды, а с 3 по 6 – только один раз. Этот метод имеет явный уклон в сторону низких результатов. Экстраполируя это: большинство случаев, связанных с этим алгоритмом, будут вести себя аналогичным образом.
Теперь давайте посмотрим на результат функции getRandomNumber()
выше, используя те же параметры, что и выше (rand()
возвращает число от 0 до 9 (включительно), min
= 0 и max
= 6). В этом случае fraction
= 1 / (9 + 1) = 0,1. max - min + 1
по-прежнему 7.
Расчет всех возможных исходов:
0 + static_cast(7 * (0 * 0.1))) = 0 + static_cast(0) = 0
0 + static_cast(7 * (1 * 0.1))) = 0 + static_cast(0.7) = 0
0 + static_cast(7 * (2 * 0.1))) = 0 + static_cast(1.4) = 1
0 + static_cast(7 * (3 * 0.1))) = 0 + static_cast(2.1) = 2
0 + static_cast(7 * (4 * 0.1))) = 0 + static_cast(2.8) = 2
0 + static_cast(7 * (5 * 0.1))) = 0 + static_cast(3.5) = 3
0 + static_cast(7 * (6 * 0.1))) = 0 + static_cast(4.2) = 4
0 + static_cast(7 * (7 * 0.1))) = 0 + static_cast(4.9) = 4
0 + static_cast(7 * (8 * 0.1))) = 0 + static_cast(5.6) = 5
0 + static_cast(7 * (9 * 0.1))) = 0 + static_cast(6.3) = 6
Здесь всё еще есть небольшое смещение в сторону меньших чисел (0, 2 и 4 появляются дважды, тогда как 1, 3, 5 и 6 появляются один раз), но результаты распределены гораздо более равномерно.
Несмотря на то, что getRandomNumber()
немного сложнее для понимания, чем альтернатива с остатком от деления, мы выступаем за метод с делением, потому что он дает менее предсказуемый результат.
Что такое хороший генератор псевдослучайных чисел?
Как я уже упоминал выше, ГПСЧ, который мы написали, не очень хороший. В этом разделе мы обсудим причины, почему. Это дополнительный материал, потому что он не имеет прямого отношения к C или C++, но если вам нравится программировать, вам, вероятно, всё равно будет интересно.
Чтобы быть хорошим, ГПСЧ должен обладать рядом свойств:
Во-первых, ГПСЧ должен генерировать каждое число примерно с одинаковой вероятностью. Это называется равномерностью распределения. Если одни числа генерируются чаще других, результат программы, использующей ГПСЧ, будет искажен!
Например, предположим, вы пытаетесь написать генератор случайных предметов для игры. Вы выбираете случайное число от 1 до 10, и если результат равен 10, с монстра выпадет мощный предмет вместо обычного. Вы ожидаете, что это произойдет с вероятностью 1 из 10. Но если результаты простого ГПСЧ распределены неравномерно, и он генерирует намного больше десяток, чем должен, ваши игроки в конечном итоге получат больше редких предметов, чем вы предполагали, что, возможно, упростит вашу игру.
Создание ГПСЧ, дающих равномерно распределенные результаты, является сложной задачей, и это одна из основных причин, по которой ГПСЧ, который мы написали в начале этого урока, не очень хороший.
Во-вторых, метод, с помощью которого генерируется следующее число в последовательности, не должен быть очевидным или предсказуемым. Например, рассмотрим следующий алгоритм ГПСЧ: число = число + 1. Результаты этого ГПСЧ идеально равномерно распределены, но они не очень полезны в качестве последовательности случайных чисел!
В-третьих, ГПСЧ должен иметь хорошее пространственное распределение чисел. Это означает, что он должен возвращать низкие, средние и высокие значения, казалось бы, случайным образом. ГПСЧ, который вернул все низкие значения, а затем все высокие значения, может быть равномерно распределенным и непредсказуемыми, но он всё равно приведет к смещенным результатам, особенно если количество случайных чисел, которые вы реально используете, невелико.
В-четвертых, все ГПСЧ являются периодическими, что означает, что в какой-то момент сгенерированная последовательность чисел начнет повторяться. Длина последовательности до того, как ГПСЧ начинает повторяться, называется периодом.
Например, вот первые 100 чисел, сгенерированных из ГПСЧ с плохой периодичностью:
112 9 130 97 64
31 152 119 86 53
20 141 108 75 42
9 130 97 64 31
152 119 86 53 20
141 108 75 42 9
130 97 64 31 152
119 86 53 20 141
108 75 42 9 130
97 64 31 152 119
86 53 20 141 108
75 42 9 130 97
64 31 152 119 86
53 20 141 108 75
42 9 130 97 64
31 152 119 86 53
20 141 108 75 42
9 130 97 64 31
152 119 86 53 20
141 108 75 42 9
Вы можете заметить, что он сгенерировал 9 как второе число и снова 9 как 16-е число. ГПСЧ застревает, постоянно генерируя последовательность между этими двумя девятками: 9-130-97-64-31-152-119-86-53-20-141-108-75-42- (повтор).
Это происходит потому, что ГПСЧ детерминированы – при некотором наборе входных значений они каждый раз будут выдавать одно и то же выходное значение. Это означает, что как только ГПСЧ встречает набор входных данных, которые он использовал ранее, он начинает производить ту же последовательность выходных данных, которую он создавал раньше, что приводит к циклу.
Хороший ГПСЧ должен иметь длительный период для всех начальных значений. Разработка алгоритма, отвечающего этому свойству, может быть чрезвычайно сложной задачей – большинство ГПСЧ будут иметь длительные периоды для одних начальных значений и короткие периоды для других. Если пользователь выберет начальное число с коротким периодом, тогда ГПСЧ не будет работать хорошо.
Несмотря на сложность разработки алгоритмов, отвечающих всем этим критериям, в этой области было проведено множество исследований из-за ее важности для научных вычислений.
std::rand()
– посредственный ГПСЧ
Алгоритм, используемый для реализации std::rand()
, может варьироваться от компилятора к компилятору, что приводит к результатам, которые могут не совпадать между компиляторами. В большинстве реализаций rand()
используется метод, называемый линейным конгруэнтным генератором (LCG, Linear Congruential Generator). Если вы посмотрите на первый пример в этом уроке, вы заметите, что на самом деле это LCG, хотя и с намеренно выбранными плохими константами. LCG, как правило, имеют недостатки, из-за которых они не подходят для решения большинства проблем.
Одним из основных недостатков rand()
является то, что RAND_MAX
обычно устанавливается равным 32767 (по сути, 15 бит). Это означает, что если вы хотите генерировать числа в большем диапазоне (например, 32-битные целые числа), rand()
не подходит. Кроме того, rand()
не подходит, если вы хотите генерировать случайные числа с плавающей запятой (например, от 0,0 до 1,0), что часто бывает полезно при статистическом моделировании. Наконец, rand()
имеет относительно короткий период по сравнению с другими алгоритмами.
Тем не менее, rand()
идеально подходит для обучения программированию и для программ, в которых высококачественный ГПСЧ не является необходимостью.
Для приложений, где полезен высококачественный ГПСЧ, я бы порекомендовал вихрь Мерсенна (Mersenne Twister) (или один из его вариантов), который дает отличные результаты и относительно прост в использовании. Вихрь Мерсенна был введен в C++11, и мы покажем, как его использовать позже в этом уроке.
Отладка программ, использующих случайные числа
Программы, использующие случайные числа, может быть трудно отлаживать, потому что программа при каждом запуске может вести себя по-разному. Иногда это может сработать, а иногда нет. При отладке полезно следить за тем, чтобы ваша программа каждый раз выполняла один и тот же (неправильный) путь. Таким образом, вы можете запускать программу столько раз, сколько необходимо, чтобы определить причину ошибки.
По этой причине при отладке полезно установить случайное начальное число (через std::srand
) на определенное значение (например, 0), которое вызывает ошибочное поведение. Это гарантирует, что ваша программа каждый раз будет генерировать одни и те же результаты, что упростит отладку. Как только вы обнаружите ошибку, вы можете снова использовать системные часы, чтобы снова начать генерировать рандомизированные результаты.
Получение лучших случайных чисел с помощью вихря Мерсенна
В C++11 в стандартную библиотеку C++ добавлено множество функций генерации случайных чисел, включая алгоритм вихря Мерсенна, а также генераторы для различных типов случайных распределений (равномерного, нормального, пуассоновского и т.д.). Доступ к нему осуществляется через заголовок <random>
.
Вот небольшой пример, показывающий, как сгенерировать случайные числа в C++11 с помощью вихря Мерсенна:
#include <iostream>
#include <random> // для std::mt19937
#include <ctime> // для std::time
int main()
{
// Инициализируем наш вихрь Мерсенна случайным начальным значением на основе часов
std::mt19937 mersenne{ static_cast<std::mt19937::result_type>(std::time(nullptr)) };
// Создаем многоразовый генератор случайных чисел,
// который равномерно генерирует числа от 1 до 6
std::uniform_int_distribution die{ 1, 6 };
// Если ваш компилятор не поддерживает C++17, вместо этого используйте следующее
// std::uniform_int_distribution<> die{ 1, 6 };
// Распечатываем пачку случайных чисел
for (int count{ 1 }; count <= 48; ++count)
{
std::cout << die(mersenne) << 't'; // здесь генерируем результат броска кубика
// Если мы напечатали 6 чисел, начинаем новую строку
if (count % 6 == 0)
std::cout << 'n';
}
return 0;
}
Примечание автора
До C++17 вам нужно было после типа добавить пустые скобки для создания die
std::uniform_int_distribution<> die{ 1, 6 }
Вы можете заметить, что вихрь Мерсенна генерирует случайные 32-битные целочисленные значения без знака (а не 15-битные целочисленные значения, как std::rand()
), что дает гораздо больший диапазон. Также существует версия (std::mt19937_64
) для генерации 64-битных целочисленных значений без знака.
Случайные числа в нескольких функциях
В приведенном выше примере генератор псевдослучайных чисел создается для использования в одной функции. Что произойдет, если мы захотим использовать генератор случайных чисел в нескольких функциях?
Хотя вы можете создать статическую локальную переменную std::mt19937
в каждой функции, которая в ней нуждается (статическая, чтобы она была инициализирована только один раз); но это немного излишне – чтобы каждая функция получала начальное значение для генератора случайных чисел и поддерживала свой собственный локальный генератор. В большинстве случаев лучшим вариантом является создание глобального генератора случайных чисел (внутри пространства имен!). Помните, как мы говорили вам избегать неконстантных глобальных переменных? Это исключение (также обратите внимание: std::rand()
и std::srand()
обращаются к глобальному объекту, поэтому для этого есть прецедент).
#include <iostream>
#include <random> // для std::mt19937
#include <ctime> // для std::time
namespace MyRandom
{
// Инициализируем наш вихрь Мерсенна случайным значением на основе часов
// (один раз при запуске программы)
std::mt19937 mersenne{ static_cast<std::mt19937::result_type>(std::time(nullptr)) };
}
int getRandomNumber(int min, int max)
{
// мы можем создать распределение в любой функции, которая в нем нуждается
std::uniform_int_distribution die{ min, max };
// а затем генерировать случайное число из нашего глобального генератора
return die(MyRandom::mersenne);
}
int main()
{
std::cout << getRandomNumber(1, 6) << 'n';
std::cout << getRandomNumber(1, 10) << 'n';
std::cout << getRandomNumber(1, 20) << 'n';
return 0;
}
Использование библиотеки генерирования случайных чисел
Возможно, лучшее решение – использовать стороннюю библиотеку, которая обрабатывает всё это за вас, например, библиотеку генерирования случайных чисел Effolkronium, использующую только заголовочные файлы. Вы просто добавляете заголовочный файл в свой проект, включаете его с помощью #include
, после чего можете начать генерировать случайные числа с помощью Random::get(min, max)
.
Вот приведенная выше программа, модифицированная под использование библиотеки Effolkronium:
#include <iostream>
#include "random.hpp"
// получаем псевдоним генератора случайных чисел, который автоматически
// заполняется и имеет статические API и внутреннее состояние
using Random = effolkronium::random_static;
int main()
{
std::cout << Random::get(1, 6) << 'n';
std::cout << Random::get(1, 10) << 'n';
std::cout << Random::get(1, 20) << 'n';
return 0;
}
Помогите! Мой генератор случайных чисел генерирует одну и ту же последовательность случайных чисел!
Если ваш генератор случайных чисел при каждом запуске вашей программы генерирует одну и ту же последовательность случайных чисел, вы, вероятно, неправильно его инициализировали. Убедитесь, что вы инициализируете его значением, которое изменяется при каждом запуске программы (например, std::time(nullptr)
).
Помогите! Мой генератор случайных чисел всегда генерирует одно и то же первое число!
Реализация rand()
в Visual Studio и некоторых других компиляторах имеет недостаток – первое сгенерированное случайное число не сильно меняется для похожих начальных значений. Это означает, что при использовании std::time(nullptr)
для инициализации вашего генератора случайных чисел первый результат rand()
не сильно изменится при последующих запусках. Однако на результаты последующих вызовов rand()
это не повлияет, и они будут достаточно рандомизированы.
Решение здесь, и хорошее практическое правило в целом, – отбросить первое число, сгенерированное генератором случайных чисел.
Помогите! Мой генератор случайных чисел вообще не генерирует случайные числа!
Если ваш генератор случайных чисел генерирует одно и то же число каждый раз, когда вы запрашиваете случайное число, то вы, вероятно, либо повторно инициализируете генератор случайных чисел перед генерацией случайного числа, либо создаете новый генератор случайных чисел для каждого получения случайного числа.
Вот две функции, которые показывают проблему:
// Это та же функция, которую мы показали ранее
int getRandomNumber(int min, int max)
{
static constexpr double fraction { 1.0 / (RAND_MAX + 1.0) };
return min + static_cast<int>((max - min + 1) * (std::rand() * fraction));
}
int rollDie()
{
std::srand(static_cast<unsigned int>(std::time(nullptr)));
return getRandomNumber(1, 6);
}
int getOtherRandomNumber()
{
std::mt19937 mersenne{ static_cast<std::mt19937::result_type>(std::time(nullptr)) };
std::uniform_int_distribution rand{ 1, 52 };
return rand(mersenne);
}
В обоих случаях генератор случайных чисел инициализируется перед каждой генерацией случайного числа. Это приведет к тому, что каждый раз будет генерироваться одно и то же число.
В верхнем случае std::srand()
повторно инициализирует встроенный генератор случайных чисел перед вызовом rand()
(с помощью getRandomNumber()
).
В нижнем случае мы создаем новый вихрь Мерсенна, инициализируем его, генерируем одно случайное число и затем уничтожаем его.
Для получения случайных результатов вы должны инициализировать генератор случайных чисел только один раз (обычно при инициализации программы для std::srand()
или в точке создания для других генераторов случайных чисел), а затем использовать тот же генератор случайных чисел для каждого последующего генерируемого случайного числа.
Предупреждение
Пример getOtherRandomNumber()
– одна из самых распространенных ошибок, которые допускаются в тестах. Вы не заметите, что getOtherRandomNumber()
не работает, пока вы не начнете вызывать его чаще, чем один раз в секунду (поскольку начальное число изменяется только один раз в секунду). Не забудьте сделать генератор случайных чисел статическим или объявить его вне функции.
Теги
C++ / CppLearnCppВихрь Мерсенна / Mersenne TwisterГенератор псевдослучайных чисел (ГПСЧ) / Pseudo-random number generator (PRNG)Для начинающихОбучениеПрограммирование
В статье вы узнаете, как использовать std.random и чем он хорош
Содержание
В 2011 году новый стандарт C++11 добавил в язык заголовок <random>
, в котором описаны средства для работы со случайными числами на профессиональном уровне. Эти средства заменяют функцию rand и дают гораздо больше гибкости.
Но чтобы этим воспользоваться, надо немного разбираться в теории.
Почему в IT все числа неслучайные
Казалось бы, что мешает использовать в программах случайные числа? К сожалению, процессор на это не способен: его поведение строго детерминировано и не допускает никаких случайностей.
- Для генерации по-настоящему случайных, ничем не связанных чисел операционной системе приходится использовать средства, недоступные обычным приложениям; такие случайные числа называются криптографически стойкими случайными числами
- Генерация системой таких случайных чисел работает медленно, и при нехватке скорости система просто отдаёт приложениями псевдослучайные числа либо заставляет их ожидать, пока появится возможность вернуть случайное число
Вы можете узнать об этом подробнее, прочитав о разнице между устройствами
/dev/random
и/dev/urandom
в ОС Linux
Pseudo-random Numbers Generator (PRNG)
Генератор псевдослучайных чисел (PRNG) — это алгоритм генерации последовательности чисел, похожих на случайные числа. Псевдослучайные числа не являются по-настоящему случайные, т.е. между ними остаются связывающие их закономерности.
Общий принцип генерации легко показать в примере:
#include <iostream>
int main()
{
unsigned value = 18;
// Порождаем и выводим 20 чисел, используя число 18 как зерно
for (int i = 0; i < 20; ++i)
{
// Итеративно вычисляем новое значение value.
value = (value * 73129 + 95121) % 100000;
std::cout << value << std::endl;
}
}
Несложная итеративная формула, содержащая умножение, сложение и деление по остатку на разные константы, создаёт целую серию чисел, похожих на случайные:
11443
10268
83693
13222
6759
74032
13953
64058
25307
70724
3221
43630
13391
65560
65065
66210
98915
82860
96765
55510
Очевидно, что между числами есть взаимосвязь: они вычислены по одной и той же формуле. Но для пользователя видимой взаимосвязи нет.
Время как источник случайности
Если вы запустите предыдущую программу несколько раз, вы обнаружите проблему: числа будут те же самые. Причина проста — в начале последовательности мы используем всегда одно и то же число, 18. Для последовательности это число является зерном (англ. seed), и чтобы последовательность менялась с каждым запуском, зерно должно быть случайным.
Простейший, но не самый лучший способ получения зерна: взять текущее календарное время в секундах. Для этой цели мы воспользуемся функцией std::time_t time(std::time_t* arg).
Функция std::time возвращает число, представляющее время, прошедшее с полуночи 1 января 1970 года. Обычно это время в секундах, также известное как UNIX Timestamp. Параметр
arg
можно игнорировать и передавать нулевой указатель (вы можете в этом убедиться, прочитав документацию).
#include <iostream>
#include <ctime>
int main()
{
unsigned value = unsigned(std::time(nullptr));
// Порождаем и выводим 20 чисел, используя время UNIX как зерно.
for (int i = 0; i < 20; ++i)
{
value = (value * 73129 + 95121) % 100000;
std::cout << value << std::endl;
}
}
Теперь программа при каждом запуске будет выводить разные цепочки псевдослучайных чисел. При условии, что вы запускаете её не чаще одного раза в секунду.
Такие случайные числа далеко не идеальны: например, их нельзя использовать в криптографии, потому что злоумышленник может примерно оценить, в каком промежутке времени были созданы случайные числа, и сильно сократить затраты на дешифровку сообщения. В разработке простых учебных приложений такие числа вполне подходят
Ограничение числа по диапазону
Ограничить числа по диапазону можно путём деления по остатку (что сократит длину входного диапазона) и добавления нижней границы диапазона:
#include <iostream>
#include <ctime>
#include <cassert>
// Структура генератора псевдослучайных чисел хранит одно число,
// зерно следующего случайного числа.
struct PRNG
{
unsigned seed = 0;
};
void initGenerator(PRNG& generator)
{
// Получаем случайное зерно последовательности
generator.seed = unsigned(std::time(nullptr));
}
// Генерирует число на отрезке [minValue, maxValue].
unsigned random(PRNG& generator, unsigned minValue, unsigned maxValue)
{
// Проверяем корректность аргументов
assert(minValue < maxValue);
// Итеративно изменяем текущее число в генераторе
generator.seed = (generator.seed * 73129 + 95121);
// Приводим число к отрезку [minValue, maxValue]
return (generator.seed % (maxValue + 1 - minValue)) + minValue;
}
int main()
{
PRNG generator;
initGenerator(generator);
// Порождаем и выводим 10 чисел на отрезке [0, 7].
std::cout << "ten numbers in range [0, 7]:" << std::endl;
for (int i = 0; i < 10; ++i)
{
std::cout << random(generator, 0, 7) << std::endl;
}
// Порождаем и выводим 10 чисел на отрезке [10, 20].
std::cout << "ten numbers in range [10, 20]:" << std::endl;
for (int i = 0; i < 10; ++i)
{
std::cout << random(generator, 10, 20) << std::endl;
}
}
Всё то же, только лучше: заголовок <random>
Заголовок random разделяет генерацию псевдослучайных чисел на 3 части и предоставляет три инструмента:
- класс std::random_device, который запрашивает у операционной системы почти случайное целое число; этот класс более удачные зёрна, чем если брать текущее время
- класс std::mt19937 и другие классы псевдо-случайных генераторов, задача которых — размножить одно зерно в целую последовательность чисел
- класс std::uniform_int_distribution и другие классы распределений
Класс mt19937 реализует алгоритм размножения псевдослучайных чисел, известный как Вихрь Мерсенна. Этот алгоритм работает быстро и даёт хорошие результаты — гораздо более “случайные”, чем наш самописный метод, показанный ранее.
О распределениях скажем подробнее:
- линейное распределение вероятностей (uniform distribution) возникает, когда вероятность появления каждого из допустимых чисел одинакова, т.е. каждое число может появиться с равным шансом
- в некоторых прикладных задачах нужны другие распределения, в которых одни числа появляются чаще других — например, часто используется нормальное распределение (normal distribution)
В большинстве случаев вам подойдёт линейное распределение. Изредка пригодится нормальное, в котором вероятность появления числе тем ниже, чем дальше оно от среднего значения:
Теперь мы можем переписать
#include <iostream>
#include <random>
#include <cassert>
struct PRNG
{
std::mt19937 engine;
};
void initGenerator(PRNG& generator)
{
// Создаём псевдо-устройство для получения случайного зерна.
std::random_device device;
// Получаем случайное зерно последовательности
generator.engine.seed(device());
}
// Генерирует целое число в диапазоне [minValue, maxValue)
unsigned random(PRNG& generator, unsigned minValue, unsigned maxValue)
{
// Проверяем корректность аргументов
assert(minValue < maxValue);
// Создаём распределение
std::uniform_int_distribution<unsigned> distribution(minValue, maxValue);
// Вычисляем псевдослучайное число: вызовем распределение как функцию,
// передав генератор произвольных целых чисел как аргумент.
return distribution(generator.engine);
}
// Генерирует число с плавающей точкой в диапазоне [minValue, maxValue)
float getRandomFloat(PRNG& generator, float minValue, float maxValue)
{
// Проверяем корректность аргументов
assert(minValue < maxValue);
// Создаём распределение
std::uniform_real_distribution<float> distribution(minValue, maxValue);
// Вычисляем псевдослучайное число: вызовем распределение как функцию,
// передав генератор произвольных целых чисел как аргумент.
return distribution(generator.engine);
}
int main()
{
PRNG generator;
initGenerator(generator);
// Порождаем и выводим 10 чисел на отрезке [0, 7]
std::cout << "ten integer numbers in range [0, 7]:" << std::endl;
for (int i = 0; i < 10; ++i)
{
std::cout << random(generator, 0, 7) << std::endl;
}
// Порождаем и выводим 10 чисел на отрезке [10, 20]
std::cout << "ten float numbers in range [10, 20]:" << std::endl;
for (int i = 0; i < 10; ++i)
{
std::cout << getRandomFloat(generator, 10.f, 20.f) << std::endl;
}
}
Особенность платформы: random_device в MinGW
Согласно стандарту C++, принцип работы std::random_device
отдаётся на откуп разработчикам компилятора и стандартной библиотеки. В компиляторе G++ и его стандартной библиотеке libstdc++
класс random_device правильно работает на UNIX-платформах, но на Windows в некоторых дистрибутивах MinGW вместо случайных зёрен он возвращает одну и ту же константу!
В качестве обходного манёвра мы можем использовать текущее время в качестве зерна случайности. Для этого изменим функцию initGenerator:
#include <iostream>
#include <random>
#include <cassert>
#include <ctime>
struct PRNG
{
std::mt19937 engine;
};
void initGenerator(PRNG& generator)
{
// Используем время с 1 января 1970 года в секундах как случайное зерно
const unsigned seed = unsigned(std::time(nullptr));
generator.engine.seed(seed);
}
Приём №1: выбор случайного значения из предопределённого списка
Допусти, вы хотите случайно выбрать имя для кота. У вас есть список из 10 имён, которые подошли бы коту, но вы хотите реализовать случайный выбор. Достаточно случайно выбрать индекс в массиве имён! Такой же метод подошёл не только для генерации имени, но также для генерации цвета из заранее определённой палитры и для других задач.
Идея проиллюстрирована в коде
#include <iostream>
#include <vector>
#include <string>
#include <random>
#include <ctime>
struct PRNG
{
std::mt19937 engine;
};
void initGenerator(PRNG& generator)
{
// Используем время с 1 января 1970 года в секундах как случайное зерно
const unsigned seed = unsigned(std::time(nullptr));
generator.engine.seed(seed);
}
// Генерирует индекс в диапазоне [0, size)
size_t getRandomIndex(PRNG& generator, size_t size)
{
// Создаём распределение
std::uniform_int_distribution<size_t> distribution(0, size - 1);
// Вычисляем псевдослучайное число: вызовем распределение как функцию,
// передав генератор произвольных целых чисел как аргумент.
return distribution(generator.engine);
}
int main()
{
std::vector<std::string> names = {
"Barsik",
"Murzik",
"Pushok",
"Amor",
"Balu",
"Vert",
"Damar",
"Kamelot",
"Mavrik",
"Napoleon"
};
PRNG generator;
initGenerator(generator);
// Порождаем и выводим 3 случайных имёни из заданного списка
std::cout << "3 random cat names:" << std::endl;
for (int i = 0; i < 3; ++i)
{
const size_t nameIndex = getRandomIndex(generator, names.size());
std::cout << names[nameIndex] << std::endl;
}
}
Приём №2: отбрасываем неподходящее значение
Если предыдущую программу запустить несколько раз, то рано или поздно у вас возникнет ситуация, когда одно и то же имя было выведено дважды. Что, если вам нужно уникальное значение, не выпадавшее прежде за время запуска программы?
Тогда используйте цикл, чтобы запрашивать случайные значения до тех пор, пока очередное значение не попадёт под ваши требования. Будьте аккуратны: если требования нереалистичные, вы получите бесконечный цикл!
Доработаем программу, добавив цикл while в функцию main. Для сохранения уже использованных имён воспользуемся структурой данных std::set из заголовка <set>
, представляющей динамическое множество.
#include <iostream>
#include <vector>
#include <string>
#include <set>
#include <random>
#include <ctime>
struct PRNG
{
std::mt19937 engine;
};
void initGenerator(PRNG& generator)
{
// Используем время с 1 января 1970 года в секундах как случайное зерно
const unsigned seed = unsigned(std::time(nullptr));
generator.engine.seed(seed);
}
// Генерирует индекс в диапазоне [0, size)
size_t getRandomIndex(PRNG& generator, size_t size)
{
// Создаём распределение
std::uniform_int_distribution<size_t> distribution(0, size - 1);
// Вычисляем псевдослучайное число: вызовем распределение как функцию,
// передав генератор произвольных целых чисел как аргумент.
return distribution(generator.engine);
}
int main()
{
std::vector<std::string> names = {
"Barsik",
"Murzik",
"Pushok",
"Amor",
"Balu",
"Vert",
"Damar",
"Kamelot",
"Mavrik",
"Napoleon"
};
PRNG generator;
initGenerator(generator);
// Множество, хранящее индексы использованных имён.
std::set<size_t> usedIndexes;
// Порождаем и выводим 3 случайных имёни из заданного списка
std::cout << "3 random cat names:" << std::endl;
for (int i = 0; i < 3; ++i)
{
size_t nameIndex = 0;
while (true)
{
// Запрашиваем случайный индекс
nameIndex = getRandomIndex(generator, names.size());
// Проверяем, что индекс ранее не встречался
if (usedIndexes.find(nameIndex) == usedIndexes.end())
{
// Если не встречался, добавляем в множество и выходим
// из цикла: уникальный индекс найден
usedIndexes.insert(nameIndex);
break;
}
else
{
// Отладочная печать отброшенного индекса
std::cout << "discard index " << nameIndex << std::endl;
}
}
std::cout << "index: " << nameIndex << std::endl;
std::cout << names[nameIndex] << std::endl;
}
}
Содержание
- 1. Как в C++ сгенерировать случайное число? Функция rand(). Пример
- 2. Функция srand(). Назначение. Пример
- 3. Функция time(). Назначение. Сочетание функций rand(), srand(), time(). Пример
- 4. Как сгенерировать случайное целое число в заданных пределах? Пример
- 5. Заполнение двумерной матрицы случайными целыми числами в указанных пределах. Пример
- 6. Как сгенерировать случайное число с плавающей запятой в указанных пределах? Пример
- Связанные темы
Поиск на других ресурсах:
1. Как в C++ сгенерировать случайное число? Функция rand(). Пример
В языке C++ существуют средства для генерирования случайных чисел. Чтобы сгенерировать случайное число используется функция rand(), которая размещается в библиотечном файле stdlib.h. Синтаксис объявления функции следующий:
int rand();
Функция возвращает случайное целочисленное значение, которое лежит в пределах от 0 до 32767.
Пример.
#include <iostream> #include <stdlib.h> using namespace std; void main() { // Получить случайное число int x; x = rand(); cout << "x = " << x << endl; // Получить еще одно случайное число int y; y = rand(); cout << "y = " << y << endl; }
Результат выполнения программы
x = 41 y = 18467
⇑
2. Функция srand(). Назначение. Пример
Если несколько раз запустить текст программы из п. 1, то будет получен один и тот же результат (одни и те же числа). Значит, сама по себе функция rand() генерирует одни и те же последовательности чисел. Чтобы получить разные последовательности чисел нужно объединить функцию rand() с функцией srand().
Функция srand() из библиотеки stdlib.h предназначена для установки начальной точки, из которой происходит генерирование случайных чисел. Синтаксис объявления функции следующий:
void srand(unsigned int startValue);
здесь startValue – целочисленное значение, которое служит отправной точкой для генерирования последовательности случайных чисел функцией rand(). Изменяя значение startValue, можно получать разные последовательности случайных чисел.
Пример.
#include <iostream> #include <stdlib.h> using namespace std; void main() { // Установить начальную точку генерирования последовательности srand(55); // Получить случайное число int x; x = rand(); cout << "x = " << x << endl; // Получить еще одно случайное число int y; y = rand(); cout << "y = " << y << endl; }
Результат выполнения программы
x = 218 y = 9057
⇑
3. Функция time(). Назначение. Сочетание функций rand(), srand(), time(). Пример
Как видно из примера в п. 2, последовательность случайных чисел изменилась. Если в функции srand() вместо числа 55 установить другое число, то будет получена другая последовательность. Однако, текст программы статический и при многократном запуске программы это число будет неизменным. В результате будет получаться одна и та же последовательность случайных чисел. Во избежание этого недостатка, нужно чтобы стартовое значение в функции srand() постоянно изменялось.
Для того, чтобы в функции srand() получить разные начальные значения используется функция time() из библиотеки time.h.
Если функцию time() вызвать с параметром NULL, то эта функция возвратит количество миллисекунд, которые прошли с 1 января 1970 года. Значит, число миллисекунд будет зависеть от момента времени, в который пользователь запустил программу на выполнение. А этот момент каждый раз будет другим.
Если эти миллисекунды поместить в функцию srand() как показано ниже
srand(time(NULL));
то каждый раз при запуске программы будет создана новая отправная точка в генерировании последовательности чисел функцией rand(). И, как следствие, будут получаться разные последовательности случайных чисел.
Пример. В примере демонстрируется генерирование последовательности из двух случайных чисел. Каждый раз при запуске программы будет получена новая последовательность.
#include <iostream> #include <stdlib.h> #include <time.h> using namespace std; void main() { // Установить начальную точку генерирования последовательности // использовать функцию time(NULL) srand(time(NULL)); // Получить случайное число int x; x = rand(); cout << "x = " << x << endl; // Получить еще одно случайное число int y; y = rand(); cout << "y = " << y << endl; }
Результат выполнения программы
x = 19040 y = 24635
⇑
4. Как сгенерировать случайное целое число в заданных пределах? Пример
В примере приведена функция GetRandomNumber(), которая генерирует случайное число в заданных пределах.
#include <iostream> #include <stdlib.h> // нужен для вызова функций rand(), srand() #include <time.h> // нужен для вызова функции time() using namespace std; // Функция генерирования случайного целочисленного числа в указанных пределах. // Диапазон чисел: [min, max] int GetRandomNumber(int min, int max) { // Установить генератор случайных чисел srand(time(NULL)); // Получить случайное число - формула int num = min + rand() % (max - min + 1); return num; } void main() { // Использование функции GetRandomNumber() int number; number = GetRandomNumber(-10, 10); // Диапазон чисел: [-10, 10] cout << "number = " << number << endl;; }
⇑
5. Заполнение двумерной матрицы случайными целыми числами в указанных пределах. Пример
Условие задачи. Дана двумерная матрица порядка n (n столбцов, n строк) целых чисел. Найти наибольшее из значений элементов, которые размещены в закрашенной части матрицы. Значение элементов матрицы формируются случайным образом и находятся в пределах [-5; +5].
Текст программы следующий
#include <iostream> #include <stdlib.h> // нужен для вызова функции rand(), srand() #include <time.h> // нужен для вызова функции time() using namespace std; void main() { // Двумерные массивы. // Вычислить максимальный элемент нижней части матрицы // 1. Объявление переменных const int MAX_N = 10; // максимально-допустимая размерность матрицы const int MIN_VALUE = -5; // максимальное значение элементов матрицы const int MAX_VALUE = 5; // минимальное значение int A[MAX_N][MAX_N]; // исходная матрица int n; // текущий размер матрицы n*n int max; // результат - максимальное значение // 2. Ввод n cout << "n = "; cin >> n; // 3. Проверка n на корректность if ((n <= 1) || (n > MAX_N)) { cout << "Wrong size of array." << endl; return; } // 4. Формирование матрицы A случайных чисел, // установить генератор случайных чисел srand(time(NULL)); for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) { // взять случайное число A[i][j] = MIN_VALUE + rand() % (MAX_VALUE - MIN_VALUE + 1); } // 5. Вывести массив A для проверки cout << endl << "Array A:" << endl; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) cout << A[i][j] << 't'; cout << endl; } // 6. Вычисление максимального значения bool f_first = true; // флажок первого элемента for (int i = 0; i < n; i++) // i - строки for (int j = 0; j < i; j++) // j - столбцы if (f_first) { // первый элемент принимается максимальным max = A[i][j]; f_first = false; } else { if (max < A[i][j]) max = A[i][j]; } // 7. Вывод результата cout << endl << "max = " << max << endl; }
⇑
6. Как сгенерировать случайное число с плавающей запятой в указанных пределах? Пример
В примере демонстрируется функция GetRandomNumberFloat(), которая генерирует случайное число с плавающей запятой в указанных пределах.
#include <iostream> #include <stdlib.h> // нужен для вызова функции rand(), srand() #include <time.h> // нужен для вызова функции time() using namespace std; // Функция, которая генерирует случайное число с плавающей запятой и указанной точностью // Функция получает 3 параметра: // - min - нижний предел; // - max - верхний предел; // - precision - точность, количество знаков после комы. double GetRandomNumberFloat(double min, double max, int precision) { // Установить стартовую точку srand(time(NULL)); double value; // получить случайное число как целое число с порядком precision value = rand() % (int)pow(10, precision); // получить вещественное число value = min + (value / pow(10, precision)) * (max - min); return value; } void main() { // Использование функции GetRandomNumberFloat() double number; srand(time(NULL)); // Получить число в диапазоне [0; 2] с точностью 2 знака после запятой number = GetRandomNumberFloat(0, 2, 2); cout << "number = " << number << endl; }
⇑
Связанные темы
- Разработка класса Random генерирования случайных чисел
⇑
Given n numbers, each with some frequency of occurrence. Return a random number with probability proportional to its frequency of occurrence.
Example:
Let following be the given numbers. arr[] = {10, 30, 20, 40} Let following be the frequencies of given numbers. freq[] = {1, 6, 2, 1} The output should be 10 with probability 1/10 30 with probability 6/10 20 with probability 2/10 40 with probability 1/10
It is quite clear that the simple random number generator won’t work here as it doesn’t keep track of the frequency of occurrence.
We need to somehow transform the problem into a problem whose solution is known to us.
One simple method is to take an auxiliary array (say aux[]) and duplicate the numbers according to their frequency of occurrence. Generate a random number(say r) between 0 to Sum-1(including both), where Sum represents summation of frequency array (freq[] in above example). Return the random number aux[r] (Implementation of this method is left as an exercise to the readers).
The limitation of the above method discussed above is huge memory consumption when frequency of occurrence is high. If the input is 997, 8761 and 1, this method is clearly not efficient.
How can we reduce the memory consumption? Following is detailed algorithm that uses O(n) extra space where n is number of elements in input arrays.
1. Take an auxiliary array (say prefix[]) of size n.
2. Populate it with prefix sum, such that prefix[i] represents sum of numbers from 0 to i.
3. Generate a random number(say r) between 1 to Sum(including both), where Sum represents summation of input frequency array.
4. Find index of Ceil of random number generated in step #3 in the prefix array. Let the index be indexc.
5. Return the random number arr[indexc], where arr[] contains the input n numbers.
Before we go to the implementation part, let us have quick look at the algorithm with an example:
arr[]: {10, 20, 30}
freq[]: {2, 3, 1}
Prefix[]: {2, 5, 6}
Since last entry in prefix is 6, all possible values of r are [1, 2, 3, 4, 5, 6]
1: Ceil is 2. Random number generated is 10.
2: Ceil is 2. Random number generated is 10.
3: Ceil is 5. Random number generated is 20.
4: Ceil is 5. Random number generated is 20.
5: Ceil is 5. Random number generated is 20.
6. Ceil is 6. Random number generated is 30.
In the above example
10 is generated with probability 2/6.
20 is generated with probability 3/6.
30 is generated with probability 1/6.
How does this work?
Any number input[i] is generated as many times as its frequency of occurrence because there exists count of integers in range(prefix[i – 1], prefix[i]] is input[i]. Like in the above example 3 is generated thrice, as there exists 3 integers 3, 4 and 5 whose ceil is 5.
C++
#include <bits/stdc++.h>
using
namespace
std;
int
findCeil(
int
arr[],
int
r,
int
l,
int
h)
{
int
mid;
while
(l < h)
{
mid = l + ((h - l) >> 1);
(r > arr[mid]) ? (l = mid + 1) : (h = mid);
}
return
(arr[l] >= r) ? l : -1;
}
int
myRand(
int
arr[],
int
freq[],
int
n)
{
int
prefix[n], i;
prefix[0] = freq[0];
for
(i = 1; i < n; ++i)
prefix[i] = prefix[i - 1] + freq[i];
int
r = (
rand
() % prefix[n - 1]) + 1;
int
indexc = findCeil(prefix, r, 0, n - 1);
return
arr[indexc];
}
int
main()
{
int
arr[] = {1, 2, 3, 4};
int
freq[] = {10, 5, 20, 100};
int
i, n =
sizeof
(arr) /
sizeof
(arr[0]);
srand
(
time
(NULL));
for
(i = 0; i < 5; i++)
cout << myRand(arr, freq, n) << endl;
return
0;
}
C
#include <stdio.h>
#include <stdlib.h>
int
findCeil(
int
arr[],
int
r,
int
l,
int
h)
{
int
mid;
while
(l < h)
{
mid = l + ((h - l) >> 1);
(r > arr[mid]) ? (l = mid + 1) : (h = mid);
}
return
(arr[l] >= r) ? l : -1;
}
int
myRand(
int
arr[],
int
freq[],
int
n)
{
int
prefix[n], i;
prefix[0] = freq[0];
for
(i = 1; i < n; ++i)
prefix[i] = prefix[i - 1] + freq[i];
int
r = (
rand
() % prefix[n - 1]) + 1;
int
indexc = findCeil(prefix, r, 0, n - 1);
return
arr[indexc];
}
int
main()
{
int
arr[] = {1, 2, 3, 4};
int
freq[] = {10, 5, 20, 100};
int
i, n =
sizeof
(arr) /
sizeof
(arr[0]);
srand
(
time
(NULL));
for
(i = 0; i < 5; i++)
printf
(
"%dn"
, myRand(arr, freq, n));
return
0;
}
Java
class
GFG
{
static
int
findCeil(
int
arr[],
int
r,
int
l,
int
h)
{
int
mid;
while
(l < h)
{
mid = l + ((h - l) >>
1
);
if
(r > arr[mid])
l = mid +
1
;
else
h = mid;
}
return
(arr[l] >= r) ? l : -
1
;
}
static
int
myRand(
int
arr[],
int
freq[],
int
n)
{
int
prefix[] =
new
int
[n], i;
prefix[
0
] = freq[
0
];
for
(i =
1
; i < n; ++i)
prefix[i] = prefix[i -
1
] + freq[i];
int
r = ((
int
)(Math.random()*(
323567
)) % prefix[n -
1
]) +
1
;
int
indexc = findCeil(prefix, r,
0
, n -
1
);
return
arr[indexc];
}
public
static
void
main(String args[])
{
int
arr[] = {
1
,
2
,
3
,
4
};
int
freq[] = {
10
,
5
,
20
,
100
};
int
i, n = arr.length;
for
(i =
0
; i <
5
; i++)
System.out.println( myRand(arr, freq, n) );
}
}
Python3
import
random
def
findCeil(arr, r, l, h) :
while
(l < h) :
mid
=
l
+
((h
-
l) >>
1
);
if
r > arr[mid] :
l
=
mid
+
1
else
:
h
=
mid
if
arr[l] >
=
r :
return
l
else
:
return
-
1
def
myRand(arr, freq, n) :
prefix
=
[
0
]
*
n
prefix[
0
]
=
freq[
0
]
for
i
in
range
(n) :
prefix[i]
=
prefix[i
-
1
]
+
freq[i]
r
=
random.randint(
0
, prefix[n
-
1
])
+
1
indexc
=
findCeil(prefix, r,
0
, n
-
1
)
return
arr[indexc]
arr
=
[
1
,
2
,
3
,
4
]
freq
=
[
10
,
5
,
20
,
100
]
n
=
len
(arr)
for
i
in
range
(
5
) :
print
(myRand(arr, freq, n))
C#
using
System;
class
GFG{
static
int
findCeil(
int
[] arr,
int
r,
int
l,
int
h)
{
int
mid;
while
(l < h)
{
mid = l + ((h - l) >> 1);
if
(r > arr[mid])
l = mid + 1;
else
h = mid;
}
return
(arr[l] >= r) ? l : -1;
}
static
int
myRand(
int
[] arr,
int
[] freq)
{
int
n = arr.Length;
int
[] prefix =
new
int
[n];
int
i;
prefix[0] = freq[0];
for
(i = 1; i < n; ++i)
prefix[i] = prefix[i - 1] + freq[i];
Random rand =
new
Random();
int
r = rand.Next(prefix[n - 1] + 1;
int
indexc = findCeil(prefix, r, 0, n - 1);
return
arr[indexc];
}
static
void
Main()
{
int
[] arr = { 1, 2, 3, 4 };
int
[] freq = { 10, 5, 20, 100 };
for
(
int
i = 0; i < 5; i++)
Console.WriteLine(myRand(arr, freq));
}
}
Javascript
<script>
function
findCeil(arr, r, l, h)
{
let mid;
while
(l < h)
{
mid = l + ((h - l) >> 1);
(r > arr[mid]) ? (l = mid + 1) : (h = mid);
}
return
(arr[l] >= r) ? l : -1;
}
function
myRand(arr, freq, n) {
let prefix= [];
let i;
prefix[0] = freq[0];
for
(i = 1; i < n; ++i)
prefix[i] = prefix[i - 1] + freq[i];
let r = Math.floor((Math.random()* prefix[n - 1])) + 1;
let indexc = findCeil(prefix, r, 0, n - 1);
return
arr[indexc];
}
let arr = [1, 2, 3, 4];
let freq = [10, 5, 20, 100];
let i;
let n = arr.length;
for
(i = 0; i < 5; i++)
document.write(myRand(arr, freq, n));
</script>
Output: May be different for different runs
4 3 4 4 4
Time Complexity: O(n)
Auxiliary Space: O(n) because extra space for the array has been used
This article is compiled by Aashish Barnwal. Please write comments if you find anything incorrect, or you want to share more information about the topic discussed above.
Given n numbers, each with some frequency of occurrence. Return a random number with probability proportional to its frequency of occurrence.
Example:
Let following be the given numbers. arr[] = {10, 30, 20, 40} Let following be the frequencies of given numbers. freq[] = {1, 6, 2, 1} The output should be 10 with probability 1/10 30 with probability 6/10 20 with probability 2/10 40 with probability 1/10
It is quite clear that the simple random number generator won’t work here as it doesn’t keep track of the frequency of occurrence.
We need to somehow transform the problem into a problem whose solution is known to us.
One simple method is to take an auxiliary array (say aux[]) and duplicate the numbers according to their frequency of occurrence. Generate a random number(say r) between 0 to Sum-1(including both), where Sum represents summation of frequency array (freq[] in above example). Return the random number aux[r] (Implementation of this method is left as an exercise to the readers).
The limitation of the above method discussed above is huge memory consumption when frequency of occurrence is high. If the input is 997, 8761 and 1, this method is clearly not efficient.
How can we reduce the memory consumption? Following is detailed algorithm that uses O(n) extra space where n is number of elements in input arrays.
1. Take an auxiliary array (say prefix[]) of size n.
2. Populate it with prefix sum, such that prefix[i] represents sum of numbers from 0 to i.
3. Generate a random number(say r) between 1 to Sum(including both), where Sum represents summation of input frequency array.
4. Find index of Ceil of random number generated in step #3 in the prefix array. Let the index be indexc.
5. Return the random number arr[indexc], where arr[] contains the input n numbers.
Before we go to the implementation part, let us have quick look at the algorithm with an example:
arr[]: {10, 20, 30}
freq[]: {2, 3, 1}
Prefix[]: {2, 5, 6}
Since last entry in prefix is 6, all possible values of r are [1, 2, 3, 4, 5, 6]
1: Ceil is 2. Random number generated is 10.
2: Ceil is 2. Random number generated is 10.
3: Ceil is 5. Random number generated is 20.
4: Ceil is 5. Random number generated is 20.
5: Ceil is 5. Random number generated is 20.
6. Ceil is 6. Random number generated is 30.
In the above example
10 is generated with probability 2/6.
20 is generated with probability 3/6.
30 is generated with probability 1/6.
How does this work?
Any number input[i] is generated as many times as its frequency of occurrence because there exists count of integers in range(prefix[i – 1], prefix[i]] is input[i]. Like in the above example 3 is generated thrice, as there exists 3 integers 3, 4 and 5 whose ceil is 5.
C++
#include <bits/stdc++.h>
using
namespace
std;
int
findCeil(
int
arr[],
int
r,
int
l,
int
h)
{
int
mid;
while
(l < h)
{
mid = l + ((h - l) >> 1);
(r > arr[mid]) ? (l = mid + 1) : (h = mid);
}
return
(arr[l] >= r) ? l : -1;
}
int
myRand(
int
arr[],
int
freq[],
int
n)
{
int
prefix[n], i;
prefix[0] = freq[0];
for
(i = 1; i < n; ++i)
prefix[i] = prefix[i - 1] + freq[i];
int
r = (
rand
() % prefix[n - 1]) + 1;
int
indexc = findCeil(prefix, r, 0, n - 1);
return
arr[indexc];
}
int
main()
{
int
arr[] = {1, 2, 3, 4};
int
freq[] = {10, 5, 20, 100};
int
i, n =
sizeof
(arr) /
sizeof
(arr[0]);
srand
(
time
(NULL));
for
(i = 0; i < 5; i++)
cout << myRand(arr, freq, n) << endl;
return
0;
}
C
#include <stdio.h>
#include <stdlib.h>
int
findCeil(
int
arr[],
int
r,
int
l,
int
h)
{
int
mid;
while
(l < h)
{
mid = l + ((h - l) >> 1);
(r > arr[mid]) ? (l = mid + 1) : (h = mid);
}
return
(arr[l] >= r) ? l : -1;
}
int
myRand(
int
arr[],
int
freq[],
int
n)
{
int
prefix[n], i;
prefix[0] = freq[0];
for
(i = 1; i < n; ++i)
prefix[i] = prefix[i - 1] + freq[i];
int
r = (
rand
() % prefix[n - 1]) + 1;
int
indexc = findCeil(prefix, r, 0, n - 1);
return
arr[indexc];
}
int
main()
{
int
arr[] = {1, 2, 3, 4};
int
freq[] = {10, 5, 20, 100};
int
i, n =
sizeof
(arr) /
sizeof
(arr[0]);
srand
(
time
(NULL));
for
(i = 0; i < 5; i++)
printf
(
"%dn"
, myRand(arr, freq, n));
return
0;
}
Java
class
GFG
{
static
int
findCeil(
int
arr[],
int
r,
int
l,
int
h)
{
int
mid;
while
(l < h)
{
mid = l + ((h - l) >>
1
);
if
(r > arr[mid])
l = mid +
1
;
else
h = mid;
}
return
(arr[l] >= r) ? l : -
1
;
}
static
int
myRand(
int
arr[],
int
freq[],
int
n)
{
int
prefix[] =
new
int
[n], i;
prefix[
0
] = freq[
0
];
for
(i =
1
; i < n; ++i)
prefix[i] = prefix[i -
1
] + freq[i];
int
r = ((
int
)(Math.random()*(
323567
)) % prefix[n -
1
]) +
1
;
int
indexc = findCeil(prefix, r,
0
, n -
1
);
return
arr[indexc];
}
public
static
void
main(String args[])
{
int
arr[] = {
1
,
2
,
3
,
4
};
int
freq[] = {
10
,
5
,
20
,
100
};
int
i, n = arr.length;
for
(i =
0
; i <
5
; i++)
System.out.println( myRand(arr, freq, n) );
}
}
Python3
import
random
def
findCeil(arr, r, l, h) :
while
(l < h) :
mid
=
l
+
((h
-
l) >>
1
);
if
r > arr[mid] :
l
=
mid
+
1
else
:
h
=
mid
if
arr[l] >
=
r :
return
l
else
:
return
-
1
def
myRand(arr, freq, n) :
prefix
=
[
0
]
*
n
prefix[
0
]
=
freq[
0
]
for
i
in
range
(n) :
prefix[i]
=
prefix[i
-
1
]
+
freq[i]
r
=
random.randint(
0
, prefix[n
-
1
])
+
1
indexc
=
findCeil(prefix, r,
0
, n
-
1
)
return
arr[indexc]
arr
=
[
1
,
2
,
3
,
4
]
freq
=
[
10
,
5
,
20
,
100
]
n
=
len
(arr)
for
i
in
range
(
5
) :
print
(myRand(arr, freq, n))
C#
using
System;
class
GFG{
static
int
findCeil(
int
[] arr,
int
r,
int
l,
int
h)
{
int
mid;
while
(l < h)
{
mid = l + ((h - l) >> 1);
if
(r > arr[mid])
l = mid + 1;
else
h = mid;
}
return
(arr[l] >= r) ? l : -1;
}
static
int
myRand(
int
[] arr,
int
[] freq)
{
int
n = arr.Length;
int
[] prefix =
new
int
[n];
int
i;
prefix[0] = freq[0];
for
(i = 1; i < n; ++i)
prefix[i] = prefix[i - 1] + freq[i];
Random rand =
new
Random();
int
r = rand.Next(prefix[n - 1] + 1;
int
indexc = findCeil(prefix, r, 0, n - 1);
return
arr[indexc];
}
static
void
Main()
{
int
[] arr = { 1, 2, 3, 4 };
int
[] freq = { 10, 5, 20, 100 };
for
(
int
i = 0; i < 5; i++)
Console.WriteLine(myRand(arr, freq));
}
}
Javascript
<script>
function
findCeil(arr, r, l, h)
{
let mid;
while
(l < h)
{
mid = l + ((h - l) >> 1);
(r > arr[mid]) ? (l = mid + 1) : (h = mid);
}
return
(arr[l] >= r) ? l : -1;
}
function
myRand(arr, freq, n) {
let prefix= [];
let i;
prefix[0] = freq[0];
for
(i = 1; i < n; ++i)
prefix[i] = prefix[i - 1] + freq[i];
let r = Math.floor((Math.random()* prefix[n - 1])) + 1;
let indexc = findCeil(prefix, r, 0, n - 1);
return
arr[indexc];
}
let arr = [1, 2, 3, 4];
let freq = [10, 5, 20, 100];
let i;
let n = arr.length;
for
(i = 0; i < 5; i++)
document.write(myRand(arr, freq, n));
</script>
Output: May be different for different runs
4 3 4 4 4
Time Complexity: O(n)
Auxiliary Space: O(n) because extra space for the array has been used
This article is compiled by Aashish Barnwal. Please write comments if you find anything incorrect, or you want to share more information about the topic discussed above.