Главная Веб-разработка Что такое рекурсия – Основы работы с рекурсией – Tproger

Что такое рекурсия – Основы работы с рекурсией – Tproger

от admin

В статье рассказываем об основах рекурсии и разбираем эту функцию на примерах.

025 открытий25 показов

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

Рекурсия: что это такое и зачем нужна

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

Что такое рекурсия - Основы работы с рекурсией - Tproger

А теперь давайте разберемся на простом примере из математики: представим, что у нас есть функция F, которая выполняет определенное действие. В нашем случае — вычисляет факториал числа (это самый классический пример). Внутри этой функции F в качестве одного из значений используем вызов этой же функции F, но с уменьшенным аргументом. Так функция будет вызывать саму себя до тех пор, пока не достигнет базового случая (о нем расскажем ниже), после чего начнет возвращать результаты.

Как парсить данные на Python: BeautifulSoup и Scrapytproger.ru

В виде кода это выглядит так:

			def F(n):     if n == 1:           return 1       return n * F(n - 1) 		

Здесь функция F вычисляет факториал числа n, умножая его на факториал n-1, и так до тех пор, пока не дойдёт до 1, после чего начнёт возвращать результаты вверх по цепочке вызовов. Этот пример как раз о том, что нужно выполнить несколько одинаковых действий.

А вот пример из жизни: вы листаете ленту в известной социальной сети, а новые посты обновляются автоматически — это рекурсия в действии.

Принципы работы рекурсии

Рекурсия строится на двух ключевых принципах: базовый случай и рекурсивный вызов. Они позволяют функции вызывать саму себя, пока она не завершится.

Базовый случай

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

Один из самых простых примеров — факториал, о котором мы писали выше. Другой — возведение числа в степень до тех пор, пока значение не станет равно искомому.

Так это выглядит в виде кода:

			def power(base, exponent):     if exponent == 0:  # базовый случай, любое число в степени 0 равно 1         return 1 		

То есть, базовый случай — если exponent == 0, то возвращаем 1.

Рекурсивный вызов

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

			def fibonacci(n):     if n == 0:  # базовый случай         return 0     if n == 1:  # базовый случай         return 1     return fibonacci(n - 1) + fibonacci(n - 2)  # рекурсивный вызов 		

Числа Фибоначчи — это последовательность, где каждое число равно сумме двух предыдущих: 0, 1, 1, 2, 3, 5, 8, 13, 21, …

Если вызвать fibonacci(5), то увидите следующее:

			fibonacci(5) → fibonacci(4) + fibonacci(3) fibonacci(4) → fibonacci(3) + fibonacci(2) fibonacci(3) → fibonacci(2) + fibonacci(1) fibonacci(2) → fibonacci(1) + fibonacci(0) fibonacci(1) → 1  (базовый случай) fibonacci(0) → 0  (базовый случай)  		

После этого функция возвращает результаты обратно вверх по стеку вызовов. Обычно этот метод используется в криптографии и алгосах.

🔥 Яндекс представил SourceCraft — свой аналог GitHubtproger.ru

Кстати, на примере чисел Фибоначчи можно разобрать одну из проблем рекурсии — плохая производительность при отсутствии мемоизации.

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

Так, функция в задаче с числами Фибоначчи имеет экспоненциальную сложность O(2^n), так как она многократно вычисляет одни и те же значения. Например, чтобы вычислить fibonacci(5), функция вызывает сама себя 15 раз, хотя уникальных вычислений всего 6.

Чтобы улучшить производительность, нужно использовать мемоизацию, чтобы сохранить результаты:

			def fibonacci_memo(n, memo={}):     if n in memo:         return memo[n]     if n <= 1:         return n     memo[n] = fibonacci_memo(n - 1, memo) + fibonacci_memo(n - 2, memo)     return memo[n] 		

Здесь сложность уменьшается до O(n), так как каждое значение вычисляется только один раз.

Стек вызовов и управление памятью

Каждый рекурсивный вызов функции добавляет новый элемент в стек вызовов. Это значит, что вызовы складывается в стеке до тех пор, пока не будет достигнут базовый случай. После этого стек начинает разворачиваться, возвращая результаты на каждом уровне.

Например:

			def countdown(n):     if n == 0:  # базовый случай, завершение рекурсии         print("Старт")         return       print(n)       countdown(n - 1)  # рекурсивный вызов  countdown(3) 		

Так, когда мы вызываем сountdows(3), в стеке появляются новые вызовы:

call countdown(3) # → печатает 3

call countdown(2) # → печатает 2

call countdown(1) # → печатает 1

call countdown(0) # → печатает “Старт” и выходит

