Rekurzivní algoritmy prof. Ing. Pavel Tvrdík CSc. Katedra počítačových systémů Fakulta informačních technologií České vysoké učení technické v Praze c Pavel Tvrdík, 2010 Efektivní algoritmy (BI-EFA) ZS 2010/11, Přednáška 8 Evropský sociální fond. Praha & EU: Investujeme do vaší budoucnosti prof. Pavel Tvrdík (FIT ČVUT) Rekurzivní algoritmy BI-EFA, 2010, Předn. 8 1 / 22
Rekurze Rekurze Rekurze je metoda zápisu algoritmu, kde se stejný postup opakuje na částech vstupních dat. Výhody: úsporná: zápis kódu je kratší a konečným zápisem lze definovat množinu dat, přirozená: opakování a samopodobnost jsou v přírodě běžné, intuitivní: explicite pojmenovává to, co se opakuje v menším, expresivní: rekurzivní specifikace umožňuje snadnou analýzu složitosti a ověření správnosti (viz např. MergeSort v Přednášce 5). Nevýhody: Interpretace nebo provádění rekurzivního kódu používá systémový zásobník pro předání parametrů volání a proto vyžaduje systémovou paměť. Dynamická alokace systémového zásobníku a ukládání parametrů na něj navíc představuje časovou režii. prof. Pavel Tvrdík (FIT ČVUT) Rekurzivní algoritmy BI-EFA, 2010, Předn. 8 2 / 22
Typy rekurze Koncová rekurze Typy rekurze Koncová rekurze: rekurzivní volání je posledním příkazem algoritmu, po kterém se už neprovádějí žádné další odložené operace. Příklad 1 (Euclidův algoritmus) Největší společný dělitel: Nechť n m 0. { n if m = 0, GCD(n, m) = GCD(m, Remainder(n, m)) if m > 0. prof. Pavel Tvrdík (FIT ČVUT) Rekurzivní algoritmy BI-EFA, 2010, Předn. 8 3 / 22
Typy rekurze Lineární rekurze Lineární rekurze: V těle algoritmu je pouze jedno rekurzivní volání anebo jsou dvě, ale vyskytují se v disjunktních větvích podmíněných příkazů a nikdy se neprovedou současně. Strom rekurzivních volání je lineární. Příklad 2 (Vložení prvku do BVS) procedure insertrec(x, z, p) { (1) if (x = Null) (2) {P arent[z] p; return z}; (3) Cnt[x] + +; (4) if (V al[z] < V al[x]) (5) then Lef t[x] insertrec(lef t[x], z, x) (6) else Right[x] insertrec(right[x], z, x); (7) return x; } prof. Pavel Tvrdík (FIT ČVUT) Rekurzivní algoritmy BI-EFA, 2010, Předn. 8 4 / 22
Typy rekurze Kaskádní rekurze Kaskádní rekurze: V těle algoritmu jsou vedle sebe aspoň dvě rekurzivní volání. Viz MergeSort, QuickSort. Příklad 3 (Fibonacciho posloupnost) { 1 if n = 1, 2, F(n) = F(n 1) + F(n 2) if n > 2. Příklad 4 (Obecná lineární rekurence) F(n) = { 1 if n 3, a 1 F(n 1) + a 2 F(n 2) + a 3 F(n 3) if n > 3. Strom rekurzivních volání je více-ární. prof. Pavel Tvrdík (FIT ČVUT) Rekurzivní algoritmy BI-EFA, 2010, Předn. 8 5 / 22
Typy rekurze Vnořená rekurze Vnořená rekurze: Algoritmus volaný rekurzivně s parametry, jejichž hodnoty jsou také výsledkem rekurzivního volání. Příklad 5 (Ackermannova funkce) n + 1 if m = 0, A(m, n) = A(m 1, 1) if m > 0 and n = 0, A(m 1, A(m, n 1)) if m > 0 and n > 0. Neuvěřitelně rychle rostoucí funkce, viz http://mathworld.wolfram.com/ackermannfunction.html. prof. Pavel Tvrdík (FIT ČVUT) Rekurzivní algoritmy BI-EFA, 2010, Předn. 8 6 / 22
Rekurze vs. iterace Rekurze vs. iterace Rekurzivní programování má základní oporu v teoretické informatice, kde bylo dokázáno, že: 1 Každou funkci, která může být implementovaná na vonneumannovském počítači, lze vyjádřit rekurzivně bez použití iterace. 2 Každá rekurzivní funkce se dá vyjádřit iterativně s použitím zásobníku (implicitní zásobník se stane viditelný uživateli). Koncovou rekurzi lze vždy nahradit iterací bez nutnosti zásobníku. (Zformulujte intuitivně zřejmý důvod.) V předchozích přednáškách bylo několik případů konverze rekurzivního algoritmu na iterační. prof. Pavel Tvrdík (FIT ČVUT) Rekurzivní algoritmy BI-EFA, 2010, Předn. 8 7 / 22
Rekurze vs. iterace Rekurzivní heapify procedure heapifyrec(a, i) { (1) l left(i); (2) r right(i); (3) if (l Heap Size(A) & A[l] > A[i]) (4) then Largest l else Largest i; (5) if (r Heap Size(A) & A[r] > A[Largest]) (6) then Largest r; (7) if (Largest i) (8) then {A[i] A[Largest]; heapifyrec(a, Largest)} } prof. Pavel Tvrdík (FIT ČVUT) Rekurzivní algoritmy BI-EFA, 2010, Předn. 8 8 / 22
Rekurze vs. iterace Iterativní heapify procedure heapifyiter(a, i) { (1) while (i Heap Size(A)/2 ) (2) do {l left(i); (3) r right(i); (4) if (l Heap Size(A) & A[l] > A[i]) (5) then Largest l else Largest i; (6) if (r Heap Size(A) & A[r] > A[Largest]) (7) then Largest r; (8) if (Largest = i) return(); (9) A[i] A[Largest]; (10) i Largest; } }; prof. Pavel Tvrdík (FIT ČVUT) Rekurzivní algoritmy BI-EFA, 2010, Předn. 8 9 / 22
Rekurze vs. iterace Rekurzivní QuickSort procedure QuickSort(A, low, high) { (1) if (low < high) (2) then { pivot SelectPivot(A, low, high); (3) mid Rozdel(A, low, high, pivot); (4) QuickSort(A, low, mid); (5) QuickSort(A, mid + 1, high) } } prof. Pavel Tvrdík (FIT ČVUT) Rekurzivní algoritmy BI-EFA, 2010, Předn. 8 10 / 22
Rekurze vs. iterace Méně rekurzivní QuickSort procedure QuickSortTail(A, low, high) { (1) while (low < high) (2) do { pivot SelectPivot(A, low, high); (3) mid Rozdel(A, low, high, pivot); (4) QuickSortTail(A, low, mid); (5) low mid + 1 } } prof. Pavel Tvrdík (FIT ČVUT) Rekurzivní algoritmy BI-EFA, 2010, Předn. 8 11 / 22
Rekurzivní algoritmy nad stromy Algoritmy nad binárními stromy Binární strom je definován rekurzivně (viz Slajd 10 v přednášce 4). Proto řada algoritmů nad stromy je rekurzivních. Zde se zaměříme na binární stromy s tím, že většina algoritmů je zobecnitelná pro k-ární stromy. struct node { int V al; //hodnota uložená v uzlu int Index; //pořadové číslo uzlu node *P arent; //ukazatel na rodiče node *Lef t; //ukazatel na levý podstrom node *Right //ukazatel na pravý podstrom } prof. Pavel Tvrdík (FIT ČVUT) Rekurzivní algoritmy BI-EFA, 2010, Předn. 8 12 / 22
Rekurzivní algoritmy nad stromy Generování náhodného binárního stromu BS s náhodnou strukturou: náhodně chybějící levé a pravé podstromy. Hodnoty v uzlech jsou náhodná čísla z intervalu [1, 99]. procedure randbintree(h) // h je výška (1){ if (h 0 & Random(1, 2) = 1) (2) then { vygeneruj dynamicky nový uzel N ewn ode; (3) Left[NewNode] randbintree(h 1); (4) V alue[n ewn ode] Random(1, 99); (5) Right[N ewn ode] randbintree(h 1); (6) return(n ewn ode)} (7) else return(null) } 23 47 87 6 3 91 19 36 51 70 prof. Pavel Tvrdík (FIT ČVUT) Rekurzivní algoritmy BI-EFA, 2010, Předn. 8 13 / 22
Rekurzivní algoritmy nad stromy 3 základní algoritmy procházení stromů a číslování uzlů PreOrder InOrder PostOrder 1 2 3 c 0; //globální čítač uzlů procedure preorderwalk(r) {// r je ukazatel na kořen stromu (1) if (r = Null) then return(); (2) Index[r] c; c c + 1; (3) print(index[r], V alue[r]); (4) preorderwalk(lef t[r]); (5) preorderwalk(right[r]); } 47 87 6 3 19 36 51 70 1 2 3 4 5 6 7 8 9 10 23 47 6 19 36 87 3 51 91 70 23 91 prof. Pavel Tvrdík (FIT ČVUT) Rekurzivní algoritmy BI-EFA, 2010, Předn. 8 14 / 22
Rekurzivní algoritmy nad stromy 3 základní algoritmy procházení stromů a číslování uzlů PreOrder InOrder PostOrder 2 1 3 c 0; //globální čítač uzlů procedure inorderwalk(r) {// r je ukazatel na kořen stromu (1) if (r = Null) then return(); (2) inorderwalk(lef t[r]); (3) Index[r] c; c c + 1; (4) print(index[r], V alue[r]); (5) inorderwalk(right[r]); } 47 87 6 3 19 36 51 70 1 2 3 4 5 6 7 8 9 10 47 19 6 36 23 51 3 87 70 91 23 91 prof. Pavel Tvrdík (FIT ČVUT) Rekurzivní algoritmy BI-EFA, 2010, Předn. 8 15 / 22
Rekurzivní algoritmy nad stromy 3 základní algoritmy procházení stromů a číslování uzlů PreOrder InOrder PostOrder 3 1 2 c 0; //globální čítač uzlů procedure postorderwalk(r) {// r je ukazatel na kořen stromu (1) if (r = Null) then return(); (2) postorderwalk(lef t[r]); (3) postorderwalk(right[r]); (4) r.index c; c c + 1; (5) print(index[r], V alue[r]); } 23 47 87 6 3 91 19 36 51 70 1 2 3 4 5 6 7 8 9 10 19 36 6 47 51 3 70 91 87 23 prof. Pavel Tvrdík (FIT ČVUT) Rekurzivní algoritmy BI-EFA, 2010, Předn. 8 16 / 22
Rekurzivní algoritmy nad stromy Shrnutí Všechny 3 průchody jsou založeny na průchodu do hloubky = DFS (Depth-First-Search). Liší se pouze podmínkou, kdy je procházený uzel označen (zpracován). PreOrder: při první návštěvě. InOrder: při přechodu z levého do pravého podstromu. PostOrder: při poslední návštěvě. PreOrder a PostOrder lze zobecnit pro libovolné k-ární stromy. Binární strom lze tedy linearizovat 3 základními způsoby. Protože v každém souvislém grafu G lze zkonstruovat kostru, lze takto linearizovat (očíslovat) uzly lib. souvislého grafu. prof. Pavel Tvrdík (FIT ČVUT) Rekurzivní algoritmy BI-EFA, 2010, Předn. 8 17 / 22
Rekurzivní algoritmy nad stromy Velikost a hloubka stromu Algoritmus 6 procedure treesize(r) {// r je ukazatel na kořen stromu (1) if (r = Null) then return(0); (2) return(treesize(lef t[r]) + treesize(right[r]) + 1) } Algoritmus 7 procedure treedepth(r) {// r je ukazatel na kořen stromu (1) if (r = Null) then return(0); (2) return(max(treedepth(lef t[r]), treedepth(right[r])) + 1) } prof. Pavel Tvrdík (FIT ČVUT) Rekurzivní algoritmy BI-EFA, 2010, Předn. 8 18 / 22
Rekurzivní definované struktury Rekurzivně definované struktury Algoritmus 8 procedure ruler(h) { (1) if (h < 1) (2) then return(); (3) ruler(h 1); (4) print(h); (5) ruler(h 1); } Pokud zavoláme ruler(4), na výstupu se objeví posloupnost čísel v tomto pořadí 1 2 1 3 1 2 1 4 1 2 1 3 1 2 1. prof. Pavel Tvrdík (FIT ČVUT) Rekurzivní algoritmy BI-EFA, 2010, Předn. 8 19 / 22
Rekurzivní definované struktury Strom rekurzivních volání Koncová volání ruler(0) jsou v obrázku vynechána. ruler(2) ruler(1) print(1) print(2) ruler(1) print(1) ruler(4) ruler(3) print(3) ruler(2) print(4) ruler(2) ruler(3) print(3) ruler(1) print(2) ruler(1) ruler(1) print(2) ruler(1) print(1) print(1) print(1) print(1) ruler(2) ruler(1) print(2) ruler(1) print(1) print(1) prof. Pavel Tvrdík (FIT ČVUT) Rekurzivní algoritmy BI-EFA, 2010, Předn. 8 20 / 22
Paměťová složitost rekurzivních algoritmů Skutečná paměťová složitost rekurzivních výpočtů V rekurzivní proceduře se při rekurzivním volání téže procedury (totéž pro funkce) musí na systémový zásobník uložit hodnoty parametrů a lokálních proměnných volající procedury. Při návratu po ukončení této vnořené procedury se tyto hodnoty obnoví, aby volající procedura mohla pokračovat se stejným kontextem jako před vstupem do rekurzivního volání. Výška zásobníku se rovná hloubce stromu rekurzivních volání procedury. Rekurzivní výpočet má tedy skrytou paměťovou náročnost úměrnou hloubce tohoto stromu. prof. Pavel Tvrdík (FIT ČVUT) Rekurzivní algoritmy BI-EFA, 2010, Předn. 8 21 / 22
Paměťová složitost rekurzivních algoritmů Skutečná paměťová složitost rekurzivních algoritmů: Příklad MergeSort (Přednáška 5, Slajd 2): vyžaduje Θ(log n) paměti na zásobník. MS(A,Aux,1,16) MS(A,Aux,1,8) MS(A,Aux,9,16) M(A,Aux,1,16) MS(A,Aux,1,4) MS(A,Aux,5,8) M(A,Aux,1,8) MS(A,Aux,9,12) MS(A,Aux,13,16) M(A,Aux,9,16) MS(A,Aux,1,2) MS(A,Aux,3,4) M(A,Aux,1,4) MS(A,Aux,5,6) MS(A,Aux,7,8) M(A,Aux,5,8) MS(A,Aux,9,10) MS(A,Aux,11,12) M(A,Aux,9,12) MS(A,Aux,13,14) MS(A,Aux,15,16) M(A,Aux,13,16) MS(A,Aux,1,1); MS(A,Aux,2,2) M(A,Aux,1,2) MS(A,Aux,3,3); MS(A,Aux,4,4) M(A,Aux,3,4) MS(A,Aux,5,5); MS(A,Aux,6,6) M(A,Aux,5,6) MS(A,Aux,7,7); MS(A,Aux,8,8) M(A,Aux,7,8) MS(A,Aux,9,9); MS(A,Aux,10,10) M(A,Aux,9,10) MS(A,Aux,11,11); MS(A,Aux,12,12) M(A,Aux,11,12) MS(A,Aux,13,13); MS(A,Aux,14,14) M(A,Aux,13,14) MS(A,Aux,15,15); MS(A,Aux,16,16) M(A,Aux,15,16) prof. Pavel Tvrdík (FIT ČVUT) Rekurzivní algoritmy BI-EFA, 2010, Předn. 8 22 / 22