Перейти к основному содержимому

Когда работа удваивается

O(2ⁿ) означает экспоненциальное время: один дополнительный элемент входных данных удваивает работу. Это класс роста, при котором алгоритмы перестают быть пригодными — не на огромных входных данных, а уже на средних.

Экспоненциальная работа обычно возникает из рекурсии, которая ветвится: функция вызывает саму себя дважды, и каждый из этих вызовов снова вызывает себя дважды. Вызовы образуют дерево, которое удваивается на каждом уровне.


Рекурсивное вычисление Фибоначчи

Последовательность Фибоначчи начинается с 0, 1, и каждое следующее число равно сумме двух предыдущих: 0, 1, 1, 2, 3, 5, 8, 13, …

Определение напрямую переводится в рекурсивную функцию: fib(n) возвращает fib(n-1) + fib(n-2). Она короткая и правильная — и экспоненциальная, потому что каждый вызов порождает ещё два, а одни и те же значения вычисляются снова и снова. Одно только fib(2) в большем вызове пересчитывается тысячи раз.

Фибоначчи с рекурсией

Глобальный счётчик считает каждый вызов. Вычисление fib(10) = 55 требует 177 вызовов — ради одного небольшого числа. Дерево вызовов удваивается на каждом уровне.

Prompt
Напишите программу на Python с рекурсивной функцией fib (fib от n равно fib от n-1 плюс fib от n-2, а n меньше 2 возвращается как есть), которая увеличивает глобальный счётчик вызовов при каждом вызове. Вычислите fib(10) и выведите результат и общее количество вызовов.
Вывод: полная программа на Python
Fibonacci with recursion

Почему происходит взрыв

Следите за количеством вызовов, когда n растёт с шагом 5. Оно растёт не на шаг — оно умножается. Каждое +1 к n даёт примерно ×1,6 к числу вызовов; каждое +5 — больше чем ×10.

При n = 50 количество превысило бы 40 миллиардов вызовов. В этом и состоит экспоненциальная ловушка: код выглядит безобидно, а небольшие тесты завершаются мгновенно.

Подсчёт вызовов при росте n

От fib(5) до fib(25) входные данные растут в 5 раз — а количество вызовов растёт более чем в 16 000 раз, с 15 до 242 785.

Prompt
Напишите программу на Python с рекурсивной функцией fib, которая считает свои вызовы в глобальном счётчике. Для n = 5, 10, 15, 20, 25 сбросьте счётчик, выполните fib(n) и выведите по одной выровненной строке на каждое n с количеством вызовов, чтобы показать экспоненциальный рост.
Вывод: полная программа на Python
Count the calls as n grows

Решение: цикл

Проблема не в Фибоначчи — она в том, что одни и те же значения вычисляются снова и снова. Цикл, который поднимается от 0 и 1 и хранит только два последних значения, вычисляет каждое число ровно один раз.

Это время O(n) и память O(1): 24 шага вместо 242 785 вызовов для fib(25). Тот же ответ, другой класс роста — именно для этого и нужен анализ алгоритмов.

Фибоначчи с циклом

Две переменные поднимаются по последовательности; каждый проход вычисляет одно новое число. fib(25) = 75025 за 24 шага — рекурсивной версии для того же ответа понадобилось 242 785 вызовов.

Prompt
Напишите программу на Python, которая вычисляет fib(25) с помощью цикла вместо рекурсии: храните два последних значения в двух переменных и поднимайтесь от 0 и 1, считая шаги цикла. Выведите результат и количество шагов.
Вывод: полная программа на Python
Fibonacci with a loop