Рекурсия (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 вызовов. Глубина при этом всего , так что стек не переполнится — программа просто будет считать очень долго.Отличие от сортировки слиянием, где тоже два вызова: там половины не пересекаются, и общий объём работы — .