Stromy Jan Hnilica Počítačové modelování 14 1
Základní pojmy strom = dynamická datová struktura, složená z vrcholů (uzlů, prvků) propojených hranami hrany chápeme jako orientované, tzn. vedou z uzlu A do uzlu B uzel A je předek (otec) uzlu B, uzel B je potomek (syn) uzlu A kořen = uzel, který nemá předka, strom má jeden kořen list = uzel, který nemá potomky, listů je obvykle více výška stromu = počet hladin, maximální vzdálenost od kořene k listu (měřeno počtem vrcholů mezi kořenem a listem, délku hran nijak neřešíme) A A B A K K - kořen L - listy výška stromu = 3 A A L A L A A L A A žádný vrchol ve stromové struktuře nemá více rodičů stromy jsou speciálním případem tzv. grafů (těmi se budeme zabývat později) A L A L A L Jan Hnilica Počítačové modelování 14 2
Podstromy pro libovolný uzel U platí, že na jeho syny lze pohlížet jako na kořeny podstromů podstromy jsou samy o sobě stromy stromová struktura má přirozeně rekurzivní charakter při práci se stromy se často využívá rekurzivních funkcí uzel U podstrom U podstrom U Jan Hnilica Počítačové modelování 14 3
Obecný a binární strom obecný strom: počet potomků uzlu není nijak omezen binární strom: uzel může mít maximálně dva potomky - binární stromy se používají častěji než obecné obecný strom binární strom Jan Hnilica Počítačové modelování 14 4
Binární strom uzel stromu typedef struct uzel // uložené informace int X; double Y; char Z;... // pointery na syny (podstromy) struct uzel * levy; struct uzel * pravy; Uzel; v programu uchováváme pointer na kořen stromu, který předáváme jako parametr funkcím (stejně jako jsme předávali pointer na první uzel lineárního seznamu) listy mají oba ukazatele na syny nastaveny na NULL Jan Hnilica Počítačové modelování 14 5
Průchod binárním stromem cílem je postupně navštívít a zpracovat všechny uzly stromu (zpracování = výpis, vynulování...) při práci se stromy se často využívá jejich rekurzivní charakter úkol vyřešíme pomocí rekurzivní funkce, která přebírá pointer na kořen stromu K void PruchodStromem(Uzel * K) if (K == NULL) return; ZPRACUJ(K); // zpracujeme aktuální uzel PruchodStromem(K->levy); // průchod levým podstromem PruchodStromem(K->pravy); // průchod pravým podstromem Jan Hnilica Počítačové modelování 14 6
Průchod binárním stromem jsou 3 možnosti, kam umístit funkci pro zpracování uzlu: void PruchodStromem(Uzel * K) if (K == NULL) return; ZPRACUJ(K); // 1: pre-order (před průchodem podstromy) PruchodStromem(K->levy); ZPRACUJ(K); // 2: in-order (mezi průchody oběma podstromy) PruchodStromem(K->pravy); ZPRACUJ(K); // 3: post-order (po průchodu podstromy) A pořadí zpracovávaných uzlů B C pre-order: A B D E C F G in-order: D B E A F C G D E F G post-order: D E B F G C A Jan Hnilica Počítačové modelování 15 7
Binární vyhledávací strom (BVS) datová struktura k rychlému ukládání a vyhledávání údajů podle hodnoty klíče klíč = jedna z položek uzlu pro každý uzel stromu platí: všechny uzly v jeho levém podstromu mají menší hodnotu klíče všechny uzly v jeho pravém podstromu mají větší hodnotu klíče 15 10 24 7 12 17 30 1 9 13 28 obecně může strom obsahovat i více uzlů se stejným klíčem (pak by např. v levém podstromu byly uzly s klíčem klíč kořene) Jan Hnilica Počítačové modelování 14 8
Binární vyhledávací strom (BVS) Uzel pro jednoduchost bude obsahovat jedinou proměnnou typu int typedef struct uzel int hodnota; // uložená informace struct uzel *levy, *pravy; // pointery na syny Uzel; Vytvoření nového uzlu funkce vrací pointer na nově vytvořený uzel (tzn. vrací paměťovou adresu, na které uzel leží) Uzel * NovyUzel(int h) Uzel * U = (Uzel*)malloc(sizeof(Uzel)); U->hodnota = h; U->levy = U->pravy = NULL; return U; Jan Hnilica Počítačové modelování 14 9
Binární vyhledávací strom (BVS) Vyhledání hodnoty průchod od kořene k listům, v každém uzlu se podle hodnoty klíče rozhodujeme, zda pokračovat doprava či doleva (popř. zda jsme uzel už našli) neprocházíme celý strom, ale jedinou cestu od kořene k listům funkce přebírá pointer na kořen stromu a hledanou hodnotu, vrací pointer na uzel s požadovanou hodnotou, případně NULL, pokud uzel ve stromu není Uzel * Hledej(Uzel * K, int hodnota) while (K!= NULL && K->hodnota!= hodnota) if (hodnota > K->hodnota) K = K->pravy; else K = K->levy; return K; Jan Hnilica Počítačové modelování 14 10
Binární vyhledávací strom (BVS) Vyhledání hodnoty rekurzivní varianta funkce Uzel * Hledej(Uzel * K, int hodnota) if (K == NULL K->hodnota == hodnota) return K; if (hodnota > K->hodnota) return Hledej(K->pravy, hodnota); else return Hledej(K->levy, hodnota); Jan Hnilica Počítačové modelování 14 11
Binární vyhledávací strom (BVS) Přidání hodnoty hodnotu ukládáme do nového listu, přitom je potřeba zachovat strukturu podle pravidel BVS uzel s novou hodnotou připojujeme tam, kde ho pak budeme hledat průchod stromem jako při hledání, dokud nerazíme na konec větve (NULL), přitom udržujeme pointer na předka, za který nový uzel připojíme časová složitost: jeden průchod od kořene k listu (výška stromu) funkce přebírá pointer na kořen stromu odkazem (může ho změnit, pokud byl doposud NULL) void Pridej(Uzel ** K, int hodnota) // strom byl dosud prázdný if (*K == NULL) *K = NovyUzel(hodnota); return; // pracovní indexy Uzel *P = *K; // pro hledaní pozice nového uzlu Uzel *Q = *K; // předek nového uzlu Jan Hnilica Počítačové modelování 14 12
Binární vyhledávací strom (BVS) Přidání hodnoty (pokračování kódu z předchozí strany) // najdeme místo pro nový uzel while (P!= NULL) Q = P; if (hodnota == P->hodnota) return; if (hodnota > P->hodnota) P = P->pravy; else P = P->levy; // vytvoříme nový uzel a připojíme ho za Q P = NovyUzel(hodnota); if (hodnota > Q->hodnota) Q->pravy = P; else Q->levy = P; Pridej(&K, hodnota); // volání funkce v programu Jan Hnilica Počítačové modelování 14 13
Binární vyhledávací strom (BVS) Přidání hodnoty totéž jako rekurzivní funkce Uzel * PridejRek(Uzel * K, int hodnota) if (K == NULL) K = NovyUzel(hodnota); else if (hodnota > K->hodnota) K->pravy = PridejRek(K->pravy, hodnota); else if (hodnota < K->hodnota) K->levy = PridejRek(K->levy, hodnota); return K; K = Pridej(K, hodnota); // volání funkce v programu Jan Hnilica Počítačové modelování 14 14
Binární vyhledávací strom (BVS) Smazání hodnoty nejprve musíme nalézt rušený uzel a jeho předka uzel U, obsahující odstraňovanou hodnotu: a. je list smažeme ho, v jeho předkovi nastavíme příslušný pointer na NULL b. má jednoho syna smažeme ho, přepojíme jeho syna na místo smazaného uzlu (předek U pak ukazuje na syna U místo na U) c. má dva syny nemůžeme ho smazat (syny by nebylo kam zapojit) hodnotu v uzlu U nahradíme hodnotou h, což může být a) největší hodnota levého podstromu U b) nejmenší hodnota pravého podstromu U uzel obsahující h je buďto list, nebo má jednoho syna smažeme ho jedním z výše uvedených postupů Pozn. Rozmyslete si, že uvedený výběr uzlu s hodnotou h zaručuje, že strom zůstane platným BVS. časová složitost smazání hodnoty opět odpovídá výšce stromu algoritmus si zkuste napsat sami jako cvičení Jan Hnilica Počítačové modelování 14 15
Binární vyhledávací strom (BVS) Setřídění uložených hodnot průchod do hloubky metodou in-order + výpis hodnot (popř. zápis do nějakého pole) void VypisVzestupne(Uzel * K) if (K == NULL) return; VypisVzestupne(K->levy); // vypíšeme všechny menší hodnoty printf( %i,, K->hodnota); // vypíšeme hodnotu uzlu VypisVzestupne(K->pravy); // vypíšeme všechny větší hodnoty 15 10 24 7 12 17 30 výstup: 1, 7, 9, 10, 12, 13, 15, 17, 24, 28, 30, 1 9 13 28 Jan Hnilica Počítačové modelování 14 16
Binární vyhledávací strom (BVS) Výška BVS operace s BVS (hledání, přidávání a mazání) mají časovou složitost odpovídající výšce stromu výška stromu ale velmi závisí na pořadí, ve kterém hodnoty do stromu přidáváme a odebíráme (analogie quicksort a výběr pivota) Krajní varianty (pro strom obsahující n uzlů): A) zcela vyvážený strom, výška log 2 n B) zcela degenerovaný strom, výška = n lze ukázat, že pokud do stromu přidáváme a rušíme hodnoty v náhodném pořadí, výška stromu je cca 1.4 log 2 n ( pro velká n je to velmi dobrá časová složitost) Jan Hnilica Počítačové modelování 14 17
Vyvažování stromů cílem je zajistit, že operace hledání, přidávání a mazání hodnot budou mít zaručenu logaritmickou časovou složitost O(log n) Definice dokonale vyvážený binární vyhledávací strom (DV BVS) pro každý uzel platí, že počet uzlů v levém a pravém podstromu se liší max. o 1 nejlepší možné vyvážení, ale je obtížné ho udržovat během přidávání a mazání hodnot AVL strom pro každý uzel platí, že výška jeho levého a pravého podstromu se liší max. o 1 slabší forma vyvážení, ale stačí k udržení logaritmické složitosti a je mnohem snáze udržovatelná název podle autorů: Adeľson-Velskij, Landis (1962) AVL strom, který není DV BVS Pozn. Operace s AVL stromy přesahují rámec tohoto kurzu. Zájemce nalezne jejich popis např. v Kombinatorické algoritmy (L. Kučera) Algoritmy a štruktúry údajov (N. Wirth) (orig. Algorithms + data structures = programs) Jan Hnilica Počítačové modelování 14 18
Halda (heap) Definice binární strom, který splňuje následující pravidla: 1. všechny hladiny s výjimkou poslední jsou zcela zaplněny 2. všechny prvky v poslední hladině jsou umístěny co nejvíc vlevo 3. pro každý uzel platí: hodnota v něm uložená je menší než hodnoty v jeho synech z definice plyne: 1. nejmenší hodnota je v kořeni 2. výška haldy obsahující n hodnot je cca log 2 n Využití pokud potřebujeme opakovaně a rychle zjišťovat a odebírat minimum z uložených hodnot (a přitom průběžně přidávat další hodnoty) třídění haldou (heapsort) 5 Pozn. Haldu můžeme realizovat i jako maximovou (otec > syn, maximum v kořeni), tzn. pomocí stejných algoritmů lze efektivně pracovat s maximy uložených hodnot. 7 14 11 9 37 15 14 27 13 Jan Hnilica Počítačové modelování 14 19
Haldové operace Zjištění minima minimum je vždy v kořeni konstantní časová složitost O(1) Přidání hodnoty hodnotu přidáme do nového listu, ten musíme vytvořit na poslední hladině co nejvíce vlevo upravíme haldu - novou hodnotu opakovaně prohazujeme s hodnotou v předkovi, dokud je předek větší (s novou hodnotou "proskáčeme" stromem vzhůru) uvědomte si, že tato prohození neporuší uspořádání haldy (proč?) časová složitost odpovídá výšce haldy, tzn. O(log n) Odstranění minima minimum v kořeni nahradíme hodnotou z posledního uzlu, poslední uzel smažeme (poslední uzel je na spodní hladině co nejvíc vpravo) upravíme haldu - s hodnotou vloženou do kořene proskáčeme stromem dolů tzn. pokud je hodnota větší než hodnota některého ze synů, prohodíme je (pokud je větší než oba synové, prohodíme s menším z nich) opět platí, že tato prohození neporuší uspořádání haldy časová složitost opět odpovídá výšce haldy, tzn. O(log n) Jan Hnilica Počítačové modelování 14 20
Uložení haldy v poli haldu lze snadno uložit do běžného 1-rozměrného pole jednotlivé uzly indexujeme po vrstvách zleva doprava: 0 5 1 2 7 14 3 4 5 6 11 9 37 15 0 1 2 3 4 5 6 7 8 9 5 7 14 11 9 37 15 14 27 13 7 8 9 14 27 13 uzel s indexem [i] má syny na indexech [2i + 1] a [2i + 2] předka na indexu [i / 2] (pro liché i) [i / 2 + 1] (pro sudé i) Jan Hnilica Počítačové modelování 14 21
Haldové operace v poli v ukázkách budeme pracovat s haldou hodnot typu double halda bude uložena v poli o n prvcích mějme na paměti, že při použití pole je nutné vždy testovat, jestli je pro přidání dalšího prvku ještě místo, zrovna tak je potřeba ošetřit, zda neodebíráme minimum z prázdné haldy tyto testy si čtenář doplní sám, v ukázkách nebudou uvedeny z důvodu úspory místa úvodem vytvoříme funkci pro výpočet indexu předka prvku, ležícího v poli na indexu i int IndexPredka(int i) if (i % 2 == 0) return (i / 2-1); else return (i / 2); Jan Hnilica Počítačové modelování 14 22
Haldové operace v poli Přidání hodnoty do haldy do haldy přidáváme číslo c, proměnná pocet vyjadřuje aktuální počet hodnot na haldě void PridejHodnotu(double c, double halda[], int * pocet) // hodnotu uložíme na konec haldy (*pocet)++; halda[*pocet] = c; // upravíme haldu double pomocny; int umistovany = *pocet - 1; // index umísťovaneho prvku int predek = IndexPredka(umistovany); // index předka while (umistovany > 0 && halda[umistovany] < halda[predek]) // prohozeni prvků pomocny = halda[umistovany]; halda[umistovany] = halda[predek]; halda[predek] = pomocny; // posun nahoru umistovany = predek; predek = indexpredka(umistovany); Jan Hnilica Počítačové modelování 14 23
Haldové operace v poli Odstranění minima z haldy double OdeberMinimum(double halda[], int * pocet) // uložíme si odebirané minimum double min = halda[0]; // snížíme velikost haldy, poslední prvek vložíme do kořene (*pocet)--; halda[0] = halda[*pocet]; // proměnné pro úpravu haldy int umistovany = 0; // index umísťované hodnoty int syn = 2 * umistovany + 1; // index menšího syna double pomocny; pokračování kódu na další straně Jan Hnilica Počítačové modelování 14 24
Haldové operace v poli odstranění minima - pokračování kódu z předchozí strany // úprava haldy = proskákání stromem dolů while (syn < *pocet) if (syn + 1 < *pocet) // test druhého syna if (halda[syn + 1] < halda[syn]) syn++; if (halda[umistovany] > halda[syn]) // prohodit pomocny = halda[umistovany]; halda[umistovany] = halda[syn]; halda[syn] = pomocny; // posunout dolů umistovany = syn; syn = 2 * umistovany + 1; else break; // funkce vrací minimum, odebrané v úvodu z kořene return min; Jan Hnilica Počítačové modelování 14 25
Třídění haldou - heapsort Cíl: setřídit údaje uložené v poli při použití haldových operací Postup: z hodnot v poli vytvoříme haldu na počátku halda tvořena jediným prvkem (prvním prvkem vlevo) (n-1) krát přidáme prvek do haldy, přidáváme vždy první prvek zprava halda se postupně tvoří v levé části pole n-krát odebereme minimum z haldy minima odebíráme z kořene (prvek na nultém indexu) halda se postupně zmenšuje, minima ukládáme doprava, na místa uvolněná po haldě časová složitost: (n-1)-krát přidání a n-krát odebrání prvku, celkově O(n log n) paměťová složitost: třídíme přímo v poli, O(n) pozn. po provedení tohoto algoritmu jsou čísla v poli uspořádána sestupně (minima byla ukládána zprava) - jaké jsou možnosti, když chceme vzestupné uspořádání? Jan Hnilica Počítačové modelování 14 26
Obecný strom počet synů není omezen pro uložení pointerů na syny se nabízí několik variant: 1) Pole pointerů na syny použitelné, pokud předem známe maximální možný počet synů (označme ho S) typedef struct uzel int hodnota; struct uzel * synove[s]; Uzel; každý uzel alokuje paměť pro uložení S pointerů, z nichž bude u většiny uzlů využita jen část pro velké S paměťově neúnosné funkce pro průchod stromem do hloubky void Pruchod(Uzel * K) Zpracuj(K); for (int i = 0; i < S; i++) if (K->synove[i]!= NULL) Pruchod(K->synove[i]); Jan Hnilica Počítačové modelování 14 27
Obecný strom 2) Lineární seznam pointerů na syny ve stromové struktuře jsou propojeny dva typy uzlů: NULL uzel stromu uzel seznamu synů // 1. uzel stromu typedef struct uzel int hodnota; // uložená informace struct seznam * synove; // pointer na počátek seznamu synů Uzel; // 2. uzel seznamu synů typedef struct seznam Uzel * syn; // pointer na synovský uzel stromu struct seznam * dalsi; // pointer na bratra v seznamu Seznam; Jan Hnilica Počítačové modelování 14 28
Obecný strom 2) Lineární seznam pointerů na syny průchod stromem do hloubky (K je pointer na kořen stromu) void Pruchod(Uzel * K) if (K == NULL) return; Zpracuj(K); Seznam * S = K->synove; while (S!= NULL) Pruchod(S->syn); S = S->dalsi; Jan Hnilica Počítačové modelování 14 29
Obecný strom 3) Kanonická reprezentace binárním stromem uzel obsahuje dva pointery (oba typu Uzel*) 1. na prvního syna 2. na mladšího bratra listy mají pointer syn nastavený na NULL synové jednoho uzlu jsou propojeni přes pointery bratr informace syn bratr typedef struct uzel int hodnota; struct uzel *syn, *bratr; Uzel; 1 1 NULL 2 3 4 2 3 4 NULL 5 6 7 8 NULL 5 6 7 8 NULL NULL NULL NULL NULL NULL Jan Hnilica Počítačové modelování 14 30
Písmenkový strom (trie) struktura umožňující úsporné uložení slovníku (množiny řetězců) a rychlé vyhledávání slov příklad: k e m o s t o y r p u a uložená slova: n y sto strom stromek stromy strop struna struny uzly uhraničené tečkovanou čarou signalizují konec slova operace vyhledání, přidání a smazání slova složitost daná délkou slova (nikoliv délkou slovníku) Jan Hnilica Počítačové modelování 14 31
Písmenkový strom (trie) Způsob uložení pointerů na syny uzlu velmi závisí na možném počtu znaků, kterými slovo může pokračovat tuto množinu znaků označujeme jako abecedu, značíme Σ počet znaků označujeme jako tzv. velikost abecedy, značíme Σ Malé abecedy pole pointerů (indexované např. ASCII kódy znaků) výhodou je okamžitý přístup k pointeru na další znak slova O(1) struct uzel int konecslova; // 0 nebo 1 struct uzel * synove[26]; // např. pro abecedu a z (ascii 97-122) ; Rozsáhlé abecedy lineární seznam pointerů výhoda: nebudou alokovány nevyužité pointery nevýhoda: prodlouží se hledání dalšího znaku O( Σ ) binární vyhledávací strom pointerů rychlejší vyhledávání dalšího znaku O(log Σ ) Jan Hnilica Počítačové modelování 14 32