Рекурсия в Python: как работает, когда применять, примеры
Этот урок переведён с английского оригинала.Read in EnglishLeer en españolСообщить об ошибке в переводе
Представьте матрёшку. Вы открываете самую большую и находите внутри матрёшку поменьше. Открываете её, а там ещё меньше. Вы открываете их одну за другой, пока не дойдёте до самой маленькой, которая уже не открывается. На ней всё и заканчивается.
Рекурсия работает так же. Рекурсивная функция вызывает саму себя для уменьшенной версии задачи, снова и снова, пока не дойдёт до точки остановки. Это один из самых изящных приёмов в программировании, и когда вы его поймёте, то начнёте замечать его повсюду.
В этом уроке вы узнаете, что такое рекурсия, как стек вызовов отслеживает каждый шаг, когда рекурсия подходит для задачи и как избежать самых частых ошибок. Мы начнём с простых примеров и дойдём до классических алгоритмов вроде факториала и чисел Фибоначчи.
Что такое рекурсия?
Рекурсия возникает, когда функция вызывает саму себя. Вот и всё определение. Но зачем функции вообще вызывать саму себя?
Возьмём обратный отсчёт от 5. Процесс можно описать так: «Чтобы отсчитать от N, назовите N, а затем отсчитайте от N минус 1. Остановитесь, когда дойдёте до 0». Это рекурсивное описание: инструкция ссылается сама на себя.
5 4 3 2 1 Go!
Функция countdown(5) выводит 5, а затем вызывает countdown(4). Та выводит 4 и вызывает countdown(3). Так продолжается, пока countdown(0) не выведет «Go!» и не остановится. Больше вызовов нет.
У каждой рекурсивной функции есть две обязательные части. Если не хватает хотя бы одной, функция будет работать вечно (или пока Python её не остановит).
Базовый случай: когда остановиться
Базовый случай задаёт условие, при котором рекурсия останавливается. Это самая маленькая матрёшка, та, что не открывается. Без базового случая функция вызывает себя бесконечно.
Рекурсивный случай описывает ту часть, где функция вызывает саму себя с более простыми входными данными. Каждый вызов должен приближать вас к базовому случаю.
3 2 1 Go!
Полезное правило: всегда пишите базовый случай первым. Затем пишите рекурсивный случай, который к нему приближается. Такой порядок не даст вам случайно забыть условие выхода.
Как работает стек вызовов
Когда функция вызывает саму себя, Python не забывает, где он был. Он использует стек вызовов: стопку «кадров» (frames), где каждый кадр помнит локальные переменные функции и место, с которого нужно продолжить работу.
Представьте стопку стикеров. Каждый раз, когда вы вызываете функцию, Python пишет новый стикер с подробностями этого вызова и кладёт его сверху. Когда функция возвращает результат, Python снимает верхний стикер и возвращается к тому, что лежит под ним.
Calling factorial(5) Calling factorial(4) Calling factorial(3) Calling factorial(2) Calling factorial(1) Base case! factorial(1) = 1 factorial(2) = 2 * factorial(1) = 2 factorial(3) = 3 * factorial(2) = 6 factorial(4) = 4 * factorial(3) = 24 factorial(5) = 5 * factorial(4) = 120 Result: 120
Обратите внимание, как сообщения «Calling» уходят всё глубже, а затем результаты всплывают обратно. Стек вызовов растёт, пока вызовы уходят внутрь, и уменьшается, пока результаты возвращаются наружу. Этот ритм «туда и обратно» и есть сердцебиение рекурсии.
Классические примеры рекурсии
Факториал
Факториал числа N (записывается как N!) равен произведению N на все положительные целые числа меньше него. Например, 5! = 5 x 4 x 3 x 2 x 1 = 120.
Факториал рекурсивен по своей природе: 5! = 5 x 4!, а 4! = 4 x 3! и так далее. Базовый случай: 1! = 1.
1 120 3628800
Сумма элементов списка
Вот ещё один шаблон: чтобы рекурсивно найти сумму списка, прибавьте первый элемент к сумме остальных.
15 10 0
Шаблон такой: обработать базовый случай (для пустого списка вернуть 0), а затем разбить задачу на «первый элемент» плюс «рекурсивный вызов для остальных». Этот шаблон подходит для многих операций со списками.
Рекурсия или цикл: что когда использовать
Всё, что можно сделать с помощью рекурсии, можно сделать и циклом. Так когда же выбирать одно, а когда другое?
def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)
print(factorial(5)) # 120def factorial(n):
result = 1
for i in range(2, n + 1):
result = result * i
return result
print(factorial(5)) # 120Для простых задач вроде факториала циклы обычно быстрее и расходуют меньше памяти. Каждый рекурсивный вызов добавляет кадр в стек вызовов, а цикл раз за разом использует одни и те же переменные.
Рекурсия раскрывается во всей красе, когда задача по своей природе древовидная или самоподобная. Например, обход структуры папок, разбор вложенных данных (вроде JSON или HTML) и обход деревьев и графов. В таких случаях рекурсия даёт более чистый и короткий код.
Когда рекурсия действительно к месту
Рекурсия особенно хороша в задачах, где нужно обойти все ветви древовидной структуры. Вот практический пример: превращение вложенного списка в плоский.
[1, 2, 3, 4, 5, 6, 7]
Попробуйте написать это без рекурсии. Вам понадобится вручную вести стек и аккуратно отслеживать, что уже обработано. Рекурсивная версия читается почти как обычная фраза: «Каждый элемент, который сам является списком, разворачиваем, а остальные оставляем как есть».
Частые ошибки в рекурсии
Ошибка 1: нет базового случая
5 4 3 2 1
Ошибка 2: вызовы не приближаются к базовому случаю
120
Практические упражнения
Начните с простых и постепенно переходите к более сложным. Каждое упражнение опирается на то, что вы уже изучили. Помните: всегда пишите базовый случай первым!
Прочитайте этот код и предскажите, что он выведет. Затем напишите инструкции print(), которые дают точно такой же вывод.
def mystery(n):
if n <= 0:
return
mystery(n - 1)
print(n)
mystery(3)Обратите внимание: print(n) стоит после рекурсивного вызова. Хорошо подумайте о порядке.
Напишите рекурсивную функцию sum_to, которая принимает положительное целое число n и возвращает сумму всех чисел от 1 до n.
Например: sum_to(5) должна вернуть 1 + 2 + 3 + 4 + 5 = 15.
Базовый случай: sum_to(1) возвращает 1.
Выведите результаты sum_to(5) и sum_to(10) на отдельных строках.
Эта рекурсивная функция должна возводить base в степень exp (как base ** exp). Но в ней две ошибки.
Исправьте обе, чтобы вывод был таким:
8
1
81Напишите рекурсивную функцию fibonacci, которая возвращает n-е число Фибоначчи.
Последовательность Фибоначчи начинается так: 0, 1, 1, 2, 3, 5, 8, 13, ...
Правила:
fibonacci(0) возвращает 0fibonacci(1) возвращает 1fibonacci(n) = fibonacci(n-1) + fibonacci(n-2)Выведите результаты для n = 0, 1, 5 и 10 на отдельных строках.
Напишите рекурсивную функцию reverse, которая принимает строку и возвращает её задом наперёд.
Например: reverse("hello") должна вернуть "olleh".
Подсказка: последний символ плюс развёрнутая версия всего, кроме последнего символа.
Выведите результаты reverse("hello") и reverse("Python") на отдельных строках.
Напишите рекурсивную функцию count_digits, которая принимает неотрицательное целое число и возвращает количество его цифр.
Например:
count_digits(0) возвращает 1count_digits(5) возвращает 1count_digits(123) возвращает 3count_digits(99999) возвращает 5Подсказка: деление на 10 с помощью // убирает последнюю цифру.
Выведите результаты для 0, 5, 123 и 99999 на отдельных строках.
Итоги
Вот что вы узнали о рекурсии:
| Понятие | Что означает |
|---|---|
| Рекурсия | Функция, которая вызывает саму себя |
| Базовый случай | Условие, которое останавливает рекурсию |
| Рекурсивный случай | Часть, где функция вызывает себя с более простыми входными данными |
| Стек вызовов | Механизм Python для отслеживания вложенных вызовов функций |
| Переполнение стека | То, что происходит, когда рекурсия уходит слишком глубоко (нет базового случая) |
| Рекурсия или цикл | Циклы для простого повторения, рекурсия для древовидных задач |
Золотое правило рекурсии: всегда пишите базовый случай первым и следите, чтобы каждый рекурсивный вызов к нему приближался. Если вы это освоите, то сможете написать любую рекурсивную функцию.
Что дальше?
Следующий урок посвящён функциям [map(), filter() и reduce()](/python/python-map-filter-reduce), мощным инструментам функционального программирования в Python. Вы научитесь преобразовывать, фильтровать и объединять коллекции данных в одну строку кода.