Programovanie (1) v C/C++
1-INF-127, ZS 2024/25

Úvod · Pravidlá · Prednášky · Softvér · Testovač
· Kontaktujte nás pomocou e-mailovej adresy E-prg.png (bude odpovedať ten z nás, kto má príslušnú otázku na starosti alebo kto má práve čas).
· Prosíme študentov, aby si pravidelne čítali e-maily na @uniba.sk adrese alebo aby si tieto emaily preposielali na adresu, ktorú pravidelne čítajú.


Prednáška 21: Rozdiel medzi revíziami

Z Programovanie
Skočit na navigaci Skočit na vyhledávání
 
(43 medziľahlých úprav od 2 ďalších používateľov nie je zobrazených)
Riadok 1: Riadok 1:
 
== Oznamy ==
 
== Oznamy ==
* Tretiu domácu úlohu treba odovzdať ''do 4. decembra, 22:00''.
 
* Semestrálna písomka bude ''v stredu 9. decembra o 18:10'':
 
** Po technickej stránke bude písomka realizovaná rovnako ako písomka, ktorá bola súčasťou 9. cvičení. Bude potrebné pripojiť sa cez MS Teams.
 
** Po obsahovej stránke bude písomka zameraná na učivo prebraté v rámci prvých 20 prednášok (a na overenie jeho praktického zvládnutia).
 
** Čas na riešenie písomky bude ''90 minút''.
 
** '''Na úspešné absolvovanie predmetu je potrebné získať z písomky aspoň 50% bodov.''' Neskôr bude vypísaný opravný termín písomky (písať sa bude počas skúškového obdobia).
 
** Pokročilým, ktorým boli uznané všetky tri pôvodne plánované písomky, bude záverečná písomka uznaná za 100% bodov. Ostatným pokročilým bude uznaná alikvótna časť záverečnej písomky, zvyšné príklady budú riešiť spolu s ostatnými.
 
** Ďalšie informácie o písomke možno nájsť [[Záverečná písomka|tu]].
 
* Boli stanovené predbežné termíny skúšok (všetko cez MS Teams):
 
** ''Piatok 18. decembra 2020'', začiatok o 12:00, začiatok ústnej skúšky približne o 16:00 (''predtermín s limitom 20 prihlásených študentov'').
 
** <del>''Piatok 8. januára 2021''</del> '''Štvrtok 7. januára 2021''', začiatok o 9:00, začiatok ústnej skúšky približne o 14:00.
 
** ''Piatok 22. januára 2021'', začiatok o 9:00, začiatok ústnej skúšky približne o 14:00 (hlavne opravné termíny, bez nároku na 2. opravný termín).
 
** ''Piatok 5. februára 2021'', začiatok o 9:00, začiatok ústnej skúšky približne o 14:00 (hlavne 2. opravné termíny, bez nároku na opravný termín).
 
* Prihlasovanie na predtermín je v AIS otvorené od zajtrajšieho večera. Prihlasovanie na zvyšné termíny bude otvorené o týždeň (v prípade kolízií s termínmi z iných predmetov nám dajte čím skôr vedieť). Na skúšku je potrebné prihlásiť sa najneskôr 24 hodín pred jej začiatkom.
 
* Prihlasuje sa iba na praktickú časť skúšky. Tú môžete absolvovať aj v prípade, že ste ešte nezískali 50% bodov z písomky. Ústnu skúšku budete v takom prípade skladať až po úspešnom absolvovaní opravnej písomky.
 
* Po každom termíne praktickej skúšky nasleduje ústna skúška, ktorej zloženie je podmienkou úspešného absolvovania predmetu. Na praktickú skúšku postupujú iba tí, ktorí úspešne zvládnu praktickú časť (a písomku). Úspešní riešitelia testu pre pokročilých sa ústnej skúšky zúčastniť nemusia (v takom prípade bude celá váha skúšky daná výsledkom praktickej časti).
 
* Plán prednášok a cvičení po zvyšok semestra:
 
** V pondelok 7. decembra bude bežná prednáška.
 
** V stredu 9. decembra bude prednáška venovaná informáciám ku skúškam (z programovania aj všeobecne). Účasť na tejto prednáške je silno odporúčaná.
 
** V pondelok 14. decembra bude prednáška o nepreberaných črtách jazykov C a C++ (učivo z tejto prednášky nebude vyžadované na skúške).
 
** V stredu 16. decembra prednáška nebude.
 
** Budúci týždeň budú štandardné cvičenia.
 
** V utorok 15. decembra bude v rámci cvičení &bdquo;tréning&rdquo; na skúšku.
 
** V piatok 18. decembra bude v čase doplnkových cvičení predtermín skúšky (so skorším začiatkom o 12:00).
 
* Po skončení tejto prednášky bude na testovači s predstihom zverejnená jedna z úloh 12. cvičení (zvyšné pribudnú v štandardnom čase) zameraná na binárne stromy.
 
  
== Binárne vyhľadávacie stromy ==
+
* Tretiu domácu úlohu odovzdávajte do zajtra 22:00.
 +
 
 +
Prednášky
 +
* V stredu v prvej polovici prednášky informácie k skúške a rady k skúškovému všeobecne, potom doberieme posledné učivo
 +
* Budúci pondelok nepovinná prednáška o nepreberaných črtách jazykov C a C++
 +
* Budúcu stredu 13.12. o 18:10 [[Zimný semester, semestrálny test|semestrálny test]]
 +
 
 +
Cvičenia
 +
* Zajtra bude rozcvička na papieri, potom pokračujte riešením úloh na testovači
 +
* Piatkové cvičenia nepovinné, avšak odporúčame účasť, ak chcete pomôcť s príkladmi alebo máte otázky
 +
* Budúci utorok v rámci cvičení tréning na skúšku
 +
* Na testovači už je niekoľko tréningových príkladov, za niektoré môžete dostať aj bonusový bod do cvičení, ešte dva príklady pribudnú neskôr
 +
* V piatok 15.12. od 13:10 predtermín skúšky, cvičenia nebudú
 +
 
 +
<!--
 +
