Главная > Уроки > Генерация случайных чисел в языке Си

Записывайся на этот курс на Stepike!

Генерация случайных чисел в языке Си

Иногда может возникнуть необходимость в генерации случайных чисел. Простой пример.

Пример: Определение победителя в конкурсе репостов.

Имеется список из 53 человек. Необходимо выбрать из них победителя. Если вы выберете его самостоятельно, то вас могут обвинить в предвзятости. Поэтому вы решили написать программу. Она будет работать следующим образом. Вы вводите количество участников N, после чего программа выводит одно число – номер победителя.

Как получить число от игрока, вам уже известно. А вот как заставить компьютер загадать случайное число? В этом уроке вы этому научитесь.

Функция rand().

Данная функция возвращает случайное целое число в диапазоне от нуля до RAND_MAX. RAND_MAX это специальная константа языка Си, в которой содержится максимальное целое число, которое может быть возвращено функцией rand().

Функция rand() определена в заголовочном файле stdlib.h. Поэтому, если хотите использовать rand в своей программе, не забудьте подключить этот заголовочный файл. Константа RAND_MAX тоже определена в этом файле. Вы можете найти этот файл у себя на компьютере и посмотреть её значение.

Давайте посмотрим на эту функцию в действии. Запустим следующий код:

Листинг 1.

#include <stdio.h>   // чтобы пользоваться функцией printf
#include <stdlib.h>  // чтобы пользоваться функцией rand
 
int main(void) {
/* генерируем пять случайных целых чисел */
  printf("%d\n", rand());
  printf("%d\n", rand());
  printf("%d\n", rand());
  printf("%d\n", rand());
  printf("%d\n", rand());
}

Должно получиться что-то вроде этого.

Пять случайных чисел, сгенерированных функцийе rand

Рис.1 Пять случайных чисел, сгенерированных функцийе rand

Но нам бы хотелось получить числа от 1 до 53, а не всё подряд. Ниже описано несколько трюков, позволяющих наложить ограничения на функцию rand().

Ограничить случайные числа сверху.

Кто в школе ждал момента, когда ему пригодится математика, приготовьтесь. Этот момент наступил. Чтобы ограничить сверху случайные числа, можно воспользоваться операцией получения остатка от деления, которую вы изучили в прошлом уроке. Наверное вы знаете, что остаток от деления на число K всегда меньше числа K. Например, при делении на 4 могут получиться остатки 0, 1, 2 и 3. Поэтому если вы хотите ограничить сверху случайные числа числом K, то просто возьмите остаток от деления на K. Вот так:

Листинг 2.

#include <stdio.h>
#include <stdlib.h>
 
int main(void) {
/* генерируем пять случайных целых чисел меньших 100 */
  printf("%d\n", rand()%100);
  printf("%d\n", rand()%100);
  printf("%d\n", rand()%100);
  printf("%d\n", rand()%100);
  printf("%d\n", rand()%100);
}
Пять случайных чисел меньше 100

Рис.2 Пять случайных чисел меньше 100

Ограничить числа снизу.

Функция rand возвращает случайные числа из отрезка [0, RAND_MAX]. А что если нам нужны только числа большие числа M (например, 1000)? Как быть? Всё просто. Просто прибавим к тому, что вернула функция rand, наше значение M. Тогда если функция вернёт 0, итоговый ответ будет M, если 2394, то итоговый ответ будет M + 2394. Этим действием мы как бы сдвигаем все числа на M единиц вперёд.

Задать границы функции rand сверху и снизу.

Например, получить числа от 80 до 100. Кажется, нужно просто объединить два способа, которые приведены выше. Получим что-то вроде этого:

Листинг 3.

#include <stdio.h>
#include <stdlib.h>
 
int main(void) {
/* генерируем пять случайных целых чисел больших 80 и меньших 100 */
  printf("%d\n", 80 + rand()%100);
  printf("%d\n", 80 + rand()%100);
  printf("%d\n", 80 + rand()%100);
  printf("%d\n", 80 + rand()%100);
  printf("%d\n", 80 + rand()%100);
}

Попробуйте запустить эту программу. Удивлены?

Да, такой способ работать не будет. Давайте прокрутим эту программу руками, чтобы убедиться в том, что мы допустили ошибку. Допустим rand() вернула число 143. Остаток от деления на 100 равен 43. Дальше 80 + 43 = 123. Значит такой способ не работает. Подобная конструкция выдаст числа от 80 до 179.

Давайте разберём по действиям наше выражение. rand()%100 может выдать числа от 0 до 99 включительно. Т.е. из отрезка [0; 99].
Операция + 80 сдвигает наш отрезок на 80 единиц вправо. Получаем [80; 179].
Как видим, проблема у нас заключается в правой границе отрезка, она сдвинута вправо на 79 единиц. Это наше исходное число 80 минус 1. Давайте наведём порядок и сдвинем правую границу назад: 80 + rand()%(100 - 80 + 1). Тогда всё должно сработать как надо.

В общем случае если нам нужно получить числа из отрезка [A;B], то необходимо воспользоваться следующей конструкцией:
A + rand()%(B-A+1).

Согласно этой формуле перепишем нашу последнюю программу:

Листинг 4.

#include <stdio.h>
#include <stdlib.h>
 
int main(void) {
/* генерируем пять случайных целых чисел из отрезка [80; 100] */
  printf("%d\n", 80 + rand()%(100 - 80 + 1));
  printf("%d\n", 80 + rand()%(100 - 79));
  printf("%d\n", 80 + rand()%21);
  printf("%d\n", 80 + rand()%21);
  printf("%d\n", 80 + rand()%21);
}

Результат работы:

Случайные числа из диапазона [80;100]

Рис.3 Случайные числа из диапазона [80;100]

Ну вот, теперь вы можете решить исходную задачу урока. Сгенерировать число из отрезка [1; N]. Или не можете?

Но прежде ещё немного полезной информации. Запустите последнюю программу три раза подряд и записывайте себе случайные числа, которые она генерирует. Заметили?

Функция srand().

Да, каждый раз появляются одни и те же одинаковые числа. «Так себе генератор!» – скажете вы. И будете не совсем правы. Действительно, генерируются всё время одинаковые числа. Но мы можем на это повлиять, для этого используется функция srand(), которая также определена в заголовочном файле stdlib.h. Она инициализирует генератор случайных чисел начальным числом.

Скомпилируйте и запустите несколько раз вот эту программу:

Листинг 5.

#include <stdio.h>
#include <stdlib.h>
 
int main(void) {
  srand(2);
/* генерируем пять случайных целых чисел из отрезка [80;100] */
  printf("%d\n", 80 + rand()%(100 - 80 + 1));
  printf("%d\n", 80 + rand()%(100 - 79));
  printf("%d\n", 80 + rand()%21);
  printf("%d\n", 80 + rand()%21);
  printf("%d\n", 80 + rand()%21);
}

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

И это можно сделать. Например, воспользуемся функцией time(), которая определена в заголовочном файле time.h. Данная функция, если ей в качестве аргумента передать NULL, возвращает количество секунд, прошедших c 1 января 1970 года. Вот посмотрите, как это делается.

Листинг 6.

#include <stdio.h>
#include <stdlib.h>
#include <time.h> // чтобы использовать функцию time()
 
int main(void) {
  srand(time(NULL));
/* генерируем пять случайных целых чисел из отрезка [80;100] */
  printf("%d\n", 80 + rand()%(100 - 80 + 1));
  printf("%d\n", 80 + rand()%(100 - 79));
  printf("%d\n", 80 + rand()%21);
  printf("%d\n", 80 + rand()%21);
  printf("%d\n", 80 + rand()%21);
}

Вы спросите, а что такое NULL? Резонный вопрос. А я вам пока отвечу, что это специальное зарезервированное слово такое. Могу ещё сказать, что им обозначает нулевой указатель, но т.к. это для вас никакой информации не несёт, то на данный момент рекомендую об этом не думать. А просто запомнить как некоторый хитрый трюк. В будущих уроках мы остановимся на этой штуке поподробнее.

Сохрани в закладки или поддержи проект.

Практика

Решите предложенные задачи. Для удобства работы сразу переходите в полноэкранный режим

