Краткое содержание
Функция может вызывать саму себя: рекурсия решает задачу, сводя её к меньшему экземпляру той же задачи. Этот урок переводит на C++ рекурсивные определения из математики (факториал, степень, сумму цифр), каждое со своим базовым и своим рекурсивным случаем; показывает, что индукция гарантирует их корректность, а убывающая мера гарантирует их завершение; прослеживает по стеку вызовов кадры, которые накапливаются и снимаются, и объясняет, когда стек исчерпывается. Урок разбирает предел типа результата, который переполняется намного раньше стека; сравнивает рекурсивный и итеративный варианты вычисления чисел Фибоначчи, подсчитывая вызовы, и завершается критерием выбора между рекурсией и циклом.
Цели обучения
- Писать рекурсивную функцию, с её базовым и рекурсивным случаем, исходя из рекурсивного определения.
- Обосновывать корректность и завершение рекурсивной функции индукцией по размеру задачи.
- Прослеживать по стеку вызовов выполнение рекурсивной функции и объяснять переполнение стека.
- Сравнивать сложность рекурсивного и итеративного вариантов вычисления чисел Фибоначчи, подсчитывая их вызовы и операции.
Рекурсивные определения
Факториал
Факториал натурального числа есть произведение натуральных чисел от 1 до : , с соглашением . Многоточие в этой формуле скрывает цикл, и урок Циклы научил его писать. Однако то же определение допускает и другую форму, в которой нет никакого цикла: факториал числа равен , умноженному на факториал числа , а факториал нуля равен 1.
Это рекурсивное определение: оно определяет факториал числа через факториал меньшего числа. Оно не порочный круг, потому что каждое применение второго правила спускается на одну ступень, а лестница кончается первым правилом: . У обоих правил есть имена. Первое называется базовым случаем: оно даёт результат, ни на что больше не ссылаясь; второе называется рекурсивным случаем: оно сводит задачу к меньшему экземпляру той же задачи.
C++ позволяет функции вызывать саму себя, и благодаря этому определение переводится почти слово в слово.
#include <print>// Факториал n.// Предусловие: 0 <= n <= 12 (граница обосновывается ниже).// Постусловие: возвращает n! = 1 · 2 · ... · n, причём 0! = 1.int factorial(int n){ if (n == 0) { return 1; // базовый случай } return n * factorial(n - 1); // рекурсивный случай}int main(){ for (int n{0}; n <= 6; ++n) { std::println("{}! = {}", n, factorial(n)); }}0! = 11! = 12! = 23! = 64! = 245! = 1206! = 720Функция в точности повторяет форму определения: if обрабатывает базовый случай, а return применяет рекурсивный. Вызов factorial(n - 1) ничем не отличается от любого другого вызова из урока Функции: вычисляется его аргумент, создаётся новый кадр со своим собственным n, а по завершении возвращённое значение умножается на n вызывающего.
Контракт не декоративен: при рекурсивный случай вызвал бы функцию с , затем с и никогда не дошёл бы до 0; а не помещается в int. Многие вводные тексты пишут этот факториал с базовым случаем if (n == 0 || n == 1), избыточным, но безвредным, и ничего не говорят ни об отрицательных числах, ни о размере результата: их код не завершается при и даёт бессмысленный результат начиная с . Оба дефекта будут рассмотрены в своём параграфе.
Базовый случай и рекурсивный случай
У всякой рекурсивной функции есть один или несколько базовых случаев, которые решаются непосредственно, и один или несколько рекурсивных случаев, которые вызывают ту же функцию с аргументом, меньшим в некотором точном смысле, и комбинируют то, что она возвращает.
Степень , где натуральное, определяется равенствами и : размер здесь есть показатель степени. Сумма цифр натурального числа (для 2026 она равна ) определяется так: если , сумма равна , потому что у числа одна цифра; иначе она есть сумма цифр числа , то есть числа без последней цифры, плюс эта последняя цифра, . Здесь размер есть количество цифр.
#include <print>// x в степени n.// Предусловие: n >= 0.// Постусловие: возвращает x^n, причём x^0 = 1.double power(double x, int n){ if (n == 0) { return 1.0; } return x * power(x, n - 1);}// Сумма десятичных цифр n.// Предусловие: n >= 0.// Постусловие: возвращает сумму цифр n.int digitSum(int n){ if (n < 10) { return n; } return digitSum(n / 10) + n % 10;}int main(){ std::println("2^10 = {}", power(2.0, 10)); std::println("1.5^3 = {}", power(1.5, 3)); std::println("сумма цифр числа 2026: {}", digitSum(2026));}2^10 = 10241.5^3 = 3.375сумма цифр числа 2026: 10В digitSum целочисленное деление n / 10 отбрасывает последнюю цифру, а n % 10 извлекает её, как при переводе в другую систему счисления в уроке Численные алгоритмы. При функция запрашивает сумму цифр числа 202, та запрашивает сумму для 20, а та для 2; это базовый случай, он возвращает 2, и результаты складываются на обратном пути: , , .
Итак, написать рекурсивную функцию значит ответить на два вопроса: какие случаи решаются без посторонней помощи и как получить решение случая из решения меньшего. Если оба ответа верны, то верна и функция, и параграф ниже это доказывает.
Загрузка содержимого…