Matrjošky

O čem to je

Ruská panenka matrjoška má v sobě menší panenku, ta zase menší a tak dál, až v nejmenší už nic není. Chceš spočítat, kolik panenek je celkem? Otevřeš jednu, řekneš „jedna plus tolik, kolik je v té menší“ a s menší uděláš totéž. U nejmenší skončíš.

Tomuhle způsobu řešení se říká rekurze. Úlohu vyřešíš tak, že ji převedeš na stejnou, ale menší úlohu. Rekurzivně se prochází složky na disku, stromy rodokmenů nebo se třídí velké seznamy.

Nové pojmy

RekurzeFunkce, která volá sama sebe s menším vstupem.Faktoriál 5 je 5 krát faktoriál 4.
Základní (ukončovací) případNejmenší úloha, kterou funkce vyřeší přímo, bez dalšího volání sebe sama. Bez něj by se funkce volala donekonečna.Nejmenší matrjoška: v ní už nic není.
Rekurzivní případČást funkce, kde se úloha zmenší a funkce zavolá sama sebe.return n * faktorial(n - 1)
Faktoriál n!Součin všech celých čísel od 1 do n. Platí 0! = 1.5! = 5 · 4 · 3 · 2 · 1 = 120

Faktoriál rekurzí

  • def faktorial(n):
  •     if n <= 1:
  •         return 1
  •     return n * faktorial(n - 1)

Všimni si, že 5! = 5 · 4!, 4! = 4 · 3! a tak dál. Funkce přesně tohle zapisuje. Řádek s if je základní případ, poslední řádek rekurzivní případ.

Co se děje při volání faktorial(4)

faktorial(4) čeká na faktorial(3).
faktorial(3) čeká na faktorial(2).
faktorial(2) čeká na faktorial(1).
faktorial(1) je základní případ a vrátí 1.
Návrat zpět: 2 · 1 = 2, 3 · 2 = 6, 4 · 6 = 24.
Napiš rekurzivně součet čísel 1 až n. Kolik vrátí pro n = 4?
Základní případ: if n == 0: return 0. Rekurzivní: return n + soucet(n - 1). soucet(4) = 4 + 3 + 2 + 1 + 0 = 10.
Spočítej rekurzivně délku řetězce bez len().
def delka(s): → if s == "": return 0 → return 1 + delka(s[1:]). Každé volání „uloupne“ první znak. Pro „ahoj“: 1 + 1 + 1 + 1 + 0 = 4.
✗def faktorial(n): return n * faktorial(n - 1)
✓Přidej základní případ: if n <= 1: return 1.

Bez základního případu se funkce volá pořád dál (1, 0, −1, …). Python po zhruba 1000 zanořeních skončí chybou RecursionError.

Rekurze řeší úlohu převodem na menší úlohu stejného druhu. Každá rekurzivní funkce potřebuje základní případ, který skončí bez dalšího volání, a rekurzivní případ, který úlohu zmenšuje.

Zásobník volání

O čem to je

Vaříš a uprostřed receptu zjistíš, že musíš udělat omáčku. Odložíš rozdělaný recept a začneš omáčku. V ní zjistíš, že potřebuješ vývar. Odložíš i omáčku. Až je vývar hotový, vrátíš se k omáčce a pak k původnímu receptu. Rozdělané úlohy máš na sobě jako hromádku talířů.

Počítač si rozdělaná volání funkcí pamatuje úplně stejně. Když tomu porozumíš, pochopíš, proč rekurze funguje, kdy je pomalá a proč může spadnout.

Nové pojmy

Rekurzivní funkceFunkce, která volá sama sebe. Má základní případ (vyřeší přímo) a rekurzivní případ (zavolá se s menším vstupem).def soucet(n): return 0 if n == 0 else n + soucet(n - 1)
Zásobník volání (call stack)Místo v paměti, kam si program ukládá rozdělaná volání funkcí i s jejich lokálními proměnnými. Nové volání se položí navrch, po návratu se odebere. Poslední dovnitř, první ven.Hromádka talířů: bereš vždy ten horní.
Přetečení zásobníkuKdyž je zanoření příliš hluboké, zásobník se zaplní. Python má limit, výchozí je 1000 úrovní, a pak vyvolá RecursionError.Rekurze bez základního případu.
IteraceŘešení pomocí cyklu (for, while) místo rekurze. Každou rekurzi lze přepsat na cyklus.Faktoriál cyklem: v = 1, for i in range(2, n + 1): v = v * i.

Pořadí výpisu

Záleží na tom, jestli funkce něco udělá před, nebo po rekurzivním volání.

FunkceTěloVýstup pro n = 3
dolu(n)if n == 0: return → print(n) → dolu(n - 1)3 2 1
nahoru(n)if n == 0: return → nahoru(n - 1) → print(n)1 2 3

U nahoru se print provede až při návratu. Nejdřív se dojde až na dno zásobníku (n = 0) a vypisuje se cestou zpět.

Fibonacciho čísla

Každé číslo je součet dvou předchozích: 0, 1, 1, 2, 3, 5, 8, 13… Rekurzivně: fib(n) = fib(n - 1) + fib(n - 2), základní případy fib(0) = 0, fib(1) = 1.

Graf ukazuje, kolikrát se funkce zavolá. Porovnej fib(10) a fib(20): vstup je jen dvakrát větší, volání je víc než stokrát víc. Stejné hodnoty se počítají pořád znovu.

Kolik je fib(6)?
Řada od fib(0): 0, 1, 1, 2, 3, 5, 8. fib(6) = fib(5) + fib(4) = 5 + 3 = 8.
Eukleidův algoritmus pro největšího společného dělitele: nsd(a, b) = a, když b == 0, jinak nsd(b, a % b). Kolik je nsd(48, 18)?
nsd(48, 18) → nsd(18, 12) → nsd(12, 6) → nsd(6, 0) → 6.
✗Rekurze je vždy lepší než cyklus, protože je kratší.
✓Rekurzi použij, když úloha je přirozeně rekurzivní (stromy, složky, „rozděl a panuj“). Jinak bývá cyklus rychlejší a bez rizika RecursionError.

Každé volání zabere místo na zásobníku a čas. Naivní fib(n) navíc počítá stejné věci mnohokrát.

Rozdělaná volání se ukládají na zásobník volání: poslední dovnitř, první ven. Příliš hluboká rekurze skončí RecursionError. Každou rekurzi lze přepsat cyklem a naivní rekurze může být velmi pomalá.