Python · Синтаксис · Промежуточный

Рекурсия

10 задач

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

Как работает рекурсия: базовый случай, рекурсивный случай, стек вызовов

#
Рекурсивная функция вызывает себя для решения меньшей версии той же задачи. Каждая рекурсивная функция требует двух вещей: **базового случая**, останавливающего рекурсию, и **рекурсивного случая**, движущегося к базовому. ```python def factorial(n): if n == 0: # базовый случай: остановиться здесь return 1 return n * factorial(n - 1) # рекурсивный случай: меньшая задача factorial(4) # 24 ``` **Что происходит при каждом вызове — стек вызовов** Когда Python вызывает функцию, он помещает фрейм на стек вызовов. Каждый фрейм хранит локальные переменные и адрес возврата. С `factorial(4)` стек растёт так: ``` factorial(4) -> вызывает factorial(3) ждёт... factorial(3) -> вызывает factorial(2) ждёт... factorial(2) -> вызывает factorial(1) ждёт... factorial(1) -> вызывает factorial(0) ждёт... factorial(0) -> возвращает 1 (базовый случай!) factorial(1) возвращает 1 * 1 = 1 factorial(2) возвращает 2 * 1 = 2 factorial(3) возвращает 3 * 2 = 6 factorial(4) возвращает 4 * 6 = 24 ``` Стек *раскручивается* после достижения базового случая — каждый ждавший вызов получает ответ и вычисляет собственное значение. **Самая распространённая ошибка: забыть базовый случай** Без базового случая рекурсия никогда не останавливается — Python генерирует `RecursionError`: ```python def bad_factorial(n): return n * bad_factorial(n - 1) # нет базового случая! bad_factorial(3) # RecursionError: maximum recursion depth exceeded ``` **Другая распространённая ошибка: не двигаться к базовому случаю** ```python def also_bad(n): if n == 0: return 0 return also_bad(n + 1) # движется от 0, а не к нему! ``` Всегда проверяй: делает ли каждый рекурсивный вызов задачу *строго меньше*? **Простой пример: сумма списка** ```python def total(lst): if not lst: # базовый случай: пустой список return 0 return lst[0] + total(lst[1:]) # первый элемент + сумма остального total([1, 2, 3, 4]) # 10 ``` Это менее эффективно, чем `sum(lst)`, но чётко показывает рекурсивный паттерн: отдели один элемент, рекурси по остальному, объедини.

Когда рекурсия — правильный инструмент: деревья, «разделяй и властвуй»

#
Рекурсия блестяще подходит, когда задача имеет естественно рекурсивную структуру: деревья, вложенные данные, алгоритмы «разделяй и властвуй». **Бинарный поиск** (разделяй и властвуй) ```python def binary_search(lst, target, lo=0, hi=None): if hi is None: hi = len(lst) - 1 if lo > hi: # базовый случай: не найдено return -1 mid = (lo + hi) // 2 if lst[mid] == target: return mid if lst[mid] < target: return binary_search(lst, target, mid + 1, hi) # ищем в правой половине return binary_search(lst, target, lo, mid - 1) # ищем в левой половине binary_search([1, 3, 5, 7, 9], 7) # 3 ``` **Обход дерева** (естественно рекурсивная структура) ```python def tree_sum(node): if node is None: # базовый случай: пустой узел return 0 return node['value'] + tree_sum(node.get('left')) + tree_sum(node.get('right')) tree = {'value': 1, 'left': {'value': 2, 'left': None, 'right': None}, 'right': {'value': 3, 'left': None, 'right': None}} tree_sum(tree) # 6 ``` **Выравнивание вложенных списков** ```python def flatten(lst): result = [] for item in lst: if isinstance(item, list): result.extend(flatten(item)) # рекурсия во вложенный список else: result.append(item) return result flatten([1, [2, [3, 4]], 5]) # [1, 2, 3, 4, 5] ``` **Хвостовая рекурсия — Python её не оптимизирует** В некоторых языках (Scheme, Erlang) рекурсивный вызов, являющийся *последней* операцией в функции (хвостовой вызов), оптимизируется компилятором для повторного использования фрейма стека. Python намеренно этого НЕ делает (Гвидо ван Россум: это сделало бы трассировки нечитаемыми). Хвостово-рекурсивная функция в Python всё равно создаёт O(n) фреймов стека: ```python def factorial_tail(n, acc=1): if n == 0: return acc return factorial_tail(n - 1, acc * n) # хвостовой вызов - но НЕ оптимизируется ``` Если нужна очень глубокая рекурсия (тысячи вызовов), вместо неё используй итерацию.

Рекурсия vs итерация, lru_cache, и преобразование в стек

