ГлавнаяБлогBig O: почему O(n²) падает на больших данных
Алгоритмы

Big O: почему O(n²) падает на больших данных

Узнайте, как Big O помогает предсказать производительность кода. Поймите, почему O(n²) сбоит на production, и как исправить с помощью Set/Map.

Al
Редакция Algolitalgolit.ru
12 мин чтения30 июня 2026 г.

Почему O(n²) работает в демо, но падает на реальных данных

Вы когда-нибудь писали код, который отлично работал на тестовых 100 записях, но на production с 100 000 записей вызывал таймауты? Скорее всего, вы столкнулись с O(n²). Big O — это не про абсолютное время, а про то, как быстро растёт количество операций при увеличении входных данных. Понимание Big O превращает загадочные тормоза в предсказуемую проблему, которую можно исправить сменой структуры данных.

Что такое Big O и как он работает

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

  • O(1) — константное время (доступ к элементу массива по индексу)
  • O(log n) — логарифмическое (бинарный поиск)
  • O(n) — линейное (один проход по массиву)
  • O(n log n) — линейно-логарифмическое (быстрая сортировка)
  • O(n²) — квадратичное (вложенные циклы)
  • O(2ⁿ) — экспоненциальное (рекурсия с ветвлением)

Рассмотрим пример: поиск дубликатов в массиве. Наивный подход — два вложенных цикла:

def has_duplicate(arr):
    for i in range(len(arr)):
        for j in range(i + 1, len(arr)):
            if arr[i] == arr[j]:
                return True
    return False

Это O(n²): для каждого из n элементов мы проходим по оставшимся. С 100 элементами — 4950 операций, с 100 000 — почти 5 миллиардов. Теперь оптимизированная версия с Set:

def has_duplicate(arr):
    seen = set()
    for x in arr:
        if x in seen:  # O(1)
            return True
        seen.add(x)
    return False

Здесь только один проход O(n), а проверка вхождения в Set — O(1). Разница колоссальна.

Главная ловушка в production: скрытый O(n²)

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

# O(n*m) — для каждого заказа ищем пользователя линейно
def enrich_orders(orders, users):
    result = []
    for order in orders:
        user = next(u for u in users if u['id'] == order['user_id'])
        result.append({**order, 'user_name': user['name']})
    return result

Здесь внутри цикла по заказам (n) мы проходим по всем пользователям (m) — итого O(n*m). Если оба списка по 10 000, это 100 миллионов операций. Исправление — построить Map один раз:

# O(n+m) — строим Map за O(m), затем ищем за O(1)
def enrich_orders(orders, users):
    user_map = {u['id']: u for u in users}
    result = []
    for order in orders:
        user = user_map.get(order['user_id'])
        result.append({**order, 'user_name': user['name']})
    return result

Теперь всего 20 000 операций — линейно.

Типичные проблемы и их решения

Проблема: .find()/.includes() внутри .map()

Методы find, includes, indexOf внутри цикла — классический O(n²). Заменяйте их на Set или Map.

Проблема: преждевременная оптимизация

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

Проблема: игнорирование памяти

Использование Map/Set увеличивает потребление памяти (O(n) дополнительно). На очень больших данных это может привести к OutOfMemoryError. Всегда оценивайте trade-off время/память.

Как диагностировать O(n²) на production

Если endpoint замедляется пропорционально квадрату входных данных, скорее всего, у вас O(n²). Проверьте: при удвоении n время растёт в 4 раза? Ищите вложенные циклы и вызовы find/includes/indexOf внутри map/forEach. Используйте профилировщик (CPU flame graph) для поиска горячих точек. Отслеживайте p99 latency — квадратичная сложность проявляется на хвосте распределения.

Когда не стоит оптимизировать

Не оптимизируйте:

  • Если n мало и не растёт (например, список из 10 элементов)
  • Если код выполняется редко (один раз при запуске)
  • Если нет профилировщика, подтверждающего, что это горячий путь

Преждевременная оптимизация усложняет код и увеличивает риск багов без реальной выгоды.

Вопрос на собеседовании

Как HashMap снижает сложность с O(n²) до O(n) и когда не стоит оптимизировать?

Ответ: Вложенный цикл для поиска (например, find внутри map) даёт O(n²). HashMap/Set заменяет линейный поиск на константный: строим хеш-таблицу за O(n), затем каждый поиск O(1) — итого O(n+m). Не стоит оптимизировать, когда n мало или код не на горячем пути; сначала профилируйте. Также учитывайте память: хеш-таблица может вызвать OOM.

Практическое задание

Возьмите реальный код, где есть find или includes внутри цикла (например, обогащение заказов пользователями). Замерьте время для n = 1000, 10 000, 100 000 — увидите квадратичный рост. Перепишите с Map и замерьте снова — время станет линейным. Используйте профилировщик для подтверждения. Затем замерьте память версии с Map на больших данных. Наконец, найдите участок с малым n и попробуйте его «оптимизировать» — убедитесь, что код стал сложнее, а выигрыша нет.

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

#Big O#O(n²)#производительность#оптимизация#Set/Map
Al
Редакция Algolit

Пишем про алгоритмы, подготовку к собеседованиям и карьеру в IT — так, чтобы было понятно и полезно.

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →