Преподаватель: Ваш преподаватель ОАиП
Сайт: gadget59.ru
Рекурсия (от латинского «recursio» — возвращение назад) — это приём в программировании, при котором функция (или метод) вызывает сама себя для решения задачи.
Проще говоря, рекурсия — это когда функция для решения большой задачи разбивает её на более мелкие подзадачи того же типа, а затем решает эти подзадачи, снова вызывая саму себя.
Рекурсия встречается не только в программировании, но и в повседневной жизни:
Чтобы понять рекурсию, нужно разобраться, как Java хранит информацию о вызовах функций. Для этого используется специальная область памяти, которая называется стек вызовов (call stack).
Стек вызовов — это структура данных, работающая по принципу LIFO (Last In, First Out — «последний пришёл, первый вышел»). Представьте себе стопку тарелок: вы можете положить тарелку только сверху и взять тоже только сверху.
Когда функция вызывает другую функцию (или саму себя), Java делает следующее:
Любая рекурсивная функция должна содержать два обязательных элемента, иначе она будет работать бесконечно:
Это условие, при котором функция перестаёт вызывать саму себя и просто возвращает результат. Это «конец цепочки», точка остановки.
Это условие, при котором функция вызывает саму себя, но с более простыми входными данными. Каждый такой вызов должен приближать нас к базовому случаю.
StackOverflowError (переполнение стека).
Факториал числа n (обозначается как n!) — это произведение всех натуральных чисел от 1 до n.
Например: 5! = 1 × 2 × 3 × 4 × 5 = 120
Заметим важную закономерность: 5! = 4! × 5, 4! = 3! × 4, и так далее. То есть:
n! = n × (n-1)! для n > 00! = 1 (это базовый случай)
Запишем это на Java:
public static int factorial(int n) {
// Базовый случай
if (n == 0 || n == 1) {
return 1;
}
// Рекурсивный случай
return n * factorial(n - 1);
}
Посмотрим, как Java вычисляет factorial(4), шаг за шагом:
factorial(4)
├─ 4 * factorial(3)
│ ├─ 3 * factorial(2)
│ │ ├─ 2 * factorial(1)
│ │ │ └─ 1 ← базовый случай, возвращаем 1
│ │ └─ возвращаем 2 * 1 = 2
│ └─ возвращаем 3 * 2 = 6
└─ возвращаем 4 * 6 = 24
Обратите внимание на два этапа:
public class Factorial {
public static int factorial(int n) {
// Базовый случай: 0! = 1 и 1! = 1
if (n == 0 || n == 1) {
return 1;
}
// Рекурсивный случай: n! = n * (n-1)!
return n * factorial(n - 1);
}
public static void main(String[] args) {
System.out.println("0! = " + factorial(0)); // 1
System.out.println("1! = " + factorial(1)); // 1
System.out.println("5! = " + factorial(5)); // 120
System.out.println("10! = " + factorial(10)); // 3628800
}
}
5! = 5 × 4 × 3 × 2 × 1 = 120. Всё сходится!
Числа Фибоначчи — это знаменитая последовательность, в которой каждое число равно сумме двух предыдущих:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...
Эта последовательность была описана итальянским математиком Леонардо Фибоначчи в 1202 году при решении задачи о размножении кроликов. Удивительно, но числа Фибоначчи встречаются в природе: в расположении лепестков цветов, в спиралях раковин, в сосновых шишках.
Рекурсивное определение:
F(0) = 0 (базовый случай)F(1) = 1 (базовый случай)F(n) = F(n-1) + F(n-2) для n ≥ 2 (рекурсивный случай)
public class Fibonacci {
public static long fib(int n) {
// Базовые случаи
if (n == 0) return 0;
if (n == 1) return 1;
// Рекурсивный случай
return fib(n - 1) + fib(n - 2);
}
public static void main(String[] args) {
for (int i = 0; i <= 10; i++) {
System.out.println("F(" + i + ") = " + fib(i));
}
}
}
Этот код работает, но... есть огромная проблема. Давайте разберёмся, в чём она.
Когда мы вычисляем fib(5), происходит следующее:
fib(5)
├─ fib(4)
│ ├─ fib(3)
│ │ ├─ fib(2)
│ │ │ ├─ fib(1) → 1
│ │ │ └─ fib(0) → 0
│ │ └─ fib(1) → 1 ← уже считали!
│ └─ fib(2) ← уже считали!
│ ├─ fib(1) → 1
│ └─ fib(0) → 0
└─ fib(3) ← уже считали!
├─ fib(2) ← уже считали!
└─ fib(1) → 1
fib(5) это не страшно, но для fib(50) программа будет работать часами, а для fib(100) — годами.
Мемоизация (от латинского «memorabilis» — достойный запоминания) — это приём, при котором мы сохраняем результаты уже вычисленных значений, чтобы не пересчитывать их повторно.
Проще говоря: «посчитал — запомнил — в следующий раз просто взял из памяти».
public class FibonacciMemo {
// Массив для хранения уже посчитанных значений
private static long[] memo;
public static long fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
// Если уже считали — берём из памяти
if (memo[n] != 0) {
return memo[n];
}
// Иначе считаем и сохраняем
memo[n] = fib(n - 1) + fib(n - 2);
return memo[n];
}
public static void main(String[] args) {
memo = new long[100];
System.out.println("F(50) = " + fib(50)); // Мгновенно!
System.out.println("F(90) = " + fib(90)); // Мгновенно!
}
}
Почти любую рекурсивную задачу можно решить с помощью цикла (итерации). Когда что использовать?
| Критерий | 🔄 Рекурсия | 🔁 Итерация (цикл) |
|---|---|---|
| Читаемость | ✅ Элегантно для древовидных задач | ✅ Проще для линейных задач |
| Производительность | ⚠️ Тратит память на стек | ✅ Быстрее и экономнее |
| Риск переполнения | ⚠️ StackOverflowError | ✅ Безопасно |
| Подходит для | Деревья, графы, обход файлов, Ханойские башни | Факториал, суммы, простые последовательности |
Для сравнения, вот итеративная версия факториала — она быстрее и не использует стек:
public class FactorialIterative {
public static long factorial(int n) {
long result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
public static void main(String[] args) {
System.out.println("5! = " + factorial(5)); // 120
System.out.println("20! = " + factorial(20)); // 2432902008176640000
}
}
Эта версия работает быстрее и не рискует переполнить стек. Но рекурсивная версия красивее и ближе к математическому определению.
Ханойские башни — это знаменитая головоломка, придуманная французским математиком Эдуардом Люка в 1883 году.
Условия задачи:
Идея решения удивительно проста:
n-1 дисков с A на B (используя C как вспомогательный).n-1 дисков с B на C (используя A как вспомогательный).public class HanoiTowers {
public static void hanoi(int n, char from, char to, char help) {
if (n == 0) return; // Базовый случай
// 1. Переносим n-1 дисков с 'from' на 'help' через 'to'
hanoi(n - 1, from, help, to);
// 2. Переносим самый большой диск с 'from' на 'to'
System.out.println("Диск " + n + ": " + from + " → " + to);
// 3. Переносим n-1 дисков с 'help' на 'to' через 'from'
hanoi(n - 1, help, to, from);
}
public static void main(String[] args) {
hanoi(3, 'A', 'C', 'B');
}
}
Вызов hanoi(3, 'A', 'C', 'B') выведет:
Диск 1: A → C
Диск 2: A → B
Диск 1: C → B
Диск 3: A → C
Диск 1: B → A
Диск 2: B → C
Диск 1: A → C
Всего 7 перемещений. Для n дисков нужно 2ⁿ − 1 перемещений:
| Количество дисков | Перемещений |
|---|---|
| 3 | 7 |
| 10 | 1 023 |
| 20 | 1 048 575 |
| 64 (легенда) | 18 446 744 073 709 551 615 |
Рекурсия идеально подходит для обхода папок, потому что папки — это дерево: в папке могут быть другие папки, в них ещё папки, и так до любой глубины.
import java.io.File;
public class FileWalker {
public static void listFiles(File folder, int depth) {
// Перебираем все файлы и папки в текущей папке
for (File file : folder.listFiles()) {
// Выводим отступы для наглядности
for (int i = 0; i < depth; i++) {
System.out.print(" ");
}
System.out.println(file.getName());
// Если это папка — рекурсивно заходим в неё
if (file.isDirectory()) {
listFiles(file, depth + 1);
}
}
}
public static void main(String[] args) {
File folder = new File("C:\\Users");
listFiles(folder, 0);
}
}
Если забыть базовый случай или сделать так, что рекурсия никогда не остановится, Java выдаст ошибку StackOverflowError (ошибка переполнения стека).
// ❌ ОШИБКА: нет базового случая!
public static int badFactorial(int n) {
return n * badFactorial(n - 1); // Бесконечная рекурсия
}
// ❌ ОШИБКА: рекурсия не приближается к базовому случаю
public static int wrongFactorial(int n) {
if (n == 0) return 1;
return n * wrongFactorial(n); // n не уменьшается!
}
// ✅ ПРАВИЛЬНО: есть базовый случай и движение к нему
public static int factorial(int n) {
if (n == 0) return 1; // Базовый случай
return n * factorial(n - 1); // n уменьшается → приближаемся к 0
}
Хорошая рекурсивная функция должна проверять входные данные:
public static int factorial(int n) {
// Защита от некорректных данных
if (n < 0) {
throw new IllegalArgumentException(
"Факториал отрицательного числа не определён: " + n
);
}
// Базовый случай
if (n == 0 || n == 1) {
return 1;
}
// Рекурсивный случай
return n * factorial(n - 1);
}
public static void main(String[] args) {
System.out.println(factorial(5)); // 120
// System.out.println(factorial(-3)); // Ошибка!
}
Напишем рекурсивную функцию, которая считает сумму цифр числа. Например, для 1234: 1 + 2 + 3 + 4 = 10.
Идея: Чтобы получить последнюю цифру, берём остаток от деления на 10 (n % 10). Чтобы отбросить последнюю цифру, делим на 10 нацело (n / 10).
public static int sumOfDigits(int n) {
// Защита от отрицательных чисел
n = Math.abs(n);
// Базовый случай: однозначное число
if (n < 10) {
return n;
}
// Рекурсивный случай: последняя цифра + сумма остальных
return (n % 10) + sumOfDigits(n / 10);
}
public static void main(String[] args) {
System.out.println(sumOfDigits(1234)); // 10
System.out.println(sumOfDigits(9876)); // 30
System.out.println(sumOfDigits(0)); // 0
}
Вычислим xⁿ рекурсивно. Заметим, что:
x⁰ = 1 (базовый случай)xⁿ = x × xⁿ⁻¹ (рекурсивный случай)public static double power(double x, int n) {
// Базовый случай
if (n == 0) return 1;
// Для отрицательной степени: x⁻ⁿ = 1 / xⁿ
if (n < 0) {
return 1.0 / power(x, -n);
}
// Рекурсивный случай
return x * power(x, n - 1);
}
public static void main(String[] args) {
System.out.println(power(2, 10)); // 1024
System.out.println(power(3, 5)); // 243
System.out.println(power(2, -3)); // 0.125
}
Обычная рекурсия делает n вызовов. Но можно быстрее! Используем наблюдение:
n чётное: xⁿ = (x²)ⁿ/²n нечётное: xⁿ = x × xⁿ⁻¹public static double fastPower(double x, int n) {
if (n == 0) return 1;
if (n < 0) return 1.0 / fastPower(x, -n);
if (n % 2 == 0) {
// Чётная степень: xⁿ = (x²)ⁿ/²
double half = fastPower(x, n / 2);
return half * half;
} else {
// Нечётная степень: xⁿ = x × xⁿ⁻¹
return x * fastPower(x, n - 1);
}
}
// Для power(2, 100) обычный метод сделает 100 вызовов,
// а fastPower — всего около 7! (логарифмическая сложность)
StackOverflowError.factorial(n) вместо factorial(n-1).n == 0, но вызвать с отрицательным числом.return.sum(5) = 1 + 2 + 3 + 4 + 5 = 15.НОД(a, b) = НОД(b, a mod b).reverse("привет") = "тевирп".StackOverflowError.На следующих лекциях мы продолжим изучать подпрограммы и структуры данных: