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 14: Rozdiel medzi revíziami

Z Programovanie
Skočit na navigaci Skočit na vyhledávání
 
(60 medziľahlých úprav od 2 ďalších používateľov nie je zobrazených)
Riadok 1: Riadok 1:
 
== Oznamy ==
 
== Oznamy ==
* Tento týždeň bude okrem bežnej rozcvičky na utorňajších cvičeniach aj bonusová rozcvička počas piatkových doplnkových cvičení (za 1 bonusový bod). Termín odovzdania zvyšných úloh z týchto cvičení bude ''streda 18. novembra, 22:00''.
+
* V stredu je dekanské voľno, prednáška nebude.
* Druhú domácu úlohu treba odovzdať do ''piatku 13. novembra, 22:00''.
+
* Piatkové cvičenia sú povinné pre tých, ktorí na cvičeniach v utorok nezískajú aspoň 4 body. Informácia bude na testovači.
* Budúci týždeň budú kvôli štátnemu sviatku iba piatkové cvičenia. V ''pondelok 16. novembra'' bude zverejnených niekoľko úloh na cvičenia č. 9 (menej, než obvykle).
+
** Väčšina príkladov bude na dvojrozmerné polia, vyskytnú sa aj spájané zoznamy z dnešnej prednášky.
* V ''piatok 20. novembra'' bude na začiatku doplnkových cvičení krátky test, body za ktorý budú riadnou súčasťou hodnotenia z cvičení č. 9. Pokyny ohľadom technickej realizácie testu budú upresnené neskôr.
+
* DÚ2 odovzdávajte do utorka 19.11. 22:00.
 +
* Pripomíname, že semestrálny test bude v stredu 11.12. o 18:10. Opravný termín v januári.
 +
** Ak sa vyskytne konflikt s iným predmetom, hláste to vyučujúcim čo najskôr.
  
== Ešte k smerníkom ==
+
==Dynamická množina==
  
=== Opakovanie z minulých dvoch prednášok ===
+
=== Motivačný príklad ===
 
+
* Na fakulte sa dvere do niektorých miestností otvárajú priložením čipovej karty k čítačke.
Základy práce so smerníkmi:
 
<syntaxhighlight lang="C++">
 
int a = 7;        // premenna typu int
 
int *b = NULL;    // smernik na int
 
b = &a;            // b obsahuje adresu premennej a
 
*b = 8;            // v premennej a je teraz 8
 
a = (*b)+1;        // v premennej a je teraz 9
 
</syntaxhighlight>
 
 
 
Smerníky a polia:
 
<syntaxhighlight lang="C++">
 
int a[3];          // a je vlastne konstantny smernik na nulty prvok pola
 
int *b = a;        // mozeme ho priradit do ineho smernika (opacne to nejde)
 
*b = 3;           
 
b[1] = 4;         
 
a[2] = 5;          // pole a teraz obsahuje cisla 3,4,5 
 
for (int i=0; i<3; i++){
 
  cout << *(a+i) << "=" << a[i] << endl;  // rozne zapisy toho isteho
 
}
 
b = new int[a[1]];  // b teraz ukazuje na nove pole dlzky 4
 
delete[] b;        // uvolnenie pamate alokovanej pre nove pole
 
</syntaxhighlight>
 
 
 
=== Smerníky a <tt>struct</tt> ===
 
 
 
Smerník môže ukazovať aj na <tt>struct</tt>. Operátory <tt>.</tt> (prístup k prvku štruktúry) a <tt>[]</tt> (prístup k prvku poľa) majú vyššiu prioritu ako operátory <tt>*</tt> (dereferencia smerníka) a <tt>&</tt> (adresa). Preto napríklad:
 
* Zápis <tt>*s.cokolvek</tt> je to isté ako <tt>*(s.cokolvek)</tt> a vyjadruje dereferenciu smerníka <tt>s.cokolvek</tt>.
 
* Zápis <tt>(*p).cokolvek</tt> vyjadruje prvok <tt>cokolvek</tt> štruktúry získanej dereferenciou smerníka <tt>p</tt>.
 
* Zvyčajne je potrebnejší zápis <tt>(*p).cokolvek</tt>; existuje preň preto skratka <tt>p->cokolvek</tt>.
 
Podobne (ako sme videli už minule):
 
* Zápis <tt>*a[10]</tt> je to isté ako <tt>*(a[10])</tt> a vyjadruje dereferenciu smerníka, ktorý je desiatym prvkom poľa <tt>a</tt>.
 
* Zápis <tt>(*p)[10]</tt> vyjadruje desiaty prvok poľa, ktoré dostaneme dereferenciou smerníka <tt>p</tt>.
 
 
 
<syntaxhighlight lang="C++">
 
struct bod {
 
  int x, y;
 
};
 
 
 
// ...
 
 
 
bod b;           
 
b.x = 0;
 
b.y = 0;
 
 
 
// ...
 
 
 
bod *p;            // smernik na strukturu typu bod
 
p = &b;            // p ukazuje na bod b
 
bod *p2 = new bod; // alokovanie noveho bodu
 
(*p2).x = 20;      // bod, na ktory ukazuje p2, bude mat x-ovu suradnicu 20
 
p2->y = 10;        // bod, na ktory ukazuje p2, bude mat y-ovu suradnicu 10
 
delete p2;        // uvolnenie pamate
 
</syntaxhighlight>
 
 
 
== Dynamické množiny: úvod ==
 
 
 
Uvažujme nasledujúce dva &bdquo;motivačné&rdquo; príklady:
 
 
 
=== Príklad č. 1 ===
 
 
 
Na fakulte sa dvere do niektorých miestností otvárajú priložením čipovej karty k čítačke. Jeden (asi nie úplne optimálny) prístup k implementácii takéhoto systému by mohol byť nasledovný:
 
 
* Každá karta má v sebe uložené identifikačné číslo.
 
* Každá karta má v sebe uložené identifikačné číslo.
* Čítačka má v pamäti zoznam identifikačných čísel oprávnených osôb (študenti, niektorí vyučujúci a pod.).
+
* Čítačka má v pamäti zoznam identifikačných čísel oprávnených osôb (študenti, vyučujúci a pod.).
* Po priložení karty z nej čítačka prečíta číslo a zisťuje, či ho má vo svojom zozname.
+
* Po priložení karty z nej prečíta číslo a zisťuje, či ho má vo svojom zozname.
 +
* Administrátor tiež potrebuje vedieť pridávať a uberať oprávnené osoby.
 +
* Ako asi môže byť systém pracujúci so zoznamom identifikačných čísel naprogramovaný?
  
Ako môže byť takýto prístupový systém naprogramovaný?
+
===Dynamická množina===
 
+
Chceli by sme vytvoriť dátovú štruktúru s nasledujúcou špecifikáciou.
=== Príklad č. 2 ===
+
* Máme množinu ''A'', ktorá sa bude postupne meniť, preto ju nazývame dynamická množina.
 
+
* Funkcia <tt>contains</tt> dostane množinu ''A'' a hodnotu ''x'' a zistí, či ''x'' patrí do ''A''.
Dispečing prepravnej spoločnosti s veľkým množstvom vozidiel má systém pracujúci nasledujúcim spôsobom:
+
* Funkcia <tt>add</tt> dostane množinu ''A'' a hodnotu ''x'' a pridá ''x'' do ''A''.
* Pre každé vozidlo si pamätá, či je momentálne k dispozícii alebo nie.
+
* Funkcia <tt>remove</tt> dostane množinu ''A'' a prvok ''x'' a odoberie ''x'' z ''A''.
* Vozidlá neustále menia svoj stav podľa toho, ako prichádzajú do resp. odchádzajú z vozovne.
+
* Pre jednoduchosť funkciu <tt>remove</tt> nebudeme dnes uvažovať.
* V prípade požiadavky na vozidlo sa vyberie niektoré, ktoré je momentálne k dispozícii.
 
* Výnimočne je potrebné pozvať niektorého vodiča k šéfovi na koberec; v takom prípade je potrebné pre konkrétne vozidlo zistiť, či je akurát k dispozícii.
 
 
 
Ako môže byť takýto systém implementovaný?
 
 
 
=== Dynamické množiny ===
 
 
 
V obidvoch horeuvedených príkladoch sa vyskytuje nejaká množina ''A''. V prvom príklade môže ísť napríklad o osoby s oprávnením vstupu do miestnosti a v druhom príklade môže ísť o vozidlá, ktoré sú momentálne k dispozícii. Tieto množiny sa v čase menia &ndash; hovoríme preto o ''dynamických množinách''. V obidvoch príkladoch pritom potrebujeme nasledujúce funkcie:
 
* Funkciu <tt>contains</tt>, ktorá pre daný prvok ''x'' zistí, či patrí do množiny ''A''.
 
* Funkciu <tt>add</tt>, ktorá do množiny pridá nový prvok ''x''.
 
* Funkciu <tt>remove</tt>, ktorá z množiny odoberie prvok ''x'' (ňou sa ale dnes zaoberať nebudeme).
 
 
* Niekedy sa môžu zísť aj iné operácie.
 
* Niekedy sa môžu zísť aj iné operácie.
  
Ako dnes uvidíme, dynamickú množinu môžeme implementovať množstvom rôznych spôsobov. Program s dynamickými množinami pracujúci si ale vystačí s operáciami popísanými vyššie, bez ohľadu na ich implementáciu. Hovoríme, že dynamické množiny sú ''abstraktný dátový typ'' &ndash; hovorí totiž iba o &bdquo;rozhraní&rdquo;, ktoré má dátová štruktúra poskytovať používateľovi, nie o jej implementácii.
+
Problém príslušnosti k množine sa vyskytuje aj v mnohých iných situáciách.
  
Obidva uvedené príklady sa líšia &bdquo;kritickosťou&rdquo; jednotlivých operácií:
+
* Ako dnes uvidíme, dynamickú množinu môžeme implementovať rôznymi spôsobmi.  
* Pri prístupovom systéme často potrebujeme predovšetkým operáciu <tt>contains</tt>, ktorá sa vykoná pri každom priložení karty k čítačke. Operácie <tt>add</tt> a <tt>remove</tt> sa vykonávajú zriedkavejšie, pri aktualizácii zoznamu oprávnených osôb.
+
* Hovoríme, že dynamická množina je '''abstraktný dátový typ''',  špecifikuje totiž iba rozhranie, ktoré má dátová štruktúra poskytovať používateľovi, nie jeho implementáciu.
* V druhom príklade naopak často voláme operácie <tt>add</tt> a <tt>remove</tt> (a prípadne ešte nejakú ďalšiu operáciu, ktorá vráti ľubovoľný prvok množiny). Operáciu <tt>contains</tt> používame zriedkavejšie (pri volaní konkrétneho vodiča na koberec).
+
* Ak by sme zmenili implementáciu z jednej na inú, nemusíme nutne meniť programy, ktoré dynamickú množinu využívajú, pokiaľ k nej pristupujú iba pomocou uvedených funkcií.
 
 
Dynamické množiny sa používajú aj v mnohých iných situáciách a &ndash; ako uvidíme &ndash; dajú sa implementovať rozličnými spôsobmi. Tieto implementácie sú typicky zamerané na jednu alebo niekoľko z poskytovaných operácií, ktoré sú pri nej vykonávané efektívne. Pri rozličných aplikáciách je teda často potrebné zvoliť rozličné implementácie.
 
  
 
== Implementácie dynamických množín ==
 
== Implementácie dynamických množín ==
  
''Pre jednoduchosť sa zaoberajme iba dynamickými množinami celých čísel.''
+
* Pre jednoduchosť budeme uvažovať iba dynamickú množinu celých čísel.
Bez ohľadu na konkrétnu implementáciu budeme takúto dynamickú množinu vždy uchovávať v štruktúre <tt>set</tt>. Navyše budeme mať implementovaných niekoľko funkcií, ktoré s dynamickými množinami pracujú. Kostra programu teda bude vyzerať pre ľubovoľnú implementáciu dynamickej množiny takto:
+
* Dynamickú množinu budeme uchovávať v štruktúre <tt>set</tt>.  
 +
* Navyše budeme mať implementovaných niekoľko funkcií, ktoré s dynamickými množinami pracujú.  
 +
* Kostra programu teda bude vyzerať pre ľubovoľnú implementáciu dynamickej množiny takto:
  
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
/* Struktura reprezentujuca dynamicku mnozinu. */
+
/* Štruktúra reprezentujúca dynamickú množinu. */
 
struct set {
 
struct set {
 
     // ...
 
     // ...
 
};
 
};
  
/* Funkcia, ktora vytvori prazdnu dynamicku mnozinu. */
+
/* Funkcia vytvorí prázdnu dynamickú množinu. */
 
void init(set &s) {
 
void init(set &s) {
 
     // ...
 
     // ...
 
}
 
}
  
/* Funkcia, ktora zisti, ci prvok x patri do mnoziny s. */
+
/* Funkcia zistí, či prvok x patrí do množiny s. */
 
bool contains(set &s, int x) {
 
bool contains(set &s, int x) {
 
     // ...
 
     // ...
 
}
 
}
  
/* Funkcia, ktora prida prvok x do mnoziny s. */
+
/* Funkcia pridá prvok x do množiny s. */
 
void add(set &s, int x) {
 
void add(set &s, int x) {
 
     // ...
 
     // ...
 
}
 
}
  
/* Funkcia, ktora uvolni mnozinu s z pamate. */
+
/* Funkcia uvoľní množinu s z pamäte. */
 
