Генерация псевдослучайных чисел. Функции математической библиотеки

Практический курс по C/C++: https://stepik.org/course/193691

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

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

int rand(void);

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

#include <stdio.h>
#include <stdlib.h>
 
int main(void)
{
    int r_1 = rand();
    int r_2 = rand();
 
    printf("%d, %d, %d, %d, %d\n", r_1, r_2, rand(), rand(), rand());
 
    return 0;
}

После запуска программы увидим значения:

41, 18467, 19169, 26500, 6334

А теперь, внимание, несколько важных моментов! Первое. Сгенерированные числа называются псевдослучайными, а не случайными, так как они все же вычисляются по некоторому алгоритму. Истинной случайности мы здесь получить не можем. В частности, это означает, что такие псевдослучайные значения не следует применять в алгоритмах шифрования.

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

Третье. Значения псевдослучайных чисел распределены по равномерному закону в диапазоне [0; RAND_MAX]. То есть, с равной вероятностью может появиться любое значение из этого диапазона.

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

Все это следует учитывать при использовании функции rand(). И, соответственно, возникают вопросы. Первый. Что если нам нужно при запуске получать все время разные случайные значения? Как это сделать? Для этого пользуются функцией:

void srand(unsigned int seed);

которая задает начальное значение «зерна» (seed) для датчика псевдослучайных чисел. Если это зерно указать, например, равным 10 (в начале функции main()):

srand(10);

то увидим уже другую последовательность чисел:

71, 16899, 13697, 13694, 3272

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

time_t time(time_t * const _Time);

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

int t = time(NULL);

Тогда значение переменной t каждый раз будет меняться от запуска к запуску. А это именно то, что нам и нужно. Объединим обе функции, получим:

srand(time(NULL));

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

Генерация псевдослучайных чисел заданного диапазона

Следующий вопрос, как сформировать псевдослучайные числа произвольного диапазона? Как правило, меньшего чем [0; RAND_MAX]. Если при этом, нам нужно сохранить целочисленные значения, то меньший диапазон легко получить с помощью операции деления по модулю, например, так:

int range = 10;
int r_1 = rand() % range;   // [0; range)

Обратите внимание, граничное значение 10 не входит в диапазон, оно исключается (поэтом в конце записана круглая скобка).

Если диапазон должен начинаться не с нуля, то соответственно, нужно сделать обычное математическое смещение значений (вычитание), например:

int r_2 = rand() % range - 5; // [-5; range-5)

И так для любого разумного диапазона целых чисел, меньшего начального [0; RAND_MAX]. Если потребуется больший, то вначале можно сложить два случайных значения:

int r_3 = rand() + rand();  // [0; 2*RAND_MAX]

А, затем, применить к нему все те же математические преобразования.

Несколько иначе обстоит дело с получением вещественных значений. Часто поступают следующим образом. Вначале весь диапазон [0; RAND_MAX] приводят к диапазону вещественных значений [0; 1], а затем, масштабируют и смещают его, если это необходимо. Например:

double range_float = (double)rand() / (double)RAND_MAX; // [0; 1]

Обратите внимание на операцию приведения типов. Так как функция rand() возвращает целочисленные значения и константа RAND_MAX тоже целочисленная, то при делении мы бы получали тоже целые числа с отбрасыванием дробной части. Поэтому перед делением мы приводим оба значения к вещественному типу double, чтобы получить также вещественное число в диапазоне [0; 1].

После этого, используя вещественное случайное значение range_float диапазона [0; 1], мы легко можем формировать любые другие диапазоны из псевдослучайных вещественных чисел.

Вот основные приемы при генерации псевдослучайных значений на языке Си.

Основные математические функции

Во второй части занятия познакомимся с часто используемыми математическими функциями, определенные в заголовочном файле math.h. Они следующие:

Функция

Описание

int abs(int)

Вычисление модуля целочисленного значения.

double fabs(double)

Вычисление модуля вещественного значения.

Функции округления

double round(double)

Округление вещественного значения до ближайшего целого.

double floor(double)

Округление вещественного значения до наименьшего целого.

double ceil(double)

Округление вещественного значения до наибольшего целого.

double trunc(double)

Отбрасывание дробной части вещественного числа.

Степенные функции

double sqrt(double)

Вычисление квадратного корня от вещественного значения.

double cbrt(double)

Вычисление кубического корня от вещественного значения.

double pow(double x, double y)

Возведение числа x в степень y.

double exp(double)

Вычисление экспоненты от вещественного значения.

double log(double)

Вычисление натурального логарифма.

double log2(double)

Вычисление логарифма по основанию 2.

double log10(double)

Вычисление десятичного логарифма.

Тригонометрические функции

double sin(double)

Вычисление синуса угла, заданного в радианах.

double cos(double)

Вычисление косинуса угла, заданного в радианах.

double tan(double)

Вычисление тангенса угла, заданного в радианах.

double asin(double)

Вычисление арксинуса угла (возвращает радианы).

double acos(double)

Вычисление арккосинуса угла (возвращает радианы).

double atan(double)

Вычисление арктангенса угла (возвращает радианы).

То есть, если вам в программе потребуется выполнять операции округления, возведения в степень, применять тригонометрические функции, то все это уже имеется в стандартной математической библиотеке языка Си. Все, что от вас требуется – это подключить заголовочный файл math.h, где все эти функции определены.

Использовать их достаточно просто и очевидно. Поэтому я просто приведу довольно популярный, но показательный пример использования функции sqrt() для вычисления действительных корней квадратного уравнения вида:

Здесь a, b, c – коэффициенты уравнения (вещественные числа). Надеюсь, все помнят школьный курс математики и знают, как находить корни такого уравнения. Вначале нам нужно вычислить дискриминант по формуле:

Если он окажется больше или равен нулю, то действительные корни существуют. Иначе, действительных корней нет. Сами же корни вычисляются по формулам:

Ниже приведена программа, которая решает такие квадратные уравнения:

#include <stdio.h>
#include <math.h>
 
int main(void)
{
    double a, b, c;
    double D, x1, x2;
 
    if(scanf("%lf, %lf, %lf", &a, &b, &c) != 3) {
        printf("Error input\n");
        return 0;
    }
 
    D = b*b - 4*a*c;
 
    if(D < 0) {
        printf("D  = %.2f < 0", D);
        return 0;
    }
 
    D = sqrt(D);
 
    x1 = -(b + D) / (2.0 * a);
    x2 = -(b - D) / (2.0 * a);
 
    printf("x1 = %.2f, x2 = %.2f\n", x1, x2);
 
    return 0;
}

Как видите, здесь используется функция sqrt(), которая возвращает квадратный корень от дискриминанта и используется при вычислении корней x1 и x2.

Вообще в функции sqrt(), равно, как и почти в любой другой функции математической библиотеки, можно прописывать любые выражения языка Си, которые возвращают целые или вещественные значения. То есть, ничто нам не мешает ту же функцию sin() записывать в таких вариациях:

double res_1 = sin(x1);
double res_2 = sin(2 * x1);
double res_3 = sin(10.0 / 15.0);

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

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме