Перейти к основному содержимому

Нотация Большое O

Разработчику Аналитику Тестировщику
Архитектору Инженеру


Нотация Большое O

Нотация Большое O — это способ описания поведения алгоритмов при увеличении объёма входных данных. Она позволяет понять, как растёт время выполнения или потребление памяти программы, когда количество обрабатываемых элементов становится больше. Это скорее язык, на котором разработчики обсуждают эффективность решений.

Вы наверняка встречали выражения о том, что некий алгоритм выполняется с O(1), к примеру. Но задавались вопросом, что за O такое?

Основная цель — дать представление о масштабируемости алгоритма. Она помогает ответить на вопрос — что произойдёт с производительностью, если входные данные вырастут в десять, сто или миллион раз? Такой подход особенно важен при проектировании систем, которые должны работать быстро даже при большом количестве пользователей, записей в базе данных или файлов на диске.

Те же идеи с обозначениями Ω и Θ, разбором кода и ограничениями модели — в анализе эффективности алгоритмов. Ниже — практический справочник по классам сложности.

Закрепление на Python: построчный разбор примеров O(1)O(n!), трассировка бинарного поиска, ловушки in / pop(0)Lab / Big-O — шпаргалка.


Почему это важно

Представьте, что вы пишете программу для поиска имени человека в списке контактов. Если список содержит десять имён, поиск займёт доли секунды. Но если список расширяется до миллиона записей, разница между хорошим и плохим алгоритмом может составлять секунды, минуты или даже часы. Нотация Большое O позволяет заранее оценить, как поведёт себя решение в таких условиях, ещё до того, как оно будет запущено на реальных данных.

Это особенно ценно при сравнении нескольких подходов к одной задаче. Например, один способ сортировки массива может показывать линейный рост времени, другой — квадратичный, третий — логарифмический. Большое O даёт возможность выбрать наиболее подходящий вариант, основываясь на ожидаемом объёме данных и требованиях к скорости.


По-человечески — что означает "скорость"

Объясним без формул — как бариста в кофейне или курьер в доставке.

Представьте, что вы курьер. Вам нужно разнести N заказов — их может быть 10, 100 или 1 000 000. "Большая O" (Big O) — это не про секунды на часах. Это про то, насколько сильно вырастет время работы, если заказов станет в 2, в 10 или в 100 раз больше.

Ниже — шкала от "мгновенно" до "практически невозможно".

O(1) — "Константа" (молниеносно)

Как это: вы заходите в кофейню и берёте готовый стаканчик со стойки. Всегда один шаг.

Правило: сколько бы заказов ни пришло — хоть миллиард — вы справляетесь за фиксированное время.

Пример в коде: взять первый элемент массива arr[0], сложить 2 + 2.

O(log n) — "Логарифм" (очень быстро)

Как это: вы ищете нужную улицу в телефонной книге. Открываете посередине, понимаете, что нужная буква слева, отбрасываете правую половину и повторяете.

Правило: если заказов стало в 100 раз больше, нужно всего на ~7 шагов больше (потому что 2⁷ = 128).

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

O(n) — "Линейная" (нормально)

Как это: вы обычный курьер — берёте список и едете по одному адресу за другим.

Правило: заказов в 2 раза больше — работаете ровно в 2 раза дольше. Честно и предсказуемо.

Пример в коде: цикл for по массиву, чтобы найти нужное число.

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

Как это: сначала сортируете посылки по маршруту (умная сортировка), потом развозите. На сортировку уходит чуть больше времени, чем на простой объезд.

Правило: растёт быстрее, чем просто n, но ещё не катастрофа. Золотой стандарт для хороших сортировок.

Пример в коде: быстрая сортировка (QuickSort), сортировка слиянием.

O(n²) — "Квадратичная" (медленно)

Как это: нерадивый курьер берёт первый заказ и объезжает все остальные адреса, чтобы проверить, не тот ли это адрес. Потом берёт второй — и снова объезжает все.

Правило: заказов в 2 раза больше — времени нужно в 4 раза больше. При 1 000 заказах — около 1 000 000 действий. Уже заметно.

Пример в коде: вложенные циклы (два for друг в друге), пузырьковая сортировка.

O(2ⁿ) и O(n!) — "Экспонента / факториал" (катастрофа)

Как это: вы перебираете все возможные варианты маршрута — сначала адрес A, потом B, или B, потом A, или C, потом A…

Правило: при 10 заказах можно не успеть за разумное время; при 20 — дольше, чем существует Вселенная.

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

Шпаргалка — от лучшего к худшему

СкоростьКлассСколько действий на 100 заказовОценка
Лучше всехO(1)1⚡⚡⚡⚡⚡
Почти бесплатноO(log n)~7⚡⚡⚡⚡
ХорошоO(n)100⚡⚡⚡
НормальноO(n log n)~700⚡⚡
ПлохоO(n²)10 000🐢
УжасноO(2ⁿ)число с десятками нулей💀
Главный секрет на пальцах

Когда программисты смотрят на O-большое, они задают один вопрос: "Что будет, если данных станет очень-очень много?"

  • O(n) — алгоритм просто устанет, но справится.
  • O(n²) — "ляжет" при первой же большой нагрузке.
  • O(1) — вообще не заметит, что данных много.

На что смотреть в коде: вложенный цикл (цикл внутри цикла) — почти всегда O(n²), красный флаг. Если массив каждый раз делится пополам — скорее O(log n), вы на верном пути.

Ниже — тот же материал в "техническом" виде: точные определения, больше классов и примеры из реальных структур данных.


Константы и детали не важны

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

Точно так же нотация не учитывает начальные затраты, такие как загрузка данных в память или подготовка структур. Она фокусируется исключительно на том, как меняются ресурсы по мере роста размера входа. Это позволяет сравнивать алгоритмы в абстрактной, идеализированной модели, где важна только зависимость от объёма данных.


Справочник классов сложности

Play ITЗагрузка интерактивного демо…

Play ITЗагрузка интерактивного демо…

При росте размера входа n классы обычно упорядочивают от "быстрых" к "медленным". На практике чаще всего встречаются девять ступеней — от константы до факториала; реже, но полезно знать и O(√n).

КлассКак растёт время при увеличении nГде встречается
O(1)не зависит от nдоступ по индексу, хеш-таблица в среднем
O(log n)чуть растёт при большом nбинарный поиск, сбалансированное дерево
O(√n)медленнее линейногорешето Эратосфена, часть задач на диапазонах
O(n)пропорционально nодин проход по массиву, линейный поиск
O(n log n)чуть хуже линейногоmerge sort, quicksort (средний случай), heapsort
O(n²)при n в 10 раз больше — примерно в 100 раз дольшевложенные циклы, простые сортировки
O(n³)кубический ростнаивное умножение матриц, тройные циклы
O(2ⁿ)удваивается с каждым новым элементомполный перебор, наивная рекурсия без мемоизации
O(n!)факториальный роствсе перестановки, полный перебор маршрутов

Ниже — смысл каждого класса и узнаваемые примеры (алгоритмы и структуры данных).

O(1) — константное время

Время (или память) не растёт с размером входа: число шагов ограничено сверху константой.

  • Доступ к элементу массива по индексу — адрес ячейки вычисляется по базе и смещению.
  • Вставка и удаление в хеш-таблице (в среднем) — после вычисления хеша обращение к "корзине" фиксировано; в худшем случае при длинных цепочках коллизий деградация до O(n) (подробнее о хеш-таблицах).
  • Операции со стеком и очередью при известном указателе на вершину/голову.

O(log n) — логарифмическое время

На каждом шаге объём оставшейся работы уменьшается в фиксированное число раз (часто пополам). Удвоение n добавляет лишь один "слой" работы.

  • Бинарный поиск в отсортированном массиве — сравнение с серединой, отбрасывание половины диапазона.
  • Поиск, вставка, удаление в сбалансированном BST (красно-чёрное дерево, AVL) — высота дерева O(log n).
  • Индексы B-дерева в СУБД — тот же принцип на диске.

O(√n) — время порядка корня из n

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

  • Решето Эратосфена — для каждого простого p вычёркиваются кратные, начиная с ; суммарно около O(n log log n), на практике часто относят к "почти линейным", а перебор делителей до √n даёт именно O(√n) на один элемент.
  • Проверка простоты перебором делителей до √n.

O(n) — линейное время

Один (или фиксированное число) полных проходов по данным размера n.

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

O(n log n) — линейно-логарифмическое время

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

  • Сортировка слиянием, пирамидальная сортировка — гарантированные O(n log n).
  • Быстрая сортировка — в среднем O(n log n), в худшем O(n²) при неудачном выборе опорного элемента (сравнение сортировок).
  • Построение сбалансированного дерева из отсортированного массива (если вставлять по одному без балансировки — уже O(n log n) на серию вставок).

O(n²) — квадратичное время

Вложенные циклы по одному и тому же n: каждый элемент сравнивается или обрабатывается с каждым.

  • Пузырьковая, сортировка выбором, сортировка вставками — учебные алгоритмы с двойным проходом.
  • Любой код с двумя циклами for i … for j … по размеру входа без сокращения области поиска.
  • Наивное сравнение всех пар в списке (аналогия "кто с кем дружит" для n человек).

При n = 10 000 квадратичный алгоритм — порядка 100 миллионов итераций; линейный — 10 тысяч.

O(n³) — кубическое время

Три вложенных зависимых цикла по n или эквивалентная работа.

  • Наивное умножение двух плотных матриц n×n — тройной цикл по строкам, столбцам и внутренней сумме; ускоренные алгоритмы (Штрассен и далее) дают лучшую асимптотику, но в учебных задачах чаще показывают именно O(n³).
  • Часть задач динамического программирования с тремя измерениями состояния (например, объём "рюкзака" × индекс предмета × дополнительный параметр).
  • Линейный поиск внутри квадратичного циклаO(n²) × O(n) = O(n³).

O(2ⁿ) — экспоненциальное время

Число подзадач удваивается (или растёт с постоянной базой > 1) при добавлении одного элемента входа.

  • Наивная рекурсия без мемоизации — одни и те же подзадачи считаются многократно (классика — числа Фибоначчи "в лоб").
  • Задача коммивояжёра полным перебором — все маршруты по n городам.
  • Подмножества и сумма подмножества перебором 2ⁿ вариантов.

Уже при n ≈ 30 такие методы становятся неприемлемыми на одном ядре.

O(n!) — факториальное время

Перебор всех перестановок n объектов: n! вариантов.

  • Генерация всех перестановок (например, перебор порядка обхода точек).
  • Полный перебор маршрутов без отсечения ветвей в задаче коммивояжёра — ещё тяжелее, чем 2ⁿ на больших n.

Это самый медленный из перечисленных классов; даже n = 15 даёт порядка триллиона перестановок.

Как запомнить порядок

На собеседованиях и в код-ревью достаточно держать в голове цепочку: `O(1) <

O(log n) <

O(n) <

O(n log n) <

O(n²) < … <

O(2ⁿ) <

O(n!)`.

Между O(n) и O(n²) часто вставляют O(n log n) — "хорошие" сортировки и многие divide-and-conquer задачи.


Как определять сложность

Чтобы оценить сложность алгоритма, нужно проанализировать его структуру. Основное внимание уделяется циклам, рекурсии и вложенным операциям. Пошаговый разбор циклов и готовый код с оценкой — в Lab / 1128.

Если алгоритм не содержит циклов и работает с фиксированным количеством шагов, его сложность — O(1).

Если есть один цикл, который проходит по всем элементам, сложность — O(n).

Если цикл находится внутри другого цикла, и оба зависят от размера входа, сложность — O(n²). Три вложенных цикла по n дают O(n³) — типичный признак наивного умножения матриц или тройного перебора.

Рекурсивные алгоритмы требуют более внимательного анализа. Иногда рекурсия разбивает задачу пополам на каждом шаге, что приводит к логарифмической глубине вызовов. Если при этом на каждом уровне выполняется линейная работа, общая сложность — O(n log n). В других случаях рекурсия может порождать экспоненциальное количество вызовов, особенно если одни и те же подзадачи решаются многократно без запоминания результатов.


Память тоже имеет значение

Нотация Большое O применяется не только ко времени выполнения, но и к объёму используемой памяти. Это называется пространственной сложностью. Например, алгоритм, который создаёт копию всего входного массива, имеет пространственную сложность O(n). Алгоритм, использующий только несколько дополнительных переменных, независимо от размера входа, имеет пространственную сложность O(1).

Иногда можно пожертвовать памятью ради скорости. Например, хеш-таблица позволяет находить элементы за O(1), но требует дополнительного места для хранения индекса. В других случаях предпочтительнее экономить память, даже если это замедляет выполнение. Выбор зависит от контекста: доступных ресурсов, требований к производительности и характера задачи.


Структуры данных и их сложность

Выбор структуры данных напрямую определяет эффективность алгоритма. Разные структуры обеспечивают разную скорость выполнения базовых операций — добавления, поиска, удаления и доступа по индексу. Нотация Большое O позволяет сравнивать эти характеристики объективно.

Массив предоставляет мгновенный доступ к любому элементу по индексу — это O(1). Однако вставка или удаление элемента в середине массива требует сдвига всех последующих записей, что даёт линейную сложность O(n). Такое поведение делает массив удобным для хранения фиксированных наборов данных, но неэффективным при частых изменениях.

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

