Изучите тему «Алгоритмы и сложность» с нуля
1Что измеряет сложность
Алгоритм — это конечный список точных шагов, решающих задачу, например сортировку списка или поиск имени в телефонной книге.
Два алгоритма могут решать одну и ту же задачу, но тратить очень разное время. Сложность описывает, как объём работы растёт при увеличении входных данных.
Размер входных данных обозначают n: число элементов для сортировки, длина списка, число вершин графа.
Представьте поиск имени в телефонной книге на 1000 страниц. Чтение каждой страницы займёт до 1000 шагов. Если открывать книгу посередине и каждый раз делить её пополам, понадобится около 10 шагов.
Временная сложность считает шаги. Пространственная сложность считает дополнительную память. Обе записываются как функция от n.
Обычно рассматривают худший случай: наибольший объём работы среди всех входов размера n. Иногда спрашивают средний или лучший случай.
Самопроверка Алгоритм, у которого объём работы растёт как n², выполняется за 1 секунду при n = 1000. Сколько времени он займёт примерно при n = 2000?
- 1 секунду
- 2 секунды
- 4 секунды
- 8 секунд
Ответ: 4 секунды. При удвоении n значение n² умножается на 2² = 4, поэтому время вырастет примерно с 1 до 4 секунд. 2 секунды были бы ответом для линейного роста, а 8 — для n³.
2Big O и классы роста
Запись Big O сохраняет только то, что важно при больших n. Постоянные множители отбрасывают и оставляют лишь самое быстрорастущее слагаемое.
3n² + 5n + 2 ⇒ O(n²)
При n = 1000 слагаемое n² равно 3 000 000, а 5n — всего 5000. Меньшие слагаемые почти не влияют на сумму.
Эти классы встречаются снова и снова, от самого медленного роста к самому быстрому:
- O(1), константа: чтение элемента массива по индексу.
- O(log n), логарифмическая: двоичный поиск, который на каждом шаге уменьшает задачу вдвое.
- O(n), линейная: один проход по всем элементам, например поиск наибольшего значения.
- O(n log n): хорошие алгоритмы сортировки, например сортировка слиянием.
- O(n²), квадратичная: два вложенных цикла по элементам, например пузырьковая сортировка.
- O(2ⁿ), экспоненциальная: перебор всех подмножеств. Быстро становится непригодной.
Big O — это верхняя оценка: объём работы растёт не быстрее этого. Ω (Big Omega) — нижняя оценка, а Θ (Theta) — точная оценка, одновременно верхняя и нижняя.
Основание логарифма в Big O не важно, потому что логарифмы с разными основаниями отличаются лишь постоянным множителем.
Самопроверка Чему равна Big O для 4n + 2n² + 100?
- O(100)
- O(2n²)
- O(n)
- O(n²)
Ответ: O(n²). При больших n доминирует слагаемое n², а постоянный множитель 2 отбрасывается. O(n) сохраняет не то слагаемое, а O(2n²) забывает отбросить константу.
3Поиск, сортировка и рекурсия
Несколько алгоритмов встречаются почти в каждом курсе. Выучите их сложности как факты.
- Линейный поиск проверяет элементы один за другим: O(n). Двоичный поиск требует отсортированного массива и на каждом шаге делит диапазон пополам: O(log n).
- Сортировки пузырьком, вставками и выбором сравнивают и меняют элементы местами во вложенных циклах: O(n²). Сортировка вставками работает за O(n) на уже отсортированном массиве.
- Сортировка слиянием делит массив пополам, сортирует части и сливает их: O(n log n) в любом случае, с O(n) дополнительной памяти.
- Быстрая сортировка разбивает массив вокруг опорного элемента: в среднем O(n log n), но в худшем случае O(n²), например для отсортированного массива, если опорным взят первый элемент.
- Никакая сортировка, основанная только на сравнении элементов, не может превзойти Ω(n log n) в худшем случае. Сортировка подсчётом быстрее, потому что не сравнивает.
Рекурсивная функция вызывает саму себя для меньшего входа, пока не дойдёт до базового случая, на который отвечает напрямую. Время её работы записывают в виде рекуррентного соотношения:
- T(n) = T(n/2) + O(1) даёт O(log n), как двоичный поиск.
- T(n) = 2T(n/2) + O(n) даёт O(n log n), как сортировка слиянием.
- T(n) = T(n − 1) + O(n) даёт O(n²).
- T(n) = 2T(n − 1) + O(1) даёт O(2ⁿ).
Стоит знать три идеи построения алгоритмов. Разделяй и властвуй делит задачу, решает части и объединяет результаты. Жадный подход на каждом шаге выбирает локально лучший вариант. Динамическое программирование запоминает ответы на перекрывающиеся подзадачи, чтобы вычислять их один раз.
Самопроверка Какова временная сложность быстрой сортировки в худшем случае?
- O(log n)
- O(n log n)
- O(n)
- O(n²)
Ответ: O(n²). При плохих опорных элементах каждое разбиение оставляет n − 1 элементов по одну сторону, что даёт O(n²). O(n log n) — это её средний случай и худший случай сортировки слиянием.
4Метод по шагам
Чтобы найти временную сложность фрагмента кода:
- Определите, что такое n: размер входных данных.
- Посчитайте, сколько раз выполняется каждый цикл. Цикл, который каждый раз прибавляет 1, выполняется около n раз. Цикл, который удваивает или делит пополам, выполняется около log₂ n раз.
- Вложенные циклы перемножаются. Циклы, идущие один за другим, складываются.
- Для рекурсивной функции запишите рекуррентное соотношение: сколько вызовов, какого размера, плюс работа на каждый вызов.
- Оставьте самое быстрорастущее слагаемое и отбросьте константы.
- Проверьте на небольшом числе, например n = 8 или n = 1024.
В теоретическом вопросе сначала назовите алгоритм или идею, о которой речь, затем вспомните его сложность и условия применения (отсортированные данные, неотрицательные веса, перекрывающиеся подзадачи).
5Разобранный пример
Пройдите каждый шаг. Попробуйте предсказать следующий, прежде чем открыть его.
Разобранный примерКакова временная сложность этого кода? for i = 1 to n: (j = 1; while j < n: j = j × 2)
1n — это число, до которого работают циклы.
2i изменяется от 1 до n с шагом 1, поэтому внешний цикл выполняется n раз.
3j принимает значения 1, 2, 4, 8, … и останавливается, достигнув n. После k витков j = 2k, поэтому цикл остановится примерно через log₂ n витков.
4Внутренний цикл находится внутри внешнего, поэтому количества перемножаются.
n · log₂ n ⇒ O(n log n)
5При n = 1024 внутренний цикл выполняется log₂ 1024 = 10 раз, поэтому тело выполняется 1024 · 10 = 10 240 раз. Это намного меньше, чем n² = 1 048 576.
6Типичные ошибки
- Перемножать циклы, идущие один за другим. n + n = 2n — это по-прежнему O(n).
- Сохранять константы, как в O(2n) или O(n/2). Обе записи равны O(n).
- Считать цикл с удвоением за n витков. Он выполняется около log₂ n раз.
- Применять двоичный поиск к неотсортированным данным или к связному списку, в котором середина недостижима за один шаг.
- Приводить средний случай быстрой сортировки, когда спрашивают худший.
- Думать, что Big O означает «точно». O(n²) — это верхняя оценка; Θ(n²) — точный рост.
Самопроверка Функция выполняет цикл из n итераций, а затем, отдельно, ещё один цикл из n итераций. Какова её временная сложность?
- O(2n)
- O(log n)
- O(n)
- O(n²)
Ответ: O(n). Циклы, идущие один за другим, складываются: n + n = 2n, а константа отбрасывается, поэтому получается O(n). O(n²) было бы верно, только если бы один цикл находился внутри другого.
7Как выглядят экзаменационные задачи
- Лёгкая: ключевые термины и факты. Сложность двоичного поиска, сортировки слиянием или доступа к элементу массива, смысл O, Ω и Θ, что такое рекурсия.
- Средняя: применение понятий. Сложность фрагмента кода, решение рекуррентного соотношения, как меняется время при удвоении n, какой алгоритм подходит для задачи.
- Сложная: случаи и анализ. Основная теорема о рекуррентных соотношениях, худшие случаи быстрой сортировки, жадный подход и динамическое программирование, классы P, NP и NP-полные.
P содержит задачи, которые можно решить за полиномиальное время. NP содержит задачи, решения которых можно проверить за полиномиальное время. Каждая задача из P входит в NP; равно ли P = NP, до сих пор неизвестно.
Изучайте шаг за шагом в приложенииИзучайте шаг за шагом в приложении