Рекурсия

Прием, при котором функция решает задачу, вызывая саму себя для более простой версии той же задачи.

Рекурсия — способ решить задачу, сведя ее к такой же, но меньшей. Функция вызывает саму себя с упрощенными данными, пока задача не станет настолько простой, что ответ известен сразу. Похоже на матрешку: открываешь одну — внутри такая же, только меньше, пока не дойдешь до последней.

Из чего состоит рекурсия

  • Базовый случай — простейшая ситуация, где ответ дается без новых вызовов.
  • Рекурсивный шаг — задача сводится к меньшей версии себя.

Классический пример — факториал: 5! = 5 · 4!, а 1! = 1.

def factorial(n):
    if n <= 1:          # базовый случай
        return 1
    return n * factorial(n - 1)   # рекурсивный шаг

Где рекурсия по-настоящему удобна

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

import os

def folder_size(path):
    total = 0
    for entry in os.scandir(path):
        if entry.is_dir(follow_symlinks=False):
            total += folder_size(entry.path)
        else:
            total += entry.stat(follow_symlinks=False).st_size
    return total

Так же обходят дерево элементов веб-страницы (DOM), разбирают вложенные JSON-документы, строят организационные структуры и справочники с иерархией. Рекурсивны и многие алгоритмы: сортировка слиянием, быстрая сортировка, обход графов.

Стек вызовов и его предел

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

Рекурсия или цикл

Любую рекурсию можно переписать циклом, и наоборот. Рекурсия часто короче и понятнее для вложенных структур, цикл — экономнее по памяти. Некоторые языки (Scheme, а в функциональном стиле и другие) оптимизируют хвостовую рекурсию, когда рекурсивный вызов стоит последним, и тогда стек не растет. Python и Java такой оптимизации не делают, поэтому для глубокой вложенности в них лучше цикл с явным стеком. В функциональном программировании рекурсия — основной способ повторения, а в структурах данных вроде деревьев — естественный способ обхода.