Исследовательские задачи для хакеров:

  1. В каких ситуациях ещё может пригодиться генерация случайных чисел? Напишите ваши варианты в комментарии к этому уроку.
  2. Напишите программу, которая выводит на экран значение целочисленной константы RAND_MAX. Найдите файл stdlib.h на вашем компьютере, найдите значение этой константы в этом файле.
  3. Найдите в интернете описание функций, которые определены в заголовочном файле time.h Вы, конечно, ещё не сможете ими пользоваться, но знать, что такие функции есть, всё равно нужно. Ведь когда-то настанет момент, когда ваших знаний будет достаточно для их использования.
  4. Числа, генерируемые функцией rand(), имеют равномерное распределение. Это значит, что если запускать функцию rand очень много раз и каждый раз записывать, какое число выпало, то количество выпадения различных чисел будет одинаковым. Например, если генерировать только числа 0 и 1, то через 100 запусков примерно 50 раз выпадет ноль и 50 раз единичка. Обратите внимание, что я говорю примерно. Может быть, например, 49 и 51, или 53 и 47. Если рассматривать это в отношении к общему числу запусков, получим (49/100 и 51/100 или 53/100 и 47/100 соответственно). Но чем больше экспериментов мы проведём, тем ближе отношение количество единичек к количеству испытаний будет стремиться к 1/2. Проведите самостоятельно эксперимент с 10, 50 и 100 запусками. Это муторно и долго, если делать руками, но что поделать? В будущем мы напишем программу, чтобы проверить свойство равномерности распределения наших случайных чисел.

Дополнительные материалы

  1. Другие функции, определённые в заголовочном файле stdlib.h
  2. Хотя я и употребляю везде словосочетание «случайные числа», но на самом деле получить действительно случайные числа – сложная задача. И в компьютерах обычно используются псевдослучайные числа. Подробнее об этом можно прочитать здесь.
  3. Если не терпится узнать хоть что-то про NULL, то почитайте вот этот урок.
  4. Дата 1 января 1970 года особенная. С неё начинается отсчёт эры UNIX. Подробнее об этом и проблемах, которые нас ожидают.

Оставить комментарий

Чтобы код красиво отображался на странице заключайте его в теги [code] здесь писать код [/code]

Комментарии

Василий Громов
1 задача. Например, используя генератор случайных чисел можно сделать игральный кубик или подобие игрового автомата.
4 задача. Я постарался сделать, но на 30 остановился. У меня получились следующие результаты:
10 генераций: 0-2, 1-8
20 генераций: 0-12, 1-8
30 генераций: 0-15, 1-15

Т.е. 30 попыток достаточно?
KaDeaT
1 задача. Да! Очень хорошие примеры.

4 задача. Нет, 30 попыток недостаточно. То, что у вас получилось 15/15 это совпадение. Я провёл 5 опытов по 30 раз, вот результаты:
1. 15/15
2. 11/19
3. 15/15
4. 17/13
5. 12/18
Никита
у вас ошибка в примере с генерацией числе от 80 до 100, программа выдаст значения больше 100!
KaDeaT
Проверил, вроде бы нет ошибок. Поясните, что вы имеете ввиду?
Александр
Здравствуйте! Мой комментарий по поводу формулы расчета диапазона случайных чисел, мы тут посовещались и признали ее не совсем верной. Ваща формула: A + rand()%(B-A+1), при условии которое дано в задаче Введите максимально число, которое может быть сгенерировано следующей конструкцией: int rand_a = 66 + rand()%601; выдает максимальное число в 536. Верная формула: max (a + rand() %b) = a + b - 1 = 666

Человек который мне помогает изучать язык Си, написал тест, для сравнения Вашей формулы, которая дается на сайте и верной формулы.

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void sleep (float);
void print_symbol (int);

int main (void)
{
// A + rand()%(B-A+1)
// 66+rand()%601
// 600-66+1 = 535, а правильный ответ оказался 666
int a, b;
a = 66;
b = 601;

printf("a = %dn", a);
printf("b = %dnn", b);
printf("33[32;22m");
printf("верная формула:33[0mn");
printf("max (a + rand() %%b) = a + b - 1 = %dnn", a+b-1);
printf("33[33;22m");
printf("а вот ваша формула:33[0mn");
printf("max (a + rand() %%b)");
printf(" = b - a + 1 = %dnn", b-a+1);

int i, z, max = 0;
int s = 0, count = 0;
for (i=0; i<1000000; i++)
{
z = a + rand() %b;
if (z>max)max = z;
if (++count > 2000)
{
count = 0;
if (++s > 3) s = 0;
print_symbol(s); // строку на экран
fflush (stdout); // обновляем вывод
sleep (0.03); // временная задержка
}
}
printf("nnmax = %dn", max); //max = a+b-1
exit(0);
}
//------------------------------------------------
KaDeaT
Подождите-подождите. Почему вы за A и B берёте значения 66 и 601. А и B это начало и конец промежутка из которого вы хотите генерировать числа же.