Хеш-таблица (или словарь) обеспечивает среднее время поиска, вставки и удаления O(1). Это достигается за счёт внутреннего преобразования ключа в адрес памяти с помощью хеш-функции. Однако в редких случаях, когда возникают коллизии и данные группируются в длинные цепочки, производительность может деградировать до O(n). Тем не менее, при хорошем распределении хеш-таблица остаётся одной из самых эффективных структур для хранения пар "ключ–значение".

Деревья, особенно сбалансированные, такие как красно-чёрное дерево или AVL-дерево, гарантируют логарифмическое время O(log n) для всех основных операций. Они полезны, когда важен упорядоченный доступ к данным — например, при реализации очередей с приоритетом или индексов в базах данных.

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

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


Распространённые ловушки

Одна из самых частых ошибок — игнорирование скрытой сложности стандартных библиотечных функций. Например, проверка наличия элемента в списке на многих языках выполняется за O(n), потому что реализована как линейный поиск. Тот же запрос к множеству (set) или хеш-таблице завершится за O(1). Разработчик, не знающий этого, может случайно создать квадратичный алгоритм, просто используя "удобный" метод в цикле.

Другая ошибка — предположение, что более короткий код всегда быстрее. Иногда компактная строка с вызовом встроенной функции скрывает тяжёлую операцию, такую как сортировка или полный обход коллекции. Читаемость важна, но она не отменяет необходимости понимать, что происходит "под капотом".

Третья ловушка — преждевременная оптимизация. Некоторые начинающие разработчики стараются применять самые "быстрые" алгоритмы даже там, где данные никогда не вырастут до критического размера. Это усложняет код без реальной выгоды. Нотация Большое O помогает принимать решения на основе масштаба задачи, а не на основе страха перед несуществующими проблемами.


Влияние на архитектуру

На уровне системы сложность алгоритмов влияет на выбор подходов к проектированию. Например, кэширование часто применяется для замены дорогостоящей операции O(n) или O(log n) на быстрый O(1) доступ к уже вычисленному результату. Это особенно эффективно, когда одни и те же запросы повторяются многократно.

Индексация в базах данных — ещё один пример использования сложности в архитектуре. Без индекса поиск записи по условию требует полного сканирования таблицы — O(n). Создание индекса по нужному полю превращает эту операцию в O(log n) или даже O(1), в зависимости от типа индекса. Цена — дополнительное место на диске и замедление операций вставки, но выигрыш в скорости чтения обычно перевешивает.

Потоковая обработка данных также строится с учётом сложности. Алгоритмы, которые могут обрабатывать элементы по одному, не сохраняя всю историю в памяти, имеют пространственную сложность O(1) и подходят для бесконечных потоков. Такие решения критически важны в системах мониторинга, аналитики в реальном времени и интернете вещей.


Как обучаться анализу

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

Затем переходите к сравнению. Реализуйте одну и ту же задачу двумя способами — например, линейный и бинарный поиск — и проанализируйте разницу. Запустите оба варианта на разных объёмах данных и наблюдайте, как растёт время выполнения. Это закрепит интуитивное понимание теории.

Постепенно включайте в анализ рекурсию, работу с графами, динамическое программирование (мемоизация, таблицы dp[i][w]). Каждая новая тема раскрывает новые типы зависимостей и помогает глубже понять, как устроены эффективные решения.

Два смысла "динамического программирования"

В алгоритмах — перекрывающиеся подзадачи и кэш (рюкзак, пути на сетке).

В математическом программировании — этапы, состояние s и уравнение Беллмана для оптимального управления. Рекуррентная идея общая, постановки разные; см. также лабораторию по DP.

Для линейной оптимизации (распределение ресурсов, план производства) см. раздел Математическое программирование — симплекс, двойственность, транспортная задача.


Алгоритмы в контексте реальных задач

Нотация Большое O проявляется не только в учебных примерах, но и в повседневной работе разработчика — даже если он не пишет алгоритмы с нуля. Современные приложения редко состоят из "чистых" вычислений; они взаимодействуют с сетью, базами данных, файловой системой и пользовательским интерфейсом. Тем не менее, понимание сложности помогает избегать узких мест на всех уровнях стека.

