Рекурсия
Проблем
Колко подредби има?
На рафта има 5 книги. По колко начина можеш да ги подредиш? За първото място има 5 избора, за второто 4, после 3, 2 и 1: 5 · 4 · 3 · 2 · 1 = 120. Това е факториел, пише се 5!.
С цикъл вече го можеш:
static int factorialLoop(int n) { int result = 1; for (int i = 2; i <= n; i++) { result *= i; } return result;}
System.out.println(factorialLoop(5));120Погледни го и така: 5! = 5 · 4!. А 4! = 4 · 3!. Задачата се решава чрез същата задача, само по-малка. Може ли методът да каже точно това?
Идеята
Метод, който вика себе си
Може. Метод, който вика сам себе си, се казва рекурсивен, а подходът е рекурсия.
static int factorial(int n) { if (n == 1) { return 1; } return n * factorial(n - 1);}
System.out.println(factorial(5));120Две неща го правят да работи:
- Дъно. Случай, който се решава направо, без ново извикване. Тук: 1! = 1.
- По-малка задача. Всяко извикване вика себе си с число, по-близо до дъното:
n - 1.
Виж го
Купчината от извиквания
static int factorial(int n) { if (n == 1) { return 1; } return n * factorial(n - 1);}
System.out.println(factorial(3)); // 6Стъпка по стъпка, с думи
- main вика factorial(3).
- Нова памет за factorial с n = 3.
- 3 не е 1, затова методът вика factorial(2) и чака отговора.
- Същото: factorial(2) вика factorial(1). Всяко извикване има свое n.
- n е 1: това е дъното. Връща 1, без да вика повече.
- Отговорът слиза надолу: 2 * 1 = 2.
- И пак: 3 * 2 = 6.
- factorial(3) връща 6 и main го отпечатва.
Всяко извикване получава своя памет със свое n. Те се трупат едно над друго и чакат. Тази купчина се казва стек от извиквания (call stack). Когато дъното върне 1, отговорите слизат надолу един по един и купчината се разтуря.
Пробвай
Слизане и качване
Какво ще отпечата програмата?
static void steps(int n) { if (n == 0) { return; } System.out.println("слизам " + n); steps(n - 1); System.out.println("качвам " + n);}
steps(2);Редът преди рекурсивното извикване се изпълнява на път надолу. Редът след него чака, докато по-малкото извикване свърши. Затова „качвам“ излиза в обратен ред: първо се връща последното извикване.
Пробвай
Намери грешката
sumTo(4) трябва да върне 1 + 2 + 3 + 4 = 10. Вместо това програмата спира с StackOverflowError. На кой ред е грешката?
Избери реда с грешката.
Методът вика себе си със същото n, а не с по-малко. Задачата не се смалява, дъното не идва и стекът се препълва. Поправката е sumTo(n - 1).
Идеята
Рекурсия или цикъл?
Факториелът става и с цикъл, и с рекурсия. Цикълът тук е по-добрият избор: не трупа памет за всяко извикване, а за 100 000 извиквания стекът няма да стигне.
Рекурсията е естествена там, където нещото съдържа по-малки неща от същия вид: папка с папки вътре, категория с подкатегории, разклонение с разклонения. Такива задачи ще срещнеш по-нататък в курса. Там цикълът става тромав, а рекурсивният метод остава кратък.
По-точно, за любопитните
- Факториелът често се пише с дъно 0! = 1 (
if (n == 0)). Тогава работи и за 0.
intстига до 12!. За 13! резултатът не се събира и числото излиза грешно. За по-големи стойности ползвайlong.
Пробвай
Подреди редовете
Методът трябва да смята степен: power(2, 10) е 1024. Подреди редовете му.
return 1;}return base * power(base, exp - 1);if (exp == 0) {static int power(int base, int exp) {}
Упражнение
Рекурсивни задачи
1. Факториел по два начина. Напиши factorialLoop(int n) с цикъл for и factorial(int n) с рекурсия. Прочети число и отпечатай резултата и от двата метода.
5Факториел на 5 е 120Факториел на 5 е 1202. Сума на цифрите. sumDigits(int n): последната цифра (n % 10) плюс сумата на цифрите на останалото (n / 10). Дъното е едноцифрено число.
3. Фибоначи. Всяко число е сумата на предишните две: 0, 1, 1, 2, 3, 5, 8… fib(int n) връща n-тото число. Пробвай с 10, а после с 45. Защо е толкова бавно? Подсказка: колко пъти се смята всяко число?
4. Палиндром. isPalindrome(String s): първата и последната буква са еднакви и средата е палиндром. Дъното е дума с 0 или 1 букви.
sumDigits(2024)fib(10)isPalindrome("капак")isPalindrome("АБВГД")855truefalseТест