Как определить сложность алгоритма: пошаговое решение
Дано: фрагмент кода из двух частей: цикл по от 1 до с вложенным циклом по от до , а после них цикл, который делит пополам, пока . Размер входа . Найти: точное число операций и оценку сложности в O-нотации.
Считаем, сколько раз выполняется тело каждого цикла, складываем последовательные части и оставляем самый быстрорастущий член. Ответ: , при это операций, сложность . Калькулятор сверху пересчитает всё под другое и другие виды циклов, ниже решение по шагам.
Решение по шагам
Дано. Фрагмент на Python. За одну операцию принимаем одно выполнение тела цикла:
s = 0
for i in range(1, n + 1): # внешний цикл: i = 1..n
for j in range(i, n + 1): # внутренний цикл: j = i..n
s += 1 # операция A
k = n
t = 0
while k > 1: # цикл деления пополам
k //= 2
t += 1 # операция B
Шаг 1. Внутренний цикл при фиксированном . Переменная пробегает значения от до включительно, то есть значений. При тело выполнится 16 раз, при пятнадцать раз и так далее, пока при не останется одна итерация:
| Значение i | 1 | 2 | 3 | … | 15 | 16 |
|---|---|---|---|---|---|---|
| Итераций по j | 16 | 15 | 14 | … | 2 | 1 |
Шаг 2. Суммируем по внешнему циклу. Число итераций внутреннего цикла зависит от , поэтому перемножать границы нельзя: итерации надо сложить по всем .
При получаем .
Шаг 3. Цикл с делением пополам. Переменная стартует с 16 и после каждой итерации уменьшается вдвое:
| Итерация | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| k до шага | 16 | 8 | 4 | 2 |
| k после шага | 8 | 4 | 2 | 1 |
После четвёртой итерации , условие ложно, и цикл останавливается. В общем виде число итераций равно , при это .
Шаг 4. Складываем последовательные части. Вложенный блок и цикл деления идут друг за другом, а не один внутри другого, поэтому их операции складываются:
Шаг 5. Оставляем главный член. Раскроем скобки: . С ростом квадратичное слагаемое обгоняет линейное и логарифмическое, а множитель в O-нотации отбрасывается.
Ответ. Тело цикла выполняется раз: 136 раз во вложенных циклах и 4 раза в цикле деления пополам. Временная сложность фрагмента , точнее .
Формула и откуда она берётся
Запись означает, что начиная с некоторого функция не превосходит , умноженную на константу:
Для нашего фрагмента подходят и : например, . Снизу ограничена функцией , поэтому оценка точная и записывается как . Константы и младшие члены на класс не влияют: они меняют время в разы, но не меняют порядок роста.
Из определения вытекают три правила, по которым сложность определяют прямо по коду. Последовательные блоки складываются, и в сумме остаётся самый тяжёлый: . Вложенные циклы с независимыми границами перемножаются: два цикла по дают . Если граница внутреннего цикла зависит от внешнего счётчика, как здесь от , итерации суммируют по всем .
Треугольная сумма дала вдвое меньше операций, чем полный квадрат , но класс остался тем же. Считают при этом не секунды, а число элементарных действий: присваиваний, сравнений, выполнений тела цикла. Проверки условий мы опустили: при каждом запуске цикла их на одну больше, чем итераций, и это меняет только константу.
Почему цикл с делением пополам даёт логарифм
После итераций переменная равна с округлением вниз. Цикл заканчивается, когда она дошла до единицы, то есть когда . Отсюда . Так же ведёт себя любой цикл, который умножает или делит счётчик на константу: шаг , или . Основание логарифма в O-нотации не пишут, потому что и отличаются лишь постоянным множителем.
Важно, где стоит такой цикл. У нас он идёт после вложенных и добавляет всего 4 операции. Если поставить цикл с удвоением на место внутреннего цикла по , он выполнится раз по итераций, и весь фрагмент станет . Этот вариант есть в калькуляторе: при вложенная часть даёт операций, итог 84.
Тот же порядок получается у сортировки слиянием: уровней деления и по операций слияния на каждом. Приём деления пополам лежит и в основе двоичного поиска, подробнее он разобран в статье про бинарный поиск по ответу.
Если же счётчик уменьшается вычитанием (), цикл сделает итерацию, и это уже линейная часть. Переключи хвостовой цикл в калькуляторе: при он даст 15 операций вместо 4, итог станет 151, но оценка не изменится.
Проверка ответа удвоением n
Оценку легко проверить без формул: посчитать операции при и при и посмотреть, во сколько раз выросло число. Для нашего фрагмента , и . При переходе к отношение равно и приближается к 4, как и положено квадратичному алгоритму.
| Класс сложности | Во сколько раз растёт T при удвоении n |
|---|---|
| O(1) | 1 |
| O(log n) | прибавляется константа, отношение стремится к 1 |
| O(n) | 2 |
| O(n log n) | чуть больше 2 |
| O(n²) | 4 |
| O(n³) | 8 |
| O(2ⁿ) | значение возводится в квадрат |
Тот же приём работает и с замером времени программы, если брать большие и повторять замер несколько раз. Но это лишь проверка: доказательством оценки служит подсчёт итераций.
Рекурсия: основная теорема кратко
Если алгоритм рекурсивный, вместо суммы по циклам получается рекуррентное соотношение. Для алгоритмов, которые делят задачу на подзадач размера и тратят на разбиение и сборку, работает основная теорема:
Сравнивают с . Если растёт медленнее, ответ . Если они одного порядка, ответ . Если растёт быстрее и выполнено условие регулярности, ответ .
Двоичный поиск: , здесь , , , случай равенства, итог . Сортировка слиянием: , итог . Умножение Карацубы: , итог .
Теорема не покрывает соотношения вида , где задача уменьшается на единицу, а не в разы. Их раскрывают подстановкой в сумму и получают тот же треугольник , что и в нашем фрагменте. Именно так выводится квадратичный худший случай быстрой сортировки.
Частые ошибки
- Перемножают границы, когда внутренний цикл зависит от внешнего. Для от 1 до произведение даст неверный ответ: сумма растёт как , и сложность будет .
- Считают цикл с делением пополам линейным. Цикл делает итераций: при их 4, а не 16.
- Перемножают последовательные блоки. Цикл по , за которым идёт ещё один цикл по , даёт операций, то есть , а не . Умножение нужно только для вложенности.
- Оставляют в ответе константы и младшие члены. Запись формально не ошибка, но в ответе ждут : класс определяется главным членом.
- Принимают цикл с постоянной границей за зависящий от n. Цикл от 1 до 100 внутри цикла по даёт операций, это , а не .
- Путают вычислительную сложность с цикломатической. Цикломатическая сложность считает число независимых путей в графе программы и от не зависит, время работы она не оценивает.
FAQ
Чем отличаются O, Θ и Ω? задаёт верхнюю границу роста, нижнюю, обе сразу. Для нашего фрагмента верны все три записи: , и . Когда в задаче просят определить сложность, обычно ждут точную оценку, записанную через .
Какую сложность писать: худшего или среднего случая? По умолчанию указывают худший случай: он гарантирует, что алгоритм не будет работать дольше. В нашем фрагменте нет ветвлений, поэтому все случаи совпадают. У быстрой сортировки они различаются: в среднем и в худшем.
Как определить сложность алгоритма по памяти? Тем же способом, только считают не операции, а объём дополнительных данных: массивы, глубину рекурсии, вспомогательные структуры. Наш фрагмент хранит лишь , и , поэтому его затраты памяти .
Можно ли определить сложность, просто замерив время программы? Замер подсказывает класс, если удваивать и смотреть на отношение времён, но не доказывает его. Время зависит от кэша процессора, интерпретатора и от младших членов, которые на малых ещё заметны. Строгий ответ даёт подсчёт итераций.
Коротко
- Для каждого цикла определи, сколько раз выполнится его тело в зависимости от .
- Если внутренний цикл зависит от внешнего счётчика, просуммируй итерации: .
- Цикл, который делит или умножает счётчик на константу, даёт итераций, а не .
- Последовательные части сложи, вложенные перемножь или просуммируй, оставь главный член без констант.
- В примере , сложность , а удвоение увеличивает число операций почти в 4 раза.
Похожие задачи
Как вычислить числа Фибоначчи: решение по шагам
Как вычислить числа Фибоначчи: рекуррентная формула, итеративный проход за n минус одно сложение, проверка формулой Бине через золотое сечение и цена наивной рекурсии.
Программирование/алгоритмыКак реализовать дек: пошаговое решение
Как реализовать дек на кольцевом массиве: формулы индексов head и tail через остаток от деления, трассировка push и pop с обоих концов, вариант на двусвязном списке, сложность операций.
Программирование/алгоритмыКак сложить числа в двоичной системе: разбор столбиком
Как сложить двоичные числа столбиком: правило для одного разряда, перенос в старший разряд, трассировка по всем восьми битам, проверка через десятичную систему и переполнение байта.
Орг./аналит. химияОкисление перманганатом калия: реакции в трёх средах
Как написать окисление перманганатом калия в кислой, нейтральной и щелочной средах: продукты восстановления марганца, метод электронного баланса, расстановка коэффициентов, расчёт титранта.
Химия (физич./структурная)Как найти активность иона: расчёт по Дебаю-Хюккелю
Как найти активность иона в растворе: ионная сила по всем ионам, коэффициент активности по предельному закону Дебая-Хюккеля, произведение f на c, разбор с числами и калькулятор.
ГенетикаКак найти частоту генотипов: закон Харди-Вайнберга
Разбор задачи по популяционной генетике: как найти частоту генотипов по закону Харди-Вайнберга, формула p2 плюс 2pq плюс q2, расчёт числа особей и калькулятор частот.