#
**Рекурсия vs итерация — когда переключаться** Рекурсия элегантна для естественно иерархических задач. Итерация часто лучше, когда глубина рекурсии может быть большой, или структура линейная (связный список, плоская последовательность). ```python # Рекурсивно - понятно, но O(n) фреймов стека def sum_recursive(n): if n == 0: return 0 return n + sum_recursive(n - 1) # Итеративно - тот же результат, O(1) места в стеке def sum_iterative(n): total = 0 while n > 0: total += n n -= 1 return total ``` Лимит рекурсии Python по умолчанию — 1000. Можно поднять через `sys.setrecursionlimit(n)`, но это временная мера — большая глубина рекурсии обычно означает, что стоит перейти на итерацию. **`functools.lru_cache` — мемоизация дорогих рекурсивных вызовов** Без кешировани наивный Фибоначчи повторно вычисляет одни и те же подзадачи экспоненциально: ```python def fib(n): if n <= 1: return n return fib(n - 1) + fib(n - 2) # O(2^n) вызовов fib(35) # занимает ~2 секунды ``` Добавь `@lru_cache` и каждая подзадача решается один раз: ```python from functools import lru_cache @lru_cache(maxsize=None) def fib(n): if n <= 1: return n return fib(n - 1) + fib(n - 2) # теперь O(n) вызовов fib(35) # мгновенно fib.cache_info() # CacheInfo(hits=..., misses=36, ...) ``` **Преобразование рекурсии в итерацию с явным стеком** Любую рекурсию можно преобразовать в итерацию, поддерживая собственный стек (список). Это полностью избегает лимита рекурсии Python: ```python def flatten_iterative(lst): result = [] stack = [lst] while stack: current = stack.pop() for item in reversed(current): if isinstance(item, list): stack.append(item) # поместить вложенный список для обработки позже else: result.append(item) return result flatten_iterative([1, [2, [3, 4]], 5]) # [1, 2, 3, 4, 5] ``` **Практическое правило**: используй рекурсию, когда глубина ограничена и мала (например, высота дерева < 100), и `lru_cache`, когда подзадачи пересекаются. Переходи на итерацию (с явным стеком), когда глубина неограничена или критична производительность.

Классические рекурсивные паттерны

#
Классические рекурсивные паттерны, которые стоит знать. **Степень (возведение в степень через возведение в квадрат)** ```python def power(base, exp): if exp == 0: return 1 if exp % 2 == 0: half = power(base, exp // 2) return half * half # O(log n) вместо O(n) return base * power(base, exp - 1) power(2, 10) # 1024 ``` **Перестановки списка** ```python def permutations(lst): if len(lst) <= 1: return [lst] result = [] for i, item in enumerate(lst): rest = lst[:i] + lst[i+1:] for perm in permutations(rest): result.append([item] + perm) return result permutations([1, 2, 3]) # [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]] ``` **Степень множества (все подмножества)** ```python def power_set(lst): if not lst: return [[]] first, rest = lst[0], lst[1:] subsets = power_set(rest) return subsets + [[first] + s for s in subsets] power_set([1, 2, 3]) # [[], [3], [2], [2,3], [1], [1,3], [1,2], [1,2,3]] ``` **Сортировка слиянием** ```python def merge_sort(lst): if len(lst) <= 1: return lst mid = len(lst) // 2 left = merge_sort(lst[:mid]) right = merge_sort(lst[mid:]) return merge(left, right) def merge(a, b): result = [] i = j = 0 while i < len(a) and j < len(b): if a[i] <= b[j]: result.append(a[i]); i += 1 else: result.append(b[j]); j += 1 return result + a[i:] + b[j:] merge_sort([5, 2, 8, 1, 9]) # [1, 2, 5, 8, 9] ``` **Сводка паттернов** ``` Тип задачи Структура рекурсии ─────────────────────── ────────────────────────────────────────────── Линейная (список, строка) отдели один элемент, рекурси по остальному Разделяй и властвуй раздели пополам, рекурси оба, слияние Дерево/граф рекурси по дочерним/соседним узлам Комбинаторика выбери один элемент, рекурси по остальному Подзадачи пересекаются добавь @lru_cache к любому из вышеперечисленных ```
01

Факториал

#

Напишите рекурсивную функцию, которая вычисляет факториал неотрицательного целого числа n (n! = n × (n−1) × ... × 1, где 0! = 1).

def factorial(n):
    pass


print(factorial(0))
print(factorial(5))
print(factorial(10))
Решение
def factorial(n):
    if n == 0:
        return 1
    return n * factorial(n - 1)


print(factorial(0))
print(factorial(5))
print(factorial(10))
02

Сумма списка

#

Напишите рекурсивную функцию, которая вычисляет сумму всех чисел в списке без использования встроенного sum() или циклов.

def recursive_sum(numbers):
    pass


print(recursive_sum([1, 2, 3, 4, 5]))
print(recursive_sum([]))
Решение
def recursive_sum(numbers):
    if not numbers:
        return 0
    return numbers[0] + recursive_sum(numbers[1:])


print(recursive_sum([1, 2, 3, 4, 5]))
print(recursive_sum([]))
03

Степень числа

#

Напишите рекурсивную функцию, которая возводит base в степень exponent. Не используйте оператор ** или math.pow().

def power(base, exponent):
    pass


print(power(2, 10))
print(power(3, 4))
print(power(5, 0))
Решение
def power(base, exponent):
    if exponent == 0:
        return 1
    return base * power(base, exponent - 1)


print(power(2, 10))
print(power(3, 4))
print(power(5, 0))
04

Числа Фибоначчи

#

Напишите рекурсивную функцию, которая возвращает n-е число Фибоначчи. Последовательность: 0, 1, 1, 2, 3, 5, 8, 13... (fib(0)=0, fib(1)=1, fib(n)=fib(n-1)+fib(n-2)).

def fib(n):
    pass


for i in range(8):
    print(fib(i), end=" ")
Решение
def fib(n):
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)


for i in range(8):
    print(fib(i), end=" ")
05

Перевернуть строку

#

Напишите рекурсивную функцию, которая переворачивает строку.

def reverse_string(s):
    pass


print(reverse_string("hello"))
print(reverse_string(""))
print(reverse_string("a"))
Решение
def reverse_string(s):
    if len(s) <= 1:
        return s
    return reverse_string(s[1:]) + s[0]


print(reverse_string("hello"))
print(reverse_string(""))
print(reverse_string("a"))
06

Подсчёт вхождений

#

Напишите рекурсивную функцию, которая подсчитывает, сколько раз целевое значение встречается во вложенном списке (списке, который может содержать другие списки).

def count_in_nested(data, target):
    pass


data = [1, [2, 1, [3, 1]], [1, 4]]
print(count_in_nested(data, 1))
Решение
def count_in_nested(data, target):
    count = 0
    for item in data:
        if isinstance(item, list):
            count += count_in_nested(item, target)
        elif item == target:
            count += 1
    return count


data = [1, [2, 1, [3, 1]], [1, 4]]
print(count_in_nested(data, 1))
07

Разворачивание вложенного списка

#

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

def flatten(data):
    pass


print(flatten([1, [2, [3, [4]], 5], 6]))
Решение
def flatten(data):
    result = []
    for item in data:
        if isinstance(item, list):
            result.extend(flatten(item))
        else:
            result.append(item)
    return result


print(flatten([1, [2, [3, [4]], 5], 6]))
08

Бинарный поиск (рекурсивный)

#

Напишите рекурсивную функцию, которая выполняет бинарный поиск в отсортированном списке. Возвращайте индекс целевого элемента, если найден, или -1, если нет.

def binary_search(arr, target, low=0, high=None):
    pass


nums = [1, 3, 5, 7, 9, 11, 13, 15]
print(binary_search(nums, 7))
print(binary_search(nums, 6))
Решение
def binary_search(arr, target, low=0, high=None):
    if high is None:
        high = len(arr) - 1
    if low > high:
        return -1
    mid = (low + high) // 2
    if arr[mid] == target:
        return mid
    if arr[mid] < target:
        return binary_search(arr, target, mid + 1, high)
    return binary_search(arr, target, low, mid - 1)


nums = [1, 3, 5, 7, 9, 11, 13, 15]
print(binary_search(nums, 7))
print(binary_search(nums, 6))
09

Подсчёт цифр

#

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

def count_digits(n):
    pass


print(count_digits(1))
print(count_digits(42))
print(count_digits(12345))
Решение
def count_digits(n):
    if n < 10:
        return 1
    return 1 + count_digits(n // 10)


print(count_digits(1))
print(count_digits(42))
print(count_digits(12345))
10

Ханойские башни

#

Напишите рекурсивную функцию, которая выводит шаги решения головоломки Ханойских башен для n дисков. Переместите все диски со стержня A на стержень C, используя стержень B как вспомогательный. Выводите каждый ход как 'Move disk from X to Y'.

def hanoi(n, source, target, auxiliary):
    pass


hanoi(3, 'A', 'C', 'B')
Решение
def hanoi(n, source, target, auxiliary):
    if n == 1:
        print(f"Move disk from {source} to {target}")
        return
    hanoi(n - 1, source, auxiliary, target)
    print(f"Move disk from {source} to {target}")
    hanoi(n - 1, auxiliary, target, source)


hanoi(3, 'A', 'C', 'B')