В веб-разработке часто встречается ситуация, когда сервер получает запрос на отображение списка пользователей с дополнительной информацией — например, количеством постов каждого из них. Наивная реализация может выполнить один запрос для получения списка пользователей, а затем отдельный запрос к базе данных для каждого пользователя, чтобы узнать число постов. При ста пользователях это приведёт к ста однотипным запросам — так называемой проблеме N+1. Сложность такого подхода — O(n) по числу обращений к базе, что создаёт высокую нагрузку на сеть и диск. Оптимизированное решение выполняет один объединённый запрос с использованием JOIN или агрегатных функций, снижая количество обращений до O(1). Разница становится критической при росте числа записей.

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

При работе с файлами и мультимедиа сложность определяет, насколько быстро приложение отреагирует на действия пользователя. Например, поиск фрагмента текста в большом документе с помощью линейного сканирования даёт O(n). Использование индекса или суффиксного дерева может снизить это до O(log n) или даже O(1) при предварительной обработке. Такие оптимизации особенно ценны в редакторах кода, поисковых системах и цифровых библиотеках.

Даже в пользовательском интерфейсе можно встретить проявления сложности. Рендеринг длинного списка элементов без виртуализации приводит к созданию тысяч DOM-узлов, что замедляет прокрутку и потребляет память — O(n) по числу видимых элементов. Виртуализация ограничивает отрисовку только теми строками, которые находятся в поле зрения, превращая задачу в O(1) по отношению к общему размеру списка.

Таким образом, нотация Большое O — это не только инструмент для теоретиков, но и практический ориентир для принятия архитектурных решений в самых разных областях разработки.


Границы применимости

Несмотря на свою полезность, нотация Большое O не даёт полной картины производительности. Она описывает асимптотическое поведение — то есть то, как алгоритм ведёт себя при стремлении размера входа к бесконечности. На малых объёмах данных более "медленный" алгоритм может оказаться быстрее из-за меньших накладных расходов или лучшего использования кэша процессора.

Например, сортировка вставками имеет квадратичную сложность O(n²), но на массивах из десяти–двадцати элементов она часто работает быстрее, чем быстрая сортировка O(n log n), потому что не требует рекурсии и имеет минимальные константы. Именно поэтому многие промышленные реализации сортировки (например, Timsort в Python или introsort в C++) переключаются на сортировку вставками на малых подмассивах.

Ещё один аспект — аппаратная специфика. Нотация игнорирует такие факторы, как иерархия памяти, параллелизм, ввод-вывод и задержки сети. Алгоритм с O(n) может быть медленнее O(n log n), если первый постоянно читает данные с диска, а второй полностью помещается в оперативную память. Аналогично, распараллеливание может превратить O(n) в O(n/k), где k — число ядер, но только если задача допускает независимую обработку частей.

Кроме того, нотация не учитывает качество реализации. Один и тот же алгоритм, написанный на высокоуровневом языке с динамической типизацией, может работать медленнее своей версии на компилируемом языке, даже при одинаковой теоретической сложности. Это не делает анализ бесполезным, но напоминает: он — лишь один из инструментов в наборе разработчика.

Для полной оценки производительности нужны дополнительные методы — профилирование, нагрузочное тестирование, анализ использования памяти и замеры времени выполнения на реальных данных. Нотация Большое O указывает направление, но окончательное решение принимается на основе эмпирических данных.


Историческая справка

Нотация Большое O была введена в математический анализ в конце XIX века немецким математиком Паулем Бахманном. Он использовал символ O (от немецкого Ordnung — "порядок") для описания порядка роста функций. Позже Эдмунд Ландау популяризировал эту нотацию, и она стала известна как "символика Ландау".

Изначально она применялась в теории чисел и анализе, но в середине XX века, с появлением первых компьютеров и формализацией понятия алгоритма, её начали использовать в информатике. Дональд Кнут, один из основоположников анализа алгоритмов, внёс решающий вклад в популяризацию нотации в программировании. В своей многотомной работе "Искусство программирования" он систематически применял Большое O для сравнения эффективности методов, что сделало его стандартом в академической и инженерной среде.

Сегодня нотация Большое O — неотъемлемая часть компьютерных наук. Она входит в учебные программы, используется на технических собеседованиях и служит общим языком для обсуждения производительности. Её сила — в простоте и абстрактности: она позволяет отвлечься от деталей реализации и сосредоточиться на сути — на том, как алгоритм масштабируется.

Практика на Python: Lab / Big-O — 1128 — примеры с построчным разбором, трассировками и блоком "частые вопросы из поиска"; готовые задачи ЕГЭ и олимпиад — Lab / 1122. На PascalLab / 1140 (пузырёк, линейный и бинарный поиск).