Основы сложности

Базовый Algorithms
≈ 2 мин чтения

Размер входа — отправная точка

Чтобы говорить о сложности, сначала нужно понять, что именно растёт. Обычно размер входа обозначают буквой n. Для массива это чаще всего количество элементов, для строки — количество символов, а для числовой задачи размер входа зависит от того, какие данные алгоритм действительно обрабатывает.

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

boolean containsZero = false;

for (int value : numbers) {
    if (value == 0) {
        containsZero = true;
    }
}

Если массив станет в два раза длиннее, цикл в общем случае выполнит примерно в два раза больше итераций. Такой рост называют линейным и записывают как O(n).

Почему константы обычно отбрасывают

Представим, что алгоритм два раза проходит по одному массиву:

for (int value : numbers) {
    System.out.println(value);
}

for (int value : numbers) {
    System.out.println(value * 2);
}

При длине массива n получится примерно 2n повторений. Но при асимптотической оценке нас интересует форма роста, поэтому постоянный множитель не меняет класс сложности: O(2n) записывают как O(n).

По той же причине в выражении вроде n * n + n при больших n главным становится квадратный член. Такую сложность записывают как O(n²).

Последовательно — складываем, вложенно — умножаем

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

for (int i = 0; i < numbers.length; i++) {
    for (int j = 0; j < numbers.length; j++) {
        System.out.println(numbers[i] + numbers[j]);
    }
}

Внутреннее действие выполняется для каждой пары индексов: примерно n * n раз. Отсюда оценка O(n²).

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