1. Основы анализа сложности алгоритмов и нотация Big O
Основы анализа сложности алгоритмов и нотация Big O
Добро пожаловать в курс «Алгоритмы и структуры данных: Практический подход». Мы начинаем наше путешествие с фундаментальной темы, без которой невозможно стать действительно сильным разработчиком. Сегодня мы разберемся, как измерять эффективность кода, не используя секундомер.
Зачем нам измерять сложность?
Представьте, что вы написали функцию для поиска дубликатов в списке пользователей. На вашем мощном рабочем ноутбуке с 10 тестовыми пользователями она работает мгновенно. Вы отправляете код в продакшн, где пользователей — миллион. Внезапно сервер зависает на несколько минут. Почему так произошло?
Проблема в том, что «быстро работает на моем компьютере» — это не метрика. Скорость выполнения кода в секундах зависит от множества факторов:
* Мощности процессора * Загруженности операционной системы * Языка программирования * Компилятора или интерпретатора
Нам нужен универсальный способ оценки алгоритмов, который не зависит от «железа». Нам нужно знать, как будет вести себя алгоритм при увеличении объема входных данных. Именно для этого используется Big O Notation (О-нотация).
Что такое Big O?
Big O — это способ описать, как быстро растет время выполнения алгоритма (или потребление памяти) по мере роста входных данных.
Мы не считаем секунды. Мы считаем количество операций.
Представьте, что — это размер входных данных (например, количество элементов в массиве). Big O показывает зависимость количества операций от .
!Сравнение скорости роста различных алгоритмических сложностей
Давайте разберем основные виды сложности на простых примерах.
O(1) — Константная сложность
Это идеальный сценарий. Алгоритм выполняется за одно и то же время, независимо от того, сколько данных вы ему скормили — 10 элементов или 10 миллионов.
Пример из жизни: Вы берете книгу и открываете её на первой странице. Неважно, в книге 100 страниц или 1000 — действие «открыть первую страницу» занимает одинаковое время.
В программировании это часто доступ к элементу массива по индексу:
Здесь сложность , где символизирует постоянное количество операций, не зависящее от размера массива items.
O(n) — Линейная сложность
Время выполнения растет прямо пропорционально количеству данных. Если данных стало в 2 раза больше, алгоритм будет работать в 2 раза дольше.
Пример из жизни: Вам нужно прочитать книгу. Если в книге 100 страниц, вы потратите 2 часа. Если 200 страниц — 4 часа.
В коде это обычно простой цикл, который проходит по всем элементам:
Если в списке items 5 элементов, цикл выполнится 5 раз. Если элементов — раз. Сложность записывается как , где — количество элементов во входных данных.
O(n²) — Квадратичная сложность
Это зона опасности. Время выполнения растет пропорционально квадрату количества элементов. Часто встречается в алгоритмах с вложенными циклами.
Пример из жизни: У вас есть группа из человек, и каждый должен пожать руку каждому другому человеку.
В коде это выглядит так:
Давайте посчитаем операции: * Если , мы выполним операций. * Если , мы выполним операций.
Математически это записывается как , где — размер входных данных. Поэтому сложность — .
> Важно: Старайтесь избегать при работе с большими данными. Если , то будет равен 10 миллиардам операций, что может «повесить» ваш сервер.
O(log n) — Логарифмическая сложность
Это очень эффективные алгоритмы. Время выполнения растет очень медленно, даже если данных становится очень много. Обычно это алгоритмы, которые на каждом шаге отсеивают половину данных.
Пример из жизни: Поиск имени в телефонном справочнике. Вы открываете книгу посередине. Если имя на букву «К», а вы открыли на «М», вы отбрасываете вторую половину книги и ищете только в первой. Затем снова делите оставшуюся часть пополам.
Формула логарифма выглядит так:
Где — это количество входных данных, а — количество операций (шагов), необходимых для решения задачи. Это вопрос: «Сколько раз нужно разделить на 2, чтобы получить 1?».
Например, если у вас 1024 элемента (), то алгоритму потребуется всего 10 шагов, так как .
Как определять сложность: Правила упрощения
При анализе Big O нас интересует только доминирующий фактор роста. Мы отбрасываем константы и менее значимые слагаемые.
1. Отбрасывайте константы
Рассмотрим код:
Казалось бы, мы проходим по списку дважды, значит это . Но в Big O константы не важны. Разница между и ничтожна по сравнению с разницей между и . Поэтому превращается просто в .
2. Оставляйте только старшую степень
Пусть время работы вашего алгоритма описывается формулой:
Где — общее время выполнения, — квадратичная часть, — линейная часть, а — константная часть.
При огромных значениях (например, миллион), будет настолько больше, чем , что линейной частью можно пренебречь.
* *
Триллион против пятидесяти миллионов. Поэтому мы говорим, что сложность этого алгоритма — .
Пространственная сложность (Space Complexity)
До сих пор мы говорили о времени (Time Complexity). Но память компьютера тоже не бесконечна. Space Complexity показывает, сколько дополнительной памяти требует алгоритм.
* Если вы создаете несколько переменных, это (константная память). * Если вы копируете массив длины в новый массив, это . * Если вы создаете таблицу размером , это .
!Визуализация потребления памяти различными алгоритмами
Худший, средний и лучший случаи
Обычно, когда говорят о Big O, имеют в виду худший случай (Worst Case).
Представьте, что вы ищете число в массиве. * Лучший случай: Число находится первым. Сложность . * Худший случай: Число находится последним или его вообще нет. Сложность .
Мы ориентируемся на худший случай, чтобы гарантировать, что наша программа не «упадет» даже при самых неудачных данных.
Резюме
В следующей статье мы применим эти знания на практике и разберем массивы и списки — самые популярные структуры данных, с которыми вы будете работать ежедневно.