Rekurze V programování ve dvou hladinách: - rekurzivní algoritmus (řešení úlohy je definováno pomocí řešení podúloh stejného charakteru) - rekurzivní volání procedury nebo funkce (volá sama sebe přímo nebo nepřímo prostřednictvím jiných procedur resp. funkcí) Většinou se rekurzivní algoritmy realizují pomocí rekurzivních volání, ale není to nezbytné: - rekurzivní algoritmus lze realizovat bez rekurzivních volání (pomocí vlastního zásobníku na uložení rozpracovaných nedokončených podúloh) více práce pro programátora, program obvykle delší a méně přehledný, výpočet ale může být o něco efektivnější - rekurzivní volání lze teoreticky použít i při realizaci nerekurzivních iteračních algoritmů (dokonce každý cyklus lze nahradit rekurzivní procedurou) většinou nevhodné, nečitelný a méně efektivní program Pavel Töpfer, 2017 Programování 1-8 1
Rekurzivní volání - najednou je rozpočítáno více exemplářů téže procedury - všechny počítají podle téhož kódu - každý exemplář má na zásobníku svůj vlastní aktivační záznam s lokálními proměnnými, parametry a technickými údaji (kde je rozpočítán, návratová adresa) - procedura nemá přístup k lokálním proměnným jiného rekurzivního exempláře procedure Otoc; var U: char; read(u); if U <> then Otoc; write(u) end; Vstup: ABC_ Výstup: _CBA Pavel Töpfer, 2017 Programování 1-8 2
Příklad: Eukleidův algoritmus realizovaný funkcí s cyklem (bylo dříve) function NSD(X, Y: integer): integer; while X <> Y do if X > Y then X:=X - Y else Y:=Y - X; NSD:=X end; {function NSD} Pavel Töpfer, 2017 Programování 1-8 3
Eukleidův algoritmus realizovaný rekurzivní funkcí - přesně kopíruje rekurzivní vztah pro NSD, na němž je Eukleidův algoritmus založen (bylo dříve): když X < Y NSD(X,Y) = NSD(X,Y-X) když X > Y NSD(X,Y) = NSD(X-Y,Y) když X = Y NSD(X,Y) = X function NSD(X, Y: integer): integer; if X > Y then NSD:=NSD(X-Y, Y) else if Y > X then NSD:=NSD(X, Y-X) else {X=Y} NSD:=X end; {function NSD} Pavel Töpfer, 2017 Programování 1-8 4
Faktoriál N! (součin čísel od 1 do N) rekurzivní definice: N! = 1 pro N = 0 N.(N-1)! pro N > 0 function Faktorial (N: integer): integer; var F, I: integer; F:=1; for I:=2 to N do F:=F * I; Faktorial:=F end; časová složitost O(N) function Faktorial (N: integer): integer; if N=0 then Faktorial:=1 else Faktorial:=N * Faktorial(N-1) end; časová složitost O(N) Pavel Töpfer, 2017 Programování 1-8 5
Fibonacciho čísla 0 pro N = 0 F N = 1 pro N = 1 F N-1 + F N-2 pro N > 1 rekurzivní definice posloupnosti čísel realizace rekurzivní funkcí přesně podle definice: function Fib(N: integer): integer; if N=0 then Fib:=0 else if N=1 then Fib:=1 else Fib:=Fib(N-1) + Fib(N-2) end; funkce teoreticky správná, ale časová složitost O(2 N ) pro N > cca 30 prakticky nepoužitelná důvod: mnohokrát se opakovaně počítají stejné věci Pavel Töpfer, 2017 Programování 1-8 6
Možnosti řešení: 1. rekurzivní algoritmus + pomocné pole velikosti O(N) pro uložení již spočítaných funkčních hodnot každé F i se počítá jen jednou časová složitost O(N) chytrá rekurze, memoizace, dynamické programování 2. počítat hodnoty iteračně odspodu v pořadí F 1, F 2, F N časová složitost O(N), navíc stačí konstantní paměť dynamické programování 3. z rekurzivní definice odvodit explicitní vzorec a počítat podle něj F N 5 5 1 2 5 N 1 2 5 N Pavel Töpfer, 2017 Programování 1-8 7
Pavel Töpfer, 2017 Programování 1-8 8
Použití rekurze Úlohy typu zkoušení všech možností nebo generování všech možností Příklad: vypsat všechna K-ciferná čísla v poziční soustavě o základu N Pokud K je předem pevně dáno: for i1:=0 to N-1 do for i2:=0 to N-1 do for i3:=0 to N-1 do for ik:=0 to N-1 do writeln(i1, i2, i3,, ik) Je-li K vstupním údajem, nemůžeme toto zapsat pomocí vnořených cyklů nevíme předem, kolik jich máme v programu napsat. Pavel Töpfer, 2017 Programování 1-8 9
Řešení: rekurzivní procedura obsahující jeden takový cyklus, rekurzivní zanoření jde vždy do hloubky K. const MaxK = 20; {max. přípustné K} var C: array[1..maxk] of byte; {uložení čísla} N, K: byte; write('zadejte hodnoty K a N: '); readln(k,n); Cislo(1); end. Pavel Töpfer, 2017 Programování 1-8 10
procedure Cislo(p:byte); {p - pořadí vybírané cifry čísla} {procedura používá globální proměnné C, K, N} var i,j:byte; for i:=0 to N-1 do C[p] := i; if p < K then Cislo(p+1) else for j:= 1 to K do write(c[j]); writeln end end end; Pavel Töpfer, 2017 Programování 1-8 11
procedure Cislo(p:byte); {p - pořadí vybírané cifry čísla} {procedura používá globální proměnné C, K, N} var i:byte; if p > K then {hotovo} for i:= 1 to K do write(c[i]); writeln end else {doplnit C[p]} for i:=0 to N-1 do C[p] := i; Cislo(p+1) end end; Pavel Töpfer, 2017 Programování 1-8 12
Variace s opakováním K-prvkové z N-prvkové množiny {1,2,,N} = všechny uspořádané K-tice tvořené prvky z {1,2,,N} s možností opakování hodnot Např. pro K=2, N=4: (1,1) (1,2) (1,3) (1,4) (2,1), (2,2) (2,3) (2,4) (3,1) (3,2) (3,3) (3,4) (4,1), (4,2) (4,3) (4,4) Řešení: na každou z K pozic vytvářené variace postupně umístíme každé z N čísel Pavel Töpfer, 2017 Programování 1-8 13
Kombinace bez opakování K-prvkové z N-prvkové množiny {1,2,,N} = všechny K-prvkové podmnožiny vybrané z množiny {1,2,,N} (bez možnosti opakování hodnot) Např. pro K=2, N=4: (1,2) (1,3) (1,4) (2,3) (2,4) (3,4) Řešení: generujeme pouze ostře rostoucí K-tice hodnot z množiny {1,2,,N} Pavel Töpfer, 2017 Programování 1-8 14
Doplnění znamének Je dáno N celých čísel a požadovaný součet C. Před čísla doplňte znaménka + nebo tak, aby byl součet čísel se znaménky roven danému C. Nalezněte všechna řešení úlohy. Řešení: před každé číslo zkusíme postupně dát + nebo, po vytvoření celé N-tice znamének otestujeme součet 2 N možností, tedy časové složitost O(2 N ) Pavel Töpfer, 2017 Programování 1-8 15
Rozklad čísla Zadané kladné celé číslo N rozložte všemi různými způsoby na součet kladných celých sčítanců. Rozklady lišící se pouze pořadím sčítanců nepovažujeme za různé. Příklad: 5 = 4 + 1 = 3 + 2 = 3 + 1 + 1 = 2 + 2 + 1 = 2 + 1 + 1 + 1 = 1 + 1 + 1 + 1 + 1 Řešení: Aby se neopakovaly stejné rozklady s různým pořadím sčítanců, budeme vytvářet pouze rozklady s nerostoucím pořadím sčítanců. Na každou pozici rozkladu vždy vyzkoušíme všechny přípustné hodnoty (maximálně kolik ještě zbývá a max. kolik je na předchozí pozici, minimálně 1). Provádíme, dokud je co rozkládat. Pavel Töpfer, 2017 Programování 1-8 16