## Базовый алгоритм: один проход по массиву
Задача поиска минимума и максимума решается линейным сканированием. Инициализируем оба значения первым элементом, затем проходим по оставшимся элементам, обновляя минимум или максимум при необходимости.
Сложность: O(n) по времени и O(1) по памяти.
Это оптимально: любой алгоритм должен посмотреть каждый элемент хотя бы один раз.
## Почему инициализация первым элементом, а не нулём или INT_MAX
Частая ошибка новичка — инициализировать
Если все элементы массива положительны,
Инициализация через
Инициализация первым элементом проще и универсальнее:
## Возврат индексов
Часто нужны не только минимальное и максимальное значения, но и позиции соответствующих элементов.
Для этого добавим переменные
При наличии дубликатов этот вариант вернёт индекс первого вхождения.
Если нужен индекс последнего вхождения, используйте
## Вынесение в функцию
В C массив при передаче в функцию фактически передаётся как указатель, поэтому его размер необходимо передавать отдельно.
Результат можно вернуть через указатели-параметры:
Ключевые моменты:
## Обработка пустого массива
Пустой массив не имеет ни минимума, ни максимума.
Обращение к:
при
Возможны несколько вариантов обработки:
## Типичные ошибки новичков
### 1. Выход за границы массива
Неправильно:
Когда
Правильно:
Чтение за пределами массива приводит к неопределённому поведению. Программа может получить мусорное значение, завершиться с ошибкой или внешне продолжить работу, скрывая проблему.
### 2. Неверный расчёт размера через sizeof
Например:
Внутри функции
Фактически параметр:
интерпретируется как:
Поэтому
Размер следует передавать отдельно:
### 3. Инициализация нулём
Неправильно:
Если массив содержит только положительные числа:
то
Если массив содержит только отрицательные числа:
то
Надёжнее использовать первый элемент:
### 4. Сравнение беззнаковых значений с отрицательными числами
Если массив имеет тип:
его элементы не могут быть отрицательными.
Проблемы могут возникнуть при сравнении беззнакового значения с отрицательной константой из-за неявных преобразований типов.
Например:
В подобных ситуациях важно учитывать правила преобразования знаковых и беззнаковых целых типов в C.
### 5. Неверное использование sizeof для динамического массива
Для обычного массива:
такой способ работает.
Но для динамической памяти:
выражение:
вернёт размер указателя, а не размер выделенного блока памяти.
Поэтому размер динамического массива необходимо хранить отдельно:
Если размер поступает извне — например, из файла, сети или пользовательского ввода — его также следует проверять на разумные пределы перед выделением памяти.
## Работа с числами с плавающей точкой
Для
Сравнения с NaN:
возвращают false.
Поэтому, если массив может содержать NaN, необходимо заранее определить желаемую политику обработки таких значений.
Например, можно просто пропускать NaN:
Такой вариант корректно работает даже в случае, если первым элементом является NaN.
## Два прохода или один проход
Иногда минимум и максимум ищут двумя отдельными циклами:
Это корректно.
Асимптотическая сложность всё равно останется:
Однако один проход обычно предпочтительнее, поскольку каждый элемент массива загружается и обрабатывается сразу для обеих задач.
Для больших массивов это может быть выгоднее с точки зрения работы кэша процессора и пропускной способности памяти.
## Можно ли уменьшить количество сравнений
Простой алгоритм выполняет примерно два сравнения на каждый элемент:
То есть примерно:
сравнений.
Существует алгоритм, позволяющий уменьшить количество сравнений примерно до:
Идея заключается в обработке элементов парами:
1. Сначала два элемента сравниваются между собой.
2. Меньший сравнивается с текущим минимумом.
3. Больший сравнивается с текущим максимумом.
Пример:
Такой вариант теоретически эффективнее по количеству сравнений, но код становится сложнее.
Для большинства прикладных задач простой однопроходный вариант остаётся лучшим выбором.
## Проверка результата
После реализации полезно проверить несколько граничных случаев.
1. Массив из одного элемента — минимум и максимум должны быть равны этому элементу.
2. Все элементы одинаковы —
3. Отрицательные числа — минимум и максимум должны находиться корректно.
4. Пустой массив — функция должна вернуть ошибку, а не обращаться к
5. INT_MIN и INT_MAX — сравнение таких значений должно работать корректно.
Пример простых тестов:
Важно, что вызов:
безопасен только потому, что функция сначала проверяет:
и не обращается к
## Итоговый чек-лист
## Источники
Задача поиска минимума и максимума решается линейным сканированием. Инициализируем оба значения первым элементом, затем проходим по оставшимся элементам, обновляя минимум или максимум при необходимости.
C:
#include <stdio.h>
int main(void)
{
int arr[] = {3, 1, 4, 1, 5, 9, 2, 6};
int n = sizeof(arr) / sizeof(arr[0]);
int min = arr[0];
int max = arr[0];
for (int i = 1; i < n; i++) {
if (arr[i] < min)
min = arr[i];
if (arr[i] > max)
max = arr[i];
}
printf("min = %d, max = %d\n", min, max);
return 0;
}
Сложность: O(n) по времени и O(1) по памяти.
Это оптимально: любой алгоритм должен посмотреть каждый элемент хотя бы один раз.
## Почему инициализация первым элементом, а не нулём или INT_MAX
Частая ошибка новичка — инициализировать
min = 0 или max = 0.Если все элементы массива положительны,
min останется равным нулю, хотя нуля в массиве нет. Если все элементы отрицательны, max также останется равным нулю.Инициализация через
INT_MAX и INT_MIN из <limits.h> тоже работает корректно, но требует подключения дополнительного заголовка и привязана к диапазону конкретного типа.Инициализация первым элементом проще и универсальнее:
C:
int min = arr[0];
int max = arr[0];
## Возврат индексов
Часто нужны не только минимальное и максимальное значения, но и позиции соответствующих элементов.
Для этого добавим переменные
min_idx и max_idx:
C:
#include <stdio.h>
int main(void)
{
int arr[] = {7, 2, 9, 1, 5, 9, 0};
int n = sizeof(arr) / sizeof(arr[0]);
int min_val = arr[0];
int min_idx = 0;
int max_val = arr[0];
int max_idx = 0;
for (int i = 1; i < n; i++) {
if (arr[i] < min_val) {
min_val = arr[i];
min_idx = i;
}
if (arr[i] > max_val) {
max_val = arr[i];
max_idx = i;
}
}
printf("min = %d at index %d\n", min_val, min_idx);
printf("max = %d at index %d\n", max_val, max_idx);
return 0;
}
При наличии дубликатов этот вариант вернёт индекс первого вхождения.
Если нужен индекс последнего вхождения, используйте
<= вместо < и >= вместо >:
C:
if (arr[i] <= min_val) {
min_val = arr[i];
min_idx = i;
}
if (arr[i] >= max_val) {
max_val = arr[i];
max_idx = i;
}
## Вынесение в функцию
В C массив при передаче в функцию фактически передаётся как указатель, поэтому его размер необходимо передавать отдельно.
Результат можно вернуть через указатели-параметры:
C:
#include <stdio.h>
#include <stddef.h>
int find_minmax(const int *arr, size_t n,
int *out_min, int *out_max)
{
if (n == 0)
return -1;
*out_min = arr[0];
*out_max = arr[0];
for (size_t i = 1; i < n; i++) {
if (arr[i] < *out_min)
*out_min = arr[i];
if (arr[i] > *out_max)
*out_max = arr[i];
}
return 0;
}
int main(void)
{
int data[] = {-3, 8, 12, -1, 0, 5};
size_t len = sizeof(data) / sizeof(data[0]);
int lo;
int hi;
if (find_minmax(data, len, &lo, &hi) == 0)
printf("min = %d, max = %d\n", lo, hi);
else
printf("массив пуст\n");
return 0;
}
Ключевые моменты:
const int *arr— функция не изменяет содержимое массива.size_t n— стандартный беззнаковый тип для размеров и индексов.sizeofвозвращает значение типаsize_t.- Возвращаемое значение
intиспользуется как код ошибки. - При
n == 0функция не обращается кarr[0].
## Обработка пустого массива
Пустой массив не имеет ни минимума, ни максимума.
Обращение к:
C:
arr[0]
при
n == 0 является неопределённым поведением.Возможны несколько вариантов обработки:
| Подход | Когда уместен |
|---|---|
| Возврат кода ошибки | Функция общего назначения |
assert(n > 0) | Внутренняя функция, где пустой массив означает ошибку вызывающего кода |
| Возврат структуры с флагом | API с более богатым возвращаемым значением |
## Типичные ошибки новичков
### 1. Выход за границы массива
Неправильно:
C:
/* ОШИБКА: i <= n вместо i < n */
for (int i = 0; i <= n; i++) {
if (arr[i] < min)
min = arr[i];
}
Когда
i == n, происходит чтение за пределами массива.Правильно:
C:
for (int i = 0; i < n; i++) {
if (arr[i] < min)
min = arr[i];
}
Чтение за пределами массива приводит к неопределённому поведению. Программа может получить мусорное значение, завершиться с ошибкой или внешне продолжить работу, скрывая проблему.
### 2. Неверный расчёт размера через sizeof
Например:
C:
void process(int arr[])
{
int n = sizeof(arr) / sizeof(arr[0]); /* ОШИБКА */
}
Внутри функции
arr уже не является массивом в смысле операции sizeof.Фактически параметр:
C:
int arr[]
интерпретируется как:
C:
int *arr
Поэтому
sizeof(arr) вернёт размер указателя, обычно 4 или 8 байт, а не размер исходного массива.Размер следует передавать отдельно:
C:
void process(int arr[], size_t n)
{
/* ... */
}
### 3. Инициализация нулём
Неправильно:
C:
int min = 0;
int max = 0;
Если массив содержит только положительные числа:
C:
int arr[] = {5, 8, 10};
то
min ошибочно останется равным 0.Если массив содержит только отрицательные числа:
C:
int arr[] = {-5, -8, -10};
то
max ошибочно останется равным 0.Надёжнее использовать первый элемент:
C:
int min = arr[0];
int max = arr[0];
### 4. Сравнение беззнаковых значений с отрицательными числами
Если массив имеет тип:
C:
unsigned int
его элементы не могут быть отрицательными.
Проблемы могут возникнуть при сравнении беззнакового значения с отрицательной константой из-за неявных преобразований типов.
Например:
C:
unsigned int value = 10;
if (value < -1) {
/* результат может быть неожиданным */
}
В подобных ситуациях важно учитывать правила преобразования знаковых и беззнаковых целых типов в C.
### 5. Неверное использование sizeof для динамического массива
Для обычного массива:
C:
int arr[] = {1, 2, 3, 4};
size_t n = sizeof(arr) / sizeof(arr[0]);
такой способ работает.
Но для динамической памяти:
C:
int *arr = malloc(100 * sizeof(int));
выражение:
C:
sizeof(arr)
вернёт размер указателя, а не размер выделенного блока памяти.
Поэтому размер динамического массива необходимо хранить отдельно:
C:
size_t n = 100;
Если размер поступает извне — например, из файла, сети или пользовательского ввода — его также следует проверять на разумные пределы перед выделением памяти.
## Работа с числами с плавающей точкой
Для
float и double основной алгоритм остаётся тем же, однако появляется важный нюанс — значение NaN.Сравнения с NaN:
C:
x < NAN
x > NAN
x == NAN
возвращают false.
Поэтому, если массив может содержать NaN, необходимо заранее определить желаемую политику обработки таких значений.
Например, можно просто пропускать NaN:
C:
#include <stdio.h>
#include <math.h>
int main(void)
{
double arr[] = {3.14, NAN, -2.7, 1.0, NAN};
int n = sizeof(arr) / sizeof(arr[0]);
double min;
double max;
int found = 0;
for (int i = 0; i < n; i++) {
if (isnan(arr[i]))
continue;
if (!found) {
min = arr[i];
max = arr[i];
found = 1;
} else {
if (arr[i] < min)
min = arr[i];
if (arr[i] > max)
max = arr[i];
}
}
if (found)
printf("min = %f, max = %f\n", min, max);
else
printf("все элементы NaN или массив пуст\n");
return 0;
}
Такой вариант корректно работает даже в случае, если первым элементом является NaN.
## Два прохода или один проход
Иногда минимум и максимум ищут двумя отдельными циклами:
C:
for (...) {
/* поиск минимума */
}
for (...) {
/* поиск максимума */
}
Это корректно.
Асимптотическая сложность всё равно останется:
Код:
O(n)
Однако один проход обычно предпочтительнее, поскольку каждый элемент массива загружается и обрабатывается сразу для обеих задач.
Для больших массивов это может быть выгоднее с точки зрения работы кэша процессора и пропускной способности памяти.
## Можно ли уменьшить количество сравнений
Простой алгоритм выполняет примерно два сравнения на каждый элемент:
C:
if (arr[i] < min)
...
if (arr[i] > max)
...
То есть примерно:
Код:
2n
сравнений.
Существует алгоритм, позволяющий уменьшить количество сравнений примерно до:
Код:
1.5n
Идея заключается в обработке элементов парами:
1. Сначала два элемента сравниваются между собой.
2. Меньший сравнивается с текущим минимумом.
3. Больший сравнивается с текущим максимумом.
Пример:
C:
if (arr[i] < arr[i + 1]) {
if (arr[i] < min)
min = arr[i];
if (arr[i + 1] > max)
max = arr[i + 1];
} else {
if (arr[i + 1] < min)
min = arr[i + 1];
if (arr[i] > max)
max = arr[i];
}
Такой вариант теоретически эффективнее по количеству сравнений, но код становится сложнее.
Для большинства прикладных задач простой однопроходный вариант остаётся лучшим выбором.
## Проверка результата
После реализации полезно проверить несколько граничных случаев.
1. Массив из одного элемента — минимум и максимум должны быть равны этому элементу.
2. Все элементы одинаковы —
min == max.3. Отрицательные числа — минимум и максимум должны находиться корректно.
4. Пустой массив — функция должна вернуть ошибку, а не обращаться к
arr[0].5. INT_MIN и INT_MAX — сравнение таких значений должно работать корректно.
Пример простых тестов:
C:
#include <assert.h>
void test_single_element(void)
{
int arr[] = {42};
int lo;
int hi;
assert(find_minmax(arr, 1, &lo, &hi) == 0);
assert(lo == 42);
assert(hi == 42);
}
void test_empty(void)
{
int lo;
int hi;
assert(find_minmax(NULL, 0, &lo, &hi) == -1);
}
Важно, что вызов:
C:
find_minmax(NULL, 0, &lo, &hi)
безопасен только потому, что функция сначала проверяет:
C:
if (n == 0)
return -1;
и не обращается к
arr[0].## Итоговый чек-лист
- Инициализируйте
minиmaxпервым элементом массива. - Проверяйте
n == 0до обращения кarr[0]. - Передавайте размер массива в функцию явно.
- Используйте
size_tдля размеров и индексов. - Не используйте
sizeofдля определения длины массива через параметр функции. - Для динамически выделенной памяти храните размер отдельно.
- Для
floatиdoubleучитывайте наличие NaN. - Учитывайте неявные преобразования между знаковыми и беззнаковыми типами.
- Проверяйте граничные случаи.
- Для большинства задач одного линейного прохода достаточно.
## Источники
