Урок 10. Псевдослучайные числа

Особенности языка С. Учебное пособие

Функции rand() и srand()

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

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

Задание
Напишите программу, в которой целочисленной переменной присваивается результат выполнения функции rand(). Выведите значение переменной на экран.

Функция rand() возвращает целое число от 0 до значения присвоенного константе RAND_MAX. Значение RAND_MAX зависит от системы и определено в заголовочном файле stdlib.h. Так, например, оно может быть равно 32767 (двухбайтовое целое) или 2147483647 (четырехбайтовое целое).

Задание
Определите значение RAND_MAX в вашей системе. Для этого не забудьте подключить к файлу исходного кода заголовочный файл stdlib.h.

Код ниже выводит на экран 50 случайных чисел:

#include <stdio.h>
#include <stdlib.h>
 
main () {
     char i;
 
     for (i = 1; i <= 50; i++) {
          printf("%15d", rand());
          if (i % 5 == 0) printf("\n");
     }
 
}

В теле цикла осуществляется переход на новую строку после каждых выведенных на экран пяти чисел. Для этого используется выражение, в котором находится остаток от деления i на 5, результат сравнивается с 0. Чтобы после первого числа не происходил переход на новую строку, i сначала присваивается единица, а не ноль (т.к. 0 делится на 5 без остатка).

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

Вы должны были заметить, что при каждом запуске программы числа остаются одинаковыми. Даже если вы перекомпилируете программу, результат не изменится. Данный эффект связан с тем, что начальное (инициализирующее) число, которое подставляется в формулу вычисления первого и последующих псевдослучайных чисел, для каждой системы всегда одно и то же. Однако это начальное число можно изменить с помощью функции srand(), которой в качестве параметра передается любое целое число. Понятно, что если вы зададите конкретный аргумент для функции, например, srand(1000), то от вызова к вызову программы числа будут также одни и те же. Хотя и не те, что были бы без srand(). Поэтому появляется проблема, как сделать так, чтобы аргумент для srand() был тоже случайным? Получается замкнутый круг.

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

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

Текущее время можно узнать с помощью функции time(), прототип которой описан в файле time.h. Передав time() в качестве параметра NULL, мы получим целое число, которое можно передать в srand():
srand(time(NULL));

Задание
Переделайте вашу программу так, чтобы инициализирующее значение зависело от системного времени.

Получение целых случайных чисел в заданных диапазонах

Функция rand() выдает случайное число от 0 до значения RAND_MAX. Что делать, если требуется получать случайные числа в иных диапазонах, например, от 100 до 999?

Сначала рассмотрим более простую ситуацию: получить случайные числа от 0 до 5. Если любое целое число попытаться разделить на 5 нацело, то можно получить как 0 (когда число делится на 5 без остатка), так и 1, 2, 3, 4. Например, rand() вернула число 283. Применяя к этому числу операцию нахождения остатка от деления на 5, получим 3. Т.е. выражение rand() % 5 дает любое число в диапазоне [0, 5).

Однако, что если надо, чтобы число 5 так же входило в диапазон, т.е. диапазон имеет вид [0, 5]? Логично предположить, что следует найти остаток от деления на 6. При этом более грамотным будет следующее рассуждение: надо находить остаток от деления на размер диапазона. В данном случае он равен шести значениям: 0, 1, 2, 3, 4, 5. Чтобы найти размер диапазона надо из допустимого максимума вычесть допустимый минимум и прибавить единицу: max - min + 1. Будьте внимательны: если, например, требуется, чтобы указанный в задаче максимум не входил в диапазон, то единицу прибавлять не надо или надо вычитать единицу из максимума.

Задание
Напишите программу, выдающую 50 случайных чисел от 0 до 99 включительно.

Итак, мы знаем формулу получения длины диапазона: max - min + 1. Если требуется получить число от 6 до 10 включительно, то длина диапазона будет равна 10 - 6 + 1 = 5. Выражение rand() % 5 даст любое число от 0 до 4 включительно. Но нам надо от 6 до 10. В таком случае достаточно к полученному случайному остатку прибавить 6, т.е. минимум. Другими словами, надо выполнить сдвиг. Действительно для приведенного примера:

  • если остаток был равен 0, то добавляя 6, получаем 6;
  • остаток 1, добавляем 6, получаем 7;
  • остаток 4, прибавляем 6, получаем 10;
  • остатка больше 4 не может быть.

В таком случае формула для получения случайного числа в диапазоне [a, b] выглядит так:

rand() % длина_диапазона + сдвиг

,
где длина_диапазона вычисляется как b - a + 1, сдвиг является значением a.

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

Задание
Выведите на экран ряд случайных чисел, принадлежащих диапазону от 100 до 299 включительно.

С таким же успехом можно получать случайные отрицательные числа. Действительно, если диапазон задан как [-35, -1], то его длина будет равна -1 - (-35) + 1 = 35, что соответствует действительности; выражение получения случайного числа будет выглядеть так:
rand() % 35 - 35

Так, если остаток от деления составил 0, то мы получим -35, а если 34, то -1. Остальные остатки дадут значения в промежутке от -35 до -1.

Задание
Выведите на экран ряд случайных чисел, принадлежащих диапазону от -128 до 127 включительно.

