Древовидная рекурсия
Древовидная рекурсия — это форма рекурсии, при которой функция в процессе своего выполнения вызывает себя более одного раза, порождая структуру вызовов, напоминающую дерево. В отличие от линейной рекурсии, где каждый вызов порождает не более одного нового рекурсивного вызова, древовидная рекурсия приводит к экспоненциальному росту числа вызовов в зависимости от глубины рекурсии.
Определение и принцип работы
Древовидная рекурсия возникает, когда тело функции содержит два или более рекурсивных вызова. Каждый такой вызов, в свою очередь, может породить новые вызовы, и так далее. Граф вызовов такой функции представляет собой дерево, где корнем является исходный вызов, а листьями — базовые случаи (терминальные условия), при которых рекурсия завершается.
Ключевая особенность древовидной рекурсии — многократное повторное вычисление одних и тех же подзадач. Это приводит к высокой вычислительной сложности, часто экспоненциальной, что делает её неэффективной для решения задач с перекрывающимися подзадачами без применения оптимизаций, таких как мемоизация.
Примеры классических задач
Числа Фибоначчи
Наиболее известный пример древовидной рекурсии — наивное рекурсивное вычисление чисел Фибоначчи. Функция fib(n) определяется как:
fib(0) = 0fib(1) = 1fib(n) = fib(n-1) + fib(n-2)дляn > 1
При вычислении fib(5) функция вызывает fib(4) и fib(3). Каждый из этих вызовов порождает свои подвызовы, образуя дерево. Количество вызовов растёт экспоненциально (по закону, близкому к O(2^n)), хотя фактическое число вызовов для fib(n) равно 2*fib(n+1) - 1. Например, для вычисления fib(30) требуется более 2,6 миллионов вызовов, в то время как итеративный алгоритм или рекурсия с мемоизацией выполняют всего 31 операцию.
Вычисление биномиальных коэффициентов
Биномиальный коэффициент C(n, k) может быть вычислен рекурсивно с использованием свойства Паскаля:
C(n, 0) = C(n, n) = 1C(n, k) = C(n-1, k-1) + C(n-1, k)для0 < k < n
Эта рекурсия также является древовидной, так как каждый вызов (кроме базовых) порождает два новых. Без оптимизации она приводит к экспоненциальному росту числа вызовов.
Задача о размене монет
Классическая задача комбинаторики: сколькими способами можно разменять сумму S, используя монеты заданных номиналов. Рекурсивное решение, перебирающее варианты включения или исключения каждой монеты, также образует дерево вызовов. Каждый шаг порождает две ветви: одна использует текущую монету (уменьшая сумму), другая переходит к следующей монете.
Сравнение с другими типами рекурсии
| Тип рекурсии | Количество рекурсивных вызовов на шаг | Пример | Вычислительная сложность |
|---|---|---|---|
| Линейная | 1 | Факториал, обход списка | O(n) |
| Хвостовая | 1 (вызов в конце) | Факториал с аккумулятором | O(n) |
| Древовидная | 2 и более | Числа Фибоначчи (наивно) | O(2^n) (часто) |
| Взаимная | Вызовы разных функций | Алгоритм пар-нечет | Зависит от задачи |
Преимущества и недостатки
Преимущества
- Простота реализации: Древовидная рекурсия часто является прямым отражением математического определения задачи, что делает код коротким и понятным.
- Естественность для задач с ветвлением: Задачи, связанные с обходом деревьев (например, бинарных деревьев поиска), игровыми деревьями (минимакс), рекурсивным спуском в синтаксическом анализе — естественно решаются с помощью древовидной рекурсии.
- Параллелизм: Ветви дерева вызовов могут вычисляться независимо, что открывает возможности для распараллеливания.
Недостатки
- Экспоненциальная сложность: Для многих задач наивная древовидная рекурсия приводит к катастрофически большому числу вызовов, делая алгоритм непрактичным для больших входных данных.
- Повторные вычисления: Одна и та же подзадача может решаться многократно. Например, в
fib(5)подзадачаfib(3)вычисляется дважды. - Переполнение стека: Глубина рекурсии может быть большой, хотя для древовидной рекурсии это менее критично, чем для линейной, из-за экспоненциального роста числа вызовов на каждом уровне.
Оптимизация: мемоизация
Основной способ борьбы с недостатками древовидной рекурсии — мемоизация (кэширование результатов). При мемоизации результат каждого вызова функции для конкретных аргументов сохраняется в таблице (например, в словаре или массиве). При повторном вызове с теми же аргументами результат не вычисляется заново, а берётся из кэша.
Мемоизация превращает древовидную рекурсию в рекурсию с запоминанием (top-down dynamic programming). Для чисел Фибоначчи это снижает сложность с O(2^n) до O(n). В общем случае, если количество уникальных подзадач полиномиально, мемоизация делает алгоритм полиномиальным.
Применение в информатике
Несмотря на потенциальную неэффективность, древовидная рекурсия широко используется в тех областях, где её применение естественно и где мемоизация или иные оптимизации решают проблему производительности:
- Обход деревьев: Рекурсивный обход бинарных деревьев (префиксный, инфиксный, постфиксный) — классический пример древовидной рекурсии, где каждый узел порождает вызовы для левого и правого поддеревьев.
- Алгоритмы на графах: Поиск в глубину (DFS) на графах, представленных в виде дерева, может быть реализован с помощью древовидной рекурсии.
- Синтаксический анализ: Рекурсивный спуск — метод синтаксического анализа, где каждая грамматическая конструкция описывается своей функцией, которая может вызывать функции для подконструкций.
- Игровые алгоритмы: Минимакс и альфа-бета отсечение для игр с двумя игроками (шахматы, шашки) используют древовидную рекурсию для обхода дерева игры.
- Динамическое программирование: Нисходящее динамическое программирование (top-down) с мемоизацией — это, по сути, древовидная рекурсия, оптимизированная кэшированием.
- Комбинаторные задачи: Генерация всех перестановок, сочетаний, подмножеств — часто реализуется через рекурсивный перебор, образующий дерево вызовов.
Интересные факты
- Термин «древовидная рекурсия» ввёл в широкий обиход Харольд Абельсон и Джеральд Джей Сассман в книге «Структура и интерпретация компьютерных программ» (SICP), где они подробно разбирают пример с числами Фибоначчи.
- В функциональных языках программирования (например, Haskell, Scheme) древовидная рекурсия используется особенно часто из-за отсутствия циклов и естественной поддержки рекурсии.
- Без мемоизации вычисление
fib(100)с помощью наивной древовидной рекурсии потребовало бы больше атомов, чем существует в наблюдаемой Вселенной, что делает такой подход полностью непрактичным.
Источники
- Абельсон Х., Сассман Д. Дж. «Структура и интерпретация компьютерных программ»
- Кормен Т., Лейзерсон Ч., Ривест Р., Штайн К. «Алгоритмы: построение и анализ»
- Седжвик Р., Уэйн К. «Алгоритмы на Java»
- Документация и учебные материалы по рекурсии в различных языках программирования (Python, C++, JavaScript, Haskell)
BFOmetr — база данных и аналитика по компаниям России.
На главную BFOmetr →