Programovanie (2) v Jave
1-INF-166, letný semester 2023/24

Prednášky · Pravidlá · Softvér · Testovač
· Vyučujúcich predmetu možno kontaktovať mailom na adresách uvedených na hlavnej stránke. Hromadná mailová adresa zo zimného semestra v letnom semestri nefunguje.
· JavaFX: cesta k adresáru lib je v počítačových učebniach /usr/share/openjfx/lib.


Prednáška 17: Rozdiel medzi revíziami

Z Programovanie
Skočit na navigaci Skočit na vyhledávání
d
 
(25 medziľahlých úprav od 2 ďalších používateľov nie je zobrazených)
Riadok 1: Riadok 1:
 
== Oznamy ==
 
== Oznamy ==
* Budúci týždeň budú cvičenia v utorok a v piatok iba online, nebude rozcvička, iba príklady, ktoré môžete riešiť do ďalšieho utorka
+
* Domácu úlohu 3 odovzdávajte do 5. decembra, 22:00.
* Na testovači pribudlo zadanie tretej (a poslednej) domácej úlohy s odovzdaním do 6. decembra, 22:00.
+
* Zajtrajšia rozcvička bude zo súborov, pozrite si prednášky z minulého týždňa.
* Tento piatok sa ešte dá prísť na cvičenia osobne, v prípade záujmu si dajte pozor, aby ste boli zapísaní v tabuľke
+
* Piatkové cvičenia sú povinné pre tých, ktorí nevyriešia v utorok počas cvičení úspešne rozcvičku.
* Blíži sa '''semestrálny test'''
+
* Blíži sa '''semestrálny test'''.
** Treba z neho získať aspoň 50% bodov, inak známka Fx
+
** Bude sa konať v stredu v stredu 13.12. o 18:10.
** Opravný termín bude v januári (iba jeden)
+
** Treba z neho získať aspoň 50% bodov, inak známka Fx.
** Pôvodne bola písomka ohlásená na stredu 8.12. večer, ale presúvame ju na '''piatok 10.12. počas doplnkových cvičení o 11:30'''
+
** Opravný termín bude v januári (iba jeden).
** Ak máte s týmto presunom problém, dajte nám vedieť dnes alebo zajtra
+
** Viac podrobností zverejníme budúci týždeň.
** Večerné termíny bývajú pre študentov náročné
 
** Ak bude umožnená prezenčná výučba, písomka bude na fakukulte, inak online
 
  
 
== Abstraktný dátový typ ==
 
== Abstraktný dátový typ ==
Riadok 28: Riadok 26:
 
* Napríklad program pracujúci s dynamickou množinou prostredníctvom funkcií <tt>contains</tt>, <tt>add</tt> a <tt>remove</tt> možno rovnako dobre použiť pri implementácii množiny pomocou neutriedených polí, ako pri jeho implementácii pomocou hešovania.  
 
* Napríklad program pracujúci s dynamickou množinou prostredníctvom funkcií <tt>contains</tt>, <tt>add</tt> a <tt>remove</tt> možno rovnako dobre použiť pri implementácii množiny pomocou neutriedených polí, ako pri jeho implementácii pomocou hešovania.  
  
Témou dnešnej prednášky sú dva nové abstraktné dátové typy: '''zásobník''' (angl. '''stack''') a '''rad''' alebo '''front''' (angl. '''queue''').
+
Témou dnešnej prednášky sú dva nové abstraktné dátové typy.
  
 
== Rad a zásobník ==
 
== Rad a zásobník ==
  
''Rad'' (často aj ''front''; angl. ''queue'') a ''zásobník'' (angl. ''stack'') sú jednoduché abstraktné dátové typy umožňujúce udržiavať postupnosť nejakých prvkov (typicky môže ísť o úlohy alebo dáta čakajúce na spracovanie). Rad aj zásobník poskytujú funkciu umožňujúcu ''vložiť'' do nich používateľom zadaný prvok. Druhou základnou funkciou poskytovanou oboma týmito abstraktnými dátovými typmi je ''výber'' jedného prvku z radu resp. zo zásobníka. Rad sa od zásobníka líši najmä správaním tejto druhej operácie.
+
* '''Zásobník''' (angl. '''stack''') a '''rad''' alebo '''front''' (angl. '''queue''') sú jednoduché abstraktné dátové typy, ktoré udržiavajú postupnosť nejakých prvkov.
 +
* Typicky ide o úlohy alebo dáta čakajúce na spracovanie.  
 +
* Rad aj zásobník poskytujú funkciu, ktorá vkladá nový prvok.  
 +
* Druhou základnou funkciou je výber jedného prvku, pričom rad sa od zásobníka líši tým, ktorý prvok sa vyberá.
  
===Rad===
+
Prvky radu a zásobníka môžu byť ľubovoľného typu. Namiesto konkrétneho typu (ako napríklad <tt>int</tt> alebo <tt>char</tt>) tak budeme pracovať so všeobecným typom, ktorý nazveme <tt>dataType</tt>. Za ten možno dosadiť ľubovoľný konkrétny typ &ndash; napríklad <tt>int</tt> dosadíme za <tt>dataType</tt> takto:
 
 
Z radu sa zakaždým vyberie ten jeho prvok, ktorý doň bol vložený ako prvý spomedzi jeho aktuálnych prvkov &ndash; možno ho tak pripodobniť k radu pri pokladni v obchode. Takáto metóda manipulácie s dátami sa v angličtine označuje skratkou FIFO, podľa ''first in, first out''.
 
 
 
Prvky radu môžu byť ľubovoľného typu. Namiesto konkrétneho typu (ako napríklad <tt>int</tt> alebo <tt>char</tt>) tak budeme pracovať so všeobecným typom, ktorý nazveme <tt>dataType</tt>. Za ten možno dosadiť ľubovoľný konkrétny typ &ndash; napríklad <tt>int</tt> dosadíme za <tt>dataType</tt> takto:
 
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
 
typedef int dataType;
 
typedef int dataType;
 
</syntaxhighlight>
 
</syntaxhighlight>
Pri využití tohto prístupu tak napríklad bude možné získať z radu prvkov typu <tt>int</tt> rad prvkov typu <tt>char</tt> zmenou v jedinom riadku programu.
+
* Pri využití tohto prístupu tak napríklad bude možné získať z radu prvkov typu <tt>int</tt> rad prvkov typu <tt>char</tt> zmenou v jedinom riadku programu.
 +
* Taká istá úprava by sa dala spraviť aj pri ADT množina a dynamické pole.
  
Abstraktný dátový typ pre rad môže poskytovať napríklad tieto operácie (kde <tt>queue</tt> je názov štruktúry reprezentujúcej rad):  
+
 
 +
===Rad===
 +
 
 +
* Z radu sa zakaždým vyberie ten jeho prvok, ktorý doň bol vložený ako prvý spomedzi jeho aktuálnych prvkov.
 +
* Možno ho tak pripodobniť k radu pri pokladni v obchode.
 +
* Takáto metóda manipulácie s dátami sa v angličtine označuje skratkou FIFO, podľa ''first in, first out''.
 +
 
 +
Abstraktný dátový typ pre rad poskytuje tieto operácie (kde <tt>queue</tt> je názov štruktúry reprezentujúcej rad):  
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
 
/* Inicializuje prazdny rad */
 
/* Inicializuje prazdny rad */
Riadok 67: Riadok 72:
 
===Zásobník===
 
===Zásobník===
  
Zo zásobníka sa naopak zakaždým vyberie ten prvok, ktorý doň bol vložený ako posledný. Táto metóda manipulácie s dátami sa v angličtine označuje skratkou LIFO, podľa ''last in, first out''.  
+
* Zo zásobníka sa naopak zakaždým vyberie ten prvok, ktorý doň bol vložený ako posledný.  
 +
* Môžeme si ho predstaviť ako stĺpec tanierov, kde umyté taniere dávame na vrch stĺpca a tiež z vrchu aj berieme taniere na použitie.
 +
* Táto metóda manipulácie s dátami sa v angličtine označuje skratkou LIFO, podľa ''last in, first out''.  
  
Abstraktný dátový typ pre zásobník tak môže poskytovať napríklad nasledujúce operácie (<tt>stack</tt> je názov štruktúry reprezentujúcej zásobník a opäť pracujeme s prvkami všeobecného typu <tt>dataType</tt>):
+
Abstraktný dátový typ pre zásobník poskytuje tieto operácie (<tt>stack</tt> je názov štruktúry reprezentujúcej zásobník):
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
 
/* Inicializuje prazdny zasobnik */
 
/* Inicializuje prazdny zasobnik */
Riadok 103: Riadok 110:
  
  
int main(void) {
+
int main() {
 
     queue q;
 
     queue q;
 
     init(q);
 
     init(q);
Riadok 113: Riadok 120:
 
     cout << dequeue(q) << endl;  // Vypise 3
 
     cout << dequeue(q) << endl;  // Vypise 3
 
     destroy(q);
 
     destroy(q);
    return 0;
 
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
Riadok 123: Riadok 129:
  
 
typedef int dataType;
 
typedef int dataType;
 
  
 
/* Sem pride definicia struktury stack a vsetkych potrebnych funkcii. */
 
/* Sem pride definicia struktury stack a vsetkych potrebnych funkcii. */
  
 
+
int main() {
int main(void) {
 
 
     stack s;
 
     stack s;
 
     init(s);
 
     init(s);
Riadok 138: Riadok 142:
 
     cout << pop(s) << endl;  // Vypise 1
 
     cout << pop(s) << endl;  // Vypise 1
 
     destroy(s);
 
     destroy(s);
    return 0;
 
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
 +
 +
Poznámka:
 +
* V objektovo-orientovanom programovaní (budúci semester) sa namiesto napr. <tt>push(s,10)</tt> píše niečo ako <tt>s.push(10)</tt>
  
 
==Implementácia zásobníka a radu==
 
==Implementácia zásobníka a radu==
  
 
===Zásobník pomocou poľa===
 
===Zásobník pomocou poľa===
 
+
* Na úvod implementujeme zásobník pomocou poľa <tt>items</tt>, ktoré budeme alokovať na fixnú dĺžku <tt>maxN</tt> (rovnako dobre by sme však mohli použiť aj dynamické pole).  
Na úvod implementujeme zásobník pomocou poľa <tt>items</tt>, ktoré budeme alokovať na fixnú dĺžku <tt>maxN</tt> (rovnako dobre by sme však mohli použiť aj dynamické pole). Spodok zásobníka pritom bude v tomto poli uložený na pozícii <tt>0</tt> a jeho vrch na pozícii <tt>top</tt>. V prípade, že je zásobník prázdny, bude hodnota premennej <tt>top</tt> rovná <tt>-1</tt>.
+
* Spodok zásobníka pritom bude v tomto poli uložený na pozícii <tt>0</tt> a jeho vrch na pozícii <tt>top</tt>.  
 +
* V prípade, že je zásobník prázdny, bude hodnota premennej <tt>top</tt> rovná <tt>-1</tt>.
  
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
 
#include <cassert>
 
#include <cassert>
  
// ...
+
typedef int dataType;
 
 
 
const int maxN = 1000;
 
const int maxN = 1000;
  
 
struct stack {
 
struct stack {
     dataType *items; // Alokujeme na pole reprezentujuce jednotlive prvky zasobnika.
+
     // Alokovane pole prvkov zasobnika.
     int top;         // Index vrchu zasobnika v poli items. Ak je zasobnik prazdny, ma hodnotu -1.
+
     dataType *items;  
 +
    // Index vrchu zasobnika v poli items, -1 ak prazdny
 +
    int top;       
 
};
 
};
  
Riadok 199: Riadok 208:
 
===Rad pomocou poľa===
 
===Rad pomocou poľa===
  
Rad sa od zásobníka líši tým, že prvky sa z neho vyberajú z opačnej strany, než sa doň vkladajú. Keby sa teda prvý prvok radu udržiaval na pozícii <tt>0</tt>, museli by sa po každom výbere prvku tie zvyšné posunúť o jednu pozíciu &bdquo;doľava&rdquo;, čo je časovo neefektívne. Rad teda implementujeme tak, aby jeho začiatok mohol byť na ľubovoľnej pozícii <tt>first</tt> poľa <tt>items</tt>. Pole <tt>items</tt> pritom budeme chápať ako cyklické &ndash; prvky s indexom menším ako <tt>first</tt> pritom budeme interpretovať ako nasledujúce za posledným prvkom poľa.   
+
* Rad sa od zásobníka líši tým, že prvky sa z neho vyberajú z opačnej strany, než sa doň vkladajú.  
 +
* Keby sa prvý prvok radu udržiaval na pozícii <tt>0</tt>, museli by sa po každom výbere prvku tie zvyšné posunúť o jednu pozíciu doľava, čo je časovo neefektívne.  
 +
* Rad teda implementujeme tak, aby jeho začiatok mohol byť na ľubovoľnej pozícii <tt>first</tt> poľa <tt>items</tt>.  
 +
* Pole <tt>items</tt> pritom budeme chápať ako cyklické &ndash; prvky s indexom menším ako <tt>first</tt> budeme implementovať ako nasledujúce za posledným prvkom poľa.   
  
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
 
#include <cassert>
 
#include <cassert>
  
// ...
+
typedef int dataType;
  
 
const int maxN = 1000;
 
const int maxN = 1000;
  
 
struct queue {
 
struct queue {
     dataType *items; // Alokujeme na pole reprezentujuce jednotlive prvky radu.
+
     dataType *items; // Alokovane pole obsahujuce prvky radu.
 
     int first;      // Index prveho prvku radu v poli items.
 
     int first;      // Index prveho prvku radu v poli items.
 
     int count;      // Pocet prvkov v rade.  
 
     int count;      // Pocet prvkov v rade.  
Riadok 257: Riadok 269:
 
===Zásobník pomocou spájaného zoznamu===
 
===Zásobník pomocou spájaného zoznamu===
  
Zásobník teraz interpretujeme pomocou spájaného zoznamu. Narozdiel od implementácie pomocou poľa bude výhodnejšie uchovávať vrch zásobníka ako ''prvý'' prvok zoznamu (pri jednosmerne spájaných zoznamoch je totiž jednoduchšie vkladať a odoberať prvky na jeho začiatku).   
+
* Zásobník teraz implementujeme pomocou spájaného zoznamu.  
 +
* Na rozdiel od implementácie pomocou poľa bude výhodnejšie uchovávať vrch zásobníka ako ''prvý'' prvok zoznamu.
 +
* Pri jednosmerne spájaných zoznamoch je totiž jednoduchšie vkladať a odoberať prvky na jeho začiatku.   
  
Výhoda tohto prístupu oproti implementácii pomocou poľa bude spočívať predovšetkým v tom, že maximálny počet prvkov v zásobníku nebude obmedzený konštantou <tt>maxN</tt>. Podobný efekt síce možno docieliť použitím dynamického poľa, jeho realokácia je však časovo neefektívna.
+
* Výhoda tohto prístupu oproti implementácii pomocou poľa je v tom, že maximálny počet prvkov v zásobníku nebude obmedzený konštantou <tt>maxN</tt>. Podobný efekt je možné docieliť aj dynamického poľa, tam však dochádza k realokácii.
 
   
 
   
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
 
#include <cassert>
 
#include <cassert>
  
// ...
+
typedef int dataType;
  
 
struct node {
 
struct node {
Riadok 317: Riadok 331:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
==Rad pomocou spájaného zoznamu==
+
===Rad pomocou spájaného zoznamu===
 
 
Pri implementácii radu pomocou spájaného zoznamu rozšírime spájané zoznamy zo [[Prednáška 14|14. prednášky]] o smerník <tt>last</tt> na posledný prvok zoznamu. Bude tak možné jednoducho vkladať prvky na koniec zoznamu, ako aj odoberať prvky zo začiatku zoznamu.
 
  
Výhodou oproti implementácii radu pomocou poľa bude, podobne ako pri zásobníkoch, eliminácia obmedzenia na maximálny počet prvkov v rade.
+
* Pri implementácii radu pomocou spájaného zoznamu rozšírime spájané zoznamy zo [[Prednáška_14#Sp.C3.A1jan.C3.A9_zoznamy|14. prednášky]] o smerník <tt>last</tt> na posledný prvok zoznamu.
 +
* Bude tak možné efektívne vkladať prvky na koniec zoznamu, ako aj odoberať prvky zo začiatku zoznamu.
 +
* Výhodou oproti implementácii radu pomocou poľa je, podobne ako pri zásobníkoch, eliminácia obmedzenia na maximálny počet prvkov v rade.
  
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
 
#include <cassert>
 
#include <cassert>
  
// ...
+
typedef int dataType;
  
 
struct node {
 
struct node {
Riadok 334: Riadok 348:
  
 
struct queue {
 
struct queue {
     node *first; // Smernik na prvy prvok radu (spajaneho zoznamu). Ak je rad prazdny, ma hodnotu NULL.
+
     // Smernik na prvy uzol. Ak je rad prazdny, ma hodnotu NULL.
     node *last; // Smernik na posledny prvok radu (spajaneho zoznamu). Ak je rad prazdny, ma hodnotu NULL.
+
     node *first;  
 +
    // Smernik na posledny uzol. Ak je rad prazdny, ma hodnotu NULL.
 +
    node *last; 
 
};
 
};
  
Riadok 391: Riadok 407:
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
 +
== Použitie zásobníka a radu ==
 +
Zásobník aj rad často uchovávajú dáta určené na spracovanie, zoznamy úloh, atď. Rad sa zvyčajne používa v prípadoch, keď je žiadúce zachovať ich poradie. Typicky môže ísť o situácie, keď jeden proces generuje úlohy spracúvané iným procesom, napríklad:
 +
* Textový procesor pripravuje strany na tlač a vkladá ich do radu, z ktorého ich tlačiareň (resp. jej ovládač) postupne vyberá.
 +
* Sekvenčne vykonávané výpočtové úlohy čakajú v rade na spustenie.
 +
* Zákazníci čakajú na zákazníckej linke na voľného operátora.
 +
* Pasažieri na standby čakajú na voľné miesto v lietadle.
 +
 +
Zásobník sa, ako o niečo implementačne jednoduchší koncept, zvyčajne používa v situáciách, keď na poradí spracúvania nezáleží, alebo keď je žiadúce vstupné poradie obrátiť. Najvýznamnejší príklad situácie druhého typu je nasledujúci:
 +
* Operačný systém ukladá lokálne premenné volaných funkcií na tzv. ''zásobníku volaní'' (angl. ''call stack''), čo umožňuje používanie rekurzie.
 +
* Rekurzívne programy sa dajú prepísať na nerekurzívne pomocou &bdquo;ručne vytvoreného&rdquo; zásobníka (neskôr si ukážeme nerekurzívnu verziu triedenia ''Quick Sort'').
 +
 +
=== Príklad: kontrola uzátvorkovania ===
 +
 +
Ako jednoduchý príklad na použitie zásobníka uvažujme nasledujúcu situáciu: na vstupe je daný reťazec pozostávajúci (okrem prípadných ďalších znakov, ktoré možno ignorovať) zo zátvoriek <tt>(,),[,],{,}</tt>. Úlohou je zistiť, či je tento reťazec dobre uzátvorkovaný. To znamená, že:
 +
* Pre každú uzatváraciu zátvorku musí byť posledná dosiaľ neuzavretá otváracia zátvorka rovnakého typu, pričom musí existovať aspoň jedna dosiaľ neuzavretá zátvorka.
 +
* Každá otváracia zátvorka musí byť niekedy neskôr uzavretá.
 +
 +
Príklady očakávaného vstupu a výstupu:
 +
<pre>
 +
()
 +
Retazec je dobre uzatvorkovany
 +
 +
nejaky text bez zatvoriek
 +
Retazec je dobre uzatvorkovany
 +
 +
[((({}[])[]))]()
 +
Retazec je dobre uzatvorkovany
 +
 +
[[#))
 +
Retazec nie je dobre uzatvorkovany
 +
 +
())(
 +
Retazec nie je dobre uzatvorkovany
 +
 +
((
 +
Retazec nie je dobre uzatvorkovany
 +
 +
((cokolvek
 +
Retazec nie je dobre uzatvorkovany
 +
</pre>
 +
 +
* Nasledujúci program postupne prechádza cez vstupný reťazec, pričom pre každú otváraciu zátvorku si na zásobník pridá uzatváraciu zátvorku rovnakého typu.
 +
* Ak narazí na uzatváraciu zátvorku, výraz môže byť dobre uzátvorkovaný len v prípade, že je na zásobníku aspoň jedna zátvorka, pričom zátvorka na vrchu zásobníka sa zhoduje so zátvorkou na vstupe.
 +
* V prípade úspešného prechodu cez celý vstup je reťazec dobre uzátvorkovaný práve vtedy, keď na zásobníku nezostala žiadna zátvorka.
 +
 +
<syntaxhighlight lang="C++">
 +
#include <iostream>
 +
#include <cassert>
 +
using namespace std;
 +
 +
typedef char dataType;
 +
 +
/* Sem pride definicia struktury stack a vsetkych potrebnych funkcii. */
 +
 +
int main() {
 +
    char vyraz[100];
 +
    cin.getline(vyraz, 100);
 +
   
 +
    stack s;
 +
    init(s);
 +
   
 +
    bool dobre = true;
 +
   
 +
    for (int i = 0; vyraz[i] != 0; i++) {
 +
        switch (vyraz[i]) {
 +
            case '(':
 +
                push(s, ')');
 +
                break;
 +
            case '[':
 +
                push(s, ']');
 +
                break;
 +
            case '{':
 +
                push(s, '}');
 +
                break;
 +
            case ')':
 +
            case ']':
 +
            case '}':
 +
                if (isEmpty(s)) {
 +
                    dobre = false;
 +
                } else {
 +
                    char c = pop(s);
 +
                    if (c != vyraz[i]) {
 +
                        dobre = false;
 +
                    }
 +
                }
 +
                break;
 +
        }
 +
    }
 +
   
 +
    dobre = dobre && isEmpty(s);
 +
       
 +
    destroy(s);
 +
   
 +
    if (dobre) {
 +
        cout << "Retazec je dobre uzatvorkovany." << endl;
 +
    } else {
 +
        cout << "Retazec nie je dobre uzatvorkovany." << endl;
 +
    }
 +
 +
}
 +
</syntaxhighlight>
 +
 +
''Cvičenie:'' Prepíšte program na kontrolu zátvoriek do rekurzívnej podoby. Použite pritom iba premenné typu <tt>char</tt>; špeciálne nepoužívajte žiadne polia. Reťazec načítavajte pomocou funkcií <tt>getc</tt> a <tt>ungetc</tt>. Môžete predpokladať, že je ukončený koncom riadku.

Aktuálna revízia z 10:19, 20. november 2023

Oznamy

  • Domácu úlohu 3 odovzdávajte do 5. decembra, 22:00.
  • Zajtrajšia rozcvička bude zo súborov, pozrite si prednášky z minulého týždňa.
  • Piatkové cvičenia sú povinné pre tých, ktorí nevyriešia v utorok počas cvičení úspešne rozcvičku.
  • Blíži sa semestrálny test.
    • Bude sa konať v stredu v stredu 13.12. o 18:10.
    • Treba z neho získať aspoň 50% bodov, inak známka Fx.
    • Opravný termín bude v januári (iba jeden).
    • Viac podrobností zverejníme budúci týždeň.

Abstraktný dátový typ

Abstraktný dátový typ (ADT) je abstrakcia dátovej štruktúry nezávislá od samotnej implementácie.

  • Býva zadaný pomocou množiny operácií (hlavičiek funkcií), ktoré poskytuje
  • Jeden abstraktný dátový typ môže byť implementovaný pomocou viacerých dátových štruktúr.

Témou 14. prednášky bol napríklad abstraktný dátový typ dynamická množina, ktorý poskytuje tri základné operácie:

  • Zistenie príslušnosti prvku do množiny (contains).
  • Pridanie prvku do množiny (add).
  • Odobranie prvky z množiny (remove).

Videli sme implementácie (pre jednoduchosť bez operácie remove) pomocou neutriedeného poľa, utriedeného poľa, spájaného zoznamu, priameho adresovania a hašovania.

Podobne by sa za ADT dalo považovať dynamické pole s operáciami add, length, get a set.

Výhodou abstraktných dátových typov je oddelenie implementácie dátovej štruktúry od programu, ktorý ju používa.

  • Napríklad program pracujúci s dynamickou množinou prostredníctvom funkcií contains, add a remove možno rovnako dobre použiť pri implementácii množiny pomocou neutriedených polí, ako pri jeho implementácii pomocou hešovania.

Témou dnešnej prednášky sú dva nové abstraktné dátové typy.

Rad a zásobník

  • Zásobník (angl. stack) a rad alebo front (angl. queue) sú jednoduché abstraktné dátové typy, ktoré udržiavajú postupnosť nejakých prvkov.
  • Typicky ide o úlohy alebo dáta čakajúce na spracovanie.
  • Rad aj zásobník poskytujú funkciu, ktorá vkladá nový prvok.
  • Druhou základnou funkciou je výber jedného prvku, pričom rad sa od zásobníka líši tým, ktorý prvok sa vyberá.

Prvky radu a zásobníka môžu byť ľubovoľného typu. Namiesto konkrétneho typu (ako napríklad int alebo char) tak budeme pracovať so všeobecným typom, ktorý nazveme dataType. Za ten možno dosadiť ľubovoľný konkrétny typ – napríklad int dosadíme za dataType takto:

typedef int dataType;
  • Pri využití tohto prístupu tak napríklad bude možné získať z radu prvkov typu int rad prvkov typu char zmenou v jedinom riadku programu.
  • Taká istá úprava by sa dala spraviť aj pri ADT množina a dynamické pole.


Rad

  • Z radu sa zakaždým vyberie ten jeho prvok, ktorý doň bol vložený ako prvý spomedzi jeho aktuálnych prvkov.
  • Možno ho tak pripodobniť k radu pri pokladni v obchode.
  • Takáto metóda manipulácie s dátami sa v angličtine označuje skratkou FIFO, podľa first in, first out.

Abstraktný dátový typ pre rad poskytuje tieto operácie (kde queue je názov štruktúry reprezentujúcej rad):

/* Inicializuje prazdny rad */
void init(queue &q);

/* Zisti, ci je rad prazdny */
bool isEmpty(queue &q);

/* Prida prvok item na koniec radu */
void enqueue(queue &q, dataType item);

/* Odoberie prvok zo zaciatku radu a vrati jeho hodnotu */
dataType dequeue(queue &q);

/* Vrati prvok zo zaciatku radu, ale necha ho v rade */
dataType peek(queue &q);

/* Uvolni pamat */
void destroy(queue &q);

Zásobník

  • Zo zásobníka sa naopak zakaždým vyberie ten prvok, ktorý doň bol vložený ako posledný.
  • Môžeme si ho predstaviť ako stĺpec tanierov, kde umyté taniere dávame na vrch stĺpca a tiež z vrchu aj berieme taniere na použitie.
  • Táto metóda manipulácie s dátami sa v angličtine označuje skratkou LIFO, podľa last in, first out.

Abstraktný dátový typ pre zásobník poskytuje tieto operácie (stack je názov štruktúry reprezentujúcej zásobník):

/* Inicializuje prazdny zasobnik */
void init(stack &s);

/* Zisti, ci je zasobnik prazdny */
bool isEmpty(stack &s);

/* Prida prvok item na vrch zasobnika */
void push(stack &s, dataType item);

/* Odoberie prvok z vrchu zasobnika a vrati jeho hodnotu */
dataType pop(stack &s);

/* Vrati prvok na vrchu zasobnika, ale necha ho v zasobniku */
dataType peek(stack &s);

/* Uvolni pamat */
void destroy(stack &s);

Programy využívajúce rad a zásobník

Bez ohľadu na samotnú implementáciu vyššie uvedených funkcií vieme písať programy, ktoré ich využívajú. Napríklad nasledujúci program pracuje s radom:

#include <iostream>
using namespace std;

typedef int dataType;


/* Sem pride definicia struktury queue a vsetkych potrebnych funkcii. */


int main() {
    queue q;
    init(q);
    enqueue(q, 1);
    enqueue(q, 2);
    enqueue(q, 3);
    cout << dequeue(q) << endl;  // Vypise 1
    cout << dequeue(q) << endl;  // Vypise 2
    cout << dequeue(q) << endl;  // Vypise 3
    destroy(q);
}

Podobne nasledujúci program pracuje so zásobníkom:

#include <iostream>
using namespace std;

typedef int dataType;

/* Sem pride definicia struktury stack a vsetkych potrebnych funkcii. */

int main() {
    stack s;
    init(s);
    push(s, 1);
    push(s, 2);
    push(s, 3);
    cout << pop(s) << endl;  // Vypise 3
    cout << pop(s) << endl;  // Vypise 2
    cout << pop(s) << endl;  // Vypise 1
    destroy(s);
}


Poznámka:

  • V objektovo-orientovanom programovaní (budúci semester) sa namiesto napr. push(s,10) píše niečo ako s.push(10)

Implementácia zásobníka a radu

Zásobník pomocou poľa

  • Na úvod implementujeme zásobník pomocou poľa items, ktoré budeme alokovať na fixnú dĺžku maxN (rovnako dobre by sme však mohli použiť aj dynamické pole).
  • Spodok zásobníka pritom bude v tomto poli uložený na pozícii 0 a jeho vrch na pozícii top.
  • V prípade, že je zásobník prázdny, bude hodnota premennej top rovná -1.
#include <cassert>

typedef int dataType;
const int maxN = 1000;

struct stack {
    // Alokovane pole prvkov zasobnika.
    dataType *items; 
    // Index vrchu zasobnika v poli items, -1 ak prazdny
    int top;         
};

/* Inicializuje prazdny zasobnik */
void init(stack &s) {
    s.items = new dataType[maxN];
    s.top = -1; 
}

/* Zisti, ci je zasobnik prazdny */
bool isEmpty(stack &s) {
    return s.top == -1;
}

/* Prida prvok item na vrch zasobnika */
void push(stack &s, dataType item) {
    assert(s.top <= maxN - 2);
    s.top++;
    s.items[s.top] = item;
} 

/* Odoberie prvok z vrchu zasobnika a vrati jeho hodnotu */
dataType pop(stack &s) {
    assert(!isEmpty(s));
    s.top--;
    return s.items[s.top + 1];
}

/* Vrati prvok na vrchu zasobnika, ale necha ho v zasobniku */          
dataType peek(stack &s) {
    assert(!isEmpty(s));
    return s.items[s.top];
}

/* Uvolni pamat */
void destroy(stack &s) {
    delete[] s.items;
}

Rad pomocou poľa

  • Rad sa od zásobníka líši tým, že prvky sa z neho vyberajú z opačnej strany, než sa doň vkladajú.
  • Keby sa prvý prvok radu udržiaval na pozícii 0, museli by sa po každom výbere prvku tie zvyšné posunúť o jednu pozíciu doľava, čo je časovo neefektívne.
  • Rad teda implementujeme tak, aby jeho začiatok mohol byť na ľubovoľnej pozícii first poľa items.
  • Pole items pritom budeme chápať ako cyklické – prvky s indexom menším ako first budeme implementovať ako nasledujúce za posledným prvkom poľa.
#include <cassert>

typedef int dataType;

const int maxN = 1000;

struct queue {
    dataType *items; // Alokovane pole obsahujuce prvky radu.
    int first;       // Index prveho prvku radu v poli items.
    int count;       // Pocet prvkov v rade. 
};

/* Inicializuje prazdny rad */
void init(queue &q) {
    q.items = new dataType[maxN];
    q.first = 0;
    q.count = 0;
}

/* Zisti, ci je rad prazdny */
bool isEmpty(queue &q) {
    return q.count == 0;
}

/* Prida prvok item na koniec radu */
void enqueue(queue &q, dataType item) {
    assert(q.count < maxN);
    int index = (q.first + q.count) % maxN;
    q.items[index] = item;
    q.count++;
} 

/* Odoberie prvok zo zaciatku radu a vrati jeho hodnotu */
dataType dequeue(queue &q) {
    assert(!isEmpty(q));
    dataType result = q.items[q.first];
    q.first = (q.first + 1) % maxN;
    q.count--;
    return result;
}

/* Vrati prvok zo zaciatku radu, ale necha ho v rade */
dataType peek(queue &q) {
    assert(!isEmpty(q));
    return q.items[q.first];
}         

/* Uvolni pamat */
void destroy(queue &q) {
    delete[] q.items;
}

Zásobník pomocou spájaného zoznamu

  • Zásobník teraz implementujeme pomocou spájaného zoznamu.
  • Na rozdiel od implementácie pomocou poľa bude výhodnejšie uchovávať vrch zásobníka ako prvý prvok zoznamu.
  • Pri jednosmerne spájaných zoznamoch je totiž jednoduchšie vkladať a odoberať prvky na jeho začiatku.
  • Výhoda tohto prístupu oproti implementácii pomocou poľa je v tom, že maximálny počet prvkov v zásobníku nebude obmedzený konštantou maxN. Podobný efekt je možné docieliť aj dynamického poľa, tam však dochádza k realokácii.
#include <cassert>

typedef int dataType;

struct node {
    dataType data;
    node *next;
};

struct stack {
    node *top; // Smernik na vrch zasobnika (zaciatok spajaneho zoznamu). Ak je zasobnik prazdny, ma hodnotu NULL.
};

/* Inicializuje prazdny zasobnik */
void init(stack &s) {
    s.top = NULL;
}

/* Zisti, ci je zasobnik prazdny */
bool isEmpty(stack &s) {
    return s.top == NULL;
}

/* Prida prvok item na vrch zasobnika */
void push(stack &s, dataType item) {
    node *tmp = new node;
    tmp->data = item;
    tmp->next = s.top;
    s.top = tmp;  
} 

/* Odoberie prvok z vrchu zasobnika a vrati jeho hodnotu */
dataType pop(stack &s) {
    assert(!isEmpty(s));
    dataType result = s.top->data;
    node *tmp = s.top->next;
    delete s.top;
    s.top = tmp;
    return result;
}

/* Vrati prvok na vrchu zasobnika, ale necha ho v zasobniku */          
dataType peek(stack &s) {
    assert(!isEmpty(s));
    return s.top->data;
}

/* Uvolni pamat */
void destroy(stack &s) {
    while (!isEmpty(s)) {
        pop(s);
    }
}

Rad pomocou spájaného zoznamu

  • Pri implementácii radu pomocou spájaného zoznamu rozšírime spájané zoznamy zo 14. prednášky o smerník last na posledný prvok zoznamu.
  • Bude tak možné efektívne vkladať prvky na koniec zoznamu, ako aj odoberať prvky zo začiatku zoznamu.
  • Výhodou oproti implementácii radu pomocou poľa je, podobne ako pri zásobníkoch, eliminácia obmedzenia na maximálny počet prvkov v rade.
#include <cassert>

typedef int dataType;

struct node {
    dataType data;
    node *next;
};

struct queue {
    // Smernik na prvy uzol. Ak je rad prazdny, ma hodnotu NULL.
    node *first; 
    // Smernik na posledny uzol. Ak je rad prazdny, ma hodnotu NULL.
    node *last;  
};

/* Inicializuje prazdny rad */
void init(queue &q) {
    q.first = NULL;
    q.last = NULL;
}

/* Zisti, ci je rad prazdny */
bool isEmpty(queue &q) {
    return q.first == NULL;
}

/* Prida prvok item na koniec radu */
void enqueue(queue &q, dataType item) {
    node *tmp = new node;
    tmp->data = item;
    tmp->next = NULL;
    if (isEmpty(q)) {
        q.first = tmp;
        q.last = tmp;
    } else {
        q.last->next = tmp;
        q.last = tmp;
    }
} 

/* Odoberie prvok zo zaciatku radu a vrati jeho hodnotu */
dataType dequeue(queue &q) {
    assert(!isEmpty(q));
    dataType result = q.first->data;
    node *tmp = q.first->next;
    delete q.first;
    if (tmp == NULL) {
        q.first = NULL; 
        q.last = NULL;
    } else {
        q.first = tmp;
    } 
    return result;
}

/* Vrati prvok zo zaciatku radu, ale necha ho v rade */
dataType peek(queue &q) {
    assert(!isEmpty(q));
    return q.first->data;
}         

/* Uvolni pamat */
void destroy(queue &q) {
    while (!isEmpty(q)) {
        dequeue(q);
    }
}

Použitie zásobníka a radu

Zásobník aj rad často uchovávajú dáta určené na spracovanie, zoznamy úloh, atď. Rad sa zvyčajne používa v prípadoch, keď je žiadúce zachovať ich poradie. Typicky môže ísť o situácie, keď jeden proces generuje úlohy spracúvané iným procesom, napríklad:

  • Textový procesor pripravuje strany na tlač a vkladá ich do radu, z ktorého ich tlačiareň (resp. jej ovládač) postupne vyberá.
  • Sekvenčne vykonávané výpočtové úlohy čakajú v rade na spustenie.
  • Zákazníci čakajú na zákazníckej linke na voľného operátora.
  • Pasažieri na standby čakajú na voľné miesto v lietadle.

Zásobník sa, ako o niečo implementačne jednoduchší koncept, zvyčajne používa v situáciách, keď na poradí spracúvania nezáleží, alebo keď je žiadúce vstupné poradie obrátiť. Najvýznamnejší príklad situácie druhého typu je nasledujúci:

  • Operačný systém ukladá lokálne premenné volaných funkcií na tzv. zásobníku volaní (angl. call stack), čo umožňuje používanie rekurzie.
  • Rekurzívne programy sa dajú prepísať na nerekurzívne pomocou „ručne vytvoreného” zásobníka (neskôr si ukážeme nerekurzívnu verziu triedenia Quick Sort).

Príklad: kontrola uzátvorkovania

Ako jednoduchý príklad na použitie zásobníka uvažujme nasledujúcu situáciu: na vstupe je daný reťazec pozostávajúci (okrem prípadných ďalších znakov, ktoré možno ignorovať) zo zátvoriek (,),[,],{,}. Úlohou je zistiť, či je tento reťazec dobre uzátvorkovaný. To znamená, že:

  • Pre každú uzatváraciu zátvorku musí byť posledná dosiaľ neuzavretá otváracia zátvorka rovnakého typu, pričom musí existovať aspoň jedna dosiaľ neuzavretá zátvorka.
  • Každá otváracia zátvorka musí byť niekedy neskôr uzavretá.

Príklady očakávaného vstupu a výstupu:

()
Retazec je dobre uzatvorkovany

nejaky text bez zatvoriek
Retazec je dobre uzatvorkovany

[((({}[])[]))]()
Retazec je dobre uzatvorkovany

[[#))
Retazec nie je dobre uzatvorkovany

())(
Retazec nie je dobre uzatvorkovany

((
Retazec nie je dobre uzatvorkovany

((cokolvek
Retazec nie je dobre uzatvorkovany
  • Nasledujúci program postupne prechádza cez vstupný reťazec, pričom pre každú otváraciu zátvorku si na zásobník pridá uzatváraciu zátvorku rovnakého typu.
  • Ak narazí na uzatváraciu zátvorku, výraz môže byť dobre uzátvorkovaný len v prípade, že je na zásobníku aspoň jedna zátvorka, pričom zátvorka na vrchu zásobníka sa zhoduje so zátvorkou na vstupe.
  • V prípade úspešného prechodu cez celý vstup je reťazec dobre uzátvorkovaný práve vtedy, keď na zásobníku nezostala žiadna zátvorka.
#include <iostream>
#include <cassert>
using namespace std;

typedef char dataType;

/* Sem pride definicia struktury stack a vsetkych potrebnych funkcii. */

int main() {
    char vyraz[100];
    cin.getline(vyraz, 100);
    
    stack s;
    init(s);
    
    bool dobre = true;
    
    for (int i = 0; vyraz[i] != 0; i++) {
        switch (vyraz[i]) {
            case '(':
                push(s, ')');
                break;
            case '[':
                push(s, ']');
                break;
            case '{':
                push(s, '}');
                break;
            case ')':
            case ']':
            case '}':
                if (isEmpty(s)) {
                    dobre = false;
                } else {
                    char c = pop(s);
                    if (c != vyraz[i]) {
                        dobre = false;
                    }
                }
                break;
        }
    }
    
    dobre = dobre && isEmpty(s);
        
    destroy(s);
    
    if (dobre) {
        cout << "Retazec je dobre uzatvorkovany." << endl;
    } else {
        cout << "Retazec nie je dobre uzatvorkovany." << endl;
    }

}

Cvičenie: Prepíšte program na kontrolu zátvoriek do rekurzívnej podoby. Použite pritom iba premenné typu char; špeciálne nepoužívajte žiadne polia. Reťazec načítavajte pomocou funkcií getc a ungetc. Môžete predpokladať, že je ukončený koncom riadku.