Алгоритмика: рекурсия
Алгоритмика: рекурсия
Glossary overview

Алгоритмика: рекурсия

Рекурсия в PHP — это прием программирования, при котором функция вызывает саму себя.

Любая рекурсивная функция должна иметь два компонента:

  1. Условие выхода (базовый случай): точка, где вызовы прекращаются. Без неё возникнет бесконечный цикл и ошибка переполнения стека.
  2. Рекурсивный шаг: вызов функцией самой себя с измененными аргументами.

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

Теперь функция работает так, как было задумано. Это выглядит примерно так:

Стек

Концепция стека вызовов играет важную роль в программировании вообще; кроме того, ее важно понимать при использовании рекурсии.

Стек вызовов это просто способ, которым PHP помнит, где он сейчас находится в коде и как туда вернуться. Представь стопку тарелок. Ты кладешь тарелку сверху, потом еще одну, потом еще. Снять можно только верхнюю. Вот стек работает ровно так же.

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

Внутри этой функции ты вызываешь другую функцию. PHP снова кладет новый уровень в стек. И так далее. Получается цепочка вызовов, вложенная друг в друга.

Когда функция заканчивается (return или просто конец функции), PHP снимает верхний уровень стека и возвращается туда, откуда пришел. Потом еще раз. Пока не вернется в самый низ, в основной файл.

def greet(name):
    print("hello, " + name + "!")
    greet2(name)
    print("getting ready to say bye...")
    bye()

// Call
greet("Alex")

Шаги выполнения

ШагВыполняемая строкаЧто происходитСостояние стека
1greet("Alex")старт программы, вызов greet[ main -> greet ]
2print("hello, Alex!")вывод строки[ main -> greet -> print ]
3возврат из printprint закончил работу[ main -> greet ]
4greet2(name)вызов greet2[ main -> greet -> greet2 ]
5возврат из greet2greet2 завершилась[ main -> greet ]
6print("getting ready to say bye...")вывод строки[ main -> greet -> print ]
7возврат из printprint закончил работу[ main -> greet ]
8bye()вызов bye[ main -> greet -> bye ]
9возврат из byebye завершилась[ main -> greet ]
10конец greetфункция закончилась[ main ]

Очень важно понять это:

  • Пока greet2 или bye выполняются, код greet стоит на паузе. Он не исчезает, не “идет параллельно”. Он буквально заморожен в стеке и ждет, пока верхний вызов завершится.
  • PHP помнит, где он сейчас находится в коде и как туда вернуться. Он знает, что мы пришли из функции greet и что туда надо будет вернуться.
  • Все вызовы функций сохраняются в стеке вызовов.
  • Когад функция заканчиваетя выполнение, она пропадает из стека.

Стек вызовов с рекурсией

Рекурсивные функции тоже используют стек вызовов! Посмотрим, как это делается , на примере функции вычисления факториала. Вызов factorial(5) записывается в виде 5! и определяется следующим образом: 5! = 5*4*3*2*1.
По тому же принципу factorial(З) соответствует 3*2*1. Рекурсивная функция для вычисления факториала числа выглядит так:

В программу включается вызов fact(З). Проанализируем этот вызов строку за строкой и посмотрим, как изменяется стек вызовов. Стоит напомнить, что верхний блок в стеке сообщает, какой вызов fact является текущим.

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

Очень важно: рекурсия делает два этапа: сначала “спуск” (наращиваем стек), потом “подъем” (разматываем стек и считаем).

Фишка рекурсии: каждая функция “зависает” на строчке return $n * fact($n-1); пока нижний вызов не вернет число. Именно это и хранит стек: кто ждет чего и где продолжать выполнение. Мы ждем пока самый последний вызов функции вернет 1 и начинаем подниматься вверх и умножать.

ШагТекущий вызовЧто происходитСтек вызововВозвращаемое значение
1fact(3)вызов функции, n=3[ main -> fact(3) ]
2fact(3)нужно посчитать 3 * fact(2)[ main -> fact(3) ]
3fact(2)рекурсивный вызов[ main -> fact(3) -> fact(2) ]
4fact(2)нужно посчитать 2 * fact(1)[ main -> fact(3) -> fact(2) ]
5fact(1)рекурсивный вызов, база[ main -> fact(3) -> fact(2) -> fact(1) ]
6fact(1)база, сразу return 1[ main -> fact(3) -> fact(2) ]1
7fact(2)получили 1, считаем 2 * 1[ main -> fact(3) -> fact(2) ]2
8fact(2)return 2, выходим[ main -> fact(3) ]2
9fact(3)получили 2, считаем 3 * 2[ main -> fact(3) ]6
10fact(3)return 6, выходим[ main ]6

Первые шаги это спуск. Мы только накапливаем вызовы и ничего не считаем по-настоящему.
Начиная с fact(1) начинается подъем
. Значения возвращаются вверх, стек сжимается, и каждый уровень домножает свое число.

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

Важно:

  • самая глубокая функция не вызывается первой
  • она первой возвращает результат

Почему возникает ощущение, что она “первая”?

Потому что до базы ничего не считается. Все функции “висят” на паузе, ожидая результат снизу.

<?php

function factorial($n) {
	
	if ($n == 1) return 1;
	
	/*Когда получили 1, выходим с функции factorial(1), и по стеку вызовов поднимаемся вверх.
	Попадем на factorial(2), на возвращает "return $n * factorial($n - 1)", так как factorial($n - 1) вернул 1,
	значит умножаем 2 * 1.
	
	Поднимаемся по стеку вверх. Теперь factorial(3), factorial(2) вернул 2, поэтому делаем 3*2=6
	*/
	
	 return $n * factorial($n - 1);
}

$result = factorial(5);

var_dump($result);