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.


2018/19 Programovanie (1) v C/C++: Rozdiel medzi revíziami

Z Programovanie
Skočit na navigaci Skočit na vyhledávání
 
(Jedna medziľahlá úprava od rovnakého používateľa nie je zobrazená.)
Riadok 470: Riadok 470:
 
===Neinicializovaná premenná===
 
===Neinicializovaná premenná===
 
Nasledujúci program vypisuje neinicializovanú premennú ''i''  
 
Nasledujúci program vypisuje neinicializovanú premennú ''i''  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 476: Riadok 476:
 
     int i; cout << i << endl;
 
     int i; cout << i << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
 
Valgrind vypíše okrem iného
 
Valgrind vypíše okrem iného
 
<pre>
 
<pre>
Riadok 497: Riadok 497:
 
===Neinicializovaný smerník===
 
===Neinicializovaný smerník===
 
Nasledujúci program zapisuje do pamäte, na ktorú ukazuje smerník s neinicializovnaou hodnotou
 
Nasledujúci program zapisuje do pamäte, na ktorú ukazuje smerník s neinicializovnaou hodnotou
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 505: Riadok 505:
 
     cout << *p << endl;
 
     cout << *p << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
<pre>
 
<pre>
Riadok 528: Riadok 528:
 
===Chybné odalokovanie===
 
===Chybné odalokovanie===
 
Tento program sa pokúša odalokovať pamäť, ktorá nebola alokovaná
 
Tento program sa pokúša odalokovať pamäť, ktorá nebola alokovaná
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 536: Riadok 536:
 
     delete p;
 
     delete p;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
<pre>
 
<pre>
Riadok 548: Riadok 548:
 
===Písanie za koniec poľa===
 
===Písanie za koniec poľa===
 
Ani valgrind nemusí nájsť všetky chyby, napr. tu sa k premennej správame ako ku poľu a píšeme mimo, ale valgrind si to nevšimne:
 
Ani valgrind nemusí nájsť všetky chyby, napr. tu sa k premennej správame ako ku poľu a píšeme mimo, ale valgrind si to nevšimne:
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 558: Riadok 558:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
 
Ak index 2 nahradíme 200, valgrind už vypíše chybu...
 
Ak index 2 nahradíme 200, valgrind už vypíše chybu...
 
* Celkovo valgrind lepšie deteguje chyby týkajúce sa dynamickz alokovanej pamäte (pomocou new)
 
* Celkovo valgrind lepšie deteguje chyby týkajúce sa dynamickz alokovanej pamäte (pomocou new)
Riadok 566: Riadok 566:
 
Valgrind nám tiež môže pomôcť nájsť pamäť, ktorú sme alokovali cez new, ale zabudli odalokovať cez delete alebo delete[].
 
Valgrind nám tiež môže pomôcť nájsť pamäť, ktorú sme alokovali cez new, ale zabudli odalokovať cez delete alebo delete[].
 
* V programe nižšie máme dve volanie new, ku ktorým chýba odalokovanie
 
* V programe nižšie máme dve volanie new, ku ktorým chýba odalokovanie
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 577: Riadok 577:
 
   }
 
   }
 
}
 
}
</pre>
+
</syntaxhighlight>
 
Valgrind vypíše
 
Valgrind vypíše
 
<pre>
 
<pre>
Riadok 631: Riadok 631:
 
Nasledujúci program by mal správne vypísať text "AhojAhojAhojAhoj", ale je v ňom zopár chýb. Skúste nájsť a opraviť chyby čítaním programu, použitím debugera, programu valgrind, prípadne si pridajte nejaké pomocné výpisy premenných.  
 
Nasledujúci program by mal správne vypísať text "AhojAhojAhojAhoj", ale je v ňom zopár chýb. Skúste nájsť a opraviť chyby čítaním programu, použitím debugera, programu valgrind, prípadne si pridajte nejaké pomocné výpisy premenných.  
 
* v programe valgrind je vždy dobré začať od prvej vypísanej chyby, opraviť ju a spustiť valgrind znovu
 
* v programe valgrind je vždy dobré začať od prvej vypísanej chyby, opraviť ju a spustiť valgrind znovu
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 654: Riadok 654:
 
     cout << vysledok << endl;
 
     cout << vysledok << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
 
=SVGdraw=
 
=SVGdraw=
  
Riadok 747: Riadok 747:
  
 
* Tradične sa v učebniciach programovania ako prvý uvádza program, ktorý iba vypíše na obrazovku text <tt>"Hello world!"</tt>. Tu je v jazyku C++:
 
* Tradične sa v učebniciach programovania ako prvý uvádza program, ktorý iba vypíše na obrazovku text <tt>"Hello world!"</tt>. Tu je v jazyku C++:
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 754: Riadok 754:
 
     cout << "Hello world!" << endl;
 
     cout << "Hello world!" << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
* Samotný text je vypísaný príkazom <tt>cout << "Hello world!" << endl;</tt>
 
* Samotný text je vypísaný príkazom <tt>cout << "Hello world!" << endl;</tt>
Riadok 772: Riadok 772:
  
 
* Podobným spôsobom môžeme vypísať aj iný text. Napríklad dnešný dátum:
 
* Podobným spôsobom môžeme vypísať aj iný text. Napríklad dnešný dátum:
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 779: Riadok 779:
 
     cout << "Dnes je 24.9.2018!" << endl;
 
     cout << "Dnes je 24.9.2018!" << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
* Cvičenie: Vypíšte dátum v rôznych formátoch (skrátený formát, americký formát, ...). Každý na jeden riadok.
 
* Cvičenie: Vypíšte dátum v rôznych formátoch (skrátený formát, americký formát, ...). Každý na jeden riadok.
Riadok 787: Riadok 787:
 
Príklad z cvičenia by mohol vyzerať napríklad takto:
 
Príklad z cvičenia by mohol vyzerať napríklad takto:
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 796: Riadok 796:
 
     cout << "9/24/2018" << endl;
 
     cout << "9/24/2018" << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Ak by sme v ňom chceli zmeniť dátum na prvú prednášku o rok, museli by sme pomeniť vhodné čisla v celom programe. Navyše keď vidíme v programe nejaké číslo, nemusí byť úplne jasné, ako sme k nemu prišli.
 
Ak by sme v ňom chceli zmeniť dátum na prvú prednášku o rok, museli by sme pomeniť vhodné čisla v celom programe. Navyše keď vidíme v programe nejaké číslo, nemusí byť úplne jasné, ako sme k nemu prišli.
Riadok 802: Riadok 802:
 
Program teraz prepíšeme tak, aby sme deň, mesiac a rok mali zapísané symbolicky a mohli ich meniť na jednom mieste.
 
Program teraz prepíšeme tak, aby sme deň, mesiac a rok mali zapísané symbolicky a mohli ich meniť na jednom mieste.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 815: Riadok 815:
 
     cout << mesiac << "/" << den << "/" << rok << endl;
 
     cout << mesiac << "/" << den << "/" << rok << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Symbolickým hodnotám '''den, mesiac, rok''' sa hovorí '''premenné'''.
 
Symbolickým hodnotám '''den, mesiac, rok''' sa hovorí '''premenné'''.
Riadok 855: Riadok 855:
 
Nasledujúci program od užívateľa vypýta dve čísla a vypíše ich súčet.
 
Nasledujúci program od užívateľa vypýta dve čísla a vypíše ich súčet.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 870: Riadok 870:
 
     cout << x << "+" << y << "=" << result << endl;
 
     cout << x << "+" << y << "=" << result << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Tu je príklad behu programu, keď užívateľ zadal čísla 10 a 3:
 
Tu je príklad behu programu, keď užívateľ zadal čísla 10 a 3:
Riadok 913: Riadok 913:
 
* Nasledujúci program si vypýta od užívateľa číslo a vypíše, či je toto číslo záporné (negative) alebo nezáporné (non-negative).
 
* Nasledujúci program si vypýta od užívateľa číslo a vypíše, či je toto číslo záporné (negative) alebo nezáporné (non-negative).
  
<pre>#include <iostream>
+
<syntaxhighlight lang="C++">#include <iostream>
 
using namespace std;
 
using namespace std;
  
Riadok 926: Riadok 926:
 
         cout << "Number " << x << " is non-negative." << endl;
 
         cout << "Number " << x << " is non-negative." << endl;
 
     }
 
     }
}</pre>
+
}</syntaxhighlight>
  
 
* Tu je príklad dvoch behov programu:
 
* Tu je príklad dvoch behov programu:
Riadok 937: Riadok 937:
 
<pre>
 
<pre>
 
Please enter some number: -3
 
Please enter some number: -3
Number -3 is odd.
+
Number -3 is negative.
 
</pre>
 
</pre>
  
Riadok 956: Riadok 956:
 
* Test na rovnosť sa robí operátorom ==
 
* Test na rovnosť sa robí operátorom ==
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 976: Riadok 976:
  
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Upozornenie===
 
=== Upozornenie===
 
Častá chyba, ktorá sa vyskytuje pri podmienke, je použitie priradenia namiesto porovnania. Keby sme napísali
 
Častá chyba, ktorá sa vyskytuje pri podmienke, je použitie priradenia namiesto porovnania. Keby sme napísali
<pre>if (x=0) cout << "Zero" << endl; </pre>
+
<syntaxhighlight lang="C++">if (x=0) cout << "Zero" << endl; </syntaxhighlight>
 
tak program do premennej x priradí nulu.
 
tak program do premennej x priradí nulu.
  
 
Ďalšia bežná chyba je zabudnutie zložených zátvoriek
 
Ďalšia bežná chyba je zabudnutie zložených zátvoriek
<pre>
+
<syntaxhighlight lang="C++">
 
   if (x==0) cout << "Zero"; cout << endl;
 
   if (x==0) cout << "Zero"; cout << endl;
</pre>
+
</syntaxhighlight>
 
Tento program vykoná <tt>cout << endl</tt> vždy, nezávisle od podmienky. V prípade, že chceme vykonať v podmienke viacero príkazov, nesmieme zabudnúť ich uzátvorkovať:  
 
Tento program vykoná <tt>cout << endl</tt> vždy, nezávisle od podmienky. V prípade, že chceme vykonať v podmienke viacero príkazov, nesmieme zabudnúť ich uzátvorkovať:  
<pre>
+
<syntaxhighlight lang="C++">
 
   if (x==0) { cout << "Zero"; cout << endl; }
 
   if (x==0) { cout << "Zero"; cout << endl; }
</pre>
+
</syntaxhighlight>
 +
 
 
=Cvičenia 1=
 
=Cvičenia 1=
 
Cieľom prvých cvičení je:
 
Cieľom prvých cvičení je:
Riadok 1 023: Riadok 1 024:
 
* Podmienky (<tt>if</tt>).
 
* Podmienky (<tt>if</tt>).
  
<pre>#include <iostream>
+
<syntaxhighlight lang="C++">#include <iostream>
 
using namespace std;
 
using namespace std;
  
Riadok 1 037: Riadok 1 038:
 
     }
 
     }
 
     return 0;
 
     return 0;
}</pre>
+
}</syntaxhighlight>
  
 
== Komentáre ==
 
== Komentáre ==
Riadok 1 047: Riadok 1 048:
 
<span style="font-size:85%">''Poznámka'': Staršie štandardy jazyka C (bez ++) nepodporujú notáciu <tt>//</tt> pre jednoriadkové komentáre.</span>
 
<span style="font-size:85%">''Poznámka'': Staršie štandardy jazyka C (bez ++) nepodporujú notáciu <tt>//</tt> pre jednoriadkové komentáre.</span>
  
<pre>#include <iostream>
+
<syntaxhighlight lang="C++">#include <iostream>
 
using namespace std;
 
using namespace std;
  
Riadok 1 059: Riadok 1 060:
 
     */
 
     */
 
     return 0;
 
     return 0;
}</pre>
+
}</syntaxhighlight>
  
 
== Dátové typy <tt>int</tt>, <tt>double</tt> a <tt>bool</tt> ==  
 
== Dátové typy <tt>int</tt>, <tt>double</tt> a <tt>bool</tt> ==  
Riadok 1 085: Riadok 1 086:
 
Možnosti pretypovania sú ilustrované nasledujúcim ukážkovým programom. Čitateľ si môže sám skúsiť vymyslieť preň niekoľko vstupov, odhadnúť výstupy  programu na týchto vstupoch a následne svoj odhad overiť spustením programu.
 
Možnosti pretypovania sú ilustrované nasledujúcim ukážkovým programom. Čitateľ si môže sám skúsiť vymyslieť preň niekoľko vstupov, odhadnúť výstupy  programu na týchto vstupoch a následne svoj odhad overiť spustením programu.
  
<pre>#include <iostream>
+
<syntaxhighlight lang="C++">#include <iostream>
 
using namespace std;
 
using namespace std;
  
Riadok 1 107: Riadok 1 108:
 
    
 
    
 
   return 0;
 
   return 0;
}</pre>
+
}</syntaxhighlight>
  
 
=== Aritmetické operátory a výrazy ===
 
=== Aritmetické operátory a výrazy ===
Riadok 1 118: Riadok 1 119:
 
Operátor <tt>/</tt> sa pritom na argumentoch typu <tt>int</tt> správa ako '''celočíselné delenie''' &ndash; hodnota podielu sa zaokrúhli smerom k nule. Napríklad výraz <tt>5/3</tt> teda má hodnotu <tt>1</tt>. Akonáhle je však aspoň jeden operand typu <tt>double</tt>, interpretuje sa operátor <tt>/</tt> ako '''delenie reálnych čísel'''. Výrazy <tt>5.0/3.0</tt>, <tt>5.0/3</tt>, <tt>5/3.0</tt> a <tt>5/(double)3</tt> teda majú všetky hodnotu <tt>1.66667</tt>. Výstup aritmetickej operácie má navyše rovnakú hodnotu ako &bdquo;všeobecnejší&rdquo; z operandov. Napríklad <tt>2+3.0</tt> je teda typu <tt>double</tt> a výraz <tt>(2+3.0)/3</tt> tak má hodnotu <tt>1.66667</tt>; naopak výraz <tt>(2+3)/3</tt> má hodnotu <tt>1</tt>.
 
Operátor <tt>/</tt> sa pritom na argumentoch typu <tt>int</tt> správa ako '''celočíselné delenie''' &ndash; hodnota podielu sa zaokrúhli smerom k nule. Napríklad výraz <tt>5/3</tt> teda má hodnotu <tt>1</tt>. Akonáhle je však aspoň jeden operand typu <tt>double</tt>, interpretuje sa operátor <tt>/</tt> ako '''delenie reálnych čísel'''. Výrazy <tt>5.0/3.0</tt>, <tt>5.0/3</tt>, <tt>5/3.0</tt> a <tt>5/(double)3</tt> teda majú všetky hodnotu <tt>1.66667</tt>. Výstup aritmetickej operácie má navyše rovnakú hodnotu ako &bdquo;všeobecnejší&rdquo; z operandov. Napríklad <tt>2+3.0</tt> je teda typu <tt>double</tt> a výraz <tt>(2+3.0)/3</tt> tak má hodnotu <tt>1.66667</tt>; naopak výraz <tt>(2+3)/3</tt> má hodnotu <tt>1</tt>.
  
<pre>#include <iostream>
+
<syntaxhighlight lang="C++">#include <iostream>
 
using namespace std;
 
using namespace std;
  
Riadok 1 136: Riadok 1 137:
 
      
 
      
 
     return 0;
 
     return 0;
}</pre>
+
}</syntaxhighlight>
  
 
* Na celých číslach je ďalej definovaný operátor <tt>%</tt>, ktorého výstupom je ''zvyšok po celočíselnom delení jeho operandov''. Napríklad výraz <tt>5%3</tt> tak má hodnotu <tt>2</tt>.
 
* Na celých číslach je ďalej definovaný operátor <tt>%</tt>, ktorého výstupom je ''zvyšok po celočíselnom delení jeho operandov''. Napríklad výraz <tt>5%3</tt> tak má hodnotu <tt>2</tt>.
Riadok 1 179: Riadok 1 180:
  
 
Logické výrazy môžu byť efektívnym nástrojom na elimináciu množstva vnorených podmienok: napríklad konštrukcia typu
 
Logické výrazy môžu byť efektívnym nástrojom na elimináciu množstva vnorených podmienok: napríklad konštrukcia typu
<pre>if (a == 0) {
+
<syntaxhighlight lang="C++">if (a == 0) {
 
     if (b == 0) {
 
     if (b == 0) {
 
         čokoľvek
 
         čokoľvek
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
 
je ekvivalentná konštrukcii
 
je ekvivalentná konštrukcii
<pre>if (a == 0 && b == 0) {
+
<syntaxhighlight lang="C++">if (a == 0 && b == 0) {
 
     čokoľvek
 
     čokoľvek
 
}
 
}
</pre>
+
</syntaxhighlight>
 
a podobne.  
 
a podobne.  
  
 
Každé nenulové prirodzené číslo sa pri pretypovaní na <tt>bool</tt> zmení na 1 (čiže <tt>true</tt>). V dôsledku toho je korektná napríklad aj podmienka
 
Každé nenulové prirodzené číslo sa pri pretypovaní na <tt>bool</tt> zmení na 1 (čiže <tt>true</tt>). V dôsledku toho je korektná napríklad aj podmienka
<pre>if (42) {
+
<syntaxhighlight lang="C++">if (42) {
 
     čokoľvek
 
     čokoľvek
 
}
 
}
</pre>
+
</syntaxhighlight>
 
a čitateľ sa môže sám presvedčiť o tom, že telo takejto podmienky sa ''vždy vykoná''. Táto črta, spoločne s vlastnosťami operátora priradenia, je príčinou častej programátorskej chyby demonštrovanej nasledujúcim ukážkovým programom.
 
a čitateľ sa môže sám presvedčiť o tom, že telo takejto podmienky sa ''vždy vykoná''. Táto črta, spoločne s vlastnosťami operátora priradenia, je príčinou častej programátorskej chyby demonštrovanej nasledujúcim ukážkovým programom.
  
<pre>#include <iostream>
+
<syntaxhighlight lang="C++">#include <iostream>
 
using namespace std;
 
using namespace std;
  
Riadok 1 212: Riadok 1 213:
 
     }  
 
     }  
 
     return 0;
 
     return 0;
}</pre>
+
}</syntaxhighlight>
  
 
=== Priorita a asociativita operátorov ===
 
=== Priorita a asociativita operátorov ===
Riadok 1 247: Riadok 1 248:
 
Nasledujúci program načíta zo vstupu číslo <tt>n</tt> a postupne vypíše prirodzené čísla od 1 po <tt>n</tt> oddelené medzerami.
 
Nasledujúci program načíta zo vstupu číslo <tt>n</tt> a postupne vypíše prirodzené čísla od 1 po <tt>n</tt> oddelené medzerami.
  
<pre>#include <iostream>
+
<syntaxhighlight lang="C++">#include <iostream>
 
using namespace std;
 
using namespace std;
  
Riadok 1 258: Riadok 1 259:
 
     cout << endl;
 
     cout << endl;
 
     return 0;
 
     return 0;
}</pre>
+
}</syntaxhighlight>
  
 
Tu je výstup programu pre <tt>n = 9</tt>:
 
Tu je výstup programu pre <tt>n = 9</tt>:
Riadok 1 267: Riadok 1 268:
  
 
Novou črtou programu z predchádzajúceho príkladu je konštrukcia pre cyklus <tt>for</tt>:  
 
Novou črtou programu z predchádzajúceho príkladu je konštrukcia pre cyklus <tt>for</tt>:  
<pre>for (int i = 1; i <= n; i++) {
+
<syntaxhighlight lang="C++">for (int i = 1; i <= n; i++) {
 
     telo cyklu
 
     telo cyklu
}</pre>
+
}</syntaxhighlight>
 
*Táto konštrukcia pozostáva z kľúčového slova <tt>for</tt> nasledovaným zátvorkou s troma &bdquo;časťami&rdquo; oddelenými bodkočiarkami:
 
*Táto konštrukcia pozostáva z kľúčového slova <tt>for</tt> nasledovaným zátvorkou s troma &bdquo;časťami&rdquo; oddelenými bodkočiarkami:
 
** Príkaz <tt>int i = 1</tt> vytvorí novú celočíselnú premennú i a priradí jej hodnotu 1.
 
** Príkaz <tt>int i = 1</tt> vytvorí novú celočíselnú premennú i a priradí jej hodnotu 1.
Riadok 1 285: Riadok 1 286:
 
Drobnou zmenou predchádzajúceho programu môžeme napríklad vypísať všetky čísla od 0 po <tt>n-1</tt>:
 
Drobnou zmenou predchádzajúceho programu môžeme napríklad vypísať všetky čísla od 0 po <tt>n-1</tt>:
  
<pre>#include <iostream>
+
<syntaxhighlight lang="C++">#include <iostream>
 
using namespace std;
 
using namespace std;
  
Riadok 1 296: Riadok 1 297:
 
     cout << endl;
 
     cout << endl;
 
     return 0;
 
     return 0;
}</pre>
+
}</syntaxhighlight>
  
 
Tu je výstup programu pre <tt>n = 9</tt>:
 
Tu je výstup programu pre <tt>n = 9</tt>:
Riadok 1 306: Riadok 1 307:
 
Nasledujúci program od používateľa načíta číslo <tt>n</tt> a vypíše <tt>n</tt> simulovaných hodov kocky (každý na samostatný riadok).
 
Nasledujúci program od používateľa načíta číslo <tt>n</tt> a vypíše <tt>n</tt> simulovaných hodov kocky (každý na samostatný riadok).
  
<pre>#include <iostream>
+
<syntaxhighlight lang="C++">#include <iostream>
 
#include <cstdlib>
 
#include <cstdlib>
 
#include <ctime>
 
#include <ctime>
Riadok 1 324: Riadok 1 325:
 
    
 
    
 
     return 0;
 
     return 0;
}</pre>
+
}</syntaxhighlight>
  
 
Príklad behu programu:
 
Príklad behu programu:
Riadok 1 346: Riadok 1 347:
 
Nasledujúci program si od používateľa vypýta číslo ''n'' a vypočíta ''n''!, t.j. súčin celých čísel od 1 po ''n''.
 
Nasledujúci program si od používateľa vypýta číslo ''n'' a vypočíta ''n''!, t.j. súčin celých čísel od 1 po ''n''.
  
<pre>#include <iostream>
+
<syntaxhighlight lang="C++">#include <iostream>
 
using namespace std;
 
using namespace std;
  
Riadok 1 362: Riadok 1 363:
 
     cout << n << "! = " << vysledok << endl;
 
     cout << n << "! = " << vysledok << endl;
 
     return 0;
 
     return 0;
}</pre>
+
}</syntaxhighlight>
  
 
Príklad behu programu pre ''n''=4 (1*2*3*4=24):
 
Príklad behu programu pre ''n''=4 (1*2*3*4=24):
Riadok 1 399: Riadok 1 400:
 
Nasledujúci program načíta od používateľa prirodzené číslo a vypíše zoznam jeho deliteľov.
 
Nasledujúci program načíta od používateľa prirodzené číslo a vypíše zoznam jeho deliteľov.
  
<pre>#include <iostream>
+
<syntaxhighlight lang="C++">#include <iostream>
 
using namespace std;
 
using namespace std;
  
Riadok 1 415: Riadok 1 416:
 
     cout << endl;
 
     cout << endl;
 
     return 0;
 
     return 0;
}</pre>
+
}</syntaxhighlight>
 
   
 
   
 
Beh programu:
 
Beh programu:
Riadok 1 434: Riadok 1 435:
 
** Pozor, výraz a<b<c treba rozpísať na dve porovnania spojené logickou spojkou
 
** Pozor, výraz a<b<c treba rozpísať na dve porovnania spojené logickou spojkou
 
* Nasledujúce dva úryvky programu robia to isté, pričom prvý používa logický operátor <tt>&&</tt> (and) a druhý používa dva príkazy <tt>if</tt>:
 
* Nasledujúce dva úryvky programu robia to isté, pričom prvý používa logický operátor <tt>&&</tt> (and) a druhý používa dva príkazy <tt>if</tt>:
<pre>
+
{{subst:C}}
 
//Verzia 1:
 
//Verzia 1:
 
if(x>0 && y>0) { cout << "Yes!"; }
 
if(x>0 && y>0) { cout << "Yes!"; }
Riadok 1 442: Riadok 1 443:
 
   if(y>0) { cout << "Yes!"; }
 
   if(y>0) { cout << "Yes!"; }
 
}
 
}
</pre>
+
{{subst:/C}}
 
* Podobne nahraďte logické operátory && a || aj v nasledujúcich príkazoch:
 
* Podobne nahraďte logické operátory && a || aj v nasledujúcich príkazoch:
<pre>
+
{{subst:C}}
 
if(x>0 || y>0) { cout << "Yes!"; }
 
if(x>0 || y>0) { cout << "Yes!"; }
 
if(x>0 || (x<0 && y<0)) { cout << "Yes!"; }
 
if(x>0 || (x<0 && y<0)) { cout << "Yes!"; }
 
if(x<0 && (y<0 || z<0)) { cout << "Yes!"; }
 
if(x<0 && (y<0 || z<0)) { cout << "Yes!"; }
</pre>
+
{{subst:/C}}
  
 
* Nech x je nejaká logická hodnota. Čomu sa rovnajú nasledovné výrazy?  
 
* Nech x je nejaká logická hodnota. Čomu sa rovnajú nasledovné výrazy?  
<pre>
+
{{subst:C}}
 
x && true  
 
x && true  
 
x && false  
 
x && false  
Riadok 1 462: Riadok 1 463:
 
x == true  
 
x == true  
 
x == false
 
x == false
</pre>
+
{{subst:/C}}
 
Ako by ste si overili, že máte výsledok tohto cvičenia správne?
 
Ako by ste si overili, že máte výsledok tohto cvičenia správne?
  
Riadok 1 489: Riadok 1 490:
  
 
Okrem cyklu <tt>for</tt> môžeme použiť aj cyklus <tt>while</tt>, ktorý vyzerá nasledovne:
 
Okrem cyklu <tt>for</tt> môžeme použiť aj cyklus <tt>while</tt>, ktorý vyzerá nasledovne:
<pre>
+
{{subst:C}}
 
while (podmienka) {  
 
while (podmienka) {  
 
   príkaz;  
 
   príkaz;  
 
}
 
}
</pre>
+
{{subst:/C}}
  
 
Čítame: kým je splnená podmienka vykonávaj príkaz. Podrobnejšie:
 
Čítame: kým je splnená podmienka vykonávaj príkaz. Podrobnejšie:
Riadok 1 512: Riadok 1 513:
 
Potrebujeme cyklus, ktorý sa bude vykonávať, kým platí nejaká podmienka - práve na to je vhodný cyklus <tt>while</tt>. Pomocou neho zapíšeme algoritmus nasledovne:
 
Potrebujeme cyklus, ktorý sa bude vykonávať, kým platí nejaká podmienka - práve na to je vhodný cyklus <tt>while</tt>. Pomocou neho zapíšeme algoritmus nasledovne:
  
<pre>
+
{{subst:C}}
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 1 526: Riadok 1 527:
 
     }
 
     }
 
}
 
}
</pre>
+
{{subst:/C}}
  
 
<pre>
 
<pre>
Riadok 1 563: Riadok 1 564:
 
** taktiež ''x'' = ''a'' - ''yb'' a preto každý deliteľ ''a'' aj ''b'' delí aj ''x'', t.j. <math>A\subseteq B</math>
 
** taktiež ''x'' = ''a'' - ''yb'' a preto každý deliteľ ''a'' aj ''b'' delí aj ''x'', t.j. <math>A\subseteq B</math>
  
<pre>
+
{{subst:C}}
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 1 578: Riadok 1 579:
 
     cout << "Their gcd: " << a << endl;
 
     cout << "Their gcd: " << a << endl;
 
}
 
}
</pre>
+
{{subst:/C}}
  
 
Príklad behu programu:
 
Príklad behu programu:
Riadok 1 595: Riadok 1 596:
  
 
===Nekonečný cyklus===
 
===Nekonečný cyklus===
<pre>
+
{{subst:C}}
 
while (true) {
 
while (true) {
 
  prikaz;
 
  prikaz;
 
}
 
}
</pre>
+
{{subst:/C}}
 
Opakuje príkaz donekonečna (kým program nezastavíme).  
 
Opakuje príkaz donekonečna (kým program nezastavíme).  
  
 
Napríklad môžeme donekonečna niečo vypisovať na konzolu.
 
Napríklad môžeme donekonečna niečo vypisovať na konzolu.
  
<pre>
+
{{subst:C}}
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 1 613: Riadok 1 614:
 
     }
 
     }
 
}
 
}
</pre>
+
{{subst:/C}}
  
 
===Hra hádaj číslo===
 
===Hra hádaj číslo===
Riadok 1 619: Riadok 1 620:
  
 
Program používa premennú typu '''bool''', do ktorej môžeme priraďovať logické hodnoty true alebo false.
 
Program používa premennú typu '''bool''', do ktorej môžeme priraďovať logické hodnoty true alebo false.
<pre>
+
{{subst:C}}
 
#include <iostream>
 
#include <iostream>
 
#include <cstdlib>
 
#include <cstdlib>
Riadok 1 649: Riadok 1 650:
 
     }
 
     }
 
}
 
}
</pre>
+
{{subst:/C}}
  
 
Príklad priebehu programu:
 
Príklad priebehu programu:
Riadok 1 669: Riadok 1 670:
  
 
Program uvedený vyššie môžeme prepísať bez boolovskej premennej s použitím nekonečného cyklu, z ktorého ale vyskočíme príkazom <tt>break</tt>, keď užívateľ uhádne.
 
Program uvedený vyššie môžeme prepísať bez boolovskej premennej s použitím nekonečného cyklu, z ktorého ale vyskočíme príkazom <tt>break</tt>, keď užívateľ uhádne.
<pre>
+
{{subst:C}}
 
#include <iostream>
 
#include <iostream>
 
#include <cstdlib>
 
#include <cstdlib>
Riadok 1 697: Riadok 1 698:
 
     }
 
     }
 
}
 
}
</pre>
+
{{subst:/C}}
 
-->
 
-->
 
=Prednáška 3=
 
=Prednáška 3=
Riadok 1 714: Riadok 1 715:
 
Nasledujúci program načíta od používateľa prirodzené číslo ''n'' a vypíše zoznam jeho deliteľov.
 
Nasledujúci program načíta od používateľa prirodzené číslo ''n'' a vypíše zoznam jeho deliteľov.
  
<pre>#include <iostream>
+
<syntaxhighlight lang="C++">#include <iostream>
 
using namespace std;
 
using namespace std;
  
Riadok 1 730: Riadok 1 731:
 
     cout << endl;
 
     cout << endl;
 
     return 0;
 
     return 0;
}</pre>
+
}</syntaxhighlight>
  
 
==Malá odbočka: úprava a čitateľnosť programov==
 
==Malá odbočka: úprava a čitateľnosť programov==
Riadok 1 785: Riadok 1 786:
  
 
Okrem cyklu <tt>for</tt> môžeme použiť aj cyklus <tt>while</tt>, ktorý vyzerá nasledovne:
 
Okrem cyklu <tt>for</tt> môžeme použiť aj cyklus <tt>while</tt>, ktorý vyzerá nasledovne:
<pre>
+
<syntaxhighlight lang="C++">
 
while (podmienka) {  
 
while (podmienka) {  
 
   príkaz;  
 
   príkaz;  
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Čítame: kým je splnená podmienka vykonávaj príkaz. Podrobnejšie:
 
Čítame: kým je splnená podmienka vykonávaj príkaz. Podrobnejšie:
Riadok 1 808: Riadok 1 809:
 
Potrebujeme cyklus, ktorý sa bude vykonávať, kým platí nejaká podmienka - práve na to je vhodný cyklus <tt>while</tt>. Pomocou neho zapíšeme algoritmus nasledovne:
 
Potrebujeme cyklus, ktorý sa bude vykonávať, kým platí nejaká podmienka - práve na to je vhodný cyklus <tt>while</tt>. Pomocou neho zapíšeme algoritmus nasledovne:
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 1 822: Riadok 1 823:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
<pre>
 
<pre>
Riadok 1 859: Riadok 1 860:
 
** taktiež ''x'' = ''a'' - ''yb'' a preto každý deliteľ ''a'' aj ''b'' delí aj ''x'', t.j. <math>A\subseteq B</math>
 
** taktiež ''x'' = ''a'' - ''yb'' a preto každý deliteľ ''a'' aj ''b'' delí aj ''x'', t.j. <math>A\subseteq B</math>
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 1 874: Riadok 1 875:
 
     cout << "Their gcd: " << a << endl;
 
     cout << "Their gcd: " << a << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Príklad behu programu:
 
Príklad behu programu:
Riadok 1 891: Riadok 1 892:
  
 
===Nekonečný cyklus===
 
===Nekonečný cyklus===
<pre>
+
<syntaxhighlight lang="C++">
 
while (true) {
 
while (true) {
 
  prikaz;
 
  prikaz;
 
}
 
}
</pre>
+
</syntaxhighlight>
 
Opakuje príkaz donekonečna (kým program nezastavíme).  
 
Opakuje príkaz donekonečna (kým program nezastavíme).  
  
 
Napríklad môžeme donekonečna niečo vypisovať na konzolu.
 
Napríklad môžeme donekonečna niečo vypisovať na konzolu.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 1 909: Riadok 1 910:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
===Hra hádaj číslo===
 
===Hra hádaj číslo===
 
V nasledujúcom programe si počítač "myslí číslo" od 1 do 100 a užívateľ háda, o ktoré číslo ide.  
 
V nasledujúcom programe si počítač "myslí číslo" od 1 do 100 a užívateľ háda, o ktoré číslo ide.  
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
#include <cstdlib>
 
#include <cstdlib>
Riadok 1 944: Riadok 1 945:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Príklad priebehu programu:
 
Príklad priebehu programu:
Riadok 1 964: Riadok 1 965:
  
 
Program uvedený vyššie môžeme prepísať bez boolovskej premennej s použitím nekonečného cyklu, z ktorého ale vyskočíme príkazom <tt>break</tt>, keď užívateľ uhádne.
 
Program uvedený vyššie môžeme prepísať bez boolovskej premennej s použitím nekonečného cyklu, z ktorého ale vyskočíme príkazom <tt>break</tt>, keď užívateľ uhádne.
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
#include <cstdlib>
 
#include <cstdlib>
Riadok 1 992: Riadok 1 993:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
==Späť k cyklu for==
 
==Späť k cyklu for==
  
 
Cyklus <tt>for</tt> má tvar
 
Cyklus <tt>for</tt> má tvar
<pre>
+
<syntaxhighlight lang="C++">
 
for(prikaz1; podmienka; prikaz2) {
 
for(prikaz1; podmienka; prikaz2) {
 
     prikaz3;
 
     prikaz3;
 
}
 
}
</pre>
+
</syntaxhighlight>
 
Je ekvivalentný nasledujúcemu príkazu <tt>while</tt>:
 
Je ekvivalentný nasledujúcemu príkazu <tt>while</tt>:
<pre>
+
<syntaxhighlight lang="C++">
 
prikaz1;
 
prikaz1;
 
while(podmienka) {
 
while(podmienka) {
Riadok 2 009: Riadok 2 010:
 
     prikaz2;
 
     prikaz2;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Napríklad, nasledujúce kúsky kódu oba vypisujú čísla 0 až 9:
 
Napríklad, nasledujúce kúsky kódu oba vypisujú čísla 0 až 9:
<pre>
+
<syntaxhighlight lang="C++">
 
for (int i = 0; i < 10; i++) {
 
for (int i = 0; i < 10; i++) {
 
     cout << " " << i;
 
     cout << " " << i;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
<pre>
+
<syntaxhighlight lang="C++">
 
int i = 0;
 
int i = 0;
 
while(i < 10) {
 
while(i < 10) {
Riadok 2 024: Riadok 2 025:
 
     i++;
 
     i++;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Cyklus <tt>for</tt> spravidla používame, ak má náš cyklus krátky a jednoduchý iterátor (prikaz2) a jednoduchú podmienku. V opačnom prípade väčšinou používame cyklus <tt>while</tt>.
 
Cyklus <tt>for</tt> spravidla používame, ak má náš cyklus krátky a jednoduchý iterátor (prikaz2) a jednoduchú podmienku. V opačnom prípade väčšinou používame cyklus <tt>while</tt>.
Riadok 2 035: Riadok 2 036:
 
* od najväčších:  <tt>for (int i = n; i > 0; i--) {</tt>
 
* od najväčších:  <tt>for (int i = n; i > 0; i--) {</tt>
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 2 052: Riadok 2 053:
 
     cout << endl;
 
     cout << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
  
Riadok 2 062: Riadok 2 063:
  
 
Zrýchlime program na vypisovanie deliteľov: ak ''i'' je deliteľ ''n'', aj ''n/i'' je deliteľ, môžeme ho rovno vypísať. Aspoň jeden z tejto dvojice je najviac odmocnina z ''n''.
 
Zrýchlime program na vypisovanie deliteľov: ak ''i'' je deliteľ ''n'', aj ''n/i'' je deliteľ, môžeme ho rovno vypísať. Aspoň jeden z tejto dvojice je najviac odmocnina z ''n''.
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 2 081: Riadok 2 082:
 
     cout << endl;
 
     cout << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
 
Cvičenie: Tento program nefunguje celkom správne, občas vypíše nejaké číslo dvakrát. Kedy? Ako ho vieme opraviť?
 
Cvičenie: Tento program nefunguje celkom správne, občas vypíše nejaké číslo dvakrát. Kedy? Ako ho vieme opraviť?
  
Riadok 2 092: Riadok 2 093:
 
* Ak row + column je párne, píšeme 0, inak píšeme 1.
 
* Ak row + column je párne, píšeme 0, inak píšeme 1.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
  
Riadok 2 110: Riadok 2 111:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
==Zhrnutie==
 
==Zhrnutie==
Riadok 2 138: Riadok 2 139:
  
 
Obvod spočítame ako súčet dĺžok jednotlivých strán, v programe teda trikrát opakujeme výpočet dĺžky strany. Pri opakovaní dlhšieho vzorca ľahko spravíme chybu.
 
Obvod spočítame ako súčet dĺžok jednotlivých strán, v programe teda trikrát opakujeme výpočet dĺžky strany. Pri opakovaní dlhšieho vzorca ľahko spravíme chybu.
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
#include <cmath>
 
#include <cmath>
Riadok 2 157: Riadok 2 158:
 
     cout << "Obvod trojuholnika ABC: " << da + db + dc << endl;
 
     cout << "Obvod trojuholnika ABC: " << da + db + dc << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
===Definícia funkcie===
 
===Definícia funkcie===
 
Vzorec na výpočet dĺžky úsečky s koncovými bodmi (x1,y1) a (x2,y2) môžeme zapísať ako funkciu:
 
Vzorec na výpočet dĺžky úsečky s koncovými bodmi (x1,y1) a (x2,y2) môžeme zapísať ako funkciu:
<pre>
+
<syntaxhighlight lang="C++">
 
double dlzka(double x1, double y1, double x2, double y2) {
 
double dlzka(double x1, double y1, double x2, double y2) {
 
     return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
 
     return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Definícia funkcie má nasledovné časti:
 
Definícia funkcie má nasledovné časti:
Riadok 2 177: Riadok 2 178:
 
Celý program s funkciou <tt>dlzka</tt>
 
Celý program s funkciou <tt>dlzka</tt>
 
* vzorec na výpočet dĺžky je teraz iba na jednom mieste
 
* vzorec na výpočet dĺžky je teraz iba na jednom mieste
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
#include <cmath>
 
#include <cmath>
Riadok 2 200: Riadok 2 201:
 
     cout << "Obvod trojuholnika ABC: " << da + db + dc << endl;
 
     cout << "Obvod trojuholnika ABC: " << da + db + dc << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Funkcia môže obsahovať aj viacero príkazov. V našom príklade môžeme napríklad výpočet rozložiť na tri kroky:
 
Funkcia môže obsahovať aj viacero príkazov. V našom príklade môžeme napríklad výpočet rozložiť na tri kroky:
<pre>
+
<syntaxhighlight lang="C++">
 
double dlzka(double x1, double y1, double x2, double y2) {
 
double dlzka(double x1, double y1, double x2, double y2) {
 
     double dx = x1 - x2;
 
     double dx = x1 - x2;
Riadok 2 209: Riadok 2 210:
 
     return sqrt(dx * dx + dy * dy);
 
     return sqrt(dx * dx + dy * dy);
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
'''Cvičenie:''' v Pascale je funkcia <tt>sqr(x)</tt>, ktorá vráti ''x'' na druhú. Naprogramujte túto funkciu v jazyku C a použite ju na zjednodušenie funkcie <tt>dlzka</tt>.  
 
'''Cvičenie:''' v Pascale je funkcia <tt>sqr(x)</tt>, ktorá vráti ''x'' na druhú. Naprogramujte túto funkciu v jazyku C a použite ju na zjednodušenie funkcie <tt>dlzka</tt>.  
Riadok 2 243: Riadok 2 244:
 
==Opakovanie: funkcie ==
 
==Opakovanie: funkcie ==
 
Výpočet dĺžky úsečky:
 
Výpočet dĺžky úsečky:
<pre>
+
<syntaxhighlight lang="C++">
 
double dlzka(double x1, double y1, double x2, double y2) {
 
double dlzka(double x1, double y1, double x2, double y2) {
 
     double dx = x1 - x2;
 
     double dx = x1 - x2;
Riadok 2 249: Riadok 2 250:
 
     return sqrt(dx * dx + dy * dy);
 
     return sqrt(dx * dx + dy * dy);
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Definícia funkcie má nasledovné časti:
 
Definícia funkcie má nasledovné časti:
Riadok 2 259: Riadok 2 260:
  
 
Celý program s funkciou <tt>dlzka</tt>:
 
Celý program s funkciou <tt>dlzka</tt>:
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
#include <cmath>
 
#include <cmath>
Riadok 2 285: Riadok 2 286:
 
     cout << "Obvod trojuholnika ABC: " << da + db + dc << endl;
 
     cout << "Obvod trojuholnika ABC: " << da + db + dc << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
  
Riadok 2 295: Riadok 2 296:
  
 
Dva spôsoby, ako napísať funkciu <tt>min</tt>, ktorá vráti menšie z dvoch zadaných čísel ''a'' a ''b'':
 
Dva spôsoby, ako napísať funkciu <tt>min</tt>, ktorá vráti menšie z dvoch zadaných čísel ''a'' a ''b'':
<pre>
+
<syntaxhighlight lang="C++">
 
int min(int a, int b) {
 
int min(int a, int b) {
 
     int minval;
 
     int minval;
Riadok 2 305: Riadok 2 306:
 
     return minval;
 
     return minval;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
<pre>
+
<syntaxhighlight lang="C++">
 
int min(int a, int b) {
 
int min(int a, int b) {
 
     if (a < b) {
 
     if (a < b) {
Riadok 2 315: Riadok 2 316:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Vo funkciách, ktoré vracajú <tt>void</tt>, môže ale nemusí byť <tt>return</tt>. To znamená, že oba nasledovné programy sú v poriadku.
 
Vo funkciách, ktoré vracajú <tt>void</tt>, môže ale nemusí byť <tt>return</tt>. To znamená, že oba nasledovné programy sú v poriadku.
<pre>
+
<syntaxhighlight lang="C++">
 
void printGreeting () {
 
void printGreeting () {
 
   cout << "Hello world!" << endl;
 
   cout << "Hello world!" << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
<pre>
+
<syntaxhighlight lang="C++">
 
void printGreeting () {
 
void printGreeting () {
 
   cout << "Hello world!" << endl;
 
   cout << "Hello world!" << endl;
 
   return;
 
   return;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
'''Cvičenie:''' čo vráti nasledujúca funkcia pre ''n=6''? Čo vráti pre ''n=7''? Viete stručne popísať, čo robí pre všeobecné ''n''?
 
'''Cvičenie:''' čo vráti nasledujúca funkcia pre ''n=6''? Čo vráti pre ''n=7''? Viete stručne popísať, čo robí pre všeobecné ''n''?
<pre>
+
<syntaxhighlight lang="C++">
 
int zahada(int n) {
 
int zahada(int n) {
 
     for (int i = 2; i * i <= n; i++) {
 
     for (int i = 2; i * i <= n; i++) {
Riadok 2 339: Riadok 2 340:
 
     return n;
 
     return n;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Lokálne a globálne premenné ==
 
== Lokálne a globálne premenné ==
Riadok 2 351: Riadok 2 352:
  
 
Príklad: program s globálnou premennou x a dvomi lokálnymi premennými v rôznych funkciách
 
Príklad: program s globálnou premennou x a dvomi lokálnymi premennými v rôznych funkciách
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 2 367: Riadok 2 368:
 
     vypis();
 
     vypis();
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
* Viaceré funkcie môžu mať lokálne premenné s tým istým menom, každá používa tú svoju
 
* Viaceré funkcie môžu mať lokálne premenné s tým istým menom, každá používa tú svoju
Riadok 2 373: Riadok 2 374:
  
 
Príklad:
 
Príklad:
<pre>
+
<syntaxhighlight lang="C++">
 
void funkcia1 () {
 
void funkcia1 () {
 
//  i=13; – nemôžeme používať premennú i – žiadna premenná i nie je viditeľná
 
//  i=13; – nemôžeme používať premennú i – žiadna premenná i nie je viditeľná
Riadok 2 404: Riadok 2 405:
 
     // lokálna premenná i má stále hodnotu 1, globálna premenná i má hodnotu 50,
 
     // lokálna premenná i má stále hodnotu 1, globálna premenná i má hodnotu 50,
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Parametre funkcií ==
 
== Parametre funkcií ==
Riadok 2 416: Riadok 2 417:
  
 
Príklad:
 
Príklad:
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 2 435: Riadok 2 436:
 
     vypis(y + 7);  /* vypise Predtym: 10, potom: 11 */
 
     vypis(y + 7);  /* vypise Predtym: 10, potom: 11 */
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Odovzdávanie parametrov referenciou===
 
=== Odovzdávanie parametrov referenciou===
Riadok 2 443: Riadok 2 444:
 
* Táto sa bude vo funkcii používať pod novým menom, prípadné zmeny sa prejavia aj na mieste, odkiaľ sme funkciu volali
 
* Táto sa bude vo funkcii používať pod novým menom, prípadné zmeny sa prejavia aj na mieste, odkiaľ sme funkciu volali
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 2 462: Riadok 2 463:
 
     /* vypis(y + 7);  toto teraz nemozeme pouzit, y+7 nie je premenna */
 
     /* vypis(y + 7);  toto teraz nemozeme pouzit, y+7 nie je premenna */
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Odovzdávanie referenciou: viac ako jedna návratová hodnota ===
 
=== Odovzdávanie referenciou: viac ako jedna návratová hodnota ===
Riadok 2 470: Riadok 2 471:
 
Funkcia <tt>stred</tt> dostane hodnotou súradnice dvoch bodov (x1,y1) a (x2,y2) a do parametrov xs, ys, ktoré sú odovzdané referenciou, uloží súradnice stredu úsečky.
 
Funkcia <tt>stred</tt> dostane hodnotou súradnice dvoch bodov (x1,y1) a (x2,y2) a do parametrov xs, ys, ktoré sú odovzdané referenciou, uloží súradnice stredu úsečky.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 2 493: Riadok 2 494:
 
     cout << "Stred usecky je [" << x << "," << y << "]." << endl;
 
     cout << "Stred usecky je [" << x << "," << y << "]." << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Odovzdávanie referenciou: Funkcia, ktorej úlohou je zmeniť svoj parameter ===
 
=== Odovzdávanie referenciou: Funkcia, ktorej úlohou je zmeniť svoj parameter ===
Riadok 2 499: Riadok 2 500:
 
* Typickým príkladom je funkcia <tt>swap</tt>, ktorá vymení hodnoty dvoch premenných, ktoré dostane ako parametre
 
* Typickým príkladom je funkcia <tt>swap</tt>, ktorá vymení hodnoty dvoch premenných, ktoré dostane ako parametre
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 2 515: Riadok 2 516:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Keby sme funkcii odovzdali parameter hodnotou <tt>void swap(int a, int b)</tt>, vo funkcii <tt>main</tt> by sa premenné nevymenili.
 
Keby sme funkcii odovzdali parameter hodnotou <tt>void swap(int a, int b)</tt>, vo funkcii <tt>main</tt> by sa premenné nevymenili.
Riadok 2 524: Riadok 2 525:
  
 
* Môžeme okamžite ukončiť celý program. V knižnici <tt>cstdlib</tt> je funkcia <tt>exit</tt>, ktorá robí presne to. Má ako parameter návratovú hodnotu celého programu
 
* Môžeme okamžite ukončiť celý program. V knižnici <tt>cstdlib</tt> je funkcia <tt>exit</tt>, ktorá robí presne to. Má ako parameter návratovú hodnotu celého programu
<pre>
+
<syntaxhighlight lang="C++">
 
double mySqrt(double x) {
 
double mySqrt(double x) {
 
     if (x < 0) { exit(1); }  // zlý vstup: ukončíme program
 
     if (x < 0) { exit(1); }  // zlý vstup: ukončíme program
 
     else { return sqrt(x); }  // dobrý vstup: spočítame odmocninu
 
     else { return sqrt(x); }  // dobrý vstup: spočítame odmocninu
 
}
 
}
</pre>
+
</syntaxhighlight>
 
* Pohodlnejší spôsob ukončenia programu pri zlom vstupe je funkcia <tt>assert</tt> z knižnice <tt>cassert</tt>, ktorá ako parameter dostane logickú hodnotu, a ak je tá hodnota nepravda, ukončí program s chybovou hláškou.
 
* Pohodlnejší spôsob ukončenia programu pri zlom vstupe je funkcia <tt>assert</tt> z knižnice <tt>cassert</tt>, ktorá ako parameter dostane logickú hodnotu, a ak je tá hodnota nepravda, ukončí program s chybovou hláškou.
 
** Pri dôležitých programoch ale nechceme, aby len tak skončili pri každej chybe
 
** Pri dôležitých programoch ale nechceme, aby len tak skončili pri každej chybe
<pre>
+
<syntaxhighlight lang="C++">
 
double mySqrt(double x) {
 
double mySqrt(double x) {
 
     assert(x >= 0);  // tvrdime, ze x>=0, inak program skonci  
 
     assert(x >= 0);  // tvrdime, ze x>=0, inak program skonci  
 
     return sqrt(x);  // teraz mozeme veselo odmocnovat  
 
     return sqrt(x);  // teraz mozeme veselo odmocnovat  
 
}
 
}
</pre>
+
</syntaxhighlight>
 
* Bežný spôsob, o ktorom sa budeme učiť v druhom semestri, sú takzvané ''výnimky'' (exceptions)
 
* Bežný spôsob, o ktorom sa budeme učiť v druhom semestri, sú takzvané ''výnimky'' (exceptions)
 
* Môžeme vrátiť nejakú špeciálnu hodnotu, ktorá odzrkadľuje, že vstup bol zlý. Používateľ funkcie by mal túto hodnotu rozoznať od správneho výstupu funkcie. Ale dôvera v používateľa sa všeobecne v softvérovom inžinierstve neodporúča.
 
* Môžeme vrátiť nejakú špeciálnu hodnotu, ktorá odzrkadľuje, že vstup bol zlý. Používateľ funkcie by mal túto hodnotu rozoznať od správneho výstupu funkcie. Ale dôvera v používateľa sa všeobecne v softvérovom inžinierstve neodporúča.
 
** Knižničná funkcia sqrt vráti pre záporné čísla špeciálnu hodnotu NaN (not a number), my vrátime napr. -1
 
** Knižničná funkcia sqrt vráti pre záporné čísla špeciálnu hodnotu NaN (not a number), my vrátime napr. -1
<pre>
+
<syntaxhighlight lang="C++">
 
double mySqrt(double x) {
 
double mySqrt(double x) {
 
     if (x < 0) { return -1; }  // zlý vstup: vrátime -1
 
     if (x < 0) { return -1; }  // zlý vstup: vrátime -1
 
     else { return sqrt(x);  }  // dobrý vstup: spočítame odmocninu
 
     else { return sqrt(x);  }  // dobrý vstup: spočítame odmocninu
 
}
 
}
</pre>
+
</syntaxhighlight>
 
* Môžeme ako výstup funkcie vrátiť, či sa podarilo alebo nepodarilo správne vypočítať hodnotu. Výstupom funkcie bude logická hodnota typu bool a samotnú hodnotu vrátime v parametri odovzdanom referenciou
 
* Môžeme ako výstup funkcie vrátiť, či sa podarilo alebo nepodarilo správne vypočítať hodnotu. Výstupom funkcie bude logická hodnota typu bool a samotnú hodnotu vrátime v parametri odovzdanom referenciou
<pre>
+
<syntaxhighlight lang="C++">
 
bool mySqrt(double x, double & result) {
 
bool mySqrt(double x, double & result) {
 
     if (x < 0) { return false; } // zlý vstup: vrátime false
 
     if (x < 0) { return false; } // zlý vstup: vrátime false
Riadok 2 556: Riadok 2 557:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Ešte jeden príklad: Fibonacciho čísla ==
 
== Ešte jeden príklad: Fibonacciho čísla ==
Riadok 2 569: Riadok 2 570:
  
 
Napíšeme funkciu, ktorá bude počítať Fibonacciho čísla. Na otázku "Aké je ôsme Fibonacciho číslo?" teda odpovie "34".
 
Napíšeme funkciu, ktorá bude počítať Fibonacciho čísla. Na otázku "Aké je ôsme Fibonacciho číslo?" teda odpovie "34".
<pre>
+
<syntaxhighlight lang="C++">
 
int fibonacci(int n) {
 
int fibonacci(int n) {
  
Riadok 2 582: Riadok 2 583:
  
 
     return f;
 
     return f;
}</pre>
+
}</syntaxhighlight>
 
'''Cvičenie:''' napíšte funkciu printFibonacci, ktorá vypíše prvých ''n'' Fibonacciho čísel.  
 
'''Cvičenie:''' napíšte funkciu printFibonacci, ktorá vypíše prvých ''n'' Fibonacciho čísel.  
 
* Akú by mala mať hlavičku?  
 
* Akú by mala mať hlavičku?  
Riadok 2 615: Riadok 2 616:
 
* V príkladoch s obvodom trojuholníka a stredom úsečky sme funkciám posielali veľa parametrov (súradnice x a y)
 
* V príkladoch s obvodom trojuholníka a stredom úsečky sme funkciám posielali veľa parametrov (súradnice x a y)
 
* Program bude krajší, ak si údaje o jednom bode spojíme do jedného záznamu
 
* Program bude krajší, ak si údaje o jednom bode spojíme do jedného záznamu
<pre>
+
<syntaxhighlight lang="C++">
 
struct bod {
 
struct bod {
 
     double x, y;
 
     double x, y;
 
};
 
};
</pre>
+
</syntaxhighlight>
 
* Pomocou <tt>struct</tt> vytvoríme nový dátový typ <tt>bod</tt>, ktorý má zložky x a y
 
* Pomocou <tt>struct</tt> vytvoríme nový dátový typ <tt>bod</tt>, ktorý má zložky x a y
 
** V jednom struct-e môžu byť aj položky rôznych typov (napr. <tt>struct bod { double x,y; int id; bool visible; };</tt>)
 
** V jednom struct-e môžu byť aj položky rôznych typov (napr. <tt>struct bod { double x,y; int id; bool visible; };</tt>)
Riadok 2 627: Riadok 2 628:
  
 
Nasledujúci program načíta súradnice troch bodov, spočíta obvod trojuholníka a stredy všetkých troch strán.
 
Nasledujúci program načíta súradnice troch bodov, spočíta obvod trojuholníka a stredy všetkých troch strán.
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
#include <cmath>
 
#include <cmath>
Riadok 2 686: Riadok 2 687:
 
     vypisBod(stredBC);
 
     vypisBod(stredBC);
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Príklad behu programu:
 
Príklad behu programu:
Riadok 2 710: Riadok 2 711:
  
 
=== Priemer ===
 
=== Priemer ===
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 2 730: Riadok 2 731:
 
     cout << "Priemer je " << priemer << "." << endl;
 
     cout << "Priemer je " << priemer << "." << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
* Čo by sa stalo, keby sme vo výpočte priemeru vynechali <tt>(double)</tt>?
 
* Čo by sa stalo, keby sme vo výpočte priemeru vynechali <tt>(double)</tt>?
Riadok 2 750: Riadok 2 751:
  
 
=== Priemer ===
 
=== Priemer ===
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 2 770: Riadok 2 771:
 
     cout << "Priemer je " << priemer << "." << endl;
 
     cout << "Priemer je " << priemer << "." << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Maximum ===
 
=== Maximum ===
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 2 796: Riadok 2 797:
 
     cout << endl << "Maximum je " << max << endl;
 
     cout << endl << "Maximum je " << max << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Ako ale začať? Ako nastaviť maximum na začiatok?
 
Ako ale začať? Ako nastaviť maximum na začiatok?
Riadok 2 803: Riadok 2 804:
 
* Ďalšia možnosť je si pamätať, že ešte nemáme správne nastavené maximum a po načítaní prvého čísla ho nastaviť alebo spracovať prvé číslo zvlášť (mimo cyklu)
 
* Ďalšia možnosť je si pamätať, že ešte nemáme správne nastavené maximum a po načítaní prvého čísla ho nastaviť alebo spracovať prvé číslo zvlášť (mimo cyklu)
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 2 826: Riadok 2 827:
 
     cout << endl << "Maximum je " << max << endl;
 
     cout << endl << "Maximum je " << max << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
'''Cvičenie:'''  
 
'''Cvičenie:'''  
Riadok 2 842: Riadok 2 843:
 
** pozor, <tt>a[20]</tt> neexistuje
 
** pozor, <tt>a[20]</tt> neexistuje
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 2 869: Riadok 2 870:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Na zamyslenie:
 
Na zamyslenie:
Riadok 2 879: Riadok 2 880:
  
 
Rozsah poľa je konštantný výraz väčší ako 0. Prvky sa indexujú od 0 po počet - 1
 
Rozsah poľa je konštantný výraz väčší ako 0. Prvky sa indexujú od 0 po počet - 1
<pre>
+
<syntaxhighlight lang="C++">
 
int a[10];
 
int a[10];
 
const int N=20;
 
const int N=20;
 
double b[N];
 
double b[N];
</pre>
+
</syntaxhighlight>
  
 
Ak nepoznáme vopred počet prvkov, ktoré chceme dať do poľa, môžeme odhadnúť, že ich nebude viac ako NMax, ktoré definujeme ako konštantu v programe.
 
Ak nepoznáme vopred počet prvkov, ktoré chceme dať do poľa, môžeme odhadnúť, že ich nebude viac ako NMax, ktoré definujeme ako konštantu v programe.
 
* Vytvoríme pole veľkosti NMax, použijeme z neho len prých N hodnôt
 
* Vytvoríme pole veľkosti NMax, použijeme z neho len prých N hodnôt
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 2 903: Riadok 2 904:
 
   cout << "Zadavajte " << N << " cisel: ";
 
   cout << "Zadavajte " << N << " cisel: ";
 
   ...
 
   ...
</pre>
+
</syntaxhighlight>
  
  
 
Čo ak ako veľkosť poľa použijeme premennú N, ktorú si prečítame od používateľa alebo inak spočítame za behu?
 
Čo ak ako veľkosť poľa použijeme premennú N, ktorú si prečítame od používateľa alebo inak spočítame za behu?
<pre>
+
<syntaxhighlight lang="C++">
 
     int N;
 
     int N;
 
     cout << "Zadaj pocet cisel: ";
 
     cout << "Zadaj pocet cisel: ";
 
     cin >> N;
 
     cin >> N;
 
     int p[N];
 
     int p[N];
</pre>
+
</syntaxhighlight>
 
* V starších verziách C resp. C++ to nefunguje, aj keď niektoré novšie kompilátory to zvládajú
 
* V starších verziách C resp. C++ to nefunguje, aj keď niektoré novšie kompilátory to zvládajú
 
* Na prednáškach tento spôsob nebudeme používať
 
* Na prednáškach tento spôsob nebudeme používať
Riadok 2 921: Riadok 2 922:
 
V definícii môžeme pole inicializovať zoznamom prvkov.  
 
V definícii môžeme pole inicializovať zoznamom prvkov.  
  
<pre>
+
<syntaxhighlight lang="C++">
 
int A[4]={3, 6, 8, 10}; //spravne
 
int A[4]={3, 6, 8, 10}; //spravne
 
int B[4]; //spravne
 
int B[4]; //spravne
 
B[4]={3, 6, 8, 10};  //nespravne, pole tu nedefinujeme
 
B[4]={3, 6, 8, 10};  //nespravne, pole tu nedefinujeme
 
B[0]=3; B[1]=6; B[2]=8; B[3]=10;  // spravne - menime prvky existujuceho pola
 
B[0]=3; B[1]=6; B[2]=8; B[3]=10;  // spravne - menime prvky existujuceho pola
</pre>
+
</syntaxhighlight>
  
 
=== Indexovanie hodnotou mimo intervalu ===
 
=== Indexovanie hodnotou mimo intervalu ===
  
 
Pozor, kompilátor nekontroluje indexy prvkov
 
Pozor, kompilátor nekontroluje indexy prvkov
<pre>
+
<syntaxhighlight lang="C++">
 
   int a[10];
 
   int a[10];
 
   a[10] = 1234;
 
   a[10] = 1234;
</pre>
+
</syntaxhighlight>
 
* Skompiluje, ale hodnota 1234 sa zapíše do pamäti na zlé miesto
 
* Skompiluje, ale hodnota 1234 sa zapíše do pamäti na zlé miesto
 
* Môže to mať nepredvídateľné následky: prepísanie obsahu iných premenných (chybný výpočet alebo „nevysvetliteľné“ správanie sa programu) alebo dokonca prepísanie časti kódu vášho programu
 
* Môže to mať nepredvídateľné následky: prepísanie obsahu iných premenných (chybný výpočet alebo „nevysvetliteľné“ správanie sa programu) alebo dokonca prepísanie časti kódu vášho programu
Riadok 2 943: Riadok 2 944:
 
Takýto príkaz však neskompiluje – nedá sa takto priraďovať, treba kopírovať prvok po prvku.
 
Takýto príkaz však neskompiluje – nedá sa takto priraďovať, treba kopírovať prvok po prvku.
  
<pre>
+
<syntaxhighlight lang="C++">
 
   for (i=0; i<10; i++) b[i]=a[i];
 
   for (i=0; i<10; i++) b[i]=a[i];
</pre>
+
</syntaxhighlight>
  
 
Polia sa tiež nedajú porovnávať pomocou operátora <tt>==</tt>. Podmienku <tt>if (a==b) cout << "Ok";</tt>.
 
Polia sa tiež nedajú porovnávať pomocou operátora <tt>==</tt>. Podmienku <tt>if (a==b) cout << "Ok";</tt>.
Riadok 2 951: Riadok 2 952:
 
Treba opäť porovnávať prvok po prvku.
 
Treba opäť porovnávať prvok po prvku.
  
<pre>
+
<syntaxhighlight lang="C++">
 
   bool rovne = true;
 
   bool rovne = true;
 
   for (i = 0; i < 10; i++) {  
 
   for (i = 0; i < 10; i++) {  
Riadok 2 957: Riadok 2 958:
 
   }
 
   }
 
   if (rovne) cout << "Rovnaju sa" << endl;  
 
   if (rovne) cout << "Rovnaju sa" << endl;  
</pre>
+
</syntaxhighlight>
  
 
Ten istý kúsok programu môžeme napísať napr. aj takto:
 
Ten istý kúsok programu môžeme napísať napr. aj takto:
  
<pre>
+
<syntaxhighlight lang="C++">
 
   bool rovne = true;
 
   bool rovne = true;
 
   for (i = 0; i < 10; i++) {  
 
   for (i = 0; i < 10; i++) {  
Riadok 2 970: Riadok 2 971:
 
   }
 
   }
 
   if (rovne) cout << "Rovnaju sa" << endl;  
 
   if (rovne) cout << "Rovnaju sa" << endl;  
</pre>
+
</syntaxhighlight>
  
 
== Výskyty čísel 0...9 ==
 
== Výskyty čísel 0...9 ==
Riadok 2 984: Riadok 2 985:
 
* vytvoríme si pole počítadiel dĺžky 10, v ktorom p[i] bude počet výskytov čísla ''i''
 
* vytvoríme si pole počítadiel dĺžky 10, v ktorom p[i] bude počet výskytov čísla ''i''
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 3 011: Riadok 3 012:
 
         cout << "Pocet vyskytov " << i << " je " << p[i] << endl; // výpis
 
         cout << "Pocet vyskytov " << i << " je " << p[i] << endl; // výpis
 
     }
 
     }
}</pre>
+
}</syntaxhighlight>
  
  
Riadok 3 023: Riadok 3 024:
 
* Na začiatku programu zapnite knižnicu pomocou <tt>#include "SVGdraw.h"</tt>
 
* Na začiatku programu zapnite knižnicu pomocou <tt>#include "SVGdraw.h"</tt>
 
* Tu je malý ukážkový program, ktorý vykreslí zelený štvorec a v ňom červený kruh:
 
* Tu je malý ukážkový program, ktorý vykreslí zelený štvorec a v ňom červený kruh:
<pre>
+
<syntaxhighlight lang="C++">
 
#include "SVGdraw.h"
 
#include "SVGdraw.h"
  
Riadok 3 045: Riadok 3 046:
 
   drawing.finish();
 
   drawing.finish();
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Kreslíme kruhy ==
 
== Kreslíme kruhy ==
Riadok 3 054: Riadok 3 055:
 
* Navyše o každom kruhu zistí, či sa pretína s iným kruhom, a ak áno, pri vykresľovaní ho orámuje červenou farbou.
 
* Navyše o každom kruhu zistí, či sa pretína s iným kruhom, a ak áno, pri vykresľovaní ho orámuje červenou farbou.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include "SVGdraw.h"
 
#include "SVGdraw.h"
 
#include <cstdlib>
 
#include <cstdlib>
Riadok 3 125: Riadok 3 126:
 
     drawing.finish();
 
     drawing.finish();
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Eratostenovo sito ==
 
== Eratostenovo sito ==
Riadok 3 136: Riadok 3 137:
 
* Potom prechádzame v poli, kým nenájdeme najbližšiu ďalšiu hodnotu <tt>true</tt>. Toto číslo je prvočíslo (vypíšeme ho) a vyškrtáme jeho násobky.  
 
* Potom prechádzame v poli, kým nenájdeme najbližšiu ďalšiu hodnotu <tt>true</tt>. Toto číslo je prvočíslo (vypíšeme ho) a vyškrtáme jeho násobky.  
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 3 157: Riadok 3 158:
 
     cout << endl;
 
     cout << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Výstup programu
 
Výstup programu
Riadok 3 192: Riadok 3 193:
 
** Bez <tt>&</tt> sa skopíruje hodnota
 
** Bez <tt>&</tt> sa skopíruje hodnota
 
** S <tt>&</tt> premenná dostane vo funkcii nové meno  
 
** S <tt>&</tt> premenná dostane vo funkcii nové meno  
<pre>
+
<syntaxhighlight lang="C++">
 
void f1(int x) {
 
void f1(int x) {
 
     x++; // zmena x sa neprenesie do main (y zostane rovnaká)
 
     x++; // zmena x sa neprenesie do main (y zostane rovnaká)
Riadok 3 210: Riadok 3 211:
 
     //zle: f2(y+1);
 
     //zle: f2(y+1);
 
}
 
}
</pre>
+
</syntaxhighlight>
 
* Polia odovzdávame bez <tt>&</tt>
 
* Polia odovzdávame bez <tt>&</tt>
 
** väčšinou potrebujeme poslať aj veľkosť poľa, ak nie je globálne známa
 
** väčšinou potrebujeme poslať aj veľkosť poľa, ak nie je globálne známa
 
** zmeny v poli zostanú aj po skončení funkcie
 
** zmeny v poli zostanú aj po skončení funkcie
<pre>
+
<syntaxhighlight lang="C++">
 
void f(int a[], int n) {
 
void f(int a[], int n) {
 
     for (int i = 0; i < n; i++) {
 
     for (int i = 0; i < n; i++) {
Riadok 3 225: Riadok 3 226:
 
     f(b, 3);
 
     f(b, 3);
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
* SVGdraw obrázky sú v skutočnosti objekty, väčšinou ich chcete posielať s <tt>&</tt>
 
* SVGdraw obrázky sú v skutočnosti objekty, väčšinou ich chcete posielať s <tt>&</tt>
 
** všetky zmeny na nich spravené pretrvávajú aj po skončení funkcie
 
** všetky zmeny na nich spravené pretrvávajú aj po skončení funkcie
<pre>
+
<syntaxhighlight lang="C++">
 
#include "SVGdraw.h"
 
#include "SVGdraw.h"
 
void kresli(SVGdraw &drawing, int n, int y, int rozostup, int velkost) {
 
void kresli(SVGdraw &drawing, int n, int y, int rozostup, int velkost) {
Riadok 3 241: Riadok 3 242:
 
     drawing.finish();
 
     drawing.finish();
 
}
 
}
</pre>
+
</syntaxhighlight>
 
* Štruktúry (<tt>struct</tt>) väčšinou tiež posielame pomocou <tt>&</tt>
 
* Štruktúry (<tt>struct</tt>) väčšinou tiež posielame pomocou <tt>&</tt>
 
* Návratové hodnoty:  
 
* Návratové hodnoty:  
Riadok 3 261: Riadok 3 262:
 
* zopakujeme si tiež ako sa pracuje s poliami vo funkciách
 
* zopakujeme si tiež ako sa pracuje s poliami vo funkciách
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdlib>
 
#include <cstdlib>
 
#include <iostream>
 
#include <iostream>
Riadok 3 303: Riadok 3 304:
 
}
 
}
  
</pre>
+
</syntaxhighlight>
  
 
==Triedenia==
 
==Triedenia==
Riadok 3 316: Riadok 3 317:
  
 
Idea: Kontrolujeme všetky dvojice susedných prvkov a keď vidíme menšie číslo za väčším, vymeníme ich
 
Idea: Kontrolujeme všetky dvojice susedných prvkov a keď vidíme menšie číslo za väčším, vymeníme ich
<pre>
+
<syntaxhighlight lang="C++">
 
for (int i = 1; i < n; i++) {
 
for (int i = 1; i < n; i++) {
 
     if (a[i] < a[i - 1]) {
 
     if (a[i] < a[i - 1]) {
Riadok 3 322: Riadok 3 323:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
 
* Ak sme nenašli v poli žiadnu dvojicu, ktorú treba vymeniť, skončili sme.
 
* Ak sme nenašli v poli žiadnu dvojicu, ktorú treba vymeniť, skončili sme.
 
* Inak celý proces opakujeme znova.
 
* Inak celý proces opakujeme znova.
  
 
Celé triedenie:
 
Celé triedenie:
<pre>
+
<syntaxhighlight lang="C++">
 
void swap(int &x, int &y) {
 
void swap(int &x, int &y) {
 
     /* Vymeň hodnoty premenných x a y. */
 
     /* Vymeň hodnoty premenných x a y. */
Riadok 3 354: Riadok 3 355:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
 
* Čo ak vo for cykle dám <tt>for (int i = 0; i < n; i++) {</tt>
 
* Čo ak vo for cykle dám <tt>for (int i = 0; i < n; i++) {</tt>
 
* Ako nahradím premennú <tt>hotovo</tt> príkazom <tt>break</tt>?
 
* Ako nahradím premennú <tt>hotovo</tt> príkazom <tt>break</tt>?
Riadok 3 399: Riadok 3 400:
 
* V cykle uvádzame ako komentár invariant, podmienku, ktorá na tom mieste vždy platí. Takýto invariant nám pomôže si uvedomiť, že je náš program správny.
 
* V cykle uvádzame ako komentár invariant, podmienku, ktorá na tom mieste vždy platí. Takýto invariant nám pomôže si uvedomiť, že je náš program správny.
  
<pre>
+
<syntaxhighlight lang="C++">
 
int maxIndex(int a[], int n) {
 
int maxIndex(int a[], int n) {
 
     /* vráť index, na ktorom je najväčší prvok z prvkov a[0]...a[n-1] */
 
     /* vráť index, na ktorom je najväčší prvok z prvkov a[0]...a[n-1] */
Riadok 3 422: Riadok 3 423:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Príklad behu programu
 
Príklad behu programu
Riadok 3 456: Riadok 3 457:
 
* Utriedené prvky posúvame o jedno doprava, kým nenájdeme správne miesto pre odložený prvok
 
* Utriedené prvky posúvame o jedno doprava, kým nenájdeme správne miesto pre odložený prvok
  
<pre>
+
<syntaxhighlight lang="C++">
 
void sort(int a[], int n) {
 
void sort(int a[], int n) {
 
     /* usporiadaj prvky v poli a od najmenšieho po najväčší */
 
     /* usporiadaj prvky v poli a od najmenšieho po najväčší */
Riadok 3 470: Riadok 3 471:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
* Všimnime si podmienku <tt>(kam > 0 && a[kam - 1] > prvok)</tt>
 
* Všimnime si podmienku <tt>(kam > 0 && a[kam - 1] > prvok)</tt>
Riadok 3 506: Riadok 3 507:
  
 
==Zdrojový kód programu s triedeniami==
 
==Zdrojový kód programu s triedeniami==
<pre>
+
<syntaxhighlight lang="C++">
 
/* Program s triedeniami z prednášky 6.
 
/* Program s triedeniami z prednášky 6.
 
   http://compbio.fmph.uniba.sk/vyuka/prog/index.php/Predn%C3%A1%C5%A1ka_6 */
 
   http://compbio.fmph.uniba.sk/vyuka/prog/index.php/Predn%C3%A1%C5%A1ka_6 */
Riadok 3 613: Riadok 3 614:
 
     printArray(a,n);
 
     printArray(a,n);
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Vyhľadávanie prvkov poľa ==  
 
== Vyhľadávanie prvkov poľa ==  
Riadok 3 621: Riadok 3 622:
 
* Tento algoritmus sa nazýva lineárne vyhľadávanie
 
* Tento algoritmus sa nazýva lineárne vyhľadávanie
  
<pre>
+
<syntaxhighlight lang="C++">
 
/* Funkcia find vráti index výskytu prvku x
 
/* Funkcia find vráti index výskytu prvku x
 
  * v poli a. Ak sa x v poli nevyskytuje, vráti -1.
 
  * v poli a. Ak sa x v poli nevyskytuje, vráti -1.
Riadok 3 631: Riadok 3 632:
 
     return -1;
 
     return -1;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
* Toto môžeme urobiť aj v prípade, že máme pole usporiadané.
 
* Toto môžeme urobiť aj v prípade, že máme pole usporiadané.
Riadok 3 651: Riadok 3 652:
 
* ak už je interval zlý (t.j. pravý a ľavý kraj sú naopak), tak skončíme
 
* ak už je interval zlý (t.j. pravý a ľavý kraj sú naopak), tak skončíme
  
<pre>
+
<syntaxhighlight lang="C++">
 
int find(int a[], int n, int x) {
 
int find(int a[], int n, int x) {
 
     int left = 0, right = n - 1;
 
     int left = 0, right = n - 1;
Riadok 3 668: Riadok 3 669:
 
     return -1;
 
     return -1;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Ako zvoliť hodnotu index?
 
Ako zvoliť hodnotu index?
Riadok 3 728: Riadok 3 729:
 
Pre niektoré programy môžeme spočítať počet operácií, ktoré program vykoná. Avšak tento počet môže závisieť od konkrétneho vstupu.
 
Pre niektoré programy môžeme spočítať počet operácií, ktoré program vykoná. Avšak tento počet môže závisieť od konkrétneho vstupu.
 
* Ukážme si túto metódu pre lineárne vyhľadávanie v neutriedenom poli
 
* Ukážme si túto metódu pre lineárne vyhľadávanie v neutriedenom poli
<pre>
+
<syntaxhighlight lang="C++">
 
int find(int a[], int n, int x) {
 
int find(int a[], int n, int x) {
 
     for (int i = 0; i < n; i++) {
 
     for (int i = 0; i < n; i++) {
Riadok 3 735: Riadok 3 736:
 
     return -1;
 
     return -1;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
* Najmenej operácií spravíme, ak ''x'' je v ''a[0]''
 
* Najmenej operácií spravíme, ak ''x'' je v ''a[0]''
Riadok 3 779: Riadok 3 780:
 
* Na to musíme posunúť všetky väčšie prvky o jedna doprava, aby sme mu spravili miesto
 
* Na to musíme posunúť všetky väčšie prvky o jedna doprava, aby sme mu spravili miesto
  
<pre>
+
<syntaxhighlight lang="C++">
 
void sort(int a[], int n) {
 
void sort(int a[], int n) {
 
     /* usporiadaj prvky v poli a od najmenšieho po najväčší */
 
     /* usporiadaj prvky v poli a od najmenšieho po najväčší */
Riadok 3 793: Riadok 3 794:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
* V najhoršom prípade pre dané ''i'' bude ''a[i]'' menšie ako všetky doteraz utriedené prvky a teda while cyklus bude bežať ''i'' krát
 
* V najhoršom prípade pre dané ''i'' bude ''a[i]'' menšie ako všetky doteraz utriedené prvky a teda while cyklus bude bežať ''i'' krát
Riadok 3 840: Riadok 3 841:
  
 
Do premennej typu char môžeme priraďovať, jej obsah zapísať alebo prečítať:
 
Do premennej typu char môžeme priraďovať, jej obsah zapísať alebo prečítať:
<pre>
+
<syntaxhighlight lang="C++">
 
   char c='A';
 
   char c='A';
 
   char z;
 
   char z;
Riadok 3 846: Riadok 3 847:
 
   cout << c;
 
   cout << c;
 
   cin >> z;  // prečíta jeden znak (pozor, preskakujú sa biele znaky)
 
   cin >> z;  // prečíta jeden znak (pozor, preskakujú sa biele znaky)
</pre>
+
</syntaxhighlight>
  
 
Znaky môžeme porovnávať. Na konci programu vyššie platí nasledovné:
 
Znaky môžeme porovnávať. Na konci programu vyššie platí nasledovné:
Riadok 3 855: Riadok 3 856:
 
Pri čítaní zo vstupu pomocou <tt>cin</tt> do premennej typu <tt>char</tt> sa preskakujú tzv. biele znaky (napr. medzera, tabulátor, koniec riadku).  
 
Pri čítaní zo vstupu pomocou <tt>cin</tt> do premennej typu <tt>char</tt> sa preskakujú tzv. biele znaky (napr. medzera, tabulátor, koniec riadku).  
 
* Toto nie je vždy žiadúce a preto môžeme použiť modifikátor <tt>noskipws</tt>, ktorý zruší preskakovanie takýchto znakov. Do premennej teda budeme vedieť prečítať aj medzeru.
 
* Toto nie je vždy žiadúce a preto môžeme použiť modifikátor <tt>noskipws</tt>, ktorý zruší preskakovanie takýchto znakov. Do premennej teda budeme vedieť prečítať aj medzeru.
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 3 864: Riadok 3 865:
 
   cout << a << b << c;
 
   cout << a << b << c;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Hodnota jednoduchého výrazu ===
 
=== Hodnota jednoduchého výrazu ===
Riadok 3 872: Riadok 3 873:
 
Napr. pre vstup <tt>1 / 2</tt> program vypíše <tt>0.5</tt>.
 
Napr. pre vstup <tt>1 / 2</tt> program vypíše <tt>0.5</tt>.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
#include <cstdlib>
 
#include <cstdlib>
Riadok 3 899: Riadok 3 900:
 
     cout << vysledok << endl;
 
     cout << vysledok << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Switch ==
 
== Switch ==
Riadok 3 907: Riadok 3 908:
  
 
V našom jednoduchom príklade by mohol <tt>switch</tt> vyzerať nasledovne:
 
V našom jednoduchom príklade by mohol <tt>switch</tt> vyzerať nasledovne:
<pre>
+
<syntaxhighlight lang="C++">
 
     switch (znamienko) {
 
     switch (znamienko) {
 
     case '+' :
 
     case '+' :
Riadok 3 925: Riadok 3 926:
 
         exit(1);
 
         exit(1);
 
     }
 
     }
</pre>
+
</syntaxhighlight>
  
 
Vo všeobecnosti obsahuje príkaz switch viacero rôznych prípadov vyhodnotenia výrazu v podmienke.
 
Vo všeobecnosti obsahuje príkaz switch viacero rôznych prípadov vyhodnotenia výrazu v podmienke.
  
<pre>
+
<syntaxhighlight lang="C++">
 
switch (výraz)
 
switch (výraz)
 
{
 
{
Riadok 3 936: Riadok 3 937:
 
default: príkazyd
 
default: príkazyd
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Takýto príkaz funguje nasledovne:
 
Takýto príkaz funguje nasledovne:
Riadok 3 946: Riadok 3 947:
 
** Toto je častá chyba pri použití príkazu switch
 
** Toto je častá chyba pri použití príkazu switch
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream.h>
 
#include <iostream.h>
  
Riadok 3 962: Riadok 3 963:
 
   cout << "Koniec." << endl;
 
   cout << "Koniec." << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Pre n=2 sa začnú vykonávať príkazy uvedené za vetvou case 2:. Vypíše sa:
 
Pre n=2 sa začnú vykonávať príkazy uvedené za vetvou case 2:. Vypíše sa:
Riadok 3 978: Riadok 3 979:
  
 
Predstavme si, že v programe sa pýtame užívateľa, či chce pokračovať s ďalším vstupom, pričom odpoveď má byť znak 'A' alebo 'N'. Ak by sme program napísali takto, nefungoval by:
 
Predstavme si, že v programe sa pýtame užívateľa, či chce pokračovať s ďalším vstupom, pričom odpoveď má byť znak 'A' alebo 'N'. Ak by sme program napísali takto, nefungoval by:
<pre>
+
<syntaxhighlight lang="C++">
 
     while (true) {
 
     while (true) {
 
         // nejaky vypocet
 
         // nejaky vypocet
Riadok 3 990: Riadok 3 991:
 
         }
 
         }
 
     }
 
     }
</pre>
+
</syntaxhighlight>
  
 
* Príkaz <tt>break</tt> nevyskočí zo všetkých cyklov, ale iba z najvnútornejšieho - a tým je v tomto prípade <tt>switch</tt>. Program teda pokračuje aj keď užívateľ zadá 'N'.
 
* Príkaz <tt>break</tt> nevyskočí zo všetkých cyklov, ale iba z najvnútornejšieho - a tým je v tomto prípade <tt>switch</tt>. Program teda pokračuje aj keď užívateľ zadá 'N'.
Riadok 4 000: Riadok 4 001:
 
Znakové premenné teda ukladajú kódy jednotlivých znakov, čo sú celé čísla. Preto medzi znakmi a celými číslami môžeme prechádzať úplne jednoducho.
 
Znakové premenné teda ukladajú kódy jednotlivých znakov, čo sú celé čísla. Preto medzi znakmi a celými číslami môžeme prechádzať úplne jednoducho.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include<iostream>
 
#include<iostream>
 
using namespace std;
 
using namespace std;
Riadok 4 018: Riadok 4 019:
 
   N = c;                        // do celočíselnej premennej ho priamo vieme priradiť
 
   N = c;                        // do celočíselnej premennej ho priamo vieme priradiť
 
   cout << N << endl;            // vypíšeme ho ako číslo
 
   cout << N << endl;            // vypíšeme ho ako číslo
}</pre>
+
}</syntaxhighlight>
  
 
Okrem toho však vieme urobiť aj pretypovanie, keď chceme aby výsledok bol konkrétneho typu.
 
Okrem toho však vieme urobiť aj pretypovanie, keď chceme aby výsledok bol konkrétneho typu.
 
Napríklad, aby nám v prvej časti vypísalo nie ďalší kód ale ďalší znak, mohli sme výsledok pretypovať.
 
Napríklad, aby nám v prvej časti vypísalo nie ďalší kód ale ďalší znak, mohli sme výsledok pretypovať.
  
<pre>
+
<syntaxhighlight lang="C++">
 
   cout << (char)(c+1) << endl;          // vďaka pretypovaniu dostávame na výpise znak
 
   cout << (char)(c+1) << endl;          // vďaka pretypovaniu dostávame na výpise znak
</pre>
+
</syntaxhighlight>
  
 
Pretypovanie sme už používali, keď sme chceli dva inty vydeliť bez zaokrúhlenia na celé číslo:
 
Pretypovanie sme už používali, keď sme chceli dva inty vydeliť bez zaokrúhlenia na celé číslo:
  
<pre>
+
<syntaxhighlight lang="C++">
 
   int a = 5;
 
   int a = 5;
 
   int b = 2;
 
   int b = 2;
 
   cout << a/b << " " << (double)(a/b) << " " << ((double)a)/b << endl;
 
   cout << a/b << " " << (double)(a/b) << " " << ((double)a)/b << endl;
</pre>
+
</syntaxhighlight>
  
 
Tento program vypíše nasledovný výstup:
 
Tento program vypíše nasledovný výstup:
Riadok 4 079: Riadok 4 080:
 
* Namiesto niekoľkých vnorených <tt>if</tt> s tou istou premennou v podmienke môžeme použiť <tt>switch</tt>.
 
* Namiesto niekoľkých vnorených <tt>if</tt> s tou istou premennou v podmienke môžeme použiť <tt>switch</tt>.
 
* Vo všeobecnosti obsahuje príkaz switch viacero rôznych prípadov vyhodnotenia výrazu v podmienke.
 
* Vo všeobecnosti obsahuje príkaz switch viacero rôznych prípadov vyhodnotenia výrazu v podmienke.
<pre>
+
<syntaxhighlight lang="C++">
 
switch (výraz)
 
switch (výraz)
 
{
 
{
Riadok 4 086: Riadok 4 087:
 
default: príkazyd
 
default: príkazyd
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
* Pozor: Na rozdiel od pascalovského <tt>case</tt>, vykonávanie nekončí vykonaním posledného príkazu v <tt>prikazyi</tt>, ale pokračuje ďalej, kým nie je prerušené príkazom <tt>break</tt>.  
 
* Pozor: Na rozdiel od pascalovského <tt>case</tt>, vykonávanie nekončí vykonaním posledného príkazu v <tt>prikazyi</tt>, ale pokračuje ďalej, kým nie je prerušené príkazom <tt>break</tt>.  
Riadok 4 095: Riadok 4 096:
 
Vďaka znakom môžeme spraviť kontrolu toho, čo vlastne používateľ napísal na vstup. Napríklad, či zadal správne celé číslo a nenamiešal medzi cifry nejaký iný znak.
 
Vďaka znakom môžeme spraviť kontrolu toho, čo vlastne používateľ napísal na vstup. Napríklad, či zadal správne celé číslo a nenamiešal medzi cifry nejaký iný znak.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 4 118: Riadok 4 119:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Reťazec ako postupnosť znakov ==
 
== Reťazec ako postupnosť znakov ==
Riadok 4 125: Riadok 4 126:
 
* Napríklad funkcia <tt>nacitajRiadok</tt>, ktorá znak po znaku načíta text z konzoly, znaky uloží do poľa ''a'', dĺžku textu do premennej ''n''.
 
* Napríklad funkcia <tt>nacitajRiadok</tt>, ktorá znak po znaku načíta text z konzoly, znaky uloží do poľa ''a'', dĺžku textu do premennej ''n''.
 
* Naopak funkcia <tt>vypisRetazec</tt> dostane pole znakov a jeho dĺžku a vypisuje znak po znaku.
 
* Naopak funkcia <tt>vypisRetazec</tt> dostane pole znakov a jeho dĺžku a vypisuje znak po znaku.
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 4 159: Riadok 4 160:
 
     vypisRetazec(a, n);
 
     vypisRetazec(a, n);
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
* Nevýhoda tohto riešenia: reťazec musíme posielať všade ako dve premenné (pole a dĺžku)
 
* Nevýhoda tohto riešenia: reťazec musíme posielať všade ako dve premenné (pole a dĺžku)
Riadok 4 178: Riadok 4 179:
 
* Chceme vytvoriť premennú <tt>str</tt> obsahujúce reťazec <tt>Ahoj</tt> spolu s koncom riadku
 
* Chceme vytvoriť premennú <tt>str</tt> obsahujúce reťazec <tt>Ahoj</tt> spolu s koncom riadku
 
* Prvý spôsob je zdĺhavý:
 
* Prvý spôsob je zdĺhavý:
<pre>
+
<syntaxhighlight lang="C++">
 
     char str[10];
 
     char str[10];
 
     str[0] = 'A';
 
     str[0] = 'A';
Riadok 4 186: Riadok 4 187:
 
     str[4] = '\n'; // znak pre koniec riadku
 
     str[4] = '\n'; // znak pre koniec riadku
 
     str[5] = 0;
 
     str[5] = 0;
</pre>
+
</syntaxhighlight>
 
* Alebo ako inicializácia poľa: <tt>char str[10]={'A','h','o','j','\n',0};</tt>
 
* Alebo ako inicializácia poľa: <tt>char str[10]={'A','h','o','j','\n',0};</tt>
 
* Namiesto toho sa používa špeciálna skratka: <tt>char str[10]="Ahoj\n";</tt>
 
* Namiesto toho sa používa špeciálna skratka: <tt>char str[10]="Ahoj\n";</tt>
Riadok 4 195: Riadok 4 196:
  
 
'''Znaky reťazca môžeme meniť'''
 
'''Znaky reťazca môžeme meniť'''
<pre>
+
<syntaxhighlight lang="C++">
 
char a[100] = "vlk";
 
char a[100] = "vlk";
 
char ch = a[0]; // ch obsahuje hodnotu 'v'
 
char ch = a[0]; // ch obsahuje hodnotu 'v'
Riadok 4 203: Riadok 4 204:
 
b[0] = 'd';    // priradíme do jedného prvku reťazca konštantný znak. Výsledkom je 'des'.  
 
b[0] = 'd';    // priradíme do jedného prvku reťazca konštantný znak. Výsledkom je 'des'.  
 
b[0] = a[1];  // priradíme do jedného prvku reťazca prvok iného reťazca. Výsledkom je 'les'.  
 
b[0] = a[1];  // priradíme do jedného prvku reťazca prvok iného reťazca. Výsledkom je 'les'.  
</pre>
+
</syntaxhighlight>
  
 
'''Reťazec sa nedá kopírovať jednoduchým priradením''', nemôžeme teda spraviť
 
'''Reťazec sa nedá kopírovať jednoduchým priradením''', nemôžeme teda spraviť
<pre>
+
<syntaxhighlight lang="C++">
 
char a[100];
 
char a[100];
 
a = "Ahoj";          // chyba
 
a = "Ahoj";          // chyba
 
char b[100] = "Ahoj"; // ok - inicializacia
 
char b[100] = "Ahoj"; // ok - inicializacia
 
a = b;                // chyba
 
a = b;                // chyba
</pre>
+
</syntaxhighlight>
  
 
'''Reťazce sa nedajú ani porovnávať pomocou ==, !=, < atď'''
 
'''Reťazce sa nedajú ani porovnávať pomocou ==, !=, < atď'''
Riadok 4 226: Riadok 4 227:
  
 
Všetky tieto funkcie by sme si však vedeli naprogramovať aj sami. Tu je napríklad výpočet dĺžky:
 
Všetky tieto funkcie by sme si však vedeli naprogramovať aj sami. Tu je napríklad výpočet dĺžky:
<pre>
+
<syntaxhighlight lang="C++">
 
int myStrLen(char a[]) {
 
int myStrLen(char a[]) {
 
     int n=0;  
 
     int n=0;  
Riadok 4 232: Riadok 4 233:
 
     return n;
 
     return n;
 
}
 
}
</pre>
+
</syntaxhighlight>
 
* čo bude funkcia robiť ak reťazcu chýba na konci 0?
 
* čo bude funkcia robiť ak reťazcu chýba na konci 0?
  
 
Dve verzie kopírovania:
 
Dve verzie kopírovania:
<pre>
+
<syntaxhighlight lang="C++">
 
void myStrCpy(char a[], char b[]) {
 
void myStrCpy(char a[], char b[]) {
 
     /* Skopiruj obsah retazca b do retazca a.
 
     /* Skopiruj obsah retazca b do retazca a.
Riadok 4 255: Riadok 4 256:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
 
* Ktorá je rýchlejšia pre dlhé reťazce?
 
* Ktorá je rýchlejšia pre dlhé reťazce?
 
* Aká je ich zložitosť ako funkcia dĺžky reťazca ''n''?
 
* Aká je ich zložitosť ako funkcia dĺžky reťazca ''n''?
  
 
Namiesto <tt>strcmp</tt> naprogramujeme len test na rovnosť:
 
Namiesto <tt>strcmp</tt> naprogramujeme len test na rovnosť:
<pre>
+
<syntaxhighlight lang="C++">
 
bool rovnostRetazcov(char a[], char b[]) {
 
bool rovnostRetazcov(char a[], char b[]) {
 
     /* vrati true ak su retazce a, b rovnake, inak vrati false */
 
     /* vrati true ak su retazce a, b rovnake, inak vrati false */
Riadok 4 269: Riadok 4 270:
 
     return true;
 
     return true;
 
}
 
}
</pre>
+
</syntaxhighlight>
 
* Ako bude prebiehať funkcia, ak jeden reťazec je začiatkom druhého?
 
* Ako bude prebiehať funkcia, ak jeden reťazec je začiatkom druhého?
  
Riadok 4 280: Riadok 4 281:
 
* Vypisovanie funguje normálne pomocou <tt>cout << str</tt>
 
* Vypisovanie funguje normálne pomocou <tt>cout << str</tt>
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 4 299: Riadok 4 300:
 
     cout << "str3: \"" << str3 << "\"" << endl;
 
     cout << "str3: \"" << str3 << "\"" << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Príklad behu programu (prvé dva riadky zadal užívateľ, na začiatku a konci každého je medzera)
 
Príklad behu programu (prvé dva riadky zadal užívateľ, na začiatku a konci každého je medzera)
Riadok 4 318: Riadok 4 319:
 
Chceme zistiť, či a kde sa v reťazci nachádza určité slovo alebo iná vzorka.
 
Chceme zistiť, či a kde sa v reťazci nachádza určité slovo alebo iná vzorka.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
#include <cstring>
 
#include <cstring>
Riadok 4 351: Riadok 4 352:
 
     cout << find(A,B) << endl;
 
     cout << find(A,B) << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
* Predpočítame si dĺžky a uložíme do premenných, aby sa zbytočne nerátali znova a znova
 
* Predpočítame si dĺžky a uložíme do premenných, aby sa zbytočne nerátali znova a znova
Riadok 4 366: Riadok 4 367:
 
* Preto ešte celé číslo otočíme naopak.
 
* Preto ešte celé číslo otočíme naopak.
  
<pre>
+
<syntaxhighlight lang="C++">
 
void reverse(char a[]) {
 
void reverse(char a[]) {
 
     int n = strlen(a);
 
     int n = strlen(a);
Riadok 4 395: Riadok 4 396:
 
     reverse(a);
 
     reverse(a);
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
* Ako upravíme funkciu, aby fungovala aj pre x=0, prípadne záporné x?
 
* Ako upravíme funkciu, aby fungovala aj pre x=0, prípadne záporné x?
Riadok 4 403: Riadok 4 404:
 
* Chceme číslo zapísať do reťazca a doplniť naľavo medzerami na šírku width.  
 
* Chceme číslo zapísať do reťazca a doplniť naľavo medzerami na šírku width.  
  
<pre>
+
<syntaxhighlight lang="C++">
 
const int maxN = 100;
 
const int maxN = 100;
  
Riadok 4 426: Riadok 4 427:
 
     strcat(A, B);
 
     strcat(A, B);
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
* Čo by sa stalo, ak by sme nedali do ''A'' ukončovaciu 0?
 
* Čo by sa stalo, ak by sme nedali do ''A'' ukončovaciu 0?
Riadok 4 433: Riadok 4 434:
  
 
Využijeme na vypísanie pekne zarovnanej tabuľky faktoriálov:
 
Využijeme na vypísanie pekne zarovnanej tabuľky faktoriálov:
<pre>
+
<syntaxhighlight lang="C++">
 
int factorial(int n) {
 
int factorial(int n) {
 
     int result = 1;
 
     int result = 1;
Riadok 4 453: Riadok 4 454:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
<pre>
 
<pre>
Riadok 4 471: Riadok 4 472:
  
 
Dalo by sa aj jednoduchšie pomocou nastavenia <tt>width</tt> v <tt>cin</tt>:
 
Dalo by sa aj jednoduchšie pomocou nastavenia <tt>width</tt> v <tt>cin</tt>:
<pre>
+
<syntaxhighlight lang="C++">
 
int main(void) {
 
int main(void) {
 
     int n = 12;
 
     int n = 12;
Riadok 4 482: Riadok 4 483:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
===Zalamovanie riadkov===
 
===Zalamovanie riadkov===
Riadok 4 552: Riadok 4 553:
 
* Spravíme načítanie a vypísanie.
 
* Spravíme načítanie a vypísanie.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
#include <cstring>
 
#include <cstring>
Riadok 4 662: Riadok 4 663:
 
     cout << A;
 
     cout << A;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
* Akú zložitosť má načítanie vzhľadom na celkový počet načítaných písmen? Dalo by sa zlepšiť?
 
* Akú zložitosť má načítanie vzhľadom na celkový počet načítaných písmen? Dalo by sa zlepšiť?
Riadok 4 697: Riadok 4 698:
  
 
Výpočet pomocou cyklu z prednášky 3:
 
Výpočet pomocou cyklu z prednášky 3:
<pre>
+
<syntaxhighlight lang="C++">
 
int factorial(int n) {
 
int factorial(int n) {
 
     int result = 1;
 
     int result = 1;
Riadok 4 705: Riadok 4 706:
 
     return result;
 
     return result;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Rekurzívna definícia faktoriálu:
 
Rekurzívna definícia faktoriálu:
Riadok 4 711: Riadok 4 712:
  
 
Túto matematickú definíciu môžeme priamočiaro prepísať do rekurzívnej funkcie:
 
Túto matematickú definíciu môžeme priamočiaro prepísať do rekurzívnej funkcie:
<pre>
+
<syntaxhighlight lang="C++">
 
int factorial(int n) {
 
int factorial(int n) {
 
     if (n < 2) return 1;
 
     if (n < 2) return 1;
 
     else return n * factorial(n-1);
 
     else return n * factorial(n-1);
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Aby sa nám rekurzia nezacyklila, mali by sme dodržiavať nasledujúce zásady:
 
Aby sa nám rekurzia nezacyklila, mali by sme dodržiavať nasledujúce zásady:
Riadok 4 726: Riadok 4 727:
 
* Euklidov algoritmus z prednášky 3 bol založený na rovnosti gcd(''a'',''b'') = gcd(''b'', ''a'' mod ''b'')
 
* Euklidov algoritmus z prednášky 3 bol založený na rovnosti gcd(''a'',''b'') = gcd(''b'', ''a'' mod ''b'')
 
* Tú sme použili v cykle:
 
* Tú sme použili v cykle:
<pre>
+
<syntaxhighlight lang="C++">
 
int gcd(int a, int b) {
 
int gcd(int a, int b) {
 
     while(b != 0) {
 
     while(b != 0) {
Riadok 4 735: Riadok 4 736:
 
     return a;
 
     return a;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Avšak opäť to isté môžeme ešte kratšie a elegantnejšie napísať rekurziou:
 
Avšak opäť to isté môžeme ešte kratšie a elegantnejšie napísať rekurziou:
<pre>
+
<syntaxhighlight lang="C++">
 
int gcd(int a, int b) {
 
int gcd(int a, int b) {
 
   if (b == 0) return a;
 
   if (b == 0) return a;
 
   else return gcd(b, a % b);
 
   else return gcd(b, a % b);
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
===Fibonacciho čísla===
 
===Fibonacciho čísla===
Riadok 4 753: Riadok 4 754:
 
Z tejto definície vieme opäť urobiť rekurzívnu funkciu jednoducho:
 
Z tejto definície vieme opäť urobiť rekurzívnu funkciu jednoducho:
  
<pre>
+
<syntaxhighlight lang="C++">
 
int fib(int n){
 
int fib(int n){
 
     if (n<2) return 1;
 
     if (n<2) return 1;
 
     else return fib(n-1)+fib(n-2);
 
     else return fib(n-1)+fib(n-2);
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Toto je opäť krajšie ako nerekurzívna verzia:
 
Toto je opäť krajšie ako nerekurzívna verzia:
<pre>
+
<syntaxhighlight lang="C++">
 
int fib(int n) {
 
int fib(int n) {
 
     int f = 1;    // Cislo F(i)  
 
     int f = 1;    // Cislo F(i)  
Riadok 4 773: Riadok 4 774:
 
     return f;
 
     return f;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Binárne vyhľadávanie ===
 
=== Binárne vyhľadávanie ===
Riadok 4 780: Riadok 4 781:
  
 
Pôvodná nerekurzívna funkcia vrátila polohu prvku ''x'' v poli ''a'' alebo hodnotou -1, ak sa tam nenachádzal:
 
Pôvodná nerekurzívna funkcia vrátila polohu prvku ''x'' v poli ''a'' alebo hodnotou -1, ak sa tam nenachádzal:
<pre>
+
<syntaxhighlight lang="C++">
 
int find(int a[], int n, int x) {
 
int find(int a[], int n, int x) {
 
     int left = 0, right = n - 1;
 
     int left = 0, right = n - 1;
Riadok 4 791: Riadok 4 792:
 
     return -1;
 
     return -1;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
V rekurzívnej verzii si okraje aktuálneho úseku poľa si v rekurzii posielame ako parametre:
 
V rekurzívnej verzii si okraje aktuálneho úseku poľa si v rekurzii posielame ako parametre:
<pre>
+
<syntaxhighlight lang="C++">
 
int find(int a[], int left, int right, int x) {
 
int find(int a[], int left, int right, int x) {
 
     if (left > right) return -1;
 
     if (left > right) return -1;
Riadok 4 802: Riadok 4 803:
 
     else return find(a, left, index - 1, x);
 
     else return find(a, left, index - 1, x);
 
}
 
}
</pre>
+
</syntaxhighlight>
 
Ak chceme vyhľadať ''x'' v poli ''a'' s ''n'' prvkami, voláme <tt>find(a, 0, n-1, x)</tt>.
 
Ak chceme vyhľadať ''x'' v poli ''a'' s ''n'' prvkami, voláme <tt>find(a, 0, n-1, x)</tt>.
  
Riadok 4 813: Riadok 4 814:
 
* pri porovnávaní ''x'' a <tt>a[index]</tt> rozlišujeme iba dva prípady, nie tri
 
* pri porovnávaní ''x'' a <tt>a[index]</tt> rozlišujeme iba dva prípady, nie tri
 
* končíme pri intervale dĺžky 1, nie 0
 
* končíme pri intervale dĺžky 1, nie 0
<pre>
+
<syntaxhighlight lang="C++">
 
bool contains (int a[], int left, int right, int x){
 
bool contains (int a[], int left, int right, int x){
 
     if (left == right) return (a[left] == x);
 
     if (left == right) return (a[left] == x);
Riadok 4 827: Riadok 4 828:
 
   cout << find(a, 0, n-1, 467) << endl;
 
   cout << find(a, 0, n-1, 467) << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
===Zhrnutie===
 
===Zhrnutie===
Riadok 4 839: Riadok 4 840:
 
* napr. funkcia find by prirodzene dostávala pole ''a'', dĺžku ''n'' a hľadaný prvok, ale kvôli rekurzii potrebuje ľavý a pravý okraj
 
* napr. funkcia find by prirodzene dostávala pole ''a'', dĺžku ''n'' a hľadaný prvok, ale kvôli rekurzii potrebuje ľavý a pravý okraj
 
* pre pohodlie užívateľa môžeme pridať pomocnú funkciu (wrapper):
 
* pre pohodlie užívateľa môžeme pridať pomocnú funkciu (wrapper):
<pre>
+
<syntaxhighlight lang="C++">
 
int find(int a[], int n, int x) {
 
int find(int a[], int n, int x) {
 
   return find(a, 0, n-1, x);
 
   return find(a, 0, n-1, x);
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Viac o rekurzii ==
 
== Viac o rekurzii ==
Riadok 4 855: Riadok 4 856:
 
Ako príklad uveďme rekurzívne definície predikátov párnosti a nepárnosti:
 
Ako príklad uveďme rekurzívne definície predikátov párnosti a nepárnosti:
  
<pre>
+
<syntaxhighlight lang="C++">
 
bool even(int n) {
 
bool even(int n) {
 
     if (n == 0) return true;
 
     if (n == 0) return true;
Riadok 4 865: Riadok 4 866:
 
     else return even(n - 1);
 
     else return even(n - 1);
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Rekurzia pomocou zásobníka - ako je rekurzia implementovaná ===
 
=== Rekurzia pomocou zásobníka - ako je rekurzia implementovaná ===
Riadok 4 882: Riadok 4 883:
 
Teraz si môžeme jednoduchý zásobník odsimulovať napríklad na výpočte faktoriálu.  
 
Teraz si môžeme jednoduchý zásobník odsimulovať napríklad na výpočte faktoriálu.  
  
<pre>
+
<syntaxhighlight lang="C++">
 
int factorial(int n) {
 
int factorial(int n) {
 
     if (n < 2) return 1;
 
     if (n < 2) return 1;
 
     else return n * factorial(n-1);
 
     else return n * factorial(n-1);
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
===Zložitejšie príklady rekurzie===
 
===Zložitejšie príklady rekurzie===
Riadok 4 907: Riadok 4 908:
 
* Program by sa dal ľahko rozšíriť na vykresľovanie pravidelného n-uholníka (stačí zmeniť uhol otočenia a počet opakovaní cyklu)
 
* Program by sa dal ľahko rozšíriť na vykresľovanie pravidelného n-uholníka (stačí zmeniť uhol otočenia a počet opakovaní cyklu)
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include "SVGdraw.h"
 
#include "SVGdraw.h"
  
Riadok 4 925: Riadok 4 926:
 
     turtle.finish();
 
     turtle.finish();
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Fraktály ==
 
== Fraktály ==
Riadok 4 947: Riadok 4 948:
 
** na presný popis umiestnenia a natočenia týchto kriviek nižšieho stupňa použijeme korytnačiu grafiku, čo máme spravené vo funkcii nižšie.
 
** na presný popis umiestnenia a natočenia týchto kriviek nižšieho stupňa použijeme korytnačiu grafiku, čo máme spravené vo funkcii nižšie.
  
<pre>  
+
<syntaxhighlight lang="C++">  
 
#include "SVGdraw.h"
 
#include "SVGdraw.h"
  
Riadok 4 979: Riadok 4 980:
 
     turtle.finish();
 
     turtle.finish();
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
===Rekurzívny strom===
 
===Rekurzívny strom===
Riadok 4 999: Riadok 5 000:
 
* Korytnačka teda prejde po každej vetve dvakrát, raz smerom dopredu a raz naspäť.
 
* Korytnačka teda prejde po každej vetve dvakrát, raz smerom dopredu a raz naspäť.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include "SVGdraw.h"
 
#include "SVGdraw.h"
  
Riadok 5 032: Riadok 5 033:
 
     turtle.finish();
 
     turtle.finish();
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Hanojské veže ==
 
== Hanojské veže ==
Riadok 5 053: Riadok 5 054:
 
* Preložíme N-1 kameňov z B na C (s použitím A)
 
* Preložíme N-1 kameňov z B na C (s použitím A)
 
   
 
   
<pre>
+
<syntaxhighlight lang="C++">
 
void presunHanoi(int odkial, int cez, int kam, int n){
 
void presunHanoi(int odkial, int cez, int kam, int n){
 
     if (n == 1) {
 
     if (n == 1) {
Riadok 5 067: Riadok 5 068:
 
     presunHanoi(1, 2, 3, 3); // z veze 1 na vezu 3 (pomocou veze 2)
 
     presunHanoi(1, 2, 3, 3); // z veze 1 na vezu 3 (pomocou veze 2)
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Dôležité je si uvedomiť, že nasledovný postup dodržuje pravidlá. Po zavolaní funkcie presunHanoi vždy platí:
 
Dôležité je si uvedomiť, že nasledovný postup dodržuje pravidlá. Po zavolaní funkcie presunHanoi vždy platí:
Riadok 5 088: Riadok 5 089:
  
 
* Rekurentné definície vieme často priamočiaro zapísať do '''rekurzívnych funkcií''' (aj keď môžu byť pomalé)
 
* Rekurentné definície vieme často priamočiaro zapísať do '''rekurzívnych funkcií''' (aj keď môžu byť pomalé)
<pre>
+
<syntaxhighlight lang="C++">
 
int fib(int n) {
 
int fib(int n) {
 
     if (n<2) return 1;
 
     if (n<2) return 1;
 
     else return fib(n-1) + fib(n-2);
 
     else return fib(n-1) + fib(n-2);
 
}
 
}
</pre>
+
</syntaxhighlight>
 
* V '''rekurzívnej funkcii''' riešime problém pomocou menších podproblémov toho istého typu
 
* V '''rekurzívnej funkcii''' riešime problém pomocou menších podproblémov toho istého typu
 
** Napríklad aby sme našli číslo ''x'' v utriedenom poli medzi indexami <tt>left</tt> a <tt>right</tt>, potrebujeme ho porovnať so stredným prvkom tohoto intervalu a potom riešiť tú istú úlohu pre menší interval
 
** Napríklad aby sme našli číslo ''x'' v utriedenom poli medzi indexami <tt>left</tt> a <tt>right</tt>, potrebujeme ho porovnať so stredným prvkom tohoto intervalu a potom riešiť tú istú úlohu pre menší interval
 
** Aj keď sme pôvodne chceli hľadať prvok v celom poli, úlohu rozšírime o parametre <tt>left</tt> a <tt>right</tt>, aby sa dala spraviť rekurzia
 
** Aj keď sme pôvodne chceli hľadať prvok v celom poli, úlohu rozšírime o parametre <tt>left</tt> a <tt>right</tt>, aby sa dala spraviť rekurzia
<pre>
+
<syntaxhighlight lang="C++">
 
bool contains (int a[], int left, int right, int x){
 
bool contains (int a[], int left, int right, int x){
 
     if (left == right) return (a[left] == x);
 
     if (left == right) return (a[left] == x);
Riadok 5 103: Riadok 5 104:
 
     if (x <= a[index]) return contains(a, left, index, x);
 
     if (x <= a[index]) return contains(a, left, index, x);
 
     else return contains(a, index+1, right, x);
 
     else return contains(a, index+1, right, x);
}</pre>
+
}</syntaxhighlight>
  
 
===Zásobník volaní===
 
===Zásobník volaní===
Riadok 5 119: Riadok 5 120:
 
Skúsme napríklad odsimulovať, čo sa deje ak vo funkcii <tt>main</tt> zavoláme <tt>fib(3)</tt>
 
Skúsme napríklad odsimulovať, čo sa deje ak vo funkcii <tt>main</tt> zavoláme <tt>fib(3)</tt>
 
* Kvôli prehľadnosti si <tt>fib</tt> rozpíšeme na viac riadkov:
 
* Kvôli prehľadnosti si <tt>fib</tt> rozpíšeme na viac riadkov:
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 5 136: Riadok 5 137:
 
     cout << x << endl;  
 
     cout << x << endl;  
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Tu je priebeh programu (obsah zásobníka)
 
Tu je priebeh programu (obsah zásobníka)
Riadok 5 181: Riadok 5 182:
 
Spomeňme si na zápis výpočtu bez rekurzie, s dvomi premennými:
 
Spomeňme si na zápis výpočtu bez rekurzie, s dvomi premennými:
 
* F(n) spočíta v čase O(n)
 
* F(n) spočíta v čase O(n)
<pre>
+
<syntaxhighlight lang="C++">
 
int fibonacci(int n) {
 
int fibonacci(int n) {
  
Riadok 5 195: Riadok 5 196:
 
     return f;
 
     return f;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Prepíšeme ho na rekurziu:
 
Prepíšeme ho na rekurziu:
<pre>
+
<syntaxhighlight lang="C++">
 
int fib1(int a, int b, int i, int n) {
 
int fib1(int a, int b, int i, int n) {
 
     /* a je F(i-1), b je F(i), spočítaj F(n), kde n>=i */
 
     /* a je F(i-1), b je F(i), spočítaj F(n), kde n>=i */
Riadok 5 208: Riadok 5 209:
 
     return fib1(1, 1, 1, n);
 
     return fib1(1, 1, 1, n);
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Namiesto dvoch premenných ''i'' a ''n'' si môžeme pamätať len ich rozdiel ''k=n-i''
 
Namiesto dvoch premenných ''i'' a ''n'' si môžeme pamätať len ich rozdiel ''k=n-i''
 
* hodnota ''k'' nám vraví, koľkokrát máme ešte sčitovať dve posledné čísla
 
* hodnota ''k'' nám vraví, koľkokrát máme ešte sčitovať dve posledné čísla
<pre>
+
<syntaxhighlight lang="C++">
 
int fib1(int a, int b, int k) {
 
int fib1(int a, int b, int k) {
 
     /* a je F(i-1), b je F(i), spočítaj F(i+k) */
 
     /* a je F(i-1), b je F(i), spočítaj F(i+k) */
Riadok 5 222: Riadok 5 223:
 
     fib1(1, 1, n-1);
 
     fib1(1, 1, n-1);
 
}
 
}
</pre>
+
</syntaxhighlight>
 
Táto rekurzívna funkcia pracuje v čase ''O(n)'', rovnako ako verzia s cyklom.
 
Táto rekurzívna funkcia pracuje v čase ''O(n)'', rovnako ako verzia s cyklom.
  
Riadok 5 239: Riadok 5 240:
  
 
Veľmi jednoduchý program s troma cyklami:
 
Veľmi jednoduchý program s troma cyklami:
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 5 254: Riadok 5 255:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
===Rekurzívne riešenie pre všeobecné k===
 
===Rekurzívne riešenie pre všeobecné k===
Riadok 5 262: Riadok 5 263:
 
* V každej skupine ak odoberieme prvý prvok, dostaneme variácie triedy ''k-1''
 
* V každej skupine ak odoberieme prvý prvok, dostaneme variácie triedy ''k-1''
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 5 295: Riadok 5 296:
 
     generuj(a, 0, k, n);
 
     generuj(a, 0, k, n);
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
===Ďalšie rozšírenia===
 
===Ďalšie rozšírenia===
Riadok 5 301: Riadok 5 302:
 
* Čo ak chceme všetky DNA reťazce dĺžky ''k'' (DNA pozostáva z "písmen" A,C,G,T)?
 
* Čo ak chceme všetky DNA reťazce dĺžky ''k'' (DNA pozostáva z "písmen" A,C,G,T)?
  
<pre>
+
<syntaxhighlight lang="C++">
 
// pouzi n=26
 
// pouzi n=26
 
void vypis(int a[], int k) {
 
void vypis(int a[], int k) {
Riadok 5 319: Riadok 5 320:
 
     cout << endl;
 
     cout << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
'''Cvičenia'''
 
'''Cvičenia'''
Riadok 5 342: Riadok 5 343:
  
 
Prvý pokus:
 
Prvý pokus:
<pre>
+
<syntaxhighlight lang="C++">
 
bool spravne(int a[], int k, int n) {
 
bool spravne(int a[], int k, int n) {
 
     /* je v poli a dlzky k kazde cislo od 0 po n-1 najviac raz? */
 
     /* je v poli a dlzky k kazde cislo od 0 po n-1 najviac raz? */
Riadok 5 371: Riadok 5 372:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
'''Cvičenie:''' ako by sme napísali funkciu <tt>spravne</tt>, ak by nedostala ako parameter hodnotu ''n''?
 
'''Cvičenie:''' ako by sme napísali funkciu <tt>spravne</tt>, ak by nedostala ako parameter hodnotu ''n''?
Riadok 5 382: Riadok 5 383:
 
* Testovanie správnosti vo funkcii <tt>generuj</tt> sa dá vynechať
 
* Testovanie správnosti vo funkcii <tt>generuj</tt> sa dá vynechať
  
<pre>
+
<syntaxhighlight lang="C++">
 
bool moze(int a[], int i, int x) {
 
bool moze(int a[], int i, int x) {
 
     /* Mozeme dat hodnotu x na poziciu i v poli a?
 
     /* Mozeme dat hodnotu x na poziciu i v poli a?
Riadok 5 407: Riadok 5 408:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Možné zrýchlenie: vytvoríme trvalé pole <tt>bolo</tt>, v ktorom bude zaznamené, ktoré cifry sa už vyskytli a to použijeme vo funkcii <tt>moze</tt>.  
 
Možné zrýchlenie: vytvoríme trvalé pole <tt>bolo</tt>, v ktorom bude zaznamené, ktoré cifry sa už vyskytli a to použijeme vo funkcii <tt>moze</tt>.  
 
* Po návrate z rekurzie nesmieme zabudúť príslušnú hodnotu odznačiť!
 
* Po návrate z rekurzie nesmieme zabudúť príslušnú hodnotu odznačiť!
<pre>
+
<syntaxhighlight lang="C++">
 
void generuj(int a[], bool bolo[], int i, int k, int n) {
 
void generuj(int a[], bool bolo[], int i, int k, int n) {
 
     /* v poli a dlzky k mame prvych i cifier,
 
     /* v poli a dlzky k mame prvych i cifier,
Riadok 5 444: Riadok 5 445:
 
     generuj(a, bolo, 0, k, n);
 
     generuj(a, bolo, 0, k, n);
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Cvičenia: ako potrebujeme zmeniť program, aby sme generovali všetky postupnosti ''k'' cifier z množiny ''{0,..,n-1}'', také, že:
 
Cvičenia: ako potrebujeme zmeniť program, aby sme generovali všetky postupnosti ''k'' cifier z množiny ''{0,..,n-1}'', také, že:
Riadok 5 461: Riadok 5 462:
  
 
Všeobecná schéma  
 
Všeobecná schéma  
<pre>
+
<syntaxhighlight lang="C++">
 
void generuj(int a[], int i) {
 
void generuj(int a[], int i) {
 
     /* v poli a dlzky k mame prvych i cisel z riesenia */
 
     /* v poli a dlzky k mame prvych i cisel z riesenia */
Riadok 5 475: Riadok 5 476:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Prehľadávanie s návratom môže byť vo všeobecnosti veľmi pomalé, čas výpočtu exponenciálne rastie.
 
Prehľadávanie s návratom môže byť vo všeobecnosti veľmi pomalé, čas výpočtu exponenciálne rastie.
Riadok 5 501: Riadok 5 502:
 
** Mohli by sme si tiež spraviť <tt>struct</tt> obsahujúci všetky premenné potrebné v rekurzii a odovzdávať si ten  
 
** Mohli by sme si tiež spraviť <tt>struct</tt> obsahujúci všetky premenné potrebné v rekurzii a odovzdávať si ten  
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 5 575: Riadok 5 576:
 
     cout << "Pocet rieseni: " << pocet << endl;
 
     cout << "Pocet rieseni: " << pocet << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
==Generovanie všetkých podmnožín==
 
==Generovanie všetkých podmnožín==
Riadok 5 587: Riadok 5 588:
 
Podmnožinu vieme vyjadriť ako binárne pole dĺžky ''m'', kde <tt>a[i]=0</tt> znamená, že ''i'' nepatrí do množiny a <tt>a[i]=1</tt> znamená, že patrí. Teda môžeme použiť predchádzajúci program pre ''n=2'', ''k=m'' a zmeniť iba výpis:
 
Podmnožinu vieme vyjadriť ako binárne pole dĺžky ''m'', kde <tt>a[i]=0</tt> znamená, že ''i'' nepatrí do množiny a <tt>a[i]=1</tt> znamená, že patrí. Teda môžeme použiť predchádzajúci program pre ''n=2'', ''k=m'' a zmeniť iba výpis:
  
<pre>
+
<syntaxhighlight lang="C++">
 
void vypis(int a[], int m) {
 
void vypis(int a[], int m) {
 
     cout << "{";
 
     cout << "{";
Riadok 5 603: Riadok 5 604:
 
     cout << "}" << endl;
 
     cout << "}" << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
 
* V premennej <tt>prve</tt> si pamätáme, či máme oddeliť ďalšie vypisované číslo od predchádzajúceho.  
 
* V premennej <tt>prve</tt> si pamätáme, či máme oddeliť ďalšie vypisované číslo od predchádzajúceho.  
 
** Ak ešte žiadne nebolo, oddeľovač je prázdny reťazec.
 
** Ak ešte žiadne nebolo, oddeľovač je prázdny reťazec.
Riadok 5 609: Riadok 5 610:
  
 
Namiesto poľa intov môžeme použiť pole boolovských hodnôt a celý program trochu prispôsobiť tomu, že generujeme podmnožiny:
 
Namiesto poľa intov môžeme použiť pole boolovských hodnôt a celý program trochu prispôsobiť tomu, že generujeme podmnožiny:
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
#include <cstring>
 
#include <cstring>
Riadok 5 651: Riadok 5 652:
 
     generuj(a, 0, m);
 
     generuj(a, 0, m);
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Pre n=3 program vypíše:
 
Pre n=3 program vypíše:
Riadok 5 703: Riadok 5 704:
  
 
Podmnožiny budeme, ako obvykle, reprezentovať poľom typu <tt>bool</tt>, v ktorom si pre každý prvok univerza pamätáme, či do danej podmnožiny patrí.
 
Podmnožiny budeme, ako obvykle, reprezentovať poľom typu <tt>bool</tt>, v ktorom si pre každý prvok univerza pamätáme, či do danej podmnožiny patrí.
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 5 811: Riadok 5 812:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Optimalizácia č. 1: ukončenie prehľadávania vždy, keď je prekročená nosnosť ===
 
=== Optimalizácia č. 1: ukončenie prehľadávania vždy, keď je prekročená nosnosť ===
Riadok 5 819: Riadok 5 820:
 
Časti horeuvedeného programu zmenené touto optimalizáciou sú uvedené nižšie. Okrem samotnej funkcie <tt>generujLupy</tt> je potrebné prispôsobiť aj funkciu <tt>spocitajHmotnostLupu</tt> tak, aby ju bolo možné aplikovať aj na &bdquo;neúplne vygenerované lupy&rdquo;.
 
Časti horeuvedeného programu zmenené touto optimalizáciou sú uvedené nižšie. Okrem samotnej funkcie <tt>generujLupy</tt> je potrebné prispôsobiť aj funkciu <tt>spocitajHmotnostLupu</tt> tak, aby ju bolo možné aplikovať aj na &bdquo;neúplne vygenerované lupy&rdquo;.
  
<pre>
+
<syntaxhighlight lang="C++">
 
/* Potrebujeme vediet spocitat hmotnost len pre cast predmetov: */
 
/* Potrebujeme vediet spocitat hmotnost len pre cast predmetov: */
  
Riadok 5 851: Riadok 5 852:
 
     }
 
     }
 
}  
 
}  
</pre>
+
</syntaxhighlight>
  
 
=== Optimalizácia č. 2: hmotnosť a cenu lupu netreba zakaždým počítať odznova ===
 
=== Optimalizácia č. 2: hmotnosť a cenu lupu netreba zakaždým počítať odznova ===
Riadok 5 857: Riadok 5 858:
 
Predchádzajúci program vždy znovu a znovu prepočítava hmotnosť a cenu lupu, aj keď sa zoznam vybraných predmetov zmení iba trochu. Namiesto toho môžeme cenu a hmotnosť doposiaľ vygenerovanej časti lupu predávať funkcii <tt>generuj</tt> ako parameter.  
 
Predchádzajúci program vždy znovu a znovu prepočítava hmotnosť a cenu lupu, aj keď sa zoznam vybraných predmetov zmení iba trochu. Namiesto toho môžeme cenu a hmotnosť doposiaľ vygenerovanej časti lupu predávať funkcii <tt>generuj</tt> ako parameter.  
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 5 929: Riadok 5 930:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Rýchle triedenia prostredníctvom paradigmy &bdquo;rozdeľuj a panuj&rdquo; ==
 
== Rýchle triedenia prostredníctvom paradigmy &bdquo;rozdeľuj a panuj&rdquo; ==
Riadok 5 950: Riadok 5 951:
 
Základ triedenia zlučovaním tak bude vyzerať nasledovne (pričom zostáva doimplementovať funkciu ''merge''):   
 
Základ triedenia zlučovaním tak bude vyzerať nasledovne (pričom zostáva doimplementovať funkciu ''merge''):   
  
<pre>
+
<syntaxhighlight lang="C++">
 
void mergesort(int a[], int low, int high) {
 
void mergesort(int a[], int low, int high) {
 
/* Funkcia utriedi prvky pola a[] od indexu low po index high (vratane). */
 
/* Funkcia utriedi prvky pola a[] od indexu low po index high (vratane). */
Riadok 5 969: Riadok 5 970:
 
     merge(a,low,mid,high);
 
     merge(a,low,mid,high);
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Zlúčenie dvoch utriedených podpostupností ===
 
=== Zlúčenie dvoch utriedených podpostupností ===
Riadok 5 980: Riadok 5 981:
  
 
Po spojení oboch podpostupností do utriedeného poľa <tt>aux</tt> toto pole prekopírujeme naspäť do poľa <tt>a</tt>.
 
Po spojení oboch podpostupností do utriedeného poľa <tt>aux</tt> toto pole prekopírujeme naspäť do poľa <tt>a</tt>.
<pre>
+
<syntaxhighlight lang="C++">
 
const int maxN = 1000;
 
const int maxN = 1000;
  
Riadok 6 019: Riadok 6 020:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Výsledný program ===
 
=== Výsledný program ===
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 6 097: Riadok 6 098:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Ukážka na príklade ===
 
=== Ukážka na príklade ===
Riadok 6 151: Riadok 6 152:
  
 
Základ triedenia tak bude vyzerať nasledovne (pričom zostáva doimplementovať funkciu <tt>partition</tt>):
 
Základ triedenia tak bude vyzerať nasledovne (pričom zostáva doimplementovať funkciu <tt>partition</tt>):
<pre>
+
<syntaxhighlight lang="C++">
 
void quicksort(int a[], int low, int high) {
 
void quicksort(int a[], int low, int high) {
 
/* Utriedi cast pola a[] od indexu low po index high (vratane) */
 
/* Utriedi cast pola a[] od indexu low po index high (vratane) */
Riadok 6 168: Riadok 6 169:
 
     quicksort(a, mid+1, high);   
 
     quicksort(a, mid+1, high);   
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Funkcia <tt>partition</tt> ===
 
=== Funkcia <tt>partition</tt> ===
Riadok 6 191: Riadok 6 192:
  
 
Nakoniec je ešte nutné vymeniť <tt>a[low]</tt> s <tt>a[lastSmaller]</tt>, čím sa pivot dostane na svoje miesto.
 
Nakoniec je ešte nutné vymeniť <tt>a[low]</tt> s <tt>a[lastSmaller]</tt>, čím sa pivot dostane na svoje miesto.
<pre>
+
<syntaxhighlight lang="C++">
 
void swap (int &x, int &y) {
 
void swap (int &x, int &y) {
 
     int tmp = x;
 
     int tmp = x;
Riadok 6 211: Riadok 6 212:
 
     return lastSmaller;
 
     return lastSmaller;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Výsledný program ===
 
=== Výsledný program ===
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 6 272: Riadok 6 273:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Ukážka na príklade ===
 
=== Ukážka na príklade ===
Riadok 6 313: Riadok 6 314:
 
Občas sa možno stretnúť aj s nasledujúcou implementáciou triedenia Quick Sort. Skúste samostatne odôvodniť jej správnosť.
 
Občas sa možno stretnúť aj s nasledujúcou implementáciou triedenia Quick Sort. Skúste samostatne odôvodniť jej správnosť.
  
<pre>
+
<syntaxhighlight lang="C++">
 
void quicksort(int a[], int low, int high) {
 
void quicksort(int a[], int low, int high) {
 
     if (low >= high) {
 
     if (low >= high) {
Riadok 6 337: Riadok 6 338:
 
     quicksort(a, i, high);   
 
     quicksort(a, i, high);   
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Triediace algoritmy: zhrnutie ==
 
== Triediace algoritmy: zhrnutie ==
Riadok 6 350: Riadok 6 351:
  
 
Reálnu rýchlosť triedení na náhodne zvolenom veľkom vstupe možno porovnať napríklad nasledujúcim programom:
 
Reálnu rýchlosť triedení na náhodne zvolenom veľkom vstupe možno porovnať napríklad nasledujúcim programom:
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
#include <cstdlib>
 
#include <cstdlib>
Riadok 6 484: Riadok 6 485:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Pre <tt>N = 100000</tt> môžeme dostať napríklad nasledujúci výstup (líši sa od počítača k počítaču a od volania k volaniu):
 
Pre <tt>N = 100000</tt> môžeme dostať napríklad nasledujúci výstup (líši sa od počítača k počítaču a od volania k volaniu):
Riadok 6 503: Riadok 6 504:
 
Pamäť počítača si možno ''veľmi zjednodušene'' predstaviť ako konečnú postupnosť pamäťových miest, kde každé pamäťové miesto má kapacitu práve jeden byte. To možno znázorniť nasledujúcim obrázkom.
 
Pamäť počítača si možno ''veľmi zjednodušene'' predstaviť ako konečnú postupnosť pamäťových miest, kde každé pamäťové miesto má kapacitu práve jeden byte. To možno znázorniť nasledujúcim obrázkom.
  
[[#Súbor:Pamat1.png]]
+
[[Súbor:Pamat1.png]]
  
 
Každé pamäťové miesto má pridelené svoju ''adresu''. Presný formát adries závisí od konkrétnej architektúry (môže napríklad ísť o 32-bitové alebo 64-bitové čísla), z pohľadu programátora však zvyčajne nie je dôležitý.   
 
Každé pamäťové miesto má pridelené svoju ''adresu''. Presný formát adries závisí od konkrétnej architektúry (môže napríklad ísť o 32-bitové alebo 64-bitové čísla), z pohľadu programátora však zvyčajne nie je dôležitý.   
Riadok 6 509: Riadok 6 510:
 
Premenným základných typov (ako napríklad <tt>int</tt>, <tt>char</tt>, <tt>double</tt>,...) sú počas vykonávania programu pridelené ''súvislé úseky pamäťových miest''. Napríklad na architektúre s 32-bitovým &ndash; čiže 4-bytovým &ndash; typom <tt>int</tt> sa premenným tohto typu priraďujú úseky štyroch po sebe idúcich pamäťových miest. ''Adresou premennej'' potom rozumieme adresu ''prvého'' z týchto pamäťových miest. Táto situácia je znázornená na obrázku nižšie.
 
Premenným základných typov (ako napríklad <tt>int</tt>, <tt>char</tt>, <tt>double</tt>,...) sú počas vykonávania programu pridelené ''súvislé úseky pamäťových miest''. Napríklad na architektúre s 32-bitovým &ndash; čiže 4-bytovým &ndash; typom <tt>int</tt> sa premenným tohto typu priraďujú úseky štyroch po sebe idúcich pamäťových miest. ''Adresou premennej'' potom rozumieme adresu ''prvého'' z týchto pamäťových miest. Táto situácia je znázornená na obrázku nižšie.
  
[[#Súbor:Pamat2.png]]
+
[[Súbor:Pamat2.png]]
  
 
Poliam sa taktiež prideľujú ''súvislé'' úseky pamäte. Pre pole dĺžky <tt>N</tt> pozostávajúce z prvkov typu <tt>T</tt> sa pritom vyhradí <tt>N</tt> po sebe idúcich pamäťových úsekov, kde každý z nich postačuje práve na uchovanie jednej hodnoty typu <tt>T</tt>. Napríklad poľu <tt>a</tt> dĺžky <tt>N</tt> prvkov typu <tt>int</tt> sa na architektúre s 32-bitovým (4-bytovým) typom <tt>int</tt> pridelí súvislý úsek <tt>4*N</tt> pamäťových miest tak, ako na nasledujúcom obrázku.  
 
Poliam sa taktiež prideľujú ''súvislé'' úseky pamäte. Pre pole dĺžky <tt>N</tt> pozostávajúce z prvkov typu <tt>T</tt> sa pritom vyhradí <tt>N</tt> po sebe idúcich pamäťových úsekov, kde každý z nich postačuje práve na uchovanie jednej hodnoty typu <tt>T</tt>. Napríklad poľu <tt>a</tt> dĺžky <tt>N</tt> prvkov typu <tt>int</tt> sa na architektúre s 32-bitovým (4-bytovým) typom <tt>int</tt> pridelí súvislý úsek <tt>4*N</tt> pamäťových miest tak, ako na nasledujúcom obrázku.  
  
[[#Súbor:Pamat3.png]]
+
[[Súbor:Pamat3.png]]
  
 
== Smerníky ==
 
== Smerníky ==
Riadok 6 519: Riadok 6 520:
 
''Smerník'' (niekde tiež ''ukazovateľ'', angl. ''pointer'') je premenná, ktorej hodnotou je pamäťová adresa. Napríklad na nasledujúcom obrázku je na adrese <tt>[adresa 2]</tt> uložená premenná typu <tt>int</tt>, ktorej hodnota je 12345. Na adrese <tt>[adresa 1]</tt> je uložený smerník, ktorého hodnota je <tt>[adresa 2]</tt> &ndash; hovoríme, že smerník ukazuje na pamäťovú adresu <tt>[adresa 2]</tt>.
 
''Smerník'' (niekde tiež ''ukazovateľ'', angl. ''pointer'') je premenná, ktorej hodnotou je pamäťová adresa. Napríklad na nasledujúcom obrázku je na adrese <tt>[adresa 2]</tt> uložená premenná typu <tt>int</tt>, ktorej hodnota je 12345. Na adrese <tt>[adresa 1]</tt> je uložený smerník, ktorého hodnota je <tt>[adresa 2]</tt> &ndash; hovoríme, že smerník ukazuje na pamäťovú adresu <tt>[adresa 2]</tt>.
  
[[#Súbor:Pamat5.png]]
+
[[Súbor:Pamat5.png]]
  
 
=== Definovanie smerníka ===
 
=== Definovanie smerníka ===
  
 
V C++ sa smerník ukazujúci na &bdquo;pamäťový objekt&rdquo; typu <tt>T</tt> definuje takto:
 
V C++ sa smerník ukazujúci na &bdquo;pamäťový objekt&rdquo; typu <tt>T</tt> definuje takto:
<pre>
+
<syntaxhighlight lang="C++">
 
T *p;
 
T *p;
</pre>
+
</syntaxhighlight>
 
Napríklad teda môžeme písať:
 
Napríklad teda môžeme písať:
<pre>
+
<syntaxhighlight lang="C++">
 
int *p1;    // smernik p1 na int
 
int *p1;    // smernik p1 na int
 
char *p2;  // smernik p2 na char
 
char *p2;  // smernik p2 na char
Riadok 6 534: Riadok 6 535:
  
 
// ...
 
// ...
</pre>
+
</syntaxhighlight>
  
 
Smerníky ukazujúce na &bdquo;pamäťové objekty&rdquo; rôznych typov sú teda takisto rôznych typov. Vo všeobecnosti nemožno realizovať priradenia medzi smerníkmi rôznych typov.
 
Smerníky ukazujúce na &bdquo;pamäťové objekty&rdquo; rôznych typov sú teda takisto rôznych typov. Vo všeobecnosti nemožno realizovať priradenia medzi smerníkmi rôznych typov.
<pre>
+
<syntaxhighlight lang="C++">
 
int *p1;
 
int *p1;
 
int *p2;
 
int *p2;
Riadok 6 544: Riadok 6 545:
 
p1 = p2;  // korektne priradenie
 
p1 = p2;  // korektne priradenie
 
p3 = p1;  // chyba, lebo p3 a p1 su smerniky roznych typov
 
p3 = p1;  // chyba, lebo p3 a p1 su smerniky roznych typov
</pre>
+
</syntaxhighlight>
  
 
Bez ohľadu na typ smerníka je jeho hodnotou vždy len ''pamäťová adresa''. Smerníky rôznych typov sa však, ako onedlho uvidíme, v určitých situáciách môžu &bdquo;správať&rdquo; rozdielne.
 
Bez ohľadu na typ smerníka je jeho hodnotou vždy len ''pamäťová adresa''. Smerníky rôznych typov sa však, ako onedlho uvidíme, v určitých situáciách môžu &bdquo;správať&rdquo; rozdielne.
Riadok 6 554: Riadok 6 555:
 
Nasledujúci program vytvorí celočíselnú premennú <tt>n</tt>, ktorej adresu priradí do smerníka <tt>p</tt>.
 
Nasledujúci program vytvorí celočíselnú premennú <tt>n</tt>, ktorej adresu priradí do smerníka <tt>p</tt>.
  
<pre>
+
<syntaxhighlight lang="C++">
 
int main() {
 
int main() {
 
     int n = 12345;
 
     int n = 12345;
Riadok 6 561: Riadok 6 562:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Stav pamäte tesne pred skončením vykonávania tohto programu je znázornený na nasledujúcom obrázku.
 
Stav pamäte tesne pred skončením vykonávania tohto programu je znázornený na nasledujúcom obrázku.
  
[[#Súbor:Pamat4.png]]
+
[[Súbor:Pamat4.png]]
  
 
Rovnakým spôsobom možno operátor <tt>&</tt> aplikovať aj na prvky poľa (t.j. napríklad <tt>&a[2]</tt> je adresa druhého &ndash; resp. tretieho &ndash; prvku poľa <tt>a</tt>).
 
Rovnakým spôsobom možno operátor <tt>&</tt> aplikovať aj na prvky poľa (t.j. napríklad <tt>&a[2]</tt> je adresa druhého &ndash; resp. tretieho &ndash; prvku poľa <tt>a</tt>).
Riadok 6 571: Riadok 6 572:
 
Operátor <tt>&</tt> ''nemožno'' aplikovať na konštanty (ako napríklad <tt>3.14</tt> alebo <tt>'c'</tt>), ani na výrazy (ako napríklad <tt>n+2</tt>).
 
Operátor <tt>&</tt> ''nemožno'' aplikovať na konštanty (ako napríklad <tt>3.14</tt> alebo <tt>'c'</tt>), ani na výrazy (ako napríklad <tt>n+2</tt>).
  
<pre>
+
<syntaxhighlight lang="C++">
 
     int n = 0;
 
     int n = 0;
 
     int a[5] = {1, 2, 3, 4, 5};
 
     int a[5] = {1, 2, 3, 4, 5};
Riadok 6 581: Riadok 6 582:
 
     p = &42;    // chyba (konstanta 42 nema adresu)
 
     p = &42;    // chyba (konstanta 42 nema adresu)
 
     p = &a;    // chyba (vysvetlime neskor)  
 
     p = &a;    // chyba (vysvetlime neskor)  
</pre>
+
</syntaxhighlight>
  
 
=== Operátor <tt>*</tt> (dereferencia) ===
 
=== Operátor <tt>*</tt> (dereferencia) ===
Riadok 6 587: Riadok 6 588:
 
Kľúčovým operátorom na smerníkoch je tzv. ''dereferencia'', ktorá sa realizuje operátorom <tt>*</tt>. Ak <tt>p</tt> je smerník, možno pomocou zápisu <tt>*p</tt> pristúpiť k údajom na adrese reprezentovanej smerníkom <tt>p</tt>. Tieto údaje potom možno aj meniť, t. j. <tt>*p</tt> môže vystupovať napríklad aj na ľavej strane priradenia. To demonštrujeme na drobnom rozšírení predchádzajúceho príkladu.
 
Kľúčovým operátorom na smerníkoch je tzv. ''dereferencia'', ktorá sa realizuje operátorom <tt>*</tt>. Ak <tt>p</tt> je smerník, možno pomocou zápisu <tt>*p</tt> pristúpiť k údajom na adrese reprezentovanej smerníkom <tt>p</tt>. Tieto údaje potom možno aj meniť, t. j. <tt>*p</tt> môže vystupovať napríklad aj na ľavej strane priradenia. To demonštrujeme na drobnom rozšírení predchádzajúceho príkladu.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 6 604: Riadok 6 605:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Operátor dereferencie vysvetľuje aj spôsob, ktorým sa smerníky deklarujú. Riadok
 
Operátor dereferencie vysvetľuje aj spôsob, ktorým sa smerníky deklarujú. Riadok
<pre>
+
<syntaxhighlight lang="C++">
 
int *p;
 
int *p;
</pre>
+
</syntaxhighlight>
 
deklarujúci smerník na <tt>int</tt> totiž treba chápať takto: ''ak vezmeme smerník <tt>p</tt> a aplikujeme na neho operátor <tt>*</tt>, získame hodnotu typu <tt>int</tt>''. Takáto interpretácia sa ukáže byť veľmi užitočnou pri komplikovanejších deklaráciách so smerníkmi.
 
deklarujúci smerník na <tt>int</tt> totiž treba chápať takto: ''ak vezmeme smerník <tt>p</tt> a aplikujeme na neho operátor <tt>*</tt>, získame hodnotu typu <tt>int</tt>''. Takáto interpretácia sa ukáže byť veľmi užitočnou pri komplikovanejších deklaráciách so smerníkmi.
  
Riadok 6 622: Riadok 6 623:
 
V &bdquo;čistom C&rdquo; okrem iného nie je možné predávať parametre funkcií referenciou. Rovnaký efekt však možno docieliť predávaním hodnotou tak, že sa ako hodnota pošle smerník. To demonštrujeme pomocou nasledujúcej &bdquo;smerníkovej&rdquo; verzie funkcie realizujúcej výmenu hodnôt dvoch premenných.
 
V &bdquo;čistom C&rdquo; okrem iného nie je možné predávať parametre funkcií referenciou. Rovnaký efekt však možno docieliť predávaním hodnotou tak, že sa ako hodnota pošle smerník. To demonštrujeme pomocou nasledujúcej &bdquo;smerníkovej&rdquo; verzie funkcie realizujúcej výmenu hodnôt dvoch premenných.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 6 640: Riadok 6 641:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Poznať uvedenú alternatívu k predávaniu parametrov referenciou môže byť užitočné aj pri práci v C++, keďže ju často využívajú rôzne knižničné funkcie.
 
Poznať uvedenú alternatívu k predávaniu parametrov referenciou môže byť užitočné aj pri práci v C++, keďže ju často využívajú rôzne knižničné funkcie.
Riadok 6 655: Riadok 6 656:
 
* Nepotrebnú pamäť vyhradenú takýmto spôsobom je dobrým zvykom uvoľniť pomocou operátora ''delete''.
 
* Nepotrebnú pamäť vyhradenú takýmto spôsobom je dobrým zvykom uvoľniť pomocou operátora ''delete''.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 6 668: Riadok 6 669:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Smerníky a polia ==
 
== Smerníky a polia ==
  
 
Smerníky a polia spolu veľmi úzko súvisia. Operátor <tt>[ ]</tt> je totiž ''v prvom rade'' definovaný na smerníkoch. Nech <tt>p</tt> je smerník definovaný ako
 
Smerníky a polia spolu veľmi úzko súvisia. Operátor <tt>[ ]</tt> je totiž ''v prvom rade'' definovaný na smerníkoch. Nech <tt>p</tt> je smerník definovaný ako
<pre>
+
<syntaxhighlight lang="C++">
 
T *p;
 
T *p;
</pre>
+
</syntaxhighlight>
 
kde <tt>T</tt> označuje nejaký typ. V takom prípade:
 
kde <tt>T</tt> označuje nejaký typ. V takom prípade:
 
* Zápis <tt>p[0]</tt> vyjadruje to isté ako <tt>*p</tt> &ndash; ide o hodnotu typu <tt>T</tt> uloženú na adrese reprezentovanej smerníkom <tt>p</tt>.
 
* Zápis <tt>p[0]</tt> vyjadruje to isté ako <tt>*p</tt> &ndash; ide o hodnotu typu <tt>T</tt> uloženú na adrese reprezentovanej smerníkom <tt>p</tt>.
Riadok 6 682: Riadok 6 683:
 
Táto situácia je znázornená na nasledujúcom obrázku.
 
Táto situácia je znázornená na nasledujúcom obrázku.
  
[[#Súbor:Pamat7.png]]
+
[[Súbor:Pamat7.png]]
  
 
Pole prvkov typu <tt>T</tt> definované ako  
 
Pole prvkov typu <tt>T</tt> definované ako  
<pre>
+
<syntaxhighlight lang="C++">
 
T a[N];
 
T a[N];
</pre>
+
</syntaxhighlight>
 
je potom iba ''konštantný smerník'' na prvok typu <tt>T</tt> (prvý &ndash; t. j. vlastne nultý &ndash; prvok poľa). Konštantným je preto, lebo adresa prvého prvku takto definovaného poľa je počas behu programu fixná a nemôže sa meniť. (To okrem iného vysvetľuje, prečo pre pole <tt>a</tt> nemožno písať <tt>&a</tt> &ndash; pole <tt>a</tt> totiž nie je &bdquo;premenná v pravom slova zmysle&rdquo;, ale iba konštanta smerníkového typu.)
 
je potom iba ''konštantný smerník'' na prvok typu <tt>T</tt> (prvý &ndash; t. j. vlastne nultý &ndash; prvok poľa). Konštantným je preto, lebo adresa prvého prvku takto definovaného poľa je počas behu programu fixná a nemôže sa meniť. (To okrem iného vysvetľuje, prečo pre pole <tt>a</tt> nemožno písať <tt>&a</tt> &ndash; pole <tt>a</tt> totiž nie je &bdquo;premenná v pravom slova zmysle&rdquo;, ale iba konštanta smerníkového typu.)
  
[[#Súbor:Pamat6.png]]
+
[[Súbor:Pamat6.png]]
  
 
Každé pole je teda zároveň aj smerníkom na svoj prvý prvok. Korektný je tak napríklad aj nasledujúci program.
 
Každé pole je teda zároveň aj smerníkom na svoj prvý prvok. Korektný je tak napríklad aj nasledujúci program.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 6 733: Riadok 6 734:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Dynamické alokovanie poľa ===
 
=== Dynamické alokovanie poľa ===
  
 
Operátorom <tt>new</tt> možno alokovať aj pole zadanej dĺžky <tt>N</tt> (t. j. súvislý úsek pamäte postačujúci na uchovanie práve <tt>N</tt> hodnôt daného typu). Napríklad príkaz  
 
Operátorom <tt>new</tt> možno alokovať aj pole zadanej dĺžky <tt>N</tt> (t. j. súvislý úsek pamäte postačujúci na uchovanie práve <tt>N</tt> hodnôt daného typu). Napríklad príkaz  
<pre>
+
<syntaxhighlight lang="C++">
 
int *p = new int[10];
 
int *p = new int[10];
</pre>
+
</syntaxhighlight>
 
vyhradí pamäť postačujúcu práve na uchovanie desiatich celých čísel &ndash; čiže vlastne pole veľkosti 10. Z pozorovaní učinených vyššie vyplýva, že smerník <tt>p</tt> je potom možné používať úplne rovnakým spôsobom, ako polia:  
 
vyhradí pamäť postačujúcu práve na uchovanie desiatich celých čísel &ndash; čiže vlastne pole veľkosti 10. Z pozorovaní učinených vyššie vyplýva, že smerník <tt>p</tt> je potom možné používať úplne rovnakým spôsobom, ako polia:  
 
* Hodnota <tt>p[0]</tt> je tá istá ako <tt>*p</tt>.
 
* Hodnota <tt>p[0]</tt> je tá istá ako <tt>*p</tt>.
Riadok 6 749: Riadok 6 750:
 
Takúto dynamickú alokáciu polí možno využiť napríklad na vytvorenie poľa o používateľom zadanej veľkosti tak, ako v nasledujúcom ukážkovom programe.
 
Takúto dynamickú alokáciu polí možno využiť napríklad na vytvorenie poľa o používateľom zadanej veľkosti tak, ako v nasledujúcom ukážkovom programe.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 6 770: Riadok 6 771:
 
     return 0;   
 
     return 0;   
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
<span style="font-size:85%">''Poznámka'': Niektoré kompilátory (ako napríklad <tt>gcc</tt>) umožňujú vytvoriť pole o používateľom zadanej veľkosti aj jednoduchším spôsobom, keďže akceptujú deklarácie typu <tt>int a[N];</tt>, kde <tt>N</tt> môže byť aj premenná, ktorej hodnota už bola načítaná príkazom <tt>cin >> N;</tt>. Nemusí to ale fungovať vždy &ndash; odporúčané je používať radšej dynamickú alokáciu poľa tak, ako v príklade vyššie.</span>
 
<span style="font-size:85%">''Poznámka'': Niektoré kompilátory (ako napríklad <tt>gcc</tt>) umožňujú vytvoriť pole o používateľom zadanej veľkosti aj jednoduchším spôsobom, keďže akceptujú deklarácie typu <tt>int a[N];</tt>, kde <tt>N</tt> môže byť aj premenná, ktorej hodnota už bola načítaná príkazom <tt>cin >> N;</tt>. Nemusí to ale fungovať vždy &ndash; odporúčané je používať radšej dynamickú alokáciu poľa tak, ako v príklade vyššie.</span>
Riadok 6 802: Riadok 6 803:
 
Bez ohľadu na implementáciu samotného dynamického poľa už teda vieme napísať kostru programu, ktorý ho využíva:
 
Bez ohľadu na implementáciu samotného dynamického poľa už teda vieme napísať kostru programu, ktorý ho využíva:
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
#include <cassert>
 
#include <cassert>
Riadok 6 853: Riadok 6 854:
 
     return 0;     
 
     return 0;     
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Môžeme teraz prejsť k samotnej implementácii dynamického poľa:
 
Môžeme teraz prejsť k samotnej implementácii dynamického poľa:
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
#include <cassert>
 
#include <cassert>
Riadok 6 932: Riadok 6 933:
 
     return 0;     
 
     return 0;     
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Smerníková aritmetika ==
 
== Smerníková aritmetika ==
  
 
Na smerníkoch možno vykonávať určité operácie, súhrn ktorých býva honosne nazývaný ''smerníkovou aritmetikou''. Nech <tt>p</tt>, <tt>p1</tt>, <tt>p2</tt> sú smerníky definované ako
 
Na smerníkoch možno vykonávať určité operácie, súhrn ktorých býva honosne nazývaný ''smerníkovou aritmetikou''. Nech <tt>p</tt>, <tt>p1</tt>, <tt>p2</tt> sú smerníky definované ako
<pre>
+
<syntaxhighlight lang="C++">
 
T *p;
 
T *p;
 
T *p1;
 
T *p1;
 
T *p2;
 
T *p2;
</pre>
+
</syntaxhighlight>
 
kde <tt>T</tt> označuje nejaký typ. Nech <tt>n</tt> je typu <tt>int</tt>. Potom:
 
kde <tt>T</tt> označuje nejaký typ. Nech <tt>n</tt> je typu <tt>int</tt>. Potom:
 
* <tt>p + n</tt> označuje smerník na <tt>n</tt>-tý pamäťový úsek (postačujúci práve na uchovanie hodnoty typu <tt>T</tt>) za adresou <tt>p</tt>.
 
* <tt>p + n</tt> označuje smerník na <tt>n</tt>-tý pamäťový úsek (postačujúci práve na uchovanie hodnoty typu <tt>T</tt>) za adresou <tt>p</tt>.
Riadok 6 951: Riadok 6 952:
  
 
Program, ktorý najprv načíta pole a následne prvky tohto poľa vypíše od konca, tak možno napísať napríklad aj takto:
 
Program, ktorý najprv načíta pole a následne prvky tohto poľa vypíše od konca, tak možno napísať napríklad aj takto:
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 6 972: Riadok 6 973:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Ladenie programov so smerníkmi ==
 
== Ladenie programov so smerníkmi ==
Riadok 6 991: Riadok 6 992:
  
 
Napríklad deklaráciu  
 
Napríklad deklaráciu  
<pre>
+
<syntaxhighlight lang="C++">
 
int *a[10]; // t. j. *(a[10])
 
int *a[10]; // t. j. *(a[10])
</pre>
+
</syntaxhighlight>
 
teraz treba chápať takto: ak vezmeme <tt>a</tt>, pozrieme sa na niektorý z desiatich prvkov tohto poľa a nakoniec aplikujeme dereferenciu, dostaneme hodnotu typu <tt>int</tt>. Zadeklarovali sme teda desaťprvkové pole smerníkov na <tt>int</tt>. Rovnako možno intepretovať aj nasledujúce deklarácie:
 
teraz treba chápať takto: ak vezmeme <tt>a</tt>, pozrieme sa na niektorý z desiatich prvkov tohto poľa a nakoniec aplikujeme dereferenciu, dostaneme hodnotu typu <tt>int</tt>. Zadeklarovali sme teda desaťprvkové pole smerníkov na <tt>int</tt>. Rovnako možno intepretovať aj nasledujúce deklarácie:
<pre>
+
<syntaxhighlight lang="C++">
 
int **a;        // a je smernik na smernik na int; kazde pole smernikov na int tak mozno priradit do a (ale nie opacne)
 
int **a;        // a je smernik na smernik na int; kazde pole smernikov na int tak mozno priradit do a (ale nie opacne)
 
int (*a)[10];    // a je smernik na pole desiatich celych cisel
 
int (*a)[10];    // a je smernik na pole desiatich celych cisel
 
int *(*(a[10])); // a je desatprvkove pole smernikov na smerniky na int
 
int *(*(a[10])); // a je desatprvkove pole smernikov na smerniky na int
 
int **a[10];    // to iste, ako na predchadzajucom riadku
 
int **a[10];    // to iste, ako na predchadzajucom riadku
</pre>
+
</syntaxhighlight>
  
 
== Dvojrozmerné polia ==
 
== Dvojrozmerné polia ==
Riadok 7 008: Riadok 7 009:
 
Najjednoduchší &ndash; hoci väčšinou nepraktický &ndash; spôsob práce s takýmito údajmi poskytuje priamo C++ v podobe takzvaných ''viacrozmerných polí''. Prácu s ''dvojrozmernými poľami'' si teraz v stručnosti ukážeme. Ide o jednoduché rozšírenie jednorozmerných polí &ndash; akurát namiesto <tt>i</tt>-teho prvku <tt>a[i]</tt> pristupujeme k prvkom <tt>a[i][j]</tt> v <tt>i</tt>-tom riadku a <tt>j</tt>-tom stĺpci dvojrozmerného poľa.
 
Najjednoduchší &ndash; hoci väčšinou nepraktický &ndash; spôsob práce s takýmito údajmi poskytuje priamo C++ v podobe takzvaných ''viacrozmerných polí''. Prácu s ''dvojrozmernými poľami'' si teraz v stručnosti ukážeme. Ide o jednoduché rozšírenie jednorozmerných polí &ndash; akurát namiesto <tt>i</tt>-teho prvku <tt>a[i]</tt> pristupujeme k prvkom <tt>a[i][j]</tt> v <tt>i</tt>-tom riadku a <tt>j</tt>-tom stĺpci dvojrozmerného poľa.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 7 026: Riadok 7 027:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>   
+
</syntaxhighlight>   
  
 
Dvojrozmerné pole o <tt>m</tt> riadkoch a <tt>n</tt> stĺpcoch sa v pamäti reprezentuje ako jediný súvislý úsek, v ktorom idú za sebou jednotlivé riadky reprezentované ako jednorozmerné polia. To je znázornené na nasledujúcom obrázku.
 
Dvojrozmerné pole o <tt>m</tt> riadkoch a <tt>n</tt> stĺpcoch sa v pamäti reprezentuje ako jediný súvislý úsek, v ktorom idú za sebou jednotlivé riadky reprezentované ako jednorozmerné polia. To je znázornené na nasledujúcom obrázku.
  
[[#Súbor:Pamat8.png]]
+
[[Súbor:Pamat8.png]]
  
 
Z tejto reprezentácie ale vyplýva, že typ dvojrozmerného poľa je inherentne previazaný s dĺžkou jednotlivých riadkov (t. j. s počtom stĺpcov). To sa ukazuje byť nepraktické napríklad pri predávaní dvojrozmerných polí ako parametrov pre funkcie, v hlavičkách ktorých je nutné tento počet stĺpcov zadať:
 
Z tejto reprezentácie ale vyplýva, že typ dvojrozmerného poľa je inherentne previazaný s dĺžkou jednotlivých riadkov (t. j. s počtom stĺpcov). To sa ukazuje byť nepraktické napríklad pri predávaní dvojrozmerných polí ako parametrov pre funkcie, v hlavičkách ktorých je nutné tento počet stĺpcov zadať:
  
<pre>
+
<syntaxhighlight lang="C++">
 
...
 
...
  
Riadok 7 065: Riadok 7 066:
 
     ...
 
     ...
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Budeme preto hľadať pohodlnejší spôsob práce s dvojrozmernými údajmi.
 
Budeme preto hľadať pohodlnejší spôsob práce s dvojrozmernými údajmi.
Riadok 7 073: Riadok 7 074:
 
Omnoho pohodlnejšou alternatívou k dvojrozmerným poliam sú ''polia smerníkov''. Dvojrozmerné dáta tu uchovávame prostredníctvom poľa, <tt>i</tt>-ty prvok ktorého je smerníkom ukazujúcim na prvý &ndash; t. j. vlastne nultý &ndash; prvok <tt>i</tt>-teho riadku. Pre každý riadok potom môžeme naalokovať samostatné pole pre jeho prvky. Má to okrem iného tú výhodu, že jednotlivé riadky nemusia byť rovnako dlhé. Zatiaľ si však len ukážme spôsob, ako vytvoriť pole smerníkov reprezentujúce obdĺžnikovú tabuľku typu <tt>m</tt> krát <tt>n</tt>; to bude v pamäti reprezentované podobne, ako na nasledujúcom obrázku.  
 
Omnoho pohodlnejšou alternatívou k dvojrozmerným poliam sú ''polia smerníkov''. Dvojrozmerné dáta tu uchovávame prostredníctvom poľa, <tt>i</tt>-ty prvok ktorého je smerníkom ukazujúcim na prvý &ndash; t. j. vlastne nultý &ndash; prvok <tt>i</tt>-teho riadku. Pre každý riadok potom môžeme naalokovať samostatné pole pre jeho prvky. Má to okrem iného tú výhodu, že jednotlivé riadky nemusia byť rovnako dlhé. Zatiaľ si však len ukážme spôsob, ako vytvoriť pole smerníkov reprezentujúce obdĺžnikovú tabuľku typu <tt>m</tt> krát <tt>n</tt>; to bude v pamäti reprezentované podobne, ako na nasledujúcom obrázku.  
  
[[#Súbor:Pamat9.png]]
+
[[Súbor:Pamat9.png]]
  
 
Nasledujúci program vytvorí pole smerníkov, ktoré reprezentuje obdĺžnikovú tabuľku o <tt>m</tt> krát <tt>n</tt> celých číslach, načíta do nej prvky zo vstupu a nakoniec na výstup vypíše aritmetické priemery hodnôt v jednotlivých jej stĺpcoch.
 
Nasledujúci program vytvorí pole smerníkov, ktoré reprezentuje obdĺžnikovú tabuľku o <tt>m</tt> krát <tt>n</tt> celých číslach, načíta do nej prvky zo vstupu a nakoniec na výstup vypíše aritmetické priemery hodnôt v jednotlivých jej stĺpcoch.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 7 120: Riadok 7 121:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Dynamicky alokované polia smerníkov ==
 
== Dynamicky alokované polia smerníkov ==
Riadok 7 126: Riadok 7 127:
 
Aj samotné polia smerníkov je možné alokovať dynamicky, čo umožňuje počas behu nastaviť nielen veľkosť jednotlivých riadkov, ale aj ich počet. Príklad s priemermi jednotlivých stĺpcov tak vieme prepísať napríklad nasledovne:  
 
Aj samotné polia smerníkov je možné alokovať dynamicky, čo umožňuje počas behu nastaviť nielen veľkosť jednotlivých riadkov, ale aj ich počet. Príklad s priemermi jednotlivých stĺpcov tak vieme prepísať napríklad nasledovne:  
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 7 171: Riadok 7 172:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Príklad: výšková mapa ===
 
=== Príklad: výšková mapa ===
Riadok 7 212: Riadok 7 213:
 
Samotný program:
 
Samotný program:
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
#include "SVGdraw.h"
 
#include "SVGdraw.h"
Riadok 7 322: Riadok 7 323:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Polia reťazcov ==
 
== Polia reťazcov ==
Riadok 7 330: Riadok 7 331:
 
Nasledujúci jednoduchý program je ukážkou použitia takto implementovaných polí reťazcov. Zo vstupu postupne načítava riadky, až kým je zadaný prázdny riadok. Tie postupne ukladá do poľa. Na záver sa všetky tieto reťazce vypíšu na výstup, oddelené medzerami.
 
Nasledujúci jednoduchý program je ukážkou použitia takto implementovaných polí reťazcov. Zo vstupu postupne načítava riadky, až kým je zadaný prázdny riadok. Tie postupne ukladá do poľa. Na záver sa všetky tieto reťazce vypíšu na výstup, oddelené medzerami.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
#include <cstring>
 
#include <cstring>
Riadok 7 365: Riadok 7 366:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
''Cvičenie:'' prerobte tento program tak, aby namiesto poľa <tt>a</tt> fixnej veľkosti <tt>maxN</tt> používal dynamické pole.
 
''Cvičenie:'' prerobte tento program tak, aby namiesto poľa <tt>a</tt> fixnej veľkosti <tt>maxN</tt> používal dynamické pole.
Riadok 7 378: Riadok 7 379:
  
 
Na písanie programov umožňujúcich spracovanie takýchto argumentov je potrebné využiť &bdquo;jemne pokročilejšiu&rdquo; verziu funkcie <tt>main</tt> s hlavičkou
 
Na písanie programov umožňujúcich spracovanie takýchto argumentov je potrebné využiť &bdquo;jemne pokročilejšiu&rdquo; verziu funkcie <tt>main</tt> s hlavičkou
<pre>
+
<syntaxhighlight lang="C++">
 
int main(int argc, char** argv)
 
int main(int argc, char** argv)
</pre>
+
</syntaxhighlight>
 
&ndash; tú automaticky generuje napríklad aj prostredie NetBeans. Význam parametrov <tt>argc</tt> a <tt>argv</tt> je nasledovný:
 
&ndash; tú automaticky generuje napríklad aj prostredie NetBeans. Význam parametrov <tt>argc</tt> a <tt>argv</tt> je nasledovný:
 
* <tt>argv</tt> je pole reťazcov (resp. pole smerníkov na <tt>char</tt>) a <tt>argc</tt> je počet reťazcov v tomto poli.  
 
* <tt>argv</tt> je pole reťazcov (resp. pole smerníkov na <tt>char</tt>) a <tt>argc</tt> je počet reťazcov v tomto poli.  
Riadok 7 388: Riadok 7 389:
 
Nasledujúci jednoduchý program postupne vypíše všetky argumenty, ktoré dostal z príkazového riadku.
 
Nasledujúci jednoduchý program postupne vypíše všetky argumenty, ktoré dostal z príkazového riadku.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 7 398: Riadok 7 399:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
 
=Prednáška 14=
 
=Prednáška 14=
 
== Oznamy ==
 
== Oznamy ==
Riadok 7 434: Riadok 7 435:
 
** Ale pozor na prekročenie kapacity poľa (môžeme použiť dynamické pole)
 
** Ale pozor na prekročenie kapacity poľa (môžeme použiť dynamické pole)
  
<pre>
+
<syntaxhighlight lang="C++">
 
/* štruktúra reprezentujúca množinu pomocou poľa */
 
/* štruktúra reprezentujúca množinu pomocou poľa */
 
struct set {
 
struct set {
Riadok 7 459: Riadok 7 460:
 
     a.length++;
 
     a.length++;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
===Množina ako utriedené pole ===
 
===Množina ako utriedené pole ===
Riadok 7 468: Riadok 7 469:
 
* Funkcia <tt>add</tt> musí vložiť prvok na správne miesto v utriedenom poradí
 
* Funkcia <tt>add</tt> musí vložiť prvok na správne miesto v utriedenom poradí
  
<pre>
+
<syntaxhighlight lang="C++">
 
bool contains(set &a, int x) {
 
bool contains(set &a, int x) {
 
     int left = 0, right = a.length - 1;
 
     int left = 0, right = a.length - 1;
Riadok 7 495: Riadok 7 496:
 
     a.length++;
 
     a.length++;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
==Plán na dnes==
 
==Plán na dnes==
Riadok 7 515: Riadok 7 516:
  
 
Uzol spájaného zoznamu zapíšeme ako <tt>struct</tt>:
 
Uzol spájaného zoznamu zapíšeme ako <tt>struct</tt>:
<pre>
+
<syntaxhighlight lang="C++">
 
/* štruktúra reprezentujúca uzol spájaného zoznamu */
 
/* štruktúra reprezentujúca uzol spájaného zoznamu */
 
struct node {
 
struct node {
Riadok 7 521: Riadok 7 522:
 
     node* next;  // smerník na další uzol
 
     node* next;  // smerník na další uzol
 
};
 
};
</pre>
+
</syntaxhighlight>
 
Všimnite si, že vo vnútri definície typu <tt>node</tt> používame smerník na <tt>node</tt>.
 
Všimnite si, že vo vnútri definície typu <tt>node</tt> používame smerník na <tt>node</tt>.
  
Riadok 7 534: Riadok 7 535:
 
* ak je zoznam prázdny, first bude NULL
 
* ak je zoznam prázdny, first bude NULL
  
<pre>
+
<syntaxhighlight lang="C++">
 
/* štruktúra reprezentujúca množinu spájaným zoznamom */
 
/* štruktúra reprezentujúca množinu spájaným zoznamom */
 
struct set {
 
struct set {
Riadok 7 543: Riadok 7 544:
 
     a.first = NULL;  // inicializácia prázdnej množiny
 
     a.first = NULL;  // inicializácia prázdnej množiny
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
===Odbočka: práca so smerníkmi na struct===
 
===Odbočka: práca so smerníkmi na struct===
 
Ak smerník p ukazuje na uzol zoznamu (má typ <tt>node *</tt>) a chceme vypísať dáta v tomto uzle, môžeme použiť tieto možnosti:
 
Ak smerník p ukazuje na uzol zoznamu (má typ <tt>node *</tt>) a chceme vypísať dáta v tomto uzle, môžeme použiť tieto možnosti:
<pre>
+
<syntaxhighlight lang="C++">
 
cout << (*p).data;
 
cout << (*p).data;
 
cout << p->data;  
 
cout << p->data;  
</pre>
+
</syntaxhighlight>
 
* <tt>p->data</tt> je teda skratka za <tt>(*p).data</tt>
 
* <tt>p->data</tt> je teda skratka za <tt>(*p).data</tt>
  
Riadok 7 556: Riadok 7 557:
  
 
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
<pre>
+
<syntaxhighlight lang="C++">
 
void add(set &a, int x) {
 
void add(set &a, int x) {
 
     node* p = new node;  // vytvoríme nový uzol
 
     node* p = new node;  // vytvoríme nový uzol
Riadok 7 563: Riadok 7 564:
 
     a.first = p;        // tento uzol je novým začiatkom
 
     a.first = p;        // tento uzol je novým začiatkom
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Použitie v programe na načítanie čísel do zoznamu
 
Použitie v programe na načítanie čísel do zoznamu
 
* načítavame nezáporné čísla, zastaneme na prvom zápornom
 
* načítavame nezáporné čísla, zastaneme na prvom zápornom
<pre>
+
<syntaxhighlight lang="C++">
 
int main(void) {
 
int main(void) {
 
   set a;
 
   set a;
Riadok 7 578: Riadok 7 579:
 
   }
 
   }
 
}
 
}
</pre>
+
</syntaxhighlight>
 
Pozor, čísla budú v zozname odzadu, posledné na začiatku
 
Pozor, čísla budú v zozname odzadu, posledné na začiatku
  
Riadok 7 585: Riadok 7 586:
  
 
Prvá funkcia vypíše všetky čísla v zozname
 
Prvá funkcia vypíše všetky čísla v zozname
<pre>
+
<syntaxhighlight lang="C++">
 
void print(set &a) {
 
void print(set &a) {
 
     node* p = a.first;
 
     node* p = a.first;
Riadok 7 594: Riadok 7 595:
 
     cout << endl;
 
     cout << endl;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Druhá funkcia hľadá, či je číslo x v zozname (ako čítačka kariet)
 
Druhá funkcia hľadá, či je číslo x v zozname (ako čítačka kariet)
<pre>
+
<syntaxhighlight lang="C++">
 
bool contains(set &a, int x) {
 
bool contains(set &a, int x) {
 
     node* p = a.first;
 
     node* p = a.first;
Riadok 7 608: Riadok 7 609:
 
     return false;
 
     return false;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Tretia funkcia uvoľní pamäť spájaného zoznamu, keď ho už nepotrebujeme
 
Tretia funkcia uvoľní pamäť spájaného zoznamu, keď ho už nepotrebujeme
<pre>
+
<syntaxhighlight lang="C++">
 
void destroy(set &a) {
 
void destroy(set &a) {
 
     while (a.first != NULL) {
 
     while (a.first != NULL) {
Riadok 7 619: Riadok 7 620:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
===Spájané zoznamy, zhrnutie===
 
===Spájané zoznamy, zhrnutie===
Riadok 7 638: Riadok 7 639:
 
* Funkcie <tt>contains</tt> aj <tt>add</tt> sú veľmi rýchle
 
* Funkcie <tt>contains</tt> aj <tt>add</tt> sú veľmi rýchle
  
<pre>
+
<syntaxhighlight lang="C++">
 
/* štruktúra reprezentujúca množinu priamym adresovaním */
 
/* štruktúra reprezentujúca množinu priamym adresovaním */
 
struct set {
 
struct set {
Riadok 7 658: Riadok 7 659:
 
   a.p[x] = true;
 
   a.p[x] = true;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Jednoduché hešovanie ==
 
== Jednoduché hešovanie ==
Riadok 7 672: Riadok 7 673:
 
** pozor, -10 % 3 je -1, takže radšej použijeme absolútnu hodnotu z ''x'' (funkcia <tt>abs</tt> z knižnice <tt>cstdlib</tt>)
 
** pozor, -10 % 3 je -1, takže radšej použijeme absolútnu hodnotu z ''x'' (funkcia <tt>abs</tt> z knižnice <tt>cstdlib</tt>)
  
<pre>
+
<syntaxhighlight lang="C++">
 
int hash(int x, int m) {
 
int hash(int x, int m) {
 
     return abs(x) % m;
 
     return abs(x) % m;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Vkladanie
 
Vkladanie
Riadok 7 702: Riadok 7 703:
 
Namiesto prvkov typu int budeme mať v každom políčku hešovacej tabuľky  spájaný zoznam so všetkými prvkami množiny, ktoré hešovacia funkcia priradila na toto políčko.
 
Namiesto prvkov typu int budeme mať v každom políčku hešovacej tabuľky  spájaný zoznam so všetkými prvkami množiny, ktoré hešovacia funkcia priradila na toto políčko.
 
   
 
   
<pre>
+
<syntaxhighlight lang="C++">
 
/* štruktúra reprezentujúca uzol spájaného zoznamu */
 
/* štruktúra reprezentujúca uzol spájaného zoznamu */
 
struct node {
 
struct node {
Riadok 7 714: Riadok 7 715:
 
     int m;        // dlžka poľa lists
 
     int m;        // dlžka poľa lists
 
};
 
};
</pre>
+
</syntaxhighlight>
  
  
Riadok 7 720: Riadok 7 721:
 
* Správny index dostaneme pomocou <tt>hash(x, a.m)</tt>, kde ''x'' je hľadaný kľúč a <tt>a.m</tt> je veľkosť tabuľky ''a.lists''.
 
* Správny index dostaneme pomocou <tt>hash(x, a.m)</tt>, kde ''x'' je hľadaný kľúč a <tt>a.m</tt> je veľkosť tabuľky ''a.lists''.
  
<pre>
+
<syntaxhighlight lang="C++">
 
bool contains(set &a, int x) {
 
bool contains(set &a, int x) {
 
     int index = hash(x, a.m);
 
     int index = hash(x, a.m);
Riadok 7 730: Riadok 7 731:
 
     return false;
 
     return false;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Pri vkladaní stačí nový prvok pridať na začiatok spájaného zoznamu na správnom mieste tabuľky
 
Pri vkladaní stačí nový prvok pridať na začiatok spájaného zoznamu na správnom mieste tabuľky
<pre>
+
<syntaxhighlight lang="C++">
 
void add(set &a, int x) {
 
void add(set &a, int x) {
 
     int index = hash(x, a.m);
 
     int index = hash(x, a.m);
Riadok 7 741: Riadok 7 742:
 
     a.lists[index] = temp;
 
     a.lists[index] = temp;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Ďalšie funkcie
 
Ďalšie funkcie
Riadok 7 776: Riadok 7 777:
  
 
===Program: množina ako neutriedené pole===
 
===Program: množina ako neutriedené pole===
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
#include <cassert>
 
#include <cassert>
Riadok 7 831: Riadok 7 832:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
===Program: množina ako utriedené pole===
 
===Program: množina ako utriedené pole===
<pre>
+
<syntaxhighlight lang="C++">
 
bool contains(set &a, int x) {
 
bool contains(set &a, int x) {
 
     int left = 0, right = a.length - 1;
 
     int left = 0, right = a.length - 1;
Riadok 7 860: Riadok 7 861:
 
     a.length++;
 
     a.length++;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
  
 
===Program: množina ako spájaný zoznam===
 
===Program: množina ako spájaný zoznam===
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
#include <cassert>
 
#include <cassert>
Riadok 7 925: Riadok 7 926:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
===Program: množina priamym adresovaním===
 
===Program: množina priamym adresovaním===
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
#include <cassert>
 
#include <cassert>
Riadok 7 978: Riadok 7 979:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
===Program: množina hešovaním===
 
===Program: množina hešovaním===
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
#include <cassert>
 
#include <cassert>
Riadok 8 054: Riadok 8 055:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
 
=Prednáška 15=
 
=Prednáška 15=
 
== Oznamy ==
 
== Oznamy ==
Riadok 8 064: Riadok 8 065:
  
 
Základy práce so smerníkmi:  
 
Základy práce so smerníkmi:  
<pre>
+
<syntaxhighlight lang="C++">
 
int a = 7;        // premenna typu int  
 
int a = 7;        // premenna typu int  
 
int *b = NULL;    // smernik na int  
 
int *b = NULL;    // smernik na int  
Riadok 8 070: Riadok 8 071:
 
*b = 8;            // v premennej a je teraz 8  
 
*b = 8;            // v premennej a je teraz 8  
 
a = (*b)+1;        // v premennej a je teraz 9  
 
a = (*b)+1;        // v premennej a je teraz 9  
</pre>
+
</syntaxhighlight>
  
 
Smerníky a polia:
 
Smerníky a polia:
<pre>
+
<syntaxhighlight lang="C++">
 
int a[3];          // a je vlastne konstantny smernik na prvy prvok pola
 
int a[3];          // a je vlastne konstantny smernik na prvy prvok pola
 
int *b = a;        // mozeme ho priradit do ineho smernika (opacne to nejde)
 
int *b = a;        // mozeme ho priradit do ineho smernika (opacne to nejde)
Riadok 8 084: Riadok 8 085:
 
b = new int[a[1]];  // b teraz ukazuje na nove pole dlzky 4
 
b = new int[a[1]];  // b teraz ukazuje na nove pole dlzky 4
 
delete[] b;        // uvolnenie pamate alokovanej pre nove pole
 
delete[] b;        // uvolnenie pamate alokovanej pre nove pole
</pre>
+
</syntaxhighlight>
  
 
Smerník môže ukazovať aj na <tt>struct</tt>:
 
Smerník môže ukazovať aj na <tt>struct</tt>:
<pre>
+
<syntaxhighlight lang="C++">
 
struct bod {
 
struct bod {
 
   int x,y;
 
   int x,y;
Riadok 8 099: Riadok 8 100:
 
p2->y = 10;        // bod, na ktory ukazuje p2, bude mat y-ovu suradnicu 10
 
p2->y = 10;        // bod, na ktory ukazuje p2, bude mat y-ovu suradnicu 10
 
delete p2;        // uvolnenie pamate
 
delete p2;        // uvolnenie pamate
</pre>
+
</syntaxhighlight>
  
 
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:
 
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:
Riadok 8 117: Riadok 8 118:
 
S použitím knižnice <tt>cstdio</tt> možno na konzolu písať pomocou funkcie <tt>[http://www.cplusplus.com/reference/cstdio/printf/ printf]</tt>. Jej základné použitie môže vyzerať napríklad takto:
 
S použitím knižnice <tt>cstdio</tt> možno na konzolu písať pomocou funkcie <tt>[http://www.cplusplus.com/reference/cstdio/printf/ printf]</tt>. Jej základné použitie môže vyzerať napríklad takto:
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
  
Riadok 8 124: Riadok 8 125:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Funkciu <tt>printf</tt> možno volať aj s viac ako jedným argumentom. Vo všeobecnosti vyzerá jej volanie nasledovne:
 
Funkciu <tt>printf</tt> možno volať aj s viac ako jedným argumentom. Vo všeobecnosti vyzerá jej volanie nasledovne:
<pre>
+
<syntaxhighlight lang="C++">
 
printf(format, hodnota1, hodnota2, ...)
 
printf(format, hodnota1, hodnota2, ...)
</pre>
+
</syntaxhighlight>
  
 
Prvým argumentom je takzvaný ''formátovací reťazec'', za ním nasleduje niekoľko ďalších argumentov (prípadne aj žiaden). Formátovací reťazec pozostáva z dvoch druhov znakov: bežné znaky, ktoré sa priamo vypíšu na výstup a takzvané ''špecifikácie konverzií'' začínajúce symbolom <tt>%</tt> a končiace tzv. ''znakom konverzie'', ktoré majú za následok vypísanie niektorého z ďalších argumentov funkcie <tt>printf</tt> (presnejšie prvého ešte nevypísaného argumentu). V rámci špecifikácie konverzie možno zadať formát, v ktorom sa má ten-ktorý argument vypísať.  
 
Prvým argumentom je takzvaný ''formátovací reťazec'', za ním nasleduje niekoľko ďalších argumentov (prípadne aj žiaden). Formátovací reťazec pozostáva z dvoch druhov znakov: bežné znaky, ktoré sa priamo vypíšu na výstup a takzvané ''špecifikácie konverzií'' začínajúce symbolom <tt>%</tt> a končiace tzv. ''znakom konverzie'', ktoré majú za následok vypísanie niektorého z ďalších argumentov funkcie <tt>printf</tt> (presnejšie prvého ešte nevypísaného argumentu). V rámci špecifikácie konverzie možno zadať formát, v ktorom sa má ten-ktorý argument vypísať.  
  
 
Napríklad
 
Napríklad
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
  
Riadok 8 142: Riadok 8 143:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
 
vypíše
 
vypíše
 
<pre>
 
<pre>
Riadok 8 172: Riadok 8 173:
  
 
Nasledujúci program vo vhodnom formáte vypíše hodnoty faktoriálu prirodzených čísel od 1 po 20:
 
Nasledujúci program vo vhodnom formáte vypíše hodnoty faktoriálu prirodzených čísel od 1 po 20:
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
  
Riadok 8 189: Riadok 8 190:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Nasledujúci program vypíše vo vhodnom formáte zadaný dátum:
 
Nasledujúci program vypíše vo vhodnom formáte zadaný dátum:
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
  
Riadok 8 204: Riadok 8 205:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Celá špecifikácia konverzie pozostáva z nasledujúcich častí:
 
Celá špecifikácia konverzie pozostáva z nasledujúcich častí:
Riadok 8 216: Riadok 8 217:
 
=== Načítanie formátovaných dát z konzoly: <tt>scanf</tt> ===
 
=== Načítanie formátovaných dát z konzoly: <tt>scanf</tt> ===
 
Funkciu [http://www.cplusplus.com/reference/cstdio/scanf/ <tt>scanf</tt>] s typickým volaním
 
Funkciu [http://www.cplusplus.com/reference/cstdio/scanf/ <tt>scanf</tt>] s typickým volaním
<pre>
+
<syntaxhighlight lang="C++">
 
scanf(format, smernik1, smernik2, ...)  
 
scanf(format, smernik1, smernik2, ...)  
</pre>
+
</syntaxhighlight>
 
možno využiť na načítanie dát z konzoly.
 
možno využiť na načítanie dát z konzoly.
 
* Napríklad <tt>scanf("%d", &x)</tt> načíta celočíselnú hodnotu do premennej <tt>x</tt>.
 
* Napríklad <tt>scanf("%d", &x)</tt> načíta celočíselnú hodnotu do premennej <tt>x</tt>.
Riadok 8 224: Riadok 8 225:
  
 
Jednoduchý príklad použitia:
 
Jednoduchý príklad použitia:
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
  
Riadok 8 239: Riadok 8 240:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
 
Pomocou <tt>scanf</tt> možno načítať aj viacero premenných naraz:
 
Pomocou <tt>scanf</tt> možno načítať aj viacero premenných naraz:
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
  
Riadok 8 256: Riadok 8 257:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Formátovací reťazec sa teraz interpretuje nasledovne:
 
Formátovací reťazec sa teraz interpretuje nasledovne:
Riadok 8 265: Riadok 8 266:
  
 
Nasledujúci príkaz tak napríklad načíta dátum vo formáte <tt>deň.mesiac.rok</tt>:
 
Nasledujúci príkaz tak napríklad načíta dátum vo formáte <tt>deň.mesiac.rok</tt>:
<pre>
+
<syntaxhighlight lang="C++">
 
scanf("%d.%d.%d", &d, &m, &r);
 
scanf("%d.%d.%d", &d, &m, &r);
</pre>
+
</syntaxhighlight>
  
 
=== Kontrola správnosti vstupu ===
 
=== Kontrola správnosti vstupu ===
Riadok 8 275: Riadok 8 276:
 
Príklad: zadávanie dátumu vo formáte <tt>deň.mesiac.rok</tt> s kontrolou vstupu:  
 
Príklad: zadávanie dátumu vo formáte <tt>deň.mesiac.rok</tt> s kontrolou vstupu:  
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
  
Riadok 8 293: Riadok 8 294:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Ďalším príkladom môže byť program, ktorý počíta súčet postupne zadávaných čísel, až kým je zadané nekorektné číslo alebo koniec súboru:
 
Ďalším príkladom môže byť program, ktorý počíta súčet postupne zadávaných čísel, až kým je zadané nekorektné číslo alebo koniec súboru:
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
  
Riadok 8 308: Riadok 8 309:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Textové súbory ==
 
== Textové súbory ==
Riadok 8 319: Riadok 8 320:
  
 
So súbormi sa pri použití knižnice <tt>cstdio</tt> pracuje pomocou typu <tt>FILE *</tt>. Ide tu o smerník na štruktúru typu <tt>FILE</tt>, ktorá obsahuje nejaké (pre programátora zväčša nepodstatné) informácie o súbore, s ktorým sa práve pracuje. Premenné pre prácu so súbormi tak možno definovať napríklad takto:
 
So súbormi sa pri použití knižnice <tt>cstdio</tt> pracuje pomocou typu <tt>FILE *</tt>. Ide tu o smerník na štruktúru typu <tt>FILE</tt>, ktorá obsahuje nejaké (pre programátora zväčša nepodstatné) informácie o súbore, s ktorým sa práve pracuje. Premenné pre prácu so súbormi tak možno definovať napríklad takto:
<pre>
+
<syntaxhighlight lang="C++">
 
FILE *f;
 
FILE *f;
 
FILE *fr, *fw;
 
FILE *fr, *fw;
</pre>  
+
</syntaxhighlight>  
  
 
''Pozor'': v názve typu <tt>FILE</tt> treba dodržať veľké písmená (čiže treba písať <tt>FILE *</tt>, nie <tt>file *</tt>).
 
''Pozor'': v názve typu <tt>FILE</tt> treba dodržať veľké písmená (čiže treba písať <tt>FILE *</tt>, nie <tt>file *</tt>).
Riadok 8 347: Riadok 8 348:
  
 
Nasledujúci program načíta číslo ''n'' a následne ''n'' celých čísel zo súboru <tt>vstup.txt</tt>. Do súboru <tt>vystup.txt</tt> vypíše vstupné čísla v opačnom poradí.
 
Nasledujúci program načíta číslo ''n'' a následne ''n'' celých čísel zo súboru <tt>vstup.txt</tt>. Do súboru <tt>vystup.txt</tt> vypíše vstupné čísla v opačnom poradí.
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
 
#include <cassert>
 
#include <cassert>
Riadok 8 373: Riadok 8 374:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Štandardný vstup a výstup ako súbor ===
 
=== Štandardný vstup a výstup ako súbor ===
  
 
So štandardným vstupom a výstupom sa pracuje rovnako ako so súborom. V <tt>cstdio</tt> sú definované dva konštantné smerníky
 
So štandardným vstupom a výstupom sa pracuje rovnako ako so súborom. V <tt>cstdio</tt> sú definované dva konštantné smerníky
<pre>
+
<syntaxhighlight lang="C++">
 
FILE *stdin, *stdout;
 
FILE *stdin, *stdout;
</pre>
+
</syntaxhighlight>
 
pre štandardný vstupný a výstupný prúd. Tie tak môžu byť použité v ľubovoľnom kontexte, v ktorom sa očakáva súbor. Napríklad volanie <tt>fscanf(stdin,"%d",&x)</tt> je ekvivalentné volaniu <tt>scanf("%d",&x)</tt>.
 
pre štandardný vstupný a výstupný prúd. Tie tak môžu byť použité v ľubovoľnom kontexte, v ktorom sa očakáva súbor. Napríklad volanie <tt>fscanf(stdin,"%d",&x)</tt> je ekvivalentné volaniu <tt>scanf("%d",&x)</tt>.
  
 
Ten istý kód sa tak dá použiť na prácu so súbormi aj so štandardným vstupom resp. výstupom &ndash; stačí len podľa potreby nastaviť premennú typu <tt>FILE *</tt>. Typické použitie je napríklad nasledovné:
 
Ten istý kód sa tak dá použiť na prácu so súbormi aj so štandardným vstupom resp. výstupom &ndash; stačí len podľa potreby nastaviť premennú typu <tt>FILE *</tt>. Typické použitie je napríklad nasledovné:
<pre>
+
<syntaxhighlight lang="C++">
 
     FILE *fr, *fw;
 
     FILE *fr, *fw;
 
     ...
 
     ...
Riadok 8 395: Riadok 8 396:
 
     fprintf(fw, "Hello world!\n");
 
     fprintf(fw, "Hello world!\n");
 
     ...
 
     ...
</pre>
+
</syntaxhighlight>
  
 
=== Testovanie konca súboru ===
 
=== Testovanie konca súboru ===
Riadok 8 408: Riadok 8 409:
  
 
''Formát 1'': <tt>N</tt> (počet čísel) a následne <tt>N</tt> ďalších čísel.
 
''Formát 1'': <tt>N</tt> (počet čísel) a následne <tt>N</tt> ďalších čísel.
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
 
#include <cassert>
 
#include <cassert>
Riadok 8 430: Riadok 8 431:
 
     // tu pride spracovanie dat v poli a
 
     // tu pride spracovanie dat v poli a
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
''Formát 2'': postupnosť čísel ukončená číslom -1 alebo inou špeciálnou hodnotu.  
 
''Formát 2'': postupnosť čísel ukončená číslom -1 alebo inou špeciálnou hodnotu.  
<pre>
+
<syntaxhighlight lang="C++">
 
     // otvorime subor f ako vyssie
 
     // otvorime subor f ako vyssie
 
     N = 0;
 
     N = 0;
Riadok 8 447: Riadok 8 448:
 
     }
 
     }
 
     // zatvorime subor a spracujeme data
 
     // zatvorime subor a spracujeme data
</pre>
+
</syntaxhighlight>
  
 
''Formát 3'': čísla, až kým neskončí súbor (najtypickejší prípad v praxi).
 
''Formát 3'': čísla, až kým neskončí súbor (najtypickejší prípad v praxi).
  
 
Priamočiary prístup nefunguje vždy správne:
 
Priamočiary prístup nefunguje vždy správne:
<pre>
+
<syntaxhighlight lang="C++">
 
     // otvorime subor f ako vyssie
 
     // otvorime subor f ako vyssie
 
     N = 0;
 
     N = 0;
Riadok 8 462: Riadok 8 463:
 
     }
 
     }
 
     // zatvorime subor a spracujeme data
 
     // zatvorime subor a spracujeme data
</pre>
+
</syntaxhighlight>
  
 
Po poslednom čísle v súbore často nasleduje ešte koniec riadku, v dôsledku čoho môže posledné volanie funkcie <tt>fscanf</tt> vyústiť v návratovú hodnotu <tt>-1</tt> (predchádzajúce volanie <tt>fscanf</tt> totiž ešte &bdquo;nenarazilo&rdquo; na koniec súboru, v dôsledku čoho je pred čítaním posledného riadku hodnota <tt>feof(f)</tt> stále rovná <tt>false</tt>). Tým pádom program zlyhá na riadku <tt>assert(kod == 1)</tt>. Tento nedostatok môžeme napraviť napríklad tak, že vo volaní funkcie <tt>fscanf</tt> dáme vo formátovacom reťazci za <tt>%d</tt> medzeru. Tá sa bude pokúšať preskočiť všetky biele znaky až po najbližší nebiely; pritom natrafí na koniec súboru a <tt>feof(f)</tt> už bude vracať <tt>true</tt>.
 
Po poslednom čísle v súbore často nasleduje ešte koniec riadku, v dôsledku čoho môže posledné volanie funkcie <tt>fscanf</tt> vyústiť v návratovú hodnotu <tt>-1</tt> (predchádzajúce volanie <tt>fscanf</tt> totiž ešte &bdquo;nenarazilo&rdquo; na koniec súboru, v dôsledku čoho je pred čítaním posledného riadku hodnota <tt>feof(f)</tt> stále rovná <tt>false</tt>). Tým pádom program zlyhá na riadku <tt>assert(kod == 1)</tt>. Tento nedostatok môžeme napraviť napríklad tak, že vo volaní funkcie <tt>fscanf</tt> dáme vo formátovacom reťazci za <tt>%d</tt> medzeru. Tá sa bude pokúšať preskočiť všetky biele znaky až po najbližší nebiely; pritom natrafí na koniec súboru a <tt>feof(f)</tt> už bude vracať <tt>true</tt>.
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
 
#include <cassert>
 
#include <cassert>
Riadok 8 487: Riadok 8 488:
 
     // tu pride spracovanie dat v poli a
 
     // tu pride spracovanie dat v poli a
 
}
 
}
</pre>
+
</syntaxhighlight>
 
=Prednáška 16=
 
=Prednáška 16=
 
== Oznamy ==
 
== Oznamy ==
Riadok 8 531: Riadok 8 532:
  
 
Dĺžka načítavaných reťazcov bude vo volaniach funkcie <tt>fscanf</tt> obmedzená na 19 znakov (to teda bude maximálna dĺžka názvu súboru, s ktorou bude program vedieť pracovať).
 
Dĺžka načítavaných reťazcov bude vo volaniach funkcie <tt>fscanf</tt> obmedzená na 19 znakov (to teda bude maximálna dĺžka názvu súboru, s ktorou bude program vedieť pracovať).
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
 
#include <cassert>
 
#include <cassert>
Riadok 8 563: Riadok 8 564:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Čítanie a zapisovanie po znakoch==
 
== Čítanie a zapisovanie po znakoch==
  
 
Knižnica <tt>cstdio</tt> obsahuje funkciu
 
Knižnica <tt>cstdio</tt> obsahuje funkciu
<pre>
+
<syntaxhighlight lang="C++">
 
int getc(FILE *f);
 
int getc(FILE *f);
</pre>
+
</syntaxhighlight>
 
ktorá načíta jeden znak zo súboru, na ktorý odkazuje smerník <tt>f</tt>. V prípade, že načítanie prebehlo úspešne, je výstupom funkcie <tt>getc</tt> kód tohto znaku. V opačnom prípade je výstupom špeciálna konštanta <tt>EOF</tt>, ktorá je vždy rôzna od ľubovoľnej hodnoty typu <tt>char</tt>; to je aj dôvod, prečo funkcia <tt>getc</tt> nevracia hodnotu typu <tt>char</tt>, ale hodnotu typu <tt>int</tt>. Je preto dôležité vyvarovať sa ukladania výstupnej hodnoty funkcie <tt>getc</tt> do premennej typu <tt>char</tt> &ndash; v takom prípade nie je možné rozoznať koniec súboru. Funkcia
 
ktorá načíta jeden znak zo súboru, na ktorý odkazuje smerník <tt>f</tt>. V prípade, že načítanie prebehlo úspešne, je výstupom funkcie <tt>getc</tt> kód tohto znaku. V opačnom prípade je výstupom špeciálna konštanta <tt>EOF</tt>, ktorá je vždy rôzna od ľubovoľnej hodnoty typu <tt>char</tt>; to je aj dôvod, prečo funkcia <tt>getc</tt> nevracia hodnotu typu <tt>char</tt>, ale hodnotu typu <tt>int</tt>. Je preto dôležité vyvarovať sa ukladania výstupnej hodnoty funkcie <tt>getc</tt> do premennej typu <tt>char</tt> &ndash; v takom prípade nie je možné rozoznať koniec súboru. Funkcia
<pre>
+
<syntaxhighlight lang="C++">
 
int getchar(void);
 
int getchar(void);
</pre>
+
</syntaxhighlight>
 
je skratkou pre <tt>getc</tt> s parametrom <tt>stdin</tt>; načíta tak jeden znak z konzoly (rovnako ako napríklad pri <tt>scanf</tt> sa však vstup začne spracovávať až potom, ako používateľ stlačí <tt>Enter</tt> &ndash; nie je takto možné reagovať priamo na stlačenie nejakej klávesy).  
 
je skratkou pre <tt>getc</tt> s parametrom <tt>stdin</tt>; načíta tak jeden znak z konzoly (rovnako ako napríklad pri <tt>scanf</tt> sa však vstup začne spracovávať až potom, ako používateľ stlačí <tt>Enter</tt> &ndash; nie je takto možné reagovať priamo na stlačenie nejakej klávesy).  
  
 
Výstupným náprotivkom k funkcii <tt>getc</tt> je funkcia
 
Výstupným náprotivkom k funkcii <tt>getc</tt> je funkcia
<pre>
+
<syntaxhighlight lang="C++">
 
int putc(int c, FILE *f);
 
int putc(int c, FILE *f);
</pre>
+
</syntaxhighlight>
 
Tá zapíše znak <tt>c</tt> do súboru, na ktorý odkazuje smerník <tt>f</tt>. Funkcia
 
Tá zapíše znak <tt>c</tt> do súboru, na ktorý odkazuje smerník <tt>f</tt>. Funkcia
<pre>
+
<syntaxhighlight lang="C++">
 
int putchar(int c);
 
int putchar(int c);
</pre>
+
</syntaxhighlight>
 
je skratkou pre <tt>putc</tt> s parametrami <tt>c</tt> a <tt>stdout</tt>; vypíše teda daný znak na konzolu.
 
je skratkou pre <tt>putc</tt> s parametrami <tt>c</tt> a <tt>stdout</tt>; vypíše teda daný znak na konzolu.
  
Riadok 8 591: Riadok 8 592:
 
Nasledujúci program skopíruje obsah súboru <tt>original.txt</tt> do súboru <tt>kopia.txt</tt>.
 
Nasledujúci program skopíruje obsah súboru <tt>original.txt</tt> do súboru <tt>kopia.txt</tt>.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
  
Riadok 8 609: Riadok 8 610:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Načítavanie pritom možno realizovať aj priamo v podmienke cyklu <tt>while</tt> &ndash; výstupom priradenia <tt>c = getc(fr)</tt> je nová hodnota premennej <tt>c</tt>, ktorú možno hneď porovnať s <tt>EOF</tt>. Kľúčová časť horeuvedeného programu sa tak dá kratšie, hoci menej čitateľne, prepísať takto:
 
Načítavanie pritom možno realizovať aj priamo v podmienke cyklu <tt>while</tt> &ndash; výstupom priradenia <tt>c = getc(fr)</tt> je nová hodnota premennej <tt>c</tt>, ktorú možno hneď porovnať s <tt>EOF</tt>. Kľúčová časť horeuvedeného programu sa tak dá kratšie, hoci menej čitateľne, prepísať takto:
<pre>
+
<syntaxhighlight lang="C++">
 
     int c;
 
     int c;
 
     while ((c = getc(fr)) != EOF) {
 
     while ((c = getc(fr)) != EOF) {
 
         putc(c, fw);
 
         putc(c, fw);
 
     }
 
     }
</pre>
+
</syntaxhighlight>
  
 
=== Konce riadkov ===
 
=== Konce riadkov ===
Riadok 8 624: Riadok 8 625:
  
 
''Cvičenie:'' čo robí nasledujúci program?
 
''Cvičenie:'' čo robí nasledujúci program?
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
  
Riadok 8 640: Riadok 8 641:
 
     return 0;  
 
     return 0;  
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Funkcia <tt>ungetc</tt> ===
 
=== Funkcia <tt>ungetc</tt> ===
Riadok 8 646: Riadok 8 647:
 
Signálom na ukončenie načítavania znakov často býva až &bdquo;náraz&rdquo; na znak, ktorý už nie je žiadúce prečítať. V takom prípade je užitočné &bdquo;posunúť sa v načítavaní o jeden krok nazad&rdquo;. Túto úlohu realizuje funkcia
 
Signálom na ukončenie načítavania znakov často býva až &bdquo;náraz&rdquo; na znak, ktorý už nie je žiadúce prečítať. V takom prípade je užitočné &bdquo;posunúť sa v načítavaní o jeden krok nazad&rdquo;. Túto úlohu realizuje funkcia
  
<pre>
+
<syntaxhighlight lang="C++">
 
int ungetc(int c, FILE * f);
 
int ungetc(int c, FILE * f);
</pre>
+
</syntaxhighlight>
  
 
Ak bol posledným načítaným znakom (zo súboru, na ktorý odkazuje smerník <tt>f</tt>) znak <tt>c</tt>, volanie <tt>ungetc(c,f)</tt> má skutočne efekt &bdquo;kroku nazad&rdquo;. Ako parameter funkcie <tt>ungetc</tt> však možno okrem naposledy prečítaného znaku použiť aj ľubovoľný iný znak <tt>c</tt> &ndash; funkcia <tt>ungetc</tt> potom tento znak &bdquo;virtuálne&rdquo; pridá na začiatok neprečítanej časti súboru. Súbor sa teda reálne nemení, ale pri nasledujúcom čítaní z neho sa ako prvý prečíta znak <tt>c</tt>. V prípade úspechu sa po volaní <tt>ungetc(c,f)</tt> vráti hodnota <tt>c</tt>; v prípade neúspechu je výstupom konštanta <tt>EOF</tt>.  
 
Ak bol posledným načítaným znakom (zo súboru, na ktorý odkazuje smerník <tt>f</tt>) znak <tt>c</tt>, volanie <tt>ungetc(c,f)</tt> má skutočne efekt &bdquo;kroku nazad&rdquo;. Ako parameter funkcie <tt>ungetc</tt> však možno okrem naposledy prečítaného znaku použiť aj ľubovoľný iný znak <tt>c</tt> &ndash; funkcia <tt>ungetc</tt> potom tento znak &bdquo;virtuálne&rdquo; pridá na začiatok neprečítanej časti súboru. Súbor sa teda reálne nemení, ale pri nasledujúcom čítaní z neho sa ako prvý prečíta znak <tt>c</tt>. V prípade úspechu sa po volaní <tt>ungetc(c,f)</tt> vráti hodnota <tt>c</tt>; v prípade neúspechu je výstupom konštanta <tt>EOF</tt>.  
Riadok 8 655: Riadok 8 656:
  
 
''Príklad č. 1:'' Nasledujúci kus programu skonvertuje reťazec pozostávajúci zo znakov <tt>'0'</tt> až <tt>'9'</tt> na zodpovedajúcu číselnú hodnotu. Keď narazí na prvý znak, ktorý nie je cifra, vráti ho, aby sa dal použiť pri ďalšom spracovávaní.
 
''Príklad č. 1:'' Nasledujúci kus programu skonvertuje reťazec pozostávajúci zo znakov <tt>'0'</tt> až <tt>'9'</tt> na zodpovedajúcu číselnú hodnotu. Keď narazí na prvý znak, ktorý nie je cifra, vráti ho, aby sa dal použiť pri ďalšom spracovávaní.
<pre>
+
<syntaxhighlight lang="C++">
 
     int hodnota = 0;
 
     int hodnota = 0;
 
     int c = getchar();
 
     int c = getchar();
Riadok 8 663: Riadok 8 664:
 
     }
 
     }
 
     ungetc(c, stdin);
 
     ungetc(c, stdin);
</pre>
+
</syntaxhighlight>
  
 
''Príklad č. 2:'' Nasledujúci program prečíta číslo pomocou funkcie <tt>fscanf</tt>, predtým však musí prečítať neznámy počet znakov <tt>'$'</tt>.
 
''Príklad č. 2:'' Nasledujúci program prečíta číslo pomocou funkcie <tt>fscanf</tt>, predtým však musí prečítať neznámy počet znakov <tt>'$'</tt>.
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
  
Riadok 8 685: Riadok 8 686:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Čítanie a zapisovanie po riadkoch ==
 
== Čítanie a zapisovanie po riadkoch ==
  
 
V knižnici <tt>cstdio</tt> je definovaná funkcia
 
V knižnici <tt>cstdio</tt> je definovaná funkcia
<pre>
+
<syntaxhighlight lang="C++">
 
char *fgets(char *str, int n, FILE * f);
 
char *fgets(char *str, int n, FILE * f);
</pre>
+
</syntaxhighlight>
 
pomocou ktorej možno načítať zo súboru, na ktorý ukazuje smerník <tt>f</tt>, práve jeden riadok (alebo nejakú jeho časť, ak je tento riadok príliš dlhý). Vstupnými argumentmi funkcie <tt>fgets</tt> sú:
 
pomocou ktorej možno načítať zo súboru, na ktorý ukazuje smerník <tt>f</tt>, práve jeden riadok (alebo nejakú jeho časť, ak je tento riadok príliš dlhý). Vstupnými argumentmi funkcie <tt>fgets</tt> sú:
 
* Pole znakov <tt>str</tt>, do ktorého sa v prípade úspechu riadok načíta.
 
* Pole znakov <tt>str</tt>, do ktorého sa v prípade úspechu riadok načíta.
Riadok 8 701: Riadok 8 702:
  
 
''Príklad:'' nasledujúci program spočíta počet riadkov v súbore <tt>vstup.txt</tt> (za predpokladu, že žiaden z týchto riadkov nie je dlhší ako 100 znakov vrátane znaku <tt>\n</tt> na konci riadku):
 
''Príklad:'' nasledujúci program spočíta počet riadkov v súbore <tt>vstup.txt</tt> (za predpokladu, že žiaden z týchto riadkov nie je dlhší ako 100 znakov vrátane znaku <tt>\n</tt> na konci riadku):
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
  
Riadok 8 720: Riadok 8 721:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
 
''Cvičenie:'' Zistite, ako sa správa uvedený program, keď posledným znakom v súbore je resp. nie je znak <tt>\n</tt>. Zistite, čo program vypíše na výstup pre súbor, ktorý obsahuje jediný riadok o 200 znakoch.
 
''Cvičenie:'' Zistite, ako sa správa uvedený program, keď posledným znakom v súbore je resp. nie je znak <tt>\n</tt>. Zistite, čo program vypíše na výstup pre súbor, ktorý obsahuje jediný riadok o 200 znakoch.
  
 
Výstupným náprotivkom funkcie <tt>fgets</tt> je funkcia
 
Výstupným náprotivkom funkcie <tt>fgets</tt> je funkcia
<pre>
+
<syntaxhighlight lang="C++">
 
int fputs(const char *str, FILE *f);
 
int fputs(const char *str, FILE *f);
</pre>
+
</syntaxhighlight>
 
ktorá do súboru, na ktorý ukazuje smerník <tt>f</tt>, vypíše reťazec <tt>str</tt>. Vypisovaný reťazec <tt>str</tt> pritom môže obsahovať ľubovoľný  (aj nulový) počet výskytov symbolu <tt>\n</tt> pre koniec riadku. V prípade úspechu vráti funkcia <tt>fputs</tt> nezáporné celé číslo; v prípade neúspechu vráti konštantu <tt>EOF</tt>.  
 
ktorá do súboru, na ktorý ukazuje smerník <tt>f</tt>, vypíše reťazec <tt>str</tt>. Vypisovaný reťazec <tt>str</tt> pritom môže obsahovať ľubovoľný  (aj nulový) počet výskytov symbolu <tt>\n</tt> pre koniec riadku. V prípade úspechu vráti funkcia <tt>fputs</tt> nezáporné celé číslo; v prípade neúspechu vráti konštantu <tt>EOF</tt>.  
  
Riadok 8 742: Riadok 8 743:
 
* Druhý program číta súbor po znakoch, pričom si udržiava premennú <tt>pocet</tt> uchovávajúcu informáciu o tom, koľko písmen sa už v momentálne spracovávanom riadku načítalo.
 
* Druhý program číta súbor po znakoch, pričom si udržiava premennú <tt>pocet</tt> uchovávajúcu informáciu o tom, koľko písmen sa už v momentálne spracovávanom riadku načítalo.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
 
#include <cstring>
 
#include <cstring>
Riadok 8 762: Riadok 8 763:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
  
Riadok 8 789: Riadok 8 790:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
''Príklad č. 2:'': nasledujúci program spracúva vstupný súbor obsahujúci čísla oddelené bielymi znakmi (medzery, tabulátory, konce riadkov,...), pričom medzi dvoma číslami môže byť aj viac ako jeden oddeľovač. Pre každý riadok program vypíše súčet čísel, ktoré sa v tomto riadku vyskytujú (predpokladá pritom, že každý &ndash; t. j. aj posledný &ndash; riadok vstupného súboru je ukončený symbolom <tt>\n</tt>).
 
''Príklad č. 2:'': nasledujúci program spracúva vstupný súbor obsahujúci čísla oddelené bielymi znakmi (medzery, tabulátory, konce riadkov,...), pričom medzi dvoma číslami môže byť aj viac ako jeden oddeľovač. Pre každý riadok program vypíše súčet čísel, ktoré sa v tomto riadku vyskytujú (predpokladá pritom, že každý &ndash; t. j. aj posledný &ndash; riadok vstupného súboru je ukončený symbolom <tt>\n</tt>).
Riadok 8 798: Riadok 8 799:
 
* Na zistenie, či je prečítaný znak biely, využíva nasledujúci program funkciu <tt>isspace</tt> z knižnice <tt>cctype</tt>.
 
* Na zistenie, či je prečítaný znak biely, využíva nasledujúci program funkciu <tt>isspace</tt> z knižnice <tt>cctype</tt>.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
 
#include <cctype>
 
#include <cctype>
Riadok 8 825: Riadok 8 826:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
 
''Cvičenie:'' upravte program tak, aby pracoval správne aj v prípade, že posledný riadok nie je ukončený symbolom <tt>\n</tt>.
 
''Cvičenie:'' upravte program tak, aby pracoval správne aj v prípade, že posledný riadok nie je ukončený symbolom <tt>\n</tt>.
  
Riadok 8 838: Riadok 8 839:
  
 
''Zašifrovanie súboru'' realizuje nasledujúci program:
 
''Zašifrovanie súboru'' realizuje nasledujúci program:
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
 
#include <cassert>
 
#include <cassert>
Riadok 8 875: Riadok 8 876:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
''Dešifrovanie súboru'' zašifrovaného Cézarovou šifrou realizuje tento program:
 
''Dešifrovanie súboru'' zašifrovaného Cézarovou šifrou realizuje tento program:
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
 
#include <cassert>
 
#include <cassert>
Riadok 8 916: Riadok 8 917:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Vigenèrova šifra ===
 
=== Vigenèrova šifra ===
Riadok 8 926: Riadok 8 927:
 
''Zašifrovanie súboru'' realizuje nasledujúci program:
 
''Zašifrovanie súboru'' realizuje nasledujúci program:
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
  
Riadok 8 969: Riadok 8 970:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
''Dešifrovanie súboru'' zašifrovaného Vigenèrovou šifrou realizuje tento program:
 
''Dešifrovanie súboru'' zašifrovaného Vigenèrovou šifrou realizuje tento program:
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
 
#include <cassert>
 
#include <cassert>
Riadok 9 017: Riadok 9 018:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Binárne súbory ==
 
== Binárne súbory ==
Riadok 9 037: Riadok 9 038:
  
 
Funkcia
 
Funkcia
<pre>
+
<syntaxhighlight lang="C++">
 
size_t fwrite(const void *p, size_t size, size_t count, FILE *f);
 
size_t fwrite(const void *p, size_t size, size_t count, FILE *f);
</pre>  
+
</syntaxhighlight>  
 
zapíše do súboru, na ktorý ukazuje smerník <tt>f</tt>, presne <tt>count</tt> pamäťových úsekov veľkosti <tt>size</tt> bytov, nachádzajúcich sa v pamäti od adresy <tt>p</tt>. Výstupom je potom počet reálne zapísaných úsekov. V hlavičke funkcie <tt>fwrite</tt> sa vyskytujú dva nové prvky:
 
zapíše do súboru, na ktorý ukazuje smerník <tt>f</tt>, presne <tt>count</tt> pamäťových úsekov veľkosti <tt>size</tt> bytov, nachádzajúcich sa v pamäti od adresy <tt>p</tt>. Výstupom je potom počet reálne zapísaných úsekov. V hlavičke funkcie <tt>fwrite</tt> sa vyskytujú dva nové prvky:
 
* Typ <tt>size_t</tt> je celočíselný typ, ktorý sa používa na meranie veľkosti pamäte (každý index poľa je napríklad určite tohto typu). Je definovaný vo viacerých knižniciach, napríklad aj v <tt>cstdio</tt> a je prakticky ekvivalentný nezáporným celým číslam.
 
* Typ <tt>size_t</tt> je celočíselný typ, ktorý sa používa na meranie veľkosti pamäte (každý index poľa je napríklad určite tohto typu). Je definovaný vo viacerých knižniciach, napríklad aj v <tt>cstdio</tt> a je prakticky ekvivalentný nezáporným celým číslam.
Riadok 9 048: Riadok 9 049:
 
''Príklad:'' nasledujúci program vytvorí pole pozostávajúce z druhých odmocnín čísel <tt>0</tt> až <tt>99</tt> a toto pole uloží do binárneho súboru <tt>binarny.dat</tt>. Využíva pritom operátor <tt>sizeof</tt>, ktorý vracia veľkosť reprezentácie daného typu v bytoch.
 
''Príklad:'' nasledujúci program vytvorí pole pozostávajúce z druhých odmocnín čísel <tt>0</tt> až <tt>99</tt> a toto pole uloží do binárneho súboru <tt>binarny.dat</tt>. Využíva pritom operátor <tt>sizeof</tt>, ktorý vracia veľkosť reprezentácie daného typu v bytoch.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
 
#include <cmath>
 
#include <cmath>
Riadok 9 066: Riadok 9 067:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Funkcia <tt>fread</tt> ===
 
=== Funkcia <tt>fread</tt> ===
 
Funkcia
 
Funkcia
<pre>
+
<syntaxhighlight lang="C++">
 
size_t fread(void *p, size_t size, size_t count, FILE * f);
 
size_t fread(void *p, size_t size, size_t count, FILE * f);
</pre>
+
</syntaxhighlight>
 
zo súboru, na ktorý ukazuje smerník <tt>f</tt>, prečíta <tt>count</tt> položiek veľkosti <tt>size</tt> a uloží ich do pamäte od adresy <tt>p</tt>.
 
zo súboru, na ktorý ukazuje smerník <tt>f</tt>, prečíta <tt>count</tt> položiek veľkosti <tt>size</tt> a uloží ich do pamäte od adresy <tt>p</tt>.
  
 
Ak by sme napríklad chceli overiť, že piatym reálnym číslom uloženým v súbore vytvorenom pomocou programu z predchádzajúceho príkladu je skutočne číslo 2, môžeme napríklad do poľa <tt>a</tt> uložiť prvých 5 položiek zo súboru <tt>binarny.dat</tt> a skontrolovať prvok <tt>a[4]</tt>:
 
Ak by sme napríklad chceli overiť, že piatym reálnym číslom uloženým v súbore vytvorenom pomocou programu z predchádzajúceho príkladu je skutočne číslo 2, môžeme napríklad do poľa <tt>a</tt> uložiť prvých 5 položiek zo súboru <tt>binarny.dat</tt> a skontrolovať prvok <tt>a[4]</tt>:
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
  
Riadok 9 091: Riadok 9 092:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Nasledujúce alternatívne riešenie je o niečo pamäťovo efektívnejšie:
 
Nasledujúce alternatívne riešenie je o niečo pamäťovo efektívnejšie:
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
  
Riadok 9 112: Riadok 9 113:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Práca so súbormi prostredníctvom knižnice <tt>fstream</tt> ==
 
== Práca so súbormi prostredníctvom knižnice <tt>fstream</tt> ==
Riadok 9 120: Riadok 9 121:
 
Základy tohto prístupu demonštrujeme na príklade programu, ktorý zo súboru <tt>vstup.txt</tt> prečíta prirodzené číslo <tt>N</tt> a následne <tt>N</tt> ďalších čísel. Tieto čísla potom v obrátenom poradí a oddelené medzerami vypíše do súboru <tt>vystup.txt</tt>.
 
Základy tohto prístupu demonštrujeme na príklade programu, ktorý zo súboru <tt>vstup.txt</tt> prečíta prirodzené číslo <tt>N</tt> a následne <tt>N</tt> ďalších čísel. Tieto čísla potom v obrátenom poradí a oddelené medzerami vypíše do súboru <tt>vystup.txt</tt>.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <fstream>
 
#include <fstream>
 
using namespace std;
 
using namespace std;
Riadok 9 148: Riadok 9 149:
 
     return 0;  
 
     return 0;  
 
}
 
}
</pre>
+
</syntaxhighlight>
 
=Prednáška 17=
 
=Prednáška 17=
 
==Oznamy==
 
==Oznamy==
Riadok 9 177: Riadok 9 178:
  
 
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:
 
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:
<pre>
+
<syntaxhighlight lang="C++">
 
typedef int dataType;
 
typedef int dataType;
</pre>
+
</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.
  
 
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):  
 
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):  
<pre>
+
<syntaxhighlight lang="C++">
 
/* Inicializuje prazdny rad */
 
/* Inicializuje prazdny rad */
 
void init(queue &q);
 
void init(queue &q);
Riadok 9 201: Riadok 9 202:
 
/* Uvolni pamat */
 
/* Uvolni pamat */
 
void destroy(queue &q);
 
void destroy(queue &q);
</pre>
+
</syntaxhighlight>
  
 
===Zásobník===
 
===Zásobník===
Riadok 9 208: Riadok 9 209:
  
 
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 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>):
<pre>
+
<syntaxhighlight lang="C++">
 
/* Inicializuje prazdny zasobnik */
 
/* Inicializuje prazdny zasobnik */
 
void init(stack &s);
 
void init(stack &s);
Riadok 9 223: Riadok 9 224:
 
/* Uvolni pamat */
 
/* Uvolni pamat */
 
void destroy(stack &s);
 
void destroy(stack &s);
</pre>
+
</syntaxhighlight>
  
 
===Programy využívajúce rad a zásobník===
 
===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:
 
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:
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 9 250: Riadok 9 251:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Podobne nasledujúci program pracuje so zásobníkom:
 
Podobne nasledujúci program pracuje so zásobníkom:
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 9 275: Riadok 9 276:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
==Implementácia zásobníka a radu==
 
==Implementácia zásobníka a radu==
Riadok 9 283: Riadok 9 284:
 
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>.
 
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>.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cassert>
 
#include <cassert>
  
Riadok 9 330: Riadok 9 331:
 
     delete[] s.items;
 
     delete[] s.items;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
===Rad pomocou poľa===
 
===Rad pomocou poľa===
Riadok 9 336: Riadok 9 337:
 
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 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.   
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cassert>
 
#include <cassert>
  
Riadok 9 388: Riadok 9 389:
 
     delete[] q.items;
 
     delete[] q.items;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
===Zásobník pomocou spájaného zoznamu===
 
===Zásobník pomocou spájaného zoznamu===
Riadok 9 396: Riadok 9 397:
 
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 už 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 bude spočívať predovšetkým v tom, že maximálny počet prvkov v zásobníku už 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.
 
   
 
   
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cassert>
 
#include <cassert>
  
Riadok 9 450: Riadok 9 451:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
===Rad pomocou spájaného zoznamu===
 
===Rad pomocou spájaného zoznamu===
Riadok 9 458: Riadok 9 459:
 
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.
 
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.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cassert>
 
#include <cassert>
  
Riadok 9 525: Riadok 9 526:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Použitie zásobníka a radu ==
 
== Použitie zásobníka a radu ==
Riadok 9 570: Riadok 9 571:
 
Túto úlohu rieši nasledujúci program, ktorý 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.
 
Túto úlohu rieši nasledujúci program, ktorý 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.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
#include <cassert>
 
#include <cassert>
Riadok 9 626: Riadok 9 627:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</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.
 
''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.
Riadok 9 634: Riadok 9 635:
 
Pripomeňme si triedenie ''Quick Sort'' z [[#Prednáška 11|11. prednášky]]:
 
Pripomeňme si triedenie ''Quick Sort'' z [[#Prednáška 11|11. prednášky]]:
  
<pre>
+
<syntaxhighlight lang="C++">
 
void swap (int &x, int &y) {
 
void swap (int &x, int &y) {
 
     int tmp = x;
 
     int tmp = x;
Riadok 9 671: Riadok 9 672:
 
   // ...
 
   // ...
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Namiesto rekurzie môžeme použiť aj zásobník úsekov, ktoré ešte treba dotriediť.
 
Namiesto rekurzie môžeme použiť aj zásobník úsekov, ktoré ešte treba dotriediť.
  
<pre>
+
<syntaxhighlight lang="C++">
 
struct usek {
 
struct usek {
 
     int low;
 
     int low;
Riadok 9 722: Riadok 9 723:
 
   // ...
 
   // ...
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Tento program triedi úseky v rovnakom poradí, ako rekurzívny Quicksort, lebo po rozdelení poľa na dve časti dá na vrch zásobníka úsek zodpovedajúci jeho ľavej časti. Až keď sa táto ľavá časť a všetky podúlohy, ktoré z nej vzniknú, spracuje, dôjde na spracovanie pravej časti poľa. Pri triedení ''Quick Sort'' však na tomto poradí nezáleží, takže by sme mohli jednotlivé úseky vkladať na zásobník aj v opačnom poradí.
 
Tento program triedi úseky v rovnakom poradí, ako rekurzívny Quicksort, lebo po rozdelení poľa na dve časti dá na vrch zásobníka úsek zodpovedajúci jeho ľavej časti. Až keď sa táto ľavá časť a všetky podúlohy, ktoré z nej vzniknú, spracuje, dôjde na spracovanie pravej časti poľa. Pri triedení ''Quick Sort'' však na tomto poradí nezáleží, takže by sme mohli jednotlivé úseky vkladať na zásobník aj v opačnom poradí.
Riadok 9 744: Riadok 9 745:
  
 
Uvažujme obrazec daný obdĺžnikovou maticou o <tt>m</tt> riadkoch a <tt>n</tt> stĺpcoch. Obdĺžnikové plátno je v takom prípade rozdelené na <tt>m</tt> krát <tt>n</tt> &bdquo;štvorčekov&rdquo; určitej konštantnej veľkosti, pričom jednotlivé prvky matice zodpovedajú farbám jednotlivých týchto štvorčekov. V našom jednoduchom príklade budeme pracovať iba s piatimi farbami, ktoré budeme reprezentovať číslami <tt>0,..,4</tt> podľa nasledujúceho poľa (napríklad číslo <tt>0</tt> teda reprezentuje bielu farbu):
 
Uvažujme obrazec daný obdĺžnikovou maticou o <tt>m</tt> riadkoch a <tt>n</tt> stĺpcoch. Obdĺžnikové plátno je v takom prípade rozdelené na <tt>m</tt> krát <tt>n</tt> &bdquo;štvorčekov&rdquo; určitej konštantnej veľkosti, pričom jednotlivé prvky matice zodpovedajú farbám jednotlivých týchto štvorčekov. V našom jednoduchom príklade budeme pracovať iba s piatimi farbami, ktoré budeme reprezentovať číslami <tt>0,..,4</tt> podľa nasledujúceho poľa (napríklad číslo <tt>0</tt> teda reprezentuje bielu farbu):
<pre>
+
<syntaxhighlight lang="C++">
 
const char *farby[5] = {"white", "blue", "black", "yellow", "red"};  
 
const char *farby[5] = {"white", "blue", "black", "yellow", "red"};  
</pre>
+
</syntaxhighlight>
  
 
Napríklad obrazec
 
Napríklad obrazec
  
[[#Súbor:Matica1.png|400px]]
+
[[Súbor:Matica1.png|400px]]
  
 
tak môže byť reprezentovaný nasledujúcim textovým súborom obsahujúcim najprv rozmery matice (čísla <tt>m</tt> a <tt>n</tt>) a za nimi samotné prvky matice:
 
tak môže byť reprezentovaný nasledujúcim textovým súborom obsahujúcim najprv rozmery matice (čísla <tt>m</tt> a <tt>n</tt>) a za nimi samotné prvky matice:
Riadok 9 770: Riadok 9 771:
 
Zameriame sa teraz na nasledujúci problém: používateľ zvolí (zadá na konzolu) súradnice niektorého &bdquo;štvorčeka&rdquo; a cieľom je ofarbiť nejakou farbou (napríklad červenou) celú súvislú oblasť rovnakej farby obsahujúcu daný štvorček. Napríklad pre obrazec vyššie a vstupné súradnice <tt>(2,1)</tt> &ndash; to znamená pre &bdquo;štvorček&rdquo; v treťom riadku a druhom stĺpci, keďže matica sa bude indexovať od nuly &ndash; by mal byť výstupom nasledujúci obrazec:   
 
Zameriame sa teraz na nasledujúci problém: používateľ zvolí (zadá na konzolu) súradnice niektorého &bdquo;štvorčeka&rdquo; a cieľom je ofarbiť nejakou farbou (napríklad červenou) celú súvislú oblasť rovnakej farby obsahujúcu daný štvorček. Napríklad pre obrazec vyššie a vstupné súradnice <tt>(2,1)</tt> &ndash; to znamená pre &bdquo;štvorček&rdquo; v treťom riadku a druhom stĺpci, keďže matica sa bude indexovať od nuly &ndash; by mal byť výstupom nasledujúci obrazec:   
  
[[#Súbor:Matica2.png|400px]]
+
[[Súbor:Matica2.png|400px]]
  
 
Podobný problém je napríklad často potrebné riešiť v rôznych nástrojoch na prácu s grafikou (kde sa namiesto &bdquo;štvorčekov&rdquo; ofarbujú pixely) a podobne.
 
Podobný problém je napríklad často potrebné riešiť v rôznych nástrojoch na prácu s grafikou (kde sa namiesto &bdquo;štvorčekov&rdquo; ofarbujú pixely) a podobne.
Riadok 9 778: Riadok 9 779:
 
Funkciu na vyfarbovanie súvislých oblastí budeme dorábať do nasledujúcej kostry programu, ktorá obsahuje funkcie na inicializáciu matice, jej načítanie zo súboru, vykresľovanie jednotlivých štvorčekov a celej matice, ako aj uvoľnenie pamäte. Všetky tieto funkcie pracujú podobne ako pri príklade s výškovou mapou z [[#Prednáška 13|prednášky č. 13]]. Nasledujúca kostra tiež obsahuje funkciu <tt>main</tt>, ktorá načíta maticu zo súboru <tt>vstup.txt</tt> a následne zatiaľ len vykreslí ňou reprezentovaný obrazec do súboru <tt>matica.svg</tt>.
 
Funkciu na vyfarbovanie súvislých oblastí budeme dorábať do nasledujúcej kostry programu, ktorá obsahuje funkcie na inicializáciu matice, jej načítanie zo súboru, vykresľovanie jednotlivých štvorčekov a celej matice, ako aj uvoľnenie pamäte. Všetky tieto funkcie pracujú podobne ako pri príklade s výškovou mapou z [[#Prednáška 13|prednášky č. 13]]. Nasledujúca kostra tiež obsahuje funkciu <tt>main</tt>, ktorá načíta maticu zo súboru <tt>vstup.txt</tt> a následne zatiaľ len vykreslí ňou reprezentovaný obrazec do súboru <tt>matica.svg</tt>.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include "SVGdraw.h"
 
#include "SVGdraw.h"
 
#include <cstdio>
 
#include <cstdio>
Riadok 9 851: Riadok 9 852:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
===Rekurzívne vyfarbovanie===
 
===Rekurzívne vyfarbovanie===
Riadok 9 859: Riadok 9 860:
 
Za každým vyfarbením &bdquo;štvorčeka&rdquo; navyše voláme funkciu <tt>drawing.wait</tt> s parametrom <tt>pauza</tt>, čo je konštanta, ktorú na úvod nastavíme na 0,3 sekundy. Výsledný SVG súbor tak bude obsahovať animáciu postupného vyfarbovania jednotlivých políčok. Farbou &bdquo;rámika&rdquo; okolo políčka budeme navyše rozlišovať, či už bolo dané políčko úplne spracované (t. j. či sa už ukončilo rekurzívne volanie funkcie <tt>vyfarbi</tt> pre toto políčko).
 
Za každým vyfarbením &bdquo;štvorčeka&rdquo; navyše voláme funkciu <tt>drawing.wait</tt> s parametrom <tt>pauza</tt>, čo je konštanta, ktorú na úvod nastavíme na 0,3 sekundy. Výsledný SVG súbor tak bude obsahovať animáciu postupného vyfarbovania jednotlivých políčok. Farbou &bdquo;rámika&rdquo; okolo políčka budeme navyše rozlišovať, či už bolo dané políčko úplne spracované (t. j. či sa už ukončilo rekurzívne volanie funkcie <tt>vyfarbi</tt> pre toto políčko).
  
<pre>
+
<syntaxhighlight lang="C++">
 
const double pauza = 0.3;  // pauza po kazdom kroku vysvetlovania v sekundach
 
const double pauza = 0.3;  // pauza po kazdom kroku vysvetlovania v sekundach
  
Riadok 9 886: Riadok 9 887:
 
     drawing.wait(pauza);
 
     drawing.wait(pauza);
 
}  
 
}  
</pre>
+
</syntaxhighlight>
  
 
Funkcia <tt>main</tt> potom môže vyzerať napríklad nasledovne:
 
Funkcia <tt>main</tt> potom môže vyzerať napríklad nasledovne:
<pre>
+
<syntaxhighlight lang="C++">
 
int main() {
 
int main() {
 
     FILE *fr = fopen("vstup.txt", "r");
 
     FILE *fr = fopen("vstup.txt", "r");
Riadok 9 914: Riadok 9 915:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
* [[:Image:Animacia1.svg|Výsledná animácia]]
 
* [[:Image:Animacia1.svg|Výsledná animácia]]
Riadok 9 947: Riadok 9 948:
  
 
Do programu z vyššia teda dorobíme funkciu  
 
Do programu z vyššia teda dorobíme funkciu  
<pre>
+
<syntaxhighlight lang="C++">
 
int najdiOstrovy(int m, int n, int **a, SVGdraw &drawing) {
 
int najdiOstrovy(int m, int n, int **a, SVGdraw &drawing) {
 
     int ostrovov = 0;
 
     int ostrovov = 0;
Riadok 9 960: Riadok 9 961:
 
     return ostrovov;
 
     return ostrovov;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
a funkciu <tt>main</tt> môžeme zmeniť napríklad takto:
 
a funkciu <tt>main</tt> môžeme zmeniť napríklad takto:
<pre>
+
<syntaxhighlight lang="C++">
 
int main() {
 
int main() {
 
     FILE *fr = fopen("ostrovy.txt", "r");
 
     FILE *fr = fopen("ostrovy.txt", "r");
Riadok 9 985: Riadok 9 986:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
* [[:Image:Animacia2.svg|Výsledná animácia]]
 
* [[:Image:Animacia2.svg|Výsledná animácia]]
Riadok 9 996: Riadok 9 997:
  
 
Drobnou zmenou bude, že súradnice jednotlivých susedov budeme počítať s použitím cyklu <tt>for</tt> a polí <tt>deltaStlpec</tt> a <tt>deltaRiadok</tt>, ktoré pre <tt>i = 0,1,2,3</tt> obsahujú ''posuny'' jednotlivých súradníc <tt>i</tt>-teho suseda oproti práve spracúvanému políčku.
 
Drobnou zmenou bude, že súradnice jednotlivých susedov budeme počítať s použitím cyklu <tt>for</tt> a polí <tt>deltaStlpec</tt> a <tt>deltaRiadok</tt>, ktoré pre <tt>i = 0,1,2,3</tt> obsahujú ''posuny'' jednotlivých súradníc <tt>i</tt>-teho suseda oproti práve spracúvanému políčku.
<pre>
+
<syntaxhighlight lang="C++">
 
struct policko {
 
struct policko {
 
     int riadok, stlpec;
 
     int riadok, stlpec;
Riadok 10 045: Riadok 10 046:
 
     destroy(s);
 
     destroy(s);
 
}  
 
}  
</pre>
+
</syntaxhighlight>
  
 
* [[:Image:Animacia3.svg|Výsledná animácia]]
 
* [[:Image:Animacia3.svg|Výsledná animácia]]
Riadok 10 052: Riadok 10 053:
  
 
Namiesto zásobníka môžeme použiť aj rad &ndash; obrazec sa potom bude vyfarbovať v poradí podľa vzdialenosti od počiatočného políčka. Pôjde o takzvané ''prehľadávanie do šírky'', kým rekurzívna verzia a verzia so zásobníkom zodpovedajú takzvanému ''prehľadávaniu do hĺbky''.
 
Namiesto zásobníka môžeme použiť aj rad &ndash; obrazec sa potom bude vyfarbovať v poradí podľa vzdialenosti od počiatočného políčka. Pôjde o takzvané ''prehľadávanie do šírky'', kým rekurzívna verzia a verzia so zásobníkom zodpovedajú takzvanému ''prehľadávaniu do hĺbky''.
<pre>
+
<syntaxhighlight lang="C++">
 
struct policko {
 
struct policko {
 
     int riadok, stlpec;
 
     int riadok, stlpec;
Riadok 10 101: Riadok 10 102:
 
     destroy(q);
 
     destroy(q);
 
}  
 
}  
</pre>
+
</syntaxhighlight>
  
 
* [[:Image:Animacia4.svg|Výsledná animácia]]
 
* [[:Image:Animacia4.svg|Výsledná animácia]]
  
 
Program potom môžeme upraviť aj tak, aby do každého ofarbeného políčka vypísal jeho vzdialenosť od počiatočného políčka:
 
Program potom môžeme upraviť aj tak, aby do každého ofarbeného políčka vypísal jeho vzdialenosť od počiatočného políčka:
<pre>
+
<syntaxhighlight lang="C++">
 
struct policko {
 
struct policko {
 
     int riadok, stlpec, vzd;
 
     int riadok, stlpec, vzd;
Riadok 10 166: Riadok 10 167:
 
     destroy(q);
 
     destroy(q);
 
}  
 
}  
</pre>
+
</syntaxhighlight>
  
 
* [[:Image:Animacia5.svg|Výsledná animácia]]
 
* [[:Image:Animacia5.svg|Výsledná animácia]]
Riadok 10 222: Riadok 10 223:
 
* Tento postup opakujeme, až kým neprídeme na koniec výrazu. V tom okamihu by sme mali mať na zásobníku jediný prvok &ndash; výslednú hodnotu výrazu.  
 
* Tento postup opakujeme, až kým neprídeme na koniec výrazu. V tom okamihu by sme mali mať na zásobníku jediný prvok &ndash; výslednú hodnotu výrazu.  
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
 
#include <cctype>
 
#include <cctype>
Riadok 10 277: Riadok 10 278:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Nasledujúca alternatívna implementácia vyhodnocovania postfixových výrazov sa od predchádzajúcej líši tým, že vstupný výraz najprv uloží do reťazca, ktorý následne vyhodnotí prostredníctvom funkcie <tt>evaluatePostfix</tt>. Drobnou nevýhodou tohto prístupu je, že dĺžka vstupného reťazca je obmedzená konštantou; funkcia vyhodnocujúca výraz uložený v reťazci sa nám však nabudúce zíde.
 
Nasledujúca alternatívna implementácia vyhodnocovania postfixových výrazov sa od predchádzajúcej líši tým, že vstupný výraz najprv uloží do reťazca, ktorý následne vyhodnotí prostredníctvom funkcie <tt>evaluatePostfix</tt>. Drobnou nevýhodou tohto prístupu je, že dĺžka vstupného reťazca je obmedzená konštantou; funkcia vyhodnocujúca výraz uložený v reťazci sa nám však nabudúce zíde.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
 
#include <cctype>
 
#include <cctype>
Riadok 10 346: Riadok 10 347:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
 
=Prednáška 19=
 
=Prednáška 19=
 
== Oznamy ==
 
== Oznamy ==
Riadok 10 415: Riadok 10 416:
 
Funkcia <tt>infixToPostfix</tt>, realizujúca prevod z infixovej do postfixovej notácie, tak môže vyzerať napríklad nasledovne:
 
Funkcia <tt>infixToPostfix</tt>, realizujúca prevod z infixovej do postfixovej notácie, tak môže vyzerať napríklad nasledovne:
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
 
#include <cassert>
 
#include <cassert>
Riadok 10 505: Riadok 10 506:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
''Cvičenie'': Rozšírte program vyššie o operáciu umocňovania <tt>^</tt> s prioritou vyššou ako <tt>*</tt> (dajte si pritom pozor na fakt, že umocňovanie je narozdiel od operácií <tt>+, -, *, /</tt> sprava asociatívne).
 
''Cvičenie'': Rozšírte program vyššie o operáciu umocňovania <tt>^</tt> s prioritou vyššou ako <tt>*</tt> (dajte si pritom pozor na fakt, že umocňovanie je narozdiel od operácií <tt>+, -, *, /</tt> sprava asociatívne).
Riadok 10 513: Riadok 10 514:
 
Pri prevode infixového výrazu do postfixového tvaru sme predpokladali, že neobsahuje žiadny výskyt unárneho operátora <tt>-</tt>. V nasledujúcom tento nedostatok napravíme: napíšeme funkciu <tt>normalise</tt>, ktorá v danom infixovom výraze <tt>in</tt> nahradí všetky výskyty unárneho mínusu reťazcom <tt>(0-1)*</tt> a výsledok uloží do (opäť infixového) výrazu <tt>out</tt>. Mínus je pritom v korektnom infixovom výraze unárny práve vtedy, keď (modulo biele znaky, ktoré budeme ignorovať) nenasleduje za číslom, ani za pravou zátvorkou.
 
Pri prevode infixového výrazu do postfixového tvaru sme predpokladali, že neobsahuje žiadny výskyt unárneho operátora <tt>-</tt>. V nasledujúcom tento nedostatok napravíme: napíšeme funkciu <tt>normalise</tt>, ktorá v danom infixovom výraze <tt>in</tt> nahradí všetky výskyty unárneho mínusu reťazcom <tt>(0-1)*</tt> a výsledok uloží do (opäť infixového) výrazu <tt>out</tt>. Mínus je pritom v korektnom infixovom výraze unárny práve vtedy, keď (modulo biele znaky, ktoré budeme ignorovať) nenasleduje za číslom, ani za pravou zátvorkou.
  
<pre>
+
<syntaxhighlight lang="C++">
 
void normalise(const char *in, char *out) {
 
void normalise(const char *in, char *out) {
 
     int j = 0; // index do vystupneho retazca
 
     int j = 0; // index do vystupneho retazca
Riadok 10 537: Riadok 10 538:
 
     out[j] = 0;
 
     out[j] = 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Prevod infixového výrazu s unárnymi mínusmi do postfixového tvaru teda môžeme realizovať tak, že najprv zavoláme funkciu <tt>normalise</tt> a následne funkciu <tt>infixToPostfix</tt>. (O niečo krajším riešením by bolo nahradenie unárnych mínusov nejakým novým špeciálnym znakom &ndash; v takom prípade by sa však musela zmeniť aj funkcia <tt>infixToPostfix</tt>.) Následne môžeme výsledný výraz aj vyhodnotiť pomocou funkcie <tt>evaluatePostfix</tt> z [[#Prednáška 18|minulej prednášky]]:  
 
Prevod infixového výrazu s unárnymi mínusmi do postfixového tvaru teda môžeme realizovať tak, že najprv zavoláme funkciu <tt>normalise</tt> a následne funkciu <tt>infixToPostfix</tt>. (O niečo krajším riešením by bolo nahradenie unárnych mínusov nejakým novým špeciálnym znakom &ndash; v takom prípade by sa však musela zmeniť aj funkcia <tt>infixToPostfix</tt>.) Následne môžeme výsledný výraz aj vyhodnotiť pomocou funkcie <tt>evaluatePostfix</tt> z [[#Prednáška 18|minulej prednášky]]:  
  
<pre>
+
<syntaxhighlight lang="C++">
 
const int maxN = 100;
 
const int maxN = 100;
  
Riadok 10 561: Riadok 10 562:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Pri implementácii uvedeného programu sa však treba mať na pozore: kým totiž funkcia <tt>evaluatePostfix</tt> využíva zásobník prvkov typu <tt>double</tt>, funkcia <tt>infixToPostfix</tt> využíva zásobník prvkov typu <tt>char</tt>. Pri pokuse o skopírovanie kódu pre zásobník do zdrojového súboru tohto programu tak stojíme pred voľbou medzi
 
Pri implementácii uvedeného programu sa však treba mať na pozore: kým totiž funkcia <tt>evaluatePostfix</tt> využíva zásobník prvkov typu <tt>double</tt>, funkcia <tt>infixToPostfix</tt> využíva zásobník prvkov typu <tt>char</tt>. Pri pokuse o skopírovanie kódu pre zásobník do zdrojového súboru tohto programu tak stojíme pred voľbou medzi
<pre>
+
<syntaxhighlight lang="C++">
 
typedef char dataType;
 
typedef char dataType;
</pre>
+
</syntaxhighlight>
 
a
 
a
<pre>
+
<syntaxhighlight lang="C++">
 
typedef double dataType;
 
typedef double dataType;
</pre>
+
</syntaxhighlight>
 
V tomto prípade ešte môžeme zvoliť druhú možnosť a za <tt>dataType</tt> považovať <tt>double</tt>; o zvyšok sa postará automatické pretypovanie znakov (čiže špeciálnych celých čísel) na reálne čísla a naopak. Toto riešenie má však ďaleko od ideálneho a pre iné dvojice typov nemusí riešenie tohto typu existovať vôbec. (Ozaj elegantný prístup k tomuto problému vyžaduje pokročilejšie programátorské techniky z letného semestra.)
 
V tomto prípade ešte môžeme zvoliť druhú možnosť a za <tt>dataType</tt> považovať <tt>double</tt>; o zvyšok sa postará automatické pretypovanie znakov (čiže špeciálnych celých čísel) na reálne čísla a naopak. Toto riešenie má však ďaleko od ideálneho a pre iné dvojice typov nemusí riešenie tohto typu existovať vôbec. (Ozaj elegantný prístup k tomuto problému vyžaduje pokročilejšie programátorské techniky z letného semestra.)
  
Riadok 10 586: Riadok 10 587:
 
Uzol aritmetického stromu tak môžeme reprezentovať napríklad nasledujúcou štruktúrou:
 
Uzol aritmetického stromu tak môžeme reprezentovať napríklad nasledujúcou štruktúrou:
  
<pre>
+
<syntaxhighlight lang="C++">
 
struct treeNode {
 
struct treeNode {
 
     double val;      // ciselna hodnota (zmysluplna len v listoch)
 
     double val;      // ciselna hodnota (zmysluplna len v listoch)
Riadok 10 593: Riadok 10 594:
 
     treeNode *right; // smernik na koren podstromu reprezentujuceho pravy podvyraz
 
     treeNode *right; // smernik na koren podstromu reprezentujuceho pravy podvyraz
 
};
 
};
</pre>
+
</syntaxhighlight>
  
 
Pre vnútorné uzly stromu (zodpovedajúce operátorom) pritom:
 
Pre vnútorné uzly stromu (zodpovedajúce operátorom) pritom:
Riadok 10 613: Riadok 10 614:
 
Nasledujúce funkcie vytvoria nový vnútorný uzol (pre operátor) resp. nový list (pre číslo):
 
Nasledujúce funkcie vytvoria nový vnútorný uzol (pre operátor) resp. nový list (pre číslo):
  
<pre>
+
<syntaxhighlight lang="C++">
 
treeNode *createOp(char op, treeNode *left, treeNode *right) {
 
treeNode *createOp(char op, treeNode *left, treeNode *right) {
 
     treeNode *v = new treeNode;
 
     treeNode *v = new treeNode;
Riadok 10 630: Riadok 10 631:
 
     return v;
 
     return v;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
&bdquo;Ručne&rdquo; teraz môžeme vytvoriť strom pre výraz <tt>(65 – 3 * 5)/(2 + 3)</tt>:
 
&bdquo;Ručne&rdquo; teraz môžeme vytvoriť strom pre výraz <tt>(65 – 3 * 5)/(2 + 3)</tt>:
<pre>
+
<syntaxhighlight lang="C++">
 
treeNode *root = createOp('/',  
 
treeNode *root = createOp('/',  
 
                   createOp('-',  
 
                   createOp('-',  
Riadok 10 639: Riadok 10 640:
 
                     createOp('*', createNum(3), createNum(5))),
 
                     createOp('*', createNum(3), createNum(5))),
 
                   createOp('+', createNum(2), createNum(3)));
 
                   createOp('+', createNum(2), createNum(3)));
</pre>
+
</syntaxhighlight>
 
Alebo po častiach:
 
Alebo po častiach:
<pre>
+
<syntaxhighlight lang="C++">
 
node *v65 = createNum(65);
 
node *v65 = createNum(65);
 
node *v3 = createNum(3);
 
node *v3 = createNum(3);
Riadok 10 651: Riadok 10 652:
 
node *vPlus = createOp('+', v2, v3b);
 
node *vPlus = createOp('+', v2, v3b);
 
node *vDeleno = createOp('/', vMinus, vPlus);
 
node *vDeleno = createOp('/', vMinus, vPlus);
</pre>
+
</syntaxhighlight>
  
 
=== Uvoľnenie pamäte ===
 
=== Uvoľnenie pamäte ===
Riadok 10 657: Riadok 10 658:
 
Nasledujúca funkcia uvoľní z pamäte celý strom s koreňom <tt>root</tt>:
 
Nasledujúca funkcia uvoľní z pamäte celý strom s koreňom <tt>root</tt>:
  
<pre>
+
<syntaxhighlight lang="C++">
 
void destroyTree(treeNode *root) {
 
void destroyTree(treeNode *root) {
 
     if (root != NULL) {
 
     if (root != NULL) {
Riadok 10 665: Riadok 10 666:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Vyhodnotenie výrazu reprezentovaného stromom ===
 
=== Vyhodnotenie výrazu reprezentovaného stromom ===
Riadok 10 671: Riadok 10 672:
 
Nasledujúca rekurzívna funkcia vypočíta hodnotu aritmetického výrazu reprezentovaného stromom s koreňom <tt>root</tt>:
 
Nasledujúca rekurzívna funkcia vypočíta hodnotu aritmetického výrazu reprezentovaného stromom s koreňom <tt>root</tt>:
  
<pre>
+
<syntaxhighlight lang="C++">
 
double evaluateTree(treeNode *root) {
 
double evaluateTree(treeNode *root) {
 
     assert(root != NULL);
 
     assert(root != NULL);
Riadok 10 698: Riadok 10 699:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Vypísanie výrazu reprezentovaného stromom v rôznych notáciách ===
 
=== Vypísanie výrazu reprezentovaného stromom v rôznych notáciách ===
Riadok 10 704: Riadok 10 705:
 
Infixovú, prefixovú, resp. postfixovú reprezentáciu aritmetického výrazu reprezentovaného stromom s koreňom <tt>root</tt> možno získať pomocou nasledujúcich funkcií:  
 
Infixovú, prefixovú, resp. postfixovú reprezentáciu aritmetického výrazu reprezentovaného stromom s koreňom <tt>root</tt> možno získať pomocou nasledujúcich funkcií:  
  
<pre>
+
<syntaxhighlight lang="C++">
 
void printInorder(FILE *fw, treeNode *root) {
 
void printInorder(FILE *fw, treeNode *root) {
 
     if (root->op == ' ') {
 
     if (root->op == ' ') {
Riadok 10 716: Riadok 10 717:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
<pre>
+
<syntaxhighlight lang="C++">
 
void printPreorder(FILE *fw, treeNode *root) {
 
void printPreorder(FILE *fw, treeNode *root) {
 
     if (root->op == ' ') {
 
     if (root->op == ' ') {
Riadok 10 728: Riadok 10 729:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
<pre>
+
<syntaxhighlight lang="C++">
 
void printPostorder(FILE *fw, treeNode *root) {
 
void printPostorder(FILE *fw, treeNode *root) {
 
     if (root->op == ' ') {
 
     if (root->op == ' ') {
Riadok 10 740: Riadok 10 741:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Vytvorenie stromu z postfixového výrazu ===
 
=== Vytvorenie stromu z postfixového výrazu ===
  
 
Pripomeňme si z [[#Prednáška 18|minulej prednášky]] funkciu na vyhodnocovanie postfixového výrazu:
 
Pripomeňme si z [[#Prednáška 18|minulej prednášky]] funkciu na vyhodnocovanie postfixového výrazu:
<pre>
+
<syntaxhighlight lang="C++">
 
typedef double dataType;
 
typedef double dataType;
  
Riadok 10 796: Riadok 10 797:
 
     return result;
 
     return result;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Túto funkciu možno jednoducho prepísať tak, aby namiesto vyhodnocovania výrazu konštruovala zodpovedajúci aritmetický strom. Namiesto hodnôt jednotlivých podvýrazov stačí na zásobníku uchovávať korene stromov, ktoré tieto podvýrazy reprezentujú. Aplikácii aritmetickej operácie potom bude zodpovedať &bdquo;spojenie&rdquo; dvoch podstromov do jedného stromu:
 
Túto funkciu možno jednoducho prepísať tak, aby namiesto vyhodnocovania výrazu konštruovala zodpovedajúci aritmetický strom. Namiesto hodnôt jednotlivých podvýrazov stačí na zásobníku uchovávať korene stromov, ktoré tieto podvýrazy reprezentujú. Aplikácii aritmetickej operácie potom bude zodpovedať &bdquo;spojenie&rdquo; dvoch podstromov do jedného stromu:
  
<pre>
+
<syntaxhighlight lang="C++">
 
typedef treeNode *dataType;
 
typedef treeNode *dataType;
  
Riadok 10 838: Riadok 10 839:
 
     return result;
 
     return result;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Ukážkový program pracujúci s aritmetickými stromami ===
 
=== Ukážkový program pracujúci s aritmetickými stromami ===
Riadok 10 844: Riadok 10 845:
 
Nasledujúci program prečíta z konzoly aritmetický výraz v postfixovom tvare, skonštruuje jeho aritmetický strom a následne preň zavolá funkcie na výpočet hodnoty výrazu a jeho výpis v rôznych notáciách:
 
Nasledujúci program prečíta z konzoly aritmetický výraz v postfixovom tvare, skonštruuje jeho aritmetický strom a následne preň zavolá funkcie na výpočet hodnoty výrazu a jeho výpis v rôznych notáciách:
  
<pre>
+
<syntaxhighlight lang="C++">
 
int main() {           
 
int main() {           
 
     char expr[100];     
 
     char expr[100];     
Riadok 10 865: Riadok 10 866:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
 
=Prednáška 20=
 
=Prednáška 20=
 
== Oznamy ==
 
== Oznamy ==
Riadok 10 903: Riadok 10 904:
 
Keďže neskôr budeme binárne stromy vypisovať, budeme predpokladať, že pre hodnoty typu <tt>dataType</tt> máme k dispozícii funkciu <tt>printDataType</tt>, ktorá ich v nejakom vhodnom formáte vypisuje. Nasledujúci kus kódu zodpovedá situácii, keď <tt>dataType</tt> je <tt>int</tt>.
 
Keďže neskôr budeme binárne stromy vypisovať, budeme predpokladať, že pre hodnoty typu <tt>dataType</tt> máme k dispozícii funkciu <tt>printDataType</tt>, ktorá ich v nejakom vhodnom formáte vypisuje. Nasledujúci kus kódu zodpovedá situácii, keď <tt>dataType</tt> je <tt>int</tt>.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
  
Riadok 10 928: Riadok 10 929:
 
     node *right;    // smernik na praveho syna (NULL, ak tento syn neexistuje)
 
     node *right;    // smernik na praveho syna (NULL, ak tento syn neexistuje)
 
};
 
};
</pre>
+
</syntaxhighlight>
  
 
=== Vytvorenie binárneho stromu ===
 
=== Vytvorenie binárneho stromu ===
Riadok 10 934: Riadok 10 935:
 
Nasledujúca funkcia vytvorí uzol binárneho stromu s dátami <tt>data</tt>, ľavým podstromom zakoreneným v uzle <tt>*left</tt> a pravým podstromom zakoreneným v uzle <tt>*right</tt> (parametre <tt>left</tt> a <tt>right</tt> sú teda smerníkmi na uzly). Ako výstup funkcia vráti smerník na novovytvorený uzol.
 
Nasledujúca funkcia vytvorí uzol binárneho stromu s dátami <tt>data</tt>, ľavým podstromom zakoreneným v uzle <tt>*left</tt> a pravým podstromom zakoreneným v uzle <tt>*right</tt> (parametre <tt>left</tt> a <tt>right</tt> sú teda smerníkmi na uzly). Ako výstup funkcia vráti smerník na novovytvorený uzol.
  
<pre>
+
<syntaxhighlight lang="C++">
 
/* Vytvori uzol binarneho stromu */
 
/* Vytvori uzol binarneho stromu */
 
node *createNode(dataType data, node *left, node *right) {
 
node *createNode(dataType data, node *left, node *right) {
Riadok 10 943: Riadok 10 944:
 
     return v;
 
     return v;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Nasledujúca volanie tak napríklad vytvorí binárny strom so šiestimi uzlami zakorenený v uzle <tt>*root</tt>.
 
Nasledujúca volanie tak napríklad vytvorí binárny strom so šiestimi uzlami zakorenený v uzle <tt>*root</tt>.
  
<pre>
+
<syntaxhighlight lang="C++">
 
node *root = createNode(1,
 
node *root = createNode(1,
 
                   createNode(2,  
 
                   createNode(2,  
Riadok 10 955: Riadok 10 956:
 
                     NULL,  
 
                     NULL,  
 
                     createNode(6, NULL, NULL)));
 
                     createNode(6, NULL, NULL)));
</pre>
+
</syntaxhighlight>
  
 
''Cvičenie'': nakreslite binárny strom vytvorený predchádzajúcim volaním.
 
''Cvičenie'': nakreslite binárny strom vytvorený predchádzajúcim volaním.
Riadok 10 963: Riadok 10 964:
 
Nasledujúca rekurzívna funkcia zlikviduje celý podstrom zakorenený v uzle <tt>*root</tt> (t. j. pouvoľňuje pamäť pre všetky jeho uzly).
 
Nasledujúca rekurzívna funkcia zlikviduje celý podstrom zakorenený v uzle <tt>*root</tt> (t. j. pouvoľňuje pamäť pre všetky jeho uzly).
  
<pre>
+
<syntaxhighlight lang="C++">
 
/* Zlikviduje podstrom s korenom *root (pouvolnuje pamat) */
 
/* Zlikviduje podstrom s korenom *root (pouvolnuje pamat) */
 
void destroyTree(node *root) {
 
void destroyTree(node *root) {
Riadok 10 972: Riadok 10 973:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Výška binárneho stromu ===
 
=== Výška binárneho stromu ===
Riadok 10 980: Riadok 10 981:
 
Nasledujúca funkcia počíta výšku stromu (kvôli elegancii zápisu pritom pracuje s rozšírením definície výšky stromu na prázdne stromy, za ktorých výšku sa považuje číslo ''-1'').
 
Nasledujúca funkcia počíta výšku stromu (kvôli elegancii zápisu pritom pracuje s rozšírením definície výšky stromu na prázdne stromy, za ktorých výšku sa považuje číslo ''-1'').
  
<pre>
+
<syntaxhighlight lang="C++">
 
/* Spocita vysku podstromu s korenom *root. Pre root == NULL vrati -1. */
 
/* Spocita vysku podstromu s korenom *root. Pre root == NULL vrati -1. */
 
int height(node *root) {
 
int height(node *root) {
Riadok 10 994: Riadok 10 995:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Pre výšku ''h'' stromu s ''n'' uzlami pritom platia nasledujúce vzťahy:
 
Pre výšku ''h'' stromu s ''n'' uzlami pritom platia nasledujúce vzťahy:
Riadok 11 007: Riadok 11 008:
 
Často je potrebné prejsť celý strom a spracovať (napríklad vypísať) hodnoty vo všetkých uzloch. Toto prehľadávanie možno, podobne ako pri aritmetických stromoch, realizovať v troch základných poradiach: ''preorder'', ''inorder'' a ''postorder''.
 
Často je potrebné prejsť celý strom a spracovať (napríklad vypísať) hodnoty vo všetkých uzloch. Toto prehľadávanie možno, podobne ako pri aritmetických stromoch, realizovať v troch základných poradiach: ''preorder'', ''inorder'' a ''postorder''.
  
<pre>
+
<syntaxhighlight lang="C++">
 
/* Vypise podstrom s korenom *root v poradi preorder */
 
/* Vypise podstrom s korenom *root v poradi preorder */
 
void printPreorder(node *root) {
 
void printPreorder(node *root) {
Riadok 11 037: Riadok 11 038:
 
     printDataType(root->data);
 
     printDataType(root->data);
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== ''Príklad'': úplné binárne stromy ===
 
=== ''Príklad'': úplné binárne stromy ===
Riadok 11 043: Riadok 11 044:
 
Binárny strom výšky ''h'' s maximálnym počtom vrcholov <math>2^{h+1}-1</math> sa nazýva ''úplny binárny strom''. Nasledujúca funkcia <tt>createCompleteTree</tt> vytvorí takýto strom a vráti smerník na jeho koreň. Jeho uzlom pritom priradí po dvoch rôzne hodnoty typu <tt>int</tt> (predpokladáme teda, že <tt>dataType</tt> je <tt>int</tt>) podľa globálnej premennej <tt>count</tt>.
 
Binárny strom výšky ''h'' s maximálnym počtom vrcholov <math>2^{h+1}-1</math> sa nazýva ''úplny binárny strom''. Nasledujúca funkcia <tt>createCompleteTree</tt> vytvorí takýto strom a vráti smerník na jeho koreň. Jeho uzlom pritom priradí po dvoch rôzne hodnoty typu <tt>int</tt> (predpokladáme teda, že <tt>dataType</tt> je <tt>int</tt>) podľa globálnej premennej <tt>count</tt>.
  
<pre>
+
<syntaxhighlight lang="C++">
 
// ...
 
// ...
  
Riadok 11 077: Riadok 11 078:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
''Cvičenie'': popíšte poradie, v ktorom sa v uvedenom programe jednotlivým uzlom priraďujú ich hodnoty.
 
''Cvičenie'': popíšte poradie, v ktorom sa v uvedenom programe jednotlivým uzlom priraďujú ich hodnoty.
Riadok 11 103: Riadok 11 104:
 
Na binárny vyhľadávací strom kladieme globálne podmienky ohľadom kľúčov jeho uzlov. V prípade &bdquo;ručnej&rdquo; manipulácie s jeho uzlami by mohlo dôjsť k narušeniu platnosti týchto podmienok (napríklad by sme mohli niektorému ľavému synovi priradiť väčší kľúč, než má jeho otec). Aby sme predišli takýmto problémom, definujeme okrem štruktúry <tt>node</tt> pre jednotlivé uzly aj štruktúru <tt>binarySearchTree</tt> realizujúcu &bdquo;obal&rdquo; pre celý binárny vyhľadávací strom. Následne definujeme niekoľko funkcií na prácu s binárnymi vyhľadávacími stromami prostredníctvom štruktúry <tt>binarySearchTree</tt>. Používateľ, ktorý bude na prácu s binárnymi vyhľadávacími stromami používať výhradne tieto funkcie, by nikdy nemal mať možnosť porušiť podmienky platné v binárnych vyhľadávacích stromoch.  
 
Na binárny vyhľadávací strom kladieme globálne podmienky ohľadom kľúčov jeho uzlov. V prípade &bdquo;ručnej&rdquo; manipulácie s jeho uzlami by mohlo dôjsť k narušeniu platnosti týchto podmienok (napríklad by sme mohli niektorému ľavému synovi priradiť väčší kľúč, než má jeho otec). Aby sme predišli takýmto problémom, definujeme okrem štruktúry <tt>node</tt> pre jednotlivé uzly aj štruktúru <tt>binarySearchTree</tt> realizujúcu &bdquo;obal&rdquo; pre celý binárny vyhľadávací strom. Následne definujeme niekoľko funkcií na prácu s binárnymi vyhľadávacími stromami prostredníctvom štruktúry <tt>binarySearchTree</tt>. Používateľ, ktorý bude na prácu s binárnymi vyhľadávacími stromami používať výhradne tieto funkcie, by nikdy nemal mať možnosť porušiť podmienky platné v binárnych vyhľadávacích stromoch.  
  
<pre>
+
<syntaxhighlight lang="C++">
 
/* Uzol binarneho vyhladavacieho stromu. */
 
/* Uzol binarneho vyhladavacieho stromu. */
 
struct node {
 
struct node {
Riadok 11 123: Riadok 11 124:
 
     node *root;
 
     node *root;
 
};
 
};
</pre>
+
</syntaxhighlight>
  
 
=== Inicializácia binárneho vyhľadávacieho stromu ===
 
=== Inicializácia binárneho vyhľadávacieho stromu ===
Riadok 11 129: Riadok 11 130:
 
Nasledujúca funkcia realizuje inicializáciu binárneho vyhľadávacieho stromu <tt>t</tt>.
 
Nasledujúca funkcia realizuje inicializáciu binárneho vyhľadávacieho stromu <tt>t</tt>.
  
<pre>
+
<syntaxhighlight lang="C++">
 
/* Inicializuje prazdny binarny vyhladavaci strom. */
 
/* Inicializuje prazdny binarny vyhladavaci strom. */
 
void bstInit(binarySearchTree &t) {
 
void bstInit(binarySearchTree &t) {
 
     t.root = NULL;
 
     t.root = NULL;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Likvidácia binárneho vyhľadávacieho stromu ===
 
=== Likvidácia binárneho vyhľadávacieho stromu ===
Riadok 11 140: Riadok 11 141:
 
Likvidáciu podstromu zakoreneného v danom uzle <tt>*root</tt> realizujeme funkciou <tt>destroy</tt>, obdobne ako pri všeobecných binárnych stromoch. Používateľovi navyše dáme k dispozícii aj &bdquo;baliacu&rdquo; funkciu <tt>bstDestroy</tt>, ktorá zlikviduje binárny vyhľadávací strom <tt>t</tt> tak, že zavolá funkciu <tt>destroy</tt> na jeho koreň.
 
Likvidáciu podstromu zakoreneného v danom uzle <tt>*root</tt> realizujeme funkciou <tt>destroy</tt>, obdobne ako pri všeobecných binárnych stromoch. Používateľovi navyše dáme k dispozícii aj &bdquo;baliacu&rdquo; funkciu <tt>bstDestroy</tt>, ktorá zlikviduje binárny vyhľadávací strom <tt>t</tt> tak, že zavolá funkciu <tt>destroy</tt> na jeho koreň.
  
<pre>
+
<syntaxhighlight lang="C++">
 
/* Uvolni pamat pre podstrom s korenom *root. */
 
/* Uvolni pamat pre podstrom s korenom *root. */
 
void destroy(node *root) {
 
void destroy(node *root) {
Riadok 11 158: Riadok 11 159:
 
     destroy(t.root);
 
     destroy(t.root);
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Hľadanie v binárnom vyhľadávacom strome ===
 
=== Hľadanie v binárnom vyhľadávacom strome ===
Riadok 11 168: Riadok 11 169:
 
Používateľovi pritom opäť poskytneme aj &bdquo;baliacu&rdquo; funkciu <tt>bstFind</tt>, ktorá zavolá funkciu <tt>findNode</tt> pre koreň daného binárneho vyhľadávacieho stromu <tt>t</tt> a pomocou nej zistí, či tento strom obsahuje uzol s kľúčom <tt>key</tt>.
 
Používateľovi pritom opäť poskytneme aj &bdquo;baliacu&rdquo; funkciu <tt>bstFind</tt>, ktorá zavolá funkciu <tt>findNode</tt> pre koreň daného binárneho vyhľadávacieho stromu <tt>t</tt> a pomocou nej zistí, či tento strom obsahuje uzol s kľúčom <tt>key</tt>.
  
<pre>
+
<syntaxhighlight lang="C++">
 
/* Ak v strome s korenom *root existuje uzol s klucom key, vrati ho na vystupe. Inak vrati NULL. */
 
/* Ak v strome s korenom *root existuje uzol s klucom key, vrati ho na vystupe. Inak vrati NULL. */
 
node *findNode(node *root, int key) {
 
node *findNode(node *root, int key) {
Riadok 11 188: Riadok 11 189:
 
     return findNode(t.root, key) != NULL;
 
     return findNode(t.root, key) != NULL;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Čas výpočtu je v najhoršom prípade úmerný výške stromu. Poznamenajme ešte, že funkciu <tt>findNode</tt> je možné realizovať aj nerekurzívne, napríklad takto:
 
Čas výpočtu je v najhoršom prípade úmerný výške stromu. Poznamenajme ešte, že funkciu <tt>findNode</tt> je možné realizovať aj nerekurzívne, napríklad takto:
  
<pre>
+
<syntaxhighlight lang="C++">
 
node *findNode(node *root, int key) {
 
node *findNode(node *root, int key) {
 
     node *v = root;
 
     node *v = root;
Riadok 11 204: Riadok 11 205:
 
     return v;
 
     return v;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Vkladanie do binárneho vyhľadávacieho stromu ===
 
=== Vkladanie do binárneho vyhľadávacieho stromu ===
Riadok 11 212: Riadok 11 213:
 
Používateľovi následne poskytneme &bdquo;baliacu&rdquo; funkciu <tt>bstInsert</tt>, ktorá vytvorí uzol s daným kľúčom <tt>key</tt> a pomocou funkcie <tt>insertNode</tt> ho vloží do binárneho vyhľadávacieho stromu <tt>t</tt>.
 
Používateľovi následne poskytneme &bdquo;baliacu&rdquo; funkciu <tt>bstInsert</tt>, ktorá vytvorí uzol s daným kľúčom <tt>key</tt> a pomocou funkcie <tt>insertNode</tt> ho vloží do binárneho vyhľadávacieho stromu <tt>t</tt>.
  
<pre>
+
<syntaxhighlight lang="C++">
 
/* Vlozi uzol *v na spravne miesto podstromu zakoreneneho v *root */
 
/* Vlozi uzol *v na spravne miesto podstromu zakoreneneho v *root */
 
void insertNode(node *root, node *v) {
 
void insertNode(node *root, node *v) {
Riadok 11 250: Riadok 11 251:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Čas vkladania je tiež v najhoršom prípade úmerný hĺbke stromu.
 
Čas vkladania je tiež v najhoršom prípade úmerný hĺbke stromu.
Riadok 11 264: Riadok 11 265:
 
&bdquo;Obalom&rdquo; pre používateľa bude funkcia <tt>bstMin</tt>, ktorá pomocou funkcie <tt>minNode</tt> nájde minimálny kľúč v danom binárnom vyhľadávacom strome <tt>t</tt>.
 
&bdquo;Obalom&rdquo; pre používateľa bude funkcia <tt>bstMin</tt>, ktorá pomocou funkcie <tt>minNode</tt> nájde minimálny kľúč v danom binárnom vyhľadávacom strome <tt>t</tt>.
  
<pre>
+
<syntaxhighlight lang="C++">
 
/* Vrati (niektory) uzol s minimalnou hodnotou key v podstrome s korenom *root. */
 
/* Vrati (niektory) uzol s minimalnou hodnotou key v podstrome s korenom *root. */
 
node *minNode(node *root) {
 
node *minNode(node *root) {
Riadok 11 284: Riadok 11 285:
 
     return minNode(t.root)->key;  
 
     return minNode(t.root)->key;  
 
}  
 
}  
</pre>
+
</syntaxhighlight>
  
 
''Cvičenie'': napíšte nerekurzívny variant funkcie <tt>minNode</tt>.
 
''Cvičenie'': napíšte nerekurzívny variant funkcie <tt>minNode</tt>.
Riadok 11 292: Riadok 11 293:
 
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.
 
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.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
  
Riadok 11 329: Riadok 11 330:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
 
=Prednáška 21=
 
=Prednáška 21=
 
== Oznamy ==
 
== Oznamy ==
Riadok 11 345: Riadok 11 346:
 
* V opačnom prípade môže byť následníkom uzla <tt>*v</tt> jeho otec (ak <tt>*v</tt> je jeho ľavý syn). Ak je <tt>*v</tt> pravým synom svojho otca, môže to byť aj jeho starý otec (ak je otec uzla <tt>*v</tt> ľavým synom tohto starého otca), atď. Vo všeobecnosti teda ide o najbližšieho predka uzla <tt>*v</tt> takého, že <tt>*v</tt> patrí do jeho ľavého podstromu.
 
* V opačnom prípade môže byť následníkom uzla <tt>*v</tt> jeho otec (ak <tt>*v</tt> je jeho ľavý syn). Ak je <tt>*v</tt> pravým synom svojho otca, môže to byť aj jeho starý otec (ak je otec uzla <tt>*v</tt> ľavým synom tohto starého otca), atď. Vo všeobecnosti teda ide o najbližšieho predka uzla <tt>*v</tt> takého, že <tt>*v</tt> patrí do jeho ľavého podstromu.
  
<pre>
+
<syntaxhighlight lang="C++">
 
/* Vrati uzol, ktory vo vzostupnom poradi uzlov podla klucov nasleduje za *v. Ak taky uzol neexistuje, vrati NULL. */
 
/* Vrati uzol, ktory vo vzostupnom poradi uzlov podla klucov nasleduje za *v. Ak taky uzol neexistuje, vrati NULL. */
 
node *successorNode(node *v) {
 
node *successorNode(node *v) {
Riadok 11 357: Riadok 11 358:
 
     return v->parent;
 
     return v->parent;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Mazanie z binárneho vyhľadávacieho stromu ===
 
=== Mazanie z binárneho vyhľadávacieho stromu ===
Riadok 11 363: Riadok 11 364:
 
Nasledujúca funkcia <tt>bstRemove</tt> zmaže z binárneho vyhľadávacieho stromu <tt>t</tt> práve jeden uzol s kľúčom <tt>key</tt> (ak sa taký uzol v strome vyskytuje). Pracuje tak, že najprv pomocou funkcie <tt>findNode</tt> nájde uzol <tt>*v</tt> s kľúčom <tt>key</tt>. V prípade úspechu zistí počet synov uzla <tt>*v</tt>. Ak totiž <tt>*v</tt> nemá žiadneho syna alebo má len jedného syna, možno ho zo stromu <tt>t</tt> zmazať jednoducho tak, že sa prípadný syn uzla <tt>*v</tt> stane synom otca uzla <tt>*v</tt>. V prípade, že má <tt>*v</tt> dvoch synov je však zrejmé, že jeho následník sa musí nachádzať v jeho neprázdnom pravom podstrome. Tento následník  <tt>*rm</tt> navyše nemôže mať ľavého syna. Odstránenie kľúča <tt>key</tt> je teda možné realizovať tak, že sa kľúč uzla <tt>*rm</tt> presunie do uzla <tt>*v</tt> a následne sa odstráni uzol <tt>*rm</tt> tak, ako je popísané vyššie.
 
Nasledujúca funkcia <tt>bstRemove</tt> zmaže z binárneho vyhľadávacieho stromu <tt>t</tt> práve jeden uzol s kľúčom <tt>key</tt> (ak sa taký uzol v strome vyskytuje). Pracuje tak, že najprv pomocou funkcie <tt>findNode</tt> nájde uzol <tt>*v</tt> s kľúčom <tt>key</tt>. V prípade úspechu zistí počet synov uzla <tt>*v</tt>. Ak totiž <tt>*v</tt> nemá žiadneho syna alebo má len jedného syna, možno ho zo stromu <tt>t</tt> zmazať jednoducho tak, že sa prípadný syn uzla <tt>*v</tt> stane synom otca uzla <tt>*v</tt>. V prípade, že má <tt>*v</tt> dvoch synov je však zrejmé, že jeho následník sa musí nachádzať v jeho neprázdnom pravom podstrome. Tento následník  <tt>*rm</tt> navyše nemôže mať ľavého syna. Odstránenie kľúča <tt>key</tt> je teda možné realizovať tak, že sa kľúč uzla <tt>*rm</tt> presunie do uzla <tt>*v</tt> a následne sa odstráni uzol <tt>*rm</tt> tak, ako je popísané vyššie.
  
<pre>
+
<syntaxhighlight lang="C++">
 
/* Zmaze zo stromu t prave jeden uzol s klucom key (ak tam taky je). */
 
/* Zmaze zo stromu t prave jeden uzol s klucom key (ak tam taky je). */
 
void bstRemove(binarySearchTree &t, int key) {
 
void bstRemove(binarySearchTree &t, int key) {
Riadok 11 400: Riadok 11 401:
 
     delete rm;
 
     delete rm;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Zložitosť jednotlivých operácií ===
 
=== Zložitosť jednotlivých operácií ===
Riadok 11 414: Riadok 11 415:
 
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.
 
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.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
  
Riadok 11 455: Riadok 11 456:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
== Lexikografické stromy ==
 
== Lexikografické stromy ==
Riadok 11 472: Riadok 11 473:
 
Pri rôznych praktických aplikáciách môže štruktúra <tt>node</tt> obsahovať aj rozličné ďalšie informácie &ndash; pre každé slovo z reprezentovanej množiny si napríklad môžeme pamätať jeho frekvenciu výskytu, preklad do nejakého iného jazyka a podobne.
 
Pri rôznych praktických aplikáciách môže štruktúra <tt>node</tt> obsahovať aj rozličné ďalšie informácie &ndash; pre každé slovo z reprezentovanej množiny si napríklad môžeme pamätať jeho frekvenciu výskytu, preklad do nejakého iného jazyka a podobne.
  
<pre>
+
<syntaxhighlight lang="C++">
 
const int alphSize = 'z' - 'a' + 1;
 
const int alphSize = 'z' - 'a' + 1;
  
Riadok 11 480: Riadok 11 481:
 
     // dalsie data (napriklad preklad slova, pocet vyskytov slova v texte, ...)
 
     // dalsie data (napriklad preklad slova, pocet vyskytov slova v texte, ...)
 
};
 
};
</pre>
+
</syntaxhighlight>
  
 
Samotný lexikografický strom je potom daný iba smerníkom na svoj koreň:
 
Samotný lexikografický strom je potom daný iba smerníkom na svoj koreň:
<pre>
+
<syntaxhighlight lang="C++">
 
struct trie {
 
struct trie {
 
     node *root;       
 
     node *root;       
 
};
 
};
</pre>
+
</syntaxhighlight>
  
 
=== Inicializácia lexikografického stromu ===
 
=== Inicializácia lexikografického stromu ===
Riadok 11 493: Riadok 11 494:
 
Nasledujúca funkcia inicializuje prázdny lexikografický strom <tt>t</tt>:
 
Nasledujúca funkcia inicializuje prázdny lexikografický strom <tt>t</tt>:
  
<pre>
+
<syntaxhighlight lang="C++">
 
void trieInit(trie &t) {
 
void trieInit(trie &t) {
 
     t.root = NULL;  
 
     t.root = NULL;  
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Likvidácia lexikografického stromu ===
 
=== Likvidácia lexikografického stromu ===
Riadok 11 503: Riadok 11 504:
 
Uvoľnenie pamäte alokovanej pre podstrom zakorenený v uzle <tt>root</tt> realizujeme obdobne ako pri binárnych vyhľadávacích stromoch. Jediný rozdiel spočíva v potenciálne väčšom počte synov uzla <tt>root</tt>.
 
Uvoľnenie pamäte alokovanej pre podstrom zakorenený v uzle <tt>root</tt> realizujeme obdobne ako pri binárnych vyhľadávacích stromoch. Jediný rozdiel spočíva v potenciálne väčšom počte synov uzla <tt>root</tt>.
  
<pre>
+
<syntaxhighlight lang="C++">
 
void destroySubtree(node *root) {
 
void destroySubtree(node *root) {
 
     if (root != NULL) {
 
     if (root != NULL) {
Riadok 11 512: Riadok 11 513:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Nasledujúca funkcia potom zlikviduje celý lexikografický strom <tt>t</tt>:
 
Nasledujúca funkcia potom zlikviduje celý lexikografický strom <tt>t</tt>:
  
<pre>
+
<syntaxhighlight lang="C++">
 
void trieDestroy(trie &t) {
 
void trieDestroy(trie &t) {
 
     destroySubtree(t.root);
 
     destroySubtree(t.root);
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Vkladanie do lexikografického stromu ===
 
=== Vkladanie do lexikografického stromu ===
Riadok 11 526: Riadok 11 527:
 
Pri vkladaní reťazca do množiny realizovanej lexikografickým stromom často vznikne potreba vytvárať nové uzly tohto stromu. Túto podúlohu realizuje funkcia <tt>createNode</tt>, ktorá vytvorí nový uzol s hodnotou <tt>isWord</tt> danou jej argumentom a so všetkými smerníkmi na synov rovnými <tt>NULL</tt>.
 
Pri vkladaní reťazca do množiny realizovanej lexikografickým stromom často vznikne potreba vytvárať nové uzly tohto stromu. Túto podúlohu realizuje funkcia <tt>createNode</tt>, ktorá vytvorí nový uzol s hodnotou <tt>isWord</tt> danou jej argumentom a so všetkými smerníkmi na synov rovnými <tt>NULL</tt>.
  
<pre>
+
<syntaxhighlight lang="C++">
 
node *createNode(bool isWord) {
 
node *createNode(bool isWord) {
 
     node *v = new node;
 
     node *v = new node;
Riadok 11 535: Riadok 11 536:
 
     return v;
 
     return v;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Vloženie reťazca <tt>word</tt> do lexikografického stromu <tt>t</tt> potom realizuje funkcia <tt>trieInsert</tt>, ktorá pracuje nasledovne:
 
Vloženie reťazca <tt>word</tt> do lexikografického stromu <tt>t</tt> potom realizuje funkcia <tt>trieInsert</tt>, ktorá pracuje nasledovne:
Riadok 11 542: Riadok 11 543:
 
* Ak v nejakom uzle <tt>v</tt> príde na koniec slova <tt>word</tt>, nastaví hodnotu <tt>v->isWord</tt> na <tt>true</tt>.
 
* Ak v nejakom uzle <tt>v</tt> príde na koniec slova <tt>word</tt>, nastaví hodnotu <tt>v->isWord</tt> na <tt>true</tt>.
  
<pre>
+
<syntaxhighlight lang="C++">
 
void trieInsert(trie &t, const char *word) {
 
void trieInsert(trie &t, const char *word) {
 
     if (t.root == NULL) {
 
     if (t.root == NULL) {
Riadok 11 557: Riadok 11 558:
 
     v->isWord = true;
 
     v->isWord = true;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Hľadanie v lexikografickom strome ===
 
=== Hľadanie v lexikografickom strome ===
Riadok 11 563: Riadok 11 564:
 
Funkcia <tt>treeFind</tt> pre daný lexikografický strom <tt>t</tt> a reťazec <tt>word</tt> zistí, či slovo <tt>word</tt> patrí do množiny reprezentovanej stromom <tt>t</tt>. Opäť pritom postupuje po písmenách reťazca <tt>word</tt>. Kým nedôjde na koniec slova, snaží sa ísť po hranách, ktoré zodpovedajú jednotlivým písmenám. V prípade, že v niektorom bode narazí na <tt>NULL</tt>, slovo <tt>word</tt> sa v strome nenachádza. V opačnom prípade toto slovo dočíta v nejakom uzle <tt>v</tt>. V takom prípade slovo <tt>word</tt> patrí do reprezentovanej množiny práve vtedy, keď <tt>v->isWord</tt> má hodnotu <tt>true</tt>.
 
Funkcia <tt>treeFind</tt> pre daný lexikografický strom <tt>t</tt> a reťazec <tt>word</tt> zistí, či slovo <tt>word</tt> patrí do množiny reprezentovanej stromom <tt>t</tt>. Opäť pritom postupuje po písmenách reťazca <tt>word</tt>. Kým nedôjde na koniec slova, snaží sa ísť po hranách, ktoré zodpovedajú jednotlivým písmenám. V prípade, že v niektorom bode narazí na <tt>NULL</tt>, slovo <tt>word</tt> sa v strome nenachádza. V opačnom prípade toto slovo dočíta v nejakom uzle <tt>v</tt>. V takom prípade slovo <tt>word</tt> patrí do reprezentovanej množiny práve vtedy, keď <tt>v->isWord</tt> má hodnotu <tt>true</tt>.
  
<pre>
+
<syntaxhighlight lang="C++">
 
bool trieFind(trie t, const char *word) {
 
bool trieFind(trie t, const char *word) {
 
     node *v = t.root;
 
     node *v = t.root;
Riadok 11 577: Riadok 11 578:
 
     return v->isWord;
 
     return v->isWord;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Vymazávanie z lexikografického stromu ===
 
=== Vymazávanie z lexikografického stromu ===
Riadok 11 590: Riadok 11 591:
 
* V prípade, že po vykonaní jednej z predchádzajúcich dvoch operácií nemá uzol <tt>root</tt> žiadneho syna a súčasne má <tt>root->isWord</tt> hodnotu <tt>false</tt>, uvoľní pamäť alokovanú pre uzol <tt>root</tt> a informáciu o jeho zmazaní vráti na výstupe.
 
* V prípade, že po vykonaní jednej z predchádzajúcich dvoch operácií nemá uzol <tt>root</tt> žiadneho syna a súčasne má <tt>root->isWord</tt> hodnotu <tt>false</tt>, uvoľní pamäť alokovanú pre uzol <tt>root</tt> a informáciu o jeho zmazaní vráti na výstupe.
  
<pre>
+
<syntaxhighlight lang="C++">
 
bool removeFromSubtree(node *root, const char *word, int index) {
 
bool removeFromSubtree(node *root, const char *word, int index) {
 
     assert(root != NULL);
 
     assert(root != NULL);
Riadok 11 615: Riadok 11 616:
 
     return false;
 
     return false;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Samotné odstránenie reťazca <tt>word</tt> z množiny reprezentovanej stromom <tt>t</tt> potom realizuje nasledujúca funkcia <tt>trieRemove</tt>. Tá len zavolá funkciu <tt>removeFromSubtree</tt> pre koreň stromu <tt>t</tt> a v prípade, že volanie tejto funkcie koreň zo stromu odstráni, nastaví <tt>t.root</tt> na <tt>NULL</tt>.
 
Samotné odstránenie reťazca <tt>word</tt> z množiny reprezentovanej stromom <tt>t</tt> potom realizuje nasledujúca funkcia <tt>trieRemove</tt>. Tá len zavolá funkciu <tt>removeFromSubtree</tt> pre koreň stromu <tt>t</tt> a v prípade, že volanie tejto funkcie koreň zo stromu odstráni, nastaví <tt>t.root</tt> na <tt>NULL</tt>.
  
<pre>
+
<syntaxhighlight lang="C++">
 
void trieRemove(trie &t, const char *word) {
 
void trieRemove(trie &t, const char *word) {
 
     bool rootRemoved = removeFromSubtree(t.root, word, 0);
 
     bool rootRemoved = removeFromSubtree(t.root, word, 0);
Riadok 11 626: Riadok 11 627:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Výška lexikografického stromu ===
 
=== Výška lexikografického stromu ===
Riadok 11 632: Riadok 11 633:
 
Nasledujúca funkcia vypočíta výšku podstromu zakoreneného v uzle <tt>*root</tt> (definovaná je rovnako ako pre binárne stromy):
 
Nasledujúca funkcia vypočíta výšku podstromu zakoreneného v uzle <tt>*root</tt> (definovaná je rovnako ako pre binárne stromy):
  
<pre>
+
<syntaxhighlight lang="C++">
 
int subtreeHeight(node *root) {
 
int subtreeHeight(node *root) {
 
     if (root == NULL) {
 
     if (root == NULL) {
Riadok 11 646: Riadok 11 647:
 
     return maxHeight + 1;
 
     return maxHeight + 1;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Výšku samotného lexikografického stromu <tt>t</tt> potom spočíta nasledujúca funkcia:
 
Výšku samotného lexikografického stromu <tt>t</tt> potom spočíta nasledujúca funkcia:
  
<pre>
+
<syntaxhighlight lang="C++">
 
int trieHeight(trie t) {
 
int trieHeight(trie t) {
 
     return subtreeHeight(t.root);
 
     return subtreeHeight(t.root);
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Vypisovanie slov reprezentovaných lexikografickým stromom ===
 
=== Vypisovanie slov reprezentovaných lexikografickým stromom ===
Riadok 11 660: Riadok 11 661:
 
Nasledujúca funkcia <tt>printSubtree</tt> prehľadáva podstrom zakorenený v uzle <tt>*root</tt> a v reťazci <tt>s</tt> postupne generuje všetky slová z reprezentovanej množiny, ktoré zároveň vypisuje na konzolu.
 
Nasledujúca funkcia <tt>printSubtree</tt> prehľadáva podstrom zakorenený v uzle <tt>*root</tt> a v reťazci <tt>s</tt> postupne generuje všetky slová z reprezentovanej množiny, ktoré zároveň vypisuje na konzolu.
  
<pre>
+
<syntaxhighlight lang="C++">
 
void printSubtree(node *root, char *s, int index) {
 
void printSubtree(node *root, char *s, int index) {
 
     if (root == NULL) {
 
     if (root == NULL) {
Riadok 11 674: Riadok 11 675:
 
     }
 
     }
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Funkcia <tt>triePrint</tt> vypisujúca všetky slová v množine reprezentovanej lexikografickým stromom <tt>t</tt> najprv spočíta výšku stromu <tt>t</tt>, ktorá je rovná dĺžke najdlšieho reťazca tejto množiny. Následne dynamicky alokuje reťazec dostatočnej dĺžky na uchovanie každého slova množiny a zavolá funkciu <tt>printSubtree</tt> pre koreň stromu <tt>t</tt>.
 
Funkcia <tt>triePrint</tt> vypisujúca všetky slová v množine reprezentovanej lexikografickým stromom <tt>t</tt> najprv spočíta výšku stromu <tt>t</tt>, ktorá je rovná dĺžke najdlšieho reťazca tejto množiny. Následne dynamicky alokuje reťazec dostatočnej dĺžky na uchovanie každého slova množiny a zavolá funkciu <tt>printSubtree</tt> pre koreň stromu <tt>t</tt>.
  
<pre>
+
<syntaxhighlight lang="C++">
 
void triePrint(trie t) {
 
void triePrint(trie t) {
 
     int height = trieHeight(t);
 
     int height = trieHeight(t);
Riadok 11 687: Riadok 11 688:
 
     }
 
     }
 
}   
 
}   
</pre>
+
</syntaxhighlight>
  
 
=== Program pracujúci s lexikografickými stromami ===
 
=== Program pracujúci s lexikografickými stromami ===
Riadok 11 693: Riadok 11 694:
 
Nasledujúci program načítava z konzoly príkazy zodpovedajúce operáciám na lexikografickom strome a za každým načítaným príkazom túto operáciu vykonáva.
 
Nasledujúci program načítava z konzoly príkazy zodpovedajúce operáciám na lexikografickom strome a za každým načítaným príkazom túto operáciu vykonáva.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdio>
 
#include <cstdio>
 
#include <cassert>
 
#include <cassert>
Riadok 11 739: Riadok 11 740:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
 
=Prednáška 22=
 
=Prednáška 22=
 
==Oznamy==
 
==Oznamy==
Riadok 11 760: Riadok 11 761:
 
* podmienky (if, else, switch), cykly (for, while)
 
* podmienky (if, else, switch), cykly (for, while)
 
* funkcie (a parametre funkcií - odovzdávanie hodnotou, referenciou, smerníkom)
 
* funkcie (a parametre funkcií - odovzdávanie hodnotou, referenciou, smerníkom)
<pre>
+
<syntaxhighlight lang="C++">
 
void f1(int x){}                                //hodnotou
 
void f1(int x){}                                //hodnotou
 
void f2(int &x){}                                //referenciou
 
void f2(int &x){}                                //referenciou
Riadok 11 766: Riadok 11 767:
 
void f(int a[], int n){}                        //polia bez & (ostanú zmeny)
 
void f(int a[], int n){}                        //polia bez & (ostanú zmeny)
 
void kresli(Turtle &t){}                        //korytnačky, SVGdraw a pod. s &
 
void kresli(Turtle &t){}                        //korytnačky, SVGdraw a pod. s &
</pre>
+
</syntaxhighlight>
  
 
'''Polia, reťazce''' (char[])
 
'''Polia, reťazce''' (char[])
<pre>
+
<syntaxhighlight lang="C++">
 
int A[4]={3, 6, 8, 10};  
 
int A[4]={3, 6, 8, 10};  
 
int B[4];               
 
int B[4];               
Riadok 11 776: Riadok 11 777:
 
char C[100] = "pes";
 
char C[100] = "pes";
 
char D[100] = {'p', 'e', 's', 0};
 
char D[100] = {'p', 'e', 's', 0};
</pre>
+
</syntaxhighlight>
 
* funkcie strlen, strcpy, strcmp, strcat
 
* funkcie strlen, strcpy, strcmp, strcat
  
Riadok 11 787: Riadok 11 788:
  
 
'''Smerníky, dynamicky alokovaná pamäť, dvojrozmerné polia'''
 
'''Smerníky, dynamicky alokovaná pamäť, dvojrozmerné polia'''
<pre>
+
<syntaxhighlight lang="C++">
 
int i;    // „klasická“ celočíselná premenná
 
int i;    // „klasická“ celočíselná premenná
 
int *p;  // ukazovateľ na celočíselnú premennú
 
int *p;  // ukazovateľ na celočíselnú premennú
Riadok 11 814: Riadok 11 815:
 
for (int i = 0; i < n; i++) delete[] a[i];
 
for (int i = 0; i < n; i++) delete[] a[i];
 
delete[] a;
 
delete[] a;
</pre>
+
</syntaxhighlight>
  
 
=== Abstraktné dátové typy ===
 
=== Abstraktné dátové typy ===
Riadok 11 848: Riadok 11 849:
 
=== Dátové štruktúry ===
 
=== Dátové štruktúry ===
 
[[Image:PROG-list.png|right|200px]]'''Spájané zoznamy'''
 
[[Image:PROG-list.png|right|200px]]'''Spájané zoznamy'''
<pre>
+
<syntaxhighlight lang="C++">
 
struct node {
 
struct node {
 
     int data;
 
     int data;
Riadok 11 863: Riadok 11 864:
 
     z.first = p;          // tento prvok je novým začiatkom
 
     z.first = p;          // tento prvok je novým začiatkom
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
[[Image:PROG-P21-aritm.png|thumb|right|Strom pre výraz (65 – 3*5)/(2 + 3)]]'''Binárne stromy'''
 
[[Image:PROG-P21-aritm.png|thumb|right|Strom pre výraz (65 – 3*5)/(2 + 3)]]'''Binárne stromy'''
<pre>
+
<syntaxhighlight lang="C++">
 
struct node {
 
struct node {
 
     /* vrchol stromu  */
 
     /* vrchol stromu  */
Riadok 11 881: Riadok 11 882:
 
     return v;
 
     return v;
 
}
 
}
</pre>
+
</syntaxhighlight>
 
* prehľadávanie inorder, preorder, postorder
 
* prehľadávanie inorder, preorder, postorder
 
* použitie na uloženie aritmetických výrazov
 
* použitie na uloženie aritmetických výrazov
Riadok 11 893: Riadok 11 894:
 
* nie sú binárne: vrchol môže mať veľa synov
 
* nie sú binárne: vrchol môže mať veľa synov
 
* insert, find, remove v čase závisiacom od dĺžky kľúča, ale nie od počtu kľúčov, ktoré už sú v strome
 
* insert, find, remove v čase závisiacom od dĺžky kľúča, ale nie od počtu kľúčov, ktoré už sú v strome
<pre>
+
<syntaxhighlight lang="C++">
 
struct node {
 
struct node {
 
     /* vrchol lexikografickeho stromu  */
 
     /* vrchol lexikografickeho stromu  */
Riadok 11 900: Riadok 11 901:
 
     node* next[Abeceda]; // pole smernikov na deti     
 
     node* next[Abeceda]; // pole smernikov na deti     
 
};
 
};
</pre>
+
</syntaxhighlight>
  
  
Riadok 11 909: Riadok 11 910:
 
* v ideálnom prípade sa prvky rozhodia pomerne rovnomerne, zoznamy krátke, rýchle hľadanie, vkladenie, mazanie
 
* v ideálnom prípade sa prvky rozhodia pomerne rovnomerne, zoznamy krátke, rýchle hľadanie, vkladenie, mazanie
 
* v najhoršom prípade všetky prvky v jednom zozname, pomalé hľadanie a mazanie
 
* v najhoršom prípade všetky prvky v jednom zozname, pomalé hľadanie a mazanie
<pre>
+
<syntaxhighlight lang="C++">
 
int hash(int k, int m){ // veľmi jednoduchá hešovacia funkcia, v praxi väčšinou zložitejšie
 
int hash(int k, int m){ // veľmi jednoduchá hešovacia funkcia, v praxi väčšinou zložitejšie
 
     return abs(k) % m;
 
     return abs(k) % m;
Riadok 11 922: Riadok 11 923:
 
     int m;
 
     int m;
 
};
 
};
</pre>
+
</syntaxhighlight>
  
 
=== Algoritmy ===
 
=== Algoritmy ===
Riadok 11 967: Riadok 11 968:
  
 
''Príklad'':
 
''Príklad'':
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 11 978: Riadok 11 979:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
=== Zložený typ (<tt>union</tt>) ===
 
=== Zložený typ (<tt>union</tt>) ===
Riadok 11 985: Riadok 11 986:
  
 
''Príklad'':
 
''Príklad'':
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
#include <cstring>
 
#include <cstring>
Riadok 12 004: Riadok 12 005:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Napríklad pri aritmetických stromoch by použitie zloženého typu umožnilo ušetriť trochu pamäte tým, že by sme si v každom uzle pamätali ''buď'' jeho hodnotu, ''alebo'' operátor a smerníky na synov.
 
Napríklad pri aritmetických stromoch by použitie zloženého typu umožnilo ušetriť trochu pamäte tým, že by sme si v každom uzle pamätali ''buď'' jeho hodnotu, ''alebo'' operátor a smerníky na synov.
Riadok 12 018: Riadok 12 019:
  
 
Cyklus <tt>do-while</tt> je obdobou cyklu <tt>while</tt> s vyhodnocovaním podmienky na konci iterácie. Nasledujúce dva spôsoby písania cyklu sú viac-menej ekvivalentné:
 
Cyklus <tt>do-while</tt> je obdobou cyklu <tt>while</tt> s vyhodnocovaním podmienky na konci iterácie. Nasledujúce dva spôsoby písania cyklu sú viac-menej ekvivalentné:
<pre>
+
<syntaxhighlight lang="C++">
 
do {  
 
do {  
 
     prikazy;
 
     prikazy;
Riadok 12 027: Riadok 12 028:
 
     if (!podmienka) break;
 
     if (!podmienka) break;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
===Makrá a konštanty===
 
===Makrá a konštanty===
 
Konštantu možno zadefinovať napríklad aj takto:
 
Konštantu možno zadefinovať napríklad aj takto:
<pre>
+
<syntaxhighlight lang="C++">
 
#define MAXN 100
 
#define MAXN 100
</pre>
+
</syntaxhighlight>
 
Narozdiel od konštanty s definíciou
 
Narozdiel od konštanty s definíciou
<pre>
+
<syntaxhighlight lang="C++">
 
const int maxN = 100;
 
const int maxN = 100;
</pre>
+
</syntaxhighlight>
 
sa tu konštantná premenná <tt>MAXN</tt> reálne nevytvára (nevyhradzuje sa pre ňu pamäťové miesto); všetky výskyty <tt>MAXN</tt> sú preprocesorom kompilátora nahradené &bdquo;ešte v zdrojovom kóde&rdquo; konštantou <tt>100</tt>.
 
sa tu konštantná premenná <tt>MAXN</tt> reálne nevytvára (nevyhradzuje sa pre ňu pamäťové miesto); všetky výskyty <tt>MAXN</tt> sú preprocesorom kompilátora nahradené &bdquo;ešte v zdrojovom kóde&rdquo; konštantou <tt>100</tt>.
  
 
Okrem konštánt možno definovať aj zložitejšie makrá s parametrami:
 
Okrem konštánt možno definovať aj zložitejšie makrá s parametrami:
<pre>
+
<syntaxhighlight lang="C++">
 
/* Definicia makra: */
 
/* Definicia makra: */
 
#define MIN(X,Y) ((X) < (Y) ? (X) : (Y))
 
#define MIN(X,Y) ((X) < (Y) ? (X) : (Y))
Riadok 12 050: Riadok 12 051:
 
/* Preprocesor vykona substituciu za MIN, ktorou dostane: */
 
/* Preprocesor vykona substituciu za MIN, ktorou dostane: */
 
cout << ((a*a) < (b+5) ? (a*a) : (b+5));
 
cout << ((a*a) < (b+5) ? (a*a) : (b+5));
</pre>
+
</syntaxhighlight>
 
* Bez dostatočného množstva zátvoriek by pri použití makra <tt>MIN</tt> mohlo dôjsť k &bdquo;interakcii s okolím&rdquo;.  
 
* Bez dostatočného množstva zátvoriek by pri použití makra <tt>MIN</tt> mohlo dôjsť k &bdquo;interakcii s okolím&rdquo;.  
 
* Vo všeobecnosti je odporúčané vyvarovať sa použitia makier.
 
* Vo všeobecnosti je odporúčané vyvarovať sa použitia makier.
Riadok 12 066: Riadok 12 067:
  
 
V súbore <tt>lib.cpp</tt> môžeme mať napríklad definície dvoch funkcií <tt>f</tt> a <tt>g</tt>:
 
V súbore <tt>lib.cpp</tt> môžeme mať napríklad definície dvoch funkcií <tt>f</tt> a <tt>g</tt>:
<pre>
+
<syntaxhighlight lang="C++">
 
/* Subor lib.cpp */
 
/* Subor lib.cpp */
  
Riadok 12 076: Riadok 12 077:
 
     return n*2;
 
     return n*2;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Môžeme teraz do súboru <tt>prog.cpp</tt> napísať program, ktorý tieto funkcie deklaruje a následne využíva:
 
Môžeme teraz do súboru <tt>prog.cpp</tt> napísať program, ktorý tieto funkcie deklaruje a následne využíva:
<pre>
+
<syntaxhighlight lang="C++">
 
/* Subor prog.cpp */
 
/* Subor prog.cpp */
  
Riadok 12 094: Riadok 12 095:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Program potom možno skompilovať volaním
 
Program potom možno skompilovať volaním
Riadok 12 104: Riadok 12 105:
  
 
Pre náš príklad vyššie tak teraz máme 3 súbory. Súbor <tt>lib.cpp</tt> je rovnaký ako vyššie, súbor <tt>lib.h</tt> obsahuje
 
Pre náš príklad vyššie tak teraz máme 3 súbory. Súbor <tt>lib.cpp</tt> je rovnaký ako vyššie, súbor <tt>lib.h</tt> obsahuje
<pre>
+
<syntaxhighlight lang="C++">
 
/* Subor lib.h */
 
/* Subor lib.h */
  
 
int f(int n);
 
int f(int n);
 
int g(int n);
 
int g(int n);
</pre>
+
</syntaxhighlight>
 
a súbor <tt>prog.cpp</tt> obsahuje
 
a súbor <tt>prog.cpp</tt> obsahuje
<pre>
+
<syntaxhighlight lang="C++">
 
/* Subor prog.cpp */
 
/* Subor prog.cpp */
  
Riadok 12 124: Riadok 12 125:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
Program skompilujeme rovnako ako vyššie:
 
Program skompilujeme rovnako ako vyššie:
Riadok 12 143: Riadok 12 144:
 
* Pri výpočte veľkosti potrebnej pamäte sa zvyčajne používa operátor <tt>sizeof</tt>.
 
* Pri výpočte veľkosti potrebnej pamäte sa zvyčajne používa operátor <tt>sizeof</tt>.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <cstdlib>  // resp. #include <stdlib.h> v C
 
#include <cstdlib>  // resp. #include <stdlib.h> v C
  
Riadok 12 150: Riadok 12 151:
 
/* odalokujeme pole a */
 
/* odalokujeme pole a */
 
free(a);
 
free(a);
</pre>
+
</syntaxhighlight>
  
 
'''Triedenie'''
 
'''Triedenie'''
Riadok 12 159: Riadok 12 160:
 
** Vráti záporné číslo, ak prvý prvok je menší, nulu, ak sú rovnaké a kladné číslo, ak je prvý väčší.
 
** Vráti záporné číslo, ak prvý prvok je menší, nulu, ak sú rovnaké a kladné číslo, ak je prvý väčší.
 
* Ak si napíšeme porovnávaciu funkciu, môžeme triediť prvky hocijakého typu.
 
* Ak si napíšeme porovnávaciu funkciu, môžeme triediť prvky hocijakého typu.
<pre>
+
<syntaxhighlight lang="C++">
 
int compare(const void *a, const void *b) {
 
int compare(const void *a, const void *b) {
 
     return (*(int*)a - *(int*)b);
 
     return (*(int*)a - *(int*)b);
Riadok 12 165: Riadok 12 166:
 
int a[] = {5, 3, 2, 4, 1};
 
int a[] = {5, 3, 2, 4, 1};
 
qsort(a, 5, sizeof(int), compare);
 
qsort(a, 5, sizeof(int), compare);
</pre>
+
</syntaxhighlight>
 
* Existuje napríklad aj funkcia <tt>bsearch</tt> na binárne vyhľadávanie v utriedenom poli.
 
* Existuje napríklad aj funkcia <tt>bsearch</tt> na binárne vyhľadávanie v utriedenom poli.
  
Riadok 12 181: Riadok 12 182:
  
 
V C++ sa dajú písať takzvané ''generické funkcie'', ktoré možno &bdquo;parametrizovať&rdquo; podľa typu:
 
V C++ sa dajú písať takzvané ''generické funkcie'', ktoré možno &bdquo;parametrizovať&rdquo; podľa typu:
<pre>
+
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <iostream>
 
using namespace std;
 
using namespace std;
Riadok 12 197: Riadok 12 198:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
 
* Viac v letnom semestri.
 
* Viac v letnom semestri.
  
Riadok 12 204: Riadok 12 205:
 
Napríklad v nasledujúcom príklade definujeme operátor <tt><</tt> na menách, ktorý najprv porovnáva podľa priezviska a následne podľa krstného mena.
 
Napríklad v nasledujúcom príklade definujeme operátor <tt><</tt> na menách, ktorý najprv porovnáva podľa priezviska a následne podľa krstného mena.
  
<pre>
+
<syntaxhighlight lang="C++">
 
struct meno {
 
struct meno {
 
     char *krstne, *priezvisko;
 
     char *krstne, *priezvisko;
Riadok 12 214: Riadok 12 215:
 
             && strcmp(x.krstne, y.krstne) < 0;
 
             && strcmp(x.krstne, y.krstne) < 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
 
* Podobne môžeme zadefinovať napríklad operátory <tt>+</tt> a <tt>*</tt> pre štruktúry reprezentujúce polynómy alebo komplexné čísla...
 
* Podobne môžeme zadefinovať napríklad operátory <tt>+</tt> a <tt>*</tt> pre štruktúry reprezentujúce polynómy alebo komplexné čísla...
 
* <tt>cout << "Hello"</tt> používa preťažený operátor <tt><<</tt> pre výstupný prúd na ľavej strane a reťazec na pravej strane.
 
* <tt>cout << "Hello"</tt> používa preťažený operátor <tt><<</tt> pre výstupný prúd na ľavej strane a reťazec na pravej strane.
Riadok 12 225: Riadok 12 226:
 
* K jednotlivým znakom pristupujeme pomocou <tt>[]</tt> (ako u polí) alebo pomocou metódy <tt>at</tt>.
 
* K jednotlivým znakom pristupujeme pomocou <tt>[]</tt> (ako u polí) alebo pomocou metódy <tt>at</tt>.
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <string>
 
#include <string>
 
#include <iostream>
 
#include <iostream>
Riadok 12 261: Riadok 12 262:
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
  
 
* Pomocou metódy <tt>c_str()</tt> možno získať z reťazca typu <tt>string</tt> premennú typu <tt>const char*</tt>.
 
* Pomocou metódy <tt>c_str()</tt> možno získať z reťazca typu <tt>string</tt> premennú typu <tt>const char*</tt>.
Riadok 12 270: Riadok 12 271:
 
* Budeme využívať typy a funkcie zadefinované v knižnici fstream.
 
* Budeme využívať typy a funkcie zadefinované v knižnici fstream.
  
<pre>
+
{{subst:C}}
 
#include <fstream>
 
#include <fstream>
  
Riadok 12 285: Riadok 12 286:
 
   o.close ();              // zatvorenie súboru
 
   o.close ();              // zatvorenie súboru
 
}
 
}
</pre>
+
{{subst:/C}}
  
 
* ofstream tiež umožňuje formátovanie výstupu napríklad pomocou modifikátorov <tt>fixed,setprecision(),setfill(),setw()</tt>
 
* ofstream tiež umožňuje formátovanie výstupu napríklad pomocou modifikátorov <tt>fixed,setprecision(),setfill(),setw()</tt>
Riadok 12 291: Riadok 12 292:
 
* Podobne môžeme otvoriť aj súbor, z ktorého budeme čítať vstup. V príklade načítame pole celých čísel.
 
* Podobne môžeme otvoriť aj súbor, z ktorého budeme čítať vstup. V príklade načítame pole celých čísel.
  
<pre>
+
{{subst:C}}
 
#include <fstream>
 
#include <fstream>
 
#include <iostream>
 
#include <iostream>
Riadok 12 314: Riadok 12 315:
 
   f.close();
 
   f.close();
 
}
 
}
</pre>
+
{{subst:/C}}
  
 
* Pri načítaní sme sa už stretli s funkciou <tt>f.fail()</tt>, ktorá vrátila true, ak vznikla chyba (väčšinou pri otvorení neexistujúceho súboru). Druhou možnosťou je testovať správne otvorenie pomocou testovania premennej typu ofstream alebo ifstream.
 
* Pri načítaní sme sa už stretli s funkciou <tt>f.fail()</tt>, ktorá vrátila true, ak vznikla chyba (väčšinou pri otvorení neexistujúceho súboru). Druhou možnosťou je testovať správne otvorenie pomocou testovania premennej typu ofstream alebo ifstream.
 
** technicky ide o preťaženie operátora konverzie na bool resp. na void*, robí negáciu funkcie fail
 
** technicky ide o preťaženie operátora konverzie na bool resp. na void*, robí negáciu funkcie fail
<pre>
+
{{subst:C}}
 
   ifstream fin("vstup.txt"); // input
 
   ifstream fin("vstup.txt"); // input
 
   if(!fin) {
 
   if(!fin) {
Riadok 12 330: Riadok 12 331:
 
     return 1;
 
     return 1;
 
   }
 
   }
</pre>
+
{{subst:/C}}
  
 
* Testovanie konca súboru môžeme robiť pomocou funkcie eof
 
* Testovanie konca súboru môžeme robiť pomocou funkcie eof
  
<pre>
+
{{subst:C}}
 
ifstream fin("VSTUP.txt"); // input
 
ifstream fin("VSTUP.txt"); // input
 
char c;
 
char c;
Riadok 12 342: Riadok 12 343:
 
   cout<<c;
 
   cout<<c;
 
}
 
}
</pre>
+
{{subst:/C}}
  
 
* Pre streamy je praktická funkcia getline, ktorá načíta zo streamu celý riadok. Nasledovný príklad spočíta v jednotlivých riadkoch počet bodiek.
 
* Pre streamy je praktická funkcia getline, ktorá načíta zo streamu celý riadok. Nasledovný príklad spočíta v jednotlivých riadkoch počet bodiek.
  
<pre>
+
{{subst:C}}
 
int main(void){
 
int main(void){
 
ifstream f("VSTUP.txt");;
 
ifstream f("VSTUP.txt");;
Riadok 12 359: Riadok 12 360:
 
  cout << poc <<endl;
 
  cout << poc <<endl;
 
}
 
}
</pre>-->
+
{{subst:/C}}-->
  
 
===Štruktúra <tt>vector</tt>===
 
===Štruktúra <tt>vector</tt>===
Riadok 12 368: Riadok 12 369:
 
* O niečo podarenejšia verzia dynamických polí z prednášky.
 
* O niečo podarenejšia verzia dynamických polí z prednášky.
 
* Deklarovať ho možno jedným z nasledujúcich spôsobov:
 
* Deklarovať ho možno jedným z nasledujúcich spôsobov:
<pre>
+
<syntaxhighlight lang="C++">
 
vector<int> a;      // vytvori pole celych cisel
 
vector<int> a;      // vytvori pole celych cisel
 
vector<int> a(10);  // vytvori pole 10 celych cisel, ktore vsetky nastavi na vychodziu hodnotu
 
vector<int> a(10);  // vytvori pole 10 celych cisel, ktore vsetky nastavi na vychodziu hodnotu
 
vector<int> a(5,1);  // vytvori pole 5 celych cisel, ktore nastavi na 1
 
vector<int> a(5,1);  // vytvori pole 5 celych cisel, ktore nastavi na 1
</pre>
+
</syntaxhighlight>
 
* Prístup k prvkom <tt>vector</tt>-u je možný dvoma spôsobmi:
 
* Prístup k prvkom <tt>vector</tt>-u je možný dvoma spôsobmi:
 
** Klasicky pomocou <tt>a[index]</tt> &ndash; podobne ako pri poliach sa ale v takom prípade nekontroluje rozsah.
 
** Klasicky pomocou <tt>a[index]</tt> &ndash; podobne ako pri poliach sa ale v takom prípade nekontroluje rozsah.
Riadok 12 384: Riadok 12 385:
 
** <tt>a.capacity()</tt> vráti dĺžku aktuálne alokovaného poľa (môže sa líšiť od počtu prvkov v poli) a <tt>a.reserve(n)</tt> umožňuje objem alokovanej pamäte zväčšiť (väčšinou sa o tieto implementačné detaily nestaráme, vhodné použitie týchto funkcií však občas môže vykonávanie programu zefektívniť).
 
** <tt>a.capacity()</tt> vráti dĺžku aktuálne alokovaného poľa (môže sa líšiť od počtu prvkov v poli) a <tt>a.reserve(n)</tt> umožňuje objem alokovanej pamäte zväčšiť (väčšinou sa o tieto implementačné detaily nestaráme, vhodné použitie týchto funkcií však občas môže vykonávanie programu zefektívniť).
  
<pre>
+
<syntaxhighlight lang="C++">
 
#include <vector>
 
#include <vector>
 
using namespace std;
 
using namespace std;
Riadok 12 395: Riadok 12 396:
 
     cout << a[i] << endl;  // alebo a.at(i)
 
     cout << a[i] << endl;  // alebo a.at(i)
 
}
 
}
</pre>
+
</syntaxhighlight>
 
<!--
 
<!--
 
Štruktúra '''map''':
 
Štruktúra '''map''':
 
* Implementuje slovník, pričom zadáme dva typy: kľúč a hodnota
 
* Implementuje slovník, pričom zadáme dva typy: kľúč a hodnota
<pre>
+
{{subst:C}}
 
   map <string, string> zoznam;
 
   map <string, string> zoznam;
 
   zoznam["Jozko Mrkvicka"] = "02/12345678";
 
   zoznam["Jozko Mrkvicka"] = "02/12345678";
Riadok 12 413: Riadok 12 414:
 
     cout << i->first << " " << i->second << endl;
 
     cout << i->first << " " << i->second << endl;
 
   }
 
   }
</pre>
+
{{subst:/C}}
  
 
Štruktúra '''deque''':
 
Štruktúra '''deque''':
 
* Implementuje zásobník aj rad naraz (''double-ended queue'').
 
* Implementuje zásobník aj rad naraz (''double-ended queue'').
<pre>
+
{{subst:C}}
 
   deque <int> a;
 
   deque <int> a;
 
   a.push_back(0);
 
   a.push_back(0);
Riadok 12 425: Riadok 12 426:
 
   a.pop_back();
 
   a.pop_back();
 
   a.pop_front();
 
   a.pop_front();
</pre>
+
{{subst:/C}}
  
 
Algoritmus na '''triedenie''':
 
Algoritmus na '''triedenie''':
 
* V knižnici <tt><algorithm></tt>
 
* V knižnici <tt><algorithm></tt>
<pre>
+
{{subst:C}}
 
//triedime normalne pole
 
//triedime normalne pole
 
int A[6] = {1, 4, 2, 8, 5, 7};
 
int A[6] = {1, 4, 2, 8, 5, 7};
Riadok 12 444: Riadok 12 445:
 
cmp c;
 
cmp c;
 
sort(A.begin(), A.end(), c);
 
sort(A.begin(), A.end(), c);
</pre>-->
+
{{subst:/C}}-->
  
 
<!--===Range-based for loop (v štandarde C++11)===
 
<!--===Range-based for loop (v štandarde C++11)===
Riadok 12 454: Riadok 12 455:
 
** Implementácia tohto štandardu ešte nemusí byť úplná
 
** Implementácia tohto štandardu ešte nemusí byť úplná
  
<pre>
+
{{subst:C}}
 
#include <iostream>
 
#include <iostream>
 
#include <vector>
 
#include <vector>
Riadok 12 485: Riadok 12 486:
 
   vypis(a); // vypise 1 2 3 4 5 6
 
   vypis(a); // vypise 1 2 3 4 5 6
 
}
 
}
</pre>-->
+
{{subst:/C}}-->

Aktuálna revízia z 16:39, 24. september 2019

Týždeň 24.-30.9. Úvod, premenné, podmienky
#Prednáška 1 · #Cvičenia 1 ·
Týždeň 1.-7.10. Výrazy, cyklus for, ďalšie príklady na cykly, Euklidov algoritmus, cyklus while, funkcie
#Prednáška 2 · #Prednáška 3 · Cvičenia 2
Týždeň 8.-14.10. Funkcie, struct, polia, Eratostenovo sito
#Prednáška 4 · #Prednáška 5 · Cvičenia 3 · DÚ1
Týždeň 15.-21.10. Jednoduché triedenia, binárne vyhľadávanie, zložitosť, znaky, switch
#Prednáška 6 · #Prednáška 7 · Cvičenia 4
Týždeň 22.-28.10. Reťazce, úvod do rekurzie
#Prednáška 8 · #Prednáška 9 · Cvičenia 5 · DÚ2
Týždeň 29.10.-4.11. Sviatky
Týždeň 5.-11.11. Prehľadávanie s návratom, Mergesort, Quicksort
#Prednáška 10 · #Prednáška 11 · Cvičenia 6 · DÚ3
Týždeň 12.-18.11. Smerníky, dynamické polia, práca s dvojrozmernými údajmi
#Prednáška 12 · #Prednáška 13 · Cvičenia 7
Týždeň 19.-25.11. Spájaný zoznam, hešovanie, práca s konzolou na spôsob jazyka C, úvod to textových súborov
#Prednáška 14 · #Prednáška 15 · Cvičenia 8 · DÚ4
Týždeň 26.11.-2.12. Pokračovanie textových súborov, binárne súbory, zásobník, rad
#Prednáška 16 · #Prednáška 17 · Cvičenia 9
Týždeň 3.-9.12. Vyfarbovanie, aritmetické výrazy, aritmetické stromy
#Prednáška 18 · #Prednáška 19 · Cvičenia 10
Týždeň 10.-16.12. Stromy vo všeobecnosti, binárne vyhľadávacie stromy, lexikografické stromy
#Prednáška 20 · #Prednáška 21 · Cvičenia 11
Týždeň 17.-23.12. Opakovanie, informácie ku skúške, nepreberané črty C a C++
#Prednáška 22 · #Prednáška 23 · Cvičenia 12

Obsah

Zimný semester, úvodné informácie

Základné údaje

Rozvrh

  • Prednášky: pondelok 9:50-11:20 F1-328 a streda 9:50-11:20 F1-328
  • Hlavné cvičenia: utorok 14:50 I-H6
  • Doplnkové cvičenia: streda 15:40 F1-248

Vyučujú

Konzultácie po dohode e-mailom.

Ak nemáte otázku na konkrétnu osobu, odporúčame vyučujúcich kontaktovať pomocou spoločnej adresy e-mailovej adresy E-prg.png. Odpovie vám ten z nás, kto má na starosti príslušnú otázku alebo kto má práve čas.

Ciele predmetu

  • Naučiť sa algoritmicky uvažovať, písať kratšie programy a hľadať v nich chyby, porozumieť existujúcemu kódu
  • Oboznámiť sa so základnými programovými a dátovými štruktúrami jazyka C resp. C++, nie je však nutne so všetkými črtami týchto jazykov
    • Cykly, podmienky, premenné a ich typy, funkcie a odovzdávanie parametrov, polia, smerníky, reťazce, súbory
  • Oboznámiť sa s niektorými základnými algoritmami a dátovými štruktúrami
    • Triedenia, spájané zoznamy, hašovacie tabuľky, stromy, aritmetické výrazy, rad a zásobník, rekurzia, prehľadávanie, vyfarbovanie
  • Aj štruktúry, ktoré sú hotové v C++ knižniciach, si budeme programovať sami, aby sme videli, čo sa za nimi skrýva

Literatúra

  • Predmet sa nebude striktne riadiť žiadnou učebnicou. Prehľad preberaných tém a stručné poznámky nájdete na stránke predmetu, doporučujeme Vám si na prednáškach a cvičeniach robiť vlastné poznámky.
  • Pri štúdiu Vám môžu pomôcť knihy o jazykoch C a C++, o programovaní všeobecne a o algoritmoch preberaných na prednáške. Tu je výber z vhodných titulov, ktoré sú k dispozícii na prezenčné štúdium vo fakultnej knižnici:
    • Prokop: Algoritmy v jazyku C a C++ praktický pruvodce, Grada 2008, I-INF-P-26
    • Sedgewick: Algorithms in C. Parts 1-4 I-INF-S-43/I-IV
    • Kochan: Programming in C, 2005 D-INF-K-7a
  • Referenčnú príručku k jazyku C++ nájdete napríklad na tejto webstránke: http://cplusplus.com/
  • Môže vás zaujímať aj video prednášok z iných škôl v angličtine

Priebeh semestra

  • Na prednáškach budeme preberať obsah predmetu. Prednášky budú štyri vyučovacie hodiny do týždňa.
  • Hlavné cvičenia budú dve vyučovacie hodiny do týždňa v počítačovej učebni a ich cieľom je aktívne si precvičiť učivo. Hlavnou náplňou cvičenia je riešenie zadaných príkladov individuálne alebo vo dvojiciach. Cvičiaci vám podľa potreby pomôže a poradí.
  • Príklady z hlavných cvičení, ktoré nestihnete vyriešiť, odporúčame dokončiť doma.
  • Okrem toho sa každý týždeň konajú doplnkové cvičenia (tiež dve vyučovacie hodiny). Sú silne odporúčané pre študentov, ktorí doteraz programovali málo alebo vôbec, ale radi uvidíme aj tých, ktorým robia problémy niektoré ťažšie časti učiva, napríklad rekurzia alebo smerníky. Na tomto cvičení s pomocou cvičiacich môžete dokončovať príklady z predchádzajúcich cvičení, pýtať sa otázky k učivu, prípadne pracovať na domácej úlohe.
  • Domáce úlohy budú cca 4 cez semester. Pracujte na nich samostatne doma, prípadne na doplnkových cvičeniach. Nechajte si na ne dosť času, nezačnite tesne pred termínom.
  • Príklady na cvičenia a domáce úlohy navrhujeme tak, aby vám ich riešenie pomohlo precvičiť si učivo, čím sa okrem iného pripravujete aj na záverečnú skúšku. Okrem tohto sú za tieto príklady body do záverečného hodnotenia. Najviac sa naučíte, ak sa vám príklad podarí samostatne vyriešiť, ale ak sa vám to napriek vášmu úsiliu nedarí, neváhajte sa spýtať o pomoc vyučujúcich. Možno s malou radou od nás sa Vám podarí úlohu spraviť.
  • Cieľom vyučujúcich tohto predmetu je vás čo najviac naučiť, ale musíte aj vy byť aktívni partneri. Ak vám na prednáške alebo cvičení nie je niečo jasné, spýtajte sa. V prípade problémov odporúčame navštíviť doplnkové cvičenia, alebo si dohodnúť konzultáciu. Môžete nám klásť tiež otázky emailom. Ak sa dostanete do väčších problémov s plnením študijných povinností, poraďte sa s vyučujúcimi alebo s tútorom, ako tieto problémy riešiť.

Celkové odporúčania

Prichádzajúci študenti v prvom ročníku majú veľmi rôznu úroveň znalosti programovania, v závislosti od toho, koľko sa mu venovali na strednej škole. Preto pre niektorých môže byť tento predmet veľmi ľahký, pre iných veľmi ťažký. Môže sa to zdať nespravodlivé, ale pokročilí študenti už nad programovaním strávili dlhé hodiny a začiatočníci ich bez určitej námahy nikdy nedobehnú. Veľmi radi vám však pomôžeme prekonať nástrahy tohto predmetu. Tu sú naše odporúčania podľa toho, aké znalosti už máte na začiatku semestra. Učebnú látku možno zhruba rozdeliť na základné programovacie konštrukty jazyka C resp. C++ a základné algoritmy, ktoré sa budú počas semestra striedať.

Úroveň znalostí Náročnosť látky: základy programovania v C Náročnosť látky: algoritmy, rekurzia Odporúčanie
Programovať viem len málo alebo vôbec ťažké ťažké Dôležité je začať usilovne pracovať už od začiatku semestra. Odporúčame chodiť aj na doplnkové cvičenia, ďalšie príklady riešiť doma. Neváhajte sa nás spýtať, ak vám niečo nie je jasné.
Som skúsený programátor, ale neovládam C ani C++ ľahké ťažké Aj keď prvé prednášky sa vám môžu zdať ľahké, sledujte učebnú látku, aby sa nestalo, že ste sa niektorými dôležitými vecami ešte nestretli. Nezabudnite robiť rozcvičky a domáce úlohy. Hlavne ale nezaspite na vavrínoch: už po pár týždňoch začneme preberať algoritmy a rekurziu, čo môžu byť pre vás ťažšie témy. Treba preto zamakať aj na tomto predmete a v prípade, že Vám učivo robí problémy, neváhajte prísť na doplnkové cvičenia.
Som skúsený programátor a ovládam C alebo C++ viem ťažké Podobne ako predchádzajúci riadok. Môžete si prípadne skúsiť napísať test pre pokročilých, môže sa vám podariť preskočiť zopár cvičení.
Som skúsený programátor a ovládam aj rekurziu a základné algoritmy (napr. z programátorských súťaží alebo rozšírenej výučby programovania na strednej škole) ľahké/viem ľahké/viem Aby ste sa nenudili riešením ľahkých príkladov, odporúčame test pre pokročilých a pokročilé domáce úlohy. Aj tak však potrebujte absolvovať skúšku, prípadne aj niektoré písomky, takže priebežne sledujte učivo a v prípade nejasností sa pýtajte.

Zimný semester, pravidlá

Známkovanie

  • 25% známky je na základe príkladov z cvičení
  • 15% známky je za domáce úlohy
  • 30% známky je za písomky počas semestra
  • 30% známky je za praktickú skúšku pri počítači

Pozor, body získavané za jednotlivé príklady nezodpovedajú priamo percentám záverečnej známky. Body za každú formu známkovania sa preváhujú tak, aby maximálny získateľný počet zodpovedal váham uvedených vyššie. Úlohy označené ako bonusové sa nerátajú do maximálneho počtu získateľných bodov v danej aktivite.

Stupnica

  • Na úspešné absolvovanie predmetu je potrebné splniť nasledovné tri podmienky:
    • Získať aspoň 50% bodov v celkovom hodnotení
    • Získať aspoň 50% z celkového súčtu písomiek
    • Na skúške úspešne odovzdať aspoň jeden z dvoch príkladov
  • Ak niektorú z týchto troch podmienok nesplníte, dostávate známku Fx.
  • V prípade úspešného absolvovania predmetu získate známku podľa bodov v celkovom hodnotení takto:
A: 90% a viac, B:80...89%, C: 70...79%, D: 60...69%, E: 50...59%

Príklady z cvičení

  • Na hlavnom cvičení bude zverejnených niekoľko príkadov. Príklady odovzdávate do automatického testovača. Ak úspešne prejdú všetkými testami, môžete za ne dostať body (podmienkou však je dodržať aj ďalšie pokyny v zadaní úlohy).
  • Jeden príklad, označený ako rozcvička, bude mať termín odovzdania počas hlavného cvičenia, neskôr teda zaňho body nedostanete.
  • Ďalšie príklady môžete odovzdávať až do začiatku ďalšieho cvičenia.
  • Na doplnkovom cvičení môže byť zadaná ešte jedna rozcvička za malý počet bonusových bodov.
  • Príklady z cvičení môžete robiť aj vo dvojicich. Príklad potom odovzdáva jeden člen dvojice a uvedie svojho partnera. Body dostanú obaja.
    • V prípade problémov môžu vyučujúci prácu vo dvojicich regulovať.
    • Jeden príklad vždy riešte s najviac jedným spolužiakom/spolužiačkou.
    • Na riešení pracujte spolu, obaja mu musia do detailov rozumieť. Ideálne je byť v dvojici s niekým na podobnej úrovni programátorských skúseností.
  • Ak v niektorom týždni nezískate žiadne body z príkladov z cvičení, dostanete za tento týždeň -5 bodov. Nakoľko každé cvičenia predstavujú materiál z dvoch prednášok, nie je rozumné celý týždeň preskočiť.

Domáce úlohy

  • Domáce úlohy sa budú tiež odovzdávať na testovači, budú však opravované ručne.
  • Plný počet bodov môžu dosť iba programy, ktoré prejdú všetkými testami, čiastočné body však môžete dosť aj za nedokončený program.
  • Budeme kontrolovať správnosť celkovej myšlienky, správnosť implementácie ale body môžete stratiť aj za neprehľadný štýl.
  • Domáce úlohy robte samostatne, nie v dvojiciach.

Písomné testy

  • Počas semestra budú 3 písomné testy (na papieri) v rozsahu 60 minút.
  • Pri teste nemôžete používať žiadne pomocné materiály (písomné ani elektronické) okrem povoleného ťaháku v rozsahu jedného listu formátu A4 s ľubovoľným obsahom na oboch stranách.
  • Predbežné termíny (ešte potvrdíme):
    • 1. test streda 17.10. 18:10 (opravná v týždni od 5.11.)
    • 2. test streda 21.11. 18:10 (opravná cez skúškové)
    • 3. test streda 12.12. 18:10 (opravná cez skúškové)

Skúška

  • Na skúške budete riešiť 2 úlohy pri počítači v celkovom trvaní 2 hodiny.
  • Na skúške nemôžete používať žiadne pomocné materiály okrem povoleného ťaháku v rozsahu jedného listu formátu A4 s ľubovoľným obsahom na oboch stranách. Nebude k dispozícii ani internet. Budete používať rovnaké programátorské prostredie ako na cvičeniach.
  • Na skúške budú úlohy automaticky testované podobne ako domáce úlohy. Aspoň jedna úloha musí správne prejsť cez všetky testy, inak má študent z daného termínu skúšky známku Fx.
  • Po skončení skúšky sa koná krátky ústny pohovor s vyučujúcimi, počas ktorého prediskutujeme programy, ktoré ste odovzdali a uzavrieme vašu známku.
  • Bližšie informácie o skúške poskytneme koncom semestra.

Neprítomnosť a opravné termíny

  • Domáce úlohy a príklady z cvičení je potrebné odovzdať do určeného termínu. Neskoršie odovzdané riešenia nebudú braté do úvahy ak nezískate výnimočné predĺženie termínu od vyučujúcich.
  • Účasť na hlavných cvičeniach veľmi silne odporúčame a v prípade neprítomnosti stratíte body z rozcvičky. Väčšiu časť bodov môžete získať aj riešením príkladov doma.
  • Ak zo závažných dôvodov (napr. zdravotných) nemôžete prísť na písomku, skúšku resp. načas odovzdať domácu úlohu či príklady z cvičení, kontaktujte vyučujúcich emailom. Treba tak spraviť čím skôr, nie až spätne cez skúškové. Môžeme požadovať potvrdenku od lekára.
  • Každý písomný test má jeden opravný termín.
    • Ak sa zúčastníte opravného termínu, strácate body z predchádzajúceho termínu, aj keby ste na opravnom získali menej bodov.
    • Opravné termíny testov môžu byť aj cez skúškové.
  • Opakovanie skúšky sa riadi študijným poriadkom fakulty. Máte nárok na dva opravné termíny (ale len v rámci termínov, ktoré sme určili).
    • Ak po skúške pri počítači máte nárok na známu E alebo lepšiu, ale chceli by ste si známku ešte opraviť, musíte sa dohodnúť so skúšajúcimi pred zapísaním známky do indexu.
    • Ak po skúške pri počítači ešte opravujete písomku, je potrebné prísť uzavrieť a zapísať známku v termíne určenom vyučujúcimi.

Opisovanie

  • Máte povolené sa so spolužiakmi a ďalšími osobami rozprávať o domácich úlohách a stratégiách na ich riešenie. Kód, ktorý odovzdáte, musí však byť vaša samostatná práca. Je zakázané ukazovať svoj kód spolužiakom. Domáce úlohy môžu byť kontrolované softvérom na detekciu plagiarizmu.
  • Podobne pri riešení príkladov z cvičení pracujte buď samostatne alebo v dvojici.
  • Tiež je zakázané opisovať kód z literatúry alebo z internetu (s výnimkou webstránky predmetu). Pri práci môžete používať webstránky s popisom programovacieho jazyka, nesnažte sa však nájsť priamo riešenie zadaného príkladu.
  • Počas testov a skúšok môžete používať iba povolené pomôcky a nesmiete komunikovať so žiadnymi osobami okrem vyučujúcich.
  • Ak nájdeme prípady opisovania, všetci zúčastnení študenti získajú za príslušnú domácu úlohu 0 bodov (aj študenti, ktorí dali spolužiakom odpísať). Opakované alebo obzvlášť závažné prípady opisovania alebo porušovania pravidiel predmetu budú podstúpené na riešenie disciplinárnej komisii fakulty.
  • Za závažné porušenie pravidiel budeme považovať aj akýkoľvek pokus narušiť činnosť testovača riešení.

Osobné stretnutia

  • Vyučujúci vás môžu vyzvať emailom, aby ste prišli na stretnutie ohľadom príkladov, ktoré ste riešili a odovzdali mimo času cvičení (domáce úlohy, príklady z cvičení)
  • Na tomto stretnutí im vysvetlíte, ako ste príklad riešili
  • Ak ste príklad riešili vo dvojici, osobné stretnutie má každý zvlášť, každý by mal rozumieť celému riešeniu
  • Stretnutia sa budú konať počas doplnkových cvičení alebo po dohode v inom čase
  • Ak na stretnutie neprídete alebo nebudete vedieť svoj program vysvetliť, stratíne zaňho body

Možnosti pre pokročilých programátorov

  • Študenti, ktorí už ovládajú väčšiu časť učiva na tento semester, majú možnosť získať známku zmysluplnejším spôsobom, ako riešením ľahkých príkladov počas semestra.

Test pre pokročilých

  • V druhom týždni semestra sa bude konať nepovinný test pre pokročilých.
  • Ak na test prídete a napíšete ho na menej ako 50%, nezískate žiadne výhody (ako keby ste na test ani neprišli).
  • V opačnom prípade za každých celých získaných 10% získavate 100% bodov z jedných cvičení (bez bonusov). Napr. ak ste získali 59% z testu, dostanete plný počet bodov z prvých 5 bodovaných cvičení po opravení testu. Tieto body nie je možné presúvať na iné termíny cvičení a z týchto uznaných cvičení už nemôžete získať ďalšie body.
  • Navyše budete mať uznané aj niektoré semestrálne písomné testy nasledovne:
    • 50-75% z testu: prvý test za plný počet bodov, ostatné píšete so spolužiakmi
    • 75-90% z testu: dva písomné testy za plný počet bodov (vyučujúci určia, ktoré dva), jeden test píšete so spolužiakmi
    • 90-100% z testu: zo všetkých 3 semestrálnych testov dostanete plný počet bodov

Domáce úlohy pre pokročilých

  • Pokročilí programátori môžu namiesto bežných domácich úloh získať body za úlohy vyriešené na predmete Rýchlostné programovanie (1)
  • Rýchlostné programovanie je určené na precvičenie programovania, algoritmov a hlavne ako príprava na programátorské súťaže. Úlohy na tomto predmete vyžadujú aj znalosti nepreberané na prednáškach z Programovania.
  • Tie isté úlohy môžete použiť aj do hodnotenia Programovania aj Rýchlostného.
  • Do hodnotenia Programovania (1) môžete započítať iba tie úlohy z Rýchlostného, ktoré vyriešite v jazyku C alebo C++ a počas semestra, t.j. najneskôr 21.12.
  • Aby ste mohli namiesto bežných úloh robiť pokročilé, potrebujete povolenie od vyučujúcich. Všetci, čo napíšu test pre pokročilých aspoň na 50%, toto povolenie automaticky majú. Ostatní kontaktujte vyučujúce emailom a popíšte svoje programátorské skúsenosti.
  • Ak chcete namiesto úloh použiť Rýchlostné (a máte povolenie), najneskôr 12.10. odovzdajte na našom testovači do špeciálnej úlohy Rýchlostné textový súbor, ktorý bude obsahovať vašu prezývku používanú na serveri Rýchlostného programovania.
  • Potom najneskôr 21.12. odovzdajte novú verziu tohto súboru, ktorá bude obsahovať vašu prezývku a zoznam všetkých príkladov z Rýchlostného, ktoré chcete započítať ako DÚ na Programovaní. Musia to byť len príklady vyriešené jazyku C alebo C++. Pri každom príklade napíšte, koľko za neho máte bodov na Rýchlostnom (1 alebo 2 body).
  • Body z Rýchlostného sa prepočítajú na body z DÚ nasledovne:
    • Rýchlostné aspoň 6 bodov: 50% bodov z DÚ, t.j. 7.5% do celkového hodnotenia predmetu
    • Rýchlostné aspoň 8 bodov: 100% bodov z DÚ, t.j. 15% do celkového hodnotenia predmetu
    • Rýchlostné aspoň 12 bodov: 110% bodov z DÚ, t.j. 16.5% do celkového hodnotenia predmetu
    • Rýchlostné aspoň 16 bodov: 120% bodov z DÚ, t.j. 18% do celkového hodnotenia predmetu
  • Body z bežných DÚ a z Rýchlostného programovania nie je možné sčítať. Ak do 21.12. neodovzdáte zoznam príkladov, započítame vám body z odovzdaných domácich úloh. Ak odovzdáte neprázdny zoznam príkladov, použijeme ten.

Nepreberané črty jazykov C a C++

  • Z jazykov C a C++ uvidíme len malú časť.
  • Preberané črty týchto jazykov je potrebné ovládať, pre vlastnú potrebu si však môžete v literatúre doštudovať aj ďalšie užitočné príkazy, knižnice a konštrukty.
  • Ak je v zadaní uvedené, aké prostriedky máte použiť, držte sa týchto pokynov.
  • V opačnom prípade môžete použiť aj nepreberané črty. Aby ste sa vyhli problémom pri opravovaní, je vhodné ich doplniť vysvetľujúcim komentárom.
  • Vždy používajte len štandardné súčasti jazykov C a C++ , nie špeciálne knižnice. (Výnimkou sú samozrejme knižnice poskytnuté vyučujúcimi).
  • Vaše programy by mali fungovať na testovači bez zvláštnych nastavení kompilátora a pod.

Zimný semester, skúška

Na tejto stránke budú postupne pribúdať informácie týkajúce sa praktickej skúšky pri počítači v zimnom semestri. Odporúčame tiež si preštudovať pravidlá predmetu.

Termíny

Termíny skúšok

  • pondelok 7.1. 9:00 H6 riadny termín
  • utorok 22.1. 9:00 H6 riadny termín / 1. opravný termín
  • pondelok 4.2. 9:00 H6 1. opravný termín / 2. opravný termín
  • 2. opravný termín v poslednom týždni skúškového (presný dátum neskôr)

Termín opravných písomiek 2 a 3

  • dohodnete na prednáške v stredu 19.12.

K zapisovaniu na skúšky

  • Na termín skúšky sa zapisujte v systéme AIS.
    • Prihlasovanie/odhlasovanie na skúšku najneskôr 24 hodín pred skúškou
    • Ak by ste sa potrebovali prihlásiť / odhlásiť neskôr, pošlite email na prog@... adresu
  • Na skúšku môžete ísť, aj keď ešte nemáte úspešne absolvovaný test (ale kým nespravíte test, nezapíšeme vám známku).
  • Celkovo budú štyri termíny, každý sa môže zúčastniť na najviac troch z nich, ďalšie termíny neplánujeme pridávať

Pokyny k skúške

  • Prineste si ISIC a index, písacie potreby na písanie pracovných poznámok, ťahák v rozsahu jedného listu A4. Žiadne ďalšie pomôcky nie sú povolené
  • Stretávame sa vždy desať minút pred začiatkom skúšky pred počítačovou miestnosťou, kde sa dozviete pokyny a rozsadenie
  • Skúška: 2 hodiny práca pri počítačoch
    • Za špeciálnych pravidiel môže byť čas predĺžený o 30 minút, viď pravidlá nižšie
    • Počas skúšky vám nebudeme pomáhať hľadať chyby vo vašom programe. Môžete sa však spýtať na nejasnosti v zadaní. Dajte nám tiež vedieť v prípade technických problémov alebo ak si myslíte, že v zadaní / kostre / vstupoch je chyba.
  • Poobede vyhodnotenie u prednášajúcich, zapisovanie známok
    • Príďte, aj ak ste skúšku nespravili, môžeme vám poradiť, čo robiť inak na opravnom termíne.
    • Ak z vážnych dôvodov nemôžete poobede prísť, dohodnite si stretnutie v inom čase (už pred skúškou alebo hneď po nej)

Technické detaily

  • Skúška bude v Linuxe, rovnaké prostredie ako na cvičeniach
  • Odovzdávanie prostredníctvom špeciálnej verzie testovača
  • Okrem testovača nebude k dispozícii internet
  • Budete používať špeciálne skúškové konto, takže nebudete mať k dispozícii žiadne svoje súbory alebo nastavenia
  • Pri reštarte počítača sa stratia všetky súbory, používajte testovač ako zálohu (odovzdajte aj nedokončený program)
  • Môžete použiť Netbeans, Kate, valgrind ale aj iné nástroje, ktoré bežia v Linuxe v učebniach. Prípadné problémy s použitím iného softvéru vám však nebudeme pomáhať riešiť
  • Nebudeme používať SVGdraw
  • Môžete používať aj črty C/C++, ktoré sme nebrali. Používajte len štandardné súčasti jazyka. Vaše programy by mali fungovať na testovači bez zvláštnych nastavení kompilátora a pod.
    • Odporúčame používať iba tie časti jazyka, s ktorými máte dostatočné skúsenosti. Príkazy, ktoré si nepamätáte, si dajte na ťahák.

Opravné termíny

  • Opakovanie skúšky sa riadi študijným poriadkom fakulty. Máte nárok na dva opravné termíny (ale len v rámci termínov, ktoré sme určili).
  • Ak po skúške pri počítači máte nárok na známku E alebo lepšiu, ale chceli by ste si známku ešte opraviť, musíte sa dohodnúť so skúšajúcimi pred zapísaním známky do indexu.
  • Ak po skúške pri počítači píšete opravnú písomku, je potrebné prísť uzavrieť a zapísať známku v termíne určenom vyučujúcimi.
  • Ak sa zo závažných dôvodov (napr. zdravotných, alebo konflikt s inou skúškou) nemôžete zúčastniť termínu skúšky alebo písomky, dajte o tom vyučujúcim vedieť čím skôr.

Príklady

Na skúške budete riešiť dva príklady za rovnaký počet bodov

Prvý príklad

  • V prvom príklade budete mať za úlohu samostatne napísať celý program, ktorý rieši zadanú úlohu. Typicky bude treba načítať dáta, spracovať ich a vypísať výsledok.
  • V tomto príklade môžete použiť ľubovoľný postup.
  • Budú však zakázané polia pevných veľkostí. Polia alokujte dynamicky cez new, alebo použite štruktúry, ktoré menia veľkosť podľa potreby (vector). Alokovanú pamäť odalokujte.
  • Predtým ako začnete programovať, si poriadne rozmyslite, aké dátové štruktúry (polia, matice, struct-y a pod.) chcete v programe použiť.

Druhý príklad

  • V druhom príklade dostanete kostru programu, pričom vašou úlohou bude doprogramovať niektoré funkcie.
  • V tomto príklade môžete mať v zadaní predpísaný spôsob, ako máte niektoré časti naprogramovať.
  • Budú sa vyžadovať aj zložitejšie časti učiva, ako napríklad zoznamy, stromy a rekurzia.

Hodnotenie

Aby ste mali šancu úspešne ukončiť predmet, aspoň jeden z príkladov vám musí prejsť všetky testy na testovači

  • Túto podmienku nebudeme považovať za splnenú, ak váš program nerieši zadanú úlohu (t.j. jeho myšlienka nie je v zásade správna)
  • Podmienku však považujeme za splnenú, ak váš program prejde všetky vstupy, má v zásade správnu myšlienku, ale nedostane plný počet bodov napríklad kvôli chýbajúcemu uvoľneniu pamäte, statickým poliam, menšej chybe, ktorá sa neprejavila na daných vstupoch a pod.
  • Dobre si rozmyslite, s ktorým príkladom chcete začať a snažte sa ho dokončiť, kým nedostanete OK na testovači. Potom ho môžete ešte vylepšovať alebo sa snažiť vyriešiť aspoň časť príkladu, ktorý ste ešte neriešili.

Bodové hodnotenie

  • V prvom rade budeme hodnotiť správnosť myšlienky vášho programu. Predtým, ako začnete programovať, si dobre rozmyslite, ako budete úlohu riešiť.
  • Ďalej je veľmi dôležité, aby sa program dal skompilovať (v štandardnom prostredí) a aby správne fungoval na všetkých vstupoch spĺňajúcich podmienky v zadaní.
  • V druhej úlohe budeme jednotlivé funkcie hodnotiť zvlášť, takže môžete získať čiastočné body, ak ste niekoľko funkcií napísali správne.
  • Na hodnotenie môže mať menší vplyv aj úprava a štýl programu (komentáre, mená premenných, odsadzovanie, členenie dlhšieho programu na funkcie,...)
  • Na tejto skúške nezáleží na rýchlosti vášho programu. Radšej napíšte jednoduchý, prehľadný a hlavne správny pomalší program, než rýchlejší, ale zbytočne zložitý, či nesprávny.

Predĺženie času

  • Ak v riadnom čase 2 hodiny nemáte na testovači OK ani z jedného príkladu, zo skúšky by ste mali dostať Fx.
  • Dovolíme vám však predĺžiť čas skúšky o najviac 30 minút.
  • Ak zostanete na predĺženie, budeme vám rátať do výsledku body iba z jedného príkladu. Konkrétne z toho, za ktorý ste dostali OK na testovači (ak z oboch, tak z toho, za ktorý ste mali OK skôr)

Ukážkové príklady na skúšku pri počítači

Niektoré ukážkové príklady na skúšku budú k dispozícii na testovači, môžete si ich v rámci tréningu vyriešiť a odovzdať. Pre realistickejší tréning si vždy prečítajte zadanie tesne predtým, ako príklad začnete riešiť, aby ste odhadli, koľko času vám príklad zaberie vrátane čítania zadania a rozmýšľania nad riešením.

Netbeans

Na cvičeniach a skúške odporúčame používať programátorské prostredie Netbeans (popísaný tu) alebo editor #Kate

Nevyžadujeme striktne, aby ste na tomto predmete používali Netbeans/Kate, ale

  • k iným prostrediam vám nemusíme vedieť poradiť, ako ich používať
  • na skúške budete môcť používať iba tie programy, ktoré sú k dispozícii v učebniach na fakulte v Linuxe, pričom nebude k dispozícii internet

Ako spustiť Netbeans v učebni

  • Po spustení počítača zvoľte Linux a prihláste sa pomocou toho istého mena a hesla, ako používate v systéme AIS
  • V ľavom dolnom rohu obrazovky je menu s ponukou programov, v oddelení Development nájdete Netbeans

Základy práce v Netbeans

Vytvorenie nového projektu

  • Každý program v Netbeans potrebuje svoj "projekt", čo je adresár so všetkými potrebnými súbormi
  • V menu zvoľte File, potom New project
  • V Categories zvoľte C/C++, v Project: C/C++ Application
  • Na ďalšej obrazovke projekt nejako nazvite a zvoľte do akého adresára sa má uložiť (Project Location). Odporúčame použiť cestu na svieťovom disku net, ku ktorému máte prístup zo všetkých počítačov v učebniach, napríklad v adresári /home/x/vasemeno/net/NetBeansProjects
  • Stlačte Finish

Editovanie programu

  • V ľavej časti okna máte panel Projects, v ktorom nájdite projekt, ktorý ste práve vytvorili.
  • V projekte rozbaľte Source Files a nájdete tam main.cpp, ktorý si dvojitým kliknutím otvoríte v editore. Jeho obsah môžete modifikovať alebo celý zmazať a nahradiť programom z prednášky.
  • Súbor main.cpp nezabudnite uložiť (menu File, Save, alebo Ctrl-S)

Kompilovanie a spúšťanie

  • V menu Run zvoľte Build main project (alebo klávesa F11 alebo ikonka kladivka na lište), program sa skompiluje. Prípadné chyby sa objavia v dolnej časti okna.
  • V menu Run zvoľte Run main project (alebo klávesa F6 alebo ikonka zelenej šípky na lište), program sa spustí.

Prenášanie programov a odovzdávanie domácich úloh

  • Pri odovzdávaní domácich úloh odovzdávajte súbor main.cpp s vašim programom (prípadne ďalšie súbory, ak to vyžaduje zadanie). Tento súbor nájdete v adresári net/NetBeansProjects/menoprojektu
  • Ak pracujete na rôznych počítačoch v rámci FMFI učební, svoje projekty si ukladajte na sieťovom disku net
  • Dáta zo sieťového disku si môžete stiahnuť v učebni na USB kľúčik, alebo aj cez sieť z domu prihlásením sa na študentský Linuxový klaster daVinci (davinci.fmph.uniba.sk). Na prenos dát môžete použiť napríklad windowsovský program winscp
  • Ak chcete prenášať projekt medzi rôznymi počítačmi, odporúčame skopírovať iba main.cpp, prípadne ďalšie potrebné súbory.
    • Na druhom počítači vytvoríte nový projekt, nakopírujete main.cpp do jeho adresára.
    • Potom pridáte main.cpp do projektu takto: kliknite pravým tlačidlom na Source Files v paneli Projects, zvoľte Add Existing Item

Práca v Netbeans s grafickou knižnicou SVGdraw

  • Stiahnite si knižnicu
  • V Netbeans si vytvorte nový projekt
    • Do adresára s projektom (väčšinou NetBeansProjects/meno_projektu) nakopírujte stiahnuté súbory SVGdraw.h a SVGdraw.cpp
  • V NetBeans kliknite pravým tlačidlom na Source Files v paneli Projects, zvoľte Add Existing Item a zvoľte súbor SVGdraw.cpp (môžete pridať aj SVGdraw.h, ale nie je to nutné)
  • Do súboru main.cpp potom píšte program používajúci knižnicu, pričom na prvom riadku uvediete #include "SVGdraw.h"
  • Po spustení programu sa vám SVG súbor s obrázkom vytvorí v adresári so súbormi projektu (t.j. NetBeansProjects/meno_projektu)
    • Knižnica vie vytvárať aj animované obrázky, tie však väčšina prehliadačov nevie správne zobraziť. Odporúčame ich otvoriť v internetovom prehliadači, napr. vo firefoxe.

Práca s Netbeans na vlastnom počítači

Linux

Ak máte na počítači operačný systém Linux, budete potrebovať nainštalovať nasledujúce softvérové balíčky:

  • Prostredie netbeans
  • Kompilátor g++
  • Debuger gdb

Všetky tieto balíčky existujú napríklad v distribúcii Ubuntu. Ak vo vašej distribúcii nie je k dispozícii balíček pre Netbeans, stiahnite si ho zo stránky http://netbeans.org/downloads/

Po nainštalovaní týchto balíčkov spustite Netbeans, v menu Tools zvoľte Plugins a pridajte si plugin C/C++ (možno bude potrebné v záložke Settings okna Plugins zvoliť zdroje pluginov a stlačiť Reload Catalog).

Windows

Ak máte na počítači operačný systém Windows, je možné tiež si nainštalovať Netbeans a kompilátor C++, postupujte podľa návodu https://netbeans.org/community/releases/80/cpp-setup-instructions.html

Ak máte Windows, ale chceli by ste si vyskúšať aj prácu v Linuxe, odporúčame si nainštalovať Linux do virtuálneho počítača, napr. pomocou programu VirtualBox.

Ak hľadáte alternatívu k Netbeans pre Windows, môžete skúsiť napríklad

On-line prostredia

Ak máte problémy nainštalovať na svoj počítač vhodné prostredie na programovanie, môžete skúsiť webstránky, ktoré vám umožňujú písať, kompilovať a spúšťať jednoduché programy.

Tieto stránky nebudete môcť používať na skúške.

Kate

Kate je úplne základný textový editor, ponúka však dostatok špeciálnych nastavení, aby sa s v nej pohodlne písali jednoduché programy (nie je však úplne vhodný na väčšie projekty).

Ako spustiť Kate v učebni

  • Prihláste sa do Linuxu rovnakým menom a heslom, aké používate v AISe
  • V menu s ponukou programov nájdite Kate (v časti Utilities), alebo stlačte ALT+F2 a napíšte Kate
  • Odporúčame sedieť vždy pri tom istom počítači, máte uložené nastavenia

Vytvorenie nového programu

  • File -> New (Ctrl+N) vytvorí nový textový súbor
  • uložte ho pomocou File -> Save (Ctrl+S), bude od vás žiadať nejaké meno a môžete si zvoliť kam, bude daný súbor umiestnený, nazvať si ho môžeme napr. program.cpp
  • dôležité je pridať koncovku .cpp , vďaka nej Kate vie, že chcete programovať v C++ a mal by vám automaticky zapnúť C++ zvýrazňovanie (ktoré je veľmi praktické)

Nastavenia editora

Na programovanie odporúčame spraviť / skontrolovať nasledujúce nastavenia (keď máte otvorený .cpp súbor)

  • automatické zvýrazňovanie Tools -> Highlighting -> Sources, malo by byť zaškrtnuté C++
  • automatické C++ odsádzanie Tools -> Indentation malo by byť zaškrtnuté C Style
  • zobrazovanie terminálu
    • Settings -> Configure Kate -> Plugins a tam zaškrtnite Plugin s terminálom
    • View -> Tool Views a zaškrtnite Show Terminal
    • Tools -> Synchronize Terminal with Current Document
  • Tools -> align vám preformátuje vybranú časť programu

Kompilovanie a spustenie programu

  • Kate nemá vstavané kompilovanie ani spúšťanie (keďže je to textový editor), preto na to treba používať terminál (textové príkazy
  • v Kate viete mať priamo otvorenú lištu s terminálom, čo je veľmi praktické, mala by sa nachádzať dole pod textovým oknom (prípade kliknite na malú ikonku Terminal)
  • Kliknite do okna s terminálom, aby sa stalo aktívnym
  • v termináli sa treba dostať do priečinku s vaším súborom
    • buď sa to stane automaticky vďaka nastaveniu Tools -> Synchronize Terminal with Current Document
    • alebo použite príkazy nižšie
  • Ak sa nachádzate v priečinku, v ktorom sa nachádza váš .cpp program, môžete ho pomocou konzoly kompilovať a spúšťať
  • Príkaz make meno_suboru_bez_koncovky - napíšeme meno súboru, ale bez koncovky, v tom istom priečinku sa vytvorí súbor meno_suboru_bez_koncovky, čo bude spustiteľný program (linuxový ekvivalent .exe)
  • Príkaz g++ program.cpp -o program - vytvorí to isté ako príkaz pred tým, akurát vieme nastavovať parametre g++
  • Príkaz ./spustitelny_subor - spustí daný program v priečinku, ak mal niečo vypísať, vypíše to do konzoly, ak mal niečo čítať, načítava to tiež z konzoly (ak nie je povedané inak)

Ďalšia práca s teminálom

  • V termináli by ste mali vidieť svoje meno, nejaké ďalšie veci a potom :~$ za ktorým kliká kurzor
  • časť za : vám hovorí, v ktorom priečinku sa nachádzate, ~ je domový priečinok
  • príkaz ls vypíše zoznam súborov a priečinkov v priečinku, v ktorom ste (skratka z list)
  • príkaz cd meno_priečinka - presunie sa do priečinka s daným menom, ak sa taký priečinok nachádza v aktuálnom priečinku (skratka z change directory)
  • príkaz cd .. - posuniete sa o jeden priečinok vyššie
  • ak budete pri písaní mena priečinka/súboru stláčať Tabulátor, bude sa vám to snažiť automaticky doplniť hľadaný súbor, ak je možností viac, doplní čo najviac znakov, ktoré sú rovnaké
  • šípkou hore a dole listujete v histórii príkazov a stlačením Enter ho môžete spustiť znovu

Práca v Kate s grafickou knižnicou SVGdraw

  • stiahnite si knižnicu
    • Stiahnuté súbory SVDdraw.cpp a SVGdraw.h si uložte do priečinku, v ktorom máte svoje programy
  • do vlastného programu potom musíte na začiatok pridať riadok #include "SVGdraw.h"

Kompilácia s grafickou knižnicou SVGdraw

  • kompilátor potrebuje vedieť, že váš program chce používať funkcie z iného súboru (SVGdraw.cpp) a preto mu musíte povedať, aby ich nalinkoval. Obyčajný make fungovať nebude
  • použite príkaz g++ program.cpp SVGdraw.cpp -o program
  • vytvorí sa vám súbor program, ktorý môžete normálne spustiť pomocou ./program
  • v priečinku s programom sa vytvorí súbor s príponou .svg, ktorý si môžete pozrieť napr. v internetových prehliadačoch firefox alebo chrome

Valgrind

  • C-čko pri použití polí a smerníkov nekontroluje, či ich používame správne
  • Chybou v programe sa nám teda ľahko môže stať, že čítame alebo píšeme mimo alokovanej pamäte
  • Takéto chyby sa niekedy ťažko hľadajú
  • V Linuxe nám na hľadanie takýchto chýb pomôže nástroj valgrind, ktorý môžete použiť aj na skúške
  • Vo Windows môžete použiť Dr. Memory

Spustenie programu v nástroji valgrind pri použití Netbeans

  • Keď v Netbeans spustíme nástroj Build (ikonka kladivka; spúšťa sa tiež automaticky pred spustením programu), Netbeans zavolá kompilátor a vytvorí spustiteľný súbor
  • Tento spustiteľný súbor nájdeme v adresári typu NetBeansProjects/meno_projektu/dist/Debug/GNU-Linux-x86/, volá sa rovnako ako projekt
  • V Linuxe si ho môžeme na príkazovom riadku spustiť aj mimo prostredia Netbeans, stačí napísať NetBeansProjects/meno_projektu/dist/Debug/GNU-Linux-x86/meno_projektu
  • Namiesto toho ho môžeme spustiť valgrind NetBeansProjects/meno_projektu/dist/Debug/GNU-Linux-x86/meno_projektu
  • Nástroj valgrind bude náš program pozorne sledovať a keď robí divné veci v pamäti, vypíše nám o tom správu

Spustenie programu v nástroji valgrind pri použití Kate

  • Na príkazovom riadku v editore Kate spúšťate váš program príkazom typu ./prog, kde prog.cpp je meno vášho súboru
  • Namiesto toho napíšete valgrind ./prog
  • Nástroj valgrind bude náš program pozorne sledovať a keď robí divné veci v pamäti, vypíše nám o tom správu
  • Aby boli tieto správy zrozumiteľnejšie (obsahovali čísla riadkov), lepšie je skompilovať program s prepínačom -g
  • Namiesto make prog teda napíšete g++ -g prog.cpp -o prog alebo ešte lepšie je zapnúť si aj varovania kompilátora
g++ -g -Wall prog.cpp -o prog

Ukážky chýb a výsledok z valgrind

Neinicializovaná premenná

Nasledujúci program vypisuje neinicializovanú premennú i

#include <iostream>
using namespace std;
int main(void) {
    int i; cout << i << endl;
}

Valgrind vypíše okrem iného

==25895== Conditional jump or move depends on uninitialised value(s)
==25895==    at 0x4F3CCAE: std::ostreambuf_iterator<char, std::char_traits<char> > std::num_put<char, std::ostreambuf_iterator<char, std::char_traits<char> > >::_M_insert_int<long>(std::ostreambuf_iterator<char, std::char_traits<char> >, std::ios_base&, char, long) const (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==25895==    by 0x4F3CEDC: std::num_put<char, std::ostreambuf_iterator<char, std::char_traits<char> > >::do_put(std::ostreambuf_iterator<char, std::char_traits<char> >, std::ios_base&, char, long) const (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==25895==    by 0x4F493F9: std::ostream& std::ostream::_M_insert<long>(long) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==25895==    by 0x40082C: main (prog.cpp:4)
==25895== 
==25895== Use of uninitialised value of size 8
==25895==    at 0x4F3BB13: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==25895==    by 0x4F3CCD9: std::ostreambuf_iterator<char, std::char_traits<char> > std::num_put<char, std::ostreambuf_iterator<char, std::char_traits<char> > >::_M_insert_int<long>(std::ostreambuf_iterator<char, std::char_traits<char> >, std::ios_base&, char, long) const (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==25895==    by 0x4F3CEDC: std::num_put<char, std::ostreambuf_iterator<char, std::char_traits<char> > >::do_put(std::ostreambuf_iterator<char, std::char_traits<char> >, std::ios_base&, char, long) const (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==25895==    by 0x4F493F9: std::ostream& std::ostream::_M_insert<long>(long) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==25895==    by 0x40082C: main (prog.cpp:4)

Dôležitá informácia je, že chyba nastala na riadku 4 v programe prog.cpp

Neinicializovaný smerník

Nasledujúci program zapisuje do pamäte, na ktorú ukazuje smerník s neinicializovnaou hodnotou

#include <iostream>
using namespace std;
int main(void) {
    int *p; 
    *p = 7; 
    cout << *p << endl;
}
==25923== Use of uninitialised value of size 8
==25923==    at 0x400822: main (prog.cpp:5)
==25923== 
==25923== Invalid write of size 4
==25923==    at 0x400822: main (prog.cpp:5)
==25923==  Address 0x0 is not stack'd, malloc'd or (recently) free'd
==25923== 
==25923== 
==25923== Process terminating with default action of signal 11 (SIGSEGV)
==25923==  Access not within mapped region at address 0x0
==25923==    at 0x400822: main (prog.cpp:5)
==25923==  If you believe this happened as a result of a stack
==25923==  overflow in your program's main thread (unlikely but
==25923==  possible), you can try to increase the size of the
==25923==  main thread stack using the --main-stacksize= flag.
==25923==  The main thread stack size used in this run was 8388608.

Chybné odalokovanie

Tento program sa pokúša odalokovať pamäť, ktorá nebola alokovaná

#include <iostream>
using namespace std;
int main(void) {
    int i = 7; 
    int *p = &i; 
    delete p;
}
==25952== Invalid free() / delete / delete[] / realloc()
==25952==    at 0x4C2F24B: operator delete(void*) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==25952==    by 0x4007A7: main (prog.cpp:6)
==25952==  Address 0xfff0002bc is on thread 1's stack
==25952==  in frame #1, created by main (prog.cpp:3)

Písanie za koniec poľa

Ani valgrind nemusí nájsť všetky chyby, napr. tu sa k premennej správame ako ku poľu a píšeme mimo, ale valgrind si to nevšimne:

#include <iostream>
using namespace std;
int main(void) {
    int i;
    int *p = &i; 
    for(int j = 0; j<2; j++) {
      p[j] = j;
    }
}

Ak index 2 nahradíme 200, valgrind už vypíše chybu...

  • Celkovo valgrind lepšie deteguje chyby týkajúce sa dynamickz alokovanej pamäte (pomocou new)

Hľadanie neodalokovanej pamäte

Valgrind nám tiež môže pomôcť nájsť pamäť, ktorú sme alokovali cez new, ale zabudli odalokovať cez delete alebo delete[].

  • V programe nižšie máme dve volanie new, ku ktorým chýba odalokovanie
#include <iostream>
using namespace std;
int main(void) {
  int n = 100;
  int *a = new int[n];
  double *b = new double;
  for(int i = 0; i<n; i++) {
    a[i] = i;
  }
}

Valgrind vypíše

==1785== Memcheck, a memory error detector
==1785== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==1785== Using Valgrind-3.12.0.SVN and LibVEX; rerun with -h for copyright info
==1785== Command: ./prog
==1785== 
==1785== 
==1785== HEAP SUMMARY:
==1785==     in use at exit: 408 bytes in 2 blocks
==1785==   total heap usage: 3 allocs, 1 frees, 73,112 bytes allocated
==1785== 
==1785== LEAK SUMMARY:
==1785==    definitely lost: 408 bytes in 2 blocks
==1785==    indirectly lost: 0 bytes in 0 blocks
==1785==      possibly lost: 0 bytes in 0 blocks
==1785==    still reachable: 0 bytes in 0 blocks
==1785==         suppressed: 0 bytes in 0 blocks
==1785== Rerun with --leak-check=full to see details of leaked memory
==1785== 
==1785== For counts of detected and suppressed errors, rerun with: -v
==1785== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
  • Vidíme teda, že počas beho programu sa alokovali 3 kusy pamäte a iba 1 sa odalokoval
  • Z toho jedno alokovanie bolo v štandardnej knižnici, ale tie ďalšie dve sú naše a bolo by pekné ich odalokovať
  • Podľa pokynov programu spustíme valgrind --leak-check=full ./prog
==2085== HEAP SUMMARY:
==2085==     in use at exit: 408 bytes in 2 blocks
==2085==   total heap usage: 3 allocs, 1 frees, 73,112 bytes allocated
==2085== 
==2085== 8 bytes in 1 blocks are definitely lost in loss record 1 of 2
==2085==    at 0x4C2C21F: operator new(unsigned long) (vg_replace_malloc.c:334)                                                                                                                                   
==2085==    by 0x10890C: main (prog.cpp:6)                                                                                                                                                                        
==2085==                                                                                                                                                                                                          
==2085== 400 bytes in 1 blocks are definitely lost in loss record 2 of 2                                                                                                                                          
==2085==    at 0x4C2C93F: operator new[](unsigned long) (vg_replace_malloc.c:423)                                                                                                                                 
==2085==    by 0x1088FE: main (prog.cpp:5)                                                                                                                                                                        
==2085==                                                                                                                                                                                                          
==2085== LEAK SUMMARY:                                                                                                                                                                                            
==2085==    definitely lost: 408 bytes in 2 blocks                                                                                                                                                                
==2085==    indirectly lost: 0 bytes in 0 blocks                                                                                                                                                                  
==2085==      possibly lost: 0 bytes in 0 blocks                                                                                                                                                                  
==2085==    still reachable: 0 bytes in 0 blocks                                                                                                                                                                  
==2085==         suppressed: 0 bytes in 0 blocks      
  • valgrind nám teraz vypísal, na ktorom riadku je new, ku ktorému nebol volaný delete (riadky 5 a 6 v súbore prog.cpp)
  • tu to vidíme ľahko aj bez valgrind, ale vo väčšom programe vám táto informácia môže pomôcť

Cvičenie

Nasledujúci program by mal správne vypísať text "AhojAhojAhojAhoj", ale je v ňom zopár chýb. Skúste nájsť a opraviť chyby čítaním programu, použitím debugera, programu valgrind, prípadne si pridajte nejaké pomocné výpisy premenných.

  • v programe valgrind je vždy dobré začať od prvej vypísanej chyby, opraviť ju a spustiť valgrind znovu
#include <iostream>
using namespace std;

void opakuj(char kam[], char co[], char kolko) {
    /* Funkcia dostane na vstupe retazec co a cislo kolko a nakopiruje ho tolkokrat
     * za sebou do retazca kam. */

    int i=0; // pozicia v kam
    for(int opakovanie=0; opakovanie<kolko; opakovanie++) {  // opakuj kopirovanie
        for(int j=0; co[j]!=0; j++) {  // prechod cez znaky retazca co
            kam[i] = co[j];
            i++;
        }        
    }
}

int main(void) {
    char ahoj[4] = {'A', 'h', 'o', 'j'};
    char vysledok[16];  
    opakuj(vysledok, ahoj, 4);
    cout << vysledok << endl;
}

SVGdraw

Knižnica SVGdraw umožňuje vytvoriť obrázok v SVG formáte a vykresľovať do neho rôzne geometrické útvary, animovať ich a používať korytnačiu grafiku.

Vykresľovanie v SVG formáte

  • Ako prvé musíme vytvoriť súbor s obrázkom v SVG formáte s určitými rozmermi príkazom typu SVGdraw drawing(150, 100, "hello.svg");
  • Do obrázku môžeme kresliť príkazmi drawRectangle, drawEllipse, drawLine, drawText.
  • Ak chceme vykresľovať mnohouholníky, použijeme skupinu príkazov startPolygon, addPolygonPoint a drawPolygon. Pomocou startPolygon a addPolygonPoint postupne vymenujeme vrcholy a pomocou drawPolygon mnohouholník uzavrieme a vykreslíme.
  • Pomocou príkazov setLineColor, setFillColor, setNoFill nastavujeme farbu čiar a vyfarbovania. Farby zadávame buď troma číslami od 0 do 255 určujúcimi intenzitu červenej, zelenej a modrej, alebo názvom, napr. "red" (zoznam mien farieb). Príkaz setFontSize nastavuje veľkosť písma a setLineWidth nastavuje hrúbku čiary.
  • Po vykreslení všetkých útvarov ukončíme vykresľovanie príkazom drawing.finish();
  • Po spustení programu by mal vzniknúť súbor hello.svg, ktorý si môžete prezrieť napríklad v internetovom prehliadači.

Animácie

  • Príkaz wait umožní pozastaviť vykresľovanie SVG súboru o zadaný čas v sekundách, takže jednotlivé útvary sa objavujú postupne.
  • Príkaz clear schová všetky vykreslené útvary, takže môžeme kresliť znova na prázdnu plochu. Pred príkazom clear je vhodné použiť wait.
  • Príkaz hideItem schová objekt (napr. čiaru) so zadaným číslom. Každý kresliaci príkaz vráti číslo práve vykresleného objektu, takže si ho stačí uložiť v nejakej premennej pre neskoršie mazanie.

Príkazy pre korytnačiu grafiku

Namiesto vykresľovania obdĺžnikov, čiar a pod na zadané súradnice môžeme obrázok vytvoriť aj korytnačou grafikou. Na obrázku bude pohyb korytnačky znázornený ako červený trojuholník a za sebou bude nechávať čiernu čiaru.

  • Príkazom typu Turtle turtle(200, 300, "domcek2.svg", 50, 250, 0); vytvoríme SVG obrázok určitej veľkosti a s určitým menom súboru. Posledné tri čísla udávajú počiatočnú polohu korytnačky a jej natočenie.
  • Korytnačka si pamätá svoju polohu a natočenie na ploche. Príkaz forward posunie korytnačku dopredu, príkazy turnLeft a turnRight ju otočia.
  • Príkaz setSpeed umožňuje zmeniť rýchlosť korytnačky, aby sme lepšie videli, ako sa postupne hýbe.

Testovač

Príklady z cvičení, domácich úloh, ale aj na záverečnej skúške budete odovzdávať na stránke http://prog.dcs.fmph.uniba.sk/ , ktorá bude súčasne mnohé z vašich riešení aj testovať.

  • Na stránku sa prihláste heslom, ktoré dostanete na prvých cvičeniach, po prihlásení si ho zmeňte.
  • Na testovači uvidíte zoznam príkladov, spolu s ich zadaniami v pdf formáte a s ďalšími potrebnými súbormi (napr. kostra programu, príklady vstupu)
  • Vaše programy môžete odovzdať buď zvolením súboru s programom (.cpp) z disku alebo nakopírovaním programu pomocou myši do textového poľa.
  • Váš program sa uloží na testovači a môžete si neskôr skontrolovať, či ste odovzdali správnu verziu.
  • Testovač váš program skompiluje a spustí na niekoľkých vstupoch. Výsledok testovania vám zobrazí.
  • V závislosti od zaťaženia servera zobrazenie výsledku môže nejaký čas trvať.
  • V ideálnom prípade dostanete výsledok OK, ak váš program vypísal na všetkých vstupoch správnu odpoveď.
  • Môže však dôjsť k rôznym chybám:
    • CE (compile error): chyba pri kompilácii, testovač vypíše výstup kompilátora. Odporúčame pred odovzdaním program skompilovať na vašom počítači.
    • WA (wrong answer): na niektorom vstupe váš program vypísať zlú odpoveď. Môže ísť o závažnú chybu v programe, ale aj len o malý problém vo formáte výstupu. Testovač väčšinou porovnáva váš výsledok znak po znaku so správnym výsledkom a každý rozdiel, ako napríklad medzera navyše, považuje za chybu. Pozrite si detaily testovania, či nezbadáte rozdiely vo formáte.
    • TO (time limit exceeded): program na príslušnom vstupe bežal príliš dlho. Nakoľko dávame pomerne veľké časové limity, pravdepodobne váš program sa "zacyklil", t.j. beží do nekonečna.
    • SG (segmentation fault): váš program spadol na chybu pri behu, napr. delenie nulou, prístup mimo hraníc poľa, prípadne priveľká spotreba pamäte.
    • Kód WAITING znamená, že váš program ešte čaká na spustenie a RUNNING znamená, že prebieha jeho testovanie.
  • Pri týchto problémoch skúste program spúšťať na viacerých vstupoch na vašom počítači a chybu objaviť. Niekedy vám poskytneme aj vstupy a výstupy použité na testovači, ktoré vám pomôžu pri hľadaní chyby. V opačnom prípade si skúste nejaké vstupy vymyslieť sami.
  • Program opravujte až kým nedostanete OK na všetkých vstupoch.
  • Testovač vám dovolí odovzdať program aj po termíne, ale takéto pokusy už nebudú brané do úvahy pri bodovaní, pokiaľ nemáte dohodnuté individuálne predĺženie termínu s vyučujúcimi.
  • Ak testovač nefunguje alebo ak nájdete chybu v zadaní, dajte nám vedieť na adrese E-prg.png Takisto na túto adresu posielajte otázky k zadaniam alebo prosby o pomoc s konkrétnou úlohou.

Poznámka o vstupe a výstupe

  • Ak v zadaní nie je povedané inak, všetok vstup načítavajte z konzoly príkazmi cin (prípadne scanf a pod.) a vypisujte na konzolu príkazmi cout (prípadne printf a pod.)
  • Vstup uvedený v zadaní má testovač v súbore a presmeruje ho vášmu programu na konzolu, t.j. správa sa podobne, ako keby niekto zadával príslušné hodnoty ručne počas behu vášho programu
  • Naopak testovač zachytí do súboru všetko, čo váš program vypíše na konzolu. Tento súbor potom porovnáva so správnou odpoveďou.
  • Napr. program na sčítanie čísel z prvej prednášky by mohol mať nasledovný vstup a výstup na testovači:
10
3
Please enter the first number: Please enter the second number: 10+3=13
  • V príkladoch na testovači väčšinou vynecháme interaktívne časti, program si teda nebude pýtať čísla od užívateľa, predpokladá, že ten ich sám od seba zadá.

Prednáška 1

Pozrite si úvod k predmetu a pravidlá.

Organizačné poznámky

  • Zajtra budú hlavné cvičenia s prvými bodovanými príkladmi
    • Oboznámenie sa s prostredím, s testovačom, riešenie jednoduchých príkladov k prvej prednáške
    • Budete potrebovať prihlasovacie meno a heslo do AIS2 na prihlásenie na počítač v učebni
    • Aspoň na úvod cvičenia odporúčame prísť aj pokročilým
  • Prednáška v stredu nebude (rektorské voľno do 12:00), ale doplnkové cvičenia sa budú konať
  • Do piatka sa prihlásiť na test pre pokročilých
  • Test pre pokročilých bude budúcu stredu počas doplnkových cvičení
  • Ak si neviete C++ nainštalovať na notebook, prineste ho na doplnkové cvičenia budúcu stredu, skúsime pomôcť (hlavne ak máte Windows)

Programátorské prostredie

  • Na tomto predmete budeme programovať v jazyku C++, budeme však z neho používať len malú časť
  • Budeme používať programátorské prostredie NetBeans, ktoré spríjemňuje a zjednodušuje prácu alebo jednoduchší editor Kate
  • Cvičenia a skúšky budú v operačnom systéme Linux
  • Môžete používať aj iné programátorské prostredia, ale
    • odovzdané programy (DÚ, skúška) musia správne pracovať v prostredí ako na cvičeniach
    • počas skúšky budete mať k dispozícii len to, čo beží v učebniach v Linuxe
    • viac informácií na stránke o alternatívach k Netbeans

Prvý program

  • Tradične sa v učebniciach programovania ako prvý uvádza program, ktorý iba vypíše na obrazovku text "Hello world!". Tu je v jazyku C++:
#include <iostream>
using namespace std;

int main(void) {
    cout << "Hello world!" << endl;
}
  • Samotný text je vypísaný príkazom cout << "Hello world!" << endl;
  • Všimnite si, že text Hello world! sme dali do úvodzoviek, čím poukazujeme na to, že to nie sú príkazy programovacieho jazyka, ale text, s ktorým treba niečo robiť.
  • Za príkazom sme dali bodkočiarku, ktorá ho ukončuje.
  • O vypisovaní si povieme viac neskôr, ale už teraz môžete vypisovať rôzne texty tým, že zmeníte text medzi úvodzovkami.
  • Riadok int main(void) { označuje začiatok programu, program ide až po ukončovaciu zloženú zátvorku }
  • Jazyk C++ sám o sebe neobsahuje príkazy na vypisovanie (cout <<...). Na to potrebujeme použiť knižnicu: súbor príkazov, ktoré niekto už naprogramoval a my ich len používame. Prvé dva riadky programu nám umožnia používať štandardnú knižnicu iostream, ktorá je súčasťou C++ a ktorá obsahuje príkazy na vypisovanie.

Spúšťanie programu

  • Na to, aby sme náš program mohli spustiť na počítači, potrebujeme ho najskôr skompilovať, t.j. preložiť do spustiteľného strojového kódu.
  • Ako na to, nájdete v návode k práci v prostredí Netbeans
  • V prostredí Netbeans vieme program aj spustiť, môžeme si ho však aj skopírovať a spúšťať na iných počítačoch nezávisle od Netbeans.

Ďalší jednoduchý program

  • Podobným spôsobom môžeme vypísať aj iný text. Napríklad dnešný dátum:
#include <iostream>
using namespace std;

int main(void) {
    cout << "Dnes je 24.9.2018!" << endl;
}
  • Cvičenie: Vypíšte dátum v rôznych formátoch (skrátený formát, americký formát, ...). Každý na jeden riadok.

Premenné

Príklad z cvičenia by mohol vyzerať napríklad takto:

#include <iostream>
using namespace std;

int main(void) {
    cout << "24.9.2018" << endl;
    cout << "24.9." << endl;
    cout << "9/24/2018" << endl;
}

Ak by sme v ňom chceli zmeniť dátum na prvú prednášku o rok, museli by sme pomeniť vhodné čisla v celom programe. Navyše keď vidíme v programe nejaké číslo, nemusí byť úplne jasné, ako sme k nemu prišli.

Program teraz prepíšeme tak, aby sme deň, mesiac a rok mali zapísané symbolicky a mohli ich meniť na jednom mieste.

#include <iostream>
using namespace std;

int main(void) {
    int den = 24;
    int mesiac = 9;
    int rok = 2018;

    cout << den << "." << mesiac << "." << rok << endl;
    cout << den << "." << mesiac << "." << endl;
    cout << mesiac << "/" << den << "/" << rok << endl;
}

Symbolickým hodnotám den, mesiac, rok sa hovorí premenné.

  • Premmená je vyhradené miesto v pamäti počítača, ku ktorému v programe pristupujeme pod určitým názvom.
  • Do tejto pamäti si môžeme zapísať hodnotu a neskôr ju použiť.
  • Príkaz int x = 100; vytvorí novú premennú a uloží do nej hodnotu 100.
  • Každá premenná má určitý typ, ktorý určuje, aké hodnoty do nej môžeme ukladať.
  • Tieto premenné majú typ int, čo je skratka zo slova integer, celé číslo.

Ak v programe premenným priradíme iné čísla, môžeme vypísať iný dátum.

Príkaz int x = 100; vieme rozpísať aj na dva príkazy int x; x = 100;. Prvý z nich vytvorí premennú x, ktorá bude mať nejakú ľubovoľnú hodnotu a druhý túto počiatočnú hodnotu zmení na 100.

Zhrnutie

  • Programy, ktoré sme doteraz videli, vyzerali takto:
    • Najprv sme zapli používanie niekoľkých knižníc
    • Samotný program začínal int main(void) { a končil zloženou zátvorkou }
    • Program mohol mať niekoľko príkazov ukončených bodkočiarkami, ktoré sa vykonávajú jeden po druhom.
  • Logiku za tým, prečo jednotlivé príkazy píšu tak, ako sa píšu, sme zatiaľ ešte nevysvetľovali, mali by ste však byť schopní modifikovať príklady uvedené v prednáške menením čísel, textov v úvodzovkách, pridávaním ďalších príkazov a podobne.
  • Upozornenia:
    • Je rozdiel medzi malými a veľkými písmenami
    • Všetky čiarky, bodkočiarky, zátvorky a podobne sú dôležité
    • Na väčšine miest v programe môžeme voľne pridávať medzery a konce riadku, snažíme sa tým program spraviť prehľadný
  • Programy, ktoré sme videli doteraz, nie sú veľmi zaujímavé, lebo vždy robia to isté a robia pevný počet krokov, ktoré sme museli ručne všetky vypísať. Ďalej uvidíme
    • načítanie, ktoré nám umožní získať dáta od používateľa
    • podmienky, ktoré nám umožnia vykonávať príkazy podľa okolností
    • cykly, ktoré nám umožnia opakovať tie isté príkazy veľa krát

Textový výpis a načítanie

Vieme už vypísať niečo na obrazovku (výstup - output) a podobne môžeme aj čítať, čo nám používateľ napíše na klávesnici (vstup - input). Takéto zadané hodnoty tiež uložíme do premenných, aby sme s nimi mohli ďalej pracovať.

Sčítanie čísel

Nasledujúci program od užívateľa vypýta dve čísla a vypíše ich súčet.

#include <iostream>
using namespace std;

int main(void) {
    int x, y;

    cout << "Please enter the first number: ";
    cin >> x;
    cout << "Please enter the second number: ";
    cin >> y;

    int result = x + y;
    cout << x << "+" << y << "=" << result << endl;
}

Tu je príklad behu programu, keď užívateľ zadal čísla 10 a 3:

Please enter the first number: 10
Please enter the second number: 3
10+3=13
  • Tento program používa na vstup a výstup príkazy z knižnice iostream a teda do hlavičky programu dáme #include <iostream> a using namespace std;
  • Program najskôr vytvorí dve premenné x a y typu int (a nepriradzuje im zatiaľ žiadne hodnoty)
  • Potom príkazom cout vypíše text "Please enter the first number: " aby užívateľ vedel, čo má robiť.
  • Potom pomocou príkazu cin načíta číslo od používateľa do premennej x
  • To isté opakuje pre premennú y
  • Potom vytvorí novú premennú result a uloží do nej súčet x a y.
  • Nakoniec vypíše výsledok aj s výrazom, ktorý sme počítali, pomocou príkazu cout.

Viac o príkaze cout

  • Pomocou cout vypisujeme na konzolu, t.j. textovú obrazovku
  • To, čo chceme vypísať pošleme na cout pomocou šípky <<
  • cout << endl; vypíše koniec riadku
  • Môžeme naraz vypísať aj viac vecí oddelených šípkami <<
    • Napr. cout << x << "+" << y << "=" << result << endl; vypíše najskôr obsah premennej x (napr. hodnotu 10), potom znamienko plus (ktoré máme v úvodzovkách), potom obsah premennej y, potom znamienko rovnosti, potom obsah premennej result a nakoniec koniec riadku.

Viac o príkaze cin

  • Pomocu cin načítavame z konzoly údaje od užívateľa
  • Tieto údaje pošleme do premenných pomocou šípky >>
  • Opäť môžeme načítať aj viac vecí naraz, napr. nasledovný úryvok si vypýta obe čísla naraz a uloží ich do premenných x a y
   cout << "Please enter two numbers separated by space: ";
   cin >> x >> y;
  • Pozor, cin nekontroluje, že užívateľ zadáva rozumné hodnoty. Čo sa stane, ak namiesto čísla zadá nejaké písmená a podobne?

Podmienka (if)

Niekedy chceme vykonať určité príkazy len ak sú splnené nejaké podmienky. To nám umožňuje príkaz if.

  • Nasledujúci program si vypýta od užívateľa číslo a vypíše, či je toto číslo záporné (negative) alebo nezáporné (non-negative).
#include <iostream>
using namespace std;

int main(void) {
    int x;
    cout << "Please enter some number: ";
    cin >> x;

    if (x < 0) {
        cout << "Number " << x << " is negative." << endl;
    } else {
        cout << "Number " << x << " is non-negative." << endl;
    }
}
  • Tu je príklad dvoch behov programu:
Please enter some number: 10
Number 10 is non-negative.
Please enter some number: -3
Number -3 is negative.


  • Ako vidíme, za príkazom if je zátvorka s podmienkou. V našom príklade podmienka je x < 0.
  • Ak je podmienka v zátvorke splnená (t.j. ak x je menšie ako nula), vykonáme príkazy v zloženej zátvorke za príkazom if.
  • Ak podmienka nie je splnená (t.j. ak je x väčšie alebo rovné nule), vykonáme príkazy v zloženej zátvorke za slovom else
  • Časť else {...} je možné vynechať, ak nechceme vykonávať žiadne príkazy.
  • Ak za if alebo else nasleduje iba jeden príkaz, zátvorky { a } môžeme vynechať. To však ľahko vedie k chybám, preto je lepšie ich vždy použiť.

Cvičenie:

  • Pomocou podmienky vypíšte absolútnu hodnotu načítaného čísla.
  • Namiesto vypísania uložte túto hodnotu do premennej y, ktorá by sa dala ďalej v programe použiť.

Vnorené podmienky

Príkazy if môžeme navzájom vnárať. Príklad: načítaj číslo a zisti, či je kladné, záporné alebo nula.

  • Test na rovnosť sa robí operátorom ==
#include <iostream>
using namespace std;

int main(void) {
    int x;
    cout << "Please enter some number: ";
    cin >> x;

    if (x == 0) {
        cout << "Zero" << endl;
    } else {
        if (x > 0) {
            cout << "Positive" << endl;
        } else {
            cout << "Negative" << endl;
        }
    }

}

Upozornenie

Častá chyba, ktorá sa vyskytuje pri podmienke, je použitie priradenia namiesto porovnania. Keby sme napísali

if (x=0) cout << "Zero" << endl;

tak program do premennej x priradí nulu.

Ďalšia bežná chyba je zabudnutie zložených zátvoriek

   if (x==0) cout << "Zero"; cout << endl;

Tento program vykoná cout << endl vždy, nezávisle od podmienky. V prípade, že chceme vykonať v podmienke viacero príkazov, nesmieme zabudnúť ich uzátvorkovať:

   if (x==0) { cout << "Zero"; cout << endl; }

Cvičenia 1

Cieľom prvých cvičení je:

  1. vyskúšať si prihlásenie na počítače v učebni a prácu v prostredí Netbeans a Kate, prípadne v iných prostrediach
  2. precvičiť si písanie jednoduchých programov
  3. vyskúšať si prácu s testovačom

Príklad 0: Sčítanie čísel

Príklad 1: odovzdávanie na testovači domácich úloh

  • Prihláste sa na testovač, zmeňte si heslo
  • Pozrite si Návod na prácu s testovačom
  • Nájdite si zadanie príkladu 1 na testovači, je to len malá modifikácia sčítania dvoch čísel
    • Rozdiel je, že testovaču nebudeme vypisovať, aby zadal číslo a na výstupe vypíšeme iba samotný súčet.
  • Zmodifikujte program podľa požiadaviek zadania a odovzdajte ho na testovači
    • Pozrite si výsledky testovania, ak nedostanete odpoveď OK, skúste chybu opraviť
  • Tento príklad je rozcvička, body zaňho dostanete, len ak ho dokončíte počas cvičenia

Ďalšie bodované príklady

  • Ďalšie zadania na tento týždeň nájdete priamo na testovači
  • Ak ste všetky príklady nevyriešili, odporúčame vám prísť na doplnkové cvičenia v stredu a pokračovať v ich riešení
  • Môžete ich riešiť aj doma, až do začiatku cvičení budúci utorok
  • Tento týždeň na doplnkových cvičeniach ešte nebude rozcvička za body

Prednáška 2

Opakovanie

Doteraz sme videli:

  • Načítavanie pomocou cin, výpis pomocou cout.
  • Celočíslené premenné typu int.
  • Podmienky (if).
#include <iostream>
using namespace std;

int main(void) {
    int x;
    cout << "Zadajte cislo: ";
    cin >> x;

    if (x < 0) {
        cout << "Cislo " << x << " je zaporne." << endl;
    } else {
        cout << "Cislo " << x << " je nezaporne." << endl;
    }
    return 0;
}

Komentáre

Do zdrojových kódov programov v jazykoch C a C++ je možné pridávať komentáre, čo sú časti kódu ignorované kompilátorom. Možno tak urobiť dvoma spôsobmi:

  • Za komentár sa považuje akákoľvek časť programu začínajúca /* a končiaca */. Táto konštrukcia môže byť aj na viac riadkov.
  • Čokoľvek za // až po koniec riadku sa tiež považuje za komentár. To je užitočné na písanie krátkych komentárov na jeden riadok.

Poznámka: Staršie štandardy jazyka C (bez ++) nepodporujú notáciu // pre jednoriadkové komentáre.

#include <iostream>
using namespace std;

int main() {
    cout << "Som program s komentarmi." << endl; // Som komentar na jeden riadok.
    /* Som komentar
       na
       velmi
       vela
       riadkov
    */
    return 0;
}

Dátové typy int, double a bool

Na začiatok budeme pracovať s troma dátovými typmi:

  • S typom int pre celé čísla – príkladmi konštánt typu int1, 42, -2, alebo 0.
  • S typom double pre reálne čísla (s pohyblivou rádovou čiarkou a dvojnásobnou presnosťou) – príkladmi konštánt typu double4.2, -3.0, 3.14159, alebo 1.5e3 (v poslednom prípade ide o tzv. semilogaritmický zápis v tomto prípade znamenajúci Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle 1,5 \cdot 10^3} , t.j. 1500).
  • S typom bool pre logické hodnoty – jedinými konštantami sú true (ekvivalentná číselnej hodnote 1) a false (ekvivalentná číselnej hodnote 0).

Poznámka: V jazyku C++ ide vo všetkých troch prípadoch o primitívne (zabudované) typy. V jazyku C je potrebné na prácu s typom bool použiť knižnicu stdbool.h a staršie štandardy ho nepodporujú vôbec. Typ bool sa však vždy dá nahradiť napríklad typom int.

Rozsahy (čiže „množiny všetkých reprezentovateľných čísel”) premenných typov int a double sú obmedzené. Tieto obmedzenia sa môžu líšiť v závislosti od konkrétnej architektúry. V súčasnosti však:

  • Typ int zvyčajne zaberá 4 bajty (32 bitov) pamäte a dajú sa ním reprezentovať celé čísla z intervalu <-2 147 483 648, +2 147 483 647>. (Na 64-bitových architektúrach môže niekedy ísť až o 8 bajtov.)
  • Typ double zvyčajne zaberá 8 bajtov. Ním reprezentované reálne čísla sú v pamäti uložené vo forme Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle z\cdot a\cdot 2^b} , kde z je znamienko, a je reálne číslo z intervalu <1,2) (mantisa) a b je celé číslo (exponent). Na uloženie mantisy sa používa 52 bitov a na uloženie exponentu 11 bitov. Typ double tak možno použiť na prácu s reálnymi číslami približne v rozsahu od Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle 10^{-300}} po Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle 10^{300}} s presnosťou na 15 až 16 platných číslic. Pri takejto reprezentácii sa zjavne nevyhradzujú pevné počty bitov na reprezentáciu celej resp. destatinnej časti; počet cifier pred a za rádovou čiarkou je naopak určený exponentom. Hovoríme preto o pohyblivej rádovej čiarke.

Pretypovanie

Hodnotu niektorého z typov bool, int, double je možné skonvertovať na „zodpovedajúcu” hodnotu ľubovoľného ďalšieho z týchto typov. V takom prípade hovoríme o pretypovaní. Platí pritom nasledujúce:

  • Konverzia z „menej všeobecného” typu na „všeobecnejší” sa správa očakávateľným spôsobom. Booleovské hodnoty 0 resp. 1 sa teda konvertujú na celé čísla 0 resp. 1, prípadne na reálne čísla 0.0 resp, 1.0; celé číslo sa tiež konvertuje na reálne číslo, ktoré je mu rovné.
  • Konverzia zo „všeobecnejšieho” typu na „menej všeobecný” dodržiava určité vopred stanovené pravidlá. Napríklad pri konverzii z int alebo double na bool sa ľubovoľná nenulová hodnota skonvertuje na true a nula sa skonvertuje na false. Pri konverzii z double na int dôjde k zaokrúhleniu smerom k nule (čiže nadol pri kladných číslach, nahor pri záporných).

Pretypovanie je možné realizovať dvoma spôsobmi:

  • Implicitne, napríklad priradením premennej jedného typu do premennej iného typu, alebo vo všeobecnosti použitím premennej jedného typu v kontexte, kde sa očakáva premenná druhého typu.
  • Explicitne, použitím pretypovacieho operátora: (nazov_noveho_typu) vyraz_stareho_typu.

Možnosti pretypovania sú ilustrované nasledujúcim ukážkovým programom. Čitateľ si môže sám skúsiť vymyslieť preň niekoľko vstupov, odhadnúť výstupy programu na týchto vstupoch a následne svoj odhad overiť spustením programu.

#include <iostream>
using namespace std;

int main() {
  bool b1 = true;
  int n1 = 4;
  double x1 = 1.234;

  bool b2;
  int n2;
  
  b2 = n1; // b2 = 1
  n2 = x1; // n2 = 1 
  cout << b2 << " " << n2 << endl; // Vypise 1 1
  
  x1 = n2; // x1 = 1.0
  cout << x1 << endl; // Vypise 1
  
  cout << (bool) 7 << " " << (bool) 0 << endl; // Vypise 1 0 
  cout << (int) 4.2 << " " << (int) -4.2 << endl; // Vypise 4 -4
  
  return 0;
}

Aritmetické operátory a výrazy

Základnými štyrmi aritmetickými operátormi na celých číslach (napríklad typu int) aj reálnych číslach (napríklad typu double) sú:

  • + pre sčítanie;
  • - pre odčítanie;
  • * pre násobenie;
  • / pre delenie.

Operátor / sa pritom na argumentoch typu int správa ako celočíselné delenie – hodnota podielu sa zaokrúhli smerom k nule. Napríklad výraz 5/3 teda má hodnotu 1. Akonáhle je však aspoň jeden operand typu double, interpretuje sa operátor / ako delenie reálnych čísel. Výrazy 5.0/3.0, 5.0/3, 5/3.0 a 5/(double)3 teda majú všetky hodnotu 1.66667. Výstup aritmetickej operácie má navyše rovnakú hodnotu ako „všeobecnejší” z operandov. Napríklad 2+3.0 je teda typu double a výraz (2+3.0)/3 tak má hodnotu 1.66667; naopak výraz (2+3)/3 má hodnotu 1.

#include <iostream>
using namespace std;

int main() {
    int a = 4;
    int b = 3;
    double d = 3; // Automaticky pretypuje cele cislo 3 na realne cislo 3.0

    cout << a / b << endl; // Celociselne delenie: 4 / 3 = 1
    cout << a / d << endl; // Necelociselne delenie: 4 / 3.0 = 1.33333
    cout << (1.0 * a) / b << endl; // Necelociselne delenie: (1.0 * 4) / 3 = 4.0 / 3 = 1.33333
    cout << ((double)a) / b << endl; // Necelociselne delenie: 3.0 / 4 = 1.33333 
    
    double e = a / b; // Do e je priradeny vysledok celociselneho delenia 4 / 3 = 1; po pretypovani je to rovne 1.0
    cout << e << endl; // Vypise 1
    cout << e / 2 << endl; // Vypise 0.5, lebo 1.0 / 2 je necelociselne delenie  
    
    return 0;
}
  • Na celých číslach je ďalej definovaný operátor %, ktorého výstupom je zvyšok po celočíselnom delení jeho operandov. Napríklad výraz 5%3 tak má hodnotu 2.
  • Ďalšie matematické operácie a funkcie vyžadujú #include <cmath> (pre jazyk C++) resp. #include <math.h> (pre jazyk C, ale funguje aj v C++) v hlavičke programu:
    • Napríklad cos(x), sin(x), tan(x) (tangens), acos(x) (arkus kosínus), exp(x) (Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle e^x} ), log(x) (prirodzený logaritmus), pow(x,y) (Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle x^y} ), sqrt(x) (odmocnina), abs(x) (absolútna hodnota), floor(x) (dolná celá časť), ...
    • Viac detailov možno nájsť v dokumentácii.

Relačné operátory

Hodnoty typov int, double a bool možno porovnávať nasledujúcimi relačnými operátormi:

  • == pre rovnosť;
  • != pre nerovnosť;
  • < pre reláciu „menší ako”;
  • > pre reláciu „väčší ako”;
  • <= pre reláciu „menší alebo rovný ako”;
  • >= pre reláciu „väčší alebo ako”.

Výstupom relačného operátora je potom booleovská hodnota 1 (true), ak je daná relácia splnená a 0 (false) v opačnom prípade.

Poznámka: V jazyku C narozdiel od C++ nemusí pri výstupoch relačných operácií vždy ísť o booleovskú hodnotu, ale výstupom môže byť aj celé číslo, ktoré je nenulové práve vtedy, keď je daná relácia splnená. Pri väčšine kompilátorov je však týmto nenulovým číslom aj tak číslo 1.

Logické operátory a výrazy

Na výrazoch typu bool sú v jazykoch C a C++ definované logické operátory, ktoré sa správajú rovnako ako logické spojky známe z výrokovej logiky:

  • || pre disjunkciu;
  • && pre konjunkciu;
  • ! pre negáciu – tu ide o unárny operátor, dobre utvoreným logickým výrazom je napríklad !((1 >= 2) && (3 >= 4)) alebo !true.

Operátory priradenia, inkrementu a dekrementu

Operátor priradenia sme už využívali, má však jednu (občas jemne zákernú) črtu, o ktorej sme zatiaľ nehovorili. Napríklad výraz a = b priradí premennú b do premennej a; okrem toho však má aj sám o sebe hodnotu, ktorou je nová hodnota premennej a. To umožňuje písať napríklad cout << (a = b) << endl;.

Často realizovanou operáciou na číslach je zvýšenie hodnoty o 1. To možno urobiť napríklad nasledujúcimi spôsobmi:

  • x=x+1;
  • x+=1;
  • x++;
  • ++x.

Analogicky sú definované operátory ako --, -=, *=, atď.

Viac o podmienkach

Logické výrazy môžu byť efektívnym nástrojom na elimináciu množstva vnorených podmienok: napríklad konštrukcia typu

if (a == 0) {
    if (b == 0) {
        čokoľvek
    }
}

je ekvivalentná konštrukcii

if (a == 0 && b == 0) {
    čokoľvek
}

a podobne.

Každé nenulové prirodzené číslo sa pri pretypovaní na bool zmení na 1 (čiže true). V dôsledku toho je korektná napríklad aj podmienka

if (42) {
    čokoľvek
}

a čitateľ sa môže sám presvedčiť o tom, že telo takejto podmienky sa vždy vykoná. Táto črta, spoločne s vlastnosťami operátora priradenia, je príčinou častej programátorskej chyby demonštrovanej nasledujúcim ukážkovým programom.

#include <iostream>
using namespace std;

int main() {
    int a = 1;
    int b = 2;
    if (a == b) {
        cout << "Prva podmienka splnena." << endl; // Nevypise sa, lebo 1 sa nerovna 2
    }
    if (a = b) {
        cout << "Druha podmienka splnena." << endl; // Vypise sa, lebo a = b ma hodnotu 2, co je nenulove cislo 
    } 
    return 0;
}

Priorita a asociativita operátorov

Výrazy sa vyhodnocujú v nasledujúcom poradí preferencie jednotlivých operátorov. Operátory v jednom riadku majú rovnakú prioritu a operátory vo vyššom riadku majú vyššiu prioritu, než operátory v nižších riadkoch.

  • ++ (inkrement), -- (dekrement), ! (logická negácia)
  • *, /, %
  • +, -
  • <, >, <=, >=
  • ==, !=
  • && (logická konjunkcia)
  • || (logická disjunkcia)
  • = (priradenie)

Poradie vyhodnocovania je možné meniť zátvorkami, ako napríklad vo výraze 4*(5-3).

Uvedené operátory sa väčšinou vyhodnocujú zľava doprava (hovoríme, že sú zľava asociatívne) – napríklad 1 - 2 - 3 sa teda vyhodnotí ako (1 - 2) - 3, t.j. -4 a nie ako 1 - (2 - 3), t.j. 2. Výnimkou sú operátory !, ++, -- a =, ktoré sú sprava asociatívne. To umožňuje napríklad viacnásobné priradenie a = b = c, ktoré najprv priradí hodnotu c do b a následne hodnotu výrazu b = c – tou je nová hodnota premennej b, čiže hodnota premennej c –, do a.

Viac sa o operátoroch v C++ možno dočítať napríklad tu.

Cyklus for

Doposiaľ sme písali programy, ktoré na každom vstupe vykonajú nejaký konečný (a hlavne od vstupu nezávislý) počet krokov a zastavia sa. Je zrejmé, že možnosti takýchto programov sú veľmi obmedzené. Stačí napríklad uvažovať program, ktorý má postupne vypisovať prirodzené čísla od 1 po nejaké n:

  • Ak n = 10, môžeme sa pokúsiť všetky čísla vypísať ručne.
  • Pre n = 100 je to už pomerne prácne.
  • Pre n = 1000 by sa o to už málokto pokúšal.
  • Pre n dané na vstupe to z pohľadu teoretika uvažujúceho n ako ľubovoľne veľké ani možné nie je (a nie je to možné ani z pohľadu praktika, akurát z trochu odlišných dôvodov).

V nasledujúcom sa zoznámime s prvou z niekoľkých konštrukcií umožňujúcich opakovanie nejakej skupiny príkazov. Pôjde o takzvaný cyklus for.

Príklad č. 1: vypisovanie čísel od 1 po n

Nasledujúci program načíta zo vstupu číslo n a postupne vypíše prirodzené čísla od 1 po n oddelené medzerami.

#include <iostream>
using namespace std;

int main() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cout << " " << i;
    }
    cout << endl;
    return 0;
}

Tu je výstup programu pre n = 9:

 1 2 3 4 5 6 7 8 9

Základné použitie cyklu for

Novou črtou programu z predchádzajúceho príkladu je konštrukcia pre cyklus for:

for (int i = 1; i <= n; i++) {
    telo cyklu
}
  • Táto konštrukcia pozostáva z kľúčového slova for nasledovaným zátvorkou s troma „časťami” oddelenými bodkočiarkami:
    • Príkaz int i = 1 vytvorí novú celočíselnú premennú i a priradí jej hodnotu 1.
    • Podmienka i <= n určuje dokedy sa má cyklus opakovať. V tomto prípade to má byť kým je hodnota premennej i menšia alebo rovná n.
    • Príkaz i++ hovorí, že po každom „zopakovaní” cyklu (t.j. po každej jeho iterácii) sa má hodnota premennej i zvýšiť o jedna.
  • Medzi zloženými zátvorkami { a } je potom tzv. telo cyklu – čiže jeden alebo viac príkazov, ktoré sa budú opakovať postupne pre rôzne hodnoty premennej i.

V príklade č. 1 je telom cyklu iba príkaz cout << " " << i;, ktorý vypíše medzeru a hodnotu premennej i.

Nateraz ostaňme pri tomto intuitívnom chápaní cyklu for. Detaily jeho funkcionality budú jednou z tém nasledujúcej prednášky.

Poznámka: Staršie štandardy jazyka C (bez ++) nepodporujú definíciu premennej priamo v cykle for (ako napríklad int i = 1 v našom príklade). Premennú je potom nutné zadefinovať samostatne pred začatím cyklu.

Príklad č. 2: vypisovanie čísel od 0 po n-1

Drobnou zmenou predchádzajúceho programu môžeme napríklad vypísať všetky čísla od 0 po n-1:

#include <iostream>
using namespace std;

int main() {
    int n;
    cin >> n;
    for (int i = 0; i < n; i++) {
        cout << " " << i;
    }
    cout << endl;
    return 0;
}

Tu je výstup programu pre n = 9:

 0 1 2 3 4 5 6 7 8

Príklad č. 3: simulovanie hodov kocky

Nasledujúci program od používateľa načíta číslo n a vypíše n simulovaných hodov kocky (každý na samostatný riadok).

#include <iostream>
#include <cstdlib>
#include <ctime>

using namespace std;

int main() {
    srand(time(NULL)); // Inicializacia generatora pseudonahodnych cisel
 
    int n;
    cout << "Zadajte pocet hodov: ";
    cin >> n;
    
    for (int i = 1; i <= n; i++) {
        cout << rand() % 6 + 1 << endl; // Vygenerovanie a vypisanie hodu kockou
    }
  
    return 0;
}

Príklad behu programu:

Zadajte pocet hodov: 6
2
6
1
2
1
4
  • Program využíva funkciu rand(), ktorá generuje pseudonáhodné celé čísla. (Nie sú v skutočnosti náhodné, lebo ide o pevne definovanú matematickú postupnosť, ktorá však má mnohé vlastnosti náhodných čísel). Aby bolo možné použiť túto funkciu, treba do hlavičky pridať #include <cstdlib>.
    • Výstupom funkcie rand() je nezáporné celé číslo medzi nulou a nejakou veľkou konštantou.
    • Zvyšok po delení tohto čísla šiestimi, t.j. rand() % 6, je potom číslo medzi 0 a 5. Ak k tomu pripočítame 1, dostaneme číslo od 1 po 6.
  • Funkcia srand inicializuje generátor pseudonáhodných čísel na základe parametra „určujúceho bod pseudonáhodnej postupnosti”, počnúc ktorým sa budú jej hodnoty generovať. My ako tento parameter používame aktuálny čas (v sekundách od začiatku roku 1970), čo zaručuje dostatočný efekt náhodnosti. Aby bolo možné použiť funkciu time, je treba do hlavičky pridať #include <ctime>.

Príklad č. 4: výpočet faktoriálu

Nasledujúci program si od používateľa vypýta číslo n a vypočíta n!, t.j. súčin celých čísel od 1 po n.

#include <iostream>
using namespace std;

int main() {
    int n;
    
    cout << "Zadajte n: ";
    cin >> n;
    
    int vysledok = 1;
    for (int i = 1; i <= n; i++) {
       vysledok = vysledok * i;
    }
    
    cout << n << "! = " << vysledok << endl;
    return 0;
}

Príklad behu programu pre n=4 (1*2*3*4=24):

Zadajte n: 4
4! = 24
  • Program používa premennú vysledok, ktorú na začiatku inicializuje hodnotou 1 a postupne ju násobí číslami 1, 2, ..., n.
  • Riadok vysledok = vysledok * i; zoberie pôvodnú hodnotu premennej vysledok, vynásobí ju hodnotou premennej i (t.j. jedným z čísel 1, 2, ..., n) a výsledok uloží naspäť do premennej vysledok (prepíše pôvodnú hodnotu). To isté sa dá napísať ako vysledok *= i;

Funkcia n! však veľmi rýchlo rastie a už pre n=13 sa výsledok nezmestí do premennej typu int. Dostávame nezmyselné hodnoty:

12! = 479001600
13! = 1932053504
14! = 1278945280
15! = 2004310016
16! = 2004189184
17! = -288522240

Správne hodnoty (ktoré možno získať zmenou typu premennej vysledok na long long int) sú:

12! = 479001600
13! = 6227020800
14! = 87178291200
15! = 1307674368000
16! = 20922789888000
17! = 355687428096000

Príklad č. 5: vypisovanie deliteľov (podmienka v cykle)

Nasledujúci program načíta od používateľa prirodzené číslo a vypíše zoznam jeho deliteľov.

#include <iostream>
using namespace std;

int main() {
    int n;
    cout << "Zadajte cislo: ";
    cin >> n;
    
    cout << "Delitele cisla " << n << ":";
    for (int i = 1; i <= n; i++) {
        if (n % i == 0) {
            cout << " " << i;
        }
    }    
    cout << endl;
    return 0;
}

Beh programu:

Zadajte cislo: 12
Delitele cisla 12: 1 2 3 4 6 12

Prednáška 3

Oznamy

  • Dnes doplnkové cvičenia
    • Dokončujú sa príklady zo včera, nebudú nové zadania
    • Príďte, ak sa vám podarilo včera vyriešiť iba málo príkladov
    • Takisto vám môžeme poradiť s inštaláciou Netbeans na váš notebook
  • Počas doplnkových cvičení v miestnosti M213 test pre pokročilých (iba pre prihlásených)
  • Ďalšie hlavné cvičenia budúci utorok sa budú týkať prednášky z dnes a z pondelka
    • Je dobrý nápad si tieto prednášky pred cvičeniami pozrieť
    • Na cvičeniach začnite riešením prvého príkladu (rozcvička), lebo ten má termín už počas cvičení
    • Na konci cvičení po termíne rozcvičky si ukážeme možné riešenia

Opakovanie: vypisovanie deliteľov (podmienka v cykle)

Nasledujúci program načíta od používateľa prirodzené číslo n a vypíše zoznam jeho deliteľov.

#include <iostream>
using namespace std;

int main() {
    int n;
    cout << "Zadajte cislo: ";
    cin >> n;
    
    cout << "Delitele cisla " << n << ":";
    for (int i = 1; i <= n; i++) {
        if (n % i == 0) {
            cout << " " << i;
        }
    }    
    cout << endl;
    return 0;
}

Malá odbočka: úprava a čitateľnosť programov

Okrem počítača budú váš program čítať aj ľudia: vy sami, keď v ňom potrebujete nájsť chybu alebo ho v budúcnosti rozširovať, vaši kolegovia vo firme, učitelia v škole. Preto treba programy písať tak, aby boli nielen správne, ale aj prehľadné a ľahko pochopiteľné. Tu sú zásady, ktoré takejto čitateľnosti pomáhajú:

  • Odsadzovanie: príkazy vykonávané v cykle alebo podmienke (medzi { a }) odsaďte o niekoľko pozícií doprava. Pri vnorených cykloch a podmienkach odsaďte každú ďalšiu úroveň viac.
    • V Netbeans sa dá odsadzovanie napraviť na vybranom texte pomocou položky menu Source->Format
  • Medzery a voľné riadky: prehľadnosť zvýšite, ak oddelíte ucelené časti programu voľným riadkom. V zložitejších výrazoch doporučujeme vkladať medzery okolo operátorov:
    • for(int i=0;i<count;i++){
    • for (int i = 0; i < count; i++) {
  • Dĺžka riadku: doporučujeme sa vyhýbať sa dlhým riadkom nad 80 znakov. Sú problémy s ich tlačením alebo zobrazovaním v menších oknách, aj na veľkom monitore namáhajú čitateľa. V prípade potreby je možné dlhšiu podmenku alebo iný výraz rozdeliť na viac riadkov.
  • Názvy premenných: je vhodné používať názvy premenných, ktoré vyjadrujú ich obsah (po anglicky príp. slovensky). Pri premenných v kratších programoch, alebo ktoré sa používajú len lokálne v kratšom kúsku programu môžete použiť aj krátke zaužívané názvy, napr. i a j pre premenné v cykloch, n pre počet, a pre pole.
  • Komentáre: význam jednotlivých úsekov kódu je dobré popísať v komentároch.

Pri známkovaní budeme brať do úvahy aj prehľadnosť vašich programov.

Programy Netbeans a Kate vám vedia napísaný program správne preformátovať.

Hľadanie chýb v programe

  • Väčšina programov nefunguje na prvý krát, hľadanie chýb patrí medzi základné činnosti programátora
  • Podobné chyby sa často opakujú, tréningom sa ich naučíte nájsť rýchlejšie
  • Program najskôr skúste skompilovať a opravte chyby, ktoré nájde kompilátor
  • Aj varovania kompilátora môžu poukazovať na chybu v programe
  • Potom ho skúste spustiť na niekoľkých vstupoch a pozrite sa, či dáva správne výsledky
  • Ak nedáva správne výsledky, treba nájsť chybu
    • Môžete skúsiť znovu prečítať program, či nezbadáte chybu
    • Alebo experimentami zistiť, kde sa jeho správanie prvýkrát začne odlišovať od toho, čo očakávate
    • To sa dá robiť spúšťaním programu po krokoch v nástroji nazvanom debugger
    • Alternatíva k debuggeru je do programu pridať pomocné výpisy, ktoré vám prezradia, ktorá časť programu sa práve vykonáva a aké sú hodnoty dôležitých premenných
    • Pozor, po nájdení chyby treba tieto pomocné výpisy odstrániť, pozor, aby ste tým nespravili ďalšiu chybu
  • Debugger alebo výpisy vám pomôžu nájsť chybu iba vtedy, ak máte predstavu o tom, ako by program mal fungovať a hľadáte, kde sa od nej skutočné správanie líši

Krokovanie programu

  • Skúsme si odkrokovať, ako sa budú postupne meniť hodnoty premenných počas behu programu
  • Krokovanie programu a vypisovanie hodnôt premenných je možné robiť v prostredí Netbeans, mali by ste však byť schopní to robiť aj ručne, lebo tým lepšie pochopíte, ako program pracuje.
  • Krokovanie v prostredí Netbeans:
    • Klikneme pravým na meno projektu v záložke Projects, zvolíme Step into
    • Tým sa naštartuje a hneď aj zastaví náš program, ďalej sa môžeme púšťať program riadok po riadku ikonkou Step Over (F8)
    • V dolnej časti okna v záložke Variables vidíme hodnoty vybraných premenných, ďalšie si môžeme ručne pridať

Netbeans vs Kate

Výhody Netbeans oproti Kate:

  • ľahšie kompilovanie a spúšťanie pomocou ikoniek
  • zabudovaný debugger
  • ľahšie hľadanie kompilačných chýb
  • mnoho ďalších nástrojov na podporu programovania, ktoré sa ale hodia hlavne pri väčších projektoch

Nevýhody Netbeans oproti Kate:

  • zdĺhavé vytváranie nového projektu
  • pomalší štart programu
  • veľa nástrojov, ktoré zatiaľ nevyužijete

Cyklus while

Okrem cyklu for môžeme použiť aj cyklus while, ktorý vyzerá nasledovne:

while (podmienka) { 
  príkaz; 
}

Čítame: kým je splnená podmienka vykonávaj príkaz. Podrobnejšie:

  • podmienka je výraz, ktorý sa najskôr vyhodnotí:
    • ak je jeho hodnota logická nepravda, cyklus je ukončený a program pokračuje ďalším príkazom za while{...}
    • ak je jeho hodnota logická pravda, vykoná sa príkaz a celý cyklus sa opäť opakuje.

Úroky v banke

Na začiatku každého roku uložíme 1000 EUR na úrokovú vkladnú knižku s ročným úrokom 5%. Zistite, za koľko rokov naše úspory dosiahnu sumu aspoň 20 000 EUR.

Vieme určite, že budeme musieť peniaze vkladať niekoľko rokov. Problém je v tom, že dopredu nevieme povedať koľko, pretože počet rokov je vlastne výstupom programu. Neformálne by sme algoritmus na vyriešenie mohli popísať takto:

  • Založ si účet v banke (zatiaľ šetríš nula rokov)
  • Kým nemáš na začiatku roka našetrené 20 000 EUR opakuj:
    • Vlož čiastku a čakaj na nový rok, kým sa zúročia

Potrebujeme cyklus, ktorý sa bude vykonávať, kým platí nejaká podmienka - práve na to je vhodný cyklus while. Pomocou neho zapíšeme algoritmus nasledovne:

#include <iostream>
using namespace std;

int main(void) {
    double ucet = 0;
    int rok = 0;

    while (ucet < 20000) {
        ucet = (ucet + 1000) * 1.05;
        rok++;   /* to isté ako rok = rok + 1 */
        cout << "Na konci roku " << rok << " máme na účte " << ucet << " EUR" << endl;
    }
}
Na konci roku 1 máme na účte 1050 EUR
Na konci roku 2 máme na účte 2152.5 EUR
Na konci roku 3 máme na účte 3310.12 EUR
Na konci roku 4 máme na účte 4525.63 EUR
Na konci roku 5 máme na účte 5801.91 EUR
Na konci roku 6 máme na účte 7142.01 EUR
Na konci roku 7 máme na účte 8549.11 EUR
Na konci roku 8 máme na účte 10026.6 EUR
Na konci roku 9 máme na účte 11577.9 EUR
Na konci roku 10 máme na účte 13206.8 EUR
Na konci roku 11 máme na účte 14917.1 EUR
Na konci roku 12 máme na účte 16713 EUR
Na konci roku 13 máme na účte 18598.6 EUR
Na konci roku 14 máme na účte 20578.6 EUR

Euklidov algoritmus na nájdenie najväčšieho spoločného deliteľa

  • Jeden z najstarších dodnes používaných algoritmov, Euklides ho popísal v svom diele Základy, cca 300 pred Kr.
  • Máme dané dve kladné celé čísla a a b a chceme nájsť najväčšie číslo d, ktoré delí a aj b.
  • Skratka nsd(a,b), po anglicky gcd(a,b) (greatest common divisor)
  • Príklad:
    • Delitele 12: 1, 2, 3, 4, 6, 12
    • Delitele 8: 1, 2, 4, 8
    • Spoločné delitele 8 a 12: 1, 2, 4
    • gcd(8,12)=4
  • Lema: pre všetky kladné celé čísla a a b platí: gcd(a,b) = gcd(b, a mod b)
    • nech Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle x = a \bmod b, y = \lfloor a/b\rfloor}
    • nech A je množina spoločných deliteľov a a b, B je množina spoločných deliteľov b a x
    • ukážeme, že A=B (a teda aj Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle \max A = \max B} )
    • a = yb + x a preto každý deliteľ b a x delí aj a, t.j. Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle B\subseteq A}
    • taktiež x = a - yb a preto každý deliteľ a aj b delí aj x, t.j. Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle A\subseteq B}
#include <iostream>
using namespace std;

int main(void) {
    int a, b;
    cout << "Enter two positive integers a and b: ";
    cin >> a >> b;
    while(b != 0) {
        int x = a % b;
        a = b;
        b = x;
    }
    cout << "Their gcd: " << a << endl;
}

Príklad behu programu:

Enter two positive integers a and b: 30 8
Their gcd: 2

Tento výpočet prešiel cez dvojice:

30 8
8 6
6 2
2 0

Nekonečný cyklus

while (true) {
 prikaz;
}

Opakuje príkaz donekonečna (kým program nezastavíme).

Napríklad môžeme donekonečna niečo vypisovať na konzolu.

#include <iostream>
using namespace std;

int main(void) {
    while (true) {
      cout << "Hello world!" << endl;
    }
}

Hra hádaj číslo

V nasledujúcom programe si počítač "myslí číslo" od 1 do 100 a užívateľ háda, o ktoré číslo ide.

#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

int main(void) {
    /* vygenerujeme náhodné číslo medzi 1 a 100 */
    srand(time(NULL));
    int number = rand() % 100 + 1;

    /* Pamätáme si, či sme už uhádli alebo nie. */
    bool guessed = false;
    cout << "Guess a number between 1 and 100: ";
    /* Kým užívateľ neuhádne, spýtame sa ho na ďalšiu
       odpoveď. */
    while (!guessed) {
        int guess;
        cin >> guess;
        /* Vyhodnotíme odpoveď. */
        if (guess < number) {
            cout << "Too low, try again: ";
        } else if (guess > number) {
            cout << "Too high, try again: ";
        } else if (guess == number) {
            guessed = true;
            cout << "Correct guess" << endl;
        }
    }
}

Príklad priebehu programu:

Guess a number between 1 and 100: 50
Too low, try again: 75
Too high, try again: 63
Too low, try again: 69
Too high, try again: 66
Too low, try again: 67
Correct guess

Príkazy break a continue (používať s mierou)

  • Príkaz break: skončí sa cyklus, v ktorom práve sme, pokračuje sa prvým príkazom za cyklom.
  • Príkaz continue: ide na ďalšiu iteráciu cyklu, nevykoná zvyšok tela cyklu.

Používať s mierou, robia program menej prehľadný, ľahko zavlečú chyby.

Program uvedený vyššie môžeme prepísať bez boolovskej premennej s použitím nekonečného cyklu, z ktorého ale vyskočíme príkazom break, keď užívateľ uhádne.

#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

int main(void) {
    /* vygenerujeme náhodné číslo medzi 1 a 100 */
    srand(time(NULL));
    int number = rand() % 100 + 1;

    cout << "Guess a number between 1 and 100: ";
    /* Kým užívateľ neuhádne, spýtame sa ho na ďalšiu
       odpoveď. */
    while (true) {
        int guess;
        cin >> guess;
        /* Vyhodnotíme odpoveď. */
        if (guess < number) {
            cout << "Too low, try again: ";
        } else if (guess > number) {
            cout << "Too high, try again: ";
        } else if (guess == number) {
            cout << "Correct guess" << endl;
            break;
        }
    }
}

Späť k cyklu for

Cyklus for má tvar

for(prikaz1; podmienka; prikaz2) {
    prikaz3;
}

Je ekvivalentný nasledujúcemu príkazu while:

prikaz1;
while(podmienka) {
    prikaz3;
    prikaz2;
}

Napríklad, nasledujúce kúsky kódu oba vypisujú čísla 0 až 9:

for (int i = 0; i < 10; i++) {
     cout << " " << i;
}
int i = 0;
while(i < 10) {
     cout << " " << i;
     i++;
}

Cyklus for spravidla používame, ak má náš cyklus krátky a jednoduchý iterátor (prikaz2) a jednoduchú podmienku. V opačnom prípade väčšinou používame cyklus while.


Ešte vypisovanie deliteľov

Ak chceme vypísať deliteľov od najväčších, stačí prepísať for cyklus:

  • od najmenších: for (int i = 1; i <= n; i++) {
  • od najväčších: for (int i = n; i > 0; i--) {
#include <iostream>
using namespace std;

int main(void) {
    int n;
    cout << "Please enter some number: ";
    cin >> n;

    cout << "Divisors of " << n << ":";
    for (int i = n; i > 0; i--) {
        if (n % i == 0) {
            cout << " " << i;
        }
    }
    cout << endl;
}


Príklad behu programu:

Please enter some number: 30
Divisors of 30: 30 15 10 6 5 3 2 1

Zrýchlime program na vypisovanie deliteľov: ak i je deliteľ n, aj n/i je deliteľ, môžeme ho rovno vypísať. Aspoň jeden z tejto dvojice je najviac odmocnina z n.

#include <iostream>
using namespace std;

int main(void) {
    int n;
    cout << "Please enter some number: ";
    cin >> n;

    cout << "Divisors of " << n << ":";

    for(int i=1; i*i<=n; i++) {
        if (n % i == 0) {
            int j = n / i;
            cout << " " << i << " " << j;
        }
    }
    cout << endl;
}

Cvičenie: Tento program nefunguje celkom správne, občas vypíše nejaké číslo dvakrát. Kedy? Ako ho vieme opraviť?

Pomalší program, ktorý skúša všetky čísla po n, trvá pre n=1234567890 na mojom počítači 4.5s. Program, ktorý ide iba po odmocninu trvá 0.0002s.

Vnorené cykly

Vykreslíme štvorec n x n čísel, ktoré sú striedavo 0 a 1, podobne ako biele a čierne políčka na šachovnici.

  • Potrebujeme na to dva vnorené cykly, jeden pôjde cez riadky, druhý pre stĺpce.
  • Ak row + column je párne, píšeme 0, inak píšeme 1.
#include <iostream>

int main(void) {
    int n = 8; /* počet štvorcov v riadku a stĺpci*/

    for (int row = 0; row < n; row++) {
        for (int column = 0; column < n; column++) {
            if ((row + column) % 2 == 0) {
                cout << "0";
            } else {
                cout << "1";
            }
            cout << " ";
        }
        cout << endl;
    }
}

Zhrnutie

  • Videli sme niekoľko príkladov využitia cyklov for a while.
  • Cyklus for je možné zapísať ako while (a naopak).
  • Z cyklu vieme vyskočiť príkazom break, prejsť na ďalšiu iteráciu príkazom continue. Používať s mierou.
  • Euklidov algoritmus rýchlo nájde najväčšieho spoločného deliteľa dvoch čísel.
  • Dôležitá je prehľadná úprava programov a názvy premenných súvisiace s ich obsahom.
  • Netbeans umožňuje krokovať program, mali by ste to však vedieť robiť aj ručne.

Funkcie

  • Funkcia je skupina príkazov s určitým menom, ktorú môžeme spustiť a vykonať tak určitú akciu alebo vypočítať určitú hodnotu
    • Možno ste stretli iné termíny s podobným významom, ako procedúra, metóda, podprogram
  • V našich programoch sme už používali hotové funkcie
    • Napríklad sqrt(x) je funkcia, ktorá nám povie druhú odmocninu čísla x.

Teraz sa pozrieme na to, ako vytvoriť v programe vlastnú funkciu. Doteraz sme vytvorili jedinú funkciu - funkciu main.

Motivačný príklad: Obvod trojuholníka

Chceme napísať program, ktorý načíta súradnice vrcholov trojuholníka a spočíta jeho obvod, tu je príklad jeho behu:

Zadaj suradnice vrcholu A oddelene medzerou: 0 0
Zadaj suradnice vrcholu B oddelene medzerou: 3 0
Zadaj suradnice vrcholu C oddelene medzerou: 0 4
Obvod trojuholnika ABC: 12

Obvod spočítame ako súčet dĺžok jednotlivých strán, v programe teda trikrát opakujeme výpočet dĺžky strany. Pri opakovaní dlhšieho vzorca ľahko spravíme chybu.

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

int main(void) {
    double xa, ya, xb, yb, xc, yc;
    cout << "Zadaj suradnice vrcholu A oddelene medzerou: ";
    cin >> xa >> ya;
    cout << "Zadaj suradnice vrcholu B oddelene medzerou: ";
    cin >> xb >> yb;
    cout << "Zadaj suradnice vrcholu C oddelene medzerou: ";
    cin >> xc >> yc;
    /* spocitaj dlzky stran */
    double da = sqrt((xb - xc) * (xb - xc) + (yb - yc) * (yb - yc));
    double db = sqrt((xa - xc) * (xa - xc) + (ya - yc) * (ya - yc));
    double dc = sqrt((xa - xb) * (xa - xb) + (ya - yb) * (ya - yb));
    cout << "Obvod trojuholnika ABC: " << da + db + dc << endl;
}

Definícia funkcie

Vzorec na výpočet dĺžky úsečky s koncovými bodmi (x1,y1) a (x2,y2) môžeme zapísať ako funkciu:

double dlzka(double x1, double y1, double x2, double y2) {
    return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}

Definícia funkcie má nasledovné časti:

  • Návratová hodnota funkcie: Naša funkcia vracia dĺžku, teda hodnotu typu double (desatinné číslo). V prípade, že by funkcia nemala vrátiť nič, jej návratová hodnota je typu void. Také sú napríklad funkcie, ktoré niečo vypisujú alebo vykresľujú.
  • Názov funkcie: zvolíme si ako chceme, podobne ako pri premenných. Vhodné je použiť názov, ktorý vystihuje, čo funkcia počíta, napríklad dlzka.
  • Zoznam parametrov funkcie: V zátvorkách za názvom funkcie je zoznam typov a názvov premenných, ktoré funkcia očakáva. V našom príklade funkcia očakáva súradnice dvoch vrcholov, teda štyri hodnoty typu double. Funkcie môžu mať zoznam parametrov aj prázdny.
  • Telo funkcie: V zložených zátvorkách za definíciou funkcie je jej vlastný obsah, teda zoznam príkazov, ktoré má funkcia vykonať.
  • Príkaz return: Vracia hodnotu funkcie.

Program s využitím funkcie

Celý program s funkciou dlzka

  • vzorec na výpočet dĺžky je teraz iba na jednom mieste
#include <iostream>
#include <cmath>
using namespace std;

double dlzka(double x1, double y1, double x2, double y2) {
    return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}

int main(void) {
    double xa, ya, xb, yb, xc, yc;
    cout << "Zadaj suradnice vrcholu A oddelene medzerou: ";
    cin >> xa >> ya;
    cout << "Zadaj suradnice vrcholu B oddelene medzerou: ";
    cin >> xb >> yb;
    cout << "Zadaj suradnice vrcholu C oddelene medzerou: ";
    cin >> xc >> yc;
    /* spocitaj dlzky stran */
    double da = dlzka(xb, yb, xc, yc);
    double db = dlzka(xa, ya, xc, yc);
    double dc = dlzka(xa, ya, xb, yb);
    cout << "Obvod trojuholnika ABC: " << da + db + dc << endl;
}

Funkcia môže obsahovať aj viacero príkazov. V našom príklade môžeme napríklad výpočet rozložiť na tri kroky:

double dlzka(double x1, double y1, double x2, double y2) {
    double dx = x1 - x2;
    double dy = y1 - y2;
    return sqrt(dx * dx + dy * dy);
}

Cvičenie: v Pascale je funkcia sqr(x), ktorá vráti x na druhú. Naprogramujte túto funkciu v jazyku C a použite ju na zjednodušenie funkcie dlzka.

  • V jazyku C by sme mohli použiť pow(x,2), ale môže byť jednoduchšie zrátať x*x.

Prednáška 4

Oznamy

Prvý test

  • Koná sa budúcu stredu 17.10. o 18:10 v posluchárni B
  • Pokrýva učivo prednášok 1-4, resp. cvičení 1 až 3
  • Prineste si perá a ťahák v rozsahu jedného listu (dvoch strán) A4
  • Typy príkladov:
    • napíšte krátky program alebo funkciu (podobne ako na cvičeniach, ale na papieri)
    • zistite, čo program vypíše pre určitý vstup
    • doplňte chýbajúce časti programu alebo v ňom nájdite chyby
  • Písanie programu na papieri môže byť zo začiatku ťažké
    • skúste si niektoré príklady z cvičení napísať najskôr na papier, potom prepísať do počítača

Domáca úloha 1

  • Plánujeme zverejniť v stredu, na riešenie budete mať cca 2 týždne
  • Dobrý tréning doteraz odprednášaných tém, hodí sa pred písomkou

Body z cvičení, domácich úloh a písomiek budete vidieť na testovači

  • Momentálne body z cvičení 1 a písomky pre pokročilých

Organizačné poznámky k štúdiu

  • Poriadne si skontrolujte, či máte v AIS a v indexe to isté a či sú to tie predmety, ktoré chcete brať.
    • Prípadné problémy riešte na študijnom
  • Do štvrtka 11.10. je potrebné si prípadné zmeny v zápise dať potvrdiť na študijnom. Pred koncom tejto doby bývajú na študijnom dlhé rady, choďte čím skôr. Úradné hodiny mimoriadne aj 13:00-15:00.
  • Informácia na stránke fakulty: [2]

Opakovanie: funkcie

Výpočet dĺžky úsečky:

double dlzka(double x1, double y1, double x2, double y2) {
    double dx = x1 - x2;
    double dy = y1 - y2;
    return sqrt(dx * dx + dy * dy);
}

Definícia funkcie má nasledovné časti:

  • Návratová hodnota funkcie: Naša funkcia vracia dĺžku, teda hodnotu typu double (desatinné číslo). V prípade, že by funkcia nemala vrátiť nič, jej návratová hodnota je typu void. Také sú napríklad funkcie, ktoré niečo vypisujú alebo vykresľujú.
  • Názov funkcie: zvolíme si ako chceme, podobne ako pri premenných. Vhodné je použiť názov, ktorý vystihuje, čo funkcia počíta, napríklad dlzka.
  • Zoznam parametrov funkcie: V zátvorkách za názvom funkcie je zoznam typov a názvov premenných, ktoré funkcia očakáva. V našom príklade funkcia očakáva súradnice dvoch vrcholov, teda štyri hodnoty typu double. Funkcie môžu mať zoznam parametrov aj prázdny.
  • Telo funkcie: V zložených zátvorkách za definíciou funkcie je jej vlastný obsah, teda zoznam príkazov, ktoré má funkcia vykonať.
  • Príkaz return: Vracia hodnotu funkcie.

Celý program s funkciou dlzka:

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

double dlzka(double x1, double y1, double x2, double y2) {
    double dx = x1 - x2;
    double dy = y1 - y2;
    return sqrt(dx * dx + dy * dy);
}


int main(void) {
    double xa, ya, xb, yb, xc, yc;
    cout << "Zadaj suradnice vrcholu A oddelene medzerou: ";
    cin >> xa >> ya;
    cout << "Zadaj suradnice vrcholu B oddelene medzerou: ";
    cin >> xb >> yb;
    cout << "Zadaj suradnice vrcholu C oddelene medzerou: ";
    cin >> xc >> yc;
    /* spocitaj dlzky stran */
    double da = dlzka(xb, yb, xc, yc);
    double db = dlzka(xa, ya, xc, yc);
    double dc = dlzka(xa, ya, xb, yb);
    cout << "Obvod trojuholnika ABC: " << da + db + dc << endl;
}


Príkaz return

  • Pri vykonaní príkazu return je funkcia okamžite zastavená a jej výstupná hodnota je výraz za slovom return.
  • Ak funkcia má niečo vrátiť (jej návratový typ nie je void), musí obsahovať return.
    • Príkazov return môže obsahovať viac, ale tak, aby každé možné prejdenie funkciou bolo ukončené príkazom return.

Dva spôsoby, ako napísať funkciu min, ktorá vráti menšie z dvoch zadaných čísel a a b:

int min(int a, int b) {
    int minval;
    if (a < b) {
        minval = a;
    } else {
        minval = b;
    }
    return minval;
}
int min(int a, int b) {
    if (a < b) {
        return a;
    } else {
        return b;
    }
}

Vo funkciách, ktoré vracajú void, môže ale nemusí byť return. To znamená, že oba nasledovné programy sú v poriadku.

void printGreeting () {
  cout << "Hello world!" << endl;
}
void printGreeting () {
  cout << "Hello world!" << endl;
  return;
}

Cvičenie: čo vráti nasledujúca funkcia pre n=6? Čo vráti pre n=7? Viete stručne popísať, čo robí pre všeobecné n?

int zahada(int n) {
    for (int i = 2; i * i <= n; i++) {
        if (n % i == 0) { return i; }
    }
    return n;
}

Lokálne a globálne premenné

  • Globálne premenné sú definované mimo akejkoľvek funkcie
    • môžu ich používať všetky funkcie definované pod nimi
  • Lokálne premenné sú definované vo vnútri funkcie
    • môže ich používať iba táto funkcia

Odporúčame používať hlavne lokálne premenné. Vo väčších programoch s veľa funkciami nemusí byť jasné kto kedy a na čo používa jednotlivé globálne premenné, čo znižuje prehľadnosť programu.

Príklad: program s globálnou premennou x a dvomi lokálnymi premennými v rôznych funkciách

#include <iostream>
using namespace std;

int x = 0;  // globálna premenná

void vypis() {
    int y = 1;  // lokálna premenná vo funkcii vypis
    cout << x << " " << y << endl;    
}

int main(void) {
    int z = 2;  // lokálna premenná vo funkcii main
    cout << x << " " << z << endl;
    vypis();
}
  • Viaceré funkcie môžu mať lokálne premenné s tým istým menom, každá používa tú svoju
  • Ak má lokálna a globálna premenná to isté meno, lokálna premenná prekryje globálnu, funkcia teda používa svoju lokálnu premennú (bližšia košeľa ako kabát)

Príklad:

void funkcia1 () {
//  i=13; – nemôžeme používať premennú i – žiadna premenná i nie je viditeľná
}

int i;  // definícia globálnej premennej

void funkcia2 () {
  i = 50; // priradí sa do globálne premennej i
  funkcia1();  
}

void funkcia3 () {
  int i;
  i = 2; // priradí sa do lokálnej premennej i, ktorá vznikla v tejto funkcii
         //  v tomto okamihu existujú 3 premenné s menom i :
         //   i – globálna premenná,
         //   lokálna premenná i, ktorá vznikla vo funkcii main,
         //   a lokálna premenná i, ktorá vznikla v tejto funkcii.
}

void main () {
  int i;
  i = 1;
    // priraďuje sa do lokálnej premennej i a nie do globálnej premennej i,
    // globálna premenná i je prekrytá – identifikátor i označuje lokálnu premennú i.
  funkcia3 ();
    // lokálna premenná i má stále hodnotu 1
  funkcia2 ();
    // lokálna premenná i má stále hodnotu 1, globálna premenná i má hodnotu 50,
}

Parametre funkcií

Odovzdávanie parametrov hodnotou

Funkcie majú často parametre.

  • Tieto parametre sa správajú ako lokálne premenné danej funkcie
  • Pri volaní funkcie sa im priradí hodnota, ktorú zadal používateľ funkcie (tento mechanizmus voláme odovzdávanie parametrov hodnotou)
  • Túto hodnotu síce môžeme meniť, ale táto zmena sa neprejaví na mieste, odkiaľ funkciu voláme

Príklad:

#include <iostream>
using namespace std;

void vypis(int x) {
    cout << "Predtym: " << x;   /* vypiseme povodnu hodnotu x */
    x++;                        /* zvysime hodnotu x o 1 */
    cout << ", potom: " << x << endl;  /* vypiseme nove x */
}

int main(void) {
    int x = 0;
    int y = 3;
    vypis(x);       /* vypise Predtym: 0, potom: 1 */
    vypis(x);       /* vypise Predtym: 0, potom: 1 */
    vypis(y);       /* vypise Predtym: 3, potom: 4 */
    vypis(y);       /* vypise Predtym: 3, potom: 4 */
    vypis(y + 7);   /* vypise Predtym: 10, potom: 11 */
}

Odovzdávanie parametrov referenciou

  • Ak pred meno parametra v hlavičke funkcie dáme &, parameter sa bude odovzdávať referenciou
  • Pri použití takejto funkcie môžeme ako parameter zadať iba premennú, nie iný výraz
  • Namiesto samotnej hodnoty sa funkcii pošle adresa premennej v pamäti (referencia)
  • Táto sa bude vo funkcii používať pod novým menom, prípadné zmeny sa prejavia aj na mieste, odkiaľ sme funkciu volali
#include <iostream>
using namespace std;

void vypis(int & x) {
    cout << "Predtym: " << x;   /* vypiseme povodnu hodnotu x */
    x++;                        /* zvysime hodnotu x o 1 */
    cout << ", potom: " << x << endl;  /* vypiseme nove x */
}

int main(void) {
    int x = 0;
    int y = 3;
    vypis(x);       /* vypise Predtym: 0, potom: 1 */
    vypis(x);       /* vypise Predtym: 1, potom: 2 */
    vypis(y);       /* vypise Predtym: 3, potom: 4 */
    vypis(y);       /* vypise Predtym: 4, potom: 5 */
    /* vypis(y + 7);   toto teraz nemozeme pouzit, y+7 nie je premenna */
}

Odovzdávanie referenciou: viac ako jedna návratová hodnota

Odovzdávanie parametra referenciou používame napríklad vtedy, ak máme vrátiť viac ako jednu hodnotu.

Funkcia stred dostane hodnotou súradnice dvoch bodov (x1,y1) a (x2,y2) a do parametrov xs, ys, ktoré sú odovzdané referenciou, uloží súradnice stredu úsečky.

#include <iostream>
using namespace std;

void stred(double x1, double y1, double x2, double y2, double &xs, double &ys) {
    xs = (x1 + x2) / 2;
    ys = (y1 + y2) / 2;
}

int main(void) {

    double x1, y1, x2, y2;
    double x, y;

    cout << "Napiste suradnice jedneho konca usecky (oddelene medzerou): ";
    cin >> x1 >> y1;

    cout << "Napiste suradnice druheho konca usecky (oddelene medzerou): ";
    cin >> x2 >> y2;

    stred(x1, y1, x2, y2, x, y);
    cout << "Stred usecky je [" << x << "," << y << "]." << endl;
}

Odovzdávanie referenciou: Funkcia, ktorej úlohou je zmeniť svoj parameter

Ďalší dôvod pre odovzdávanie parametrov referenciou je, keď chceme vo funkcii priamo parameter meniť s trvalými následkami.

  • Typickým príkladom je funkcia swap, ktorá vymení hodnoty dvoch premenných, ktoré dostane ako parametre
#include <iostream>
using namespace std;

void swap(int &a, int &b) {
    int tmp = a;
    a = b;
    b = tmp;
}

int main() {
    int x = 1, y = 3;
    swap(x, y);
    cout << "x=" << x << ", y=" << y << endl;
    return 0;
}

Keby sme funkcii odovzdali parameter hodnotou void swap(int a, int b), vo funkcii main by sa premenné nevymenili.

Odovzdávanie indikátoru chyby

Niektoré funkcie potrebujú, aby parameter spĺňal určité podmienky. Napríklad odmocninu vieme spočítať iba z nezáporného čísla. Čo vlastne môžeme urobiť, keď dostaneme zlý parameter?

  • Môžeme okamžite ukončiť celý program. V knižnici cstdlib je funkcia exit, ktorá robí presne to. Má ako parameter návratovú hodnotu celého programu
double mySqrt(double x) {
    if (x < 0) { exit(1); }   // zlý vstup: ukončíme program
    else { return sqrt(x); }  // dobrý vstup: spočítame odmocninu
}
  • Pohodlnejší spôsob ukončenia programu pri zlom vstupe je funkcia assert z knižnice cassert, ktorá ako parameter dostane logickú hodnotu, a ak je tá hodnota nepravda, ukončí program s chybovou hláškou.
    • Pri dôležitých programoch ale nechceme, aby len tak skončili pri každej chybe
double mySqrt(double x) {
    assert(x >= 0);  // tvrdime, ze x>=0, inak program skonci 
    return sqrt(x);  // teraz mozeme veselo odmocnovat 
}
  • Bežný spôsob, o ktorom sa budeme učiť v druhom semestri, sú takzvané výnimky (exceptions)
  • Môžeme vrátiť nejakú špeciálnu hodnotu, ktorá odzrkadľuje, že vstup bol zlý. Používateľ funkcie by mal túto hodnotu rozoznať od správneho výstupu funkcie. Ale dôvera v používateľa sa všeobecne v softvérovom inžinierstve neodporúča.
    • Knižničná funkcia sqrt vráti pre záporné čísla špeciálnu hodnotu NaN (not a number), my vrátime napr. -1
double mySqrt(double x) {
    if (x < 0) { return -1; }  // zlý vstup: vrátime -1
    else { return sqrt(x);  }  // dobrý vstup: spočítame odmocninu
}
  • Môžeme ako výstup funkcie vrátiť, či sa podarilo alebo nepodarilo správne vypočítať hodnotu. Výstupom funkcie bude logická hodnota typu bool a samotnú hodnotu vrátime v parametri odovzdanom referenciou
bool mySqrt(double x, double & result) {
    if (x < 0) { return false; } // zlý vstup: vrátime false
    else {
        result = sqrt(x);    // dobrý vstup: spočítame odmocninu a vrátime true
        return true; 
    }
}

Ešte jeden príklad: Fibonacciho čísla

Fibonacciho postupnosť je postupnosť čísel, v ktorej každý ďalší člen je súčtom dvoch predchádzajúcich. Jednotlivé členy postupnosti sa nazývajú Fibonacciho čísla.

   1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393… 

Na výpočet Fibonacciho čísel používame nasledovný vzťah:

  • F(0) = F(1) = 1
  • F(n)=F(n-1)+F(n-2)

Napíšeme funkciu, ktorá bude počítať Fibonacciho čísla. Na otázku "Aké je ôsme Fibonacciho číslo?" teda odpovie "34".

int fibonacci(int n) {

    int f = 1;     // Cislo F(i) 
    int oldF = 0;  // Cislo F(i-1)
    
    for (int i = 1; i <= n; i++) {
        int newF = f + oldF;  // spocitaj nove F(i) pre vyssie i
        oldF = f;             // poposuvaj hodnoty
        f = newF;
    }

    return f;
}

Cvičenie: napíšte funkciu printFibonacci, ktorá vypíše prvých n Fibonacciho čísel.

  • Akú by mala mať hlavičku?
  • Skúste dva spôsoby implementácie:
    • volaním funkcie fibonacci pre rôzne hodnoty n
    • prepísaním tejto funkcie, aby sa hodnoty vypisovali priamo ako sa počítajú
  • Ktorý spôsob bude rýchlejší pre veľké n a prečo?

Ďalšie poznámky o funkciách

Kde definovať funkciu?

Funkcia musí byť definovaná pred tým, ako ju v programe použijeme, aby kompilátor vedel skontrolovať korektnosť jej použitia (počet parametrov, ich typ..).

Funkcia main

Má od ostatných funkcií niekoľko odlišností

  • Síce má návratový typ (int), ale nemusí nič vracať.
  • V programe sa nemôže volať - jediné jej volanie je automatické (začína sa ním beh programu)
  • Má predpísané argumenty, ktoré sa dajú vynechať (viac o nich neskôr)

Funkcie - zhrnutie

  • Funkcie nám umožňujú rozbiť väčší program na menšie logické časti a tým ho sprehľadniť. Tiež nám umožňujú vyhnúť sa opakovaniu podobných kusov kódu.
  • Hlavička funkcie obsahuje návratový typ (môže byť void), meno funkcie, typy a mená parametrov.
  • V tele funkcie sú samotné príkazy. Vypočítanú hodnotu vrátime príkazom return.
  • Lokálne premenné sú viditeľné len vo funkcii, ktorá ich definuje, globálne vo všetkých funkciách.
  • Parametre odovzdávané hodnotou sú lokálne premenné inicializované určitou hodnotou, ktorú zadá užívateľ funkcie.
  • Parametre odovzdávané referenciou (&) sú len novým menom pre inú premennú.

Záznam typu struct

  • V príkladoch s obvodom trojuholníka a stredom úsečky sme funkciám posielali veľa parametrov (súradnice x a y)
  • Program bude krajší, ak si údaje o jednom bode spojíme do jedného záznamu
struct bod {
    double x, y;
};
  • Pomocou struct vytvoríme nový dátový typ bod, ktorý má zložky x a y
    • V jednom struct-e môžu byť aj položky rôznych typov (napr. struct bod { double x,y; int id; bool visible; };)
  • Môžeme vytvárať premenné typu bod, napr. bod a, b;
  • K položkám bodu pristupujeme pomocou bodky, napr. a.x = 4.0;
  • Do funkcií body posielame radšej referenciou, aby sa zbytočne nekopírovalo veľa hodnôt

Nasledujúci program načíta súradnice troch bodov, spočíta obvod trojuholníka a stredy všetkých troch strán.

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

struct bod {
    double x, y;  // suradnice bodu v rovine
};

double dlzka(bod &bod1, bod &bod2) {
    // funkcia vrati dlzku usecky z bodu 1 do bodu 2
    double dx = bod1.x - bod2.x;
    double dy = bod1.y - bod2.y;
    return sqrt(dx * dx + dy * dy);
}

void stred(bod &bod1, bod &bod2, bod &stred) {
    // funkcia do bodu stred spocita stred usecky z bodu 1 do bodu 2
    stred.x = (bod1.x + bod2.x) / 2;
    stred.y = (bod1.y + bod2.y) / 2;
}

void vypisBod(bod &b) {
    // funkcia vypise surandice bodu v zatvorke a koniec riadku
    cout << "(" << b.x << "," << b.y << ")" << endl;
}

int main(void) {
    // nacitame suradnice vrcholov trojuholnika
    bod A, B, C;
    cout << "Zadaj suradnice vrcholu A oddelene medzerou: ";
    cin >> A.x >> A.y;
    cout << "Zadaj suradnice vrcholu B oddelene medzerou: ";
    cin >> B.x >> B.y;
    cout << "Zadaj suradnice vrcholu C oddelene medzerou: ";
    cin >> C.x >> C.y;
    // spocitame dlzky stran
    double da = dlzka(B, C);
    double db = dlzka(A, C);
    double dc = dlzka(A, B);
    // vypiseme obvod
    cout << "Obvod trojuholnika ABC: " << da + db + dc << endl;

    // spocitame stredy stran
    bod stredAB;
    stred(A, B, stredAB);
    bod stredAC;
    stred(A, C, stredAC);
    bod stredBC;
    stred(B, C, stredBC);

    // vypiseme stredy stran
    cout << "Stred strany AB: ";
    vypisBod(stredAB);
    cout << "Stred strany AC: ";
    vypisBod(stredAC);
    cout << "Stred strany BC: ";
    vypisBod(stredBC);
}

Príklad behu programu:

Zadaj suradnice vrcholu A oddelene medzerou: 0 0
Zadaj suradnice vrcholu B oddelene medzerou: 0 3
Zadaj suradnice vrcholu C oddelene medzerou: 4 0
Obvod trojuholnika ABC: 12
Stred strany AB: (0,1.5)
Stred strany AC: (2,0)
Stred strany BC: (2,1.5)

Spracovanie väčšieho množstva dát

Naše programy doteraz spracovávali len malý počet vstupných dát načítaných od užívateľa (napr. súradnice trocho bodov). Často však chceme pracovať s väčším množstvom dát

  • Na budúcej prednáške si ukážeme, ako uložiť väčšie množstvo dát do poľa
  • Na niektoré úlohy však pole nepotrebujeme - údaje môžeme spracovávať rovno ako ich užívateľ zadáva

V nasledujúcich príkladoch užívateľ zadá číslo N a potom N celých čísel

  • Predstavme si napríklad, že učiteľ zadá body, ktoré študenti dostali na písomke (napr. celé čísla v rozsahu 0..10)
  • Z týchto bodov chceme spočítať nejaké štatistiky

Priemer

#include <iostream>
using namespace std;

int main(void) {
    int N;
    cout << "Zadaj pocet cisel: ";
    cin >> N;

    int sucet = 0;
    cout << "Zadavaj cisla: ";
    for (int i = 0; i < N; i++) {
        int x;
        cin >> x;
        sucet += x;
    }

    double priemer = sucet / (double) N;
    cout << "Priemer je " << priemer << "." << endl;
}
  • Čo by sa stalo, keby sme vo výpočte priemeru vynechali (double)?

Prednáška 5

Oznamy

  • DÚ1 bude zverejnená pravdepodobne dnes večer
  • Budúcu stredu večer test
  • Príďte na dnešné doplnkové cvičenia, ak sa vám včera nepodarilo vyriešiť veľa príkladov. Pomôžeme vám s ďalšími príkladmi na tento týždeň
    • Aj ak ste nespravili rozcvičku, stále môžete z tohto týždňa dostať 8 bodov z 10
  • Skúste si niektoré príklady vyriešiť najskôr na papieri ako tréning na test
  • Dnes polia (nebudú na teste), budúci pondelok hlavne ďalšie príklady a algoritmy s použitím tých častí Cčka, ktoré už poznáte

Opakovanie: spracovanie väčšieho množstva dát

V nasledujúcich príkladoch užívateľ zadá číslo N a potom N celých čísel

  • Predstavme si napríklad, že učiteľ zadá body, ktoré študenti dostali na písomke (napr. celé čísla v rozsahu 0..10)
  • Z týchto bodov chceme spočítať nejaké štatistiky

Priemer

#include <iostream>
using namespace std;

int main(void) {
    int N;
    cout << "Zadaj pocet cisel: ";
    cin >> N;

    int sucet = 0;
    cout << "Zadavaj cisla: ";
    for (int i = 0; i < N; i++) {
        int x;
        cin >> x;
        sucet += x;
    }

    double priemer = sucet / (double) N;
    cout << "Priemer je " << priemer << "." << endl;
}

Maximum

#include <iostream>
using namespace std;

int main(void) {
    int max, x, N;

    cout << "Zadaj pocet cisel: ";
    cin >> N;

    cout << "Zadavajte cisla: ";

    max = ?
    for (int i = 0; i < N; i++) {
        cin >> x;
        if (x > max) {
            max = x;
        }
    }

    cout << endl << "Maximum je " << max << endl;
}

Ako ale začať? Ako nastaviť maximum na začiatok?

  • Jedna možnosť je nastaviť ho na nejakú veľmi malú hodnotu, aby sa iste neskôr zmenila. Kto nám ale zaručí, že používateľ nedá všetky čísla ešte menšie?
  • Riešením je použiť najmenšie možné číslo - ale je príliš viazané na konkrétny rozsah, nebude fungovať po zmene typu premenných.
  • Ďalšia možnosť je si pamätať, že ešte nemáme správne nastavené maximum a po načítaní prvého čísla ho nastaviť alebo spracovať prvé číslo zvlášť (mimo cyklu)
#include <iostream>
using namespace std;

int main(void) {
    int max, x, N;

    cout << "Zadaj pocet cisel: ";
    cin >> N;

    cout << "Zadavajte cisla: ";
    cin >> x;   // načítanie prvého čísla
    max = x;

    for (int i = 1; i < N; i++) {  // cyklus cez N-1 ďalších čísel 
        cin >> x;
        if (x > max) {
            max = x;
        }
    }

    cout << endl << "Maximum je " << max << endl;
}

Cvičenie:

  • Ako by sme program rozšírili tak, aby vedel vypísať aj koľké číslo v poradí bolo najväčšie?
  • Čo treba v programe zmeniť, ak chceme hľadať minimum namiesto maxima?
  • Spočítajte, koľkokrát sa na vstupe vyskytuje číslo 0

Podpriemer / nadpriemer

Chceme spočítať priemer a o každom vstupnom čísle vypísať, či je nadpriemerné alebo podpriemerné.

  • Priemer vieme až keď načítame všetky čísla, musíme si ich teda zapamätať
  • Na to používame tabuľky, ktoré sa volajú polia.
  • Na začiatok pre jednoduchosť predpokladajme, že vopred vieme, že počet údajov N je napr. 20 (N teda nenačítavame)
  • Príkaz int a[20]; vytvorí pole s 20 premennými typu int, ku ktorým pristupujeme a[0], a[1], ..., a[19]
    • pozor, a[20] neexistuje
#include <iostream>
using namespace std;

int main(void) {
    const int N = 20; // premennu N uz nebude mozne menit, ma konstantnu hodnotu 20
    int a[N];
    double sucet = 0;

    cout << "Zadavajte " << N << " cisel: ";
    for (int i = 0; i < N; i++) {
        cin >> a[i];
        sucet += a[i];
    }

    double priemer = sucet / N;
    cout << "Priemer je " << priemer << "." << endl;
    for (int i = 0; i < N; i++) {
        if (a[i] > priemer) { 
             cout << a[i] << ": vacsie ako priemer." << endl;
        } else if (a[i] < priemer) {
             cout << a[i] << ": mensie ako priemer." << endl;
        } else {
             cout << a[i] << ": priemer." << endl;
        }
    }
}

Na zamyslenie:

  • Pozor, chyby pri zaokrúhľovaní môžu spôsobiť, že niekedy priemerné číslo bude považované za nad/podpriemerné
    • Vedeli by ste program prerobiť tak, aby používal iba premenné typu int a nerobil žiadnu chybu v zaokrúhľovaní?
    • Môže aj po takejto zmene niekedy dať zlú odpoveď?

Polia

Rozsah poľa je konštantný výraz väčší ako 0. Prvky sa indexujú od 0 po počet - 1

int a[10];
const int N=20;
double b[N];

Ak nepoznáme vopred počet prvkov, ktoré chceme dať do poľa, môžeme odhadnúť, že ich nebude viac ako NMax, ktoré definujeme ako konštantu v programe.

  • Vytvoríme pole veľkosti NMax, použijeme z neho len prých N hodnôt
#include <iostream>
using namespace std;

int main(void) {
    const int NMax = 1000;
    int p[NMax];
    int N;
    cout << "Zadaj pocet cisel: ";
    cin >> N;
    if (N > NMax) {
        cout << "Prilis velke N" << endl;
        return 1;  // ukončíme funkciu main a tým aj program
    }
   cout << "Zadavajte " << N << " cisel: ";
   ...


Čo ak ako veľkosť poľa použijeme premennú N, ktorú si prečítame od používateľa alebo inak spočítame za behu?

    int N;
    cout << "Zadaj pocet cisel: ";
    cin >> N;
    int p[N];
  • V starších verziách C resp. C++ to nefunguje, aj keď niektoré novšie kompilátory to zvládajú
  • Na prednáškach tento spôsob nebudeme používať
  • Pole veľkosti N, ktorá nie je konštanta, sa naučíme vytvárať inak v druhej polovici semestra

Vytvorenie a inicializácia poľa

V definícii môžeme pole inicializovať zoznamom prvkov.

int A[4]={3, 6, 8, 10}; //spravne
int B[4]; //spravne
B[4]={3, 6, 8, 10};  //nespravne, pole tu nedefinujeme
B[0]=3; B[1]=6; B[2]=8; B[3]=10;  // spravne - menime prvky existujuceho pola

Indexovanie hodnotou mimo intervalu

Pozor, kompilátor nekontroluje indexy prvkov

  int a[10];
  a[10] = 1234;
  • Skompiluje, ale hodnota 1234 sa zapíše do pamäti na zlé miesto
  • Môže to mať nepredvídateľné následky: prepísanie obsahu iných premenných (chybný výpočet alebo „nevysvetliteľné“ správanie sa programu) alebo dokonca prepísanie časti kódu vášho programu

Kopírovanie a testovanie rovnosti

V prípade, že chceme vytvoriť pole, ktoré je kópiou už existujúceho poľa, ponúka sa možnosť príkazu priradenia b=a;. Takýto príkaz však neskompiluje – nedá sa takto priraďovať, treba kopírovať prvok po prvku.

  for (i=0; i<10; i++) b[i]=a[i];

Polia sa tiež nedajú porovnávať pomocou operátora ==. Podmienku if (a==b) cout << "Ok";. síce skompilujete, ale nikdy to nebude pravda – neporovná sa obsah poľa, ale niečo úplne iné (adresy polí v pamäti). Treba opäť porovnávať prvok po prvku.

  bool rovne = true;
  for (i = 0; i < 10; i++) { 
      rovne = rovne && a[i] == b[i]; 
  }
  if (rovne) cout << "Rovnaju sa" << endl;

Ten istý kúsok programu môžeme napísať napr. aj takto:

  bool rovne = true;
  for (i = 0; i < 10; i++) { 
      if(a[i] != b[i]) {
          rovne = false;
          break;
      }
  }
  if (rovne) cout << "Rovnaju sa" << endl;

Výskyty čísel 0...9

Na vstupe je číslo N' a N celých čísel od 0 do 9 a chceme vedieť, koľkokrát sa jednotlivé čísla na vstupe vyskytli.

Prvý prístupy:

  • vstupné čísla uložíme do poľa
  • pre každú hodnotu i od 0 po 9 prejdeme pole a spočítame počet výskytov i

Druhý prístup

  • samotné vstupné čísla neukladáme do poľa, spracovávame ich po jednom
  • vytvoríme si pole počítadiel dĺžky 10, v ktorom p[i] bude počet výskytov čísla i
#include <iostream>
using namespace std;

int main(void) {
    int p[10];  // pole dlzky 10
    int N;
    cout << "Zadajte pocet cisel: ";
    cin >> N;

    for (int i = 0; i < 10; i++) {
       p[i] = 0; // inicializácia poľa p[0]=0; p[1]=0; ... p[9]=0;
    }

    cout << "Zadavajte " << N << "cisel z intervalu 0-9: ";
    for (int i = 0; i < N; i++) {
        int x;
        cin >> x;
        if (x >= 0 && x < 10) { // test, či je číslo z požadovaného rozsahu
            p[x]++;             // zvýšime počítadlo pre hodnotu x
        }
    }

    cout << endl;
    for (int i = 0; i < 10; i++) {
        cout << "Pocet vyskytov " << i << " je " << p[i] << endl; // výpis
    }
}


Odbočka: grafická knižnica SVGdraw

Výsledný obrázok
  • Aplikácie s grafickým rozhraním budeme programovať až budúci semester
  • V tomto semestri, ale budeme kresliť obrázky v SVG formáte pomocou jednoduchej knižnice #SVGdraw
  • Knižnicu si stiahnite a nainštalujte podľa návodu
  • Na začiatku programu zapnite knižnicu pomocou #include "SVGdraw.h"
  • Tu je malý ukážkový program, ktorý vykreslí zelený štvorec a v ňom červený kruh:
#include "SVGdraw.h"

int main(void) {
  /* Vytvor obrázok s šírkou 150 a výškou 100 a 
   * ulož ho do súboru stvorec.svg*/
  SVGdraw drawing(150, 100, "stvorec.svg");

  /* Nastav farbu vyfarbovania na zelenú. */
  drawing.setFillColor("green");
  /* Vykresli štvorec s ľavým horným rohom v bode (20,10)
   * a s dĺžkou strany 80, t.j. pravým dolným rohom 100,90 */
  drawing.drawRectangle(20,10,80,80);
  
  /* Nastav farbu vyfarbovania na červenú. */
  drawing.setFillColor("red");
  /* Vykresli kruh so stredom v bode (60,50) a polomerom 40. */
  drawing.drawEllipse(60,50,40,40);

  /* Ukonči vypisovanie obrázka. */
  drawing.finish();
}

Kreslíme kruhy

P5-kruhy.png

Nasledujúci program náhodne vygeneruje súradnice niekoľkých kruhov a vykreslí ich pomocou kružnice SVGdraw.

  • Údaje o jednom kruhu si uloží do záznamu struct kruh, v programe používame pole takýchto kruhov.
  • Navyše o každom kruhu zistí, či sa pretína s iným kruhom, a ak áno, pri vykresľovaní ho orámuje červenou farbou.
#include "SVGdraw.h"
#include <cstdlib>
#include <ctime>
#include <cmath>

struct kruh {
    int x, y; /* suradnice stredu */
    int polomer; /* polomer kruhu */
    bool pretinaSa; /* pretína sa s iným kruhom? */
};

void generujKruh(kruh &k, int velkost, int polomer) {
    /* inicializuj kruh s nahodnou polohou a danym polomerom */
    k.x = rand() % (velkost - 2 * polomer) + polomer;
    k.y = rand() % (velkost - 2 * polomer) + polomer;
    k.polomer = polomer;
}

bool pretinajuSa(kruh &k1, kruh &k2) {
    /* zisti, ci sa dva kruhy pretinaju */
    int dx = k1.x - k2.x;
    int dy = k1.y - k2.y;
    double d2 = sqrt(dx * dx + dy * dy);
    return d2 <= k1.polomer + k2.polomer;
}

int main(void) {
    const int pocet = 10; /* počet kruhov */
    const int velkost = 300; /* veľkosť obrázku */
    const int polomer = 15; /* polomer kruhu */

    /* inicializácia generátora pseudonáhodných čísel */
    srand(time(NULL));

    /* inicializácia obrázku */
    SVGdraw drawing(velkost, velkost, "kruhy.svg");

    /* pole kruhov */
    kruh kruhy[pocet];

    for (int i = 0; i < pocet; i++) {
        /* kazdemu kruhu vygeneruj nahodnu polohu */
        generujKruh(kruhy[i], velkost, polomer);
    }

    /* zisti, ktoré kruhy pretínajú iné kruhy */
    for (int i = 0; i < pocet; i++) {
        kruhy[i].pretinaSa = false;
        for (int j = 0; j < pocet; j++) {
            if (i != j && pretinajuSa(kruhy[i], kruhy[j])) {
                kruhy[i].pretinaSa = true;
            }
        }
    }

    /* vykresluj kruhy */
    drawing.setFillColor("gray");
    for (int i = 0; i < pocet; i++) {
        if (kruhy[i].pretinaSa) {
            drawing.setLineColor("red");
        } else {
            drawing.setLineColor("black");
        }
        drawing.drawEllipse(kruhy[i].x, kruhy[i].y,
                            kruhy[i].polomer, kruhy[i].polomer);
    }

    /* ukoncenie vykreslovania */
    drawing.finish();
}

Eratostenovo sito

Chceme vypísať všetky prvočísla medzi 2 a N. Mohli by sme ísť cez všetky čísla a pre každé testovať, koľko má deliteľov (deliteľov sme už hľadali predtým), ale vieme to spraviť aj rýchlejšie. Použijeme algoritmus zvaný Eratostenovo sito.

  • Vytvoríme pole A pravdivostných hodnôt, kde A[i] nám hovorí, či je i ešte potenciálne prvočíslo.
  • Na začiatku budú všetky hodnoty true, lebo sme ešte žiadne číslo nevylúčili.
  • Začneme číslom 2 - toto je iste prvočíslo (tak ho vypíšeme). O jeho násobkoch však vieme, že iste nemôžu byť prvočísla - nastavíme preto pre každý násobok j=2*k pravdivostnú hodnotu A[j] na false.
  • Potom prechádzame v poli, kým nenájdeme najbližšiu ďalšiu hodnotu true. Toto číslo je prvočíslo (vypíšeme ho) a vyškrtáme jeho násobky.
#include <iostream>
using namespace std;

int main(void) {
    const int N = 25;
    bool A[N + 1];

    for (int i = 2; i <= N; i++) {
        A[i] = true;
    }
    for (int i = 2; i <= N; i++) {
        if (A[i]) {
            cout << i << " ";
            for (int j = 2 * i; j <= N; j = j + i) {
                A[j] = false;
            }
        }
    }
    cout << endl;
}

Výstup programu

2 3 5 7 11 13 17 19 23

Priebeh programu:

0  1  2  3  4  5  6  7  8  9 10 11 12 ...
?  ?  T  T  T  T  T  T  T  T  T  T  T ... na zaciatku
?  ?  T  T  F  T  F  T  F  T  F  T  F ... po vyskrtani i=2
?  ?  T  T  F  T  F  T  F  F  F  T  F ... po vyskrtani i=3
?  ?  T  T  F  T  F  T  F  F  F  T  F ... dalej sa uz skrtaju len vacsie cisla

Cvičenie: Napíšte funkciu, ktorá uloží prvočísla medzi 2 a N do poľa (ak by sme ich chceli použiť na ďalšie výpočty).

Ďalšie príklady na prácu s poľom

  • Načítajte pole čísel a vypíšte ho v opačnom poradí.
  • Skúste poradie povymieňať priamo v poli a nie iba pri výpise.
  • Načítajte pole čísel, náhodne ich premiešajte a zase vypíšte.

Polia: zhrnutie

  • Pole je tabuľka hodnôt. V poli dĺžky n máme hodnoty p[0], p[1], ..., p[n-1]
  • Kopírovanie a porovnávanie polí si musíme naprogramovať
  • C resp. C++ nekontrolujú, či index nie je mimo rozsahu poľa

Parametre funkcií - prehľad, opakovanie

  • Jednoduché typy, napr. int, double, bool
    • Bez & sa skopíruje hodnota
    • S & premenná dostane vo funkcii nové meno
void f1(int x) {
    x++; // zmena x sa neprenesie do main (y zostane rovnaká)
    cout << x << endl;
}

void f2(int &x) {
    x++; // zmena x sa prenesie ako zmena y v main
    cout << x << endl;
}

int main(void) {
    int y = 0;
    f1(y);
    f2(y);
    f1(y + 1);
    //zle: f2(y+1);
}
  • Polia odovzdávame bez &
    • väčšinou potrebujeme poslať aj veľkosť poľa, ak nie je globálne známa
    • zmeny v poli zostanú aj po skončení funkcie
void f(int a[], int n) {
    for (int i = 0; i < n; i++) {
        cout << a[i] << endl;
    }
}

int main(void) {
    int b[3] = {1, 2, 3};
    f(b, 3);
}
  • SVGdraw obrázky sú v skutočnosti objekty, väčšinou ich chcete posielať s &
    • všetky zmeny na nich spravené pretrvávajú aj po skončení funkcie
#include "SVGdraw.h"
void kresli(SVGdraw &drawing, int n, int y, int rozostup, int velkost) {
   for(int i=0; i<n; i++) {
       drawing.drawRectangle(i*rozostup+velkost, y, velkost, velkost);
   }
}
int main(void) {
    SVGdraw drawing(320, 100, "stvorce.svg");
    kresli(drawing, 10, 50, 30, 10);
    drawing.finish();
}
  • Štruktúry (struct) väčšinou tiež posielame pomocou &
  • Návratové hodnoty:
    • ak je výsledkom funkcie jedno číslo alebo pravdivostná hodnota, vrátime ju príkazom return
    • ak je výsledkom viac hodnôt, alebo niečo zložitejšie (pole, struct,...), odovzdáme ho ako parameter pomocou &, návratová hodnota môže zostať void
  • Tieto pravidlá súvisia so smerníkmi a správou pamäti, povieme si viac o pár týždňov

Prednáška 6

  • DÚ1 do piatku 26.10. 22:00
  • V stredu o 18:10 písomka, pripravte si ťahák 1 A4

Prebrali sme premenné, polia, podmienky, cykly a funkcie.

  • Z týchto stavebných prvkov sa dajú vystavať pomerne komplikované programy. Menej skúsení programátori si potrebujú prácu s týmito pojmami čo najviac precvičiť. Skúste si vyriešiť všetky príklady z cvičení, pracujte na domácej úlohe. Kontaktujte nás, ak Vám niečo nie je jasné.
  • Predbežný plán: dnes algoritmy s poliami. V stredu začneme rekurziu (potenciálne náročné učivo).
  • Rozcvička zajtra sa pravdepodobne tiež bude týkať polí

Funkcie a polia: načítanie a vypísanie poľa

Dve základné funkcie, ktoré sa nám môžu zísť v programoch

  • zopakujeme si tiež ako sa pracuje s poliami vo funkciách
#include <cstdlib>
#include <iostream>
using namespace std;


void readArray(int a[], int &n, int maxN) {
    /* Od užívateľa načíta počet vstupných čísel,
     * tento počet uloží do premennej n. 
     * Potom načíta n celých čísel a uloží ich do poľa a,
     * Hodnota maxN je veľkosť poľa,
     * ktorú nemožno prekročiť. */

    cin >> n;
    if (n > maxN) {
        cout << "Prilis velke n!" << endl;
        exit(1);
    }
    for (int i = 0; i < n; i++) {
        cin >> a[i];
    }
}

void printArray(int a[], int n) {
   for (int i = 0; i < n; i++) {
       cout << " " << a[i];
   }
   cout << endl;
}

int main(void) {
    const int maxN = 100;
    int n;
    int a[maxN];

    readArray(a, n, maxN);

    // tu môžeme pole nejako upraviť

    printArray(a, n);
}

Triedenia

Máme pole čísel, chceme ich usporiadať od najmenšieho po najväčšie.

  • Napr. pre pole 9 3 7 4 5 2 chceme dostať 2 3 4 5 7 9
  • Jeden z najštudovanejších problémov v informatike.
  • Súčasť mnohých zložitejších algoritmov.
  • Veľa spôsobov, ako triediť, dnes si ukážeme zopár najjednoduchších.

Bublinkové triedenie (Bubble Sort)

Idea: Kontrolujeme všetky dvojice susedných prvkov a keď vidíme menšie číslo za väčším, vymeníme ich

for (int i = 1; i < n; i++) {
     if (a[i] < a[i - 1]) {
         swap(a[i - 1], a[i]);
     }
}
  • Ak sme nenašli v poli žiadnu dvojicu, ktorú treba vymeniť, skončili sme.
  • Inak celý proces opakujeme znova.

Celé triedenie:

void swap(int &x, int &y) {
    /* Vymeň hodnoty premenných x a y. */
    int tmp = x;
    x = y;
    y = tmp;
}

void sort(int a[], int n) {
    /* usporiadaj prvky v poli a od najmenšieho po najväčší */

    bool hotovo = false;
    while (!hotovo) {
        bool vymenil = false;
        /* porovnávaj všetky dvojice susedov, vymeň ak menší za väčším */
        for (int i = 1; i < n; i++) {
            if (a[i] < a[i - 1]) {
                swap(a[i - 1], a[i]);
                vymenil = true;
            }
        }
        /* ak sme žiadnu dvojicu nevymenili, môžeme skončiť. */
        if (!vymenil) {
            hotovo = true;
        }
    }
}
  • Čo ak vo for cykle dám for (int i = 0; i < n; i++) {
  • Ako nahradím premennú hotovo príkazom break?

Príklad behu:

prvá iterácia while cyklu
 9 3 7 4 5 2
 3 9 7 4 5 2
 3 7 9 4 5 2
 3 7 4 9 5 2
 3 7 4 5 9 2
 3 7 4 5 2 9

druhá iterácia while cyklu
 3 7 4 5 2 9
 3 4 7 5 2 9
 3 4 5 7 2 9
 3 4 5 2 7 9

tretia iterácia while cyklu
 3 4 5 2 7 9
 3 4 2 5 7 9

štvrtá iterácia while cyklu
 3 4 2 5 7 9
 3 2 4 5 7 9

piata iterácia while cyklu
 3 2 4 5 7 9
 2 3 4 5 7 9

Cvičenie: Ako sa bude správať algoritmus na nasledujúcich vstupoch, koľkokrát zopakuje vonkajší while cyklus?

  • Utriedené pole 1,2,...,n
  • Pole n,1,2,...,n-1
  • Pole 2,3,...,n,1
  • Pole n,n-1,...,1

Triedenie výberom (selection sort, max sort)

Idea: nájdime najväčší prvok a uložme ho na koniec. Potom nájdime najväčší medzi zvyšnými a uložme ho na druhé miesto odzadu atď.

  • V cykle uvádzame ako komentár invariant, podmienku, ktorá na tom mieste vždy platí. Takýto invariant nám pomôže si uvedomiť, že je náš program správny.
int maxIndex(int a[], int n) {
    /* vráť index, na ktorom je najväčší prvok z prvkov a[0]...a[n-1] */
    int index = 0;
    for(int i=1; i<n; i++) {
        if(a[i]>a[index]) {
            index = i;
        }
        /* invariant: a[j]<=a[index] pre vsetky j=0,...,i*/
    }
    return index;
}

void sort(int a[], int n) {
    /* usporiadaj prvky v poli a od najmenšieho po najväčší */

    for(int kam=n-1; kam>=1; kam--) {
        /* invariant: a[kam+1]...a[n-1] sú utriedené
         * a pre každé i,j také že 0<=i<=kam, kam<j<n platí a[i]<=a[j] */
        int index = maxIndex(a, kam+1);
        swap(a[index], a[kam]);
    }
}

Príklad behu programu

Vstup           9 3 7 4 5 2 
Po výmene (9,2) 2 3 7 4 5 9
Po výmene (7,5) 2 3 5 4 7 9
Po výmene (5,4) 2 3 4 5 7 9
Po výmene (4,4) 2 3 4 5 7 9
Po výmene (3,3) 2 3 4 5 7 9

Cvičenie: Bude čas behu algoritmu výrazne odlišný pre pole utriedené správne a pole utriedené v opačnom poradí?

Triedenie vkladaním (Insertion Sort)

Idea:

  • v prvej časti poľa prvky v utriedenom poradí
  • zober prvý prvok z druhej časti a vlož ho na správne miesto v utriedenom poradí

Príklad behu algoritmu:

 9 3 7 4 5 2
 3 9 7 4 5 2
 3 7 9 4 5 2
 3 4 7 9 5 2
 3 4 5 7 9 2
 2 3 4 5 7 9

Ako spraviť vkladanie:

  • Vkladaný prvok si zapamätáme v pomocnej premennej
  • Utriedené prvky posúvame o jedno doprava, kým nenájdeme správne miesto pre odložený prvok
void sort(int a[], int n) {
    /* usporiadaj prvky v poli a od najmenšieho po najväčší */

    for (int i = 1; i < n; i++) {
        int prvok = a[i];
        int kam = i;
        while (kam > 0 && a[kam - 1] > prvok) {
            a[kam] = a[kam - 1];
            kam--;
        }
        a[kam] = prvok;
    }
}
  • Všimnime si podmienku (kam > 0 && a[kam - 1] > prvok)
    • Ak kam==0, prvá časť je false, druhá časť sa už nevyhodnocuje
    • Ak by sme prehodili časti, program by mohol spadnúť (a[kam - 1] > prvok && kam > 0)

Cvičenie: Ako sa bude správať algoritmus na nasledujúcich vstupoch, koľkokrát zopakuje priradenie a[kam]=a[kam-1]?

  • Utriedené pole 1,2,...,n
  • Pole n,1,2,...,n-1
  • Pole 2,3,...,n,1
  • Pole n,n-1,...,1

Triedenia: zhrnutie

  • Videli sme tri jednoduché algoritmy na triedenie
  • Neskôr sa naučíme rýchlejšie algoritmy na triedenie, ktoré používajú rekurziu
  • Precvičili sme si funkcie, parametre a polia
  • K funkciám je dobré napísať, čo robia
  • Do cyklov si môžeme písať invarianty
    • Používajú sa pri formálnom dokazovaní správnosti
    • Pomáhajú pochopeniu kódu
    • Môžeme ich použiť na ručnú alebo automatickú kontrolu správnosti hodnôt premenných
    • Príkaz assert v knižnici cassert kontroluje podmienku, napr. assert(i>=0 && i<n); ukončí program s chybovou hláškou ak podmienka neplatí

Cvičenie:

  • Na vstupe sú dve n-prvkové množiny (čísla v množine sa neopakujú, ale môžu byť zadané v ľubovoľnom poradí)
  • Zistite, tieto množiny obsahujú rovnaké prvky
    • Viete pri tom použiť triedenie?

Načo sú v programovaní dobré funkcie

  • Rozbijeme veľký problém na menšie dobre definované časti (napr. načítaj pole, utrie pole, vypíš pole), každou časťou sa môžeme zaoberať zvlášť. Výsledný program je ľahšie pochopiteľný, najmä ak u každej funkcie napíšeme, čo robí.
  • Vyhneme sa opakovaniu kusov kódu, jednu funkciu môžeme zavolať viackrát. Pri kopírovaní kusov kódu ľahko narobíme chyby, a ak chceme niečo meniť, musíme meniť na veľa miestach.
  • Hotové funkcie môžeme použiť aj v ďalších programoch, prípadne z nich zostavovať nové knižnice, napríklad knižnicu na prácu s poliami.

Zdrojový kód programu s triedeniami

/* Program s triedeniami z prednášky 6.
   http://compbio.fmph.uniba.sk/vyuka/prog/index.php/Predn%C3%A1%C5%A1ka_6 */
#include <iostream>
#include <cstdlib>
using namespace std;

void swap(int &x, int &y) {
    /* Vymeň hodnoty premenných x a y. */
    int tmp = x;
    x = y;
    y = tmp;
}

void readArray(int a[], int &n, int maxN) {
    /* Od užívateľa načíta počet vstupných čísel,
     * tento počet uloží do premennej n. 
     * Potom načíta n celých čísel a uloží ich do poľa a,
     * Hodnota maxN je veľkosť poľa,
     * ktorú nemožno prekročiť. */

    cin >> n;
    if (n > maxN) {
        cout << "Prilis velke n!" << endl;
        exit(1);
    }
    for (int i = 0; i < n; i++) {
        cin >> a[i];
    }
}

void printArray(int a[], int n) {
   for (int i = 0; i < n; i++) {
       cout << " " << a[i];
   }
   cout << endl;
}

void bubbleSort(int a[], int n) {
    /* usporiadaj prvky v poli a od najmenšieho po najväčší */

    bool hotovo = false;
    while (!hotovo) {
        bool vymenil = false;
        /* porovnávaj všetky dvojice susedov, vymeň ak menší za väčším */
        for (int i = 1; i < n; i++) {
            if (a[i] < a[i - 1]) {
                swap(a[i - 1], a[i]);
                vymenil = true;
            }
        }
        /* ak sme žiadnu dvojicu nevymenili, môžeme skončiť. */
        if (!vymenil) {
            hotovo = true;
        }
    }
}
int maxIndex(int a[], int n) {
    /* vráť index, na ktorom je najväčší prvok z prvkov a[0]...a[n-1] */
    int index = 0;
    for(int i=1; i<n; i++) {
        if(a[i]>a[index]) {
            index = i;
        }
        /* invariant: a[j]<=a[index] pre vsetky j=0,...,i*/
    }
    return index;
}

void selectionSort(int a[], int n) {
    /* usporiadaj prvky v poli a od najmenšieho po najväčší */

    for(int kam=n-1; kam>=1; kam--) {
        /* invariant: a[kam+1]...a[n-1] sú utriedené
         * a pre každé i,j také že 0<=i<=kam, kam<j<n platí a[i]<=a[j] */
        int index = maxIndex(a, kam+1);
        swap(a[index], a[kam]);
    }
}

void insertionSort(int a[], int n) {
    /* usporiadaj prvky v poli a od najmenšieho po najväčší */

    for (int i = 1; i < n; i++) {
        // inv1
        int prvok = a[i];
        int kam = i;
        while (kam > 0 && a[kam - 1] > prvok) {
            // inv2
            a[kam] = a[kam - 1];
            kam--;
        }
        a[kam] = prvok;
    }
}

int main(void) {
    const int maxN = 100;
    int n;
    int a[maxN];

    readArray(a, n, maxN);

    printArray(a, n);
    insertionSort(a, n);
    printArray(a,n);
}

Vyhľadávanie prvkov poľa

Chceme zistiť, či pole obsahuje prvok zadanej hodnoty.

  • Musíme prejsť celé pole, lebo nevieme, kde sa prvok môže nachádzať.
  • Tento algoritmus sa nazýva lineárne vyhľadávanie
/* Funkcia find vráti index výskytu prvku x
 * v poli a. Ak sa x v poli nevyskytuje, vráti -1.
 * Hodnota n určuje počet prvkov poľa. */
int find(int a[], int n, int x) {
    for (int i = 0; i < n; i++) {
        if (a[i] == x) return i;
    }
    return -1;
}
  • Toto môžeme urobiť aj v prípade, že máme pole usporiadané.
  • Ale neexistuje lepšie riešenie?

Binárne vyhľadávanie

Rýchlejšie vyhľadávane v utriedenom poli je založené na nasledovnej myšlienke:

  • Ak sa pozrieme v utriedenom poli na nejaký prvok a[i], tak v intervale a[0],...,a[i-1] sú čísla veľkosti nanajvýš a[i] a v intervale a[i+1]...a[n-1] sú čísla veľkosti aspoň a[i].
  • Keď teda hľadáme prvok x, tak po jednom nahliadnutí do poľa vieme
    • buď priamo povedať, že ten prvok sa tam nachádza - ak sme trafili pozíciu i takú, že x==a[i]
    • vyhodiť (t.j. ďalej neuvažovať) časť poľa v intervale (i+1)..(n-1) - ak sme trafili pozíciu i takú, že x < a[i]
    • vyhodiť (t.j. ďalej neuvažovať) časť poľa v intervale 0..(i-1) - ak sme trafili pozíciu i takú, že x > a[i]

V utriedenom poli teda môžeme vyhľadávať nasledovne:

  • pamätáme si ľavý a pravý okraj intervalu, kde ešte môže byť hľadaný prvok
  • vyberieme nejaký prvok a[index] z tohoto intervalu a patrične interval skrátime
  • ak už je interval zlý (t.j. pravý a ľavý kraj sú naopak), tak skončíme
int find(int a[], int n, int x) {
    int left = 0, right = n - 1;
    while (left <= right) {
        int index = (left + right) / 2;
        if (a[index] == x) {
            return index;
        }
        else if (a[index] < x) {
            left = index + 1;
        }
        else {
            right = index - 1;
        }
    }
    return -1;
}

Ako zvoliť hodnotu index?

  • Ak by sme zvolili za index hodnotu left, dostali by sme niečo veľmi podobné na lineárne vyhľadávanie
  • V binárnom vyhľadávaní používame ako index stredný prvok intervalu, teda index=(left+right)/2
  • Tým dosiahneme, že v každom kroku zahodíme polovicu poľa.
int a[7]={2,5,21,32,38,45,50}
x=21
   left=0 right=6: index=3; A[index]>x (32>21)
   left=0 right=2; index=1; A[index]<x (5<21)
   left=2 right=2; index=2; A[index]=x -> return 2
x=11
   left=0 right=6: index=3; A[index]>x (32>11)
   left=1 right=2; index=1; A[index]<x (5<11)
   left=2 right=2; index=2; A[index]>x (21>11)
   left=2 right=1; left>right           -> koniec while cyklu -> return -1

Intuitívne máme pocit, že binárne vyhľadávanie je lepšie, ako lineárne.

Prednáška 7

Oznamy

  • Niektorým sa nedaria cvičenia
    • Začiatok semestra je pre začiatočníkov ťažký, ale naučíte sa to len riešením čo najväčšieho počtu príkladov
    • Bez dobrej znalosti cyklov, podmienok, premenných, polí a funkcií nebudete rozumieť ďalšiemu učivu a nespravíte skúšku
    • Snažte sa každý týždeň vyriešiť čo najviac príkladov
      • Na dobehnutie učiva môžete riešiť aj príklady po termíne, hoci za ne nie sú body
    • Skúste sa na cvičeniach pracovať vo dvojici, najlepšie s niekým na podobnej úrovni, striedajte sa pri klávesnici
    • Pred cvičením si pozrite poznámky z prednášok
    • Dobre si prečítajte zadanie, vrátane príkladu vstupu a výstupu
    • Najskôr si vymyslite postup, ako idete úlohu riešiť. Vytiahnite si papier na poznámky a náčrtky.
  • Domáca úloha do budúceho piatku
    • Preštudujte si zadanie, pýtajte sa otázky
    • Časť bodov môžete dostať aj za neúplný program, začnite od jednoduchších častí, napr. načítanie vstupu a uloženie do poľa, vykreslenie mnohouholníka a bodov jednotnou farbou, vykreslenie zvislej čiary z prvého bodu
    • Potom môžete prejsť na ťažšiu časť - implementáciu detekcie priesečníkov
    • Úloha je dobrá príležitosť precvičiť si doteraz preberanú látku
  • Dnes písomka o 18:10 v posluchárni B

Zmena plánu prednášok

  • Dnes znaky, v pondelok reťazce (ďalšie precvičenie polí a funkcií, trochu nových pojmov z C)
  • Budúcu stredu začneme rekurziu

Budúci týždeň cvičenia v špeciálnom režime

  • V utorok rozcvička a zopár príkladov na znaky a reťazce
  • V stredu na doplnkových cvičeniach pribudne rozcvička a ďalšie príklady na rekurziu
    • Odporúčame budúci týždeň na doplnkové cvičenia prísť aj stredne pokročilým programátorom, ak ste ešte nerobili s rekurziou (ak sa nezmestíme do F1 248, použijeme aj F2-T3)

Zložitosť algoritmu

Ako sme videli napríklad pri triedení a vyhľadávaní, jednu úlohu môžeme často riešiť viacerými spôsobmi. Intuitívne máme o niektorých pocit, že sú lepšie ako iné. Pozrime sa, prečo sú niektoré riešenia lepšie a ako môžeme niečo také odhadovať.

Časová zložitosť lineárneho vyhľadávania

Často nás zaujíma, ako rýchlo nám program bude bežať. Väčšinou táto rýchlosť nejako závisí od vstupných dát. Iste bude kratšie trvať triedenie trojprvkového poľa ako poľa s milión prvkami. Časovú zložitosť teda budeme odhadovať v závislosti od veľkosti vstupu.

Pre niektoré programy môžeme spočítať počet operácií, ktoré program vykoná. Avšak tento počet môže závisieť od konkrétneho vstupu.

  • Ukážme si túto metódu pre lineárne vyhľadávanie v neutriedenom poli
int find(int a[], int n, int x) {
    for (int i = 0; i < n; i++) {
        if (a[i] == x) return i;
    }
    return -1;
}
  • Najmenej operácií spravíme, ak x je v a[0]
  • Vtedy spravíme 4 kroky: i=0, i<n, a[i]==x, return i (závisí aj od toho, čo presne považujeme za "jeden krok")
  • Najviac operácií spravíme, ak sa x v poli nenachádza
  • Vtedy spravíme raz i=0, (n+1) krát i<n, n krát i++, n krát a[i]==x, raz return -1, spolu 3n+3 krokov
  • Celkový počet krokov bude teda teda niečo medzi 4 a 3n+3

Vo väčších programoch však toto rozmedzie nie je jednoduché vypočítať.

  • Preto uvažujeme väčšinou iba najhorší prípad, ktorý môže nastať
  • Okrem toho nás nezaujímajú presné čísla, ale iba akýsi odhad (približná funkcia) závislá od vstupu.
  • Pri lineárnom vyhľadávaní je v najhoršom prípade počet krokov lineárna funkcia od n, vravíme teda, že tento algoritmus má lineárnu zložitosť, značíme O(n)
  • Presnú definíciu O uvidíte budúci rok na predmete Algoritmy a dátové štruktúry

Časová zložitosť binárneho vyhľadávania

  • Najhorší možný scenár pre danú veľkosť vstupu n nastane, keď prvok nájdeme až v poslednom kroku alebo v poli nebude vôbec.
  • V prvom kroku máme celé pole a v ňom sa pozrieme na stredný prvok a podľa jeho hodnoty zoberieme buď ľavú alebo pravú (zhruba) polovicu poľa.
  • Tým pádom v druhom kroku máme pole polovičnej veľkosti a robíme na ňom zase to isté.
  • V každom kroku teda máme pole o polovicu menšie, až kým nemáme pole veľkosti 1. Potom už prvok nájdeme alebo v ďalšom kroku povieme, že tam nie je.
  • Akú zložitosť bude mať tento algoritmus?
    • Zapíšeme si číslo n (počet prvkov) v dvojkovej sústave.
    • Pri delení poľa na polovicu bude ďalšia veľkosť poľa toto číslo bez poslednej cifry.
    • Počet krokov, ktoré potrebujeme, je teda zhruba počet cifier n v dvojkovej sústave, čo je Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle \log_2 n} .
  • Vravíme teda, že zložitosť binárneho vyhľadávania je logaritmická, značíme O(log n)
  • Logaritmus rastie pre veľké n oveľa pomalšie ako lineárna funkcia, čiže binárne vyhľadávanie považujeme za efektívnejší algoritmus ako lineárne vyhľadávanie
    • pozor, neplatí to však pre každý vstup, iba pre porovnanie najhorších prípadov pre dosť veľké n

Pre ilustráciu som na mojom počítači som namerala, koľko sekúnd v priemere trvá lineárne a binárne vyhľadávanie v poli s n číslami:

n       lineárne binárne
10      3.1e-8   3.7e-8
100     2.0e-7   6.9e-8
1000    1.8e-6   1.0e-7
10000   1.8e-5   1.4e-7
100000  1.8e-4   1.8e-7
1000000 1.8e-3   2.4e-7

Pripomínam, že napr. 1.8e-3 je Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle 1.8\cdot 10^{-3}} , t.j. 0.0018. Pri hľadaní v poli dĺžky 10 je teda lineárne vyhľadávanie rýchlejšie, ale v poli dĺžky milión je už vyše 7000 krát pomalšie...

Časová zložitosť triedenia vkladaním

Triedenie vkladaním (Insertion sort) z minulej prednášky

  • Pripomíname ideu: prvých i prvkov máme utriedených, prvok a[i] sa snažíme vložiť na správne miesto
  • Na to musíme posunúť všetky väčšie prvky o jedna doprava, aby sme mu spravili miesto
void sort(int a[], int n) {
    /* usporiadaj prvky v poli a od najmenšieho po najväčší */

    for (int i = 1; i < n; i++) {
        int prvok = a[i];
        int kam = i;
        while (kam > 0 && a[kam - 1] > prvok) {
            a[kam] = a[kam - 1];
            kam--;
        }
        a[kam] = prvok;
    }
}
  • V najhoršom prípade pre dané i bude a[i] menšie ako všetky doteraz utriedené prvky a teda while cyklus bude bežať i krát
  • Ak je pole na začiatku usporiadané naopak, t.j. od najväčšieho prvku po najmenší, tento najhorší prípad nastane pri každej hodnote i
  • Teraz si už iba spočítame: pre i=1 posúvame 1 prvok, pre i=2 dva prvky, ..., pre i=n-1 posúvame n-1 prvkov
  • Teda čas, ktorý na to potrebujeme je Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle 1+2+...+(n-1)=\frac{n(n-1)}{2}=\frac{n^2}{2}-\frac{n}{2}} .
  • Zložitosť tohto triedenia bude teda kvadratická, čiže Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle O(n^2)}
  • Bude sa však správať rovnako (kvadraticky) na všetkých vstupoch? Čo ak dostaneme na vstupe pole už správne utriedené?

Ostatné triedenia z prednášky (výberom a bublinkové) majú tiež v najhoršom prípade kvadratickú zložitosť

  • Premyslite si prečo
  • Ako dlho im to potrvá v najlepšom prípade?

Existujú však aj triedenia s časovou zložitosťou O(n log n), ako uvidíme neskôr v semestri

Cvičenie:

  • Na vstupe máme n čísel usporiadaných od najmenšieho po najväčšie a číslo x, chceme zistiť, či sa x nachádza medzi n číslami
  • Načítame čísla do poľa a spustíme lineárne alebo binárne vyhľadávanie
  • Aká bude časová zložitosť týchto dvoch verzií programu?
  • Čo ak nechceme vyhľadávať jednu hodnotu x, ale m rôznych hodnôt?
  • Čo ak nie sú čísla na vstupe utriedené a pred binárnym vyhľadávaním musíme najskôr triediť?

Znaky

Doteraz sme pracovali iba s číselnými dátami, ale pri programovaní často pracujeme z reťazcami (textami).

  • Reťazce budú na ďalšej prednáške, dnes si ukážeme, ako pracovať s ich jednotlivými súčasťami, znakmi (písmená, čísla, medzery,...)
  • Znakové konštanty sa zapisujú v apostrofoch, napr. 'A', '1', ' ' a pod.
  • Znakové premenné sú typu char, z anglického character. Ich veľkosť je spravidla 1 bajt, t.j. 8 bitov.

Znaky majú svoje kódy uvedené v tabuľke ASCII. Najbežnejšie sa budeme stretávať s týmito:

  • 48...57: '0'...'9'
  • 65...90: 'A'...'Z'
  • 97...122: 'a'...'z'
  • 32: medzera ' '
  • 9: tabulátor '\t'
  • 10: koniec riadku '\n'
  • 0: špeciálny nulový znak (uvidíme nabudúce) '\0'

Poznámky

  • bežné znaky z US klávesnice sú v rozsahu 0..127 (7 bitov)
  • nakoľko char je 8-bitový, môže ešte nadobúdať hodnoty -1 ... -128 alebo 128..255 podľa kompilátora
  • moderný softvér väčšinou namiesto klasických 8-bitových znakov používa Unicode, aby sa dali reprezentovať aj rôzne špeciálne symboly, znaky s diakritikou, jazyky nepoužívajúce latinku a pod.
  • na tomto predmete si vystačíme s klasickými znakmi v rozsahu 0..127
  • znaky sa dajú zapísať aj pomocou ich kódu v osmičkovej alebo šestnástkovej sústave: '\101' a '\x41' reprezentujú znak s kódom 65, t.j. 'A'.

Do premennej typu char môžeme priraďovať, jej obsah zapísať alebo prečítať:

  char c='A';
  char z;
  z=c;
  cout << c;
  cin >> z;  // prečíta jeden znak (pozor, preskakujú sa biele znaky)

Znaky môžeme porovnávať. Na konci programu vyššie platí nasledovné:

  • c=='A' ... je pravda,
  • c=='a' ... nie je pravda – rozlišujú sa malé a veľké písmená,
  • c<='Z' ... je pravda – písmená sú usporiadané: A<B< ... <Z, a<b< ... <z, aj cifry sú usporiadané: 0<1< ... <9.

Pri čítaní zo vstupu pomocou cin do premennej typu char sa preskakujú tzv. biele znaky (napr. medzera, tabulátor, koniec riadku).

  • Toto nie je vždy žiadúce a preto môžeme použiť modifikátor noskipws, ktorý zruší preskakovanie takýchto znakov. Do premennej teda budeme vedieť prečítať aj medzeru.
#include <iostream>
using namespace std;

int main(void) {
  char a,b,c; 
  cin >> noskipws >> a >> b >> c;
  cout << a << b << c;
}

Hodnota jednoduchého výrazu

Nasledujúci program spočíta hodnotu jednoduchého výrazu, ktorý pozostáva z dvoch čísel spojených znamienkom +, -, * alebo /. Okolo sú medzery (kvôli jednoduchšiemu načítaniu).

Napr. pre vstup 1 / 2 program vypíše 0.5.

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

int main(void) {
    double a, b;
    char znamienko;
    cin >> a >> znamienko >> b;
    double vysledok;
    if (znamienko == '+') {
        vysledok = a + b;
    }
    else if (znamienko == '-') {
        vysledok = a - b;
    }
    else if (znamienko == '*') {
        vysledok = a * b;
    }
    else if (znamienko == '/') {
        vysledok = a / b;
    } else {
        cout << "zle znamienko " << znamienko << endl;
        exit(1);
    }
    cout << vysledok << endl;
}

Switch

  • V predchádzajúcom programe bola pomerne dlhá a komplikovaná séria príkazov if, else
  • Namiesto toho sa dá použiť príkaz switch, ktorý podľa hodnoty výrazu pokračuje jednou z viacerých vetiev.

V našom jednoduchom príklade by mohol switch vyzerať nasledovne:

    switch (znamienko) {
    case '+' :
        vysledok = a + b;
        break;
    case '-' :
        vysledok = a - b;
        break;
    case '*' :
        vysledok = a * b;
        break;
    case '/' :
        vysledok = a / b;
        break;
    default:
        cout << "zle znamienko " << znamienko << endl;
        exit(1);
    }

Vo všeobecnosti obsahuje príkaz switch viacero rôznych prípadov vyhodnotenia výrazu v podmienke.

switch (výraz)
{
case k1: príkazy1
case k2: príkazy2
default: príkazyd
}

Takýto príkaz funguje nasledovne:

  • Vyhodnotí výraz.
    • Ak sa hodnota zhoduje s konštantným výrazom ki v niektorom z prípadov, pokračuje časťou príkazyi
    • Ak sa nezhoduje a máme vetvu default, pokračuje sa časťou prikazyd
    • Ak nie je vetva default, pokračuje sa za koncom switch bloku.
  • Pozor: Na rozdiel od pascalovského case, vykonávanie nekončí vykonaním posledného príkazu v prikazyi, ale pokračuje ďalej, kým nie je prerušené príkazom break.
    • Toto je častá chyba pri použití príkazu switch
#include <iostream.h>

void main () {
  int n;
  cout << "Zadaj n (1,2,3,4): ";
  cin >> n;
  switch (n) {
    case 1: cout << "Jeden" << endl;
    case 2: cout << "Dva" << endl;
    case 3:
    case 4: cout << "Tri alebo styri" << endl;
    default: cout << "Chyba!" << endl;
  }
  cout << "Koniec." << endl;
}

Pre n=2 sa začnú vykonávať príkazy uvedené za vetvou case 2:. Vypíše sa:

    Dva
    Tri alebo styri
    Chyba!
    Koniec.

Výhodou je, že môžeme zlúčiť viacero prípadov do jednej vetvy tým, ze príkazy napíšeme až za posledný prípad (tu vidíme napr. v situácii n=3 a n=4).

Dôležité upozornenie: break switch while

Predstavme si, že v programe sa pýtame užívateľa, či chce pokračovať s ďalším vstupom, pričom odpoveď má byť znak 'A' alebo 'N'. Ak by sme program napísali takto, nefungoval by:

    while (true) {
        // nejaky vypocet
        cout << "Chcete pokracovat? (Zadajte odpoved A alebo N)" << endl;
        char odpoved;
        cin >> odpoved;
        switch (odpoved) {
        case 'N':
            break;
        // spracovanie inych pripadov...
        }
    }
  • Príkaz break nevyskočí zo všetkých cyklov, ale iba z najvnútornejšieho - a tým je v tomto prípade switch. Program teda pokračuje aj keď užívateľ zadá 'N'.
  • Ak by break bol použitý s podmienkou, všetko by fungovalo: while (true) { ... if (odpoved=='N') { break; } }

Ešte znaky

Pretypovanie

Znakové premenné teda ukladajú kódy jednotlivých znakov, čo sú celé čísla. Preto medzi znakmi a celými číslami môžeme prechádzať úplne jednoducho.

#include<iostream>
using namespace std;

int main(void) {
  int N;
  char c;

  cout << "Napiste cislo: ";
  cin >> N;                     // prečíta číslo 
  c = N;                        // do znakovej premennej môžeme číslo priradiť bez problémov
  cout << c << endl;            // vieme vypísať znak
  cout << (c+1) << endl;        // ale keď už použijeme aritm. operáciu, je to ako číslo

  cout << "Napiste znak: ";     
  cin >> c;                     // prečíta znak
  N = c;                        // do celočíselnej premennej ho priamo vieme priradiť
  cout << N << endl;            // vypíšeme ho ako číslo
}

Okrem toho však vieme urobiť aj pretypovanie, keď chceme aby výsledok bol konkrétneho typu. Napríklad, aby nám v prvej časti vypísalo nie ďalší kód ale ďalší znak, mohli sme výsledok pretypovať.

  cout << (char)(c+1) << endl;          // vďaka pretypovaniu dostávame na výpise znak

Pretypovanie sme už používali, keď sme chceli dva inty vydeliť bez zaokrúhlenia na celé číslo:

  int a = 5;
  int b = 2;
  cout << a/b << " " << (double)(a/b) << " " << ((double)a)/b << endl;

Tento program vypíše nasledovný výstup:

2 2 2.5

Prednáška 8

  • V pondelok po prázdninách 5.11. o 14:00 bude na fakulte Linux Installfest (v IT kobke pri vchode do pavilónu M za šatňou)
    • Dobrovoľníci budú záujemcom pomáhať nainštalovať si Linux
    • Môže sa vám zísť na programovaní aj na ďalších predmetoch
  • Body z písomky na testovači
    • Opravná písomka v piatok 9.11. o 12:20
    • Ak chcete opravnú písomku písať, ale máte konflikt s navrhnutým časom, dajte mi vedieť do zajtra
    • Na opravnú písomku sa treba prihlásiť cez AIS alebo VOTR (termín sa objaví v stredu)
    • Ak prídete na opravnú, strácate body z prvej písomky, aj keď dopadnete horšie
    • Celkovo potrebujete z troch písomiek aspoň polovicu bodov, prvá bola najľahšia
    • Ak ste teda z prvej nezískali polovicu bodov, odporúčame vám ísť na opravnú
  • Domáca úloha do piatku
    • Časť bodov môžete dostať aj za neúplný program
    • Ďalšia DÚ zverejnená koncom týždňa
  • Prednášky a cvičenia tento týždeň
    • Dnešná prednáška reťazce
    • Zajtra na cvičeniach rozcvička na reťazce plus ďalšie príklady na reťazce, znaky, polia
    • Prednáška v stredu rekurzia
    • V stredu na doplnkových cvičeniach pribudne rozcvička a ďalšie príklady na rekurziu
      • Odporúčame tento týždeň na doplnkové cvičenia prísť aj stredne pokročilým programátorom, ak ste ešte nerobili s rekurziou (ak sa nezmestíme do F1 248, použijeme aj F2-T3=F2-128)
    • V rekurzii pokračujeme aj po prázdninách


Z minulej prednášky

Znaky

  • Znakové premenné sú typu char, z anglického character.
  • Znaky majú svoje kódy uvedené v tabuľke ASCII.
  • Do premennej typu char môžeme priraďovať, jej obsah zapísať alebo prečítať. Znaky môžeme porovnávať (navzájom alebo proti konštantám napr. 'Z').
  • Znakové premenné ukladajú kódy jednotlivých znakov, čo sú celé čísla. Preto medzi znakmi a celými číslami môžeme prechádzať úplne jednoducho.
    • Keď chceme aby výsledok bol konkrétneho typu, použijeme pretypovanie.

Switch

  • Namiesto niekoľkých vnorených if s tou istou premennou v podmienke môžeme použiť switch.
  • Vo všeobecnosti obsahuje príkaz switch viacero rôznych prípadov vyhodnotenia výrazu v podmienke.
switch (výraz)
{
case k1: príkazy1
case k2: príkazy2
default: príkazyd
}
  • Pozor: Na rozdiel od pascalovského case, vykonávanie nekončí vykonaním posledného príkazu v prikazyi, ale pokračuje ďalej, kým nie je prerušené príkazom break.
    • Výhodou je, že môžeme zlúčiť viacero prípadov do jednej vetvy.

Využitie znakov

Vďaka znakom môžeme spraviť kontrolu toho, čo vlastne používateľ napísal na vstup. Napríklad, či zadal správne celé číslo a nenamiešal medzi cifry nejaký iný znak.

#include <iostream>
using namespace std;

int main(void) {
    int N = 0;
    char c;

    cout << "Zadajte cele kladne cislo: ";

    cin >> noskipws >> c;
    while ((c >= '0') && (c <= '9')) { // kym je nacitany znak cislo (t.j. jedna cifra)
        int cifra = c - '0'; // prevod z kódu znaku na cifru 0..9 ('0' ma kod 48)
        N = N * 10 + cifra; // upravime cislo N
        cin >> noskipws >> c; // a nacitame dalsi znak
    }

    if ((c == ' ') || (c == '\n')) { // ak sme skoncili medzerou alebo koncom riadku, tak je to pekne cislo
        cout << "Zadali ste " << N << endl;
    } else {
        cout << "Toto je cele cislo?" << endl; // ak sme skoncili niecim divnym, tak to asi nebude ok
    }
}

Reťazec ako postupnosť znakov

  • Reťazec je nejaký text, postupnosť znakov
  • Vedeli by sme si naprogramovať vlastné funkcie na prácu s reťazcami, ukladať ich ako pole znakov
  • Napríklad funkcia nacitajRiadok, ktorá znak po znaku načíta text z konzoly, znaky uloží do poľa a, dĺžku textu do premennej n.
  • Naopak funkcia vypisRetazec dostane pole znakov a jeho dĺžku a vypisuje znak po znaku.
#include <iostream>
using namespace std;

void nacitajRiadok(char a[], int &n, int maxN) {
    /* Z konzoly nacita riadok a ulozi ho do pola a,
     * jeho dlzku do premennej n. Premenna maxN 
     * obsahuje dlzku pola, ktoru nesmieme prekrocit. 
     * Ak je riadok dlhsi ako maxN, nacita sa z neho iba cast. */
    n = 0;
    while(n < maxN) {
        cin >> noskipws >> a[n];
        n++;
        if(a[n-1]=='\n') { break; }
    }
}

void vypisRetazec(char a[], int n) {
    /* Na konzolu vypise prvych n znakov z pola a */
    for(int i=0; i<n; i++) {
        cout << a[i];
    }
}

int main(void) {
    const int maxN = 100;
    char a[maxN];
    int n;
       
    cout << "Zadajte text: ";
    nacitajRiadok(a, n, maxN);
    cout << "Zadali ste: ";
    vypisRetazec(a, n);
}
  • Nevýhoda tohto riešenia: reťazec musíme posielať všade ako dve premenné (pole a dĺžku)
  • Preto v Cčku reťazce štandardne fungujú trochu inak.

Reťazce v jazyku C

Textový reťazec je v jazyku C štandardne uložený v poli ako postupnosť znakov (char) ukončená znakom s kódom 0.

  • Nemusíme teda ukladať zvlášť dĺžku, reťazec ide po prvú nulu
  • Pozor, rozdiel medzi znakom s kódom 0 (píše sa aj '\0') a znakom pre cifru nula '0' s kódom 48
  • Reťazce teda nemôžu obsahovať vo vnútri znak s kódom 0, ten je rezervovaný na ukončovanie
  • Na reťazec s n znakmi potrebujeme pole dĺžky aspoň n+1, lebo jeden znak sa minie na ukončovací symbol

Teraz by sme vedeli prepísať nacitajRiadok a vypisRetazec, ale netreba, lebo existujú v knižniciach

Inicializácia reťazcov

  • Chceme vytvoriť premennú str obsahujúce reťazec Ahoj spolu s koncom riadku
  • Prvý spôsob je zdĺhavý:
    char str[10];
    str[0] = 'A';
    str[1] = 'h';
    str[2] = 'o';
    str[3] = 'j';
    str[4] = '\n'; // znak pre koniec riadku
    str[5] = 0;
  • Alebo ako inicializácia poľa: char str[10]={'A','h','o','j','\n',0};
  • Namiesto toho sa používa špeciálna skratka: char str[10]="Ahoj\n";
  • Ako vytvoríme prázdny reťazec?

Reťazec je naozaj pole

Znaky reťazca môžeme meniť

char a[100] = "vlk";
char ch = a[0]; // ch obsahuje hodnotu 'v'
char b[100] = "pes";

b[0] = ch;     // priradíme do jedného prvku reťazca premmennú typu char. Výsledkom je 'ves'.
b[0] = 'd';    // priradíme do jedného prvku reťazca konštantný znak. Výsledkom je 'des'. 
b[0] = a[1];   // priradíme do jedného prvku reťazca prvok iného reťazca. Výsledkom je 'les'.

Reťazec sa nedá kopírovať jednoduchým priradením, nemôžeme teda spraviť

char a[100];
a = "Ahoj";           // chyba
char b[100] = "Ahoj"; // ok - inicializacia
a = b;                // chyba

Reťazce sa nedajú ani porovnávať pomocou ==, !=, < atď

Kopírovanie a porovnávanie si musíme naprogramovať cez cykly, alebo použiť hotové funkcie z knižníc.

Knižnica cstring

Obsahuje mnohé funkcie na prácu s reťazcami, napríklad tieto:

  • strlen(retazec): vráti dĺžku reťazca
  • strcpy(kam, co): skopíruje reťazec co do reťazca kam (pole kam musí byť dosť dlhé)
  • strcat(kam, co): za koniec reťazca kam pridá reťazec co (pole kam musí byť dosť dlhé)
  • strcmp(retazec1, retazec2): vráti nulu ak sa reťazce rovnajú, kladné číslo keď je prvý neskôr v abecednom poradí, záporné číslo, ak je skôr. Pozor, to či je skôr alebo neskôr sa berie podľa kódov znakov, takže napr. 'Z' je skôr ako 'a'.

Všetky tieto funkcie by sme si však vedeli naprogramovať aj sami. Tu je napríklad výpočet dĺžky:

int myStrLen(char a[]) {
    int n=0; 
    while(a[n] != 0) {  n++; }
    return n;
}
  • čo bude funkcia robiť ak reťazcu chýba na konci 0?

Dve verzie kopírovania:

void myStrCpy(char a[], char b[]) {
    /* Skopiruj obsah retazca b do retazca a.
     * Pole a musi mat dost miesta. */
    int n = 0;
    while (b[n] != 0) {
        a[n] = b[n];
        n++;
    }
    a[n] = 0; // reťazec musí končiť 0
}

void myStrCpy2(char a[], char b[]) {
    /* Skopiruj obsah retazca b do retazca a.
     * Pole a musi mat dost miesta. */
    for (int i = 0; i <= strlen(b); i++) {
        a[i] = b[i];
    }
}
  • Ktorá je rýchlejšia pre dlhé reťazce?
  • Aká je ich zložitosť ako funkcia dĺžky reťazca n?

Namiesto strcmp naprogramujeme len test na rovnosť:

bool rovnostRetazcov(char a[], char b[]) {
    /* vrati true ak su retazce a, b rovnake, inak vrati false */

    for (int i = 0; a[i] != 0 || b[i] != 0; i++) {
        if (a[i] != b[i]) return false;
    }
    return true;
}
  • Ako bude prebiehať funkcia, ak jeden reťazec je začiatkom druhého?

Načítavanie a vypisovanie reťazcov

  • Bežné načítanie z konzoly do reťazca (cin >> str) načíta jedno slovo
    • Preskočí biele znaky (medzery, konce riadkov, tabulátory), potom prečíta všetko po ďalší biely znak (alebo koniec vstupu) a uloží do premennej.
    • Pri čítaní je vhodné nastaviť maximálny počet znakov na načítanie, aby sme nevyšli z poľa
  • Na načítanie jedného riadku je možné použiť funkciu getline. Načíta až po koniec riadku, ten zahodí.
  • Vypisovanie funguje normálne pomocou cout << str
#include <iostream>
using namespace std;

int main(void) {
    const int maxN = 100;
    char str[maxN], str2[maxN], str3[maxN];

    cin.getline(str, maxN); // cely riadok, ale najviac maxN-1 znakov

    cin.width(maxN); // najviac maxN-1 znakov pri najbližšom načítaní
    cin >> str2;     // nacita jedno slovo
    cin.width(maxN); // najviac maxN-1 znakov pri najbližšom načítaní
    cin >> str3;     // nacita dalsie slovo

    cout << "str: \"" << str << "\"" << endl;
    cout << "str2: \"" << str2 << "\"" << endl;
    cout << "str3: \"" << str3 << "\"" << endl;
}

Príklad behu programu (prvé dva riadky zadal užívateľ, na začiatku a konci každého je medzera)

 a b c 
 g h i 
str: " a b c "
str2: "g"
str3: "h"

Algoritmy s reťazcami

Prácu s reťazcami si precvičíme na niekoľkých menších príkladoch.

Vyhľadávanie podreťazca

Chceme zistiť, či a kde sa v reťazci nachádza určité slovo alebo iná vzorka.

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

int find(char text[], char pattern[]) {
    /* Vráti -1 ak sa reťazec pattern nevyskytuje v reťazci text,
     * inak vráti polohu jeho prvého výskytu. */

    int n = strlen(text);
    int m = strlen(pattern);
    for (int i = 0; i < n - m + 1; i++) {
        int j = 0;
        while (j < m && text[i + j] == pattern[j]) {
            j++;
        }
        if (j == m) {
            return i;
        }
    }
    return -1;
}

int main(void) {
    const int maxN = 2000;
    char A[maxN], B[maxN];

    cout << "Zadaj text: ";
    cin.getline(A,maxN);
    cout << "Zadaj vzorku: ";
    cin.getline(B,maxN);
    cout << find(A,B) << endl;
}
  • Predpočítame si dĺžky a uložíme do premenných, aby sa zbytočne nerátali znova a znova
  • Vedeli by sme do poľa uložiť polohy všetkých výskytov?

Prevod čísla na reťazec

Máme danú premennú x typu int, chceme ju uložiť v desiatkovej sústave do reťazca.

  • Zvyšok po delení 10 je posledná cifra, uložíme si ju do reťazca, vydelíme x desiatimi
  • Opakujeme, kým nespracujeme celé číslo.
  • Prevod z čísla c (0..9) na cifru: '0'+c
  • Nezabudneme na ukončovací znak 0
  • Dostaneme číslo v opačnom poradí, napr pre x=12 budeme mať reťazec {'2', '1', 0}
  • Preto ešte celé číslo otočíme naopak.
void reverse(char a[]) {
    int n = strlen(a);
    int i = 0;
    int j = n - 1;
    while (i < j) {
        char tmp = a[i];
        a[i] = a[j];
        a[j] = tmp;
        i++; j--;
    }
}

void int2str(int x, char a[]) {
    /* prevedie kladne cele cislo x na retazec,
     * vysledok ulozi do retazca a, ktory musi mat dost miesta. */
    assert(x > 0);

    int n = 0;
    while(x > 0) {
        a[n] = '0' + x % 10;
        x /= 10;
        n++;
    }
    a[n] = 0;

    /* teraz je cislo naopak, treba otocit */
    reverse(a);
}
  • Ako upravíme funkciu, aby fungovala aj pre x=0, prípadne záporné x?
  • Pozor na rozdiel medzi znakom 0 a '0' (a medzi reťazcom "0")

Formátovanie čísla

  • Chceme číslo zapísať do reťazca a doplniť naľavo medzerami na šírku width.
const int maxN = 100;

void formatInt(int x, char A[], int width) {
    /* číslo x konvertujeme na reťazec
     * a uložíme do poľa A zarované doprava na šírku width */

    /* najprv x uložíme do pomocného reťazca B  a zrátame jeho dĺžku n */
    char B[maxN];
    int2str(x, B);
    int n = strlen(B);

    /* do A dáme width-n medzier a ukončovaciu 0 */
    assert(n <= width);
    int i;
    for (i = 0; i < width - n; i++) {
        A[i] = ' ';
    }
    A[i] = 0;

    /* za A prikopírujeme B */
    strcat(A, B);
}
  • Čo by sa stalo, ak by sme nedali do A ukončovaciu 0?
  • Vedeli by ste prepísať program, aby pracoval priamo v poli A (bez poľa B)?


Využijeme na vypísanie pekne zarovnanej tabuľky faktoriálov:

int factorial(int n) {
    int result = 1;
    for (int i = 1; i <= n; i++) {
        result *= i;
    }
    return result;
}

int main(void) {
    char A[maxN];
    int n = 12;
    for (int i = 1; i <= n; i++) {
        int x = factorial(i);
        formatInt(i, A, 2);
        cout << A << "! = ";
        formatInt(x, A, 10);
        cout << A << endl;
    }
}
 1! =          1
 2! =          2
 3! =          6
 4! =         24
 5! =        120
 6! =        720
 7! =       5040
 8! =      40320
 9! =     362880
10! =    3628800
11! =   39916800
12! =  479001600

Dalo by sa aj jednoduchšie pomocou nastavenia width v cin:

int main(void) {
    int n = 12;
    for (int i = 1; i <= n; i++) {
        int x = factorial(i);
        cout.width(2);
        cout << i << "! = ";
        cout.width(10);
        cout << x << endl;
    }
}

Zalamovanie riadkov

Pre zaujímavosť: ukážka trochu dlhšieho programu na prácu s textom

  • Máme reťazec s nejakým textom, v ktorom sa vyskytujú rôzne biele znaky, napríklad medzery a konce riadkov. Máme danú šírku riadku W, napr. 80 znakov. Úlohou je ho upraviť tak:
    • aby na každom riadku bolo najviac W znakov, pričom nový riadok začína tam, kde by už ďalšie slovo presahovalo cez W
    • medzi dvoma slovami má byť vždy buď jedna medzera alebo jeden koniec riadku
    • predpokladáme, že žiadne slovo nemá viac ako W znakov
Zadavaj text ukonceny prazdnym riadkom.
A AA A AAA 
A  A AA AAA
AAA A AA AA  A

Zadaj sirku riadku:
5
Sformatovany odstavec:
A AA
A AAA
A A
AA
AAA
AAA A
AA AA
A
Zadavaj text ukonceny prazdnym riadkom.
Martin Kukucin: Do skoly.    Vakacie sa koncia. Ondrej Rybar sa vse zamysli nad marnostou sveta i vsetkeho, co je v nom. 
Predstupuje mu tu i tu pred oci profesor, ako stoji pred ciernou tabulou, drziac kruzidlo v ruke a demonstruje pamatnu poucku Pytagorovu. 
A zimomriavky naskakuju na chrbat, lebo s geometriou stoji od pociatku na nohe valecnej. 
Ani matematika nenie lepsia, menovite odvtedy, co sa do nej vplichtili miesto cisel vsakove litery. 
Neraz hutal, naco ich ucenci vpustili do matematiky - ved i bez nich je dost strapata: ci sa im malilo cisel a tak preto vsantrocili medzi ne a a b a ci fantazia sa im tak rozihrala, 
ze prekrocila hranice cisel celych, zlomkov obycajnych i desatinnych i bohvieakych, a zabludila na nivy, kde rastu nestastne litery? 
 ,Uz akokolvek,' huta Ondro, ,litery tam nemaju co hladat. Tazko je uverit, ze a/b = c, lebo nevies, co je a, alebo b.' 

Zadaj sirku riadku:
50
Sformatovany odstavec:
Martin Kukucin: Do skoly. Vakacie sa koncia.
Ondrej Rybar sa vse zamysli nad marnostou sveta i
vsetkeho, co je v nom. Predstupuje mu tu i tu pred
oci profesor, ako stoji pred ciernou tabulou,
drziac kruzidlo v ruke a demonstruje pamatnu
poucku Pytagorovu. A zimomriavky naskakuju na
chrbat, lebo s geometriou stoji od pociatku na
nohe valecnej. Ani matematika nenie lepsia,
menovite odvtedy, co sa do nej vplichtili miesto
cisel vsakove litery. Neraz hutal, naco ich ucenci
vpustili do matematiky - ved i bez nich je dost
strapata: ci sa im malilo cisel a tak preto
vsantrocili medzi ne a a b a ci fantazia sa im tak
rozihrala, ze prekrocila hranice cisel celych,
zlomkov obycajnych i desatinnych i bohvieakych, a
zabludila na nivy, kde rastu nestastne litery? ,Uz
akokolvek,' huta Ondro, ,litery tam nemaju co
hladat. Tazko je uverit, ze a/b = c, lebo nevies,
co je a, alebo b.'

Plán: úlohu si rozdelíme na viac častí

  • Prerobíme reťazec tak, aby sme všetky biele znaky nahradili medzerami. Na rozpoznanie bielych znakov použijeme funkciu isspace z knižnice cctype.
  • Každý súvislý úsek medzier nahradíme práve jednou medzerou, zmažeme medzery na začiatku a na konci.
    • Viac možností na riešenie, napríklad znaky presýpame do nového poľa. My ale použijeme len jedno pole
  • Niektoré medzery nahradíme koncom riadku, aby každý riadok mal šírku najviac W.
  • Spravíme načítanie a vypísanie.
#include <iostream>
#include <cstring>
#include <cctype>
#include <cassert>
using namespace std;

void simplify(char A[]) {
    /* V retazci A nahradi kazdy suvisly usek bielych znakov prave jednou medzerou.
     * Na zaciatku a konci retazca nebudu medzery. */

    /* prepis hocijake biele znaky na medzeru */
    for (int i = 0; A[i] != 0; i++) {
        if (isspace(A[i])) {
            A[i] = ' ';
        }
    }

    int kam = 0; /* prve este neobsadene miesto */
    char prev = ' '; /* predchadzajuci znak */

    for (int i = 0; A[i] != 0; i++) {
        /* ak nemame viac medzier po sebe, skopirujeme znak */
        if (A[i] != ' ' || prev != ' ') {
            A[kam] = A[i];
            kam++;
        }
        /* zapamatame si posledny znak */
        prev = A[i];
    }

    /* zrusime pripadnu medzeru na konci */
    if (kam > 0 && A[kam - 1] == ' ') {
        kam--;
    }

    /* retazec ukoncime nulou */
    A[kam] = 0;
}

bool breakLines(char A[], int width) {
    /* Preformatuje odstavec na sirku riadku width, vyhodi zbytocne medzery.
     * Dlzka kazdeho slova musi byt najviac width, inak funkcia vrati false */

    simplify(A);
    int n = strlen(A);

    int zac = 0;  /* index prveho pismena v riadku */
    while (zac < n) {
        int kon = zac + width;  /* potencialny koniec riadku */
        /* ak uz nemame dost pismen na cely riadok */
        if (kon > n) {
            kon = n;
        }
        /* ak sme na konci, pridame koniec riadku za koniec retazca */
        if (kon == n) {
            A[kon] = '\n';
            A[kon + 1] = 0;
            n++;
        } else {
            /* ideme späť, kým nenájdeme medzeru */
            while (kon > zac && A[kon] != ' ') {
                kon--;
            }
            /* nenašli sme medzeru: slovo bolo príliš dlhé. */
            if (kon == zac) {
                return false;
            }
            /* medzeru prepíšeme na koniec riadku */
            assert(A[kon]==' ');
            A[kon] = '\n';
        }
        /* za koncom riadku bude novy zaciatok */
        zac = kon + 1;
    }
    return true;
}

int main(void) {
    const int maxN = 2000;
    char A[maxN];
    A[0] = 0;

    cout << "Zadavaj text ukonceny prazdnym riadkom." << endl;
    while (true) {
        /* nacitame jeden riadok */
        char tmp[maxN];
        cin.getline(tmp, maxN);
        /* ak je prazdny, koncime nacitavanie */
        if (strcmp(tmp, "") == 0) {
            break;
        }
        /* ak je miesto v poli A, pridame do neho novy riadok */
        if (strlen(A) + strlen(tmp) + 2 < maxN) {
            strcat(A, tmp);
            strcat(A, "\n");
        } else {
            cout << "Text je prilis dlhy." << endl;
            return 1;
        }
    }

    cout << "Zadaj sirku riadku:" << endl;
    int width;
    cin >> width;

    breakLines(A, width);
    cout << "Sformatovany odstavec:" << endl;
    cout << A;
}
  • Akú zložitosť má načítanie vzhľadom na celkový počet načítaných písmen? Dalo by sa zlepšiť?

Zhrnutie

  • Reťazec je pole znakov, za posledným znakom reťazca dáme špeciálny znak s kódom 0
  • V knižnici cstring sú funkcie na porovnávanie a kopírovanie reťazcov atď. a pomocou cin a cout môžeme reťazce načítavať a vypisovať.
  • Ďalšie funkcie si vieme naprogramovať aj sami, zvyčajne jednoduchá práca s poľom

Prednáška 9

Oznamy

  • Opravná písomka v piatok 9.11. o 12:20
    • Ak ju chcete písať, prihláste sa cez AIS alebo VOTR (termín sa objaví dnes poobede)
  • Domáca úloha do piatku
    • Časť bodov môžete dostať aj za neúplný program
    • Ďalšia DÚ zverejnená koncom týždňa
  • Dnes na doplnkových cvičeniach pribudne rozcvička a ďalší príklad na rekurziu
    • Odporúčame na doplnkové cvičenia prísť aj stredne pokročilým programátorom, ak ste ešte nerobili s rekurziou (ak sa nezmestíme do F1 248, použijeme aj F2-T3=F2-128)
    • Rozcvička sa bude dať rátať aj doma do 22:00

Poznámky k reťazcom

  • Naučte sa používať funkcie zo štandardných knižníc, ako strlen, strcmp, strcpy, strcat, cin.getline, nie ich verzie z prednášky

Klasické úvodné príklady na rekurziu

Rekurzia je metóda, pri ktorej definujeme objekt (funkciu, pojem, . . . ) pomocou jeho samého.

Na začiatok sa pozrieme na „klasické“ príklady algoritmov využívajúcich rekurziu.

Výpočet faktoriálu

Faktoriál prirodzeného čísla n značíme n! a je to súčin všetkých celých čísel od 1 po n. Pre úplnosť 0! definujeme ako 1.

Výpočet pomocou cyklu z prednášky 3:

int factorial(int n) {
    int result = 1;
    for (int i = 1; i <= n; i++) {
        result = result * i;
    }
    return result;
}

Rekurzívna definícia faktoriálu: Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle n! = \left\{\begin{array}{ll} 1 & \mathrm{ak~} n<2 \\ n \cdot (n-1)! & \mathrm{inak} \\ \end{array}\right.}

Túto matematickú definíciu môžeme priamočiaro prepísať do rekurzívnej funkcie:

int factorial(int n) {
    if (n < 2) return 1;
    else return n * factorial(n-1);
}

Aby sa nám rekurzia nezacyklila, mali by sme dodržiavať nasledujúce zásady:

  • Rekurzívna funkcia musí obsahovať vetvu pre triviálny prípad niektorého vstupu. Táto vetva nebude obsahovať rekurzívne volanie funkcie, ktorú práve definujeme.
  • Rekurzívne volanie funkcie by malo mať vhodne redukovaný niektorý vstup, aby sme sa časom dopracovali k triviálnemu prípadu.

Najväčší spoločný deliteľ (Euklidov algoritmus)

Ďalším tradičným príkladom na rekurziu, s ktorým ste sa už stretli, je počítanie najväčšieho spoločného deliteľa.

  • Euklidov algoritmus z prednášky 3 bol založený na rovnosti gcd(a,b) = gcd(b, a mod b)
  • Tú sme použili v cykle:
int gcd(int a, int b) {
    while(b != 0) {
        int x = a % b;
        a = b;
        b = x;
    }
    return a;
}

Avšak opäť to isté môžeme ešte kratšie a elegantnejšie napísať rekurziou:

int gcd(int a, int b) {
   if (b == 0) return a;
   else return gcd(b, a % b);
}

Fibonacciho čísla

Nemôžeme vynechať obľúbený rekurzívny príklad - Fibonacciho čísla, ktoré sme videli na prednáške 4. Aj tam sa rekurzia priam pýta, keďže Fibonacciho čísla sú samé o sebe definované rekurzívne:

  • F(0)=F(1)=1
  • F(n)=F(n-1)+F(n-2) pre n>2

Z tejto definície vieme opäť urobiť rekurzívnu funkciu jednoducho:

int fib(int n){
    if (n<2) return 1;
    else return fib(n-1)+fib(n-2);
}

Toto je opäť krajšie ako nerekurzívna verzia:

int fib(int n) {
    int f = 1;     // Cislo F(i) 
    int oldF = 1;  // Cislo F(i-1)
    
    for (int i = 2; i <= n; i++) {
        int newF = f + oldF;  // spocitaj nove F(i) pre vyssie i
        oldF = f;             // poposuvaj hodnoty
        f = newF;
    }
    return f;
}

Binárne vyhľadávanie

Aj binárne vyhľadávanie prvku v utriedenom poli z prednášky 7 sa do sprehľadní v rekurzívnom zápise.

Pôvodná nerekurzívna funkcia vrátila polohu prvku x v poli a alebo hodnotou -1, ak sa tam nenachádzal:

int find(int a[], int n, int x) {
    int left = 0, right = n - 1;
    while (left <= right) {
        int index = (left + right) / 2;
        if (a[index] == x) return index;
        else if (a[index] < x) left = index + 1;
        else right = index - 1;
    }
    return -1;
}

V rekurzívnej verzii si okraje aktuálneho úseku poľa si v rekurzii posielame ako parametre:

int find(int a[], int left, int right, int x) {
    if (left > right) return -1;
    int index = (left + right) / 2;
    if (a[index] == x) return index;
    else if (a[index] < x) return find(a, index+1, right, x);
    else return find(a, left, index - 1, x);
}

Ak chceme vyhľadať x v poli a s n prvkami, voláme find(a, 0, n-1, x).

Na zamyslenie:

  • Táto funkcia má dva triviálne (nerekurzívne) prípady. Ktoré?
  • Aká veličina klesá v každom rekurzívnom volaní?

Tu je ešte trochu iná verzia binárneho vyhľadávania s niekoľkými rozdielmi:

  • vraciame iba či sa x nachádza v poli alebo nie (dalo by sa rozšíriť aj na index)
  • pri porovnávaní x a a[index] rozlišujeme iba dva prípady, nie tri
  • končíme pri intervale dĺžky 1, nie 0
bool contains (int a[], int left, int right, int x){
    if (left == right) return (a[left] == x);
    int index = (left + right) / 2;
    if (x <= a[index]) return contains(a, left, index, x);
    else return contains(a, index+1, right, x);
}

int main(void) {
  const int n = 9;
  int a[n]={1,5,7,12,45,55,72,95,103};
  cout << contains(a, 0, n-1, 467) << endl;
  cout << find(a, 0, n-1, 467) << endl;
}

Zhrnutie

Pri rekurzii vyjadríme riešenie nejakej úlohy pomocou riešenia jednej alebo viacerých úloh toho istého typu, ale s menším vstupom plus ďalšie potrebné nerekurzívne výpočty

  • výpočet n! vyjadríme pomocou výpočtu (n-1)! a násobenia
  • výpočet gcd(a, b) vyjadríme pomocou výpočtu gcd(b, a % b)
  • výpočet F[n] vyjadríme pomocou výpočtu F[n-1] a F[n-2]
  • binárne vyhľadávanie v dlhšom intervale vyjadríme pomocou binárneho vyhľadávania v kratšom intervale

Všimnite si, že občas musíme zoznam parametrov nejakej funkcie rozšíriť pre potreby rekurzie

  • napr. funkcia find by prirodzene dostávala pole a, dĺžku n a hľadaný prvok, ale kvôli rekurzii potrebuje ľavý a pravý okraj
  • pre pohodlie užívateľa môžeme pridať pomocnú funkciu (wrapper):
int find(int a[], int n, int x) {
  return find(a, 0, n-1, x);
}

Viac o rekurzii

Nepriama rekurzia

Všetky doteraz uvedené funkcie sú príkladom priamej rekurzie – definovaná funkcia používa seba samú priamo. Druhým možným prípadom je nepriama rekurzia (alebo tiež vzájomná), kedy funkcia neodkazuje vo svojej definícii priamo na seba, ale využíva inú funkciu, ktorá sa odkazuje naspäť na prvú (všeobecnejšie sa kruh môže uzavrieť na viac krokov). Ako príklad uveďme rekurzívne definície predikátov párnosti a nepárnosti:

bool even(int n) {
    if (n == 0) return true;
    else return odd(n - 1);
}

bool odd(int n) {
    if (n == 0) return false;
    else return even(n - 1);
}

Rekurzia pomocou zásobníka - ako je rekurzia implementovaná

O rekurzívne volania sa stará zásobník volaní (call stack)

  • Ide o všeobecnú štruktúru potrebnú aj v nerekurzívnych programoch, ktoré volajú funkcie
  • Po zavolaní nejakej funkcie f sa pre ňu vytvorí na zásobníku záznam, ktorý obsahuje všetky lokálne premenné a argumenty funkcie
  • Keď potom z funkcie f zavoláme nejakú funkciu g (pričom v rekurzii môže byť aj f=g), tak sa vytvorí nový záznam pre g. Navyše v zázname pre f si uložíme aj to, v ktorom kroku sme prestali s výpočtom, aby sme vedeli správne pokračovať
  • Po skončení výpočtu funkcie g sa jej záznam zruší zo zásobníka. Vrátime k záznamu pre funkciu f a pokračujeme vo výpočte so správnymi hodnotami všetkých premenných a od správneho miesta.

Záznamy v zásobníku si môžeme predstaviť uložené v stĺpci jeden nad druhým

  • vrchný záznam je aktuálny, pre funkciu, ktorá sa vykonáva
  • pod ním je záznam pre funkciu, ktorá ju volala atď
  • na spodku je záznam pre funkciu main

Teraz si môžeme jednoduchý zásobník odsimulovať napríklad na výpočte faktoriálu.

int factorial(int n) {
    if (n < 2) return 1;
    else return n * factorial(n-1);
}

Zložitejšie príklady rekurzie

Každý z predchádzajúcich príkladov sme vedeli pomerne jednoducho zapísať aj bez rekurzie, aj keď rekurzívny výpočet bol často prehľadnejší, zrozumiteľnejší, kratší a krajší.

Ukážeme si však aj príklady, ktoré by sa bez rekurzie písali obtiažne (aj keď ako si neskôr ukážeme, rekurzia sa dá vždy odstrániť, v najhoršom prípade simuláciou zásobníka). Takéto príklady, kde rekurzia veľmi pomáha, uvidíme na zvyšku dnešnej prednášky, ale aj na dvoch ďalších a k rekurzii sa vrátime aj neskôr v semestri a samozrejme na ďalších predmetoch.

Odbočka: korytnačia grafika v SVGdraw

Náš prvý dnešný príklad rekurzie budú rekurzívne obrázky, fraktály. Aby sa nám lepšie vykresľovali, v knižnici SVGdraw je možnosť kresliť pomocou korytnačej grafiky.

  • Vytvoríme si virtuálnu korytnačku, ktorá má určitú polohu a natočenie.
  • Môžeme jej povedať, aby sa otočila doľava alebo doprava o určitý počet stupňov (turtle.turnLeft(uhol) a turtle.turnRight(uhol)).
  • Môžeme jej povedať, aby išla o určitú dĺžku dopredu (turtle.forward(dlzka))
  • Keď ide korytnačka dopredu, zanecháva v piesku chvostom čiarku (vykreslí teda čiaru do nášho obrázku).

Napríklad na vykreslenie štvorca s dĺžkou strany 100 môžeme korytnačke striedavo prikazovať ísť o 100 dopredu a otáčať sa o 90 stupňov doľava.

  • Na obrázku sa animuje pohyb korytnačky (pozri tu)
  • Program by sa dal ľahko rozšíriť na vykresľovanie pravidelného n-uholníka (stačí zmeniť uhol otočenia a počet opakovaní cyklu)
#include "SVGdraw.h"

int main(void) {
    /* Vytvor korytnačku na súradniciach (25,175)
     * otočenú doprava na obrázku s rozmermi 200x200 pixelov,
     * ktorý bude uložený do súboru stvorec.svg. */
    Turtle turtle(200, 200, "stvorec.svg", 25, 175, 0);

    for (int i = 0; i < 4; i++) {
        turtle.forward(150);  /* vykresli čiaru dĺžky 100 */
        turtle.turnLeft(90);  /* otoč sa doľava o 90 stupňov */
    }
    /* strany sú vykreslené v poradí dolná, pravá, horná, ľavá */

    /* Ukonči vypisovanie obrázka. */
    turtle.finish();
}

Fraktály

Fraktály sú útvary, ktorých časti na rôznych úrovniach zväčšenia sa podobajú na celý útvar. Mnohé fraktály vieme definovať a vykresliť pomocou jednoduchej rekurzie.

Ḱochova krivka

Kochova krivka stupňa 3

Príkladom fraktálu je Kochova krivka. Ako vzniká?

  • Predstavme si úsečku, ktorá meria d centimetrov.
  • Spravíme s ňou nasledujúcu transformáciu:
    • Úsečka sa rozdelí na tretiny a nad strednou tretinou sa zostrojí rovnostranný trojuholník. Základňa trojuholníka v krivke nebude.
    • Dostávame tak útvar pozostávajúci zo štyroch úsečiek s dĺžkou d/3
  • Tú istú tranformáciu môžeme teraz spraviť na každej zo štyroch nových úsečiek, t.j. dostávame 16 úsečiek dĺžky d/9
  • Takéto transformácie môžeme robiť do nekončna

Druhá možnosť je popísať krivku pomocou dvoch parametrov: dĺžka d a stupeň n

  • Kochova krivka stupňa 0 je úsečka dĺžky d
  • Kochova krivka stupňa n pozostáva zo štyroch kriviek stupňa n-1 a dĺžky n/3
    • na presný popis umiestnenia a natočenia týchto kriviek nižšieho stupňa použijeme korytnačiu grafiku, čo máme spravené vo funkcii nižšie.
 
#include "SVGdraw.h"

void drawKoch(double d, int n, Turtle& turtle){
    if (n==0) turtle.forward(d);
    else {
        drawKoch(d/3, n-1, turtle);
        turtle.turnLeft(60);
        drawKoch(d/3, n-1, turtle);
        turtle.turnRight(120);
        drawKoch(d/3, n-1, turtle);
        turtle.turnLeft(60);
        drawKoch(d/3, n-1, turtle);
    }
}

int main(void) {
    int width = 310; /* rozmery obrazku */
    int height = 150;

    double d = 300; /* velkost krivky */
    int n = 5; /* stupen krivky */

    /* Vytvor korytnačku otočenú doprava. */
    Turtle turtle(width, height, "fraktal.svg", 1, height-10, 0);

    /* nakresli Kochovu krivku rekurzívne */
    drawKoch(d, n, turtle);

    /* Schovaj korytnačku. */
    turtle.finish();
}

Rekurzívny strom

A ešte jeden príklad fraktálu: strom definovaný nasledovne:

  • strom má dva parametre: veľkosť d a stupeň n
  • strom stupňa 0 je prázdna množina
  • strom stupňa n pozostáva z kmeňa, ktorý tvorí úsečka dĺžky d a z dvoch podstromov, ktoré sú stromy stupňa n-1, veľkosti d/2 a otočené o 30 stupnov doľava a doprava od hlavnej osi stromu (pozri obrázky nižšie)

Rekurzívnu funkciu na vykresľovanie stromu napíšeme tak, aby sa po skončení vrátila na miesto a otočenie, kde začala

  • Bez toho by sa sme nevedeli, kde korytnačka je po vykreslení ľavého podstromu a nemohli by sme teda kresliť pravý
  • Korytnačka teda prejde po každej vetve dvakrát, raz smerom dopredu a raz naspäť.
#include "SVGdraw.h"

void drawTree(double d, int n, Turtle& turtle) {
    if (n == 0) {
        return;  /* stupen 0 - nerob nic */
    } else {
        turtle.forward(d);              /* kmen stromu */
        turtle.turnLeft(30);
        drawTree(d / 2, n - 1, turtle); /* lava cast koruny */
        turtle.turnRight(60);
        drawTree(d / 2, n - 1, turtle); /* prava cast koruny */
        turtle.turnLeft(30);
        turtle.forward(-d);             /* navrat na spodok kmena */
    }
}

int main(void) {
    int width = 150; /* rozmery obrazku */
    int height = 200;

    double d = 100; /* velkost stromu */
    int n = 5; /* stupen krivky */

    /* vytvor korytnačku otočenú hore */
    Turtle turtle(width, height, "fraktal.svg", width / 2, height - 10, 90);

    /* nakresli strom rekurzívne */
    drawTree(d, n, turtle);

    /* schovaj korytnačku */
    turtle.finish();
}

Hanojské veže

  • Problém Hanojských veží pozostáva z troch stĺpcov (tyčiek) a niekoľkých kruhov rôznej veľkosti. Začína sa postavením pyramídy z kruhov (kameňov) na prvú tyčku od najväčšieho po najmenší.
  • Úlohou je potom presunúť celú pyramídu na inú tyčku, avšak pri dodržaní nasledovných pravidiel:
    • v jednom ťahu (na jedenkrát) je možné premiestniť iba jeden hrací kameň
    • väčší kameň nesmie byť nikdy položený na menší

Úlohu budeme riešiť rekurzívne

  • Ak máme iba jeden kameň, úloha je veľmi jednoduchá - preložíme ho z pôvodného stĺpika na cieľový stĺpik.
  • Ak chceme preložiť viac kameňov (nech ich je N), tak
    • Všetky okrem posledného preložíme na pomocný stĺpik (na to použijeme taký istý postup len s N-1 kameňmi)
    • Premiestnime jeden kameň kam potrebujeme
    • Zatiaľ odložené kamene (na pomocnom stĺpiku) preložíme z pomocného na cieľový stĺpik (na to použijeme opäť taký istý postup s N-1 kameňmi)

Aby sme to popísali konkrétnejšie - preloženie N kameňov z A na C (s pomocným stĺpikom B) urobíme takto:

  • Preložíme N-1 kameňov z A na B (s použitím C)
  • Preložíme 1 kameň z A na C (s použitím B - ale reálne to potrebovať nebudeme)
  • Preložíme N-1 kameňov z B na C (s použitím A)
void presunHanoi(int odkial, int cez, int kam, int n){
    if (n == 1) {
        cout << "Prelozim kamen z " << odkial <<" na " << kam << endl;
    } else {
        presunHanoi(odkial, kam, cez, n-1); // odlozime si n-1 na pomocny-cez
        presunHanoi(odkial, cez, kam, 1);   // prelozime najvacsi na finalne miesto
        presunHanoi(cez, odkial, kam, n-1); // zvysnych n-1 prelozime z docasneho odkladiska na finalne
    }
}

int main (void){
    presunHanoi(1, 2, 3, 3); // z veze 1 na vezu 3 (pomocou veze 2)
}

Dôležité je si uvedomiť, že nasledovný postup dodržuje pravidlá. Po zavolaní funkcie presunHanoi vždy platí:

  • Funkcia bude presúvať n horných kameňov z tyče odkial na tyč kam pomocou pomocnej tyče cez.
  • Ak n>1, tak na tyčiach kam a cez sú len väčšie kamene ako horných n kameňov na odkial.
  • Ak n=1, na tyči kam sú len väčšie kamene, obsah tyče cez môže byť ľubovoľný.

Prednáška 10

Oznamy

  • Dnes od 14:00 v IT kobke Linux Installfest
  • Zajtrajšia rozcvička bude z dnešnej prednášky
  • Do štvrtka obeda sa v AIS môžete prihlásiť na opravnú písomku, ktorá bude v piatok o 12:20 (ak sa neviete prihlásiť, pošlite nám email)
  • V piatok o 22:00 termín DÚ2 (fraktál)
  • Druhá písomka bude v stredu 21.11. o 18:10 v posluchárni B.

Opakovanie rekurzie

  • Rekurentná definícia: určitý objekt definujeme pomocou menších objektov toho istého typu
    • Napr. Fibonacciho čísla F(n) = F(n-1) + F(n-2)
    • Nezabudnime na triviálne prípady, napr. F(0)=F(1)=1
  • Rekurentné definície vieme často priamočiaro zapísať do rekurzívnych funkcií (aj keď môžu byť pomalé)
int fib(int n) {
    if (n<2) return 1;
    else return fib(n-1) + fib(n-2);
}
  • V rekurzívnej funkcii riešime problém pomocou menších podproblémov toho istého typu
    • Napríklad aby sme našli číslo x v utriedenom poli medzi indexami left a right, potrebujeme ho porovnať so stredným prvkom tohoto intervalu a potom riešiť tú istú úlohu pre menší interval
    • Aj keď sme pôvodne chceli hľadať prvok v celom poli, úlohu rozšírime o parametre left a right, aby sa dala spraviť rekurzia
bool contains (int a[], int left, int right, int x){
    if (left == right) return (a[left] == x);
    int index = (left + right) / 2;
    if (x <= a[index]) return contains(a, left, index, x);
    else return contains(a, index+1, right, x);
}

Zásobník volaní

Druhý pohľad na rekurziu je dynamický: môžeme simulovať, čo sa v programe deje so zásobníkom volaní (call stack)

  • Ide o všeobecnú štruktúru potrebnú aj v nerekurzívnych programoch, ktoré volajú funkcie
  • Po zavolaní nejakej funkcie f sa pre ňu vytvorí na zásobníku záznam, ktorý obsahuje všetky lokálne premenné a argumenty funkcie
  • Keď potom z funkcie f zavoláme nejakú funkciu g (pričom v rekurzii môže byť aj f=g), tak sa vytvorí nový záznam pre g. Navyše v zázname pre f si uložíme aj to, v ktorom kroku sme prestali s výpočtom, aby sme vedeli správne pokračovať
  • Po skončení výpočtu funkcie g sa jej záznam zruší zo zásobníka. Vrátime sa k záznamu pre funkciu f a pokračujeme vo výpočte so správnymi hodnotami všetkých premenných a od správneho miesta.

Záznamy v zásobníku si môžeme predstaviť uložené v stĺpci jeden nad druhým

  • vrchný záznam je aktuálny, pre funkciu, ktorá sa vykonáva
  • pod ním je záznam pre funkciu, ktorá ju volala atď
  • na spodku je záznam pre funkciu main

Skúsme napríklad odsimulovať, čo sa deje ak vo funkcii main zavoláme fib(3)

  • Kvôli prehľadnosti si fib rozpíšeme na viac riadkov:
#include <iostream>
using namespace std;

int fib(int n) {
    if (n < 2) return 1;
    else {
        int a = fib(n - 1); // riadok (A)
        int b = fib(n - 2); // riadok (B)
        return a+b;
    }
}

int main(void) {
    int x = fib(3);    // riadok (C)
    cout << x << endl; 
}

Tu je priebeh programu (obsah zásobníka)

(1)          (2)                      (3)

                                       fib n=2
              fib n=3                  fib n=3, a=?, b=?, riadok A
main, x=?     main, x=?, riadok C      main, x=?, riadok C

(4)                             (5)
 
fib n=1                         
fib n=2, a=?, b=?, riadok A     fib n=2, a=1, b=?, riadok A
fib n=3, a=?, b=?, riadok A     fib n=3, a=?, b=?, riadok A
main, x=?, riadok C             main, x=?, riadok C             


(6)                             (7)
 
fib n=0                         
fib n=2, a=1, b=?, riadok B     fib n=2, a=1, b=1, riadok B
fib n=3, a=?, b=?, riadok A     fib n=3, a=?, b=?, riadok A
main, x=?, riadok C             main, x=?, riadok C             


(8)                             (9)

                                fib n=1
fib n=3, a=2, b=?, riadok A     fib n=3, a=2, b=?, riadok B 
main, x=?, riadok C             main, x=?, riadok C                 


(10)                            (11)

fib n=3, a=2, b=1, riadok B     
main, x=?, riadok C             main, x=3, riadok C 

Efektívnejší výpočet Fibonacciho čísel

Priamočiary rekurzívny zápis výpočtu Fibonacciho čísel je neefektívny, lebo výpočet Fibonacciho čísel sa opakuje

  • Napr. pre n=4 počítame fib(2) dvakrát, pre n=6 päťkrát a pre n=20 až 4181-krát

Spomeňme si na zápis výpočtu bez rekurzie, s dvomi premennými:

  • F(n) spočíta v čase O(n)
int fibonacci(int n) {

    int f = 1;     // Cislo F(i) 
    int oldF = 0; // Cislo F(i-1)
    
    for (int i = 2; i <= n; i++) {
        int newF = f + oldF;  // spocitaj nove F(i) pre vyssie i
        oldF = f;             // poposuvaj hodnoty
        f = newF;
    }

    return f;
}

Prepíšeme ho na rekurziu:

int fib1(int a, int b, int i, int n) {
    /* a je F(i-1), b je F(i), spočítaj F(n), kde n>=i */
    if (n == i) return b;
    else return fib1(b, a+b, i+1, n);
}

int fib(int n){
    return fib1(1, 1, 1, n);
}

Namiesto dvoch premenných i a n si môžeme pamätať len ich rozdiel k=n-i

  • hodnota k nám vraví, koľkokrát máme ešte sčitovať dve posledné čísla
int fib1(int a, int b, int k) {
    /* a je F(i-1), b je F(i), spočítaj F(i+k) */
    if (k == 0) return b;
    else return fib1(b, a+b, k-1);
}

int fib(int n){
    fib1(1, 1, n-1);
}

Táto rekurzívna funkcia pracuje v čase O(n), rovnako ako verzia s cyklom.

Vypisovanie variácií s opakovaním

Vypíšte všetky trojice cifier, pričom každá cifra je z množiny {0..n-1} a cifry sa môžu opakovať (variácie 3-tej triedy z n prvkov). Napr. pre n=2:

000
001
010
011
100
101
110
111

Veľmi jednoduchý program s troma cyklami:

#include <iostream>
using namespace std;

int main(void) {
    int n;
    cin >> n;
    for(int i=0; i<n; i++) {
        for(int j=0; j<n; j++) {
            for(int k=0; k<n; k++) {
                cout << i << j << k << endl;
            }
        }
    }
}

Rekurzívne riešenie pre všeobecné k

Čo ak chceme k-tice pre všeobecné k? Využijeme rekurziu.

  • Variácie k-tej triedy vieme rozdeliť na n skupín podľa prvého prvku:
    • tie čo začínajú na 0, tie čo začínajú na 1, ..., tie čo začínajú na n-1.
  • V každej skupine ak odoberieme prvý prvok, dostaneme variácie triedy k-1
#include <iostream>
using namespace std;

void vypis(int a[], int k) {
    for (int i = 0; i < k; i++) {
        cout << a[i];
    }
    cout << endl;
}

void generuj(int a[], int i, int k, int n) {
    /* v poli a dlzky k mame prvych i cifier,
     * chceme vygenerovat vsetky moznosti
     * poslednych k-i cifier */
    if (i == k) {
        vypis(a, k);
    } else {
        for (int x = 0; x < n; x++) {
            a[i] = x;
            generuj(a, i + 1, k, n);
        }
    }
}

int main(void) {
    const int maxK = 100;
    int a[maxK];
    int k, n;
    cout << "Zadajte k a n: ";
    cin >> k >> n;
    generuj(a, 0, k, n);
}

Ďalšie rozšírenia

  • Čo ak chceme všetky k-tice písmen A-Z?
  • Čo ak chceme všetky DNA reťazce dĺžky k (DNA pozostáva z "písmen" A,C,G,T)?
// pouzi n=26
void vypis(int a[], int k) {
    for (int i = 0; i < k; i++) {
        char c = 'A'+a[i];
        cout << c;
    }
    cout << endl;
}

// pouzi n=4
void vypis(int a[], int k) {
    char abeceda[5] = "ACGT";
    for (int i = 0; i < k; i++) {
        cout << abeceda[a[i]];
    }
    cout << endl;
}

Cvičenia

  • Ako by sme vypisovali všetky k-ciferné hexadecimálne čísla (šestnástková sústava), kde používame cifry 0-9 a písmená A-F?
  • Ako by sme vypisovali všetky k-tice písmen v opačnom poradí, od ZZZ po AAA?

Variácie bez opakovania

Teraz chceme vypísať všetky k-tice cifier z množiny {0,..,n-1}, v ktorých sa žiaden prvok neopakuje (pre k=n dostávame permutácie)

Príklad pre k=3, n=3

012
021
102
120
201
210

Skúšanie všetkých možností

  • Jednoduchá možnosť: použijeme predchádzajúci program a pred výpisom skontrolujeme, či je riešenie správne

Prvý pokus:

bool spravne(int a[], int k, int n) {
    /* je v poli a dlzky k kazde cislo od 0 po n-1 najviac raz? */
    bool bolo[maxN];
    for (int i = 0; i < n; i++) {
        bolo[i] = false;
    }
    for (int i = 0; i < k; i++) {
        if (bolo[a[i]]) return false;
        bolo[a[i]] = true;
    }
    return true;
}

void generuj(int a[], int i, int k, int n) {
    /* v poli a dlzky k mame prvych i cifier,
     * chceme vygenerovat vsetky moznosti
     * poslednych k-i cifier */
    if (i == k) {
        if (spravne(a, k, n)) {
            vypis(a, k);
        }
    } else {
        for (int x = 0; x < n; x++) {
            a[i] = x;
            generuj(a, i + 1, k, n);
        }
    }
}

Cvičenie: ako by sme napísali funkciu spravne, ak by nedostala ako parameter hodnotu n?

Prehľadávanie s návratom, backtracking

  • Predchádzajúce riešenie je neefektívne, lebo prechádza cez všetky variácie s opakovaním a veľa z nich zahodí.
    • Napríklad pre k=7 a n=10 pozeráme Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle 10^7} variácií s opakovaním, ale iba 604800 z nich je správnych, čo je asi 6%
  • Len čo sa v poli a vyskytne opakujúca sa cifra, chceme túto vetvu prehľadávania ukončiť, lebo doplnením ďalších cifier problém neodstránime
  • Spravíme funkciu moze(a,i,x), ktorá určí, či je možné na miesto i v poli a dať cifru x
  • Testovanie správnosti vo funkcii generuj sa dá vynechať
bool moze(int a[], int i, int x) {
    /* Mozeme dat hodnotu x na poziciu i v poli a?
     * Mozeme, ak sa nevyskytuje v a[0..i-1] */
    for (int j = 0; j < i; j++) {
        if (a[j] == x) return false;
    }
    return true;
}

void generuj(int a[], int i, int k, int n) {
    /* v poli a dlzky k mame prvych i cifier,
     * chceme vygenerovat vsetky moznosti
     * poslednych k-i cifier */
    if (i == k) {
        vypis(a, k);
    } else {
        for (int x = 0; x < n; x++) {
            if (moze(a, i, x)) {
                a[i] = x;
                generuj(a, i + 1, k, n);
            }
        }
    }
}

Možné zrýchlenie: vytvoríme trvalé pole bolo, v ktorom bude zaznamené, ktoré cifry sa už vyskytli a to použijeme vo funkcii moze.

  • Po návrate z rekurzie nesmieme zabudúť príslušnú hodnotu odznačiť!
void generuj(int a[], bool bolo[], int i, int k, int n) {
    /* v poli a dlzky k mame prvych i cifier,
     * v poli bolo mame zaznamenane, ktore cifry su uz pouzite,
     * chceme vygenerovat vsetky moznosti
     * poslednych k-i cifier */
    if (i == k) {
        vypis(a, k);
    } else {
        for (int x = 0; x < n; x++) {
            if (!bolo[x]) {
                a[i] = x;
                bolo[x] = true;
                generuj(a, bolo, i + 1, k, n);
                bolo[x] = false;
            }
        }
    }
}

int main(void) {
    const int maxK = 100;
    const int maxN = 100;
    int a[maxK];
    bool bolo[maxN];
    int k, n;
    cout << "Zadajte k a n (k<=n): ";
    cin >> k >> n;
    for (int i = 0; i < n; i++) {
        bolo[i] = false;
    }
    generuj(a, bolo, 0, k, n);
}

Cvičenia: ako potrebujeme zmeniť program, aby sme generovali všetky postupnosti k cifier z množiny {0,..,n-1}, také, že:

  • z každej cifry sú v postupnosti najviac 2 výskyty?
  • súčet cifier je aspoň S?

Technika rekurzívneho prehľadávania všetkých možností s orezávaním beznádejných vetiev sa nazýva prehľadávanie s návratom alebo backtracking.

  • Hľadáme všetky postupnosti, ktoré spĺňajú nejaké podmienky
    • Vo všeobecnosti nemusia byť rovnako dlhé
  • Ak máme celú postupnosť, vieme otestovať, či spĺňa podmienku (funkcia spravne)
  • Ak máme časť postupnosti a nový prvok, vieme otestovať, či po pridaní tohto prvku má ešte šancu tvoriť časť riešenia (funkcia moze)
    • Funkcia moze nesmie vrátiť false, ak ešte je možné riešenie
    • Môže vrátiť true, ak už nie je možné riešenie, ale nevie to ešte odhaliť
    • Snažíme sa však odhaliť problém čím skôr

Všeobecná schéma

void generuj(int a[], int i) {
    /* v poli a dlzky k mame prvych i cisel z riesenia */
    if (spravne(a, i)) { /* ak uz mame cele riesenie, vypisme ho */
        vypis(a, i);
    } else {
        pre vsetky hodnoty x {
            if (moze(a,i,x) {
                a[i] = x;
                generuj(a, i + 1);
            }
        }
    }
}

Prehľadávanie s návratom môže byť vo všeobecnosti veľmi pomalé, čas výpočtu exponenciálne rastie.

Problém 8 dám

Cieľom je rozmiestniť n dám na šachovnici nxn tak, aby sa žiadne dve navzájom neohrozovali, tj. aby žiadne dve neboli v rovnakom riadku, stĺpci, ani na rovnakej uhlopriečke.

Príklad pre n=4:

 . * . .
 . . . *
 * . . .
 . . * .
  • V každom riadku bude práve jedna dáma, teda môžeme si riešenie reprezentovať ako pole damy dĺžky n, kde damy[i] je stĺpec, v ktorom je dáma na riadku i
  • Podobne ako v predchádzajúcom príklade chceme do poľa dať čísla od 1 po n, aby spĺňali ďalšie podmienky (v každom stĺpci a na každej uhlopriečke najviac 1 dáma)
  • Vytvoríme si polia, kde si budeme pamätať pre každý stĺpec a uhlopriečku, či už je obsadený
  • Uhlopriečky v oboch smeroch očísľujeme číslami od 0 po 2n-2
    • V jednom smere majú miesta na uhlopriečke rovnaký súčet, ten teda bude číslom uhlopriečky
    • V druhom smere majú rovnaký rozdiel, ten však môže byť aj záporný, pričítame n-1
  • Pre jednoduchosť použijeme globálne premenné, lebo potrebujeme veľa polí
    • Globálne premenné spôsobujú problémy vo väčších programoch: mená premenných sa môžu "biť", môžeme si omylom prepísať číslo dôležité v inej časti programu
    • Mohli by sme si tiež spraviť struct obsahujúci všetky premenné potrebné v rekurzii a odovzdávať si ten
#include <iostream>
using namespace std;

/* globalne premenne */
const int maxN = 100;
int n;
int damy[maxN];       /* pole ktore obsahuje stlpec s damou v riadku i*/
bool bolStlpec[maxN]; /* pole ktore obsahuje true ak stlpec obsadeny damou */
bool bolaUhl1[2 * maxN - 1];  /* polia ktore obsahuju true ak uhlopriecky obsadene */
bool bolaUhl2[2 * maxN - 1];
int pocet;       /* pocet najdenych rieseni */

int uhl1(int i, int j) {
    /* na ktorej uhlopriecke je riadok i, stlpec j v smere 1? */
    return i + j;
}

int uhl2(int i, int j) {
    /* na ktorej uhlopriecke je riadok i, stlpec j v smere 2? */
    return n - 1 + i - j;
}

void vypis() {
    /* vypis sachovnicu textovo a zvys pocitadlo rieseni */
    pocet++;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            if (damy[i] == j) cout << " *";
            else cout << " .";
        }
        cout << endl;
    }
    cout << endl;
}

void generuj(int i) {
    /* v poli damy mame prvych i dam, dopln dalsie */
    if (i == n) {
        vypis();
    } else {
        for (int j = 0; j < n; j++) {
            /* skus dat damu na riadok i, stlpec j */
            if (!bolStlpec[j]
                    && !bolaUhl1[uhl1(i, j)] && !bolaUhl2[uhl2(i, j)]) {
                damy[i] = j;
                bolStlpec[j] = true;
                bolaUhl1[uhl1(i, j)] = true;
                bolaUhl2[uhl2(i, j)] = true;
                generuj(i + 1);
                bolStlpec[j] = false;
                bolaUhl1[uhl1(i, j)] = false;
                bolaUhl2[uhl2(i, j)] = false;
            }
        }
    }
}

int main(void) {
    cout << "Zadajte velkost sachovnice: ";
    cin >> n;
    for (int i = 0; i < n; i++) {
        bolStlpec[i] = false;
    }
    for (int i = 0; i < 2 * n + 1; i++) {
        bolaUhl1[i] = false;
        bolaUhl2[i] = false;
    }

    /* rekuzia */
    pocet=0;
    generuj(0);
    cout << "Pocet rieseni: " << pocet << endl;
}

Generovanie všetkých podmnožín

Chceme vypísať všetky podmnožiny množiny {0,..,m-1}. Na rozdiel od variácií nám v podmnožine nezáleží na poradí (napr. {0,1} = {1,0}), prvky teda budeme vždy vypisovať od najmenšieho po najväčší. Napr. pre m=2 máme podmnožiny

{}
{0}
{0,1}
{1}

Podmnožinu vieme vyjadriť ako binárne pole dĺžky m, kde a[i]=0 znamená, že i nepatrí do množiny a a[i]=1 znamená, že patrí. Teda môžeme použiť predchádzajúci program pre n=2, k=m a zmeniť iba výpis:

void vypis(int a[], int m) {
    cout << "{";
    bool prve = true;
    for (int i = 0; i < m; i++) {
        if (a[i] == 1) {
            if (prve) {
                cout << "" << i;
                prve=false;
            } else {
                cout << "," << i;
            }
        }
    }
    cout << "}" << endl;
}
  • V premennej prve si pamätáme, či máme oddeliť ďalšie vypisované číslo od predchádzajúceho.
    • Ak ešte žiadne nebolo, oddeľovač je prázdny reťazec.
    • Ak už sme niečo vypísali, oddeľovač je čiarka.

Namiesto poľa intov môžeme použiť pole boolovských hodnôt a celý program trochu prispôsobiť tomu, že generujeme podmnožiny:

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

void vypis(bool a[], int m) {
    cout << "{";
    bool prve = true;
    for (int i = 0; i < m; i++) {
        if (a[i]) {
            if (prve) {
                cout << "" << i;
                prve=false;
            } else {
                cout << "," << i;
            }
        }
    }
    cout << "}" << endl;
}

void generuj(bool a[], int i, int m) {
    /* v poli a dlzky k mame rozhodnutie o prvych i
     * prvkoch, chceme vygenerovat vsetky podmnoziny
     * prvkov {i..m-1} */
    if (i == m) {
        vypis(a, m);
    } else {
        a[i] = true;
        generuj(a, i + 1, m);
        a[i] = false;
        generuj(a, i + 1, m);
    }
}

int main(void) {
    const int maxM = 100;
    int m;
    cin >> m;
    bool a[maxM];
    generuj(a, 0, m);
}

Pre n=3 program vypíše:

{0,1,2}
{0,1}
{0,2}
{0}
{1,2}
{1}
{2}
{}

Cvičenie: Čo by vypísal, ak by sme prehodili true a false v rekurzii?

Prednáška 11

Oznamy

  • Do zajtrajšieho obeda sa v AIS môžete prihlásiť na opravnú písomku, ktorá bude v piatok o 12:20 (ak sa neviete prihlásiť, pošlite nám email).
  • V piatok o 22:00 termín DÚ2 (fraktál).
  • Druhá písomka bude v stredu 21.11. o 18:10 v posluchárni B.

Problém batoha (Knapsack problem)

Metódu prehľadávania s návratom (angl. backtracking), prebratú na minulej prednáške, teraz využijeme na riešenie tzv. problému batoha. Ide o relatívne dôležitý teoretický problém, s ktorým sa typický študent informatiky počas štúdia stretne viackrát. Populárne ho možno sformulovať napríklad takto:

  • Zlodej sa vlúpal do obchodu, v ktorom našiel nejaký počet „odcudziteľných” predmetov (potenciálnych lupov).
  • Pozná pritom cenu aj hmotnosť všetkých týchto predmetov.
  • Vo svojom batohu dokáže odniesť iba lup nepresahujúci jeho nosnosť (ktorú tiež pozná).
  • Ktoré predmety má zlodej odcudziť, aby ich celková hmotnosť nepresahovala nosnosť batoha a aby odišiel s čo najcennejším lupom?

S využitím prehľadávania s návratom napíšeme program, ktorý tento problém rieši. Jeho vstup bude vyzerať napríklad nejako takto:

Zadaj pocet predmetov v obchode (potencialnych lupov): 3
Zadaj hmotnost a cenu predmetu cislo 1: 5 9
Zadaj hmotnost a cenu predmetu cislo 2: 4 6
Zadaj hmotnost a cenu predmetu cislo 2: 4 4
Zadaj nosnost batoha: 8

Výstup programu na horeuvedenom vstupe potom bude takýto:

Zober nasledujuce predmety: 2, 3.
Celkova hodnota lupu: 10.

Prvé riešenie: preskúmanie všetkých možných výberov

Najjednoduchšie riešenie problému batoha spočíva v preskúmaní všetkých podmnožín množiny predmetov v obchode – čiže vlastne všetkých potenciálnych lupov. Program vypisujúci všetky podmnožiny danej množiny bol jednou z tém minulej prednášky. Tento program teraz iba mierne poupravíme – nebudeme nájdené podmnožiny predmetov (potenciálne lupy) vypisovať, ale zakaždým:

  • Spočítame celkovú hmotnosť a cenu nájdeného potenciálneho lupu.
  • Ak hmotnosť tohto lupu nepresahuje cenu batoha, porovnáme jeho cenu s „najlepším” (t.j. najcennejším) doposiaľ nájdeným lupom.
  • Ak je cennejší, ako doposiaľ „najlepší” lup, ide o nového kandidáta na optimálny lup.

Podmnožiny budeme, ako obvykle, reprezentovať poľom typu bool, v ktorom si pre každý prvok univerza pamätáme, či do danej podmnožiny patrí.

#include <iostream>
using namespace std;

const int maxN = 100;

/* Struktura reprezentujuca jeden "odcudzitelny" predmet: */

struct predmet {
    int hmotnost;
    int cena;
}; 

/* Globalne premenne pouzivane v rekurzii: */

int N;                           // pocet predmetov v obchode
predmet a[maxN];                 // pole s udajmi o jednotlivych predmetoch
int nosnost;                     // nosnost batoha

bool najlepsiLup[maxN];          // najcennejsi doposial najdeny potencialny lup (na uvod neinicializovany)
int cenaNajlepsiehoLupu = -1;    // jeho cena (kazdy lup bude urcite cennejsi ako -1)

/* Pomocne funkcie: */

int spocitajHmotnostLupu(bool lup[]) {
    int hmotnost = 0;
    for (int i = 0; i <= N-1; i++) {
        if (lup[i]) {
            hmotnost += a[i].hmotnost;
        }
    }
    return hmotnost;
}

int spocitajCenuLupu(bool lup[]) {
    int cena = 0;
    for (int i = 0; i <= N-1; i++) {
        if (lup[i]) {
            cena += a[i].cena;
        }
    }
    return cena;
}

void vypisLup(bool lup[]) {
    cout << "Zober nasledujuce predmety: ";
    bool prvy = true;
    for (int i = 0; i <= N-1; i++) {
        if (lup[i]) {
            if (prvy) {
                cout << i + 1;
                prvy = false;
            } else {
                cout << ", " << i + 1;
            }        
        }    
    }
    cout << "." << endl;
}

/* Generovanie vsetkych moznych lupov (podmnozin predmetov v obchode): */

void generujLupy(bool lup[], int index) {
    /* 
       V poli lup[] dlzky N postupne generujeme lupy (podmnoziny mnoziny predmetov).
       O hodnotach prvkov lup[0],...,lup[index-1] uz je rozhodnute.
       Postupne vygenerujeme vsetky moznosti pre lup[index],...,lup[N-1].
       Kazdy vysledny lup porovname s doposial najlepsim a v pripade potreby optimum aktualizujeme.    
    */
    if (index == N) {                                // Lup je vygenerovany; zisti, ci ho batoh unesie.                               
        if (spocitajHmotnostLupu(lup) <= nosnost) {  // Ak ano, porovnaj cenu lupu s doposial najlepsim lupom.
            int cenaLupu = spocitajCenuLupu(lup);
            if (cenaLupu > cenaNajlepsiehoLupu) {    // Ak je najdeny lup drahsi, ide o zatial najlepsi lup.
                cenaNajlepsiehoLupu = cenaLupu;
                for (int i = 0; i <= N-1; i++) {
                    najlepsiLup[i] = lup[i];
                } 
            }
        }
    } else {                                         // Lup este nie je vygenerovany; skus postupne obe moznosti pre lup[index]. 
        lup[index] = false;
        generujLupy(lup, index+1);
        lup[index] = true;
        generujLupy(lup, index+1);
    }
} 

/* Funkcia main() -- nacitanie vstupov a spustenie generovania lupov: */

int main() {
    cout << "Zadaj pocet predmetov v obchode (potencialnych lupov): ";
    cin >> N;
    for (int i = 0; i <= N-1; i++) {
        cout << "Zadaj hmotnost a cenu predmetu cislo " << (i+1) << ": ";
        cin >> a[i].hmotnost >> a[i].cena;
    }
    cout << "Zadaj nosnost batoha: ";
    cin >> nosnost;
    
    bool lup[maxN];
    generujLupy(lup, 0);
    
    cout << endl;
    vypisLup(najlepsiLup);
    cout << "Celkova hodnota lupu: " << cenaNajlepsiehoLupu << "." << endl;

    return 0;
}

Optimalizácia č. 1: ukončenie prehľadávania vždy, keď je prekročená nosnosť

Keď je už po vygenerovaní nejakej časti lupu (čiže prvých niekoľko hodnôt poľa lup) jasné, že jeho hmotnosť bude presahovať nosnosť batoha, možno túto vetvu prehľadávania ukončiť.

Časti horeuvedeného programu zmenené touto optimalizáciou sú uvedené nižšie. Okrem samotnej funkcie generujLupy je potrebné prispôsobiť aj funkciu spocitajHmotnostLupu tak, aby ju bolo možné aplikovať aj na „neúplne vygenerované lupy”.

/* Potrebujeme vediet spocitat hmotnost len pre cast predmetov: */

int spocitajHmotnostLupu(bool lup[], int pokial) {
    int hmotnost = 0;
    for (int i = 0; i <= pokial; i++) {
        if (lup[i]) {
            hmotnost += a[i].hmotnost;
        }
    }
    return hmotnost;
}

void generujLupy(bool lup[], int index) {
    if (spocitajHmotnostLupu(lup, index-1) > nosnost) {
        return;  // Akonahle dosial vygenerovana cast lupu presahuje nosnost batoha, mozno prehladavanie ukoncit
    }
    if (index == N) {
        int cenaLupu = spocitajCenuLupu(lup);
        if (cenaLupu > cenaNajlepsiehoLupu) {
            cenaNajlepsiehoLupu = cenaLupu;
            for (int i = 0; i <= N-1; i++) {
                najlepsiLup[i] = lup[i];
            } 
        }
    } else {
        lup[index] = false;
        generujLupy(lup, index+1);
        lup[index] = true;
        generujLupy(lup, index+1);
    }
}

Optimalizácia č. 2: hmotnosť a cenu lupu netreba zakaždým počítať odznova

Predchádzajúci program vždy znovu a znovu prepočítava hmotnosť a cenu lupu, aj keď sa zoznam vybraných predmetov zmení iba trochu. Namiesto toho môžeme cenu a hmotnosť doposiaľ vygenerovanej časti lupu predávať funkcii generuj ako parameter.

#include <iostream>
using namespace std;

const int maxN = 100;

struct predmet {
    int hmotnost;
    int cena;
}; 

int N;
predmet a[maxN];
int nosnost;

bool najlepsiLup[maxN];
int cenaNajlepsiehoLupu = -1;

void vypisLup(bool lup[]) {
    cout << "Zober nasledujuce predmety: ";
    bool prvy = true;
    for (int i = 0; i <= N-1; i++) {
        if (lup[i]) {
            if (prvy) {
                cout << i + 1;
                prvy = false;
            } else {
                cout << ", " << i + 1;
            }        
        }    
    }
    cout << "." << endl;
}

void generujLupy(bool lup[], int index, int hmotnostLupu, int cenaLupu) {
    if (hmotnostLupu > nosnost) {
        return;
    }
    if (index == N) {
        if (cenaLupu > cenaNajlepsiehoLupu) {
            cenaNajlepsiehoLupu = cenaLupu;
            for (int i = 0; i <= N-1; i++) {
                najlepsiLup[i] = lup[i];
            } 
        }
    } else {
        lup[index] = false;
        generujLupy(lup, index+1, hmotnostLupu, cenaLupu);
        lup[index] = true;
        generujLupy(lup, index+1, hmotnostLupu + a[index].hmotnost, cenaLupu + a[index].cena);
    }
} 

int main() {
    cout << "Zadaj pocet predmetov v obchode (potencialnych lupov): ";
    cin >> N;
    for (int i = 0; i <= N-1; i++) {
        cout << "Zadaj hmotnost a cenu predmetu cislo " << (i+1) << ": ";
        cin >> a[i].hmotnost >> a[i].cena;
    }
    cout << "Zadaj nosnost batoha: ";
    cin >> nosnost;
    
    bool lup[maxN];
    generujLupy(lup, 0, 0, 0); // Doposial nie je nic vygenerovane; hmotnost aj cena lupu su teda zatial nulove.
    
    cout << endl;
    vypisLup(najlepsiLup);
    cout << "Celkova hodnota lupu: " << cenaNajlepsiehoLupu << "." << endl;

    return 0;
}

Rýchle triedenia prostredníctvom paradigmy „rozdeľuj a panuj”

Doposiaľ boli na tomto predmete prebraté tri triediace algoritmy: Bubble Sort, Insertion Sort a Max Sort. Všetky sú jednoduché, ale pomalé: majú kvadratickú zložitosť Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle O(n^2)} .

Dnes pridáme ďalšie dve triedenia, ktoré budú omnoho rýchlejšie: Merge Sort a Quick Sort. Obe budú založené na paradigme rozdeľuj a panuj (angl. divide and conquer, lat. divide et impera).

Rozdeľuj a panuj je paradigma rekurzívneho riešenia problémov v nasledujúcich troch fázach:

  • Rozdeľuj – problém rozdelíme na nejaké menšie časti (t.j. podproblémy), ktoré sa dajú riešiť samostatne.
  • Vyrieš podproblémy – rekurzívne vyriešime úlohu pre každý podproblém.
  • Panuj – riešenia podproblémov spojíme do riešenia pôvodného problému.

Triedenie zlučovaním (Merge Sort)

Pri triedení zlučovaním (angl. Merge Sort) sa pole veľkosti N najprv rozdelí na dve približne rovnaké polovice. Tieto polovice sa následne rekurzívne utriedia, čím vzniknú dve utriedené podpostupnosti o približne N/2 prvkoch. Vo fáze „panuj” sa nakoniec tieto dve utriedené časti poľa zlúčia (angl. merge) do výsledného utriedeného poľa.

Základ triedenia zlučovaním tak bude vyzerať nasledovne (pričom zostáva doimplementovať funkciu merge):

void mergesort(int a[], int low, int high) {
/* Funkcia utriedi prvky pola a[] od indexu low po index high (vratane). */
    
    /* Osetri trivialne pripady:*/
    if (low >= high) {
        return;           
    }
    
    /* Rozdeluj -- spocitaj priblizny stred triedeneho useku: */
    int mid = (low + high) / 2;
          
    /* Rekurzivne vyries podproblemy: */
    mergesort(a,low,mid);
    mergesort(a,mid+1,high);
       
    /* Panuj -- zluc obe utriedene casti do jednej: */ 
    merge(a,low,mid,high);
}

Zlúčenie dvoch utriedených podpostupností

Zostáva doprogramovať zlúčenie dvoch utriedených podpostupností a[low..mid], a[mid+1..high] do jedinej, taktiež utriedenej. Zlúčenú postupnosť budeme postupne ukladať do pomocného poľa aux, pričom postupovať budeme nasledovne:

  • Prvým prvkom poľa aux bude menší z prvkov a[low] a a[mid+1]. Môžu nám potom ostať podpostupnosti a[low+1..mid] a a[mid+1..high], prípadne a[low..mid] a a[mid+2..high].
  • Vo všeobecnosti máme podpostupnosti a[i..mid] a a[j..high]. Ďalším prvkom poľa aux bude buď a[i] alebo a[j] a ostanú nám podpostupnosti a[i+1..mid] a a[j..high], prípadne a[i..mid] a a[j+1..high].
  • Toto robíme dovtedy, kým niektorú z podpostupností nevyčerpáme celú. Potom už len na koniec poľa aux dokopírujeme zvyšok druhej podpostupnosti.

Po spojení oboch podpostupností do utriedeného poľa aux toto pole prekopírujeme naspäť do poľa a.

const int maxN = 1000;

...

void merge(int a[], int low, int mid, int high) {
    int aux[maxN];
    int i = low;
    int j = mid+1;
    int k = 0;
    
    while ((i <= mid) && (j <= high)) { // Kym su obe podpostupnosti a[i..mid], a[j..high] neprazdne
        if (a[i] <= a[j]) {             // Mensi z prvkov a[i], a[j] pridaj na koniec pola aux a aktualizuj indexy
            aux[k] = a[i];
            i++;
            k++;
        } else {
            aux[k] = a[j];
            j++;
            k++;
        }
    }
    
    while (i <= mid) {  // Ak nieco ostalo v prvej podpostupnosti, dokopiruj ju na koniec
        aux[k] = a[i];
        i++;
        k++;
    }
    
    while (j <= high) { // Ak nieco ostalo v druhej podpostupnosti, dokopiruj ju na koniec
        aux[k] = a[j];
        j++;
        k++;
    }
    
    for (int t = low; t <= high; t++) { // Prekopiruj pole aux naspat do pola a
        a[t] = aux[t - low];
    }
}

Výsledný program

#include <iostream>
using namespace std;

const int maxN = 1000;

void merge(int a[], int low, int mid, int high) {
    int aux[maxN];
    int i = low;
    int j = mid+1;
    int k = 0;
    
    while ((i <= mid) && (j <= high)) {
        if (a[i] <= a[j]) {
            aux[k] = a[i];
            i++;
            k++;
        } else {
            aux[k] = a[j];
            j++;
            k++;
        }
    }
    
    while (i <= mid) {
        aux[k] = a[i];
        i++;
        k++;
    }
    
    while (j <= high) {
        aux[k] = a[j];
        j++;
        k++;
    }
    
    for (int t = low; t <= high; t++) {
        a[t] = aux[t - low];
    }
}

void mergesort(int a[], int low, int high) {
    if (low >= high) {
        return;
    }
    
    int mid = (low + high) / 2;
          
    mergesort(a,low,mid);
    mergesort(a,mid+1,high);
       
    merge(a,low,mid,high);
}

int main() {
    int N;
    int a[maxN];
    
    cout << "Zadaj pocet cisel: ";
    cin >> N;
    cout << "Zadaj " << N << " cisel: ";
    for (int i = 0; i <= N-1; i++) {
        cin >> a[i];
    }
    
    mergesort(a,0,N-1);
    
    cout << "Utriedene cisla:";
    for (int i = 0; i <= N-1; i++) {
        cout << " " << a[i];
    }                        
    cout << endl;
    return 0;
}

Ukážka na príklade

Uvažujme pole a = {6, 1, 5, 7, 2, 4, 8, 9, 3, 0}.

Volanie mergesort(a,0,9) potom utriedi pole a pomocou nasledujúcich rekurzívnych volaní (namiesto mergesort(a,l,h) budeme písať vždy len sort(l,h) a namiesto merge(a,l,m,h) len merge(l,m,h)):

sort(0,9) sort(0,4) sort(0,2) sort(0,1) sort(0,0) 
          .         .         .         sort(1,1)
          .         .         .         merge(0,0,1)
          .         .         sort(2,2)
          .         .         merge(0,1,2)
          .         sort(3,4) sort(3,3)
          .         .         sort(4,4)
          .         .         merge(3,3,4)
          .         merge(0,2,4)
          sort(5,9) sort(5,7) sort(5,6) sort(5,5)
          .         .         .         sort(6,6)
          .         .         .         merge(5,5,6)
          .         .         sort(7,7)
          .         .         merge(5,6,7)
          .         sort(8,9) sort(8,8)
          .         .         sort(9,9)
          .         .         merge(8,8,9)
          .         merge(5,7,9)
          merge(0,4,9)

Pole a sa počas týchto volaní mení nasledovne:

merge(a,0,0,1): |6|1|5 7 2 4 8 9 3 0  -> |1 6|5 7 2 4 8 9 3 0
merge(a,0,1,2): |1 6|5|7 2 4 8 9 3 0  -> |1 5 6|7 2 4 8 9 3 0
merge(a,3,3,4):  1 5 6|7|2|4 8 9 3 0  ->  1 5 6|2 7|4 8 9 3 0
merge(a,0,2,4): |1 5 6|2 7|4 8 9 3 0  -> |1 2 5 6 7|4 8 9 3 0
merge(a,5,5,6):  1 2 5 6 7|4|8|9 3 0  ->  1 2 5 6 7|4 8|9 3 0
merge(a,5,6,7):  1 2 5 6 7|4 8|9|3 0  ->  1 2 5 6 7|4 8 9|3 0
merge(a,8,8,9):  1 2 5 6 7 4 8 9|3|0| ->  1 2 5 6 7 4 8 9|0 3|
merge(a,5,7,9):  1 2 5 6 7|4 8 9|0 3| ->  1 2 5 6 7|0 3 4 8 9|
merge(a,0,4,9): |1 2 5 6 7|0 3 4 8 9| -> |0 1 2 3 4 5 6 7 8 9|

Quick Sort

Quick Sort je tiež založený na metóde rozdeľuj a panuj. Postupuje ale nasledovne:

  • V rámci fázy rozdeľuj vyberie niektorý prvok poľa (napríklad jeho prvý prvok), ktorý nazve pivotom. Prvky poľa následne preusporiada tak, aby ho bolo možné rozdeliť na tri po sebe idúce skupiny: prvky menšie ako pivot, pivot samotný a prvky väčšie alebo rovné ako pivot.
  • Rekurzívne utriedi prvú a tretiu skupinu, čím v poli vzniknú tri utriedené podpostupnosti – utriedená podpostupnosť prvkov menších ako pivot, jednoprvková podpostupnosť pozostávajúca len z pivota a napokon utriedená podpostupnosť prvkov väčších ako pivot.
  • Vo fáze panuj už potom nemusí robiť nič – po utriedení spomínaných troch podpostupností totiž zjavne vznikne utriedené pole.

Základ triedenia tak bude vyzerať nasledovne (pričom zostáva doimplementovať funkciu partition):

void quicksort(int a[], int low, int high) {
/* Utriedi cast pola a[] od indexu low po index high (vratane) */

    /* Osetri trivialne pripady: */ 
    if (low >= high) {
        return; 
    }
    
    /* Rozdel pole na tri podpostupnosti: */ 
    int mid = partition(a, low, high); 
    // Po vykonani funkcie: a[low..mid-1] su mensie ako pivot, a[mid] je pivot, a[mid+1..high] su vacsie ako pivot
    
    /* Rekurzivne utried podpostupnosti a[low..mid-1], a[mid+1..high]: */    
    quicksort(a, low, mid-1);  
    quicksort(a, mid+1, high);   
}

Funkcia partition

Najmenej triviálnym krokom tu je rozdelenie poľa, ktoré realizuje funkcia partition. Tá na vstupe dostane pole a spolu s hraničnými indexmi low a high. Následne prvok a[low] vyberie ako pivot (rovnako dobre by sa však dal vziať aj iný prvok) a postupnosť a[low..high] preusporiada tak, aby pre nejakú hodnotu mid takú, že low <= mid <= high platilo nasledovné:

  • Prvky a[low],...,a[mid-1] sú menšie, než pivot.
  • Prvok a[mid] je pivot.
  • Prvky a[mid+1],...,a[high] sú väčšie alebo rovné ako pivot.

Hodnotu mid potom funkcia partition vráti ako svoj výstup.

Až do záverečnej fázy svojho vykonávania si funkcia partition bude udržiavať nasledujúce invarianty:

  • Prvok a[low] je pivot.
  • Prvky a[low+1],...,a[lastSmaller] sú menšie ako pivot.
  • Prvky a[lastSmaller+1],...,a[unknown-1] sú väčšie alebo rovné ako pivot.
  • Prvky a[unknown],...,a[high] sa ešte s pivotom neporovnávali.

Funkcia partition pritom zakaždým bude porovnávať prvok a[unknown] s pivotom:

  • Ak je menší ako pivot, je nutné „presunúť ho doľava”; vymení ho teda s a[lastSmaller+1] a hodnotu lastSmaller zvýši o jedna.
  • Ak je väčší alebo rovný ako pivot, môže ostať na svojom mieste.

Následne zvýši index unknown o jedna a tento postup opakuje, až kým prejde cez všetky prvky danej časti poľa.

Nakoniec je ešte nutné vymeniť a[low] s a[lastSmaller], čím sa pivot dostane na svoje miesto.

void swap (int &x, int &y) {
    int tmp = x;
    x = y;
    y = tmp;
}

int partition(int a[], int low, int high) {
    int pivot = a[low];     // Ak za pivot chceme zvolit iny prvok, vymenime ho najprv s a[low]
    int lastSmaller = low;
    
    for (int unknown = low + 1; unknown <= high; unknown++) {
        if (a[unknown] < pivot) {
            lastSmaller++;
            swap(a[unknown], a[lastSmaller]);
        }
    }   
    swap(a[low],a[lastSmaller]); 
    return lastSmaller;
}

Výsledný program

#include <iostream>
using namespace std;

const int maxN = 1000;

void swap (int &x, int &y) {
    int tmp = x;
    x = y;
    y = tmp;
}

int partition(int a[], int low, int high) {
    int pivot = a[low];
    int lastSmaller = low;
    
    for (int unknown = low + 1; unknown <= high; unknown++) {
        if (a[unknown] < pivot) {
            lastSmaller++;
            swap(a[unknown], a[lastSmaller]);
        }
    }   
    swap(a[low],a[lastSmaller]); 
    return lastSmaller;
}

void quicksort(int a[], int low, int high) {
    if (low >= high) {
        return; 
    }
    
    int mid = partition(a, low, high);
        
    quicksort(a, low, mid-1);  
    quicksort(a, mid+1, high);   
}

int main() {
    int N;
    int a[maxN];
    
    cout << "Zadaj pocet cisel: ";
    cin >> N;
    cout << "Zadaj " << N << " cisel: ";
    for (int i = 0; i <= N-1; i++) {
        cin >> a[i];
    }
    
    quicksort(a,0,N-1);
    
    cout << "Utriedene cisla:";
    for (int i = 0; i <= N-1; i++) {
        cout << " " << a[i];
    }                        
    cout << endl;
    return 0;
}

Ukážka na príklade

Opäť uvažujme pole a = {6, 1, 5, 7, 2, 4, 8, 9, 3, 0}.

Volanie quicksort(0,9) potom utriedi pole a pomocou nasledujúcich rekurzívnych volaní (namiesto quicksort(a,l,h) píšeme zakaždým len sort(l,h)):

sort(0,9) sort(0,5) sort(0,-1)
          .         sort(1,5) sort(1,0)
          .         .         sort(2,5) sort(2,4) sort(2,2)
          .         .         .         .         sort(4,4)
          .         .         .         sort(6,5)
          sort(7,9) sort(7,8) sort(7,7)
                    .         sort(9,8)
                    sort(10,9)

Volania funkcie partition sú počas tohto behu nasledovné:

partition(a,0,9): |6 1 5 7 2 4 8 9 3 0| -> |0 1 5 2 4 3|6|9 7 8|
partition(a,0,5): |0 1 5 2 4 3|6 9 7 8  -> |0|1 5 2 4 3|6 9 7 8
partition(a,1,5):  0|1 5 2 4 3|6 9 7 8  ->  0|1|5 2 4 3|6 9 7 8
partition(a,2,5):  0 1|5 2 4 3|6 9 7 8  ->  0 1|3 2 4|5|6 9 7 8
partition(a,2,4):  0 1|3 2 4|5 6 9 7 8  ->  0 1|2|3|4|5 6 9 7 8
partition(a,7,9):  0 1 2 3 4 5 6|9 7 8| ->  0 1 2 3 4 5 6|8 7|9|
partition(a,7,8):  0 1 2 3 4 5 6|8 7|9  ->  0 1 2 3 4 5 6|7|8|9

Cvičenie:

  • Ako sa bude Quick Sort správať, keď na vstupe dostane už utriedené pole?
  • Ako sa bude správať, keď na vstupe dostane zostupne utriedené pole?

Iná implementácia

Občas sa možno stretnúť aj s nasledujúcou implementáciou triedenia Quick Sort. Skúste samostatne odôvodniť jej správnosť.

void quicksort(int a[], int low, int high) {
    if (low >= high) {
        return;
    }
    
    /* partition */
    int pivot = a[(low + high)/2];
    int i = low;
    int j = high;
    
    while (i <= j) {
        while (a[i] < pivot) i++;
        while (a[j] > pivot) j--;
        if (i <= j) {
            swap(a[i],a[j]);
            i++; j--;
        }
    }
    
    /* rekurzia */    
    quicksort(a, low, j);
    quicksort(a, i, high);   
}

Triediace algoritmy: zhrnutie

Jednoduché triedenia: Bubble Sort, Insertion Sort, Max Sort.

  • Jednoduché, ale pomalé: zložitosť Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle O(n^2)} .

Rekurzívne triedenia založené na technike rozdeľuj a panuj.

  • Rýchlejšie, zložitejšie.
  • Merge Sort: zložitosť Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle O(n\log n)} .
  • Quick Sort: zložitosť Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle O(n^2)} v najhoršom prípade, pre väčšinu vstupov Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle O(n\log n)} , väčšinou rýchlejší ako Merge Sort.

Reálnu rýchlosť triedení na náhodne zvolenom veľkom vstupe možno porovnať napríklad nasledujúcim programom:

#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

const int maxN = 100000;

void insertionsort(int a[], int n) {
    for (int i = 1; i < n; i++) {
        int prvok = a[i];
        int kam = i;
        while (kam > 0 && a[kam - 1] > prvok) {
            a[kam] = a[kam - 1];
            kam--;
        }
        a[kam] = prvok;
    }
}

void merge(int a[], int low, int mid, int high) {
    int aux[maxN];
    int i = low;
    int j = mid+1;
    int k = 0;
    
    while ((i <= mid) && (j <= high)) {
        if (a[i] <= a[j]) {
            aux[k] = a[i];
            i++;
            k++;
        } else {
            aux[k] = a[j];
            j++;
            k++;
        }
    }
    
    while (i <= mid) {
        aux[k] = a[i];
        i++;
        k++;
    }
    
    while (j <= high) {
        aux[k] = a[j];
        j++;
        k++;
    }
    
    for (int k = low; k <= high; k++) {
        a[k] = aux[k - low];
    }
}

void mergesort(int a[], int low, int high) {
    if (low >= high) {
        return;
    }
    
    int mid = (low + high) / 2;
          
    mergesort(a,low,mid);
    mergesort(a,mid+1,high);
       
    merge(a,low,mid,high);
}

void swap (int &x, int &y) {
    int tmp = x;
    x = y;
    y = tmp;
}

int partition(int a[], int low, int high) {
    int pivot = a[low];
    int lastSmaller = low;
    
    for (int unknown = low + 1; unknown <= high; unknown++) {
        if (a[unknown] < pivot) {
            lastSmaller++;
            swap(a[unknown], a[lastSmaller]);
        }
    }   
    swap(a[low],a[lastSmaller]); 
    return lastSmaller;
}

void quicksort(int a[], int low, int high) {
    if (low >= high) {
        return;
    }
    
    int mid = partition(a, low, high);
        
    quicksort(a, low, mid-1);
    quicksort(a, mid+1, high);   
} 

int main() {
    int N;
    int a1[maxN];
    int a2[maxN];
    int a3[maxN];
    
    cout << "Zadaj pocet nahodnych cisel v poli: ";
    cin >> N;
    
    srand(time(NULL));
    for (int i = 0; i <= N-1; i++) {
        a1[i] = rand() % 1000;
        a3[i] = a2[i] = a1[i];
    }
    
    clock_t start1, end1, start2, end2, start3, end3;
    
    start1 = clock();
    insertionsort(a1, N);   
    end1 = clock();
    
    start2 = clock();
    mergesort(a2,0,N-1);
    end2 = clock();
    
    start3 = clock();
    quicksort(a3,0,N-1);
    end3 = clock();
    
    cout << "Insertion sort: " << (end1 - start1) * 1.0 / CLOCKS_PER_SEC << " CPU sekund" << endl;   
    cout << "Merge sort: " << (end2 - start2) * 1.0 / CLOCKS_PER_SEC << " CPU sekund" << endl;
    cout << "Quick sort: " << (end3 - start3) * 1.0 / CLOCKS_PER_SEC << " CPU sekund" << endl;       
    
    return 0;
}

Pre N = 100000 môžeme dostať napríklad nasledujúci výstup (líši sa od počítača k počítaču a od volania k volaniu):

Insertion sort: 7.474 CPU sekund
Merge sort: 0.076 CPU sekund
Quick sort: 0.032 CPU sekund

Prednáška 12

Oznamy

  • Bolo zverejnené zadanie tretej domácej úlohy, ktorú treba odovzdať do piatka 23. novembra, 22:00.
  • Body z prvej opravnej písomky sú na testovači.
  • Druhá písomka bude v stredu 21. novembra o 18:10 v posluchárni B.

Zjednodušený model pamäte

Pamäť počítača si možno veľmi zjednodušene predstaviť ako konečnú postupnosť pamäťových miest, kde každé pamäťové miesto má kapacitu práve jeden byte. To možno znázorniť nasledujúcim obrázkom.

Pamat1.png

Každé pamäťové miesto má pridelené svoju adresu. Presný formát adries závisí od konkrétnej architektúry (môže napríklad ísť o 32-bitové alebo 64-bitové čísla), z pohľadu programátora však zvyčajne nie je dôležitý.

Premenným základných typov (ako napríklad int, char, double,...) sú počas vykonávania programu pridelené súvislé úseky pamäťových miest. Napríklad na architektúre s 32-bitovým – čiže 4-bytovým – typom int sa premenným tohto typu priraďujú úseky štyroch po sebe idúcich pamäťových miest. Adresou premennej potom rozumieme adresu prvého z týchto pamäťových miest. Táto situácia je znázornená na obrázku nižšie.

Pamat2.png

Poliam sa taktiež prideľujú súvislé úseky pamäte. Pre pole dĺžky N pozostávajúce z prvkov typu T sa pritom vyhradí N po sebe idúcich pamäťových úsekov, kde každý z nich postačuje práve na uchovanie jednej hodnoty typu T. Napríklad poľu a dĺžky N prvkov typu int sa na architektúre s 32-bitovým (4-bytovým) typom int pridelí súvislý úsek 4*N pamäťových miest tak, ako na nasledujúcom obrázku.

Pamat3.png

Smerníky

Smerník (niekde tiež ukazovateľ, angl. pointer) je premenná, ktorej hodnotou je pamäťová adresa. Napríklad na nasledujúcom obrázku je na adrese [adresa 2] uložená premenná typu int, ktorej hodnota je 12345. Na adrese [adresa 1] je uložený smerník, ktorého hodnota je [adresa 2] – hovoríme, že smerník ukazuje na pamäťovú adresu [adresa 2].

Pamat5.png

Definovanie smerníka

V C++ sa smerník ukazujúci na „pamäťový objekt” typu T definuje takto:

T *p;

Napríklad teda môžeme písať:

int *p1;    // smernik p1 na int
char *p2;   // smernik p2 na char
double *p3; // smernik p3 na double

// ...

Smerníky ukazujúce na „pamäťové objekty” rôznych typov sú teda takisto rôznych typov. Vo všeobecnosti nemožno realizovať priradenia medzi smerníkmi rôznych typov.

int *p1;
int *p2;
double *p3;

p1 = p2;   // korektne priradenie
p3 = p1;   // chyba, lebo p3 a p1 su smerniky roznych typov

Bez ohľadu na typ smerníka je jeho hodnotou vždy len pamäťová adresa. Smerníky rôznych typov sa však, ako onedlho uvidíme, v určitých situáciách môžu „správať” rozdielne.

Operátor & (adresa)

Operátor & poskytuje azda najjednoduchší spôsob, ako získať zmysluplný smerník. Adresu premennej x nejakého základného typu (ako napríklad int, char,...) získame tak, že napíšeme &x – túto adresu potom možno priradiť do smerníka.

Nasledujúci program vytvorí celočíselnú premennú n, ktorej adresu priradí do smerníka p.

int main() {
    int n = 12345;
    int *p;
    p = &n;
    return 0;
}

Stav pamäte tesne pred skončením vykonávania tohto programu je znázornený na nasledujúcom obrázku.

Pamat4.png

Rovnakým spôsobom možno operátor & aplikovať aj na prvky poľa (t.j. napríklad &a[2] je adresa druhého – resp. tretieho – prvku poľa a).

Operátor & nemožno aplikovať na konštanty (ako napríklad 3.14 alebo 'c'), ani na výrazy (ako napríklad n+2).

    int n = 0;
    int a[5] = {1, 2, 3, 4, 5};
    int *p;

    p = &n;     // korektne priradenie
    p = &a[2];  // korektne priradenie
    p = &(n+1); // chyba (vyraz nema adresu)
    p = &42;    // chyba (konstanta 42 nema adresu)
    p = &a;     // chyba (vysvetlime neskor)

Operátor * (dereferencia)

Kľúčovým operátorom na smerníkoch je tzv. dereferencia, ktorá sa realizuje operátorom *. Ak p je smerník, možno pomocou zápisu *p pristúpiť k údajom na adrese reprezentovanej smerníkom p. Tieto údaje potom možno aj meniť, t. j. *p môže vystupovať napríklad aj na ľavej strane priradenia. To demonštrujeme na drobnom rozšírení predchádzajúceho príkladu.

#include <iostream>
using namespace std;

int main() {
    int n = 12345;
    int *p;       
    p = &n;              // smernik p odteraz ukazuje na adresu premennej n
    cout << *p << endl;  // vypise hodnotu uchovavanu na adrese p == &n, t. j. 12345
    *p = 9;              // hodnota uchovavana na adrese p == &n sa zmeni na 9; preto uz aj n == 9 
    cout << n << endl;   // vypise hodnotu premennej n, t. j. 9
    (*p)++;              // hodnota uchovavana na adrese p == &n sa zmeni na 10; preto uz aj n == 10 
    cout << n << endl;   // vypise hodnotu premennej n, t. j. 10
    n = 42;
    cout << *p << endl;  // vypise hodnotu uchovavanu na adrese p == &n, t. j. 42
    return 0;
}

Operátor dereferencie vysvetľuje aj spôsob, ktorým sa smerníky deklarujú. Riadok

int *p;

deklarujúci smerník na int totiž treba chápať takto: ak vezmeme smerník p a aplikujeme na neho operátor *, získame hodnotu typu int. Takáto interpretácia sa ukáže byť veľmi užitočnou pri komplikovanejších deklaráciách so smerníkmi.

Smerník NULL

Dôležitým špeciálnym prípadom smerníka je konštanta NULL reprezentujúca smerník, ktorý nikam neukazuje.

  • Je definovaná vo viacerých štandardných knižniciach, ako napríklad cstdlib alebo iostream.
  • Možno ju priradiť do smerníka ľubovoľného typu.

Smerník ako parameter funkcie

V „čistom C” okrem iného nie je možné predávať parametre funkcií referenciou. Rovnaký efekt však možno docieliť predávaním hodnotou tak, že sa ako hodnota pošle smerník. To demonštrujeme pomocou nasledujúcej „smerníkovej” verzie funkcie realizujúcej výmenu hodnôt dvoch premenných.

#include <iostream>
using namespace std;

void swap(int *p_x, int *p_y) {                      // parametre su smerniky (adresy v pamati)
    int tmp = *p_x;                                  // hodnotu na adrese p_x ulozime do tmp
    *p_x = *p_y;                                     // hodnotu na adrese p_x zmenime na hodnotu na adrese p_y
    *p_y = tmp;                                      // hodnotu na adrese p_y zmenime na tmp
}

int main() {
    int x,y;
    cout << "Zadaj x,y: ";
    cin >> x >> y;
    swap(&x,&y);                                     // ako parametre posleme adresy premennych x,y
    cout << "x = " << x << ", y = " << y << endl;
    return 0;
}

Poznať uvedenú alternatívu k predávaniu parametrov referenciou môže byť užitočné aj pri práci v C++, keďže ju často využívajú rôzne knižničné funkcie.

Dynamická alokácia a dealokácia pamäte

Doteraz sme videli:

  • Globálne premenné, ktoré majú vopred známu veľkosť a vyhradenú pamäť.
  • Lokálne premenné, ktoré majú vopred známu veľkosť, ale pamäť sa im prideľuje až pri volaní funkcie na tzv. zásobníku volaní funkcií (angl. call stack).

Program si ale počas behu môže podľa potreby vyhradiť aj ďalšiu pamäť:

  • Používa sa na to operátor new.
  • Pamäť sa vyhradí v oblasti zvanej halda (angl. heap).
  • Nepotrebnú pamäť vyhradenú takýmto spôsobom je dobrým zvykom uvoľniť pomocou operátora delete.
#include <iostream>
using namespace std;

int main() {
    int *p;        
    p = new int;   // new int vyhradi usek pamate postacujuci na uchovanie prave jednej hodnoty typu int
                   // adresa tohto novovytvoreneho useku pamate sa ulozi do smernika p   
    *p = 50;       // do alokovanej pamate sa ulozi hodnota 50 
    cout << *p << endl;  
    delete p;      // uvolnenie alokovanej pamate
    return 0;
}

Smerníky a polia

Smerníky a polia spolu veľmi úzko súvisia. Operátor [ ] je totiž v prvom rade definovaný na smerníkoch. Nech p je smerník definovaný ako

T *p;

kde T označuje nejaký typ. V takom prípade:

  • Zápis p[0] vyjadruje to isté ako *p – ide o hodnotu typu T uloženú na adrese reprezentovanej smerníkom p.
  • Zápis p[i] vyjadruje hodnotu typu T uloženú na i-tom pamäťovom úseku (o veľkosti postačujúcej práve na uloženie hodnoty typu T) za úsekom s adresou reprezentovanou smerníkom p.

Táto situácia je znázornená na nasledujúcom obrázku.

Pamat7.png

Pole prvkov typu T definované ako

T a[N];

je potom iba konštantný smerník na prvok typu T (prvý – t. j. vlastne nultý – prvok poľa). Konštantným je preto, lebo adresa prvého prvku takto definovaného poľa je počas behu programu fixná a nemôže sa meniť. (To okrem iného vysvetľuje, prečo pre pole a nemožno písať &a – pole a totiž nie je „premenná v pravom slova zmysle”, ale iba konštanta smerníkového typu.)

Pamat6.png

Každé pole je teda zároveň aj smerníkom na svoj prvý prvok. Korektný je tak napríklad aj nasledujúci program.

#include <iostream>
using namespace std;

const int maxN = 1000;

void vypisPole(int a[], int pokial) {
    for (int i = 0; i <= pokial; i++) {
        cout << a[i] << " ";
    }
    cout << endl;
}

void vypisPoleOdzadu(int *a, int odkial) {
    for (int i = odkial; i >= 0; i--) {
        cout << a[i] << " ";
    }
    cout << endl;
}

int main() {
    int N;
    int a[maxN];
    int *b;
    cout << "Zadaj pocet cisel: ";
    cin >> N;
    for (int i = 0; i <= N-1; i++) {
        cout << "Zadaj cislo " << i+1 << ": ";
        cin >> a[i];
    }
    
    vypisPole(a,N-1); 
    b = a;
    vypisPole(b,N-1);
    
    vypisPoleOdzadu(a,N-1);
    
    return 0;
}

Dynamické alokovanie poľa

Operátorom new možno alokovať aj pole zadanej dĺžky N (t. j. súvislý úsek pamäte postačujúci na uchovanie práve N hodnôt daného typu). Napríklad príkaz

int *p = new int[10];

vyhradí pamäť postačujúcu práve na uchovanie desiatich celých čísel – čiže vlastne pole veľkosti 10. Z pozorovaní učinených vyššie vyplýva, že smerník p je potom možné používať úplne rovnakým spôsobom, ako polia:

  • Hodnota p[0] je tá istá ako *p.
  • Hodnota p[i] pre i = 0,...,9 reprezentuje hodnotu i-teho prvku poľa.

Vytvorené a už nepotrebné pole je dobrým zvykom uvoľniť operátorom delete[].

Takúto dynamickú alokáciu polí možno využiť napríklad na vytvorenie poľa o používateľom zadanej veľkosti tak, ako v nasledujúcom ukážkovom programe.

#include <iostream>
using namespace std;

int main() {
    cout << "Zadaj pocet cisel: ";
    int N;
    cin >> N;
    int *a = new int[N];
    
    for (int i = 0; i <= N-1; i++) {
        cout << "Zadaj cislo " << i+1 << ": ";
        cin >> a[i];
    }
    for (int i = N-1; i >= 0; i--) {
        cout << a[i] << " ";
    }
    delete[] a;
    cout << endl;
    return 0;   
}

Poznámka: Niektoré kompilátory (ako napríklad gcc) umožňujú vytvoriť pole o používateľom zadanej veľkosti aj jednoduchším spôsobom, keďže akceptujú deklarácie typu int a[N];, kde N môže byť aj premenná, ktorej hodnota už bola načítaná príkazom cin >> N;. Nemusí to ale fungovať vždy – odporúčané je používať radšej dynamickú alokáciu poľa tak, ako v príklade vyššie.

Aplikácia smerníkov: dynamické polia

Prejdime teraz k prvej „ozajstnej” aplikácii smerníkov.

Asi najvýraznejším nedostatkom polí je ich fixná veľkosť. Počas behu programu totiž môže vzniknúť potreba pridávať ďalšie a ďalšie prvky, ktoré sa postupne do poľa nemusia vojsť. Riešenie tejto situácie nadhodnotením veľkosti poľa nie je ideálne, keďže sa tak plytvá pamäťou.

Ako riešenie tohto problému teraz naprogramujeme tzv. dynamické pole, ktoré mení svoju veľkosť s tým, ako sa doň pridávajú prvky. Základná idea pritom bude nasledovná:

  • Zakaždým, keď sa pole plne naplní, alokujeme preň nový a väčší pamäťový úsek, kam celé pole presunieme. Starý pamäťový úsek z pamäte uvoľníme.
  • Keďže kopírovanie poľa do novoalokovaného pamäťového úseku bude mierne neefektívne (bude potrebné prejsť cez celé pole), budeme sa snažiť vyvarovať toho, aby sme ho museli realizovať zakaždým, keď do poľa pridáme nejaký prvok. Na druhej strane však nechceme alokovať zbytočne veľké úseky pamäte. Rozumným kompromisom sa javí byť zdvojnásobenie veľkosti alokovaného úseku zakaždým, keď sa pole naplní.
  • V štandardných C++ knižniciach je definovaná dátová štruktúra vector, ktorá sa správa podobne. My teraz vo svojej podstate implementujeme zjednodušenú verziu tejto štruktúry.

Pre jednoduchosť napíšeme iba verziu dynamického poľa pre celé čísla (typ int). Analogicky by sme však mohli postupovať aj pre iné typy.

Dynamické pole celých čísel budeme reprezentovať ako štruktúru typu dynArray, ktorá bude pozostávať z nasledujúcich troch zložiek:

  • Zo smerníku p ukazujúceho na prvý prvok poľa (čiže vlastne pole samotné).
  • Z celočíselnej premennej size, v ktorej bude uchovávaná veľkosť alokovanej pamäte pre pole p.
  • Z celočíselnej premennej length, v ktorej bude uchovávaný počet prvkov doposiaľ pridaných do poľa.

Napíšeme potom niekoľko funkcií, pomocou ktorých budeme s dynamickými poľami manipulovať.

  • Funkcia void init(dynArray &a) inicializuje dynamické pole a, pričom mu alokuje nejaký rozumne malý objem pamäte (ten v našej implementácii bude postačovať na uchovanie práve dvoch prvkov typu int).
  • Funkcia void add(dynArray &a, int x) pridá na koniec dynamického poľa a prvok s hodnotou x. V prípade potreby ešte predtým realokuje pamäť.
  • Funkcia int get(dynArray a, int index) vráti prvok dynamického poľa a na pozícii index. V prípade, že index nereprezentuje korektnú pozíciu prvku poľa (teda je menší ako 0 alebo väčší, než a.length - 1), ukončí vykonávanie programu pomocou assert.
  • Funkcia void set(dynArray &a, int index, int x) nastaví prvok dynamického poľa na pozícii index na hodnotou x. Ak index nereprezentuje korektnú pozíciu prvku poľa, ukončí vykonávanie programu pomocou assert.
  • Funkcia int length(dynArray a) vráti počet prvkov doposiaľ uložených do dynamického poľa a.
  • Funkcia void destroy(dynArray &a) zlikviduje dynamické pole a (uvoľní pamäť).

Bez ohľadu na implementáciu samotného dynamického poľa už teda vieme napísať kostru programu, ktorý ho využíva:

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

struct dynArray {
// ...
};

void init(dynArray &a) {
// ...
}

void add(dynArray &a, int x) {
// ...
}

int get(dynArray a, int index) {
// ...
} 

void set(dynArray &a, int index, int x) {
// ...
}

int length(dynArray a) {
// ...
}

void destroy(dynArray &a) {
// ...
}

int main() {
    dynArray a;
    init(a);
    int k;
    cin >> k;
    while (k >= 0) {                 
        add(a,k);                    // pridava prvky do pola, kym su nezaporne
        cin >> k;    
    }
    for (int i = length(a) - 1; i >= 0; i--) {
        cout << get(a,i) << " ";     // vypise prvky pola od konca
    }
    cout << endl;
    set(a,0,42);
    cout << get(a,0) << endl;
    destroy(a);
    return 0;    
}

Môžeme teraz prejsť k samotnej implementácii dynamického poľa:

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

/* Dynamicke pole celych cisel */

struct dynArray {
    int *p;                   // smernik na prvy prvok pola
    int size;                 // velkost alokovaneho pola
    int length;               // pocet prvkov pridanych do pola
};

void init(dynArray &a) {
/* Inicializuje dynamicke pole, pricom na zaciatok pren alokuje pole velkosti 2 */
    a.size = 2;
    a.length = 0;
    a.p = new int[a.size];
}

void add(dynArray &a, int x) {
/* Prida na koniec dynamickeho pola prvok x a v pripade potreby realokuje pole */
    if (a.length == a.size) {                // ak uz sa x do pola nevojde
        a.size *= 2;
        int *newp = new int[a.size];         // alokuje pole dvojnasobnej velkosti
        for (int i = 0; i <= a.length - 1; i++) { 
            newp[i] = a.p[i];                // prekopiruje stare pole do noveho
        }
        delete[] a.p;                        // zmaze stare pole
        a.p = newp;                          // a.p odteraz ukazuje na nove pole
    }
    a.p[a.length] = x;                       // ulozi x na koniec pola
    a.length++;                              // zvysi pocet prvkov ulozenych v poli 
}

int get(dynArray a, int index) {
/* Vrati prvok dynamickeho pola a na pozicii index (ak ide o korektnu poziciu)*/
    assert (index >= 0 && index <= a.length - 1);
    return a.p[index];
} 

void set(dynArray &a, int index, int x) {
/* Nastavi prvok dynamickeho pola a na pozicii index na hodnotu x (ak ide o korektnu poziciu)*/
    assert (index >= 0 && index <= a.length - 1);
    a.p[index] = x;
}

int length(dynArray a) {
/* Vrati pocet prvkov ulozenych v dynamickom poli a */
    return a.length;
}

void destroy(dynArray &a) {
/* Zlikviduje dynamicke pole a (uvolni alokovanu pamat) */
    delete[] a.p;
}

int main() {
    dynArray a;
    init(a);
    int k;
    cin >> k;
    while (k >= 0) {                 
        add(a,k);                    // pridava prvky do pola, kym su nezaporne
        cin >> k;    
    }
    for (int i = length(a) - 1; i >= 0; i--) {
        cout << get(a,i) << " ";     // vypise prvky pola od konca
    }
    cout << endl;
    set(a,0,42);
    cout << get(a,0) << endl;
    destroy(a);
    return 0;    
}

Smerníková aritmetika

Na smerníkoch možno vykonávať určité operácie, súhrn ktorých býva honosne nazývaný smerníkovou aritmetikou. Nech p, p1, p2 sú smerníky definované ako

T *p;
T *p1;
T *p2;

kde T označuje nejaký typ. Nech n je typu int. Potom:

  • p + n označuje smerník na n-tý pamäťový úsek (postačujúci práve na uchovanie hodnoty typu T) za adresou p.
    • Napríklad p+n je to isté ako &p[n] a *(p+n) je to isté ako p[n].
    • p++ je skratkou pre p = p+1, ...
  • p - n označuje smerník na n-tý pamäťový úsek (postačujúci práve na uchovanie hodnoty typu T) pred adresou p.
  • p1 - p2 je celé číslo k také, že p1 == p2 + k. Zmysluplný výsledok možno očakávať len vtedy, keď p1 a p2 sú adresami prvkov v tom istom poli (v jedinom súvislom kuse pamäte).
  • Smerníky potom možno prirodzene porovnávať operátormi ==, <, >, <=, >=, !=. Výsledok je zmysluplný opäť len vtedy, keď p1 a p2 sú adresami prvkov v tom istom poli.

Program, ktorý najprv načíta pole a následne prvky tohto poľa vypíše od konca, tak možno napísať napríklad aj takto:

#include <iostream>
using namespace std;

const int maxN = 1000;

int main() {
    int a[maxN];
    int N;
    cout << "Zadaj pocet cisel: ";
    cin >> N;
    for (int i = 0; i <= N-1; i++) {
        cout << "Zadaj cislo " << i + 1 << ": ";
        cin >> *(a + i);    
    }
    for (int i = N-1; i >= 0; i--) {
        cout << *(a + i) << " ";
    }
    cout << endl;
    return 0;
}

Ladenie programov so smerníkmi

  • Smerníky môžu byť nepríjemným zdrojom chýb, keďže kompilátor nekontroluje, či sú používané správne.
  • Napríklad možno čítať aj zapisovať mimo alokovanej pamäte.
  • S odchytávaním takýchto chýb môžu pomôcť automatizované nástroje, ako napríklad Valgrind (pre Linux) alebo Dr. Memory (pre Windows aj Linux).

Prednáška 13

Oznamy

  • Druhá písomka bude v stredu 21. novembra o 18:10 v posluchárni B.
  • Tretiu domácu úlohu treba odovzdať do piatka 23. novembra, 22:00.

Deklarácie so smerníkmi a poľami

Na dnešnej prednáške budeme kombinovať smerníky a polia – budeme tak písať výrazy obsahujúce súčasne operátor [ ] pre prístup k danému prvku poľa a dereferenčný operátor * pre smerníky. Na správnu interpretáciu takýchto výrazov je potrebné vedieť nasledujúce:

  • Operátor [ ] má vyššiu prioritu ako *. Napríklad zápis *a[10] teda treba chápať ako *(a[10]). To znamená, že vezmeme pole a, pozrieme sa na jeho desiaty prvok a následne na tento desiaty prvok aplikujeme derefernciu. Prvky poľa a sú teda (v prípade, že robíme niečo zmysluplné) smerníky. Naopak (*p)[10] znamená nasledovné: vezmeme p, aplikujeme dereferenciu (p je teda smerník), ktorej výsledkom je pole a pozrieme sa na desiaty prvok tohto poľa.
  • Operátor [ ] je zľava asociatívny a operátor * je sprava asociatívny. To znamená, že napríklad a[2][3] je to isté ako (a[2])[3] a **p je to isté ako *(*p).

Napríklad deklaráciu

int *a[10]; // t. j. *(a[10])

teraz treba chápať takto: ak vezmeme a, pozrieme sa na niektorý z desiatich prvkov tohto poľa a nakoniec aplikujeme dereferenciu, dostaneme hodnotu typu int. Zadeklarovali sme teda desaťprvkové pole smerníkov na int. Rovnako možno intepretovať aj nasledujúce deklarácie:

int **a;         // a je smernik na smernik na int; kazde pole smernikov na int tak mozno priradit do a (ale nie opacne)
int (*a)[10];    // a je smernik na pole desiatich celych cisel
int *(*(a[10])); // a je desatprvkove pole smernikov na smerniky na int
int **a[10];     // to iste, ako na predchadzajucom riadku

Dvojrozmerné polia

Doposiaľ sme pracovali iba s jednorozmernými poľami. Často je však potrebné pracovať s viacrozmernými údajmi – tými sú napríklad tabuľky, matice, atď. Na tejto prednáške sa zameriame na dvojrozmerný prípad, potreba ktorého vyvstáva najčastejšie.

Najjednoduchší – hoci väčšinou nepraktický – spôsob práce s takýmito údajmi poskytuje priamo C++ v podobe takzvaných viacrozmerných polí. Prácu s dvojrozmernými poľami si teraz v stručnosti ukážeme. Ide o jednoduché rozšírenie jednorozmerných polí – akurát namiesto i-teho prvku a[i] pristupujeme k prvkom a[i][j] v i-tom riadku a j-tom stĺpci dvojrozmerného poľa.

#include <iostream>
using namespace std;

int main() {
    /* Vytvorime pole s dvoma riadkami a piatimi stlpcami a rovno ho aj inicializujeme: */ 
    int a[2][5] = {{1,2,3,4,5},{6,7,8,9,10}}; 

    /* Vypiseme pole ako tabulku: */
    for (int i = 0; i <= 1; i++) {
        for (int j = 0; j <= 4; j++) {
            cout << a[i][j] << " ";
        }
        cout << endl;
    }

    return 0;
}

Dvojrozmerné pole o m riadkoch a n stĺpcoch sa v pamäti reprezentuje ako jediný súvislý úsek, v ktorom idú za sebou jednotlivé riadky reprezentované ako jednorozmerné polia. To je znázornené na nasledujúcom obrázku.

Pamat8.png

Z tejto reprezentácie ale vyplýva, že typ dvojrozmerného poľa je inherentne previazaný s dĺžkou jednotlivých riadkov (t. j. s počtom stĺpcov). To sa ukazuje byť nepraktické napríklad pri predávaní dvojrozmerných polí ako parametrov pre funkcie, v hlavičkách ktorých je nutné tento počet stĺpcov zadať:

...

void f1(int a[2][5]) {
    ...
}

void f2(int a[][5]) {
    ...
}

void f3(int (*a)[5]) {
    ...
}

void f4(int a[][]) {   // uz samotny tento zapis vyusti v chybu
    ...
}

int main() {
    int a[2][5];
    
    ...
   
    f1(a); // OK
    f2(a); // OK
    f3(a); // OK
    f4(a); // nefunguje

    ...
}

Budeme preto hľadať pohodlnejší spôsob práce s dvojrozmernými údajmi.

Polia smerníkov

Omnoho pohodlnejšou alternatívou k dvojrozmerným poliam sú polia smerníkov. Dvojrozmerné dáta tu uchovávame prostredníctvom poľa, i-ty prvok ktorého je smerníkom ukazujúcim na prvý – t. j. vlastne nultý – prvok i-teho riadku. Pre každý riadok potom môžeme naalokovať samostatné pole pre jeho prvky. Má to okrem iného tú výhodu, že jednotlivé riadky nemusia byť rovnako dlhé. Zatiaľ si však len ukážme spôsob, ako vytvoriť pole smerníkov reprezentujúce obdĺžnikovú tabuľku typu m krát n; to bude v pamäti reprezentované podobne, ako na nasledujúcom obrázku.

Pamat9.png

Nasledujúci program vytvorí pole smerníkov, ktoré reprezentuje obdĺžnikovú tabuľku o m krát n celých číslach, načíta do nej prvky zo vstupu a nakoniec na výstup vypíše aritmetické priemery hodnôt v jednotlivých jej stĺpcoch.

#include <iostream>
using namespace std;

const int max_m = 200;

int main() {
    int m,n;
    int *a[max_m];
    cout << "Zadaj pocet riadkov: ";
    cin >> m;
    cout << "Zadaj pocet stlpcov: ";
    cin >> n;
    
    /* Alokuj jednotlive riadky: */
    for (int i = 0; i <= m-1; i++) {
        a[i] = new int[n];           // a[i] je smernik na i-ty riadok
    }
    
    /* Nacitanie prvkov tabulky: */
    cout << "Zadaj cisla tabulky:" << endl;
    for (int i = 0; i <= m-1; i++) {
        for (int j = 0; j <= n-1; j++) {
            cin >> a[i][j];          // nacitaj j-ty prvok i-teho riadku 
        }
    }
    
    /* Spocitaj a vypis priemery hodnot v jednotlivych stlpcoch: */
    for (int j = 0; j <= n-1; j++) {
        int sum = 0;
        for (int i = 0; i <= m-1; i++) {
            sum += a[i][j];
        }
        cout << "Priemer hodnot v stlpci " << j + 1 << " je " << (sum * 1.0)/m << endl;
    } 

    /* Uvolnenie pamate: */
    for (int i = 0; i <= m-1; i++) {
        delete[] a[i];
    }

    return 0;
}

Dynamicky alokované polia smerníkov

Aj samotné polia smerníkov je možné alokovať dynamicky, čo umožňuje počas behu nastaviť nielen veľkosť jednotlivých riadkov, ale aj ich počet. Príklad s priemermi jednotlivých stĺpcov tak vieme prepísať napríklad nasledovne:

#include <iostream>
using namespace std;

int main() {
    int m,n;
    int **a;
    cout << "Zadaj pocet riadkov: ";
    cin >> m;
    cout << "Zadaj pocet stlpcov: ";
    cin >> n;
    
    /* Alokuj pole smernikov na riadky: */
    a = new int *[m];
    
    /* Alokuj jednotlive riadky: */
    for (int i = 0; i <= m-1; i++) {
        a[i] = new int[n];           // a[i] je smernik na i-ty riadok
    }
    
    /* Nacitanie prvkov tabulky: */
    cout << "Zadaj cisla tabulky:" << endl;
    for (int i = 0; i <= m-1; i++) {
        for (int j = 0; j <= n-1; j++) {
            cin >> a[i][j];          // nacitaj j-ty prvok i-teho riadku 
        }
    }
    
    /* Spocitaj a vypis priemery hodnot v jednotlivych stlpcoch: */
    for (int j = 0; j <= n-1; j++) {
        int sum = 0;
        for (int i = 0; i <= m-1; i++) {
            sum += a[i][j];
        }
        cout << "Priemer hodnot v stlpci " << j + 1 << " je " << (sum * 1.0)/m << endl;
    } 
    
    /* Uvolnenie pamate: */
    for (int i = 0; i <= m-1; i++) {
        delete[] a[i];
    }
    delete[] a;
    
    return 0;
}

Príklad: výšková mapa

Pokračujme ukážkou o niečo väčšieho programu využívajúceho dynamicky alokované polia smerníkov. Ten bude v obdĺžnikovej tabuľke celých čísel uchovávať výškovú mapu nejakého územia, v ktorom nadmorská výška nadobúda hodnoty medzi 0 a 2000 metrami nad morom.

Program na vstupe najprv dostane dvojicu prirodzených čísel m a n. Výškovou mapou potom bude obdĺžnik pozostávajúci z m krát n štvorčekov, kde každý zo štvorčekov bude mať danú nejakú nadmorskú výšku od 0 po 2000 metrov nad morom (nadmorská výška 0 znamená more a kladná nadmorská výška znamená pevninu). Následne program postupne prečíta zo vstupu nadmorské výšky všetkých štvorčekov.

Takto zadanú mapu program vykreslí pomocou knižnice SVGdraw, pričom každý štvorček dostane určitú farbu podľa svojej nadmorskej výšky. Následne zavolá funkciu najvyssiVrch, ktorá nájde najvyšší bod (resp. jeden z najvyšších bodov) vykresľovaného územia a v mape ho zvýrazní rámikom.

Príklad vstupu a výstupu:

PROG-P12-mapa.png
22 11
 0 0 0 0 0 0 0 0 0 0 0
 0 20 40 60 80 100 120 140 120 0 0
 0 40 80 120 160 200 240 280 190 100 0
 0 60 120 180 240 300 360 420 260 100 0
 0 80 160 240 320 400 480 560 260 100 0
 0 100 200 300 400 500 600 700 330 100 0
 0 120 240 360 480 600 720 840 400 100 0
 0 140 280 420 560 700 840 980 470 100 0
 0 160 320 480 640 800 960 700 200 0 0
 0 180 360 540 720 900 700 500 0 0 0
 0 200 400 600 800 1000 1200 1400 680 100 0
 0 220 440 660 880 1100 1320 1540 750 100 0
 0 240 480 720 960 1200 1440 1680 820 100 0
 0 260 520 780 1040 1300 1560 1820 1200 400 0
 0 280 560 840 1120 1400 1680 1960 1500 600 0
 0 240 480 720 960 1200 1440 1680 1000 400 0
 0 200 400 600 800 1000 1200 1400 680 100 0
 0 160 320 480 640 800 960 1120 540 100 0
 0 120 240 360 480 600 720 840 400 100 0
 0 80 160 240 320 400 480 560 260 100 0
 0 40 80 120 160 200 240 280 120 0 0
 0 0 0 0 0 0 0 0 0 0 0

Samotný program:

#include <iostream>
#include "SVGdraw.h"
using namespace std;

/* velkost stvorceka mapy v pixeloch */
const int stvorcek = 15;

int **vytvorMapu(int m, int n) {
    /* Vytvori a vrati na vystupe mapu (obdlznikovu tabulku) s m riadkami a n stlpcami. */
    int **a;
    a = new int *[m];
    for (int i = 0; i <= m-1; i++) {
        a[i] = new int[n];
    }
    return a;
}

void zmazMapu(int m, int n, int **a) {
    /* Uvolni z pamate mapu s m riadkami a n stlpcami. */
    for (int i = 0; i <= m-1; i++) {
        delete[] a[i];
    }
    delete[] a;
}

void nacitajMapu(int m, int n, int **a) {
    /* Nacita hodnoty (nadmorske vysky) do uz vytvorenej mapy velkosti m krat n. */
    for (int i = 0; i <= m-1; i++) {
        for (int j = 0; j <= n-1; j++) {
            cin >> a[i][j];
        }
    }
}

void farba(SVGdraw &drawing, int r, int g, int b) {
    /* Nastavi farbu ciary aj vyplne na dane hodnoty. */
    drawing.setLineColor(r, g, b);
    drawing.setFillColor(r, g, b);
}

void vykresliMapu(int m, int n, int **a, SVGdraw &drawing) {
    /* Ofarbi jednotlive stvorceky mapy podla ich nadmorskej vysky:
     * modra -- more (nadmorska vyska 0)
     * zelena -- niziny (nadmorska vyska 1,...,200)
     * hneda -- "pohoria" (nadmorska vyska 200,...,2000) */
    for (int i = 0; i <= m-1; i++) {
        for (int j = 0; j <= n-1; j++) {
            /* nastavenie farby podla hodnoty */
            if (a[i][j] == 0) {
                farba(drawing, 0, 0, 255);
            } else if (a[i][j] <= 200) {
                double x = a[i][j] / 200.0;
                farba(drawing, x * 255, 127 + x * 127, 0);
            } else {
                double x = (a[i][j] - 200) / 2000.0;
                farba(drawing, 255 - x * 150, 255 - x * 200, 0);
            }
            /* Vykreslenie stvorceka; POZOR: vymena suradnic */
            drawing.drawRectangle(j * stvorcek, i * stvorcek, stvorcek, stvorcek);
        }
    }
}

void najvyssiVrch(int m, int n, int **a, int &riadok, int &stlpec) {
    /* Najde v mape a o rozmeroch m krat n stvorcek s najvyssou nadmorskou vyskou
       a jeho suradnice ulozi do premennych riadok resp. stlpec. */
    riadok = 0;
    stlpec = 0;
    for (int i = 0; i <= m-1; i++) {
        for (int j = 0; j <= n-1; j++) {
            if (a[i][j] > a[riadok][stlpec]) {
                riadok = i;
                stlpec = j;
            }
        }
    }
}

int main() {
    /* nacitaj rozmery matice */
    int m, n;
    cin >> m >> n;

    /* vytvor a nacitaj maticu */
    int **a = vytvorMapu(m, n);

    nacitajMapu(m, n, a);

    /* zobraz maticu */
    SVGdraw drawing(n * stvorcek, m * stvorcek, "mapa.svg"); // POZOR: vymena suradnic
    vykresliMapu(m, n, a, drawing);

    /* najdi najvyssi vrch a zvyrazni ho stvorcekom */
    int riadok, stlpec;
    najvyssiVrch(m, n, a, riadok, stlpec);

    drawing.setLineColor("black");
    drawing.setLineWidth(3);
    drawing.setNoFill();
    drawing.drawRectangle(stlpec * stvorcek, riadok * stvorcek, stvorcek, stvorcek); // POZOR: vymena suradnic

    /* ukonci vykreslovanie */
    drawing.finish();

    /* uvolni pamat matice */
    zmazMapu(m, n, a);
    
    return 0;
}

Polia reťazcov

Každý reťazec je pole znakov, ktoré možno interpretovať aj ako smerník na char. Pole reťazcov teda možno implementovať ako pole smerníkov na char. Keďže sa vo väčšine aplikácií môžu vyskytovať reťazce rôznych dĺžok, ukazuje sa tu byť užitočná vlastnosť polí smerníkov spomínaná vyššie – jednotlivé ich riadky môžu mať rôzne dĺžky.

Nasledujúci jednoduchý program je ukážkou použitia takto implementovaných polí reťazcov. Zo vstupu postupne načítava riadky, až kým je zadaný prázdny riadok. Tie postupne ukladá do poľa. Na záver sa všetky tieto reťazce vypíšu na výstup, oddelené medzerami.

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

const int maxN = 1000;
const int maxRiadok = 1000;

int main() {
    char *a[maxN];
    char riadok[maxRiadok];
    int N = 0;
    while (N <= maxN-1) {                 
        cin.getline(riadok, maxRiadok);      // nacitame jeden riadok zo vstupu
        if (strcmp(riadok, "") == 0) {       // v pripade prazdneho riadku ukoncime nacitavanie
            break;
        }
        /* 
           Alokujeme pamat pre N-ty retazec pola a.
           (Musi byt o 1 vacsia, nez jeho dlzka -- dovodom je znak \0 na konci).
        */
        a[N] = new char[strlen(riadok) + 1]; 
        strcpy(a[N], riadok);
        N++;
    }
    // Vypiseme jednotlive riadky oddelene medzerami:
    for (int i = 0; i <= N-1; i++) {
        cout << a[i] << " ";
    }
    // Uvolnime pamat:
    for (int i = 0; i <= N-1; i++) {
        delete[] a[i];
    }
    return 0;
}

Cvičenie: prerobte tento program tak, aby namiesto poľa a fixnej veľkosti maxN používal dynamické pole.

Zadávanie argumentov programu z príkazového riadku

Polia reťazcov umožňujú okrem iného aj napísať program, ktorý dostane a spracuje jeden alebo viacero argumentov z príkazového riadku a na základe nich prípadne „upraví svoje správanie”. Príkladom programu využívajúcim túto funkcionalitu je aj samotný kompilátor g++. Jeho najjednoduchšie volanie

g++ program.cpp

obsahuje okrem názvu programu g++ aj argument program.cpp – ten dáva kompilátoru informáciu o tom, ktorý zdrojový súbor má kompilovať.

Na písanie programov umožňujúcich spracovanie takýchto argumentov je potrebné využiť „jemne pokročilejšiu” verziu funkcie main s hlavičkou

int main(int argc, char** argv)

– tú automaticky generuje napríklad aj prostredie NetBeans. Význam parametrov argc a argv je nasledovný:

  • argv je pole reťazcov (resp. pole smerníkov na char) a argc je počet reťazcov v tomto poli.
  • Reťazec argv[0] je vždy názov programu.
  • Reťazce argv[1],...,argv[argc-1] sú jednotlivé argumenty.

Nasledujúci jednoduchý program postupne vypíše všetky argumenty, ktoré dostal z príkazového riadku.

#include <iostream>
using namespace std;

int main(int argc, char **argv) {
    for (int i = 0; i <= argc-1; i++) {
        cout << argv[i] << endl;
    }
    return 0;
}

Prednáška 14

Oznamy

  • Zajtra na cvičení rozcvička z dvojrozmerných polí (pomocou poľa smerníkov)
  • V stredu druhá písomka
    • Materiál po prednášku 12 (polia, reťazce, rekurzia, triedenia, úvod do smerníkov)
    • Opravná až cez skúškové
    • Opäť si môžete priniesť ťahák
  • Tretiu domácu úlohu treba odovzdať do piatka 23. novembra, 22:00.
  • Ak sa program správa inak na vašom počítači ako na testovači, väčšinou je to tým, že niekde pristupujete k neinicializovanej pamäti, alebo k pamäti, ktorú nemáte ani alokovanú
    • Odporúčam naučiť sa pracovať s programom valgrind
    • Záujemcom predvediem na doplnkovom cvičení v stredu

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
  • Ako asi môže byť toto zisťovanie naprogramované?

V matematickej reči

  • máme množinu A a hodnotu x, pýtame sa, či x patrí do A
  • naprogramujeme to ako funkciu contains
  • spravíme aj funkciu add, ktorá administrátorovi umožní pridať do množiny ďalšie identifikačné číslo
  • hodila by sa aj funkcia remove, tú však dnes nebudeme robiť

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

Čo sme videli doteraz

Množina ako pole

  • Prvky množiny dáme do poľa v ľubovoľnom poradí
  • Funkcia contains musí prejsť celé pole lineárnym prehľadávaním
    • Čas výpočtu O(n)
  • Funkcia add pridá prvok na koniec poľa v O(1) čase
    • Ale pozor na prekročenie kapacity poľa (môžeme použiť dynamické pole)
/* štruktúra reprezentujúca množinu pomocou poľa */
struct set {
    int *p;        // pole prvkov 
    int length;    // počet prvkov
};

/* inicializácia prázdnej množiny */
void init(set &a) {
    a.p = new int[MAXLENGTH];  // alokácia poľa
    a.length = 0;         
}

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

void add(set &a, int x) {
    assert(a.length < MAXLENGTH);
    a.p[a.length] = x;
    a.length++;
}

Množina ako utriedené pole

  • Prvky množiny dáme do poľa od najmenšieho po najväčšie
  • Funkcia contains hľadá binárnym vyhľadávaním
    • Čas výpočtu O(log n), oveľa rýchlešie
    • napr. pre milión prvkov potrebujeme porovnať x s asi 20 číslami z poľa
  • Funkcia add musí vložiť prvok na správne miesto v utriedenom poradí
bool contains(set &a, int x) {
    int left = 0, right = a.length - 1;
    while (left <= right) {
        int index = (left + right) / 2;
        if (a.p[index] == x) {
            return true;
        }
        else if (a.p[index] < x) {
            left = index + 1;
        }
        else {
            right = index - 1;
        }
    }
    return false;
}

void add(set &a, int x) {
    int kam = a.length;
    while (kam > 0 && a.p[kam - 1] > x) {
        a.p[kam] = a.p[kam - 1];
        kam--;
    }
    a.p[kam] = x;
    a.length++;
}

Plán na dnes

  • 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 hešovania
    • Často veľmi rýchle vyhľadávanie
    • Použijeme polia aj spájané zoznamy

Spájané zoznamy

Spájaný zoznam (linked list) je postupnosť uzlov rovnakého typu usporiadaných za sebou.

Každý uzol pozostáva z dvoch častí:

  • samotné dáta, v našom prípade jedno identifikačné číslo typu int
  • smerník, ktorý ukazuje na nasledovníka a pomáha nám pohybovať sa po zozname

Uzol spájaného zoznamu zapíšeme ako struct:

/* štruktúra reprezentujúca uzol spájaného zoznamu */
struct node {
    int data;      // prvok uložený v uzle
    node* next;  // smerník na další uzol
};

Všimnite si, že vo vnútri definície typu node používame smerník na node.

Na rozdiel od poľa, v ktorom je poradie stanovené indexmi, je poradie v spájanom zozname určované ukazovateľmi next. Ak x je uzol zoznamu, tak x.next je

  • nasledujúci uzol zoznamu
  • NULL (ak x je posledný uzol zoznamu)

PROG-list.png

V programe budeme množinu reprezentovať premenou first, čo je smerník na prvý uzol zoznamu

  • ak je zoznam prázdny, first bude NULL
/* štruktúra reprezentujúca množinu spájaným zoznamom */
struct set {
    node *first;  // smerník na prvý uzol v zozname
};

void init(set &a) {
    a.first = NULL;  // inicializácia prázdnej množiny
}

Odbočka: práca so smerníkmi na struct

Ak smerník p ukazuje na uzol zoznamu (má typ node *) a chceme vypísať dáta v tomto uzle, môžeme použiť tieto možnosti:

cout << (*p).data;
cout << p->data;
  • p->data je teda skratka za (*p).data

Vkladanie na začiatok zoznamu

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

void add(set &a, int x) {
    node* p = new node;  // vytvoríme nový uzol
    p->data = x;         // naplníme dáta
    p->next = a.first;   // za novým uzol pojde doterajší prvý
    a.first = p;         // tento uzol je novým začiatkom
}

Použitie v programe na načítanie čísel do zoznamu

  • načítavame nezáporné čísla, zastaneme na prvom zápornom
int main(void) {
   set a;
   init(i);
   while(true) {
      int x;
      cin >> x;
      if(x < 0) break;
      add(a, x);
   }
}

Pozor, čísla budú v zozname odzadu, posledné na začiatku

Výpis, vyhľadávanie, uvoľnenie zoznamu

Ukážeme si tri podobné funkcie, ktoré prejdú všetky prvky zoznamu

Prvá funkcia vypíše všetky čísla v zozname

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

Druhá funkcia hľadá, či je číslo x v zozname (ako čítačka kariet)

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

Tretia funkcia uvoľní pamäť spájaného zoznamu, keď ho už nepotrebujeme

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

Spájané zoznamy, zhrnutie

  • Výhoda je, že nepotrebujeme poznať veľkosť, rastie podľa potreby
  • Ľahko sa pridáva na začiatok zoznamu ďalšie číslo
  • Ľahko sa tiež prejde celý zoznam
  • Nevieme však ľahko skočiť do stredu zoznamu, musíme pochodovať po smerníkoch od začiatku
  • Funkcia contains funguje podobne ako na neutriedenom poli, čas O(n)

Existujú aj rôzne varianty spájaných zoznamov

  • kruhové zoznamy, kde posledný prvok ukazuje na prvý
  • obojsmerne spájané zoznamy, kde si pamätáme nasledovníka aj predchodcu, vieme sa hýbať oboma smermi

Implementácia množiny priamym adresovaním

  • Ak sú prvky množiny čísla z {0,1,...,m-1}, môžeme množinu reprezentovať ako pole boolovských hodnôt dĺžky m (viď pole bolo používané pri generovaní variácií bez opakovania)
  • Tento prístup sa volá priame adresovanie (direct addressing)
  • Funkcie contains aj add sú veľmi rýchle
/* štruktúra reprezentujúca množinu priamym adresovaním */
struct set {
  bool *p;
};

void init(set &a) {
   a.p = new bool[MAXVALUE];
   for(int i=0; i<MAXVALUE; i++) {
     a.p[i] = false;
   }
}

bool contains(set &a, int x) {
  return a.p[x];
}

void add(set &a, int x) {
  a.p[x] = true;
}

Jednoduché hešovanie

Priame adresovanie sa príliš nehodí, ak prvky môžu byť veľmi veľké, lebo potrebuje veľa pamäte.

Hešovanie je jednoduchá finta, ktorá funguje nasledovne:

  • vytvoríme si hešovaciu tabuľku: pole veľkosti m rovnakého typu ako prvky množiny, napr. int
  • nech K je množina všetkých možných prvkov množiny (kľúčov)
  • Naprogramujeme hešovaciu funkciu, ktorá transformuje prvky množiny K na indexy poľa, teda pôjde o funkciu h : K -> {0 , 1 , ... , m−1}.
  • Hešovacia funkcia by mala byť jednoduchá a rýchla, ale pritom by nemala prideľovať často rovnaké indexy (mala by kľúče do tabuľky distribuovať rovnomerne).
  • Najjednoduchšia hešovacia funkcia je h(x) = x mod m (je dobré, ak v tomto prípade m je prvočíslo a nie je blízko mocniny 2).
    • pozor, -10 % 3 je -1, takže radšej použijeme absolútnu hodnotu z x (funkcia abs z knižnice cstdlib)
int hash(int x, int m) {
    return abs(x) % m;
}

Vkladanie

  • spočítame miesto hash(x, m) a prvok tam vložíme

Vyhľadávanie

  • ak prvok s kľúčom x je v tabuľke, musí byť na mieste hash(x, m).
  • skontrolujeme toto miesto a ak tam je niečo iné ako x, x sa v tabuľke nenachádza

Problémy:

  • na akú hodnotu inicializovať pole a?
  • čo ak chceme vložiť prvok na miesto, kde už je niečo uložené?

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. Existuje viacero spôsobov, ako ju riešiť.

  • Budeme aj tak vkladať na toto miesto, len tam nebude iba jeden prvok, ale potenciálne viac prvkov v spájanom zozname
  • Budeme hľadať inú voľnú pozíciu v tabuľke
    • napr. postupným prezeraním nasledovných políčok
    • o tomto sa dozviete viac o rok na predmete ADŠ

Riešenie kolízií pomocou spájaných zoznamov

Namiesto prvkov typu int budeme mať v každom políčku hešovacej tabuľky spájaný zoznam so všetkými prvkami množiny, ktoré hešovacia funkcia priradila na toto políčko.

/* štruktúra reprezentujúca uzol spájaného zoznamu */
struct node {
    int data;      // prvok uložený v uzle
    node* next;  // smerník na další uzol
};

/* štruktúra reprezentujúca množinu spájaným zoznamom */
struct set {
    node** lists;   // pole smerníkova na prvé prvky zoznamov
    int m;        // dlžka poľa lists
};


Vyhľadávanie pracuje na spájanom zozname, ktorý sa nachádza na správnom mieste hešovacej tabuľky.

  • Správny index dostaneme pomocou hash(x, a.m), kde x je hľadaný kľúč a a.m je veľkosť tabuľky a.lists.
bool contains(set &a, int x) {
    int index = hash(x, a.m);
    node* p = a.lists[index];
    while (p != NULL) {
        if (p->data == x) return true;
        p = p->next;
    }
    return false;
}

Pri vkladaní stačí nový prvok pridať na začiatok spájaného zoznamu na správnom mieste tabuľky

void add(set &a, int x) {
    int index = hash(x, a.m);
    node* temp = new node;
    temp->data = x;
    temp->next = a.lists[index];
    a.lists[index] = temp;
}

Ďalšie funkcie

  • Vymazávanie prebieha na spájanom zozname, ktorý je na mieste tabuľky, kam nás nasmeroval zahešovaný kľúč.
  • Inicializácia vytvorí tabuľku data a nastaví všetky smerníky v nej na NULL.

Príklad: 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?

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.
  • V najhoršom prípade sa všetky prvky zahešujú do toho istého políčka, teda máme add O(1) a contains O(n), kde n je počet prvkov množiny.
  • Ak máme šťastie a v každom políčku máme len niekoľko málo (konštantný počet) prvkov, budeme mať add aj contains O(1).
    • Ak je tabuľka dosť veľká a hešovacia funkcia vhodne zvolená, tento príklad je pomerne obvyklý.
    • Hešovacie tabuľky sa často používajú v praxi.
  • Viac budúci rok na ADŠ.

Programy s implementáciami množiny

  • Množina implementovaná ako netriedené pole, triedené pole, spájaný zoznam, priame adresovanie a hešovacia tabuľka
  • Všetky programy majú presne rovnakú funkciu main, lebo hlavičky funkcii add a contains sa nelíšia, líši sa len definícia typu set
  • Funkcia main načítava a vykonáva príkazy add a contains až kým nenačíta príkaz end.

Príklad vstupu:

add 5
add 1
add 3
add 7
contains 1
contains 2
contains 7
end

Program: množina ako neutriedené pole

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

const int MAXLENGTH = 100;

/* štruktúra reprezentujúca množinu pomocou poľa */
struct set {
    int *p;        // pole prvkov
    int length;    // počet prvkov
};

/* inicializácia prázdnej množiny */
void init(set &a) {
    a.p = new int[MAXLENGTH];  // alokácia poľa
    a.length = 0;
}

/* test, či prvok x patrí do množiny a */
bool contains(set &a, int x) {
    for (int i = 0; i < a.length; i++) {
        if (a.p[i] == x) return true;
    }
    return false;
}

/* pridanie prvku x do množiny a */
void add(set &a, int x) {
    assert(a.length < MAXLENGTH);
    a.p[a.length] = x;
    a.length++;
}

int main(void) {
    set a;
    init(a);
    /* načítame a vykonávame príkazy */
    while (true) {
        char prikaz[MAXLENGTH];
        cin >> prikaz;
        if (strcmp(prikaz, "add") == 0) {
            int x;
            cin >> x;
            add(a, x);
        } else if (strcmp(prikaz, "contains") == 0) {
            int x;
            cin >> x;
            cout << contains(a, x) << endl;
        } else if (strcmp(prikaz, "end") == 0) {
            break;
        }
    }
}

Program: množina ako utriedené pole

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


Program: množina ako spájaný zoznam

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

const int MAXLENGTH = 100;

/* štruktúra reprezentujúca uzol spájaného zoznamu */
struct node {
    int data;      // prvok uložený v uzle
    node* next;  // smerník na další uzol
};

/* štruktúra reprezentujúca množinu spájaným zoznamom */
struct set {
    node *first;  // smerník na prvý uzol v zozname
};

void init(set &a) {
    a.first = NULL;  // inicializácia prázdnej množiny
}

void add(set &a, int x) {
    node* p = new node;  // vytvoríme nový uzol
    p->data = x;         // naplníme dáta
    p->next = a.first;   // za novým uzol pojde doterajší prvý
    a.first = p;         // tento uzol je novým začiatkom
}

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

int main(void) {
    set a;
    init(a);
    /* načítame a vykonávame príkazy */
    while (true) {
        char prikaz[MAXLENGTH];
        cin >> prikaz;
        if (strcmp(prikaz, "add") == 0) {
            int x;
            cin >> x;
            add(a, x);
        } else if (strcmp(prikaz, "contains") == 0) {
            int x;
            cin >> x;
            cout << contains(a, x) << endl;
        } else if (strcmp(prikaz, "end") == 0) {
            break;
        }
    }
}

Program: množina priamym adresovaním

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

const int MAXLENGTH = 100;
const int MAXVALUE = 1000;


/* štruktúra reprezentujúca množinu priamym adresovaním */
struct set {
    bool *p;
};

void init(set &a) {
    a.p = new bool[MAXVALUE];
    for (int i = 0; i < MAXVALUE; i++) {
        a.p[i] = false;
    }
}

bool contains(set &a, int x) {
    return a.p[x];
}

void add(set &a, int x) {
    a.p[x] = true;
}

int main(void) {
    set a;
    init(a);
    /* načítame a vykonávame príkazy */
    while (true) {
        char prikaz[MAXLENGTH];
        cin >> prikaz;
        if (strcmp(prikaz, "add") == 0) {
            int x;
            cin >> x;
            add(a, x);
        } else if (strcmp(prikaz, "contains") == 0) {
            int x;
            cin >> x;
            cout << contains(a, x) << endl;
        } else if (strcmp(prikaz, "end") == 0) {
            break;
        }
    }
}

Program: množina hešovaním

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

const int MAXLENGTH = 100;
const int HASHSIZE = 991;

/* štruktúra reprezentujúca uzol spájaného zoznamu */
struct node {
    int data;      // prvok uložený v uzle
    node* next;  // smerník na další uzol
};

/* štruktúra reprezentujúca množinu spájaným zoznamom */
struct set {
    node** lists;   // pole smerníkova na prvé prvky zoznamov
    int m;        // dlžka poľa lists
};

int hash(int x, int m) {
    return abs(x) % m;
}

void init(set &a) {
    a.m = HASHSIZE;
    a.lists = new node *[a.m];
    for (int i = 0; i < a.m; i++) {
        a.lists[i] = NULL;
    }
}


bool contains(set &a, int x) {
    int index = hash(x, a.m);
    node* p = a.lists[index];
    while (p != NULL) {
        if (p->data == x) return true;
        p = p->next;
    }
    return false;
}

void add(set &a, int x) {
    int index = hash(x, a.m);
    node* temp = new node;
    temp->data = x;
    temp->next = a.lists[index];
    a.lists[index] = temp;
}

int main(void) {
    set a;
    init(a);
    /* načítame a vykonávame príkazy */
    while (true) {
        char prikaz[MAXLENGTH];
        cin >> prikaz;
        if (strcmp(prikaz, "add") == 0) {
            int x;
            cin >> x;
            add(a, x);
        } else if (strcmp(prikaz, "contains") == 0) {
            int x;
            cin >> x;
            cout << contains(a, x) << endl;
        } else if (strcmp(prikaz, "end") == 0) {
            break;
        }
    }
}

Prednáška 15

Oznamy

  • Dnes o 18:10 v posluchárni B bude druhá písomka.
  • Dnes na doplnkových cvičeniach bude krátka prezentácia programu #Valgrind na hľadanie chýb súvisiacich s pamäťou.
  • Tretiu domácu úlohu treba odovzdať do piatka 23. novembra, 22:00.

Opakovanie smerníkov

Základy práce so smerníkmi:

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

Smerníky a polia:

int a[3];          // a je vlastne konstantny smernik na prvy 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

Smerník môže ukazovať aj na struct:

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

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.

Podobne:

  • Zápis *a[10] je to isté ako *(a[10]) a vyjadruje dereferenciu smerníka, ktorý je desiatym prvkom poľa a.
  • Zápis (*p)[10] vyjadruje desiaty prvok poľa, ktoré dostaneme dereferenciou smerníka p.

Práca s konzolou na spôsob jazyka C: printf a scanf

Doposiaľ sme s konzolou pracovali prostredníctvom knižnice iostream, ktorá patrí medzi štandardné knižnice jazyka C++ a v ktorej sú definované štandardné vstupno-výstupné prúdy cin a cout. V nasledujúcom si ukážeme alternatívny prístup k práci s konzolou založený na knižnici cstdio, ktorá je štandardnou knižnicou jazyka C. Rovnako ako nižšie sa tak so vstupom a výstupom dá pracovať aj v jazyku C.

Výpis formátovaných dát na konzolu: printf

S použitím knižnice cstdio možno na konzolu písať pomocou funkcie printf. Jej základné použitie môže vyzerať napríklad takto:

#include <cstdio>

int main() {
    printf("Ahoj svet, este raz!\n");
    return 0;
}

Funkciu printf možno volať aj s viac ako jedným argumentom. Vo všeobecnosti vyzerá jej volanie nasledovne:

printf(format, hodnota1, hodnota2, ...)

Prvým argumentom je takzvaný formátovací reťazec, za ním nasleduje niekoľko ďalších argumentov (prípadne aj žiaden). Formátovací reťazec pozostáva z dvoch druhov znakov: bežné znaky, ktoré sa priamo vypíšu na výstup a takzvané špecifikácie konverzií začínajúce symbolom % a končiace tzv. znakom konverzie, ktoré majú za následok vypísanie niektorého z ďalších argumentov funkcie printf (presnejšie prvého ešte nevypísaného argumentu). V rámci špecifikácie konverzie možno zadať formát, v ktorom sa má ten-ktorý argument vypísať.

Napríklad

#include <cstdio>

int main() {
    int n = 7;
    printf("Prve cislo je %d a druhe cislo je %d.\n",1+1,n);
    return 0;
}

vypíše

Prve cislo je 2 a druhe cislo je 7.

Špecifikácia %d tu pozostáva iba zo znaku konverzie d, ktorý zodpovedá výpisu celého čísla v desiatkovej sústave.

Ďalšie príklady znakov konverzie:

  • %f: reálne číslo.
  • %e: reálne čislo vo vedeckej notácii, napr. 5.4e7.
  • %x: celé číslo v šestnástkovej sústave.
  • %c: znak (char).
  • %s: reťazec (char *).
  • %%: vypíše samotný znak %.

Pozor: typy jednotlivých argumentov musia byť v súlade s formátovacím reťazcom.

Pred samotný znak konverzie možno pridávať aj modifikátory l, ll, resp. h zodpovedajúce modifikátorom typov long, long long, resp. short. Napríklad

  • %lld: vypíše „veľmi dlhé” celé číslo.
  • %lf: vypíše reálne číslo typu double; pri vypisovaní možno to isté urobiť aj pomocou %f, ale neskôr pri načítavaní bude nutné pre typ double naozaj používať %lf.

Formátovanie výstupu

Formát vypísania daného argumentu možno zadať niekoľkými nepovinnými parametrami medzi symbolom % a znakom konverzie. Napríklad:

  • %.2f: vypíše reálne číslo na 2 desatinné miesta.
  • %4d: ak má celé číslo menej ako 4 cifry, doplní vľavo medzery.
  • %04d: podobne, ale dopĺňa nuly.

Nasledujúci program vo vhodnom formáte vypíše hodnoty faktoriálu prirodzených čísel od 1 po 20:

#include <cstdio>

long long int factorial(int n) {                      
    if (n == 0) {
        return 1;
    } else {
        return n * factorial(n-1);
    }
}

int main() {
    for (int i = 1; i <= 20; i++) {                   
        printf("%2d! = %22lld\n",i,factorial(i));
    }
    return 0;
}

Nasledujúci program vypíše vo vhodnom formáte zadaný dátum:

#include <cstdio>

void vypisDatum(int d, int m, int r) {
    printf("%02d.%02d.%04d\n",d,m,r);
}

int main() {
    vypisDatum(2,1,2019);
    return 0;
}

Celá špecifikácia konverzie pozostáva z nasledujúcich častí:

  • Z povinného úvodného znaku %.
  • Z nepovinného znaku -, ktorého použitie vyústi v zarovnanie vypisovaného textu vľavo (bez jeho použitia sa text zarovná vpravo).
  • Z nepovinného celého čísla udávajúceho minimálnu šírku výpisu (minimálny počet „políčok”, do ktorých sa text vypíše).
  • Z nepovinnej bodky nasledovanej celým číslom udávajúcim presnosť výpisu (pri reálnych číslach napríklad počet desatinných miest; presnosť má však svoju interpretáciu aj pri iných typoch dát).
  • Z nepovinného modifikátora l, ll, alebo h pre long, long long, resp. short.
  • Z povinného symbolu konverzie (napr. d, f, s, ...).

Načítanie formátovaných dát z konzoly: scanf

Funkciu scanf s typickým volaním

scanf(format, smernik1, smernik2, ...)

možno využiť na načítanie dát z konzoly.

  • Napríklad scanf("%d", &x) načíta celočíselnú hodnotu do premennej x.
  • Zatiaľ čo argumentmi printf sú priamo hodnoty, scanf potrebuje smerníky na premenné, pretože ich modifikuje.

Jednoduchý príklad použitia:

#include <cstdio>

void vypisDatum(int d, int m, int r) {
    printf("%02d.%02d.%04d\n",d,m,r);
}

int main() {
    int r;

    printf("Zadaj rok: ");  
    scanf("%d", &r);        
    vypisDatum(1,1,r);                   
    return 0;
}

Pomocou scanf možno načítať aj viacero premenných naraz:

#include <cstdio>

void vypisDatum(int d, int m, int r) {
    printf("%02d.%02d.%04d\n",d,m,r);
}

int main() {
    int d,m,r;

    printf("Zadaj den, mesiac a rok: ");  
    scanf("%d %d %d", &d, &m, &r);        
    vypisDatum(d,m,r);                    
    return 0;
}

Formátovací reťazec sa teraz interpretuje nasledovne:

  • Špecifikácie formátov načítavaných premenných (začínajúce znakom %) možno zadávať podobne ako pri funkcii printf.
    • Pre načítanie reálneho čísla typu double je potrebné použiť %lf, kým pri funkcii printf stačí aj %f.
  • Biele znaky (angl. whitespace; t. j. medzery, konce riadkov, tabulátory) vo formátovacom reťazci spôsobia, že funkcia scanf číta a ignoruje všetky biele znaky pred ďalším nebielym znakom. Jeden biely znak vo formátovacom reťazci tak umožní ľubovoľný počet bielych znakov na vstupe.
  • Ostatné znaky formátovacieho reťazca musia presne zodpovedať vstupu.

Nasledujúci príkaz tak napríklad načíta dátum vo formáte deň.mesiac.rok:

scanf("%d.%d.%d", &d, &m, &r);

Kontrola správnosti vstupu

Funkcia scanf vracia na výstupe počet úspešne načítaných hodnôt zo vstupu. V prípade chyby hneď na začiatku vstupu tak napríklad vráti 0. V prípade, že hneď na začiatku narazí na koniec súboru (ktorý na konzole možno zadať pod Linuxom ako Ctrl+D resp. pod Windowsom ako Ctrl+Z a Enter), vráti hodnotu EOF (typicky -1).

Príklad: zadávanie dátumu vo formáte deň.mesiac.rok s kontrolou vstupu:

#include <cstdio>

void vypisDatum(int d, int m, int r) {
    printf("%02d.%02d.%04d\n",d,m,r);
}

int main() {
    int d,m,r;
    printf("Zadaj datum: ");  
    if (scanf("%d.%d.%d", &d, &m, &r) == 3) {
        printf("Datum je ");
        vypisDatum(d,m,r);
    } else {
        printf("Nebol zadany korektny datum.\n");
    }
    return 0;
}

Ďalším príkladom môže byť program, ktorý počíta súčet postupne zadávaných čísel, až kým je zadané nekorektné číslo alebo koniec súboru:

#include <cstdio>

int main() {
    double sum = 0;
    double x;
    while (scanf("%lf", &x) == 1) {
         sum += x;
    }
    printf("Sucet je %.2f\n", sum);
    return 0;
}

Textové súbory

Na načítavanie a vypisovanie dát sme doposiaľ používali výhradne konzolu. V praxi však často vzniká potreba spracovávať dáta uložené v súboroch. Zameriame sa teraz na súbory v textovom formáte, s ktorými sa pracuje podobne ako s konzolou.

Základy: typ FILE * a funkcie fopen, fclose, fprintf, fscanf

So súbormi sa pri použití knižnice cstdio pracuje pomocou typu FILE *. Ide tu o smerník na štruktúru typu FILE, ktorá obsahuje nejaké (pre programátora zväčša nepodstatné) informácie o súbore, s ktorým sa práve pracuje. Premenné pre prácu so súbormi tak možno definovať napríklad takto:

FILE *f;
FILE *fr, *fw;

Pozor: v názve typu FILE treba dodržať veľké písmená (čiže treba písať FILE *, nie file *).

Otvorenie súboru pre čítanie

  • fr = fopen("vstup.txt", "r");
  • Otvorí súbor s názvom vstup.txt (prípadne možno zadať kompletnú cestu k súboru).
  • Ak taký súbor neexistuje alebo sa nedá otvoriť, do fr priradí NULL.
  • Z takto otvoreného súboru môžeme čítať napríklad pomocou fscanf, ktorá je analógiou k scanf.
  • Napríklad fscanf(fr, "%d", &x);

Otvorenie súboru pre zápis

  • fw = fopen("vystup.txt", "w");
  • Vytvorí súbor s menom vystup.txt. Ak už existoval, zmaže jeho obsah.
  • Ak sa nepodarí súbor otvoriť, do fw priradí NULL.
  • Do takto otvoreného súboru môžeme zapisovať napr. pomocou funkcie fprintf, ktorá je analógiou k printf.
  • Napr. fprintf(fw, "%d", x);

Zatvorenie súboru

  • Po ukončení práce so súborom je ho potrebné zavrieť pomocou fclose(f);
  • Počet súčasne otvorených súborov je obmedzený.

Príklad

Nasledujúci program načíta číslo n a následne n celých čísel zo súboru vstup.txt. Do súboru vystup.txt vypíše vstupné čísla v opačnom poradí.

#include <cstdio>
#include <cassert>

int main() {
    FILE *fr = fopen("vstup.txt", "r");  
    FILE *fw = fopen("vystup.txt", "w");
    assert(fr != NULL && fw != NULL);
    
    int n,r;
    r = fscanf(fr, "%d", &n);
    assert(r == 1 && n >= 0);
    int *a = new int[n];
    
    for (int i = 0; i <= n-1; i++) {
        r = fscanf(fr, "%d", &a[i]);
        assert(r == 1);
    }
    fclose(fr);
    for (int i = n-1; i >= 0; i--) {
        fprintf(fw, "%d ", a[i]);
    }
    fclose(fw);
    delete[] a;    
    return 0;
}

Štandardný vstup a výstup ako súbor

So štandardným vstupom a výstupom sa pracuje rovnako ako so súborom. V cstdio sú definované dva konštantné smerníky

FILE *stdin, *stdout;

pre štandardný vstupný a výstupný prúd. Tie tak môžu byť použité v ľubovoľnom kontexte, v ktorom sa očakáva súbor. Napríklad volanie fscanf(stdin,"%d",&x) je ekvivalentné volaniu scanf("%d",&x).

Ten istý kód sa tak dá použiť na prácu so súbormi aj so štandardným vstupom resp. výstupom – stačí len podľa potreby nastaviť premennú typu FILE *. Typické použitie je napríklad nasledovné:

    FILE *fr, *fw;
    ...
    fscanf(fr, "%s", str);
    if (strcmp(str,"-") == 0) {
        fw = stdout;
    } else {
        fw = fopen(str, "w");
    }
    fprintf(fw, "Hello world!\n");
    ...

Testovanie konca súboru

Existujú dve možnosti testovania „nárazu” na koniec súboru:

  • V knižnici cstdio je definovaná symbolická konštanta EOF, ktorá má väčšinou hodnotu -1. Ak sa funkcii fscanf nepodarí načítať žiadnu hodnotu, pretože načítavanie dospelo ku koncu súboru, vráti konštantu EOF ako svoj výstup.
  • Funkcia feof(subor) vráti true práve vtedy, keď sa funkcia fscanf (alebo nejaká iná funkcia) už niekedy pokúšala čítať za koncom súboru subor.

Spracovanie vstupu pozostávajúceho z postupnosti čísel

Často na vstupe očakávame postupnosť číselných hodnôt oddelených bielymi znakmi. Pozrime sa na tri obvyklé možnosti, ako môže byť takýto vstup zadaný a spracovaný pomocou funkcie fscanf.

Formát 1: N (počet čísel) a následne N ďalších čísel.

#include <cstdio>
#include <cassert>
int main(void) {
    FILE *f;
    const int MAXN = 100;
    int a[MAXN], N, kod;

    f = fopen("vstup.txt", "r");
    assert(f != NULL);

    kod = fscanf(f, "%d ", &N);
    assert(kod == 1 && N >= 0 && N < MAXN);

    for (int i = 0; i < N; i++) {
        kod = fscanf(f, "%d ", &a[i]);
        assert(kod == 1);
    }
    fclose(f);

    // tu pride spracovanie dat v poli a
}

Formát 2: postupnosť čísel ukončená číslom -1 alebo inou špeciálnou hodnotu.

    // otvorime subor f ako vyssie
    N = 0;
    int x;
    kod = fscanf(f, "%d ", &x);
    assert(kod == 1);
    while (x != -1) {
        assert(N < MAXN);
        a[N] = x;
        N++;
        kod = fscanf(f, "%d ", &x);
        assert(kod == 1);
    }
    // zatvorime subor a spracujeme data

Formát 3: čísla, až kým neskončí súbor (najtypickejší prípad v praxi).

Priamočiary prístup nefunguje vždy správne:

    // otvorime subor f ako vyssie
    N = 0;
    while (!feof(f)) {
        assert(N < MAXN);
        kod = fscanf(f, "%d", &a[N]);
        assert(kod == 1);
        N++;
    }
    // zatvorime subor a spracujeme data

Po poslednom čísle v súbore často nasleduje ešte koniec riadku, v dôsledku čoho môže posledné volanie funkcie fscanf vyústiť v návratovú hodnotu -1 (predchádzajúce volanie fscanf totiž ešte „nenarazilo” na koniec súboru, v dôsledku čoho je pred čítaním posledného riadku hodnota feof(f) stále rovná false). Tým pádom program zlyhá na riadku assert(kod == 1). Tento nedostatok môžeme napraviť napríklad tak, že vo volaní funkcie fscanf dáme vo formátovacom reťazci za %d medzeru. Tá sa bude pokúšať preskočiť všetky biele znaky až po najbližší nebiely; pritom natrafí na koniec súboru a feof(f) už bude vracať true.

#include <cstdio>
#include <cassert>
int main(void) {
    FILE *f;
    const int MAXN = 100;
    int a[MAXN], N, kod;

    f = fopen("vstup.txt", "r");
    assert(f != NULL);

    N = 0;
    while (!feof(f)) {
        assert(N < MAXN);
        kod = fscanf(f, "%d ", &a[N]);
        assert(kod == 1);
        N++;
    }
    fclose(f);

    // tu pride spracovanie dat v poli a
}

Prednáška 16

Oznamy

  • Body z druhej písomky sú na testovači. Opravené písomky sú k dispozícii po dnešnej prednáške (alebo neskôr na vyžiadanie).
  • Tretia písomka bude v stredu 12. decembra o 18:10.

Príklad na prácu s textovými súbormi

Na minulej prednáške sme sa zaoberali základnými technikami práce s textovými súbormi s využitím knižnice cstdio. V rámci ich opakovania uvažujme nasledujúci problém: máme daných niekoľko „čiastkových” textových súborov, z ktorých každý obsahuje postupnosť niekoľkých celých čísel. Hlavný vstupný súbor vstup.txt potom pozostáva z:

  • Prvého riadku obsahujúceho názov výstupného súboru.
  • Niekoľkých ďalších riadkov zakaždým obsahujúcich názov niektorého „čiastkového” súboru nasledovaný celým číslom.

Úlohou je pre každú dvojicu tvorenú názvom „čiastkového” súboru a číslom N, uvedenú v súbore vstup.txt, prekopírovať z daného „čiastkového” súboru do výstupného súboru prvých N čísel.

Napríklad pre „čiastkový” súbor a.txt pozostávajúci z čísel

1 2 3 4 5 6 7 8 9

a „čiastkový” súbor b.txt pozostávajúci z čísel

10 20 30 40 50 60 70 80 90

sa pre hlavný vstupný súbor vstup.txt daný ako

vystup.txt
a.txt 2
b.txt 1
a.txt 3

majú do výstupného súboru vystup.txt nakopírovať hodnoty

1 2 10 1 2 3

Túto úlohu realizuje program uvedený nižšie, ktorý pracuje v nasledujúcich krokoch:

  • Otvorí hlavný vstupný súbor vstup.txt a prečíta z neho názov výstupného súboru.
  • Otvorí výstupný súbor.
  • Následne, až kým nenarazí na koniec hlavného vstupného súboru, opakuje nasledujúce:
    • Z hlavného vstupného súboru prečíta názov „čiastkového” súboru a prirodzené číslo N.
    • Otvorí „čiastkový” súbor s práve načítaným názvom, prekopíruje z neho N čísel do výstupného súboru a následne tento „čiastkový” súbor zatvorí.
  • Zatvorí hlavný vstupný súbor aj výstupný súbor.

Dĺžka načítavaných reťazcov bude vo volaniach funkcie fscanf obmedzená na 19 znakov (to teda bude maximálna dĺžka názvu súboru, s ktorou bude program vedieť pracovať).

#include <cstdio>
#include <cassert>

int main() {
    FILE *fr_main, *fr_part, *fw;
    int N,r,num;
    fr_main = fopen("vstup.txt", "r");
    assert(fr_main != NULL);
    
    char filename[20];
    r = fscanf(fr_main,"%19s",filename);
    assert(r == 1); 
    
    fw = fopen(filename, "w");
    assert(fw != NULL);
    while (!feof(fr_main)) {
        r = fscanf(fr_main,"%19s %d ",filename,&N);
        assert(r == 2);
        fr_part = fopen(filename, "r");
        assert(fr_part != NULL);
        for (int i = 0; i <= N-1; i++) {
            r = fscanf(fr_part, "%d ", &num);
            assert(r == 1);
            fprintf(fw, "%d ", num);
        }
        fclose(fr_part);
    }
    fclose(fw);
    fclose(fr_main); 
    return 0;
}

Čítanie a zapisovanie po znakoch

Knižnica cstdio obsahuje funkciu

int getc(FILE *f);

ktorá načíta jeden znak zo súboru, na ktorý odkazuje smerník f. V prípade, že načítanie prebehlo úspešne, je výstupom funkcie getc kód tohto znaku. V opačnom prípade je výstupom špeciálna konštanta EOF, ktorá je vždy rôzna od ľubovoľnej hodnoty typu char; to je aj dôvod, prečo funkcia getc nevracia hodnotu typu char, ale hodnotu typu int. Je preto dôležité vyvarovať sa ukladania výstupnej hodnoty funkcie getc do premennej typu char – v takom prípade nie je možné rozoznať koniec súboru. Funkcia

int getchar(void);

je skratkou pre getc s parametrom stdin; načíta tak jeden znak z konzoly (rovnako ako napríklad pri scanf sa však vstup začne spracovávať až potom, ako používateľ stlačí Enter – nie je takto možné reagovať priamo na stlačenie nejakej klávesy).

Výstupným náprotivkom k funkcii getc je funkcia

int putc(int c, FILE *f);

Tá zapíše znak c do súboru, na ktorý odkazuje smerník f. Funkcia

int putchar(int c);

je skratkou pre putc s parametrami c a stdout; vypíše teda daný znak na konzolu.

Príklad: kopírovanie súboru

Nasledujúci program skopíruje obsah súboru original.txt do súboru kopia.txt.

#include <cstdio>

int main(void) {
    FILE *fr = fopen("original.txt", "r");
    FILE *fw = fopen("kopia.txt", "w");

    int c = getc(fr);
    while (c != EOF) {
        putc(c, fw);
        c = getc(fr);
    }

    fclose(fr);
    fclose(fw);

    return 0;
}

Načítavanie pritom možno realizovať aj priamo v podmienke cyklu while – výstupom priradenia c = getc(fr) je nová hodnota premennej c, ktorú možno hneď porovnať s EOF. Kľúčová časť horeuvedeného programu sa tak dá kratšie, hoci menej čitateľne, prepísať takto:

    int c;
    while ((c = getc(fr)) != EOF) {
        putc(c, fw);
    }

Konce riadkov

Koniec riadku je reprezentovaný znakom '\n'. Pri čítaní alebo zápise sa môže prekladať na jeden alebo dva znaky v závislosti od operačného systému (<LF>, <CR><LF>, alebo <CR>).

Cvičenie: čo robí nasledujúci program?

#include <cstdio>

int main(void) {
    FILE *fr;
    int c;

    fr = fopen("vstup.txt", "r");
    while ((c = getc(fr)) != '\n') {
        putchar(c);
    }
    putchar(c);           // vypis \n
    fclose(fr);
   
    return 0; 
}

Funkcia ungetc

Signálom na ukončenie načítavania znakov často býva až „náraz” na znak, ktorý už nie je žiadúce prečítať. V takom prípade je užitočné „posunúť sa v načítavaní o jeden krok nazad”. Túto úlohu realizuje funkcia

int ungetc(int c, FILE * f);

Ak bol posledným načítaným znakom (zo súboru, na ktorý odkazuje smerník f) znak c, volanie ungetc(c,f) má skutočne efekt „kroku nazad”. Ako parameter funkcie ungetc však možno okrem naposledy prečítaného znaku použiť aj ľubovoľný iný znak c – funkcia ungetc potom tento znak „virtuálne” pridá na začiatok neprečítanej časti súboru. Súbor sa teda reálne nemení, ale pri nasledujúcom čítaní z neho sa ako prvý prečíta znak c. V prípade úspechu sa po volaní ungetc(c,f) vráti hodnota c; v prípade neúspechu je výstupom konštanta EOF.

Takéto správanie funkcie ungetc je však garantované len v prípade, že sa táto funkcia nevolá viackrát za sebou.

Príklad č. 1: Nasledujúci kus programu skonvertuje reťazec pozostávajúci zo znakov '0''9' na zodpovedajúcu číselnú hodnotu. Keď narazí na prvý znak, ktorý nie je cifra, vráti ho, aby sa dal použiť pri ďalšom spracovávaní.

    int hodnota = 0;
    int c = getchar();
    while (c >= '0' && c <= '9') {
        hodnota = hodnota * 10 + (c - '0');
        c = getchar();
    }
    ungetc(c, stdin);

Príklad č. 2: Nasledujúci program prečíta číslo pomocou funkcie fscanf, predtým však musí prečítať neznámy počet znakov '$'.

#include <cstdio>

int main(void) {
    FILE *fr = fopen("vstup.txt", "r");
    int c = getc(fr);
    while (c == '$') {
        c = getc(fr);
    }
    ungetc(c, fr);

    int hodnota;
    fscanf(fr, "%d", &hodnota);
    printf("%d\n", hodnota);
   
    fclose(fr);

    return 0;
}

Čítanie a zapisovanie po riadkoch

V knižnici cstdio je definovaná funkcia

char *fgets(char *str, int n, FILE * f);

pomocou ktorej možno načítať zo súboru, na ktorý ukazuje smerník f, práve jeden riadok (alebo nejakú jeho časť, ak je tento riadok príliš dlhý). Vstupnými argumentmi funkcie fgets sú:

  • Pole znakov str, do ktorého sa v prípade úspechu riadok načíta.
  • Číslo n určujúce maximálny počet znakov skopírovaných do poľa str. Presnejšie: do poľa str sa z daného riadku súboru skopíruje najviac n-1 znakov a reťazec str sa následne ukončí znakom \0. Pri typickom volaní funkcie gets je teda n rovné dĺžke poľa str.
  • Smerník f na súbor, z ktorého sa má riadok prečítať.

Funkcia gets na týchto argumentoch postupne načítava znaky zo súboru, na ktorý ukazuje smerník f, pričom ich ukladá do str. To robí až dovtedy, kým narazí na koniec riadku (\n) alebo koniec súboru, prípadne kým sa zo súboru neprečíta n-1 znakov. Prípadný znak \n na konci riadku sa (pokiaľ nebolo načítaných príliš veľa znakov) nezahodí, ale pridá sa na koniec reťazca str. Výstupom funkcie fgets je v prípade úspechu str a v prípade neúspechu NULL.

Príklad: nasledujúci program spočíta počet riadkov v súbore vstup.txt (za predpokladu, že žiaden z týchto riadkov nie je dlhší ako 100 znakov vrátane znaku \n na konci riadku):

#include <cstdio>

const int maxN = 101;

int main() {
    char str[maxN];
    int num = 0;
    
    FILE *fr = fopen("vstup.txt", "r");
    while (fgets(str, maxN, fr) != NULL) {
        num++;
    }
    fclose(fr);
    
    printf("%d\n",num);
    
    return 0;
}

Cvičenie: Zistite, ako sa správa uvedený program, keď posledným znakom v súbore je resp. nie je znak \n. Zistite, čo program vypíše na výstup pre súbor, ktorý obsahuje jediný riadok o 200 znakoch.

Výstupným náprotivkom funkcie fgets je funkcia

int fputs(const char *str, FILE *f);

ktorá do súboru, na ktorý ukazuje smerník f, vypíše reťazec str. Vypisovaný reťazec str pritom môže obsahovať ľubovoľný (aj nulový) počet výskytov symbolu \n pre koniec riadku. V prípade úspechu vráti funkcia fputs nezáporné celé číslo; v prípade neúspechu vráti konštantu EOF.

Prístupy k spracovaniu textového vstupu

Vstup môže byť v textovom súbore zadaný v rôznych formátoch. V závislosti od formátu potom môžu byť výhodnými rôzne spôsoby jeho spracovania. Často používanými prístupmi k spracovaniu textového vstupu sú napríklad nasledujúce:

  • Pomocou funkcie fscanf postupne načítať jednotlivé čísla, slová, a podobne. Tento prístup býva zvyčajne výhodný vtedy, keď sa všetky biele znaky považujú za ekvivalentné oddeľovače.
  • Pomocou funkcie getc spracovať vstupný súbor po znakoch. Tu ide o relatívne univerzálny spôsob spracovania vstupu, ktorý je však v niektorých situáciách pomerne prácny.
  • Pomocou funkcie fgets postupne prečítať jednotlivé riadky do reťazca a tento reťazec následne spracovať. Tento prístup je výhodný najmä vtedy, keď má koniec riadku funkciu prirodzeného oddeľovača vstupov a keď je dĺžka riadku predom obmedzená.

Často môže byť užitočné horeuvedené prístupy aj navzájom kombinovať.

Príklad č. 1: predpokladajme, že potrebujeme nájsť dĺžku najdlhšieho riadku v súbore (vrátane symbolu \n, ktorý sa môže vyskytovať na jeho konci). Nasledujúce dva programy túto úlohu riešia dvoma odlišnými spôsobmi:

  • Prvý program postupne načítava riadky do reťazca, ktorý následne spracúva (problém, ak je riadok príliš dlhý).
  • Druhý program číta súbor po znakoch, pričom si udržiava premennú pocet uchovávajúcu informáciu o tom, koľko písmen sa už v momentálne spracovávanom riadku načítalo.
#include <cstdio>
#include <cstring>

const int maxN = 100;

int main() {
    FILE *fr = fopen("vstup.txt", "r");
    int maxDlzka = 0;
    char str[maxN];
    while (fgets(str, maxN, fr) != NULL) {
        int dlzka = strlen(str);
        if (dlzka > maxDlzka) {
            maxDlzka = dlzka;
        }
    }
    fclose(fr);
    printf("Najdlhsi riadok ma dlzku %d\n", maxDlzka);
    return 0;
}
#include <cstdio>

int main() {
    FILE *fr = fopen("vstup.txt", "r");
    int maxDlzka = 0;
    int dlzka = 0;
    int c = getc(fr);
    while (c != EOF) {
        dlzka++;
        if (c == '\n') {
            if (dlzka > maxDlzka) {
                maxDlzka = dlzka;
            }
            dlzka = 0;
        }
        c = getc(fr);
    }
    if (dlzka > maxDlzka) { // Posledny riadok nemusi koncit symbolom \n.
        maxDlzka = dlzka;
    }
    fclose(fr);
    printf("Najdlhsi riadok ma dlzku %d\n", maxDlzka);
    return 0;
}

Príklad č. 2:: nasledujúci program spracúva vstupný súbor obsahujúci čísla oddelené bielymi znakmi (medzery, tabulátory, konce riadkov,...), pričom medzi dvoma číslami môže byť aj viac ako jeden oddeľovač. Pre každý riadok program vypíše súčet čísel, ktoré sa v tomto riadku vyskytujú (predpokladá pritom, že každý – t. j. aj posledný – riadok vstupného súboru je ukončený symbolom \n).

Vzhľadom na to, že tu ide o pomerne nepríjemnú kombináciu rozlišovania koncov riadku od iných bielych znakov a čítania formátovaných hodnôt (čísel), kombinuje nasledujúci program čítanie po znakoch s využívaním funkcie fscanf. Pracuje pritom nasledovne:

  • Kým sú na vstupe biele znaky, spracúva ich pomocou funkcie getc. Ak je niektorý z týchto znakov koncom riadku, vypíše zistený súčet čísel.
  • Po „náraze” na prvý nebiely znak použije funkciu ungetc na jeho vrátenie do vstupného prúdu. Následne prečíta číslo pomocou funkcie fscanf a na základe prečítanej hodnoty aktualizuje súčet.
  • Na zistenie, či je prečítaný znak biely, využíva nasledujúci program funkciu isspace z knižnice cctype.
#include <cstdio>
#include <cctype>

int main() {
    FILE *fr = fopen("vstup.txt", "r");
    int sucet = 0;
    int hodnota;
    while (!feof(fr)) {
        int c = getc(fr);
        while (c != EOF && isspace(c)) { // Precitaj biele znaky po najblizsi nebiely.
            if (c == '\n') {             // Na konci riadku vypis sucet.
                printf("Sucet %d\n", sucet);
                sucet = 0;
            }
            c = getc(fr);
        }
        if (c == EOF) {                  // Pri naraze na koniec suboru nepokracuj dalej.
            break;
        }                
        ungetc(c, fr);                   // Posledny precitany znak nebol biely; vrat ho do vstupneho prudu.
        fscanf(fr, "%d", &hodnota);      // Precitaj cislo a pripocitaj ho k suctu.
        sucet += hodnota;
    }
    fclose(fr);
    return 0;
}

Cvičenie: upravte program tak, aby pracoval správne aj v prípade, že posledný riadok nie je ukončený symbolom \n.

Jednoduché šifrovanie

Prácu so súbormi si v nasledujúcom precvičíme na dvoch jednoduchých šifrách.

Cézarova šifra

Cézarova šifra je šifra, pri ktorej sa každé písmeno vstupného reťazca posunie cyklicky o K miest v abecednom poradí, kde K je zadaný parameter šifry (tzv. posun).

  • Napríklad pre K=2 sa písmeno A zmení na C, písmeno b sa zmení na d a písmeno Z sa zmení na B.
  • Ukážeme si jej použitie pre anglickú abecedu (t. j. znaky 'a''z' a 'A''Z' bez diakritiky); je ju ale možné upraviť napríklad aj tak, aby pracovala s ASCII kódmi.

Zašifrovanie súboru realizuje nasledujúci program:

#include <cstdio>
#include <cassert>

void encryptCaesar(FILE *fr, FILE *fw, int shift) {
    assert(shift >= 0 && shift <= 25);
    int c;
    while ((c = getc(fr)) != EOF) {
        if ((c >= 'A') && (c <= 'Z')) {
            c = c + shift;
            if (c > 'Z') {
                c -= 26;
            }
        } else if ((c >= 'a') && (c <= 'z')) {
            c = c + shift;
            if (c > 'z') {
                c -= 26;
            }
        }
        putc(c, fw);
    }
}

int main() {
    int shift;
    scanf("%d", &shift);
    
    FILE *fr = fopen("plaintext.txt", "r");
    FILE *fw = fopen("ciphertext.txt", "w");
    
    encryptCaesar(fr, fw, shift);
    
    fclose(fr);
    fclose(fw);
    
    return 0;
}

Dešifrovanie súboru zašifrovaného Cézarovou šifrou realizuje tento program:

#include <cstdio>
#include <cassert>

void decryptCaesar(FILE *fr, FILE *fw, int shift) {
    assert(shift >= 0 && shift <= 25);
    int c;
    while ((c = getc(fr)) != EOF) {
        if ((c >= 'A') && (c <= 'Z')) {
            c = c - shift;
            if (c < 'A') {
                c += 26;
            }
        } else if ((c >= 'a') && (c <= 'z')) {
            c = c - shift;
            if (c < 'a') {
                c += 26;
            }
        }
        putc(c, fw);
    }
}

int main() {
    int shift;
    scanf("%d", &shift);
    
    FILE *fr = fopen("ciphertext.txt", "r");
    FILE *fw = fopen("plaintext2.txt", "w");
    
    decryptCaesar(fr, fw, shift);
    
    fclose(fr);
    fclose(fw);
    
    return 0;
}

Vigenèrova šifra

Vigenèrova šifra je veľmi podobná Cézarovej; posun už ale nie je konštantný a realizuje sa podľa kľúča.

  • Kľúčom je reťazec zložený z písmen AZ, pričom tieto predstavujú posuny o 025 pozícií v abecede.
  • Pri šifrovaní aj dešifrovaní sa jednotlivé abecedné posuny realizujú podľa kľúča. Prvý symbol otvoreného textu je tak zašifrovaný podľa prvého symbolu kľúča, druhý symbol podľa druhého symbolu kľúča, atď. Po vyčerpaní celého kľúča sa pokračuje cyklicky, opäť od jeho začiatku.

Zašifrovanie súboru realizuje nasledujúci program:

#include <cstdio>

const int maxKeyLength = 100;

void encryptVigenere(FILE *fr, FILE *fw, char *key) {
    int c;
    int i = 0;
    while ((c = getc(fr)) != EOF) {
        if ((c >= 'A') && (c <= 'Z')) {
            c = c + (key[i] - 'A');
            if (c > 'Z') {
                c -= 26;
            }
            i++;
        } else if ((c >= 'a') && (c <= 'z')) {
            c = c + (key[i] - 'A');
            if (c > 'z') {
                c -= 26;
            }
            i++;
        }
        if (key[i] == 0) {
            i = 0;
        }
        putc(c, fw);
    }
}

int main() {
    char key[maxKeyLength];
    scanf("%s", key);
    
    FILE *fr = fopen("plaintext.txt", "r");
    FILE *fw = fopen("ciphertext.txt", "w");
    
    encryptVigenere(fr, fw, key);
    
    fclose(fr);
    fclose(fw);

    return 0;
}

Dešifrovanie súboru zašifrovaného Vigenèrovou šifrou realizuje tento program:

#include <cstdio>
#include <cassert>

const int maxKeyLength = 100;

void decryptVigenere(FILE *fr, FILE *fw, char *key) {
    int c;
    int i = 0;
    while ((c = getc(fr)) != EOF) {
        if ((c >= 'A') && (c <= 'Z')) {
            c = c - (key[i] - 'A');
            if (c < 'A') {
                c += 26;
            }
            i++;
        } else if ((c >= 'a') && (c <= 'z')) {
            c = c - (key[i] - 'A');
            if (c < 'a') {
                c += 26;
            }
            i++;
        }
        if (key[i] == 0) {
            i = 0;
        }
        putc(c, fw);
    }
}

int main() {
    char key[maxKeyLength];
    scanf("%s", key);
    
    FILE *fr = fopen("ciphertext.txt", "r");
    FILE *fw = fopen("plaintext2.txt", "w");
    
    decryptVigenere(fr, fw, key);
    
    fclose(fr);
    fclose(fw);
    
    return 0;
}

Binárne súbory

Textové súbory sú v praxi často nepostačujúcim formátom na ukladanie dát. Ich nevýhody sú napríklad nasledujúce:

  • Aj číselné dáta sú v textovom súbore uložené ako postupnosti znakov, čoho dôsledkom je plytvanie pamäťou a pomalé čítanie a zápis (v dôsledku nutnosti konvertovať reťazce na čísla a opačne).
  • Pri zápise reálnych čísel môže dochádzať k strate presnosti.
  • Pri zápise štruktúr alebo polí je nutné vymyslieť formát ich textovej reprezentácie a naprogramovať funkcie realizujúce ich načítanie a zápis.
  • ...

Alternatívnym riešením je použitie binárnych súborov, do ktorých sa namiesto textovej reprezentácie dát ukladá priamo postupnosť bytov zodpovedajúca reprezentácii týchto dát v pamäti počítača. Napríklad 4-bytová premenná typu int s hodnotou 2000000000 sa tak v binárnom súbore reprezentuje 4 bytmi, kým v textovom súbore by bol nutný jeden byte pre každý znak (čiže dohromady 10 bytov) a pravdepodobne aj nejaký oddeľovač od ďalších dát, čomu zodpovedá minimálne jeden ďalší byte.

Binárne súbory teda poskytujú vo všeobecnosti pamäťovo aj časovo efektívnejší spôsob ukladania dát. Nevýhodou tohto prístupu však je, že typicky nie je prenositeľný medzi rôznymi architektúrami, operačnými systémami, či kompilátormi. Napísať program, ktorý na ľubovoľnom systéme vytvorí rovnaký binárny súbor, je pomerne zložité – typicky sa na tento účel používajú špecializované knižnice.

Na tomto predmete budeme pracovať len s textovými súbormi. V nasledujúcom sa však pre úplnosť v krátkosti zameriame aj na základy práce s binárnymi súbormi.

Funkcia fwrite

Funkcia

size_t fwrite(const void *p, size_t size, size_t count, FILE *f);

zapíše do súboru, na ktorý ukazuje smerník f, presne count pamäťových úsekov veľkosti size bytov, nachádzajúcich sa v pamäti od adresy p. Výstupom je potom počet reálne zapísaných úsekov. V hlavičke funkcie fwrite sa vyskytujú dva nové prvky:

  • Typ size_t je celočíselný typ, ktorý sa používa na meranie veľkosti pamäte (každý index poľa je napríklad určite tohto typu). Je definovaný vo viacerých knižniciach, napríklad aj v cstdio a je prakticky ekvivalentný nezáporným celým číslam.
  • Smerník na void je špeciálny smerníkový typ, ktorý slúži ako vyjadrenie toho, že argumentom môže byť smerník ľubovoľného typu.

Aby bolo možné použiť túto funkciu, musí byť súbor f otvorený na zápis v móde wb (b tu znamená binárny súbor).

Príklad: nasledujúci program vytvorí pole pozostávajúce z druhých odmocnín čísel 099 a toto pole uloží do binárneho súboru binarny.dat. Využíva pritom operátor sizeof, ktorý vracia veľkosť reprezentácie daného typu v bytoch.

#include <cstdio>
#include <cmath>

int main() {
    double a[100];
    FILE *f;
    
    for (int i = 0; i <= 99; i++) {
        a[i] = sqrt(i);
    }
    
    f = fopen("binarny.dat", "wb");
    fwrite(a, sizeof(double), 100, f);
    fclose(f);
    
    return 0;
}

Funkcia fread

Funkcia

size_t fread(void *p, size_t size, size_t count, FILE * f);

zo súboru, na ktorý ukazuje smerník f, prečíta count položiek veľkosti size a uloží ich do pamäte od adresy p.

Ak by sme napríklad chceli overiť, že piatym reálnym číslom uloženým v súbore vytvorenom pomocou programu z predchádzajúceho príkladu je skutočne číslo 2, môžeme napríklad do poľa a uložiť prvých 5 položiek zo súboru binarny.dat a skontrolovať prvok a[4]:

#include <cstdio>

int main() {
    FILE *f;
    double a[5];

    f = fopen("binarny.dat", "rb");
    fread(a, sizeof(double), 5, f);
    fclose(f);
    printf("%f\n", a[4]);
    
    return 0;
}

Nasledujúce alternatívne riešenie je o niečo pamäťovo efektívnejšie:

#include <cstdio>

int main() {
    FILE *f;
    double *x = new double;

    f = fopen("binarny.dat", "rb");
    for (int i = 0; i <= 4; i++) {
        fread(x, sizeof(double), 1, f);
    }
    fclose(f);
    printf("%f\n", *x);
    delete x;
                     
    return 0;
}

Práca so súbormi prostredníctvom knižnice fstream

So súbormi sme pracovali prostredníctvom knižnice cstdio, ktorá je štandardnou knižnicou jazyka C. Druhou možnosťou, ktorú tu iba telegraficky spomenieme, je využitie knižnice fstream. Tá je štandardnou knižnicou jazyka C++ a umožňuje so súbormi pracovať podobne ako s konzolou prostredníctvom knižnice iostream.

Základy tohto prístupu demonštrujeme na príklade programu, ktorý zo súboru vstup.txt prečíta prirodzené číslo N a následne N ďalších čísel. Tieto čísla potom v obrátenom poradí a oddelené medzerami vypíše do súboru vystup.txt.

#include <fstream>
using namespace std;

const int maxN = 100;

int main() {
    ifstream input;
    ofstream output;
    
    int N;
    int a[maxN];
    
    input.open("vstup.txt");
    input >> N;
    for (int i = 0; i <= N-1; i++) {
        input >> a[i];
    }
    input.close();
    
    output.open("vystup.txt");
    for (int i = N-1; i >= 0; i--) {
        output << a[i] << " ";
    }
    output.close();
 
    return 0; 
}

Prednáška 17

Oznamy

  • Štvrtú domácu úlohu treba odovzdať do piatka 7. decembra, 22:00.
  • Tretia písomka bude v stredu 12. decembra o 18:10.

Abstraktný dátový typ

Abstraktné dátové typy (ADT) sú abstrakciami dátových štruktúr nezávislými od samotnej implementácie. Zvyčajne bývajú dané prostredníctvom množiny operácií (hlavičiek funkcií), ktoré daný abstraktný dátový typ poskytuje na prácu s dátami používateľovi. Témou 14. prednášky bol napríklad abstraktný dátový typ 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).

Jeden abstraktný dátový typ môže byť implementovaný pomocou viacerých dátových štruktúr. Na 14. prednáške bol napríklad abstraktný dátový typ množina (presnejšie jeho operácie contains a add) implementovaný pomocou neutriedeného poľa, utriedeného poľa, spájaného zoznamu, priameho adresovania a hešovania.

Výhodou abstraktných dátových typov je predovšetkým oddelenie implementácie dátovej štruktúry od programu, ktorý ju používa. Napríklad program pracujúci s 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 budú dva nové abstraktné dátové typy: zásobník (angl. stack) a rad alebo front (angl. queue).

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.

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.

Prvky radu 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.

Abstraktný dátový typ pre rad môže poskytovať napríklad 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ý. 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 (stack je názov štruktúry reprezentujúcej zásobník a opäť pracujeme s prvkami všeobecného typu dataType):

/* 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);

/* 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);
    return 0;
}

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);
    return 0;
}

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>

// ...

const int maxN = 1000;

struct stack {
    dataType *items; // Alokujeme na pole reprezentujuce jednotlive prvky zasobnika.
    int top;         // Index vrchu zasobnika v poli items. Ak je zasobnik prazdny, ma hodnotu -1.
};

/* 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 */          
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 teda 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 pritom budeme interpretovať ako nasledujúce za posledným prvkom poľa.

#include <cassert>

// ...

const int maxN = 1000;

struct queue {
    dataType *items; // Alokujeme na pole reprezentujuce jednotlive 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 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).

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 už nebude obmedzený konštantou maxN. Podobný efekt síce možno docieliť použitím dynamického poľa, jeho realokácia je však časovo neefektívna.

#include <cassert>

// ...

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 */          
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é 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.

#include <cassert>

// ...

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

struct queue {
    node *first; // Smernik na prvy prvok radu (spajaneho zoznamu). Ak je rad prazdny, ma hodnotu NULL.
    node *last;  // Smernik na posledny prvok radu (spajaneho zoznamu). Ak je rad prazdny, ma hodnotu NULL.
};

/* 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 (nižšie ako príklad uvádzame nerekurzívnu verziu triedenia Quick Sort).

Príklad č. 1: 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

Túto úlohu rieši nasledujúci program, ktorý 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;
    }
    
    return 0;
}

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.

Príklad č. 2: nerekurzívny Quick Sort

Pripomeňme si triedenie Quick Sort z 11. prednášky:

void swap (int &x, int &y) {
    int tmp = x;
    x = y;
    y = tmp;
}

int partition(int a[], int low, int high) {
    int pivot = a[low];
    int lastSmaller = low;
    
    for (int unknown = low + 1; unknown <= high; unknown++) {
        if (a[unknown] < pivot) {
            lastSmaller++;
            swap(a[unknown], a[lastSmaller]);
        }
    }   
    swap(a[low],a[lastSmaller]); 
    return lastSmaller;
}

void quicksort(int a[], int low, int high) {
    if (low >= high) {
        return;
    }
    
    int mid = partition(a, low, high);
        
    quicksort(a, low, mid-1);
    quicksort(a, mid+1, high);   
}

int main() {
  // ...
  quicksort(a, 0, N-1);
  // ...
}

Namiesto rekurzie môžeme použiť aj zásobník úsekov, ktoré ešte treba dotriediť.

struct usek {
    int low;
    int high;
};

typedef usek dataType;

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

/* Sem pridu funkcie swap a partition rovnake ako vyssie. */

void quicksort(int a[], int n) {
    stack s;
    init(s);
    
    usek u;
    u.low = 0;
    u.high = n-1;
    push(s,u);
    
    while (!isEmpty(s)) {
        u = pop(s);
        if (u.low >= u.high) {
            continue;
        }
        
        int mid = partition(a, u.low, u.high);
        
        usek u1;
        u1.low = u.low;
        u1.high = mid-1;
        usek u2;
        u2.low = mid+1;
        u2.high = u.high;
        push(s,u2);
        push(s,u1);
    }
    
    destroy(s);
}

int main() {
  // ...
  quicksort(a, N);
  // ...
}

Tento program triedi úseky v rovnakom poradí, ako rekurzívny Quicksort, lebo po rozdelení poľa na dve časti dá na vrch zásobníka úsek zodpovedajúci jeho ľavej časti. Až keď sa táto ľavá časť a všetky podúlohy, ktoré z nej vzniknú, spracuje, dôjde na spracovanie pravej časti poľa. Pri triedení Quick Sort však na tomto poradí nezáleží, takže by sme mohli jednotlivé úseky vkladať na zásobník aj v opačnom poradí.

Na zamyslenie: ako by mohla vyzerať nerekurzívna verzia triedenia Merge Sort? Prečo sa nedá použiť rovnaký prístup ako pri triedení Quick Sort?

Prednáška 18

Oznamy

  • Zajtrajšia rozcvička ešte bude zameraná na zoznamy.
  • Štvrtú domácu úlohu treba odovzdať do piatka 7. decembra, 22:00.
  • Tretia písomka bude v stredu 12. decembra o 18:10.
  • Skúšky budú predbežne v nasledujúcich termínoch:
    • V pondelok 7. januára 2019 o 9:00 v miestnosti I-H6.
    • V utorok 22. januára 2019 o 9:00 v miestnosti I-H6.
    • V pondelok 4. februára 2019 o 9:00 v miestnosti I-H6 (predposledný termín v tomto semestri určený najmä pre opravné termíny).
    • Posledný termín niekedy v poslednom týždni skúškového obdobia (bude upresnené neskôr).
  • Prihlasovanie na skúšky v AIS by malo byť otvorené od pondelka 10. decembra 2018, 12:00. Na každý termín sa treba prihlásiť najneskôr 24 hodín pred začiatkom skúšky.

Vyfarbovanie súvislých oblastí

Uvažujme obrazec daný obdĺžnikovou maticou o m riadkoch a n stĺpcoch. Obdĺžnikové plátno je v takom prípade rozdelené na m krát n „štvorčekov” určitej konštantnej veľkosti, pričom jednotlivé prvky matice zodpovedajú farbám jednotlivých týchto štvorčekov. V našom jednoduchom príklade budeme pracovať iba s piatimi farbami, ktoré budeme reprezentovať číslami 0,..,4 podľa nasledujúceho poľa (napríklad číslo 0 teda reprezentuje bielu farbu):

const char *farby[5] = {"white", "blue", "black", "yellow", "red"};

Napríklad obrazec

Matica1.png

tak môže byť reprezentovaný nasledujúcim textovým súborom obsahujúcim najprv rozmery matice (čísla m a n) a za nimi samotné prvky matice:

11 17
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 1 1 1 1 1 1 1 1 1 0 0 0 0 0
 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0
 0 0 0 1 0 0 0 0 0 2 2 1 2 2 2 2 0
 0 0 0 1 0 0 0 0 0 2 0 1 0 0 0 2 0
 0 2 2 2 2 2 2 2 0 2 2 1 2 2 2 2 0
 0 2 0 1 0 0 0 2 0 0 0 1 0 0 0 0 0
 0 2 0 1 0 0 0 2 0 0 0 1 0 1 1 1 1
 0 2 0 1 1 1 1 2 1 1 1 1 0 1 0 0 1
 0 2 0 0 0 0 0 2 0 0 0 0 0 1 1 1 1
 0 2 2 2 2 2 2 2 0 0 0 0 0 0 0 0 0

Zameriame sa teraz na nasledujúci problém: používateľ zvolí (zadá na konzolu) súradnice niektorého „štvorčeka” a cieľom je ofarbiť nejakou farbou (napríklad červenou) celú súvislú oblasť rovnakej farby obsahujúcu daný štvorček. Napríklad pre obrazec vyššie a vstupné súradnice (2,1) – to znamená pre „štvorček” v treťom riadku a druhom stĺpci, keďže matica sa bude indexovať od nuly – by mal byť výstupom nasledujúci obrazec:

Matica2.png

Podobný problém je napríklad často potrebné riešiť v rôznych nástrojoch na prácu s grafikou (kde sa namiesto „štvorčekov” ofarbujú pixely) a podobne.

Základ programu

Funkciu na vyfarbovanie súvislých oblastí budeme dorábať do nasledujúcej kostry programu, ktorá obsahuje funkcie na inicializáciu matice, jej načítanie zo súboru, vykresľovanie jednotlivých štvorčekov a celej matice, ako aj uvoľnenie pamäte. Všetky tieto funkcie pracujú podobne ako pri príklade s výškovou mapou z prednášky č. 13. Nasledujúca kostra tiež obsahuje funkciu main, ktorá načíta maticu zo súboru vstup.txt a následne zatiaľ len vykreslí ňou reprezentovaný obrazec do súboru matica.svg.

#include "SVGdraw.h"
#include <cstdio>
#include <cassert>

const char *farby[5] = {"white", "blue", "black", "yellow", "red"}; 

const int stvorcek = 40;    // velkost stvorceka v pixeloch
const int hrubkaCiary = 2;  // hrubka ciary v pixeloch

/* Vytvori maticu s n riadkami a m stlpcami. */
int **vytvorMaticu(int m, int n) {
    int **a;
    a = new int *[m];
    for (int i = 0; i <= m - 1; i++) {
        a[i] = new int[n];
    }
    return a;
}

/* Uvolni pamat matice a s n riadkami a m stlpcami. */
void zmazMaticu(int m, int n, int **a) {
    for (int i = 0; i <= m - 1; i++) {
        delete[] a[i];
    }
    delete[] a;
}

/* Vykresli stvorcek v riadku i a stlpci j s farbou vyplne farba a farbou ciary farbaCiary. */
void vykresliStvorcek(int i, int j, const char *farba, const char *farbaCiary, SVGdraw &drawing) {
    drawing.setLineColor(farbaCiary);
    drawing.setLineWidth(hrubkaCiary);
    drawing.setFillColor(farba);
    drawing.drawRectangle(j * stvorcek, i * stvorcek, stvorcek, stvorcek);
}

/* Vykresli maticu a s n riadkami a m stlpcami. */
void vykresliMaticu(int m, int n, int **a, SVGdraw &drawing) {
    for (int i = 0; i <= m - 1; i++) {
        for (int j = 0; j <= n - 1; j++) {
            vykresliStvorcek(i, j, farby[a[i][j]], "lightgray", drawing);
        }
    }
}

/* Nacita z textoveho suboru, na ktory ukazuje fr, prvky matice a s n riadkami a m stlpcami. */
void nacitajMaticu(FILE *fr, int m, int n, int **a) {
    assert(fr != NULL);
    for (int i = 0; i <= m - 1; i++) {
        for (int j = 0; j <= n - 1; j++) {
            fscanf(fr, "%d", &a[i][j]);
        }
    }
}

int main() {
    FILE *fr = fopen("vstup.txt", "r");
    assert(fr != NULL);
    int m, n;
    fscanf(fr, "%d", &m);               // nacitaj rozmery matice
    fscanf(fr, "%d", &n);
    int **a = vytvorMaticu(m, n);
    nacitajMaticu(fr, m, n, a);         // nacitaj jednotlive prvky matice
    fclose(fr);
        
    SVGdraw drawing(n * stvorcek, m * stvorcek, "matica.svg");
    vykresliMaticu(m, n, a, drawing);

    drawing.finish();
    zmazMaticu(m, n, a);
    
    return 0;
}

Rekurzívne vyfarbovanie

Vyfarbovanie súvislých oblastí potom môžeme realizovať napríklad nasledujúcou rekurzívnou funkciou vyfarbi, ktorá vždy na cieľovú farbu farba prefarbí políčko so súradnicami (riadok, stlpec) a následne sa rekurzívne zavolá pre všetkých susedov tohto políčka, ktoré sú zafarbené pôvodnou farbou prefarbovanej oblasti.

Za každým vyfarbením „štvorčeka” navyše voláme funkciu drawing.wait s parametrom pauza, čo je konštanta, ktorú na úvod nastavíme na 0,3 sekundy. Výsledný SVG súbor tak bude obsahovať animáciu postupného vyfarbovania jednotlivých políčok. Farbou „rámika” okolo políčka budeme navyše rozlišovať, či už bolo dané políčko úplne spracované (t. j. či sa už ukončilo rekurzívne volanie funkcie vyfarbi pre toto políčko).

const double pauza = 0.3;   // pauza po kazdom kroku vysvetlovania v sekundach

/* Prefarbi suvislu jednofarebnu oblast obsahujucu poziciu (riadok,stlpec) na farbu s cislom farba. */
void vyfarbi(int m, int n, int **a, int riadok, int stlpec, int farba, SVGdraw &drawing) {
    int staraFarba = a[riadok][stlpec];
    if (staraFarba == farba) {
        return;
    }
    a[riadok][stlpec] = farba;
    vykresliStvorcek(riadok, stlpec, farby[farba], "brown", drawing);
    drawing.wait(pauza);
    if (riadok - 1 >= 0 && a[riadok - 1][stlpec] == staraFarba) {
        vyfarbi(m, n, a, riadok - 1, stlpec, farba, drawing);
    }
    if (riadok + 1 <= m - 1 && a[riadok + 1][stlpec] == staraFarba) {
        vyfarbi(m, n, a, riadok + 1, stlpec, farba, drawing);
    }
    if (stlpec - 1 >= 0 && a[riadok][stlpec - 1] == staraFarba) {
        vyfarbi(m, n, a, riadok, stlpec - 1, farba, drawing);
    }
    if (stlpec + 1 <= n - 1 && a[riadok][stlpec + 1] == staraFarba) {
        vyfarbi(m, n, a, riadok, stlpec + 1, farba, drawing);
    }
    vykresliStvorcek(riadok, stlpec, farby[farba], "lightgray", drawing);
    drawing.wait(pauza);
}

Funkcia main potom môže vyzerať napríklad nasledovne:

int main() {
    FILE *fr = fopen("vstup.txt", "r");
    assert(fr != NULL);
    int m, n;
    fscanf(fr, "%d", &m);               // nacitaj rozmery matice
    fscanf(fr, "%d", &n);
    int **a = vytvorMaticu(m, n);
    nacitajMaticu(fr, m, n, a);         // nacitaj jednotlive prvky matice
    fclose(fr);
        
    SVGdraw drawing(n * stvorcek, m * stvorcek, "matica.svg");
    vykresliMaticu(m, n, a, drawing);

    int riadok, stlpec;
    scanf("%d", &riadok);
    scanf("%d", &stlpec);

    vyfarbi(m, n, a, riadok, stlpec, 4, drawing);

    drawing.finish();
    zmazMaticu(m, n, a);
    
    return 0;
}

Počítanie ostrovov

Obrazec, s ktorým sme pracovali vyššie, môže reprezentovať napríklad jednoduchú mapu súostrovia, kde more je znázornené modrou farbou a pevnina je znázornená žltou farbou. Úlohou môže byť zistiť počet ostrovov. Ten môžeme zistiť napríklad takto:

  • Prechádzame postupne všetky políčka mapy.
  • Ak narazíme na pevninu (t. j. žlté políčko), zvýšime doposiaľ nájdený počet ostrovov o 1 a ofarbíme celý ostrov (napríklad) na červeno.
  • Ak narazíme na ďalšie žlté políčko, opäť urobíme to isté.
  • Toto robíme, až kým prejdeme cez všetky políčka mapy.

Príklad mapy a jej zobrazenie pred začiatkom hľadania ostrovov, po nájdení prvých troch ostrovov a po nájdení všetkých ostrovov:

11 17
 1 1 1 1 1 1 3 1 1 1 1 1 1 1 1 1 1
 1 1 1 3 3 3 3 1 1 3 1 1 1 1 3 3 1
 1 1 1 3 3 1 1 1 1 1 1 1 1 1 3 3 1
 1 1 3 3 1 1 1 1 1 3 3 1 3 3 3 3 1
 1 1 1 3 3 1 1 1 1 3 1 1 3 3 3 3 1
 1 3 3 3 3 1 3 3 1 3 3 1 3 3 3 3 1
 1 1 3 3 1 1 1 3 1 1 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 3 3 3 1 1 3 3 3 1 1
 1 1 1 1 1 1 1 3 3 3 1 1 3 1 3 1 1
 1 1 3 3 3 3 1 3 1 1 1 1 3 3 3 1 1
 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

Do programu z vyššia teda dorobíme funkciu

int najdiOstrovy(int m, int n, int **a, SVGdraw &drawing) {
    int ostrovov = 0;
    for (int i = 0; i <= m - 1; i++) {
        for (int j = 0; j <= n - 1; j++) {
            if (a[i][j] == 3) {
                ostrovov++;
                vyfarbi(m, n, a, i, j, 4, drawing);
            }
        }
    }
    return ostrovov;
}

a funkciu main môžeme zmeniť napríklad takto:

int main() {
    FILE *fr = fopen("ostrovy.txt", "r");
    assert(fr != NULL);
    int m, n;
    fscanf(fr, "%d", &m);
    fscanf(fr, "%d", &n);
    int **a = vytvorMaticu(m, n);
    nacitajMaticu(fr, m, n, a);
    fclose(fr);
        
    SVGdraw drawing(n * stvorcek, m * stvorcek, "mapa.svg");
    vykresliMaticu(m, n, a, drawing);

    int pocetOstrovov = najdiOstrovy(m, n, a, drawing);
    printf("Pocet ostrovov je %d.\n", pocetOstrovov);

    drawing.finish();
    zmazMaticu(m, n, a);
    
    return 0;
}

Cvičenie: upravte funkciu najdiOstrovy tak, aby ešte navyše zistila, či má niektorý z ostrovov jazero.

Vyfarbovanie s použitím zásobníka

S použitím niektorej implementácie zásobníka z minulej prednášky môžeme napísať aj nerekurzívnu verziu funkcie vyfarbi. Tá zakaždým vyberie zo zásobníka niektoré políčko. Ak ešte nebolo ofarbené, ofarbí ho a vloží na zásobník všetkých jeho susedov, ktorých je ešte potrebné ofarbiť.

Drobnou zmenou bude, že súradnice jednotlivých susedov budeme počítať s použitím cyklu for a polí deltaStlpec a deltaRiadok, ktoré pre i = 0,1,2,3 obsahujú posuny jednotlivých súradníc i-teho suseda oproti práve spracúvanému políčku.

struct policko {
    int riadok, stlpec;
};

typedef policko dataType;


/* Sem pride definicia struktury pre zasobnik a funkcii poskytovanych zasobnikom. */


const int deltaRiadok[4] = {0, 0, 1, -1};
const int deltaStlpec[4] = {1, -1, 0, 0};

/* Prefarbi suvislu jednofarebnu oblast obsahujucu poziciu (riadok,stlpec) na farbu s cislom farba. */  
void vyfarbi(int m, int n, int **a, int riadok, int stlpec, int farba, SVGdraw &drawing) {
    int staraFarba = a[riadok][stlpec];
    if (staraFarba == farba) {
        return;
    }
    
    stack s;
    init(s);
    
    policko p;
    p.riadok = riadok;
    p.stlpec = stlpec;
    push(s, p);
    
    while (!isEmpty(s)) {
        p = pop(s);
        if (a[p.riadok][p.stlpec] == farba) {
            continue;
        }
        a[p.riadok][p.stlpec] = farba;
        vykresliStvorcek(p.riadok, p.stlpec, farby[farba], "lightgrey", drawing);
        drawing.wait(pauza);
        for (int i = 0; i <= 3; i++) {
            policko sused;
            sused.riadok = p.riadok + deltaRiadok[i];
            sused.stlpec = p.stlpec + deltaStlpec[i];
            if (sused.riadok >= 0 && sused.riadok <= m - 1 && sused.stlpec >= 0 && sused.stlpec <= n - 1 &&
                    a[sused.riadok][sused.stlpec] == staraFarba) {
                push(s, sused);    
            }        
        }
    }
    destroy(s);
}

Vyfarbovanie s použitím radu

Namiesto zásobníka môžeme použiť aj rad – obrazec sa potom bude vyfarbovať v poradí podľa vzdialenosti od počiatočného políčka. Pôjde o takzvané prehľadávanie do šírky, kým rekurzívna verzia a verzia so zásobníkom zodpovedajú takzvanému prehľadávaniu do hĺbky.

struct policko {
    int riadok, stlpec;
};

typedef policko dataType;


/* Sem pride definicia struktury pre rad a funkcii poskytovanych radom. */


const int deltaRiadok[4] = {0, 0, 1, -1};
const int deltaStlpec[4] = {1, -1, 0, 0};

/* Prefarbi suvislu jednofarebnu oblast obsahujucu poziciu (riadok,stlpec) na farbu s cislom farba. */  
void vyfarbi(int m, int n, int **a, int riadok, int stlpec, int farba, SVGdraw &drawing) {
    int staraFarba = a[riadok][stlpec];
    if (staraFarba == farba) {
        return;
    }
    
    queue q;
    init(q);
    
    policko p;
    p.riadok = riadok;
    p.stlpec = stlpec;
    enqueue(q, p);
    
    while (!isEmpty(q)) {
        p = dequeue(q);
        if (a[p.riadok][p.stlpec] == farba) {
            continue;
        }
        a[p.riadok][p.stlpec] = farba;
        vykresliStvorcek(p.riadok, p.stlpec, farby[farba], "lightgrey", drawing);
        drawing.wait(pauza);
        for (int i = 0; i <= 3; i++) {
            policko sused;
            sused.riadok = p.riadok + deltaRiadok[i];
            sused.stlpec = p.stlpec + deltaStlpec[i];
            if (sused.riadok >= 0 && sused.riadok <= m - 1 && sused.stlpec >= 0 && sused.stlpec <= n - 1 &&
                    a[sused.riadok][sused.stlpec] == staraFarba) {
                enqueue(q, sused);    
            }        
        }
    }
    destroy(q);
}

Program potom môžeme upraviť aj tak, aby do každého ofarbeného políčka vypísal jeho vzdialenosť od počiatočného políčka:

struct policko {
    int riadok, stlpec, vzd;
};

typedef policko dataType;


/* Sem pride definicia struktury pre rad a funkcii poskytovanych radom. */


void vypisVzdialenost(int i, int j, int vzd, const char *farbaTextu, SVGdraw &drawing) {
    drawing.setLineColor(farbaTextu);
    drawing.setFontSize(20);
    char text[15];
    sprintf(text, "%d", vzd);
    drawing.drawText((j + 0.5) * stvorcek, (i + 0.5) * stvorcek, text);
}

const int deltaRiadok[4] = {0, 0, 1, -1};
const int deltaStlpec[4] = {1, -1, 0, 0};

/* Prefarbi suvislu jednofarebnu oblast obsahujucu poziciu (riadok,stlpec) na farbu s cislom farba. */  
void vyfarbi(int m, int n, int **a, int riadok, int stlpec, int farba, SVGdraw &drawing) {
    int staraFarba = a[riadok][stlpec];
    if (staraFarba == farba) {
        return;
    }
    
    queue q;
    init(q);
    
    policko p;
    p.riadok = riadok;
    p.stlpec = stlpec;
    p.vzd = 0;
    enqueue(q, p);
    
    while (!isEmpty(q)) {
        p = dequeue(q);
        if (a[p.riadok][p.stlpec] == farba) {
            continue;
        }
        a[p.riadok][p.stlpec] = farba;
        vykresliStvorcek(p.riadok, p.stlpec, farby[farba], "lightgrey", drawing);
        vypisVzdialenost(p.riadok, p.stlpec, p.vzd, "white", drawing);
        drawing.wait(pauza);
        for (int i = 0; i <= 3; i++) {
            policko sused;
            sused.riadok = p.riadok + deltaRiadok[i];
            sused.stlpec = p.stlpec + deltaStlpec[i];
            sused.vzd = p.vzd + 1;
            if (sused.riadok >= 0 && sused.riadok <= m - 1 && sused.stlpec >= 0 && sused.stlpec <= n - 1 &&
                    a[sused.riadok][sused.stlpec] == staraFarba) {
                enqueue(q, sused);    
            }        
        }
    }
    destroy(q);
}

Aritmetické výrazy

Nejaký čas sa teraz budeme venovať spracovaniu aritmetických výrazov pozostávajúcich z reálnych čísel, operácií +,-,*,/ a zátvoriek (,). Prakticky najdôležitejšou úlohou tu je vyhodnotenie daného výrazu; napríklad pre výraz

(65 – 3 * 5) / (2 + 3)

chceme vedieť povedať, že jeho hodnota je 10. K tejto úlohe sa však dostaneme až na nasledujúcej prednáške. Dnes len zavedieme a zľahka preskúmame dva alternatívne spôsoby zápisu aritmetických výrazov, z ktorých jeden nám nabudúce vyhodnocovanie výrazov značne uľahčí.

Infixová notácia

Bežný spôsob zápisu aritmetických výrazov v matematike sa zvykne nazývať aj infixovou notáciou (prípadne infixovou formou alebo infixovým zápisom). Toto pomenovanie odkazuje na skutočnosť, že binárne operátory (ako napríklad +,-,*,/) sa v tejto notácii píšu medzi svojimi dvoma operandmi. Poradie vykonávania jednotlivých operácií sa pritom riadi zátvorkami a prioritou operácií.

Napríklad

(65 – 3 * 5) / (2 + 3)

je infixový výraz s hodnotou 10.

Prefixová notácia

Pri takzvanej prefixovej notácii (často podľa jej pôvodcu Jana Łukasiewicza nazývanej aj poľskou notáciou) sa každý operátor v aritmetickom výraze zapisuje pred svojimi dvoma operandmi.

Napríklad výraz (65 – 3 * 5) / (2 + 3) má prefixový zápis

/ - 65 * 3 5 + 2 3

Postfixová notácia

Pri postfixovej notácii (často nazývanej aj obrátenou poľskou notáciou) je situácia opačná: operátor sa zapisuje bezprostredne za svojimi dvoma operandmi.

Výraz (65 – 3 * 5) / (2 + 3) má teda postfixový zápis

65 3 5 * - 2 3 + /

Postfixová a prefixová notácia sú pre človeka o niečo ťažšie čitateľné, než bežná notácia infixová (čo môže byť aj otázkou zvyku). Uvidíme však, že najmä výrazy v postfixovej notácii sa dajú pomerne jednoducho vyhodnocovať. Postfixová a prefixová notácia majú oproti infixovej notácii ešte jednu výhodu – nepotrebujú zátvorky. Nevýhodou naopak je, že tieto notácie nie sú konštruované na používanie unárnych operátorov.

V nasledujúcom teda budeme predpokladať, že výrazy neobsahujú unárne mínus. Tento nedostatok nie je nijak závažný – každý infixový podvýraz typu -cokolvek možno totiž ekvivalentne prepísať ako (0 - 1) * cokolvek.

Vyhodnocovanie postfixových zápisov aritmetických výrazov

Na vyhodnocovanie výrazov v postfixovej forme budeme používať zásobník, do ktorého budeme postupne vkladať jednotlivé operandy. Využijeme pritom vlastnosť, že operátor má v postfixovom zápise obidva operandy pred sebou. Keď teda narazíme na operátor, jeho operandy sme už prečítali a spracovali. Ide navyše o posledné dve prečítané alebo vypočítané hodnoty.

Výraz budeme postupne čítať zľava doprava:

  • Keď pritom narazíme na operand, vložíme ho na zásobník.
  • Keď narazíme na operátor:
    • Vyberieme zo zásobníka jeho dva operandy.
    • Prvé z týchto čísel je pritom druhým operandom a druhé z nich je prvým operandom.
    • Vykonáme s týmito operandmi danú operáciu a výsledok tejto operácie vložíme naspäť na zásobník.
  • Tento postup opakujeme, až kým neprídeme na koniec výrazu. V tom okamihu by sme mali mať na zásobníku jediný prvok – výslednú hodnotu výrazu.
#include <cstdio>
#include <cctype>
#include <cassert>

typedef double dataType;


/* Sem pride definicia struktury pre zasobnik a funkcii poskytovanych zasobnikom. */


int main() {
    FILE *fr = stdin;
        
    stack s;
    init(s);
    
    while (true) {
        int c = getc(fr);
        while (c != EOF && c != '\n' && isspace(c)) {
            c = getc(fr);
        }
        if (c == EOF || c == '\n') {
            break;
        } else if (isdigit(c)) {
            ungetc(c, fr);
            double num;
            fscanf(fr, "%lf", &num);
            push(s, num);
        } else {
            double num2 = pop(s);         // druhy operand vyberieme ako prvy ...
            double num1 = pop(s);         // ... a prvy operand ako druhy
            switch (c) {
                case '+':
                    push(s, num1 + num2);
                    break;
                case '-':
                    push(s, num1 - num2);
                    break;
                case '*':
                    push(s, num1 * num2);
                    break;    
                case '/':
                    push(s, num1 / num2);
                    break;    
            }
        }
    }
    
    printf("%f\n", pop(s));
    assert(isEmpty(s));

    destroy(s);
    return 0;
}

Nasledujúca alternatívna implementácia vyhodnocovania postfixových výrazov sa od predchádzajúcej líši tým, že vstupný výraz najprv uloží do reťazca, ktorý následne vyhodnotí prostredníctvom funkcie evaluatePostfix. Drobnou nevýhodou tohto prístupu je, že dĺžka vstupného reťazca je obmedzená konštantou; funkcia vyhodnocujúca výraz uložený v reťazci sa nám však nabudúce zíde.

#include <cstdio>
#include <cctype>
#include <cassert>

typedef double dataType;


/* Sem pride definicia struktury pre zasobnik a funkcii poskytovanych zasobnikom. */


double evaluatePostfix(char *expr) {
    stack s;
    init(s);
    
    int i = 0;
    while (true) {
        while (expr[i] != 0 && expr[i] != '\n' && isspace(expr[i])) {
            i++;
        }
        if (expr[i] == 0 || expr[i] == '\n') {
            break;
        } else if (isdigit(expr[i])) {
            double num;
            sscanf(&expr[i], "%lf", &num);
            push(s, num);
            while (isdigit(expr[i]) || expr[i] == '.') {
                i++;
            } 
        } else {
            double num2 = pop(s);         // druhy operand vyberieme ako prvy ...
            double num1 = pop(s);         // ... a prvy operand ako druhy
            switch (expr[i]) {
                case '+':
                    push(s, num1 + num2);
                    break;
                case '-':
                    push(s, num1 - num2);
                    break;
                case '*':
                    push(s, num1 * num2);
                    break;    
                case '/':
                    push(s, num1 / num2);
                    break;    
            }
            i++;
        }
    }
    double result = pop(s);
    assert(isEmpty(s));
    destroy(s);
    
    return result;
}

int main() {
    FILE *fr = stdin;
        
    char expr[100];    
    fgets(expr, 100, fr);
    printf("%f\n", evaluatePostfix(expr));    
    
    return 0;
}

Prednáška 19

Oznamy

  • Štvrtú domácu úlohu treba odovzdať do piatka 7. decembra, 22:00.
  • Tretia písomka bude v stredu 12. decembra o 18:10 v posluchárni B. Zameraná bude na látku prebranú v rámci prvých 18 prednášok (vrátane infixovej, prefixovej a postfixovej notácie pre aritmetické výrazy).
  • V štvrtej úlohe cvičení boli v kostre programu „skonzistentnené” parametre m a n tak, aby m vždy označovalo počet riadkov a n počet stĺpcov.

Prevod výrazu z infixovej notácie do postfixovej

Jednou z tém minulej prednášky bol program vyhodnocujúci postfixové výrazy. Oveľa zaujímavejšou a dôležitejšou úlohou je však vyhodnocovanie výrazov v klasickej infixovej notácii. V nasledujúcom túto úlohu vyriešime tým, že napíšeme program realizujúci prevod aritmetického výrazu z infixovej notácie do postfixovej. Výraz v infixovej notácii teda budeme môcť vyhodnotiť tak, že ho najprv prevedieme do postfixovej notácie a tento postfixový výraz následne vyhodnotíme algoritmom z minulej prednášky. (Elegantnejšie prístupy k vyhodnocovaniu aritmetických výrazov vyžadujú istú dávku teórie, s ktorou sa typický študent informatiky zoznámi vo vyšších ročníkoch štúdia.)

Algoritmus pre infixové výrazy neobsahujúce unárne mínus

Začneme tým, že prevod do postfixovej notácie realizujeme pre infixové výrazy neobsahujúce unárny operátor mínus – prítomnosť unárneho mínusu vo výslednom postfixovom výraze by totiž mohla viesť k nejednoznačnostiam.

Uvažujme najprv výrazy bez zátvoriek. Tie pozostávajú z čísel a binárnych operátorov +, -, *, /, kde * a / majú vyššiu prioritu (precedenciu) ako + a -. Všetky tieto operátory sú navyše zľava asociatívne. Na prevod takéhoto jednoduchého výrazu do postfixového tvaru ním teda stačí prejsť zľava doprava a všimnúť si dve skutočnosti:

  • Poradie jednotlivých čísel v postfixovom výraze je rovnaké, ako v pôvodnom infixovom výraze.
    • Napríklad výraz 1 + 2 + 3 * 4 - 5 má postfixový tvar 1 2 + 3 4 * + 5 -.
    • Z toho vyplýva, že pri prechádzaní vstupným infixovým výrazom možno čísla priamo vypisovať do výstupného postfixového výrazu bez toho, aby nás ďalej zaujímali.
  • Každý operátor treba presunúť spomedzi jeho dvoch argumentov za jeho druhý argument.
    • Ak teda vo vstupnom infixovom výraze narazíme na operátor, nevypíšeme ho hneď do výstupného výrazu, ale uložíme ho pre neskorší výpis na správnej pozícii.
    • Uložený operátor potom treba vypísať za jeho druhým argumentom. Ak pritom aj samotný tento argument obsahuje nejaké ďalšie operátory, určite musia byť vypísané skôr. Operátory teda budeme ukladať na zásobníku.
    • Zakaždým, keď vo vstupnom infixovom výraze narazíme na operátor, je možné, že tesne pred ním končí argument jedného alebo niekoľkých operátorov uložených na zásobníku. Vďaka ľavej asociatívnosti pritom ide o práve všetky operátory na vrchu zásobníka, ktoré majú vyššiu alebo rovnakú prioritu, ako nájdený operátor. Všetky tieto operátory teda postupne vyberieme zo zásobníka a vypíšeme ich do výstupného reťazca. Až následne na zásobník vložíme nájdený operátor.
    • Podobnú činnosť treba vykonať aj na konci vstupného reťazca – v takom prípade musíme vypísať všetky operátory na zásobníku.

Z technických dôvodov budeme na spodku zásobníka uchovávať „umelé dno” #, ktoré budeme chápať ako symbol s nižšou prioritou ako všetky operátory. Situáciu, keď pri vyberaní operátorov zo zásobníka narazíme na jeho dno tak budeme môcť riešiť konzistentne so situáciou, keď narazíme na operátor s nižšou prioritou – v oboch prípadoch chceme s vyberaním prestať.

Na vstupnom výraze 1 + 2 + 3 * 4 - 5 bude práve popísaný algoritmus pracovať nasledovne:

Krok     Vstupný symbol     Výstupný reťazec     Zásobník (dno naľavo)      
------------------------------------------------------------------------------------------------------------------------
   1                  1                          #                         vypíš číslo 1 na výstup 
                            1                    #
   2                  +     1                    #                         # má nižšiu prioritu ako +, nevyberaj ho
                      +     1                    #                         vlož + na zásobník
                            1                    # + 
   3                  2     1                    # +                       vypíš číslo 2 na výstup
                            1 2                  # + 
   4                  +     1 2                  # +                       + má rovnakú prioritu ako +, vyber ho a vypíš
                      +     1 2 +                #                         # má nižšiu prioritu ako +, nevyberaj ho
                      +     1 2 +                #                         vlož + na zásobník
                            1 2 +                # +   
   5                  3     1 2 +                # +                       vypíš číslo 3 na výstup
                            1 2 + 3              # + 
   6                  *     1 2 + 3              # +                       + má nižšiu prioritu ako *, nevyberaj ho
                      *     1 2 + 3              # +                       vlož * na zásobník
                            1 2 + 3              # + *     
   7                  4     1 2 + 3              # + *                     vypíš číslo 4 na výstup
                            1 2 + 3 4            # + *                     
   8                  -     1 2 + 3 4            # + *                     * má vyššiu prioritu ako -, vyber ho a vypíš
                      -     1 2 + 3 4 *          # +                       + má rovnakú prioritu ako -, vyber ho a vypíš
                      -     1 2 + 3 4 * +        #                         # má nižšiu prioritu ako -, nevyberaj ho
                      -     1 2 + 3 4 * +        #                         vlož - na zásobník
                            1 2 + 3 4 * +        # -             
   9                  5     1 2 + 3 4 * +        # -                       vypíš číslo 5 na výstup
                            1 2 + 3 4 * + 5      # -                 
  10    [koniec vstupu]     1 2 + 3 4 * + 5      # -                       vyber a vypíš operátory na zásobníku

                            1 2 + 3 4 * + 5 -    #                         <-- VÝSLEDNÝ POSTFIXOVÝ VÝRAZ
                           

Do tejto schémy je už jednoduché zapracovať aj zátvorky:

  • Zakaždým, keď vo vstupnom infixovom reťazci narazíme na ľavú zátvorku, vložíme ju do zásobníka. Podobne ako pri # ju budeme považovať za symbol s nižšou prioritou, než majú všetky binárne operátory (argument operátora spred tejto zátvorky totiž určite nemôže končiť v jej vnútri).
  • Keď naopak narazíme na pravú zátvorku, potrebujeme vypísať všetky doposiaľ nevypísané operátory uzatvorené touto zátvorkou. Preto v takom prípade vyberieme a vypíšeme na výstup všetky operátory zo zásobníka až po prvú ľavú zátvorku (ktorú zo zásobníka taktiež vyberieme).

Funkcia infixToPostfix, realizujúca prevod z infixovej do postfixovej notácie, tak môže vyzerať napríklad nasledovne:

#include <cstdio>
#include <cassert>
#include <cctype>

typedef char dataType;


/* Sem pride definicia struktury pre zasobnik a funkcii poskytovanych zasobnikom. */


int precedence(char op) {
    switch (op) {
        case '#':
        case '(':
            return 0;
            break;
        case '+':
        case '-':
            return 1;
            break;
        case '*':
        case '/':
            return 2;
            break;
    }
    return -1;  // specialne pre biele znaky vrati -1
}

/* Skonvertuje infixovy vyraz infix to postfixovej formy a vysledok ulozi do retazca postfix */
void infixToPostfix(const char *infix, char *postfix) {
    stack s;
    init(s);
    push(s, '#');
    
    int j = 0; // index do vystupneho retazca
    for (int i = 0; infix[i] != 0; i++) {
        if (isdigit(infix[i]) || infix[i] == '.') {
            postfix[j++] = infix[i];
        } 
        switch (infix[i]) {
            case '(':
                push(s, '(');
                break;
            case ')': 
                char c;
                while ((c = pop(s)) != '(') {
                    postfix[j++] = ' '; 
                    postfix[j++] = c;
                }
                break;
            default:
                int p = precedence(infix[i]);
                if (p == -1) {                      // predovsetkym biele znaky
                    break;
                }
                postfix[j++] = ' ';
                while (p <= precedence(peek(s))) {
                    postfix[j++] = pop(s);
                    postfix[j++] = ' ';
                }
                push(s, infix[i]);
                break;
        }
    }
    while (!isEmpty(s)) {
        char c = pop(s);
        if (c != '#') {
            postfix[j++] = ' ';
            postfix[j++] = c;
        }
    }
    postfix[j] = 0;
    
    destroy(s);
}

const int maxN = 100;

int main() {
    char infix[maxN];
    char postfix[2 * maxN];
    
    fgets(infix, maxN, stdin);
    infixToPostfix(infix, postfix);
    
    printf("Postfixovy tvar vyrazu: %s\n", postfix);
    
    return 0;
}

Cvičenie: Rozšírte program vyššie o operáciu umocňovania ^ s prioritou vyššou ako * (dajte si pritom pozor na fakt, že umocňovanie je narozdiel od operácií +, -, *, / sprava asociatívne).

Rozšírenie na infixové výrazy obsahujúce unárne mínus

Pri prevode infixového výrazu do postfixového tvaru sme predpokladali, že neobsahuje žiadny výskyt unárneho operátora -. V nasledujúcom tento nedostatok napravíme: napíšeme funkciu normalise, ktorá v danom infixovom výraze in nahradí všetky výskyty unárneho mínusu reťazcom (0-1)* a výsledok uloží do (opäť infixového) výrazu out. Mínus je pritom v korektnom infixovom výraze unárny práve vtedy, keď (modulo biele znaky, ktoré budeme ignorovať) nenasleduje za číslom, ani za pravou zátvorkou.

void normalise(const char *in, char *out) {
    int j = 0; // index do vystupneho retazca
    bool hasOperand = false;
    for (int i = 0; in[i] != 0; i++) {
        if (in[i] == '-' && !hasOperand) {
            out[j++] = '(';
            out[j++] = '0';
            out[j++] = '-';
            out[j++] = '1';
            out[j++] = ')';
            out[j++] = '*';
        } else if (isdigit(in[i]) || in[i] == '.' || in[i] == ')') {
            out[j++] = in[i];
            hasOperand = true;
        } else if (isspace(in[i])) {
            out[j++] = in[i];
        } else {
            out[j++] = in[i];
            hasOperand = false;
        }
    }
    out[j] = 0;
}

Prevod infixového výrazu s unárnymi mínusmi do postfixového tvaru teda môžeme realizovať tak, že najprv zavoláme funkciu normalise a následne funkciu infixToPostfix. (O niečo krajším riešením by bolo nahradenie unárnych mínusov nejakým novým špeciálnym znakom – v takom prípade by sa však musela zmeniť aj funkcia infixToPostfix.) Následne môžeme výsledný výraz aj vyhodnotiť pomocou funkcie evaluatePostfix z minulej prednášky:

const int maxN = 100;

int main() {
    char infix1[maxN];
    char infix2[6 * maxN];
    char postfix[12 * maxN];
    
    fgets(infix1, maxN, stdin);
  
    normalise(infix1, infix2);
    printf("Vyraz po odstraneni unarnych minusov: %s\n", infix2);
    
    infixToPostfix(infix2, postfix);
    printf("Vyraz v postfixovom tvare: %s\n", postfix);
    
    printf("Hodnota vyrazu: %f\n", evaluatePostfix(postfix));
    
    return 0;
}

Pri implementácii uvedeného programu sa však treba mať na pozore: kým totiž funkcia evaluatePostfix využíva zásobník prvkov typu double, funkcia infixToPostfix využíva zásobník prvkov typu char. Pri pokuse o skopírovanie kódu pre zásobník do zdrojového súboru tohto programu tak stojíme pred voľbou medzi

typedef char dataType;

a

typedef double dataType;

V tomto prípade ešte môžeme zvoliť druhú možnosť a za dataType považovať double; o zvyšok sa postará automatické pretypovanie znakov (čiže špeciálnych celých čísel) na reálne čísla a naopak. Toto riešenie má však ďaleko od ideálneho a pre iné dvojice typov nemusí riešenie tohto typu existovať vôbec. (Ozaj elegantný prístup k tomuto problému vyžaduje pokročilejšie programátorské techniky z letného semestra.)

Aritmetické stromy

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

Aritmetické výrazy možno reprezentovať aj vo forme stromu nazývaného aj aritmetickým stromom:

  • Operátory a čísla tvoria tzv. uzly stromu.
  • Operátory tvoria tzv. vnútorné uzly stromu – každý z nich má dvoch synov zodpovedajúcich podvýrazom pre jednotlivé operandy.
  • Čísla tvoria tzv. listy aritmetického stromu – tie už nemajú žiadnych synov.
  • Strom obsahuje jediný uzol, ktorý nie je synom žiadneho iného uzla – to je tzv. koreň stromu a reprezentuje celý aritmetický výraz.
  • Informatici stromy väčšinou kreslia „hore nohami”, s koreňom na vrchu.

Uzol aritmetického stromu tak môžeme reprezentovať napríklad nasledujúcou štruktúrou:

struct treeNode {
    double val;      // ciselna hodnota (zmysluplna len v listoch)
    char op;         // operator (zmysluplny len vo vnutornych uzloch, pre listy vzdy rovny medzere)
    treeNode *left;  // smernik koren podstromu reprezentujuceho lavy podvyraz
    treeNode *right; // smernik na koren podstromu reprezentujuceho pravy podvyraz
};

Pre vnútorné uzly stromu (zodpovedajúce operátorom) pritom:

  • Smerníky left a right budú ukazovať na korene podstromov reprezentujúcich ľavý resp. pravý podvýraz.
  • Znak op bude zodpovedať danému operátoru (napríklad '+').
  • Hodnota val ostane nevyužitá.

Pre listy (zodpovedajúce číselným hodnotám) naopak:

  • Smerníky left a right budú mať hodnotu NULL.
  • Znak op bude medzera ' ' (podľa op teda môžeme rozlišovať, či ide o číslo alebo o operátor).
  • Vo val bude uložená hodnota daného čísla.

Celý strom pritom budeme reprezentovať jeho koreňom.

Ide tu o jednoduchú, no nie veľmi elegantnú reprezentáciu aritmetických stromov, keďže viaceré položky štruktúry treeNode môžu byť nevyužité. S využitím objektového programovania (letný semester) možno aritmetické stromy reprezentovať omnoho krajšie.

Vytvorenie uzlu aritmetického stromu

Nasledujúce funkcie vytvoria nový vnútorný uzol (pre operátor) resp. nový list (pre číslo):

treeNode *createOp(char op, treeNode *left, treeNode *right) {
    treeNode *v = new treeNode;
    v->left = left;
    v->right = right;
    v->op = op;
    return v; 
}

treeNode *createNum(double val) {
    treeNode *v = new treeNode;
    v->left = NULL;
    v->right = NULL;
    v->op = ' ';
    v->val = val;
    return v;
}

„Ručne” teraz môžeme vytvoriť strom pre výraz (65 – 3 * 5)/(2 + 3):

treeNode *root = createOp('/', 
                   createOp('-', 
                     createNum(65),
                     createOp('*', createNum(3), createNum(5))),
                   createOp('+', createNum(2), createNum(3)));

Alebo po častiach:

node *v65 = createNum(65);
node *v3 = createNum(3);
node *v5 = createNum(5);
node *v2 = createNum(2);
node *v3b = createNum(3);
node *vKrat = createOp('*', v3, v5);
node *vMinus = createOp('-', v65, vKrat);
node *vPlus = createOp('+', v2, v3b);
node *vDeleno = createOp('/', vMinus, vPlus);

Uvoľnenie pamäte

Nasledujúca funkcia uvoľní z pamäte celý strom s koreňom root:

void destroyTree(treeNode *root) {
    if (root != NULL) {
        destroyTree(root->left);
        destroyTree(root->right);
        delete root;
    }
}

Vyhodnotenie výrazu reprezentovaného stromom

Nasledujúca rekurzívna funkcia vypočíta hodnotu aritmetického výrazu reprezentovaného stromom s koreňom root:

double evaluateTree(treeNode *root) {
    assert(root != NULL);
    if (root->op == ' ') {
        return root->val;
    } else {
        double valLeft = evaluateTree(root->left);
        double valRight = evaluateTree(root->right);
        switch (root->op) {
            case '+':
                return valLeft + valRight;
                break;
            case '-':
                return valLeft - valRight;
                break;
            case '*':
                return valLeft * valRight;
                break;
            case '/':
                return valLeft / valRight;
                break;
            default:
                assert(false);
                break;
        }
    }
}

Vypísanie výrazu reprezentovaného stromom v rôznych notáciách

Infixovú, prefixovú, resp. postfixovú reprezentáciu aritmetického výrazu reprezentovaného stromom s koreňom root možno získať pomocou nasledujúcich funkcií:

void printInorder(FILE *fw, treeNode *root) {
    if (root->op == ' ') {
        fprintf(fw, "%.2f", root->val);
    } else {
        fprintf(fw, "(");
        printInorder(fw, root->left);
        fprintf(fw, " %c ", root->op);
        printInorder(fw, root->right);
        fprintf(fw, ")");
    }
}
void printPreorder(FILE *fw, treeNode *root) {
    if (root->op == ' ') {
        fprintf(fw, "%.2f ", root->val);
    } else {
        fprintf(fw, "%c ", root->op);
        printPreorder(fw, root->left);
        printPreorder(fw, root->right);
    }
}
void printPostorder(FILE *fw, treeNode *root) {
    if (root->op == ' ') {
        fprintf(fw, "%.2f ", root->val);
    } else {
        printPostorder(fw, root->left);
        printPostorder(fw, root->right);
        fprintf(fw, "%c ", root->op);
    }
}

Vytvorenie stromu z postfixového výrazu

Pripomeňme si z minulej prednášky funkciu na vyhodnocovanie postfixového výrazu:

typedef double dataType;


/* Sem pride definicia struktury pre zasobnik a vsetkych funkcii poskytovanych zasobnikom. */


double evaluatePostfix(char *expr) {
    stack s;
    init(s);
    
    int i = 0;
    while (true) {
        while (expr[i] != 0 && expr[i] != '\n' && isspace(expr[i])) {
            i++;
        }
        if (expr[i] == 0 || expr[i] == '\n') {
            break;
        } else if (isdigit(expr[i])) {
            double num;
            sscanf(&expr[i], "%lf", &num);
            push(s, num);
            while (isdigit(expr[i]) || expr[i] == '.') {
                i++;
            } 
        } else {
            double num2 = pop(s);         
            double num1 = pop(s);         
            switch (expr[i]) {
                case '+':
                    push(s, num1 + num2);
                    break;
                case '-':
                    push(s, num1 - num2);
                    break;
                case '*':
                    push(s, num1 * num2);
                    break;    
                case '/':
                    push(s, num1 / num2);
                    break;    
            }
            i++;
        }
    }
    double result = pop(s);
    assert(isEmpty(s));
    destroy(s);
    
    return result;
}

Túto funkciu možno jednoducho prepísať tak, aby namiesto vyhodnocovania výrazu konštruovala zodpovedajúci aritmetický strom. Namiesto hodnôt jednotlivých podvýrazov stačí na zásobníku uchovávať korene stromov, ktoré tieto podvýrazy reprezentujú. Aplikácii aritmetickej operácie potom bude zodpovedať „spojenie” dvoch podstromov do jedného stromu:

typedef treeNode *dataType;


/* Sem pride definicia struktury pre zasobnik a vsetkych funkcii poskytovanych zasobnikom. */


treeNode *parsePostfix(char *expr) {
    stack s;
    init(s);
    
    int i = 0;
    while (true) {
        while (expr[i] != 0 && expr[i] != '\n' && isspace(expr[i])) {
            i++;
        }
        if (expr[i] == 0 || expr[i] == '\n') {
            break;
        } else if (isdigit(expr[i])) {
            double num;
            sscanf(&expr[i], "%lf", &num);
            push(s, createNum(num));
            while (isdigit(expr[i]) || expr[i] == '.') {
                i++;
            } 
        } else {
            treeNode *right = pop(s);         
            treeNode *left = pop(s);         
            push(s, createOp(expr[i], left, right));
            i++;
        }
    }
    treeNode *result = pop(s);
    assert(isEmpty(s));
    destroy(s);
    
    return result;
}

Ukážkový program pracujúci s aritmetickými stromami

Nasledujúci program prečíta z konzoly aritmetický výraz v postfixovom tvare, skonštruuje jeho aritmetický strom a následne preň zavolá funkcie na výpočet hodnoty výrazu a jeho výpis v rôznych notáciách:

int main() {           
    char expr[100];    
    fgets(expr, 100, stdin);
    treeNode *root = parsePostfix(expr); 

    printf("Hodnota vyrazu je: %.2f\n", evaluateTree(root));
    printf("Vyraz v infixovej notacii: ");
    printInorder(stdout, root);
    printf("\n");
    printf("Vyraz v prefixovej notacii: ");
    printPreorder(stdout, root);
    printf("\n");
    printf("Vyraz v postfixovej notacii: ");
    printPostorder(stdout, root);
    printf("\n");
      
    destroyTree(root);  
      
    return 0;
}

Prednáška 20

Oznamy

  • Tretia písomka bude v stredu 12. decembra o 18:10 v posluchárni B. Zameraná bude na látku prebranú v rámci prvých 18 prednášok (vrátane infixovej, prefixovej a postfixovej notácie pre aritmetické výrazy).
  • Termíny skúšok boli stanovené nasledovne:
    • V pondelok 7. januára 2019 o 9:00 v miestnosti I-H6.
    • V utorok 22. januára 2019 o 9:00 v miestnosti I-H6.
    • V pondelok 4. februára 2019 o 9:00 v miestnosti I-H6 (predposledný termín v tomto semestri určený najmä pre opravné termíny).
    • Posledný termín bude niekedy v poslednom týždni skúškového obdobia (upresní sa neskôr).
  • Prihlasovanie na skúšky v AIS bude otvorené od dnešného poludnia. Na každý termín sa treba prihlásiť najneskôr 24 hodín pred začiatkom skúšky.
  • V pondelok 17.12. budú na prednáške informácie o skúške z programovania, ako aj rady od tútorov o skúškach všeobecne.

Binárne stromy

Na minulej prednáške sme sa stretli s aritmetickými stromami, ktoré sú špeciálnym prípadom binárnych stromov. V informatike nachádzajú binárne stromy množstvo rozličných uplatnení – na úvod dnešnej prednášky sa preto zameriame na všeobecnú dátovú štruktúru binárneho stromu. V tomto všeobecnejšom kontexte si pritom aj zopakujeme niektoré techniky, ktoré sme na minulej prednáške používali pre špeciálny prípad aritmetických stromov.

Terminológia stromov

Pod stromom budeme rozumieť množinu vrcholov pospájaných hranami tak, aby každá dvojica vrcholov bola spojená práve jednou postupnosťou hrán (vrcholy teda nemôžu byť pospájané cyklicky). Navyše nás na tomto predmete budú zaujímať iba zakorenené stromy, ktoré obsahujú práve jeden špeciálny vrchol nazývaný koreňom. Vždy, keď budeme hovoriť o stromoch, budeme mať na mysli zakorenené stromy. V súvislosti s nimi budeme o vrcholoch väčšinou hovoriť ako o uzloch.

Poznámka. Takáto definícia stromov nie je úplne matematicky presná (ide o „popularizáciu” ozajstnej definície). Poriadne definície pojmov súvisiacich so stromami patria do náplne predmetu Úvod do kombinatoriky a teórie grafov (letný semester).

Každý uzol u zakoreneného stromu okrem koreňa je spojený hranou s práve jedným otcom (alebo rodičom; angl. parent), ktorým je nejaký uzol v (ide o jediný vrchol stromu spojený hranou s uzlom u, ktorý je bližšie ku koreňu, než uzol u). Naopak potom hovoríme, že uzol u je synom (alebo dieťaťom; angl. child resp. son) uzla v. Vo všeobecnom zakorenenom strome pritom môže mať každý uzol ľubovoľný prirodzený (a teda aj nulový) počet synov. Na tomto predmete budeme navyše predpokladať, že pre každý vrchol je dané nejaké úplné usporiadanie jeho synov (možno teda rozlišovať medzi „najľavejším” synom, synom druhým zľava, atď.). Strom je binárny, ak má každý uzol najviac dvoch synov. Budeme pritom rozlišovať medzi pravým a ľavým synom – každý uzol má najviac jedného ľavého a najviac jedného pravého syna.

Na tomto predmete budeme odteraz pod stromom vždy rozumieť zakorenený binárny strom s rozlišovaním medzi pravými a ľavými synmi.

Predkom uzla u nazveme ľubovoľný uzol v rôzny od u ležiaci na nejakej ceste z u do koreňa stromu. Naopak potom hovoríme, že u je potomkom uzla v. Uzly zakoreneného stromu, ktoré nemajú žiadneho syna, nazývame listami; zvyšné uzly potom nazývame vnútornými uzlami.

Podstromom stromu T zakoreneným v nejakom uzle v stromu T budeme rozumieť strom s koreňom v pozostávajúci z uzla v, všetkých jeho potomkov a všetkých hrán stromu T vedúcich medzi týmito uzlami.

Každý binárny strom je teda buď prázdny, alebo je tvorený jeho koreňom a dvoma podstromami – ľavým a pravým.

Štruktúra pre uzol binárneho stromu

V nasledujúcom budeme pracovať výhradne s binárnymi stromami. So špeciálnym prípadom takýchto stromov – s aritmetickými stormami – sme sa už stretli na minulej prednáške. Štruktúra pre uzol všeobecného binárneho stromu je podobná, ako pri aritmetických stromoch – namiesto operátora alebo hodnoty si však v každom uzle môžeme pamätať hodnotu ľubovoľného typu dataType.

Keďže neskôr budeme binárne stromy vypisovať, budeme predpokladať, že pre hodnoty typu dataType máme k dispozícii funkciu printDataType, ktorá ich v nejakom vhodnom formáte vypisuje. Nasledujúci kus kódu zodpovedá situácii, keď dataType je int.

#include <cstdio>


// ...


/* Typ prvkov ukladanych v uzloch binarneho stromu -- moze byt prakticky lubovolny */
typedef int dataType;          

/* Funkcia pre vypis hodnoty typu dataType */
void printDataType(dataType d) {
    printf("%d ", d);  // pre int
}


// ...


/* Uzol binarneho stromu */
struct node {
    dataType data;  // hodnota ulozena v danom uzle
    node *left;     // smernik na laveho syna (NULL, ak tento syn neexistuje)
    node *right;    // smernik na praveho syna (NULL, ak tento syn neexistuje)
};

Vytvorenie binárneho stromu

Nasledujúca funkcia vytvorí uzol binárneho stromu s dátami data, ľavým podstromom zakoreneným v uzle *left a pravým podstromom zakoreneným v uzle *right (parametre left a right sú teda smerníkmi na uzly). Ako výstup funkcia vráti smerník na novovytvorený uzol.

/* Vytvori uzol binarneho stromu */
node *createNode(dataType data, node *left, node *right) {
    node *v = new node;
    v->data = data;
    v->left = left;
    v->right = right;
    return v;
}

Nasledujúca volanie tak napríklad vytvorí binárny strom so šiestimi uzlami zakorenený v uzle *root.

node *root = createNode(1,
                   createNode(2, 
                     createNode(3, NULL, NULL),
                     createNode(4,NULL,NULL)),
                   createNode(5,
                     NULL, 
                     createNode(6, NULL, NULL)));

Cvičenie: nakreslite binárny strom vytvorený predchádzajúcim volaním.

Likvidácia binárneho stromu

Nasledujúca rekurzívna funkcia zlikviduje celý podstrom zakorenený v uzle *root (t. j. pouvoľňuje pamäť pre všetky jeho uzly).

/* Zlikviduje podstrom s korenom *root (pouvolnuje pamat) */
void destroyTree(node *root) {
    if (root != NULL) {
        destroyTree(root->left);
        destroyTree(root->right);
        delete root;
    }
}

Výška binárneho stromu

Hĺbkou uzla binárneho stromu nazveme jeho vzdialenosť od koreňa. Koreň má teda hĺbku 0, jeho synovia majú hĺbku 1, atď. Výškou binárneho stromu (niekedy nazývanou aj jeho hĺbkou) potom nazveme maximálnu hĺbku niektorého z jeho vrcholov. Strom s jediným vrcholom má teda hĺbku 0; pre ostatné stromy je ich výška daná ako 1 plus maximum z výšok ľavého a pravého podstromu.

Nasledujúca funkcia počíta výšku stromu (kvôli elegancii zápisu pritom pracuje s rozšírením definície výšky stromu na prázdne stromy, za ktorých výšku sa považuje číslo -1).

/* Spocita vysku podstromu s korenom *root. Pre root == NULL vrati -1. */
int height(node *root) {
    if (root == NULL) {
        return -1;
    }
    int hLeft = height(root->left);    // rekurzivne spocita vysku laveho podstromu
    int hRight = height(root->right);  // rekurzivne spocita vysku praveho podstromu
    if (hLeft >= hRight) {             // vrati max(hLeft, hRight) + 1
        return hLeft + 1;
    } else {
        return hRight + 1;
    }
}

Pre výšku h stromu s n uzlami pritom platia nasledujúce vzťahy:

  • Určite Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle h \leq n-1} . Tento prípad nastáva, ak sú všetky uzly „navešané jeden pod druhý”.
  • Strom s výškou h má najviac Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle \sum_{j=0}^h 2^j = 2^{h+1}-1} uzlov (ako možno ľahko dokázať indukciou vzhľadom na h).
  • Z toho Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle h \geq \log_2(n+1)-1} .
  • Dostávame teda Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle \log_2(n+1)-1 \leq h \leq n-1} .
  • Napríklad strom s milión vrcholmi má teda hĺbku medzi 19 a 999999.

Prehľadávanie stromov a vypisovanie ich uzlov

Často je potrebné prejsť celý strom a spracovať (napríklad vypísať) hodnoty vo všetkých uzloch. Toto prehľadávanie možno, podobne ako pri aritmetických stromoch, realizovať v troch základných poradiach: preorder, inorder a postorder.

/* Vypise podstrom s korenom *root v poradi preorder */
void printPreorder(node *root) {
    if (root == NULL) {
        return;
    }
    printDataType(root->data);
    printPreorder(root->left);
    printPreorder(root->right);
}

/* Vypise podstrom s korenom *root v poradi inorder */
void printInorder(node *root) {
    if (root == NULL) {
        return;
    }
    printInorder(root->left);
    printDataType(root->data);
    printInorder(root->right);
}

/* Vypise podstrom s korenom *root v poradi postorder */
void printPostorder(node *root) {
    if (root == NULL) {
        return;
    }
    printPostorder(root->left);
    printPostorder(root->right);
    printDataType(root->data);
}

Príklad: úplné binárne stromy

Binárny strom výšky h s maximálnym počtom vrcholov Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle 2^{h+1}-1} sa nazýva úplny binárny strom. Nasledujúca funkcia createCompleteTree vytvorí takýto strom a vráti smerník na jeho koreň. Jeho uzlom pritom priradí po dvoch rôzne hodnoty typu int (predpokladáme teda, že dataType je int) podľa globálnej premennej count.

// ...

int count;

/* Vytvori uplny binarny strom vysky height s datami uzlov count, count + 1, ... */ 
node *createCompleteTree(int height) {    
    if (height == -1) {
        return NULL;
    }
    node *v = createNode(count++, NULL, NULL);
    v->left = createCompleteTree(height - 1);
    v->right = createCompleteTree(height - 1);
    return v;
}

int main() {
    count = 1;
    node *root = createCompleteTree(3);
                     
    printf("Vyska: %d\n", height(root));                 
    printf("Inorder: ");
    printInorder(root);
    printf("\n");
    printf("Preorder: ");
    printPreorder(root);
    printf("\n");
    printf("Postorder: ");
    printPostorder(root);
    printf("\n");
                     
    destroyTree(root);                 
    return 0;
}

Cvičenie: popíšte poradie, v ktorom sa v uvedenom programe jednotlivým uzlom priraďujú ich hodnoty.

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 množiny ako abstraktného dátového typu.

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

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.

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>


// ...


int main() {
    binarySearchTree t;
    bstInit(t);
    char command[20];
    int key;
    while (true) {
        scanf("%19s", command);
        if (strcmp(command, "insert") == 0) {
            scanf("%d", &key);
            bstInsert(t, key);
        }
        if (strcmp(command, "find") == 0) {
            scanf("%d", &key);
            bool b = bstFind(t, key);
            if (b) {
                printf("YES\n");
            } else {
                printf("NO\n");
            }
        }
        if (strcmp(command, "min") == 0) {
            printf("%d\n", bstMin(t));
        }
        if (strcmp(command, "exit") == 0) {
            break;
        }
    }
    bstDestroy(t);                     
    return 0;
}

Prednáška 21

Oznamy

  • Dnes o 18:10 bude v posluchárni B tretia písomka.
  • V pondelok 17. decembra bude prednáška venovaná informáciám o skúške z programovania a o skúškach vo všeobecnosti.
  • Prípadný termín opravnej písomky dohodneme na prednáške v stredu 19. decembra.

Pokračovanie binárnych vyhľadávacích stromov

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.
/* 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í

  • Zložitosť operácií bstFind(t), bstInsert(t) aj bstRemove(t) je O(height(t)), kde height(t) je výška stromu t.
  • Vyššie sme ukázali, že pre výšku h stromu s n vrcholmi platí Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle \log_2(n+1)-1 \leq h \leq n-1} .
  • Zložitosť uvedených operácií je teda v najhoršom prípade lineárna (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ť O(log n).
  • 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í O(log n) 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>


// ...


int main() {
    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;
}

Lexikografické stromy

Lexikografický strom reprezentujúci množinu reťazcov a, aj, ale, aleba, alebo, cez, na, nad.

Lexikografické stromy (niekde tiež prefixové stromy; angl. trie zo slova retrieval) sú dátová štruktúra na uchovávanie množiny reťazcov. Ide o stromy, ktoré nemusia byť binárne:

  • Každý uzol lexikografického stromu má najviac toľko synov, koľko je písmen v uvažovanej abecede. Každý zo synov zodpovedá práve jednému písmenu abecedy a každému písmenu abecedy zodpovedá najviac jeden syn daného uzla. Graficky možno písmená zodpovedajúce jednotlivým synom znázorniť ohodnotením hrán spájajúcich dané uzly.
  • Koreň lexikografického stromu zodpovedá prázdnemu reťazcu.
  • Uzol v hĺbke k zodpovedá reťazcu dĺžky k, ktorý dostaneme prečítaním písmen na ceste z koreňa do daného uzla.
  • Každý uzol lexikografického stromu obsahuje informáciu (true alebo false) o tom, či k nemu prislúchajúci reťazec patrí do množiny reprezentovanej týmto lexikografickým stromom alebo nie.
  • V korektnom lexikografickom strome všetky listy zodpovedajú reťazcom z reprezentovanej množiny; niektoré reťazce reprezentovanej množiny však môžu zodpovedať aj vnútorným vrcholom stromu. Každý uzol lexikografického stromu tak zodpovedá nejakému prefixu slova z ním reprezentovanej množiny – odtiaľ alternatívne pomenovanie prefixový strom.

Štruktúra node pre uzol lexikografického stromu tak obsahuje booleovskú premennú isWord, v ktorej je uložená informácia o tom, či reťazec prislúchajúci k danému uzlu patrí alebo nepatrí do reprezentovanej množiny a pole children smerníkov na jednotlivých synov daného uzla. Veľkosť alphSize tohto poľa je rovná veľkosti uvažovanej abecedy – pre písmená malej anglickej abecedy je táto hodnota rovná 'z' - 'a' + 1.

Pri rôznych praktických aplikáciách môže štruktúra node obsahovať aj rozličné ďalšie informácie – pre každé slovo z reprezentovanej množiny si napríklad môžeme pamätať jeho frekvenciu výskytu, preklad do nejakého iného jazyka a podobne.

const int alphSize = 'z' - 'a' + 1;

struct node {
    node *children[alphSize]; // pole smernikov na deti
    bool isWord;              // udava, ci uzol prislucha k slovu z reprezentovanej mnoziny
    // dalsie data (napriklad preklad slova, pocet vyskytov slova v texte, ...)
};

Samotný lexikografický strom je potom daný iba smerníkom na svoj koreň:

struct trie {
    node *root;      
};

Inicializácia lexikografického stromu

Nasledujúca funkcia inicializuje prázdny lexikografický strom t:

void trieInit(trie &t) {
    t.root = NULL; 
}

Likvidácia lexikografického stromu

Uvoľnenie pamäte alokovanej pre podstrom zakorenený v uzle root realizujeme obdobne ako pri binárnych vyhľadávacích stromoch. Jediný rozdiel spočíva v potenciálne väčšom počte synov uzla root.

void destroySubtree(node *root) {
    if (root != NULL) {
        for (int i = 0; i <= alphSize - 1; i++) {
            destroySubtree(root->children[i]);
        }
        delete root;
    }
}

Nasledujúca funkcia potom zlikviduje celý lexikografický strom t:

void trieDestroy(trie &t) {
    destroySubtree(t.root);
}

Vkladanie do lexikografického stromu

Pri vkladaní reťazca do množiny realizovanej lexikografickým stromom často vznikne potreba vytvárať nové uzly tohto stromu. Túto podúlohu realizuje funkcia createNode, ktorá vytvorí nový uzol s hodnotou isWord danou jej argumentom a so všetkými smerníkmi na synov rovnými NULL.

node *createNode(bool isWord) {
    node *v = new node;
    for (int i = 0; i <= alphSize - 1; i++) {
        v->children[i] = NULL;
    }
    v->isWord = isWord;
    return v;
}

Vloženie reťazca word do lexikografického stromu t potom realizuje funkcia trieInsert, ktorá pracuje nasledovne:

  • Začne v koreni stromu, odkiaľ postupuje nižšie smerom k listom.
  • V každom uzle sa pozrie na ďalšie písmeno slova word. Ak danému uzlu chýba syn pre toto písmeno, vytvorí ho pomocou funkcie createNode. Následne sa presunie do tohto syna.
  • Ak v nejakom uzle v príde na koniec slova word, nastaví hodnotu v->isWord na true.
void trieInsert(trie &t, const char *word) {
    if (t.root == NULL) {
        t.root = createNode(false);
    }
    node *v = t.root;
    for (int i = 0; word[i] != 0; i++) {
        int c = word[i] - 'a';
        if (v->children[c] == NULL) {
            v->children[c] = createNode(false);
        }
        v = v->children[c];
    }
    v->isWord = true;
}

Hľadanie v lexikografickom strome

Funkcia treeFind pre daný lexikografický strom t a reťazec word zistí, či slovo word patrí do množiny reprezentovanej stromom t. Opäť pritom postupuje po písmenách reťazca word. Kým nedôjde na koniec slova, snaží sa ísť po hranách, ktoré zodpovedajú jednotlivým písmenám. V prípade, že v niektorom bode narazí na NULL, slovo word sa v strome nenachádza. V opačnom prípade toto slovo dočíta v nejakom uzle v. V takom prípade slovo word patrí do reprezentovanej množiny práve vtedy, keď v->isWord má hodnotu true.

bool trieFind(trie t, const char *word) {
    node *v = t.root;
    if (v == NULL) {
        return false;
    }
    for (int i = 0; word[i] != 0; i++) {
        v = v->children[word[i] - 'a'];
        if (v == NULL) {
            return false;
        }
    }
    return v->isWord;
}

Vymazávanie z lexikografického stromu

Vymazávanie slov z množiny reprezentovanej lexikografickým stromom budeme realizovať prostredníctvom pomocnej rekurzívnej funkcie removeFromSubtree, ktorá z podstromu zakorenenom v uzle *root vymaže sufix reťazca word začínajúci na pozícii index. Táto funkcia vráti na výstupe booleovskú hodnotu podľa toho, či sa pri tomto vymazaní slova z množiny súčasne aj vymazal zodpovedajúci uzol stromu (táto situácia nenastane vždy: napríklad pri mazaní slova a z množiny obsahujúcej slová a,ab uzol prislúchajúci k reťazcu a v strome ostáva; zmení sa len jeho hodnota isWord).

Ak sa slovo word v reprezentovanej množine nenachádza, funkcia removeFromSubtree vyhlási chybu pomocou funkcie assert.

Funkcia removeFromSubtree pracuje nasledovne:

  • Ak je sufix reťazca word začínajúci na indexe index prázdny, nastaví hodnotu root->isWord na false.
  • V opačnom prípade funkcia removeFromSubtree zavolá rekurzívne samú seba pre syna zodpovedajúceho písmenu na pozícii index reťazca word. Ak toto volanie daného syna zmaže, prestaví smerník na tohto syna na NULL.
  • V prípade, že po vykonaní jednej z predchádzajúcich dvoch operácií nemá uzol root žiadneho syna a súčasne má root->isWord hodnotu false, uvoľní pamäť alokovanú pre uzol root a informáciu o jeho zmazaní vráti na výstupe.
bool removeFromSubtree(node *root, const char *word, int index) {
    assert(root != NULL);
    if (word[index] == 0) {
        assert(root->isWord);
        root->isWord = false;
    } else {
        int c = word[index] - 'a';
        bool deleted = removeFromSubtree(root->children[c], word, index + 1);
        if (deleted) {          
            root->children[c] = NULL;
        }
    }
    int numChildren = 0;                        
    for (int i = 0; i <= alphSize - 1; i++) {
        if (root->children[i] != NULL) {
            numChildren++;
        }
    }
    if (numChildren == 0 && !root->isWord) {
        delete root;
        return true;
    }
    return false;
}

Samotné odstránenie reťazca word z množiny reprezentovanej stromom t potom realizuje nasledujúca funkcia trieRemove. Tá len zavolá funkciu removeFromSubtree pre koreň stromu t a v prípade, že volanie tejto funkcie koreň zo stromu odstráni, nastaví t.root na NULL.

void trieRemove(trie &t, const char *word) {
    bool rootRemoved = removeFromSubtree(t.root, word, 0);
    if (rootRemoved) {
        t.root = NULL;
    }
}

Výška lexikografického stromu

Nasledujúca funkcia vypočíta výšku podstromu zakoreneného v uzle *root (definovaná je rovnako ako pre binárne stromy):

int subtreeHeight(node *root) {
    if (root == NULL) {
        return -1;
    }
    int maxHeight = -1;
    for (int i = 0; i <= alphSize - 1; i++) {
        int height = subtreeHeight(root->children[i]);
        if (height > maxHeight) {
            maxHeight = height;
        }
    }
    return maxHeight + 1;
}

Výšku samotného lexikografického stromu t potom spočíta nasledujúca funkcia:

int trieHeight(trie t) {
    return subtreeHeight(t.root);
}

Vypisovanie slov reprezentovaných lexikografickým stromom

Nasledujúca funkcia printSubtree prehľadáva podstrom zakorenený v uzle *root a v reťazci s postupne generuje všetky slová z reprezentovanej množiny, ktoré zároveň vypisuje na konzolu.

void printSubtree(node *root, char *s, int index) {
    if (root == NULL) {
        return;
    }
    if (root->isWord) {
        s[index] = 0;
        printf("%s\n", s);
    }
    for (int i = 0; i <= alphSize - 1; i++) {
        s[index] = 'a' + i;
        printSubtree(root->children[i], s, index + 1);
    }
}

Funkcia triePrint vypisujúca všetky slová v množine reprezentovanej lexikografickým stromom t najprv spočíta výšku stromu t, ktorá je rovná dĺžke najdlšieho reťazca tejto množiny. Následne dynamicky alokuje reťazec dostatočnej dĺžky na uchovanie každého slova množiny a zavolá funkciu printSubtree pre koreň stromu t.

void triePrint(trie t) {
    int height = trieHeight(t);
    if (height >= 0) { 
        char *s = new char[height + 1];
        printSubtree(t.root, s, 0);
        delete[] s;
    }
}

Program pracujúci s lexikografickými stromami

Nasledujúci program načítava z konzoly príkazy zodpovedajúce operáciám na lexikografickom strome a za každým načítaným príkazom túto operáciu vykonáva.

#include <cstdio>
#include <cassert>
#include <cstring>


// ...


int main() {
    trie t;
    trieInit(t);
    char command[20];
    char arg[20];
    while (true) {
        scanf("%19s", command);
        if (strcmp(command, "insert") == 0) {
            scanf("%19s", arg);
            trieInsert(t, arg);
        }
        if (strcmp(command, "find") == 0) {
            scanf("%19s", arg);
            bool b = trieFind(t, arg);
            if (b) {
                printf("YES\n");
            } else {
                printf("NO\n");
            }
        }
        if (strcmp(command, "remove") == 0) {
            scanf("%19s", arg);
            trieRemove(t, arg);
        }
        if (strcmp(command, "height") == 0) {
            printf("%d\n", trieHeight(t));
        }
        if (strcmp(command, "print") == 0) {
            triePrint(t);
        }
        if (strcmp(command, "exit") == 0) {
            break;
        }
    }
    trieDestroy(t);
    return 0;
}

Prednáška 22

Oznamy

Tento týždeň:

  • Dnešná prednáška: informácia o skúškach od tútorov, detaily skúšky z programovania, opakovanie, vaše otázky, precvičovanie učiva (napr. tieto úlohy)
  • Cvičenia tento týždeň: už dnes po prednáške sa objavia tréningové príklady na skúšku. Jeden z nich je rozcvička, môžete riešiť až do konca doplnkových cvičení v stredu. Niekoľko ďalších príkladov má bonusové body, ak ich vyriešite do 4.1. (ako tréning sa dajú riešiť aj neskôr). Na hlavných aj doplnkových cvičeniach odporúčame riešiť tieto príklady.
  • Streda prednáška: nepreberané črty C resp. C++. Nebudeme skúšať, ale môžu sa zísť.
  • Pred skúškou si skontrolujte body na testovači. Prípadné reklamácie pošlite na prog@.... Na skúške už reklamácie bodov z cvičení a domácich úloh nebudeme riešiť.
  • Ak ste namiesto úloh robili rýchlostné programovanie, nezabudnite do 21.12. odovzdať zoznam príkladov vyriešených v C alebo C++.

Iné

  • Do konca skúškového môžete vypĺňať študentskú anketu: https://anketa.uniba.sk/fmph/ Tešíme sa na konštruktívne návrhy, ktoré nám pomôžu zlepšiť predmet do budúcnosti.
  • Ak by ste mali záujem v januári o konzultácie pred opravným testom alebo pred skúškou, dajte vedieť.

Sylaby predmetu

Základy

Konštrukcie jazyka C

  • premenné typov int, double, char, bool (vzťah int a char)
  • podmienky (if, else, switch), cykly (for, while)
  • funkcie (a parametre funkcií - odovzdávanie hodnotou, referenciou, smerníkom)
void f1(int x){}                                 //hodnotou
void f2(int &x){}                                //referenciou
void f3(int* x){}                                //smerníkom
void f(int a[], int n){}                         //polia bez & (ostanú zmeny)
void kresli(Turtle &t){}                         //korytnačky, SVGdraw a pod. s &

Polia, reťazce (char[])

int A[4]={3, 6, 8, 10}; 
int B[4];               
B[0]=3; B[1]=6; B[2]=8; B[3]=10;

char C[100] = "pes";
char D[100] = {'p', 'e', 's', 0};
  • funkcie strlen, strcpy, strcmp, strcat

Súbory, spracovanie vstupu

  • cin, cout alebo printf, scanf
  • fopen, fclose, feof
  • fprintf, fscanf
  • getc, putc, ungetc, fgets, fputs
  • spracovanie súboru po znakoch, po riadkoch, po číslach alebo slovách

Smerníky, dynamicky alokovaná pamäť, dvojrozmerné polia

int i;    // „klasická“ celočíselná premenná
int *p;   // ukazovateľ na celočíselnú premennú

p = &i;         // spravne
p = &(i + 3);   // zle i+3 nie je premenna
p = &15;        // zle konstanta nema adresu
i = *p;         // spravne ak p bol inicializovany

int * cislo = new int;  // alokovanie jednej premennej
*cislo = 50;
..
delete cislo;

int a[4];
int *b = a;  // a,b su teraz takmer rovnocenne premenne 

int *A = new int[n]; // alokovanie 1D pola danej dlzky
..
delete[] A;

int **a;       // alokovanie 2D matice
a = new int *[n];
for (int i = 0; i < n; i++) a[i] = new int[m];
..
for (int i = 0; i < n; i++) delete[] a[i];
delete[] a;

Abstraktné dátové typy

Abstraktný dátový typ dynamické pole (rastúce pole)

  • operácie init, add, get, set, length

Abstraktný dátový typ množina (set)

  • operácie init, find, insert, remove
  • implementácie pomocou
    • neutriedeného poľa
    • utriedeného poľa
    • spájaných zoznamov
    • binárnych vyhľadávacích stromov
    • hešovacej tabuľky
    • lexikografického stromu (ak kľúč je reťazec)


Abstraktné dátové typy rad a zásobník

  • operácie pre rad (frontu, queue): init, isEmpty, enqueue, dequeue, peek
  • operácie pre zásobník (stack): init, isEmpty, push, pop
  • implementácie: v poli alebo v spájanom zozname
  • využitie: ukladanie dát na spracovanie, odstránenie rekurzie
  • kontrola zátvoriek a vyhodnocovanie výrazov pomocou zásobníka


Dátové štruktúry

PROG-list.png

Spájané zoznamy

struct node {
    int data;
    item* next;
};
struct linkedList {
    item* first;
};
void insertFirst(linkedList &z, int d){
    /* do zoznamu z vlozi na zaciatok novy prvok s datami d */
    item* p = new item;   // vytvoríme nový prvok
    p->data = d;          // naplníme dáta
    p->next = z.first;    // prvok bude prvým prvkom zoznamu (ukazuje na doterajší začiatok)
    z.first = p;          // tento prvok je novým začiatkom
}
Strom pre výraz (65 – 3*5)/(2 + 3)

Binárne stromy

struct node {
    /* vrchol stromu  */
    dataType data;
    node * left;  /* lavy syn */
    node * right; /* pravy syn */
};

node * createNode(dataType data, node *left, node *right) {
    node *v = new node;
    v->data = data;
    v->left = left;
    v->right = right;
    return v;
}
  • prehľadávanie inorder, preorder, postorder
  • použitie na uloženie aritmetických výrazov
P22-BST.png

Binárne vyhľadávacie stromy

  • vrcholy vľavo od koreňa menší kľúč, vpravo od koreňa väčší
  • insert, find, remove v čase závisiacom od hĺbky stromu
Trie.jpg

Lexikografické stromy

  • ukladajú množinu reťazcov
  • nie sú binárne: vrchol môže mať veľa synov
  • insert, find, remove v čase závisiacom od dĺžky kľúča, ale nie od počtu kľúčov, ktoré už sú v strome
struct node {
    /* vrchol lexikografickeho stromu  */
    char data; // pismeno ulozene v tomto vrchole
    bool isWord; // je tento vrchol koncom slova?
    node* next[Abeceda]; // pole smernikov na deti    
};


Hešovanie

  • hešovacia tabuľka veľkosti m
  • kľúč k premietneme nejakou funkciou na index v poli (0,...,m-1}
  • každé políčko hešovacej tabuľky spájaný zoznam prvkov, ktoré sa tam zahešovali
  • v ideálnom prípade sa prvky rozhodia pomerne rovnomerne, zoznamy krátke, rýchle hľadanie, vkladenie, mazanie
  • v najhoršom prípade všetky prvky v jednom zozname, pomalé hľadanie a mazanie
int hash(int k, int m){ // veľmi jednoduchá hešovacia funkcia, v praxi väčšinou zložitejšie
    return abs(k) % m;
}
struct node {
    int item;
    node* next;
};

struct set {
    node** data;
    int m;
};

Algoritmy

Rekurzia

  • Rekurzívne funkcie
  • Vykresľovanie fraktálov
  • Prehľadávanie s návratom (backtracking)
  • Vyfarbovanie
  • Prehľadávanie stromov

Triedenia

  • nerekurzívne: Bubblesort, Selectionsort, Insertsort
  • rekurzívne: Mergesort, Quicksort
  • súvisiace algoritmy: binárne vyhľadávanie

Matematické úlohy

  • Euklidov algoritmus, Eratostenovo sito
  • Práca s aritmetickými výrazmi: vyhodnocovanie postfixovej formy, prevod z infixovej do postfixovej, reprezentácia vo forme stromu

Prednáška 23

Dnešná prednáška bude venovaná (pomerne plytkému) prehľadu rôznych čŕt jazykov C a C++, ktoré sa počas semestra nepreberali. Tento prehľad by mal poslúžiť ako pomôcka pri štúdiu existujúcich programov, resp. ako inšpirácia pre ďalšie samoštúdium.

Znalosť materiálu z tejto prednášky nebude vyžadovaná na skúške. Rovnako nie je odporúčané na skúške tento materiál využívať bez dôkladného samostatného oboznámenia sa s ním.

Táto prednáška nepokrýva objektovo-orientované programovanie v jazyku C++. Objektovo-orientované programovanie (v jazyku Java) bude hlavnou náplňou druhého semestra programovania.

Rozdiely medzi jazykmi C a C++

Jazyk C vznikol okolo roku 1972 na podporu vývoja operačného systému Unix; jazyk C++ okolo roku 1985. Programy v jazyku C sú väčšinou súčasne aj korektnými programami v jazyku C++ (ide ale o isté zjednodušenie). Obidva tieto jazyky existujú vo viacerých štandardoch, v ktorých sa postupne pridávali nové črty.

V priebehu semestra sme programovali v jazyku C++. Veľká časť konštrukcií, ktoré sme v programoch využívali, však pochádza už z jazyka C. Výnimkami sú však napríklad nasledujúce črty jazyka C++, ktoré v jazyku C nie sú k dispozícii:

  • V jazyku C nie je možné predávanie parametrov funkcií referenciou. Namiesto toho je potrebné používať smerníky.
  • Na používanie typu bool a konštánt true a false je potrebná knižnica stdbool (t. j. #include <stdbool.h>). Zabudovaný booleovský typ má názov _Bool a nadobúda hodnoty 0 a 1.
  • V jazyku C nefungujú operátory new a delete. Namiesto nich treba použiť funkcie popísané nižšie.
  • Štandardnými knižnicami jazyka C sú spomedzi štandardných knižníc jazyka C++ v zásade tie začínajúce písmenom c, napríklad cstdio. Namiesto #include <cstdio> potom v C píšeme #include <stdio.h>.
    • V jazyku C špeciálne nie je možné používať knižnicu iostream a v nej definované štandardné vstupno-výstupné prúdy cin a cout.
  • V starších verziách jazyka C nefungujú mnohé konštrukcie jazyka C++, ktoré sme bežne používali – napríklad komentáre vo forme //, deklarácie premenných inde ako na začiatku funkcie, atď.

Nepreberané črty jazyka C (použiteľné aj v C++)

Enumerované typy

Enumerované typy pozostávajú z vymenovania niekoľkých hodnôt, ktoré sa stanú celočíselnými konštantami. To je občas užitočné na sprehľadnenie zdrojového kódu.

Príklad:

#include <iostream>
using namespace std;

int main() {
    enum farba {biela, modra, cervena, zelena, cierna};  // definicia enumerovaneho typu farba
    farba f = biela;                                     // definicia premennej typu farba     
    f = zelena;                                          // priradenie do premennej typu farba  
    cout << f << endl;                                   // vypise 3
    return 0;
}

Zložený typ (union)

Zložený typ umožňuje na jednom mieste pamäte uchovávať hodnotu, ktorá môže byť viacerých dátových typov (narozdiel od štruktúr však vždy ide o práve jednu hodnotu). Definuje sa podobne ako štruktúra, avšak s použitím kľúčového slova union namiesto struct.

Príklad:

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

union zlozenyTyp {
    int n;
    char s[100];
};

int main() {
    zlozenyTyp z;
    z.n = 10;
    cout << z.n << endl;  // vypise 10
    strcpy(z.s, "abcd");
    cout << z.s << endl;  // vypise abcd
    cout << z.n << endl;  // vypise nejaky nezmysel
    return 0;
}

Napríklad pri aritmetických stromoch by použitie zloženého typu umožnilo ušetriť trochu pamäte tým, že by sme si v každom uzle pamätali buď jeho hodnotu, alebo operátor a smerníky na synov.

Operátory

Okrem operátorov, ktoré sme používali, existuje niekoľko ďalších, ako napríklad nasledujúce:

  • Bitové operátory pracujú s celým číslom ako s poľom bitov (vhodnejšie sú unsigned typy):
    • << a >> posúvajú bity doľava a doprava, zodpovedajú násobeniu a deleniu mocninami dvojky.
    • & (and po bitoch), | (or po bitoch), ^ (xor po bitoch), ~ (negácia po bitoch).
  • Ternárny operátor ? s použitím (podmienka)?(hodnota pre true):(hodnota pre false), napríklad cout << x << " je " << ((x%2==0) ? "parne" : "neparne") << endl;

Cyklus do-while

Cyklus do-while je obdobou cyklu while s vyhodnocovaním podmienky na konci iterácie. Nasledujúce dva spôsoby písania cyklu sú viac-menej ekvivalentné:

do { 
    prikazy;
} while(podmienka);

while (true) {
    prikazy;
    if (!podmienka) break;
}

Makrá a konštanty

Konštantu možno zadefinovať napríklad aj takto:

#define MAXN 100

Narozdiel od konštanty s definíciou

const int maxN = 100;

sa tu konštantná premenná MAXN reálne nevytvára (nevyhradzuje sa pre ňu pamäťové miesto); všetky výskyty MAXN sú preprocesorom kompilátora nahradené „ešte v zdrojovom kóde” konštantou 100.

Okrem konštánt možno definovať aj zložitejšie makrá s parametrami:

/* Definicia makra: */
#define MIN(X,Y) ((X) < (Y) ? (X) : (Y))

/* Priklad pouzitia: */
cout << MIN(a*a, b+5);

/* Preprocesor vykona substituciu za MIN, ktorou dostane: */
cout << ((a*a) < (b+5) ? (a*a) : (b+5));
  • Bez dostatočného množstva zátvoriek by pri použití makra MIN mohlo dôjsť k „interakcii s okolím”.
  • Vo všeobecnosti je odporúčané vyvarovať sa použitia makier.

Delenie programu na súbory

Väčšie programy je zvyčajne žiadúce rozdeliť na viacero zdrojových súborov. Často sa tiež môže zísť vytvorenie vlastnej knižnice.

Kompilátory, ako napríklad g++, spravidla umožňujú skompilovať viacero zdrojových súborov naraz. Pri volaní g++ z príkazového riadku môžeme písať napríklad

g++ -o program subor1.cpp subor2.cpp subor3.cpp

Takéto volanie ale môže byť úspešné len za dvoch podmienok:

  • Funkciu main musí obsahovať práve jeden z kompilovaných zdrojových súborov.
  • Ak sa v niektorom súbore suborA.cpp využíva funkcia f z iného súboru suborB.cpp, musí byť funkcia f v súbore suborA.cpp zadeklarovaná (t. j. uvedie sa hlavička funkcie f nasledovaná bodkočiarkou, bez samotného tela – čiže definície – funkcie f).

V súbore lib.cpp môžeme mať napríklad definície dvoch funkcií f a g:

/* Subor lib.cpp */

int f(int n) {
    return n + 1;
}

int g(int n) {
    return n*2;
}

Môžeme teraz do súboru prog.cpp napísať program, ktorý tieto funkcie deklaruje a následne využíva:

/* Subor prog.cpp */

#include <iostream>
using namespace std;

int f(int n);
int g(int n);

int main() {
    int n;
    cin >> n;
    cout << f(n) << " " << g(n) << endl;
    return 0;
}

Program potom možno skompilovať volaním

g++ -o prog prog.cpp lib.cpp

Manuálne deklarovanie všetkých funkcií môže byť obzvlášť pri veľkých knižniciach a programoch pozostávajúcich z veľkého množstva súborov ťažkopádne. Typickým riešením je preto presunutie všetkých deklarácií do špeciálneho hlavičkového súboru (angl. header file), v našom prípade napríklad lib.h. Direktíva #include "lib.h" potom prekopíruje do súboru, ktorý ju obsahuje, kompletný obsah súboru lib.h, čím sa vlastne deklarujú všetky funkcie z knižnice lib.cpp.

Pre náš príklad vyššie tak teraz máme 3 súbory. Súbor lib.cpp je rovnaký ako vyššie, súbor lib.h obsahuje

/* Subor lib.h */

int f(int n);
int g(int n);

a súbor prog.cpp obsahuje

/* Subor prog.cpp */

#include "lib.h"
#include <iostream>
using namespace std;

int main() {
    int n;
    cin >> n;
    cout << f(n) << " " << g(n) << endl;
    return 0;
}

Program skompilujeme rovnako ako vyššie:

g++ -o prog prog.cpp lib.cpp

Rozdiel medzi direktívami #include "lib.h" a #include <lib.h> spočíva v tom, že kým v prvom prípade sa hlavičkový súbor hľadá najprv v aktuálnom adresári, v druhom prípade sa prehľadávajú iba adresáre, ktoré sú na danom systéme predvolené (typicky ide o adresáre obsahujúce hlavičky štandardných knižníc).

Zopár užitočných funkcií

Alokácia pamäte

  • V jazyku C nie sú definované operátory new a delete, resp. new[] a delete[].
  • Pamäť sa alokuje funkciou malloc, ktorá alokuje kus pamäte s daným počtom bajtov.
    • V prípade neúspechu vráti NULL.
    • V prípade úspechu vráti smerník na void, ktorý je následne nutné pretypovať.
  • Uvoľnenie pamäte realizuje funkcia free.
  • Pri výpočte veľkosti potrebnej pamäte sa zvyčajne používa operátor sizeof.
#include <cstdlib>   // resp. #include <stdlib.h> v C

/* vytvorime pole 100 int-ov */
int *a = (int *)malloc(sizeof(int) * 100);
/* odalokujeme pole a */
free(a);

Triedenie

  • Funkcia qsort z knižnice stdlib.h.
  • Dostane pole, počet jeho prvkov, veľkosť každého prvku a funkciu, ktorá porovná dva prvky.
    • Funkciu teda posielame ako parameter.
    • Táto porovnávacia funkcia dostane dva smerníky typu void * (na dva prvky poľa).
    • Vráti záporné číslo, ak prvý prvok je menší, nulu, ak sú rovnaké a kladné číslo, ak je prvý väčší.
  • Ak si napíšeme porovnávaciu funkciu, môžeme triediť prvky hocijakého typu.
int compare(const void *a, const void *b) {
    return (*(int*)a - *(int*)b);
}
int a[] = {5, 3, 2, 4, 1};
qsort(a, 5, sizeof(int), compare);
  • Existuje napríklad aj funkcia bsearch na binárne vyhľadávanie v utriedenom poli.

Konštantné argumenty

  • V príklade vyššie máme funkciu s hlavičkou int compare(const void *a, const void *b).
  • Argumentom typu const void *a programátor „sľubuje”, že nebude meniť obsah pamäte, na ktorú ukazuje smerník a.
  • Napríklad pre funkciu void f(const int *a) {*a = 5;} teda kompilátor vyhlási chybu.
  • Ak by sme naopak pri triedení chceli použiť funkciu s hlavičkou int compare(void *a, void *b), kompilátor tiež vyhlási chybu, lebo qsort očakáva const void * a nie void *.
  • Za dobrú prax sa považuje používanie const na všetky parametre typu smerník alebo referencia, ktoré funkcia nepotrebuje meniť.

Nepreberané črty jazyka C++

Generické funkcie

Občas sa zíde napísať algoritmus, ktorý by mohol pracovať na dátach rôznych typov. Napríklad triediť môžeme celé alebo desatinné čísla, reťazce, zložitejšie štruktúry s určitým kľúčom a pod.

V C++ sa dajú písať takzvané generické funkcie, ktoré možno „parametrizovať” podľa typu:

#include <iostream>
using namespace std;

template <typename T> 
T vratPrvok(T *a, int k) {
    T result = a[k];
    return result;
} 

int main() {
    int a[5] = {1,2,3,4,5};
    cout << vratPrvok(a, 1) << endl;  // vypise 2

    return 0;
}
  • Viac v letnom semestri.

Preťaženie operátorov

Pre novovytvorené typy je možné štadardným operátorom jazyka C++ priradiť novú sémantiku pomocou tzv. preťaženia. Napríklad v nasledujúcom príklade definujeme operátor < na menách, ktorý najprv porovnáva podľa priezviska a následne podľa krstného mena.

struct meno {
    char *krstne, *priezvisko;
};

bool operator < (const meno &x, const meno &y) {
    return strcmp(x.priezvisko, y.priezvisko) < 0
            || strcmp(x.priezvisko, y.priezvisko) == 0
            && strcmp(x.krstne, y.krstne) < 0;
}
  • Podobne môžeme zadefinovať napríklad operátory + a * pre štruktúry reprezentujúce polynómy alebo komplexné čísla...
  • cout << "Hello" používa preťažený operátor << pre výstupný prúd na ľavej strane a reťazec na pravej strane.

Reťazce typu string

  • V C++ je možné okrem klasických C-čkových reťazcov použiť aj typ string z C++.
  • Jeho použitie je elegantnejšie, sám si určuje potrebnú veľkosť pamäte.
  • Reťazce tohto typu sú objekty, do funkcií ich odovzdávame väčšinou referenciou.
  • K jednotlivým znakom pristupujeme pomocou [] (ako u polí) alebo pomocou metódy at.
#include <string>
#include <iostream>
using namespace std;

int main() {
    char cstr[100] = "Ahoj\n";
    string str = "Ako sa mas?\n";
    string str2;

    /* Do str mozno pomocou operatora = korektne priradit konstantne retazce,
       C-ckove retazce (polia znakov), aj ine premenne typu string. */
    str2 = "Ahoj\n";
    str2 = cstr;
    str2 = str;

    /* Meranie dlzky retazca: */
    cout << "Dlzka je: " << str.length() << endl;

    /* Funguje porovnanie pomocou ==, !=, <, ...
     * (bud dvoch C++ stringov, alebo C++ stringu a C stringu)
     * Pomocou operatora + mozno realizovat zretazenie. */
    str2 = cstr + str;
    str2.push_back('X');   // prida jeden symbol na koniec retazca
    str2.push_back('\n');
    cout << str2 << endl;

    if (str < str2) {
        cout << "Prvy je mensi" << endl;
    } else if (str == str2) {
        cout << "Rovnaju sa" << endl;
    } else {
        cout << "Druhy je mensi" << endl;
    }
    return 0;
}
  • Pomocou metódy c_str() možno získať z reťazca typu string premennú typu const char*.

Štruktúra vector

Súčasťou štandardnej knižnice jazyka C++ je viacero rôznych dátových štruktúr. Ide pritom o generické štruktúry, ktoré môžu uchovávať dáta rôznych typov (čo je o poznanie elegantnejšie riešenie, ako to naše s dataType; v jednom programe napríklad môžeme mať štruktúry uchovávajúce rôzne typy).

Na tomto mieste spomeňme dátovú štruktúru vector [3]:

  • O niečo podarenejšia verzia dynamických polí z prednášky.
  • Deklarovať ho možno jedným z nasledujúcich spôsobov:
vector<int> a;       // vytvori pole celych cisel
vector<int> a(10);   // vytvori pole 10 celych cisel, ktore vsetky nastavi na vychodziu hodnotu
vector<int> a(5,1);  // vytvori pole 5 celych cisel, ktore nastavi na 1
  • Prístup k prvkom vector-u je možný dvoma spôsobmi:
    • Klasicky pomocou a[index] – podobne ako pri poliach sa ale v takom prípade nekontroluje rozsah.
    • Alternatívne možno použiť a.at(index) – v prípade indexu mimo rozsahu program hneď spadne (presnejšie vyhodí výnimku) a nenarobí chaos v pamäti.
  • V obidvoch prípadoch môžeme aj priraďovať: a[index] = value; a.at(index) = value;
  • Ďalšie metódy na prácu s vector-mi:
    • a.push_back(x) vloží hodnotu x ako nový prvok na koniec poľa, podľa potreby pritom pole realokuje a pod.
    • a.size() vráti počet prvkov v poli.
    • a.resize(n) alebo a.resize(n, value) zmení počet prvkov v poli na n, pričom buď zahodí nadbytočné prvky alebo pridá nové – tie budú mať hodnotu value alebo východziu hodnotu.
    • a.capacity() vráti dĺžku aktuálne alokovaného poľa (môže sa líšiť od počtu prvkov v poli) a a.reserve(n) umožňuje objem alokovanej pamäte zväčšiť (väčšinou sa o tieto implementačné detaily nestaráme, vhodné použitie týchto funkcií však občas môže vykonávanie programu zefektívniť).
#include <vector>
using namespace std;

vector<int> a;
for (int i = 0; i <= 10; i++) {
    a.push_back(i);
}
for (int i = 0; i <= a.size() - 1; i++) {
    cout << a[i] << endl;  // alebo a.at(i)
}