1. Основы анализа сложности алгоритмов и нотация Big O
Основы анализа сложности алгоритмов и нотация Big O
Представь, что ты пишешь программу для поиска контакта в телефонной книге на 10 000 имён. Один вариант — листать страницы по порядку. Другой — открыть нужную букву алфавита и искать только там. Оба способа найдут результат, но сколько времени займёт каждый? Именно этот вопрос — сколько времени и памяти потребует алгоритм — и решает анализ сложности.
На собеседовании тебя не попросят доказать математическую теорему. Но попросят ответить: «Какова сложность твоего решения?» — и от этого ответа зависит, пройдёшь ты дальше или нет.
Что такое Big O и зачем она нужна
Нотация Big O — это способ описать, как растёт время выполнения алгоритма при увеличении входных данных. Она не говорит «программа работает 3 миллисекунды». Она говорит: «если данных станет вдвое больше, время вырастет вдвое» или «время вырастет вчетверо».
Почему нас не интересует точное время? Потому что оно зависит от процессора, операционной системы, компилятора. А скорость роста — это свойство самого алгоритма, которое не меняется от железа.
> Big O — это не про секунды. Это про то, как алгоритм масштабируется. Два решения одной задачи могут отличаться не в разы, а в тысячи раз при больших входах.
Основные классы сложности
Разберём самые частые классы от лучшего к худшему:
— константная сложность. Время не зависит от размера входа. Пример: доступ к элементу массива по индексу.
— логарифмическая. На каждом шаге задача делится пополам. Классический пример — бинарный поиск в отсортированном массиве.
При бинарный поиск сделает около 20 итераций. Это в 50 000 раз быстрее линейного перебора.
— линейная. Время растёт пропорционально количеству элементов. Любой цикл, который проходит по всем элементам один раз.
— линейно-логарифмическая. Характерна для эффективных сортировок: merge sort, quick sort (в среднем). Это лучшая возможная сложность для сравнительной сортировки.
— квадратичная. Два вложенных цикла по итераций каждый. Сортировка пузырьком, наивное сравнение всех пар.
— экспоненциальная. Время удваивается с каждым новым элементом. Наивная рекурсия для чисел Фибоначчи — классический пример.
Сложность по памяти
Кроме времени, на собеседовании часто спрашивают про 空间复杂度 — сложность по памяти. Принцип тот же: сколько дополнительной памяти потребляет алгоритм.
| Сложность | Пример | Память | |-----------|--------|--------| | | Обмен двух переменных | Константа | | | Копирование массива | Линейно от входа | | | Матрица | Квадратично |
Когда интервьюер спрашивает «Can you do it in-place?», он спрашивает: можешь ли ты решить задачу без выделения дополнительной памяти — с по памяти.
Как определять сложность на практике
Три простых правила:
Типичная ошибка: скрытые операции внутри цикла
На первый взгляд — два цикла, . Но push_back внутри может вызвать перевыделение памяти. В худшем случае каждая вставка — из-за копирования. Итого: . На собеседовании именно такие нюансы отделяют сильных кандидатов от остальных.
Amortized complexity — амортизированная сложность
Иногда операция иногда дорогая, но в среднем дешёвая. Например, push_back в std::vector: обычно это , но при расширении буфера — . Однако если усреднить по серии операций, получается амортизированно. Это важное понятие — интервьюеры ценят, когда кандидат его понимает и может объяснить.
Что запомнить
На собеседовании тебе нужно мгновенно определять сложность по структуре кода: один цикл — , два вложенных — , деление пополам — . И всегда уточняй: «Это худший случай или средний?»