Рекурсия
Проблем
Колко подредби има?
На рафта има 5 книги. По колко начина можеш да ги подредиш? За първото място има 5 избора, за второто 4, после 3, 2 и 1: 5 · 4 · 3 · 2 · 1 = 120. Това е факториел, пише се 5!.
С цикъл вече го можеш:
def factorial_loop(n): result = 1 for i in range(2, n + 1): result *= i return result
print(factorial_loop(5))120Погледни го и така: 5! = 5 · 4!. А 4! = 4 · 3!. Задачата се решава чрез същата задача, само по-малка. Може ли функцията да каже точно това?
Идеята
Функция, която вика себе си
Може. Функция, която вика сама себе си, се казва рекурсивна, а подходът е рекурсия.
def factorial(n): if n == 1: return 1 return n * factorial(n - 1)
print(factorial(5))120Две неща я правят да работи:
- Дъно. Случай, който се решава направо, без ново извикване. Тук: 1! = 1.
- По-малка задача. Всяко извикване вика себе си с число, по-близо до дъното:
n - 1.
Виж го
Купчината от извиквания
def factorial(n): if n == 1: return 1 return n * factorial(n - 1)
print(factorial(3)) # 6Стъпка по стъпка, с думи
- Програмата вика 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 и програмата го отпечатва.
Всяко извикване получава своя памет със свое n. Те се трупат едно над друго и чакат. Тази купчина се казва стек от извиквания (call stack). Когато дъното върне 1, отговорите слизат надолу един по един и купчината се разтуря.
Пробвай
Слизане и качване
Какво ще отпечата програмата?
def steps(n): if n == 0: return print(f"слизам {n}") steps(n - 1) print(f"качвам {n}")
steps(2)Редът преди рекурсивното извикване се изпълнява на път надолу. Редът след него чака, докато по-малкото извикване свърши. Затова „качвам“ излиза в обратен ред: първо се връща последното извикване.
Пробвай
Намери грешката
sum_to(4) трябва да върне 1 + 2 + 3 + 4 = 10. Вместо това програмата спира с RecursionError: maximum recursion depth exceeded. На кой ред е грешката?
Избери реда с грешката.
Функцията вика себе си със същото n, а не с по-малко. Задачата не се смалява, дъното не идва и стекът се препълва. Поправката е sum_to(n - 1).
Идеята
Рекурсия или цикъл?
Факториелът става и с цикъл, и с рекурсия. Цикълът тук е по-добрият избор: не трупа памет за всяко извикване. А Python слага граница на стека: при около 1000 вложени извиквания спира програмата с RecursionError. Още factorial(1100) не минава, а цикълът смята и него.
Рекурсията е естествена там, където нещото съдържа по-малки неща от същия вид: папка с папки вътре, категория с подкатегории, разклонение с разклонения. Такива задачи ще срещнеш по-нататък в курса. Там цикълът става тромав, а рекурсивната функция остава кратка.
По-точно, за любопитните
- Факториелът често се пише с дъно 0! = 1 (
if n == 0:). Тогава работи и за 0.
- Целите числа в Python нямат граница: колкото е голям резултатът, толкова цифри има. 30! излиза точно:
def factorial(n): if n == 1: return 1 return n * factorial(n - 1)
print(factorial(30))265252859812191058636308480000000- Границата на стека може да се вдигне с
sys.setrecursionlimit(), но това рядко е добра идея: за дълбоки сметки пиши цикъл.
Пробвай
Подреди редовете
Функцията трябва да смята степен: power(2, 10) е 1024. Подреди редовете ѝ.
def power(base, exp):return 1return base * power(base, exp - 1)if exp == 0:
Упражнение
Рекурсивни задачи
1. Факториел по два начина. Напиши factorial_loop(n) с цикъл for и factorial(n) с рекурсия. Прочети число и отпечатай резултата и от двете функции.
5Факториел на 5 е 120Факториел на 5 е 1202. Сума на цифрите. sum_digits(n): последната цифра (n % 10) плюс сумата на цифрите на останалото (n // 10). Дъното е едноцифрено число.
3. Фибоначи. Всяко число е сумата на предишните две: 0, 1, 1, 2, 3, 5, 8… fib(n) връща n-тото число. Пробвай с 10, а после с 35. Защо е толкова бавно? Подсказка: колко пъти се смята всяко число?
4. Палиндром. is_palindrome(s): първата и последната буква са еднакви и средата е палиндром. Дъното е дума с 0 или 1 букви.
sum_digits(2024)fib(10)is_palindrome("капак")is_palindrome("АБВГД")855TrueFalseТест