Пожалуйста, напишите мне в сообщения группы вк. Обсудим подробнее этот вопрос, если у меня есть ошибка, я с удовольствием внесу правки.
Александр
Ну, давайте, подумаем логически. Дана задача, найти максимальное сгенерированное число, этой конструкции. int rand_a = 66 + rand()%601;
Находим верхнюю и нижнюю границу диапазона, согласно вашей формуле: A + rand()%(B-A+1) она будет int rand_a = 66 + rand()%(601 - 66 + 1), то есть от 66 до 536. Соответственно максимально сгенерированное число получается 536, а валидатор на степике принимает число 666. Да и я сидел запускал программу, раз 20, выпадали числа больше 536. Я засомневался и посоветовался с наставником, он мне по полочкам все разложил, и вышло, что верная формула как для получения диапазона, так и определения максимального сгенерированного числа получается int rand_a = A + rand()%(B + A - 1). Если посмотреть на наш пример, диапазон будет от 66 до 666, соответственно и максимальное сгенерированное число будет 666, это же число и принимает валидатор на степике.
KaDeaT
Всё-таки вы не правы.
Ищем нижнюю и верхнюю границы диапазона.

1 способ.
rand()%601 может сгенерировать любое число от 0 до 600. Максимально 600. К нему прибавляется 66. Итого получается 666.

2 способ.
Используем формулу из урока:
A + rand()%(B-A+1)

В задаче имеем
66 + rand()%601

Сопоставим их
A = 66
B-A+1 =601

Подставим вместо А значение 66. Получим
B - 66 +1 = 601

Отсюда B = 601-1+66 = 666. Итого верхняя граница диапазона 666.

У вас ошибка. Вы почему-то считаете, что B = 601. Хотя B вам надо найти. Разберите внимательно 2 способ.

Александр
И все таки мы с вами не согласны:)
У нас с вами получается разный подход к вопросу у вас со стороны математики, у нас со стороны информатики.

Как работает генератор случайных чисел?

В общем случае команда выглядит так:

a = b + rand() %n;

где
a - получившееся случайное число;
b - минимальное значение диапазона случайных чисел;
n - смещение для команды rand();

т.е. сначала выполняется rand() %n, а затем полученный результат
прибавляется к b, и получившаяся сумма записывается в a.

Каким образом работает rand() ?

Она всегда генерирует число, начиная с 0 с заданным смещением, при
этом число 0 тоже учитывается в смещении.

Допустим у нас задано смещение 1:

смещение: 1
число num: 0

т.е. команда a = rand() %1;

всегда будет давать результат 0

Допустим, у нас задано другое смещение 5:

смещение: 1 2 3 4 5
число num: 0 1 2 3 4

т.е. команда a = rand() %5;

будет генерировать число >= 0 и <=4.

Т.е. из-за этой особенности максимальное число, которое генерирует
rand(), всегда на 1 меньше, чем заданное смещение.

Если смещение 25, максимальный результат всегда 24.
Если смещение 100, максимальный результат всегда 99.

И так далее.

И как теперь посчитать максимум?

a(max) = b + (n - 1);

Вот и все.

И если b = 66 и n = 601, максимум всегда будет:
66 + (601 - 1) = 66 + 600 = 666.

На мой взгляд тут нужно не формулу учить, а понимать как работает
генератор случайных чисел, т.е. понимать, как генерируется случайное
число по заданному смещению.

Например:

a = 10 + rand() %9;

(смещение) n: 1 2 3 4 5 6 7 8 9
KaDeaT
Подождите, а с чем конкретно вы не согласны? Выскажите, пожалуйста, тезис.

По-моему мы говорим об одном и том же, но только разными словами. Я не призываю заучивать формулы для отрезка, я призываю разобраться в том, как это работает, используя для примера конкретную задачу.) В любом случае спасибо за неравнодушие и дискуссию!
Анастасия
здравствуйте, подскажите, пожалуйста, разницу между <cstdlib> и stdlib.h>)))
KaDeaT
cstdlib -- это стандартный заголовочный файле в С++
stdlib.h -- это заголовочный файл Си.