Затем стек начинает разворачиваться:

  • countdown(0) завершился → удаляется из стека
  • countdown(1) завершился → удаляется из стека
  • countdown(2) завершился → удаляется из стека
  • countdown(3) завершился → удаляется из стека

Если же в рекурсии нет базового случая, то стек вызовов никогда не освободится, и программа (в нашем случае на Питоне) выдаст ошибку RecursionError.

Вот пример бесконечной рекурсии:

			def infinite_recursion():     return infinite_recursion()  infinite_recursion() 		

В Питоне есть ограничения до 1000 вызовов, чтобы не перегружать стек, но этот лимит можно увеличить:

			import sys sys.setrecursionlimit(2000) 		

Рекурсия на реальных задачах

С базовым случаем и рекурсивным вызовом все понятно, поэтому давайте разбираться на более реальных и сложных примерах — с деревьями, графами и массивами.

Обход структуры данных

Рекурсия часто используется для обхода деревьев и графов. Например, бинарного дерева — в нем не более двух дочерних узлов (левый и правый). Вот три основных способа обхода:

  1. Прямой обход (Pre-order): Корень → Левый узел → Правый узел.
  2. Симметричный обход (In-order): Левый узел → Корень → Правый узел.
  3. Обратный обход (Post-order): Левый узел → Правый узел → Корень.
Читать также:
Нейросеть HeyGen: для чего нужна и как ей пользоваться

Запишем в виде кода:

			class TreeNode:     def __init__(self, value):         self.value = value         self.left = None         self.right = None  # прямой обход def pre_order_traversal(node):     if node is None:  # базовый случай         return     print(node.value)  # обработка текущего узла     pre_order_traversal(node.left)  # обход левого поддерева     pre_order_traversal(node.right)  # обход правого поддерева  # Симметричный обход def in_order_traversal(node):     if node is None:  # базовый случай         return     in_order_traversal(node.left)  # обход левого поддерева     print(node.value)  # текущего узла     in_order_traversal(node.right)  # обход правого поддерева  # Обратный обход def post_order_traversal(node):     if node is None:  # базовый случай         return     post_order_traversal(node.left)  # обход левого поддерева     post_order_traversal(node.right)  # обход правого поддерева     print(node.value)  # обработка текущего узла 		

Дерево будет выглядеть так:

1

/

2 3

/

4 5

  • Прямой обход: 1 → 2 → 4 → 5 → 3.
  • Симметричный обход: 4 → 2 → 5 → 1 → 3.
  • Обратный обход: 4 → 5 → 2 → 3 → 1.

Обходить можно и графы, особенно в глубине (DFS). Вот пример:

			def dfs(graph, node, visited=None):     if visited is None:         visited = set()  # отслеживание вершин     if node not in visited:         print(node)  # обработка текущей вершины         visited.add(node)  # помечаем вершину посещенной         for neighbor in graph[node]:  # рекурсивный обход соседних вершин             dfs(graph, neighbor, visited) 		

Сам граф:

			graph = {     'A': ['B', 'C'],     'B': ['D', 'E'],     'C': ['F'],     'D': [],     'E': ['F'],     'F': [] } 		

Результат обхода: A → B → D → E → F → C.

Разбиение

Разбить можно массив или строку. Давайте разбираться на примере строки. Так, рекурсия может разбить строку на слова или разделить ее по конкретным символам. Выглядит это так:

			s = "Hello World How Are You" def split_string(s, space=' '):     if space not in s:  # базовый случай         return [s]     index = s.index(space)  # ищем индекс разделителя     return [s[:index]] + split_string(s[index + 1:], space)  # рекурсивное разбиение print(split_string(s))    > ['Hello', 'World', 'How', 'Are', 'You']   		

Что такое хвостовая рекурсия

Хвостовая (оконечная) рекурсия — частный случай рекурсии. Главное отличие от обычной в том, что единственный рекурсивный вызов находится в конце перед выходом функции. В традиционной же рекурсии после рекурсивного вызова могут быть дополнительные вычисления.

Вот пример:

			def factorial_tail_recursive(n, accumulator=1):     if n == 1:  # базовый случай         return accumulator     return factorial_tail_recursive(n - 1, n * accumulator)  # хвостовой вызов 		

Кстати, с помощью хвостовой рекурсии тоже можно решать задачи с числами Фибоначчи:

			def fibonacci__recursive(n, a=0, b=1):     if n == 0:  # базовый случай         return a     if n == 1:  # базовый случай         return b     return fibonacci__recursive(n - 1, b, a + b)  # хвостовой вызов 		

Некоторые языки программирования (например, Haskell и Scala) поддерживают оптимизацию хвостовой рекурсии. Это означает, что компилятор или интерпретатор преобразует хвостовую рекурсию в итерацию — это помогает избежать переполнение стека.

