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.


2017/18 Programovanie (1) v C/C++

Z Programovanie
Skočit na navigaci Skočit na vyhledávání
Týždeň 25.9.-1.10. Úvod, premenné, podmienky, cyklus for
#Prednáška 1 · #Prednáška 2 · #Cvičenia 1
Týždeň 2.-8.10. Ďalšie príklady na cykly, Euklidov algoritmus, cyklus while, funkcie
#Prednáška 3 · #Prednáška 4 · Cvičenia 2
Týždeň 9.-15.10. Polia, struct, Eratostenovo sito, polynómy, jednoduché triedenia
#Prednáška 5 · #Prednáška 6 · Cvičenia 3
Týždeň 16.-22.10. Binárne vyhľadávanie, zložitosť, rekurzia, prehľadávanie s návratom
#Prednáška 7 · #Prednáška 8 · Cvičenia 4
Týždeň 23.-29.10. Prehľadávanie s návratom, Mergesort, Quicksort, znaky, switch
#Prednáška 9 · #Prednáška 10 · Cvičenia 5
Týždeň 30.10.-5.11. Sviatok vš. svätých
Týždeň 6.-12.11. Znaky, reťazce, smerníky
#Prednáška 11 · #Prednáška 12 · Cvičenia 6
Týždeň 13.-19.11. Vektor, matice, práca s konzolou v C
#Prednáška 13 · #Prednáška 14 · Cvičenia 7
Týždeň 20.-26.11. Súbory, spájaný zoznam
#Prednáška 15 · #Prednáška 16 · Cvičenia 8
Týždeň 27.11.-3.12. Zásobník, rad, vyfarbovanie, aritmetické výrazy
#Prednáška 17 · #Prednáška 18 · Cvičenia 9
Týždeň 4.-10.12. Aritmetické výrazy, stromy, binárne vyhľadávacie stromy
#Prednáška 19 · #Prednáška 20 · Cvičenia 10
Týždeň 11.-17.12. Lexikografické stromy, hešovanie, slovník
#Prednáška 21 · #Prednáška 22 · Cvičenia 11
Týždeň 18.-24.12. Opakovanie, informácie ku skúške, nepreberané črty C a C++
#Prednáška 23 · #Prednáška 24 · Cvičenia 12

Obsah

Zimný semester, úvodné informácie

Základné údaje

Rozvrh

  • Prednášky: podnelok 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: pondelok 12:20-13:50 F1-248 (1BIN študenti, ktorý majú predmet Základné chemické výpočty a názvoslovie prichádzajú až o 13:30)

Prednášajúce

Konzultácie po dohode e-mailom.

Cvičiaci

  • Mgr. Michal Anderle, miestnosť M-249, E-ma.png

Ak nemáte otázku na konkrétnu osobu, odporúčame vyučujúcich a cvičiacich 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/

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čiaceho 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 prednášajúcich alebo cvičiacich. 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 doporučujeme 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 na konci 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.
  • Avšak ak odovzdáte nejaký príklad po skončení hlavného cvičenia, môžeme vás emailom požiadať, aby ste prišli svoje riešenie predviesť na doplnkové cvičenia. Ak riešenie príkladu nepredvediete, nezískate zaňho body. Počas tohto stretnutia budeme zvedaví, či odovzdanému riešeniu naozaj rozumiete.
    • (Stará verzia tohto pravidla Avšak ak odovzdáte nejaký príklad po skončení hlavného cvičenia, musíte prísť svoje riešenie osobne predviesť na doplnkové cvičenia. Toto treba spraviť najneskôr na najbližších doplnkových cvičeniach po termíne odovzdania príkladu (väčšinou teda budú dvoje doplnkové cvičenia, počas ktorých môžete príklady z daného týždňa predviesť: jedny deň pred termínom a druhé 6 dní po termíne). Ak riešenie príkladu nepredvediete, nezískate zaňho body. Počas tohto stretnutia budeme zvedaví, či odovzdanému riešeniu naozaj rozumiete. Povinnosť predviesť domáce riešenia na doplnkovom cvičení môžeme počas semestra odpustiť študentom, ktorí nebudú mať s učebnou látkou problémy. Toto vám vhodnou formou oznámime.)
  • 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, každý však potrebuje prísť predviesť riešenie individuálne.
    • 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. Budeme kontrolovať správnosť celkovej myšlienky, správnosť implementácie ale body môžete stratiť aj za neprehľadný štýl.
  • Program, ktorý odovzdáte ako domácu úlohu by mal byť skompilovateľný a spustiteľný v prostredí používanom na cvičeniach.
  • Domáce úlohy robte samostatne, nie v dvojiciach.

Písomné testy

  • Počas semestra budú 4 písomné testy (na papieri) v rozsahu 45-90 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 11.10. 18:10 (opravná niekedy v priebehu októbra)
    • 2. test pravdepodobne streda 8.11. 18:10 alebo piatok 11.11. poobede (opravná niekedy v priebehu novembra)
    • 3. test streda 29.11. 18:10 (opravná cez skúškové)
    • 4. test streda 20.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, ale potom ich treba predviesť na doplnkovom cvičení.
  • 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é alebo v sobotu.
  • 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í.

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í. 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é dva písomné testy za plný počet bodov, ostatné píšete so spolužiakmi
    • 75-90% z testu: tri písomné testy za plný počet bodov (určia vyučujúce určia, ktoré tri), jeden test píšete so spolužiakmi
    • 90-100% z testu: zo všetkých 4 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 22.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 9.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 22.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 22.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ť v prostredí používanom v učebni 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 záverečného písomného testu a 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 8.1. 9:00 H6 riadny termín
  • pondelok 22.1. 9:00 H6 riadny termín
  • streda 31.1. 9:00 H3 1. opravný termín
  • 2. opravný termín vo februári (presný dátum neskôr)

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

  • 17.1. poobede

K zapisovaniu na skúšky

  • Na termín skúšky sa zapisujte v systéme AIS.
    • Prihlasovanie/odhlasovanie na skúšku do 14:00 deň pred skúškou.
  • 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).
  • Ak na prvom riadnom termíne skúšku nespravíte, môžete použiť druhý riadny termín ako váš prvý opravný atď. Okrem týchto štyroch už však neplánujeme ďalšie termíny a každý sa môže zúčastniť na najviac troch termínoch.
  • Po skúške poobede (prípadne ďalší deň) budú určené časy na osobné stretnutie, diskusiu k vašim riešeniam, reklamácie bodov, zapisovanie známok do indexov.
    • Ak máte ku skúške akékoľvek otázky (čo som mal zle, ako sa to dalo robiť lepšie atď), príďte na osobné stretnutie.
    • Príďte na stretnutie, aj ak ste skúšku nespravili. Môžeme vám poradiť, čo robiť inak na opravnom termíne.
    • Ak spravíte skúšku v riadnom termíne, stretnutie je nepovinné. Známku do indexu si môžete zapísať aj na inom termíne skúšky alebo poslať index po spolužiakovi (zapisovať známky ale budeme len v určené časy, nie keď zrovna idete okolo...).
    • Prípadné otázky k bodovaniu riešte na osobnom stretnutí po skúške, alebo emailom najneskôr deň po skúške, nie neskôr.
    • Ak ste písali opravný termín, osobné stretnutie je povinné.

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é, nebude k dispozícii ani internet.
  • Stretávame sa vždy desať minút pred začiatkom skúšky pred počítačovou miestnosťou, kde sa dozviete pokyny a prípadné rozsadenie do miestností
  • Skúška: 2 hodiny práca pri počítačoch.
    • Prostredie ako na cvičeniach (Linux, Netbeans, ale môžete používať aj iné nainštalované editory, valgrind a pod.)
    • Budete používať špeciálne skúškové konto, takže nebudete mať k dispozícii žiadne svoje súbory alebo nastavenia.
    • Odovzdávanie prostredníctvom špeciálnej verzie testovača, slúži súčasne ako záloha.
  • Poobede alebo ďalší deň: vyhodnotenie u prednášajúcich, zapisovanie známok, viď vyššie.

Príklady

  • Na skúške budete riešiť dva príklady za rovnaký počet bodov
    • Nie sú rovnako ťažké, preto si dobre premyslite, ako si rozdelíte čas.
  • 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.
    • 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ť.
  • 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.
  • 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ť v prostredí používanom v učebni resp. na testovači bez zvláštnych nastavení kompilátora a pod.

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.
  • 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, menšej chybe, ktorá sa neprejavila na daných vstupoch a pod.

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.

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

Niektoré ukážkové príklady na skúšku sú 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.

Prvý príklad

Druhý príklad

Zimný semester, príklady na test

Na štvrtom teste budú podobné typy príkladov, aké poznáte z prvých troch testov, napríklad

  • napíšte funkciu, ktorá robí zadanú činnosť (napr. so stromom, zoznamom, zásobníkom, radom atď)
  • doplňte chýbajúce časti funkcie
  • zistite, čo funkcia robí (pre daný vstup alebo všeobecne)

Vyskytne sa však aj nový typ príkladov, kde je úlohou napísať, ako bude na nejakom vstupe fungovať algoritmus alebo dátová štruktúra z prednášky. Nižšie sú ukážky takýchto príkladov. Svoje odpovede si môžete skontrolovať na spodku stránky.

Ukážkové príklady na písomný test

  • Príklad 1: Prepíšte výraz 8 3 4 * + 2 3 + / z postfixovej notácie do bežnej infixovej notácie. Aká je jeho hodnota? Nakreslite ho aj ako strom.
  • Príklad 2: Prepíšte výraz ((2+4)/(3*5))/(1-2) do postfixovej a prefixovej notácie.
  • Príklad 3: Vyhodnocujeme výraz 8 3 4 * + 2 3 - / v postfixovej notácii algoritmom z prednášky. Aký bude obsah zásobníka v čase, keď začneme spracovávať znamienko +?
  • Príklad 4: Máme zásobník s a rad q, pričom obidve štruktúry uchovávajú dáta typu char. Aký bude ich obsah po nasledujúcej postupnosti príkazov?
init(s);
init(q);
push(s, 'A');
push(s, 'B');
push(s, 'C');
enqueue(q, pop(s));
enqueue(q, pop(s));
push(s, 'D');
push(s, dequeue(q));
  • Príklad 5: Máme binárny strom, v ktorom má každý vrchol buď dve deti a v dátovom poli uložený znak '#' alebo nemá žiadne deti a v dátovom poli má uložený znak '*'. Keď tento strom vypíšeme v preorder poradí, dostaneme postupnosť ##*#*** Nakreslite, ako vyzerá tento strom.
  • Príklad 6: Nakreslite binárny vyhľadávací strom, ktorý dostaneme, ak do prázdneho stromu postupne vkladáme záznamy s kľúčami 3, 4, 1, 2, 5, 6 (v tomto poradí).
  • Príklad 7: Nakreslite lexikografický strom s abecedou {a,b}, do ktorého sme vložili reťazce aba, aaab, baa, bab, ba. Vrcholy, ktoré zodpovedajú niektorému reťazcu zo vstupu, zvýraznite dvojitým krúžkom.
  • Príklad 8 Ako bude vyzerať hešovacia tabuľka pri riešení kolízií pomocou spájaných zoznamov, ak hešovacia funkcia je |k| mod 5 a vkladáme prvky 13, -2, 0, 8, 10, 17?

Vzorové riešenia ukážkových príkladov na písomný test

  • Príklad 1: (8+3*4)/(2+3), hodnota 4, strom:
        /
      /   \
     +     +  
    / \   / \
   8   * 2   3
      / \
     3   4
  • Príklad 2: postfix 2 4 + 3 5 * / 1 2 - / prefix: / / + 2 4 * 3 5 - 1 2
  • Príklad 3: na zásobníku budú čísla 8 a 12 (8 je na spodku zásobníka). Číslo 12 vzniklo vynásobením 3 a 4.
  • Príklad 4: na zásobníku budú znaky A, D, C (A na spodku zásobníka), v rade bude písmeno B
  • Príklad 5:
        #
       / \
      #   *
     /\
    *  #
      /\
     *  *
  • Príklad 6:
        3
       / \
      1   4
      \    \
       2    5
             \
              6
  • Príklad 7: (namiesto dvojiteho krúžku používame *)
          .
         / \
        /   \
       /     \ 
      a       b
     / \     /
    a   b   a*
   /   /   / \
  a   a*  a*  b*
 /
b*
  • Príklad 8:

Pre každý index tabuľky 0,..,4 uvádzame zoznam prvkov, ktoré sa do neho zahešujú. Tieto budú pospájané v zozname v uvedenom poradí.

0: 10, 0
1:
2: 17, -2
3: 8, 13
4:

Netbeans

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

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

  • Prostredie netbeans (v učebni máme verziu 8.0.2., mala by však postačovať aj iná verzia)
  • 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).

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.

Alternatívy k Netbeans

Nevyžadujeme striktne, aby ste na tomto predmete používali Netbeans, 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
  • preto dôrazne odporúčame aspoň časť cvičení robiť v Netbeans na školských počítačoch, aby ste sa s týmto prostredí naučili pracovať

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 pre Linux

Ak sa nebojíte práce s príkazovým riadkom v linuxe, jednoduché programy sa pohodlne programujú v editore Kate (k dispozícii aj v učebniach a na skúške)

Prostredie pre Windows

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

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

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é)

Zvýrazňovanie a odsádzanie

  • ak sa zvýrazňovanie nezaplo automaticky (alebo neviete, či je zapnuté), overiť to môžete v Tools -> Highlighting -> Sources, tam by malo byť zaškrtnuté C++
  • užitočná funkcia je tiež automatické C++ odsádzanie, ktoré spôsobí, že vaše programy majú peknú štruktúru a sú prehľadnejšie
  • zapnúť si ho viete v Tools -> Indentation, mali by ste mať zaškrtnuté C Style

Terminál

  • Kate nemá vstavané kompilovanie ani spúšťanie (keďže je to textový editor), preto na to treba používať terminál
  • v Kate však viete mať priamo otvorenú lištu s terminálom, čo je veľmi praktické, mala by sa nachádzať dole pod textovým oknom (poprípade tam máte malú ikonku Terminal, keď na ňu kliknete, otvorí sa vám terminál)
  • ak ju nevidíte, skúste View -> Tool Views a zaškrtnite Show Terminal
  • ak ju stále nevidíte, skúste Settings -> Configure Kate -> Plugins a tam zaškrtnite Plugin s terminálom

Práca s teminálom

  • to čo potrebujete vedieť s terminálom ohľadom Kate je len to, ako sa v termináli dostať do priečinku s vaším súborom
  • ľahká cesta je cez Tools -> Synchronize Terminal with Current Document
  • lepšie je však naučiť sa asi 3 príkazy a byť schopný sa orientovať v termináli, zíde sa vám to
  • v termináli by ste mali vidieť asi svoje meno, nejaké ďalšie veci a potom :~$ za ktorým kliká kurzor (keď kliknete do terminálu)
  • časť za : vám hovorí, v ktorom priečinku sa nachádzate, ~ je domový priečinok
  • príkaz list - ls - napíšte tieto dva znaky a stlačte Enter, objaví sa vám zoznam súborov a priečinkov v priečinku v ktorom ste
  • príkaz change directory - 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
  • príkaz change directory - cd .. - posuniete sa o jeden priečinok dozadu
  • 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é

Kompilovanie a spustenie

  • 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 (tu Tabulátor nefunguje), 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)

Pozor: ak chcete niečo písať do terminálu, musíte naň kliknúť aby ste zmenili focus

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"
  • po spustení sa vám vytvorí obrázok v priečinku, v ktorom máte svoj program

Upozornenie: do #include musíte dať cestu k súboru SVGdraw.h. Preto je na prednáškach používavný riadok #include "../SVGdraw.h", keďže daný súbor je o uložený o jeden priečinok vyššie. Je na vás, kam si to uložíte, akurát majte nalinkovanú správnu cestu

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

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ď zvolemí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á.

Čo je programovanie

Algoritmus

Algoritmus: Postupnosť konečného počtu elementárnych krokov vedúca k vyriešeniu daného typu úlohy

Príklady:

  • Ako sčítať dve celé čísla v desiatkovej sústave
  • Ako nájsť najväčšieho spoločného deliteľa dvoch čísel
  • Ako riešiť Sudoku

Správnosť algoritmu

  • Keď vždy dáva správne výsledky.
  • Keď vždy skončí.

Program

  • Predpis, pomocou ktorého počítač môže vykonávať algoritmus
  • Zapísaný v programovacom jazyku

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.
  • 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 25.9.2017!" << 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>

int main(void) {
    cout << "25.9.2017" << endl;
    cout << "25.9." << endl;
    cout << "9/25/2017" << 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>

int main(void) {
    int den=25;
    int mesiac=9;
    int rok=2017;

    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 určité 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á teraz bude mať nejakú ľubovoľnú hodnotu a druhý túto počiatočnú hodnotu zmení na 100.

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 znamieko 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?

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.
    • Okrem toho môžu byť v programe komentáre medzi /* a */, ktoré počítač ignoruje.
  • 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
    • 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

Organizačné poznámky

  • Dnes poobede doplnkové cvičenia nebudú
  • Zajtra budú hlavné cvičenia, ale bez bodovaných príkladov
    • 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
  • V stredu druhá prednáška
  • Do piatka sa prihlásiť na test pre pokročilých
  • Budúci pondelok 14:00 test pre pokročilých
  • Budúci týždeň bežný rozvrh, vrátane oboch prednášok a oboch cvičení.
  • Doplnkové cvičenia budúci pondelok:
    • Ak si neviete C++ nainštalovať na notebook, prineste si ho, skúsime pomôcť (hlavne ak máte Windows)
    • Mimoriadne už budú zverejnené niektoré príklady z utorkového cvičenia, začiatočníkom odporúčame prísť a začať ich riešiť

Prednáška 2

Opakovanie: Textový výpis a načítanie

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;
}
  • Pozor, cin nekontroluje, že užívateľ zadáva rozumné hodnoty. Čo sa stane, ak namiesto čísla zadá nejaké písmená a podobne?
  • Ak pomocou cin načítavame čísla, príkaz preskočí ľubovoľné medzery a konce riadkov

Číslené hodnoty typu int a double

  • Pre začiatok budeme pracovať s premennými typu int a double.
  • Premenná typu int reprezentuje celé číslo.
  • Premenná typu double reprezentuje reálne číslo.
  • Ich rozsah je však obmedzený.
    • Typ int väčšinou zaberá 4 bajty (32 bitov) pamäte a vie ukladať čísla z intervalu <-2 147 483 648, +2 147 483 647>
    • Typ double väčšinou zaberá 8 bajtov a je uložený vo formáte s pohyblivou rádovou čiarkou, t.j. 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. Vieme teda spracovávať zhruba čísla 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 cifier.
  • Keď priradíme hodnotu typu double do hodnoty typu int, dôjde k jej zaokrúhleniu (nadol pri kladných číslach, nahor pri záporných).

V programe vieme použiť aj konštanty (konkrétne číselné hodnoty):

  • Celočíselné konštanty, ako napríklad 0, 1, 100, -5, majú typ int
  • Konštanty s desatinnou bodkou, ako napríklad 1.5, 1.0, 3.13, -0.5 majú typ double. Môžeme tiež používať semilogaritmický zápis typu 1.5e3, čo znamená 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.

Cvičenie: Ako treba zmeniť program na sčítanie dvoch čísel tak, aby pracoval s reálnymi číslami (t.j. napr. po zadaní 1.5 a 1.2 vypíše 2.7)?

Aritmetické výrazy

  • +, -, * (násobenie), / (delenie)
    • delenie celočíselných premenných vráti dolnú celú časť podielu, napr. 5/3 je 1 (pre záporné čísla to môže byť horná celá časť)
  •  % je modulo, napr. 5%3 bude 2, lebo 5 má zvyšok 2 po delení 3
  • ďalšie matematické funkcie vyžadujú #include <cmath> 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ť)
    • pozri tiež zoznam tu

Skratky: Ak chceme zvýšiť hodnotu premennej x o 1, môžeme použiť niektorý z nasledujúcich spôsobov:

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

Podobne ako ++ existuje aj --, podobne ako += existuje aj -=, *= atď

  • Pozor, pri delení celých čísel je výsledkom celé číslo (dolná celá časť podielu)
    • Ak je čitateľ alebo menovateľ typu double, výsledok je typu double
int i=3; int j=2; double d=2;
cout << i / j << endl;  // celočíselné delenie 3/2
cout << i / d << endl;  // neceločíselné delenie 3/2 
   
double c = i / j;
cout << c << endl;      // c je síce desatinné číslo ale bolo do neho priradené celé číslo
cout << c / 3 << endl;  // ale ďalej s ním pracujeme ako s desatinným
   
double b = (1.0 * i) / j; //toto delenie už nie je celočíselné
cout << b << endl;   

double a = ((double)i) / j; //tiež neceločíselné delenie
cout << a << endl;   

Cvičenie: Napíšte program, ktorý načíta reálne čísla a,b,c a vypíše korene kvadratickej rovnice ax2+bx+c=0, pričom predpokladajme, že rovnica má reálne korene.

Podmienka (if)

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

  • Nasledujúci program si vypýta od užívateľa číslo a vypíše, či je toto číslo párne (even) alebo nepárne (odd).
 #include <iostream>
using namespace std;

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

    if (x % 2 == 0) {
        cout << "Number " << x << " is even." << endl;
    } else {
        cout << "Number " << x << " is odd." << endl;
    }

}
  • Tu je príklad dvoch behov programu:
Please enter some number: 10
Number 10 is even.
Please enter some number: 3
Number 3 is odd.
  • Ako vidíme, za príkazom if je zátvorka s podmienkou. V našom príklade podmienka je x % 2 == 0. Zoberieme teda hodnotu x, pomocou operátora % zistíme zvyšok po delení 0 a pomocou dvoch rovnítok == testujeme, či je tento zvyšok rovný nule.
  • Ak je podmienka v zátvorke splnená (t.j. ak je zvyšok rovný nule), vykonáme príkazy v zloženej zátvorke za príkazom if.
  • Ak podmienka nie je splnená (t.j. ak zvyšok nie je 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ť.

Logické konštanty a výrazy

  • Logické konštanty: true (1) a false (0)
  • Logické výrazy dávajú ako výsledok pravdivostnú hodnotu -- logickú konštantu
    • == (rovnosť), != (nerovnosť), <, <=, >, >=
    • && (logický AND), || (logický OR), ! (logický NOT)

Vyhodnocovanie výrazov

Výrazy sa vyhodnocujú s preferenciou podľa nasledovnej tabuľky. Výrazy v jednom riadku tabuľky majú rovnakú prednosť a vyhodnocujú sa väčšinou zľava doprava, okrem !,++,-- a priradenia.

  • ++, --, logický NOT
  • *, /, %
  • +, -
  • <, >, <=, >=
  • ==, !=
  • && (logický AND)
  • || (logický OR)
  • priradenie

Poradie vyhodnocovania môžeme meniť zátvorkami, napr. 4*(5-3)

Viac o operátoroch v C++ nájdete napríklad tu.

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.

#include <iostream>
using namespace std;

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

    if (x == 0) {
        cout << "Null" << 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 << “Null” << endl; 

tak program do premennej x priradí nulu, ktorá sa premení na logické false pre účely vyhodnotenia podmienky.

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

   if (x==0) cout << “Null”; 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 << “Null”; cout << endl; }

Cvičenia

  • V programe na hľadanie koreňov doplňte, aby užívateľa upozornil, keď rovnica nemá reálny koreň. Takisto zvlášť vypíšte prípad keď rovnica má jeden koreň a keď má dva korene.
  • Napíšte program, ktorý načíta čísla a,b,c a vypíše, či sú usporiadané vzostupne, t.j. či platí a<b<c
    • 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 && (and) a druhý používa dva príkazy if:
//Verzia 1:
if(x>0 && y>0) { cout << "Yes!"; }

//Verzia 2:
if(x>0) {
  if(y>0) { cout << "Yes!"; }
}
  • Podobne nahraďte logické operátory && a || aj v nasledujúcich príkazoch:
if(x>0 || y>0) { cout << "Yes!"; }
if(x>0 || (x<0 && y<0)) { cout << "Yes!"; }
if(x<0 && (y<0 || z<0)) { cout << "Yes!"; }
  • Nech x je nejaká logická hodnota. Čomu sa rovnajú nasledovné výrazy?
x && true 
x && false 
x || true 
x || false
x && x
x || x 
x && !x
x || !x
x == true 
x == false

Ako by ste si overili, že máte výsledok tohto cvičenia správne?

Cyklus (for)

Teraz si ukážeme príkaz for, ktorý nám umožňuje opakovať viackrát nejakú skupinu príkazov.

Na úvod trochu motivácie. Ako by ste napísali program, ktorý vypíše čísla od 0 po 9? Čo ak chceme vypísať čísla až po 99?

Vypisovanie čísiel

Nasledujúci program vypíše čísla 0 až 9 oddelené medzerami bez toho, aby sme ich niekde v programe explicitne vymenovali.

#include <iostream>
using namespace std;

int main(void) {
    for (int i = 0; i < 10; i++) {
        cout << " " << i;
    }
    cout << endl;
}

Tu je výstup programu.

0 1 2 3 4 5 6 7 8 9

Ak by sme v programe číslo 10 zmenili napr. na 25, vypíše čísla 0 do 24.

  • Novou črtou tohto programu je príkaz for pre cyklus: for (int i = 0; i < 10; i++)
  • Cyklus nám umožňuje opakovať určitú časť programu viackrát.
  • V zátvorke za for sú tri časti oddelené bodkočiarkami.
    • Príkaz int i = 0 vytvorí novú celočíselnú premennú i a priradí jej hodnotu 0.
    • Podmienka i < 10 určuje dokedy sa má cyklus opakovať, t.j. kým hodnota i je menšia ako 10.
    • Príkaz i++ hovorí, že v každom kroku sa má premenná i zvýšiť o jedna.
  • Medzi zložené zátvorky { a } môžeme dať jeden alebo viac príkazov, ktoré sa budú opakovať pre rôzne hodnoty premennej i.
    • V našom príklade máme vo vnútri cyklu iba príkaz cout << " " << i;, ktorý vypíše medzeru a hodnotu premennej i.
  • Po skončení cyklu sa pokračuje príkazmi za končiacou zloženou zátvorkou, v našom prípade vypíšeme ešte koniec riadku.
  • Všimnite si, že náš program obsahuje dve sady zložených zátvoriek vnorených v sebe: jedna ohraničuje celý program, jedna ohraničuje príkazy, ktorá sa robia vo vnútri cyklu.
  • O presnom fungovaní príkazu for si povieme viac neskôr, ale teraz sa pozrime, čo spraví jednoduchá zmena v tomto príkaze:
for (int i = 1; i <= 10; i++) {
  • Počiatočnú príkaz sme zmenili z int i = 0 na int i = 1, premenná i teda začne s hodnotou 1, nie 0. Cyklus môže začať od ľubovoľnej hodnoty (napríklad aj zápornej)
  • Podmienku ukončenia sme zmenili z i < 10 na i <= 10, t.j. cyklus sa opakuje kým je hodnota premennej i menšia alebo rovná 10 (to isté by sme dosiahli aj pomocou i < 11)
  • Program teda vypíše čísla od 1 po 10:
 1 2 3 4 5 6 7 8 9 10

Simulovanie hodov kocky

Nasledujúci program od užívateľa načíta číslo n a vypíše n simulovaných hodov kocky, pred každý dá medzeru.

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

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

    int n;
    cout << "Zadajte pocet hodov: ";
    cin >> n;

    for (int i = 0; i < n; i++) {
        int hod = rand() % 6 + 1; // vygenerovanie náhodného hodu
        cout << " " << hod;
    }
    cout << endl;
}

Príklad behu programu:

Zadajte pocet hodov: 10
 1 4 3 2 6 1 6 1 1 5
  • Program využíva príkaz 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).
    • Výsledkom rand() je celé nezáporné číslo medzi 0 a nejakou veľkou konštantou.
    • rand() % 6 je číslo medzi 0 a 5. Ak k tomu pripočítame 1, dostaneme číslo od 1 po 6
  • Príkaz srand inicializuje generátor pseudonáhodných čísel na určitú hodnotu, my použijeme aktuálny čas.
  • Potrebujeme knižnice cstdlib a ctime.

Výpočet faktoriálu

Tento program si od užívateľa vypýta číslo n a spočíta n!, t.j. súčin celých čísel od 1 po n.

#include <iostream>
using namespace std;

int main(void) {
    int n;

    cout << "Please enter n: ";
    cin >> n;

    int result = 1;
    for (int i = 1; i <= n; i++) {
        result = result * i;
    }
    cout << n << "!=" << result << endl;
}

Tu je príklad behu programu pre n=4 (1*2*3*4=24)

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

Cvičenie: Do programu na vypisovanie hodov kocky doprogramujte, aby program vypísal aj ich súčet.

Organizačné poznámky

  • Do piatka sa v prípade záujmu e-mailom prihláste na test pre pokročilých
  • Budúci pondelok 14:00 test pre pokročilých
  • Budúci týždeň bežný rozvrh, vrátane oboch prednášok a oboch cvičení.
  • Doplnkové cvičenia budúci pondelok:
    • Ak si neviete C++ nainštalovať na notebook, prineste si ho, skúsime pomôcť (hlavne ak máte Windows)
    • Mimoriadne už budú zverejnené niektoré príklady z utorkového cvičenia, začiatočníkom odporúčame prísť a začať ich riešiť

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, prípadne v iných prostrediach
  2. precvičiť si písanie veľmi jednoduchých programov
  3. vyskúšať si prácu s testovačom

Tento týždeň ešte príklady nie sú bodované, cieľom je zoznámiť sa s prostredím. Od budúceho týždňa budete za vyriešené príklady dostávať body.

Začiatočníkom odporúčame prísť budúci týždeň na doplnkové cvičenia, budú už k dispozícii niektoré bodované príklady.

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ýstuoe 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ť
    • Naopak, ak ste mali program hneď prvýkrát dobre, skúste cvične odovzdať nesprávny program a pozrite si, čo testovač vypíše

Príklady 2 a 3

  • Ďalšie dve jednoduché zadania nájdete priamo na testovači

Prednáška 3

Úvod

Doteraz sme videli:

  • Vypisovanie a načítavanie v textovom režime
  • Aritmetické a logické výrazy, premenné typu int a double
  • Podmienku if
  • Cyklus for

Dnes:

  • Ďalšie príklady na cykly a podmienky, zopár menších noviniek.

Viac príkladov na cyklus for

Výpočet faktoriálu

Tento program si od užívateľa vypýta číslo n a spočíta n!, t.j. súčin celých čísel od 1 po n.

#include <iostream>
using namespace std;

int main(void) {
    int n;

    cout << "Please enter n: ";
    cin >> n;

    int result = 1;
    for (int i = 1; i <= n; i++) {
        result = result * i;
    }
    cout << n << "!=" << result << endl;
}

Tu je príklad behu programu pre n=4 (1*2*3*4=24)

Please enter n: 4
4!=24
  • Program používa premennú result, v ktorej sa ukladá dočasný výsledok. Na začiatok ju nastavíme na 1 a postupne ju násobíme číslami 1, 2, ..., n.
  • Riadok result = result * i; zoberie pôvodnú hodnotu premennej result, vynásobí ju hodnotou premennej i (t.j. jedným z čísel 1, 2, ..., n) a výsledok uloží naspäť do premennej result (prepíše pôvodnú hodnotu). To isté sa dá napísať ako result *= 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 projeku v záložke Projects, zvolíme Step into
    • Tým sa naštaruje 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ť

Int má obmedzený rozsah

Funkcia n! 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 sú:

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

Cvičenie: Prepíšme program na počítanie faktoriálu tak, aby vypísal aj faktoriál ako súčin čísel, napríklad

Please enter n: 4
4! = 1*2*3*4 = 24

Vypisovanie deliteľov: podmienka v cykle

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

#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 <= n; i++) {
        if (n % i == 0) {
            cout << " " << i;
        }
    }
    cout << endl;
}

Beh programu:

Please enter some number: 12
Divisors of 12: 1 2 3 4 6 12

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.

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.

Program používa premennú typu bool, do ktorej môžeme priraďovať logické hodnoty true alebo false.

#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).
  • Logické hodnoty true a false vieme ukladať do premenných typu bool.
  • 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.

Organizačné poznámky

  • Prvé príklady z cvičení budú zverejnené výnimočne už dnes na doplnkovom cvičení, zajtra na cvičeniach prvá rozcvička.
  • Pre prihlásených dnes o 14:00 test pre pokročilých.
  • Ak chcete namiesto DÚ robiť rýchlostné programovanie, nezabudnite sa do piatka prihlásiť (viď pravidlá)
  • Ak máte problémy s inštaláciou Netbeans na vašom počítači, skúste niektoré z týchto možností:
    • Prineste si notebook na doplnkové cvičenia, skúsime vám pomôcť
    • Ak chcete skúsiť Linux, môžete si ho nainštalovať vo virtuálnom počítači pod Windows, napr. VirtualBox, ale treba na to mať dosť operačnej pamäte, aby mohli bežať naraz dva operačné systémy. V Linuxe (napr. distribúcia Ubuntu) by nemal byť problém nainštalovať potrebný softvér.
    • Namiesto Netbeans si môžete nainštalovať niektoré prostredie určené špecificky pre Windows, napr. DevC++ alebo nástroje od Microsoftu, alebo neinštalovať nič a používať online nástroje (podrobnosti na zvláštnej stránke).
    • Ak však doma používate alternatívny softvér, naučte sa používať aj Netbeans na cvičeniach kvôli skúške

Prednáška 4

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 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.

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 printmessage ()
{
  cout << "I'm a function!";
}
void printmessage ()
{
  cout << "I'm a function!";
  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

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 Fnc () {
//  i=13; – nemôžeme používať premennú i – žiadna premenná i nie je viditeľná
}

int i;

void Fnc2 () {
  i=50; // priradí sa do globálne premennej i!
  Fnc ();
}

void Fnc1 () {
  int i;
  i=2; // priradí sa do lokálnej premennej i, ktorá vznikla v tejto funkcii
    // uvedomte si, že 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.
  Fnc1 ();
    // lokálna premenná i má stále hodnotu 1
  Fnc2 ();
    // 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 tj, 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 "21".

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..).

V prípade, že to nie je možné urobiť, potrebujeme aspoň dopredu oznámiť, že takéto funkcie budú. Urobíme to pomocou prototypu funkcie type name ( argument_type1, argument_type2, ...);

#include <iostream>
using namespace std;

void odd (int a);   // prototypy
void even (int a);

int main ()
{
  int i;
  do {
    cout << "Type a number (0 to exit): ";
    cin >> i;
    odd (i);
  } while (i!=0);
  return 0;
}

void odd (int a)
{
  if ((a%2)!=0) cout << "Number is odd.\n";
  else even (a);
}

void even (int a)
{
  if ((a%2)==0) cout << "Number is even.\n";
  else odd (a);
}

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)

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)

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ú.
  • Viaceré premenné vieme spojiť do jedného logického celku pomocou struct

Oznamy

Prvý test

  • Koná sa budúcu stredu 18:10 v posluchárni B
  • Pokrýva učivo prednášok 1-3, resp. cvičení 1 a 2 (cin, cout, premenné, výrazy, podmienky, cykly)
  • Prineste si perá a ťahák v rozsahu jedného listu (dvoch strán) A4.
  • Typy príkladov:
    • napíšte krátky program (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

Domáca úloha 1

  • Plánujeme zverejniť začiatkom budúceho týždňa, na riešenie budete mať cca 2 týždne

Cvičenia:

  • Príklady z cvičení 2 (okrem rozcvičky) môžete riešiť do budúceho utorka 14:50
  • Budúci utorok zverejníne ďalšiu sadu úloh
  • Doplnkové cvičenia v pondelok:
    • Dokončujete príklady z cvičení 2
    • Môžete si dať uznať riešenia, ktoré ste robili doma (stačí aj o týždeň neskôr, niektorým odpustíme na základe výsledkov písomky)
    • Pribudne malá bonusová rozcvička za 1 bod, dá sa riešiť iba počas cvičení
    • Otázky k písomke
    • Inštalácia Netbeans na notebooky s Windows
    • Môžete začať pracovať na DÚ1
  • Pripomínam, že na príkladoch z cvičení môžete pracovať aj vo dvojiciach, uveďte druhé meno študenta pri odovzdávaní, započíta sa obom
    • Ak robíte vo dvojici doma, obaja musíte prísť na doplnkové cvičenia
    • Obaja v dvojici by sa mali aktívne zapájať do riešenia príkladu a mali by rozumieť finálnemu programu

Na testovači by malo koncom budúceho týždňa pribudnúť zobrazovanie uznaných príkladov a bodov, ktoré ste už získali.


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

  • Do nedele 8.10. je možnosť meniť v AISe zapísané predmety na zimný semester. Do štvrtku 12.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.
  • Poriadne si skontrolujte, či máte v AIS a v indexe to isté a či sú to tie predmety, ktoré chcete brať.
  • Informácia na stránke fakulty: [2]

Prednáška 5

Oznamy

  • Nezabudnite si prípadné zmeny v zápise dať tento týždeň potvrdiť na študijnom.
  • Prvý test v stredu 11.10. o 18:10 v posluchárni B. Bude pokrývať učivo prednášok 1-3, resp. cvičení 1 a 2. Môžete si priniesť ťahák v rozsahu jedného listu (dvoch strán) A4.
  • DÚ1 zverejnená, odovzdávajte do pondelka 23.10. 22:00 (o dva týždne)

Cvičenia:

  • Hlavné cvičenia 3 v utorok: objaví sa ďalšia úloh, ktorá sa bude týkať prednášok 4 a 5
  • Doplnkové cvičenia dnes:
    • Dokončujete príklady z cvičení 2
    • Pribudne malá bonusová rozcvička za 1 bod, dá sa riešiť iba počas cvičení
    • Otázky k učivu, ktoré bude na písomke
    • Inštalácia Netbeans na notebooky s Windows
    • Môžete začať pracovať na DÚ1

Štatistika z N čísel

Od užívateľa postupne načítame N čísel a chceme o nich zistiť nejaké štatistické údaje.

  • Predstavme si napríklad, že učiteľ zadá body, ktoré študenti dostali na písomke.

Maximum

Zrejme stačí čítať čísla postupne a pamätať si zatiaľ najväčšie číslo.

#include <iostream>
#include <cstdlib>

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ť.
#include <iostream>
#include <cstdlib>

using namespace std;

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

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

    cout << "Zadavajte cisla: ";
    cin >> x;

    max = x;
    for (int i = 1; i < N; i++) {
        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?
  • Ako by sme spočítali priemer čísel?

Výskyty čísel 0...9

Na vstupe sú iba čísla od 0 do 9 a chceme vedieť, koľkokrát sa jednotlivé čísla na vstupe vyskytli. Mohli by sme to riešiť takto:

  • Pre každú hodnotu 0...9 si vytvoríme jednu premennú (napríklad p0, p1, ..., p9) na začiatku nastavenú na 0.
  • V prípade, že prečítané číslo bolo 0 zväčšíme hodnotu p0, ak bolo 1 zväčšíme p1 ...

Bol by to dlhý a komplikovaný program, v ktorom sa ľahko spraví chyba. Už vôbec by nebolo praktické to robiť takto, ak by hodnoty boli od 0 po milión...

  • To, že štvrtá premenná je p4, vieme iba my ako programátori a počítač o tom nevie - nemá žiaden súvis medzi jednotlivými premennými.

Teraz ukážeme riešenie tohto príkladu pomocou poľa.

  • int p[10] vytvorí pole dĺžky 10, t.j. tabuľku s prvkami p[0], p[1],..., p[9]
  • s každým p[i] môžeme pracovať ako s premennou typu int
#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
    }
}

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ť
  • Keby sme vedeli dopredu, že čísel bude napr. 20, môžeme vytvoriť pole pevnej veľkosti podobne ako v predchádzajúcom príklade.
#include <iostream>
using namespace std;

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

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

    double priemer = sucet / N;
    cout << "Priemer je " << priemer << "." << endl;
    for (int i = 0; i < N; i++) {
        if (p[i] > priemer) { 
             cout << p[i] << ": vacsie ako priemer." << endl;
        } else if (p[i] < priemer) {
             cout << p[i] << ": mensie ako priemer." << endl;
        } else {
             cout << p[i] << ": priemer." << endl;
        }
    }
}
  • 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

Pri inicializácii sa dá dodať aj menej hodnôt ako má pole. Pri čiastočnej inicializácii nastaví prekladač ostatné prvky na nulu.

double C[5]={5.0, 13.9}; // inicializuje C[0]=5.0, C[1]=13.9 a C[2]..C[4]=0
double C[5]={0}; // jednoduchá inicializácia všetkých prvkov na 0

Ak pri inicializácii poľa necháme hranaté zátvorky prázdne, prekladač si sám spočíta prvky poľa. Nie je to však odporúčaný postup.

int A[]={1, 5, 3, 8}; // zistí rozsah poľa 0..3

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; 

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.

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

Prednáška 6

Oznamy

  • DÚ1 do pondelka 23.10. 22:00
  • Dnes o 18:10 písomka. Doneste si perá, ISIC, ťahák 1 A4
    • Body plánujeme zverejniť zajtra poobede/večer, uvidíte ich po prihlásení na testovači
  • Doplnkové cvičenia v pondelok
    • Pokračujete v riešení cvičení 3 a DÚ1,
    • Pribudne malá bonusová rozcvička za 1 bod, dá sa riešiť iba počas doplnkových cvičení
  • Hlavné cvičenia budúci utorok:
    • nová sada úloh k dnešnej a pondelkovej prednáške
  • Zmena v pravidlách predmetu (k lepšiemu)
    • Všetky príklady z cvičení, ktoré do termínu odovzdania uvedeného v testovači úspešne odovzdáte s výsledkom OK, vám budú automaticky uznané (odfajfknuté), aj keď neprídete na doplnkové cvičenia
    • Avšak môžeme vás kontaktovať e-mailom, aby ste prišli na doplnkové cvičenia ukázať niektoré vaše programy a v prípade, že neprídete a nedohodnete si náhradný termín, môžete stratiť body z príkladov počítaných mimo cvičení
    • Takisto môžete stratiť body, ak nedodržujete pokyny v zadaní (použitie kostry a pod.)
    • Nezabudnite si čítať emaily na adrese @uniba.sk (môže byť ideálne presmerovať si ich na adresu, ktorú pravidelne sledujete)

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 a časť budúcej prednášky ešte algoritmy s poliami. Budúci týždeň potom začneme rekurziu (potenciálne náročné učivo).

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

Polynómy

  • Príklad polynómu: 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 2x^3 + 3x -1} .
  • Polynóm si môžeme uložiť do poľa, pričom koeficient pri 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^i} dáme do a[i]
  • Pre náš príklad vytvoríme pole napríklad príkazom double a[4] = {-1, 3, 0, 2};
  • Teraz si ukážeme niekoľko funkcií, ktoré s polynómami pracujú.

Vyhodnocovanie polynómu

  • Chceme spočítať hodnotu polynómu pre nejaké konkrétne x
  • Príklad: pre 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 2x^3 + 3x -1} a pre x=2 dostaneme 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 21 = (2\cdot 2^3 + 3\cdot 2 -1)}

Pokus 1:

double evaluatePolynomial(double a[], int n, double x) {
    /* a je pole koeficientov s n hodnotami.
     * Funkcia vráti hodnotu tohto polynómu v bode x.
     *
     * Táto implementácia na výpočet x na i používa funkciu pow */
    double value = 0;
    for (int i = 0; i < n; i++) {
        value += a[i] * pow(x, i);
    }
    return value;
}
  • Pripomíname: value += x je to isté ako value = value + x
  • Všimnite si, že v komentári na začiatku funkcie popisujeme, čo tá funkcia robí, to je väčšinou dobrý nápad spraviť.
  • Aký najvyšší stupeň môže mať polynóm a (ako funkcia n)?

Pokus 2:

  • Namiesto volania pow budeme nejakú premennú opakovanie násobiť hodnotou x (potenciálne rýchlejšie)
  • 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.
  • Čo by sa stalo, ak by sme prehodili dva príkazy vo vnútri cyklu?
double evaluatePolynomial(double a[], int n, double x) {
    /* a je pole koeficientov s n hodnotami.
     * Funkcia vráti hodnotu tohto polynómu v bode x.
     *
     * Táto implementácia počíta x na i v cykle spolu
     * s vyhodnocovaním polynómu.
     */
    double value = 0;
    double xpow = 1;
    for (int i = 0; i < n; i++) {
        /* Invariant: xpow sa rovna x na i */
        value += a[i] * xpow;
        xpow *= x;
    }
    return value;
}

Pokus 3: Hornerova schéma

  • O kúsoček lepšia ako pokus 2 - nepotrebuje pomocnú premennú a v každej iterácii cyklu robí iba jedno násobenie, nie dve
  • Idea je začať od najvyšších mocnín 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 2x^3 + 0x^2 + 3x -1 = ((2\cdot x+0)x+3)x-1}
double evaluatePolynomial(double a[], int n, double x) {
    /* a je pole koeficientov s n hodnotami.
     * Funkcia vráti hodnotu tohto polynómu v bode x.
     *
     * Táto implementácia používa tzv. Hornerovu schému,
     * ktorá začína od najvyšších koeficentov a
     * a pre každé i robí iba jedno sčítanie a jedno násobenie.
     */
    double value = 0;
    for (int i = n - 1; i >= 0; i--) {
        value = value * x + a[i];
    }
    return value;
}

Cvičenie: aký je invariant po vykonaní príkazu v cykle?


Hlavný program:

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

double evaluatePolynomial(double a[], int n, double x) {
  // jedna z funkcií vyššie
}

void enterPolynomial(double a[], int &n, int maxN) {
    /* Od užívateľa načíta koeficienty polynómu do a,
     * ich počet uloží do n, maxN je veľkosť poľa,
     * ktorú nemožno prekročiť. */
    cout << "Zadaj pocet koeficientov n: ";
    cin >> n;
    if (n > maxN) {
        cout << "Prilis velke n!" << endl;
        exit(1);
    }
    for (int i = 0; i < n; i++) {
        cout << "Zadaj koeficient pri x na " << i << ": ";
        cin >> a[i];
    }
}

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

    enterPolynomial(a, n, maxN);
    double x;
    cout << "Zadaj x: ";
    cin >> x;
    cout << "Hodnota pre x=" << x << " je " << evaluatePolynomial(a, n, x) << endl;
}
Zadaj pocet koeficientov n: 4
Zadaj koeficient pri x na 0: -1
Zadaj koeficient pri x na 1: 3
Zadaj koeficient pri x na 2: 0
Zadaj koeficient pri x na 3: 2
Zadaj x: 2
Hodnota pre x=2 je 21

Sčítanie polynómov

  • Pri sčítaní polynómov len sčítame koeficienty pri zodpovedajúcich mocninách x
  • Napr. 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 (2x^3 + 3x -1) + (-4x^2+2x+7) = (2+0)x^3 + (0-4)x^2 +(3+2)x^1 + (-1+7)x^0 = 2x^3-4x^2+5x+6}
  • Musíme dávať pozor na to, že dĺžky polynómov môžu byť rôzne a hodnoty v poli za n ďalej sú nedefinované.
void addPolynomials(double a[], int na, double b[], int nb, double c[], int &nc) {
    /* a je pole koeficientov polynomu s na hodnotami,
     * b je pole koeficientov s nb hodnotami
     * do c zratame ich sucet, do nc pocet hodnot, ktory bude vacsie z
     * na, nb. Predpokladame, ze c je dost velke, aby sa do neho nc
     * prvkov zmestilo. */
    if (na > nb) {
        nc = na;
    } else {
        nc = nb;
    }

    for (int i = 0; i < nc; i++) {
        c[i] = 0;
        if (i < na) {
            c[i] += a[i];
        }
        if (i < nb) {
            c[i] += b[i];
        }
    }
}

Hlavný program: (teraz vidíme, načo nám je funkcia enterPolynomial, bez nej by sme cyklus na načítavanie museli písať dvakrát)

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

void enterPolynomial(double a[], int &n, int maxN) {
  // pozri vyššie
}

void writePolynomial(double a[], int n) {
    /* a je pole koeficientov polynómu s n hodnotami */
    for (int i = 0; i < n; i++) {
        cout << "Koeficient polynomu pri x na " << i << " je " << a[i] << endl;
    }
}

int main(void) {
    const int maxN = 100;
    int na, nb;
    double a[maxN], b[maxN];
 
    cout << "Prvy polynom:" << endl;
    enterPolynomial(a, na, maxN);
    cout << endl << "Druhy polynom:" << endl;
    enterPolynomial(b, nb, maxN);

    double c[maxN];
    int nc;
    addPolynomials(a, na, b, nb, c, nc);
    cout << endl << "Ich sucet:" << endl;
    writePolynomial(c, nc);
}

Príklad behu:

Prvy polynom:
Zadaj pocet koeficientov n: 4
Zadaj koeficient pri x na 0: -1
Zadaj koeficient pri x na 1: 3
Zadaj koeficient pri x na 2: 0
Zadaj koeficient pri x na 3: 2

Druhy polynom:
Zadaj pocet koeficientov n: 3
Zadaj koeficient pri x na 0: 7
Zadaj koeficient pri x na 1: 2
Zadaj koeficient pri x na 2: -4

Ich sucet:
Koeficient polynomu pri x na 0 je 6
Koeficient polynomu pri x na 1 je 5
Koeficient polynomu pri x na 2 je -4
Koeficient polynomu pri x na 3 je 2

Ďalšie príklady s polynómami

Vieme napísať aj ďalšie funkcie alebo programy na prácu s polynómami:

  • Vykresľovanie grafu (rátame hodnotu polynómu v husto rozmiestnených bodoch)
  • Násobenie polynómov
  • Delenie so zvyškom a Euklidov algoritmus
  • Derivácia
  • A mnohé ďalšie

Načo sú v programovaní dobré funkcie

  • Rozbijeme veľký problém na menšie dobre definované časti (napr. načítaj polynóm, spočítaj jeho hodnotu), 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 (napr. načítanie polynómu A a B). 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 polynómami.

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ď.

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 zamä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

Zhrnutie

  • Videli sme niekoľko nových algoritmov:
    • Vyhodnocovanie a sčítavanie polynómov
    • 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í

Zdrojový kód programu s polynómami

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

const int maxN = 100;

double evaluatePolynomialPow(double a[], int n, double x) {
    /* a je pole koeficientov s n hodnotami.
     * Funkcia vráti hodnotu tohto polynómu v bode x.
     *
     * Táto implementácia na výpočet x na i používa funkciu
     * pow, čo ale pomalé a potenciálne nie úplne presné.
     */
    double value = 0;
    for (int i = 0; i < n; i++) {
        value += a[i] * pow(x, i);
    }
    return value;
}

double evaluatePolynomialMult(double a[], int n, double x) {
    /* a je pole koeficientov s n hodnotami.
     * Funkcia vráti hodnotu tohto polynómu v bode x.
     *
     * Táto implementácia počíta x na i v cykle spolu
     * s vyhodnocovaním polynómu.
     */
    double value = 0;
    double xpow = 1;
    for (int i = 0; i < n; i++) {
        /* Invariant: pow sa rovna x na i */
        value += a[i] * xpow;
        xpow *= x;
    }
    return value;
}

double evaluatePolynomialHorner(double a[], int n, double x) {
    /* a je pole koeficientov s n hodnotami.
     * Funkcia vráti hodnotu tohto polynómu v bode x.
     *
     * Táto implementácia používa tzv. Hornerovu schému,
     * ktorá začína od najvyšších koeficentov a
     * a pre každé i robí iba jedno sčítanie a jedno násobenie.
     */
    double value = 0;
    for (int i = n - 1; i >= 0; i--) {
        value = value * x + a[i];
    }
    return value;
}

void addPolynomials(double a[], int na, double b[], int nb, double c[], int &nc) {
    /* a je pole koeficientov polynomu s na hodnotami,
     * b je pole koeficientov s nb hodnotami
     * do c zratame ich sucet, do nc pocet hodnot, ktory bude vacsie z
     * na, nb. Predpokladame, ze c je dost velke, aby sa do neho nc
     * prvkov zmestilo. */
    if (na > nb) {
        nc = na;
    } else {
        nc = nb;
    }

    for (int i = 0; i < nc; i++) {
        c[i] = 0;
        if (i < na) {
            c[i] += a[i];
        }
        if (i < nb) {
            c[i] += b[i];
        }
    }
}

void enterPolynomial(double a[], int &n, int maxN) {
    /* Od užívateľa načíta koeficienty polynómu do a,
     * ich počet uloží do n, maxN je veľkosť poľa,
     * ktorú nemožno prekročiť. */
    cout << "Zadaj pocet koeficientov n: ";
    cin >> n;
    if (n > maxN) {
        cout << "Prilis velke n!" << endl;
        exit(1);
    }
    for (int i = 0; i < n; i++) {
        cout << "Zadaj koeficient pri x na " << i << ": ";
        cin >> a[i];
    }
}

void writePolynomial(double a[], int n) {
    /* a je pole koeficientov polynómu s n hodnotami */
    for (int i = 0; i < n; i++) {
        cout << "Koeficient polynomu pri x na " << i << " je " << a[i] << endl;
    }
}

void testEvaluate() {
    int n;
    double a[maxN];

    enterPolynomial(a, n, maxN);
    double x;
    cout << "Zadaj x: ";
    cin >> x;
    cout << "Hodnota pre x=" << x << " je " << evaluatePolynomialPow(a, n, x) << endl;
    cout << "Hodnota pre x=" << x << " je " << evaluatePolynomialMult(a, n, x) << endl;
    cout << "Hodnota pre x=" << x << " je " << evaluatePolynomialHorner(a, n, x) << endl;
}

void testAdd() {
    int na, nb;
    double a[maxN], b[maxN];
    cout << "Prvy polynom:" << endl;
    enterPolynomial(a, na, maxN);
    cout << endl << "Druhy polynom:" << endl;
    enterPolynomial(b, nb, maxN);
    double c[maxN];
    int nc;
    addPolynomials(a, na, b, nb, c, nc);
    cout << endl << "Ich sucet:" << endl;
    writePolynomial(c, nc);
}

int main(void) {

    testEvaluate();
    testAdd();

}

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>
using namespace std;

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

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) {
    int n = 6;
    int a[6] = {9, 3, 7, 4, 5, 2};

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

Prednáška 7

Organizačné poznámky

  • Viacerí ste mali písomky menej bodov ako ste čakali alebo dúfali
    • Nie je to dôvod na paniku, ale ak máte pocit, že vám príklady z cvičení, DÚ alebo písomky nejdú, treba s tým rýchlo začať niečo robiť (premenné, podmienky, cykly, polia a funkcie budeme používať aj zvyšok semestra)
    • Neváhajte sa pýtať na prednáškach a cvičeniach, prípadne emailom
    • Pred cvičeniami si pozrite predchádzajúce cvičenia a predchádzajúce prednášky (aby ste vedeli rýchlejšie začať riešiť príklady na cvičení)
    • Ak vám učebná látka robí problémy, skúste preriešiť čo najviac príkladov z cvičení
    • Príďte na doplnkové cvičenia
    • Nechajte si dosť času na domáce úlohy
    • Opravná písomka bude v piatok 20.10. o 13:00, prihlásiť sa treba v AIS do stredy 18.10.
  • Tento týždeň začíname rekurziu - prednášky dnes aj v stredu a aj na cvičení.

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
  • Hlavný rozdiel je, že v prípade, že nájdeme prvok poľa, ktorý je väčší ako hľadané x, tak zastavíme a odpovieme, že prvok tam nie je.
int a[7]={2,5,21,32,38,45,50}
x=21
   left=0 right=6: index=0; A[index]<x
   left=1 right=6; index=1; A[index]<x
   left=2 right=6; index=2; A[index]=x  -> return 2
x=11
   left=0 right=6: index=0; A[index]<x
   left=1 right=6; index=1; A[index]<x 
   left=2 right=6; index=2; A[index]>x
   left=2 right=1; left>right           -> koniec while cyklu -> return -1
  • V binárnom vyhľadávaní preto 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.

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 minulej 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 budúci týždeň

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ť?

Pamäťová zložitosť

Ďalší bežný dôvod, prečo nejaký algoritmus môže byť lepší ako iný je, že potrebuje na svoju činnosť menšiu pamäť. Majme napríklad klasickú úlohu - hľadanie najväčšieho prvku. To môžeme riešiť dvomi spôsobmi:

  • Všetky čísla si zapamätáme do poľa a následne nájdeme maximum v poli.
  • Budeme zisťovať maximum priebežne počas načítavania vstupu a pamätať si ho v jednej premennej.

Na prvý spôsob si potrebujeme zapamätať pole čísel veľkosti n a ešte nejaké pomocné premenné k tomu. Pre druhý spôsob (priebežné počítanie) si potrebujeme pamätať iba zopár pomocných premenných. Zjavne teda pre nejaké väčšie n bude druhý spôsob potrebovať oveľa menšiu pamäť.

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.

Prednáška 8

Oznamy

  • Do dnes sa v AIS môžete prihlásiť na opravnú písomku, ktorá bude v piatok o 13:00 (ak sa neviete prihlásiť, pošlite nám email)
  • Budúci pondelok termín DÚ1, nenechávajte si ju na poslednú chvíľu
  • Budúci pondelok od 14:00 v H6 Linux Installfest
    • Dobrovoľníci pomáhajú inštalovať Linux na donesené počítače, napr. aj ako dual boot (po zapnutí počítača si volíte Windows alebo Linux)
    • Na tento predmet Linux na svojom počítači nutne nepotrebujete, ale neskôr v štúdiu sa vám môže zísť
    • V prípade záujmu vyplňte do piatku dotazník

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.

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ší. Dnes a na ďalších prednáškach si ukážeme 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).

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 n 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ý.

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);
}

Cvičenie

  • Ako by sme vypisovali všetky k-tice v opačnom poradí, od n-1,n-1,n-1 po 0,0,0?
  • Ako by sme vypisovali všetky postupnosti dĺžky k pozostávajúce z písmen "A" a "B"?

Prednáška 9

Organizačné poznámky

Plán na najbližšie obdobie:

  • Dnes normálne prednáška a doplnkové cvičenia s rozcvičkou
  • Dnes do 22:00 termín na DÚ1
  • V utorok cvičenia - cvičíme rekurziu
  • V stredu prednáška - rekurzia a rekurzívne triedenia
  • Budúci týždeň odpadne - rektorské a dekanské voľno a sviatok (odpadnú obe prednášky a obe cvičenia)
  • Druhá písomka bude v stredu 8.11. o 18:10. Miestnosť oznámime neskôr. Bude pokrývať učivo po prednášku 9 (funkcie, polia, rekurzia).

Opakovanie 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
  • 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);
}
  • Ako by sme vypisovali všetky k-tice v opačnom poradí, od 111 po 000?

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?
  • žiadne dve po sebe idúce cifry nie sú rovnaké?
  • 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?

Problém batoha, Knapsack problem

Zlodej sa vlúpal do obchodu a vidí n vecí, pričom pre každú z nich vie odhadnúť jej hmostnosť a cenu, za ktorú by ju vedel predať. V svojom batohu však vie odniesť len veci s celkovou hmotnosťou najviac W kilogramov. Ako má vybrať veci, aby mali čo najväčšiu cenu a aby ich celková hmostnosť neprekročila W?

Príklad: majme n=3 veci, pričom vec 0 má hmostnoť 10 a cenu 6, vec 1 má hmotnosť 8 a cenu 4 a vec 2 má hmotnosť 6 a cenu 3. Zlodej unesie najviac 15. Na vstupe to zapíšeme takto:

Zadaj pocet predmetov: 3
Zadaj hmotnost a cenu 0-teho predmetu: 10 6
Zadaj hmotnost a cenu 1-teho predmetu: 8 4
Zadaj hmotnost a cenu 2-teho predmetu: 6 3
Zadaj nosnost batoha: 15
Zober predmety 1, 2. Ich cena je 7.

Najlepšie je zobrať veci 1 a 2. Ich cena je 7 a súčet hmotností 14.

Tento problém ešte stretnete v ďalších ročníkoch štúdia, teraz si ukážeme jednoduchý program, ktorý prehľadáva všetky možnosti.

Jednoduché riešenie: pozeráme všetky podmnožiny

Ako predtým, generujeme všetky podmnožiny a pre každú spočítame, či jej hmotnosť nepresahuje nosnosť batoha. Podmnožiny však nevypisujeme, ale porovnávame s najlepšou nájdenou doteraz.

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

/* struktura na ukladanie udajov o jednej veci */
struct vec {
    int hmotnost;
    int cena;
};

const int maxN = 100; /* maximalny pocet veci */

/* globalne premenne pouzivane v rekurzii */
int n;           /* celkovy pocet veci v obchode */
vec a[maxN];     /* pole veci */
int maxCena;     /* najlepsie doteraz najdene riesenie */
bool maxZober[maxN];  /* ktore veci su v najlepsom rieseni */
int nosnost;     /* kolko unesie batoh */

/* spocitaj sucet hmotnosti vybranych predmetov */
int sucetHmotnosti(bool zober[]) {
    int sucet = 0;
    for (int i = 0; i < n; i++) {
        if(zober[i]) sucet += a[i].hmotnost;
    }
    return sucet;
}

/* spocitaj sucet cien vybranych predmetov */
int sucetCien(bool zober[]) {
    int sucet = 0;
    for (int i = 0; i < n; i++) {
        if(zober[i]) sucet += a[i].cena;
    }
    return sucet;
}

/* vypis zoznam vybranych predmetov */
void vypis(bool zober[]) {
    cout << "Zober predmety ";
    bool prve = true;    
    for (int i = 0; i < n; i++) {
        if (zober[i]) {
            if (prve) {
                cout << i;
                prve=false;
            } else {
                cout << "," << i;
            }
        }
    }
    cout << ". Ich cena je " << sucetCien(zober) << "." << endl;
}

void generuj(bool zober[], int i) {
    /* v poli a dlzky k mame rozhodnutie o prvych i
     * prvkoch, chceme vygenerovat vsetky podmnoziny
     * prvkov {i..n-1} a kazdu skontrolovat a porovnat s maximom */
    if (i == n) {
        /* uz sme sa rozhodli o kazdom prvku. Zistime, ci mame hmostnost
         * vybranych predmetov <= nosnost */
        if(sucetHmotnosti(zober)<=nosnost) {
            /* ak ano, zistime, ci cena vybranych predmetov
             * je viac ako doteraz najlepsie maximum */
            int cena = sucetCien(zober);
            if(cena>maxCena) {
                /* prekopiruj sucasny vyber do najlepsieho */
                maxCena = cena;
                for(int i=0; i<n; i++) {
                    maxZober[i] = zober[i];
                }
            }
        }
    } else {
        /* dosad true aj false na miesto i
         * a skusaj vsetky moznosti pre zvysok pola */
        zober[i] = true;
        generuj(zober, i+1);
        zober[i] = false;
        generuj(zober, i+1);
    }
}

int main(void) {
    cout << "Zadaj pocet predmetov: ";
    cin >> n;
    /* nacitame hmotnost a cenu predmetov */
    for(int i=0; i<n; i++) {
        cout << "Zadaj hmotnost a cenu " << i << "-teho predmetu: ";
        cin >> a[i].hmotnost >> a[i].cena;
    }
    /* nacitame nostnost */
    cout << "Zadaj nosnost batoha: ";
    cin >> nosnost;
    
    bool zober[maxN];
    maxCena = -1; /* doteraz najlepsie riesenie ma cenu -1 */
    generuj(zober, 0); /* prehladavaj vsetky moznosti */

    vypis(maxZober); /* vypis najlepsie najdene riesenie */
}

Trochu rýchlejší program: skončíme vždy keď prekročíme nosnosť

Keď už sme sa rozhodli o prvých i veciach, sčítame hmotnosť tých, čo sme vybrali, a ak prekračuje nosnosť, túto vetvu hľadania ukončíme - nemá zmysel dopĺňať ďalšie hodnoty do zvyšku poľa, ak už zvolené veci sú príliš ťažké.

/* spocitaj sucet hmotnosti vybranych predmetov,
 * ale uvazujme iba prvych k predmetov  */
int sucetHmotnosti(bool zober[], int k) {
    int sucet = 0;
    for (int i = 0; i < k; i++) {
        if(zober[i]) sucet += a[i].hmotnost;
    }
    return sucet;
}

void generuj(bool zober[], int i) {
    /* v poli a dlzky k mame rozhodnutie o prvych i
     * prvkoch. Chceme prehladat vsetky perspektivne podmnoziny
     * prvkov {i..n-1} a kazdu skontrolovat a porovnat s maximom */

    /* ak uz sme prekrocili nosnost, nemusime pokracovat v hladani */
    if(sucetHmotnosti(zober, i) > nosnost) return;
    if (i == n) {
        /* uz sme sa rozhodli o kazdom prvku.
         * Sucasne vieme, ze hmostnost
         * vybranych predmetov <= nosnost.
         * Zistime teda, ci cena vybranych predmetov
         * je viac ako doteraz najlepsie maximum */
        int cena = sucetCien(zober);
        if (cena > maxCena) {
            /* prekopiruj sucasny vyber do najlepsieho */
            maxCena = cena;
            for (int i = 0; i < n; i++) {
                maxZober[i] = zober[i];
            }
        }
    } else {
        /* dosad true aj false na miesto i
         * a skusaj vsetky moznosti pre zvysok pola */
        zober[i] = true;
        generuj(zober, i + 1);
        zober[i] = false;
        generuj(zober, i + 1);
    }
}

Ešte rýchlejší program: neprepočítavame nosnosť a cenu vždy znovu a znovu

Predchádzajúci program vždy znovu a znovu prepočítava hmotnosť a cenu, aj keď sa zoznam vybraných predmetov zmení iba trochu. Namiesto toho si môžeme v rekurzii udržiavať aktuálnu hmotnosť aj cenu doteraz vybraných predmetov.

void generuj(bool zober[], int i, int hmotnost, int cena) {
    /* v poli a dlzky k mame rozhodnutie o prvych i
     * prvkoch, hmotnost je sucet hmotnosti uz vybranych predmetov
     * a cena je sucet ich cien.
     * Chceme prehladat vsetky perspektivne podmnoziny
     * prvkov {i..n-1} a kazdu skontrolovat a porovnat s maximom */

    /* ak uz sme prekrocili nosnost, nemusime pokracovat v hladani */
    if(hmotnost > nosnost) return;
    if (i == n) {
        /* uz sme sa rozhodli o kazdom prvku.
         * Sucasne vieme, ze hmostnost
         * vybranych predmetov <= nosnost.
         * Zistime teda, ci cena vybranych predmetov
         * je viac ako doteraz najlepsie maximum */
        if (cena > maxCena) {
            /* prekopiruj sucasny vyber do najlepsieho */
            maxCena = cena;
            for (int i = 0; i < n; i++) {
                maxZober[i] = zober[i];
            }
        }
    } else {
        /* dosad true aj false na miesto i
         * a skusaj vsetky moznosti pre zvysok pola */
        zober[i] = true;
        generuj(zober, i + 1, hmotnost + a[i].hmotnost, cena + a[i].cena);
        zober[i] = false;
        generuj(zober, i + 1, hmotnost, cena);
    }
}
  • V main zavoláme generuj(zober, 0, 0, 0);
    • Prečo sme nastavili cenu aj hmotnost na 0?

Prednáška 10

Oznamy

  • Budúci týždeň programovanie nebude - rektorské a dekanské voľno a sviatok
  • Pondelok 6.11. opäť normálny rozvrh (vrátane doplnkových cvičení k tomuto týždňu)
  • DÚ2 odovzdávajte do pondelka 6.11. 22:00.
    • V prípade otázok nás kontaktujte emailom aj budúci týždeň
  • Druhá písomka bude v stredu 8.11. o 18:10. Miestnosť oznámime neskôr. Bude pokrývať učivo po prednášku 9 (funkcie, polia, rekurzia).

Zopár komentárov k DÚ a cvičeniam

  • Odporúčam správne programy odsadzovať
    • je to prehľadnejšie, ľahšie sa hľadajú chyby
    • na DÚ za škaredé programy môžete aj stratiť body
    • v Netbeans môžete vysvietiť program, kliknúť pravým tlačidlom myši a zvoliť Format, preformátuje vám automaticky; v Linuxe odporúčam program astyle
  • Bodujeme len odovzdania spravené načas
    • ak niečo odovzdáte o pár minút neskoro alebo ak máte na neskoré odovzdanie závažný dôvod, môžete nás požiadať o predĺženie termínu, ale iniciatíva musí prísť od vás
  • Ospravedlňujem sa za medzeru na konci riadku v príklade o BubbleSorte (cvičenia 4, príklad 5 bonus). Ak vidíte v zadaní alebo na testovači niečo divné, kontaktujte nás, chybu opravíme.

Rozdeľuj a panuj, rýchle triedenia

Videli sme tri algoritmy na triedenie: bubblesort, insertsort, maxsort

  • Jednoduché, ale pomalé, 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 si ukážeme dve rýchlejšie triedenia, mergesort a quicksort.

  • obe používajú metódu rozdeľuj a panuj

Rozdeľuj a panuj je všeobecný rekurzívny postup, ktorým sa dá riešiť viacero problémov. Má nasledujúce fázy:

  • Rozdeľuj: rozdelíme problém na nejaké menšie časti (podproblémy), ktoré sa dajú riešiť ďalej samostatne.
  • Vyriešime podproblémy: rekurzívne vyriešime úlohu pre každý podproblém
  • Panuj: spojíme výsledky pre podproblémy do výsledku celkového problému.

Triedenie zlučovaním, MergeSort

V triedení zlučovaním sa pole veľkosti N rozdelí na polovicu najjednoduchším spôsobom - na prvú a druhú polovicu. Tieto rekurzívne utriedime, čím dostávame 2 utriedené postupnosti. Preto vo fáze panuj potrebujeme tieto dve polia zlúčiť (merge) do výsledného poľa.

void mergesort(int a[], int low, int high) {
/* utriedi prvky poľa a od indexu low po index high (vrátane) */

    // triviálny prípad: ak máme 1 alebo 0 prvkov
    if (low >= high) return;

    // rozdeľuj: spočíta stred triedeného úseku
    int mid = (low+high) / 2;

    //rekurzívne volanie na dva podproblémy
    mergesort(a, low, mid);       // triedi prvky od low po mid
    mergesort(a, mid+1, high);    // triedi prvky od mid+1 po high 

    // panuj: zlúči dve utriedené postupnosti do jednej 
    merge(a, low, mid, high);
}

Zlučovanie utriedených postupností

Zostáva nám naprogramovať zlúčenie dvoch utriedených postupností. V prvej verzii predpokladajme, že máme dve utriedené postupnosti A[0..N-1] a B[0..M-1] a chceme ich zlúčiť do utriedeného poľa C dĺžky N+M.

  • Prvým prvkom výslednej postupnosti bude menší z prvkov A[0] a B[0]. Potom ostávajú postupnosti A[1..N-1] a B[0..M-1] alebo A[0..N-1] a B[1..M-1].
  • Vo všeobecnosti máme postupnosti A[i..N-1] a B[j..M-1]. Ďalším prvkom postupnosti bude buď A[i] alebo B[j] a ostanú nám postupnosti A[i+1..N-1] a B[j..M-1] alebo A[i..N-1] a B[j+1..M-1].
  • Toto robíme dovtedy, kým s jedným poľom neskončíme. Vtedy na koniec dokopírujeme zvyšok druhého poľa.

Z toho dostávame nasledovnú funkciu.

void merge(int A[], int N, int B[], int M, int C[]) {
    /* zlúči utriedené pole A dĺžky N a utriedené pole B dĺžky M 
     * do poľa C, ktoré musí mať dĺžku aspoň N+M */

    int i = 0;
    int j = 0;
    while (i < N && j < M) { // kym v oboch poliach zostavaju prvky
        if (A[i] <= B[j]) {
            C[i + j] = A[i]; // vysledok ukladame na (i+j)-te miesto
            i++;
        } else {
            C[i + j] = B[j];
            j++;
        }
    }

    while (i < N) { // ak skoncilo pole B, prekopirujeme zvysok pola A
        C[i + j] = A[i];
        i++;
    }

    while (j < M) {  // ak skoncilo pole A, prekopirujeme zvysok pola B
        C[i + j] = B[j];
        j++;
    }
}

Výsledný MergeSort

Ostáva už iba drobnosť: nezlučujeme dve nezávislé polia, ale prvky jedného poľa a navyše by sme radi, aby tie prvky v tomto poli aj skončili.

  • Funkcia namiesto dvoch vstupných polí dostane iba pole A a pozície začiatkov a koncov úsekov.
    • Keďže zlučované úseky idú za sebou, stačia nám tri indexy: low, mid a high
    • Hodnotu mid by sme si mohli aj dopočítať z low a high.
  • Druhý problém vyriešime pomocným poľom C, ktoré na konci nakopírujeme naspäť do A.
    • Keďže merge už nevolá ďalšie funkcie, v zásobníku bude vždy len jedno pole C, po skončení zlučovania sa vždy vymaže.
#include <iostream>
using namespace std;

const int MAX = 100;

void merge(int A[], int low, int mid, int high) {
    /* V poli A su utriedene useky A[low..mid] a A[mid+1..high].
     * Zluci ich do utriedeneho useku A[low..high]. */

    int C[MAX];       // pomocne pole
    int i = low;      // poloha v prvej catsi pola
    int j = mid + 1;  // poloha v druhej casti pola
    int k = 0;        // poloha v zlucenom poli
    while (i <= mid && j <= high) { // kym v oboch poliach zostavaju prvky
        if (A[i] <= A[j]) {  // pouzi prvok z lavej casti
            C[k] = A[i];
            i++;
            k++;
        } else {            // pouzi prvok z pravej casti
            C[k] = A[j];
            j++;
            k++;
        }
    }

    while (i <= mid) { // ak skoncila prava cast, prekopirujeme zvysok lavej
        C[k] = A[i];
        i++;
        k++;
    }

    while (j <= high) { // ak skoncila lava cast, prekoprujeme zvysok pravej
        C[k] = A[j];
        j++;
        k++;
    }

    for (int k = low; k <= high; k++) {
        A[k] = C[k - low];
    }
}

void mergesort(int A[], int low, int high) {
    /* utriedi prvky pola a od indexu low po index high (vratane) */

    // trivialny pripad: ak mame 1 alebo 0 prvkov
    if (low >= high) return;

    // rozdeluj: spocita stred triedeneho useku
    int mid = (low + high) / 2;

    //rekurzivne volanie na dva podproblemy
    mergesort(A, low, mid);
    mergesort(A, mid + 1, high);

    // panuj: zluci dve utriedene postupnosti do jednej
    merge(A, low, mid, high);
}

int main(void) {
    const int N = 10;
    int A[N] = {13, 1, 5, 7, 2, 4, 8, 10, 3, 24};
    mergesort(A, 0, N - 1);
    for (int i = 0; i < N; i++) cout << A[i] << " ";
}

Ukážka na príklade

Pole A[]= {6, 1, 5, 7, 2, 4, 8, 9, 3, 0};

Všetky volania:

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)

Čo sa deje v poli:

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|

Odhad zložitosti

Zlučovanie dvoch postupností, ktoré spolu obsahujú N prvkov, trvá čas O(N). Prečo?

V algoritme má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 \log_2 N} úrovní rekurzie, lebo na prvej spracovávame úseky dĺžky N, na druhej N/2 na tretej N/4 atď, až pol 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} úrovniach dostaneme úseky dĺžky 1. Na každej úrovni je každý prvok najviac v jednom zlučovaní, teda celkový čas zlučovaní na každej úrovni je O(N). Celkový čas výpočtu je O(N log N).

Quicksort

Quicksort je tiež založený na metóde rozdeľuj a panuj. Postupuje nasledovne:

  • Rozdeľuj: prvky poľa rozdelí na dve skupiny: prvky menšie ako nejaké x a prvky väčšie alebo rovné x
    • hodnotu x nazývame pivot
  • Rekurzívne utriedi obe skupiny prvkov a dostáva dve utriedené postupnosti - utriedenú postupnosť menších prvkov a utriedenú postupnosť väčších prvkov.
  • Panuj: stačí dať tieto utriedené postupnosti za seba (najskôr menšie, potom väčšie)
void quicksort(int A[], int low, int high) {
    /* utriedi prvky v A medzi low a high (vratane) */
    
    if (low >= high) return;  // trivialny pripad: 0 alebo 1 prvok

    // rozdelenie: mid je posledny index medzi mensimi prvkami
    int mid = partition(A, low, high);

    quicksort(A, low, mid);     // rekurzivne utried mensie prvky
    quicksort(A, mid + 1, high); // rekurzivne utried vacsie prvky

    //zlucenie nebude treba - su spravne za sebou
}

Čo sa stane, ak funkcia partition zvolí taký pivot, že jedna zo skupín je prázdna?

Aby sme sa takýmto problémom vyhli, upresníme trochu úlohu funkcie partition:

  • ako pivot x si zvolí jeden z prvkov A[low..high]
  • preusporiada túto časť poľa tak, že
    • A[low..mid-1] obsahuje prvky < x
    • A[mid+1..high] onsahuje prvky >= x
    • A[mid] obsahuje x, ktorý je teda už na svojom finálnom mieste
  • vráti index mid

Potom stačí rekurziu volať na A[low..mid-1] a A[mid+1..high]

Ak sme teda začali s N prvkami, ľavá aj pravá časť poľa bude mať najviac N-1 prvkov.

Rozdelenie prvkov, funkcia partition

Jednoduchá implementácia funkcie partition by mohla používať dve pomocné polia: do jedného si dá menšie prvky, do druhého väčšie.

Dá sa však ľahko napísať priamo v poli A, bez pomocných polí. Počas algoritmu budeme udržiavať nasledujúci invariant:

  • na indexe low je pivot x
  • na indexoch low+1..lastLeft sú prvky menšie ako x
  • na indexoch lastLeft+1..unknown-1 sú prvky >= x
  • na indexoch unknown..high sú prvky, ktoré sme ešte s x neporovnali

V každom kroku porovnáme A[unknown] s x

  • ak je menší, potrebujeme ho dať do ľavej časti, vymeníme ho teda s A[lastLeft+1] a rozšírime ľavú časť
  • ak je >= x, jednoducho rozšírime pravú časť

Nakoniec ešte vymeníme A[low] a A[lastLeft], čím sa pivot dostane na svoje miesto

int partition(int A[], int low, int high) {
    /* vrati index mid (low<=mid<=high)
     * a preusporiada prvky v A[low..high] tak, ze plati
     * A[low..mid-1] obsahuje prvky < A[mid]
     * A[mid+1..high] obsahuje prvky >= A[mid] */

    // ak chceme pouzit iny prvok ako pivot, vymenime ho s A[low]
    int pivot = A[low];
    int lastLeft = low;
    for (int unknown = low + 1; unknown <= high; unknown++) {
        if (A[unknown] < pivot) {
            lastLeft++;
            swap(A[unknown], A[lastLeft]);
        }
    }
    swap(A[low], A[lastLeft]);
    return lastLeft;
}

Výsledný Quicksort

#include <iostream>
using namespace std;

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

int partition(int A[], int low, int high) {
    /* vrati index mid (low<=mid<=high)
     * a preusporiada prvky v A[low..high] tak, ze plati
     * A[low..mid-1] obsahuje prvky < A[mid]
     * A[mid+1..high] obsahuje prvky >= A[mid] */

    // ak chceme pouzit iny prvok ako pivot, vymenime ho s A[low]
    int pivot = A[low];
    int lastLeft = low;
    for (int unknown = low + 1; unknown <= high; unknown++) {
        if (A[unknown] < pivot) {
            lastLeft++;
            swap(A[unknown], A[lastLeft]);
        }
    }
    swap(A[low], A[lastLeft]);
    return lastLeft;
}

void quicksort(int A[], int low, int high) {
    /* utriedi prvky v A medzi low a high (vratane) */
    
    if (low >= high) return;  // trivialny pripad: 0 alebo 1 prvok

    // rozdelenie: mid je posledny index medzi mensimi prvkami
    int mid = partition(A, low, high);

    quicksort(A, low, mid - 1);     // rekurzivne utried mensie prvky
    quicksort(A, mid + 1, high); // rekurzivne utried vacsie prvky
}

int main(void) {
    const int N = 10;

    int A[N] = {6, 1, 5, 7, 2, 4, 8, 9, 3, 0};
    quicksort(A, 0, N - 1);
    for (int i = 0; i < N; i++) cout << A[i] << " ";
}

Príklad

Volania funkcií

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)

Čo robí partition:

partition(0,9): |6 1 5 7 2 4 8 9 3 0| -> |0 1 5 2 4 3|6|9 7 8|
partition(0,5): |0 1 5 2 4 3|6 9 7 8  -> |0|1 5 2 4 3|6 9 7 8
partition(1,5):  0|1 5 2 4 3|6 9 7 8  ->  0|1|5 2 4 3|6 9 7 8
partition(2,5):  0 1|5 2 4 3|6 9 7 8  ->  0 1|3 2 4|5|6 9 7 8
partition(2,4):  0 1|3 2 4|5 6 9 7 8  ->  0 1|2|3|4|5 6 9 7 8
partition(7,9):  0 1 2 3 4 5 6|9 7 8| ->  0 1 2 3 4 5 6|8 7|9|
partition(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 Quicksort správať, keď mu dáme utriedené pole?
  • Ako sa bude správať, ak budú prvky v poli od najväčšieho po najmenšie?

Odhad zložitosti

V ideálnom prípade pivot rozdelí pole na dve rovnako veľké časti. Vtedy je čas výpočtu O(N log N), podobne ako pre Mergesort.

Nepríjemné je, keď pivot je vždy najmenší alebo najväčší prvk v danom úseku. Vtedy je čas 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)} . Dobrá správa je, že takýchto prípadov nie je príliš veľa.

  • Aby sme sa vyhli problémom, ak je vstupné pole (takmer) utriedené, môžeme ho pred triedením náhodne premiešať alebo vyberať ako pivot náhodný prvok z intervalu

Iná implementácia

Občas sa môžete stretnúť aj s takto napísaným quicksortom. Skúste si premyslieť, prečo funguje a prečo si môžeme dovoliť niektoré prvky vynechať.

void quickSort(int A[], int left, int right) {
    if (left>=right) return;
    int i = left, j = right;
    int pivot = A[(left + right) / 2];

    /* partition */
    while (i <= j) {
        while (A[i] < pivot) i++;
        while (A[j] > pivot) j--;
        if (i <= j) {
            int tmp = A[i]; A[i] = A[j]; A[j] = tmp;
            i++; j--;
        }
    };

    /* rekurzia */
    quickSort(A, left, j);
    quickSort(A, i, right);
}

Zhrnutie: triedenia

Jednoduché triedenia: bubblesort, insertsort, maxsort

  • 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, rozdeľuj a panuj

  • Rýchlejšie, zložitejšie
  • Mergesort, 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)}
  • Quicksort, 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 stupov 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 Mergesort

Príklad: na mojom počítači som triedila pol milióna náhodne permutovaných čísel programami z prednášok

  • quicksort: 0.25s
  • mergesort: 0.27s
  • insertionsort: 58s

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 << "\n" << "Zadaj n (1,2,3,4): ";
  cin >> n;
  switch (n) {
    case 1: cout << "Jeden\n";
    case 2: cout << "Dva\n";
    case 3:
    case 4: cout << "Tri alebo styri\n";
    default: cout << "Chyba!\n";
  }
  cout << "Koniec.\n";
}

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 11

Organizačné poznámky

  • Druhá písomka bude v stredu (8.11.) o 18:10 v F1-328. Bude pokrývať učivo po prednášku 9, vrátane (funkcie, polia, rekurzia)
  • Opravná písomka bude v utorok 21.11. o 16:30 (po cvičeniach). Nejaký konflikt o ktorom nevieme?
  • Na domácich úlohách a cvičeniach dodržujte pokyny zo zadania ohľadom vecí, ktoré máte alebo nesmiete v programoch použiť. Cieľom týchto pokynov je dosiahnuť, aby ste si precvičili určité časti učiva.

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 dotazom na tú istú premennú je vhodnejšie 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.
    • Pozor na použitie switch v cykle a použitie break, ktorý vyskočí iba z najvnútornejšieho prostredia (v tomto prípade zo switch a nie z cyklu).

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)
        /* vsimnite si ze nepripocitavame priamo c ale c-'0': ide totiz o kod znaku a '0' ma kod 48 */
        N = N * 10 + (c - '0'); // 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 a znakom '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};
  • Š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

  • 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.
    • Podobá sa na príklad s vyhadzovaním núl z poľa z cvičení
    • 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 12

Organizačné poznámky

  • Druhá písomka bude dnes (8.11.) o 18:10 v F1-328.
    • Opravná písomka bude v utorok 21.11. o 16:30 (po cvičeniach).
  • DÚ3 do pondelka 20.11. 22:00
  • Bodujeme len odovzdania spravené načas
    • ak niečo odovzdáte o pár minút neskoro alebo ak máte na neskoré odovzdanie závažný dôvod, môžete nás požiadať o predĺženie termínu (emailom na E-prg.png)

Opakovanie: reťazce

  • 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
  • Skúsme si naprogramovať včerajšiu rozcvičku o palindrómoch

Ukazovateľ, smerník, pointer

  • Pamäť v počítači je rozdelená na dieliky, do ktorých sa zapisujú hodnoty premenných
  • Väčšinou k týmto dielikom pristupujeme pomocou mien premenných
  • Každý dielik má adresu (niečo ako poradové číslo)
  • Ukazovateľ (resp. smerník alebo pointer) je premenná, ktorej hodnota je adresa iného dieliku pamäte

Definovanie premennej typu ukazovateľ

  • Nový ukazovateľ je definovaný pomocou typu s pridanou *
int i;      // klasická celočíselná premenná
int *pi;   // ukazovateľ na celočíselnú premennú
  • Typ ukazovateľa je teda rôzny pre rôzne typy premenných
    • nedá sa priraďovať navzájom napr. ukazovateľ na int a ukazovateľ na char
int *pi;
char *pc;
pc = pi;     // chyba

Operátor & (adresa)

  • Adresu nejakej premennej vieme zistiť operátorom &
  • Tú potom môžeme priradiť do premennej typu ukazovateľ
  • Ak do premennej typu ukazovateľ nič nepriradíme, má nedefinovanú hodnotu, ukazuje na náhodné miesto v pamäti, alebo niekde mimo
int i;
int *pi = &i;

Premenná pi teraz ukazuje na miesto, kde je uložené i

Smerník, ktorý nikam neukazuje

  • Nulový ukazovateľ NULL - konštanta definovaná v knižnici cstdlib
  • Je možné priradiť ho ukazovateľom na ľubovoľný typ
  • Testovanie: if (pi == NULL) {...}
  • V C++ môžeme používať namiesto NULL aj 0.

Operátor * (dereferencovanie, údaje na adrese)

  • Ak máme ukazovateľ x, dáta z adresy, na ktorú ukazuje, získame ako *x
    • Môžeme ich aj meniť
    int i = 1;
    int j = 2;
    int *p = &i;         // p ukazuje na i 
    cout << *p << endl;  // vypise 1
    p = &j;              // p ukazuje na j
    cout << *p << endl;  // vypise 2
    (*p)++;              // zvysime j o 1
    cout << *p << endl;  // vypise 3
    cout << j << endl;   // vypise 3

Príklady

  • Do premennej typu smerník na niečo môžeme priradiť iba adresu premennej typu niečo (alebo NULL).
int i, *p_i;

p_i = &i;       // spravne
p_i = &(i + 3); // zle, i+3 nie je premenna
p_i = &15;      // zle, konstanta nema adresu
p_i = 15;       // zle, priradovanie intu do adresy
i = p_i;        // zle, priradovanie adresy do intu
i = &p_i;       // zle, priradovanie adresy do intu
i = *p_i;       // spravne ak p_i bol inicializovany
*p_i = 4;       // spravne ak p_i bol inicializovany

Parametre funkcií

  • Odovzdávanie hodnotou: nakopíruje hodnotu do novej lokálnej premennej
void nasob(int x) {
    x = 2*x;
    cout << x;
}
int main(void){
    int x = 1;
    nasob(x);   // vypise 2
    cout << x;  // vypise 1
}
  • Odovzdávanie referenciou: vytvorí nové meno pre existujúcu premennú, zmeny zostávajú aj po skončení
void nasob(int &x) {
    x = 2*x;
    cout << x;
}
int main(void){
    int x = 1;
    nasob(x);   // vypise 2
    cout << x;  // vypise 2
}
  • Nové: parameter je smerník
    • Volajúca funkcia pošle smerník, ten sa hodnotou skopíruje do lokálnej premennej
    • Ak ale robíme zmeny na adrese uloženej v parametri, majú dosah aj mimo funkcie
void nasob(int *p_x) {
    *p_x = *p_x * 2;
    cout << *p_x;
}
int main(void){
    int x = 1;
    nasob(&x);   // vypise 2
    cout << x;   // vypise 2
}
  • V čistom C neboli referencie, týmto spôsobom sa odovzdávajú premenné, ktoré sa majú meniť
    • Stále používané v C-čkových štandardných knižniciach

Funkcia swap s použitím ukazovateľov

void swapP(int *p_x, int *p_y){ // ako parameter má dve adresy ukazujúce na celé čísla
    int pom = *p_x;             // do premennej pom nastavi hodnotu, ktorá je na adrese p_x                 
    *p_x = *p_y;                // hodnotu, ktorá je na adrese p_x zmení na hodnotu, ktorá je na adrese p_y 
    *p_y = pom;                 // hodnotu, ktorá je na adrese p_y nastaví na hodnotu premennej pom
} 

Ako budeme túto funkciu volať, ak chceme vymeniť hodnoty premenných i=5 a j=7 (obidve typu int)?

  • swapP(i, j); Zle. Vymieňame obsahy adries 5 a 7. Neskompiluje, lebo int sa nedá priradiť do int *
  • swapP(*i, *j); Zle. Vymieňame obsahy premenných uložených na adresách 5 a 7. Neskompiluje lebo operátor * sa nedá použiť na premenné typu int.
  • swapP(&i, &j); Správne. Pošle adresy premenných i a j a funkcia swapP vymení hodnoty na týchto adresách.

Dynamická alokácia a dealokácia pamäte: new, delete

Doteraz sme videli

  • globálne premenné: majú vopred známu veľkosť a vyhradenú pamäť
  • lokálne premenné: majú vopred známu veľkosť, ale nevieme, ktoré funkcie sa použijú (a v prípade rekurzie koľkokrát), preto sa im pamäť priraďuje až pri volaní funkcie na zásobníku funkčných volaní (call stack)

Teraz si ukážeme, ako môže si program vyhradiť počas behu ďalšiu pamäť podľa potreby

  • Používa sa na to príkaz new
  • Pamäť sa vyhradí v oblasti zvanej heap (halda, hromada)
  • Keď už pamäť nepotrebujeme, uvoľníme ju príkazom delete
  • Uvoľnená pamäť môže byť znovu použitá pri ďalších volaniach new
#include <iostream>
using namespace std;

int main(void){
    int * p;       // p je lokalna premenna, zatial neinicializovana
    p = new int;   // vypytam si novy kus pamate dost velky na 1 int
                   // a adresu ulozim do p
   
    *p = 50;       // do alokovanej pamati ulozim hodnotu 50 
    cout << *p << endl;  // vypisem 50
    delete p;     // alokovanu pamat uvolnim
}

Polia a smerníky

  • Polia a smerníky v jazyku C spolu veľmi úzko súvisia.
  • Pole sa správa (väčšinou) ako ukazovateľ na jeho prvý prvok.

Vezmime tento jednoduchý program s funkciou, ktorá vypíše obsah poľa

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

int main(void) {
    int a[3] = {0,1,2};
    vypisPole(a, 3);
}

Hlavičku funkcie môžeme zmeniť takto a všetko bude ďalej fungovať:

void vypisPole(int *a, int n) {

Tiež do premennej typu int * môžeme priradiť pole. Premenná bude potom fungovať ako smerník na prvý (presnejšie nultý) prvok poľa.

 int a[3] = {0,1,2};
 int *b = a;
 vypisPole(b, 3);

Alokovanie jednorozmerného poľa

  • Operátor new má verziu, ktorá alokuje pole zadanej dĺžky
  • Napr. int *a = new int[10];
  • Výsledok priradíme do ukazovateľa, odvtedy používame ako pole
  • Uvoľníme operátorom delete[].
  • Veľkosť poľa v new nemusí byť konštanta, môžeme si naalokovať také veľké pole, ako potrebujeme

Napríklad od užívateľa načítame počet čísel, vytvoríme pole príslušnej veľkosti a načítame do neho čísla:

#include <iostream>
using namespace std;

int main(void) {
    cout << "Zadaj pocet cisel: ";
    int n;
    cin >> n;
    int *a = new int[n];   // vytvorime pole

    for (int i = 0; i < n; i++) {  // nacitame data
        cout << "Zadaj cislo " << i << ": ";
        cin >> a[i];
    }
    for (int i = n-1; i >=0; i--) {  // spracujeme, napr. vypiseme odzadu
        cout << a[i] << endl;
    }
    delete[] a;
}

Kontrola programov nástrojom 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
  • 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

Nasledujúci program vypisuje neinicializovanú premennú i (knižnice pre krátkosť vynechávame):

int main(void) {
    int i; cout << i << endl;
}

Valgrind vypíše okrem iného

==6021== Conditional jump or move depends on uninitialised value(s)
==6021==    at 0x4EBAD23: 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.16)
==6021==    by 0x4EBB065: 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.16)
==6021==    by 0x4EC64DC: std::ostream& std::ostream::_M_insert<long>(long) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.16)
==6021==    by 0x40079A: main (main.cpp:7)
==6021== 
==6021== Use of uninitialised value of size 8
==6021==    at 0x4EBA823: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.16)
==6021==    by 0x4EBAD47: 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.16)
==6021==    by 0x4EBB065: 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.16)
==6021==    by 0x4EC64DC: std::ostream& std::ostream::_M_insert<long>(long) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.16)
==6021==    by 0x40079A: main (main.cpp:7)

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

int main(void) {
    int *p; 
    *p = 7; 
    cout << *p << endl;
}
==6125== Use of uninitialised value of size 8
==6125==    at 0x400790: main (main.cpp:7)
==6125== 
==6125== Invalid write of size 4
==6125==    at 0x400790: main (main.cpp:7)
==6125==  Address 0x0 is not stack'd, malloc'd or (recently) free'd
==6125== 
==6125== 
==6125== Process terminating with default action of signal 11 (SIGSEGV)
==6125==  Access not within mapped region at address 0x0
==6125==    at 0x400790: main (main.cpp:6)

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

int main(void) {
    int i = 7; 
    int *p = &i; 
    delete p;
}
==6579== Invalid free() / delete / delete[] / realloc()
==6579==    at 0x4C2A4BC: operator delete(void*) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==6579==    by 0x400696: main (main.cpp:8)
==6579==  Address 0x7ff00097c is on thread 1's stack

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:

int main(void) {
    int i;
    int *p = &i; 
    p[2] = 7;
    cout << p[2] << endl;
}

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

Zhrnutie

  • Smerník, ukazovateľ, pointer je premenná, v ktorej je uložená adresa nejakého pamäťového miesta
  • Typ smerníku určuje, na aký typ premennej by mal ukazovať, napr. int *p
  • Do smerníku môžeme priradiť NULL, adresu nejakej premennej (&i), novoalokovanú pamäť pomocou new, iný smerník toho istého typu
  • Ku políčku, na ktoré ukazuje smerník p, pristupujeme pomocou (*p)
  • Pole je vlastne smerník na svoj prvý (nultý) prvok
  • Pole určitej dĺžky (ktorá je známa až počas behu) alokujeme pomocou new typ[pocet]
  • Pamäť alokovanú cez new by sme mali odalokovať pomocou delete alebo delete[] (podľa toho, či to bolo pole)
  • Pri práci so smerníkmi ľahko spravíme chybu, pomôcť nám môže valgrind

Rastúce pole

V praxi často narazíme na nasledujúci problém: chceme zo vstupu načítať do poľa nejaké údaje, ale vopred nevieme, koľko ich bude a teda aké veľké pole potrebujeme vytvoriť.

  • Doteraz sme to riešili konštantou MaxN ohraničujúcou maximálnu povolenú veľkosť vstupu, ale to má problémy:
    • Ak je vstup väčší ako MaxN, nevieme ho spracovať, aj ak by inak kapacity počítača postačovali
    • Ak je vstup oveľa menší ako MaxN, zbytočne zaberáme pamäť veľkým poľom, ktorého veľká časť je nevyužitá
  • Pre jednoduchosť budeme uvažovať na vstupe postupnosť nezáporných čísel ukončenú -1, napr. 7 3 0 4 3 -1
    • Do poľa chceme uložiť všetko okrem poslednej -1

Riešením je postupne alokovať väčšie a väčšie polia podľa potreby

  • Začneme s malým poľom (veľkosti 2)
  • Vždy keď sa pole zaplní, alokujeme nové pole dvojnásobnej veľkosti, prvky do neho skopírujeme a staré pole odalokujeme
  • Presúvanie prvkov dlho trvá, preto pole vždy zdvojnásobíme, aby sme nemuseli presúvať často
  • Spolu pri načítaní n prvkov robíme najviac 2n presunov jednotlivých prvkov
#include <iostream>
using namespace std;

int *nacitajDoPola(int &n) {
    /* Cita cele nezaporne cisla az kym nepride na koniec. 
     * Uklada ich do dynamicky alokovaneho pola, ktore vrati, 
     * pocet nacitanych cisel ulozi do n */
    
    int velkost = 2;                // zacneme s polom s 2 prvkami
    int *pole = new int[velkost];
    n = 0;                           // 0 cisel je zatial ulozenych
    int hodnota;
    cin >> hodnota;
    while (hodnota >= 0) {
        if (n == velkost) {          // ak sme naplnili pole, presunieme do vacsieho
            velkost *= 2;
            int *nove = new int[velkost];
            /* prekopirujeme zo stareho do noveho */
            for (int i = 0; i < n; i++) {
                nove[i] = pole[i];
            }
            /* zmazeme stare */
            delete[] pole;
            pole = nove;
        }

        pole[n] = hodnota;  // v poli je miesto, ulozime novu hodnotu
        n++;
        cin >> hodnota;
    }
    return pole;
}

int main(void) {
    int n;
    int *a;
    a = nacitajDoPola(n);   // nacitame pole a a pocet n
    for (int i = 0; i < n; i++) {  // spracovavame, ako sme zvyknuti
        cout << " " << a[i];
    }
    cout << endl;
    delete[] a;                    // nakoniec zmazeme pole a
}

Prednáška 13

Oznamy

  • Výsledky druhej písomky sú na testovači, písomku si môžete pozrieť na opravných cvičeniach, prípadne na hlavných
  • Opravná písomka bude v utorok 21.11. o 16:30 (po cvičeniach), treba sa prihlásiť cez AIS do štvrtka
  • Tretia písomka bude v stredu 29.11., štvrtá v stredu 20.12.
    • Opravný termín k tretej a štvrtej písomke bude až cez skúškové
  • DÚ3 odovzdávajte do pondelka 20.11. 22:00.

Opakovanie smerníkov

Smerníky na jednoduché premenné:

int a = 7;         /* premenná typu int */
int *b = NULL;     /* smerník na premennú typu int */
b = &a;            /* b ukazuje na a */
*b = 8;            /* v premennej a je teraz 8 */
a = (*b)+1;        /* v premennej a je teraz 9 */

Smerníky a polia, alokovanie poľa:

int a[3];
int *b = a;  /* a,b sú teraz takmer rovnocenné premenné */
*b = 3;
b[1] = 4;
a[2] = 5; /* v poli sú teraz čísla 3,4,5 */
b = new int[a[1]];  /* b teraz ukazuje na nové pole dĺžky 4 */
delete[] b;         /* uvoľníme pamäť alokovanú pre nové pole */

Vector

Rastúce pole, opakovanie

  • Na konci minulej prednášky sme videli program, ktorý postupne zväčšoval pole, až kým nenačítal všetky čísla zo vstupu
  • Vždy keď sa pole naplnilo, alokoval pole dvojnásobnej veľkosti a presunul všetky prvky
  • V programe boli pomiešané príkazy na načítavanie a na prácu s poľom
  • Podobné rastúce pole by sa nám mohlo zísť aj na iné účely, preto si ho teraz spravíme poriadnejšie a budeme ho nazývať vector
  • V C++ knižniciach je štruktúra vector, ktorá podobne rastie podľa potreby
  • My si naprogramujeme jednoduchšiu verziu vektora

Funkcie na prácu s dátovou štruktúrou vector

  • Na začiatku programu si zadefinujeme, akého typu prvky budeme do vektora dávať (napr. int, doble, char a pod.)
typedef int dataType;
  • Náš vektor bude struct s potrebnými údajmi a bude poskytovať nasledujúce funkcie
    • void init(vector &a) vytvorí prázdny vektor
    • void add(vector &a, dataType x) pridá na koniec vektoru prvok x
    • dataType get(vector &a, int i) vráti prvok na pozícii i v poli, pričom kontroluje, či je index platný (od 0 po najväčší vložený prvok) a ak nie, ukončí program príkazom assert
    • void set(vector &a, int i, dataType x) na pozíciu i v poli uloží prvok x (s kontrolou indexu i)
    • int length(vector &a) vráti počet prvkov vo vektore
    • void destroy(vector &a) zmaže pamäť alokovanú pre vektor a
  • Štruktúra vector teda v sebe združuje pole aj jeho dĺžku, stačí posielať jeden parameter
  • Vector posielame do funkcií referenciou, aby sa zbytočne veľa nekopírovalo (a tiež aby sme ho mohli v prípade potreby meniť)
  • Vector by sme teraz vedeli použiť bez toho, aby sme vedeli, ako sú funkcie naprogramované vo vnútri
    • Môžeme teda implementáciu vectora meniť bez zmeny programu, ktorý ho používa
typedef double dataType;
// sem pride definicia typu vector + funkcie uvedene vyssie

int main(void) {
    vector a;
    init(a);    
    add(a, 0.5);    // mame 1-prvkove pole {0.5}
    add(a, 1.0);    // mame 2-prvkove pole {0.5, 1.0}
    set(a, 1, 1.5); // mame 2-prvkove pole {0.5, 1.5}
    cout << get(a,0) + get(a,1) << endl;  // vypise 2.0000
    destroy(a);
}
  • V objektovo-orientovanom programovaní (budúci semester) sa namiesto napr. get(a,0) píše niečo ako a.get(0)

Implementácia vektora

struct vector {
    dataType *a; /* pole prvkov typu dataType */
    int size; /* velkost alokovaneho pola */
    int n; /* pocet prvkov pridanych do pola */
};

void init(vector &a) {
    /* inicializuj vektor s kratkym polom */
    a.size = 2;
    a.a = new dataType[a.size];
    a.n = 0;
}

void add(vector &a, dataType x) {
    /* na koniec pola pridaj prvok x, zvacsi pole ak treba */

    if (a.n == a.size) {
        /* treba zvacsovat */
        dataType *nove = new dataType[a.size * 2];
        /* prekopirujeme zo stareho do noveho */
        for (int i = 0; i < a.n; i++) {
            nove[i] = a.a[i];
        }
        /* zmazeme stare */
        delete [] a.a;
        /* upravujeme premenne */
        a.a = nove;
        a.size *= 2;
    }
    /* teraz uz je pole dost velke, staci ulozit x */
    a.a[a.n] = x;
    a.n++;
}

dataType get(vector &a, int i) {
    /* vrat prvok v poli na pozicii i (s kontrolou i) */
    assert(i >= 0 && i < a.n);
    return a.a[i];
}

void set(vector &a, int i, dataType x) {
    /* na poziciu i uloz hodnotu x (s kontrolou i) */
    assert(i >= 0 && i < a.n);
    a.a[i] = x;
}

void destroy(vector &a) {
    delete[] a.a;
}

int length(vector &a) {
    return a.n;
}

Použitie vektora pri načítaní dát

  • Vráťme sa k príkladu, kde sme načítavali nezáporné čísla zo súboru až po hodnotu -1
  • Vtedy sme priamo pri načítaní čísel menili veľkosť poľa
  • Teraz vieme zapísať pomocou operácie add, oddelili sme prácu s alokovaním poľa a prácu so súborom
int main(void) {
    vector a;                           // vytvorime a inicializujeme vektor a
    init(a);
    
    int hodnota;
    cin >> hodnota;
    while (hodnota >= 0) {
        add(a, hodnota);
        cin >> hodnota;
    }

    int sucet = 0;                       // teraz pracujeme s a, napr. spocitame sucet prvkov vo vektore
    for (int i = 0; i < length(a); i++) {
        sucet += get(a, i);
    }

    cout << "pocet: " << length(a) << ", sucet: " <<  sucet << endl; // vypise pocet prvkov a ich sucet na konzolu
    destroy(a);                            // uvolnime pamat vektora a
}

Dvojrozmerné polia

  • Doteraz sme stále pracovali s jednorozmerným poľom, čo však ak potrebujeme dvojrozmerné pole, maticu?
    • Napríklad na matice z algebry, alebo na dvojrozmerné tabuľky napr. body študentov z domácich úloh
  • Spravme teraz maticu s n=10 riadkami a m=20 stĺpcami
  • Spravíme si pole n jednorozmerných polí, t.j. pole smerníkov na int: int *a[n];
  • Naalokujeme si n jednorozmerných polí veľkosti m a smerník na každé uložíme do jedného prvku poľa a
const int n = 10;
const int m = 20;
int *a[n]; // a je pole n prvkov, kazdy prvok typu int *
for (int i = 0; i < n; i++) {
    a[i] = new int[m];
}

Prvok v i-tom riadku a j-tom stĺpci dostaneme ako a[i][j]

Tu je kód, ktorý vynuluje maticu:

for (int i = 0; i < n; i++) {
   for (int j = 0; j < m; j++) {
       a[i][j] = 0;
   }
}

Na konci sa patrí pamäť uvoľniť:

for (int i = 0; i < n; i++) {
    delete[] a[i];
}

Dynamicky alokovaná matica

Ak chceme n a m určiť dynamicky, napr. načítať ich zo vstupu, musíme aj pole a alokovať pomocou new a nakoniec uvoľniť pomocou delete[]:

#include <iostream>
using namespace std;

int main(void) {
    int n, m;
    cin >> n >> m;       // nacitame n a m
    int **a;             // alokujeme maticu nxm
    a = new int *[n];
    for (int i = 0; i < n; i++) {
        a[i] = new int[m];
    }

    for (int i = 0; i < n; i++) {  // pracujeme s maticou, napr. vynulujeme ju
        for (int j = 0; j < m; j++) {
            a[i][j] = 0;
        }
    }

    for (int i = 0; i < n; i++) {  // na konci uvolnime pamat
        delete[] a[i];
    }
    delete[] a;
}

Funkcie pre alokáciu matice

Na alokáciu, dealokáciu a načítanie matice si napíšeme funkcie, môžu sa nám hodiť:

int ** vytvorMaticu(int n, int m) {
    /* vytvor maticu s n riadkami a m stlpcami */
    int **a;
    a = new int *[n];
    for (int i = 0; i < n; i++) {
        a[i] = new int[m];
    }
    return a;
}
void zmazMaticu(int n, int m, int **a) {
    /* uvolni pamat matice s n riadkami a m stlpcami */
    for (int i = 0; i < n; i++) {
        delete[] a[i];
    }
    delete[] a;
}
void nacitajMaticu(int n, int m, int **a) {
    /* matica je vytvorena, velkosti n, m, vyplnime ju cislami zo vstupu */
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            cin >> a[i][j];
        }
    }
}

Všimnite si, že všetkým funkciám dávame aj rozmery matice. Namiesto toho by sme si mohli spraviť štruktúru podobne ako pri vektore:

struct matica {
  int n, m;
  int **a;
}

Výšková mapa

V matici môžeme mať danú napr. mapu:

  • 0 znamené more
  • kladné číslo znamená pevninu a udáva nadmorskú výšku (napr. v metroch)

Táto reprezentácia neumožňuje zapísať pevninu pod úrovňou mora.

Pomocou funkcií vyššie môžeme takúto mapu načítať a môžeme spraviť napr. program, ktorý zobrazí more modrou a pevninou odtieňami zelenej a hnedej podľa nadmorskej výšky.

Príklad matice a obrázku:

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

Okrem zobrazovania môžeme napríklad nájsť polohu najvyššieho vrchu a zobraziť ho v čiernom rámčeku, ako na obrázku vyššie.

  • Čo spraví funkcia, ak je celá mapa pokrytá morom?
void najvyssiVrch(int n, int m, int **a, int &riadok, int &stlpec) {
    /* nájdi polohu najvyššej hodnoty v matici */
    riadok = 0;
    stlpec = 0;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            if(a[i][j]>a[riadok][stlpec]) {
                riadok = i;
                stlpec = j;
            }
        }
    }
}

Príklad hlavného programu, ktorý hľadá najvyšší vrch.

int main(void) {
    /* nacitaj rozmery matice */
    int n, m;
    cin >> n >> m;

    /* vytvor a nacitaj maticu */
    int **a = vytvorMaticu(n, m);
    nacitajMaticu(n, m, a);

    /* najdi najvyssi vrch */
    int riadok, stlpec;
    najvyssiVrch(n, m, a, riadok, stlpec);

    /* uvolni pamat matice */
    zmazMaticu(n, m, a);
}

Zdrojový kód celého programu

Hra life

Hra life je jednoduchá simulácia kolónie buniek, ktorá má zaujímavé teoretické vlastnosti.

  • Máme mriežku m x n štvorčekov, v každom žije najviac 1 bunka
  • Bunky sa rodia a umierajú podľa toho, koľko majú susedov v ôsmych okolitých políčkach
    • Ak v čase t má bunka 2 alebo 3 susedov, zostane žiť aj v čase t+1, inak zomiera
    • Ak v čase t má prázdne políčko presne 3 susedov, narodí sa tam v čase t+1 nová bunka

Stav hry si môžeme pamätať v matici boolovských hodnôt.

Príklad vstupu

  • Pre jednoduchosť vstup uvádzame ako nuly a jednotky bez medzier (1=živá bunka). Výslednú animáciu nájdete tu.
20 20
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
00000111111111100000
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000


Rátanie zmeny v matici

  • Stav v čase t máme v matici a, do matice b chceme dať stav v čase t+1
int zratajOkolie(int n, int m, bool **a, int riadok, int stlpec) {
    /* pocet zivych prvkov v okoli */
    int sucet = 0;
    for (int i = riadok - 1; i <= riadok + 1; i++) {
        for (int j = stlpec - 1; j <= stlpec + 1; j++) {
            /* treba osetrit okraje matice */
            if (i >= 0 && i < n && j >= 0 && j < m && a[i][j]) {
                sucet++;
            }
        }
    }
    /* samotny stvorcek nechceme zaratat */
    if (a[riadok][stlpec]) {
        sucet--;
    }
    return sucet;
}

void prepocitajMaticu(int n, int m, bool **a, bool **b) {
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            int pocet = zratajOkolie(n, m, a, i, j);
            /* prirad do b[i][j] hodnotu podla okolia a[i][j] */
            b[i][j] = (pocet == 3 || (pocet == 2 && a[i][j]));
        }
    }
}

Hlavný program

  • Prepočítavanie chceme opakovať v cykle pre viacero časových intervalov
  • Môžeme prekopírovať celú maticu z b späť do a, ale rýchlejšie je len vymeniť smerníky
    for (int i = 0; i < 10; i++) {
        /* podla a spocitaj maticu do b */
        prepocitajMaticu(n, m, a, b);
        /* vymen smerniky, aby v a bola nova matica */
        bool **tmp = b;
        b = a;
        a = tmp;
    }
  • vytvorMaticu, zmazMaticu a pod. prepíšeme tak, aby robili s maticou boolovských hodnôt namiesto intov
int main(void) {
    /* nacitaj rozmery matice */
    int n, m;
    cin >> n >> m;

    /* vytvor a nacitaj maticu */
    bool **a = vytvorMaticu(n, m);
    nacitajMaticu(n, m, a);

    /* zobraz maticu */
    SVGdraw drawing(m*stvorcek, n * stvorcek,"life.svg");
    zobrazMaticu(n, m, a, drawing);
    drawing.wait(1);

    /* pomocna matica na vypocty */
    bool **b = vytvorMaticu(n, m);

    /* simuluj 10 krokov hry life */
    for (int i = 0; i < 10; i++) {
        /* podla a spocitaj maticu do b */
        prepocitajMaticu(n, m, a, b);
        /* prekresli, co sa zmenilo */
        zobrazZmeny(n, m, a, b, drawing);
        drawing.wait(1);
        /* vymen smerniky, aby v a bola nova matica */
        bool **tmp = b;
        b = a;
        a = tmp;
    }

    /* uvolni pamat matic */
    zmazMaticu(n, m, a);
    zmazMaticu(n, m, b);
    drawing.finish();
}

Detaily vykresľovania

  • Na začiatku vykreslíme celú maticu, potom prekreslíme vždy len tie bunky, ktoré sa zmenili
void zobrazStvorcek(int i, int j, bool hodnota, SVGdraw &drawing) {
    /* zobraz stvorcek v riadku i a stlpci j */
    drawing.setLineColor("white");
    if (hodnota) {
        drawing.setFillColor("black");
    } else {
        drawing.setFillColor("white");
    }
    drawing.drawRectangle(j*stvorcek, i*stvorcek, stvorcek, stvorcek);
}

void zobrazMaticu(int n, int m, bool **a, SVGdraw &drawing) {
    /* zobraz prvky true ciernymi stvorcekmi */
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            /* nastavenie farby podla hodnoty */
            if (a[i][j]) {
                zobrazStvorcek(i, j, true, drawing);
            }
        }
    }
}

void zobrazZmeny(int n, int m, bool **a, bool **b, SVGdraw &drawing) {
    /* zobraz nove stvorceky na miestach, kde bola zmena */
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            if (a[i][j] != b[i][j]) zobrazStvorcek(i, j, b[i][j], drawing);
        }
    }
}

Zdrojový kód celého programu

Pole reťazcov

Dvojrozmerné polia v C/C++ nemusia mať všetky riadky rovnako dlhé.

  • Spomeňte si, že v C je reťazec jednoducho pole char-ov, kde za posledným znakom ide špeciálny znak 0
  • Pole reťazcov bude teda dvojrozmerné pole char-ov
  • Môžeme načítavať napr. vstup po riadkoch, pričom každý riadok načítame do dlhého poľa, ktoré by malo stačiť a potom do prekopírujeme do akurát veľkého riadku v poli
  • Vstup je ukončený prázdnym riadkom
  • Nakoniec program riadky vypíše odzadu
  • Ak by sme vopred alokovali maxN riadkov, každý veľkosti maxRiadok, vyšlo by potenciálne na zmar oveľa viac pamäte.
#include <iostream>
#include <cstring>
using namespace std;

const int maxRiadok = 1000;
const int maxN = 1000;

int main(void) {
    char *a[maxN];
    char riadok[maxRiadok];
    int n = 0;
     while (true) {
        /* nacitame jeden riadok */
        cin.getline(riadok, maxRiadok);
        /* ak je prazdny, koncime nacitavanie */
        if (strcmp(riadok, "") == 0) {
            break;
        }
        a[n] = new char[strlen(riadok)+1];
        strcpy(a[n], riadok);
        n++;
    }

    for(int i=n-1; i>=0; i--) {
      cout << a[i] << endl;
    }
}
  • Ako by sme do programu pridali správne uvoľnenie pamäte?

Program mapa pre prednášku 13

/* Program Mapa z prednášky 13.
   http://compbio.fmph.uniba.sk/vyuka/prog/index.php/Predn%C3%A1%C5%A1ka_13 */

#include "../SVGdraw.h"
#include <iostream>
using namespace std;

/* velkost stvorceka mapy v pixeloch */
const int stvorcek = 15;

int ** vytvorMaticu(int n, int m) {
    /* vytvor maticu s n riadkami a m stlpcami */
    int **a;
    a = new int *[n];
    for (int i = 0; i < n; i++) {
        a[i] = new int[m];
    }
    return a;
}

void zmazMaticu(int n, int m, int **a) {
    /* uvolni pamat matice s n riadkami a m stlpcami */
    for (int i = 0; i < n; i++) {
        delete[] a[i];
    }
    delete[] a;
}

void nacitajMaticu(int n, int m, int **a) {
    /* matica je vytvorena, velkosti n, m, vyplnime ju cislami zo vstupu */
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            cin >> a[i][j];
        }
    }
}

void farba(SVGdraw &drawing, int r, int g, int b) {
    /* nastav farbu ciary aj vyplne na dane hodnoty */
    drawing.setLineColor(r, g, b);
    drawing.setFillColor(r, g, b);
}

void zobrazMaticu(int n, int m, int **a, SVGdraw &drawing) {
    /* zobraz maticu farebnymi stvorcekmi :
     * modra: more (hodnota 0)
     * zelena: niziny 1..200,
     * hneda: pohoria 200..2000 */
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; 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);
            }
            /* vykresleni stvorceka, pozor: vymena suradnic */
            drawing.drawRectangle(j*stvorcek, i*stvorcek, stvorcek, stvorcek);
        }
    }
}

void najvyssiVrch(int n, int m, int **a, int &riadok, int &stlpec) {
    /* najdi polohu najvyssej hodnoty v matici */
    riadok = 0;
    stlpec = 0;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            if(a[i][j]>a[riadok][stlpec]) {
                riadok = i;
                stlpec = j;
            }
        }
    }
}

int main(void) {
    /* nacitaj rozmery matice */
    int n, m;
    cin >> n >> m;

    /* vytvor a nacitaj maticu */
    int **a = vytvorMaticu(n, m);

    nacitajMaticu(n, m, a);

    /* zobraz maticu */
    SVGdraw drawing(m*stvorcek, n * stvorcek, "mapa.svg");
    zobrazMaticu(n, m, a, drawing);

    /* najdi najvyssi vrch a zobraz ho stvorcekom */
    int riadok, stlpec;
    najvyssiVrch(n, m, a, riadok, stlpec);

    drawing.setLineColor("black");
    drawing.setNoFill();
    drawing.drawRectangle(stlpec*stvorcek, riadok*stvorcek, stvorcek, stvorcek);

    /* ukonci vykreslovanie */
    drawing.finish();

    /* uvolni pamat matice */
    zmazMaticu(n, m, a);
}

Program Life pre prednášku 13

/* Program Hra Life z prednášky 13.
   http://compbio.fmph.uniba.sk/vyuka/prog/index.php/Predn%C3%A1%C5%A1ka_13 */

#include "SVGdraw.h"
#include <iostream>
#include <cassert>
using namespace std;

/* velkost stvorceka */
const int stvorcek = 15;

bool ** vytvorMaticu(int n, int m) {
    /* vytvor maticu s n riadkami a m stlpcami */
    bool **a;
    a = new bool *[n];
    for (int i = 0; i < n; i++) {
        a[i] = new bool[m];
    }
    return a;
}

void zmazMaticu(int n, int m, bool **a) {
    /* uvolni pamat matice s n riadkami a m stlpcami */
    for (int i = 0; i < n; i++) {
        delete[] a[i];
    }
    delete[] a;
}

void nacitajMaticu(int n, int m, bool **a) {
    /* matica je vytvorena, velkosti n, m, vyplnime ju cislami zo vstupu */
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            char c;
            cin >> c;  // nacitaj znak, preskoc biele znaky, ak nejake su
            a[i][j] = (c == '1');
        }
    }
}

void zobrazStvorcek(int i, int j, bool hodnota, SVGdraw &drawing) {
    /* zobraz stvorcek v riadku i a stlpci j */
    drawing.setLineColor("white");
    if (hodnota) {
        drawing.setFillColor("black");
    } else {
        drawing.setFillColor("white");
    }
    drawing.drawRectangle(j*stvorcek, i*stvorcek, stvorcek, stvorcek);
}

void zobrazMaticu(int n, int m, bool **a, SVGdraw &drawing) {
    /* zobraz prvky true ciernymi stvorcekmi */
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            /* nastavenie farby podla hodnoty */
            if (a[i][j]) {
                zobrazStvorcek(i, j, true, drawing);
            }
        }
    }
}

void zobrazZmeny(int n, int m, bool **a, bool **b, SVGdraw &drawing) {
    /* zobraz nove stvorceky na miestach, kde bola zmena */
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            if (a[i][j] != b[i][j]) zobrazStvorcek(i, j, b[i][j], drawing);
        }
    }
}
int zratajOkolie(int n, int m, bool **a, int riadok, int stlpec) {
    /* pocet zivych prvkov v okoli */
    int sucet = 0;
    for (int i = riadok - 1; i <= riadok + 1; i++) {
        for (int j = stlpec - 1; j <= stlpec + 1; j++) {
            /* treba osetrit okraje matice */
            if (i >= 0 && i < n && j >= 0 && j < m && a[i][j]) {
                sucet++;
            }
        }
    }
    /* samotny stvorcek nechceme zaratat */
    if (a[riadok][stlpec]) {
        sucet--;
    }
    return sucet;
}

void prepocitajMaticu(int n, int m, bool **a, bool **b) {
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            int pocet = zratajOkolie(n, m, a, i, j);
            /* prirad do b[i][j] hodnotu podla okolia a[i][j] */
            b[i][j] = (pocet == 3 || (pocet == 2 && a[i][j]));
        }
    }
}

int main(void) {
    /* nacitaj rozmery matice */
    int n, m;
    cin >> n >> m;

    /* vytvor a nacitaj maticu */
    bool **a = vytvorMaticu(n, m);
    nacitajMaticu(n, m, a);

    /* zobraz maticu */
    SVGdraw drawing(m*stvorcek, n * stvorcek,"life.svg");
    zobrazMaticu(n, m, a, drawing);
    drawing.wait(1);

    /* pomocna matica na vypocty */
    bool **b = vytvorMaticu(n, m);

    /* simuluj 10 krokov hry life */
    for (int i = 0; i < 10; i++) {
        /* podla a spocitaj maticu do b */
        prepocitajMaticu(n, m, a, b);
        /* prekresli, co sa zmenilo */
        zobrazZmeny(n, m, a, b, drawing);
        drawing.wait(1);
        /* vymen smerniky, aby v a bola nova matica */
        bool **tmp = b;
        b = a;
        a = tmp;
    }

    /* uvolni pamat matic */
    zmazMaticu(n, m, a);
    zmazMaticu(n, m, b);
    drawing.finish();
}

Prednáška 14

Oznamy

  • Opravná písomka bude v utorok 21.11. o 16:30 (po cvičeniach), treba sa prihlásiť cez AIS do štvrtka
  • DÚ3 odovzdávajte do pondelka 20.11. 22:00.
    • Nechajte si na ňu dosť času, rozmyslite si, ako zapísať rozdelenie predmetov do krabíc ako postupnosť čísel, potom prechádzajte a vyhodnocujte všetky vhodné postupnosti
    • Na domácich úlohách neopisujte. Môžete sa o úlohe rozprávať, ale neukazujte si navzájom programy.
  • Nezabudnite, že ak z jedného týždňa cvičení nedostanete žiadne body, napíšeme vám -5 bodov.
    • Ak sa nič iné nedarí, príďte si aspoň v pondelok na cvičenia napísať opravnú rozcvičku
  • Ešte k minulým cvičeniam: pozrite si funkcie z knižnice cstring na kopírovanie reťazcov, zisťovanie dĺžky, porovnávanie (strcpy, strlen, strcmp). Nemá zmysel, aby ste ich vždy programovali sami.

Dynamická alokácia poľa a matice, opakovanie

  • 1D pole: int *a = new int[n]; cin >> a[0];
  • 2D pole: int **a = new int * [n]; for(int i=0; i<n; i++) a[i] = new int[m]; cin >> a[0][1];
    • Riadky 2D poľa nemusia byť rovnako dlhé
  • 3D pole: int ***a = new int ** [n]; ...

Smerníková aritmetika

  • Pole je vlastne smerník na svoj prvý prvok
  • Majme napr. pole int a[4] = {4, 3, 2, 1};
  • K i-temu prvku sa vieme dostať pomocou a[i]
  • Dá sa to však napísať aj ako *(a+i)
  • Konkrétne a+i je smerník na i-ty prvok poľa a
  • Kompilátor spočíta veľkosť jedného políčka poľa, takže vie, ako ďaleko sa posunúť, aby sa dostal na i-te políčko
  • int *b = a+2 vytvorí smerník na a[2], ale k b sa môžeme správať ako ku dvojprvkovému poľu {2, 1}
  • Podobne pri matici a[i][j] je to isté ako *(*(a+i)+j) (ak a je typu int **)

Smerníková aritmetika:

  • smerník + n: Posun smerníka o n. Ak napríklad smerník ukazuje na nultý prvok poľa, smerník + n bude ukazovať na n-tý prvok poľa.
    • Posunúť smerník o jeden prvok doprava môžeme pomocou smerník++.
  • smerník - n: Posun smerníka o n prvkov smerom k začiatku.
  • smerník1 - smerník2: Výsledkom rozdielu smerníkov je vzdialenosť miest, na ktoré ukazujú. Ak napríklad prvý smerník ukazuje na 5. prvok poľa a druhý smerník na 3. prvok poľa, ich rozdiel je 2. Smerníky musia patriť do toho istého poľa, inak bude výsledok nedefinovaný.

Porovnávanie ukazovateľov:

  • operátory: < <= > >= == !=
  • porovnávanie má zmysel len keď ukazovatele:
    • sú rovnakého typu
    • ukazujú do toho istého poľa

Tu je napr. zvláštny spôsob ako vypísať pole a definované vyššie:

for (int *smernik = a; smernik < a + 4; smernik++) {
     cout << "Prvok " << smernik - a << " je " << *smernik << endl;
}

Podobný kód sa ale občas používa na prechádzanie reťazcov. Napr. ak chceme zrátať počet medzier v reťazci:

int zratajMedzery(char str[]) { // mohli by sme dat aj char *str
  int pocet = 0;
  while(*str != 0) {   // kym nenajdeme ukoncovaciu nulu v retazci
     if(*str == ' ') { pocet++; }  // skontroluj znak, na ktory ukazuje smernik
     str++;                        // posun smernik na dalsi znak 
  }
  return pocet;
}

Funkcie z knižnice cstring so smerníkovou aritmetikou

  • strstr(text, vzorka) vracia smerník na char
    • NULL ak sa vzorka nenachádza v texte, smerník na začiatok prvého výskytu inak
    • pozíciu výskytu zistíme smerníkovou aritmetikou:
char *text, *vzorka;
char *where = strstr(text, vzorka);
if(where != NULL) {
  int position = where - text;
}
  • Ako by ste spočítali počet výskytov vzorky v texte?
  • Podobne strchr hľadá prvý výskyt znaku v texte

Sizeof()

Operátor sizeof() zistí "veľkosť" dátového typu v bajtoch

  • napr. pri struct nemusí byť rovné súčtu veľkostí premenných
  • presnejšie ide o vzdialenosť medzi nasledujúcimi prvkami poľa -- teda o koľko sa posunieme pri posune +1 v smerníkovej aritmetike
int i, *pi;
sizeof(*pi);   //počet bajtov potrebných na uloženie typu int 
sizeof(i);     // da sa napisat aj takto
sizeof(int);   // alebo takto

sizeof(pi);    // pocet bajtov na ulozenie smernika na int
sizeof(int *); // to iste

Práca s konzolou v Cčku: printf a scanf

  • Doteraz sme s konzolou pracovali pomocou C++ knižnice iostream cez cin a cout.
  • Teraz sa naučíme, ako sa s konzolou pracuje v klasickom Cčku pomocou príkazov printf a scanf.
  • Príkazy printf a scanf sú v knižnici cstdio.
  • Budúci týždeň si ukážeme, že podobné príkazy v Cčku existujú aj pre prácu so súbormi.
  • Knižnice jazyka C++ tiež umožňujú prácu so súbormi (podobne ako sme videli s konzolou), nebudeme ich však preberať.

printf

  • Funkcia printf vypisuje formátovaný text na konzolu.
  • Použitie: printf(format, hodnota1, hodnota2,...)
  • Formátovací reťazec obsahuje bežné znaky, ktoré sa priamo vypíšu a špeciálne formátovacie príkazy začínajúce %, ktoré určujú, ako formátovať jednotlivé hodnoty
  • Napr. predpokladajme, že x a y sú celočíslené premenné s hodnotami 10 a 20. Príkaz printf("bod (%d,%d)\n", x, y) vypíše "bod (10,20)" a koniec riadku.

Príklady formátovacích reťazcov:

  • %c - znak (char)
  • %s - reťazec (char *)
  • %d - celé číslo (int)
  • %f - reálne číslo (double)
  • %e - reálne čislo vo vedeckej notácii napr. 5.4e7
  •  %% - vypíše znak %

Pozor, typy výrazov v zozname hodnôt musia sedieť s formátovacím reťazcom.

Formátovanie

Nastavovanie šírky a počtu desatinných miest

  • %.2f - vypíše na 2 desatinné miesta
  • %4d - ak má číslo menej ako 4 znaky, doplní vľavo medzery
  • %04d - podobne, ale dopĺňa nuly

Na prednáške o reťazcoch sme videli formátovanie výsledku (zarovnanie doprava) pomocou vlastnej funkcie, ktorá číslo pripravila do pomocného poľa znakov.

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;
    }
}

Vďaka formátovacím reťazcom v printf vieme tento program prepísať jednoduchšie.

int main(void) {
    int n = 12;
    for (int i = 1; i <= n; i++) {
        printf("%2d",i);
        printf("! = ");
        printf("%10d\n",factorial(i));
    }
}
  • Dokonca by nám v cykle stačil jediný príkaz printf("%2d! = %10d\n", i, factorial(i));
  • Podobne vieme doplniť aj úvodné nuly - napríklad, v dátume môžeme doplniť deň a mesiac na dvojciferné čísla a rok na štvorciferné.
void showDate(int d, int m, int y){
    printf("%02d.%02d.%04d", d, m, y);
}

int main(void){
    showDate(1, 1, 1990);
}

scanf

Funkcia scanf(format, premenna1, premenna2,...) načítava dáta z konzoly

  • Napr. scanf("%d", &x) načíta celočíselnú hodnotu do premennej x.
  • Všimnite si, že zatiaľ čo do printf sa vkladajú priamo hodnoty (premenné), scanf potrebuje smerníky na premenné, pretože ich modifikuje.

Jednoduché načítanie mena a veku:

  char str [80];
  int i;

  printf ("Enter your last name: ");
  scanf ("%s", str);  
  printf ("Enter your age: ");
  scanf ("%d", &i);
  printf ("Mr./Ms. %s, %d years old.\n", str, i);

Formátovací reťazec obsahuje:

  • Špecifikáciu formátov načítavaných premenných (začínajú znakom %) - podobne ako u printf
    • Ak chceme načítať double, potrebujeme %lf, kým v printf dáme %f
  • Biele znaky ("whitespace" - medzery, konce riadkov, tabulátory) - funkcia číta a ignoruje všetky biele znaky pred ďalším nebielym znakom (jeden biely znak umožní ľubovoľný počet bielych znakov na vstupe)
  • Ostatné znaky formátovacieho reťazca musia presne zodpovedať vstupu

Napríklad načítavanie dátumu v tvare deň.mesiac.rok

  int den, mesiac, rok;
  printf("Zadaj datum: ");
  scanf("%d.%d.%d", &den, &mesiac, &rok); 
  printf("Rok: %d\n", rok);

Kontrola správneho vstupu

  • Funkcia scanf vracia počet úspešne priradených hodnôt
    • načítava, kým nedôjde k chybe vstupu alebo chybe konverzie na príslušný typ premennej)
    • v prípade chyby hneď na začiatku vracia hodnotu EOF (napríklad keď hneď na začiatku končí vstup).
  • Pomocou scanf však vieme robiť iba jednoduché kontroly chýb. V zložitejších prípadoch môže byť potrebné načítať zo vstupu reťazec a tento reťazec následne spracovať (viac na ďalšej prednáške).

Príklad: Program načíta tri reálne čísla, otestuje, či sa ich podarilo načítať a vypíše ich na obrazovku

int main() {
   double x, y, z;

   if(scanf("%lf %lf %lf", &x, &y, &z) == 3)
      printf("%f\n", x + y + z);
   else 
      printf("Neboli nacitane 3 realne cisla\n.");
}

Sčítavanie čísel až po koniec vstupu:

  double sum = 0;
  double v;
  while (scanf("%lf", &v) == 1) {
     sum += v;
  }
  printf("sucet %.2f\n", sum);

Ako spravíme koniec vstupu na konzole?

  • konzola čaká na to, čo napíšeme, ako jej povedať, že už nič ďalšie písať nebudeme?
  • v Linuxe Ctrl-D na začiatku riadku, vo Windows Ctrl-Z a Enter
  • program, ktorý načítava z konzoly, dostane signál, že vstup z konzoly skončil (napr. scanf vie, že už neprídu ďalšie znaky)

Vstupy do funkcie main

  • Netbeans vám vyrobí main funkciu s nasledujúcou hlavičkou:
int main(int argc, char** argv) {
  • char **argv je pole C-čkových reťazcov a argc je počet reťazcov v tomto poli
  • Prvý z nich, argv[0], je meno samotného programu a ostatné sú argumenty programu
  • Užitočné, ak spúšťame program z príkazového riadku
  • Dá sa nastaviť v Netbeans, Properties, Run
  • Tento program jednoducho argumenty vypíše
#include <iostream>
using namespace std;

int main(int argc, char** argv) {
    for (int i = 0; i < argc; i++) {
        cout << argv[i] << endl;
    }
}

Smerníkové kuriozity

    int *a[3];                /* pole 3 smerníkov na int */
    int (*b)[3];              /* jeden smerník na pole troch intov, neinicializovaný */
    int c[3] = {0,1,2};       /* pole troch intov */
    b = &c;                   /* teraz b ukazuje na pole c */
    cout << (*b)[2] << endl;  /* pristup k prvku 2 pola c */
    cout << *b[2] << endl;    /* zle: tvarime sa, ze b je pole, pristupime k jeho prvku 2 a interpretujeme ho ako smernik */

Prednáška 15

Organizačné poznámky

  • DÚ3 odovzdávajte do DNES - pondelka 20.11. 22:00.
  • Termín opravnej 2. písomky: v utorok 21.11. o 16:30 (po cvičeniach) v A.

Práca so súbormi

  • Doteraz sme na načítavanie a vypisovanie dát používali výhradne konzolu (obrazovku/klávesnicu).
  • V praxi často chceme spracovávať dáta uložené v súboroch.
  • Zameriame sa na súbory v textovom formáte, pracuje sa s nimi podobne ako s konzolou.
  • V C++ existujú ekvivalenty cin >> a cout << aj pre súbory, nájdete ich v knižnici fstream.

Základy: FILE *, fopen, fclose, fprintf, fscanf

Základný dátový typ: FILE *

  • ukazovateľ (pointer - *) na objekt typu FILE
  • dodržať veľké písmená (FILE *, nie file *)
  • obsahuje údaje o otvorenom súbore, s ktorým práve pracujeme
  • definícia premennej f pre prácu so súborom FILE *f; (pre viac premenných FILE *fr, *fw;)

Otvorenie súboru pre čítanie

  • fr = fopen("vstup.txt", "r");
  • Otvorí súbor s menom vstup.txt (môžeme samozrejme použiť aj iné meno, prípadne pridať cestu)
  • Ak taký súbor neexistuje alebo sa nedá otvoriť, do fr priradí NULL
  • Z takto otvoreného súboru môžeme čítať napr. pomocou fscanf, ktorá je analógom scanf
  • Napr. 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 fprintf, ktorá je analógom printf
  • Napr. fprintf(fw, "%d", x);

Zatvorenie súboru

  • Keď už nebudeme zo súboru čítať ani doňho zapisovať, uzatvoríme ho pomocou fclose(f);
  • Nespoliehať sa, že po skončení programu sa súbor automaticky zavrie súbor
  • Počet súčasne otvorených súborov je obmedzený
  • Pri zavretí sa zapíše buffer do súboru, predídeme tým strate dát pri páde programu
  • Ak sa nepodarí zatvoriť súbor, fclose() vráti konštantu EOF (inak vráti 0)

Súbory, zhrnutie a príklad

  • Príkazy fopen, fclose na otváranie a zatváranie súboru, typ FILE *
  • Príkazy fscanf a fprintf na načítavanie a vypisovanie formátovaných dát (čísla, slová)
  • printf a scanf sú podobné, ale pracujú s konzolou (stdin, stdout)
  • existujú aj funkcie sscanf, sprintf, ktoré načítavajú resp. vypisujú do reťazca

Príklad: načítame číslo n a n celých čísel zo súboru vstup.txt. Do súboru vystup.txt vypíšeme n a vstupné čísla v opačnom poradí.

#include <cstdio>
#include <cassert>

int main(void) {
    FILE *fr = fopen("vstup.txt", "r"); // otvorime vstupny a vystupny subor
    FILE *fw = fopen("vystup.txt", "w");
    assert(fr != NULL && fw != NULL); // skontrolujeme, ze sa podarilo

    int n, r;
    r = fscanf(fr, "%d ", &n); // nacitame pocet prvkov n
    assert(r == 1 && n >= 0);  // overime, ze sa podarilo a ze pocet nezaporny
    int *a = new int[n];       // alokujeme pole dlzky n

    for (int i = 0; i < n; i++) { // citame jednotlive prvky pola
        r = fscanf(fr, "%d ", &a[i]);
        assert(r == 1);
    }
    fclose(fr); // zavrieme vstupny subor

    fprintf(fw, "%d", n);              // vypiseme pocet prvkov do vystupu
    for (int i = n - 1; i >= 0; i--) { // vypiseme vstupne prvky naopak
        fprintf(fw, " %d", a[i]);
    }
    fclose(fw); // zavrieme vystupny subor
    delete[] a; // uvolnime pamat
}

Štandardný vstup a výstup ako súbor

  • C pracuje s klávesnicou a obrazovkou ako so súborom.
  • V cstdio sú definované dva konštantné ukazovatele stdin a stdout.
FILE *stdin, *stdout;
  • Označujú štandardný vstupný/výstupný prúd (standard intput-output stream)
  • stdin a stdout môžu byť použité v programe ako argumenty operácií so súbormi, napr. fscanf(stdin,"%d",&x)
  • Tým pádom ten istý kód sa dá použiť na prácu so súbormi aj s klávesnicou a obrazovkou - stačí len vhodne nastaviť premennú typu FILE *
  • Nasledujúci úryvok kódu načíta z už otvoreného súboru fr meno výstupného súboru a ak toto meno je -, pošle výstup namiesto súboru na obrazovku
  • Stačí pred otvorením výstupu overiť túto alternatívu a ďalej pracovať rovnako.
    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

  • Pomocou symbolickej konštanty EOF
    • definovaná v cstdio, väčšinou má hodnotu -1
    • ak sa funkcii fscanf nepodarí načítať žiadnu hodnotu kvôli koncu súboru, vráti EOF
  • Pomocou funkcie feof(subor)
    • ak sa fscanf alebo iná funkcia pokúšala čítať za koncom súboru, feof vráti pravdivú hodnotu

Spracovávanie vstupu (čísla a pod.)

Často očakávame na vstupe postupnosť číselných hodnôt oddelených bielymi znakmi, ktorú chceme načítavať pomocou fscanf

  • Pozrieme sa na tri obvyklé možnosti, ako môže byť zadaná dĺžka tejto postupnosti

Formát 1: N (počet čísel) a za tým N čí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á -1 alebo inou špeciálnou hodnotu

  • namiesto poľa konštantnej veľkosti by sme mohli použiť vector
    // 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
  • trochu podobný na predchádzajúci, mohli by sme napr. načítavať, kým je výsledok scanf 1
    • potom by sme ale nerozoznali by správny koniec od chyby
  • druhá možnosť je testovať koniec pomocou feof
    • priamočiary spôsob 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 ide v súbore často ešte koniec riadku
    • program vyššie načíta posledné číslo ale v súbore zostane neprečítaný koniec riadku
    • feof teda ešte nie je pravdivé
    • ďalšie volanie fscanf ale vráti EOF, lebo už nenačítalo číslo
  • tento problém napravíme tým, že vo fscanf dáme za %d medzeru
    • tá sa pokúša preskočiť všetky biele znaky až po najbližší nebiely
    • pritom natrafí na koniec súboru a nastaví feof
#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
}

Ešte jeden príklad so súbormi

V súbore vstup.txt máme niekoľko riadkov, prvý obsahuje názov výstupného súboru a každý ďalší obsahuje názov súboru (označme f) a číslo (označme x). Do výstupného súboru chceme pre každý riadok zo vstupu vypísať zo súboru f prvých x čísel.

Príklad: Súbor a.txt obsahuje čísla 1,2,...,9 a súbor b.txt obsahuje čísla 10,20,...,90.

Pre vstupný súbor vstup.txt

vystup.txt
a.txt 2
b.txt 1
a.txt 3

očakávame výstup v súbore vystup.txt

1 2 10 1 2 3
  • Otvoríme vstupný súbor a prečítame z neho názov výstupného súboru
  • Otvoríme aj výstupný súbor a postupne budeme zo vstupu čítať dvojice subor cislo.
    • Každý načítaný súbor si otvoríme, vypíšeme z neho čísla a zatvoríme.
  • Na konci zavrieme vstup aj výstup.
  • Pomocou assert kontrolujeme existenciu súborov aj čísel v nich.
  • Pri načítaní reťazcov obmedzíme dĺžku načítaného slova podľa dĺžky poľa
#include <cstdio>
#include <cassert>
int main(void) {
    FILE *fr1, *fr2, *fw;
    char str[20];
    int kolko, hodnota, kod;

    fr1 = fopen("vstup.txt", "r");
    assert(fr1 != NULL);  // kontrola otvorenia suboru fr1
    kod = fscanf(fr1, "%19s", str);
    assert(kod==1);      // kontrola nacitaneho mena suboru
    fw = fopen(str, "w");
    assert(fw != NULL);
    while (!feof(fr1)) {  // kym neprecitame cely fr1
      kod = fscanf(fr1, "%19s %d ", str, &kolko);
      assert(kod==2);
      fr2 = fopen(str, "r");
      assert(fr2 != NULL);
      for (int i = 0; i < kolko; i++) {
        kod = fscanf(fr2, "%d ", &hodnota);
        assert(kod==1);
        fprintf(fw, "%d ", hodnota);
      }
      fclose(fr2);
    }
    fclose(fr1);
    fclose(fw);
}

Práca so súbormi po znakoch

Čítanie po znakoch: funkcia getc(subor) načíta zo súboru jeden znak.

  • Vracia int (nie char).
  • Ak sa podarilo načítať, je to kód načítaného znaku.
  • Ak sa nepodarilo načítať znak (koniec súboru, alebo nejaká chyba), vráti špeciálnu hodnotu EOF
    • Pozor, neukladať výsledok do premennej typu char, nevedeli by sme rozpoznať koniec súboru, bol by konvertovaný na iný znak
  • Funkcia getchar() je skratka za getc(stdin), číta teda jeden znak z konzoly
    • Spracovanie vstupu z klávesnice začne až potom, ako užívateľ stlačí Enter, nevieme takto reagovať priamo na stlačenie nejakej klávesy

Písanie po znakoch: funkcia putc(znak, subor) zapíše do súboru jeden znak

  • Vracia int: zapísaný znak alebo EOF ak nastala chyba
  • Funkcia putchar(znak) je skratka za putc(znak, stdout), píše teda na konzolu

Príklad: kopírovanie súboru

#include <cstdio>
int main(void) {
   FILE *fr = fopen("list.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);
}

Načítavať môžeme aj priamo v podmienke while cyklu:

  • výsledkom priradenia je nová hodnota, túto porovnáme s EOF
  • kratšie ale trochu menej čitateľné
   int c;
   while ((c = getc(fr)) != EOF) {
      putc(c, fw);
   }

Konce riadkov

Znak pre koniec riadku je '\n'

  • pri čítaní alebo zápise sa môže prekladať na jeden alebo dva znaky podľa typu operačného systému (<CR>, <LF>, alebo <CR><LF>)

Č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);
}

Ungetc

Často zistíme, že máme prestať čítať znak až keď prečítame o znak naviac

  • funkcia ungetc(znak, subor) vráti znak späť (ako keby sme ho neprečítali)
  • ak je vrátenie úspešné, ungetc() vracia vrátený znak
  • ak je vrátenie neúspešné, vráti EOF
  • vrátiť môžeme aj iný ako práve prečítaný znak
    • použije sa pri ďalšom načítaní, ale nezmení obsah súboru
  • nemali by sme volať viackrát za sebou

Program konvertuje znakový reťazec 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);

Program prečíta číslo pomocou 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);
}

Čítanie a písanie po riadkoch

Funkcia fgets(str, n, subor) načíta riadok zo súboru

  • str je pole znakov (premenná typu char *)
  • n je veľkosť tohto poľa (aby funkcia neprepísala miesto mimo poľa)
  • Funkcia načíta do str jeden riadok súboru, ale ak by bol príliš dlhý, skončí po prvých n-1 znakoch
  • Znak '\n' nechá na konci poľa, za neho dá ukončovaciu 0
  • Skončí aj ak príde na koniec súboru
  • Vráti str, alebo NULL ak sa nepodarilo načítať ani jeden znak (vracia teda char *)

Nasledujúci program spočíta počet riadkov v súbore

  • Čo spraví, ak posledný znak súboru nie je koniec riadku?
  • Čo vypíše pre súbor, ktorý obsahuje jeden riadok s 200 znakmi?
#include <cstdio>
int main(void) {
    const int maxN = 100;
    char str[maxN];
    FILE *fr = fopen("vstup.txt", "r");
    int num = 0;
    while (fgets(str, maxN, fr) != NULL) {
        num++;
    }
    cout << num << endl;
}

Funkcia fputs(str, subor) vypíše reťazec str do súboru

  • str môže obsahovať hocikoľko koncov riadkov (aj nula)
  • pri chybe vráti EOF

Spracovávanie vstupu 2

Časté schémy spracovania textového súboru:

  • Pomocou fscanf načítavame jednotlivé čísla, slová a pod. (väčšinou všetky biele znaky považujeme za ekvivalentné oddeľovače)
  • Čítanie po znakoch pomocou getc
  • Čítanie po riadkoch pomocou fgets do reťazca, potom reťazec spracovávame

Príklad: chceme nájsť dĺžku najdlhšieho riadku v súbore

  • Prvá možnosť je čítanie riadkov do reťazca a ich spracovanie (problém ak je riadok príliš dlhý)
  • Druhá možnosť je čítať súbor po znakoch, pričom si potrebujeme udržiavať "stav": koľko písmen sme už videli v tomto riadku
#include <cstdio>
#include <cstring>
int main(void) {
    FILE *fr = fopen("vstup.txt", "r");
    int maxDlzka = 0; // dlzka najdlhsieho riadku doteraz
    const int maxN = 100;
    char str[maxN];
    while (fgets(str, maxN, fr) != NULL) {
        int dlzka = strlen(str);
        if (dlzka > maxDlzka) {
            maxDlzka = dlzka;
        }
    }
    printf("Najdlhsi riadok ma dlzku %d\n", maxDlzka);
    fclose(fr);
}
#include <cstdio>

int main(void) {
    FILE *fr = fopen("vstup.txt", "r");
    int maxDlzka = 0;   // dlzka najdlhsieho riadku doteraz
    int dlzka = 0;      // dlzka aktualneho riadku
    int c = getc(fr);
    while (c != EOF) {  // nacitavame po znaku
        dlzka++;        // zvysime dlzku riadku
        if (c == '\n') { // koniec riadku: spracujeme ho
            if (dlzka > maxDlzka) {
                maxDlzka = dlzka;
            }
            dlzka = 0; 
        }
        c = getc(fr);
    }
    if (dlzka > maxDlzka) { // nezabudneme na posledny riadok (moze chybat \n)
        maxDlzka = dlzka;
    }
    printf("Najdlhsi riadok ma dlzku %d\n", maxDlzka);
    fclose(fr);
}

Cvičenie: čo ak chceme zistiť, koľký riadok v súbore bol ten najdlhší?

Ešte jeden príklad: máme súbor s číslami oddelenými bielymi znakmi (medzery, tabulátory, konce riadkov,...), pričom medzi dvoma číslami môže byť aj viac ako jeden oddeľovač. Chceme spočítať súčet čísel na každom riadku.

  • nepríjemná kombinácia rozlišovania koncov riadku od iných bielych znakov a čítania formátovaných hodnôt (čísel)
  • môžeme biele znaky spracovávať cez getc, potom použiť ungetc a fscanf
  • alebo prečítať riadok do reťazca a rozložiť na čísla
#include <cstdio>
#include <cctype>
int main(void) {
    FILE *fr = fopen("vstup.txt", "r");
    int sucet = 0;
    while(!feof(fr)) {
        int c = getc(fr);
        while(c!=EOF && isspace(c)) {
            if (c == '\n') {
                printf("Sucet %d\n", sucet);
                sucet = 0;
            }
            c= getc(fr);
        }
        if(c==EOF) { break; }
        ungetc(c, fr);
        int hodnota;
        fscanf(fr, "%d", &hodnota);
        sucet += hodnota;
    }    
    fclose(fr);
}

Cvičenia:

  • upravte program, aby fungoval aj ak posledný riadok nie je ukončený '\n'
  • upravte program, aby na výstupe vypisoval aj čísla na riadkoch oddelené medzerami

Jednoduché šifrovanie

Prácu so súbormi si precvičíme na jednoduchých šifrách.

Cézarova šifra

Cézarova šifra je šifra, kde každé písmeno vstupného reťazca posunieme v abecede cyklicky o K miest.

  • Napr. ak K=2, tak namiesto A budeme budeme písať C, namiesto B píšeme D, namiesto Z píšeme B.
  • Ukážeme si použitie pre anglickú abecedu (t.j. znaky 'A'-'Z' bez diakritiky), ale je možné ju použiť napríklad aj na ASCII kódy.
void encryptCezar(FILE *fr, int K) {
    assert(0 <= K && K < 26);

    int c = getc(fr);
    while (c != EOF) {
        if ((c <= 'Z') && (c >= 'A')) {  // prekodujeme pismeno
            c = c + K;
            if (c > 'Z') {
                c = c - 26;
            }
            putc(c, stdout);
        }
        else if (isspace(c)) {  // medzery kopirujeme na vystup
            putc(c, stdout);
        }
        c = getc(fr);
    }
}
  • Text, ktorý chceme zašifrovať načítavame zo súboru fr a posúvame znaky 'A'-'Z' o nejakú konštantu K.
  • Biele znaky kopírujeme.
  • Zašifrovaný text vypisujeme na obrazovku, nebol pri problém to zmeniť do súboru.
  • Cvičenie: upravte program tak, aby správne šifroval aj malé písmená a aby na obrazovku kopíroval aj iné znaky, napr. čísla a interpunkciu.

Pri dešifrovaní postupujeme podobne, len číslo K od prečítaného znaku odrátame (pozor na prechod cez A).

Vigenerova šifra

Vigenerova šifra je veľmi podobná Cézarovej, ale posun nie je konštantný, ale podľa kľúča.

  • Kľúč je nejaké slovo z písmen A-Z, pričom tieto sa písmená predstavujú posuny o 0..25
  • Pri šifrovaní aj dešifrovaní cyklicky používame posuny z kľúča
  • Kľúč samozrejme musí byť známy obidvom stranám.
void encryptVigenere(FILE *fr, char kluc[]) {
    int c = getc(fr);
    int i = 0;
    while (c != EOF) {
        if ((c <= 'Z') && (c >= 'A')) {
            c = c + kluc[i] - 'A';
            if (c > 'Z') {
                c = c - 26;
            }
            putc(c, stdout);
            i++;
            if (kluc[i] == 0) i = 0;
        } else if (isspace(c)) {
            putc(c, stdout);
        }
        c = getc(fr);
    }
}

Binárne súbory

Ako ukladať do súboru desatinné čísla?

  • v textovom zápise strata presnosti

A čo väčšie štruktúry?

  • v textovom súbore potrebujeme vymyslieť formát, naprogramovať vypisovanie a načítavanie tohto formátu

Alternatívne riešenie je použitie binárnych súborov, kde sa neukladáme textovú reprezentáciu ale reprezentáciu, ktorá zodpovedá uloženiu v pamäti.

Napr. majme premennú typu int, ktorá zaberá 4 bajty a má hodnotu 2 000 000 000.

  • Do textového súboru zapíšeme ako cifru '2" a 9 cifier '0' plus nejaký oddeľovač od ďalších dát (napr. medzera), použijeme teda 11 znakov, každý v jednom bajte
  • V binárnom súbore sa hodnota uloží priamo ako 4 bajty, podobne ako v pamäti

Výhody binárnych súborov:

  • rýchlejšie čítanie a zápis, najmä čísel (netreba prevádzať do desiatkovej sústavy a späť)
  • menšia veľkosť súboru (opäť hlavne pre čísla)

Nevýhody binárnych súborov:

  • nemusia byť prenositeľné medzi operačnými systémami, kompilátormi
  • nedajú sa jednoducho pozerať a modifikovať v editore

Na tomto predmete budeme pracovať len s textovými súbormi, binárne uvedené pre úplnosť.

fwrite

  • Funkcia fwrite( const void * ptr, size_t size, size_t count, FILE * f ); do súboru f zapíše count položiek veľkosti size ležiacich v pamäti od adresy ptr.
    • Funkcia vráti počet reálne zapísaných položiek.
    • Súbor f musí byť otvorený na zápis, pričom vo fopen použijeme argument "wb" namiesto "w" (b znamená binárny)
  • Typickým príkladom je, že mám určité položky uložené v poli. Potom začiatok pamäte je určený smerníkom.

Príklad: Chceme uložiť pole reálnych čísel (napríklad odmocniny z čísel 0..100).

#include <cstdio>
#include <cmath>
int main(void) {
    double a[100];
    FILE *f;

    for (int i = 0; i < 100; i++) a[i] = sqrt(i);

    f = fopen("binarny.dat", "wb");
    fwrite(a, sizeof(double), 100, f);
    fclose(f);
}

fread

  • fread funguje podobne ako fwrite.
  • Funkcia fread (void * ptr, size_t size, size_t count, FILE * f ); zo súboru f prečíta count položiek veľkosti size a uloží ich do pamäti od adresy ptr.

Ak by sme si chceli napríklad overiť, že súbor z predchádzajúceho príkladu naozaj obsahuje na 4. mieste číslo 2, stačí si do poľa b uložiť prvých 5 položiek zo súboru binarny.dat a skontrolovať prvok b[4].

#include <cstdio>
int main() {
    FILE *f;
    double b[5];

    f = fopen("binarny.dat", "rb");
    fread(b, sizeof(double), 5, f);
    printf("%f\n", b[4]);
    fclose(f);
}

Prednáška 16

Oznamy

  • Tretia písomka bude budúcu stredu 29.11. o 18:15
    • Bude pokrývať rekurziu (prehľadávanie s návratom, rekurzívne triedenia), znaky, reťazce, smerníky, vektor, matice, a z dnešnej prednášky smerníky na struct ale nie zoznamy
    • Na písomke nebudú súbory, s konzolou stačí základné použitie (cin, cout)
  • Posledná DÚ zverejnená, odovzdávajte do pondelka 4.12.
    • Trochu ľahšia verzia tohto príkladu bola pred pár rokmi na skúške

Opakovanie smerníky

Smerníky na jednoduché premenné:

int a = 7;         /* premenna typu int */
int *b = NULL;     /* smernik na premennu typu int */
b = &a;            /* b ukazuje na a */
*b = 8;            /* v premennej a je teraz 8 */
a = (*b)+1;        /* v premennej a je teraz 9 */

Smerníky a polia, alokovanie poľa:

int a[3];
int *b = a;  /* a,b su teraz takmer rovnocenne premenne */
*b = 3;
b[1] = 4;
a[2] = 5; /* v poli sú teraz čísla 3,4,5 */
for (int i=0; i<3; i++){
   cout << *(a+i) << "=" << a[i] << endl  /* rôzne zápisy toho istého */
}
b = new int[a[1]];  /* b teraz ukazuje na nové pole dĺžky 4 */
delete[] b;         /* uvoľníme pamäť alokovanú pre nové pole */

Smerníky na struct

Smerník/ukazovateľ môže ukazovať aj na struct:

struct bod {
  int x,y;
};
bod b;             /* premenná typu bod */
b.x = 0; b.y = 0;
bod *p;            /* smerník na štruktúru typu bod */
p = &b;            /* p ukazuje na bod b */
bod *p2 = new bod; /* alokovanie nového bodu */
p2->x = 10;        /* dve formy priraďovania do súčasti struct-u */
(*p2).y = 20;
delete p2;        /* uvoľníme alokovanú pamäť */

Pozor, bodka má vyššiu prioritu ako hviezdička

  • preto *p.x znamená *(p.x), teda vezmeme hodnotu v p.x, interpretujeme ju ako adresu a pozrieme sa, čo je na tejto adrese
    • aký typ by muselo mať p, aby toto fungovalo?
  • väčšinou chceme skôr (*p).x, čo znamená, že interpretujeme p ako adresu na struct, v ktorom je položka x
  • keďže sa to často používa, existuje skratka p->x

Spájaný zoznam

Ak niekde chceme uložiť N prvkov, doteraz sme videli nasledujúce možnosti:

  • Vytvoriť pole pevnej veľkosti N, ak je N vopred známa konštanta: const int N = 100; int A[N];
  • Vytvoriť veľké pole, ktoré dúfame bude stačiť, časť z neho môže nepoužitá: const int maxN = 100; int A[maxN];
  • Dynamicky alokovať pole N prvkov, ak N je číslo spočítané niekde v programe alebo načítané zo súboru: int *A = new int[N];
  • Pomocou new vieme naprogramovať aj štruktúru vector, do ktorej môžeme postupne pridávať prvky, aj keď vopred nevieme N

Teraz si ukážeme ďalší spôsob, ako si prvky ukladať, keď vopred nevieme ich počet

Štruktúra jednoduchého spájaného zoznamu

Spájaný zoznam (linked list) je postupnosť uzlov rovnakého typu usporiadaných za sebou.

Každý uzol pozostáva z dvoch častí:

  • smerník, ktorý ukazuje na nasledovníka a pomáha nám pohybovať sa po zozname
  • samotné dáta, v našom prípade pre jednoduchosť int

Uzol spájaného zoznamu teda zapíšeme ako struct:

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

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

Samotný typ linkedList reprezentujúci celý zoznam si potrebuje pamätať smerník na prvý uzol zoznamu:

struct linkedList {
    node* first;
};

Pre prázdny zoznam z bude z.first rovné NULL.

Inicializácia zoznamu je teda veľmi jednoduchá:

void init(linkedList &z){
    z.first = NULL;
}

int main(void){
    linkedList z;
    init(z); 
}

Tiež vieme jednoducho určiť, či je zoznam prázdny:

bool empty(const linkedList &z){
    if (!z.first) return true; // resp. (z.first == NULL) 
    else return false;
}

Vkladanie a vymazanie na začiatku

Na to, aby sme zoznam naplnili, potrebujeme vedieť vytvoriť uzol zoznamu. To robíme dynamicky nasledovne:

node* p = new node;
p->data = NEJAKE_DATA;
p->next = NEJAKY_SMERNIK_NIEKAM;

Teda vytvoríme si smerník na uzol zoznamu, vyhradíme mu patričný kus pamäte a naplníme ho vhodnými dátami.

Nasledujúci kód ručne vytvorí zoznam z, ktorý bude mať prvky 1,2 a 3.

linkedList z;
node* p = new node;
node* q = new node;
node* r = new node;
p->data = 1; 
q->data = 2; 
r->data = 3;
p->next = q;
q->next = r;
r->next = NULL;
z.first = p;

Namiesto takéto kódu budeme používať funkciu na na vloženie prvku do zoznamu.

  • Najjednoduchšia možnosť je vkladanie na začiatok zoznamu.
void insertFirst(linkedList &z, int d){
    /* do zoznamu z vloží na začiatok nový uzol s dátami d */
    node* p = new node;   // vytvoríme nový uzol
    p->data = d;          // naplníme dáta
    p->next = z.first;    // uzol bude prvým prvkom zoznamu (ukazuje na doterajší začiatok)
    z.first = p;          // tento prvok je novým začiatkom
}

Pre vymazanie prvého prvku postupujeme podobne, len nesmieme zabudnúť okrem presmerovania začiatku aj uvoľniť pamäť pôvodne prvého prvku. Funkcia deleteFirst vráti hodnotu prvého prvku a tento prvok vymaže.

int deleteFirst(linkedList &z){
     /* zo zoznamu z vymaze prvy prvok a vrati jeho data */
     assert(z.first != NULL);      // nemozeme mazat, ak je zoznam prazdny
 
     node* p = z.first;           // prvok, ktorý ideme vymazat
     int d = z.first->data;       // to čo máme vrátiť
     z.first = z.first->next;     // presmerujeme zoznam na novy zaciatok
     delete p;                    // uvoľnenie pamäte
     return d;
}

Prechádzanie zoznamu

Veľmi dôležité je vedieť zoznamom prechádzať a postupne pracovať s jeho prvkami. Prechádzanie zoznamom je potrebné napríklad pre hľadanie konkrétneho prvku, hľadanie najväčšieho prvku, vypisovanie všetkých prvkov zoznamu atď.

Ako budeme postupovať?

  • Vytvoríme si smerník x na uzol zoznamu.
  • Začneme prvým uzlom zoznamu. Na ten nám ukazuje smerník z.first.
  • Kým x nebude na konci zoznamu (t.j. nebude NULL), vypíšeme dáta x->data a presunieme sa na ďalší prvok pomocou x->next
  • Keď x je NULL, ďalší prvok už nie je (toto sa môže stať aj hneď na začiatku - t.j. že z.first je NULL)
void print(linkedList &z){
    node* x = z.first;
    while (x != NULL){
        printf(" %d", x->data);
        x = x->next;
    }
    printf("\n");
}

Prejsť celý zoznam potrebujeme aj v prípade, že chceme uvoľniť pamäť (napr. na konci programu).

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

Dá sa to aj funkciami, ktoré sme si už napísali:

void destroy(linkedList &z){
    while (!empty(z)){
      deleteFirst(z);
   }
}

Alebo rekurzívne:

void destroy(node *p) {
    if(p!=NULL) {
       destroy(p->next);
       delete p;
   }
}

void destroy(linkedList &z){
    destroy(z.first);
}

Utriedený zoznam

Občas sa nám hodí, aby sme si zoznam udržiavali utriedený. Pri vkladaní nového prvku potrebujeme najskôr nájsť miesto, kam patrí a vložiť ho tam.

Môžu nastať dva prípady:

  • Nový prvok patrí na začiatok zoznamu
    • Toto sa stane ak je zoznam prázdny, alebo ak je nový prvok menší ako všetky, ktoré tam už sú
    • Okrem vytvorenia nového uzla potrebujeme v zozname meniť položku first
  • Vkladáme niekde v strede alebo na konci zoznamu
    • Potrebujeme poznať uzol, za ktorý vkladáme; jeho položku next totiž budeme meniť
void insert(linkedList& z, int data) {
    // vytvoríme nový uzol
    node* p = new node;
    p->data = data;

    // máme prázdny zoznam alebo vkladáme pred prvý prvok
    if (z.first==NULL || data < z.first->data) {
        p->next = z.first;
        z.first = p;
        return;
    }

    // prvok nepatrí na začiatok
    node* prev = z.first;
    // smerník prev posúvame až kým neukazuje na posledný prvok menší ako data
    while ((prev->next != NULL) && (prev->next->data < data)) {
        prev = prev->next;
    }    
    p->next = prev->next;
    prev->next = p;
}

Riešenie ako je popísané je správne, avšak je náročné na napísanie, aby sme vyriešili špeciálne prípady. Tu sú dve možné riešenia, ako sa takýmto prípadom vyhnúť:

Použitie zarážky (sentinel node): pridáme špeciálny uzol na začiatok (v niektorých prípadoch aj na koniec) zoznamu, pričom hodnotu jeho kľúča nepoužívame.

  • prázdny zoznam teda obsahuje iba zarážku
  • každý uzol má predchodcu a každý zoznam má prvý uzol
  • vkladanie na začiatok znamená vložiť za zarážku
  • nikdy nemažeme úplne prvý prvok, lebo to je zarážka

Zarážky sa niekedy používajú aj pri práci v poli - do poľa si umelo vložíme špeciálny prvý alebo posledný prvok, čo zjednoduší program.

Smerník na smerník:

  • dva prípady v kóde vyššie oba nastavujú p->next a ešte nejaký iný smerník
  • z.first aj prev->next majú typ node *
  • vyrobíme si premennú typu node **, ktorá ukazuje na smerník, ktorý chceme potenciálne meniť
  • touto premennou sa pohybujeme po zozname, až kým nenájdeme správne miesto
void insert2(linkedList &z, int data){
    node* p = new node;
    p->data = data;

    node** it = &z.first;
    while (*it && (*it)->data < data) {
       it = &((*it)->next);
    }
    p->next = *it;
    *it = p;
}

Podobne vymazávame prvky zo zoznamu. Vo funkcii remove nájdeme a vymažeme prvok s určitou hodnotou zo zoznamu s utriedenými prvkami. Potrebné je si opäť pamätať jeho predchodcu, nakoľko tomuto prvku meníme nasledovníka.

void remove(linkedList &z, int data) {
    // prvok sa v zozname nenachádza resp. je zoznam prázdny
    if (!z.first || z.first->data > data) return; 

    // vymazávam prvý prvok
    if (data == z.first->data) {
        node* to_del = z.first;
        z.first = z.first->next;
        delete to_del;
        return;
    }

    // ostatné prípady
    node* prev = z.first;
    while (prev->next && prev->next->data < data) {
        prev = prev->next;
    }
    if (prev->next && prev->next->data == data) {
        node* to_del = prev->next;
        prev->next = prev->next->next;
        delete to_del;
    }
}

Cvičenie:

  • čo sa stane, ak je v zozname viac prvkov s hodnotou data?
  • čo treba zmeniť vo funkcii remove, aby mazala prvok s danou hodnotou aj so zoznamu, ktorý nie je usporiadaný?

Dvojsmerne spájaný zoznam

Je to typ spájaného zoznamu, v ktorom má každý uzol smerník na svojho nasledovníka a aj predchodcu. Ak je uzol prvý, hodnota smerníka na predchodcu má hodnotu NULL a obdobne, ak je uzol posledný, tak hodnota smerníka na nasledovníka je NULL.

Prvok zoznamu teda vyzerá takto:

struct node {
    int data;
    node* next;
    node* prev;
};

Obvykle sa potom okrem začiatku zoznamu udržuje aj smerník na koniec (teda posledný prvok zoznamu). Teda zoznam vyzerá nasledovne:

struct linkedList {
    node* first;
    node* last;
};

Pre inicializáciu je potrebné okrem začiatku nastaviť aj koniec. Uvoľňovanie pamäti funguje úplne rovnako ako v jednosmerne spájanom zozname.

Vkladanie a vymazávanie na začiatku

Vkladanie na začiatok podobne ako pri jednosmernom zozname, ale upravujeme viac smerníkov.

  • Podobne sa dá pracovať aj na konci zoznamu, keďže smerník na koniec si tiež udržujeme.
void insertFirst(linkedList &z, int d){
    node* p = new node;
    p->next = z.first;
    p->prev = NULL;
    p->data = d;
    if (z.first != NULL){
        z.first->prev = p;
    }
    else {
        z.last = p;
    }
    z.first = p;
}

Rozdiely oproti jednosmerne spájanému zoznamu:

  • Ak je zoznam prázdny, po vložení treba nastaviť smerník na koniec zoznamu.
  • Ak zoznam nie je prázdny, treba nastaviť predchádzajúci prvok pôvodnému začiatku, teda smerník (z.first)->prev.

Pri vymazávaní potrebujeme ošetriť, či sme nevymazali posledný prvok. V tom prípade potrebujeme nastaviť správne aj koniec zoznamu.

int deleteFirst(linkedList &z){
     /* zo zoznamu z vymaze prvy prvok a vrati jeho data */
     assert(z.first != NULL);      // nemozeme mazat, ak je zoznam prazdny
 
     node* p = z.first;           // prvok, ktorý ideme vymazat
     int d = z.first->data;       // to čo máme vrátiť
     z.first = z.first->next;     // presmerujeme zoznam na novy zaciatok
     if(z.first==NULL) z.last = NULL; 
     else z.first->prev = NULL;
     delete p;                    // uvoľnenie pamäte
     return d;
}

Prechádzanie zoznamu odpredu/odzadu

Na prechádzanie dvojsmerne spájaného zoznamu je možné použiť rovnaký postup ako pri prechádzaní jednosmerného. Okrem toho máme možnosť urobiť aj prechádzanie/výpis zoznamu odzadu.

void vypisOdzadu(linkedList &z){
    node* x = z.last;
    while (x != NULL){
        printf("%d ", x->data);
        x = x->prev;
    }
    printf("\n");
}

Vďaka tejto možnosti teda vieme napríklad jednoducho nájsť zoznamu k-ty prvok odzadu.

int kOdzadu(linkedList &z, int k){
    node* x = z.last;
    int data;
    for (int i = 0; i < k; i++){
      if (x == NULL) return -1;
      data = x->data;
      x = x->prev;
    }
    return data;
}

Cvičenie: ako by ste hľadali k-ty prvok odzadu v jednosmerne spájanom zozname?

Zhrnutie

  • V spájanom zozname máme uzly pospájané smerníkmi
  • Môžeme si do neho uložiť postupnosť prvkov aj keď nevieme vopred, aká bude dlhá
  • Cez zoznam môžeme ľahko prejsť, ale nevieme rýchlo skočiť niekam do stredu (toto zase vieme v poli)
  • Existuje veľa variantov: jednosmerne / obojsmerne spájané, kruhové, so sentinelmi, môžeme si pamätať začiatok alebo začiatok aj koniec...
  • Pozor na smerníky, aby všetky správne ukazovali a aby sa nám zoznam nepomotal
  • Príklady využitia zoznamov na ďalších prednáškach, napr. hešovanie, zásobník a rad.

Prednáška 17

Organizačné poznámky

  • Posledná domáca úloha na tento semester - DÚ4 termín odovzdania do 4.12.
  • Tretia písomka bude v stredu 29.11. o 18:15. Bude pokrývať rekurziu, znaky, reťazce, smerníky (vrátane smerníkov na struct), vektor, matice
  • Termíny skúšok - máte už nejaké zverejnené?

Ešte k alokácii pamäti

Chceme načítať n a vytvoriť pole n intov.

const int MAXN = 100;
int n;
cin >> n;

Môžeme použiť tri spôsoby:

  • Spôsob 1: pole konštantnej veľkosti int a[MAXN];
  • Spôsob 2: pole nekonštantnej veľkosti int a[n]; (variable-length array)
  • Spôsob 3: dynamicky alokované pole: int *a = new int[n];

Rozdiely:

  • Spôsoby 1 a 2 vytvárajú pole na zásobníku volaní (call stack)
    • veľkosť tohto zásobníka môže byť limitovaná (v závislosti od operačného systému), preto je lepšie väčšie polia alokovať
    • ak sú tieto polia lokálne premenné, zmiznú po skončení funkcie
  • Spôsob 2 nefunguje vo všetkých kompilátoroch
  • Spôsob 1 vyžaduje rozumný horný odhad na n
  • Spôsob 3 je mierne prácnejší (netreba zabudnúť odalokovať!), ale
    • obzvlášť sa hodí na väčšie polia
    • môžeme tiež vytvoriť pole vo funkcii a vrátiť ho pomocou return

Dvojrozmerné polia

  • My sme videli pole smerníkov na riadky matice, pričom riadky sú 1D polia dynamicky alokované
    • int **a = new int * [n]; for(int i=0; i<n; i++) a[i] = new int[m];
  • Dá sa vytvoriť aj 2D matica, napr. int a[n][m]
    • dá sa to chápať ako pole dĺžky n, ktorého prvky sú polia intov dĺžky m
    • v pamäti to bude jeden kus s n krát m intami
    • adresácia a[i][j] funguje nasledovne:
      • a[i] je adresa začiatku i-teho pola intov dĺžky m a [j] z tohto poľa vyberie j-ty prvok
      • na spočítanie hodnoty a[i] potrebuje program vedieť m
      • takéto pole sa v C++ nedá ľahko poslať ako parameter do funkcie ak m nie je konštanta
    • z tohto dôvodu a tiež kvôli šetreniu pamäti na zásobníku je lepšie alokovať matice pomocou poľa smerníkov, ako na prednáškach

Abstraktný dátový typ, ADT

  • Určíme, aké operácie by mala dátová štruktúra spĺňať (hlavičky funkcií), nestaráme sa o implementáciu
  • Oddelíme tak samotnú implementáciu dátovej štruktúry a program, ktorý ju používa
  • Napríklad ADT vektor poskytuje operácie init, add, get, set, length a destroy
  • Pre jeden ADT môže byť viacero implementácií, napr. vector by sme mohli implementovať aj pomocou spájaného zoznamu
  • Program, ktorý vektor používa, netreba meniť kvôli zmene implementácie vektora

Dnes dve nové ADT: zásobník a rad

  • Obidve implementujeme pomocou polí aj spájaných zoznamov

Zásobník (stack) a rad (queue)

  • Jednoduché dátové štruktúry, ktoré udržujú zoznam nejakých prvkov
  • Tieto prvky sú dosť často úlohy, resp. dáta čakajúce na spracovanie
  • Vieme do nich vkladať a vyberať
  • Líšia sa tým, v akom poradí vyberáme

Rad, fronta (queue)

  • Vyberáme prvok, ktorý je v rade najdlhšie (first in, first out, FIFO)
  • Podobá sa na státie v rade pri pokladni: ten, kto tam stojí najdlhšie, bude prvý obslúžený
  • Základné operácie
/* inicializuje prázdny rad */
void init(queue &q);

/* zistí, či je rad prázdny */
bool isEmpty(queue &q);

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

/* odoberie prvok zo začiatku radu a vráti jeho hodnotu */
dataType dequeue(queue &q);

/* vráti prvok zo začiatku radu, ale nechá ho v rade */
dataType peek(queue &q);


Zásobník (stack)

  • Vyberáme prvok, ktorý je v rade najkratšie (last in, first out, LIFO)
  • Podobá sa na umývača tanierov v reštaurácii, vždy vezme horný tanier z kopy
/* inicializuje prázdny zásobník */
void init(stack &s);

/* zistí, či je zásobník prázdny */
bool isEmpty(stack &s);

/* pridá prvok item na vrch zásobníka */
void push(stack &s, dataType item); 

/* odoberie prvok z vrchu zásobníka a vráti jeho hodnotu */
dataType pop(stack &q);

Ešte potenciálne potrebujeme uvoľniť pamäť nejakou ďalšou funkciou, napr. destroy.


Príklad:

typedef int dataType;
/* sem pride definicia typov stack a queue a vsetky potrebne funkcie */

int main() {
    cout << "Stack:" << endl;
    stack s;
    init(s);
    push(s, 1);
    push(s, 2);
    push(s, 3);
    cout << pop(s) << endl;
    cout << pop(s) << endl;
    cout << pop(s) << endl;

    cout << "Queue:" << endl;
    queue q;
    init(q);
    enqueue(q, 1);
    enqueue(q, 2);
    enqueue(q, 3);
    cout << dequeue(q) << endl;
    cout << dequeue(q) << endl;
    cout << dequeue(q) << endl;
}

Zásobník prevracia poradie prvkov, rad zachováva.

Stack:
3
2
1
Queue:
1
2
3

Zásobník implementovaný pomocou poľa

Dve prirodzené možnosti:

  • vrch zásobníka v prvku 0
  • spodok zásobníka v prvku 0

Prvá možnosť by bola pomalá, na operácie push aj pop by sme museli posúvať všetky prvky v zásobníku. Použijeme teda druhú možnosť, v prvku 0 bude spodok zásobníka, v premennej top si pamätáme pozíciu vrchného prvku.

struct stack {
    int top;  /* pozicia vrchného prvku zásobníka */
    dataType *items; /* pole prvkov */
};

/* inicializuje prázdny zásobník */
void init(stack &s) {
    s.top = -1;
    s.items = new dataType[maxN];
}

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

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

/* odoberie prvok z vrchu zásobníka a vráti jeho hodnotu */
dataType pop(stack &s) {
    assert(s.top >= 0);
    s.top--;
    return s.items[s.top + 1];
}

Rad implementovaný pomocou poľa

  • Rad sa nedá implementovať tak jednoducho ako zásobník, lebo na jednej strane potrebujeme vkladať a na druhej vyberať.
  • Takže ak by sme chceli mať napr. prvý prvok v rade vždy na pozícii 0, museli by sme pri vyberaní posúvať všetky prvky, čo je pomalé.
  • Namiesto toho si budeme pamätať pozíciu prvého prvku (premenná first) a keď vyberieme prvok, len zvýšime first
  • Tým sa zaplnený úsek postupne posúva doprava
  • Keď dôjdeme na koniec poľa, zatočíme sa, ako keby za prvkom maxN-1 nasledoval prvok 0
struct queue {
    int first;  /* pozícia prvého prvku v rade */
    int count;  /* počet prvkov v rade */
    dataType *items; /* pole prvkov */
};

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

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

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

/* odoberie prvok zo začiatku radu a vráti jeho hodnotu */
dataType dequeue(queue &q) {
    assert(q.count > 0);
    int index = q.first; /* kde je prvok, ktory chceme vratit */
    q.first = (q.first + 1) % maxN;
    q.count--;
    return q.items[index];
}

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

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

  • V zásobníku v poli sme na začiatku poľa mali spodok zásobníka
  • V zozname budeme mať na začiatku zoznamu vrchol zásobníka, lebo na začiatok zoznamu sa dobre vkladá a vyberá.
  • Použijeme kód na vkladanie a vyberanie zo zoznamu z prednášky 16 (začiatok zoznamu sme premenovali na top)
  • Nepotrebujeme vopred stanoviť maximálny počet prvkov, zásobník podľa potreby rastie alebo sa zmenšuje (kým je voľná pamäť)
struct node {
    dataType data;
    node* next;
};

struct stack {
    node *top; /* vrchný prvok zásobníka */
};

/* inicializuje prázdny zásobník */
void init(stack &s) {
    s.top = NULL;
}

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

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

/* odoberie prvok z vrchu zásobníka a vráti jeho hodnotu */
dataType pop(stack &s) {
    assert(s.top != NULL);
    node *temp = s.top->next; // prvok, ktorý bude ďalej top
    dataType val = s.top->data; // to čo máme vrátiť
    delete s.top;
    s.top = temp;
    return val;
}

Rad implementovaný pomocou spájaného zoznamu

  • Potrebujeme na jednom konci zoznamu vkladať, na opačnom vyberať.
  • Použijeme kruhový zoznam, v ktorom posledný prvok ukazuje pomocou next na prvý prvok v zozname.
  • Prvý prvok radu bude prvý v zozname, bude ukazovať na druhý prvok atď.
  • V štruktúre queue si pamätáme iba smerník na posledný prvok radu.
    • Ak je rad prázdny, tento smerník je NULL.
    • Z posledného prvku sa jedným krokom vieme dostať k prvému prvku.
struct queue {
    node *last; /* posledný prvok v rade */
};

/* inicializuje prázdny rad */
void init(queue &q) {
    q.last = NULL;
}

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

/* prida prvok s hodnotou item na koniec radu */
void enqueue(queue &q, dataType item) {
    node *temp = new node;
    temp->data = item;
    if (q.last == NULL) {
        q.last = temp;
        temp->next = temp;
    } else {
        temp->next = q.last->next;
        q.last->next = temp;
        q.last = temp;
    }
}

/* odoberie prvok zo začiatku radu a vráti jeho hodnotu */
dataType dequeue(queue &q) {
    assert(q.last != NULL);
    node *first = q.last->next;
    if (first->next == first) { /* ak v zozname iba jeden prvok */
        q.last = NULL;
    } else { /* v zozname viac prvkov, preskocime first */
        q.last->next = first->next;
    }
    dataType val = first->data;
    delete first; /* zmazeme node pre first */
    return val;
}

/* vrati prvok zo zaciatku radu, ale necha ho v rade */
dataType peek(queue &q) {
    assert(q.last != NULL);
    return q.last->next->data;
}

Použitie radu a zásobníka

Zvyčajne uchovávajú dáta, ktoré ešte treba spracovať, resp. zoznam úloh.

Rad použijeme, ak chceme zachovať poradie.

  • Jeden proces pridáva úlohy, druhý ich rieši (prípadne aj viac procesov môže vkladať alebo vyberať)
  • Príklady:
    • Textový procesor pripravuje strany na tlač, vkladá ich do radu, tlačiareň ich tlačí
    • Výpočtové úlohy čakajú na veľkom serveri v rade na spustenie
    • Zákazníci čakajú na zákazníckej linke na voľného operátora
    • Pasažieri na stand-by čakajú na voľné miesto v lietadle

Zásobník je jednoduchší, používame ho, ak nám nezáleží na poradí, alebo ak chceme poradie prevracať

  • Hlavný príklad je zásobník, ktorý kompilátor vytvorí na ukladanie lokálnych premenných funkcií, čo nám umožňuje použiť rekurziu (call stack)
  • Pri nekonečnej rekurzii nám zásobník "pretečie" a program spadne, napr. s chybou Segmentation fault
void pokus() {
    int i;
    pokus();
}

int main() {
    pokus();
}
  • Rekurzívne programy vieme prepísať na nerekurzívne, napríklad pomocou ručne vytvoreného zásobníka
    • Mohli by sme mechanicky simulovať to, ako zásobník používa kompilátor, to by však bolo veľmi neprehľadné
    • Väčšinou trochu zmeníme ako presne program funguje
    • Ukážeme si niekoľko príkladov

Nerekurzívny QuickSort

  • Quicksort zvolí nejakú hodnotu (pivot), prvky menšie ako pivot dá do ľavej časti poľa, väčšie prvky do pravej časti poľa
  • Potom rekurzívne volá Quicksort na obe časti poľa
void quicksort(int A[], int l, int r) {
    if (l >= r) return;

    // rozdelenie
    int pivot = divide(A, l, r);

    // rekurzivne volanie na mensie a vacsie prvky
    quicksort(A, l, pivot - 1);
    quicksort(A, pivot + 1, r);
}
int main() {
  ...
  quicksort(A, 0, n-1);
  ...
}

Namiesto rekurzie si spravíme zásobník dvojíc l,r, ktoré ešte treba dotriediť

struct usek {
    int l, r;
};

typedef usek dataType;

void quicksort(int A[], int n) {
    stack s;
    init(s);
    
    /* do stacku vlozime usek pre cele pole */
    usek u;
    u.l = 0;
    u.r = n - 1;
    push(s, u);

    while (!isEmpty(s)) {
        u = pop(s);
        /* spracovavame usek od u.l po u.r */
        if (u.l >= u.r) continue;

        /* rozdelenie */
        int pivot = divide(A, u.l, u.r);

        /* ľavý a pravý úsek vložíme do stacku na neskoršie dotriedenie */
        usek u1;
        u1.l = u.l;
        u1.r = pivot - 1;
        usek u2;
        u2.l = pivot + 1;
        u2.r = u.r;
        push(s, u2);
        push(s, u1);
    }
}
  • Tento program triedi úseky v rovnakom poradí, ako rekurzívny Quicksort, lebo po rozdelení intervalu [0..n-1] na dve časti dá na vrchol zásobníka ľavú časť. Až keď sa táto ľavá časť a všetky podúlohy, ktoré z nej vzniknú, spracuje, vytiahne sa zo zásobníka pravá časť.
  • V Quicksorte však na poradí nezáleží, takže by sme mohli vložiť úseky aj naopak push(s, u1); push(s, u2); (potom by najprv dotriedil pravú časť až potom ľavú)
  • Alebo by sme namiesto zásobníka mohli použiť rad. Potom by najskôr rozdelil ľavú aj pravú časť na ďalšie podčasti a potom by delil každú z týchto podčastí atď

Na zamyslenie: ako by sme spravili nerekurzívny MergeSort? Prečo to nejde tak isto ako QuickSort?

Kontrola uzátvorkovania

  • Jednoduchý príklad na použitie zásobníka
  • Máme daný reťazec obsahujúci zátvorky rôznych typov (,),[,],{,} a chceme zistiť, či je dobre uzátvorkovaný
  • Ak zoberieme prvých niekoľko znakov reťazca, počet uzátvaracích zátvoriek nesmie prevýšiť počet otváracích
  • Ku každej otváracej zátvorke musí byť zatváracia toho istého typu
+2
Vyraz je dobre uzatvorkovany

(x+2)
Vyraz je dobre uzatvorkovany

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

[[#))
Vyraz nie je dobre uzatvorkovany

())(
Vyraz nie je dobre uzatvorkovany

((
Vyraz nie je dobre uzatvorkovany

Do zásobníka si budeme ukladať pre každú začínajúcu zátvorku jej zodpovedajúcu končiacu. Keď príde končiaca zátvorka, skontrolujeme, či na vrchu zásobníka je to isté (a vyberieme to zo zásobníka).

typedef char dataType;
/* tu vlozime deklaracie a funkcie pre zasobnik */

int main(){
    char vyraz[100];
    fgets(vyraz, 100, stdin);  // nacitame retazec do pola vyraz
    stack s;
    init(s);
    bool spravny = 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)) {
                    spravny = false;
                } else {
                    char c = pop(s);
                    if (c != vyraz[i]) {
                        spravny = false;
                    }
                }
                break;
        }
    }
    if (spravny) {
        cout << "Vyraz je dobre uzatvorkovany" << endl;
    } else {
        cout << "Vyraz nie je dobre uzatvorkovany" << endl;
    }
}

Cvičenie:

  • Čo spraví program, ak na vstupe je ((? Opravte ho, aby fungoval správne.
  • Pridajte do programu výpis pozície, kde bola detegovaná chyba a výpis typu chyby.
  • Prepíšte program na kontrolu zátvoriek do rekurzívnej podoby, pričom použijete iba premenné typu char (žiadne polia a pod.). Reťazec načítavajte pomocou getc a ungetc, ukončený je koncom riadku.

Prednáška 18

Oznamy

  • Tretia písomka dnes o 18:15 v posluchárni B
    • Opravná až cez skúškové
    • Na skúšku môžete ísť aj pred opravnou písomkou
  • Riadne termíny skúšok pondelok 8.1. a pondelok 22.1., prvý opravný streda 31.1., druhý opravný určíme neskôr
    • V prípade konfliktov nám dajte čím skôr vedieť!
    • Zapisovanie dnes od 20:00
    • Viac informácií o skúške na prednáške 18.12.
  • DÚ4 (poslednú) odovzdávajte do pondelka 22:00.
  • Zajtra (vo štvrtok) večer bude Beánia http://beania.matfyz.sk/
  • Od budúceho týždňa berieme stromy: spojenie smerníkov a rekurzie
    • Dnes si precvičíme rekurziu na probléme s maticami

Vyfarbovanie súvislých oblastí

Máme daný obrázok pozostávajúci z m riadkov a n stĺpcov pixelov, každý pixel určitej farby. V našom jednoduchom príklade budeme uvažovať iba 4 farby, ktoré budeme zapisovať číslami 0,..,3 a ktorých význam bude daný týmto poľom, takže napr. farba 0 je biela:

const char * farby[4] = {"white", "green", "black", "brown"};

Užívateľ si zvolí určitý pixel obrázku a chce vyfarbiť celú súvislú jednofarebnú oblasť obsahujúcu tento pixel novou farbou. Ak napríklad začneme vyfarbovať v obrázku vľavo farbou 3 (hnedou) od pixelu (0,0), dostaneme obrázok napravo:

Tu je pôvodný obrázok v textovej forme, ako matica čísel.

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

Túto úlohu vieme vyriešiť nasledujúcou rekurzívnou funkciou, ktorá prefarbí zadané políčko a potom rekurzívne pokračuje vo vyfarbovaní vo všetkých susedoch, ktorí majú rovnakú farbu, ako pôvodne malo toto políčko.

void vyfarbi(int **a, int n, int m, int riadok, int stlpec, int farba) {
    /* prefarbi suvislu jednofarebnu oblast obsahujucu 
     * a[riadok][stlpec] na farbu farba */

    if (a[riadok][stlpec] != farba) {
        int stara_farba = a[riadok][stlpec];
        a[riadok][stlpec] = farba;
        if (riadok > 0 && a[riadok - 1][stlpec] == stara_farba) {
            vyfarbi(a, n, m, riadok - 1, stlpec, farba);
        }
        if (riadok + 1 < n && a[riadok + 1][stlpec] == stara_farba) {
            vyfarbi(a, n, m, riadok + 1, stlpec, farba);
        }
        if (stlpec > 0 && a[riadok][stlpec - 1] == stara_farba) {
            vyfarbi(a, n, m, riadok, stlpec - 1, farba);
        }
        if (stlpec + 1 < m && a[riadok][stlpec + 1] == stara_farba) {
            vyfarbi(a, n, m, riadok, stlpec + 1, farba);
        }
    }
}

Susedov náš program pozerá v poradí hore, dole, doľava, doprava.

  • V akom poradí vyfarbí políčka nulovej matice 2x2 ak začne v ľavom hornom rohu?
  • V akom poradí vyfarbuje jednoriadkovú maticu, ak začneme niekde v strede?

Zdrojový kód celého programu

Použitie vyfarbovania na mape

Spomeňme si na program na prácu s výškovou mapu z prednášky 15. Máme zadanú mapu ako maticu celých čísel, v ktorej 0 znamená more a kladné čísla nadmorskú výšku pevniny. Budeme predpokladať, že na mape je niekoľko ostrovov, ktoré sú plne obkolesené morom a nemajú žiadne vnútorné moria ani jazerá. Chceme spočítať, koľko tam tých ostrovov je.

Použijeme na to vyfarbovanie z predchádzajúceho programu.

  • Prejdeme maticu a všetky kladné čísla prepíšeme na 1. Máme teda 0 vyznačené more a 1 ostrovy.
  • Prechádzame maticu a vždy, keď nájdeme 1 (ešte nepreskúmaný ostrov), vyfarbíme ho farbou 2 a zvýšime počet ostrovov.
  • Nakoniec máme more ako 0 a všetky ostrovy ako 2.

Príklad mapy a jej zobrazenie po prepísaní kladných čísel na 1 (zelená), po vyfarbení prvého ostrova číslom 2 (čiernou) a po vyfarbení všetkých ostrovov.

11 17
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 1 9 1 1 1 0 0 1 1 0 0 0 0 0
 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0
 0 0 0 1 0 0 0 0 0 1 1 1 0 0 1 1 0
 0 0 0 1 0 0 0 0 0 2 0 0 0 0 0 1 0
 0 1 0 0 2 2 2 2 0 2 2 1 7 1 1 1 0
 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 7 0 1 0 0 0 2 0 0 0 1 0 1 1 1 0
 0 0 0 1 5 1 1 2 1 1 3 1 0 1 2 2 0
 0 1 0 0 0 1 1 2 2 0 0 0 0 1 1 1 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Takto to naprogramujeme:

   for(int i=0; i<n; i++) {
        for(int j=0; j<m; j++) {
            if(a[i][j]>0) a[i][j] = 1;
        }
    }

    int ostrovov = 0;
    for(int i=0; i<n; i++) {
        for(int j=0; j<m; j++) {
            if(a[i][j]==1) {
                ostrovov++;
                vyfarbi(a, n, m, i, j, 2);
            }
        }
    }
    cout << "Pocet ostrovov: " << ostrovov << endl;

Cvičenie:

  • Ako by ste zistili, či má nejaký ostrov jazero?

Zdrojový kód celého programu

Nerekurzívne vyfarbovanie

Prepíšme najskôr štyri volania rekurzívne do jedného cyklu pomocou polí delta_riadok a delta_stlpec.

const int delta_stlpec[4] = {1, 0, -1, 0};
const int delta_riadok[4] = {0, -1, 0, 1};

void vyfarbi(int **a, int n, int m, int riadok, int stlpec, int farba) {
    /* prefarbi suvislu jednofarebnu oblast obsahujucu
     * a[riadok][stlpec] na farbu farba */

    if (a[riadok][stlpec] != farba) {
        int stara_farba = a[riadok][stlpec];
        a[riadok][stlpec] = farba;
        for (int smer = 0; smer < 4; smer++) {
            /* skus susedne policko so suradnicami r,s */
            int r = riadok + delta_riadok[smer];
            int s = stlpec + delta_stlpec[smer];
            if (r >= 0 && r < n && s >= 0 && s < m && a[r][s] == stara_farba) {
                vyfarbi(a, n, m, r, s, farba);
            }
        }
    }
}

Namiesto rekurzie budeme používať zásobník. Do zásobníka si môžeme uložiť štyroch susedov aktuálneho políčka, pretože z nich ešte treba vyfarbovať ďalej. Ale spravíme malú zmenu: pred uložením do zásobníka ich už prefarbíme novou farbou, aby sme vedeli, že ich už viackrát do zásobníka nemáme dať.

/* struct so suradnicami policka, ktory budeme ukldat do zasobnika */
struct policko {
    int r, s;
};

typedef policko dataType;

void vyfarbi(int **a, int n, int m, int riadok, int stlpec, int farba) {
    /* prefarbi suvislu jednofarebnu oblast obsahujucu
     * a[riadok][stlpec] na farbu farba */

    stack s;
    init(s);

    /* vyfarbi startovacie policko a daj ho do stacku */
    int stara_farba = a[riadok][stlpec];
    if (stara_farba == farba) return;
    a[riadok][stlpec] = farba;
    policko p;
    p.r = riadok;
    p.s = stlpec;
    push(s, p);

    while (!isEmpty(s)) {
        /* vytiahni policko zo stacku */
        p = pop(s);
        /* skontroluj jeho susedov, vyfarbi ich a uloz to stacku */
        for (int smer = 0; smer < 4; smer++) {
            policko p2;
            p2.r = p.r + delta_riadok[smer];
            p2.s = p.s + delta_stlpec[smer];
            if (p2.r >= 0 && p2.r < n && p2.s >= 0 && p2.s < m
                    && a[p2.r][p2.s] == stara_farba) {
                a[p2.r][p2.s] = farba;
                push(s, p2);
            }
        }
    }
}

Namiesto zásobníka môžeme použiť aj rad, potom bude vyfarbovať v poradí podľa vzdialenosti od štartovacieho políčka (prehľadávanie do šírky)

void vyfarbi(int **a, int n, int m, int riadok, int stlpec, int farba) {
    /* prefarbi suvislu jednofarebnu oblast obsahujucu
     * a[riadok][stlpec] na farbu farba */

    queue q;
    init(q);

    /* vyfarbi startovacie policko a daj ho do radu */
    int stara_farba = a[riadok][stlpec];
    if (stara_farba == farba) return;
    a[riadok][stlpec] = farba;
    policko p;
    p.r = riadok;
    p.s = stlpec;
    enqueue(q, p);

    while (!isEmpty(q)) {
        /* vytiahni policko z radu */
        p = dequeue(q);
        /* skontroluj jeho susedov, vyfarbi ich a uloz to radu */
        for (int smer = 0; smer < 4; smer++) {
            policko p2;
            p2.r = p.r + delta_riadok[smer];
            p2.s = p.s + delta_stlpec[smer];
            if (p2.r >= 0 && p2.r < n && p2.s >= 0 && p2.s < m
                    && a[p2.r][p2.s] == stara_farba) {
                a[p2.r][p2.s] = farba;
                enqueue(q, p2);
            }
        }
    }
}

Aritmetické výrazy

Nejaký čas sa budeme venovať spracovaniu aritmetických výrazov, pozostávajúcich z čísel a operácií +,-,*,/

  • základná úloha je vyhodnotiť daný výraz, t.j. zistiť, že napr. hodnotou výrazu (65 – 3*5)/(2 + 3) je 10
  • ukážeme si niekoľko zápisov výrazu

Infixový zápis výrazu

Infixový zápis (infixová forma alebo notácia) aritmetického výrazu

  • bežný zápis používaný v matematike
  • každý binárny operátor medzi dvoma operandami, s ktorými sa bude vykonávať príslušná operácia
  • poradie, v akom sa jednotlivé operácie vykonávajú, sa riadi umiestnením zátvoriek a prioritou operácií

Napríklad: (65 – 3*5)/(2 + 3)

Vyhodnotenie výrazu v infixovej notácii by sme mohli naprogramovať napríklad rekurzívne:

  • vstup je reťazec s výrazom, výsledkom je hodnota, napr. typu double
  • ak je výrazom jediné číslo, stačí konvertovať z reťazca na číslo (napr. pomocou sscanf)
  • ak je celý výraz v zátvorkách, môžeme ich odstrániť, napr. (2+4) je to isté ako 2+4
    • pozor, nestačí pozrieť začiatok a koniec, napr. (2+4)*(1+3) nemôžeme upraviť na 2+4)*(1+3
  • zistíme, ktorá operácia sa vykoná ako posledná
    • je to jedna z operácií, ktoré nie sú vo vnútri žiadnej zátvorky a z nich tá s najmenšou prioritou a z nich najpravejšia
    • napr vo výraze (1+2)-(1+1)*10+2 je to posledná operácia +.
  • rekurzívne vyhodnotíme časť výrazu naľavo a napravo od tejto operácie, t.j. napr. (1+2)-(1+1)*10 ako -17 a 2 ako 2
  • nakoniec spočítame výsledok skombinovaním hodnôt podvýrazov a príslušného znamienka, napr. -17+2=-15
  • veľa detailov, práca s reťazcami

Postfixová a prefixová notácia

Okrem infixovej notácie sa môžeme stretnúť s ďalšími formami zápisu aritmetických výrazov.

Postfixová notácia (obrátená poľská notácia) má v zápise výrazu operátor až po oboch operandoch.

Výraz (65 – 3*5)/(2 + 3) by teda v postfixovej forme mal notáciu 65 3 5 * - 2 3 + /

V prefixovej notácii je situácia opačná ako pri postfixovej notácii. Každý operátor stojí pred dvoma operandami, ku ktorým prislúcha.

Výraz (65 – 3*5)/(2 + 3) by v prefixovej forme bol / - 65 * 3 5 + 2 3

Postfixová a prefixová notácia sú na prvý pohľad pre človeka neprehľadné, ale ako uvidíme, dajú sa oveľa jednoduchšie vyhodnocovať. Okrem toho vidíme ešte jednu výhodu - nepotrebujú zátvorky.

Vyhodnocovanie postfixovej formy

Na vyhodnocovanie výrazov v postfixovej forme budeme používať zásobník.

  • Budeme do neho vkladať operandy, teda hodnoty podvýrazov.
  • Využijeme vlastnosť, že operátor má oba operandy pred sebou. Teda keď narazíme na operátor, jeho operandy už máme niekde prečítané.
  • Navyše sú to posledné dve prečítané alebo vypočítané hodnoty.

Pri vyhodnocovaní výrazu prechádzame výraz zľava doprava

  • Keď narazíme na operand, uložíme ho do zásobníku.
  • Keď narazíme na operátor, tak vyberieme dva operandy zo zásobníku.
    • Ale pozor! Keďže máme zásobník (LIFO), musíme prehodiť poradie operandov oproti tomu, ako sme ich vybrali zo zásobníku
    • Napríklad delenie a/b je v postfixovej notácii a b /, teda zo zásobníku najskôr vyberieme deliteľa b a ako druhého vyberieme delenca a, a preto pri vykonávaní operácie musíme prehodiť ich poradie.
  • Vykonáme operáciu s vybranými operandami a výsledok tejto operácie umiestnime späť do zásobníku.

Tento postup opakujeme, kým nedojdeme na koniec výrazu. V tom okamžiku by sme mali mať na zásobníku jeden prvok a to je výsledok výrazu.

int main(void) {
    stack s;  // vytvor prazdny zasobnik
    init(s);
    char postfix[maxN];  // nacitaj vyraz do retazca
    fgets(postfix, maxN, stdin);

    int i = 0;
    while (postfix[i] != '\0') {
        if (isspace(postfix[i])) { // preskakuj biele znaky
            i++;
        } else if (isdigit(postfix[i]) || postfix[i]=='.') { // spracuj jedno cislo zo vstupu
            push(s, evaluateNumber(postfix, i));
        } else { // binarny operator
            double elem1 = pop(s);
            double elem2 = pop(s);
            switch (postfix[i]) {
                case '+': push(s, elem2 + elem1);
                    break;
                case '-': push(s, elem2 - elem1);
                    break;
                case '*': push(s, elem2 * elem1);
                    break;
                case '/': push(s, elem2 / elem1);
                    break;
            }
            i++;
        }
    }
    printf("%f\n", pop(s));  // na zasobniku mame vysledok
    assert(isEmpty(s));      // teraz uz je zasobnik prazdny 
}

Funkcia evaluateNumber môže vyzerať napríklad takto:

double evaluateNumber(char str[], int &i) {
    /* Z retazca od pozicie i precitaj cislo az po prvy
     * iny znak (medzera, koniec retazca, operator).
     * Cislo i sa nastavi na prvu poziciu, ktora sa neda precitat. */
    double val = 0;
    while (isdigit(str[i])) {
        val = val * 10 + (str[i] - '0');
        i++;
    }
    /* desatinna cast */
    if (str[i] == '.') {
        i++;
        double power = 0.1;
        while (isdigit(str[i])) {
            val += (str[i] - '0') * power;
            power /= 10;
            i++;
        }
    }
    return val;
}

Program rekurzívne vyfarbovanie

#include "../SVGdraw.h"
#include <cstdio>

const char* farby[4] = {"white", "green", "black", "brown"};
/* velkost stvorceka mapy v pixeloch */
const int stvorcek = 15;
/* cakanie po kazdom kroku vysvetlovania */
double waitTime = 0.3;

int ** vytvorMaticu(int n, int m) {
    /* vytvor maticu s n riadkami a m stlpcami */
    int **a;
    a = new int *[n];
    for (int i = 0; i < n; i++) {
        a[i] = new int[m];
    }
    return a;
}

void zmazMaticu(int n, int m, int **a) {
    /* uvolni pamat matice s n riadkami a m stlpcami */
    for (int i = 0; i < n; i++) {
        delete[] a[i];
    }
    delete[] a;
}

void zobrazStvorcek(int i, int j, int farba, const char* farbaCiary, SVGdraw &drawing) {
    /* zobraz stvorcek v riadku i a stlpci j */
    drawing.setLineColor(farbaCiary);
    drawing.setFillColor(farby[farba]);
    drawing.drawRectangle(j*stvorcek, i*stvorcek, stvorcek, stvorcek);
}

void zobrazMaticu(int **a, int n, int m, SVGdraw &drawing) {
    /* zobraz vsetky stvorceky matice */
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            zobrazStvorcek(i, j, a[i][j], "lightgray", drawing);
        }
    }
}

void obdlznik(int **a, int riadok, int stlpec, int vyska, int sirka, int farba) {
    /* do matice vykresli ram obdlznika urcitej farby */
    for (int i = 0; i < sirka; i++) {
        a[riadok][stlpec + i] = farba;
        a[riadok + vyska - 1][stlpec + i] = farba;
    }
    for (int i = 0; i < vyska; i++) {
        a[riadok + i][stlpec] = farba;
        a[riadok + i][stlpec + sirka - 1] = farba;
    }
}

void naplnMaticu(int **a, int n, int m) {
    /* do matice vykresli tri obdlzniky. Rozmery musia byt aspon
     * 11 riadkov a 17 stlpcov */
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            a[i][j] = 0;
        }
    }
    obdlznik(a, 3, 9, 3, 7, 2);
    obdlznik(a, 1, 3, 8, 9, 1);
    obdlznik(a, 5, 1, 6, 7, 2);
    obdlznik(a, 7, 13, 3, 4, 1);
}

void vyfarbi(int **a, int n, int m, int riadok, int stlpec, 
             int farba, SVGdraw &drawing) {
    /* prefarbi suvislu jednofarebnu oblast obsahujucu
     * a[riadok][stlpec] na farbu farba */

    if (a[riadok][stlpec] != farba) {
        int stara_farba = a[riadok][stlpec];
        a[riadok][stlpec] = farba;
        zobrazStvorcek(riadok, stlpec, a[riadok][stlpec], "red", drawing);
        drawing.wait(waitTime);
        if (riadok > 0 && a[riadok - 1][stlpec] == stara_farba) {
            vyfarbi(a, n, m, riadok - 1, stlpec, farba, drawing);
        }
        if (riadok + 1 < n && a[riadok + 1][stlpec] == stara_farba) {
            vyfarbi(a, n, m, riadok + 1, stlpec, farba, drawing);
        }
        if (stlpec > 0 && a[riadok][stlpec - 1] == stara_farba) {
            vyfarbi(a, n, m, riadok, stlpec - 1, farba, drawing);
        }
        if (stlpec + 1 < m && a[riadok][stlpec + 1] == stara_farba) {
            vyfarbi(a, n, m, riadok, stlpec + 1, farba, drawing);
        }
        zobrazStvorcek(riadok, stlpec, a[riadok][stlpec], "lightgray", drawing);
        drawing.wait(waitTime);
    }
}


int main(void) {
    int n = 11;
    int m = 17;
    int **a = vytvorMaticu(n, m);
    naplnMaticu(a, n, m);

    SVGdraw drawing(m * stvorcek, n * stvorcek, "vyfarbi1.svg");
    zobrazMaticu(a, n, m, drawing);

    vyfarbi(a, n, m, 2, 1, 3, drawing);

    drawing.finish();
    zmazMaticu(n, m, a);
}

Ukážkový program ostrovy

#include "../SVGdraw.h"
#include <cstdio>

const char * farby[3] = {"white", "green", "black"};
/* velkost stvorceka mapy v pixeloch */
const int stvorcek = 15;

int ** vytvorMaticu(int n, int m) {
    /* vytvor maticu s n riadkami a m stlpcami */
    int **a;
    a = new int *[n];
    for (int i = 0; i < n; i++) {
        a[i] = new int[m];
    }
    return a;
}

void zmazMaticu(int n, int m, int **a) {
    /* uvolni pamat matice s n riadkami a m stlpcami */
    for (int i = 0; i < n; i++) {
        delete[] a[i];
    }
    delete[] a;
}


void nacitajMaticu(FILE *f, int n, int m, int **a) {
    /* matica je vytvorena, velkosti n, m, vyplnime ju cislami zo vstupu */
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            fscanf(f,"%d ",&a[i][j]);
        }
    }
}

void zobrazStvorcek(int i, int j, int farba, const char * farbaCiary, SVGdraw &drawing) {
    /* zobraz stvorcek v riadku i a stlpci j */
    drawing.setLineColor(farbaCiary);
    drawing.setFillColor(farby[farba]);
    drawing.drawRectangle(j*stvorcek, i*stvorcek, stvorcek, stvorcek);
}

void zobrazMaticu(int **a, int n, int m, SVGdraw &drawing) {
    /* zobraz vsetky stvorceky matice */
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            zobrazStvorcek(i, j, a[i][j], "lightgray", drawing);
        }
    }
}

void vyfarbi(int **a, int n, int m, int riadok, int stlpec, int farba) {
    /* prefarbi suvislu jednofarebnu oblast obsahujucu
     * a[riadok][stlpec] na farbu farba */

    if (a[riadok][stlpec] != farba) {
        int stara_farba = a[riadok][stlpec];
        a[riadok][stlpec] = farba;
        if (riadok > 0 && a[riadok - 1][stlpec] == stara_farba) {
            vyfarbi(a, n, m, riadok - 1, stlpec, farba);
        }
        if (riadok + 1 < n && a[riadok + 1][stlpec] == stara_farba) {
            vyfarbi(a, n, m, riadok + 1, stlpec, farba);
        }
        if (stlpec > 0 && a[riadok][stlpec - 1] == stara_farba) {
            vyfarbi(a, n, m, riadok, stlpec - 1, farba);
        }
        if (stlpec + 1 < m && a[riadok][stlpec + 1] == stara_farba) {
            vyfarbi(a, n, m, riadok, stlpec + 1, farba);
        }
    }
}


int main(void) {
    /* nacitaj rozmery matice */
    int n, m;
    FILE *f;
    f=fopen("vstup.txt","r");
    if (!f) return -1;
    
    fscanf(f,"%d %d ",&n,&m);

    /* vytvor a nacitaj maticu */
    int **a = vytvorMaticu(n, m);
    nacitajMaticu(f, n, m, a);

    fclose(f);
    
    for(int i=0; i<n; i++) {
        for(int j=0; j<m; j++) {
            if(a[i][j]>0) a[i][j] = 1;
        }
    }


    SVGdraw drawing(m*stvorcek, n * stvorcek, "ostrovy.svg");
    zobrazMaticu(a, n, m, drawing);

    int ostrovov = 0;
    for(int i=0; i<n; i++) {
        for(int j=0; j<m; j++) {
            if(a[i][j]==1) {
                ostrovov++;
                vyfarbi(a, n, m, i, j, 2);
                drawing.clear();
                zobrazMaticu(a, n, m, drawing);
		drawing.wait(2);
            }
        }
    }

    printf("Pocet ostrovov: %d\n",ostrovov);

    drawing.finish();
    zmazMaticu(n, m, a);
}

Prednáška 19

Opakovanie: aritmetické výrazy

Aritmetické výrazy

  • Bežná infixová notácia, napr. (65 – 3*5)/(2 + 3)
  • Postfixová notácia 65 3 5 * - 2 3 + /
  • Prefixová notácia / - 65 * 3 5 + 2 3
  • Prefixová a postfixová notácia nepotrebujú zátvorky
  • Vyhodnocovanie postfixovej notácie pomocou zásobníka
    • Čo si ukladáme do zásobníka?

Konverzia infixovej formy výrazu na postfixovú

Vidíme, že vyhodnocovanie postfixového výrazu je jednoduché. Väčšinou však pracujeme s infixovou formou zápisu a preto ju potrebujeme nejakým spôsobom prepísať do postfixovej.

Pre jednoduchosť si najskôr ukážeme iba výrazy bez zátvoriek, pričom * a / majú vyššiu prioritu ako + a -. Použijeme zásobník, do ktorého si budeme ukladať zatiaľ nevypísané časti infixovej formuly (teda operátory, ktoré ešte nemajú oba operandy).

Pri vyhodnocovaní prechádzame výrazom zľava doprava:

  • Ak objavíme operand, prepíšeme ho do postfixového reťazca a ďalej nás už nezaujíma.
  • Ak narazíme na operátor, pozrieme sa na vrch zásobníka:
    • Kým operátor zo zásobníka má väčšiu alebo rovnakú prioritu, tak vyberáme zo zásobníka
    • Vybrané operátory prepisujeme do postfixového reťazca.
    • Vypisujeme až kým nenarazíme na operátor s nižšou prioritou (alebo dno).
  • Po tomto kroku umiestnime aktuálny operátor do zásobníka.
  • Tento postup opakujeme pokiaľ nenarazíme na koniec výrazu.
    • Potom vyberieme zvyšné operátory zo zásobníka a umiestnime ich na výstup.
  • Odsimulujme na výraze 8 + 3 * 4 * 2 - 5 + 2 * 3

Pridanie zátvoriek:

  • Ľavú zátvorku iba vložíme do zásobníka
  • Keď narazíme na pravú zátvorku, znamená to, že všetky operátory, ktoré boli v zátvorke, by mali byť už vypísané na výstup. Preto ich vypisujeme, až kým nenarazíme na ľavú zátvorku.
int precedence(char op) {
    if (op == '#' || op == '(') return 0; // specialne pripady, ktore bezne nevyhadzujeme
    if (op == '-' || op == '+') return 1;
    if (op == '*' || op == '/') return 2;
    assert(false); // sem by sme sa nemali dostat    
}

int main(void) {
    char infix[maxN], postfix[maxN];
    fgets(infix, 100, stdin); // nacita aritmeticky vyraz v infixovej forme

    stack s; // vytvor prazdny zasobnik
    init(s);
    push(s, '#'); // specialne dno zasobnika

    int j = 0; /* index do vystupneho retazca */
    for (int i = 0; infix[i] != 0; i++) {
        if (isdigit(infix[i]) || infix[i]=='.') { /* cisla skopirujeme na vystup */
            postfix[j++] = infix[i];
        }
        else if (isspace(infix[i])) { /* biele znaky preskakujeme */
        } else if (infix[i] == '(') { /* zaciatok zatvorky dame na zasobnik */
            push(s, infix[i]);
        } else if (infix[i] == ')') {
            /* koniec zatvorky znamena, ze vypiseme vsetky operatory, 
             * co boli v tej zatvorke (a este nie su vypisane)*/
            char popped = pop(s);
            postfix[j++] = ' ';
            while (popped != '(') {
                postfix[j++] = popped;
                postfix[j++] = ' ';
                popped = pop(s);
            }
        }
        else { /* spracovanie operatora */
            postfix[j++] = ' '; // pridame medzeru
            int p = precedence(infix[i]); // dolezitost prichadzajuceho operatora
            while (precedence(peek(s)) >= p) {
                postfix[j++] = pop(s);
                postfix[j++] = ' ';
            }
            push(s, infix[i]);
        }
    }

    /* vsetko, co na zasobniku ostalo, vypiseme */
    char popped = pop(s);
    postfix[j++] = ' ';
    while (popped != '#') {
        postfix[j++] = popped;
        postfix[j++] = ' ';
        popped = pop(s);
    }

    postfix[j] = '\0';
    printf("Postfix: %s\n", postfix);
}
  • Program na niekoľkých miestach pridáva medzery (občas zbytočné), aby sa nám operandy nezliali do jedného čísla a vedeli sme ich následne vyhodnocovať.
  • Nefunguje pre unárne mínus (napr. 2*-3), ktoré treba detegovať a kopírovať na výstup spolu s číslom (a správne spracovávať aj v postfixovej notácii)

Cvičenia:

  • Ako by ste rozšírili o operáciu umocnenia ^ s ešte vyššou prioritou ako *?
  • Niektoré časti programu (cykly while) sa trochu opakujú, ako by sa to dalo vyriešiť pomocnou funkciou?
  • Ako by sme program upravili, aby nikde nedal dve medzery za sebou?

Aritmetický výraz ako strom

Ďalšia téma:

  • uloženie aritmetického výrazu vo forme stromu a práca so stromami všeobecne

V stredu a budúci týždeň:

  • ďalšie príklady stromov v informatike
Strom pre výraz (65 – 3*5)/(2 + 3)

Reprezentácia aritmetického výrazu vo forme stromu

  • Každý operátor a každé číslo tvorí vrchol stromu
  • Vrchol pre operátor má pod sebou zavesené menšie stromy pre podvýrazy, ktoré spája
  • Informatici stromy väčšinou kreslia hore nohami, s koreňom na vrchu
    • V našom príklade je koreň vrchol s operátorom /

Dátová štruktúra pre vrcholy stromu

struct node {
    /* vrchol stromu  */
    double val;     /* ciselna hodnota */
    char op;        /* operator '+', '-', *', '/', alebo ' ' ak ide o hodnotu */
    node * left;    /* lavy podvyraz */
    node * right;  /* pravy podvyraz */
};

Ak máme vrchol pre operátor:

  • left a right sú smerníky na ľavý a pravý podvýraz
  • znak op je znamienko operátora, napr. '+'
  • hodnota val je nevyužitá

Ak máme vrchol pre číslo vo výraze:

  • left a right majú hodnotu NULL (žiadne podvýrazy)
  • znak op má hodnotu medzera ' '
  • val obsahuje hodnotu čísla

Jednoduchá ale nie veľmi elegantná reprezentácia

  • niektoré položky sú nevyužité (val v operátoroch, left a right pri číslach)
  • budúci semester uvidíme krajšie riešenie pomocou objektov

Vytváranie vrcholov stromu

Nasledujúce dve funkcie vytvoria nový vrchol.

  • Pre vrchol typu operátor už funkcia dostane smerníky na vrcholy pre podvýrazy
node * createOp(char op, node *left, node *right) {
    /* vytvori novy vrchol stromu s operatorom op
     * a do jeho laveho a praveho podvyrazu ulozi
     * smerniky left a right. */
    node *v = new node;
    v->left = left;
    v->right = right;
    v->op = op;
    return v;
}

node * createNum(double val) {
    /* Vytvori novy vrchol stromu s danou hodnotou,
     * lavy a pravy podvyraz bude prazdny, op bude medzera */
    node *v = new node;
    v->left = NULL;
    v->right = NULL;
    v->op = ' ';
    v->val = val;
    return v;
}

Vytvorme teraz ručne strom pre náš výraz (65 – 3*5)/(2 + 3):

node *v = createOp('/',
            createOp('-', createNum(65),
                          createOp('*', createNum(3), createNum(5))),
            createOp('+', createNum(2), createNum(3)));

Alebo to môžeme rozpísať po krokoch:

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);

Vyhodnocovanie výrazu

double evaluate(node *v) {
    /* vyhodnoti vyraz dany stromom s korenom vo vrchole v */
    assert(v != NULL);

    /* ak je operator medzera, vratime jednoducho hodnotu */
    if (v->op == ' ') {
        return v->val;
    }

    /* rekurzivne vyhodnotime lavy a pravy podvyraz */
    double valLeft = evaluate(v->left);
    double valRight = evaluate(v->right);

    /* Hodnotu laveho a praveho podvyrazu spojime podla typu operatora
     * a vratime. Prikaz break netreba, pouzivame return. */
    switch (v->op) {
        case '+': return valLeft + valRight;
        case '-': return valLeft - valRight;
        case '*': return valLeft * valRight;
        case '/': return valLeft / valRight;
        default: assert(false);
    }
}

Vytvorenie stromu z postfixového výrazu

Pripomeňme si kód na vyhodnocovanie postfixového výrazu:

typedef double dataType;

double evaluatePostfix(char postfix[]) {
    stack s;
    init(s);
    int i = 0;
    while (postfix[i] != '\0') {
        if (isspace(postfix[i])) { // preskakuj biele znaky
            i++;
        } else if (isdigit(postfix[i]) || postfix[i]=='.') { // spracuj jedno cislo zo vstupu
            push(s, evaluateNumber(postfix, i));
        } else { // binarny operator
            double elem1 = pop(s);
            double elem2 = pop(s);
            switch (postfix[i]) {
                case '+': push(s, elem2 + elem1);
                    break;
                case '-': push(s, elem2 - elem1);
                    break;
                case '*': push(s, elem2 * elem1);
                    break;
                case '/': push(s, elem2 / elem1);
                    break;
            }
            i++;
        }
    }
    return pop(s);
}
  • Do zásobníka si ukladáme čísla - medzivýsledky už vyhodnotených podvýrazov.
  • Pri tvorbe stromu si tam namiesto toho budeme ukladať už vytvorené podstromy.
typedef node * dataType;

node *parsePostfix(char *postfix) {
    stack s;
    init(s);

    int i = 0;
    while (postfix[i] != '\0') {
        if (isspace(postfix[i])) { // preskakuj biele znaky
            i++;
        } else if (isdigit(postfix[i]) || postfix[i]=='.') { // spracuj jedno cislo zo vstupu
            double val = evaluateNumber(postfix, i);
            push(s, createNum(val));
        } else { // binarny operator
            node * elem1 = pop(s);
            node * elem2 = pop(s);
            node * v = createOp(postfix[i], elem2, elem1);
            push(s, v);
            i++;
        }
    }
    return pop(s);
}

Zhrnutie

  • Aritmetický výraz vieme reprezentovať aj ako strom.
  • Rekurzívnou funkciou môžeme potom ľahko spočítať jeho hodnotu.
  • Postfixovú formu vieme prerobiť na strom (a tým pádom vieme prerobiť aj infixovú, lebo tú vieme prerobiť na postfixovú).
  • Nabudúce si ukážeme, ako výraz reprezentovaný stromom vypísať vo všetkých troch formách.

Terminológia stromov

  • Stromvrcholy/uzly (nodes, vertices) a tie sú pospájané hranami (edges)
  • Nás zaujímajú zakorenené stromy, ktoré majú jeden vrchol zvolený ako koreň (root)
  • Každý iný vrchol okrem koreňa je spojený hranou s jedným rodičom/otcom (parent) a s niekoľkými (nula alebo viac) deťmi/synmi (children)
  • Listy sú vrcholy, ktoré nemajú deti, ostatné vrcholy voláme vnútorné
  • V binárnom strome má každý vrchol najviac dve deti
  • Predkovia vrchola sú všetky vrcholy na ceste od neho smerom ku koreňu, teda on sám, jeho otec, otec jeho otca atď, až kým nenarazíme na koreň
  • Ak x predkom y, tak y je potomkom x
  • Podstrom s koreňom vo vrchole x tvorí vrchol x a všetci jeho potomkovia
  • Binárny strom je teda buď prázdny, alebo je tvorený koreňom a dvoma podstromami: ľavým a pravým.

Dátovú štruktúru pre vrcholy binárnych stromov môžeme zovšeobecniť a použiť v nej nejaký všeobecný typ dataType, podobne ako pri zásobníku. Spravíme si tiež všeobecnú funkciu na vytvorenie nového vrcholu, ktorá dostane smerníky na podstromy.

struct node {
    /* vrchol stromu  */
    dataType data;
    node * left;  /* lavy podstrom */
    node * right; /* pravy podstrom */
};

node * createNode(dataType data, node *left, node *right) {
    node *v = new node;
    v->data = data;
    v->left = left;
    v->right = right;
    return v;
}

Uvažujme strom, v ktorom každý vrchol obsahuje jeden znak. Ako bude vyzerať tento strom?

node *v = createNode('A',
            createNode('B', createNode('C', NULL, NULL),
                            createNode('D', NULL, NULL)),
            createNode('E', NULL, createNode('F', NULL, NULL)));

Náš aritmetický strom

  • Je binárny
  • V listoch sú čísla, vo vnútorných vrcholoch operácie
  • Každý vnútorný vrchol má dve deti

Ak by sme takýto všeobecný strom chceli použiť na aritmetické výrazy, definujeme si dataType ako štruktúru s dvoma položkami op a val, ktoré vo vrcholoch potrebujeme. Funkcie createOp a createNum vieme napísať pomocou createNode.

struct dataType {
    double val;     /* ciselna hodnota */
    char op;     /* operator '+', '-', *', '/', alebo ' ' ak ide o hodnotu */
};

node * createOp(char op, node *left, node *right) {
    dataType d;
    d.op = op;
    return createNode(d, left, right);
}

Jednoduchá práca so stromami

Uvoľňovanie stromu

Na precvičenie práce so stromami si ešte napíšme funkciu, ktorá dostane koreň stromu a uvoľní všetku pamäť, ktorú jednotlivé vrcholy stromu zaberali.

  • Opäť použijeme rekurziu, pričom najprv uvoľníme všetku pamäť pre ľavý podstrom, potom pre pravý a nakoniec samotný koreň.
  • Ako triviálny nerekurzívny prípad použijeme prázdny strom, ktorý ma smerník na koreň NULL.
void destroyTree(node* v){
  if(v!=NULL){
    destroyTree(v->left);
    destroyTree(v->right);
    delete v;
  }
}

Výška (hĺbka) stromu

  • Hĺbka vrcholu v strome je jeho vzdialenosť od koreňa. T.j. koreň má hĺbku 0, jeho synovia hĺbku 1 atď.
  • Výška stromu (niekedy nazývaná hĺbka stromu) je maximum z hĺbok jeho vrcholov
    • Ak máme strom s jedným vrcholom, výška je 0
    • Inak spočítame výšku ľavého a pravého podstromu
    • Ku každej pripočítame 1, lebo pridávame hranu k otcovi
    • Aby to fungovalo aj pre prázdne podstromy, dodefinujeme výšku prázdneho podstromu na -1
 int height(node *v) {
    /* Vrat vysku stromu s korenom v.
     * Ak v je NULL, vrati -1. */
    if (v == NULL) return -1;
    int left = height(v->left);
    int right = height(v->right);
    /* vrat max(left, right)+1 */
    if (left >= right) return left + 1;
    else return right + 1;
}

Ak máme binárny strom s n vrcholmi, aká môže byť jeho minimálna a maximálna výška?

  • Výška stromu s n vrcholmi je najviac n-1, ak sú všetky navešané jeden pod druhý, t.j. každý okrem posledného má jedného syna
  • 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 2^{h+1}-1} vrcholov
    • Dá sa dokázať indukciou vzhľadom na h
  • Takže vieme, ž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 n\le 2^{h+1}-1} .
  • Vyjadríme h: 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\ge \log_2(n+1)-1}
  • Takže dostávame 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 \le h \le n-1}
  • Napr. strom s milión vrcholmi má hĺbku medzi 19 a 999999

Prednáška 20

Oznamy

  • Riadne termíny skúšok pondelok 8.1. a pondelok 22.1., prvý opravný streda 31.1.,
    • Vyhovovala by vám prípadná zmena opravného termínu na piatok 2.2.?

Opakovanie

Binárny strom

  • Každý vrchol stromu najviac dve deti
  • Binárny strom je buď prázdny, alebo je tvorený koreňom a dvoma podstromami: ľavým a pravým
struct node {
    /* vrchol stromu  */
    dataType data;
    node * left;  /* lavy podstrom */
    node * right; /* pravy podstrom */
};

node * createNode(dataType data, node *left, node *right) {
    node *v = new node;
    v->data = data;
    v->left = left;
    v->right = right;
    return v;
}

Nech teraz dataType je char. Ako bude vyzerať tento strom?

node *v = createNode('A',
            createNode('B', createNode('C', NULL, NULL),
                            createNode('D', NULL, NULL)),
            createNode('E', NULL, createNode('F', NULL, NULL)));

Akú má výšku? Ktoré vrcholy sú listy?

Jednoduchá práca so stromami: Úplný binárny strom

  • Strom, ktorý má pri určitej výške h maximálny počet vrcholov, 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 2^{h+1}-1} sa nazýva úplny binárny strom.
  • Chceli by sme taký strom vytvoriť.
  • Použijeme funkciu createNode na vytvorenie jedného vrcholu uvedenú vyššie
  • Rekurzívne tvoríme väčšie stromy z menších.
  • Globálna premenná count priradí vrcholom do dátovej položky poradové číslo.
    • Ako funkcia očísluje vrcholy pre strom výšky 2?
node* createTree(int height) {
    if (height == -1) return NULL;
    node* u = createNode(count++, NULL, NULL);
    u->left = createTree(height - 1);
    u->right = createTree(height - 1);
    return u;
}

Aritmetické výrazy

Videli sme:

  • Bežná infixová notácia, napr. (65 – 3*5)/(2 + 3)
  • Postfixová notácia 65 3 5 * - 2 3 + /
  • Prefixová notácia / - 65 * 3 5 + 2 3
  • Prefixová a postfixová notácia nepotrebujú zátvorky
  • Prevod z infixovej notácie na postfixovú pomocou zásobníka
  • Vyhodnocovanie postfixovej notácie pomocou zásobníka
  • Vytvorenie stromu z postfixovej notácie pomocou zásobníka
  • Vyhodnocovanie výrazu v tvare stromu (jednoduchá rekurzia)
Strom pre výraz (65 – 3*5)/(2 + 3)

Aritmetické výrazy ako stromy

  • Každý operátor a každé číslo tvorí vrchol stromu
  • Vrchol pre operátor má pod sebou zavesené podstromy pre podvýrazy, ktoré spája
struct node { /* vrchol stromu  */
    double val;     /* ciselna hodnota */
    char op;        /* operator '+', '-', '*', '/', 
                     * alebo ' ' ak ide o hodnotu */
    node * left;    /* lavy podvyraz alebo NULL */
    node * right;   /* pravy podvyraz alebo NULL */
};

node * createOp(char op, node *left, node *right) {
    /* vytvori novy vrchol stromu s operatorom op
     * a do jeho laveho a praveho podvyrazu ulozi
     * smerniky left a right. */
    node *v = new node;
    v->left = left;
    v->right = right;
    v->op = op;
    return v;
}

node * createNum(double val) {
    /* Vytvori novy vrchol stromu s danou hodnotou,
     * lavy a pravy podvyraz bude prazdny, op bude medzera */
    node *v = new node;
    v->left = NULL;
    v->right = NULL;
    v->op = ' ';
    v->val = val;
    return v;
}

Alebo ak máme všeobecný strom, ktorý má vo vrcholoch položku data typu dataType, môžeme aritmetický strom uložiť ak dataType je štruktúra s dvoma položkami op a val. Funkcie createOp a createNum vieme napísať pomocou createNode.

struct dataType {
    double val;     /* ciselna hodnota */
    char op;     /* operator '+', '-', *', '/', alebo ' ' ak ide o hodnotu */
};

node * createOp(char op, node *left, node *right) {
    dataType d;
    d.op = op;
    return createNode(d, left, right);
}

node * createNum(double val) {
    dataType d;
    d.op = ' ';
    d.val = val;
    return createNode(d, NULL, NULL);
}

Ak máme premennú v ako smerník na nejaký vrchol stromu, namiesto v->op budeme písať v->data.op.

Prehľadávanie stromov

  • Často potrebujeme prejsť celý strom a spracovať dáta vo všetkých vrcholoch.
  • Napríklad chceme vypísať hodnotu v každom vrchole
  • Opäť použijeme rekurziu, voláme na ľavý a pravý podstrom.
void print(dataType &d) {
    cout << d;
}

void preorder(node *v) {
    if (v == NULL) return;
    print(v->data);
    preorder(v->left);
    preorder(v->right);
}
  • Pre príklad stromu uvedeného vyššie vypíše ABCDEF
  • Takéto poradie sa volá preorder, lebo najprv vypíšeme (spracujeme) dáta vo vrchole, až potom v jeho podstromoch.
  • Dáta vo vrchole môžeme vypísať aj po navštívení oboch podstromov, takéto poradie nazývame postorder.
    • Pre náš strom CDBFEA
  • Alebo ich môžeme vypísať medzi navštívením ľavého a pravého vrcholu, takéto poradie nazývame inorder.
    • Pre náš strom CBDAEF
void postorder(node *v) {
    if (v == NULL) return;
    postorder(v->left);
    postorder(v->right);
    print(v->data);
}

void inorder(node *v) {
    if (v == NULL) return;
    inorder(v->left);
    print(v->data);
    inorder(v->right);
}

Vypisovanie aritmetických výrazov

  • Preorder vypisovanie vypíše výraz v prefixovej notácii / - 65 * 3 5 + 2 3
  • Postorder vypisovanie vypíše výraz v postfixovej notácii 65 3 5 * - 2 3 + /
void print(dataType &d) {
    /* funkcia na tlac jedneho vrcholu pouzita v preorder a postorder */
    if(d.op == ' ') {
      cout << ' ' << d.val;
    }
    else {
        cout << ' ' << d.op;
    }
}
  • Inorder vypisovanie nevypíše výraz v infixovej notácii, chýbajú zátvorky 65 - 3 * 5 / 2 + 3
    • Chceme úplne uzátvorkovaný výraz, napr. ( ( 65 - ( 3 * 5 ) ) / ( 2 + 3 ) )
void infix(node *v) {
    /* funkcia vypise vyraz v infixovej notacii */
    if(v->data.op == ' ') {
        cout << ' ' << v->data.val;
    }
    else {
        cout << " (";
        infix(v->left);
        cout << " " << v->data.op;
        infix(v->right);
        cout << " )";
    }
}
  • Ako by sme vynechali zbytočné zátvorky?

Cvičenia

  • Vo výrazoch by sme mohli mať aj premenné, potom za ne dosadzovať hodnoty alebo celé podvýrazy
    • Ako by sme premenné reprezentovali v štruktúre dataType?
  • Predstavme si, že v preorder poradí namiesto dát vypíšeme počet detí daného vrcholu. Napríklad pre náš strom so znakmi to bude 2 2 0 0 1 0 (preorder poradie je A B C D E F, vrcholy A a B majú po dve deti, vrcholy C a D majú 0 detí, vrchol E má jedno dieťa a vrchol F 0 detí). Ako z takejto postupnosti zostavíme strom? Je jednoznačne daný? Skúste si nakresliť strom pre postupnosť 2 0 2 1 2 0 0 2 0 0.

Binárne vyhľadávacie stromy

P22-BST.png

Stromy sa v informatike často používajú. Ďalším príkladom sú binárne vyhľadávacie stromy, ktoré slúžia na ukladanie množiny prvkov. Prvky množiny teda nemáme v poli, ani v spájanom zozname, ale vo vrcholoch binárneho stromu.

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

Cvičenie: nájdite všetky binárne vyhľadávacie stromy pre množinu kľúčov {1,2,3}

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

  • V každom vrchole data typu dataType, ktoré vieme porovnávať znamienkom < (napr. int, double, ...)
  • V každom vrchole si pamätáme aj smerník na otca (ten má hodnotu NULL v koreni)
  • Strom je potom jednoducho smerník na koreň a inicializujeme ho na NULL.
struct node {
    /* vrchol binarneho vyhladavacieho stromu  */
    dataType data; /* hodnota */
    node * parent;  /* otec vrchola */
    node * left;    /* lavy syn */
    node * right;   /* pravy syn */
};

struct binarySearchTree {
    node *root;  /* koren stromu, NULL pre prazdny strom */
};

void init(binarySearchTree &t) {
    /* inicializuje prazdny binarny vyhladavaci strom */
    t.root = NULL;
}

Hľadanie vo vyhľadávacom strome

  • Porovnáme hľadané dáta s dátami v koreni
    • Ak sa rovnajú, končíme (našli sme, čo hľadáme)
    • Ak je hľadaná hodnota menšia ako dáta v koreni, musí byť v ľavom podstrome, ak je väčšia v pravom
  • V príslušnom podstrome sa rozhodujeme podľa tých istých pravidiel
  • Keď narazíme na prázdny podstrom, dáta sa v strome nenachádzajú
  • Dá sa zapísať rekurzívne alebo cyklom, lebo vždy ideme iba do jedného podstromu
node * findNode(node *root, dataType item) {
    /* V binarnom vyhladavacom strome s korenom root najdi a vrat
     * vrchol s hodnotou item a ak neexistuje, vrat NULL. */
    node * v = root;
    while (v != NULL && v->data != item) {
        if (item < v->data) {
            v = v->left;
        } else {
            v = v->right;
        }
    }
    return v;
}

/* rekurzivna verzia */
node * findNodeR(node *root, dataType item) {
    /* V binarnom vyhladavacom strome s korenom root najdi a vrat
     * vrchol s hodnotou item a ak neexistuje, vrat NULL. */
    if (root == NULL || root->data == item) {  
        return root;
    } else if (item < root->data) {
        return findNodeR(root->left, item);
    } else {
        return findNodeR(root->right, item);
    }
}
  • Čas výpočtu je v najhoršom prípade úmerný výške stromu

Pre užívateľa rekurziu obalíme do pomocnej funkcie, ktorá vráti true ak sa daná hodnota v strome vyskytuje a false inak

bool find(binarySearchTree &t, dataType item) {
  return findNode(t.root, item) != NULL;
}

Vkladanie prvku do vyhľadávacieho stromu

  • Predpokladáme, že prvok v strome nie je.
  • Putujeme po strome podobne ako pri vyhľadávaní prvku, až kým nenarazíme na nulový smerník.
    • Na tomto mieste by mal byť nový prvok, takže ho tam pridáme ako nový list
    • Dve verzie: s cyklom a rekurzívna
node * createLeaf(dataType item, node * parent) {
    /* vytvor novy vrchol s danym kľúčom, obe deti nastav na NULL */
    node *v = new node;
    v->data = item;
    v->left = NULL;
    v->right = NULL;
    v->parent = parent;
    return v;
}

void insert(binarySearchTree &t, dataType item) {
    /* Do stromu t vlozi vrchol s hodnotou item na spravne miesto.
     * Predpokladame, ze takyto kluc este v strome nie je. */
    if (t.root == NULL) {
        /* prazdny strom - treba vytvorit koren */
        t.root = createLeaf(item, NULL);
        return;
    }
    /* Putujeme po strome podobne ako vo find */
    node * v = t.root;
    while (true) {
        assert(v != NULL && v->data != item);
        if (item < v->data) {
            /* item by mal byt vlavo. Ak tam nic nie je, spravime novy list */
            if (v->left == NULL) {
                v->left = createLeaf(item, v);
                break;
            } else {  // hladame vlavo
                v = v->left;
            }
        } else {
            /* item by mal byt pravo. Ak tam nic nie je, spravime novy list */
            if (v->right == NULL) {
                v->right = createLeaf(item, v);
                break;
            } else { // hladame vpravo
                v = v->right;
            }
        }
    }
}


node * insertR(node *v, dataType item) {
    /* Do stromu s korenom vo v vlozi vrchol s hodnotou item na
     * spravne miesto a vrati smernik na koren (tento sa moze zmenit
     * ak strom bol prazdny) */
    if (v == NULL) {   // vkladanie do prazdneho stromu
        return createLeaf(item, NULL);
    } else {
        if (item < v->data) {  // vlozime prvok do laveho podstromu
            v->left = insertR(v->left, item);
            v->left->parent = v;  // pre istotu uloz rodica (ak v->left je novy)
        } else {  // vlozime prvok do praveho podstromu
            assert(item > v->data);
            v->right = insertR(v->right, item);
            v->right->parent = v; // pre istotu uloz rodica (ak v->right je novy)
        }
        return v;
    }
}

void insertR(binarySearchTree &t, dataType item) {
    /* pomocna funkcia, zavola rekurziu pre koren */
    t.root = insertR(t.root, item);
}
  • Čas vkladania je tiež v najhoršom prípade úmerný hĺbke stromu.

Príklad: ako bude vyzerať strom po nasledujúcej postupnosti operácií?

    binarySearchTree t;
    init(t);
    insert(t, 2);
    insert(t, 5);
    insert(t, 3);
    insert(t, 10);
    insert(t, 7);  

Odbočka: minimum a nasledovník

  • Spravíme si dve funkcie, ktoré sa nám zídu pri mazaní prvku, ale môžu sa zísť aj inokedy.
  • Prvá funkcia nájde vo vyhľadávacom strome minimum.
    • Všetky prvky menšie ako koreň sú v ľavom podstrome, bude tam zrejme aj minimum.
    • Tá istá úvaha platí pre koreň ľavého podstromu.
    • Ideme teda doľava kým sa dá, posledný vrchol vrátime (list alebo vrchol s pravým synom).
    • Dá sa tiež pekne napísať rekurzívne.
node *minimumNode(node *v) {
    /* vrati vrchol s minimalnou hodnotou data vo vyhladavacom strome
     * s korenom v */
    assert(v != NULL);
    while (v->left != NULL) {
        v = v->left;
    }
    return v;
}

Druhá funkcia nájde vrchol, ktorý v utriedenom poradí nasleduje za daným vrcholom v.

  • Ak v má pravého syna, nasledovník bude v pravom podstrome, konkrétne vrchol s minimálnou hodnotou data v tomto podstrome
  • V opačnom prípade to môže byť rodič, ak v je jeho ľavý syn
  • Ak je pravý syn, môže to byť prarodič, ak je rodič jeho ľavý syn, atď
  • Nájdeme teda prvého predka, do ktorého ľavého podstromu patrí v a ten je hľadaný nasledovník
node *successorNode(node *v) {
    /* vrati vrchol, ktorý v utriedenom poradi nasleduje za vrcholom v,
     * alebo NULL ak taky vrchol nie je */
    assert(v != NULL);
    if (v->right != NULL) {
        return minimumNode(v->right);
    }
    while (v->parent != NULL && v == v->parent->right) {
        v = v->parent;
    }
    return v->parent;
}
  • Čo presne bude funkcia robiť, keď prvok v nemá nasledovníka?

Binárne vyhľadávacie stromy: zložitosť a zhrnutie

Binárny vyhľadávací strom je binárny strom, v ktorom platí:

  • Hodnota v koreni je väčšia ako všetky hodnoty v ľavom podstrome.
  • Hodnota v koreni je menšia ako všetky hodnoty v pravom podstrome.
  • Ľavý aj pravý podstrom sú binárne vyhľadávacie stromy.

Hodí sa na implementáciu operácií find, remove, insert

Pri hľadaní putujeme od koreňa dole a vždy vieme, či máme pokračovať vľavo alebo vpravo

  • podobá sa to teda na binárne vyhľadávanie

Zložitosť operácií závisí od výšky stromu.

  • T.j. zložitosť find, insert aj remove je O(height(T))

Výška h stromu s n vrcholmi 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+1)-1 \le h \le n-1} (viď minulá prednáška)

Zložitosť operácií teda je

  • V najhoršom prípade O(n)
    • nastane napr. ak prvky vkladáme od najmenšieho po najväčší alebo naopak
  • V priemernom prípade O(log n)
    • ak sme prvky vkladali v náhodnom poradí

Na druháckom predmete Algoritmy a dátové štruktúry si ukážete obmeny vyhľadávacích stromov, ktoré majú zložitosť O(log n) aj v najhoršom prípade


Cvičenie

  • Napíšte funkciu, ktorá dostane utriedené pole čísel a vytvorí z nich binárny vyhľadávací strom (priamo, bez použitia funkcie insert). Pokúste sa strom vytvoriť tak, aby ľavý a pravý podstrom každého vrcholu obsahovali približne rovnako veľa prvkov.
    • Doporučujeme postupovať rekurzívne, pričom napíšte funkciu s hlavičkou napr. node * vytvorStrom(int a[], int l, int r), ktorá vytvorí strom pre časť poľa a od pozície l po pozíciu r.

Zdrojový kód programu

Zdrojový kód programu s binárnymi vyhľadávacími stromami

/* Ukazkovy program pre pracu s binarnymi vyhladavacimi stromami */

#include <cstdio>
#include <cassert>

typedef int dataType;

struct node {
    /* vrchol binarneho vyhladavacieho stromu  */
    dataType data; /* hodnota */
    node * parent;  /* otec vrchola */
    node * left;    /* lavy syn */
    node * right;   /* pravy syn */
};

struct binarySearchTree {
    node *root;  /* koren stromu, NULL pre prazdny strom */
};

void init(binarySearchTree &t) {
    /* inicializuje prazdny binarny vyhladavaci strom */
    t.root = NULL;
}


node * findNode(node *root, dataType item) {
    /* V binarnom vyhladavacom strome s korenom root najdi a vrat
     * vrchol s hodnotou item a ak neexistuje, vrat NULL. */
    node * v = root;
    while (v != NULL && v->data != item) {
        if (item < v->data) {
            v = v->left;
        } else {
            v = v->right;
        }
    }
    return v;
}

/* rekurzivna verzia */
node * findNodeR(node *root, dataType item) {
    /* V binarnom vyhladavacom strome s korenom root najdi a vrat
     * vrchol s hodnotou item a ak neexistuje, vrat NULL. */
    if (root == NULL || root->data == item) {  
        return root;
    } else if (item < root->data) {
        return findNodeR(root->left, item);
    } else {
        return findNodeR(root->right, item);
    }
}

/* funkcia, ktora zisti, ci sa v strome t nachadza vrchol
 * s hodnotou item */
bool find(binarySearchTree &t, dataType item) {
  return findNodeR(t.root, item) != NULL;
}

node * createLeaf(dataType item, node * parent) {
    /* vytvor novy vrchol s danym kľúčom, obe deti nastav na NULL */
    node *v = new node;
    v->data = item;
    v->left = NULL;
    v->right = NULL;
    v->parent = parent;
    return v;
}

node * createLeaf(dataType item, node * parent) {
    /* vytvor novy vrchol s danym kľúčom, obe deti nastav na NULL */
    node *v = new node;
    v->data = item;
    v->left = NULL;
    v->right = NULL;
    v->parent = parent;
    return v;
}

void insert(binarySearchTree &t, dataType item) {
    /* Do stromu t vlozi vrchol s hodnotou item na spravne miesto.
     * Predpokladame, ze takyto kluc este v strome nie je. */
    if (t.root == NULL) {
        /* prazdny strom - treba vytvorit koren */
        t.root = createLeaf(item, NULL);
        return;
    }
    /* Putujeme po strome podobne ako vo find */
    node * v = t.root;
    while (true) {
        assert(v != NULL && v->data != item);
        if (item < v->data) {
            /* item by mal byt vlavo. Ak tam nic nie je, spravime novy list */
            if (v->left == NULL) {
                v->left = createLeaf(item, v);
                break;
            } else {  // hladame vlavo
                v = v->left;
            }
        } else {
            /* item by mal byt pravo. Ak tam nic nie je, spravime novy list */
            if (v->right == NULL) {
                v->right = createLeaf(item, v);
                break;
            } else { // hladame vpravo
                v = v->right;
            }
        }
    }
}

node * insertR(node *v, dataType item) {
    /* Do stromu s korenom vo v vlozi vrchol s hodnotou item na
     * spravne miesto a vrati smernik na koren (tento sa moze zmenit
     * ak strom bol prazdny) */
    if (v == NULL) {   // vkladanie do prazdneho stromu
        return createLeaf(item, NULL);
    } else {
        if (item < v->data) {  // vlozime prvok do laveho podstromu
            v->left = insertR(v->left, item);
            v->left->parent = v;  // pre istotu uloz rodica (ak v->left je novy)
        } else {  // vlozime prvok do praveho podstromu
            assert(item > v->data);
            v->right = insertR(v->right, item);
            v->right->parent = v; // pre istotu uloz rodica (ak v->right je novy)
        }
        return v;
    }
}

void insertR(binarySearchTree &t, dataType item) {
    /* pomocna funkcia, zavola rekurziu pre koren */
    t.root = insertR(t.root, item);
}

node *minimumNode(node *v) {
    /* vrati vrchol s minimalnou hodnotou data vo vyhladavacom strome
     * s korenom v */
    assert(v != NULL);
    while (v->left != NULL) {
        v = v->left;
    }
    return v;
}

node *successorNode(node *v) {
    /* vrati vrchol, ktorý v utriedenom poradi nasleduje za vrcholom v,
     * alebo NULL ak taky vrchol nie je */
    assert(v != NULL);
    if (v->right != NULL) {
        return minimumNode(v->right);
    }
    while (v->parent != NULL && v == v->parent->right) {
        v = v->parent;
    }
    return v->parent;
}

void remove(binarySearchTree &t, dataType item) {
    /* Zmaze hodnotu item zo stromu t.
     * Predpokladame, ze tam taky kluc je. */

    /* Najdi vrchol s klucom */
    node *v = findNode(t.root, item);
    assert(v != NULL && v->data == item);
    /* najdi vrchol rm s jednym synom child,
     * ktory vyhodime. */    
    node *rm, *child;
    if (v->left == NULL || v->right == NULL) rm = v;
    else rm = successorNode(v);
    assert(rm->left == NULL || rm->right == NULL);
    if (rm->left != NULL) child = rm->left;
    else child = rm->right;

    /* preves syna priamo pod otca rm */
    if (child != NULL) child->parent = rm->parent;
    if (rm->parent == NULL) t.root = child;
    else {
        /* ak rm nie je koren, jeho otcovi zaves child */
        node *parent = rm->parent;
        assert(rm == parent->left || rm == parent->right);
        if (rm == parent->left) parent->left = child;
        else parent->right = child;
    }
    /* ak rm nema mazanu hodnotu data, prekopiruj data z rm do v*/
    if (rm != v) {
        v->data = rm->data;
    }
    delete rm;
}

void inorder(node *v) {
    if (v == NULL) return;
    inorder(v->left);
    printf(" %d",  v->data);
    inorder(v->right);
}

int main() {
    /* vytvorime a inicializujeme strom */
    binarySearchTree t;
    init(t);
    /* vlozime zopar klucov */
    insert(t, 2);
    insert(t, 5);
    insert(t, 3);
    insert(t, 10);
    insert(t, 7);    
    /* skusime hladat */
    if(find(t,4)) printf("Nasiel 4\n");
    if(find(t,3)) printf("Nasiel 3\n");
    /* vypiseme vsetky kluce v inorder poradi
     * mali by byt usporiadane podla velkosti */
    inorder(t.root);
    printf("\n");
    /* pomazeme kluce */
    remove(t, 5);
    remove(t, 3);
    remove(t, 10);
    remove(t, 7);
    remove(t, 2);
}

Prednáška 21

Opakovanie: Binárne stromy

  • V každom vrchole máme uložené nejaké dáta a smerník na ľavého a pravého syna.
struct node {
    /* vrchol stromu  */
    dataType data;
    node * left;  /* lavy syn */
    node * right; /* pravy syn */
};
  • Príklad využitia: reprezentácia aritmetických výrazov, binárne vyhľadávacie stromy

Cvičenie: Hľadanie prvku v strome

  • Predstavme si, že máme binárny strom, ktorý má vo vrcholoch ľubovoľné hodnoty, nemusí ísť o binárny vyhľadávací strom
  • Chceme zistiť, či sa v strome nachádza určitá hodnota, alebo spočítať, koľkokrát sa tam nachádza.
  • Môže byť hocikde, preto musíme prejsť rekurzívne všetky vrcholy stromu
  • Funkcia nižšie vracia počet výskytov hodnoty x, pričom výslednú hodnotu spočíta z výsledkov rekurzívnych volaní pre podstromy.
int count(node *v, dataType x) {
    /* Vrat pocet vyskytov hodnoty x v strome s korenom v. */
    if (v == NULL) return 0;
    int add = 0;
    if (v->data == x) add = 1;
    return count(v->left, x) + count(v->right, x) + add;
}

Binárne vyhľadávacie stromy

P22-BST.png
  • Binárny strom, každý vrchol 0,1 alebo 2 deti
  • V každom vrchole máme položku s dátami (typu dataType)
  • Pre každý vrchol v stromu platí:
    • Každý vrchol v ľavom podstrome v má hodnotu data menšiu ako vrchol v
    • Každý vrchol v pravom podstrome v má hodnotu data väčšiu ako vrchol v
struct node {
    /* vrchol binarneho vyhladavacieho stromu  */
    dataType data; /* hodnota */
    node * parent;  /* otec vrchola */
    node * left;    /* lavy syn */
    node * right;   /* pravy syn */
};

struct binarySearchTree {
    node *root;  /* koren stromu, NULL pre prazdny strom */
};

void init(binarySearchTree &t) {
    /* inicializuje prazdny binarny vyhladavaci strom */
    t.root = NULL;
}

Hľadanie vo vyhľadávacom strome

  • Porovnáme hľadané dáta s dátami v koreni
    • Ak sa rovnajú, končíme (našli sme, čo hľadáme)
    • Ak je hľadaná hodnota menšia ako dáta v koreni, musí byť v ľavom podstrome, ak je väčšia v pravom
  • V príslušnom podstrome sa rozhodujeme podľa tých istých pravidiel
  • Keď narazíme na prázdny podstrom, dáta sa v strome nenachádzajú
  • Dá sa zapísať rekurzívne alebo cyklom, lebo vždy ideme iba do jedného podstromu
node * findNode(node *root, dataType item) {
    /* V binarnom vyhladavacom strome s korenom root najdi a vrat
     * vrchol s hodnotou item a ak neexistuje, vrat NULL. */
    node * v = root;
    while (v != NULL && v->data != item) {
        if (item < v->data) {
            v = v->left;
        } else {
            v = v->right;
        }
    }
    return v;
}

Vkladanie prvku do vyhľadávacieho stromu

  • Predpokladáme, že prvok v strome nie je.
  • Putujeme po strome podobne ako pri vyhľadávaní prvku, až kým nenarazíme na nulový smerník.
    • Na tomto mieste by mal byť nový prvok, takže ho tam pridáme ako nový list

Minimum a nasledovník

  • Dve funkcie, ktoré sa nám zídu pri mazaní prvku, ale môžu sa zísť aj inokedy.

Prvá funkcia nájde vo vyhľadávacom strome minimum.

  • Všetky prvky menšie ako koreň sú v ľavom podstrome, bude tam zrejme aj minimum.
  • Tá istá úvaha platí pre koreň ľavého podstromu.
  • Ideme teda doľava kým sa dá, posledný vrchol vrátime (list alebo vrchol s pravým synom).

Druhá funkcia nájde vrchol, ktorý v utriedenom poradí nasleduje za daným vrcholom v.

  • Ak v má pravého syna, nasledovník bude v pravom podstrome, konkrétne vrchol s minimálnou hodnotou data v tomto podstrome
  • V opačnom prípade to môže byť rodič, ak v je jeho ľavý syn
  • Ak je pravý syn, môže to byť prarodič, ak je rodič jeho ľavý syn, atď
  • Nájdeme teda prvého predka, do ktorého ľavého podstromu patrí v a ten je hľadaný nasledovník

Mazanie prvkov z vyhľadávacieho stromu

  • Nájdeme mazaný vrchol v podľa hodnoty obvyklým spôsobom
  • Ak je v list, jednoducho ho zmažeme
  • Ak má v jedno dieťa, toto dieťa prevesíme priamo pod otca v a v zmažeme
  • Ak má v dve deti, nájdeme nasledovníka v, t.j. minimum v pravom podstrome v.
  • Tento nasledovník nemá ľavé dieťa, vieme ho teda zmazať.
  • Jeho údaje presunieme do vrcholu v.
  • Tiež treba dať pozor na mazanie koreňa.
void remove(binarySearchTree &t, dataType item) {
    /* Zmaze hodnotu item zo stromu t.
     * Predpokladame, ze tam taky kluc je. */

    /* Najdi vrchol s klucom */
    node *v = findNode(t.root, item);
    assert(v != NULL && v->data == item);
    /* najdi vrchol rm s jednym synom child,
     * ktory vyhodime. */    
    node *rm, *child;
    if (v->left == NULL || v->right == NULL) rm = v;
    else rm = successorNode(v);
    assert(rm->left == NULL || rm->right == NULL);
    if (rm->left != NULL) child = rm->left;
    else child = rm->right;

    /* preves syna priamo pod otca rm */
    if (child != NULL) child->parent = rm->parent;
    if (rm->parent == NULL) t.root = child;
    else {
        /* ak rm nie je koren, jeho otcovi zaves child */
        node *parent = rm->parent;
        assert(rm == parent->left || rm == parent->right);
        if (rm == parent->left) parent->left = child;
        else parent->right = child;
    }
    /* ak rm nema mazanu hodnotu data, prekopiruj data z rm do v*/
    if (rm != v) {
        v->data = rm->data;
    }
    delete rm;
}

Lexikografické stromy (trie)

Vhodná štruktúra na ukladanie reťazcov. Ide o strom, ktorý ale nie je binárny.

  • Počet detí v každom vrchole je rovný veľkosti abecedy, každý je označený iným písmenom.
  • Koreň stromu zodpovedá prázdnemu reťazcu
  • Vrchol v hĺbke k zodpovedá reťazcu dĺžky k, ktorý dostaneme prečítaním písmen na ceste z koreňa.

Trie.jpg

Vrchol lexikografického stromu obsahuje:

  • dáta - písmeno, na ktoré sme sa do vrcholu dostali
  • pole smerníkov na deti
  • určenie, či vo vrchole končí nejaké slovo z množiny, prípadne prídavné informácie o tom slove

Vrchol je teda nasledovného tvaru:

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    
};

Alebo môžeme pole smerníkov alokovať vždy, keď vznikne vrchol. Potom definícia nebude závislá od veľkosti abecedy:

struct node {
    /* vrchol lexikografickeho stromu  */
    char data;   // pismeno ulozene v tomto vrchole
    bool isWord; // je tento vrchol koncom slova?
    node** next; // pole smernikov na deti    
};

Samotný strom je iba smerník na koreň:

struct trie {  
    node* root;
}

Inicializácia a uvoľňovanie stromu

Pri práci s lexikografickým stromom začíname s koreňom, ktorý má v poli next iba smerníky s hodnotou NULL. Koreň je špeciálny vrchol, ktorý neobsahuje žiadne dáta (jeho dáta nastavíme na 0).

void init(trie &t) {
    /* inicializuj prazdny lexikograficky strom, ktory bude mat iba koren */
    t.root = createNode('\0'); // koren si oznacime specialnym znakom 0
}

Funkcia createNode pracuje nasledovne:

  • Alokuje pamäť pre nový vrchol.
  • Alokuje pole smerníkov na deti (ak nemá statickú veľkosť v definícii node).
  • Inicializujeme nasledovníkov na NULL.
  • Nastavíme hodnoty data a isWord
node* createNode(char data) {
    /* vytvor vrchol stromu s danymi datami */
    node* v = new node;            // novy vrchol 
    v->next = new node*[Abeceda];  // alokuj pole deti
    for (int i = 0; i < Abeceda; i++) v->next[i] = NULL; // inicializacia deti
    v->isWord = false;
    v->data = data;
    return v;
}

Uvoľňovanie pamäti stromu je pomocou jednoduchej rekurzívnej funkcie destroySubtree(node*), pričom na uvoľnenie celého stromu ju potrebujeme zavolať pre koreň

void destroySubtree(node* v) {
    /* rekurzivne uvolni pamat pre podstrom lexikografickleho stromu
     * s korenom vo vrchole v */
    if (v == NULL) return;
    for (int i = 0; i < Abeceda; i++) destroySubtree(v->next[i]);
    delete[] v->next; // táto časť by nebola potrebná, ak by sme mali next statické
    delete v;
}

void destroy(trie &t) {
    /* uvolni pamat alokovanu pre lexikograficky strom t */
    destroySubtree(t.root);
}

Vkladanie do stromu

Do lexikografického stromu vkladáme reťazec zložený iba z písmen abecedy (pre jednoduchosť pracujeme s abecedou 'A'..'Z' resp. s jej začiatkom veľkosti Abeceda).

Postupujeme v cykle po jednotlivých písmenách vkladaného slova:

  • Začneme v koreni stromu.
  • Ak aktuálny vrchol nemá dieťa pre aktuálne písmeno slova, vytvoríme ho.
  • Potom sa posunieme do dieťaťa pre aktuálne písmeno, už máme zaručené, že existuje.
  • Keď prídeme na koniec slova, poznačíme si, že daný vrchol zodpovedá slovu.
void insert(trie &t, const char* word) {
    /* do lexikografickeho stromu t pridaj slovo word */
    node *v = t.root;
    for (int i = 0; word[i] != 0; i++) { // posuvame sa od korena na spravne miesto
        int c = word[i] - 'A';
        if (v->next[c] == NULL) {  // ak vrchol chyba, spravime novy
            v->next[c] = createNode(word[i]);
        }
        v = v->next[c];
    }

    /* Aktualny vrchol v je koncom slova */
    v->isWord = true;
}

Hľadanie v lexikografickom strome

Vyhľadávanie v strome opäť postupuje po písmenách vyhľadávaného slova. Kým nedôjde na koniec slova, snaží sa ísť po hranách, ktoré zodpovedajú písmenám. V prípade, že na niektorom mieste chýba patričná hrana, znamená to, že takéto slovo sa v strome nenachádza.

bool find(trie& t, const char* word) {
    /* zisti, ci v lexikografickom strome t je slovo word */
    node *v = t.root;
    for (int i = 0; word[i] != 0; i++) {
        int c = word[i] - 'A';
        if (v->next[c] == NULL) return false;
        v = v->next[c];
    }
    return true;
}

Táto funkcia však nefunguje správne.

  • Ak by sme do stromu vložili iba slovo "AHOJ" a hľadali napr. slovo "AH", funkcia nám vráti true, aj keď slovo nie je v množine.
  • Po prečítaní slova teda ešte potrebujeme skontrolovať, či v aktuálnom vrchole nejaké slovo z množiny naozaj končí
bool find(trie& t, const char* word) {
    /* zisti, ci v lexikografickom strome t je slovo word */
    node *v = t.root;
    for (int i = 0; word[i] != 0; i++) {
        int c = word[i] - 'A';
        if (v->next[c] == NULL) return false;
        v = v->next[c];
    }
    return v->isWord;
}

Vymazávanie z lexikografického stromu

Pri vymazávaní slova potrebujeme:

  • nájsť vrchol pre dané slovo a nastaviť mu isWord na false (predpokladáme, že slovo v strome bolo)
  • uvoľniť nepotrebné vrcholy

Najskôr pre jednoduchosť uvoľňovanie vrcholov vynechajme:

void deleteTrie(trie* t, const char* word){
    /* z lexikografického stromu t zmaže slovo word */
    node *v = t.root;
    for (int i = 0; word[i] != 0; i++) {
        int c = word[i] - 'A';
        assert(v->next[c] != NULL);
        v = v->next[c];
    }
    assert(v->isWord);
    v->isWord=false;
}    

Vrchol, ktorému sme nastavili isWord na false, môže (ale nemusí) byť nepotrebný

  • pozná sa to podľa toho, či má nejaké deti
  • ak nemá deti, môžeme ho zmazať
  • tým sa môže stať nepotrebný aj jeho rodič a ďalší predkovia

Použijeme rekurziu, kde najskôr vymažeme slovo z určitého podstromu a ak sa tým stal jeho rodič zbytočný, zmažeme aj jeho.

void remove(trie& t, const char* word) {
    /* z lexikografickeho stromu t zmaze slovo word */
    removeFromSubtree(t.root, word, 0);
}

bool removeFromSubtree(node *v, const char *word, int depth) {
    /* Z podstromu s korenom v zmaz slovo word, pricom aktualna hlbka je depth.
     * Vrati true, ak sme zmazali vrchol v, false inak. */
    if (word[depth] == '\0') {  // ak sme na konci slova, odznacime vrchol
        assert(v->isWord);
        v->isWord = false;
    } else {          // ak nie sme na konci slova, zmazeme ho z prislusneho podstromu
        int c = word[depth] - 'A';
        assert(v->next[c] != NULL);
        bool deleted = removeFromSubtree(v->next[c], word, depth + 1);
        if (deleted) {  // ak sme zmazali dieta, poznacime v poli next
            v->next[c] = NULL;
        }
    }
    // zistime, kolko deti ma v
    int numChildren = 0;
    for (int i = 0; i < Abeceda; i++) {
        if (v->next[i] != NULL) numChildren++;
    }
    // ak v nema deti, nie je oznaceny ako slovo a nie je koren, zmazeme ho
    if (numChildren == 0 && !v->isWord && v->data != '\0') {
        delete[] v->next;
        delete v;
        return true;
    }
    return false; 
}

Čo sa stane, ak takto napísanú funkciu spustíme na strome, v ktorom máme iba slovo "AHOJ" a skúsime z neho vymazať slovo "A"?

Vypisovanie stromu

Podobne ako pri binárnych stromoch aj lexikografický strom môžeme vypisovať napríklad mierne modifikovanou funkciou preorder

  • Aby sa aspoň trochu dalo sledovať, ako to so stromom vyzerá, pridali sme nejaké zátvorky (nie že by veľmi pomohli).
void preorder(node* v){
    if (v==NULL) { return; }
    if(v->data) {  // ak nie sme v koreni
       cout << v->data;
       if(v->isWord) { cout << "*"; }
    }

    cout << "[";
    for (int i=0; i<Abeceda; i++){
        preorder(v->next[i]);
    }
    cout <<"] ";
}

Čo funkcia vypíše pre strom so slovami A, ARE, AS, DO, DOT?

Vypisovanie slov v strome

Oveľa viac by nám pomohlo vypísať všetky slová, ktoré sú v množine.

Funkcia na vypisovanie všetkých slov opäť prejde celý strom rekurzívne

  • Ak je vo vrchole v koniec nejakého slova, potrebujeme ho vypísať
  • Aby sme vedeli, ako to slovo vyzerá, pošleme si ho v pomocnom poli ako parameter word
  • V tomto poli máme vždy uložené znaky na ceste od koreňa k aktuálnemu vrcholu
void printSubtree(int depth, node *v, char* word) {
    /* rekurzívne vypíše všetky slová v podstrome s koreňom v,
     * pričom vrchol v má hĺbku depth 
     * a zodpoveda reťazcu word (nemusí byť ukončený 0)  */
    if (v->isWord) {
        word[depth] = '\0';
        cout << word << endl;
    }
    for (int i = 0; i < Abeceda; i++) {
        if (v->next[i] != NULL) {
            word[depth] = (v->next[i])->data; // pridám si ďalšie písmeno
            printSubtree(depth + 1, v->next[i], word); // zavolám sa rekurzívne
        }
    }
}

Volanie tejto rekurzívnej funkcie je s použitím pomocného poľa, ktorého dĺžku by sme mohli nastaviť ako dĺžku najdlhšieho slova v strome.

void printTrie(trie &t) {
    /* vypise vsetky slova v lexikografickom strome t */
    char *word = new char[maxLen+1];
    printSubtree(0, t.root, word);
    delete[] word;
}

V akom poradí vypíše funkcia slová?

Príklad použitia lexikografického stromu

Na vstupe máme dlhý text a chceme zistiť, koľkokrát sa v ňom nachádzajú jednotlivé slová.

  • Do uzla pridáme počítadlo výskytov
  • Počet výskytov slova môže nahradiť aj položku isWord, lebo vrchol zodpovedá koncu slova, ak je počet jeho výskytov väčší ako 0.
struct node {
    /* vrchol lexikografickeho stromu  */
    char data; // pismeno ulozene v tomto vrchole
    int count;    //počet výskytov slova
    node** next; // pole smernikov na deti    
};

Funkcia increment dostane slovo. Ak toto slovo ešte nie je v strome, tak ho vloží s počtom jedna a ak tam už je, zvýši mu počítadlo.

void increment(trie &t, const char* word) {
    /* v lexikografickom strome t zvys pocitadlo slovu word, ak tam este nie je, pridaj ho */
    node *v = t.root;
    for (int i = 0; word[i] != 0; i++) { // posuvame sa od korena na spravne miesto
        int c = word[i] - 'A';
        if (v->next[c] == NULL) {  // ak vrchol chyba, spravime novy
            v->next[c] = createNode(word[i]);
        }
        v = v->next[c];
    }

    /* Aktualny vrchol v je koncom slova */
    v->count++;
}
  • Čo treba zmeniť vo funkcii createNode?
  • Pri výpise potom vypisujeme nielen slová, ale aj ich počty výskytov.
    • Čo ak by sme chceli vypísať iba slovo s najväčším počtom výskytov?
    • Čo ak by sme chceli vypísať slová v poradí od najčastejšieho po najmenej časté?

Zdrojový kód programu s lexikografickým stromom

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

const int Abeceda = 26;  /* velkost abecedy, znaky cislujeme 0..Abeceda-1 */
const int maxLen = 100;  /* maximalna dlzka slova v strome */

struct node {
    /* vrchol lexikografickeho stromu  */
    char data; // pismeno ulozene v tomto vrchole
    bool isWord; // je tento vrchol koncom slova?
    node** next; // pole smernikov na deti
};

struct trie {
    /* samotny lexikograficky strom si pamata smernik na koren */
    node* root;
};

node* createNode(char data) {
    /* vytvor vrchol stromu s danymi datami */
    node* v = new node;            // novy vrchol
    v->next = new node*[Abeceda];  // alokuj pole deti
    for (int i = 0; i < Abeceda; i++) v->next[i] = NULL; // inicializacia deti
    v->isWord = false;
    v->data = data;
    return v;
}

void init(trie &t) {
    /* inicializuj prazdny lexikograficky strom, ktory bude mat iba koren */
    t.root = createNode('\0'); // koren si oznacime specialnym znakom 0
}

void destroySubtree(node* v) {
    /* rekurzivne uvolni pamat pre podstrom lexikografickeho stromu
     * s korenom vo vrchole v */
    if (v == NULL) return;
    for (int i = 0; i < Abeceda; i++) destroySubtree(v->next[i]);
    delete[] v->next;
    delete v;
}

void destroy(trie &t) {
    /* uvolni pamat alokovanu pre lexikograficky strom t */
    destroySubtree(t.root);
}

void insert(trie &t, const char* word) {
    /* do lexikografixkeho stromu t pridaj slovo word */
    node *v = t.root;
    for (int i = 0; word[i] != 0; i++) { // posuvame sa od korena na spravne miesto
        int c = word[i] - 'A';
        if (v->next[c] == NULL) {  // ak vrchol chyba, spravime novy
            v->next[c] = createNode(word[i]);
        }
        v = v->next[c];
    }

    /* Aktualny vrchol v je koncom slova */
    v->isWord = true;
}

bool find(trie& t, const char* word) {
    /* zisti, ci v lexikografickom strome t je slovo word */
    node *v = t.root;
    for (int i = 0; word[i] != 0; i++) {
        int c = word[i] - 'A';
        if (v->next[c] == NULL) return false;
        v = v->next[c];
    }
    return v->isWord;
}

bool removeFromSubtree(node *v, const char *word, int depth) {
    /* Z podstromu s korenom v zmaz slovo word, pricom aktualna hlbka je depth.
     * Vrati true, ak sme zmazali vrchol v, false inak. */
    if (word[depth] == '\0') {  // ak sme na konci slova, odznacime vrchol
        assert(v->isWord);
        v->isWord = false;
    } else {          // ak nie sme na konci slova, zmazeme ho z prislusneho podstromu
        int c = word[depth] - 'A';
        assert(v->next[c] != NULL);
        bool deleted = removeFromSubtree(v->next[c], word, depth + 1);
        if (deleted) {  // ak sme zmazali dieta, poznacime v poli next
            v->next[c] = NULL;
        }
    }
    // zistime, kolko deti ma v
    int numChildren = 0;
    for (int i = 0; i < Abeceda; i++) {
        if (v->next[i] != NULL) numChildren++;
    }
    // ak v nema deti, nie je oznaceny ako slovo a nie je koren, zmazeme ho
    if (numChildren == 0 && !v->isWord && v->data != '\0') {
        delete[] v->next;
        delete v;
        return true;
    }
    return false;
}

void remove(trie& t, const char* word) {
    /* z lexikografickeho stromu t zmaze slovo word */
    removeFromSubtree(t.root, word, 0);
}

void printSubtree(int depth, node *v, char* word) {
    /* rekurzívne vypíše všetky slová v podstrome s koreňom v,
     * pričom vrchol v má hĺbku depth
     * a zodpoveda reťazcu word (nemusí byť ukončený 0)  */
    if (v->isWord) {
        word[depth] = '\0';
        cout << word << endl;
    }
    for (int i = 0; i < Abeceda; i++) {
        if (v->next[i] != NULL) {
            word[depth] = (v->next[i])->data; // pridám si ďalšie písmeno
            printSubtree(depth + 1, v->next[i], word); // zavolám sa rekurzívne
        }
    }
}

void printTrie(trie &t) {
    /* vypise vsetky slova v lexikografickom strome t */
    char *word = new char[maxLen + 1];
    printSubtree(0, t.root, word);
    delete[] word;
}


void preorder(node* v) {
    if (v == NULL) {
        return;
    }
    if (v->data) { // ak nie sme v koreni
        cout << v->data;
        if (v->isWord) {
            cout << "*";
        }
    }

    cout << "[";
    for (int i = 0; i < Abeceda; i++) {
        preorder(v->next[i]);
    }
    cout << "] ";
}

int main(void) {
    trie t;
    init(t);
    insert(t, "ARE");
    insert(t, "AS");
    insert(t, "A");
    insert(t, "DO");
    insert(t, "DOT");
    if (find(t, "ARE")) {
        cout << "Nasiel ARE" << endl;
    }
    if (find(t, "AR")) {
        cout << "Nasiel AR" << endl;
    }
    if (find(t, "AREA")) {
        cout << "Nasiel AREA" << endl;
    }
    preorder(t.root);
    cout << endl;
    printTrie(t);
    remove(t, "ARE");
    destroy(t);
}

Prednáška 22

Oznamy

  • Na štvrtom teste budú aj príklady týkajúce sa dátových štruktúr z prednášok. Viac na zvláštnej stránke
  • Budúci týždeň:
    • Pondelok prednáška: detaily skúšky, opakovanie, vaše otázky, precvičovanie učiva
      • Silne odporúčame prísť aspoň na začiatok prednášky, kedy prediskutujeme priebeh skúšky
      • Prineste si termíny ostatných skúšok, doriešime ešte 1. opravný termín a opravné písomky
    • Pondelok na začiatku prednášky pribudnú na testovači trénovacie príklady na skúšku. Za niektoré je do piatku večera možné získať bonusové body ako z cvičení, ale nebodované sa odovzdať dajú aj neskôr, ako tréning pred skúškou
    • Pondelok doplnkové cvičenia: ako obvykle, vrátane bonusovej rozcvičky
    • Utorok cvičenia: nebudú ďalšie príklady, odporúčame riešiť trénovacie príklady na skúšku, prípadne sa spýtať otázky k učivu
    • Streda prednáška: nepreberané črty C resp. C++. Nebudeme skúšať, ale môžu sa zísť.
    • Streda písomka 18:15 v posluchárni B
  • Pokúsime sa čím skôr opraviť všetky zvyšné odovzdané úlohy. Prosím skontrolujte si koncom budúceho týždňa body na testovači. Prípadné reklamácie pošlite na prog@... čím skôr, najneskôr však do 4.1. Na skúške už reklamácie bodov z cvičení a domácich úloh nebudeme riešiť.

Abstraktný dátový typ (ADT)

  • Určíme, aké operácie by mala dátová štruktúra spĺňať (hlavičky funkcií), nestaráme sa o implementáciu
  • Už sme videli nejaké ADT, napr. vektor, zásobník, rad
  • Pre jeden ADT môže byť viacero implementácií, napr. pomocou triedeného poľa, netriedeného poľa, binárneho vyhľadávacie stromu atď
  • Program, ktorý ADT používa, netreba meniť kvôli zmene implementácie (alebo len veľmi málo, napr. pridanie lepšej funkcie)
  • Budúci semester si ukážeme ako ADT krajšie implementovať pomocou objektovo-orientovaného programovania a generického programovania
    • Je to však len zmena syntaxe (hlavičky funkcií, definície typov a pod.), algoritmy a dátové štruktúry (zoznamy, polia) zostávajú tie isté

ADT Množina

  • Binárne vyhľadávacie stromy sa dajú použiť napr. na reprezentáciu nejakej množiny hodnôt
    • Napr. čítačka kariet na dverách H6 si pamätá čísla kariet študentov a učiteľov na FMFI UK
  • Zadefinujeme si množinu (set) ako abstraktný dátový typ (ADT) s nasledovnými tromi operáciami:
/* inicializuje prázdnu množinu */
void init(set &s);

/* pridá prvok item do množiny */
void insert(set &s, dataType item); 

/* zistí, či sa v množine nachádza prvok item */
bool find(set &s, dataType item);
  • Pozor, v množine sa prvky neopakujú, preto insert by sme mali volať iba vtedy, ake prvok ešte v množine nie je.
  • Ako dataType môžeme využiť typy premenných, kde vieme testovať rovnosť pomocou operátora ==, prípadne ich aj porovnávať pomocou < (napr. int, double, char).

Mohli by sme pridať aj ďalšie operácie, napríklad

  • isEmpty, ktorá nám povie, či je množina prázdna,
  • remove, ktorá odstráni prvok z množiny,
  • destroy, ktorá uvoľní pamäť, ktorú dátová štruktúra zaberá,
  • print, ktorá vypíše všetky prvky množiny.

Pre binárne vyhľadávacie stromy sme už funkcie init, insert a find implementovali, plus nejaké ďalšie, napr. remove.

Príklad použitia ADT množina

  • Na vstupe máme postupnosť celých čísel, na výstupe chceme vypísať iba prvý výskyt každého čísla (vynechať duplikáty)
  • Predstavme si, že máme k dispozícii implementáciu ADT množina
  • V množine si držíme doteraz načítané čísla
  • Ak sa práve načítané číslo v množine nachádza, nepotrebujeme ho pridávať ani vypisovať
typedef int dataType;

void removeDuplicates() {
    set s;
    init(s);
    while (!feof(stdin)) {
        dataType x;
        scanf("%d ", &x);
        if (!find(s, x)) { 
           insert(s, x);
           printf("%d\n", x);
        }
    }
}

Implementácia ADT množina pomocou neutriedeného poľa

  • Okrem binárnych vyhľadávacích stromov m;6eme ADT set reprezentovať aj jednoduchšie, napr. pomocou poľa
  • Definícia by vyzerala nasledovne:
struct set {
    dataType* a;
    int count;
};
  • Pri inicializácii potrebujeme alokovať pole a.
  • Počet prvkov množiny nastavíme na 0.
/* inicializuje prázdnu množinu */
void init(set &s){
    s.a = new dataType[Nmax];
    s.count = 0;
}
  • Pre vyhľadanie prvku potrebujeme prejsť všetky prvky a zistiť, či nie je s niektorým z nich rovný (preto potrebujeme dataType, ktorý vieme porovnávať pomocou ==).
/* zistí, či sa v množine nachádza prvok item */
bool find(set s, dataType item){
    for (int i=0; i<s.count; i++){
        if (s.a[i]==item) return true;
    }
    return false;
}
  • V prípade, že do poľa sa rozhodneme ukladať prvky v neutriedenom poradí, môžeme pridávanie spraviť jednoducho umiestnením nového prvku na koniec.
    • Namiesto pevnej veľkosti poľa by sme mohli použiť rastúci vektor
    • Nasledujúca funkcia predpokladá, že item ešte v množine nie je. Ako by sme to skontrolovali a čo by mohla funkcia robiť, ak item už v množine je?
/* prida prvok item do množiny */
void insert(set &s, dataType item){
    s.a[s.count] = item;
    s.count++;
}

Implementácia ADT množina pomocou utriedeného poľa

Pri tejto implementácii použijeme tú istú definíciu typu set a tú istú funkciu init

  • Funkcia insert zabezpečí, že prvky v poli a sú usporiadané od najmenšieho po najväčší
  • Funkcia find môže použiť binárneho vyhľadávanie v usporiadanom poli, čo je oveľa rýchlejšie (viď prednáška 7)
/* Binárne vyhľadávanie prvku item v poli a (od indexu l po index r)*/
bool binSearch(dataType *a, dataType item, int l, int r) {
    if (l>r) return false; 
    int m = (l+r)/2;
    if (a[m] == item) return true;
    if (a[m] > item)  return binSearch(a, item, l, m-1);
    else return binSearch(a, item, m+1, r);
}

/* zistí, či sa v množine nachádza prvok item */
bool find(set s, dataType item){
    return binSearch(s.a, item, 0, s.count-1);
}

Pri vkladaní do utriedeného poľa potrebujeme nájsť miesto, kam prvok vlastne máme vložiť.

  • Ak patrí na miesto index, potom všetky prvky na indexoch index..count-1 potrebujeme posunúť dozadu.
  • Správne miesto by sme mohli hľadať binárnym vyhľadávaním, ale my jednoducho budeme naraz od konca posúvať a porovnávať, ako pri triedení vkladaním
/* prida prvok item do množiny */
void insert(set &s, dataType item){
    int index = s.count;
    while (index > 0 && s.a[index - 1] > prvok) {
        s.a[index] = s.a[index - 1];
        index--;
    }
    s.a[index] = item;
    s.count++;
}

Cvičenie:

  • ako by sme v tejto funkcii overili, či sa prvok item už v poli nachádza?

Implementácia ADT množina pomocou spájaného zoznamu

ADT množina môžeme implementovať aj pomocou spájaného zoznamu.

  • V spájanom zozname binárne vyhľadávanie nefunguje a preto nemá zmysel udržiavať ho usporiadaný.
  • Vkladať môžeme na začiatok zoznamu.
  • Pri vyhľadávaní prvku prejdeme celý spájaný zoznam.
  • Všetky funkcie sú vysvetlené a implementované v prednáške 16.

Časová zložitosť jednotlivých operácií

Pre implementácie pomocou poľa a zoznamu vyzerajú zložitosti takto:

FindInsert
Usporiadané poleO(log n)O(n)
Neusporiadané poleO(n) O(1)
Neusporiadaný spájaný zoznamO(n)O(1)
  • Ak veľa vkladáme a málo hľadáme, je dobré použiť zoznam alebo neutriedené pole.
  • Ak veľa hľadáme a málo vkladáme, je dobré použiť utriedené pole.
  • Ak však potrebujeme približne n vkladaní aj hľadaní, všetky tri implementácie pobežia v čase 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)}
  • Ako je to s binárnymi vyhľadávacími stromami?

Prehľad zložitostí

Find
najhorší prípad
Insert
najhorší prípad
Find
priem. prípad
Insert
priem. prípad
Usporiadané poleO(log n)O(n)O(log n)O(n)
Neusp. poleO(n)O(1)O(n)O(1)
Neusp. zoznamO(n)O(1)O(n)O(1)
Bin. vyhľ. stromO(n)O(n)O(log n)O(log n)

Ak teda robíme zhruba n krát insert a n krát find a prvky sú vkladané v náhodnom poradí (nie napr. utriedené!), tak pole a zoznam budú spolu trvať O(n2) a binárny vyhľadávací strom bude trvať v priemere O(n log n). Pre veľké n je to značná časová úspora.

Na druháckom predmete Algoritmy a dátové štruktúry si ukážete obmeny vyhľadávacích stromov, ktoré majú zložitosť O(log n) aj v najhoršom prípade


Cvičenie

  • Ako by vyzerali zložitosti funkcie remove?
  • Napíšte funkciu, ktorá dostane utriedené pole čísel a vytvorí z nich binárny vyhľadávací strom (priamo, bez použitia funkcie insert). Pokúste sa strom vytvoriť tak, aby ľavý a pravý podstrom každého vrcholu obsahovali približne rovnako veľa prvkov.
    • Doporučujeme postupovať rekurzívne, pričom napíšte funkciu s hlavičkou napr. node * vytvorStrom(int a[], int l, int r), ktorá vytvorí strom pre časť poľa a od pozície l po pozíciu r.

Hešovacie tabuľky

Predpokladajme, že chceme implementovať jednoduchú množinu (set) celých čísel.

  • Hlavné operácie insert, find, prípadne delete
  • Zatiaľ sme videli: utriedené pole, neutriedené pole, spájaný zoznam, binárny vyhľadávací strom (keďže nejde o reťazce, lexikografické stromy sa príliš nehodia)

Priame adresovanie

  • Videli sme však ešte jeden prístup: 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 adressing)
  • insert, delete aj find v čase O(1)
struct set {
   int m;
   bool *a;
}

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

void insert(set &s, int k) {
  assert(0<=k && k<s.m);
  s.a[k] = true;
}

bool find(set &s, int k) {
  assert(0<=k && k<s.m);
  return s.a[k];
}

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).
  • Často sa používa funkcia h(k) = k 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 k (funkcia abs z knižnice cstdlib)
struct set {
   int m;
   int *data;  // data je pole dlzky m
}

int hash(int k, int m){
    return abs(k) % m;
}

Vkladanie

  • spočítame miesto hash(k) a prvok tam vložíme
void insert(set &s, int k) {
    int index = hash(k, s.m);
    s.data[index]=k;
}

Vyhľadávanie

  • ak prvok s kľúčom k je v tabuľke, musí byť na mieste hash(k).
  • skontrolujeme toto miesto a ak tam je niečo iné ako k, k sa v tabuľke nenachádza
bool find(set &s, int k){
    int index = hash(k, s.m);
    return s.data[index]==k;
}

Problémy:

  • na akú hodnotu inicializovať pole s.data?
  • č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
    • buď postupným prezeraním nasledovných prvkov
    • alebo s pozeraním prvkov s nejakým daným krokom
    • 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 hashovacej tabuľky spájaný zoznam s prvkami, ktoré hešovacia funkcia priradila na toto políčko.

struct node {
    int item;
    node* next;
};

struct set {
    node** data;
    int m;
};


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(k,s.m), kde k je hľadaný kľúč a s.m je veľkosť tabuľky s.
bool find(set &s, int k){
    int index = hash(k, s.m);
    node* v = s.data[index];
    while(v!=NULL){
        if(v->item==k) return true;
        v = v->next;
    }
    return false;
}

Pri vkladaní stačí nový prvok pridať na začiatok spájaného zoznamu na správnom mieste tabuľky.

  • Prečo vkladáme na začiatok a nie na koniec?
  • Ak by sme chceli kontrolovať, či tam vkladaný prvok už náhodou nie je, museli by sme najskôr prejsť a skontrolovať celý spájaný zoznam
void insert(set &s, int k) {
    int index = hash(k, s.m);
    node* temp = new node;
    temp->item = k;
    temp->next = s.data[index];
    s.data[index] = temp;
}

Ďalšie funkcie

  • Vymazávanie prebieha podobne ako všetky ostatné operácie 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 |k| 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 insert O(1) a find O(n), kde n je počet prvkov množiny.
  • Ak máme štastie a v každom políčku máme len niekoľko málo (konštantný počet) prvkov, budeme mať insert aj find 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Š.

ADT Slovník (asociatívne pole)

K prvkom množiny si často chceme uložiť nejakú prídavnú informáciu. Napr. k menu telefónne číslo alebo e-mailovú adresu. Takáto rozšírená štruktúra sa zvykne nazývať slovník alebo asociatívne pole.

  • V slovníku máme záznamy typu itemType
  • Súčasťou itemType je kľúč (key), podľa ktorého chceme v slovníku vyhľadávať, napr. meno osoby
  • Namiesto jedného typu dataType budeme mať itemType (celý záznam) a keyType (iba kľúč)
struct person {
  char * name;
  char * phone;
  char * address;
};
typedef person itemType;
typedef char * keyType;

Základné operácie so slovníkom potom môžu vyzerať napríklad takto:

  • void insert(dictionary &d, itemType *x);
    • do dátovej štruktúry d vloží záznam *x
    • predpokladá, že v štruktúre je dosť miesta a nie je tam záznam s rovnakým kľúčom
  • itemType *find(dictionary &d, keyType k);
    • nájde záznam s kľúčom k a vráti smerník na neho ho. Ak taký kľúč v slovníku nie je, vráti NULL
  • itemType * remove(dictionary &d, keyType k);
    • vymaže záznam s kľúčom k z d a vráti na záznam smerník

Pre naše typy záznamu a kľúča potrebujeme definovať pomocné funkcie, ktoré budú využívané implementáciou insert, find, remove. Napríklad:

keyType key(itemType *x) {
  return x->name;
}
bool equal(keyType x, keyType y) {
  return strcmp(x, y)==0;
} 

Implementácie ADT množina (poľom, zoznamom, binárnym vyhľadávacím stromom, lexikografickým stromom) sa dajú ľahko rozšíriť na implementácie ADT slovník.

Ukážeme si program, ktorý implementuje ADT slovník pomocou hešovacej tabuľky

Slovníkové funkcie

A tu sú hlavičky základných slovníkových funkcií:

  • void insert(dictionary &d, itemType *x);
    • do dátovej štruktúry d vloží záznam *x
    • predpokladá, že v štruktúre je dosť miesta a nie je tam záznam s rovnakým kľúčom
  • itemType *find(dictionary &d, keyType k);
    • nájde záznam s kľúčom k a vráti smerník na neho ho. Ak taký kľúč v slovníku nie je, vráti NULL
  • itemType* remove(dictionary &d, keyType k);
    • vymaže záznam s kľúčom k z d a vráti na záznam smerník

A pomocné funkcie:

  • void init(dictionary &d, int maxN);
    • vytvorí slovník, do ktorého sa zmestí maxN záznamov (niektoré implementácie maxN nepotrebujú, rastú podľa potreby)
  • void destroy(dictionary &d);
    • uvoľní pamäť, ktorú zaberá slovník (neuvoľnuje pamäť záznamov)
  • itemType ** items(dictionary &d, int &n);
    • vráti novo alokované pole so smerníkmi na jednotlivé záznamy, ich počet dá do n

Implementácie vyzerajú podobne ako pre implementáciu množiny, napr. funkcia find:

itemType* find(dictionary &d, keyType k) {
    // nájde záznam s kľúčom k a vráti smerník na neho ho. 
    // Ak taký kľúč v slovníku nie je, vráti NULL 
    int index = hash(k, d.m); // najdi poziciu v tabulke
    node* v = d.data[index]; // prehladaj spajany zoznam
    while (v != NULL) {
        if (equal(key(v->item), k)) { // spravny prvok najdeny
            return v->item;
        }
        v = v->next;
    }
    return NULL; // kluc nebol najdeny v slovniku
}

Využitie na počítanie slov

Chceme použiť (tento) slovník na spočítanie frekvencie výskytu slov vo vstupnom texte

  • toto sme robili aj na prednáške o lexikografických stromoch, tam sme si však spravili štruktúru špecificky na tento účel, tu použijeme všeobecný slovník

Štruktúra programu by celkovo mala obsahovať

  1. definícia typov (keyType, itemType) špecifických pre dané použitie, pomocné funkcie pre tieto typy (napr. key, equal, hash)
  2. implementácie slovníkových funkcií (insert, find, delete,...)
  3. použitie slovníka na daný účel

Druhú časť môžeme hocikedy vymeniť za inú implementáciu slovníka bez zmeny zvyšku programu. Alebo zmenou prvej a tretej časti môžeme využiť ten istý slovník na iný účel.


Najskôr si zadefinujeme typy itemType a keyType, ktoré určujú, aké záznamy a aké kľúče budeme do slovníka ukladať.

struct word {
  char * str;
  int count;
};
typedef word itemType;
typedef char * keyType;

Ďalej potrebujeme funkciu, ktorá nám pre záznam dá kľúč a funkciu, ktorá zistí, či sú dva kľúče rovnaké. Niektoré implementácie slovníka budú potrebovať aj ďalšie podobné funkcie, napr. zistiť, či je jeden kľúč menší ako druhý.

keyType key(itemType *x) {
  return x->str;
}
bool equal(keyType x, keyType y) {
  return strcmp(x, y)==0;
} 
int hash(keyType x, int m) {
    // Funkcia dostane vstupny retazec str a velkost hashovacej tabulky m.
    // Vrati cislo medzi 0 a m-1
    int h = 0;
    char *c = str;
    while (*c) {
        h = (h*256 + (*c))%m;
        c++;
    }
    h = abs(h) % m;
    return h;
}

Samotný program používajúci slovník na počítanie slov môže vyzerať nejako takto:

int main() {
    dictionary d; // vytvorenie slovnika
    init(d, 19);

    char word[maxLen + 1];
    cin >> word;
    while (strcmp(word, "*") != 0) { // nacitavanie slov zo vstupu
        itemType *item = find(d, word); // najdeme zaznam pre slovo
        if (item != NULL) { // ak existuje, zvysime mu pocitadlo
            item->count++;
        } else { // ak zaznam neexistuje, vytvorime novy
            item = new itemType;
            item->str = new char[strlen(word) + 1]; // pamat pre slovo
            strcpy(item->str, word);
            item->count = 1;
            insert(d, item); // a vlozime ho do slovnika
        }
        cin >> word;
    }

    int n; // pocet zaznamov v slovniku
    itemType **a = items(d, n); // vsetky zaznamy v slovniku

    for (int i = 0; i < n; i++) { // vypis vsetky zaznamy
        cout << a[i]->str << " " << a[i]->count << endl;
    }

    for (int i = 0; i < n; i++) { // odalokuj vsetky zaznamy
        delete[] a[i]->str;
        delete a[i];
    }
    delete[] a; // odalokuj aj pole a

    destroy(d); // odalokuj samotny slovnik
}

Prednáška 23

Oznamy

Medzi odovzdanými príkladmi sme našli nápadne podobné, viď komentáre

  • ak sa vás to týka a viete situáciu vysvetliť, príďte na doplnkové cvičenia
  • neopisujte, zapíšte do testovača, s kým ste pracovali
  • za opisovanie (a umožňovanie iným opísať) sú nepríjemné postihy
  • navyše vám to nepomôže sa naučiť učivo a budeme mať problém úspešne absolvovať skúšku resp. test

Tento týždeň:

  • Dnešná prednáška: detaily skúšky, opakovanie, vaše otázky, precvičovanie učiva
  • Dnešné doplnkové cvičenia: ako obvykle, vrátane bonusovej rozcvičky
  • Utorok cvičenia: nebudú ďalšie príklady, odporúčame riešiť trénovacie príklady na skúšku, prípadne sa spýtať otázky k učivu
    • Dva z príkladov sa dajú do piatka 22:00 odovzdať za bonusové body, ale ako tréning aj neskôr
  • Streda prednáška: nepreberané črty C resp. C++. Nebudeme skúšať, ale môžu sa zísť.
  • Streda písomka 18:15 v posluchárni B
    • Pozrite si aj ukážkové príklady týkajúce sa dátových štruktúr
    • Okrem toho aj bežné príklady ako na ostatných testoch na programovanie, dopĺňanie, ...
  • Koncom týždňa si skontrolujte body na testovači. Prípadné reklamácie pošlite na prog@... čím skôr, najneskôr však do 4.1. 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 22.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}; //spravne
int B[4];               //spravne
B={3, 6, 8, 10};        //nespravne
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 pola
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 vektor (rastúce pole)

  • operácie add, get, set, length

Abstraktný dátový typ množina (set)

  • operácie (init), insert, find
  • 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ý strom (ak kľúč je reťazec)

Abstraktný dátový typ slovník (asociatívne pole, map)

  • kľúče a ďalšie dáta
  • operácie insert, find, remove (hľadáme podľa kľúča)
  • implementácie podobné ako množina

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, top
  • 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ívné 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 polynómami (vyhodnocovanie, sčítanie)
  • Práca s aritmetickými výrazmi: vyhodnocovanie postfixovej formy, prevod z infixovej do postfixovej, reprezentácia vo forme stromu

Prednáška 24

Nepreberané črty jazykov C a C++

Na dnešnej prednáške si stručne ukážeme niektoré črty jazykov C a C++, ktoré sme počas semestra nepreberali. Nebudeme preberať väčšie detaily; cieľom je, aby ste neboli príliš prekvapení pri štúdiu existujúcich programov, resp. poskytnúť inšpiráciu pre ďalšie samoštúdium.

Tento materiál nebude vyžadovaný na skúške a ani vám neodporúčame tieto črty jazyka používať, ak ste sa s nimi dostatočne pred skúškou neoboznámili. Dnes vynecháme objektovo-orientované programovanie v jazyku C++. Objektovo-orientovanému programovaniu (v jazyku Java) sa budeme venovať budúci semester.

Jazyk C

  • Jazyk C vznikol cca 1973 na podporu vývoja operačného systému Unix (C++ asi o 10 rokov neskôr)
  • Jazyk C existuje v rôznych verziách.
  • V starších verziách nefungujú mnohé veci z jazyka C++, ktoré sme bežne používali, napr. komentáre vo forme //, všetky deklarácie premenných musia byť na začiatku funkcie a pod.

Ďalšie typy premenných

  • Celé čísla: short int, long int, unsigned int, ...
    • Veľkosť jednotlivých typov závisí od kompilátora a platformy
  • Desatinné čísla: float (menej presný ako double)
  • V staršom C-čku nie je bool, používa sa int, ktorý má hodnotu 0 pre false a nenulovú (napr. 1) pre true.
  • Typ enum: vymenujeme možné hodnoty, tie sa stanú celočíselnými konštantami: enum farby {biela, cierna, cervena};
  • Zložený typ union: v tom istom mieste v pamäti môže byť jedna z alternatívnych premenných, napr. pri aritmetickom strome by sme mohli uložiť buď smerníky na deti, alebo hodnotu v liste a ušetriť tak trochu pamäti

Príklad: vrchol stromu pre aritmetické výrazy pomocou union (zapísaný v mierne skrátenej syntaxi z C++)

  • Na mojom počítači pôvodná aj nová verzia zaberá 32 bajtov.
    • Smerník aj double zaberajú 8 bajtov, bool aj char 1 bajt
    • Teoreticky by teda mohlo stačiť 18 bajtov s union vs. 25 bajtov bez union
    • Ale kompilátor umiestňuje časti structov na miesta s rýchlejším prístupom.
  • Ak by sme namiesto bool isValue použili rovno char op, zaberala by štruktúra iba 24 bajtov.
#include <iostream>
#include <vector>
using namespace std; 

struct origNode {   /* vrchol stromu z prednasky */
    double val;     /* ciselna hodnota */
    char op;        /* operator '+', '-', '*', '/', 
                     * alebo ' ' ak ide o hodnotu */
    origNode * left;    /* lavy podvyraz alebo NULL */
    origNode * right;   /* pravy podvyraz alebo NULL */
};

struct nodeWithUnion;

struct operatorNode {
    char op;
    nodeWithUnion * left;    /* lavy podvyraz alebo NULL */
    nodeWithUnion * right;   /* pravy podvyraz alebo NULL */
};

struct nodeWithUnion {   /* vrchol stromu pomocou union */
  bool isValue;
  union {
    double val;     /* ciselna hodnota */
    operatorNode o;
  };
};

nodeWithUnion * createOp(char op, nodeWithUnion *left, nodeWithUnion *right) {
    nodeWithUnion *v = new nodeWithUnion;
    v->isValue = false;
    v->o.left = left;
    v->o.right = right;
    v->o.op = op;
    return v;
}

nodeWithUnion * createNum(double val) {
    nodeWithUnion *v = new nodeWithUnion;
    v->isValue = true;
    v->val = val;
    return v;
}

int main() 
{
  cout << "velkost vrchola z prednasky " << sizeof(origNode) <<endl;
  cout << "velkost vrchola s union " << sizeof(nodeWithUnion) << endl;

  nodeWithUnion *v = createNum(1.5);
  cout << v->val << endl;  // spravne pouzitie 
  cout << v->o.op << endl;  // zle pouzitie, nenajde kompilator ani valgrind
}

Operátory

Okrem operátorov, ktoré sme bežne používali, existuje niekoľko ďalších, napríklad:

  • 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 ?: má tvar (podmienka)?(hodnota pre true):(hodnota pre false), napr.
 cout << x << " je " << ((x%2==0) ? "parne" : "neparne") << endl;
  • Pozor na rozdiel medzi a[i++]=0 a a[++i]=0, prehľadnejšie použiť dva príkazy: a[i]=0; i++ alebo i++; a[i] = 0;

Príkaz do-while

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

  • V C-čku nie sú klasické konštanty, robia sa pomocou makier. Nasledujúce dva riadky majú podobný význam:
#define MAXN 100
const int MAXN=100;
  • Okrem konštánt môžeme 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 dosadi, dostane: */
cout << ((a*a) < (b+5) ? (a*a) : (b+5));
  • Treba dať kopu zátvoriek, aby nedošlo k interakcii s okolím použitia príkazu MIN

Delenie programu na súbory

  • Väčší program chceme rozdeliť na viac súborov
  • Chceme vytvárať a používať vlastné knižnice - skupiny funkcií s podobným účelom
    • Napríklad knižnica implementujúca funkcie pracujúce so zásobníkom
  • Knižnicu rozdelíme na dva súbory, napr. stack.h a stack.c resp. stack.cpp
  • V hlavičkovom súbore (header file) zadeklarujeme funkcie, ale neuvádzame ich kód, napr.:
typedef int dataType;
struct stack {
    int top;  /* pozicia vrchného prvku zásobníka */
    dataType *items; /* pole prvkov */
};
void init(stack &s);
bool isEmpty(stack &s);
void push(stack &s, dataType data); 
dataType pop(stack &q);
  • Programy, ktoré chcú použiť stack, použijú include na tento súbor
#include "stack.h"
  • V súbore stack.cpp uvedieme kód funkcií, napr.
#include "stack.h"
const int maxN = 100;
void init(stack &s) {
    s.top = -1;
    s.items = new dataType[maxN];
}
...
  • Všimnite si, že v include dávame meno štandardných knižníc v <>, našich vlastných v ""
  • Pri štandardných knižniciach sa v C-čku používa prípona h, v C++ sa namiesto toho väčšinou pridá c na začiatok (že je to knižnica z čistého C)
    • Napr. <cmath> vs. <math.h>
  • Ako zabrániť, aby sa vložilo include viackrát?
    • #ifndef IDENTIFIKATOR #define IDENTIFIKATOR ... #endif
    • Preprocesor umožňuje okrem toho aj ďalšie podmienené príkazy #if VYRAZ ... #else ... #endif alebo #ifdef IDENTIFIKATOR ... #else ... #endif

C nemá posielanie parametrov referenciou

  • ... používame teda smerníky
void swap(int &a, int &b) {
  int tmp = a;
  a = b;
  b = tmp;
}

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

Zopár užitočných funkcií

Alokácia pamäte

  • V C-čku sa nepoužíva new a delete, resp. new[] a delete[]
  • Pamäť sa alokuje funkciou malloc, ktorá alokuje kus pamäte s daným počtom bajtov
    • Ak sa nepodarilo, vráti NULL
    • Inak vráti pointer na void, treba pretypovať
    • Veľkosť spočítame operátorom sizeof
#include <stdlib.h>

/* 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 prvý je 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);
  • Podobne je aj funkcia bsearch na binárne vyhľadávanie v utriedenom poli

Odbočka: argumenty typu const

  • v príklade vyššie máme funkciu s hlavičkou int compare (const void * a, const void * b)
  • argument typu const void * a znamená, že programátor sľubuje, že pamäť, kam a ukazuje, nebude meniť
  • ak by sme mali funkciu void f(const int *a) { a = 5; }, kompilátor by vyhlásil 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*, nie void*.
  • preto pri použití štandardných knižníc potrebujete občas na vhodné miesta pridať const
    • dobrá prax je konzistentne dávať const na všetky parametre typu smerník alebo referencia, ktoré funkcia nepotrebuje meniť

Jazyk C++

Generické funkcie

  • Niekedy chceme napísať algoritmus, ktorý by mohol fungovať na veľa rôznych typoch
  • Napr. triediť môžeme celé alebo desatinné čísla, reťazce, zložitejšie štruktúry s určitým kľúčom a pod.
  • Funkcia qsort nám to umožňuje, ale musíme sa zapodievať veľkosťami a pretypovaním, kde sa dajú ľahko narobiť chyby.
  • V C++ sa dajú písať funkcie, ktoré majú typ ako parameter:
template <class T>
T max (T a, T b) {
  return (a>b)? a : b;
}

int i=3;
int j=5;
int k=max<int>(i,j);
  • O generických funkciách sa budeme viac učiť budúci semester.

Preťaženie operátorov

  • Pre naše typy si vieme zadefinovať nové operátory
  • Napr. dve mená porovnávame najskôr podľa priezviska, pri zhode 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. operátor + a * pre structy reprezentujúce trebárs polynómy alebo komplexné čísla...
  • cout << "Hello" používa preťažený operátor << ak na ľavej strane je stream a na pravej reťazec

String

  • Okrem klasických C-čkových reťazcov môžeme použiť aj typ string z C++.
  • Má elegantnejšie používanie, sám si určuje potrebnú veľkosť pamäte
  • Sú to objekty, do funkcií odovzdávame väčšinou referenciou (cez &)
  • K jednotlivým znakom pristupujeme pomocou [] (ako u polí) alebo pomocou metódy at
#include <string>
using namespace std;

int main(void) {

    char cstr[100] = "Ahoj\n";
    string str = "Ako sa mas?\n";
    string str2;

    /* mozeme priradovat konstantne retazce, C-ckove retazce (polia znamkov)
      aj C++ stringy. */
    str2 = "Ahoj\n";
    str2 = cstr;
    str2 = str;
    /* meranie dlzky */
    cout << "Dlzka je: " << str.length() << endl;

    /* Funguje porovnanie pomocou ==, !=, <,...
     * (bud dvoch C++ stringov, alebo C++ stringu a C stringu)
     * Znamienko + znamená zreťazenie. */
    str2 = cstr + str;
    str2.push_back('X');
    str2.push_back('\n');
    cout << str2;

    if (str < str2) {
        cout << "Prvy je skor" << endl;
    } else if (str == str2) {
        cout << "Rovnaju sa" << endl;
    } else {
        cout << "Druhy je skor" << endl;
    }
}
  • Pomocou funkcie c_str() vieme získať zo stringu premennú typu const char*

Stream

  • Každý textový súbor môžeme chápať ako postupnosť znakov (podobne ako vstup/výstup z konzoly). Teda aj textový súbor je vlastne stream, ktorý môžeme postupne (zľava doprava) čítať alebo do neho zapisovať.
  • Textové súbory teda môžeme načítávať alebo zapisovať pomocou analógie k cin a cout
  • Budeme využívať typy a funkcie zadefinované v knižnici fstream.
#include <fstream>

using namespace std;

int main (void) {
  ofstream o;
  o.open ("test.txt");     // otvorenie súboru
  o << 'z';                // zapíše 1 znak
  o << "Toto je retazec";  // zapíše reťazec 
  o << endl;               // zapíše znak nového riadku
  o << "10*32=" << 10*32 << endl;          // zapíše reťazec a hodnotu výrazu
  o << "1 rovna sa 4? " << (1==4) << endl; // pozor na priority operátorov ... 1==4 musí byť v zátvorkách !!!
  o.close ();              // zatvorenie súboru
}
  • ofstream tiež umožňuje formátovanie výstupu napríklad pomocou modifikátorov fixed,setprecision(),setfill(),setw()
  • Podobne môžeme otvoriť aj súbor, z ktorého budeme čítať vstup. V príklade načítame pole celých čísel.
#include <fstream>
#include <iostream>

using namespace std;
const int MAX=100;

int main (void) {
  ifstream f;
  int pocet, A[MAX];
  
  f.open ("vstup.txt");
  if (f.fail ()) return 1;  // return = návrat z funkcie – koniec programu
  f >> pocet;
  for (int i=0; i<pocet; i++) {
    f >> A[i];
  }

  for (int i=0; i<pocet; i++) {
    cout<< A[i] << " ";
  }
  f.close();
}
  • Pri načítaní sme sa už stretli s funkciou f.fail(), 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
  ifstream fin("vstup.txt"); // input
  if(!fin) {
    cout << "Cannot open vstup.txt file.\n";
    return 1;
  }

  ofstream fout("vystup.txt"); // output, normal file
  if(!fout) {
    cout << "Cannot open vystup.txt file.\n";
    return 1;
  }
  • Testovanie konca súboru môžeme robiť pomocou funkcie eof
ifstream fin("VSTUP.txt"); // input
char c;

while(!fin.eof()) {
  fin>>c;
  cout<<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.
int main(void){
ifstream f("VSTUP.txt");;
string line;
int poc;

while (getline(f,line)){
 poc=0;
 for (int i=0; i<line.length(); i++){
   if (line[i]=='.') poc++;
 }
 cout << poc <<endl;
}

Niektoré užitočné štruktúry z knižnice STL

STL (standard template library)

  • Veľká knižnica rôznych dátových štruktúr
  • Manuál napr. http://www.cplusplus.com/reference/stl/
  • Generické štruktúry, ktoré môžu uchovávať dáta rôznych typov
    • elegantnejšie ako naše riešenie s dataType
    • v jednom programe môžeme mať štruktúry uchovávajúce rôzne typy
  • Budúci semester sa naučíme pracovať s podobnými štruktúrami v Jave

Štruktúra vector [3]

  • Pole s dynamickou alokáciou pamäte, čo znamená, že ak nestačí pôvodne alokovaný priestor, alokuje si sám väčší.
  • Podobne ako keď sme si to programovali sami počas semestra
  • Deklarovať vector môžeme jedným z nasledujúcich spôsobov
vector<int> A;       //vytvorí pole celých čísel
vector<int> A(10);   //vytvorí pole 10 celých čísel, ktoré všetky nastaví na default hodnotu
vector<int> A(5,1);  //vytvorí pole 5 celých čísel, ktoré nastaví na 1
  • Prístup k prvkom vectora je možný dvoma spôsobmi
    • klasicky pomocou A[index] - podobne ako pri poliach nekontroluje rozsah
    • A.at(index) - v prípade indexu mimo rozsahu program hneď spadne (presnejšie vyhodí výnimku) - nenarobí chaos v pamäti, kde sa potom ťažko hľadá chyba
  • V obidvoch prípadoch môžeme aj priraďovať A[index]=value; A.at(index)=value;
  • Vkladanie nových prvkov na koniec uloženej postupnosti
    • A.push_back(x) vloží hodnotu x, podľa potreby alokuje nové polia, presúva 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 default
    • A.capacity() nám povie dĺžku alokovaného poľa a A.reserve(n) nám ju umožní zväčšiť, ale väčšinou sa o tieto implementačné detaily nestaráme
  • Ako generická dátová štruktúra:
  vector<int> A;
  for (int i=0; i<10; i++){
    A.push_back(i);
  }
  for (int i=0; i<A.size(); i++){
    cout << A[i] << endl;     // alebo A.at(i)
  }

Štruktúra map

  • implementuje slovník, pričom zadáme dva typy: kľúč a hodnota
  map <string, string> zoznam;
  zoznam["Jozko Mrkvicka"] = "02/12345678";
  zoznam["Janko Hrasko"] = "02/87654321";
  if(zoznam.count("Jozko Mrkvicka") > 0) {
    cout << zoznam["Jozko Mrkvicka"] << endl;
  }
  // prejdenie vsetkych zaznamov v slovniku pomocou iteratora
  cout << "Vsetky zaznamy:" << endl;
  for(map <string, string>::iterator i=zoznam.begin();
      i!=zoznam.end(); i++) {
    // i->first je meno, i->second je cislo
    cout << i->first << " " << i->second << endl;
  }

Štruktúra dequeue

  • implementuje zásobník aj rad naraz (double-ended queue)
  deque <int> a;
  a.push_back(0);
  a.push_front(1);
  cout << a.back() << endl;
  cout << a.front() << endl;
  a.pop_back();
  a.pop_front();

Algoritmus na triedenie

  • v knižnici <algorithm>
//triedime normalne pole
int A[6] = {1, 4, 2, 8, 5, 7};
sort(A, A + 6);

//triedime vektor
vector <int> A;
sort(A.begin(), A.end());

//triedime podla nasej porovnavacej funkcie, napr. podla absolutnej hodnoty
struct cmp {
    bool operator()(int x, int y) { return abs(x) < abs(y); }
};
cmp c;
sort(A.begin(), A.end(), c);

Range-based for loop (v štandarde C++11)

  • Špeciálny cyklus cez prvky vektora a pod.
    • nepotrebujeme zavádzať premennú pre index
    • podobný for cyklus uvidíme podrobnejšie v Jave
  • Štandard C++11 musíme zapnúť v kompilátore, napr. g++ -std=c++11
    • Implementácia tohto štandardu ešte nemusí byť úplná
#include <iostream>
#include <vector>
using namespace std; 

void vypis(const vector<int> &a) {
  // vypise vsetky prvky vektora a
  for (const int &value : a) { 
    cout << value << ' ';
  }
  cout << '\n';  
}

int main() 
{
  vector<int> a;

  // do vektora a vlozi prvky 0,..,5
  for(int value : {0,1,2,3,4,5}) {
    a.push_back(value);
  }

  vypis(a);  // vypise 0 1 2 3 4 5
 
  // zvysi kazdy prvok vektora o 1
  for (int &value : a) { 
    value++;
  }

  vypis(a); // vypise 1 2 3 4 5 6
}