void destroy(set &s) {
 
void destroy(set &s) {
 
     // ...
 
     // ...
Riadok 134: Riadok 66:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Bez ohľadu na implementáciu štruktúry <tt>set</tt> a uvedených funkcií už teraz môžeme napísať program, ktorý ich využíva &ndash; bude postupne z konzoly čítať príkazy a postupne tieto príkazy vykonávať.
+
Bez ohľadu na implementáciu štruktúry <tt>set</tt> a uvedených funkcií už teraz môžeme napísať program, ktorý ich využíva. Z konzoly číta príkazy a postupne ich vykonáva.
  
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
Riadok 167: Riadok 99:
  
 
     destroy(A);
 
     destroy(A);
   
 
    return 0;
 
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Ukážeme si teraz niekoľko rôznych implementácii dynamickej množiny; začneme s dvoma, ktoré sú nám už v princípe známe.
+
Ukážeme si teraz niekoľko rôznych implementácii dynamickej množiny; začneme s dvoma, ktoré sú nám už známe.
  
 
=== Dynamická množina ako pole ===
 
=== Dynamická množina ako pole ===
Riadok 189: Riadok 119:
  
 
struct set {
 
struct set {
     int *p;     // Smernik na nulty prvok pola.
+
     int *items; // Smerník na nultý prvok poľa
     int length;  // Momentalna dlzka pola.
+
     int length;  // Počet prvkov v poli
 
};
 
};
  
 
void init(set &s) {
 
void init(set &s) {
     s.p = new int[maxN];
+
     s.items = new int[maxN];
 
     s.length = 0;
 
     s.length = 0;
 
}
 
}
  
 
bool contains(set &s, int x) {
 
bool contains(set &s, int x) {
     for (int i = 0; i <= s.length - 1; i++) {
+
     for (int i = 0; i < s.length; i++) {
         if (s.p[i] == x) {
+
         if (s.items[i] == x) {
 
             return true;
 
             return true;
 
         }
 
         }
Riadok 209: Riadok 139:
 
void add(set &s, int x) {
 
void add(set &s, int x) {
 
     assert(s.length < maxN);
 
     assert(s.length < maxN);
     s.p[s.length] = x;
+
     s.items[s.length] = x;
 
     s.length++;
 
     s.length++;
 
}
 
}
  
 
void destroy(set &s) {
 
void destroy(set &s) {
     delete[] s.p;
+
     delete[] s.items;
 
}           
 
}           
 
</syntaxhighlight>
 
</syntaxhighlight>
Riadok 220: Riadok 150:
 
===Dynamická množina ako utriedené pole ===
 
===Dynamická množina ako utriedené pole ===
 
Prvky množiny môžeme v poli uchovávať aj utriedené od najmenšieho po najväčšie.
 
Prvky množiny môžeme v poli uchovávať aj utriedené od najmenšieho po najväčšie.
* Funkcia <tt>contains</tt> potom môže použiť binárne vyhľadávanie. Je teda rýchlejšia, ako v predchádzajúcom prípade (v poli veľkosti ''n'' sa pozrie len na približne log ''n'' pozícií; napríklad pre miliónprvkové pole sa pozrieme asi na 20 prvkov poľa).
+
* Funkcia <tt>contains</tt> potom môže použiť binárne vyhľadávanie. Je teda rýchlejšia, ako v predchádzajúcom prípade (v poli veľkosti ''n'' sa pozrie len na približne log<sub>2</sub> ''n'' pozícií; napríklad pre miliónprvkové pole sa pozrieme asi na 20 prvkov poľa).
 
* Funkcia <tt>add</tt> ale musí vložiť prvok na správne miesto v utriedenom poli; je teda o dosť pomalšia.  
 
* Funkcia <tt>add</tt> ale musí vložiť prvok na správne miesto v utriedenom poli; je teda o dosť pomalšia.  
 +
* Tento spôsob by teda bol vhodný, ak sa množina mení zriedkavo.
  
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
Riadok 231: Riadok 162:
  
 
struct set {
 
struct set {
     int *p;     // Smernik na nulty prvok pola.
+
     int *items; // smerník na nultý prvok poľa
     int length;  // Momentalna dlzka pola.
+
     int length;  // počet prvkov v poli
 
};
 
};
  
 
void init(set &s) {
 
void init(set &s) {
     s.p = new int[maxN];
+
     s.items = new int[maxN];
 
     s.length = 0;
 
     s.length = 0;
 
}
 
}
Riadok 245: Riadok 176:
 
     while (left <= right) {
 
     while (left <= right) {
 
         int index = (left + right) / 2;
 
         int index = (left + right) / 2;
         if (s.p[index] == x) {
+
         if (s.items[index] == x) {
 
             return true;
 
             return true;
         } else if (s.p[index] > x) {
+
         } else if (s.items[index] > x) {
 
             right = index - 1;
 
             right = index - 1;
 
         } else {
 
         } else {
Riadok 259: Riadok 190:
 
     assert(s.length < maxN);
 
     assert(s.length < maxN);
 
     int kam = s.length;
 
     int kam = s.length;
     while (kam > 0 && s.p[kam - 1] > x) {
+
     while (kam > 0 && s.items[kam - 1] > x) {
         s.p[kam] = s.p[kam - 1];
+
         s.items[kam] = s.items[kam - 1];
 
         kam--;
 
         kam--;
 
     }
 
     }
     s.p[kam] = x;
+
     s.items[kam] = x;
 
     s.length++;
 
     s.length++;
 
}
 
}
  
 
void destroy(set &s) {
 
void destroy(set &s) {
     delete[] s.p;
+
     delete[] s.items;
 
}           
 
}           
 
</syntaxhighlight>
 
</syntaxhighlight>
Riadok 276: Riadok 207:
 
Dnes uvidíme ďalšie dva spôsoby implementácie dynamickej množiny:
 
Dnes uvidíme ďalšie dva spôsoby implementácie dynamickej množiny:
  
* Množina ako '''jednosmerne spájaný zoznam''':
+
* Množina ako '''spájaný zoznam''':
 
** Ľahko pridáme nové prvky, nepotrebujeme vopred vedieť veľkosť.
 
** Ľahko pridáme nové prvky, nepotrebujeme vopred vedieť veľkosť.
 
** Nedá sa rýchlo binárne vyhľadávať.
 
** Nedá sa rýchlo binárne vyhľadávať.
 
** Založené na smerníkoch.
 
** Založené na smerníkoch.
* Množina pomocou '''hešovania''':
+
* Množina pomocou '''hašovania''':
 
** Často veľmi rýchle vyhľadávanie.
 
** Často veľmi rýchle vyhľadávanie.
 
** Použijeme polia aj spájané zoznamy.
 
** Použijeme polia aj spájané zoznamy.
 +
 +
== Odbočka: smerníky a <tt>struct</tt> ==
 +
 +
=== Opakovanie základnej práce so smerníkmi ===
 +
 +
<syntaxhighlight lang="C++">
 +
int n = 7;        // premenná typu int
 +
int * p = &n;      // smerník na int
 +
// n, *p teraz znamenajú to isté
 +
 +
int * p2 = new int; // p2 ukazuje na alokovanú pamäť
 +
*p2 = 7;            // cez *p2 pracujeme s touto pamäťou
 +
delete p2;          // uvoľníme pamäť
 +
p2  = p;            // meníme samotný smerník
 +
p = NULL;          // NULL "nikam neukazuje"
 +
</syntaxhighlight>
 +
 +
=== Smerníky a <tt>struct</tt> ===
 +
 +
Smerník môže ukazovať aj na <tt>struct</tt>. Operátory <tt>.</tt> (prístup k prvku štruktúry) a <tt>[]</tt> (prístup k prvku poľa) majú vyššiu prioritu ako operátory <tt>*</tt> (dereferencia smerníka) a <tt>&</tt> (adresa). Preto napríklad:
 +
* Zápis <tt>*s.cokolvek</tt> je to isté ako <tt>*(s.cokolvek)</tt> a vyjadruje dereferenciu smerníka <tt>s.cokolvek</tt>.
 +
* Zápis <tt>(*p).cokolvek</tt> vyjadruje prvok <tt>cokolvek</tt> štruktúry získanej dereferenciou smerníka <tt>p</tt>.
 +
* Zvyčajne je potrebnejší zápis <tt>(*p).cokolvek</tt>; existuje preň preto skratka <tt>p->cokolvek</tt>.
 +
 +
<syntaxhighlight lang="C++">
 +
struct bod {
 +
  int x, y;
 +
};
 +
 +
// ...
 +
 +
bod b;
 +
b.x = 0;
 +
b.y = 0;
 +
bod *p = &b;      // p ukazuje na bod b
 +
 +
bod *p2 = new bod; // alokovanie nového bodu
 +
(*p2).x = 20;      // bod, na ktorý ukazuje p2, bude mať x 20
 +
p2->y = 10;        // bod, na ktorý ukazuje p2, bude mať y 10
 +
delete p2;        // uvoľnenie pamäte
 +
</syntaxhighlight>
 +
  
 
==Spájané zoznamy==
 
==Spájané zoznamy==
  
''Spájaný zoznam'' (angl. ''linked list'') je postupnosť uzlov rovnakého typu usporiadaných za sebou. Každý uzol pritom pozostáva z dvoch častí:
+
'''Spájaný zoznam''' (angl. ''linked list'') je postupnosť uzlov rovnakého typu usporiadaných za sebou. Každý uzol pritom pozostáva z dvoch častí:
 
* Samotné dáta; v našom prípade jedno číslo typu <tt>int</tt>.
 
* Samotné dáta; v našom prípade jedno číslo typu <tt>int</tt>.
* Smerník, ktorý ukazuje na nasledujúci prvok zoznamu &ndash; ten umožňuje &bdquo;pohybovať sa&rdquo; po zozname zľava doprava.
+
* Smerník <tt>next</tt>, ktorý ukazuje na nasledujúci prvok zoznamu.
 
+
** Tieto smerníky umožňujú pohybovať sa po zozname zľava doprava.
Posledný uzol zoznamu nemá následníka &ndash; jeho smerník na následníka teda bude mať hodnotu <tt>NULL</tt>.  
+
** Posledný uzol zoznamu nemá následníka, smerník <tt>next</tt> bude mať hodnotu <tt>NULL</tt>.  
  
 
Štruktúra spájaného zoznamu je znázornená na nasledujúcom obrázku:
 
Štruktúra spájaného zoznamu je znázornená na nasledujúcom obrázku:
  
 
[[Image:PROG-list.png|300px]]
 
[[Image:PROG-list.png|300px]]
 
Keďže si v každom uzle pamätáme iba smerník na následníka (t.j. uzol &bdquo;napravo&rdquo; od daného uzla), hovoríme tiež o ''jednosmerne spájanom zozname''. Často sú užitočné aj ''obojsmerne spájané zoznamy'', kde sa v každom uzle uchováva aj smerník na predchodcu; takéto zoznamy sú ale o niečo náročnejšie na &bdquo;údržbu&rdquo;.
 
  
 
Uzol jednosmerne spájaného zoznamu budeme reprezentovať pomocou <tt>struct</tt>-u <tt>node</tt>:
 
Uzol jednosmerne spájaného zoznamu budeme reprezentovať pomocou <tt>struct</tt>-u <tt>node</tt>:
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
/* Struktura reprezentujuca uzol jednosmerne spajaneho zoznamu: */
+
/* Štruktúra reprezentujúca uzol jednosmerne spájaného zoznamu: */
 
struct node {
 
struct node {
     int data;    // Hodnota ulozena v danom uzle
+
     int data;    // Hodnota uložená v danom uzle
     node *next;  // Smernik na nasledujuci uzol
+
     node *next;  // Smerník na nasledujúci uzol
 
};
 
};
 
</syntaxhighlight>
 
</syntaxhighlight>
 
Vo vnútri definície typu <tt>node</tt> teda používame smerník na samotný typ <tt>node</tt>.
 
Vo vnútri definície typu <tt>node</tt> teda používame smerník na samotný typ <tt>node</tt>.
  
Na rozdiel od poľa, v ktorom je poradie stanovené indexmi, je poradie v spájanom zozname určované ukazovateľmi <tt>next</tt>.
+
Samotná množina reprezentovaná spájaným zoznamom je potom štruktúra <tt>set</tt> obsahujúca iba smerník na prvý prvok zoznamu. Ak je zoznam prázdny, bude tento smerník <tt>NULL</tt>.  
Ak <tt>x</tt> je uzol zoznamu, tak <tt>x.next</tt> je:
 
* Smerník na nasledujúci uzol zoznamu, ak takýto uzol existuje.
 
* <tt>NULL</tt> v opačnom prípade.
 
 
 
Samotný spájaný zoznam &ndash; resp. množina ním reprezentovaná &ndash; je potom iba štruktúra <tt>set</tt> obsahujúca smerník na prvý prvok zoznamu. Ak je zoznam prázdny, bude tento smerník <tt>NULL</tt>. V štruktúre <tt>set</tt> by sme v prípade potreby mohli uchovávať aj iné údaje, ako napríklad počet prvkov v zozname a podobne.  
 
  
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
/* Struktura realizujuca mnozinu prostrednictvom spajaneho zoznamu: */
+
/* Štruktúra implementujúca množinu ako spájaný zoznam: */
 
struct set {
 
struct set {
     node *first; // Smernik na prvy uzol zoznamu
+
     node *first; // Smerník na prvý uzol zoznamu
 
};
 
};
  
Riadok 328: Riadok 294:
 
===Vkladanie na začiatok zoznamu===
 
===Vkladanie na začiatok zoznamu===
  
Nasledujúca funkcia na začiatok zoznamu vloží nový uzol s dátami x:
+
Nasledujúca funkcia na začiatok zoznamu vloží nový uzol s dátami ''x'':
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
 
void add(set &s, int x) {
 
void add(set &s, int x) {
     node *p = new node; // Vytvorime novy uzol ...
+
    // Vytvoríme nový uzol, uložíme doňho x
     p->data = x;         // ... a jeho data nastavime na x.
+
     node * p = new node;  
     p->next = s.first;   // Naslednikom noveho uzla p v zozname bude doposial prvy prvok zoznamu ...
+
     p->data = x;
     s.first = p;        // ... a uzol p bude odteraz novym prvym prvkom v zozname.
+
    // uzol p bude novým prvým prvkom zoznamu
 +
     p->next = s.first;
 +
     s.first = p;         
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
Riadok 354: Riadok 322:
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
 +
Funkcia by sa dala napísať aj pomocou for cyklu
 +
<syntaxhighlight lang="C++">
 +
bool contains(set &s, int x) {
 +
    for(node *p = s.first; p != NULL; p = p->next) {
 +
        if (p->data == x) {
 +
            return true;
 +
        }
 +
    }
 +
    return false;
 +
}
 +
</syntaxhighlight>
 +
 +
V tejto forme sa viac podobá na funkciu pre polia:
 +
<syntaxhighlight lang="C++">
 +
bool contains(set &s, int x) {
 +
    for (int i = 0; i < s.length; i++) {
 +
        if (s.items[i] == x) {
 +
            return true;
 +
        }
 +
    }
 +
    return false;
 +
}
 +
</syntaxhighlight>
 +
 +
* Smerník <tt>p</tt> je teda v zoznamoch ekvivalentom indexu <tt>i</tt>
 +
* Inicializácia je <tt>p = s.first</tt> namiesto <tt>i = 0</tt>
 +
* Posun na ďalší prvok je <tt>p = p->next</tt> namiesto <tt>i++</tt>
 +
* Podmienka na pokračovanie je <tt>p != NULL</tt> namiesto <tt>i < s.length</tt>
  
 
=== Uvoľnenie zoznamu ===
 
=== Uvoľnenie zoznamu ===
  
Funkcia realizujúca uvoľnenie zoznamu z pamäti pracuje podobne &ndash; prechádza postupne zoznam od začiatku až po jeho koniec a uvoľňuje z pamäte jednotlivé uzly. Treba si tu však dať pozor na to, aby sme smerník na nasledujúci uzol získali ešte predtým, než z pamäti uvoľníme ten predošlý.   
+
Funkcia, ktorá uvoľní zoznam z pamäti, pracuje podobne: prechádza postupne zoznam od začiatku až po jeho koniec a uvoľňuje z pamäte jednotlivé uzly. Treba si však dať pozor na to, aby sme smerník na nasledujúci uzol získali ešte predtým, než z pamäti uvoľníme ten predošlý.   
  
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
Riadok 363: Riadok 360:
 
     node *p = s.first;
 
     node *p = s.first;
 
     while (p != NULL) {
 
     while (p != NULL) {
         node *nxt = p->next;
+
         node *p2 = p->next;
 
         delete p;
 
         delete p;
         p = nxt;
+
         p = p2;
 
     }
 
     }
 
}           
 
}           
Riadok 384: Riadok 381:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
== Hešovanie ==
+
=== Varianty spájaných zoznamov ===
 +
* V našom zozname si v každom uzle pamätáme iba smerník na následníka, hovoríme o ''jednosmerne spájanom zozname''.
 +
* Často sú užitočné aj ''obojsmerne spájané zoznamy'', kde sa v každom uzle uchováva aj smerník na predchodcu; takéto zoznamy sú ale o niečo náročnejšie na údržbu.
 +
* Používajú sa dokonca aj cyklické zoznamy, kde posledný prvok ukazuje späť na prvý prvok zoznamu.
 +
 
 +
== Hašovanie ==
 
=== Implementácia množiny priamym adresovaním ===
 
=== Implementácia množiny priamym adresovaním ===
  
Úplne odlišným spôsobom realizácie dynamickej množiny je tzv. ''priame adresovanie'' (angl. ''direct addressing''). Predpokladajme, že je univerzum všetkých hodnôt konečné &ndash; napríklad ''U'' = {0,1,...,''m''-1} pre nejaké prirodzené číslo ''m''; v praxi je skôr potrebný predpoklad, že ''U'' je dostatočne &bdquo;malé&rdquo; na to, aby sme mohli rozumne definovať pole o ''m'' prvkoch. V takom prípade môžeme množinu ''A'' prvkov ''U'' reprezentovať ako pole booleovských hodnôt dĺžky ''m'' zodpovedajúce tzv. ''charakteristickej funkcii'' množiny ''A''. To znamená, že ''i''-ty prvok tohto poľa bude <tt>true</tt> práve vtedy, keď ''i'' patrí do ''A''.
+
Úplne odlišným spôsobom implementácie dynamickej množiny je tzv. ''priame adresovanie'' (angl. ''direct addressing'').  
* Funkcie <tt>contains</tt> aj <tt>add</tt> sú veľmi rýchle.
+
* Množinu všetkých možných hodnôt, ktoré v danej implementácii môžeme chcieť do množiny pridať, nazveme univerzum ''U''.
* Problémom tohto prístupu je ale vysoká pamäťová zložitosť v prípade, že je číslo ''m'' veľké.
+
* Predchádzajúce implementácie sa ľahko dali upraviť na rôzne univerzá (celé čísla, desatinné čísla, smerníky na zložitejšie štruktúry, napr. struct, pole, reťazec).
* Veľmi efektívne pre malé univerzá (napr. cifry ''0,...,9'', všetky znaky anglickej abecedy, všetky znaky s ASCII hodnotami od 0 po 255, a pod.).
+
* Na rozdiel od toho sa priame adresovanie dá použiť iba ak univerzum je ''U'' = {0,1,...,''m''-1} pre nejaké rozumne malé prirodzené číslo ''m''.
 +
* Podmnožinu ''A'' univerza ''U'' potom môžeme reprezentovať ako pole booleovských hodnôt dĺžky ''m'', kde ''i''-ty prvok poľa bude <tt>true</tt> práve vtedy, keď ''i'' patrí do ''A''.
 +
* Túto reprezentáciu sme používali napríklad v poli <tt>bolo</tt> pri prehľadávaní s návratom.
 +
 
 +
* Funkcie <tt>contains</tt> aj <tt>add</tt> sú potom veľmi jednoduché a rýchle.
 +
* Problémom tohto prístupu je ale vysoká pamäťová zložitosť, ak je číslo ''m'' veľké.
 +
* Veľmi efektívne pre malé univerzá (napr. cifry 0,...,9, všetky znaky anglickej abecedy, všetky znaky s ASCII hodnotami od 0 po 255, a pod.).
  
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
 
#include <cassert>
 
#include <cassert>
  
// ...
+
/* Maximálne povolené číslo v množine */
 
+
const int m = 1000;
const int maxvalue = 1000;
 
  
/* Struktura realizujuca mnozinu prostrednictvom priameho adresovania: */
+
/* Štruktúra implementujúca množinu priamym adresovaním: */
 
struct set {
 
struct set {
     bool *p;
+
     bool *isin;
 
};
 
};
  
 
void init(set &s) {
 
void init(set &s) {
     s.p = new bool[maxvalue];
+
     s.isin = new bool[m];
     for (int i = 0; i <= maxvalue - 1; i++) {
+
     for (int i = 0; i < m; i++) {
         s.p[i] = false;
+
         s.isin[i] = false;
 
     }
 
     }
 
}
 
}
  
 
bool contains(set &s, int x) {
 
bool contains(set &s, int x) {
     assert(x >= 0 && x <= maxvalue - 1);
+
     assert(x >= 0 && x < m);
     return s.p[x];   
+
     return s.isin[x];   
 
}
 
}
  
 
void add(set &s, int x) {
 
void add(set &s, int x) {
     assert(x >= 0 && x <= maxvalue - 1);
+
     assert(x >= 0 && x < m);
     s.p[x] = true;
+
     s.isin[x] = true;
 
}
 
}
  
 
void destroy(set &s) {
 
void destroy(set &s) {
     delete[] s.p;
+
     delete[] s.isin;
 
}           
 
}           
 
</syntaxhighlight>
 
</syntaxhighlight>
  
=== Jednoduché hešovanie ===
+
=== Jednoduché hašovanie ===
  
Priame adresovanie sa príliš nehodí pre veľmi veľké univerzá &ndash; v takom prípade je totiž potrebné veľké množstvo pamäte.  
+
Priame adresovanie sa nehodí pre veľké univerzá, lebo by vyžadovalo veľa pamäte.  
  
''Hešovanie'' je jednoduchá finta, ktorá funguje nasledovne:
+
'''Hašovanie''' (angl. hashing) je jednoduchá finta, ktorá funguje nasledovne:
* Nech ''U'' je univerzum všetkých možných prvkov množiny (kľúčov). V nasledujúcom budeme pre jednoduchosť predpokladať, že ide o nejakú podmnožinu množiny celých čísel (prípadne priamo o množinu všetkých celých čísel).
+
* Nech ''U'' je univerzum všetkých možných prvkov množiny.  
* Vytvoríme tzv. ''hešovaciu tabuľku'' <tt>hashtable</tt> &ndash; pole nejakej rozumnej veľkosti ''m'', pozostávajúce z prvkov rovnakého typu ako prvky univerza (v našom prípade teda <tt>int</tt>).
+
* Vytvoríme '''hašovaciu tabuľku''' (angl. hash table), čo je pole nejakej rozumnej veľkosti ''m''.
* Naprogramujeme ''hešovaciu funkciu'', ktorá transformuje prvky univerza ''U'' na indexy hešovacej tabuľky; pôjde teda o funkciu ''h'': ''U'' -> {0, 1, ... , ''m''−1}.
+
* Naprogramujeme ''hašovaciu funkciu'', ktorá transformuje prvky univerza ''U'' na indexy hašovacej tabuľky; pôjde teda o funkciu ''h'': ''U'' -> {0, 1, ... , ''m''−1}.  
* Hešovacia funkcia by mala byť jednoduchá a rýchla, ale pritom by nemala &bdquo;často prideľovať rovnaké indexy&rdquo; (mala by kľúče do tabuľky distribuovať rovnomerne).  
+
* Najjednoduchšia hašovacia funkcia pre celočíselné prvky je ''h''(''x'') = |''x''| mod ''m''.  
* Najjednoduchšia hešovacia funkcia je ''h''(''x'') = ''x'' mod ''m'' (je dobré, ak je v tomto prípade ''m'' prvočíslo a nie je blízko mocniny 2).  
+
** |''x''| spočítame funkciou <tt>abs</tt> z knižnice <tt>cstdlib</tt>.
** ''Pozor'': napríklad <tt>-10 % 3</tt> je -1, takže radšej použijeme absolútnu hodnotu z ''x'' (funkcia <tt>abs</tt> z knižnice <tt>cstdlib</tt>).
+
** Absolútnu hodnotu používame, lebo napríklad <tt>-10 % 3</tt> je -1, čo mimo rozsahu indexov tabuľky.
 +
** V praxi sa používajú zložitejšie hašovacie funkcie. Ideálne je hašovacia funkcia jednoduchá a rýchla, ale pritom hodnoty do tabuľky distribuuje rovnomerne, aby sa príliš často nestávalo, že dva prvky sa namapujú na to isté políčko.
  
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
Riadok 444: Riadok 452:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Pre ''m = 5'' je táto funkcia znázornená na nasledujúcom obrázku.
+
Napríklad pre ''m = 5'': hash(12, 5) je 2, hash(-6, 5) je 1.
 
 
[[Súbor:hash1.png]]
 
  
S hešovacou tabuľkou teraz môžeme pracovať nasledovne:
+
Prvý pokus o prácu hašovacou tabuľkou by teda mohol vyzerať takto:
  
 
'''Vkladanie''' prvku ''x'':
 
'''Vkladanie''' prvku ''x'':
* Spočítame <tt>index = hash(x, m)</tt> a prvok vložíme na pozíciu <tt>hashtable[index]</tt>.
+
* Spočítame <tt>index = hash(x, m)</tt> a prvok vložíme na pozíciu <tt>table[index]</tt>.
  
 
'''Vyhľadávanie''' prvku ''x'':
 
'''Vyhľadávanie''' prvku ''x'':
Riadok 458: Riadok 464:
  
 
'''Problémy''':
 
'''Problémy''':
* Na akú hodnotu inicializovať prvky poľa <tt>hashtable</tt>?
+
* Na akú hodnotu inicializovať prvky poľa <tt>table</tt>?
 
* Čo ak budeme potrebovať vložiť prvok na miesto, kde je už niečo uložené?
 
* Čo ak budeme potrebovať vložiť prvok na miesto, kde je už niečo uložené?
  
 
=== Kolízie ===
 
=== Kolízie ===
  
Pri vkladaní prvku sme narazili na problém, ak na už obsadané miesto chceme vložiť iný prvok. Môže sa stať, že dva prvky ''x'' a ''y'' sa zahešujú na rovnakú pozíciu ''h''(''x'') = ''h''(''y''). Takémuto javu hovoríme ''kolízia'' a existuje viacero spôsobov, ako sa s ním vysporiadať. Naše riešenie bude spočívať v nasledujúcej myšlienke:
+
* Pri vkladaní prvku sme narazili na problém, ak na už obsadené miesto chceme vložiť iný prvok.  
* Kolidujúci prvok aj tak vložíme na vypočítané miesto v tabuľke, na ktorom už teda bude môcť byť uložený viac ako jeden prvok.
+
* Ak sa dva prvky ''x'' a ''y'' sa zahašujú na rovnakú pozíciu ''h''(''x'') = ''h''(''y''), hovoríme, že nastala '''kolízia'''.
* Prvky na danej pozícii hešovacej tabuľky budeme uchovávať v spájanom zozname.
+
* Existujú rôzne prístupy na riešenie kolízií, môžeme napríklad hľadať iné voľné miesto v tabuľke.
 
+
* V našom programe kolízie vyriešime tak, že v každom políčku tabuľky uložíme spájaný zoznam všetkých prvkov, ktoré sa tam zahašovali.  
Existujú však aj iné prístupy &ndash; môžeme napríklad hľadať iné voľné miesto v tabuľke a podobne (viac budúci rok na predmete ''Algoritmy a dátové štruktúry'').
+
** Táto situácia je znázornená na nasledujúcom obrázku, v ktorom sme do hašovacej tabuľky pre m=5 pridali prvky 0,2,5.
 
 
=== Riešenie kolízií pomocou spájaných zoznamov ===
 
 
 
V každom políčku hešovacej tabuľky teda budeme uchovávať spájaný zoznam so všetkými prvkami množiny, ktoré hešovacia funkcia priradila na toto políčko. Táto situácia je znázornená na nasledujúcom obrázku,
 
v ktorom šípky zodpovedajú vkladaniam prvkov (celých čísel) do množiny reprezentovanej hešovacou tabuľkou.
 
  
 
[[Súbor:Hash2.png]]
 
[[Súbor:Hash2.png]]
Riadok 479: Riadok 480:
 
#include <cstdlib>
 
#include <cstdlib>
  
// ...
+
/* hašovacia funkcia: */
 
+
int h(int x, int m) {
/* Hesovacia funkcia: */
 
int hash(int x, int m) {
 
 
     return abs(x) % m;
 
     return abs(x) % m;
 
}
 
}
  
/* Struktura reprezentujuca jeden prvok spajaneho zoznamu: */
+
/* štruktúra reprezentujúca jeden prvok spájaného zoznamu: */
 
struct node {
 
struct node {
 
     int data;
 
     int data;
Riadok 492: Riadok 491:
 
};
 
};
  
/* Struktura realizujuca dynamicku mnozinu pomocou hesovania: */
+
/* štruktúra implementujúca dynamickú množinu hašovaním: */
 
struct set {
 
struct set {
     node **hashtable;  // Pole smernikov na zaciatky jednotlivych zoznamov
+
     node **table;  // pole smerníkov na začiatky zoznamov
     int m;             // Velkost hesovacej tabulky 
+
     int m;         // veľkost hašovacej tabuľky
 
};
 
};
  
void init(set &s, int m) {  // Velkost tabulky bude parametrom funkcie init
+
void init(set &s, int m) {   
 +
    // veľkosť tabuľky je parametrom funkcie init
 
     s.m = m;
 
     s.m = m;
     s.hashtable = new node *[m];
+
     s.table = new node *[m];
     for (int i = 0; i <= m - 1; i++) {
+
     for (int i = 0; i < m; i++) {
         s.hashtable[i] = NULL;
+
         s.table[i] = NULL;
 
     }
 
     }
 
}
 
}
  
 
bool contains(set &s, int x) {
 
bool contains(set &s, int x) {
     int index = hash(x, s.m);     // Spocitame spravne policko hesovacej tabulky
+
    // spočítame políčko tabuľky
     node *p = s.hashtable[index]; // Smernik p ukazuje na prvy prvok spajaneho
+
     int index = h(x, s.m);    
                                  // zoznamu na danom policku
+
     node *p = s.table[index];
     while (p != NULL) {           // Prechadzame zoznam, hladame x
+
    // p ukazuje na začiatok zoznamu
 +
     while (p != NULL) {        
 +
        // prechádzame zoznam, hľadáme x
 
         if (p->data == x) {
 
         if (p->data == x) {
 
             return true;
 
             return true;
Riadok 520: Riadok 522:
  
 
void add(set &s, int x) {
 
void add(set &s, int x) {
     int index = hash(x, s.m);       // Spocitame spravne policko hesovacej tabulky
+
    // spočítame políčko tabuľky
     node *temp = new node;           // Vytvorime novy uzol do spajaneho zoznamu
+
     int index = h(x, s.m);
     temp->data = x;              
+
    // vytvoríme nový uzol
     temp->next = s.hashtable[index]; // Vlozime uzol temp na zaciatok zoznamu.
+
     node *p = new node;        
     s.hashtable[index] = temp;       
+
     p->data = x;
 +
     // uzol vložíme na začiatok zoznamu
 +
    p->next = s.table[index];  
 +
     s.table[index] = p;       
 
}
 
}
  
 
void destroy(set &s) {
 
void destroy(set &s) {
     for (int i = 0; i <= s.m - 1; i++) {
+
     for (int i = 0; i < s.m; i++) {
         node *p = s.hashtable[i];  // Uvolni zoznam s.hashtable[i]
+
        // uvoľníme zoznam s.table[i]
 +
         node *p = s.table[i];   
 
         while (p != NULL) {
 
         while (p != NULL) {
             node *nxt = p->next;
+
             node *p2 = p->next;
 
             delete p;
 
             delete p;
             p = nxt;
+
             p = p2;
 
         }
 
         }
 
     }
 
     }
     delete[] s.hashtable;
+
     delete[] s.table;
 
}           
 
}           
 
</syntaxhighlight>
 
</syntaxhighlight>
  
'''Cvičenie:''' Ako bude vyzerať hešovacia tabuľka pri riešení kolízií pomocou spájaných zoznamov, ak hešovacia funkcia je |x| mod 5 a vkladáme prvky 13, -2, 0, 8, 10, 17?
+
'''Cvičenie:''' Ako bude vyzerať hašovacia tabuľka pri riešení kolízií pomocou spájaných zoznamov, ak hašovacia funkcia je |x| mod 5 a vkladáme prvky 13, -2, 0, 8, 10, 17?
  
 
===Zložitosť===
 
===Zložitosť===
  
* Rýchlosť závisí od toho, akú máme veľkosť tabuľky ''m'', hešovaciu funkciu a koľko prvkov sa zahešuje do jedného políčka.
+
* Rýchlosť závisí od veľkosti tabuľky ''m'', hašovacej funkcie a počtu kolízií.
* V najhoršom prípade sa všetky prvky zahešujú do toho istého políčka, a teda musíme pri hľadaní prejsť všetky prvky množiny.
+
* V najhoršom prípade sa všetky prvky zahašujú do toho istého políčka, a teda musíme pri hľadaní prejsť všetky prvky množiny.
 
* Ak máme šťastie a v každom políčku máme len málo prvkov, bude aj vyhľadávanie rýchle.
 
* Ak máme šťastie a v každom políčku máme len málo prvkov, bude aj vyhľadávanie rýchle.
** Ak je tabuľka dosť veľká a hešovacia funkcia vhodne zvolená, tento prípad je pomerne obvyklý.
+
** Ak je tabuľka dosť veľká a hašovacia funkcia vhodne zvolená, tento prípad je pomerne obvyklý.
** Hešovacie tabuľky sa často používajú v praxi.
+
** Hašovacie tabuľky sa často používajú v praxi.
* Viac budúci rok na predmete ''Algoritmy a dátové štruktúry''.
+
* Viac budúci rok na predmete Algoritmy a dátové štruktúry.
 +
 
 +
== Zhrnutie ==
 +
 
 +
* Videli sme abstraktný dátový typ dynamická množina.
 +
* Implementovali sme ho pomocou neutriedených a utriedených polí, spájaných zoznamov, priamym adresovaním (ak prvky sú napr. malé celé čísla) a hašovaním.
 +
* Ďalšie dve implementácie uvidíme na konci semestra.
 +
* Spájané zoznamy sú ďalším príkladom využitia smerníkov. Ich výhodou je možnosť rýchlo pridávať a uberať uzly na začiatku zoznamu, alebo aj na ľubovoľnom mieste zoznamu, pokiaľ máme smerník na predchodcu. Nevieme však rýchlo pristupovať k prvku na danej pozícii. Prácu so zoznamami si precvičíme na cvičeniach tento a budúci týždeň.
 +
* Na zamyslenie: pri jednoduchej implementácii množiny v poli sme skúsili dve možnosti: utriedenú a neutriedenú. Malo by zmysel uvažovať aj utriedený spájaný zoznam?

Aktuálna revízia z 19:43, 10. november 2024

Oznamy

  • V stredu je dekanské voľno, prednáška nebude.
  • Piatkové cvičenia sú povinné pre tých, ktorí na cvičeniach v utorok nezískajú aspoň 4 body. Informácia bude na testovači.
    • Väčšina príkladov bude na dvojrozmerné polia, vyskytnú sa aj spájané zoznamy z dnešnej prednášky.
  • DÚ2 odovzdávajte do utorka 19.11. 22:00.
  • Pripomíname, že semestrálny test bude v stredu 11.12. o 18:10. Opravný termín v januári.
    • Ak sa vyskytne konflikt s iným predmetom, hláste to vyučujúcim čo najskôr.

Dynamická množina

Motivačný príklad

  • Na fakulte sa dvere do niektorých miestností otvárajú priložením čipovej karty k čítačke.
  • Každá karta má v sebe uložené identifikačné číslo.
  • Čítačka má v pamäti zoznam identifikačných čísel oprávnených osôb (študenti, vyučujúci a pod.).
  • Po priložení karty z nej prečíta číslo a zisťuje, či ho má vo svojom zozname.
  • Administrátor tiež potrebuje vedieť pridávať a uberať oprávnené osoby.
  • Ako asi môže byť systém pracujúci so zoznamom identifikačných čísel naprogramovaný?

Dynamická množina

Chceli by sme vytvoriť dátovú štruktúru s nasledujúcou špecifikáciou.

  • Máme množinu A, ktorá sa bude postupne meniť, preto ju nazývame dynamická množina.
  • Funkcia contains dostane množinu A a hodnotu x a zistí, či x patrí do A.
  • Funkcia add dostane množinu A a hodnotu x a pridá x do A.
  • Funkcia remove dostane množinu A a prvok x a odoberie x z A.
  • Pre jednoduchosť funkciu remove nebudeme dnes uvažovať.
  • Niekedy sa môžu zísť aj iné operácie.

Problém príslušnosti k množine sa vyskytuje aj v mnohých iných situáciách.

  • Ako dnes uvidíme, dynamickú množinu môžeme implementovať rôznymi spôsobmi.
  • Hovoríme, že dynamická množina je abstraktný dátový typ, špecifikuje totiž iba rozhranie, ktoré má dátová štruktúra poskytovať používateľovi, nie jeho implementáciu.
  • Ak by sme zmenili implementáciu z jednej na inú, nemusíme nutne meniť programy, ktoré dynamickú množinu využívajú, pokiaľ k nej pristupujú iba pomocou uvedených funkcií.

Implementácie dynamických množín

  • Pre jednoduchosť budeme uvažovať iba dynamickú množinu celých čísel.
  • Dynamickú množinu budeme uchovávať v štruktúre set.
  • Navyše budeme mať implementovaných niekoľko funkcií, ktoré s dynamickými množinami pracujú.
  • Kostra programu teda bude vyzerať pre ľubovoľnú implementáciu dynamickej množiny takto:
/* Štruktúra reprezentujúca dynamickú množinu. */
struct set {
    // ...
};

/* Funkcia vytvorí prázdnu dynamickú množinu. */
void init(set &s) {
    // ...
}

/* Funkcia zistí, či prvok x patrí do množiny s. */
bool contains(set &s, int x) {
    // ...
}

/* Funkcia pridá prvok x do množiny s. */
void add(set &s, int x) {
    // ...
}

/* Funkcia uvoľní množinu s z pamäte. */
void destroy(set &s) {
    // ...
}

Bez ohľadu na implementáciu štruktúry set a uvedených funkcií už teraz môžeme napísať program, ktorý ich využíva. Z konzoly číta príkazy a postupne ich vykonáva.

#include <iostream>
#include <cstring>
using namespace std;

// ...

const int maxlength = 100;

int main(void) {
    set A;
    init(A);
    
    while (true) {
        char prikaz[maxlength];
        cin.width(maxlength);
        cin >> prikaz;
        if (strcmp(prikaz, "contains") == 0) {
            int x;
            cin >> x;
            cout << contains(A, x) << endl;        
        } else if (strcmp(prikaz, "add") == 0) {
            int x;
            cin >> x;
            add(A, x);
        } else if (strcmp(prikaz, "end") == 0) {    
            break;
        }
    }

    destroy(A);
}

Ukážeme si teraz niekoľko rôznych implementácii dynamickej množiny; začneme s dvoma, ktoré sú nám už známe.

Dynamická množina ako pole

Dynamickú množinu môžeme implementovať tak, že jej prvky budeme ukladať do poľa v ľubovoľnom poradí.

  • Funkcia contains musí zakaždým prejsť celé pole lineárnym prehľadávaním (nie je teda zrovna rýchla).
  • Funkcia add je naopak veľmi rýchla: stačí pridať prvok na koniec poľa.
  • Je ale potrebné dávať pozor na prekročenie kapacity poľa (mohli by sme použiť dynamické pole).
#include <cassert>

// ...

const int maxN = 1000;

struct set {
    int *items;  // Smerník na nultý prvok poľa
    int length;  // Počet prvkov v poli
};

void init(set &s) {
    s.items = new int[maxN];
    s.length = 0;
}

bool contains(set &s, int x) {
    for (int i = 0; i < s.length; i++) {
        if (s.items[i] == x) {
            return true;
        }
    }
    return false;
}

void add(set &s, int x) {
    assert(s.length < maxN);
    s.items[s.length] = x;
    s.length++;
}

void destroy(set &s) {
    delete[] s.items;
}

Dynamická množina ako utriedené pole

Prvky množiny môžeme v poli uchovávať aj utriedené od najmenšieho po najväčšie.

  • Funkcia contains potom môže použiť binárne vyhľadávanie. Je teda rýchlejšia, ako v predchádzajúcom prípade (v poli veľkosti n sa pozrie len na približne log2 n pozícií; napríklad pre miliónprvkové pole sa pozrieme asi na 20 prvkov poľa).
  • Funkcia add ale musí vložiť prvok na správne miesto v utriedenom poli; je teda o dosť pomalšia.
  • Tento spôsob by teda bol vhodný, ak sa množina mení zriedkavo.
#include <cassert>

// ...

const int maxN = 1000;

struct set {
    int *items;  // smerník na nultý prvok poľa
    int length;  // počet prvkov v poli
};

void init(set &s) {
    s.items = new int[maxN];
    s.length = 0;
}

bool contains(set &s, int x) {
    int left = 0;
    int right = s.length - 1;
    while (left <= right) {
        int index = (left + right) / 2;
        if (s.items[index] == x) {
            return true;
        } else if (s.items[index] > x) {
            right = index - 1;
        } else {
            left = index + 1;
        }    
    }
    return false;
}

void add(set &s, int x) {
    assert(s.length < maxN);
    int kam = s.length;
    while (kam > 0 && s.items[kam - 1] > x) {
        s.items[kam] = s.items[kam - 1];
        kam--;
    }
    s.items[kam] = x;
    s.length++;
}

void destroy(set &s) {
    delete[] s.items;
}

Ďalšie možnosti implementácie dynamickej množiny (plán na dnes)

Dnes uvidíme ďalšie dva spôsoby implementácie dynamickej množiny:

  • Množina ako spájaný zoznam:
    • Ľahko pridáme nové prvky, nepotrebujeme vopred vedieť veľkosť.
    • Nedá sa rýchlo binárne vyhľadávať.
    • Založené na smerníkoch.
  • Množina pomocou hašovania:
    • Často veľmi rýchle vyhľadávanie.
    • Použijeme polia aj spájané zoznamy.

Odbočka: smerníky a struct

Opakovanie základnej práce so smerníkmi

int n = 7;         // premenná typu int 
int * p = &n;      // smerník na int 
// n, *p teraz znamenajú to isté

int * p2 = new int; // p2 ukazuje na alokovanú pamäť
*p2 = 7;            // cez *p2 pracujeme s touto pamäťou
delete p2;          // uvoľníme pamäť
p2  = p;            // meníme samotný smerník
p = NULL;           // NULL "nikam neukazuje"

Smerníky a struct

Smerník môže ukazovať aj na struct. Operátory . (prístup k prvku štruktúry) a [] (prístup k prvku poľa) majú vyššiu prioritu ako operátory * (dereferencia smerníka) a & (adresa). Preto napríklad:

  • Zápis *s.cokolvek je to isté ako *(s.cokolvek) a vyjadruje dereferenciu smerníka s.cokolvek.
  • Zápis (*p).cokolvek vyjadruje prvok cokolvek štruktúry získanej dereferenciou smerníka p.
  • Zvyčajne je potrebnejší zápis (*p).cokolvek; existuje preň preto skratka p->cokolvek.
struct bod {
  int x, y;
};

// ...

bod b; 
b.x = 0;
b.y = 0;
bod *p = &b;       // p ukazuje na bod b

bod *p2 = new bod; // alokovanie nového bodu
(*p2).x = 20;      // bod, na ktorý ukazuje p2, bude mať x 20
p2->y = 10;        // bod, na ktorý ukazuje p2, bude mať y 10
delete p2;         // uvoľnenie pamäte


Spájané zoznamy

Spájaný zoznam (angl. linked list) je postupnosť uzlov rovnakého typu usporiadaných za sebou. Každý uzol pritom pozostáva z dvoch častí:

  • Samotné dáta; v našom prípade jedno číslo typu int.
  • Smerník next, ktorý ukazuje na nasledujúci prvok zoznamu.
    • Tieto smerníky umožňujú pohybovať sa po zozname zľava doprava.
    • Posledný uzol zoznamu nemá následníka, smerník next bude mať hodnotu NULL.

Štruktúra spájaného zoznamu je znázornená na nasledujúcom obrázku:

PROG-list.png

Uzol jednosmerne spájaného zoznamu budeme reprezentovať pomocou struct-u node:

/* Štruktúra reprezentujúca uzol jednosmerne spájaného zoznamu: */
struct node {
    int data;    // Hodnota uložená v danom uzle
    node *next;  // Smerník na nasledujúci uzol
};

Vo vnútri definície typu node teda používame smerník na samotný typ node.

Samotná množina reprezentovaná spájaným zoznamom je potom štruktúra set obsahujúca iba smerník na prvý prvok zoznamu. Ak je zoznam prázdny, bude tento smerník NULL.

/* Štruktúra implementujúca množinu ako spájaný zoznam: */
struct set {
    node *first; // Smerník na prvý uzol zoznamu
};

void init(set &s) {
    s.first = NULL;
}

Vkladanie na začiatok zoznamu

Nasledujúca funkcia na začiatok zoznamu vloží nový uzol s dátami x:

void add(set &s, int x) {
    // Vytvoríme nový uzol, uložíme doňho x
    node * p = new node; 
    p->data = x;
    // uzol p bude novým prvým prvkom zoznamu
    p->next = s.first;  
    s.first = p;         
}

Vyhľadávanie v zozname

Funkcia vyhľadávajúca číslo x v zozname bude pracovať tak, že postupne prehľadáva zoznam od jeho začiatku, s využitím smerníkov na nasledujúce prvky:

bool contains(set &s, int x) {
    node *p = s.first;
    while (p != NULL) {
        if (p->data == x) {
            return true;
        } 
        p = p->next;
    }
    return false;
}

Funkcia by sa dala napísať aj pomocou for cyklu

bool contains(set &s, int x) {
    for(node *p = s.first; p != NULL; p = p->next) {
        if (p->data == x) {
            return true;
        } 
    }
    return false;
}

V tejto forme sa viac podobá na funkciu pre polia:

bool contains(set &s, int x) {
    for (int i = 0; i < s.length; i++) {
        if (s.items[i] == x) {
            return true;
        }
    }
    return false;
}
  • Smerník p je teda v zoznamoch ekvivalentom indexu i
  • Inicializácia je p = s.first namiesto i = 0
  • Posun na ďalší prvok je p = p->next namiesto i++
  • Podmienka na pokračovanie je p != NULL namiesto i < s.length

Uvoľnenie zoznamu

Funkcia, ktorá uvoľní zoznam z pamäti, pracuje podobne: prechádza postupne zoznam od začiatku až po jeho koniec a uvoľňuje z pamäte jednotlivé uzly. Treba si však dať pozor na to, aby sme smerník na nasledujúci uzol získali ešte predtým, než z pamäti uvoľníme ten predošlý.

void destroy(set &s) {
    node *p = s.first;
    while (p != NULL) {
        node *p2 = p->next;
        delete p;
        p = p2;
    }
}

Výpis zoznamu

Môžeme napísať aj nasledujúcu funkciu, ktorá po zavolaní vypíše obsah celého zoznamu:

void print(set &s) {
    node *p = s.first;
    while (p != NULL) {
        cout << p->data << " ";
        p = p->next;
    }   
    cout << endl; 
}

Varianty spájaných zoznamov

  • V našom zozname si v každom uzle pamätáme iba smerník na následníka, hovoríme o jednosmerne spájanom zozname.
  • Často sú užitočné aj obojsmerne spájané zoznamy, kde sa v každom uzle uchováva aj smerník na predchodcu; takéto zoznamy sú ale o niečo náročnejšie na údržbu.
  • Používajú sa dokonca aj cyklické zoznamy, kde posledný prvok ukazuje späť na prvý prvok zoznamu.

Hašovanie

Implementácia množiny priamym adresovaním

Úplne odlišným spôsobom implementácie dynamickej množiny je tzv. priame adresovanie (angl. direct addressing).

  • Množinu všetkých možných hodnôt, ktoré v danej implementácii môžeme chcieť do množiny pridať, nazveme univerzum U.
  • Predchádzajúce implementácie sa ľahko dali upraviť na rôzne univerzá (celé čísla, desatinné čísla, smerníky na zložitejšie štruktúry, napr. struct, pole, reťazec).
  • Na rozdiel od toho sa priame adresovanie dá použiť iba ak univerzum je U = {0,1,...,m-1} pre nejaké rozumne malé prirodzené číslo m.
  • Podmnožinu A univerza U potom môžeme reprezentovať ako pole booleovských hodnôt dĺžky m, kde i-ty prvok poľa bude true práve vtedy, keď i patrí do A.
  • Túto reprezentáciu sme používali napríklad v poli bolo pri prehľadávaní s návratom.
  • Funkcie contains aj add sú potom veľmi jednoduché a rýchle.
  • Problémom tohto prístupu je ale vysoká pamäťová zložitosť, ak je číslo m veľké.
  • Veľmi efektívne pre malé univerzá (napr. cifry 0,...,9, všetky znaky anglickej abecedy, všetky znaky s ASCII hodnotami od 0 po 255, a pod.).
#include <cassert>

/* Maximálne povolené číslo v množine */
const int m = 1000;

/* Štruktúra implementujúca množinu priamym adresovaním: */
struct set {
    bool *isin;
};

void init(set &s) {
    s.isin = new bool[m];
    for (int i = 0; i < m; i++) {
        s.isin[i] = false;
    }
}

bool contains(set &s, int x) {
    assert(x >= 0 && x < m);
    return s.isin[x];  
}

void add(set &s, int x) {
    assert(x >= 0 && x < m);
    s.isin[x] = true;
}

void destroy(set &s) {
    delete[] s.isin;
}

Jednoduché hašovanie

Priame adresovanie sa nehodí pre veľké univerzá, lebo by vyžadovalo veľa pamäte.

Hašovanie (angl. hashing) je jednoduchá finta, ktorá funguje nasledovne:

  • Nech U je univerzum všetkých možných prvkov množiny.
  • Vytvoríme hašovaciu tabuľku (angl. hash table), čo je pole nejakej rozumnej veľkosti m.
  • Naprogramujeme hašovaciu funkciu, ktorá transformuje prvky univerza U na indexy hašovacej tabuľky; pôjde teda o funkciu h: U -> {0, 1, ... , m−1}.
  • Najjednoduchšia hašovacia funkcia pre celočíselné prvky je h(x) = |x| mod m.
    • |x| spočítame funkciou abs z knižnice cstdlib.
    • Absolútnu hodnotu používame, lebo napríklad -10 % 3 je -1, čo mimo rozsahu indexov tabuľky.
    • V praxi sa používajú zložitejšie hašovacie funkcie. Ideálne je hašovacia funkcia jednoduchá a rýchla, ale pritom hodnoty do tabuľky distribuuje rovnomerne, aby sa príliš často nestávalo, že dva prvky sa namapujú na to isté políčko.
int hash(int x, int m) {
    return abs(x) % m;
}

Napríklad pre m = 5: hash(12, 5) je 2, hash(-6, 5) je 1.

Prvý pokus o prácu hašovacou tabuľkou by teda mohol vyzerať takto:

Vkladanie prvku x:

  • Spočítame index = hash(x, m) a prvok vložíme na pozíciu table[index].

Vyhľadávanie prvku x:

  • Ak je prvok s kľúčom x v tabuľke, musí byť na indexe hash(x, m).
  • Skontrolujeme túto pozíciu a ak tam je niečo iné ako x, prvok x sa v tabuľke nenachádza.

Problémy:

  • Na akú hodnotu inicializovať prvky poľa table?
  • Čo ak budeme potrebovať vložiť prvok na miesto, kde je už niečo uložené?

Kolízie

  • Pri vkladaní prvku sme narazili na problém, ak na už obsadené miesto chceme vložiť iný prvok.
  • Ak sa dva prvky x a y sa zahašujú na rovnakú pozíciu h(x) = h(y), hovoríme, že nastala kolízia.
  • Existujú rôzne prístupy na riešenie kolízií, môžeme napríklad hľadať iné voľné miesto v tabuľke.
  • V našom programe kolízie vyriešime tak, že v každom políčku tabuľky uložíme spájaný zoznam všetkých prvkov, ktoré sa tam zahašovali.
    • Táto situácia je znázornená na nasledujúcom obrázku, v ktorom sme do hašovacej tabuľky pre m=5 pridali prvky 0,2,5.

Hash2.png

#include <cstdlib>

/* hašovacia funkcia: */
int h(int x, int m) {
    return abs(x) % m;
}

/* štruktúra reprezentujúca jeden prvok spájaného zoznamu: */
struct node {
    int data;
    node *next;
};

/* štruktúra implementujúca dynamickú množinu hašovaním: */
struct set {
    node **table;  // pole smerníkov na začiatky zoznamov
    int m;         // veľkost hašovacej tabuľky
};

void init(set &s, int m) {  
    // veľkosť tabuľky je parametrom funkcie init
    s.m = m;
    s.table = new node *[m];
    for (int i = 0; i < m; i++) {
        s.table[i] = NULL;
    }
}

bool contains(set &s, int x) {
    // spočítame políčko tabuľky
    int index = h(x, s.m);      
    node *p = s.table[index];
    // p ukazuje na začiatok zoznamu
    while (p != NULL) {         
        // prechádzame zoznam, hľadáme x
        if (p->data == x) {
            return true;
        }
        p = p->next;
    } 
    return false;    
}

void add(set &s, int x) {
    // spočítame políčko tabuľky
    int index = h(x, s.m);
    // vytvoríme nový uzol
    node *p = new node;         
    p->data = x;
    // uzol vložíme na začiatok zoznamu
    p->next = s.table[index];   
    s.table[index] = p;       
}

void destroy(set &s) {
    for (int i = 0; i < s.m; i++) {
        // uvoľníme zoznam s.table[i]
        node *p = s.table[i];   
        while (p != NULL) {
            node *p2 = p->next;
            delete p;
            p = p2;
        }
    }
    delete[] s.table;
}

Cvičenie: Ako bude vyzerať hašovacia tabuľka pri riešení kolízií pomocou spájaných zoznamov, ak hašovacia funkcia je |x| mod 5 a vkladáme prvky 13, -2, 0, 8, 10, 17?

Zložitosť

  • Rýchlosť závisí od veľkosti tabuľky m, hašovacej funkcie a počtu kolízií.
  • V najhoršom prípade sa všetky prvky zahašujú do toho istého políčka, a teda musíme pri hľadaní prejsť všetky prvky množiny.
  • Ak máme šťastie a v každom políčku máme len málo prvkov, bude aj vyhľadávanie rýchle.
    • Ak je tabuľka dosť veľká a hašovacia funkcia vhodne zvolená, tento prípad je pomerne obvyklý.
    • Hašovacie tabuľky sa často používajú v praxi.
  • Viac budúci rok na predmete Algoritmy a dátové štruktúry.

Zhrnutie

  • Videli sme abstraktný dátový typ dynamická množina.
  • Implementovali sme ho pomocou neutriedených a utriedených polí, spájaných zoznamov, priamym adresovaním (ak prvky sú napr. malé celé čísla) a hašovaním.
  • Ďalšie dve implementácie uvidíme na konci semestra.
  • Spájané zoznamy sú ďalším príkladom využitia smerníkov. Ich výhodou je možnosť rýchlo pridávať a uberať uzly na začiatku zoznamu, alebo aj na ľubovoľnom mieste zoznamu, pokiaľ máme smerník na predchodcu. Nevieme však rýchlo pristupovať k prvku na danej pozícii. Prácu so zoznamami si precvičíme na cvičeniach tento a budúci týždeň.
  • Na zamyslenie: pri jednoduchej implementácii množiny v poli sme skúsili dve možnosti: utriedenú a neutriedenú. Malo by zmysel uvažovať aj utriedený spájaný zoznam?