Открыть сервис

Древовидная рекурсия

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

Определение и принцип работы

Древовидная рекурсия возникает, когда тело функции содержит два или более рекурсивных вызова. Каждый такой вызов, в свою очередь, может породить новые вызовы, и так далее. Граф вызовов такой функции представляет собой дерево, где корнем является исходный вызов, а листьями — базовые случаи (терминальные условия), при которых рекурсия завершается.

Ключевая особенность древовидной рекурсии — многократное повторное вычисление одних и тех же подзадач. Это приводит к высокой вычислительной сложности, часто экспоненциальной, что делает её неэффективной для решения задач с перекрывающимися подзадачами без применения оптимизаций, таких как мемоизация.

Примеры классических задач

Числа Фибоначчи

Наиболее известный пример древовидной рекурсии — наивное рекурсивное вычисление чисел Фибоначчи. Функция fib(n) определяется как:

  • fib(0) = 0
  • fib(1) = 1
  • fib(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) = 1
  • C(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 →