Začneme dokončením učiva z [[Prednáška 20#Ak.C3.A1_m.C3.B4.C5.BEe_by.C5.A5_v.C3.BD.C5.A1ka_bin.C3.A1rneho_stromu.3F |predchádzajúcej prednášky]]
 +
-->
 +
 
 +
== Opakovanie: aritmetický výraz ako strom ==
 +
 
 +
[[Image:PROG-P21-aritm.png|thumb|right|Strom pre výraz <tt>(65 – 3 * 5)/(2 + 3)</tt>]]
 +
 
 +
Uzol takéhoto stromu môžeme reprezentovať nasledujúcou štruktúrou:
 +
 
 +
<syntaxhighlight lang="C++">
 +
struct treeNode {
 +
    // číselná hodnota (len v listoch)
 +
    double val;
 +
 
 +
    // operátor vo vnútorných uzloch, pre listy medzera
 +
    char op;
  
Budeme sa teraz zaoberať špeciálnym prípadom binárnych stromov, ktorým sú ''binárne vyhľadávacie stromy''. Tie budú ďalšou z radu dátových štruktúr, ktoré možno použiť pri implementácii dynamickej množiny ako abstraktného dátového typu.
+
    // smerníky na podstromy
 +
    treeNode * left, * right;
 +
};
 +
</syntaxhighlight>
  
[[Image:P22-BST.png|200px|right|thumb|Príklad binárneho vyhľadávacieho stromu.]]
+
* Vnútorné uzly stromu zodpovedajú operátorom
 +
* Listy zodpovedajú číselným hodnotám
  
''Binárny vyhľadávací strom'' je binárny strom, ktorého uzly majú priradené kľúče z nejakej úplne usporiadanej množiny (my budeme pre jednoduchosť uvažovať iba prípad, keď sú kľúčmi celé čísla). Pre každý uzol ''v'' s kľúčom ''key'' pritom platí: 
+
Videli sme
* Každý vrchol v ľavom podstrome uzla ''v'' má hodnotu kľúča menšiu (alebo rovnakú) ako ''key''.
+
* Štruktúru <tt>treeNode</tt>
* Každý vrchol v pravom podstrome uzla ''v'' má hodnotu kľúča väčšiu (alebo rovnakú) ako ''key''.
+
* Vytvorenie stromu z postfixového výrazu (podobné na vyhodnocovanie, využíva sa zásobník hotových podstromov)
 +
* Vyhodnotenie výrazu, výpis v postfixovej, prefixovej a infixovej forme, uvoľnenie pamäte stromu (jednoduché rekurzívne funkcie)
  
Ak teda vypíšeme kľúče jednotlivých uzlov binárneho vyhľadávacieho stromu v poradí ''inorder'', dostaneme ich postupnosť utriedenú vzostupne.
 
  
Typicky sa budeme zaujímať o prípad, keď sú kľúče jednotlivých uzlov po dvoch rôzne (nemusí to však byť vždy tak). Pre danú (multi)množinu kľúčov typicky existuje veľa rôznych binárnych vyhľadávacích stromov.
+
Zaoberali sme sa aj všeobecnými binárnymi stromami, videli sme
 +
* Terminológiu
 +
* Štruktúru <tt>node</tt>
 +
* Výpis v poradí preorder, inorder, postorder, uvoľnenie pamäte stromu (jednoduchá rekurzia, podobne ako pre výrazy)
 +
* Hĺbka vrchola a výška stromu
  
''Cvičenie'': nájdite všetky binárne vyhľadávacie stromy pozostávajúce z troch uzlov s kľúčmi ''1, 2, 3''.
+
Dokončime teraz [[Prednáška_20#Pr.C3.ADklad:_pln.C3.A9_bin.C3.A1rne_stromy|cvičenie z minulej prednášky]].
  
=== Definícia štruktúr pre binárny vyhľadávací strom a jeho uzol ===
+
== Binárne vyhľadávacie stromy ==
  
Štruktúra <tt>node</tt> pre uzol binárneho vyhľadávacieho stromu bude veľmi podobná, ako pri všeobecných binárnych stromoch. Spomedzi dát uložených v uzle je najpodstatnejší kľúč <tt>key</tt>, pričom na tejto prednáške sa obmedzíme na celočíselné kľúče. Okrem kľúča môžu byť v uzle uložené aj ďalšie, tzv. satelitné, dáta &ndash; tie však pre jednoduchosť uvažovať nebudeme. Okrem smerníkov na ľavého a pravého syna bude navyše každý uzol obsahovať aj smerník na svojho otca (v prípade koreňa bude mať hodnotu <tt>NULL</tt>).
+
[[Image:P22-BST.png|200px|right|thumb|Príklad binárneho vyhľadávacieho stromu.]]
 +
Stromy sa v informatike používajú na veľa účelov. Ďalším príkladom sú binárne vyhľadávacie stromy, ktoré môžu slúžiť na implementovanie abstraktného dátového typu dynamická množina, ktorý sme videli na [[Prednáška 14#Dynamick.C3.A1_mno.C5.BEina_2|prednáške 14]]. Prvky množiny nebudeme ukladať  do poľa, ani v spájaného zoznamu, ale do vrcholoch binárneho stromu.
 +
* V binárnom vyhľadávacom strome má každý vrchol 0, 1 alebo 2 deti
 +
* V každom vrchole máme položku s dátami (pre jednoduchosť typu <tt>int</tt>), tieto dáta niekedy voláme aj kľúč
 +
* Pre každý vrchol ''v'' stromu platí:
 +
** Každý vrchol v ľavom podstrome ''v'' má hodnotu <tt>data</tt> menšiu ako vrchol ''v''
 +
** Každý vrchol v pravom podstrome ''v'' má hodnotu <tt>data</tt> väčšiu ako vrchol ''v''
 +
* Z toho vyplýva, že ak vypíšeme strom v inorder poradí, dostaneme prvky usporiadané
 +
* Pre danú množinu kľúčov existuje veľa vyhľadávacích stromov
  
Na binárny vyhľadávací strom kladieme globálne podmienky ohľadom kľúčov jeho uzlov. V prípade &bdquo;ručnej&rdquo; manipulácie s jeho uzlami by mohlo dôjsť k narušeniu platnosti týchto podmienok (napríklad by sme mohli niektorému ľavému synovi priradiť väčší kľúč, než má jeho otec). Aby sme predišli takýmto problémom, definujeme okrem štruktúry <tt>node</tt> pre jednotlivé uzly aj štruktúru <tt>binarySearchTree</tt> realizujúcu &bdquo;obal&rdquo; pre celý binárny vyhľadávací strom. Následne definujeme niekoľko funkcií na prácu s binárnymi vyhľadávacími stromami prostredníctvom štruktúry <tt>binarySearchTree</tt>. Používateľ, ktorý bude na prácu s binárnymi vyhľadávacími stromami používať výhradne tieto funkcie, by nikdy nemal mať možnosť porušiť podmienky platné v binárnych vyhľadávacích stromoch.  
+
''Cvičenie'': nájdite všetky binárne vyhľadávacie stromy pozostávajúce z troch uzlov s kľúčmi ''1, 2, 3''.
  
 +
===Definícia dátových štruktúr===
 +
Vrchol typu <tt>node</tt>
 +
* Položka <tt>data</tt> typu <tt>int</tt>
 +
* Smerník na ľavé a pravé dieťa
 +
* Na niektoré úlohy (napr. mazanie vrcholu) sa hodí aj smerník na rodiča (ten má hodnotu <tt>NULL</tt> v koreni)
 +
Celý strom je štruktúra obsahujúca iba smerník na koreň
 +
* Pre prázdny strom je to <tt>NULL</tt>.
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
/* Uzol binarneho vyhladavacieho stromu. */
 
 
struct node {
 
struct node {
     int key;      // kluc, podla ktoreho budeme porovnavat prvky (namiesto int aj ina uplne usporiadana mnozina)
+
     /* vrchol binárneho vyhľadávacieho stromu  */
   
+
     int data;      /* hodnota uložená vo vrchole */
     /* Sem mozu prist lubovolne dalsie satelitne data ulozene v danom uzle. */
+
     node * parent;  /* rodič vrchola, NULL v koreni */
   
+
     node * left;    /* ľavé dieťa, NULL ak neexistuje */
     node *parent;  // smernik na otca (NULL, ak neexistuje)
+
     node * right;  /* pravé dieťa, NULL ak neexistuje */
     node *left;    // smernik na laveho syna (NULL, ak tento syn neexistuje)
 
     node *right;  // smernik na praveho syna (NULL, ak tento syn neexistuje)
 
 
};
 
};
  
 
+
/* Samotná dynamická množina (obal pre používateľa). */
// ...
+
struct set {
 
+
     node *root; /* koreň stromu, NULL pre prázdny strom */
 
 
/* Samotna struktura binarneho vyhladavacieho stromu (obal pre pouzivatela). */
 
struct binarySearchTree {
 
     node *root;
 
 
};
 
};
 
</syntaxhighlight>
 
</syntaxhighlight>
  
=== Inicializácia binárneho vyhľadávacieho stromu ===
+
Inicializácia prázdneho binárneho vyhľadávacieho stromu
 
 
Nasledujúca funkcia realizuje inicializáciu binárneho vyhľadávacieho stromu <tt>t</tt>.
 
 
 
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
/* Inicializuje prazdny binarny vyhladavaci strom. */
+
/* Funkcia inicializuje prázdny binárny vyhľadávací strom */
void bstInit(binarySearchTree &t) {
+
void init(set &s) {
     t.root = NULL;
+
     s.root = NULL;
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
Riadok 83: Riadok 101:
 
=== Likvidácia binárneho vyhľadávacieho stromu ===
 
=== Likvidácia binárneho vyhľadávacieho stromu ===
  
Likvidáciu podstromu zakoreneného v danom uzle <tt>*root</tt> realizujeme funkciou <tt>destroy</tt>, obdobne ako pri všeobecných binárnych stromoch. Používateľovi navyše dáme k dispozícii aj &bdquo;baliacu&rdquo; funkciu <tt>bstDestroy</tt>, ktorá zlikviduje binárny vyhľadávací strom <tt>t</tt> tak, že zavolá funkciu <tt>destroy</tt> na jeho koreň.
+
Likvidáciu podstromu zakoreneného v danom uzle <tt>root</tt> realizujeme funkciou <tt>destroy</tt>, obdobne ako pri všeobecných binárnych stromoch. Používateľovi navyše dáme k dispozícii aj funkciu <tt>destroy</tt>, ktorá dostane množinu implementovanú ako strom a tento strom zlikviduje.
  
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
Riadok 95: Riadok 113:
 
}
 
}
  
 
+
/* Zlikviduje množinu s (uvoľní pamäť). */
// ...
+
void destroy(set &s) {
 
+
     destroy(s.root);
 
 
/* Zlikviduje strom t (uvolni pamat). */
 
void bstDestroy(binarySearchTree &t) {
 
     destroy(t.root);
 
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
Riadok 107: Riadok 121:
 
=== Hľadanie v binárnom vyhľadávacom strome ===
 
=== Hľadanie v binárnom vyhľadávacom strome ===
  
Nasledujúca funkcia <tt>findNode</tt> sa pokúsi v podstrome zakorenenom v uzle <tt>*root</tt> vyhľadať uzol, ktorého kľúč je rovný <tt>key</tt>. Ak existuje aspoň jeden taký uzol, vráti smerník na niektorý z nich (to je užitočné najmä v prípade, keď sú kľúče po dvoch rôzne). V opačnom prípade vráti <tt>NULL</tt>.
+
Funkcia <tt>findNode</tt> v podstrome s koreňom <tt>root</tt> hľadá uzol, ktorého položka dáta obsahuje hľadaný kľúč <tt>key</tt>. Vráti takýto uzol alebo NULL ak neexistuje
 +
* Najskôr porovná hľadaný kľúč s dátami v koreni
 +
** Ak sa rovnajú, končíme (našli sme, čo hľadáme)
 +
** Ak je hľadaná hodnota menšia ako dáta v koreni, musí byť v ľavom podstrome, ak je väčšia v pravom
 +
* V príslušnom podstrome sa rozhodujeme podľa tých istých pravidiel
 +
* Keď narazíme na prázdny podstrom, dáta sa v strome nenachádzajú
 +
* Dá sa zapísať rekurzívne alebo cyklom, lebo vždy ideme iba do jedného podstromu
  
Pri hľadaní uzla s hodnotou <tt>key</tt> bude funkcia <tt>findNode</tt> využívať definujúcu vlastnosť binárnych vyhľadávacích stromov: ak je hľadaná hodnota kľúča <tt>key</tt> menšia, než kľúč koreňa podstromu <tt>*root</tt>, pokračuje v hľadaní v jeho ľavom podstrome; ak je naopak väčšia, pokračuje v hľadaní v jeho pravom podstrome. Ak je kľúč koreňa <tt>*root</tt> rovný <tt>key</tt>, ide o hľadaný uzol a smerník naň tak možno ihneď vrátiť na výstupe.
+
Funkcia <tt>contains</tt> zavolá funkciu <tt>findNode</tt> pre koreň daného binárneho vyhľadávacieho stromu <tt>t</tt> a pomocou nej zistí, či tento strom obsahuje uzol s kľúčom <tt>key</tt>.
  
Používateľovi pritom opäť poskytneme aj &bdquo;baliacu&rdquo; funkciu <tt>bstFind</tt>, ktorá zavolá funkciu <tt>findNode</tt> pre koreň daného binárneho vyhľadávacieho stromu <tt>t</tt> a pomocou nej zistí, či tento strom obsahuje uzol s kľúčom <tt>key</tt>.
+
<syntaxhighlight lang="C++">
 +
/* Ak v strome s korenom root existuje uzol s klucom key,  
 +
* vrati ho na vystupe. Inak vrati NULL. */
 +
node * findNode(node *root, int key) {
 +
    node * v = root;
 +
    while (v != NULL && v->data != key) {
 +
        if (key < v->data) {
 +
            v = v->left;
 +
        } else {
 +
            v = v->right;
 +
        }
 +
    }
 +
    return v;
 +
}
  
<syntaxhighlight lang="C++">
+
/* Rekurzívna verzia */
/* Ak v strome s korenom *root existuje uzol s klucom key, vrati ho na vystupe. Inak vrati NULL. */
+
node *findNodeR(node *root, int key) {
node *findNode(node *root, int key) {
+
     if (root == NULL || key == root->data) {
     if (root == NULL || root->key == key) {
 
 
         return root;
 
         return root;
     } else if (key < root->key) {
+
     } else if (key < root->data) {
         return findNode(root->left, key);
+
         return findNodeR(root->left, key);
     } else {
+
     } else { // key > root->data
         return findNode(root->right, key);
+
         return findNodeR(root->right, key);
 
     }
 
     }
 
}
 
}
  
 
+
/* Zisti, ci strom reprezentujuci mnozinu s
// ...
+
* obsahuje uzol s klucom key. */
 
+
bool contains(set &s, int key) {
 
+
     return findNode(s.root, key) != NULL;
/* Zisti, ci strom t obsahuje uzol s klucom key. */
 
bool bstFind(binarySearchTree &t, int key) {
 
     return findNode(t.root, key) != NULL;
 
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Čas výpočtu je v najhoršom prípade úmerný výške stromu. Poznamenajme ešte, že funkciu <tt>findNode</tt> je možné realizovať aj nerekurzívne, napríklad takto:
+
Čas výpočtu je v najhoršom prípade úmerný výške stromu.
  
<syntaxhighlight lang="C++">
 
node *findNode(node *root, int key) {
 
    node *v = root;
 
    while (v != NULL && v->key != key) {
 
        if (key < v->key) {
 
            v = v->left;
 
        } else {
 
            v = v->right;
 
        }
 
    }
 
    return v;
 
}
 
</syntaxhighlight>
 
  
 
=== Vkladanie do binárneho vyhľadávacieho stromu ===
 
=== Vkladanie do binárneho vyhľadávacieho stromu ===
  
Nasledujúca funkcia <tt>insertNode</tt> vloží uzol <tt>*v</tt> na správne miesto podstromu zakoreneného v <tt>*root</tt> ako jeho list. Postupuje pritom rekurzívne: ak zistí, že uzol <tt>*v</tt> má kľúč menší, než <tt>*root</tt>, pokúsi sa ho vložiť do ľavého podstromu uzla <tt>*root</tt>; v opačnom prípade sa ho pokúsi vložiť do pravého podstromu.
+
Nasledujúca funkcia <tt>insertNode</tt> vloží uzol <tt>*v</tt> na správne miesto podstromu zakoreneného v <tt>*root</tt> ako jeho list.  
 
+
* Predpokladáme, že prvok v strome nie je.
Používateľovi následne poskytneme &bdquo;baliacu&rdquo; funkciu <tt>bstInsert</tt>, ktorá vytvorí uzol s daným kľúčom <tt>key</tt> a pomocou funkcie <tt>insertNode</tt> ho vloží do binárneho vyhľadávacieho stromu <tt>t</tt>.
+
* Putujeme po strome podobne ako pri vyhľadávaní prvku, až kým nenarazíme na nulový smerník.
 +
** Na tomto mieste by mal byť nový prvok, takže ho tam pridáme ako nový list
 +
** Uvádzame rekurzívnu verziu, ale dá sa aj cyklom, podobne ako pri hľadaní
 +
* Funkcia <tt>add</tt> vytvorí uzol s daným kľúčom <tt>key</tt> a pomocou funkcie <tt>insertNode</tt> ho vloží do binárneho vyhľadávacieho stromu.
  
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
/* Vlozi uzol *v na spravne miesto podstromu zakoreneneho v *root */
+
/* Vloží uzol v na správne miesto podstromu zakoreneného v root */
 
void insertNode(node *root, node *v) {
 
void insertNode(node *root, node *v) {
 
     assert(root != NULL && v != NULL);
 
     assert(root != NULL && v != NULL);
     if (v->key < root->key) {
+
     if (v->data < root->data) {
 
         if (root->left == NULL) {
 
         if (root->left == NULL) {
 
             root->left = v;
 
             root->left = v;
Riadok 178: Riadok 197:
 
}
 
}
  
 
+
/* Vloží do stromu pre mnozinu s nový uzol s kľúčom key. */
// ...
+
void add(set &s, int key) {
 
 
 
 
/* Vlozi do stromu t novy uzol s klucom key. */
 
void bstInsert(binarySearchTree &t, int key) {
 
 
     node *v = new node;
 
     node *v = new node;
     v->key = key;
+
     v->data = key;
 
     v->left = NULL;
 
     v->left = NULL;
 
     v->right = NULL;
 
     v->right = NULL;
 
     v->parent = NULL;
 
     v->parent = NULL;
     if (t.root == NULL) {
+
     if (s.root == NULL) {
         t.root = v;
+
         // specialny pripad, ked vkladame prvy uzol
 +
        s.root = v;
 
     } else {
 
     } else {
         insertNode(t.root, v);
+
         insertNode(s.root, v);
 
     }
 
     }
 
}
 
}
Riadok 199: Riadok 215:
 
Čas vkladania je tiež v najhoršom prípade úmerný hĺbke stromu.
 
Čas vkladania je tiež v najhoršom prípade úmerný hĺbke stromu.
  
''Cvičenie č. 1'': napíšte nerekurzívny variant funkcie <tt>insertNode</tt>.
+
'''Cvičenia'''
 
+
* Ako bude vyzerať strom po nasledujúcej postupnosti operácií?
''Cvičenie č. 2'': napíšte funkciu <tt>treeSort</tt>, ktorá z poľa celých čísel <tt>a</tt> pomocou volaní funkcie <tt>bstInsert</tt> vytvorí binárny vyhľadávací strom a následne pomocou prehľadávania tohto stromu v poradí ''inorder'' pole <tt>a</tt> utriedi.
+
<pre>
 
+
    set s;
=== Minimálny uzol ===
+
    init(s);
 +
    add(s, 2);
 +
    add(s, 5);
 +
    add(s, 3);
 +
    add(s, 10);
 +
    add(s, 7); 
 +
</pre>
 +
* Napíšte nerekurzívny variant funkcie <tt>insertNode</tt>.
 +
* Do funkcie doplňte <tt>assert</tt>, ktorý deteguje prípad, že vkladaná hodnota sa už v strome nachádza.
 +
* Napíšte funkciu <tt>treeSort</tt>, ktorá z poľa celých čísel <tt>a</tt> pomocou volaní funkcie <tt>add</tt> vytvorí binárny vyhľadávací strom a následne pomocou prehľadávania tohto stromu v poradí ''inorder'' pole <tt>a</tt> utriedi.
  
Nasledujúca funkcia <tt>minNode</tt> nájde v podstrome zakorenenom v <tt>*root</tt> uzol s minimálnym kľúčom. Je pritom založená na skutočnosti, že všetky uzly tohto podstromu s kľúčom menším ako <tt>root->key</tt> sa musia nachádzať v ľavom podstrome uzla <tt>*root</tt>.
+
===Minimum a následník===
 +
Uvedieme teraz dve funkcie, ktoré sa zídu pri mazaní prvku zo stromu, ale môžu sa zísť aj inokedy.
  
&bdquo;Obalom&rdquo; pre používateľa bude funkcia <tt>bstMin</tt>, ktorá pomocou funkcie <tt>minNode</tt> nájde minimálny kľúč v danom binárnom vyhľadávacom strome <tt>t</tt>.
+
Prvá funkcia <tt>minNode</tt> nájde vo vyhľadávacom strome uzol, v ktorom je uložená najmenšia hodnota.
 +
* Všetky prvky menšie ako koreň sú v ľavom podstrome, bude tam zrejme aj minimum.
 +
* Tá istá úvaha platí pre koreň ľavého podstromu.
 +
* Ideme teda doľava kým sa dá, posledný vrchol vrátime (list alebo vrchol, ktorý má iba pravé dieťa).
 +
* Nie je treba teda prechádzať celý strom a nemusíme sa ani pozerať na položku data v uzloch.
 +
* Dá sa napísať cyklom aj rekurzívne.
 +
* Obalom pre používateľa bude funkcia <tt>min</tt>, ktorá pomocou funkcie <tt>minNode</tt> nájde minimálny kľúč v danej množine.
  
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
/* Vrati (niektory) uzol s minimalnou hodnotou key v podstrome s korenom *root. */
+
/* Vrati uzol s minimalnou hodnotou data v podstrome s korenom v. */
node *minNode(node *root) {
+
node *minNode(node *v) {
     assert(root != NULL);
+
     assert(v != NULL);
     if (root->left != NULL) {
+
     while (v->left != NULL) {
         return minNode(root->left);
+
         v = v->left;
    } else {
 
        return root;
 
 
     }
 
     }
 +
    return v;
 
}
 
}
  
 
+
/* Vrati minimalnu hodnotu v mnozine s. */
// ...
+
int min(set &s) {
 
+
     assert(s.root != NULL);
 
+
     return minNode(s.root)->data;  
/* Vrati minimalny kluc uzla v strome t. */
 
int bstMin(binarySearchTree &t) {
 
     assert(t.root != NULL);
 
     return minNode(t.root)->key;  
 
 
}  
 
}  
 
</syntaxhighlight>
 
</syntaxhighlight>
  
''Cvičenie'': napíšte nerekurzívny variant funkcie <tt>minNode</tt>.
+
''Cvičenia'':  
<!--=== Príklad programu pracujúceho s binárnymi vyhľadávacími stromami ===
+
* Napíšte rekurzívny variant funkcie <tt>minNode</tt>.
 
+
* Ako by bolo treba funkciu zmeniť, aby hľadala maximum?
Nasledujúci program realizuje základné operácie s binárnymi vyhľadávacími stromami podľa príkazov zadávaných používateľom na konzolu.
 
 
 
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
using namespace std;
 
 
 
 
 
// ...
 
 
 
 
 
int main(void) {
 
    binarySearchTree t;
 
    bstInit(t);
 
    char command[20];
 
    int key;
 
    while (true) {
 
        scanf("%19s", command);
 
        if (strcmp(command, "insert") == 0) {
 
            scanf("%d", &key);
 
            bstInsert(t, key);
 
        }
 
        if (strcmp(command, "find") == 0) {
 
            scanf("%d", &key);
 
            bool b = bstFind(t, key);
 
            if (b) {
 
                printf("YES\n");
 
            } else {
 
                printf("NO\n");
 
            }
 
        }
 
        if (strcmp(command, "min") == 0) {
 
            printf("%d\n", bstMin(t));
 
        }
 
        if (strcmp(command, "exit") == 0) {
 
            break;
 
        }
 
    }
 
    bstDestroy(t);                   
 
    return 0;
 
}
 
</syntaxhighlight>-->
 
  
=== Následník uzla ===
 
  
Funkcia <tt>successorNode</tt> nájde pre daný uzol <tt>*v</tt> jeho ''následníka'' (angl. ''successor'') v binárnom vyhľadávacom strome &ndash; čiže uzol, ktorý vo vzostupnom poradí podľa kľúčov nasleduje bezprostredne za uzlom <tt>*v</tt>. Je pritom založená na nasledujúcich pozorovaniach:
+
Funkcia <tt>successorNode</tt> nájde pre daný uzol <tt>v</tt> jeho ''následníka'' (angl. ''successor'') v binárnom vyhľadávacom strome, čiže uzol, ktorý vo vzostupnom poradí podľa kľúčov nasleduje bezprostredne za uzlom <tt>v</tt>.  
* Ak má uzol <tt>*v</tt> pravého syna, následník uzla <tt>*v</tt> musí byť v jeho pravom podstrome &ndash; konkrétne pôjde o minimálny uzol z tohto podstromu.
+
* Ak má uzol <tt>v</tt> pravé dieťa, následník uzla <tt>v</tt> bude vrchol s minimálnou hodnotou data v pravom podstrome
* V opačnom prípade môže byť následníkom uzla <tt>*v</tt> jeho otec (ak <tt>*v</tt> je jeho ľavý syn). Ak je <tt>*v</tt> pravým synom svojho otca, môže to byť aj jeho starý otec (ak je otec uzla <tt>*v</tt> ľavým synom tohto starého otca), atď. Vo všeobecnosti teda ide o najbližšieho predka uzla <tt>*v</tt> takého, že <tt>*v</tt> patrí do jeho ľavého podstromu.
+
* V opačnom prípade môže byť následníkom uzla <tt>v</tt> jeho rodič, ak <tt>v</tt> je jeho ľavé dieťa.  
* V strome existuje práve jeden uzol bez následníka (jeden spomedzi najväčších prvkov).
+
* Ak je <tt>v</tt> pravým dieťaťom svojho rodiča, môže to byť jeho prarodič (ak je rodič uzla <tt>v</tt> ľavým dieťaťom tohto prarodiča), atď.  
 +
* Vo všeobecnosti teda ide o najbližšieho predka uzla <tt>v</tt> takého, že <tt>v</tt> patrí do jeho ľavého podstromu.
 +
* V strome existuje práve jeden uzol bez následníka (najväčší prvok).
 +
** Ako presne sa bude funkcia nižšie pre tento prvok správať?
  
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
/* Vrati uzol, ktory vo vzostupnom poradi uzlov podla klucov nasleduje za *v. Ak taky uzol neexistuje, vrati NULL. */
+
/* Vrati uzol, ktory vo vzostupnom poradi uzlov podla  
 +
* klucov nasleduje za v.  
 +
* Ak taky uzol neexistuje, vrati NULL. */
 
node *successorNode(node *v) {
 
node *successorNode(node *v) {
 
     assert(v != NULL);
 
     assert(v != NULL);
Riadok 299: Riadok 289:
 
=== Mazanie z binárneho vyhľadávacieho stromu ===
 
=== Mazanie z binárneho vyhľadávacieho stromu ===
  
Nasledujúca funkcia <tt>bstRemove</tt> zmaže z binárneho vyhľadávacieho stromu <tt>t</tt> práve jeden uzol s kľúčom <tt>key</tt> (ak sa taký uzol v strome vyskytuje). Pracuje tak, že najprv pomocou funkcie <tt>findNode</tt> nájde uzol <tt>*v</tt> s kľúčom <tt>key</tt>. V prípade úspechu zistí počet synov uzla <tt>*v</tt>. Ak totiž <tt>*v</tt> nemá žiadneho syna alebo má len jedného syna, možno ho zo stromu <tt>t</tt> zmazať jednoducho tak, že sa prípadný syn uzla <tt>*v</tt> stane synom otca uzla <tt>*v</tt>. V prípade, že má <tt>*v</tt> dvoch synov je však zrejmé, že jeho následník sa musí nachádzať v jeho neprázdnom pravom podstrome. Tento následník  <tt>*rm</tt> navyše nemôže mať ľavého syna. Odstránenie kľúča <tt>key</tt> je teda možné realizovať tak, že sa kľúč uzla <tt>*rm</tt> presunie do uzla <tt>*v</tt> a následne sa odstráni uzol <tt>*rm</tt> tak, ako je popísané vyššie.
+
Nasledujúca funkcia <tt>remove</tt> zmaže z binárneho vyhľadávacieho stromu uzol s kľúčom <tt>key</tt> (ak sa taký uzol v strome vyskytuje).  
 +
 
 +
* Najprv pomocou funkcie <tt>findNode</tt> nájde uzol <tt>v</tt> s kľúčom <tt>key</tt>.
 +
* Ak je ''v'' list, jednoducho ho zmažeme.
 +
* Ak má ''v'' jedno dieťa, toto dieťa prevesíme priamo pod rodiča ''v'' a ''v'' zmažeme.
 +
* Ak má ''v'' dve deti, nájdeme nasledovníka ''v'', t.j. minimum v pravom podstrome ''v''.
 +
* Tento nasledovník nemá ľavé dieťa, vieme ho teda zmazať.
 +
* Jeho údaje presunieme do vrcholu ''v''.
 +
* Tiež treba dať pozor na mazanie koreňa.
  
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
/* Zmaze zo stromu t prave jeden uzol s klucom key (ak tam taky je). */
+
/* Zmaze zo stromu pre mnozinu s uzol s klucom key, ak tam taky je. */
void bstRemove(binarySearchTree &t, int key) {
+
void remove(set &s, int key) {
     node *v = findNode(t.root, key);                  // Najde uzol v s hodnotou, ktoru treba vymazat.
+
    // Najde uzol s hodnotou, ktoru treba vymazat.
 +
     node *v = findNode(s.root, key);                   
 
     if (v == NULL) {
 
     if (v == NULL) {
 +
        // pozadovany uzol neexistuje
 
         return;
 
         return;
 
     }
 
     }
 
      
 
      
     node *rm;                                          // Najde uzol *rm stromu t, ktory sa napokon realne zmaze.   
+
     // Najde uzol *rm, ktory sa napokon realne zmaze.   
 +
    node *rm;                                         
 
     if (v->left == NULL || v->right == NULL) {         
 
     if (v->left == NULL || v->right == NULL) {         
 
         rm = v;
 
         rm = v;
 
     } else  {
 
     } else  {
 
         rm = successorNode(v);
 
         rm = successorNode(v);
 +
        // Presunie kluc uzla *rm do uzla *v.
 +
        v->data = rm->data;
 
     }
 
     }
 
+
 
     if (rm != v) {                                    // Ak rm != v, presunie kluc uzla *rm do uzla *v.
+
     // ak ma uzol rm dieta, jeho rodicom bude rodic rm
        v->key = rm->key;
+
     node *child;                                    
    }
 
   
 
     node *child = NULL;                               // Zmaze uzol *rm a uvolni pamat alokovanu pre tento uzol.
 
 
     if (rm->left != NULL) {
 
     if (rm->left != NULL) {
 
         child = rm->left;
 
         child = rm->left;
 
     } else {
 
     } else {
 
         child = rm->right;
 
         child = rm->right;
     }                  
+
     }
 
     if (child != NULL) {
 
     if (child != NULL) {
 
         child->parent = rm->parent;
 
         child->parent = rm->parent;
 
     }
 
     }
 
     if (rm->parent == NULL) {
 
     if (rm->parent == NULL) {
         t.root = child;
+
         s.root = child;
 
     } else if (rm == rm->parent->left) {
 
     } else if (rm == rm->parent->left) {
 
         rm->parent->left = child;     
 
         rm->parent->left = child;     
Riadok 336: Riadok 336:
 
         rm->parent->right = child;
 
         rm->parent->right = child;
 
     }
 
     }
 +
    // rm uz nie je v strome, uvolnime jeho pamat
 
     delete rm;
 
     delete rm;
 
}
 
}
Riadok 342: Riadok 343:
 
=== Zložitosť jednotlivých operácií ===
 
=== Zložitosť jednotlivých operácií ===
  
* Časová zložitosť operácií <tt>bstFind(t)</tt>, <tt>bstInsert(t)</tt> aj <tt>bstRemove(t)</tt> je úmerná hodnote ''height(<tt>t</tt>)'', čo je výška stromu <tt>t</tt>.  
+
* Časová zložitosť operácií <tt>contains</tt>, <tt>add</tt> aj <tt>remove</tt> je úmerná výške stromu, ktorú označíme <tt>h</tt>.  
* Minule sme ukázali, že pre výšku ''h'' stromu s ''n'' vrcholmi je log<sub>2</sub>''(n+1)-1 &le; h &le; n-1''.
+
* Minule sme ukázali, že pre strom s ''n'' uzlami máme log<sub>2</sub>''(n+1)-1 &le; h &le; n-1''.
 
* Zložitosť uvedených operácií je teda v najhoršom prípade lineárna od počtu uzlov stromu (tento prípad nastane, ak prvky vkladáme od najmenšieho po najväčší alebo naopak).
 
* Zložitosť uvedených operácií je teda v najhoršom prípade lineárna od počtu uzlov stromu (tento prípad nastane, ak prvky vkladáme od najmenšieho po najväčší alebo naopak).
* Dá sa však ukázať, že v priemernom prípade je ich zložitosť rádovo logaritmická od počtu uzlov.
+
* Dá sa však ukázať, že ak sa prvky vkladajú v náhodnom poradí, výška stromu bude v priemere logaritmická od počtu uzlov.
 
* Na predmete ''Algoritmy a dátové štruktúry'' (druhý ročník) sa tieto tvrdenia dokazujú poriadne a preberajú sa tam aj varianty vyhľadávacích stromov, pre ktoré je zložitosť uvedených operácií logaritmická aj v najhoršom prípade.
 
* Na predmete ''Algoritmy a dátové štruktúry'' (druhý ročník) sa tieto tvrdenia dokazujú poriadne a preberajú sa tam aj varianty vyhľadávacích stromov, pre ktoré je zložitosť uvedených operácií logaritmická aj v najhoršom prípade.
 
=== Príklad programu pracujúceho s binárnymi vyhľadávacími stromami ===
 
 
Nasledujúci program realizuje základné operácie s binárnymi vyhľadávacími stromami podľa príkazov zadávaných používateľom na konzolu.
 
 
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstring>
 
#include <cassert>
 
using namespace std;
 
 
 
// ...
 
 
 
int main(void) {
 
    binarySearchTree t;
 
    bstInit(t);
 
    char command[20];
 
    int key;
 
    while (true) {
 
        scanf("%19s", command);
 
        if (strcmp(command, "insert") == 0) {
 
            scanf("%d", &key);
 
            bstInsert(t, key);
 
        }
 
        if (strcmp(command, "remove") == 0) {
 
            scanf("%d", &key);
 
            bstRemove(t, key);
 
        }
 
        if (strcmp(command, "find") == 0) {
 
            scanf("%d", &key);
 
            bool b = bstFind(t, key);
 
            if (b) {
 
                printf("YES\n");
 
            } else {
 
                printf("NO\n");
 
            }
 
        }
 
        if (strcmp(command, "min") == 0) {
 
            printf("%d\n", bstMin(t));
 
        }
 
        if (strcmp(command, "exit") == 0) {
 
            break;
 
        }
 
    }
 
    bstDestroy(t);                   
 
    return 0;
 
}
 
</syntaxhighlight>
 

Aktuálna revízia z 09:20, 4. december 2023

Oznamy

  • Tretiu domácu úlohu odovzdávajte do zajtra 22:00.

Prednášky

  • V stredu v prvej polovici prednášky informácie k skúške a rady k skúškovému všeobecne, potom doberieme posledné učivo
  • Budúci pondelok nepovinná prednáška o nepreberaných črtách jazykov C a C++
  • Budúcu stredu 13.12. o 18:10 semestrálny test

Cvičenia

  • Zajtra bude rozcvička na papieri, potom pokračujte riešením úloh na testovači
  • Piatkové cvičenia nepovinné, avšak odporúčame účasť, ak chcete pomôcť s príkladmi alebo máte otázky
  • Budúci utorok v rámci cvičení tréning na skúšku
  • Na testovači už je niekoľko tréningových príkladov, za niektoré môžete dostať aj bonusový bod do cvičení, ešte dva príklady pribudnú neskôr
  • V piatok 15.12. od 13:10 predtermín skúšky, cvičenia nebudú


Opakovanie: aritmetický výraz ako strom

Strom pre výraz (65 – 3 * 5)/(2 + 3)

Uzol takéhoto stromu môžeme reprezentovať nasledujúcou štruktúrou:

struct treeNode {
    // číselná hodnota (len v listoch)
    double val;

    // operátor vo vnútorných uzloch, pre listy medzera
    char op;

    // smerníky na podstromy
    treeNode * left, * right;
};
  • Vnútorné uzly stromu zodpovedajú operátorom
  • Listy zodpovedajú číselným hodnotám

Videli sme

  • Štruktúru treeNode
  • Vytvorenie stromu z postfixového výrazu (podobné na vyhodnocovanie, využíva sa zásobník hotových podstromov)
  • Vyhodnotenie výrazu, výpis v postfixovej, prefixovej a infixovej forme, uvoľnenie pamäte stromu (jednoduché rekurzívne funkcie)


Zaoberali sme sa aj všeobecnými binárnymi stromami, videli sme

  • Terminológiu
  • Štruktúru node
  • Výpis v poradí preorder, inorder, postorder, uvoľnenie pamäte stromu (jednoduchá rekurzia, podobne ako pre výrazy)
  • Hĺbka vrchola a výška stromu

Dokončime teraz cvičenie z minulej prednášky.

Binárne vyhľadávacie stromy

Príklad binárneho vyhľadávacieho stromu.

Stromy sa v informatike používajú na veľa účelov. Ďalším príkladom sú binárne vyhľadávacie stromy, ktoré môžu slúžiť na implementovanie abstraktného dátového typu dynamická množina, ktorý sme videli na prednáške 14. Prvky množiny nebudeme ukladať do poľa, ani v spájaného zoznamu, ale do vrcholoch binárneho stromu.

  • V binárnom vyhľadávacom strome má každý vrchol 0, 1 alebo 2 deti
  • V každom vrchole máme položku s dátami (pre jednoduchosť typu int), tieto dáta niekedy voláme aj kľúč
  • Pre každý vrchol v stromu platí:
    • Každý vrchol v ľavom podstrome v má hodnotu data menšiu ako vrchol v
    • Každý vrchol v pravom podstrome v má hodnotu data väčšiu ako vrchol v
  • Z toho vyplýva, že ak vypíšeme strom v inorder poradí, dostaneme prvky usporiadané
  • Pre danú množinu kľúčov existuje veľa vyhľadávacích stromov

Cvičenie: nájdite všetky binárne vyhľadávacie stromy pozostávajúce z troch uzlov s kľúčmi 1, 2, 3.

Definícia dátových štruktúr

Vrchol typu node

  • Položka data typu int
  • Smerník na ľavé a pravé dieťa
  • Na niektoré úlohy (napr. mazanie vrcholu) sa hodí aj smerník na rodiča (ten má hodnotu NULL v koreni)

Celý strom je štruktúra obsahujúca iba smerník na koreň

  • Pre prázdny strom je to NULL.
struct node {
    /* vrchol binárneho vyhľadávacieho stromu  */
    int data;       /* hodnota uložená vo vrchole */
    node * parent;  /* rodič vrchola, NULL v koreni */
    node * left;    /* ľavé dieťa, NULL ak neexistuje */
    node * right;   /* pravé dieťa, NULL ak neexistuje */
};

/* Samotná dynamická množina (obal pre používateľa). */
struct set {
    node *root;  /* koreň stromu, NULL pre prázdny strom */
};

Inicializácia prázdneho binárneho vyhľadávacieho stromu

/* Funkcia inicializuje prázdny binárny vyhľadávací strom */
void init(set &s) {
    s.root = NULL;
}

Likvidácia binárneho vyhľadávacieho stromu

Likvidáciu podstromu zakoreneného v danom uzle root realizujeme funkciou destroy, obdobne ako pri všeobecných binárnych stromoch. Používateľovi navyše dáme k dispozícii aj funkciu destroy, ktorá dostane množinu implementovanú ako strom a tento strom zlikviduje.

/* Uvolni pamat pre podstrom s korenom *root. */
void destroy(node *root) {
    if (root != NULL) {
        destroy(root->left);
        destroy(root->right);
        delete root;
    }
}

/* Zlikviduje množinu s (uvoľní pamäť). */
void destroy(set &s) {
    destroy(s.root);
}

Hľadanie v binárnom vyhľadávacom strome

Funkcia findNode v podstrome s koreňom root hľadá uzol, ktorého položka dáta obsahuje hľadaný kľúč key. Vráti takýto uzol alebo NULL ak neexistuje

  • Najskôr porovná hľadaný kľúč s dátami v koreni
    • Ak sa rovnajú, končíme (našli sme, čo hľadáme)
    • Ak je hľadaná hodnota menšia ako dáta v koreni, musí byť v ľavom podstrome, ak je väčšia v pravom
  • V príslušnom podstrome sa rozhodujeme podľa tých istých pravidiel
  • Keď narazíme na prázdny podstrom, dáta sa v strome nenachádzajú
  • Dá sa zapísať rekurzívne alebo cyklom, lebo vždy ideme iba do jedného podstromu

Funkcia contains zavolá funkciu findNode pre koreň daného binárneho vyhľadávacieho stromu t a pomocou nej zistí, či tento strom obsahuje uzol s kľúčom key.

/* Ak v strome s korenom root existuje uzol s klucom key, 
 * vrati ho na vystupe. Inak vrati NULL. */
node * findNode(node *root, int key) {
    node * v = root;
    while (v != NULL && v->data != key) {
        if (key < v->data) {
            v = v->left;
        } else {
            v = v->right;
        }
    }
    return v;
}

/* Rekurzívna verzia */
node *findNodeR(node *root, int key) {
    if (root == NULL || key == root->data) {
        return root;
    } else if (key < root->data) {
        return findNodeR(root->left, key);
    } else {  // key > root->data
        return findNodeR(root->right, key);
    }
}

/* Zisti, ci strom reprezentujuci mnozinu s 
 * obsahuje uzol s klucom key. */
bool contains(set &s, int key) {
    return findNode(s.root, key) != NULL;
}

Čas výpočtu je v najhoršom prípade úmerný výške stromu.


Vkladanie do binárneho vyhľadávacieho stromu

Nasledujúca funkcia insertNode vloží uzol *v na správne miesto podstromu zakoreneného v *root ako jeho list.

  • Predpokladáme, že prvok v strome nie je.
  • Putujeme po strome podobne ako pri vyhľadávaní prvku, až kým nenarazíme na nulový smerník.
    • Na tomto mieste by mal byť nový prvok, takže ho tam pridáme ako nový list
    • Uvádzame rekurzívnu verziu, ale dá sa aj cyklom, podobne ako pri hľadaní
  • Funkcia add vytvorí uzol s daným kľúčom key a pomocou funkcie insertNode ho vloží do binárneho vyhľadávacieho stromu.
/* Vloží uzol v na správne miesto podstromu zakoreneného v root */
void insertNode(node *root, node *v) {
    assert(root != NULL && v != NULL);
    if (v->data < root->data) {
        if (root->left == NULL) {
            root->left = v;
            v->parent = root;
        } else {
            insertNode(root->left, v);
        }
    } else {
        if (root->right == NULL) {
            root->right = v;
            v->parent = root;
        } else {
            insertNode(root->right, v);
        }
    }
}

/* Vloží do stromu pre mnozinu s nový uzol s kľúčom key. */
void add(set &s, int key) {
    node *v = new node;
    v->data = key;
    v->left = NULL;
    v->right = NULL;
    v->parent = NULL;
    if (s.root == NULL) {
        // specialny pripad, ked vkladame prvy uzol
        s.root = v;
    } else {
        insertNode(s.root, v);
    }
}

Čas vkladania je tiež v najhoršom prípade úmerný hĺbke stromu.

Cvičenia

  • Ako bude vyzerať strom po nasledujúcej postupnosti operácií?
    set s;
    init(s);
    add(s, 2);
    add(s, 5);
    add(s, 3);
    add(s, 10);
    add(s, 7);  
  • Napíšte nerekurzívny variant funkcie insertNode.
  • Do funkcie doplňte assert, ktorý deteguje prípad, že vkladaná hodnota sa už v strome nachádza.
  • Napíšte funkciu treeSort, ktorá z poľa celých čísel a pomocou volaní funkcie add vytvorí binárny vyhľadávací strom a následne pomocou prehľadávania tohto stromu v poradí inorder pole a utriedi.

Minimum a následník

Uvedieme teraz dve funkcie, ktoré sa zídu pri mazaní prvku zo stromu, ale môžu sa zísť aj inokedy.

Prvá funkcia minNode nájde vo vyhľadávacom strome uzol, v ktorom je uložená najmenšia hodnota.

  • Všetky prvky menšie ako koreň sú v ľavom podstrome, bude tam zrejme aj minimum.
  • Tá istá úvaha platí pre koreň ľavého podstromu.
  • Ideme teda doľava kým sa dá, posledný vrchol vrátime (list alebo vrchol, ktorý má iba pravé dieťa).
  • Nie je treba teda prechádzať celý strom a nemusíme sa ani pozerať na položku data v uzloch.
  • Dá sa napísať cyklom aj rekurzívne.
  • Obalom pre používateľa bude funkcia min, ktorá pomocou funkcie minNode nájde minimálny kľúč v danej množine.
/* Vrati uzol s minimalnou hodnotou data v podstrome s korenom v. */
node *minNode(node *v) {
    assert(v != NULL);
    while (v->left != NULL) {
        v = v->left;
    }
    return v;
}

/* Vrati minimalnu hodnotu v mnozine s. */
int min(set &s) {
    assert(s.root != NULL);
    return minNode(s.root)->data; 
}

Cvičenia:

  • Napíšte rekurzívny variant funkcie minNode.
  • Ako by bolo treba funkciu zmeniť, aby hľadala maximum?


Funkcia successorNode nájde pre daný uzol v jeho následníka (angl. successor) v binárnom vyhľadávacom strome, čiže uzol, ktorý vo vzostupnom poradí podľa kľúčov nasleduje bezprostredne za uzlom v.

  • Ak má uzol v pravé dieťa, následník uzla v bude vrchol s minimálnou hodnotou data v pravom podstrome
  • V opačnom prípade môže byť následníkom uzla v jeho rodič, ak v je jeho ľavé dieťa.
  • Ak je v pravým dieťaťom svojho rodiča, môže to byť jeho prarodič (ak je rodič uzla v ľavým dieťaťom tohto prarodiča), atď.
  • Vo všeobecnosti teda ide o najbližšieho predka uzla v takého, že v patrí do jeho ľavého podstromu.
  • V strome existuje práve jeden uzol bez následníka (najväčší prvok).
    • Ako presne sa bude funkcia nižšie pre tento prvok správať?
/* Vrati uzol, ktory vo vzostupnom poradi uzlov podla 
 * klucov nasleduje za v. 
 * Ak taky uzol neexistuje, vrati NULL. */
node *successorNode(node *v) {
    assert(v != NULL);
    if (v->right != NULL) {
        return minNode(v->right);
    }
    while (v->parent != NULL && v == v->parent->right) {
        v = v->parent;
    }
    return v->parent;
}

Mazanie z binárneho vyhľadávacieho stromu

Nasledujúca funkcia remove zmaže z binárneho vyhľadávacieho stromu uzol s kľúčom key (ak sa taký uzol v strome vyskytuje).

  • Najprv pomocou funkcie findNode nájde uzol v s kľúčom key.
  • Ak je v list, jednoducho ho zmažeme.
  • Ak má v jedno dieťa, toto dieťa prevesíme priamo pod rodiča v a v zmažeme.
  • Ak má v dve deti, nájdeme nasledovníka v, t.j. minimum v pravom podstrome v.
  • Tento nasledovník nemá ľavé dieťa, vieme ho teda zmazať.
  • Jeho údaje presunieme do vrcholu v.
  • Tiež treba dať pozor na mazanie koreňa.
/* Zmaze zo stromu pre mnozinu s uzol s klucom key, ak tam taky je. */
void remove(set &s, int key) {
    // Najde uzol s hodnotou, ktoru treba vymazat.
    node *v = findNode(s.root, key);                   
    if (v == NULL) {
        // pozadovany uzol neexistuje
        return;
    }
    
    // Najde uzol *rm, ktory sa napokon realne zmaze.   
    node *rm;                                          
    if (v->left == NULL || v->right == NULL) {         
        rm = v;
    } else  {
        rm = successorNode(v);
        // Presunie kluc uzla *rm do uzla *v. 
        v->data = rm->data;
    }

    // ak ma uzol rm dieta, jeho rodicom bude rodic rm
    node *child;                                      
    if (rm->left != NULL) {
        child = rm->left;
    } else {
        child = rm->right;
    }
    if (child != NULL) {
        child->parent = rm->parent;
    }
    if (rm->parent == NULL) {
        s.root = child;
    } else if (rm == rm->parent->left) {
        rm->parent->left = child;    
    } else if (rm == rm->parent->right) {
        rm->parent->right = child;
    }
    // rm uz nie je v strome, uvolnime jeho pamat
    delete rm;
}

Zložitosť jednotlivých operácií

  • Časová zložitosť operácií contains, add aj remove je úmerná výške stromu, ktorú označíme h.
  • Minule sme ukázali, že pre strom s n uzlami máme log2(n+1)-1 ≤ h ≤ n-1.
  • Zložitosť uvedených operácií je teda v najhoršom prípade lineárna od počtu uzlov stromu (tento prípad nastane, ak prvky vkladáme od najmenšieho po najväčší alebo naopak).
  • Dá sa však ukázať, že ak sa prvky vkladajú v náhodnom poradí, výška stromu bude v priemere logaritmická od počtu uzlov.
  • Na predmete Algoritmy a dátové štruktúry (druhý ročník) sa tieto tvrdenia dokazujú poriadne a preberajú sa tam aj varianty vyhľadávacích stromov, pre ktoré je zložitosť uvedených operácií logaritmická aj v najhoršom prípade.