|
|
| Riadok 468: |
Riadok 468: |
| | * Ako by ste v programe odstránili globálnu premennú <tt>count</tt>? | | * Ako by ste v programe odstránili globálnu premennú <tt>count</tt>? |
| | | | |
| − | ==Aritmetické výrazy== | + | == Zhrnutie == |
| − | | |
| − | Nejaký čas sa teraz budeme venovať spracovaniu aritmetických výrazov pozostávajúcich z reálnych čísel, operácií <tt>+,-,*,/</tt> a zátvoriek <tt>(,)</tt>.
| |
| − | Hlavnou úlohou je vyhodnotenie daného výrazu; napríklad pre výraz
| |
| − | <pre>
| |
| − | (65 - 3 * 5) / (2 + 3)
| |
| − | </pre>
| |
| − | chceme vedieť povedať, že jeho hodnota je 10. Najprv ale zavedieme dva alternatívne spôsoby zápisu aritmetických výrazov, z ktorých jeden nám vyhodnocovanie výrazov značne uľahčí.
| |
| − | | |
| − | ===Infixová notácia===
| |
| − | | |
| − | * Bežný spôsob zápisu aritmetických výrazov sa v matematike nazýva aj '''infixovou notáciou'''.
| |
| − | * Binárne operátory (ako napríklad <tt>+,-,*,/</tt>) sa v tejto notácii píšu medzi svojimi dvoma operandmi.
| |
| − | * Poradie vykonávania operácií sa riadi zátvorkami a prioritou operácií.
| |
| − | | |
| − | Napríklad
| |
| − | <pre>
| |
| − | (65 – 3 * 5) / (2 + 3)
| |
| − | </pre>
| |
| − | je infixový výraz s hodnotou <tt>10</tt>.
| |
| − | | |
| − | === Prefixová notácia ===
| |
| − | | |
| − | Pri '''prefixovej notácii''' (často podľa jej pôvodcu Jana Łukasiewicza nazývanej aj ''[https://en.wikipedia.org/wiki/Polish_notation poľskou notáciou]'') sa každý operátor v aritmetickom výraze zapisuje pred svojimi dvoma operandmi.
| |
| − | | |
| − | Napríklad výraz <tt>(65 – 3 * 5) / (2 + 3)</tt> má prefixový zápis
| |
| − | <pre>
| |
| − | / - 65 * 3 5 + 2 3
| |
| − | </pre>
| |
| − | | |
| − | Zaujímavosť: programovací jazyk [https://en.wikipedia.org/wiki/Lisp_(programming_language) Lisp] a jeho varianty využívajú prefixovú notáciu na všetky výrazy, píšu však aj zátvorky, napríklad <tt>(+ 1 2)</tt>.
| |
| − | | |
| − | === Postfixová notácia ===
| |
| − | | |
| − | Pri '''postfixovej notácii''' (často nazývanej aj ''[https://en.wikipedia.org/wiki/Reverse_Polish_notation obrátenou poľskou notáciou]'') je situácia opačná: operátor sa zapisuje bezprostredne za svojimi dvoma operandmi.
| |
| − | | |
| − | Výraz <tt>(65 – 3 * 5) / (2 + 3)</tt> má teda postfixový zápis
| |
| − | <pre>
| |
| − | 65 3 5 * - 2 3 + /
| |
| − | </pre>
| |
| − | | |
| − | * Postfixová a prefixová notácia sú pre človeka o niečo ťažšie čitateľné, než bežná infixová notácia (čo môže byť aj otázkou zvyku).
| |
| − | * Uvidíme však, že výrazy v postfixovej notácii sa dajú jednoducho vyhodnocovať.
| |
| − | * Výhodou postfixovej a prefixovej notácie oproti infixovej je aj to, že nepotrebujú zátvorky.
| |
| − | | |
| − | === Unárne mínus===
| |
| − | | |
| − | * Operátory +, * a / sú vždy '''binárne''', čo znamená, že sa aplikujú sa na dva operandy.
| |
| − | * Operátor - môže byť binárny aj '''unárny''' (unárny znamená, že sa aplikuje na jeden operand).
| |
| − | ** Príklad s binárnym mínus: <tt>1 - 2</tt>.
| |
| − | ** Príklad s unárnym mínus: <tt>2 * -(2 + 3)</tt>.
| |
| − | * Nevýhodou postfixovej a prefixovej notácie je, že bez zátvoriek v nich nie je možné rozpoznať binárne a unárne mínus.
| |
| − | ** Prefixový výraz <tt>- - 2 3</tt> by napríklad mohol zodpovedať ako výrazu <tt>-2 - 3</tt> aj výrazu <tt>-(2 - 3)</tt>.
| |
| − | * Na vyriešenie tohto problému budeme na unárne mínus používať znamienko <tt>~</tt> (iba interne v našom programe, nepoužíva sa tak všeobecne). V infixových výrazoch budú unárne mínus automaticky rozpoznané a nahradené <tt>~</tt>.
| |
| − | | |
| − | == Predspracovanie výrazu ==
| |
| − | | |
| − | Používateľ zadá výraz ako text v niektorej notácii. Aby sa nám s ním lepšie pracovalo, prevedieme ho najskôr na postupnosť symbolov (angl. '''token'''), pričom každý symbol bude buď číslo reprezentované v premennej typu <tt>double</tt> alebo znak reprezentujúci operátor alebo zátvorku.
| |
| − | | |
| − | Jednotlivé symboly budeme ukladať do záznamov typu <tt>token</tt>
| |
| − | <syntaxhighlight lang="C++">
| |
| − | struct token {
| |
| − | char op;
| |
| − | double val;
| |
| − | };
| |
| − | </syntaxhighlight>
| |
| − | * Ak štruktúra obsahuje číslo, <tt>op</tt> bude medzera a samotné číslo bude v položke <tt>val</tt>.
| |
| − | * Ak štruktúra reprezentuje iný symbol, tento symbol bude v položke <tt>op</tt> a položka <tt>val</tt> sa nebude používať.
| |
| − | | |
| − | Postupnosť symbolov uložíme do štruktúry <tt>tokenSequence</tt>, ktorá obsahuje pole položiek typu <tt>token</tt> a tiež koľko položiek bolo do poľa uložených.
| |
| − | <syntaxhighlight lang="C++">
| |
| − | struct tokenSequence {
| |
| − | token * items; // pole tokenov
| |
| − | int size; // veľkosť alokovaného poľa
| |
| − | int length; // počet tokenov uložených v poli
| |
| − | };
| |
| − | </syntaxhighlight>
| |
| − | | |
| − | | |
| − | Napríklad pre výraz <tt>"(2.1+ 3.9) / 3"</tt> vznikne nasledujúca dátová štruktúra:
| |
| − | <pre>
| |
| − | tokenSequence:
| |
| − | length: 7
| |
| − | size: nejaké číslo >= 7
| |
| − | items: pole s nasledovnými položkami
| |
| − | | |
| − | token items[0]: op '(', val ?
| |
| − | token items[1]: op ' ', val 2.1
| |
| − | token items[2]: op '+', val ?
| |
| − | token items[3]: op ' ', val 3.9
| |
| − | token items[4]: op ')', val ?
| |
| − | token items[5]: op '/', val ?
| |
| − | token items[6]: op ' ', val 3
| |
| − | </pre>
| |
| − | | |
| − | Preklad výrazu na postupnosť symbolov realizuje funkcia <tt>tokenize</tt>.
| |
| − | * Na načítanie čísla používame funkciu <tt>sscanf</tt>, ktorá vyzerá podobne ako <tt>scanf</tt>, ale načítava zo zadaného reťazca.
| |
| − | ** Ako reťazec zadáme <tt>&(str[strPos])</tt>, čo spôsobí, že sa začne načítavať od pozície <tt>strPos</tt>, mohli by sme písať aj <tt>str+strPos</tt>.
| |
| − | ** Formátovací znak <tt>%n</tt> nám do premennej <tt>skip</tt> uloží počet znakov, ktoré sa pri načítaní čísla použili, o tento počet potom posunieme premennú <tt>strPos</tt>.
| |
| − | * Kód token <tt>newToken = {str[strPos], 0}</tt> inicializuje položku <tt>op</tt> na znak <tt>str[strPos]</tt> a položku <tt>val</tt> na nulu.
| |
| − | * Pomocné funkcie <tt>init</tt> a <tt>addToken</tt> pracujú s postupnosťou symbolov, nájdete ich v [[#Program_na_pr.C3.A1cu_s_v.C3.BDrazmi|programe na konci prednášky]].
| |
| − | | |
| − | <syntaxhighlight lang="C++">
| |
| − | void tokenize(char * str, tokenSequence & tokens) {
| |
| − | init(tokens, strlen(str)); // inicializujeme prázdnu postupnosť
| |
| − | | |
| − | int strPos = 0; // pozícia v rámci reťazca
| |
| − | while (str[strPos] != 0) { // kým nie sme na konci str
| |
| − | if (isspace(str[strPos])) { // preskakujeme biele znaky
| |
| − | strPos++;
| |
| − | } else if (isdigit(str[strPos]) || str[strPos] == '.') {
| |
| − | // keď nájdeme cifru alebo bodku (začiatok čísla)
| |
| − | double val;
| |
| − | int skip;
| |
| − | // načítame toto číslo pomocou sscanf,
| |
| − | // do skip uložíme počet znakov čísla
| |
| − | sscanf(&(str[strPos]), "%lf%n", &val, &skip);
| |
| − | // vytvoríme a uložíme token
| |
| − | token newToken = {' ', val};
| |
| − | addToken(tokens, newToken);
| |
| − | // preskočíme všetky znaky čísla
| |
| − | strPos += skip;
| |
| − | } else {
| |
| − | // spracovanie zátvoriek alebo operátora
| |
| − | assert(strchr("+-/*()~", str[strPos]) != NULL);
| |
| − | // vytvoríme a uložíme token
| |
| − | token newToken = {str[strPos], 0};
| |
| − | addToken(tokens, newToken);
| |
| − | strPos++;
| |
| − | }
| |
| − | }
| |
| − | }
| |
| − | </syntaxhighlight>
| |
| − | | |
| − | == Vyhodnocovanie aritmetických výrazov v postfixovej notácii ==
| |
| − | | |
| − | * Budeme používať '''zásobník''', do ktorého budeme vkladať '''čísla''' čakajúce na spracovanie.
| |
| − | * Operátor má v postfixovom zápise obidva operandy pred sebou. Keď narazíme na operátor, jeho operandy sme už prečítali a spracovali. Ide navyše o '''posledné dve''' prečítané alebo vypočítané hodnoty.
| |
| − | | |
| − | Výraz budeme postupne čítať zľava doprava:
| |
| − | * Keď narazíme na číslo, vložíme ho na zásobník.
| |
| − | * Keď narazíme na operátor:
| |
| − | ** Vyberieme zo zásobníka jeho dva operandy.
| |
| − | ** Prvé z týchto čísel je pritom druhým operandom a druhé z nich je prvým operandom.
| |
| − | ** Vykonáme s týmito operandmi danú operáciu a výsledok tejto operácie vložíme naspäť na zásobník.
| |
| − | * Pri unárnom mínus vyberáme zo zásobníka iba jeden operand.
| |
| − | * Tento postup opakujeme, až kým neprídeme na koniec výrazu. V tom okamihu by sme mali mať na zásobníku jediný prvok, výslednú hodnotu výrazu.
| |
| − | | |
| − | Technická poznámka:
| |
| − | * Keďže vo [[#Program_na_pr.C3.A1cu_s_v.C3.BDrazmi|výslednom programe]] budeme potrebovať zásobníky čísel aj znakov, používame v ňom zásobník položiek typu <tt>token</tt>. Ak by sme však robili čisto vyhodnocovanie postfixovej notácie, stačil by nám zásobník s prvkami typu <tt>double</tt>.
| |
| − | | |
| − | '''Cvičenie:'''
| |
| − | * Odsimulujme činnosť tohto algoritmu na našom vstupe <tt>65 3 5 * - 2 3 + /</tt>
| |
| − | * Aké všelijaké prípady môžu nastať, keď na vstupe nemáme správny výraz v postfixovej notácii?
| |
| − | | |
| − | <syntaxhighlight lang="C++">
| |
| − | typedef token dataType;
| |
| − | // Nasleduje kód pre zásobník tokenov
| |
| − | // ...
| |
| − | | |
| − | | |
| − | /** Funkcia aplikuje operátor uložený v opToken na čísla
| |
| − | * uložené v num1 a num2, výsledok uloží ako číslo do result.
| |
| − | * Unárny operátor sa aplikuje iba na num1. */
| |
| − | void applyOp(token opToken, token num1, token num2, token & result) {
| |
| − | result.op = ' ';
| |
| − | switch (opToken.op) {
| |
| − | case '~':
| |
| − | result.val = -num1.val;
| |
| − | break;
| |
| − | case '+':
| |
| − | result.val = num1.val + num2.val;
| |
| − | break;
| |
| − | case '-':
| |
| − | result.val = num1.val - num2.val;
| |
| − | break;
| |
| − | case '*':
| |
| − | result.val = num1.val * num2.val;
| |
| − | break;
| |
| − | case '/':
| |
| − | result.val = num1.val / num2.val;
| |
| − | break;
| |
| − | default: // iné operátory nepovoľujeme.
| |
| − | assert(false);
| |
| − | }
| |
| − | }
| |
| − | | |
| − | /** Funkcia vyhodnotí a vráti hodnotou výrazu v postfixovom tvare. */
| |
| − | double evaluatePostfix(tokenSequence & tokens) {
| |
| − | // zásobník, do ktorého ukladáme čísla
| |
| − | stack numberStack;
| |
| − | init(numberStack);
| |
| − | | |
| − | for (int i = 0; i < tokens.length; i++) {
| |
| − | // aktuálny token zo vstupu
| |
| − | token curToken = tokens.items[i];
| |
| − | if (curToken.op == ' ') {
| |
| − | // čísla rovno ukladáme na zásobník
| |
| − | push(numberStack, curToken);
| |
| − | } else {
| |
| − | // spracovanie operátora
| |
| − | token num1, num2, result;
| |
| − | // najskôr vyberieme 1 alebo 2 čísla zo zásobníka
| |
| − | if (curToken.op == '~') {
| |
| − | num1 = pop(numberStack);
| |
| − | } else {
| |
| − | num2 = pop(numberStack);
| |
| − | num1 = pop(numberStack);
| |
| − | }
| |
| − | // na operandy aplikujeme operátor
| |
| − | applyOp(curToken, num1, num2, result);
| |
| − | // výsledné číslo uložíme na zásobník
| |
| − | push(numberStack, result);
| |
| − | }
| |
| − | }
| |
| − | // zo zásobníka vyberieme výsledné číslo
| |
| − | token result = pop(numberStack);
| |
| − | // skontrolujeme, že zásobník je prázdny a výsledok je číslo
| |
| − | assert(isEmpty(numberStack) && result.op == ' ');
| |
| − | // uvoľníme pamäť zásobníka
| |
| − | destroy(numberStack);
| |
| − | return result.val;
| |
| − | }
| |
| − | </syntaxhighlight>
| |
| − | | |
| − | == Prevod výrazu z infixovej do postfixovej notácie ==
| |
| − | | |
| − | * Pre bežnú prax je dôležitejšie vyhodnocovanie výrazov v klasickej infixovej notácii.
| |
| − | * Túto úlohu teraz vyriešime tak, že napíšeme funkciu, ktorá preloží aritmetický výraz z infixovej do postfixovej notácie.
| |
| − | * Následne ho môžeme vyhodnotiť algoritmom popísaným vyššie.
| |
| − | | |
| − | === Popis algoritmu ===
| |
| − | | |
| − | Uvažujme najprv výrazy bez zátvoriek a bez unárnych mínusov.
| |
| − | * Pozostávajú teda z čísel a binárnych operátorov <tt>+, -, *, /</tt>, kde <tt>*</tt> a <tt>/</tt> majú vyššiu prioritu ako <tt>+</tt> a <tt>-</tt>.
| |
| − | * Všetky tieto operátory sú navyše zľava asociatívne, t.j. napr. <tt>30 / 6 / 5</tt> je to isté ako <tt>(30 / 6) / 5</tt>.
| |
| − | | |
| − | Na prevod takéhoto výrazu do postfixového tvaru ním stačí prejsť zľava doprava a všimnúť si dve skutočnosti:
| |
| − | * Poradie jednotlivých čísel v postfixovom výraze je rovnaké, ako v pôvodnom infixovom výraze.
| |
| − | * Napríklad výraz <tt>1 + 2 - 3 * 4 + 5</tt> má postfixový tvar <tt>1 2 + 3 4 * - 5 +</tt>.
| |
| − | * Pri prechádzaní vstupným infixovým výrazom budeme teda čísla priamo pridávať do výstupného postfixového výrazu.
| |
| − | * Každý operátor treba presunúť spomedzi jeho dvoch operandov za jeho druhý operand.
| |
| − | * Ak teda vo vstupnom infixovom výraze narazíme na operátor, nepridáme ho hneď do výstupného výrazu, ale uložíme ho pre neskoršie pridanie na správnej pozícii.
| |
| − | * Uložený operátor treba vypísať za jeho druhým operandom. Ak pritom aj samotný operand obsahuje nejaké ďalšie operátory, určite musia byť vypísané skôr. Operátory teda budeme ukladať na zásobník.
| |
| − | * Keď vo vstupnom infixovom výraze narazíme na operátor, je možné, že tesne pred ním končí operand jedného alebo niekoľkých operátorov uložených na zásobníku. Vďaka ľavej asociatívnosti pritom ide o práve všetky operátory na vrchu zásobníka, ktoré majú vyššiu alebo rovnakú prioritu, ako nájdený operátor. Všetky tieto operátory teda postupne vyberieme zo zásobníka a vypíšeme ich do výstupného reťazca. Až následne na zásobník vložíme nájdený operátor.
| |
| − | * Na konci vstupného reťazca vypíšeme všetky operátory, ktoré zostali na zásobníku.
| |
| − | | |
| − | Z technických dôvodov budeme na spodku zásobníka uchovávať „umelé dno” <tt>#</tt>, ktoré budeme chápať ako symbol s nižšou prioritou ako všetky operátory.
| |
| − | * Situáciu, keď pri vyberaní operátorov zo zásobníka narazíme na jeho dno tak budeme môcť riešiť konzistentne so situáciou, keď narazíme na operátor s nižšou prioritou: v oboch prípadoch chceme s vyberaním prestať.
| |
| − | | |
| − | Na vstupnom výraze <tt>1 + 2 - 3 * 4 + 5</tt> bude práve popísaný algoritmus pracovať nasledovne:
| |
| − | <pre>
| |
| − | Vstupný symbol Výstupný výraz Zásobník (dno vľavo)
| |
| − | -------------------------------------------------------------------------------------------------------
| |
| − | 1 # vypíš 1
| |
| − | 1 #
| |
| − | | |
| − | + 1 # # má nižšiu prioritu ako +, nevyberaj ho
| |
| − | 1 # + vlož + na zásobník
| |
| − | | |
| − | 2 1 # + vypíš 2
| |
| − | 1 2 # +
| |
| − | | |
| − | - 1 2 # + + má rovnakú prioritu ako -, vyber + a vypíš
| |
| − | 1 2 + # # má nižšiu prioritu ako -, nevyberaj ho
| |
| − | 1 2 + # vlož - na zásobník
| |
| − | 1 2 + # -
| |
| − | | |
| − | 3 1 2 + # - vypíš 3
| |
| − | 1 2 + 3 # -
| |
| − | | |
| − | * 1 2 + 3 # - - má nižšiu prioritu ako *, nevyberaj ho
| |
| − | 1 2 + 3 # - * vlož * na zásobník
| |
| − | | |
| − | 4 1 2 + 3 # - * vypíš 4
| |
| − | 1 2 + 3 4 # - *
| |
| − | | |
| − | + 1 2 + 3 4 # - * * má vyššiu prioritu ako +, vyber * a vypíš
| |
| − | 1 2 + 3 4 * # - - má rovnakú prioritu ako +, vyber - a vypíš
| |
| − | 1 2 + 3 4 * - # # má nižšiu prioritu ako +, nevyberaj ho
| |
| − | 1 2 + 3 4 * - # + vlož - na zásobník
| |
| − | | |
| − | 5 1 2 + 3 4 * - # + vypíš 5
| |
| − | 1 2 + 3 4 * - 5 # +
| |
| − | | |
| − | [koniec] 1 2 + 3 4 * - 5 # + vyber operátory na zásobníku a vypíš
| |
| − | 1 2 + 3 4 * - 5 + # <-- VÝSLEDNÝ POSTFIXOVÝ VÝRAZ
| |
| − | </pre>
| |
| − | | |
| − | Pridanie zátvoriek:
| |
| − | * Keď vo vstupnom infixovom reťazci narazíme na ľavú zátvorku, vložíme ju do zásobníka. Podobne ako pri <tt>#</tt> ju budeme považovať za symbol s nižšou prioritou, než všetky binárne operátory. To má dva dôsledky:
| |
| − | ** Pri vkladaní zátvorky teda nevyhadzujeme operátory zo zásobníka
| |
| − | ** Operátory vo vnútri zátvorky nespôsobia jej vyhodenie ani vyhodenie symbolov pod ňou
| |
| − | * Keď narazíme na pravú zátvorku, potrebujeme vypísať všetky doposiaľ nevypísané operátory uzatvorené touto zátvorkou. Preto vyberieme a vypíšeme na výstup všetky operátory zo zásobníka až po prvú ľavú zátvorku (ktorú zo zásobníka taktiež vyberieme).
| |
| − | | |
| − | Pridanie unárneho mínus:
| |
| − | * Unárne mínus má vyššiu prioritu ako binárne operátory. Predpokladáme, že ho máme zapísané ako <tt>~</tt>.
| |
| − | * Je však sprava asociatívne, t.j. <tt>--2</tt> je to isté ako <tt>-(-2)</tt> a v postfixovom tvare to bude <tt>2 ~ ~</tt>.
| |
| − | * Preto ak je aktuálny operátor unárne mínus <tt>~</tt>, nevyhadzujeme iné unárne mínus zo zásobníka. Toto je v programe dosiahnuté tak, že nové unárne mínus ("pravé") má vyššiu prioritu ako to, čo už je na zásobníku ("ľavé").
| |
| − | | |
| − | === Implementácia ===
| |
| − | | |
| − | <syntaxhighlight lang="C++">
| |
| − | /** Pomocná funkcia, ktorá vráti prioritu operátora.
| |
| − | * Argument right určuje, či ide o pravý z dvoch porovnávaných operátorov */
| |
| − | int priority(char op, bool right) {
| |
| − | if (op == '#' || op == '(') return 0;
| |
| − | if (op == '-' || op == '+') return 1;
| |
| − | if (op == '*' || op == '/') return 2;
| |
| − | if (op == '~' && !right) return 3;
| |
| − | if (op == '~' && right) return 4;
| |
| − | assert(false); // sem by sme sa nemali dostať
| |
| − | }
| |
| − | | |
| − | /** Skonvertuje infixový výraz infix do postfixovej formy */
| |
| − | void infixToPostfix(tokenSequence & infix, tokenSequence & postfix) {
| |
| − | // inicializuje výslednú postupnosť tokenov
| |
| − | init(postfix, infix.length);
| |
| − | | |
| − | // vytvoríme zásobník operátorov a na spodok dáme #
| |
| − | stack opStack;
| |
| − | init(opStack);
| |
| − | token curToken = {'#', 0};
| |
| − | push(opStack, curToken);
| |
| − | | |
| − | for (int i = 0; i < infix.length; i++) {
| |
| − | // aktuálny token zo vstupu
| |
| − | curToken = infix.items[i];
| |
| − | if (curToken.op == ' ') {
| |
| − | // čísla rovno skopírujeme do výstupu
| |
| − | addToken(postfix, curToken);
| |
| − | } else if (curToken.op == '(') {
| |
| − | // začiatok zátvorky uložíme na zásobník
| |
| − | push(opStack, curToken);
| |
| − | } else if (curToken.op == ')') {
| |
| − | // koniec zátvorky:
| |
| − | // na výstup pridáme zo zásobníka operátory,
| |
| − | // ktoré boli v zátvorke
| |
| − | token popped = pop(opStack);
| |
| − | while (popped.op != '(') {
| |
| − | addToken(postfix, popped);
| |
| − | popped = pop(opStack);
| |
| − | }
| |
| − | } else {
| |
| − | // spracovanie operátora:
| |
| − | // na výstup pridáme zo zásobníka operátory s vyššou prioritou
| |
| − | int p = priority(curToken.op, true);
| |
| − | while (priority(peek(opStack).op, false) >= p) {
| |
| − | token popped = pop(opStack);
| |
| − | addToken(postfix, popped);
| |
| − | }
| |
| − | // aktuálny operátor dáme na zásobník
| |
| − | push(opStack, curToken);
| |
| − | }
| |
| − | }
| |
| − | | |
| − | // zvyšné operátory presunieme zo zásobníka na výstup
| |
| − | while (peek(opStack).op != '#') {
| |
| − | token popped = pop(opStack);
| |
| − | addToken(postfix, popped);
| |
| − | }
| |
| − | destroy(opStack);
| |
| − | }
| |
| − | </syntaxhighlight>
| |
| − | | |
| − | ''Cvičenie'': Rozšírte program vyššie o operáciu umocňovania <tt>^</tt> s prioritou vyššou ako <tt>*</tt> (dajte pozor na fakt, že umocňovanie je na rozdiel od operácií <tt>+, -, *, /</tt> sprava asociatívne).
| |
| − | | |
| − | | |
| − | == Predspracovanie unárneho mínus ==
| |
| − | | |
| − | * Pre prevodom infixového výrazu do postfixového tvaru potrebujeme v infixovom výraze nahradiť unárne mínus znakom <tt>~</tt>.
| |
| − | * Robí to funkcia <tt>fixUnaryMinus</tt>.
| |
| − | * Mínus je v korektnom infixovom výraze unárne práve vtedy, keď nenasleduje za číslom, ani za pravou zátvorkou, čo testuje pomocná funkcia <tt>isOperand</tt>.
| |
| − | | |
| − | <syntaxhighlight lang="C++">
| |
| − | /** Pomocná funkcia, ktorá vráti, či token je koncom operandu v infixovej notácii */
| |
| − | bool isOperand(token curToken) {
| |
| − | return curToken.op == ' ' || curToken.op == ')';
| |
| − | }
| |
| − | | |
| − | /** Funkcia, ktorá v infixovom výraze zmení unárne mínus na ~ */
| |
| − | void fixUnaryMinus(tokenSequence & infix) {
| |
| − | for (int i = 0; i < infix.length; i++) {
| |
| − | if(infix.items[i].op == '-'
| |
| − | && (i == 0 || !isOperand(infix.items[i - 1]))) {
| |
| − | infix.items[i].op = '~';
| |
| − | }
| |
| − | }
| |
| − | }
| |
| − | </syntaxhighlight>
| |
| − | | |
| − | ==Záverečné poznámky==
| |
| − | | |
| − | * Infixové výrazy vieme vyhodnotiť tak, že najskôr ich prevedieme na postfixové a tie potom vyhodnotíme.
| |
| − | ** Pri vyhodnocovaní postfixovej formy používame zásobník čísel (operandov).
| |
| − | ** Pri prevode z infixovej formy na postfixovú používame zásobník operátorov.
| |
| − | * Obidva procesy sa dajú vykonávať aj súčasne (počas prechodu výrazom používame jeden zásobník čísel a jeden zásobník operátorov).
| |
| − | ** Toto je pre zaujímavosť implementované vo funkcii <tt>evaluateInfix</tt> v [[#Program_na_pr.C3.A1cu_s_v.C3.BDrazmi|priloženom programe]].
| |
| − | * Všeobecnejšie a elegantnejšie prístupy k analýze a vyhodnocovaniu výrazov ale aj celých programov uvidíte na predmete Kompilátory (Mgr. štúdium), ktorý využíva poznatky z predmetu Formálne jazyky a automaty (semester 2Z).
| |
| − | | |
| − | | |
| − | === Typ dát na zásobníku ===
| |
| − | | |
| − | V našom programe sme potrebovali zásobník čísel aj zásobník znakov.
| |
| − | * Vyriešili sme to tým, že na zásobník dávame položky typu <tt>token</tt>, ktoré obsahujú znak aj číslo.
| |
| − | * Nie je to však príliš elegantné a plytve pamäťou na nepotrebné údaje.
| |
| − | * Druhá možnosť je dvakrát implementovať celý zásobník pre rôzne typy dát, kopírovanie a menenie kódu je však tiež niečo, čomu sa chceme vyhnúť.
| |
| − | * Najlepšie by bolo použiť techniku generického programovania, ktorá je k dispozícii vo veľa jazykoch, vrátane C++, ale nie C. Touto technikou vieme zásobník implementovať raz a použiť s rôznymi typmi. Ukážku tejto techniky uvidíme na poslednej prednáške a viac sa dozviete na Programovaní (2) v letnom semestri.
| |
| − | | |
| − | Na konci prednášky sme ešte prebrali úvodné časti [[Prednáška 20|prednášky 20]].
| |
| − | | |
| − | ==Program na prácu s výrazmi==
| |
| − | | |
| − | <syntaxhighlight lang="C++">
| |
| − | #include <cstdio>
| |
| − | #include <cctype>
| |
| − | #include <cassert>
| |
| − | #include <cstring>
| |
| − | using namespace std;
| |
| − | | |
| − | /** Štruktúra pre jednu súčasť výrazu: znamienko alebo číslo */
| |
| − | struct token {
| |
| − | char op; // znamienko alebo medzera, ak ide o číslo
| |
| − | double val; // číselná hodnota, ak je op medzera
| |
| − | };
| |
| − | | |
| − | /** Štruktúra pre postupnosť tokenov */
| |
| − | struct tokenSequence {
| |
| − | token * items; // pole tokenov
| |
| − | int size; // veľkosť alokovaného poľa
| |
| − | int length; // počet tokenov uložených v poli
| |
| − | };
| |
| − | | |
| − | /** Funkcia inicializuje prázdnu postupnosť tokenov
| |
| − | pričom alokuje pole požadovanej veľkosti. */
| |
| − | void init(tokenSequence & tokens, int size) {
| |
| − | tokens.items = new token[size];
| |
| − | tokens.size = size;
| |
| − | tokens.length = 0;
| |
| − | }
| |
| − | | |
| − | /** Funkcia do postupnosti tokenov pridá nový token. */
| |
| − | void addToken(tokenSequence & tokens, token & newToken) {
| |
| − | assert(tokens.length < tokens.size);
| |
| − | tokens.items[tokens.length] = newToken;
| |
| − | tokens.length++;
| |
| − | }
| |
| − | | |
| − | /** Funkcia odalokuje pamäť alokovanú pre postupnosť tokenov */
| |
| − | void destroy(tokenSequence & tokens) {
| |
| − | delete[] tokens.items;
| |
| − | }
| |
| − | | |
| − | /** Funkcia vypíše postupnosť tokenov, každý v hranatých zátvorkách. */
| |
| − | void printTokens(tokenSequence & tokens) {
| |
| − | for (int i = 0; i < tokens.length; i++) {
| |
| − | token curToken = tokens.items[i];
| |
| − | if (curToken.op == ' ') {
| |
| − | printf(" [val %g]", curToken.val);
| |
| − | } else {
| |
| − | printf(" [op %c]", curToken.op);
| |
| − | }
| |
| − | }
| |
| − | printf("\n");
| |
| − | }
| |
| − | | |
| − | /** Funkcia vypíše postupnosť tokenov ako aritmetický výraz. */
| |
| − | void printTokenExpression(tokenSequence & tokens) {
| |
| − | for (int i = 0; i < tokens.length; i++) {
| |
| − | token curToken = tokens.items[i];
| |
| − | if (curToken.op == ' ') {
| |
| − | printf(" %g", curToken.val);
| |
| − | } else {
| |
| − | printf(" %c", curToken.op);
| |
| − | }
| |
| − | }
| |
| − | printf("\n");
| |
| − | }
| |
| − | | |
| − | /** Funkcia konvertuje výraz z reťazca na postupnosť tokenov. */
| |
| − | void tokenize(char * str, tokenSequence & tokens) {
| |
| − | init(tokens, strlen(str)); // inicializujeme prázdnu postupnosť
| |
| − | | |
| − | int strPos = 0; // pozícia v rámci reťazca
| |
| − | while (str[strPos] != 0) { // kým nie sme na konci str
| |
| − | if (isspace(str[strPos])) { // preskakujeme biele znaky
| |
| − | strPos++;
| |
| − | } else if (isdigit(str[strPos]) || str[strPos] == '.') {
| |
| − | // keď nájdeme cifru alebo bodku (začiatok čísla)
| |
| − | double val;
| |
| − | int skip;
| |
| − | // načítame toto číslo pomocou sscanf, do skip uložíme počet znakov čísla
| |
| − | sscanf(&(str[strPos]), "%lf%n", &val, &skip);
| |
| − | // vytvoríme a uložíme token
| |
| − | token newToken = {' ', val};
| |
| − | addToken(tokens, newToken);
| |
| − | // preskočíme všetky znaky čísla
| |
| − | strPos += skip;
| |
| − | } else {
| |
| − | // spracovanie zátvoriek alebo operátora
| |
| − | assert(strchr("+-/*()~", str[strPos]) != NULL);
| |
| − | // vytvoríme a uložíme token
| |
| − | token newToken = {str[strPos], 0};
| |
| − | addToken(tokens, newToken);
| |
| − | strPos++;
| |
| − | }
| |
| − | }
| |
| − | }
| |
| − | | |
| − | // Nasleduje kód pre zásobník tokenov
| |
| − | typedef token dataType;
| |
| − | | |
| − | /** Uzol spájaného zoznamu pre zásobník */
| |
| − | struct node {
| |
| − | dataType data; // dáta uložené v uzle
| |
| − | node * next; // smerník na ďalší uzol zoznamu
| |
| − | };
| |
| − | | |
| − | /** Štruktúra pre zásobník implementovaný pomocou zoznamu*/
| |
| − | struct stack {
| |
| − | node * top; // Smernik na vrch zasobníka alebo NULL
| |
| − | };
| |
| − | | |
| − | /** Funkcia inicializuje prázdny zásobník */
| |
| − | void init(stack & s) {
| |
| − | s.top = NULL;
| |
| − | }
| |
| − | | |
| − | /** Funkcia zistí, či je zásobník prázdny */
| |
| − | bool isEmpty(stack & s) {
| |
| − | return s.top == NULL;
| |
| − | }
| |
| − | | |
| − | /** Funkcia pridá prvok item na vrch zásobníka */
| |
| − | void push(stack & s, dataType item) {
| |
| − | node * tmp = new node;
| |
| − | tmp->data = item;
| |
| − | tmp->next = s.top;
| |
| − | s.top = tmp;
| |
| − | }
| |
| − | | |
| − | /** Funkcia odoberie prvok z vrchu zasobnika a vráti ho */
| |
| − | dataType pop(stack & s) {
| |
| − | assert(!isEmpty(s));
| |
| − | dataType result = s.top->data;
| |
| − | node * tmp = s.top->next;
| |
| − | delete s.top;
| |
| − | s.top = tmp;
| |
| − | return result;
| |
| − | }
| |
| − | | |
| − | /** Funkcia vráti prvok na vrchu zásobníka, ale nechá ho v zásobníku */
| |
| − | dataType peek(stack & s) {
| |
| − | assert(!isEmpty(s));
| |
| − | return s.top->data;
| |
| − | }
| |
| − | | |
| − | /** Funkcia uvoľní pamäť zásobníka */
| |
| − | void destroy(stack & s) {
| |
| − | while (!isEmpty(s)) {
| |
| − | pop(s);
| |
| − | }
| |
| − | }
| |
| − | | |
| − | /** Funkcia aplikuje operátor uložený v opToken na čísla
| |
| − | * uložené v num1 a num2, výsledok uloží ako číslo do result.
| |
| − | * Unárny operátor sa aplikuje iba na num1. */
| |
| − | void applyOp(token opToken, token num1, token num2, token & result) {
| |
| − | result.op = ' ';
| |
| − | switch (opToken.op) {
| |
| − | case '~':
| |
| − | result.val = -num1.val;
| |
| − | break;
| |
| − | case '+':
| |
| − | result.val = num1.val + num2.val;
| |
| − | break;
| |
| − | case '-':
| |
| − | result.val = num1.val - num2.val;
| |
| − | break;
| |
| − | case '*':
| |
| − | result.val = num1.val * num2.val;
| |
| − | break;
| |
| − | case '/':
| |
| − | result.val = num1.val / num2.val;
| |
| − | break;
| |
| − | default: // iné operátory nepovoľujeme.
| |
| − | assert(false);
| |
| − | }
| |
| − | }
| |
| − | | |
| − | /** Funkcia vyhodnotí a vráti hodnotou výrazu v postfixovom tvare. */
| |
| − | double evaluatePostfix(tokenSequence & tokens) {
| |
| − | // zásobník, do ktorého ukladáme čísla
| |
| − | stack numberStack;
| |
| − | init(numberStack);
| |
| − | | |
| − | for (int i = 0; i < tokens.length; i++) {
| |
| − | // aktuálny token zo vstupu
| |
| − | token curToken = tokens.items[i];
| |
| − | if (curToken.op == ' ') {
| |
| − | // čísla rovno ukladáme na zásobník
| |
| − | push(numberStack, curToken);
| |
| − | } else {
| |
| − | // spracovanie operátora
| |
| − | token num1, num2, result;
| |
| − | // najskôr vyberieme 1 alebo 2 čísla zo zásobníka
| |
| − | if (curToken.op == '~') {
| |
| − | num1 = pop(numberStack);
| |
| − | } else {
| |
| − | num2 = pop(numberStack);
| |
| − | num1 = pop(numberStack);
| |
| − | }
| |
| − | // na operandy aplikujeme operátor
| |
| − | applyOp(curToken, num1, num2, result);
| |
| − | // výsledné číslo uložíme na zásobník
| |
| − | push(numberStack, result);
| |
| − | }
| |
| − | }
| |
| − | // zo zásobníka vyberieme výsledné číslo
| |
| − | token result = pop(numberStack);
| |
| − | // skontrolujeme, že zásobník je prázdny a výsledok je číslo
| |
| − | assert(isEmpty(numberStack) && result.op == ' ');
| |
| − | // uvoľníme pamäť zásobníka
| |
| − | destroy(numberStack);
| |
| − | return result.val;
| |
| − | }
| |
| − | | |
| − | /** Pomocná funkcia, ktorá vráti prioritu operátora.
| |
| − | * Argument right určuje, či ide o pravý z dvoch porovnávaných operátorov */
| |
| − | int priority(char op, bool right) {
| |
| − | if (op == '#' || op == '(') return 0;
| |
| − | if (op == '-' || op == '+') return 1;
| |
| − | if (op == '*' || op == '/') return 2;
| |
| − | if (op == '~' && !right) return 3;
| |
| − | if (op == '~' && right) return 4;
| |
| − | assert(false); // sem by sme sa nemali dostať
| |
| − | }
| |
| − | | |
| − | /** Skonvertuje infixový výraz infix do postfixovej formy */
| |
| − | void infixToPostfix(tokenSequence & infix, tokenSequence & postfix) {
| |
| − | // inicializuje výslednú postupnosť tokenov
| |
| − | init(postfix, infix.length);
| |
| − | | |
| − | // vytvoríme zásobník operátorov a na spodok dáme #
| |
| − | stack opStack;
| |
| − | init(opStack);
| |
| − | token curToken = {'#', 0};
| |
| − | push(opStack, curToken);
| |
| − | | |
| − | for (int i = 0; i < infix.length; i++) {
| |
| − | // aktuálny token zo vstupu
| |
| − | curToken = infix.items[i];
| |
| − | if (curToken.op == ' ') {
| |
| − | // čísla rovno skopírujeme do výstupu
| |
| − | addToken(postfix, curToken);
| |
| − | } else if (curToken.op == '(') {
| |
| − | // začiatok zátvorky uložíme na zásobník
| |
| − | push(opStack, curToken);
| |
| − | } else if (curToken.op == ')') {
| |
| − | // koniec zátvorky:
| |
| − | // na výstup pridáme zo zásobníka operátory,
| |
| − | // ktoré boli v zátvorke
| |
| − | token popped = pop(opStack);
| |
| − | while (popped.op != '(') {
| |
| − | addToken(postfix, popped);
| |
| − | popped = pop(opStack);
| |
| − | }
| |
| − | } else {
| |
| − | // spracovanie operátora:
| |
| − | // na výstup pridáme zo zásobníka operátory s vyššou prioritou
| |
| − | int p = priority(curToken.op, true);
| |
| − | while (priority(peek(opStack).op, false) >= p) {
| |
| − | token popped = pop(opStack);
| |
| − | addToken(postfix, popped);
| |
| − | }
| |
| − | // aktuálny operátor dáme na zásobník
| |
| − | push(opStack, curToken);
| |
| − | }
| |
| − | }
| |
| − | | |
| − | // zvyšné operátory presunieme zo zásobníka na výstup
| |
| − | while (peek(opStack).op != '#') {
| |
| − | token popped = pop(opStack);
| |
| − | addToken(postfix, popped);
| |
| − | }
| |
| − | destroy(opStack);
| |
| − | }
| |
| − | | |
| − | /** Pomocná funkcia, ktorá zo zásobníka vyberie
| |
| − | * jedno alebo dve čísla, aplikuje na nich operátor a výsledok uloží na zásobník. */
| |
| − | void processOp(stack & numberStack, token curToken) {
| |
| − | token num1, num2, result;
| |
| − | // najskôr vyberieme 1 alebo 2 čísla zo zásobníka
| |
| − | if (curToken.op == '~') {
| |
| − | num1 = pop(numberStack);
| |
| − | } else {
| |
| − | num2 = pop(numberStack);
| |
| − | num1 = pop(numberStack);
| |
| − | }
| |
| − | // na operandy aplikujeme operátor
| |
| − | applyOp(curToken, num1, num2, result);
| |
| − | // výsledné číslo uložíme na zásobník
| |
| − | push(numberStack, result);
| |
| − | }
| |
| − | | |
| − | /** Spočíta hodnotu výrau v infixovej forme,
| |
| − | * kombinuje infixToPostfix a evaluatePostfix */
| |
| − | double evaluateInfix(tokenSequence & infix) {
| |
| − | // zásobník, do ktorého ukladáme čísla
| |
| − | stack numberStack;
| |
| − | init(numberStack);
| |
| − | | |
| − | // vytvoríme zásobník operátorov a na spodok dáme #
| |
| − | stack opStack;
| |
| − | init(opStack);
| |
| − | token curToken = {'#', 0};
| |
| − | push(opStack, curToken);
| |
| − | | |
| − | for (int i = 0; i < infix.length; i++) {
| |
| − | // aktuálny token zo vstupu
| |
| − | curToken = infix.items[i];
| |
| − | if (curToken.op == ' ') {
| |
| − | // čísla ukladáme na zásobník
| |
| − | push(numberStack, curToken);
| |
| − | } else if (curToken.op == '(') {
| |
| − | // začiatok zátvorky uložíme na zásobník
| |
| − | push(opStack, curToken);
| |
| − | } else if (curToken.op == ')') {
| |
| − | // koniec zátvorky:
| |
| − | // spracujeme zo zásobníka operátory,
| |
| − | // ktoré boli v zátvorke
| |
| − | token popped = pop(opStack);
| |
| − | while (popped.op != '(') {
| |
| − | processOp(numberStack, popped);
| |
| − | popped = pop(opStack);
| |
| − | }
| |
| − | } else {
| |
| − | // spracovanie operátora:
| |
| − | // spracujeme zo zásobníka operátory s vyššou prioritou
| |
| − | int p = priority(curToken.op, true);
| |
| − | while (priority(peek(opStack).op, false) >= p) {
| |
| − | token popped = pop(opStack);
| |
| − | processOp(numberStack, popped);
| |
| − | }
| |
| − | // aktuálny operátor dáme na zásobník
| |
| − | push(opStack, curToken);
| |
| − | }
| |
| − | }
| |
| − | | |
| − | // spracujeme zvyšné operátory zo zásobníka
| |
| − | while (peek(opStack).op != '#') {
| |
| − | token popped = pop(opStack);
| |
| − | processOp(numberStack, popped);
| |
| − | }
| |
| − | destroy(opStack);
| |
| − | | |
| − | // zo zásobníka vyberieme výsledné číslo
| |
| − | token result = pop(numberStack);
| |
| − | // skontrolujeme, že ásobník je prázdny a výsledok je číslo
| |
| − | assert(isEmpty(numberStack) && result.op == ' ');
| |
| − | // uvoľníme pamäť zásobníka čísel
| |
| − | destroy(numberStack);
| |
| − | return result.val;
| |
| − | }
| |
| − | | |
| − | /** Pomocná funkcia, ktorá vráti, či token je koncom operandu v infixovej notácii */
| |
| − | bool isOperand(token curToken) {
| |
| − | return curToken.op == ' ' || curToken.op == ')';
| |
| − | }
| |
| − | | |
| − | /** Funkcia, ktorá v infixovom výraze zmení unárne mínus na ~ */
| |
| − | void fixUnaryMinus(tokenSequence & infix) {
| |
| − | for (int i = 0; i < infix.length; i++) {
| |
| − | if (infix.items[i].op == '-'
| |
| − | && (i == 0 || !isOperand(infix.items[i - 1]))) {
| |
| − | infix.items[i].op = '~';
| |
| − | }
| |
| − | }
| |
| − | }
| |
| − | | |
| − | /** Hlavný program načítava a vykonáva postupnosť príkazov tokenize, postfix, infix a end.
| |
| − | * Príkaz tokenize tokenizuje a vypíše vstupný výraz.
| |
| − | * Príkaz postfix načíta výraz v postfixovom formáte a vypíše jeho hodnotu.
| |
| − | * Príkaz infix načíta výraz v infixovom formáte, upraví v ňom unárne - na ~.
| |
| − | * prevedie ho do postfixového formátu, vyhodnotí postfixový výraz a naopokon
| |
| − | * vyhodnotí aj priamo infixovú formu výrazu. */
| |
| − | int main() {
| |
| − | const int maxLine = 100;
| |
| − | char command[maxLine];
| |
| − | char expression[maxLine];
| |
| − | | |
| − | while (true) {
| |
| − | int ret = scanf("%s", command);
| |
| − | if (ret < 1 || strcmp(command, "end") == 0) {
| |
| − | break;
| |
| − | } else if (strcmp(command, "tokenize") == 0) {
| |
| − | fgets(expression, maxLine, stdin);
| |
| − | tokenSequence tokens;
| |
| − | tokenize(expression, tokens);
| |
| − | printf(" tokens:");
| |
| − | printTokens(tokens);
| |
| − | destroy(tokens);
| |
| − | } else if (strcmp(command, "postfix") == 0) {
| |
| − | fgets(expression, maxLine, stdin);
| |
| − | tokenSequence postfix;
| |
| − | tokenize(expression, postfix);
| |
| − | double value = evaluatePostfix(postfix);
| |
| − | printf(" value: %g\n", value);
| |
| − | destroy(postfix);
| |
| − | }
| |
| − | if (strcmp(command, "infix") == 0) {
| |
| − | fgets(expression, maxLine, stdin);
| |
| − | tokenSequence infix;
| |
| − | tokenize(expression, infix);
| |
| − | fixUnaryMinus(infix);
| |
| − | printf(" infix:");
| |
| − | printTokenExpression(infix);
| |
| − | tokenSequence postfix;
| |
| − | infixToPostfix(infix, postfix);
| |
| − | printf(" postfix:");
| |
| − | printTokenExpression(postfix);
| |
| − | double value = evaluatePostfix(postfix);
| |
| − | printf(" value of postfix: %g\n", value);
| |
| − | double value2 = evaluateInfix(infix);
| |
| − | printf(" value of infix: %g\n", value2);
| |
| − | destroy(infix);
| |
| − | destroy(postfix);
| |
| − | }
| |
| − | }
| |
| − | }
| |
| − | </syntaxhighlight>
| |
Oznamy
Prednášky
- Tento týždeň a budúci pondelok ešte prednášky v normálnom režime.
- Budúcu stredu 10.12. v prvej polovici prednášky informácie k skúške a rady k skúškovému všeobecne, potom doberieme posledné povinné učivo.
- Posledný týždeň semestra v pondelok 15.12. nepovinná prednáška o nepreberaných črtách jazykov C a C++, v stredu 17.12. prednáška pravdepodobne nebude.
Cvičenia a úlohy
- Cvičenia bežia normálne každý utorok, cvičenia v stredu už iba 2x.
- Tretí miniprojekt treba odovzdať do tohto štvrtka (4.12.) 22:00.
Semestrálny test
- Budúcu stredu o 18:10 v posluchárňach A a B, trvanie 45 minút.
- Rovnaké pravidlá ako prvý test.
- Bude obsahovať učivo po dnešnú prednášku a zajtrajšie cvičenia vrátane. Na opravnom termíne môže byť aj učivo z ďalších prednášok.
- Viac informácií na stránke Zimný semester, semestrálny test.
Na termíny skúšky sa bude dať prihlasovať od dnes 19:00
- Kapacita termínov bude obmedzená, prihláste sa teda radšej skôr, neskôr to môžete zmeniť.
- Ak vidíte konflikt niektorého termínu s hromadnou skúškou alebo písomkou z iného predmetu, dajte mi prosím vedieť čím skôr.
- Decembrový termín odporúčame hlavne študentom, ktorým programovanie nerobí problémy.
- Viac informácií o skúške je na stránke Zimný semester, skúška, spolu cez to prejdeme budúcu stredu.
- Na testovač budúcu stredu pridáme zopár tréningových príkladov na skúšku.
Binárne stromy
Na predmete sme už videli spájané zoznamy, ktoré tvoria uzly pospájané smerníkmi do reťaze. Binárne stromy sú o niečo zložitejšia štruktúra, v ktorej má každý uzol smerníky na dva ďalšie uzly, ktoré nazývame jeho deti.
Štruktúra pre uzol binárneho stromu
V každom uzle budeme pamätať hodnotu ľubovoľného typu dataType, napríklad int a smerníky na ľavé a pravé dieťa, ktoré môžu byť aj NULL, ak uzol príslušné dieťa nemá.
/* Typ prvkov ukladaných v uzloch binárneho stromu */
typedef int dataType;
/* Uzol binárneho stromu */
struct node {
// hodnota uložená v uzle
dataType data;
// smerníky na deti
node * left, * right;
};
OBR uzol, deti, rodic
Z týchto uzlov budeme vytvárať hierarchické štruktúry. Väčšinou ich kreslíme zhora nadol tak, aby deti boli pod rodičmi. Najvyššie položený uzol sa nazýva koreň.
OBR strom, koren
Definícia zakoreneného stromu
V diskrétnej matematike je strom množina uzlov alebo vrcholov a množina prepojení medzi nimi, ktoré nazývame hrany, ktoré spĺňajú určité podmienky. Presnú matematickú definíciu stromov uvidíte na predmete Úvod do kombinatoriky a teórie grafov (letný semester).
Na tomto predmete nás zaujímajú zakorenené stromy, v ktorých hrany majú určený smer rodičom k deťom (môžeme ich kresliť ako šípku). Zakorenený strom má spĺňať nasledujúce podmienky:
- Koreň nemá rodičov a každý iný uzol v strome okrem koreňa má práve jedného rodiča (jednu prichádzajúcu hranu)
- Do každého uzla sa vieme z koreňa dostať po hranách
Ako špeciálny prípad budeme povoľovať aj prázdny strom, ktorý neobsahuje žiadne uzly (ani koreň)
Binárne stromy sú špeciálnym prípadom zakorenených stromov, v ktorých má každý uzol najviac dve deti. V našich stromoch budeme rozlišovať ľavé a pravé dieťa.
Terminológia stromov
- Uzly zakoreneného stromu, ktoré nemajú žiadne dieťa, nazývame listami; zvyšné uzly nazývame vnútornými uzlami.
- Predkom uzla u nazveme ľubovoľný uzol v ležiaci na ceste z koreňa do u (vrátane u a koreňa). Naopak potom hovoríme, že u je potomkom uzla v.
- Podstromom stromu T zakoreneným v nejakom uzle v stromu T budeme rozumieť strom s koreňom v pozostávajúci zo všetkých jeho potomkov a všetkých hrán stromu T vedúcich medzi týmito uzlami.
Každý binárny strom je teda buď prázdny, alebo je tvorený jeho koreňom a dvoma podstromami – ľavým a pravým.
Anglické výrazy:
- uzol=node, vrchol=vertex, hrana=edge
- strom=tree, podstrom=subtree, koreň=root, list=leaf, predok=ancestor, potomok=descendant
Vytvorenie binárneho stromu
Nasledujúca funkcia vytvorí uzol binárneho stromu s dátami data, ľavým podstromom zakoreneným v uzle * left a pravým podstromom zakoreneným v uzle * right (parametre left a right sú teda smerníkmi na uzly). Ako výstup funkcia vráti smerník na novovytvorený uzol.
/* Vytvori uzol binarneho stromu */
node * createNode(dataType data, node * left, node * right) {
node * v = new node;
v->data = data;
v->left = left;
v->right = right;
return v;
}
Nasledujúca volanie tak napríklad vytvorí binárny strom so šiestimi uzlami zakorenený v uzle * root.
node * root = createNode(1,
createNode(2,
createNode(3, NULL, NULL),
createNode(4, NULL, NULL)),
createNode(5,
NULL,
createNode(6, NULL, NULL)));
Cvičenie: nakreslite binárny strom vytvorený predchádzajúcim volaním.
Použitie stromov, plán zvyšok semestra
- Dnes si ukážeme niekoľko funkcií, ktoré pracujú s binárnymi stromami všeobecne
- Ukážeme si aj prvý príklad použitia stromov: stromy reprezentujúce aritmetické výrazy
- V stredu budeme viac rozprávať o práci s aritmetickými výrazmi s použitím stromov aj bez neho
- Budúci pondelok si ukážeme binárne vyhľadávacie stromy, ktoré slúžia ako efektívna implementácia ADT dynamická množina
- Budúcu stredu si ukážeme prefixové stromy, ktoré nebudú binárne a ktoré implementujú ADT dynamická množina, ak prvky množiny sú reťazce
- Ďalšie príklady stromov uvidíte na skúške a vo vyšších ročníkoch
Prehľadávanie stromov a vypisovanie ich uzlov
Často je potrebné prejsť celý strom a spracovať (napríklad vypísať) hodnoty vo všetkých uzloch.
- V spájanom zozname sme prešli cyklom pomocou smerníkov next
- Tu ale máme dva smery pokračovania: left a right
- Aby sme prešli všetko použijeme rekurziu:
- Prvé rekurzívne volanie prejde všetko v ľavom podstrome
- Druhé rekurzívne volanie prejde všetko v pravom podstrome
Ak hodnoty vypisujeme, rozlišujeme tri poradia: hodnotu v koreni môžeme vypísať pred oboma podstromami, po nich alebo medzi ľavým a pravým podstromom. Tieto poradia sa nazývajú preorder, postorder a inorder.
Funkcie nižšie predpokladajú, že pre hodnoty typu dataType máme k dispozícii funkciu printDataType, ktorá ich v nejakom vhodnom formáte vypisuje.
/* Funkcia pre výpis hodnoty typu dataType */
void printDataType(dataType data) {
printf("%d ", data); // pre int
}
/* Vypíše podstrom s koreňom * root v poradí preorder */
void printPreorder(node * root) {
if (root != NULL) {
printDataType(root->data);
printPreorder(root->left);
printPreorder(root->right);
}
}
/* Vypíše podstrom s koreňom * root v poradí inorder */
void printInorder(node * root) {
if (root != NULL) {
printInorder(root->left);
printDataType(root->data);
printInorder(root->right);
}
}
/* Vypíše podstrom s koreňom * root v poradí postorder */
void printPostorder(node * root) {
if (root != NULL) {
printPostorder(root->left);
printPostorder(root->right);
printDataType(root->data);
}
}
Cvičenie:
- Čo vypíšu tieto funkcie pre strom vytvorený vyššie?
- Ako by sme spočítali súčet hodnôt uložených v uzloch stromu?
Aritmetické výrazy a stromy
Dnes a na budúcej prednáške sa budeme venovať spracovaniu aritmetických výrazov pozostávajúcich z reálnych čísel, operátorov +,-,*,/ a zátvoriek (,).
Hlavnou úlohou je vyhodnotenie daného výrazu; napríklad pre výraz
(65 - 3 * 5) / (2 + 3)
chceme vedieť povedať, že jeho hodnota je 10.
Ukážeme si
- ako výraz reprezentovať stromom, čo je veľmi pohodlné reprezentácia pre ďalšiu prácu (vyhodnotenie výrazu aj jeho ďalšie úpravy)
- dva ďalšie spôsoby zápisu výrazu vo forme textu, ktoré tiež umožňujú výrazy ľahko vyhodnocovať
- ako prevádzať výraz medzi týmito spôsobmi zápisu
Aritmetický výraz ako strom
Strom pre výraz
(65 – 3 * 5)/(2 + 3)
Aritmetické výrazy môžeme veľmi prirodzene reprezentovať vo forme stromu
- Operátory a čísla tvoria uzly
- Čísla tvoria listy stromu.
- Operátory tvoria vnútorné uzly stromu, každý z nich má dve deti zodpovedajúce podvýrazom pre jednotlivé operandy.
- Koreň stromu reprezentuje celý aritmetický výraz.
V uzloch teda niekedy potrebujeme uložiť ako dáta reálne číslo a niekedy operátor, čo môže byť jeden zo znakov '+', '-', '*' a '/'. Aby sme toto vyriešili, vytvoríme si miesto na oboje v zázname nazvanom token (v preklade niečo ako symbol).
struct token {
char op;
double val;
};
typedef token dataType;
- Ak štruktúra obsahuje číslo, op bude medzera a samotné číslo bude v položke val.
- Ak štruktúra reprezentuje iný symbol, tento symbol bude v položke op a položka val sa nebude používať.
Vytvorenie uzlov
Nasledujúce funkcie vytvoria nový vnútorný uzol (pre operátor) resp. nový list (pre číslo):
treeNode * createOp(char op, node * left, node * right) {
treeNode * v = new treeNode;
v->left = left;
v->right = right;
v->data.op = op;
return v;
}
treeNode * createNum(double val) {
treeNode * v = new treeNode;
v->left = NULL;
v->right = NULL;
v->data.op = ' ';
v->data.val = val;
return v;
}
Týmito funkciami teraz môžeme vytvoriť strom pre výraz (65 – 3 * 5)/(2 + 3):
treeNode * root = createOp('/',
createOp('-',
createNum(65),
createOp('*', createNum(3), createNum(5))),
createOp('+', createNum(2), createNum(3)));
Alebo po častiach:
treeNode * v65 = createNum(65);
treeNode * v3 = createNum(3);
treeNode * v5 = createNum(5);
treeNode * v2 = createNum(2);
treeNode * v3b = createNum(3);
treeNode * vKrat = createOp('*', v3, v5);
treeNode * vMinus = createOp('-', v65, vKrat);
treeNode * vPlus = createOp('+', v2, v3b);
treeNode * vDeleno = createOp('/', vMinus, vPlus);
Vyhodnotenie výrazu reprezentovaného stromom
Nasledujúca rekurzívna funkcia vypočíta hodnotu aritmetického výrazu reprezentovaného stromom s koreňom root.
- Ak je zadaný vrchol listom, vrátime hodnotu uloženú v položke val.
- V opačnom prípade rekurzívne spočítame hodnoty pre obidva podvýrazy a skombinujeme ich podľa typu znamienka.
- Celkovo veľmi jednoduchý a prirodzený výpočet.
double evaluateTree(treeNode * root) {
assert(root != NULL);
if (root->op == ' ') {
return root->val;
} else {
double valLeft = evaluateTree(root->left);
double valRight = evaluateTree(root->right);
switch (root->op) {
case '+':
return valLeft + valRight;
break;
case '-':
return valLeft - valRight;
break;
case '*':
return valLeft * valRight;
break;
case '/':
return valLeft / valRight;
break;
default:
assert(false);
break;
}
}
return 0; // realne nedosiahnutelny prikaz
}
Notácie na zápis výrazov
Infixová notácia
- Bežný spôsob zápisu aritmetických výrazov sa v matematike nazýva aj infixovou notáciou.
- Binárne operátory (ako napríklad +,-,*,/) sa v tejto notácii píšu medzi svojimi dvoma operandmi.
- Poradie vykonávania operácií sa riadi zátvorkami a prioritou operácií.
Napríklad
(65 – 3 * 5) / (2 + 3)
je infixový výraz s hodnotou 10.
Prefixová notácia
Pri prefixovej notácii (často podľa jej pôvodcu Jana Łukasiewicza nazývanej aj poľskou notáciou) sa každý operátor v aritmetickom výraze zapisuje pred svojimi dvoma operandmi.
Napríklad výraz (65 – 3 * 5) / (2 + 3) má prefixový zápis
/ - 65 * 3 5 + 2 3
Zaujímavosť: programovací jazyk Lisp a jeho varianty využívajú prefixovú notáciu na všetky výrazy, píšu však aj zátvorky, napríklad (+ 1 2).
Postfixová notácia
Pri postfixovej notácii (často nazývanej aj obrátenou poľskou notáciou) je situácia opačná: operátor sa zapisuje bezprostredne za svojimi dvoma operandmi.
Výraz (65 – 3 * 5) / (2 + 3) má teda postfixový zápis
65 3 5 * - 2 3 + /
- Postfixová a prefixová notácia sú pre človeka o niečo ťažšie čitateľné, než bežná infixová notácia (čo môže byť aj otázkou zvyku).
- Uvidíme však, že výrazy v postfixovej notácii sa dajú jednoducho vyhodnocovať.
- Výhodou postfixovej a prefixovej notácie oproti infixovej je aj to, že nepotrebujú zátvorky.
Notácie a vypisovanie stromu
- Ak aritmetický strom vypíšeme v preorder poradí, dostaneme prefixovú notáciu
- Ak aritmetický strom vypíšeme v postorder poradí, dostaneme postfixovú notáciu
- Ak aritmetický strom vypíšeme v inorder poradí, dostaneme niečo blízke infixovej notácii, ale treba pridať zátvorky
Pre preorder a postorder stačí prepísať funkciu printDataType, napr. takto:
/* Funkcia pre výpis hodnoty typu dataType = token */
void printDataType(dataType data) {
if (data.op == ' ') {
printf(" %g ", data.val);
} else {
printf(" %c ", data.op);
}
}
Pre inorder napíšeme vlastnú funkciu, ktorá pridáva zátvorky a pre istotu ich dá všade
- Ako by sme ich vypísali iba tam, kde treba?
- Čo vypíše pre výraz z príkladov vyššie?
** Funkcia vypíše aritmetický výraz v inorder poradí */
void printInorder(treeNode * root) {
if (root->data.op == ' ') {
printf("%g", root->data.val);
} else {
printf("(");
printInorder(root->left);
printf(" %c ", root->data.op);
printInorder(root->right);
printf(")");
}
}
Ďalšie funkcie na prácu so stromami
Likvidácia binárneho stromu
Nasledujúca rekurzívna funkcia zlikviduje celý podstrom zakorenený v uzle * root (t. j. uvoľní pamäť pre všetky jeho uzly).
- Opäť používa rekurziu na prejdenie celého stromu.
- Pozor na poradie príkazov, treba najskôr uvoľniť podstromy až potom zavolať delete na root, inak by sme stratili prístup k deťom.
- Všimnite si, ako sú riešené triviálne prípady. Funkcia ani nezisťuje, s akým typom uzla pracuje.
/* Zlikviduje podstrom s korenom * root (uvolni pamat) */
void destroyTree(node * root) {
if (root != NULL) {
destroyTree(root->left);
destroyTree(root->right);
delete root;
}
}
Výška binárneho stromu
- Hĺbkou uzla binárneho stromu nazveme jeho vzdialenosť od koreňa.
- Koreň má teda hĺbku 0, jeho deti majú hĺbku 1, atď.
- Výškou binárneho stromu potom nazveme maximálnu hĺbku niektorého z jeho vrcholov.
- Strom s jediným vrcholom má teda výšku 0; pre ostatné stromy je ich výška daná ako 1 plus maximum z výšok ľavého a pravého podstromu.
Nasledujúca funkcia počíta výšku stromu (kvôli elegancii zápisu pritom pracuje s rozšírením definície výšky stromu na prázdne stromy, za ktorých výšku sa považuje číslo -1).
/* Spočíta výšku podstromu s koreňom * root.
* Pre root == NULL vráti -1. */
int height(node * root) {
if (root == NULL) {
return -1;
}
// rekurzívne spočíta výšku ľavého a pravého podstromu
int hLeft = height(root->left);
int hRight = height(root->right);
// vráti max(hLeft, hRight) + 1
if (hLeft >= hRight) {
return hLeft + 1;
} else {
return hRight + 1;
}
}
Cvičenie: prepíšte funkciu tak, aby triviálnym prípadom bol list, nie prázdny strom. Funkcia teda vždy dostane smerník na neprázdny strom a nebude volať rekurziu na prázdne podstromy. Ktorá verzia je jednoduchšia? Ktorá sa vám zdá jednoduchšia na pochopenie?
Aká môže byť výška binárneho stromu?
Pre výšku h binárneho stromu s n uzlami platia nasledujúce vzťahy:
- Určite h ≤ n-1. Tento prípad nastáva, ak sú všetky uzly „navešané jeden pod druhý”.
- Strom s výškou h má najviac

- uzlov (ako možno ľahko dokázať indukciou vzhľadom na h).
- Z toho h ≥ log2(n+1)-1.
- Dostávame teda log2(n+1)-1 ≤ h ≤ n-1.
- Napríklad strom s milión vrcholmi má teda hĺbku medzi 19 a 999999.
Príklad: plné binárne stromy
Binárny strom výšky h s maximálnym počtom vrcholov 2h+1-1 sa nazýva plný binárny strom. Nasledujúca funkcia createFullTree vytvorí takýto strom a vráti smerník na jeho koreň. Jeho uzly pritom očísľuje 1, 2, 3,... (predpokladáme, že dataType je int) pomocou globálnej premennej count.
// ...
int count;
/* Vytvori plny binarny strom vysky height s datami uzlov count, count + 1, ... */
node * createFullTree(int height) {
if (height == -1) {
return NULL;
}
node * v = createNode(count, NULL, NULL);
count++;
v->left = createFullTree(height - 1);
v->right = createFullTree(height - 1);
return v;
}
int main() {
count = 1;
node * root = createFullTree(3);
printf("Vyska: %d\n", height(root));
printf("Inorder: ");
printInorder(root);
printf("\n");
printf("Preorder: ");
printPreorder(root);
printf("\n");
printf("Postorder: ");
printPostorder(root);
printf("\n");
destroyTree(root);
}
Cvičenie:
- Nakreslite strom aj s hodnotami v uzloch, ktorý vznikne pre výšku 2.
- Vo všeobecnosti opíšte poradie, v ktorom sa v uvedenom programe jednotlivým uzlom priraďujú ich hodnoty.
- Ako by ste v programe odstránili globálnu premennú count?
Zhrnutie