Рекурсия (recursion) — вызов в функции самой себя. Рекурсия заканчивается на граничном условии — значении, которое функция знает без нового вызова.

Зачем нужно

Многие объекты определяются через самих себя: . Рекурсивная функция повторяет такое определение почти дословно. Главный пример курса — Сортировка слиянием: отсортировать половины тем же способом и слить.

Как работает

Факториал

Определение: (аксиома), при .

функ. factorial(n)
    если n = 0              // граничное условие
        return 1
    return n * factorial(n − 1)

Вызов раскрывается до граничного условия:

.

Восходящая и нисходящая

АргументПримерГраница
нисходящаяуменьшается →
восходящаяувеличивается →

В обоих случаях вызовы идут ступеньками, пока не упрутся в граничное условие.

Не из лекции: восходящий пример

, до границы шагов, . При аргумент уходит от границы, и рекурсия не кончится.

Не из лекции: как доказывать рекурсию

Индукцией — той же, что стоит за рекурсивным определением. Для факториала: factorial(0) = 1 = 0!; если factorial(n − 1) = , то factorial(n) = .

Для рекурсии, которая делит задачу пополам, нужна сильная индукция по размеру: «для всех меньших размеров верно — значит, верно и для этого». Так доказывается Сортировка слиянием.

Стек вызовов и память

Не из лекции

Каждый вызов ждёт, пока вернётся вложенный, и держит свои переменные. Все незавершённые вызовы лежат на стеке вызовов (call stack). Память рекурсии — это её глубина, а не общее число вызовов:

Вызовов всегоГлубинаПамять стека
факториал
сортировка слиянием

Стек небольшой: в Linux по умолчанию 8 МБ. Проверка на C++ (GCC 16, -O0): факториал с глубиной отрабатывает, с — падает с Segmentation fault. Циклом тот же факториал занимает памяти. См. Время работы алгоритма.

Тонкости

  • Граничное условие должно быть достижимо из любого допустимого аргумента. Нисходящий factorial(−1) до 0 не дойдёт, восходящий до — тоже.
  • Каждый шаг должен строго приближать к границе. В сортировке слиянием это непустые половины: с базой right − left = 1 пустой отрезок вызывает сам себя бесконечно — см. L04.

Частые ошибки

  • Забыть граничное условие или проверять его не тем сравнением (= 1 вместо ≤ 1).
  • Считать, что рекурсия «бесплатна» по памяти: глубина — это на стеке.
  • Глубокая рекурсия там, где хватило бы цикла: падение по переполнению стека при .

Углубление — АиСД L05

Быстрая сортировка — рекурсия, в которой части неравные: их размеры зависят от того, куда встал опорный элемент. Отсюда разброс по глубине:

РазбиениеГлубинаПамять стека
примерно пополам
одна часть всегда пустая

Чтобы рекурсия кончалась, каждая часть должна быть строго короче отрезка. В схеме Ломуто это обеспечивает опорный элемент: он стоит на своём месте и в рекурсию не попадает. В схеме Хоара опорный не выкидывается, и короче части становятся только при правильном выборе середины — иначе на [1, 2] рекурсия бесконечна, см. L05.

Не из лекции

Худшую глубину можно убрать: рекурсивно сортировать только меньшую часть, а большую — циклом в том же вызове. Тогда каждый вложенный вызов получает не больше половины, и глубина на любом входе.

Углубление — АиСД L06

Рекурсия, которая вызывает себя дважды с перекрывающимися аргументами, пересчитывает одно и то же много раз:

функция fib(n)
    если n ≤ 2
        return 1
    иначе
        return fib(n − 1) + fib(n − 2)

fib(n − 2) вызывается и из fib(n), и из fib(n − 1), и так на каждом уровне. На лекции это рекурсивный способ: простой, но медленный («−» по времени в сравнении способов). Табличный и линейный способы считают каждое значение один раз — это уже Динамическое программирование.

Не из лекции

Вызовов у fib(n) ровно — экспонента: fib(30) делает 1 664 079 вызовов. Та же беда у рекуррентного для сочетаний: — 310 235 039 вызовов. Глубина при этом всего , так что стек не переполнится — программа просто будет считать очень долго.

Отличие от сортировки слиянием, где тоже два вызова: там половины не пересекаются, и общий объём работы — .