Давайте посмотрим, как это работает в Scala. Вот обычная рекурсия, чтобы понять принцип:

			def sumList(list: List[Int]): Int = {   if (list.isEmpty) 0   else list.head + sumList(list.tail)  // обычный рекурсивный вызов } 		

А вот рекурсия с оптимизацией:

			def sumList(list: List[Int]): Int = {   @annotation.tailrec  // Аннотация для проверки хвостовой рекурсии   def loop(acc: Int, list: List[Int]): Int = {     if (list.isEmpty) acc     else loop(acc + list.head, list.tail)  // хвостовой вызов   }   loop(0, list) } 		

К сожалению, наш любимый Питон такую функцию не поддерживает, но всегда можно преобразовать «хвостика» в итерацию вручную:

			def factorial_iterative(n):     accumulator = 1     for i in range(1, n + 1):         accumulator *= i     return accumulator 		

Это пример итеративного решения факториала.

Рекурсия vs. итерация

Основные различия между рекурсией и итерацией заключаются в следующем:

  • В рекурсии функция вызывает саму себя, а в итерации используются циклы.
  • Интеграция требует меньше памяти, потому что нет стека вызовов.
  • Рекурсия медленнее без оптимизации.
  • С рекурсией легко переполнить хранилище при большой глубине.
  • Рекурсия обычно более читаемая и лаконичная, но подходит для меньшего количества задач.

Итерацию лучше использовать в следующих случаях:

  • Задачи, где нужна производительность. Как, например, с итеративным вычислением чисел Фибоначчи.
  • Обработка больших данных, например, поиск или сортировка. Вот пример пузырьковой сортировки:
			def bubble_sort(arr):     n = len(arr)     for i in range(n):         for j in range(0, n - i - 1):             if arr[j] > arr[j + 1]:                 arr[j], arr[j + 1] = arr[j + 1], arr[j] 		
  • Задачи с большой глубиной рекурсии, чтобы стек не переполнялся. Вот как итеративно обойти дерево:
			def iterative_tree_traversal(root):     stack = [root]     while stack:         node = stack.pop()         print(node.value)         if node.right:             stack.append(node.right)         if node.left:             stack.append(node.left)  		

Еще один пример: задача «Ханойские башни»

«Ханойские башни» — классная головоломка, которую часто решают с помощью рекурсии. Считайте, что это еще одна классика.

Есть три стержня: A, B и C. На стержень A нанизано несколько дисков разного размера, причем диски упорядочены по размеру в виде пирамидки — вниз от маленького к большому. Суть в том, чтобы переместить все диски со стержня А на С, при этом: за один раз можно перемещать только один диск, нельзя класть больший диск на меньший, можно использовать стержень B как вспомогательный.

Что такое рекурсия - Основы работы с рекурсией - Tproger

Рекурсивное решение задачи «Ханойские башни» основано на следующей идее:

  1. Переместить (n−1) дисков с A на B, используя C как промежуточный.
  2. Переместить оставшийся самый большой диск с A на C.
  3. Переместить (n−1) дисков с B) на C, теперь используя A как промежуточный.

Следовательно, если количество дисков n=1, просто переместите его с А на С (базовый случай). В остальных случаях при n–1 повторить то, что указано выше.

А теперь решение с помощью кода:

			def hanoi(n, start, finish, storage):    if n == 1:  # базовый случай        print(f"Переместить диск 1 с {start} на {finish}")        return    # Рекурсивный случай    hanoi(n - 1, start, storage, finish)  # переместить n-1 дисков на вспомогательный стержень    print(f"Переместить диск {n} с {start} на {finish}")  # Переместить оставшийся диск на целевой стержень    hanoi(n - 1, storage, finish, start)  # Переместить n-1 дисков с вспомогательного на целевой стержень   # Количество дисков n = 5 # Вызов функции для решения задачи hanoi(n, 'A', 'C', 'B')  		

Вывод:

			Переместить диск 1 с A на C Переместить диск 2 с A на B Переместить диск 1 с C на B Переместить диск 3 с A на C Переместить диск 1 с B на A Переместить диск 2 с B на C Переместить диск 1 с A на C Переместить диск 4 с A на B ... Переместить диск 3 с A на C Переместить диск 1 с B на A Переместить диск 2 с B на C Переместить диск 1 с A на C  		

Поздравляем, вы великолепны. А если хотите еще потренироваться в рекурсии, то можно написать код для поиска пути в лабиринте (кстати, это чем-то напоминает онлайн-карты и навигаторы) или обратного вывода строки.

Похожие статьи