Рекурсия
Некоторые задачи удобно описывать через самих себя. Факториал числа n — это произведение всех чисел от 1 до n: факториал 5 равен 1 * 2 * 3 * 4 * 5, то есть 120. Его можно определить двумя правилами:
- факториал нуля равен
1; - факториал
nравенn, умноженному на факториалn - 1.
Функция на Swift повторяет это определение слово в слово, вызывая саму себя:
func factorial(_ n: Int) -> Int {
if n == 0 {
return 1
}
return n * factorial(n - 1)
}
print(factorial(5)) // 120Вернёмся к комбайну и посмотрим, как он считает factorial(5):
- Комбайн
factorialполучил5, но сразу ответа не знает. - Он ставит рядом такой же комбайн, отдаёт ему
4и ждёт результат, чтобы умножить его на5. - Второй комбайн так же передаёт работу третьему с числом
3, и так до комбайна, который получил0. - Последний сразу отвечает
1, никого не вызывая.
Здесь картинка с комбайнами неточна. Настоящие комбайны могут работать одновременно, а из вызовов функции в каждый момент выполняется только самый новый. Остальные ждут, пока он вернёт результат.
Два вида веток
Рекурсия — это вызов функцией самой себя. Компилятор такой вызов разрешает: имя функции видно внутри её собственного тела. У рекурсивной функции есть ветки двух видов.
Базовый случай
Ветку, в которой функция завершается без нового вызова, называют базовым случаем. В factorial это if n == 0 { return 1 }: вызов factorial(0) сразу возвращает 1.
Без базового случая вызовы не закончатся. Каждый новый вызов должен приближать аргумент к нему.
Рекурсивный шаг
Ветку, которая снова вызывает функцию, называют рекурсивным шагом. Строка return n * factorial(n - 1) вызывает ту же функцию с аргументом на единицу меньше. Вызов factorial(5) ждёт результата factorial(4), тот ждёт factorial(3), и так до factorial(0).
Потом результаты возвращаются вверх по цепочке, и каждый вызов умножает полученное на своё n:
factorial(0)возвращает1.factorial(1)возвращает1 * 1, то есть1.factorial(2)возвращает2 * 1, то есть2.factorial(3)возвращает3 * 2, то есть6.factorial(4)возвращает4 * 6, то есть24.factorial(5)возвращает5 * 24, то есть120.
Частая ошибка
Функцию, которая складывает числа от 1 до n, написали без базового случая:
func sumTo(_ n: Int) -> Int {
return n + sumTo(n - 1) // ✗
}warning: function call causes an infinite recursionПрограмма компилируется, но с предупреждением: у функции нет ни одной ветки без нового вызова. Предупреждение не останавливает сборку, но отнеситесь к нему как к ошибке: такая функция не вернёт ответ ни при каком аргументе. Добавьте базовый случай перед рекурсивным шагом: if n == 0 { return 0 }.
Главное
- Рекурсия — вызов функцией самой себя; у рекурсивной функции есть базовый случай и рекурсивный шаг.
- Базовый случай завершает функцию без нового вызова, и каждый новый вызов должен приближать аргумент к нему.
- Результаты возвращаются вверх по цепочке:
factorial(0)отдаёт1, и каждый вызов умножает полученное на своёn. - Функцию совсем без базового случая компилятор замечает и предупреждает:
function call causes an infinite recursion.