Programovanie (1) v C/C++
1-INF-127, ZS 2024/25
Prednáška 21: Rozdiel medzi revíziami
(→Oznamy) |
(→Oznamy) |
||
Riadok 2: | Riadok 2: | ||
* Boli stanovené predbežné termíny skúšok (podľa okolností cez MS Teams alebo v počítačových halách): | * Boli stanovené predbežné termíny skúšok (podľa okolností cez MS Teams alebo v počítačových halách): | ||
− | + | ** Piatok 17. decembra 2021 12:00, ústne skúšky od 15:00 (predtermín s limitom 20 študentov). | |
− | ** | + | ** Piatok 14. januára 2022 9:00, ústne skúšky od 13:00 (riadny termín). |
− | ** | + | ** Piatok 21. januára 2021 9:00, ústne skúšky od 13:00 (hlavne prvý opravný termín). |
− | ** | + | ** Piatok 4. februára 2021 9:00, ústne skúšky od 13:00 (hlavne 2. opravný termín). |
− | ** | ||
− | |||
* Prihlasovanie na predtermín je v AIS otvorené od štvrtka 9.12. 19:00. Prihlasovanie na zvyšné termíny bude otvorené v pondelok 13.12. 19:00 (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. | * Prihlasovanie na predtermín je v AIS otvorené od štvrtka 9.12. 19:00. Prihlasovanie na zvyšné termíny bude otvorené v pondelok 13.12. 19:00 (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, ústna skúška sa spravidla koná v ten istý deň, pokiaľ máte úspešne absolvovaný semestrálny test aj praktickú skúšku. | * Prihlasuje sa iba na praktickú časť skúšky, ústna skúška sa spravidla koná v ten istý deň, pokiaľ máte úspešne absolvovaný semestrálny test aj praktickú skúšku. |
Verzia zo dňa a času 12:04, 7. december 2021
Obsah
- 1 Oznamy
- 2 Binárne vyhľadávacie stromy
- 2.1 Definícia štruktúr pre binárny vyhľadávací strom a jeho uzol
- 2.2 Inicializácia binárneho vyhľadávacieho stromu
- 2.3 Likvidácia binárneho vyhľadávacieho stromu
- 2.4 Hľadanie v binárnom vyhľadávacom strome
- 2.5 Vkladanie do binárneho vyhľadávacieho stromu
- 2.6 Minimálny uzol
- 2.7 Následník uzla
- 2.8 Mazanie z binárneho vyhľadávacieho stromu
- 2.9 Zložitosť jednotlivých operácií
- 2.10 Príklad programu pracujúceho s binárnymi vyhľadávacími stromami
Oznamy
- Boli stanovené predbežné termíny skúšok (podľa okolností cez MS Teams alebo v počítačových halách):
- Piatok 17. decembra 2021 12:00, ústne skúšky od 15:00 (predtermín s limitom 20 študentov).
- Piatok 14. januára 2022 9:00, ústne skúšky od 13:00 (riadny termín).
- Piatok 21. januára 2021 9:00, ústne skúšky od 13:00 (hlavne prvý opravný termín).
- Piatok 4. februára 2021 9:00, ústne skúšky od 13:00 (hlavne 2. opravný termín).
- Prihlasovanie na predtermín je v AIS otvorené od štvrtka 9.12. 19:00. Prihlasovanie na zvyšné termíny bude otvorené v pondelok 13.12. 19:00 (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, ústna skúška sa spravidla koná v ten istý deň, pokiaľ máte úspešne absolvovaný semestrálny test aj praktickú skúšku.
- Ústna skúška nie je potrebná, ak študent úspešne absolvuje v prezenčnej forme test pre pokročilých (aspoň 50% bodov), semestrálny test alebo praktickú skúšku.
- Plán prednášok a cvičení na zvyšok semestra:
- Dnes hlavne pokyny a rady k skúškam na tomto predmete aj všeobecne
- Tento piatok 10.12. cez cvičenia semestrálna písomka
- V pondelok 13.12. bude bežná prednáška.
- V stredu 15.12. 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í „tréning” 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
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.
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í:
- Každý vrchol v ľavom podstrome uzla v má hodnotu kľúča menšiu (alebo rovnakú) ako key.
- Každý vrchol v pravom podstrome uzla v má hodnotu kľúča väčšiu (alebo rovnakú) ako key.
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.
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 štruktúr pre binárny vyhľadávací strom a jeho uzol
Štruktúra node 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ľúč key, 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 – 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 NULL).
Na binárny vyhľadávací strom kladieme globálne podmienky ohľadom kľúčov jeho uzlov. V prípade „ručnej” 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 node pre jednotlivé uzly aj štruktúru binarySearchTree realizujúcu „obal” 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 binarySearchTree. 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.
/* Uzol binarneho vyhladavacieho stromu. */
struct node {
int key; // kluc, podla ktoreho budeme porovnavat prvky (namiesto int aj ina uplne usporiadana mnozina)
/* Sem mozu prist lubovolne dalsie satelitne data ulozene v danom uzle. */
node *parent; // smernik na otca (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)
};
// ...
/* Samotna struktura binarneho vyhladavacieho stromu (obal pre pouzivatela). */
struct binarySearchTree {
node *root;
};
Inicializácia binárneho vyhľadávacieho stromu
Nasledujúca funkcia realizuje inicializáciu binárneho vyhľadávacieho stromu t.
/* Inicializuje prazdny binarny vyhladavaci strom. */
void bstInit(binarySearchTree &t) {
t.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 „baliacu” funkciu bstDestroy, ktorá zlikviduje binárny vyhľadávací strom t tak, že zavolá funkciu destroy na jeho koreň.
/* Uvolni pamat pre podstrom s korenom *root. */
void destroy(node *root) {
if (root != NULL) {
destroy(root->left);
destroy(root->right);
delete root;
}
}
// ...
/* Zlikviduje strom t (uvolni pamat). */
void bstDestroy(binarySearchTree &t) {
destroy(t.root);
}
Hľadanie v binárnom vyhľadávacom strome
Nasledujúca funkcia findNode sa pokúsi v podstrome zakorenenom v uzle *root vyhľadať uzol, ktorého kľúč je rovný key. 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 NULL.
Pri hľadaní uzla s hodnotou key bude funkcia findNode využívať definujúcu vlastnosť binárnych vyhľadávacích stromov: ak je hľadaná hodnota kľúča key menšia, než kľúč koreňa podstromu *root, 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 *root rovný key, ide o hľadaný uzol a smerník naň tak možno ihneď vrátiť na výstupe.
Používateľovi pritom opäť poskytneme aj „baliacu” funkciu bstFind, ktorá 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) {
if (root == NULL || root->key == key) {
return root;
} else if (key < root->key) {
return findNode(root->left, key);
} else {
return findNode(root->right, key);
}
}
// ...
/* Zisti, ci strom t obsahuje uzol s klucom key. */
bool bstFind(binarySearchTree &t, int key) {
return findNode(t.root, key) != NULL;
}
Čas výpočtu je v najhoršom prípade úmerný výške stromu. Poznamenajme ešte, že funkciu findNode je možné realizovať aj nerekurzívne, napríklad takto:
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;
}
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. Postupuje pritom rekurzívne: ak zistí, že uzol *v má kľúč menší, než *root, pokúsi sa ho vložiť do ľavého podstromu uzla *root; v opačnom prípade sa ho pokúsi vložiť do pravého podstromu.
Používateľovi následne poskytneme „baliacu” funkciu bstInsert, ktorá vytvorí uzol s daným kľúčom key a pomocou funkcie insertNode ho vloží do binárneho vyhľadávacieho stromu t.
/* Vlozi uzol *v na spravne miesto podstromu zakoreneneho v *root */
void insertNode(node *root, node *v) {
assert(root != NULL && v != NULL);
if (v->key < root->key) {
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);
}
}
}
// ...
/* Vlozi do stromu t novy uzol s klucom key. */
void bstInsert(binarySearchTree &t, int key) {
node *v = new node;
v->key = key;
v->left = NULL;
v->right = NULL;
v->parent = NULL;
if (t.root == NULL) {
t.root = v;
} else {
insertNode(t.root, v);
}
}
Čas vkladania je tiež v najhoršom prípade úmerný hĺbke stromu.
Cvičenie č. 1: napíšte nerekurzívny variant funkcie insertNode.
Cvičenie č. 2: napíšte funkciu treeSort, ktorá z poľa celých čísel a pomocou volaní funkcie bstInsert vytvorí binárny vyhľadávací strom a následne pomocou prehľadávania tohto stromu v poradí inorder pole a utriedi.
Minimálny uzol
Nasledujúca funkcia minNode nájde v podstrome zakorenenom v *root 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 root->key sa musia nachádzať v ľavom podstrome uzla *root.
„Obalom” pre používateľa bude funkcia bstMin, ktorá pomocou funkcie minNode nájde minimálny kľúč v danom binárnom vyhľadávacom strome t.
/* Vrati (niektory) uzol s minimalnou hodnotou key v podstrome s korenom *root. */
node *minNode(node *root) {
assert(root != NULL);
if (root->left != NULL) {
return minNode(root->left);
} else {
return root;
}
}
// ...
/* Vrati minimalny kluc uzla v strome t. */
int bstMin(binarySearchTree &t) {
assert(t.root != NULL);
return minNode(t.root)->key;
}
Cvičenie: napíšte nerekurzívny variant funkcie minNode.
Následník uzla
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. Je pritom založená na nasledujúcich pozorovaniach:
- Ak má uzol *v pravého syna, následník uzla *v musí byť v jeho pravom podstrome – konkrétne pôjde o minimálny uzol z tohto podstromu.
- V opačnom prípade môže byť následníkom uzla *v jeho otec (ak *v je jeho ľavý syn). Ak je *v pravým synom svojho otca, môže to byť aj jeho starý otec (ak je otec uzla *v ľavým synom tohto starého otca), 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 (jeden spomedzi najväčších prvkov).
/* 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 bstRemove zmaže z binárneho vyhľadávacieho stromu t práve jeden uzol s kľúčom key (ak sa taký uzol v strome vyskytuje). Pracuje tak, že najprv pomocou funkcie findNode nájde uzol *v s kľúčom key. V prípade úspechu zistí počet synov uzla *v. Ak totiž *v nemá žiadneho syna alebo má len jedného syna, možno ho zo stromu t zmazať jednoducho tak, že sa prípadný syn uzla *v stane synom otca uzla *v. V prípade, že má *v 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 *rm navyše nemôže mať ľavého syna. Odstránenie kľúča key je teda možné realizovať tak, že sa kľúč uzla *rm presunie do uzla *v a následne sa odstráni uzol *rm tak, ako je popísané vyššie.
/* Zmaze zo stromu t prave jeden uzol s klucom key (ak tam taky je). */
void bstRemove(binarySearchTree &t, int key) {
node *v = findNode(t.root, key); // Najde uzol v s hodnotou, ktoru treba vymazat.
if (v == NULL) {
return;
}
node *rm; // Najde uzol *rm stromu t, ktory sa napokon realne zmaze.
if (v->left == NULL || v->right == NULL) {
rm = v;
} else {
rm = successorNode(v);
}
if (rm != v) { // Ak rm != v, presunie kluc uzla *rm do uzla *v.
v->key = rm->key;
}
node *child; // Zmaze uzol *rm a uvolni pamat alokovanu pre tento uzol.
if (rm->left != NULL) {
child = rm->left;
} else {
child = rm->right;
}
if (child != NULL) {
child->parent = rm->parent;
}
if (rm->parent == NULL) {
t.root = child;
} else if (rm == rm->parent->left) {
rm->parent->left = child;
} else if (rm == rm->parent->right) {
rm->parent->right = child;
}
delete rm;
}
Zložitosť jednotlivých operácií
- Časová zložitosť operácií bstFind(t), bstInsert(t) aj bstRemove(t) je úmerná hodnote height(t), čo je výška stromu t.
- Minule sme ukázali, že pre výšku h stromu s n vrcholmi je 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 v priemernom prípade je ich zložitosť rádovo 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.
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.
#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;
}