Рекурсия
Идея рекурсии
Рекурсивная функция решает задачу, сводя её к такой же задаче меньшего размера. Ей нужны две части:
- Базовый случай — когда ответ известен сразу, без рекурсии.
- Рекурсивный шаг — вызов самой себя на меньших данных.
def countdown(n):
if n == 0: # базовый случай
print("Пуск!")
return
print(n)
countdown(n - 1) # рекурсивный шаг
countdown(3)
Стек вызовов
Каждый вызов функции получает свой кадр (frame) — со своими локальными переменными. Кадры складываются в стек:
def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)
print(factorial(4))
Нажмите 👣 Пошагово и следите за колонкой кадров: при спуске появляются factorial() с n = 4, 3, 2, 1, а при возврате кадры снимаются, и результаты перемножаются.
Лимит глубины
CPython ограничивает глубину рекурсии (по умолчанию около 1000), чтобы не переполнить стек процесса:
import sys
print(sys.getrecursionlimit())
def depth(n):
return depth(n + 1)
try:
depth(0)
except RecursionError as e:
print("RecursionError:", e)
sys.setrecursionlimit() позволяет поднять лимит, но это лечение симптома: глубокую рекурсию лучше переписать на цикл или явный стек.
Нет оптимизации хвостовой рекурсии
В некоторых языках вызов в конце функции (return f(n - 1)) переиспользует кадр. В CPython — нет: каждый вызов создаёт новый кадр. Поэтому рекурсия глубиной 100 000 в Python невозможна без переписывания.
Рекурсия или цикл
def sum_rec(nums):
if not nums:
return 0
return nums[0] + sum_rec(nums[1:]) # каждый срез — копия: O(n²) времени и памяти
def sum_loop(nums):
total = 0
for x in nums:
total += x
return total
print(sum_rec([1, 2, 3, 4]), sum_loop([1, 2, 3, 4]))
Рекурсия естественна для рекурсивных структур: деревья, вложенные списки и словари, файловая система, грамматики; и для алгоритмов «разделяй и властвуй»: сортировка слиянием, быстрая сортировка, бинарный поиск, перебор с возвратом. Для линейных задач цикл проще и быстрее.
Вложенные структуры
config = {
"db": {"host": "localhost", "port": 5432},
"cache": {"redis": {"host": "r1", "ttl": 60}},
"debug": True,
}
def walk(d, prefix=""):
for key, value in d.items():
path = f"{prefix}.{key}" if prefix else key
if isinstance(value, dict):
walk(value, path) # спускаемся глубже
else:
print(f"{path} = {value}")
walk(config)
Дерево
tree = {"value": 1, "children": [
{"value": 2, "children": [{"value": 4, "children": []}]},
{"value": 3, "children": []},
]}
def tree_sum(node):
return node["value"] + sum(tree_sum(child) for child in node["children"])
def tree_depth(node):
if not node["children"]:
return 1
return 1 + max(tree_depth(c) for c in node["children"])
print(tree_sum(tree), tree_depth(tree))
Мемоизация
Наивная рекурсия для чисел Фибоначчи пересчитывает одно и то же экспоненциально много раз. Кеш превращает её в линейную:
from functools import cache
calls = 0
def fib(n):
global calls
calls += 1
return n if n < 2 else fib(n - 1) + fib(n - 2)
fib(20)
print("вызовов без кеша:", calls)
@cache
def fib_fast(n):
return n if n < 2 else fib_fast(n - 1) + fib_fast(n - 2)
print(fib_fast(100))
Это первый шаг к динамическому программированию (урок в модуле алгоритмов).
Как придумать рекурсивное решение
- Сформулируйте, что возвращает функция для входа размера n.
- Найдите самый простой вход (пустой список, 0, лист дерева) — базовый случай.
- Предположите, что функция уже работает для меньшего входа («доверьтесь рекурсии»), и выразите ответ для n через ответ для меньшего.
- Убедитесь, что каждый вызов приближает к базовому случаю.
Итог
- Рекурсия = базовый случай + шаг к меньшей задаче.
- Каждый вызов — новый кадр стека; лимит ~1000, иначе
RecursionError; TCO в CPython нет. - Рекурсия хороша для деревьев, вложенных структур и «разделяй и властвуй»; для линейных задач — цикл.
- Повторные вычисления убирает мемоизация (
@cache).
Практика решено 0 из 6
Напишите рекурсивную функцию factorial(n) (без циклов).
Какие тесты проверяют код (3)
assert factorial(0) == 1 assert factorial(5) == 120 assert factorial(20) == 2432902008176640000
def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)
Напишите рекурсивную функцию flatten(data), которая «расплющивает» список произвольной вложенности:
flatten([1, [2, [3, [4]], 5]]) → [1, 2, 3, 4, 5].
Какие тесты проверяют код (3)
assert flatten([1, [2, [3, [4]], 5]]) == [1, 2, 3, 4, 5] assert flatten([]) == [] assert flatten([[[]], [1]]) == [1]
def flatten(data):
result = []
for item in data:
if isinstance(item, list):
result.extend(flatten(item))
else:
result.append(item)
return result
Напишите рекурсивную функцию sum_digits(n) — сумма цифр неотрицательного целого числа.
Какие тесты проверяют код (3)
assert sum_digits(0) == 0 assert sum_digits(12345) == 15 assert sum_digits(9) == 9
def sum_digits(n):
if n < 10:
return n
return n % 10 + sum_digits(n // 10)
Что произойдёт при вызове рекурсивной функции без базового случая?
Поддерживает ли CPython оптимизацию хвостовой рекурсии (TCO)?
Напишите рекурсивную функцию permutations(items), которая возвращает список всех перестановок
списка (каждая перестановка — список). Порядок: как в примере.
permutations([1, 2, 3]) → [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]].
Какие тесты проверяют код (3)
assert permutations([1, 2, 3]) == [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]] assert permutations([]) == [[]] assert len(permutations(list(range(5)))) == 120
def permutations(items):
if len(items) <= 1:
return [items[:]]
result = []
for i, first in enumerate(items):
rest = items[:i] + items[i + 1:]
for perm in permutations(rest):
result.append([first] + perm)
return result
Войдите, чтобы отмечать пройденные уроки.