Получение вещественных случайных чисел

Ситуация с вещественными числами выглядит несколько по-иному. Во-первых, мы не можем получить остаток от деления, если делимое или делитель дробное число. Во вторых при вычислении длины диапазона нельзя прибавлять единицу.

Поясним вторую причину. Допустим диапазон задан как [2.50, 5.30]. Он состоит не из определенного количества чисел (как в случае целых), а из неопределенного (можно сказать, бесконечного) числа значений, т.к. вещественные числа можно представлять с различной степенью точности. Позже выполняя округление все равно будет шанс получить максимальную границу диапазона, поэтому для вычисления длины диапазона достаточно из максимума вычесть минимум.

Если разделить случайное число, преобразованное к вещественному типу, которое выдала функция rand(), на значение константы RAND_MAX, то получится вещественное случайное число от 0 до 1. Теперь, если это число умножить на длину диапазона, то получится число, лежащее в диапазоне от 0 до значения длины диапазона. Далее если прибавить к нему смещение к минимальной границе, то число благополучно впишется в требуемый диапазон. Таким образом формула для получения случайного вещественного числа выглядит так:
(float) rand() / RAND_MAX * (max - min) + min

Задание
Заполните массив случайными числами в диапазоне от 0.51 до 1.00. Выведите значение элементов массива на экран.

Равновероятные случайные числа

Функция rand() генерирует любое случайное число от 0 до RAND_MAX с равной долей вероятности. Другими словами, у числа 100 есть такой же шанс выпасть, как и у числа 25876.

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

#include <stdio.h>
#include <time.h>
 
#define N 500
 
main () {
  int i;
  int arr[5] = {0};
 
  srand(time(NULL));
 
  for (i=0; i < N; i++)   
    switch (rand() % 5) {
      case 0: arr[0]++; break;
      case 1: arr[1]++; break;
      case 2: arr[2]++; break;
      case 3: arr[3]++; break;
      case 4: arr[4]++; break;
    }
 
  for (i=0; i < 5; i++) 
    printf("%d - %.2f%%\n", i, ((float) arr[i] / N) * 100);
 
}

В приведенной программе массив из пяти элементов сначала заполняется нулями. Случайные числа генерируются от 0 до 4 включительно. Если выпадает число 0, то увеличивается значение первого элемента массива, если число 1, то второго, и т.д. В конце на экран выводится процент выпадения каждого из чисел.

Задание
Спишите данную программу. Посмотрите на результат ее выполнения при различных значениях N: 10, 50, 500, 5000, 50000. Объясните увиденное.

rand() % 5 Такой способ

rand() % 5

Такой способ получения случайного числа из диапазона НЕКОРРЕКТЕН!
Используя такой способ, вы получаете НЕРАВНОМЕРНОЕ распределение случайных чисел при больших значениях диапазона. А программа должна работать корректно (в т.ч. давать равномерное распределение) при любых конкретных численных значениях.
Настоятельно рекомендую взять листок бумаги, карандаш, и посмотреть на конкретных примерах, почему так получается.
Если вы всё ещё не верите, вот программа, наглядно это демонстрирующая:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
 
#define N 10000
#define RANGE 2000000000
 
int rand_range_reminder(int range)
{
    return rand() % range;
}
 
int rand_range_correct(int range)
{
    return (float)rand()/RAND_MAX * range;
}
 
int main () {
    srand(time(NULL));
 
    int i, lower_half=0, higher_half=0;
    for (i=0; i < N; i++) {
        if (rand_range_reminder(RANGE) < RANGE/2) {lower_half++;}
        else {higher_half++;}
    }
 
    printf("Reminder variant:\n");
    printf("Numbers less    than RANGE/2: %i\n", lower_half);
    printf("Numbers greater than RANGE/2: %i\n", higher_half);
    printf("Percentage: %.1f : %.1f\n\n", (float)lower_half/N*100, (float)higher_half/N*100);
 
    lower_half=0, higher_half=0;
    for (i=0; i < N; i++) {
        if (rand_range_correct(RANGE) < RANGE/2) {lower_half++;}
        else {higher_half++;}
    }
 
    printf("Correct variant:\n");
    printf("Numbers less    than RANGE/2: %i\n", lower_half);
    printf("Numbers greater than RANGE/2: %i\n", higher_half);
    printf("Percentage: %.1f : %.1f\n", (float)lower_half/N*100, (float)higher_half/N*100);
 
    return 0;
}

Вот пример её вывода:

====
Reminder variant:
Numbers less    than RANGE/2: 5304
Numbers greater than RANGE/2: 4696
Percentage: 53.0 : 47.0

Correct variant:
Numbers less    than RANGE/2: 5035
Numbers greater than RANGE/2: 4965
Percentage: 50.3 : 49.6
====

Генерируется 53 процента чисел, меньших, чем середина диапазона. Это
очень плохо!

Кроме того, код примеров содержит ошибки и "некрасивости", за которые в достойной организации можно и получить санкции!
Например, main() объявляется не как int, а для вызова rand() Не подключается stdlib.h.
Внимательней, коллеги! Вас читают дети.
Не позорьте Линуксоидов!