Rekurzia znamená, že funkcia volá samu seba. Znie to zvláštne, ale veľa problémov sa dá prirodzene opísať práve takto: veľký problém vyriešime pomocou toho istého problému, len menšieho. Faktoriál 5 je 5 krát faktoriál 4; súčet cifier čísla 4728 je 8 plus súčet cifier čísla 472. Kľúčom je vždy vedieť, kedy sa zastaviť.
Čo sa naučíš
- napísať rekurzívnu funkciu so základným prípadom a rekurzívnym krokom,
- pochopiť, prečo rekurzia bez základného prípadu skončí chybou
RecursionError, - sledovať priebeh rekurzie (zásobník volaní, poradie výpisov),
- riešiť rekurziou klasické úlohy: súčet cifier, mocninu, NSD, Hanojské veže.
1Rekurzívna funkcia
Faktoriál sa dá definovať aj takto: 0! = 1 a n! = n · (n − 1)! pre n > 0. Túto definíciu môžeme priamo prepísať do Pythonu:
Výstup:
120
[1, 1, 2, 6, 24, 120, 720, 5040]Každá rekurzívna funkcia má dve časti:
- Základný prípad – najjednoduchší vstup, pre ktorý výsledok poznáme hneď, bez ďalšieho volania (
n == 0). - Rekurzívny krok – výsledok vyjadríme pomocou volania tej istej funkcie pre menší problém (
n - 1).
Rekurzívny krok musí problém zakaždým zmenšiť tak, aby sa raz dostal k základnému prípadu.
2Základný prípad
Čo sa stane, keď základný prípad chýba?
Výstup (program skončí chybou):
3 2 1 0 … Traceback (most recent call last): File "program.py", line 5, in <module> odpocitaj(3) File "program.py", line 3, in odpocitaj odpocitaj(n - 1) File "program.py", line 3, in odpocitaj odpocitaj(n - 1) File "program.py", line 3, in odpocitaj odpocitaj(n - 1) [Previous line repeated 992 more times] File "program.py", line 2, in odpocitaj print(n) RecursionError: maximum recursion depth exceeded while calling a Python object
Funkcia by sa volala donekonečna: 3, 2, 1, 0, −1, −2… Python preto počet vnorených volaní obmedzuje (predvolene asi na 1000) a po jeho prekročení vyvolá RecursionError. (Z dlhého výpisu sú tu ukázané iba začiatok a koniec.) Oprava – pridáme základný prípad:
Výstup:
3
2
1
Štart!Rovnako zle dopadne aj funkcia, ktorej krok problém nezmenšuje (faktorial(n) volá faktorial(n)) alebo ho zmenšuje tak, že základný prípad „preskočí“ (faktorial(-1) pri podmienke n == 0).
3Priebeh rekurzie
Pri každom volaní funkcie si Python založí nový „záznam“ s jej lokálnymi premennými. Záznamy sa ukladajú na seba ako taniere – hovoríme o zásobníku volaní. Volanie faktorial(3) prebieha takto:
faktorial(3) │ potrebuje 3 * faktorial(2) │ │ potrebuje 2 * faktorial(1) │ │ │ potrebuje 1 * faktorial(0) │ │ │ │ faktorial(0) vráti 1 ← základný prípad │ │ │ vráti 1 * 1 = 1 │ │ vráti 2 * 1 = 2 │ vráti 3 * 2 = 6 výsledok 6
Najprv ide rekurzia „dole“ až k základnému prípadu, potom sa výsledky vracajú „hore“ a postupne sa skladajú.
3.1Výpis pred volaním a po ňom
Príkaz pred rekurzívnym volaním sa vykoná cestou dole, príkaz za ním až cestou späť – v opačnom poradí:
Výstup:
dole 3
dole 2
dole 1
hore 1
hore 2
hore 3Takto sa dá napríklad vypísať číslo odzadu alebo text obrátene bez cyklu:
Výstup:
aizruker3.2Sledovanie hĺbky
Keď chceš rekurzii porozumieť, pridaj do funkcie parameter s hĺbkou a odsadzuj výpis:
Výstup:
sucet(3)
sucet(2)
sucet(1)
sucet(0)
→ 1
→ 3
→ 64Rekurzívne algoritmy
4.1Súčet cifier
Súčet cifier čísla je jeho posledná cifra plus súčet cifier čísla bez poslednej cifry. Jednociferné číslo je samo sebe súčtom:
Výstup:
214.2Mocnina
aⁿ = a · aⁿ⁻¹ a a⁰ = 1. Rýchlejšia verzia využíva, že pri párnom n je aⁿ = (a^(n/2))²; počet volaní sa tak zmenší z n na približne log₂ n:
Výstup:
1024 1024 15943234.3Najväčší spoločný deliteľ
Euklidov algoritmus je prirodzene rekurzívny: NSD(a, 0) = a a NSD(a, b) = NSD(b, a mod b):
Výstup:
424.4Palindróm
Text je palindróm, ak sa jeho prvý a posledný znak zhodujú a palindrómom je aj to, čo je medzi nimi. Prázdny text a text s jedným znakom sú palindrómy:
Výstup:
kajak True
radar True
python False
a True4.5Fibonacciho čísla a cena opakovaných výpočtov
F(1) = F(2) = 1 a F(n) = F(n − 1) + F(n − 2). Priamy rekurzívny zápis je krásne krátky, ale veľmi pomalý: tie isté hodnoty počíta znova a znova (F(5) volá F(4) aj F(3), F(4) opäť F(3)…). Spočítajme volania:
Výstup:
fib(10) = 55, počet volaní: 109
fib(20) = 6765, počet volaní: 13529
fib(25) = 75025, počet volaní: 150049Počet volaní rastie takmer dvojnásobne s každým ďalším n. Cyklom (téma Cykly) sa F(n) vypočíta v n krokoch. Rekurziu preto používaj tam, kde sa podproblémy neopakujú – alebo si už vypočítané výsledky pamätaj (napríklad v slovníku).
4.6Hanojské veže
Na prvom kolíku je veža z n diskov (väčšie dole). Treba ju presunúť na tretí kolík; naraz sa smie presúvať iba jeden disk a nikdy nesmie ležať väčší disk na menšom. Riešenie je typicky rekurzívne:
- presuň n − 1 diskov z prvého kolíka na pomocný,
- presuň najväčší disk na cieľový kolík,
- presuň n − 1 diskov z pomocného kolíka na cieľový.
Výstup:
disk 1: A → C
disk 2: A → B
disk 1: C → B
disk 3: A → C
disk 1: B → A
disk 2: B → C
disk 1: A → CNa n diskov treba 2ⁿ − 1 presunov – pre 3 disky 7, pre 64 diskov viac, než by stihol ktokoľvek za celý život.
4.7Všetky možnosti: dvojkové reťazce
Rekurzia sa výborne hodí na vymenovanie všetkých možností: na každom mieste skúsime 0 aj 1 a pre zvyšok zavoláme tú istú funkciu.
Výstup:
000 001 010 011 100 101 110 111 4.8Súčet čísel vo vnorenom zozname
Zoznam môže obsahovať ďalšie zoznamy, tie zase ďalšie… Rekurzia si s ľubovoľnou hĺbkou vnorenia poradí ľahko:
Výstup:
36isinstance(prvok, list) zisťuje, či je prvok zoznam.
5Rekurzia alebo cyklus?
- Každú rekurziu možno prepísať na cyklus a naopak.
- Rekurzia je prirodzená pri problémoch, ktoré sa rozpadajú na menšie problémy rovnakého druhu: vnorené štruktúry, stromy, „rozdeľ a panuj“ (binárne vyhľadávanie, triedenie zlučovaním), vymenovanie všetkých možností.
- Cyklus býva rýchlejší a nemá obmedzenie hĺbky. Na jednoduché opakovanie (súčet 1 až n, faktoriál) je v Pythone lepší cyklus.
6Časté chyby
- Chýbajúci alebo nedosiahnuteľný základný prípad →
RecursionError. - Krok nezmenšuje problém (volanie s rovnakým argumentom).
- Zabudnutý
returnpri rekurzívnom volaní:faktorial(n - 1)bezreturnvýsledok zahodí a funkcia vrátiNone.
Výstup (program skončí chybou):
Traceback (most recent call last):
File "program.py", line 6, in <module>
print(faktorial(3))
^^^^^^^^^^^^
File "program.py", line 4, in faktorial
n * faktorial(n - 1) # chýba return
^^^^^^^^^^^^^^^^
File "program.py", line 4, in faktorial
n * faktorial(n - 1) # chýba return
~~^~~~~~~~~~~~~~~~~~
TypeError: unsupported operand type(s) for *: 'int' and 'NoneType'Výnimka vznikla až v hĺbke: faktorial(1) vrátil None a výraz 2 * None je chyba.
- Príliš veľký vstup:
faktorial(5000)prekročí limit hĺbky – použi cyklus.
Zhrnutie
- Rekurzívna funkcia volá samu seba; má základný prípad (bez volania) a rekurzívny krok (menší problém).
- Bez dosiahnuteľného základného prípadu vznikne
RecursionError(limit asi 1000 volaní). - Volania tvoria zásobník: cestou dole sa volá, cestou hore sa výsledky skladajú; príkazy za volaním sa vykonajú v opačnom poradí.
- Klasika: faktoriál, súčet cifier, mocnina, NSD, palindróm, Hanojské veže, vymenovanie možností.
- Pri opakujúcich sa podproblémoch (naivný Fibonacci) je rekurzia veľmi pomalá.
Úlohy na precvičenie
- Napíš rekurzívnu funkciu, ktorá vráti súčet čísel od 1 po n.
- Napíš rekurzívnu funkciu
pocet_cifier(n). - Napíš rekurzívnu funkciu, ktorá vypíše čísla od n po 1 a potom späť od 1 po n.
- Napíš rekurzívnu funkciu, ktorá vráti maximum zoznamu (maximum z prvého prvku a maxima zvyšku).
- Uprav Fibonacciho funkciu tak, aby si vypočítané hodnoty pamätala v slovníku, a porovnaj počet volaní.
- Vypíš všetky slová dĺžky 3 z písmen
a,b,c(rekurzívne, ako dvojkové reťazce).