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')