По своей сути это аналоги.
не_робот_я
printf("%dn", 80 + rand()%(100 - 80 + 1));
printf("%dn", 80 + rand()%(100 - 79));

Как во второй строке получилось 79?
KaDeaT
-80 + 1 = -79
Лёха
Спасибо за ваш труд! Мне ваш курс нравится!
Хотел предложить другое объяснение формуле A + rand()%(B-A+1). Мы хотим получить числа в интервале[A..B]. Это можно сделать в два приёма. Сначала получить число в интервале[0..B-A], а затем добавить к полученному числоу А. rand()%(B-A) даёт числа в интервале [0..B-A-1], потому что, максимально возможное значение остатка от деления всегда на еденицу меньше делителя. Значит чтобы получить интервал [0..B-A] нам нужно добавить к делителю число 1.
KaDeaT
И вам спасибо за дельные комментарии.)
Lola_lala
После заголовка "Ограничить случ числа сверху" в предложении "Наверное вы знаете, что остаток от деления на числа K всегда меньше числа K". Исправьте "на числа K" -> "на число К"
KaDeaT
Спасибо! Опечатка исправлена.
Ахмед
Наверное вы знаете, что остаток от деления на числа K всегда меньше числа K. Например, при делении на 4 могут получиться остатки 0, 1, 2 и 3.

ошибка или я ошибаюсь??? остатки от деления на 4 может быть только 0, 1, 2. А у вас еще и 3 написано, и я вот тут мозг ломаю как эту 3 получить xd
KaDeaT
Вы ошибаетесь. Например, остаток от деления 7 на 4 будет равен 3.
12-летний школьник
А мне интересно, можно записать этот рандом в переменную bool? Ну вот допустим мне нужна переменная, которая будет либо 0, либо 1. Все по чистейшему рандому. Как же это сделать?
KaDeaT
Например, сгенерируйте случайное целое число и возьмите его остаток от деления на 2.
Марк
В задаче про рандомное число есть небольшие расхождения из-за того, что в первом варианте программы требуется вывести рандомное число больше 80, меньше 100(то есть отрезок (80; 100)), а во втором [80; 100]. Надо исправить условия на одинаковые и исправить разъяснение задачи, потому что а разъяснении говорится про отрезок [80; 100) и потом вместо того чтобы делить рандомное число без остатка на 20(100-80) вы делите на 21(100-80+1), как если бы мы работали с отрезком[80; 100].
sunriset
Написал код, который высчитывает количество выведенных 0 и 1 по 4 задаче.


#include <stdio.h>
#include <stdlib.h> // подключаем stdlib.h
#include <time.h> // подключаем stdlib.h

int main (void) {
srand(time(NULL));
int random_0, random_1; // переменные для счета 0 и 1
for(int i = 0; i <= 10000; i++) {
int random = rand() % (1 + 1); // [0;1], где min - 0, max - 1

if (random == 0) {
random_0++;
}

if (random == 1) {
random_1++;
}
}

printf("%d %d", random_0, random_1); // количество выведенных 0 и 1, всегда числа будут раномерно распределяться
return 0;
}
Иван
В начале вроде бы писали, что "генерируем пять случайных целых чисел больших 80 и меньших 100"

При этом, если прибавлять 80, то можно получится число 80, которое не больше 80.

Так же " 80 + rand()%(100 - 80 + 1) " даст нам число от 80 до 100 включая 80 и 100.

Скорее всего изначальная задумка была именно в том, что 80 и 100 идут включительно, просто путает.
vlas
Тоже решительно не соглашусь с учётом единички в формуле сдвига отрезка. Тут долго и нужно пытались это доказать, ещё больше запутывая принципиально простую вещь.

Отрезок сдвигается вправо целиком на 80, так? Т.е. обоими концами одинаково, так? Значит насколько нужно сдвинуть правый конец обратно? На 80! Но не на 80-1.

Вот ссылка на наглядный код:
https://rextester.com/RAXAW3423
Если потыкать запуск, то во второй части вывода иногда ожидаемо будет выскакивать 100, нарушая логику. Тогда как в первой его части максимум - 99, что логично.
Григорий
Почему в первом примере всегда случайно генерируются одни и те же числа?