Минимум и максимум массива на C: один проход, индексы и типичные ошибки новичков

## Базовый алгоритм: один проход по массиву

Задача поиска минимума и максимума решается линейным сканированием. Инициализируем оба значения первым элементом, затем проходим по оставшимся элементам, обновляя минимум или максимум при необходимости.

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.
  • Учитывайте неявные преобразования между знаковыми и беззнаковыми типами.
  • Проверяйте граничные случаи.
  • Для большинства задач одного линейного прохода достаточно.

## Источники

 
Назад
Верх Низ