1. Асимптотический анализ и Big O нотация: измерение временной сложности алгоритмов
Асимптотический анализ и Big O нотация: измерение временной сложности алгоритмов
Представьте, что вы написали алгоритм поиска дубликатов в массиве. На вашем рабочем ноутбуке он обрабатывает тысячу записей за секунды. Вы загружаете код на мощный сервер, подаете ему миллион записей, и программа «зависает» на час. Если мы измеряем эффективность кода в секундах, мы попадаем в ловушку: время выполнения зависит от тактовой частоты процессора, фоновых процессов операционной системы, языка программирования и даже температуры чипа в данный момент. Секунды не говорят ничего о самом алгоритме. На алгоритмических собеседованиях, в том числе в Яндексе, инженеров интересует не абсолютное время на конкретном железе, а то, как поведет себя алгоритм при масштабировании данных.
Для ответа на этот вопрос используется асимптотический анализ. Это математический подход, который оценивает, как изменяется количество базовых операций программы при стремлении размера входных данных к бесконечности. Размер входных данных традиционно обозначают переменной . Базовой операцией считается любое простое действие, занимающее фиксированное время: присваивание переменной, арифметическая операция, сравнение двух чисел или обращение к элементу массива по индексу.
Языком асимптотического анализа является нотация Big O (О большое). Запись вида описывает верхнюю границу роста количества операций. Проще говоря, она гарантирует, что при увеличении алгоритм сделает не больше, чем операций, умноженных на некий константный коэффициент. Big O описывает худший сценарий развития событий — пессимистичную оценку, которая так важна при проектировании высоконагруженных систем.
Главные правила вычисления сложности
Чтобы перевести реальный код на язык Big O, не нужно считать каждую строчку. Асимптотический анализ намеренно огрубляет картину, чтобы выделить главное. Для этого применяются два фундаментальных правила.
Правило 1: Отбрасывание констант. Если ваш алгоритм проходит по массиву из элементов дважды, он делает операций. Математически это записывается как . Однако в Big O константы игнорируются, и сложность сокращается до .
Почему мы так легко избавляемся от двойки? Суть асимптотики — в характере роста. Если размер данных увеличится в раз, время работы алгоритма увеличится в раз. Время работы алгоритма тоже увеличится ровно в раз. Константа влияет на наклон прямой на графике, но прямая остается прямой. При бесконечно больших разница между и меркнет по сравнению с тем, как ведет себя, например, квадратичная функция.
Правило 2: Отбрасывание менее значимых слагаемых. Часто алгоритм состоит из нескольких этапов. Допустим, сначала вы сортируете массив (что занимает операций в простейшем случае), а затем один раз проходите по нему циклом (еще операций). Общее количество действий: .
По правилам Big O мы оставляем только слагаемое с наибольшей скоростью роста. В данном случае это . Сложность алгоритма будет . Чтобы понять логику, подставим реальные числа. Пусть . Тогда . Общее число операций составит . Доля линейного прохода ( операций) в общем объеме работы составляет менее . При влияние линейного слагаемого станет микроскопическим. Младшие степени всегда поглощаются старшими.
Основные классы сложности
В программировании чаще всего встречаются несколько стандартных классов сложности. Понимание того, как они соотносятся друг с другом — базовый навык для оптимизации кода.
!Сравнение скорости роста функций сложности
Константное время: Сложность означает, что время выполнения алгоритма вообще не зависит от размера входных данных. Будь в массиве десять элементов или миллиард, программа выполнит одно и то же количество действий.
Обращение к элементу списка по индексу в Python происходит за один шаг, так как под капотом вычисляется точный адрес в памяти. К константным операциям также относятся базовые математические вычисления (сложение, умножение) и проверка условий if.
Логарифмическое время: Алгоритмы с такой сложностью на каждом шаге отбрасывают половину оставшихся данных. Классический пример из реальной жизни — поиск слова в бумажном словаре. Вы не читаете каждую страницу с первой до последней. Вы открываете словарь посередине, понимаете, в какой половине находится нужное слово, и повторяете процесс для этой половины.
Даже если в словаре миллион страниц, вам понадобится не более проверок, чтобы найти нужную. Увеличение размера данных в два раза () добавляет всего одну дополнительную операцию. Это крайне эффективный класс сложности, к которому мы будем стремиться в задачах на бинарный поиск.
Линейное время: Если алгоритму необходимо «посмотреть» на каждый элемент входных данных хотя бы один раз, его сложность будет не лучше .
Здесь размер массива напрямую диктует количество итераций. Если массив увеличится в раз, цикл выполнится в раз больше раз.
Квадратичное время: Такая сложность возникает, когда на каждый элемент массива нужно пройтись по всему массиву еще раз. Визуальный маркер квадратичной сложности — вложенные циклы, зависящие от одной и той же коллекции.
!Выполнение вложенного цикла и рост числа операций
Для массива из элементов функция напечатает пар. Для массива из элементов — уже миллион. Алгоритмы с асимптотикой часто не проходят тесты на платформах вроде LeetCode или в контестах Яндекса, если превышает . Главная задача на собеседовании — увидеть квадратичное решение (часто это перебор «в лоб») и придумать, как свести его к или .
Зависимость от нескольких переменных
Частая ошибка новичков — обозначать любой размер данных буквой . Это работает, если на вход подается одна коллекция. Но что, если алгоритм принимает два независимых массива разных размеров?
Здесь два последовательных цикла. Если размер arr_a равен , а размер arr_b равен , общая сложность составит . Мы не можем свести это к , потому что не знаем соотношение и . Возможно, равно , а равно миллиону.
Аналогично, если циклы вложены друг в друга:
В этом случае на каждый из элементов первого массива мы делаем шагов во втором. Сложность будет . Если на собеседовании вам дают две строки или два массива, сразу вводите две переменные для оценки.
Худший, лучший и средний случаи
Рассмотрим классическую задачу: найти заданное число в массиве (линейный поиск).
Сколько операций выполнит этот код? Ответ зависит от входных данных.
arr[0]. Цикл завершится на первой итерации. Сложность: (Омега большое используется для нижней границы).В индустрии и на интервью по умолчанию говорят именно о худшем случае и используют нотацию Big O. Инженеру важно знать гарантии: код точно не будет работать дольше, чем . Оценивать систему по лучшему случаю опасно — это ведет к отказам серверов при пиковых или нестандартных нагрузках.
Комплексный разбор алгоритма
Чтобы закрепить навык, проанализируем функцию, состоящую из нескольких блоков. Попробуйте сначала оценить сложность самостоятельно, читая код сверху вниз.
Разберем по частям:
arr. Это константное время , которое по правилам превращается в .Складываем все вместе: . Применяем правило отбрасывания констант: . Применяем правило отбрасывания младших слагаемых: функция растет значительно быстрее, чем . Линейным слагаемым можно пренебречь. Итоговая асимптотическая временная сложность всего алгоритма: .
Умение быстро читать код и сводить его к одной математической функции — это первый шаг к успешному прохождению алгоритмических секций. Интервьюер ожидает, что перед тем как писать код, вы озвучите: «Я планирую решить эту задачу за по времени». Это задает рамки ожиданий и показывает, что вы контролируете эффективность своего решения еще до его реализации.