Что такое О-большое (Big O notation) ?

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

Итак, что важно для кодера? Как минимум — чтобы его код работал. А на втором месте идет то, как быстро этот код будет работать. Если вы пишете бэкенд для интернет-магазина, покупатели будут не сильно рады, находясь в корзине с покупками, если сайт начнет подвисать. Спасибо за это не скажут точно.

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

О-большое (Big O notation) — это математическая нотация, используемая в программировании для описания асимптотического поведения функций. В контексте алгоритмов она позволяет оценить, как увеличивается время выполнения или потребление памяти алгоритма с ростом размера входных данных.

Говоря понятным языком, О-большое не оценивает скорость выполнения кода в абсолютных величинах (типа секунды, минуты). Чем мощнее машина/сервер, тем быстрее один и тот же код будет выполняться — это очевидно. О-большое — это именно сложность выполнения кода по наихудшему сценарию (о сценариях выполнения — ниже), и оценивает, как при равных технических возможностях будет выполняться код. А именно: сколько понадобится произвести вычислений (выполнить инструкций), чтобы исполнить код.

Пример:
Один кодер написал код, предположим, на 100 строк — инструкции print("что-то"), а второй написал всего 3 строки: два цикла for по 10 итераций и так же print("что-то"). Визуально код первого выглядит громоздким, у второго он элегантен и прост. Но второй код будет выполняться медленнее. Почему? Там столько же инструкций формально, и будет выведено столько же строк на печать, но при каждом переборе цикла тратится время и мощности железа! Каждая отдельная инструкция в первом коде имеет сложность выполнения O(1), а во втором весь код — O(n²). Думаю, тут смысл понятен. О-большое оценивает количество инструкций кода, необходимых для его выполнения по наихудшему сценарию.

Типичные классы сложности О-большого

  1. O(1) — Константная сложность (постоянная сложность)

    • Время выполнения не зависит от размера входных данных.

    • Пример: Допустим, у вас есть список с данными (не важно какими). Список может быть любого размера: может быть из одного элемента, а может — из тысяч. Но доступ к элементу списка по индексу (за исключением связных списков, там нужно перебирать, и O — линейное) всегда будет один и тот же.

  2. O(log n) — Логарифмическая сложность

    • Время растет логарифмически с ростом n. То есть у нас есть определенное основание, и неважно, какой список: скорость выполнения будет уменьшаться на логарифмическое основание.

    • Пример: бинарный поиск.

  3. O(n) — Линейная сложность

    • Время растет прямо пропорционально размеру входных данных.

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

  4. O(n log n) — Линейно-логарифмическая сложность

    • Часто встречается в эффективных алгоритмах сортировки.

    • Пример: сортировка слиянием, быстрая сортировка.

  5. O(n²) — Квадратичная сложность (и иные аналогичные)

    • Время растет пропорционально квадрату размера входных данных.

    • Пример: пузырьковая сортировка, сортировка вставками.

    • Если у нас, допустим, три цикла for, это уже не квадратичная, а кубическая сложность O(n³).

  6. O(2ⁿ) — Экспоненциальная сложность

    • Время удваивается с каждым увеличением входных данных.

    • Пример: решение задачи коммивояжера полным перебором.

Наилучший и наихудший сценарии

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

  • Наилучший сценарий (например, в линейном поиске — O(1), то есть искомый элемент нашелся сразу).

  • Наихудший сценарий (то же самое, но O(n), если элемент в самом конце).

То есть можно оценивать как: *"На 100 запусков кода реальная сложность его выполнения будет примерно O(n)/2"* — так как иногда элемент будет находиться сразу, а иногда ближе к концу последовательности.

Важно ли О-большое?

Да, важно. Учитывать сложность выполнения нужно всегда, но в зависимости от задачи этим можно пренебречь, если объем данных, которые будет обрабатывать код, небольшой. В этом случае даже квадратичная сложность (как в пузырьковой сортировке) будет выполняться быстро.

Интересный факт: Даже Python использует несколько алгоритмов сортировки, включая сортировку вставками (в некоторых случаях), а она имеет квадратичную сложность.

Более подробно об этом можно прочитать в книге Кори Альтхофа "Computer Science для программиста-самоучки".