PROGRAMIRANJE C JEZIKOM - NR Totalh

214 downloads 1276 Views 2MB Size Report
PROGRAMIRANJE. C JEZIKOM. Nastavni materijal za studente .... 5 Uvod u programiranje C jezikom............................................................................................. 55.
PROGRAMIRANJE C JEZIKOM

Nastavni materijal za studente FESB-a.

Split, 2005/2006 Autor: Ivo Mateljan

1

Sadržaj 1 Uvod........................................................................................................................................... 5 2 Matematički i elektronički temelji računarstva ........................................................................ 13 2.1 Izjavna i digitalna logika ................................................................................................... 13 2.2 Brojevni sustavi i računska sposobnost računala .............................................................. 16 3 Izrada prvog C programa.......................................................................................................... 20 3.1 Strojni, asemblerski i viši programski jezici ..................................................................... 20 3.2 Prvi program u C jeziku .................................................................................................... 21 3.3 Struktura i kompiliranje C programa ................................................................................ 25 3.4 Integrirana razvojna okolina (IDE) ................................................................................... 27 3.5 Usmjeravanje procesa kompiliranja programom nmake................................................... 32 4 Kodiranje i tipovi podataka ...................................................................................................... 33 4.1 Kodiranje i zapis podataka ................................................................................................ 33 4.2 Memorija ........................................................................................................................... 40 4.3 Prosti tipovi podataka........................................................................................................ 42 4.4 Direktiva #define............................................................................................................... 46 4.5 Specifikatori printf funkcije .............................................................................................. 47 4.6 Pristup podacima pomoću pokazivača .............................................................................. 49 4.7 Unos podataka u memoriju računala ................................................................................. 52 4.8 Inicijalizacija varijabli....................................................................................................... 54 5 Uvod u programiranje C jezikom............................................................................................. 55 5.1 Postupak izrade programa ................................................................................................. 55 5.2 Algoritamska struktura C programa? ............................................................................... 57 5.3 Funkcije C jezika............................................................................................................... 63 5.4 Zaključak........................................................................................................................... 70 6 Izrazi i sintaksa C jezika........................................................................................................... 71 6.1 Izrazi.................................................................................................................................. 71 6.2 Automatska i explicitna pretvorba tipova ......................................................................... 78 6.3 Definiranje sinonima tipa pomoću typedef ....................................................................... 81 6.4 Formalni zapis sintakse C-jezika....................................................................................... 81 7 Proste i strukturalne naredbe C jezika...................................................................................... 87 7.1 Proste naredbe ................................................................................................................... 87 7.2 Strukturalne naredbe ......................................................................................................... 89 8 Nizovi..................................................................................................................................... 102 8.1 Jednodimenzionalni nizovi.............................................................................................. 102 8.2 Prijenos nizova u funkciju............................................................................................... 107 8.3 Višedimenzionalni nizovi................................................................................................ 110 9 Blokovi, moduli i dekompozicija programa........................................................................... 112 9.1 Blokovska struktura programa ........................................................................................ 112 9.2 Funkcionalna dekompozicija programa "od vrha prema dolje" ...................................... 120 9.3 Zaključak......................................................................................................................... 128 10 Rad s pokazivačima.............................................................................................................. 129

2

10.1 Tip pokazivača .............................................................................................................. 129 10.2 Operacije s pokazivačima.............................................................................................. 130 10.3 Pokazivači kao argumenti funkcije ............................................................................... 131 10.4 Pokazivači i nizovi ........................................................................................................ 132 10.5 Pokazivači i argumenti funkcije tipa niza ..................................................................... 134 10.6 Patrametri funkcije tipa void pokazivača ...................................................................... 136 10.7 Pokazivači na funkcije .................................................................................................. 137 10.8 Kompleksnost deklaracija ............................................................................................. 139 10.9 Polimorfne funkcije....................................................................................................... 141 10.10 Zaključak..................................................................................................................... 144 11 Nizovi znakova - string ........................................................................................................ 146 11.1 Definicija stringa ........................................................................................................... 146 11.2 Standardne funkcije za rad sa stringovima.................................................................... 148 11.3 Ulazno-izlazne operacije sa stringovima....................................................................... 151 11.4 Korisnički definirane ulazne operacije sa stringovima ................................................. 152 11.5 Pretvorba stringa u numeričku vrijednost ..................................................................... 153 11.6 Nizovi stringova ............................................................................................................ 155 11.7 Generator slučajnih brojeva .......................................................................................... 157 11.8 Argumenti komandne linije operativnog sustava.......................................................... 158 12 Dinamičko alociranje memorije ........................................................................................... 160 12.1 Funkcije za dinamičko alociranje memorije ................................................................. 160 12.2 Kako se vrši alociranje memorije.................................................................................. 163 12.3 Alociranje višedimenzionalnih nizova .......................................................................... 165 12.4 Standardne funkcije za brzi pristup memoriji ............................................................... 171 13 Korisnički definirane strukture podataka ............................................................................. 172 13.1 Struktura (struct)...................................................................................................... 172 13.2 Union – zajednički memorijski objekt za različite tipova podataka.............................. 180 13.3 Bit-polja......................................................................................................................... 181 13.4 Pobrojanji tip (enum).................................................................................................... 182 13.5 Strukture i funkcije za očitanje vremena....................................................................... 183 14 Leksički pretprocesor ........................................................................................................... 188 14.1 Direktiva #include ......................................................................................................... 188 14.2 Direktiva #define za makro-supstitucije........................................................................ 188 14.3 String operatori # i ##.................................................................................................. 190 14.4 Direktiva #undef............................................................................................................ 191 14.5 Direktive za uvjetno kompiliranje................................................................................. 192 15 Rad s datotekama i tokovima ............................................................................................... 194 15.1 Ulazno-izlazni tokovi .................................................................................................... 194 15.2 Binarne i tekstualne datoteke ........................................................................................ 195 15.3 Pristup datotekama ........................................................................................................ 195 15.4 Formatirano pisanje podataka u datoteku...................................................................... 197 15.5 Formatirano čitanje podataka iz datoteke...................................................................... 199 15.6 Znakovni ulaz/izlaz ....................................................................................................... 200 15.7 Direktni ulaz/izlaz za memorijske objekte .................................................................... 203 15.8 Sekvencijani i proizvoljni pristup datotekama .............................................................. 206 15.9 Funkcije za održavanje datoteka ................................................................................... 208 16 Apstraktni tipovi podataka - ADT........................................................................................ 210

3

16.1 Koncept apstraktnog dinamičkog tipa podataka ........................................................... 210 16.2 Stog i STACK ADT ...................................................................................................... 215 16.3 Primjena stoga za proračun izraza postfiksne notacije.................................................. 218 16.4 Red i QUEUE ADT....................................................................................................... 221 16.5 Zaključak....................................................................................................................... 224 17 Rekurzija i složenost algoritama .......................................................................................... 225 17.1 Rekurzivne funkcije ...................................................................................................... 225 17.2 Matematička indukcija .................................................................................................. 227 17.3 Kule Hanoja .................................................................................................................. 227 17.4 Metoda - podijeli pa vladaj (Divide and Conquer)........................................................ 230 17.5 Pretvorba rekurzije u iteraciju ....................................................................................... 232 17.6 Standardna bsearch() funkcija ....................................................................................... 234 17.7 Složenost algoritama - "Veliki - O" notacija................................................................. 236 17.8 Sortiranje ....................................................................................................................... 239 17.9 Zaključak....................................................................................................................... 248 18 Samoreferentne strukture i liste............................................................................................ 249 18.1 Samoreferentne strukture i lista..................................................................................... 249 18.2 Operacije s vezanom listom .......................................................................................... 250 18.3 Što može biti element liste ............................................................................................ 259 18.4 Lista sa sortiranim redoslijedom elemenata .................................................................. 260 18.5 Implementacija ADT STACK pomoću linearne liste ................................................... 265 18.6 Implementacija ADT QUEUE pomoću vezane liste..................................................... 267 18.7 Dvostruko vezana lista .................................................................................................. 269 18.8 Generički dvostrani red - ADT DEQUEUE.................................................................. 271 18.9 Zaključak....................................................................................................................... 280 19 Razgranate strukture - stabla ................................................................................................ 281 19.1 Definicija stabla ............................................................................................................ 281 19.2 Binarno stablo ............................................................................................................... 282 19.3 Interpreter prefiksnih izraza .......................................................................................... 291 19.4 Stabla s proizvoljnim brojem grana .............................................................................. 305 19.5 Prioritetni redovi i hrpe ................................................................................................. 309 19.6 Zaključak....................................................................................................................... 316 20 Strukture za brzo traženje podataka .................................................................................... 317 20.1 Tablice simbola i rječnici .............................................................................................. 317 20.2 Hash tablica ................................................................................................................... 318 20.3 BST - binarno stablo traženja........................................................................................ 333 20.4 Crveno-crna stabla......................................................................................................... 344 Literatura ................................................................................................................................... 354 Dodatak ..................................................................................................................................... 355 Dodatak A - Elementi dijagrama toka................................................................................... 355 Dodatak B - Gramatika C jezika ........................................................................................... 356 Dodatak C - Standardna biblioteka C jezika ......................................................................... 361 Index.......................................................................................................................................... 392

4

1 Uvod

Naglasci: • Što je računalo ? • Što je program ? • Kako se rješavaju problemi pomoću računala? • Računarski procesi i memorijski objekti • Apstrakcija, algoritam, program

Računalo ili kompjuter (eng. computer) je naziv za uređaje koji obavljaju radnje prema programima koje izrađuje čovjek. Sastavni dijelovi računala nazivaju se hardver, a programi i njihova dokumentacija nazivaju se softver. Prvotno su računala služila za obavljanje numeričkih proračuna, odatle i potječe naziv računalo. Danas računala služe za obradu različitih problema. Korisnike računala zanima kako se koristi računalo, a one koji izučavaju računala zanima: • • •

kako se izrađuje računalo, kako se izrađuje program i kako se rješavaju problemi pomoću računala.

Ovdje će biti pokazano kako se izrađuju programi i kako se programiranjem rješavaju različiti problemi. Bit će opisana i unutarnja građa računala. Za pisanje programa koristit će se programski jeziku C i asemblerski jezik.

Što je program? Program je zapis operacija koje računalo treba obaviti. Taj zapis može biti u obliku izvršnog programa ili u obliku izvornog programa. Izvršni program sadrži kôd operacija koje izvršava stroj računala, pa se naziva i strojni program. Izvorni program se zapisuje simboličkim jezikom koji se naziva programski jezik. Prevođenje izvornog programa u strojni program vrši se pomoću programa koji se nazivaju kompilatori (ili kompajleri).

Stroj računala Postoje dva tipa elektroničkih računala: analogna i digitalna. Analognim računalima se obrađuju kontinuirani elektronički signali. Digitalnim računalom se obrađuju, prenose i pamte diskretni elektronički signali koji u jednom trenutku mogu imati samo jedno od dva moguća stanja. Ta stanja se označavaju znamenkama 0 i 1, odatle i naziv digitalna računala (eng. digit znači znamenka). Programere i korisnike ne zanimaju elektronički signali u računalu, već poruka koju oni prenose – digitalna informacija. Brojevni sustav, u kojem postoje samo dvije znamenke, naziva se binarni brojevni sustav. U tom se sustavu može kodirati različite informacije koristeći više binarnih znamenki. Znamenka binarnog brojevnog sustava se naziva bit (kratica od eng. binary digit), a može imati samo dvije vrijednosti 0 ili 1. Niz od više bitova predstavlja kodiranu informaciju koja može

5

predstavljati operaciju koju računalo treba izvršiti ili neki smisleni podatak. Uobičajeno je za nizove bitova koristiti nazive iz Tablice 1.1. U binarnom nizu često se označava redoslijed bitova. Kratica LSB označava bit najmanjeg značaja (eng. least significant bit), a MSB označava bit najvećeg značaja (eng. most significant bit). Primjer je dan na slici 1.1.

Bit Nibl Bajt Riječ

je naziv za binarnu znamenku je naziv za skupinu od četiri bita (eng. nibble) s kojom se operira kao s cjelinom. ili oktet je naziv za skupinu od osam bita (eng. byte) s kojom se operira kao s cjelinom. je naziv za skupinu od više bajta (eng. word) s kojom se operira kao s cjelinom. Kod mikro računala za riječ se uzima skupina od 2 bajta. Kod većih računala za riječ se uzima skupina od 4 ili 8 bajta.

Tablica 1.1 Nazivi temeljnih binarnih nizova MSB LSB 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 1 0 0 0 1 1 1 1 1 0 1 0 1 1 0 1 nibl 3 nibl 2 nibl 1 nibl 0 bajt 1 bajt 0 Riječ

značaj bitova položaj bita binarni niz niz nibla niz bajta riječ

Slika 1.1 Označavanje binarnog niza Za označavanje većih nizova koriste se prefiksi: k M G T

(kilo) (mega) (giga) (tera)

⇔ ⇔ ⇔ ⇔

× 1024 k × 1024 M × 1024 G × 1024

Primjerice, 2 kB (kilobajta) = 2048 bajta, 3 Mb (megabita) = 3145728 bita. Digitalno računalo može pamtiti i izvršavati programe, te dobavljati, pamtiti i prikazivati različite informacije. Te informacije, koje su na prikladan način pohranjene u računalu, su programski podaci. broj bita – n

broj kombinacija – 2n

2 3 4 8 16 32

4 8 16 256 65536 4294967296

Tablica 1.2 Broj kombinacija s n bita Često se računala klasificiraju kao 8-bitna, 16-bitna, 32-bitna ili 64-bitna. Pod time se podrazumijeva da n-bitno računalo može operirati s nizom od n bita kao s cjelinom. Broj bita koji se koristi za opisivanje nekog podatka ovisi o veličini skupa kojem taj podatak pripada.

6

Razmotrimo skup podataka kiji se kodira s tri bita. Taj skup može imati maksimalno 8 elemenata jer se s tri bita može kodirati maksimalno osam kombinacija: 000, 001, 010, 011, 100, 101, 110, 111. Lako je pokazati da se s n-bita može kodirati podatke iz skupa od maksimalno 2n elemenata. Tablica 1.2 pokazuje da se udvostručenjem broja bitova značajno povećava skup vrijednosti koje se mogu kodirati u računalu. Operacije se u računala nikada ne izvršavaju samo s jednim bitom, već se istovremeno prenosi i obrađuje više bita. Kod svih računala usvojeno je da najmanja jedinica digitalne informacije, koja se kao cjelina prenosi i pamti u računalu, sadrži 8 bita, tj. jedan bajt. Na slici 1.2 prikazani su sastavni dijelovi digitalnog računala. Centralna procesorska jedinica (CPU – central processing unit) - kontrolira izvršenje programa i aritmetičko-logičkih operacija. CPU je kod mikro i mini računala izveden kao jedinstveni integrirani elektronički sklop (čip) i naziva se mikroprocesor. Uobičajeno je koristiti naziv procesor, bilo da se radi o mikroprocesoru ili o skupini čipova koji obavljaju funkcije CPU, a programe koji se izvršavaju u računalu naziva se procesima.

Slika 1.2. Opći prikaz digitalnog računala Radna memorija – pamti digitalne informacije za vrijeme dok je računalo u operativnom stanju. U memoriji se nalazi programski kôd i podaci s kojima operira procesor na temelju naredbi sadržanih u programskom kôdu. Memorija je napravljena od poluvodičkih elemenata u koje procesor može upisati i iz kojih može čitati digitalne informacije. Ta memorija se naziva RAM (eng. random access memory). Sa programerskog stajališta RAM predstavlja linearno uređen prostor u kojem se istovremeno može pristupiti grupi od 8 bita digitalne informacije (1 bajt). Položaj ove temeljne memorijske ćelije se označava prirodnim brojem i naziva se adresa. Jedan manji dio memorije je napravljen od poluvodičkih elemenata koji mogu trajno pamtiti digitalnu informaciju, a naziva se ROM (eng. read-only memory). U ROM-u je upisan program koji služi pokretanju osnovnih funkcija računala. U samom procesoru ugrađeno je nekoliko manjih memorijskih jedinica koje se nazivaju registri. Registri služe za privremeni smještaj programskog kôda i podataka iz radne memorije, te rezultata aritmetičko-logičkih operacije koje se izvršavaju u samom procesoru. Broj bita koji može biti pohranjen u jednom registru naziva se riječ procesora. Kod većine današnjih PC računala riječ procesora sadrži 32 bita (4 bajta), pa se kaže da su to 32-bitna računala. Vanjska memorija - služi za trajnu pohranu podataka. U tu svrhu koriste se magnetski i optički mediji (tvrdi disk, savitljive diskete, magnetske trake, optički diskovi,..). Podaci se na njima pohranjuju u organiziranom i imenovanom skupu podataka koji se nazivaju datoteka. Ulazne jedinice - služe za unos podataka (tipkovnica, miš, svjetlosna olovka, mikrofon,..). Standardna ulazna jedinica je tipkovnica. Izlazne jedinice - služe za prikaz informacija korisniku računala (video-monitor, pisač, zvučnik,...). Standardna izlazna jedinica je video-monitor.

7

Računalo u operativnom stanju održava poseban program koji se naziva operativni sustav. On vrši temeljne funkcija računala: inicijalizaciju računala i priključenih vanjskih jedinica pri uključenju električnog napajanja, kontrolu i redoslijed izvođenja programa, kontrolu korištenja memorije, pohranjivanje i obradu podataka, vremensku raspodjelu funkcija računala, itd. Operativni sustav nije nužno jedinstven program, već se sastoji od više programskih cjelina. On se, jednim dijelom, trajno nalazi u ROM memoriji računala. Programi s novakvim svojstvom nazivaju se rezidentni programi. Svi ostali programi moraju se prije izvršenja upisati u memoriju računala. Može se izvršiti funkcionalna podjela softvera na sistemski i aplikativni softver. U sistemski softver spadaju programi operativnog sustava, razni jezični procesori (interpreteri, kompilatori, emulatori itd.), programi za testiranje programa (debugger), servisni i uslužni programi, te razni pomoćni programi (matematički, statistički, baze podataka i uređivači teksta). Aplikativni softver predstavljaju različiti korisnički programi.

Kako se rješavaju problemi pomoću računala? Kada se rješava neki problem, do ideje za rješenje dolazi se analizom problema. Čovjeku je često dovoljno da već iz idejnog rješenja, koristeći svoju inteligenciju i predznanje, brzo dođe do potpunog rješenja problema. Računalo, samo po sebi, ne raspolaže s inteligencijom, već jedino može izvršavati određen broj jednostavnih operacija. Zbog toga, upute za rješenje problema pomoću računala moraju biti zapisane u obliku preciznog algoritma. Računarski algoritam je precizni opis postupka za rješenje nekog problema u konačnom broju koraka i u konačnom vremenskom intervalu. Pravila kako se piše algoritam nisu strogo određena. Algoritam se može definirati običnim govornim jezikom, tablicama i matematičkim formulama koje opisuju problem, te usmjerenim grafovima koji opisuju tok izvršenja programa. Primjer: Algoritam kojim se u pet koraka opisuje postupak zamjene točka na automobilu glasi: 1. ispitaj ispravnost rezervnog točka, 2. podigni auto, 3. skini točak, 4. postavi rezervni točak, 5. spusti auto.

Ovaj algoritam je jasan svakome tko je bar jednom mijenjao točak, međutim, računalo je izvršitelj kojem upute, iskazane nizom naredbi, nisu dovoljno jasne, jer ono ne zna (1) gdje se nalazi rezervni točak, (2) kako se provjerava njegova ispravnost, (3) kako i čime podignuti auto, te (4) kojim alatom se skida i postavlja točak. Zbog toga se algoritam dorađuje preciziranjem pojedinog koraka algoritma. Primjerice, u prvom koraku treba predvidjeti sljedeće naredbe: 1. ispitaj ispravnost rezervnog točka, 1.1. otvori prtljažnik 1.2. izvadi rezervni točak 1.3. uzmi mjerač tlaka iz kutije s alatom 1.4. izmjeri razinu tlaka 1.5. dok je razina tlaka manja 1,6 ponavljaj pumpaj gumu 15 sekundi izmjeri razinu tlaka

Podrazumijeva se da je naredba označena s 1. zamijenjena s nizom naredbi koje su označene s 1.1, 1.2,..1.5. Naredbe iskazane u koracima 1.1 do 1.4 su same po sebi jasne. Korak 1.5 treba dodatno pojasniti. Njime je opisan postupak pumpanja gume do neke razine tlaka. Pošto nitko ne može unaprijed znati koliko vremena treba pumpati gumu, da bi se postigla željena razina tlaka, predviđeno je da se dvije naredbe: "pumpaj gumu 15 sekundi" i "izmjeri razinu tlaka", višekratno ponavljaju, sve dok je razina tlaka manja od 1,6. Obje ove naredbe su

8

zapisane uvlačenjem reda kako bi se točno znalo koje naredbe treba ponavljati. Ovaj se tip naredbe naziva iteracija ili petlja. Uobičajeno se kaže da petlja ima zaglavlje, u kojem se ispituje uvjet ponavljanja petlje (dok je razina tlaka manja od 1,6 ponavljaj), i tijelo petlje, koje obuhvaća jednu ili više naredbi koje treba ponavljati. Naziv petlja podsjeća na činjenicu da se uvijek nakon izvršenja posljednje naredbe tijela petlje proces vraća na izvršenje prve naredbe, ali samo u slučaju ako je zadovoljen uvjet iskazan u zaglavlju petlje. Naredbe petlje nisu posebno numerirane jer su one povezane uz zaglavlje petlje, a izvršavaju se u kao jedinstvena složena naredba. Uobičajeno se niz naredbi koji predstavljaju jedinstvenu složenu naredbu naziva i blok naredbi ili samo blok. Uvjet ponavljanja petlje je izjava: "razina tlaka manja od 1,6". Odgovor na ovu izjavu može biti "Da" ili "Ne", ovisno o trenutno izmjerenoj razini tlaka. Ako je odgovor "Da", kažemo da je ispunjen uvjet ponavljanja petlje. Računarska se znanost koristi znanjima matematičke logike. U tom kontekstu ova izjava predstavlja tzv. predikatni izraz koji može imati samo dvije logičke vrijednosti: "istina" ili "laž", pa se kaže da je uvjet održanja petlje ispunjen ako je predikatni izraz istinit. Matematička logika je zapravo znanstveni temelj cijele računarske znanosti i o njoj će biti više govora u sljedećem poglavlju. Pokušajte dalje sami precizirati korake 2, 3 , 4 i 5. Ali pazite, kad pomislite da je problem ispravno riješen, moguće je da se opet potkrade neka greška. To se obično događa kada se ne predvide sve moguće situacije, odnosno stanja u koja se može doći. Primjerice, gornji algoritam nije predvidio slučaj da je guma probušena. Kakav bi razvoj događaja tada bio, ako bi se dosljedno poštovao postupak iz koraka 1.5? Pošto je kod probušene gume razina tlaka uvijek manja od 1,6, ispada da bi tada izvršitelj naredbi ponavljao postupak pumpanja gume beskonačan broj puta. Algoritam se može popraviti tako da korak 1.5 glasi: 1.5.

ako je tlak manji od 0.1 tada ako je guma probušena onda odnesi točak na popravak inače dok je tlak manji od 1.6 ponavljaj pumpaj gumu 15 sekundi izmjeri razinu tlaka

U ovom se zapisu koriste tzv. naredbe selekcije, prema sljedećoj logici izvršenja: ako je ispunjen uvjet tada izvrši prvi niz naredbi inače izvrši alternativni niz naredbi

Ovaj se tip naredbe zove uvjetna selekcija ili grananje, jer se nakon ispitivanja logičkog uvjeta vrši selekcija jednog od dva moguća niza naredbi, odnosno program se grana u dva smjera. Specijalni oblik selekcije je uvjetna naredba tipa: ako je ispunjen uvjet tada izvrši naredbu

Njome se određuje izvršenje neke naredbe samo ako je ispunjen neki uvjet. Koristeći naredbe selekcije, algoritam se može zapisati u obliku: 1. ispitaj ispravnost rezervnog točka, 1.1 otvori prtljažnik 1.1.1. uzmi najmanji od tri ključa 1.1.2. gurni ključ u bravu i lagano ga okreni na desno 1.1.3. podigni vrata prtljažnika

9

1.2. izvadi rezervni točak 1.2.1. podigni tapetu 1.2.2.ako je točak pričvršćen vijkom onda odvij vijak 1.2 .2. izvadi točak 1.3. uzmi kutiju s alatom 1.4. ispitaj razinu tlaka 1.4.1. izvadi mjerač tlaka iz kutije alata 1.4.2. postavi ga na zračnicu točka 1.4.3. očitaj razinu tlaka 1.5. ako je tlak manji od 0,1 onda 1.5.1. provjeri da li je guma probušena 1.5.2. ako je guma probušena onda odnesi točak na popravak inače, ako je tlak manji od 1,6 onda 1.5.3. otvori prednji poklopac motora 1.5.4. uzmi zračnu pumpu 1.5.5. dok je tlak < 1,6 ponavljaj postavi crijevo pumpe na zračnicu dvadeset puta pritisni pumpu na zračnicu postavi mjerač tlaka ispitaj razinu tlaka

Očito da je potrebno dosta raditi i dosta razmišljati da bi se napisao kvalitetan algoritam. Nakon što je napisan precizan algoritam rješenja problema, pristupa se pisanju izvornog programa. Kako se to radi bit će objašnjeno u sljedećim poglavljima. Važno je uočiti da su u zapisu algoritma korištena četiri tipa iskaza: 1. proste ili primitivne naredbe – iskazi koji označavaju jednu operaciju 2. blok naredbi – iskazi koji opisuju niz naredbi koje se sekvencijalno izvršavaju jedna za drugom, a tretiramo ih kao jedinstvenu složenu operaciju. 3. naredbe selekcije – iskazi kojima se logički uvjetuje izvršenje bloka naredbi. 4. iterativne naredbe ili petlje – iskazi kojima se logički kontrolira ponovljeno izvršenje bloka naredbi.

Računarski procesi i memorijski objekti Svaki proces rezultira promjenom stanja ili atributa objekata na koje procesi djeluju. Uobičajeno se stanje nekog promjenljivog objekta označava kao varijabla koja ima neko ime. U računalu se stanje objekta pamti u memoriji računala pa se algoritamske varijable mora tretirati kao memorijske objekte. Kada se u C jeziku napiše iskaz x = 5;

on predstavlja naredbu da se memorijskom objektu, imena x, pridijeli vrijednost 5. Ako se pak napiše iskaz: x = 2*x +5;

on predstavlja proces u kojem se najprije iz memorije očitava vrijednost memorijskog objekta x zapisanog na desnoj strani znaka =. Zatim se ta vrijednost množi s 2 i pribraja joj se numerička vrijednost konstante 5. Time je dobivena numerička vrijednost izraza s desne strane znaka =. Ta se vrijednost zatim pridjeljuje memorijskom objektu s lijeve strane znaka =. Konačni je rezultat ovog procesa da je varijabli x pridijeljena vrijednost 15. Ako bi prethodni iskaz tretirali kao matematički iskaz, on bi predstavljao jednadžbu s jednom varijablom, koja uvjetuje da je vrijednost varijable x jednaka 5.

10

Znak = u C jeziku ne predstavlja znak jednakosti, kao u matematici već operator pridjele vrijednosti. Njegova upotreba označava naredbu da se vrijednost memorijskog objekta s lijeve strane znaka = postavi na vrijednost izraza koji je zapisan s desne strane znaka =. Takove naredbe se zovu naredbe pridjele vrijednosti. Zbog ove nekonzistentnosti upotrebe znaka = u matematici u odnosu na upotrebu u nekim programskim jezicima (C, Basic, Fortan, Java) često se u općim algoritamskim zapisima operator pridjele vrijednosti zapisuje znakom ←, primjerice: x ← 5 x ← 2*x +5

Operacija pridjele vrijednosti posljedica je načina kako procesor obrađuje podatke u računalu. Naime, procesor može vršiti operacije samo nad podacima koji se nalaze u registrima procesora, pa je prije svake operacije s memorijskim objektima prethodno potrebno njihov sadržaj (vrijednost) prenijeti u registre procesora, a nakon obavljene operacije se sadržaj iz registra, koji sadrži rezultat operacije, prebacuje u memorijski objekt označen s lijeve strane operatora pridjele vrijednosti. Kaže se da procesor funkcionira po principu: dobavi-izvrši-spremi (eng. fetch-execute-store).

Što je to apstrakcija? Netko može primijetiti da je opisani proces zamjene točka loš primjer primjene računala. To je točno, jer ako bi se napravio robot, koji bi obavljao navedenu funkciju, onda bi to bila vrlo neefikasna i skupa upotreba računala. Međutim, malo iskusniji programer bi prema gornjem algoritmu mogao lako napraviti program kojim se animirano simulira proces zamjene točka. To je moguće jer, iako je prethodni algoritam apstraktan, on specificira procese u obliku koji se može ostvariti računarskim programom. Apstrakcija je temeljna mentalna aktivnost programiranja. U računarskoj se terminologiji pod pojmom apstrakcije podrazumijeva prikladan način zapisa o objektima i procesima koje se obrađuje pomoću računala, a da se pri tome ne vodi računa o tome kako je izvršena stvarna računarska implementacija, niti objekta niti procesa. Važna je samo ona pojavnost koja je određena apstrakcijom. Algoritam zapisan programskim jezikom predstavlja apstrakciju strojnog koda, a algoritam zapisan prirodnim jezikom predstavlja apstrakciju programskog jezika. Programski jezik služi da se formalnim jezikom zapiše procese i stanje memorijskih objekata u računalu, pa on predstavlja apstrakciju računarskih procesa i stanja memorije. Pomoću programskih jezika se piše program koji ponovo predstavlja neku novu apstrakciju, a u toku izvršenja programa moguće je daljnje usložnjavanje apstrakcije. Primjerice, korisnik CAD programa pokretima miša zadaje program za crtanje nekog geometrijskog oblika. S obzirom na način kako je izvršena apstrakcija računarskog procesa, može se izvršiti sljedeća klasifikacija programskih jezika:

1. Imperativni (proceduralni) programski jezici (C, Pascal, Modula-2, Basic, Fortran,..) 2. Objektno orijentirani programski jezici (C++, Java, C#, Eiffel, Objective C, Smaltalk, Modula-3, ..)

3. Funkcionalni programski jezici (Lisp, Sheme, ML, Haskel..) 4. Logički programski jezici (Prolog) 5. Jezici specijalne namjene: pretraživanje baza podataka (SQL), vizuelno programiranje (Delphi, Visual Basic), uređivanje teksta (Perl, TeX, HTML), matematički proračuni (Matlab). Imperativni programski jezici koriste iskaze koji su bliski naredbama procesora (to su naredbe pridjele vrijednosti, aritmetičko-logičke operacije, uvjetni i bezuvjetni skokovi te poziv

11

potprograma). Kod objektno orijentiranih jezika naglasak je na tome da varijable predstavljaju atribute nekog objekta, a funkcije predstavljaju metode pomoću kojih objekt komunicira s drugim objektima. Specifikacije atributa i metoda određuju klase objekata. Kod funkcionalnih se jezika ne koristi temeljna imperativna naredba pridjele vrijednosti, već se sva međudjelovanja u programu opisuju funkcijama. Teorijska podloga ovih jezika je u tzv. λračunu. Kod logičkih programskih jezika međudjelovanja se u programu opisuju predikatnim logičkim izrazima i funkcijama. Naglasak je na zapisu onoga “što program treba izvršiti”, za razliku od imperativnih jezika pomoću kojih se zapisuje “kako nešto izvršiti”. Apstrakcija je dakle, temeljna mentalna aktivnost programera. Ona je moguća samo ako se dobro poznaje programski jezik i programske algoritme za efikasno korištenje računarskih resursa. O tome će biti riječi u sljedećim poglavljima.

12

2 Matematički i elektronički temelji računarstva

Naglasci: • Izjavna logika • Logičke funkcije i predikati • Booleova logika • Temeljni digitalni sklopovi • Brojevni sustavi

2.1 Izjavna i digitalna logika Bit će navedeni osnovni pojmovi potrebni za razumijevanje izjavne logike (ili propozicijske logike), koji se intenzivno koristi u programiranju, i digitalne logike koja je temelj izgradnje digitalnog računala. Osnovni objekt kojeg proučava izjavna logika je elementarna izjava. Ona može imati samo jedno svojstvo - njome se izriče "laž" ili "istina". Primjerice, izjava "osam je veće od sedam" je istinita, a izjava "broj sto je djeljiv sa sedam" je laž. Pri označavanju izjava koristit će se slovo T (true) za istinitu izjavu i F (false) za lažnu izjavu. Rečenica "broj x je veći od broja y" ne predstavlja izjavu jer njena istinitost ovisi o veličini brojeva x i y. Ako se umjesto x i y uvrste brojevi dobije se izjava. Ovakve rečenice se nazivaju izjavne funkcije, a za x i y se kaže da su (predmetne) varijable. Odnos među varijablama, kojeg izjavna funkcija izriče, naziva se predikat. Označi li se u prethodnom primjeru predikat " ... je veći od.... " sa P, navedena izjavna funkcija se može zapisati u obliku P(x,y). Izjavne funkcije se prevode u izjave kada se uvrsti vrijednost predmetnih varijabli ili ako se uz izjavne funkcije primijene neodređene zamjenice svaki (oznaka ∀ koja se naziva univerzalni kvantifikator) ili neki (oznaka ∃ koja se naziva egzistencijalni kvantifikator). ∃x se čita i "postoji x". Primjerice, prethodna izjavna funkcija primjenom kvantifikatora u predikatnom izrazu (∀ y)(∃x)P(x,y) postaje izjava koja znači: "za svaki broj y postoji broj x takav da je x veći od y". Rezultat izjavne funkcije je logička vrijednost T ili F. Varijable koje sadrže logičku vrijednost nazivaju se logičke varijable. U programiranju se često koriste izjavne funkcije iskazane tzv. relacijskim izrazima primjerice a ← (x edit hello.c ↵ c:> cl hello.c ↵ c:> hello ↵ Hello world!

- poziv editora edit (↵ je tipka enter) i unos izvornog programa u datoteku hello.c - poziv kompilatora (Microsoft-program cl.exe) koji stvara izvršnu datoteku hello.exe - komanda za izvršenje programa hello.exe - rezultat izvršenja programa

Slika 3.1 Izgled komandnog prozora u Windows operativnom sustavu Primjer za OS Linux: $ vi hello.c ↵ $ gcc hello.c –o hello ↵ $ hello ↵ Hello world!

- poziv editora vi i unos datoteke hello.c - poziv kompilatora gcc, koji stvara izvršnu datoteku hello - komanda za izvršenje programa hello - rezultat izvršenja programa

22

Analiza programa "hello1.c": C programi se sastoje od niza potprograma koji se zovu funkcije C-jezika. U programu "hello1.c" definirana je samo jedna funkcija, nazvana main(). Ona mora biti definirana u svakom C programu, jer predstavlja mjesto početka izvršenja programa. Programer može definirati nove funkcije, svaku s jedinstvenim imenom, a mogu se koristiti i prethodno definirane funkcije iz standardne biblioteke funkcija C jezika. Radnje koje obavlja neka funkcija zapisuju se unutar tijela funkcije. Tijelo funkcije je omeđeno vitičastim zagradama. U ovom je slučaju u tijelu funkcije je iskaz koji predstavlja naredbu da se pomoću standardne C funkcije printf(), na standardnoj izlaznoj jedinici, ispiše poruka "Hello World!". Pojedini dijelovi programa "hello1.c" imaju sljedeći značaj: /* Prvi C program. */

Tekst omeđen znakovima /* i */ predstavlja komentar. Kompilator ne analizira komentare, već ih tretira kao umetnuto "prazno" mjesto.

#include

predstavlja pretprocesorsku direktivu. Ona označava da u proces kompiliranja treba uključiti sadržaj datoteke imena "stdio.h". Ta datoteka sadrži deklaracije funkcija iz standardne biblioteke C-jezika.

int main()

Ovo je zaglavlje funkcije imena main.

#include

int označava tip vrijednosti (cijeli broj) koji vraća funkcija na kraju svog izvršenja (u ovom programu to nema nikakvi značaj). {

{ označava početak tijela funkcije main.

printf("Hello world!\n");

Ovo je naredba za poziv standardne funkcije printf(), kojom se ispisuje niz znakova (string) koji je argument ove funkcije. \n predstavlja oznaku za prijelaz u novi red

ispisa. Znak točka-zarez označava kraj naredbe. Return 0;

main() "vraća" vrijednost 0, što se uobičajeno koristi kao oznaka uspješnog završetka programa.

}

} označava kraja tijela funkcije main.

U objašnjenju programskih iskaza korišteni su neki novi pojmovi (deklaracija, standardna biblioteka, pretprocesorska direktiva). Oni će biti objašnjeni u sljedećim poglavljima. Ako program nije napisan u skladu s pravilima jezika, tada kažemo da je program sintaktički pogrešan. Primjerice, ukoliko u prethodnom programu nije otkucana točka-zarez iza naredbe printf("Hello world!\n"), kao u programu "hello2.c",

23

/* Datoteka: hello2.c */ /* Hello s greškom */ #include int main() { printf("Hello world!\n") return 0; }

/* greška: nema ; */

tada kompilator, u ovom slučaju program cl.exe, ispisuje poruku da postoji sintaktička pogreška u sljedećem obliku: C:\>cl hello.c Microsoft (R) 32-bit C/C Optimizing Compiler Ver.12.00.8168 for 80x86 Copyright (C) Microsoft Corp 1984-1998. All rights reserved. hello.c hello.c(5) : error C2143: syntax error : missing ';' before 'return'

Poruka o greški ima sljedeće elemente: hello.c(5) – obavijest da je greška u retku 5 datoteke "hello.c", error C2143: syntax error - kôd i tip greške, missing ';' before 'return' – kratki opis mogućeg uzroka greške.

Na temelju dojave greške često je lako izvršiti potrebne ispravke u programu. Važno je uočiti da je kompilator pronašao grešku u petom retku, iako je pogrešno napisana naredba u četvrtom retku. Razlog tome je pravilo C jezika po kojem se naredba može pisati u više redaka, a stvarni kraj naredbe predstavlja znak točka-zarez. Pošto kompilator nije pronašao točku-zarez u četvrtom retku, kompiliranje je nastavljeno s petim retkom i tek tada je utvrđeno da postoji pogreška. Zadatak: Provjerite da li je sintaktički ispravno napisan sljedeći program: /* Datoteka: hello3.c * Zapis naredbe u više redaka */ #include int main() { printf ( "Hello world!\n" ); return 0; }

24

3.3 Struktura i kompiliranje C programa Na sličan način, kao u funkciji main(), može se neka druga grupa naredbi definirati kao posebna funkcija s prikladnim imenom. Primjerice, prethodni program se može napisati pomoću dvije funkcije Hello() i main() na sljedeći način: /* Datoteka: hello4.c * Program s korisnički definiranom funkcijom Hello() */ #include void Hello() { printf("Hello world\n"); } int main() { Hello(); return 0; }

Za razumijevanje ovog programa potrebno je upoznati pravila za definiranje i pozivanje funkcija. Funkcija se definira zaglavljem i tijelom funkcije. Zaglavlje funkcije se zapisuje na sljedeći način: ime funkcije se zapisuje nizom znakova koji sadrži slova, znamenke i znak '_', ali uz uvjet da je prvi znak niza slovo ili '_'. Ispred imena funkcije se navodi vrijednost koju funkcija vraća. Ako funkcija ne vraća nikakovu vrijednost tada se ispred imena piše riječ void, koja znači “ništa ili nevažno je”. Ovakve funkcije se nazivaju procedure. U njima se ne mora koristiti naredba return, već one završavaju kada se izvrši posljednje definirana naredba. Iza imena funkcije se, u zagradama, navode formalni argumenti funkcije, ako ih ima. Kasnije će biti objašnjeno kako se definiraju i koriste argumenti funkcije. Tijelo funkcije se zapisuje unutar vitičastih zagrada, a sadrži niz naredbi i deklaracija. Poziv funkcije je naredba za izvršenje funkcije, (tj. za izvršenje naredbi koje su definirane unutar funkcije). Zapisuje se na način da se prvo napiše ime funkcije, a zatim obvezno zagrade i argumenti funkcije, ako su prethodno definirani. Primjerice, u funkciji main() iskaz Hello(); predstavlja poziv funkcije Hello(). Poziv funkcije pokreće izvršenje naredbi koje su definirane u tijelu funkcije Hello(), tj. poziva se funkcija printf() s argumentom "Hello World\n". Nakon izvršenja te naredbe program se vraća, u funkciju main() na izvršenje prve naredbe koja je napisana iza poziva funkcije Hello(). Funkcija iz koje se pokreće izvršenje pozvane funkcije naziva se pozivna funkcija. U prethodnom primjeru funkcija Hello() je definirana prije funkcije main(). Taj redoslijed je određen pravilom da se funkcija može pozivati samo ako je prethodno definirana. Iznimka od ovog pravila je ako se koriste tzv. prototipovi funkcija (ili unaprijedne deklaracije funkcija).

25

Prototip ili deklaracija funkcije je zapis u koji sadrži zaglavlje funkcije i znak točka-zarez. On služi kao najava da je funkcija definirana negdje drugdje; u standardnoj biblioteci ili u programu iza mjesta njenog poziva ili u drugoj datoteci. U skladu s ovim pravilom dozvoljeno je prethodni program pisati u obliku: /* Datoteka: hello5.c: * C program s korisnički definiranom funkcijom Hello() * i prototipom funkcije Hello() */ #include void Hello(); int main() { Hello(); return 0; }

/* prototip ili deklaracija funkcije Hello()*/ /* definicija funkcije main()

*/

void Hello() /* definicija funkcije Hello() { printf("Hello world\n"); }

*/

U C jeziku se programi mogu zapisati u više odvojenih datoteka. Primjerice, prethodni program se može zapisati u dvije datoteke "hellomain.c" i "hellosub.c". U datoteci "hellomain.c" definirana je funkcija main() i deklarirana je funkcija Hello(). Definicija funkcije Hello() zapisana je u datoteci "hellosub.c".

/* Datoteka: hellomain.c */

/* Datoteka: hellosub.c */

void Hello();

#include

int main() { Hello(); return 0; }

void Hello() { printf("Hello world\n"); }

Izvršni program se može dobiti komandom: c:> cl

hellomain.c hellosub.c /Fe"hello.exe"

U komandnoj liniji su zapisana imena datoteka koje treba kompilirati. Zatim je komandnom preklopkom /Fe zapisano da izvršnu datoteku treba formirati pod imenom "hello.exe".

26

Slika 3.2 Proces formiranja izvršnog programa Proces formiranja izvršnog programa je prikazan na slici 3.2. Najprije leksički pretprocesor kompilatora unosi sve deklaracije iz datoteke "stdio.h" u datoteku "hellosub.c". Zatim kompilator prevodi izvorne datoteke "hellomain.c" i "hellosub.c" u objektne datoteke "hellomain.obj" i "hellosub.obj". U ovim datotekama se uz prijevod izvornog koda u strojni jezik nalaze i podaci o tome kako izvršiti poziv funkcija koje su definirane u drugoj datoteci. Povezivanje strojnog kôda, iz obje datoteke, u zajednički izvršni program obavlja program link.exe, kojeg skriveno poziva program cl.exe. Dobra strana odvajanja programa u više datoteka je da se ne mora uvijek kompilirati sve datoteke, već samo one koje su mijenjane. To se može ostvariti na sljedeći način: Prvo se pomoću preklopke /c kompilatorskom pogonskom programu cl.exe zadaje da izvrši prijevod u objektne datoteke, tj. c:> cl /c hellomain.c c:> cl /c hellosub.c

Time se dobiju dvije objektne datoteke: "hellomain.obj" i "hellosub.obj". Povezivanje ovih datoteka u izvršnu datoteku vrši se komandom: c:> cl hellomain.obj hellosub.obj /Fe"hello.exe"

Ako se kasnije promijeni izvorni kôd u datoteci "hellomain.c", proces kompiliranja se može ubrzati komandnom: c:> cl hellomain.c hellosub.obj /Fe"hello.exe"

jer se na ovaj način prevodi u strojni kôd samo datoteka "hellomain.c", a u procesu formiranja izvršne datoteke koristi se prethodno formirana objektna datoteka "hellosub.obj".

3.4 Integrirana razvojna okolina (IDE) Integrirana razvojna okolina Visual Studio omogućuje editiranje izvornog koda, kompiliranje, linkanje, izvršenje i dibagiranje programa. Sadrži sustav "on-line" dokumentacije o programskom jeziku, standardnim bibliotekama i programskom sučelju prema operativnom sustavu (Win32 API). Pomoću njega se mogu izrađivati programi s grafičkim korisničkim sučeljem i programi za konzolni rad. Nakon poziva programa dobije se IDE Visual Studio prikazan na slici 3.3.

27

Slika 3.3 Izgled Visual Studio pri pokretanju programa Najprije će biti opisano kako se formira projekt za konzolni tip programa. Prije pokretanja programa, neka se u direktoriju c:\My Documents\C2002\pog2\ nalaze izvorne datoteke hellomain.c i hellosub.c. Pokretanjem komande menija: File-New-Project, dobije dijalog za postavljanje novog projekta. U dijalogu prikazanom na slici 3.4 označeno je 1. da će projekt biti klase: Win32 Console Application, 2. upisano je ime direktorija d:\src\ 3. gdje će biti zapisana dokumentacija projekta imena hello. U ovom slučaju Visual Studio zapisuje dokumentaciju projekta u datotekama hello.vsproj i hello.sln (datoteka ekstenzije .sln sadrži opis radne okoline, a datoteka ekstenzije .vcproj sadrži opis projekta). Visual Studio također automatski formira dva poddirektorija: .\Release i .\Debug, u kojima će se nalaziti objektne i izvršne datoteke. (Debug direktorij je predviđen za rad kompilatora u obliku koji je prikladan za pronalaženje grešaka u programu) Pokretanjem komande: Project - Add to project – File, u dijalogu prikazanom na slici 3.9 odabiremo datoteke "hellomain.c" i "hellosub.c", iz direktorija c:\My Documents\cpp2001\pog3\. Dobije se izgled radne okoline kao na slici 3.10. Pokretanjem komande: Build – Build hello.exe vrši se proces kompiliranja i linkanja. Ako se izvrši bez greške, nastaje izvršni program imena hello.exe. Program hello.exe se može pokrenuti pozivom komande Build – Execute hello.exe (ili pritiskom tipki Ctrl+F5).

28

Slika 3.4 Dijalog za postavljanje novog projekta

Slika 3.5 Dijalog za postavljanje tipa Win32-Console projekta

29

Slika 3.6 Dijalog koji izvještava o postavkama novog projekta

Slika 3.7 Izgled radne okoline nakon formiranja novog projekta “hello”

30

Slika 3.9 Dijalog za umetanja datoteka u projekt

Slika 3.10 Izgled IDE s aktivnim editorom

31

3.5 Usmjeravanje procesa kompiliranja programom nmake Kada se program sastoji od velikog broja datoteka, procesom kompiliranja se može upravljati pomoću program imena nmake (make na UNIX-u) i specifikacije koja se zapisuje u datoteci koji se obično naziva makefile. Za prethodni primjer datoteka makefile može biti napisana na sljedeći način: # datoteka: makefile #Simbolička definicija za spisak objektnih datoteka OBJS = hellomain.obj hellosub.obj #progam se formira povezivanjem objektnih datoteka: hello.exe : $(OBJS) cl $(OBJS) /Fe"hello.exe" # $(...) znači umetanje prethodnih makro definicija # ovisnost objektnih datoteka o izvornim datotekama # i komanda za stvaranje objektne datoteke hellomain.obj : hellomain.c cl -c hellomain.c hellosub.obj : hellosub.c cl -c hellosub.c

Ako se ovu datoteku spremi pod imenom makefile, dovoljno je u komandnoj liniji otkucati: c:> nmake

i biti će izvršen cijeli postupak kompiliranja i linkanja izvršnog programa. Ako se pak ova datoteka zapiše pod nekim drugim imenom, primjerice "hello.mak", u komandnoj liniji, iza preklopke –f treba zadati i ime datoteke, tj. c:>nmake –fhello.mak

Kako se formira makefile. Temeljna pravila formiranja makefile datoteke su: • Komentari se u makefile zapisuju tako da redak započne znakom #. • Mogu se navesti različite simboličke definicije oblika: IME = text, što omogućuje da na mjestu gdje se piše $(IME) bude supstituiran text. • U makefile se zatim navodi niz definicija koje se sastoje od dva dijela: prvi dio opisuje ovisnost datoteka, a drugi opisuje kao se ta ovisnost realizira. Primjerice, u zapisu hellosub.obj : hellosub.c cl –c hellosub.c

• • •

U prvom retku je označena ovisnost sadržaja "hellosub.obj" o sadržaju "hellosub.c". U drugom retku je specificirana komanda koja se primjenjuje na datoteku "hellosub.c" Redak u kojem se specificira komanda mora započeti znakom tabulatora

Program nmake uspoređuje vrijeme kada su nastale međuovisne datoteke. Čim se promijeni sadržaj "hellosub.c", dolazi do razlike u vremenu nastanka međuovisnih datoteka, pa program nmake pokreće program za kompiliranje koji je specificiran u drugom retku. Korištenje makefile je vrlo popularno, posebno na Unix sustavima i kod profesionalnih programera, jer se pomoću simboličkih definicija lako može definirati proces kompiliranja na različitim operativnim sustavima.

32

4 Kodiranje i tipovi podataka

Naglasci: • kodiranje brojeva • kodiranje znakova • kodiranje logičkih vrijednosti • pojam tipa podataka • tipovi konstanti i varijabli u C jeziku • adrese i pokazivači • ispis i unos podataka U ovom se poglavlju se opisuje kako se u računalu kodiraju brojevi i znakovi, objašnjava se koncept tipa podataka i pokazuje karakteristike tipova u C jeziku.

4.1 Kodiranje i zapis podataka Kodiranje je postupak kojim se znakovima, numeričkim vrijednostima i drugim tipovima podataka pridjeljuje dogovorom utvrđena kombinacija binarnih znamenki. Ovdje će biti opisano kodiranje koje se koristi u C jeziku. S programerskog stajališta, važnije od samog načina kodiranja je veličina zauzeća memorije i interval vrijednosti koji se postiže kodiranjem. Također, važno je upoznati leksička pravila po kojima se zapisuju znakovne i numeričke konstante u "literalnom" obliku.

4.1.1 Kodiranje pozitivnih cijelih brojeva (eng. unsigned integers) Pozitivni cijeli brojevi (eng. unsigned integers), ili kardinalni brojevi, su brojevi iz skupa kojeg čine prirodni brojevi i nula. Način njihovog kodiranja je opisan u poglavlju 2. U C jeziku se literalne konstante, koje predstavljaju pozitivne cijele brojeve, mogu zapisati u decimalnom, heksadecimalnom i oktalnom brojevnom sustavu, prema slijedećem leksičkom pravilu: • • •

niz decimalnih znamenki označava decimalnu konstantu ukoliko prva znamenka nije nula. niz oktalnih znamenki označava oktalnu konstantu ako je prva znamenka jednaka nuli. niz heksadecimalnih znamenki, kojem prethodi prefix 0x ili 0X, označava heksadecimalnu konstantu.

Primjer: tri ekvivalentna literalna zapisa vrijednosti binarnog niza 011011 u C jeziku su: decimalna konstanta oktalna konstanta heksadecimalna konstanta

27 033 0x1B

4.1.2 Kodiranje cijelih brojeva (eng. integers) Cijeli brojevi (eng. integers) su brojevi iz skupa kojeg čine prirodni brojevi, negativni prirodni brojevi i nula, ili drukčije kazano, to su brojevi s predznakom (eng. signed integers). Većina današnjih procesora za kodiranje cijelih brojeva s predznakom koristi tzv. komplementni

33

brojevni sustav. Puni komplement n-znamenkastog broja Nx, u brojevnom sustavu baze x matematički se definira izrazom:

N x′ = x n − N x primjerice, u decimalnom sustavu komplement troznamenkastog broja 733 je 103-733= 277. Ako se n-znamenkasti broj i njegov komplement zbroje vrijedi da će n znamenki biti jednako nuli. U prijašnjem primjeru 733+277=1000, dakle tri znamenke su jednake nuli. U binarnom se sustavu puni komplement naziva komplement dvojke i vrijedi:

N 2′ = 2 n − N 2 Komplement dvojke se koristi za označavanje negativnih brojeva. Primjerice, komplement dvojke broja +1 za n=4 iznosi:

N 2′ = 2 4 − 1 = 10000 − 0001 = 1111 . Ako se broj i njegov komplement zbroje, rezultat treba biti nula. To vrijedi, u prethodnom primjeru, jer su prva četiri bita zbroja jednaka nuli. Peti bit je jednak jedinici, ali on se u 4bitnom sustavu odbacuje. U sustavu komplementa dvojke pozitivni brojevi uvijek imaju MSB=0, a negativni brojevi imaju MSB=1. Što se događa ako se zbroje dva pozitivna broja koji imaju bitove ispod MSB jednake jedinici. Primjerice, ako zbrojimo 4+5 ( u 4-bitnom sustavu) 0100 +0101 ----1001

dobije se rezultat koji predstavlja negativan broj u sustavu komplementa dvojke. Do prijelaza u područje komplementa ne bi došlo da je rezultat zbrajanja bio manji od 7, odnosno 24-1-1. Poopći li se prethodno zapažanje na brojeve od n-bita, može se zaključiti da operacija zbrajanja ima smisla samo ako je zbroj operanada manji od 2n-1-1. Zbog toga, najveći pozitivni broj koji se može predstaviti u sustavu komplementa dvojke iznosi: max_int = (0111...111) = 2n-1-1,

a najveći iznos negativnog broja iznosi: min_int = (1000...000) = -2n-1.

Uočite da postoji za jedan više negativnih brojeva od pozitivnih brojeva. Obični komplement binarnog broja (naziva se i komplement jedinice) dobije se zamjenom svih jedinica s nulom i obratno. Iznos broja. koji se dobije na ovaj način, računa se prema izrazu:

N 2 = 2n − N 2 − 1 Obični komplement nije pogodan za izražavanje prirodnih brojeva jer nije jednoznačno određena vrijednost nule, naime obični komplement od 0000 iznosi 1111. On služi za jednostavno izračunavanje punog komplementa, jer vrijedi:

N 2′ = N 2 + 1

34

Puni komplement se izračunava tako da se običnom komplementu pribroji jedinica, primjerice, komplement dvojke broja 6 u 8-bitnoj notaciji iznosi: 00000110 -------11111001 1 -------11111010

(+6) (obični komplement od +6) (dodaj 1) (-6 u komplementu dvojke)

Izračunajmo puni komplement od 0: 00000000 -------11111111 1 -------100000000

(0) (komplement od 0) (dodaj 1) (-0 u komplementu dvojke)

Jedinica predstavlja deveti bit. Ona se u 8-bitnom sustavu odbacuje pa rezultat opet predstavlja nulu. Komplement dvojke omogućuje jednoznačno određivanje nule, pa je podesan za kodiranje cijelih brojeva.

4.1.3. Kodiranje realnih brojeva Realni brojevi se u matematici zapisuju na način da se cijeli i decimalni dio odvoje decimalnim zarezom (pr. 67,098), a koristi se i ekponentni format (eng. scientific format). Primjerice, prethodni se broj može zapisati u obliku 0,67089⋅102. U programskom jeziku C koristi se sličan zapis kao u matematici, s razlikom da se umjesto decimalnog zareza koristi "decimalna točka", a potencija broja 10 se označava velikim ili malim slovom E. matematički zapis 1,789 0,789 -178,9⋅10-2 -0,01789⋅102

ekvivalentni zapis u C-jeziku 1.789 0.789 .789 -178.9e-2 -178.9E-2 -0.01789e2 -0.01789E2 -0.01789e+2

ili ili ili ili

Tablica 4.1. Matematički i programski zapis realnih brojeva Eksponentni format se sastoji od dva dijela: mantise i eksponenta eksponentni decimalni format = mantisa 10eksponent Mantisa se zapisuje kao obični decimalni broj s predznakom, a eksponent se zapisuje kao cijeli broj. Prednost korištenja eksponentnog formata je u lakšem zapisu vrlo velikih i vrlo malih brojeva. Uočite da se promjenom vrijednosti eksponenta pomiče položaj decimalnog zareza. Kodiranje s fiksnim položajem binarne točke (eng. fixed point numbers) Umjesto pojma decimalnog zareza uvodi se pojam binarne točke. Opći oblik zapisivanja realnog broja s fiksnim položajem binarne točke, u slučaju da se N znamenki koristi za

35

označavanje cijelih vrijednosti, a n znamenki za označavanje razlomljenih vrijednosti (po bazi 2: 1/2, 1/4, 1/8 itd.) glasi:

bN −1bN − 2 ...b0 • b−1b− 2 ...b− n , a iznos mu se računa prema izrazu: N .n2 = bN −1 2 N −1 + ... + b0 2 0 + b−1 2 −1 + b− 2 2 −2 + ... + b− n 2 − n ,

bi ∈ (0,1)

Ako se ovaj broj pomnoži s 2n može ga se u operacijama smatrati cijelim brojem, a nakon izvršenih aritmetičkih operacija rezultat se skalira za iznos 2-n. Ovaj oblik kodiranja ima brojne nedostatke, i koristi se samo u izuzetnim slučajevima. Kodiranje s pomičnim položajem binarne točke (eng. floating point numbers) Ideja eksponentnog formata uzeta je kao temelj za kodiranje realnih brojeva i u binarnom brojevnom sustavu. Kodiranje se vrši prema izrazu

F = ( − 1) m 2 e s

gdje m predstavlja mantisu, e je eksponent dvojke, a s∈(0,1) određuje predznak broja. Eksponent i mantisa se kodiraju u binarnom brojevnom sustavu. Eksponent se kodira kao cijeli broj, a mantisa kao binarni broj s fiksnim položajem binarne točke. Ideja je jednostavna: promjenom eksponenta pomiče se i položaj binarne točke iako se mantisa zapisuje s fiksnim položajem binarne točke. Primjerice, neka je broj kodiran u obliku: 0.00001xxxxxxxxx 2e

gdje x može biti 0 ili 1. Ovaj oblik zapisa realnog broja naziva se nenormalizirani zapis. Pomakne li se položaj binarne točke za 5 mjesta ulijevo dobije se ekvivalentni zapis 1.xxxxxxxxx00000 2e-5,

Posmak bitova ulijevo ekvivalentan je dijeljenju s dva, stoga se vrijednost eksponenta smanjuje za 5. Ovakav kodni zapis, u kojem je uvijek jedinica na prvom mjestu, naziva se normalizirani zapis. Značaj normaliziranog zapisa je u činjenici što se njime iskorištavaju svi bitovi mantise za kodiranje vrijednosti, dakle osigurava se veća točnost zapisa. Normaliziranim se oblikom ipak ne može kodirati veoma male vrijednosti, pa je tada pogodniji nenormalizirani zapis broja. Treba pojasniti i kako je kodirana vrijednost nula. Pogodno bi bilo da sva bitna polja pri kodiranju vrijednosti nula budu jednaka nuli (zbog logičkih operacija), ali pošto se za kodiranje eksponenta također koristi binarni zapis, vrijednost eksponenta nula se matematički koristi za označavanje brojeva većih od jedinice. Da bi se zadovoljilo zahtjevu kodiranja nule s nultim zapisom eksponenta uobičajeno je da se umjesto stvarne vrijednosti eksponenta kodira vrijednost: E = e + pomak,

gdje je pomak neka konstantna vrijednost, a odabire se na način da je jednak najnižoj vrijednosti eksponenta e, koji je negativna vrijednost Ovako zapisani eksponent naziva se pomaknuti eksponent. Značaj pomaka pokazuje sljedeći primjer. Neka je eksponent opisan s 8 bita. Tada se E kreće u rasponu od 0 do 255. Ako se uzme da je pomak=127, i da je E=0 rezervirano za kodiranje nule, onda se vrijednost binarnog eksponenta kreće u rasponu od -126 do +127.

36

Postoji više različitih formata zapisa mantise i eksponenta u binarnom kodu. Danas se gotovo isključivo koristi format koji je određen ANSI/IEEE standardom br.745 iz 1985. godine. Prema tom standardu koriste se dva tipa kodiranja: jednostruki format (32 bita) i dvostruki format (64 bita).

Slika 4.1 Format kodiranja realnih brojeva prema IEEE/ANSI standardu STANDARDNI IEEE/ANSI FORMAT REALNIH parametar Jednostruki (SINGLE) ukupan broj bita 32 (+1) broj bita eksponenta 8 broj bita za predznak 1 broj bita mantise 23 (+1) pomak +127 Emax 255 Emin 0 minreal (za nenorm.) (-1)s⋅1.4⋅10-45 minreal (za norm.) (-1)s⋅1.175⋅10-38 maxreal (-1)s⋅3.4028⋅10+38

BROJEVA dvostruki (DOUBLE) 64 (+1) 11 1 52 (+1) +1023 2047 0 (-1)s⋅2.225⋅10-324 (-1)s⋅2.225⋅10-308 (-1)s⋅1.797⋅10+308

Tablica 4.2. Standardni IEEE/ANSI format realnih brojeva Bitna karakteristika ovog standarda je da je u format za kodiranje realnog broja moguće upisati i šifru o ispravno obavljenoj matematičkoj operaciji (pr. dijeljenje s nulom dalo bi beskonačnu vrijednost, koju je nemoguće kodirati, pa se ta operacija izvještava kao greška). Binarno kodirani signal greške koristi format binarno kodiranih realnih brojeva, ali kako to nije broj, u standardu se opisuje pod nazivom NaN (Not a Number). Kodiranje s normaliziranim zapisom mantise je izvršeno na način da se ne upisuje prva jedinica, čime se podrazumjeva da je mantisa kodirana s jednim bitom više nego je to predviđeno u binarnom zapisu. Vrijednost pomaka i raspona eksponenta dana je tablicom 3.2. Vrijednost eksponenta Emin je iskorištena za kodiranje nule, a vrijednost Emax za kodiranje NaN-a i beskonačnosti. Zapis formata se interpretira na sljedeći način: 1. 2. 3. 4. 5.

Ako je E=Emax i m≠0 kodna riječ predstavlja NaN, bez obzira na vrijedost predznaka s. Ako je E=Emax i m=0 kodna riječ predstavlja (-1)s (∝). Ako je Emin"); /* 3. Pozovi funkciju scan s agumentom koji je adresa varijabe*/ scanf("%d", &unos); /* 4. obavi radnje s tom varijablom ......*/ /* 5. ispiši rezultat obrada*/ printf("\nOtkucali ste broj %d.\n", unos); }

return 0;

Kada se pokrene, ovaj program ispisuje poruku: c:> Molim otipkajte jedan cijeli broj >_

i čeka da korisnik otkuca jedan broj. Unos završava kada korisnik pritisne tipku . Primjerice, ako korisnik otipka 12345, program će završiti s porukom: Otkucali ste broj 12345.

Važno je zapamtiti da argumenti funkcije scanf() moraju biti izrazi čija je vrijednost adresa. U prethodnom primjeru to je adresa varijable unos. Adresa se dobije primjenom adresnog operatora & na varijablu unos. Obično se u format_unosa ne upisuje nikakvi dodatni tekst, kao što je bio slučaj kod printf() funkcije, iako je to dozvoljeno. Razlog tome je činjenica da se tada od korisnika očekuje da otipka i taj dodatni tekst. Primjerice, ako bi se koristila naredba scanf("Broj=%d", &unos);

i ako se želi unijeti vrijednost 25, onda korisnik mora otipkati "Broj=25". Ako bi otkucao samo broj 25, funkcija scanf() ne bi dala ispravan rezultat. Pomoću scanf() funkcije može se odjednom unijeti više vrijednosti, primjerice unos jednog cijelog broja, jednog realnog broja i jednog znaka, može se ostvariti samo s jednim pozivom funkcije scanf(); int i; double x; char c; .... scanf("%d%f%c", &i, &x, &c);

Pri unosu cijelih i realnih brojeva, funkcijom scanf(), podrazumijeva se da unos broja završava tzv. “bijelim” znakovima (razmak, tab, nova linija). Svi bijeli znakovi uneseni ispred

53

broja se odbacuju. To nije slučaj kada se unosi znak, jer i bijeli znakovi predstavljaju znak. Stoga, pri unosu znaka potrebno je, u formatu zapisa, eksplicitno zadati razmak od prethodnog unosa. Razmotrimo prijašnji primjer i pretpostavimo da korisnik želi unijet cijeli broj 67, realni broj 3.14 i znak 'Z'. Prema zadanom formatu on bi morao otkucati: 86 3.14Z

dakle, znak bi trebalo otipkati bez razmaka od prethodnog broja. Problem se može riješiti tako da se u format upisa unese razmak: scanf("%d%f% %c", &i, &x, &c);

iako i ovo može stvarati probleme u komuniciranju s korisnikom, jer se smije koristiti samo jedan razmak. Primjerice ako bi korisnik otipkao: 86 3.14

Z

ne bi bio unesen znak 'Z' već znak razmaka, jer su ispred znaka 'Z' dva mjesta razmaka. Navedeni problemi su razlog da programeri rijetko koriste scanf() funkciju za komuniciranje s korisnikom. Kasnije će biti pokazano da je za unos znakova pogodnije koristiti neke druge funkcije. Također, bit će pokazano kako dijagnosticirati da li je izvršen unos koji odgovara zadanom tipu varijable.

4.8 Inicijalizacija varijabli Temeljni uvjet korištenja neke varijable je da ona prethodno mora biti deklarirana. Samom deklaracijom nije određeno i početna (inicijalna) vrijednost varijable, pa prije korištenja varijable u izrazima, treba joj pridijeliti neku početnu vrijednost. Primjerice, u dijelu programa int main() { int y, x; x = 77; y = x + 7; ...

/* deklaracija varijabli x i y */ /* početna vrijednost varijable x */ /* početna vrijednost varijable y */

koriste se dvije varijable: x i y. Početno je varijabli x pridijeljena vrijednost 77, i pomoću nje je određena početna vrijednost varijable y. Kada ne bi bila određena vrijednost od x, program bi se kompilirao bez dojave pogreške, ali tada bi pri izvršenju programa bila neodređena vrijednost varijable y. Određivanje početnih vrijednosti varijabli važan je element programiranja. Prilikom izrade većih programa, ukoliko se koriste neinicijalizirane varijable, mogu nastati greške koje je teško otkriti. U C jeziku se početna vrijednost varijable može odrediti i u samoj deklaraciji. Primjerice, prethodni program se može napisati u obliku: int main() { int y, x = 77; y = x + 7;

/* deklaracija varijabli x i y */ /* i inicijalizacija x na vrijednost 77 */ /* početna vrijednost varijable y */

Inicijalizacija varijable je deklaracija u kojoj se određuje početna vrijednost varijable.

54

5 Uvod u programiranje C jezikom

Naglasci: • • • • • • • •

postupak izrade programa algoritamska struktura programa u C jeziku složena naredba, if-else selekcija i while-petlja standardne i korisničke funkcije definiranje prototipa funkcije zaglavlje i tijelo funkcije formalni i stvarni argumenti funkcije razvoj jednostavnih algoritama

U prethodnom je poglavlju opisano nekoliko jednostavnih C programa. Cilj je bio upoznati standardne tipove podataka i jednostavne postupke komuniciranja s korisnikom u dobavi i ispisu podataka. Sada će biti opisana algoritamska i funkcionalna struktura C programa te postupci izrade jednostavnih programa.

5.1 Postupak izrade programa Izrada se programa može opisati kao aktivnost koja se odvija u četiri temeljna koraka: 1. 2. 3. 4.

Definiranje zadatka i analiza problema. Izrada detaljne specifikacije i uputa za rješenje problema. Pisanje programa, dokumentacije i formiranje izvršnog programa. Testiranje programa.

Programer treba znati: • mogućnosti programskog jezika, • kako obraditi problem: o definiranje objekata obrade (podaci), o definiranje postupaka obrade (apstraktni i programski algoritmi), o definiranje korisničkog sučelja za unos podataka i prezentiranje rezultata obrade. • kako metodološki pristupiti razradi programa (strukturalno programiranje, modularno programiranje, objektno orijentirano programiranje), • kako optimalno iskoristiti računarske resurse i mogućnosti operativnog sustava računala, • koje softverske alate koristiti za razvoj programa. Većina od ovih pitanja bit će obrađena u narednim poglavljima. Postupak izrade manjih programa se može prikazati i dijagramom toka na slici 5.1. (korišteni su standardnim elementi za opis dijagrama toka, a opisani su u Dodatku 1).

55

Slika 5.1. Postupak izrade manjih programa Bitno je uočiti: o Izradi programa prethodi analiza problema i izrada algoritama za rješenja problema o Tijekom pisanja programa često je potrebno ispravljati sintaktičke pogreške. o Ukoliko se program ne izvršava u potpunosti, moguće je postojanje pogreške u korištenju računarskih resursa (pr. u korištenju memorije). Postojanje takovih pogrešaka se ispituje posebnim programima – dibagerima (eng. debugger). o Postupak programiranja ne može biti završen ako program pri izvršavanju iskazuje nelogične rezultate. Tada ne preostaje ništa drugo nego da se krene od početka i da se ponovo kritički sagleda zadatak programiranja. Postupci izrade velikih programa, koji obrađuju kompleksne sustave, ovdje neći biti razmatrani.

56

5.2 Algoritamska struktura C programa? U uvodnom su poglavlju opisani temeljni oblici zapisa programskih algoritma. Oni se sastoje od naredbi koje se izvršavaju jedna za drugom (sekvence), od naredbi selekcije i iterativnih naredbi (petlji). Kroz niz primjera bit će pokazano kako se ove naredbe zapisuju u C jeziku. Posebnu pažnju posvetit će se problemu koji se obrađuje. Prvo će se definirati zadatak, a zatim će se vršiti analiza problema. Moguće rješenje iskazat će se podesnim algoritmom. Zatim će biti pokazano kako se izvršenje tog algoritam može ostvariti programom napisanim u C jeziku. Na kraju će se analizirati napisani program i rezultati koje on iskazuje tijekom svog izvršenja. Zadatak: Napisati program kojim se računa vrijednost od 5! (čitaj: pet faktorijela). Analiza problema: Vrijednost n! u matematici naziva n-faktorijela, a definirana je formulom:

za n = 0 ⎧1 ⎪ n n! = ⎨ ⎪∏ k za n > 0 ⎩ k =1 Ovu se formulu može opisati i sljedećim zapisom: n! je jednako 1 ako je n=0, a za vrijednosti n>0, n! je jednako 1*2*3*..*n Rješenje: Trivijalno rješenje problema dano je u programu fact0.c. Najprije je deklarirana cjelobrojna varijabla nfac. Zatim je toj varijabli pridijeljena vrijednost umnoška konstanti 1*2*3*4*5, što odgovara vrijednosti 5!. Za ispis te vrijednosti korištena je standardna funkcija printf(). /* Datoteka fact0.c - Proračun 5! */ #include int main() { int nfact; nfact = 1 * 2 * 3 * 4 * 5; printf("Vrijednost 5! iznosi: %d\n", nfact); return 0; }

Nakon izvršenja programa dobije se ispis: Vrijednost 5! iznosi: 120

Pošto argument funkcije printf() može biti bilo koji izraz koji rezultira nekom vrijednošću, prethodni se program može napisati i u obliku: /* Datoteka fact01.c */ /* Proračun 5! unutar argumenta funkcije printf() */ #include int main() { printf("Vrijednost 5! iznosi: %d\n", 2 * 3 * 4 * 5); return 0;

57

}

Oba, prethodno napisana programa nisu od neke koristi, jer se njima računa nešto što čovjek može napamet puno brže riješiti.

5.2.1 Naredba iteracije – while petlja Cilj pisanja programa je poopćenje procesa obrade nekog problema na način da se dobije rezultat za različite vrijednosti ulaznih podatka. U tu svrhu definiran je sljedeći zadatak: Zadatak: Napisati program kojim se računa vrijednost od n!. Vrijednost n zadaje korisnik. Program mora obaviti sljedeće operacije: 1. Dobaviti vrijednost od n. 2. Izračunati vrijednost n!. 3. Ispisati vrijednost od n i n!. Postavlja se pitanje kako realizirati korak 2 ovog algoritma. Problem je u tome što se unaprijed ne zna vrijednost od n, jer tu vrijednost unosi korisnik programa. Analiza problema: Polazi se od definicije n-faktorijela n! = 1 n! = 1*2 * ..(n-2)*(n-1)*n

za n = 0 za n > 0

Lako je uočiti da vrijedi i sljedeće pravilo: n! = 1 za n=0 n! = n * (n-1)! za n>0 koje kazuje da se vrijednost od n! može izračunati iz prethodno poznate vrijednosti od (n-1)!. Koristeći ovu formulu, prijašnji problem proračuna 5! bi se mogao programski riješiti uvođenjem pomoćne cjelobrojne varijable k i sljedećim nizom naredbi: k k k k k

= = = = =

0; nfact = 1; k+1; nfact = k k+1; nfact = k k+1; nfact = k k+1; nfact = k

* * * *

nfact; nfact; nfact; nfact;

/* /* /* /* /* /*

stanje nakon izvršenja naredbi */ k jednak nuli, nfact jednak 1 */ k jednak 2, nfact jednak 2 */ k jednak 3, nfact jednak 6*/ k jednak 4, nfact jednak 24*/ k jednak 5, nfact jednak 120*/

Ovaj primjer pokazuje vrlo neefikasan način proračuna 5!, međutim, značajan je jer ukazuje da se do rezultata dolazi ponavljanjem istih naredbi. U ovom se slučaju naredba k=k+1; nfact=k*nfact;

ponavlja sve dok je vrijednost varijable k manja od 5, pa se može napisati algoritamsko rješenje u obliku iterativne petlje: 1. k = 0; nfact = 1; 2. dok je k 13

(Napomena: logički operator “i” se zapisuje s &&, a logička negacija znakom ! ispred logičkog izraza). Sada se korak 1.3 može napisati u obliku: if((n < 0) || (n > 13)) { printf("Otipkali ste nedozvoljenu vrijednost"); return 1; /* forsirani izlaz iz funkcije main */ }

pa kompletni program izgleda ovako: /* Datoteka fact2.c */ /* Proračun n!. Vrijednost od n unosi korisnik. */ /* Vrijednost od n mora biti unutar intervala [0,13]*/ #include int main()

61

{

int n, k, nfact; printf("Unesite broj unutar intervala [0,13]\n"); scanf("%d", &n); if((n < 0) || (n > 13)) { printf("Otipkali ste nedozvoljenu vrijednost"); return 1; /* forsirani izlaz iz funkcije main */ } nfact = 1; k = 1; while ( k < n) { k = k + 1; nfact = k * nfact; } printf("Vrijednost %d! iznosi: %d\n", n, nfact); return 0;

}

Konačno je ostvaren kvalitetan i robustan program. On za bilo koju ulaznu vrijednost daje rezultat nakon konačnog broja operacija. Ovo svojstvo se smatra temeljnim uvjetom koji mora zadovoljiti svaki programski algoritam.

5.2.3 Naredba selekcije: if-else naredba Radi vježbe i upoznavanja još jednog programskog iskaza – if-else naredbe, prethodni algoritam se može zapisati u ekvivalentnom obliku: Dobavi vrijednost od n. Ako je n >= 0 i n=0 i n= 0) && (n 13)) printf("Otipkali ste nedozvoljenu vrijednost"); else printf("Vrijednost %d! iznosi: %d\n", n, factorial(n)); }

return 0;

Bitno je uočiti da se u glavnom programu više ne koriste varijable k i nfact. Te varijable su deklarirane unutar funkcije factorial(), jer su one potrebne samo za vrijeme dok se izvršava ta funkcija. U C jeziku vrijedi opće pravilo da sve varijable, koje se definiraju unutar bloka ili tijela funkcije, zauzimaju memoriju samo dok se izvršava taj blok ili funkcija. Kada započne izvršenje funkcije, skriveno od korisnika rezervira se dio memorije za te varijable, i to u dijelu memorije koja se uobičajeno naziva stog (eng. stack). Nakon izvršenja funkcije, a prije nego se nastavi izvršenje programa iz pozivne funkcije, ta se memorija ponovo smatra slobodnom za korištenje. Ovo ujedno znači da se varijable, koje se deklariraju u nekoj funkciji, mogu koristiti samo u toj funkciji. One se stoga po dosegu imena ili vidljivosti (eng. scope) nazivaju lokalne varijable, a pošto im je vrijeme postojanja ograničeno na vrijeme u kojem se izvršavaju naredbe funkcije, nazivaju se i automatske varijable.

5.3.4 Funkcija za proračun ex Zadatak je napisati funkciju kojom se približno određuje vrijednost funkcije ex (e = 2. 718282) i rezultat usporediti s vrijednošću koja se dobije pomoću standardne funkcije exp(), kojoj je prototip - double exp(double x) - deklariran u datoteci "math.h". Metod: Koristeći razvoj u red: ex = 1 + x/1! + x2 /2! + x3 /3! + .. zbrajati članovi reda, za dati x, sve dok razlika od prethodnog rezultata ne bude manja od zadane preciznosti eps. Primjerice, za x = 1.0, i eps = 0.0001 trebat će zbrojiti 10 članova reda. Specifikacija funkcije: double my_exp(double x, double eps); Parametri:

x - vrijednost za koju se računa ex , tipa double eps - zadana preciznost proračuna, tipa double

Rezultat:

vrijednost tipa double, jednaka vrijednosti ex

Algoritam: Razvoj u red funkcije ex ima karakteristiku da se i-ti pribrojnik reda dobije tako da se prethodni pribrojnik pomnoži s x/i. Koristeći tu činjenicu, može se primijeniti sljedeći iterativni algoritam: unesi x i eps i =1, pribrojnik = 1; ex = pribrojnik, preth_ex = 0; dok je apsolutna vrijednost od (ex – preth_ex) manja od eps ponavljaj preth_ex = ex; pribrojnik = pribrojnik * x / i; ex = ex + pribrojnik; uvećaj i;

68

Napomena: apsolutna se vrijednost realnog broja x u C jeziku dobije primjenom funkcije double fabs(double x) koja je deklarirana u . Realizacija programa: /*Datoteka: ex.c*/ #include #include double my_exp(double x, double epsilon) { int i = 1; double pribroj = 1.0; double ex = 1.0, preth_ex = 0.0;

}

while (fabs( ex - preth_ex) > epsilon) { preth_ex = ex; pribroj = pribroj * x / i; ex = ex + pribroj; i = i + 1; } return ex; int main( void) { double eps, x, ex; printf(" Unesi x i preciznost eps:\n"); scanf("%lf%lf", &x, &eps); ex = my_exp(x, eps); printf(" e^%f = %f; (tocno: %f)\n", x, ex, return 0; }

exp(x));

Izvršenjem programa dobiju su rezultati: c:>ex Unesi x i preciznost eps: 1 .00001 e^1.000000 = 2.718282; (tocno: 2.718282) c:>ex.exe Enter x and the eps: 2 .0001 e^2.000000 = 7.389047; (tocno: 7.389056)

U prethodnom programu istim imenom (ex) su deklarirane varijable u funkciji main() i u funkciji my_exp(). Postavlja se pitanje: da li je to ista varijabla ili se radi o dvije različite varijable? Na to pitanje daju odgovor pravila dosega ili postojanosti identifikatora. Pravilo je da se u različitim blokovima mogu deklarirati varijable s istim imenom. To su onda različite varijable koje postoje samo u bloku u kojem su definirane. O tome će biti više govora u poglavlju 8. Na sličan način su definirane mnoge matematičke funkcije iz standardne biblioteke (vidi Dodatak C).

69

5.4 Zaključak Do sada su korišteni sljedeći elementi C jezika: 1. Varijable i funkcije su zapisivane simboličkim imenima. U iskazima deklaracije svim varijablama je uvijek označen tip. To omogućuje kompilatoru da rezervira memoriju potrebnu za smještaj vrijednosti varijable. 2. Numeričke konstante i stringovi su zapisivani u literalnom obliku – upravo onako kako se zapisuju i u govornom jeziku.. 3. Korišteni su različiti operatori pomoću kojih se formiraju aritmetički, relacijski i logički izrazi. 4. Korištene su naredbe kojima se određuje izvršenje procesa u računalu. Najprije su korištene tzv. proste naredbe: pridjela vrijednosti i poziv izvršenja standardnih funkcija printf() i scanf(). Zatim su korištene tzv. strukturalne naredbe: sekvenca naredbi koja se omeđuje vitičastim zagradama, while-petlja kojom se kontrolira tijek iterativnih procesa, ifnaredba, pomoću koje se uvjetno određuje izvršenje neke naredbe, te if-else naredba, pomoću koje se vrši selekcija naredbi. Kasnije će biti opisane još neke naredbe za kontrolu toka programa. 5. Opisan je jednostavni način interakcije s korisnikom programa. 6. Pokazano je kako se koriste funkcije iz standardne biblioteke i kako korisnik može definirati nove funkcije. 7. Pokazano je da se funkcija može pozivati višestruko. 8. Pokazano je da se proračuni u računalu mogu izvršiti s ograničenom točnošću. Na primjeru eksponencijalne funkcije pokazano je kako je implementirana većina trigonometrijskih funkcija. 9. Razvijen je algoritam za proračun n-faktorijela i izvršena implementaciju tog algoritma u C jeziku. Sam tijek razvoja algoritma može programerima - početnicima biti zbunjujući, jer su stalno vršene dodatne analize i dorada algoritma. Iskusniji programeri znaju da je to jedini ispravni način razvoja programa, jer se samo postupnom analizom i doradom programa može napraviti kvalitetan program. Razvoj programa postupnom analizom i doradom (ili razvoj u koracima preciziranja) je metoda koju su popularizirali E. Dijkstra, u knjizi "Structured Programming", Academic Press, 1972, i N. Wirth u članku "Program Development by Stepwise Refinement",CACM, April 1971. Sam postupak se može opisati na sljedeći način: 1. Formuliraj problem na način da bude potpuno jasno što program treba obaviti. 2. Formuliraj temeljni tijek algoritamskog rješenja običnim govornim jezikom. 3. Izdvoji pogodnu manju cjelinu i razloži je detaljnijim algoritmom. 4. Ponavljaj korak (3) dok se ne dobiju algoritmi koji se mogu zapisati programskim jezikom (ili pseudo-jezikom). 5. Odaberi dio algoritamskog zapisa i zapiši ga programskim jezikom. Pri tome odredi potrebne struktura podataka. 6. Sustavno ponavljaj korak (5) i pri tome povećaj razinu dorade programskih rješenja. Na kraju, mora se nažalost reći, da ni danas u programiranju nema gotovih recepata, pa i dalje vrijedi izneseni metodološki pristup razvoju programskih algoritama.

70

6 Izrazi i sintaksa C jezika

Naglasci: • aritmetički, logički i relacijski izrazi • pravila prioriteta i asocijativnosti • bitznačajni operatori • složeni operatori • ternarni izrazi • automatska i eksplicitna pretvorba tipova • typedef • sintaksa i leksika programskih jezika • BNF notacija za zapis sintakse

6.1 Izrazi Izrazi su zapisi koji sadrže operande i operatore. Svaki izraz daje neku vrijednost. Operandi mogu biti varijable, funkcije i konstante. U izrazima može biti više operatora i više različitih tipova operanada. S obzirom na složenost izraza razlikuju se: • • • •

Unarni izrazi – imaju samo jedan operator i jedan operand, Binarni izrazi – imaju dva operanda i jedan operator, Ternarni izrazi – imaju tri operanda i dva operatora, Složeni izrazi – sastoje se od više operanada, operatora i zagrada koje služe za grupiranje izraza. Pravilo je da se najprije računa vrijednost izraza koji je napisan u zagradama, a zatim se ta vrijednost tretira kao prosti operand. Ukoliko nema zagrada, tada za redoslijed izvršenja složenog izraza vrijede posebna pravila prioriteta i asocijativnosti djelovanja operatora.

S obzirom na upotrebu različitih operatora, izrazi mogu biti aritmetički, relacijski i logički. Bit će pokazno: • Kako se izvršavaju izrazi? • Koji su pravila prioriteta i asocijativnosti djelovanja operatora? • Kako se vrši pretvorba tipova ako u nekom izrazu postoji više različitih tipova?

6.1.1 Aritmetički izrazi Binarni aritmetički izrazi koriste dva operanda i jedan operator: + za zbrajanje, - za oduzimanje, * za mnnoženje, / za djeljenje i % za ostatak dijeljenja cjelobrojnih tipova (modulo operacija). Operandi mogu biti varijable, konstante i funkcije koja vraćaju numeričku vrijednost. Operator % se može primijeniti samo na cjelobrojne operande jer se njime dobija ostatak cjelobrojnog dijeljenja, primjerice izraz

71

x % 2

daje vrijednost ostatka dijeljenja s 2. Taj ostatak može biti 0 ili 1 (ako je 0, broj x je paran, a ako je 1, broj x je neparan). Unarni aritmetički izrazi imaju jedan operand i jedan operator: - za negaciju (daje negativnu vrijednost) i + za 'afirmaciju' (ne mijenja vrijednost operanda). Operatori se zapisuju ispred imena varijable, konstante ili funkcije koja vraća vrijednost. Prefiks i postfiks unarni operatori Prefiks i postfiks operatori: ++ i --, uvećavaju, odnosno umanjuju, vrijednost numeričkih varijabli za 1. Mogu se primijeniti ispred ili iza imena varijable, ++n; --n;

/* uvećava n za 1 */ /* umanjuje n za 1 */

Prefiks operator djeluje na operand prije nego se koristi njegova nova vrijednost. n = 5; x = ++n;

/* x je jednak 6, n je jednak 6 */

Postfiks operator djeluje na operand nakon korištenja njegove trenutne vrijednosti. n = 5; x = n++;

/*

x je jednak 5, n je jednak 6 */

Operandi na koje djeluju operatori ++ i -- moraju biti varijable. Asocijativnost i prioritet djelovanja operatora Kada u izrazima ima više operanada i operatora, redoslijed kojim se računa izraz određen je pravilima prioriteta i asocijativnosti. Prioritet djelovanja operatora određuje koji se podizraz prvi izvodi. Aritmetički operatori imaju sljedeći prioritet izvršenja: viši prioritet ..... niži prioritet

unarni operatori - + prefiks op (++ --) binarni operatori * / % binarni operatori + -

Primjerice, -2* a + b se izvodi kao da je napisano (((- 2)* a) + b). Asocijativnost određuje redoslijed izvođenja izraza koji imaju više operanada istog prioriteta. Svi aritmetički operatori imaju asocijativnost s lijeva na desno. a + b + c (( a + b) + c)

Redoslijed izvođenja se uvijek može predodrediti upotrebom zagrada. Tada se najprije izvršava izraz u zagradama. Kako se vrši potenciranje? U C jeziku ne postoji operator potenciranja. Kada je potrebno potencirati neki broj ili numeričku varijablu, može se koristiti dva postupka: 1. ako se potencira s cijelim brojem tada se potenciranje može realizirati pomoću višestrukog množenja, primjerice a3 se realizira izrazom a*a*a a-3 se realizira izrazom 1/(a*a*a)

72

2. ako se potencira s realnim brojem tada se može koristiti standardna funkcija double pow(double x, double y);

koja vraća realnu vrijednost koja je jednka xy. Ova funkcija je deklarirana u .

6.1.2 Relacijski i logički izrazi Relacijski ili uvjetni izrazi se sastoje se od dva operanda numeričkog tipa i sljedećih operatora: < >=

manje manje ili jednako jednako nije jednako veće veće ili jednako

Rezultat relacijskog izraza je vrijednost 0 ili 1. Primjerice, x = (a == b); x = (a != b); x = (a > b);

/* x je 1, ako je a jednako b, inače x je 0 */ /* x je 0, ako je a jednako b, inače x je 1 */ /* x je 1, ako je a veće od b, inače x je 0 */

Pošto u C-u ne postoji logički tip varijabli, nula predstavlja logičku vrijednost false, a nenulta vrijednost predstavlja logičku vrijednost true. Logički operatori su: && || !

logička konjunkcija (i) logička disjunkcija (ili) negacija

Djelovanje logičkih operatora se određuje prema pravilu: izraz1 && izraz2 inače 0 izraz1 || izraz2 !izraz

->

1 ako su oba izraza različita od nule,

-> ->

0 ako su oba izraza jednaka nuli, inače 1 0 ako je izraz različit od nule, inače 1

Asocijativnost relacijskih i logičkih operatora je s lijeva na desno, a prioritet je manji od aritmetičkih operatora viši prioritet

Aritmetički operatori

niži prioritet

= ==, != && ||

a + b < max || max == 0 && a == b

se izvršava kao: (( a + b) < max) || (max == 0 && (a == b))

Primjer: Godina je prestupna ako je djeljiva sa 4, a ne i s 100, ali godine koje su djeljive s 400 su uvijek prestupne godine. Ta se činjenicu može programski iskazati ne sljedeći način: if ((godina % 4 == 0 && godina % 100 != 0) || godina % 400 == 0)

73

printf("%d je prestupna godina\n", godina); else printf("%d nije prestupna godina \n", godina);

Primjer: Definirana je funkcija isupper() kojom se određuje da li neka cjelobrojna vrijednost predstavlja ASCII kod kojim su kodirana velika slova int isupper(int c) /* ukoliko je argument c iz intervala ASCII vrijednosti u kojem su */ /* velika slova, funkcija vraća vrijednost 1, inače vraća 0 */ { return (c >= 'A' && c ~

bitznačajni "i" ( AND) bitznačajni "ili" (OR) bitznačajno "ekskluzivno ili" (XOR) posmak bitova u lijevo posmak bitova u desno bitznačajna negacija (unarni op.) (komplement jedinice)

Bitznačajne operacije se provode na bitovima istog značaja. Bitznačajni "i" operator & se najčešće koristi za maskiranje bitova, primjerice nakon naredbe n = n & 0x000F;

u varijabli n će svi bitovi biti postavljeni na nula osim 4 bita najmanjeg značaja, bez obzira na vrijednost od n; 1010111000011011 & 0000000000001111 ---------------0000000000001011

n 0x000F rezultat

Bitznačajni "ili" operator | se najčešće koristi za postavljanje bitova, primjerice n = n | 0x000F;

ima učinak da se u varijabli n četiri bita najmanjeg značaja postavljaju na vrijednost 1, a ostali bitovi su nepromijenjeni; 1010111000011011 | 0000000000001111 ---------------1010111000011111

n 0x000F rezultat

Bitznačajni "ekskluzivno ili" operator ^ postavlja bitove na vrijednost 1 na mjestima gdje su bitovi oba operanda različiti, odnosno na nulu na mjestima gdje su bitovi oba operanda isti. Posmačni operatori djeluju tako da pomiču bitove udesno (>>) ili ulijevo (y, u suprotnom vrijednost od max će biti jednaka vrijednosti varijable y. Ternarni izraz je zapravo skraćeni oblik naredbe selekcije:

77

if(x>y) max = x; else max = y;

međutim, često je prikladnija njegova upotreba od naredbe selekcije jer ga se može koristiti u izrazima.

6.2 Automatska i explicitna pretvorba tipova Automatska pretvorba tipova Svaki izraz daje neku vrijednost čiji tip ovisi o tipu članova izraza. Kada su u nekom izrazu svi članovi i faktori istog tipa tada je i vrijednost izraza tog tipa. Primjerice, za float y = 5, x=2;

izraz y/x daje realnu vrijednost 2.5. Ako su x i y cjelobrojne varijable, int y = 5, x=2;

tada izraz y/x daje cjelobrojnu vrijednost 2 (ostatak dijeljenja se odbacuje). U C jeziku se svi standardni tipovi tretiraju kao numerički tipovi i može ih se koristiti u svim izrazima. Kada u nekom izrazu ima više različitih tipova tada kompilator u izvršnom kodu vrši automatsku pretvorbu tipova. Princip je da se uvijek izvršava jedna operacija s maksimalno dva operanda. Ako su ta dva operanda različitog tipa onda se prije označene operacije vrši pretvorba tipa niže opsežnosti u tip više opsežnosti. Opsežnost tipa, u redoslijedu od manje prema većoj opsežnosti je: char → int → unsigned → long → float → double.

Primjerice, ako se koriste varijable int j=5, k=7; float x=2.1;

u izrazu: j+7.1*(x+k)

on se izvršava sljedećim redoslijedom: 1. najprije se izvršava proračun izraza u zagradama. U tom izrazu se najprije vrijednost varijable k pretvara (kodira) u tip float, jer je drugi operand tipa float. Zatim se toj vrijednosti dodaje vrijednost varijable x. 2. Vrijednost dobivenog izraza se zatim množi s realnom konstantom 7.1, jer množenje ima viši prioritet od zbrajanja. 3. Konačno preostaje da se zbroji vrijednost varijable j s vrijednošću prethodno izračunatog izraza (7.1*(x +k)), koji je realnog tipa. Pošto je to izraz s dva različita tipa, najprije se vrši pretvorba vrijednosti varijable i u tip float, i tek tada se izvršava operacija zbrajanja.

78

Pretvorba tipova u naredbi pridjele vrijednosti Pretvorba tipova u naredbi pridjele vrijednosti se uvijek vrši tako da se vrijednost koja se dobije iz izraza koji je na desnoj strani pretvara u tip koji ima varijabla na lijevoj strani. U slučaju da je s lijeve strane tip veće opsežnosti pretvorba se uglavnom može izvršiti bez gubitka točnosti. Primjerice, nakon izvršenja naredbi float x; int i = 3; x = i; printf("x=%f", x);

bit će ispisano: x=3.00000. U slijedećem slučaju pretvorba tipa int u tip unsigned neće imati smisla. Nakon izvršenja naredbi: unsigned u; int i = -3; u = i; printf("u=%u", u);

bit će ispisano: u= 4294967293. Kada se u izrazima miješaju tipovi int i unsigned, logični rezultat možemo očekivati samo za pozitivne brojeve. Kada se s lijeve strane nalazi tip manje opsežnosti, pretvorba se vrši sa smanjenjem točnošću. Često se vrijednost tipa float ili double pridjeljuje cjelobrojnoj varijabli, primjerice za double d = 7.99; int i ; i = d; printf("i=%d", i);

bit će ispisano i = 7. Pravilo je da se pri pretvorbi realnog u cijeli broj odbacuje decimalni dio. To vrijedi bez obzira koliki je decimalni dio. U mnogim programskim zadacima pojavit će se potreba da se pretvorba realnog broja u cijeli broj obavi na način da se vrijednost cijelog broja što manje razlikuje od vrijednosti realnog broja. To znači da ako je d=7.99, tada je poželjno da se ova vrijednost pretvori u cjelobrojnu vrijednost 8. To se može postići tako da se prije pretvorbe u cijeli broj decimalnom broju doda vrijednost 0.5, ako je pozitivan, odnosno da se od decimalnog broja odbije vrijednost 0.5 ako je negativan. U tu svrhu može se definirati funkciju Double2Int(), koja vraća cjelobrojnu vrijednost realnog argumenta; int Double2Int(double x) { /* funkcija vraća cijeli broj koji je * najbliži relnoj vrijednosi x */ if(x>0)

79

return x+0.5; else return x-0.5; }

Ekplicitna pretvorba tipova Ukoliko se ispred nekog izraza ili varijable u zagradama zapiše oznaka tipa, primjerice (float) x

time se eksplicitno naređuje kompilatoru da se na tom mjestu izvrši pretvorba vrijednosti varijable x u tip float. Kada se oznaka tipa zapiše u zagradama to predstavlja operator pretvorbe tipa (eng. cast operator). Primjenu ovog operatora ilustrira program u kojem se vrijednost dijeljenja cijelog broja s cijelim brojem pridijeljuje realnoj varijabli. int main() { int i1 = 100, i2 = 40; float f1; f1 = i1/i2; printf("%lf\n", f1); return(0); }

Dobije se ispis: 2.000000

Pri dijeljenju je izgubljen decimalni dio iako je rezultat izraza i1/i2 pridijeljen realnoj varijabli. Zašto? Zato jer se pretvorba tipa vrši samo ako se u izrazu nalaze različiti tipovi. Pošto su u izrazu i1/i2 oba operanda tipa int izvršava se dijeljenje s cijelim brojevima. Ako želimo da se sačuva i decimalni dio može se primijeniti operator pretvorbe u jednom od tri oblika: ili ili

f = (float)i1/i2; f = i / (float)j; f = (float)i / (float)j;

Dovoljno je da se pretvorba tipa označi na samo jednom operandu, jer se izrazi računaju tako da se uvijek vrši pretvorba u tip veće opsežnosti. Pokažimo još jedan primjer u kojem je potrebno primijeniti operator pretvorbe tipova short int i = 32000, j = 32000; long li; li = (long)i + j;

Operator (long) je primjenjen zbog toga jer maksimalna vrijednosti za tip short int iznosi 32767. Stoga, ako bi se zbrojile dvije short int kodirane vrijednosti iznosa 32000 rezultat bi bio veći od 32767. Operator (long) ispred jednog operanda osigurava da će se zbrajanje izvršiti na način kao da su operandi tipa long.

80

6.3 Definiranje sinonima tipa pomoću typedef Kada se ispred deklaracije napiše typedef, primjerice typedef int cijelibroj;

time se označava da identifikator, u ovom slučaju cijelibroj, neće biti deklariran kao varijabla ili funkcija, već da taj identifikator postaje sinonim za tip koji je opisan deklaracijom. U ovom primjeru, identifikator cijelibroj postaje sinonim za tip int, pa ga se u kasnije može koristiti u drugim deklaracijama, na isti način kako se koristi i originalni tip, primjerice cijelibroj i;

/* deklaracija sa typedef tipom */

Važno je napomenuti da se pomoću typedef deklaracije stvaraju sinonimi tipova; a ne neki novi tipovi. Njihova je upotreba korisna za povećanje apstraktnosti programskog zapisa. Prema ANSI standardu, u C jeziku je definirano nekoliko typedef tipova kako bi se jasnije označilo područje njihove primjene. Primjerice, size_t predstavlja tip unsigned int, kojim se često označava veličina, u bajtima, objekata smještenih u datotakama ili u memoriji. Implementacija je provedena deklaracijom typedef unsigned int size_t;

u datoteci "stddef.h". Drugi primjeri su FILE, time_t, ptrdiff_t i wchar_t (pogledajte njihovo značenje u opisu standardne C-biblioteke).

6.4 Formalni zapis sintakse C-jezika Pisanje programa podliježe jezičnim pravilima: 1. leksička pravila određuju kako se tvore leksemi na zadanom alfabetu (ASCII skup), 2. sintaktička (gramatička) pravila određuju kojim se redom leksemi slažu u programske iskaze, 3. semantička pravila određuju značenje programskih iskaza. Leksička struktura C jezika se temelji na pravilima koja određuju kako se formiraju leksemi jezika (niz znakova koji čini prepoznatljivu nedjeljivu cjelinu), na zadanom alfabetu (ASCII skup znakova). Temeljne leksičke kategorije su: 1. Ključne riječi jezika (if, while, else, do, int, char, float,..) služe za definiranje programskih iskaza. Pišu se malim slovima. 2. Identifikatori služe za zapis imena varijabli, funkcija i korisničkih tipova. Pišu se pomoću niza velikih i malih slova, znamenki i znaka podvlake ('_'), uz uvjet da prvi znak u nizu mora biti slovo ili podvlaka. 3. Literalne konstante služe za zapis numeričkih i tekstualnih (znakovnih) konstanti (pr. 135, 3.14, 'A', "Hello World").

81

4. Operatori (+,-*/,..=, []..(), &, .+=,*=.) služe označavanju aritmetičko-logičkih i drugih operacija koje se provode sa memorijskim objektima (funkcije i varijable) i konstantama. 5. Leksički separatori su znakovi koji odvajaju lekseme. Jedan ili više znakova razmaka, tabulatora i kraja retka tretiraju se kao prazno mjesto, kojim se razdvajaju leksemi. Operatori, također, imaju značaj leksičkih separatora. Znak točka-zarez (';') predstavlja specijalni separator koji se naziva terminator naredbi. 6. Komentar se piše kao proizvoljni tekst. Početak komentara se označava znakovima /*, a kraj komentara s */. Komentar se može pisati u bilo kojem dijelu programa, i u više linija teksta. Mnogi kompilatori kao komentar tretiraju i tekst koji se unosi iza dvostruke kose crte //, sve do kraja retka. 7. Specijalne leksičke direktive su označene znakom # na početku retka. Izvršavaju se prije procesa kompiliranja, pa se nazivaju i pretprocesorske direktive. Primjerice, #include je pretprocesorska direktiva kojom se određuje da se u proces kompiliranja uvrsti sadržaj datoteke imena stdio.h. Kao što se zapis u prirodnom jeziku sastoji od različitih elemenata (subjekt, predikat, pridjev, rečenica, poglavlje itd.), tako se i zapis u programskom jeziku sastoji od temeljnih elemenata, koje prikazuje tablica 6.2. Elementi programa Tipovi Konstante Varijable Izrazi Naredbe ili iskazi

Funkcije (potprogrami) Kompilacijska jedinica

Značenje oznake za skup vrijednosti s definiranim operacijama literalni zapis vrijednosti osnovnih tipova imenovane memorijskih lokacije koje sadrže vrijednosti nekog tipa zapis proračuna vrijednosti kombiniranjem varijabli, funkcija, konstanti i operatora zapisi pridjele vrijednosti, poziva funkcije i kontrole toka programa imenovano grupiranje naredbi skup međuovisnih varijabli i funkcija koji se kompilira kao jedinstvena cjelina

Primjer

int , float , char 0 , 123.6 , "Hello" i , sum sum + i sum = sum + i; while (--i) if(!x).. else ..; main() printf(...) datoteka.c

Tablica 6.2 Temeljni elementi zapisa programa u C jeziku Navedeni elementi jezika se iskazuju kombinacijom leksema prema strogim gramatičkim, odnosno sintaktičkim pravilima, koji imaju nedvosmisleno značenje. U prirodnim jezicima iskazi mogu imati više značenja, ovisno o razmještaju riječi, o morfologiji (tvorba riječi) i fonetskom naglasku. U programskim jezicima se ne koristi morfološka i fonetska komponenta jezika, pa se gramatika svodi na sintaksu, također, dozvoljen je samo onaj raspored riječi koji daje nedvosmisleno značenje. Uobičajeno se kaže da gramatika programskih jezika spada u klasu bezkontesktne gramatike.

82

Slika 2.1. Osnovne faze u procesu kompiliranja Za opis sintakse nekog jezika koristi se posebni jezik koji se naziva metajezik. Jezik koji se opisuje metajezikom naziva se ciljni jezik. Za opis semantike nekog jezika ne postoje prikladni metajezici već se semantika izražava opisno, primjenom prirodnih jezika. Prije nego se izvrši opis metajezika, koji će biti upotrijebljen za opis sintakse C jezika, bit će opisani neki pojmovi iz teorije programskih jezika. Na slici 2.1 ilustriran je proces kompiliranja. On se odvija na sljedeći način. Izvorni kod može biti spremljenu u jednoj datoteci ili u više datoteka koje se u toku jezičkog pretprocesiranja formiraju kao jedna datoteka, koja se naziva kompilacijska jedinica. Zatim se vrši leksička analiza izvornog koda, na način da se izdvoje leksemi (nizovi znakova koji predstavljaju nedjeljivu cjelinu). Ukoliko je leksem zapisan u skladu s leksičkom strukturom jezika on predstavlja terminalni simbol jezika (token) kojem se u radu kompilatora pridjeljuje jedinstveno značenje. U jezičke simbole spadaju: ključne riječi (if, else, while,...), specijalni simboli (oznake operatora i separatora), identifikatori (imena varijabli, konstanti, funkcija, procedura i labele), literalne numeričke i tekstualne konstante. Pojedinom simbolu pridjeljuju se različiti atributi koji se koriste u procesu generiranja koda. Primjerice, za varijable se unosi atribut koji opisuje tip varijable, ili uz literalno zapisanu numeričku konstantu se unosi i binarno kodirana numerička vrijednost konstante. Sintaktički analizator (parser) dobavlja jezičke simbole i određuje da li su oni grupirani u skladu s definiranom sintaksom. Ukoliko je to zadovoljeno, vrši se prevođenje u objektni kod usklađeno sa semantikom jezika. Pogreške u procesu kompiliranja se dojavljuju kao: • • •

leksičke pogreške sintaktičke pogreške semantičke pogreške

(pr. nije ispravno zapisano ime varijable) (pr. u aritmetičkom izrazu nisu zatvorene zagrade) (pr. primijenjen operator na dva nekompatibilna operanda)

U programu mogu biti prisutne i logičke pogreške (pr. petlja se ponavlja beskonačno). Njih može otkriti korisnik tek prilikom izvršenja programa. Za pojašnjenje navedenih pojmova razmotrimo iskaz: if (a > 3) max = 5.4; else max = a;

83

Ovaj iskaz predstavlja ispravno zapisani sintaktički entitet - IskazIf. U njemu se pojavljuju sljedeći simboli: ključne riječi (if, then, else), operatori (>, =), identifikatori varijable (a i max), numeričke konstante (3 i 5.4) i terminator iskaza (;). Napomenimo da "razmak" predstavlja leksički separator. On se ne smatra simbolom jezika i može se umetnuti između leksema proizvoljan broj puta. Odnos leksema, tokena i atributa prikazuje donja tablica. Leksem "if", "else" "max", "a" "=", ">" ";" ( ...) "5.4", "3"

kategorija tokena ključna riječ Identifikator operatori terminator naredbe separator izraza konstanta

atribut varijabla numerička vrijednost: 5.4 i 3

Za IskazIf u C jeziku vrijedi sintaktičko pravilo: IskazIf

"je definiran kao" if (Izraz) Iskaz else Iskaz "ili kao" if (Izraz) Iskaz

Gornji iskaz zadovoljava ovo sintaktičko pravilo jer (a>3) predstavlja relacijski izraz, dakle predstavlja sintaktički entitet Izraz, a iskazi x=5.4; i x=a; predstavljaju iskaze dodjele vrijednosti, dakle pripadaju sintaktičkom entitetu Iskaz. Ako se izneseno sintaktičko pravilo shvati kao zapis u nekom sintaksnom metajeziku onda IskazIf, Izraz i Iskaz predstavljaju metajezičke varijable koje u odnosu na ciljni jezik predstavljaju neterminalne simbole, "je definiran kao" i "ili kao" su metajezički operatori, a leksemi: if, then i else i znakovi zagrada su metajezičke konstante koje odgovaraju simbolima ciljnog jezika, pa se nazivaju terminalni simboli ili tokeni. Uočimo da "ili kao" operator ima značaj logičkog operatora ekskluzivne disjunkcije. Sintaktička pravila, kojima se jedan neterminalni simbol definira pomoću niza terminalnih i/ili neterminalnih simbola, nazivaju se produkcije jezika. Prema ANSI/ISO standardu produkcije C-jezika se zapisuju na sljedeći način: 1. Operator "je definiran kao" je zamijenjen znakom dvotočke, a produkcije imaju oblik: neterminalni_simbol : niz terminalnih i/ili neterminalnih simbola 2. Alternativna pravila ("ili kao") se pišu u odvojenim redovima. 3. Neterminalni simboli se pišu kurzivom. 4. Terminalni simboli se pišu na isti način kao u ciljnom jeziku 5. Opcioni simboli se označavaju indeksom opt (Simbolopt ili Simbolopt). Primjerice, zapis produkcije if-else iskaza glasi IskazIf : if (Izraz) Iskaz if (Izraz) Iskaz

else Iskaz

Ovo se pravilo može se napisati i na sljedeći način: IskazIf : if (Izraz) Iskaz ElseIskaz : else Iskaz

ElseIskazopt

84

U prvom je pravilu uveden je ElseIskaz kao opcioni neterminalni simbol. Ako postoji, onda je njegova sintaksa opisana drugim pravilom, a ako ne postoji onda prvo pravilo predstavlja pravilo proste uvjetne naredbe. Gornja pravila ćemo proširiti na način da se operator "ili kao" može eksplicitno označiti okomitom crtom (|), zbog dva razloga: 1. Na taj način gornja pravila (1-4) su ekvivalentna popularnoj BNF notaciji (BNF notacija je metajezik razvijen 1960. godine prilikom definicije programskog jezika ALGOL, pri čemu su bitne doprinose dali J.W.Bakus i P.Naur, pa BNF predstavlja kraticu za "Backus-ova normalna forma" ili "Backus-Naur-ova forma"). 2. Na taj način se alternativne produkcije mogu pisati u istom redu Pomoću prethodno definiranih pravila lako se može definirati i leksička struktura jezika. Primjerice, temeljni se leksički objekti znamenka i slovo mogu definirati pravilima: slovo : A⎪B⎪C⎪D⎪E⎪F⎪G⎪H⎪I⎪J⎪K⎪L⎪M⎪N⎪O⎪P⎪Q⎪R⎪S⎪T⎪U⎪V⎪W⎪X⎪Y⎪Z ⎪a⎪b⎪c⎪d⎪e⎪f⎪g⎪h⎪i⎪j⎪k⎪l⎪m⎪n⎪o⎪p⎪q⎪r⎪s⎪t⎪u⎪v⎪w⎪x⎪y⎪z. znamenka : 0⎪1⎪2⎪3⎪4⎪5⎪6⎪7⎪8⎪9. heksa_znamenka : 0⎪1⎪2⎪3⎪4⎪5⎪6⎪7⎪8⎪9⎪A⎪B⎪C⎪D⎪E⎪F⎪a⎪b⎪c⎪d⎪e⎪f. oktalna_znamenka : 0⎪1⎪2⎪3⎪4⎪5⎪6⎪7⎪.

Koristeći objekte znamenka i slovo može se definirati objekt znak (koji može biti slovo ili znamenka): znak : znamenka ⎪ slovo.

Sintaksa znaka može po potrebi biti i drugačije definirana, naročito ukoliko se pod pojmom znak mogu koristiti i specijalni znakovi, ili još i šire, cijela ASCII kolekcija simbola. Vrlo često, potreban element jezika je niz znakova. Njega se može definirati korištenjem rekurzivnog pravila: niz_znakova : znak ⎪ niz_znakova znak,

što se tumači na sljedeći način: niz znakova je ispravno zapisan ako sadrži samo jedan znak ili ako sadrži niz znakova i s desne strane još jedan znak. Dakle, alternativno pravilo prepoznaje sve nizove koji imaju dva ili više znakova. Može se napisati i slijedeće: niz_znakova : znak ⎪ znak niz_znakova,

što se tumači ovako: niz znakova je ispravno zapisan ako sadrži samo jedan znak ili ako iza znaka sadrži niz znakova. Uočimo da alternativno pravilo, također, prepoznaje nizove koji sadrže dva ili više znakova. Identifikatori u C-jeziku (nazivi varijabli, labela, funkcija i tipova) moraju početi sa slovom ili znakom podvlake '_', pa vrijedi : identifikator : slovo | _ | identifikator slovo | identifikator znamenka | identifikator _

85

Na osnovu ovog pravila, kao ispravno zapisani identifikatori, ocjenjuju se: BETA7 , A1B1 , x , xx , xxx , dok sljedeći zapisi ne predstavljaju indetifikatore: 7, A+B , 700BJ , -beta , x*5 , a=b , x(3). Pod pojmom liste identifikatora podrazumijeva niz identifikatora međusobno razdvojenih zarezom. lista_identifikatora : identifikator ⎪ identifikator , lista_identifikatora

U Dodatku B dana je potpuna specifikacija sintakse C jezika.

86

7 Proste i strukturalne naredbe C jezika

Naglasci: • proste i strukturalne naredbe • naredbe bezuvjetnog skoka i označene naredbe • naredbe s logičkom i cjelobrojnom selekcijom • tipovi petlji i beskonačne petlje Naredbe su programski iskazi pomoću kojih se kontrolira izvršenje programa. Prema razini apstrakcije računarskog procesa, kojeg predstavljaju, dijele se na proste naredbe i strukturalne naredbe. U prethodnim poglavljima se korištene strukturalne naredbe tipa sekvence, selekcije (if-else-naredba) i iteracije (while-petlja), te proste naredbe pridjele vrijednosti i poziva potprograma. Interesantno je napomenuti da se pomoću tih naredbi može napisati bilo koji algoritam koji je moguće izvršiti računalom. U ovom poglavlju će biti opisane sve naredbe C jezika koje se koriste za kontrolu toka programa.

7.1 Proste naredbe Proste ili primitivne naredbe su one naredbe koje odgovaraju naredbama strojnog jezika. U C jeziku "najprostija" naredba je izraz iza kojeg se napiše znak točka-zarez. Takova naredba se naziva naredbeni izraz. Sintaksa naredbe je: naredbeni izraz : izrazopt ; Primjerice, 1+3*7; je naredba izračun vrijednosti izraza 1+3*7. Kada računalo izvrši operacije opisane ovim izrazom, rezultat će ostati negdje u memoriji ili u procesoru računala, stoga ova naredba nema nikakvog smisla. Ako se pak napiše naredba x = 1+3*7;

tada će rezultat biti spremljen u memoriji na adresi koju označava varijabla imena x. Do sada je ovakva naredba nazivana naredba pridjele vrijednosti, jer se ona tako naziva u većini programskih jezika. U C jeziku se ova naredba zove naredbeni izraz pridjele vrijednosti, jer znak = predstavlja operator pridjele vrijednosti koji se može koristiti u izrazima. Primjerice, u naredbi x = 3 + (a=7);

znak = se koristi dva puta. Nakon izvršenja ove naredbe vrijednost varijable a je 7, a vrijednost varijable x je 10. Prema iznesenom sintaktičkom pravilu naredbom se smatra i znak točka-zarez: ;

/* ovo je naredba C jezika */

87

Ova se naredba naziva nulta ili prazna naredba. Njom se ne izvršava nikakvi proces. Makar to izgledalo paradoksalno, ovu se naredbu često koristi, i često je uzrok logičkih pogreški u programu. Zašto se koristi i kako nastaju pogreške zbog korištenja ove naredbe bit će pokazano kasnije. Upoznavanje s naredbenim izrazima završit će sa sljedećim primjerima prostih naredbi: x++; --x; printf("Hi"); x=a+3.14+sin(x);

/* /* /* /*

povećaj vrijednost x za 1 umanji vrijednost x za 1 poziv potprograma kompleksni izraz s pozivom funkcije

*/ */ */ */

Posljednju naredbu, u kojoj se računa kompleksni izraz, s pozivom funkcije sin(x), moglo se zapisati pomoću više naredbenih izraza: x = a; x += 3.14; tmp=sin(x); x += tmp;

/* /* /* /* /*

vrijednost od a pridijeli varijabli x uvećaj x za 3.14 pomoćnoj varijabli tmp pridijeli vrijednost koju vraća funkcija sin(x) uvećaj x za vrijednost varijable tmp

*/ */ */ */ */

Operacijska semantika, odnosno način kako se naredbe izvršavaju u računalu, u oba zapisa je potpuno ista, jer C prevodilac složene izraze razlaže u više prostih izraza koji se mogu direktno prevesti u strojni kôd procesora. U proste naredbe spadaju još naredbe bezuvjetnog i uvjetnog skoka. Pomoću ovih naredbi se može eksplicitno zadati da se izvršenje programa nastavi naredbom koja je označena nekim imenom. Sintaksa označene naredbe je: označena_nareba : identifikator : naredba Identifikator kojim se označava neka naredba često se naziva programska labela. Sintaksa naredbe bezuvjetnog skoka je: naredba_skoka : goto identifikator ; Semantika naredbe je da se izvrši skok, odnosno da se izvršenje programa nastavi naredbom koja je označena identifikatorom i znakom dvotočke. Primjerice, u nizu naredbi: goto next; naredba2 next: naredba3

nikad se neće izvršiti naredba2, jer se u prethodnoj naredbi vrši bezuvjetni skok na naredbu koja je označena identifikatorom next. Skok se može vršiti i unatrag, na naredbe koje su već jednom izvršene. Na taj način se mogu realizirati iterativni procesi – petlje. Naredba skoka i naredba na koju se vrši skok, moraju biti definirani unutar iste funkcije. Zapis naredbe uvjetnog skoka, koji se izvodi na temelju ispitivanja logičke vrijednosti nekog izraza, je: if ( izraz ) goto identifikator ;

88

što znači: ako je izraz logički istinit (različit od nule) vrši se skok na označenu naredbu, a ako nije izvršava se slijedeća naredba. Naredbe uvjetnog i bezuvjetnog skoka vjerno opisuju procese u računalu, međutim njihova se upotreba ne preporučuje. Sljedeći primjer pokazuje zašto programeri "ne vole goto naredbu". Razmotrimo zapis: if (izraz) goto L1; goto L2; L1: naredba L2: .....

U prvoj se naredbi ispituje vrijednost izraza. Ako je on različit od nule, izvršava se naredba označena s L1, u suprotnom izvršava se naredba goto L2. Primjenom logičke negacije na izraz u prvoj naredbi dobije se ekvivalentni algoritam: if (!izraz) goto L2: naredba L2: ......

Mnogo jednostavnije se ovaj programski tijek zapisuje tzv. uvjetnom naredbom: if (izraz) naredba

Ova naredba spada u strukturalne naredbe selekcije. Ona, već na "prvi pogled", jasno iskazuje koji proces treba izvršiti. Ako se pak pogleda prethodna dva zapisa, u kojima je korištena gotonaredba, trebat će znatno više mentalnog napora za razumijevane opisanog procesa. Ovaj problem posebno dolazi do izražaja kod većih programa, gdje primjena goto naredbe dovodi do stvaranja nerazumljivih i "zamršenih" programa. Jedino kada se može opravdati upotreba goto naredbe jest kada se želi napisati algoritam koji treba biti "ručno" preveden na asemblerski jezik. U svim ostalim slučajevima, u programiranju i u razvoju algoritama, treba koristiti naredbe selekcije i petlje kojima se dobija jasna i pregledna struktura programa.

7.2 Strukturalne naredbe 7.2.1 Složena naredba ili blok Pod pojmom složene naredbe podrazumijeva se niz naredbi i deklaracija napisan unutar vitičastih zagrada. Naziva se i blok jer se u okviru neke druge strukturalne naredbe može tretirati kao cjelina. Lijeva zagrada '{' označava početak, a desna zagrada '}' označava kraj bloka. Sintaksa složene naredbe je: složena-naredba : { niz-deklaracijaopt niz-naredbiopt } Unutar bloka dozvoljeno je deklarirati varijable, ali samo na mjestu neposredno iza vitičastih zagrada. Pogledajmo programski odsječak u kojem se vrši zamjena vrijednosti dvije varijable x i y. int x, y; ........

89

x=7; y=5; { int tmp; tmp = x; x = y; y = tmp;

/* tmp je lokalna varijabla bloka*/ /* tmp == 7 */ /* x == 5 */ /* y == 7 */

} printf("x=%d, y=%d", x, y)

Zamjena vrijednosti se vrši pomoću varijable tmp, koja je deklarirana unutar bloka, i koja ima karakter lokalne varijable, što znači da joj se ime može koristiti samo unutar bloka. Uočite da se najprije vrijednost od x upisuje u tmp. Zatim se varijabli x pridjeljuje vrijednost varijable y, i konačno se varijabli y pridjeljuje vrijednost od x, koja je bila sačuvana u varijabli tmp. Nakon izlaska iz bloka nije potrebna varijabla tmp. U C-jeziku se automatski obavlja odstranjenje iz memorije lokalnih varijabli po izlasku iz bloka u kojem su definirane. Kasnije će o ovom problemu biti više govora. Sa semantičkog stajališta blok analizirano kao niz deklaracija i naredbi, dok u analizi sintakse i strukture programa, blok predstavlja jedinstvenu naredbu. To ujedno znači da u zapisu sintakse, na svakom mjestu gdje pišemo naredba, podrazumijeva se da može biti napisana i prosta i složena naredba i ostale strukturalne naredbe.

7.2.2 Naredbe selekcije Općenito se pod selekcijom nazivaju programske strukture u kojima dolazi do grananja programa, a nakon prethodnog ispitivanja vrijednosti nekog izraza. U C jeziku se koriste se tri tipa naredbi selekcije: 1. Uvjetna naredba (if- naredba) 2. Uvjetno grananje (if-else naredba) 3. Višestruko grananje (switch-case naredba) U prva dva tipa naredbi grananje se vrši na temelju ispitivanja logičke vrijednosti nekog izraza, a u switch-case naredbi grananje može biti višestruko, ovisno o cjelobrojnoj vrijednosti nekog selektorskog izraza. Uvjetna naredba (if-naredba) Sintaksa if-naredbe je : if_naredba: if ( izraz ) naredba gdje naredba može biti bilo koja prosta, složena ili strukturalna naredba. Značenje naredbe je: ako je izraz različit od nule se izvršava naredba, a ako je izraz jednak nuli, program se nastavlja naredbom koja slijedi iza if-naredbe.

90

Slika 7.1 Dijagram toka if- naredbe Uzmimo primjer da analiziramo dvije varijable : x i y. Cilj je odrediti koja je od te dvije vrijednosti manja, a zatim tu manju vrijednost upisati u varijablu imena min. To se može ostvariti naredbama: min = y; if (x < y) min = x;

/* pretpostavimo da je y manje od x */ /* ako je x manje od y */ /* minimum je jednak x-u */

Uvjetno grananje (if-else naredba) Sintaksa if-else naredbe je if-else-naredba: if ( izraz ) naredba1 else naredba2 gdje naredba1 i naredba2 predstavljaju bilo koji prostu, složenu ili strukturalnu naredbu. Značenje if-else-naredbe je: ako je izraz različit od nule, izvršava se naredba1, inače izvršava se naredba2. Primjerice, iskaz: if (x < y) min = x; else min = y;

omogućuje određivanje minimalne vrijednosti.

Slika 7.2 Dijagram toka if-else naredbe

91

Uzmimo sada da je potrebno odrediti da li je vrijednost varijable x unutar intervala {3,9}. Problem se može riješiti tako da se unutar if-else naredbe, u kojem se ispituje donja granica intervala, umetne if-else naredba kojom se ispituje gornja granica intervala. Tri sintaktički i semantički ekvivalentna if-else iskaza (nakon izvršenja, varijabla unutar ima vrijednost 1 ako je x unutar intervala {3,9}, inače ima vrijednost 0.) if (x >= 3) { if (x = 3) if (x = 3) if (x = 3 && x 1) { f *= i; i–-; }

Ako je n 1; i--) f *= i;

U prvom naredbenom izrazu se inicira kontrolna varijabla i na vrijednost n, u drugom izrazu se zapisuje uvjet ponavljanja pelje (i>1), a u trećem izrazu se zapisuje naredbeni izraz kojim se definira promjena kontrolne varijable pri svakom ponavljanju petlje(i--). Semantiku for-petlje može se objasniti pomoću ekvivalentne while-petlje izrazopt ; while ( izrazopt ) { naredba izrazopt; } U svakom od ovih izraza može se navesti više izraza odvojenih zarezom. Primjerice, za proračun n-faktorijela (f=n!) vrijede ekvivalentni iskazi: (1)

f=1; for(i=2; i, traženje datoteke se vrši u direktoriju u kojem su smještene datoteke s deklaracijama standardnih funkcija. Ime datoteke može sadržavati potpuni ili djelomični opis staze do datoteke. Zapis ovisi o pravilima operativnog sustava. Primjerice, na Unix-u može biti oblik #include "/usr/src/include/classZ.h" #include "../include/classZ.h"

ili na Windowsima #include "C:\src\include\classZ.h" #include "..\include\classZ.h"

14.2 Direktiva #define za makro-supstitucije Leksička supstitucija teksta se definira pomoću direktive #define na dva načina. Prvi način zapisa, koji se vrši prema pravilu:

188

#define identifikator supstitucijski-tekst je već korišten za definiranje simboličkih konstante ili dijelova izvornog koda, primjerice #define EPSILON 1.0e-6 #define PRINT_LINE printf("----------\n"); #define LONG_SUBSTITION if(a>b){ printf("a>b"); } \ else { prinf("a polish \"30 5 2 - / 10 *\"\n"); exit(1); } str = argv[1]; len = strlen(str); stack = stack_new(); for (i = 0; i < len; i++) { if (str[i] == '+') Push(Pop()+ Pop()); if (str[i] == '*') Push(Pop()* Pop()); if (str[i] == '-') { tmp = Pop(); Push(Pop()- tmp); } if (str[i] == '/') { int tmp = Pop(); if (tmp==0) {printf("Djeljenje s nulom\n"); exit(1);} Push(Pop() / tmp); } if (isdigit(str[i])) /* konverzija niza znamenki u broj */ { Push(0); do { Push(10*Pop() + (int)str[i]-'0'); i++; } while (isdigit(str[i])); i--; } }

}

printf("Rezultat: %s = %d \n", str, Pop( )); stack_free(stack);

220

16.4 Red i QUEUE ADT Red (eng. queue) je struktura koja podsjeća na red za čekanje. Iz reda izlazi onaj koji je prvi u red ušao. Ovaj princip pristupa podacima se naziva FIFO – first in first out. Temeljne su operacije: ADT QUEUE get(Q )

- dobavi element iz reda Q.

put( Q , el)

- stavi element el u red Q.

empty( Q )

- vraća 1 ako je red Q prazan, inače vraća 0.

full( Q )

- vraća 1 ako je red Q popunjen, inače vraća 0.

Ovakvi se redovi mogu realizirati kao ADT QUEUE prema sljedećoj specifikaciji: /* Datoteka: queue.h */ #ifndef _QUEUE_ADT #define _QUEUE_ADT typedef int queueElemT; typedef struct queue *QUEUE; QUEUE queue_new(void); /* formira novi objekt tipa QUEUE */ void queue_free(QUEUE Q); /* dealocira objekt tipa QUEUE */ int queue_empty(QUEUE Q); /* vraća 1 ako je red prazan */ int queue_full(QUEUE Q); /* vraća 1 ako je red popunjen */ void queue_put(QUEUE Q, queueElemT el); /* stavlja element u red */ queueElemT queue_get(QUEUE Q); /* vraća element iz reda */ void queue_print(QUEUE Q); #endif

Implementacija se može provesti na više načina. Za smještaj elemenata reda najčešće se koristi niz ili linearna lista. Najprije ćemo upoznati implementaciju pomoću niza, i to implementaciju koja koristi tzv. cirkularni spremnik. On se realizira pomoću niza, kojem dva indeksa: back i front, označavaju mjesta unosa (back) i dobave (front) iz reda. Niz je veličine QUEUEARRAYSIZE. Operacije put() i get() se mogu ilustrirati na sljedeći način:

221

Početno je red prazan Front = back;

Front=back

-

Nakon operacija put() povećava put('a'); put('b') se indeks back za 1 a b front

-

(red prazan)

-

-

-

back

Operacija get() dobavlja element x = get() /* x sadrži vrijednost a */ kojem je indeks jednak front, a a b zatim se front povećava za front back jedan. Što napraviti kada, nakon višestrukog unosa, back postane jednak krajnjem indeksu niza?

put('c'); put('d'); put('e'); -

b c front

d

E

back

Ideja je da se back ponovo put('f') postavi na početak niza (takovi b C d e f spremnik se nazivaju cirkularni back front (red popunjen) spremnik). Time se maksimalno iskorištava prostor spremnika za Red popunjen ako je: (back+1) % QUEUEARRAYSIZE == front smještaj elemenata reda. U datoteci "queue_arr.c" realiziran je ADT QUEUE pomoću cirkularnog spremnika. /* Datoteka: queue-arr.c * QUEUE realiziran kao cirkularni spremnik */ #include #include #include #include

"queue.h"

#define QUEUESIZE 100 /* maksimalni broj elemenata */ #define QUEUEARRAYSIZE (QUEUESIZE +1) /* veličina niza */ /* typedef int queueElemT; */ /* typedef struct queue *QUEUE;

definirani u queue.h*/

struct queue { queueElemT A[QUEUEARRAYSIZE]; int front; int back; }; QUEUE queue_new(void) { QUEUE Q = malloc(sizeof(struct queue)); if(Q != NULL) Q->front = Q->back = 0; return Q; } void queue_free(QUEUE Q) { assert(Q != NULL); if(Q != NULL) free(Q);

222

} int queue_empty(QUEUE Q) { assert(Q != NULL); return (Q->front == Q->back); } int queue_full(QUEUE Q) { assert(Q != NULL); return ((Q->back + 1) % QUEUEARRAYSIZE == Q->front); } void queue_put(QUEUE Q, queueElemT x) { assert(Q != NULL); Q->A[Q->back] = x; Q->back = (Q->back + 1) % QUEUEARRAYSIZE; } queueElemT queue_get(QUEUE Q) { queueElemT x; assert(Q != NULL); x = Q->A[Q->front]; Q->front = (Q->front +1) % QUEUEARRAYSIZE; return x; } void queue_print(QUEUE Q) { int i; assert(Q != NULL); printf("Red: "); for(i = Q->front % QUEUEARRAYSIZE; i < Q->back; i=(i+1)% QUEUEARRAYSIZE ) printf("%d, ", Q->A[i]); printf("\n"); }

Testiranje ADT QUEUE provodi se programom queue-test.c. /* Datoteka: queue-test.c */ #include #include #include #include "queue.h" #include "queue-arr.c" void upute(void) { printf ("Izbornik:\n" " 1 Umetni broj u Red\n" " 2 Odstrani broj iz Reda\n" " 0 Kraj\n"); } int main()

223

{

int izbor, elem; QUEUE Q = queue_new(); upute(); printf("? "); scanf("%d", &izbor); while (izbor != 0) { switch(izbor) { case 1: printf("Otkucaj broj: "); scanf("\n%d", &elem); if (!queue_full(Q)) { queue_put(Q, elem); printf("%d ubacen u red.\n", elem); } queue_print(Q); break; case 2: if (!queue_empty(Q)) { elem = queue_get(Q); printf("%d odstranjen iz reda.\n", elem); } queue_print(Q); break; default: printf("Pogresan Izbor.\n\n"); upute(); break; } printf("? "); scanf("%d", &izbor);

}

} return 0;

16.5 Zaključak Opisana je metoda programiranja, pomoću koje se sustavno analizira, specificira i implementira programske objekte kao apstraktne dinamičke tipove podataka – ADT. Izrada specifikacije operacija s apstraktnim objektima sve više postaje temeljni element programiranja. To osigurava da se maksimalna pažnja posveti onome što treba programirati. Neovisnost specifikacije od implementacije ADT, osigurava fleksibilan i pouzdan razvoj programa. U specifikaciji ADT-a dva su temeljna elementa: ime ADT-a i operacije koje se mogu izvršiti. To je karakteristka tipova, pa se s ADT-om kreiraju novi apstrakni tipovi podataka. Rad s ADT predstavlja objetno temeljeno programiranje.

224

17 Rekurzija i složenost algoritama

Naglasci: • rekurzija • podijeli pa vladaj • kompleksnost algoritama • binarno pretraživanje niza • sortiranje

17.1 Rekurzivne funkcije U programiranju i matematici često se koriste rekurzivne definicije funkcija. Direktna rekurzija nastaje kada se u definiciji funkcije poziva ta ista funkcija, a indirektna rekurzija nastaje kada jedna funkcija poziva drugu funkciju, a ova ponovo poziva funkciju iz koje je pozvana. Definicija rekurzivne funkcije u pravilu se sastoji od dva dijela: temeljnog slučaja i pravila rekurzije. Primjerice, u matematici se se može rekurzivno definirati funkcija n! (n-faktorijela) na sljedeći način: Definicija n! (n-faktorijela): 1. Temeljni slučaj: 2. Pravilo rekurzije:

0! = 1 n! = n * (n-1)!

za n=0 za n>0

Sve vrijednosti od n! se mogu izračunati pomoću gornjeg rekurzivnog pravila, tj. 1! 2! 3! 4!

= = = =

1 * 0! = 1 * 1 = 1 2 * 1! = 2 * 1 * 0! = 2 * 1 * 1 = 2 3 * 2! = 3 * 2 * 1! = 3* 2 * 1 * 0! = 3 * 2 * 1 * 1 = 6 ....

Uočite da rekurzivno pravilo znači ponavljanje nekog odnosa, a temeljni slučaj označava prostu radnju nakon koje prestaje rekurzija. Programski se funkcija n! realizira kao funkcija fact(), koja prima argument tipa int i vraća vrijednost tipa int. Koristeći prethodnu matematičku definiciju , funkcija fact() se implementira na sljedeći način: int fact(int n) { if (n == 0) return 1; else return fact(n-1) * n; }

Uočite da se u tijelu funkcije fact() poziva ta ista funkcija. Kako se izvršava ova funkcija? Da bi to shvatili, potrebno je znati kako se na razini strojnog koda vrši poziv funkcije. Većina kompilatora to vrši na sljedeći način:

225

1. Pri pozivu funkcije najprije se argumenti funkcije postavljaju u dio memorije koji je predviđen za lokalne varijable. Ta memorija se naziva izvršni stog, jer se podacima pristupa s pus() i pop() opearacijama. Vrijednost, koja je posljednja stavljena na stog, predstavlja vrh stoga. 2. Zatim se izvršava kôd tijela pozvane funkcije. U njoj se argumenti funkcije tretiraju kao lokalne varijable čija je vrijednost na stogu. 3. Povrat iz funkcije se vrši tako da se stanje izvršnog stoga vrati na stanje prije poziva funkcije, a povratna se vrijednost postavlja u registar procesora, tzv. povratni registar. 4. Izvršenje programa se nastavlja naredbom u pozivnoj funkciji koja slijedi iza pozvane funkcije. Na slici 1 prikazano je izvršenje funkcije fact(4) i stanje izvršnog stoga.

izvršenje funkcije fact(4) fact(4)= 4 * fact(3)= 3 * fact(2) 2 * fact(1) 1 * fact(0) return 1 return 1*1 return 2*1 return 3*2 return 4*6 => 24

stanje stoga koji se koristi za prijenos argumenata funkcije ... 4 ... 4 3 ... 4 3 2 ... 4 3 2 1 ... 4 3 2 1 0 ... 4 3 2 1 ... 4 3 2 ... 4 3 ... 4 ...

Slika 17.1. Redoslijed izvršenja rekurzivne funkcije fact(4) Poziv funkcije fact(4) počinje tako da se argument vrijednosti 4 stavlja na vrh izvršnog stoga, a zatim se izvršavaju naredbe iz tijela funkcije. Pošto je argument različit od nule izvršava se naredba return fact(n-1) * n;. Da bi se ona mogla izvršiti, najprije se vrši poziv funkcije fact(n-1). Zbog toga se na izvršni stog stavlja vrijednost argumenta 3 i poziva funkcija fact(3). Ovaj proces se ponavlja sve dok argument funkcije ne postane jednak nuli. Nakon toga se u tijelu funkcije izvršava naredba return 1;. To znači da se odstranjuje vrijednost s vrha stoga (0), a u povratni registar se upisuje vrijednost 1. Program nastavlja izvršenje naredbom koja slijedi iza pozivne funkcije, a to je zapravo onaj dio naredbe return fact(n-1) * n; u kojoj se vrši množenje povratne vrijednosti od fact(n-1) i argumenta n, čija se vrijednost nalazi na vrhu stoga (u ovom slučaju to je vrijednost 1). Zatim se u povratni registar stavlja vrijednost 1*1 i skida argument s vrha stoga, pa na vrhu stoga ostaje vrijednost 2. Program se ponovo vraća na izvršenje u naredbu return 1 * 2;. U povratni registar se sada upisuje vrijednost 2, na vrhu stoga ostaje vrijednost 3 i izvršenje se vraća u naredbu return 2 * 3; jer je iz nje vršen poziv fact(3). Nakon toga se izvršava naredba return 6 * 4;. Ovo je posljednja naredba koja će se izvršiti. Nakon nje je vrh stoga prazan, a izvršenje programa se vraća na naredbu iz pozivne funkcije koja slijedi iza poziva fact(4). U povratnom registru je vrijednost 24, koja predstavlja vrijednost koju vraća fact(4). Moglo bi se slikovito reći da se rekurzivne funkcije izvršavaju tako da se najprije višestrukim pozivom funkcije vrši "traženje" temeljnog slučaja, pri čemu se pamte sva stanja procesa, a zatim se problem rješava "izvlačenjem" iz rekurzije. Kod funkcija koje imaju veliki broj rekurzivnih poziva može doći do značajnog ispunjenja izvršnog stoga. Kod MSDOS operativnog sustava može se maksimalno koristiti 64Kbajta za

226

stog, pa treba biti oprezan pri korištenju rekurzivnih funkcija. Kod WIN32 sustava za stog je predviđeno koristiti do 1Mbajta memorije. Zadatak: Napišite funkciju unsigned suma( unsigned n);

kojoj je argument kardinalni broj n, a funkcija vraća vrijednost koja je jednaka sumi svih kardinalnih brojeva 0,1,2...,n. Koristite rekurzivnu definiciju: 1. trivijalni slučaj: 2. rekurzivno pravilo:

ako je n=0, suma(n) = 0 ako je n>0, suma(n) = suma(n-1)+n

17.2 Matematička indukcija Rekurzija se koristi i pri dokazivanju teorema indukcijom. Princip matematičke indukcija se koristi kod problema čija se zakonitost može označiti cijelim brojem n, kao S(n). Definira se na sljedeći način. Da bi dokazali da vrijedi zakonitost S(n), za bilo koju vrijednost n: 1. Dokaži da zakonitost S(n) vrijedi u trivijalnom slučaju za n=0 2. Zatim dokaži da vrijedi S(n), ako se pretpostavi da vrijedi S(n-1). Primjer: Suma od n prirodnih brojeva se može izračunati prema izrazu: 1 + 2 + 3 + … + n = n (n +1) / 2 Dokaz: 1. Trivijalni slučaj: za n = 1, suma je jednaka 1 Pošto je 1(1+1)/2) = 1 dokazano je da vrijedi trivijalni slučaj. 2. Pretpostavimo da vrijedi za 1 + … +( n -1), pa ispitajmo da li vrijedi za 1 + … +( n -1) + n ? Pošto je 1 + … +( n -1) + n = (n -1)( n -1+1) / 2 + n = n (n +1) / 2 dokaz je izvršen. Zadatak: Dokažite da ova formula vrijedi i za proračun sume svih kardinalnih brojeva (0,1,2,..) koji su manji ili jednaki n. Zadatak: Napišite funkciju za proračun sume svih kardinalnih brojeva koji su manji ili jednaki n, koristeći prethodno izvedenu formulu.

17.3 Kule Hanoja Čovjek nije sposoban razmišljati i rješavati probleme na rekurzivan način. U programiranju se pak rekurziju može koristiti u mnogo slučajeva, posebno kada je njome prirodno definiran

227

neki problem. Jedan od najpoznatijih rekurzivnih problema u kompjuterskoj literaturi je bez sumnje rješenje inteligentne igre koja se naziva Kule Hanoja. Problem je predstavljen na slici 2.

Slika 17.2. Kule Hanoia Postoje tri štapa označena s A, B i C. Na prvom štapu su nataknuti cilindrični diskovi promjenljive veličine, koji imaju rupu u sredini. Zadatak je premjestiti sve diskove s štapa A na štap B u redoslijedu kako se nalaze na štapu A. Pri prebacivanju diskova treba poštovati sljedeće pravila: Odjednom se smije pomicati samo jedan disk. Ne smije se stavljati veći disk povrh manjeg diska. Može se koristiti štap C za privremeni smještaj diskova, ali uz poštovanje prethodna dva pravila. Problem: pomakni N diskova sa štapa A na štap B, može se riješiti rekurzivnim postupkom. Temeljni slučaj i pravilo rekurzije su: Temeljni slučaj - Najjednostavniji slučaj kojeg svatko može riješiti je kada kula sadrži samo jedan disk. Tada je rješenje jednostavno; prebaci se taj disk na ciljni štap B. Rekurzivno pravilo - Ako kula sadrži N diskova, pomicanje diskova se može izvesti u tri koraka 1. Pomakni gornjih N-1 diskova sa štapa A na pomoćni štap C. 2. Preostali donji disk s štapa A pomakni na ciljni štap B. 3. Zatim pomakni kulu od N-1 diskova s pomoćnog štapa C na ciljni štap B. Teško je na prvi pogled prihvatiti da ovo rekurzivno pravilo poštuje pravilo da se uvijek pomiče samo jedan disk, ali ako se prisjetimo da se rekurzivni problemi počinju rješavati tek kad je pronađen temeljni slučaj, u kojem se pomiče samo jedan disk, i da su prije toga zapamćena sva moguća stanja procesa, onda je jasno da se uvijek pomiče samo jedan disk. Kako napisati funkciju pomakni_kulu() koja izvršava gornje pravilo. Potrebni argumente funkcije su: broj diskova koje treba pomaknuti, ime početnog štapa , ime ciljnog štapa, ime pomoćnog štapa. void pomakni_kulu(int n, char A, char B, char C);

Također, potrebno je definirati funkciju kojom će se na prikladan način označiti prebacivanje jednog diska. Nju se može odmah definirati u obliku:

228

void pomakni_disk(char sa_kule, char na_kulu) { printf("%c -> %c\n", sa_kule, na_kulu); }

Korištenjem ove funkcije i pravila rekurzije, funkciju pomakni_kulu() se može napisati na sljedeći način: void pomakni_kulu(int n, char A, char B, char C) { if (n == 1) { /* temeljni slučaj pomakni_disk(A, B); } else { pomakni_kulu (n - 1, A, C, B); /* 1. pravilo pomakni_disk (A, B); /* 2. pravilo pomakni_kulu (n - 1, C, B, A); /* 3. pravilo } }

*/

*/ */ */

Ili još jednostavnije: void pomakni_kulu(int n, char A, char B, char C) { if (n > 0) { pomakni_kulu (n - 1, A, C, B); pomakni_disk (A, B); pomakni_kulu (n - 1, C, B, A); } }

jer se u slučaju kada je n=1 u funkciji pomakni_kulu(0, ....) ne izvršava ništa, pa se u tom slučaju izvršava funkcija pomakni_disk(A,B), što je pravilo temeljnog slučaja. Za testiranje funkcije pomakni_kulu(), koristi se program hanoi.c: /* Datoteka: hanoi.c */ #include void pomakni_kulu(int n, char A, char B, char C); void pomakni_disk(char sa_kule, char na_kulu) int main() { int n = 3; /* za slučaj 3 diska*/ pomakni_kulu(n, 'A','B','C'); return 0; }

Nakon izvršenja ovog programa dobije se izvještaj o pomaku diskova oblika: A A B A C

-> -> -> -> ->

B C C B A

229

C -> B A -> B

U ovom primjeru je očito da se pomoću rekurzije dobije fascinantno jednostavno rješenje problema. Teško da postoji neka druga metoda kojom bi se ovaj problem mogao riješiti na jednako efikasan način. Zadatak: Provjerite izvršenje programa za slučaj da broj diskova iznosi: 2, 3, 4 i 5. Pokazat će se da broj operacija iznosi 2n-1, što se može i logično zaključiti, jer se povećanjem broja diskova za jedan udvostručuje broj rekurzivnih poziva funkcije pomakni_kulu(), a u temeljnom slučaju se vrši samo jedna operacija. Procijenite koliko bi trajalo izvršenje programa pod uvjetom da izvršenje jedne operacije traje 1us i da se koristi 64 diska. Da li izvršenje tog program traje dulje od životog vijeka čovjeka?

17.4 Metoda - podijeli pa vladaj (Divide and Conquer) U analizi programskih metoda često se spominje metoda "podijeli pa vladaj". Kod nje se rekurzija nameće kao prirodni način rješenja problema. Opći princip metode je da se problem logično podijeli u više manjih problema, tako da se rješenje dalje može odrediti rješavanjem jednog od tih "manjih" problema.

17.4.1 Drugi korijen broja Metodu podijeli pa vladaj primijenit ćemo za približan proračun drugog korijena broja n. Metoda: Numerički se proračuni mogu provesti samo s ograničenom točnošću. Zbog toga zadovoljava postupak u kojem se određuje da vrijednost x predstavlja drugi korijen od n, ako je razlika (n – x2) približno jednaka nuli, odnosno manja od po volji odabrane vrijednosti epsilon. Točno rješenje se nalazi unutar nekog intervala [d,g]. Primjerice, sigurno je da se rješenje nalazi u intervalu [0,n] ako je n>1, odnosno u intervalu [0,1] ako je nkorijen 5 7.6 3.14

Dobije se ispis: Drugi korijen (5.000000) = 2.236068 (treba biti 2.236068) Drugi korijen (7.600000) = 2.756810 (treba biti 2.756810) Drugi korijen (3.140000) = 1.772004 (treba biti 1.772005)

17.4.2 Binarno pretraživanje niza Drugi primjer primjene metode podijeli pa vladaj je traženje elementa sortiranog niza metodom koja se naziva binarno pretraživanje niza. Zadatak: Zadan je niz cijelih brojeva a[n] kojem su elementi sortirani od manje prema većoj vrijednosti, tj. a[i-1] < a[i],

za i=1,..n-1

231

Potrebno je odrediti da li se u ovom nizu nalazi element vrijednosti x, i to pomoću funkcije int binSearch( int a[],int x, int d, int g);

koja vraća indeks elementa a[i], kojem je vrijednost jednaka traženoj vrijednosti x. Ako vrijednost od x nije jednaka ni jednom elementu niza, funkcija binSearch() vraća negativnu vrijednost -1. Cjelobrojne vrijednosti d i g predstavljaju indekse niza koji određuju interval [d,g] unutar kojeg se vrši traženje. Metoda: Problem se može riješiti rekurzivno na sljedeći način: Ako u nizu a[i] postoji element jednak traženoj vrijednosti x, njegov indeks je iz intervala [d,g], gdje mora biti istinito g >= d. Trivijalni slučaj je za d=0, g=n-1, koji obuhvaća cijeli niz. Temeljni slučaj: Razmatra se element niza indeksa i = (g+d)/2 (dijelimo niz na dva podniza). Ako je a[i] jednak x, pronađen je traženi element niza, a funkcija vraća indeks i. Pravilo rekurzije: Ako je a[i] g) return –1; i = (d + g)/ 2; if (a[i] == x) return i; if (a[i] < x) return binSearch( a, x, i + 1, g); else return binSearch( a, x, d, i - 1); }

Proces traženja vrijednosti x=23 u nizu od 14 elemenata, ilustriran je na slici 17.3. 0 1 D 1

1 2

2 3

3 5

4 6

5 8

2

3

5

6

8

6 9 i 9

1

2

3

5

6

8

9

7 12

8 23

9 26

10 27

11 31

12 34

12 d 12 d

23

26

31

34

23 i

26 g

27 i 27

31

34

13 42 g 42 g 42

1.korak: d=0, g=13, i=6, a[6]23 3.korak: d=7, g=i-1=9, i=8, a[8]==23

Slika 17.3. Binarno pretraživanje niza

17.5 Pretvorba rekurzije u iteraciju Neke rekurzivne funkcije se mogu transformirati u funkcije koje umjesto rekurzije koriste iteraciju. To su funkcije u kojima je rekurzivni poziv funkcije posljednja naredba u tijelu funkcije, primjerice

232

void petlja() { iskaz ... if (e) petlja(); }

U ovom slučaju, rekurzivni poziv funkcije petlja(), znači da se izvršenje vraća na početak tijela funkcije, zbog toga se ekvivalentna verzija funkcije može napisati tako da se umjesto poziva funkcije koristi goto naredba s odredištem na prvu naredbu tijela funkcije, tj. void petlja() { start: iskaz ... if (e) goto start; }

Rekurzivni poziv, koji se vrši na kraju (ili na repu) tijela funkcije, često se naziva "repna rekurzija". (eng. tail recursion). Neki optimizirajući kompilatori mogu prepoznati ovakav oblik rekurzivne funkcije i transformirati rekurzivno tijelo funkcije u iterativnu petlju. Na taj način se dobije efikasnija funkcija, jer se ne gubi vrijeme i prostor na izvršnom stogu koji su potrebni za poziv funkcije. Kod većine se rekurzivnih funkcija ne može izvršiti ova transformacije. Primjerice, funkcija fact() nije repno rekurzivna jer se u posljednjoj naredbi (return fact(n-1)*n;) najprije vrši poziv funkcije fact(), a zatim naredba množenja. Funkcija binSearch(), koja je opisana u prethodnom odjeljku, može se transformirati u funkciju s repnom rekurzijom, na sljedeći način: int binSearch( int a[],int x, int d, int g) { int i; if (d > g) return –1; i = (d + g)/ 2; if (a[i] == x) return i; if (a[i] < x) d=i+1; else g=i-1 return binSearch( a, x, d, g); }

Dalje se može provesti transformacija u iterativnu funkciju int binSearch( int a[],int x, int d, int g) { int i; start: if (d > g) return –1; i = (d + g)/ 2; if (a[i] == x) return i; if (a[i] < x) d=i+1; else g=i-1 goto start; }

Može se napisati iterativno tijelo funkcije i u obliku while petlje: int binSearch( int a[],int x, int d, int g) { int i; while (d -> ->

min min min min min niz

od od od od od je

A[0..5] zamijeni A[1..5] zamijeni A[2..5] zamijeni A[3..5] zamijeni A[4..5] zamijeni sortiran

sa sa sa sa sa

A[0] A[1] A[2] A[3] A[4]

Ovaj algoritam se može implementirati pomoću for petlje: for (i = 0; i < n-1; i++) { imin = indeks najmanjeg elementa u A[i..n-1]; Zamijeni A[i] sa A[imin]; }

Uočite da se petlja izvršava dok je i < n-1, a ne za i < n, jer ako A[0..n-2] sadrži n-1 najmanjih elemenata, tada posljednji element mora biti najveći, i on se nalazi na ispravnom položaju , tj. A[n-1]. Indeks najmanjeg elementa u A[i..n-1] pronalazi se sa: imin = i; for (j = i+1; j < n; j++) if (A[j] < A[min]) imin=j;

Zamjenu vrijednosti vrši funkcijom swap(); /* Zamjena vrijednosti dva int */ void swap(int *a, int *b) { int t = *a; *a = *b; *b = t; }

Sada se može napisati funkcija za selekcijsko sortiranje, niza A koji ima n elemenata, u obliku: void selectionSort(int *A, int n) { int i, j, imin; /* indeks najmanjeg elementa u A[i..n-1] */ for (i = 0; i < n-1; i++) { /* Odredi najmanji element u imin = i; /* for (j = i+1; j < n; j++) if (A[j] < A[imin]) /* imin = j; /*

A[i..n-1]. */ pretpostavi da je to A[i] */ ako je A[j] najmanji */ zapamti njegov indeks */

/* Sada je A[imin] najmanji element od A[i..n-1], */ /* njega zamjenjujemo sa A[i]. */

240

swap(&A[i], &A[imin]); }

}

Analiza selekcijskog sortiranja Svaka iteracija vanjske petlje (indeks i) traje konstantno vrijeme t1 plus vrijeme izvršenja unutarnje petlje (indeks j). Svaka iteracija u unutarnjoj petlji traje konstantno vrijeme t2 . Broj iteracija unutarnje petlje ovisi u kojoj se iteraciji nalazi vanjska petlja: Broj operacija u unutarnjoj petlji n-1 n-2 n-3 ... 1

i 0 1 2 ... n-2

Ukupno vrijeme je: T(n) = [t1 + (n-1) t2] + [t1 + (n-2) t2] + [t1 + (n-3) t2] + ... + [t1 + (1) t2] odnosno, grupirajući članove u oblik t1 ( …) + (...) t2 dobije se T(n) = (n-1) t1 + [ (n-1) + (n-2) + (n-3) + ... + 1 ] t2 Izraz u uglatim zagradama predstavlja sumu aritmetičkog niza 1 + 2 + 3 + ... + (n-1) = (n-1)n/2 = (n2-n)/2, pa je ukupno vrijeme jednako: T(n) = (n-1) t1 + [(n2-n)/2] t2 = - t1 + t1 n - t2 n/2 + t2 n2/2 Očito je da dominira član sa n2 , pa je složenost selekcijskog sortiranja jednaka O(n2) .

17.8.2 Sortiranje umetanjem Algoritam sortiranja umetanjem (eng. insertion sort) se temelji na postupku koji je sličan načinu kako se slažu igraće karte. Algoritam se vrši u u n-1 korak. U svakom koraku se umeće i-ti element u dio niza koji mu prethodi (A[0..i-1]), tako taj niz bude sortiran. Primjerice, sortiranje niza od n=6 brojeva izgleda ovako: 6 4 1 1 1 1

4 6 4 4 3 2

1 1 6 5 4 3

5 5 5 6 5 4

3 3 3 3 6 5

2 2 2 2 2 6

-> -> -> -> -> ->

ako ako ako ako ako niz

je je je je je je

A[1]< A[0], A[2]< A[1], A[3]< A[2], A[4]< A[3], A[5]< A[4], sortiran

umetni umetni umetni umetni umetni

A[1] A[2] A[3] A[4] A[5]

u u u u u

A[0..0] A[0..1] A[0..2] A[0..3] A[0..4]

Algoritam se može zapisati pseudokôdom: for (i = 1; i < n; i++) {

241

x = A[i]; analiziraj elemente A[0 .. i-1] počevši od indeksa j=i-1, do indeka j=0 ako je x < A[j] tada pomakni element A[j] na mjesto A[j+1] inače prekini zatim umetni x na mjesto A[j+1] }

pa se dobije funkcija: void insertionSort(int *A, int n) { int i, j, x;

}

for (i = 1; i < n; i++) { x = A[i]; for(j = i-1; j >= 0; j--) { if(x < A[j]) A[j+1] = A[j]; else break; } A[j+1] = x; }

Analiza složenosti metode sortiranja umetanjem Sortiranje umetanjem je primjer algoritma u kojem prosječno vrijeme izvršenja nije puno kraće od vremena izvršenja koje se postiže u najgorem slučaju. Najgori slučaj Vanjska petlja se izvršava u n-1 iteracija, što daje O(n) iteracija. U unutarnjoj petlji se vrši od 0 do i j. Tada je podjela završena, a j označava indeks koji vraća funkcija. Ovaj uvjet ujedno osigurava da nijedan podniz neće biti prazan. Postupak je ilustriran na slici 17.6.

Slika 17.6. Postupak podjele niza A[1..6]. Za pivot je odabran prvi element A[1] vrijednosti 6. Označeni su indeksi (i,j), a potamnjeni elementi pokazuju koje se elemente zamjenjuje. Linija podjela je prikazana u slučaju kada indeks i postane veći od indeksa j.

/* Podijeli A[d..g] u podnizove A[d..p] i A[p+1..g], * gdje je d = j, raspored je OK.

*/

246

/* inače, zamijeni A[i] sa A[j] i nastavi. */ if (i < j) swap(&A[i], &A[j]); else return j; } }

Pomoću ove funkcije se iskazuje kompletni quicksort algoritam: /* Sortiraj niz A[d..g] - quicksort algoritmom * 1.Podijeli A[d..g] u podnizove A[d..p] i A[p+1..g], * d elem = 5;

Upitnik označava da nije definiran sadržaj pokazivača List->next. Uobičajeno je da se on postavi na vrijednost NULL, jer pokazuje na “ništa”. Time se ujedno označava kraj liste (po analogiju sa stringovima, gdje znak '\0' označava kraj stringa). To simbolički prikazujemo slikom: List->next = NULL;

Sada se u ovu kolekciju može dodati još jedan element, sljedećim postupkom. Prvo, se formira čvor kojem pristupamo pomoću pokazivača q. Node *q=malloc(sizeof(Node)); q->elem =7;

Zatim se ova dva čvora “povežu” sljedećim naredbama:

q->next = List;

251

List = q;

Novi element je postavljen na početak liste. Pokazivač q više nije potreban jer on sada ima istu vrijednost kao pokazivač List (q se koristi samo kao pomoćni pokazivač za formiranje vezane liste). Na ovaj se način može formirati lista s proizvoljnim brojem članova. Početak liste (ili glava liste) je zabilježen u pokazivaču kojeg se tradicionalno naziva List. Kraj liste se je obilježen s NULL vrijednošću “next” pokazivača krajnjeg čvora liste. U ovom primjeru lista je formirana tako da se novi čvor postavlja na glavu liste. Kasnije će biti pokazano kako se novi čvor može dodati na kraj liste ili umetnuti između dva čvora liste. Sada će postupak formiranja i manipuliranja s listom biti opisan s nekoliko temeljnih funkcija, koje se mogu lako prilagoditi različitim tipovima elementa liste. Imena funkcija koje su neovisne o tipu elementa liste bit će zapisana malim slovima. Ostale funkcije, čija imena sadrže velika slova, ovise o tipu elementa liste. Kasnije će biti pokazano kako se te funkcije prilagođavaju tipu elementa liste.

18.2.1 Formiranje čvora liste U radu s listom, za formiranje novog čvora liste koristit će se funkcija newNode().Ona prima argument tipa elemT, a vraća pokazivač na alocirani čvor ili NULL ako se ne može izvršiti alociranje memorije. Node *newNode(elemT x) { Node *n = malloc(sizeof(Node)); if(n != NULL) n->elem = x; return n; }

Također, potrebno je definirati funkciju freeNode(), kojom se oslobađa memorija koju zauzima čvor. void freeNode(Node *p) { /* ako se element liste formira alociranjem memorije * tada ovdje treba dodati naredbu za dealociranje * elementa liste. */ free(p); /* dealociraj čvor liste */ }

Uočite da implementacija ove funkcije ovisi o definiciji elementa liste. Ako se element liste formira alociranjem memorije, primjerice, ako je element liste dinamički string, tada u ovu funkciju treba dodati naredbu za dealociranje elementa liste.

18.2.2 Dodavanje elementa na glavi liste Prethodno opisani postupak formiranja liste, na način da se novi čvor umeće na glavu liste, implementiran je u funkciji koja se naziva add_front_node(). void add_front_node(LIST *pList, Node *n) { if(n != NULL) /* izvrši samo ako je alociran element */ {

252

}

n->next = *pList; *pList = n;

}

Prvi argument funkcije je pokazivač na pokazivač liste. Ovakvi prijenos argumenta je nužan jer se u funkciji mijenja vrijednost pokazivača liste. Drugi argument je pokazivač čvora koji se unosi u listu. Dodavanje elementa x u listu List sada se vrši jednostavnim pozivom funkcije: add_front_node(&List, newNode(x));

Složenost ove operacije je O(1).

18.2.3 Brisanje čvora na glavi liste Brisanje čvora glave liste je jednostavna operacija. Njome čvor, koji slijedi iza glave liste, (List->next) postaje glava liste, a trenutni čvor glave liste (n) se dealocira iz memorije. To

ilustrira slika 18.2.

Node *n = List; List = List->next; freeNode(n);

Slika 18.2 Brisanje s glave liste Postupak brisanja glave liste se formalizira funkcijom delete_front_node(). void delete_front_node(LIST *pList) { Node *n = *pList; if(n != NULL) { *pList = *pList->next; freeNode(n); } }

Uočite da se i u ovoj funkciji mijenja vrijednost pokazivača glave liste, stoga se u funkciju prenosi pokazivač na pokazivač liste. Složenost ove operacije je O(1).

18.2.4 Brisanje cijele liste Brisanje cijele liste se vrši funkcijom delete_all_nodes(), na način da se brišu svi čvorovi sprijeda. void delete_all_nodes( LIST *pList ) { while (*pList != NULL) delete_front_node(pList); }

253

18.2.5 Obilazak liste Ako je poznat pokazivač liste uvijek se može odrediti pokazivač na sljedeći element pomoću “next” pokazivača. Node *ptr = List->next;

Dalje se sukcesivno može usmjeravati pokazivač ptr na sljedeći element liste naredbom ptr = ptr->next;

Na taj se način može pristupiti svim elementima liste. Taj postupak se zove obilazak liste (eng. list traversing). Obilazak liste završava kada je ptr == NULL. Primjer: Pretpostavimo da želimo ispisati sadržaj liste redoslijedom od glave prema kraju liste. To možemo ostvariti sljedećim programom: Node *p = List; /* while (p != NULL) /* { printf("%d\n", p->elem); /* p = p->next; /* /* }

koristi pomoćni pokazivač p */ ponavljaj do kraja liste */ ispiši sadržaj elementa i postavi pokazivač na sljedeći element liste

*/ */ */

U ovom primjeru na sve elemente liste je primijenjena ista funkcija. Postupak kojim se na sve elemente liste djeluje nekom funkcijom može se poopćiti funkcijom list_for_each() u sljedećem obliku: void list_for_each(LIST L, void (*pfun)(Node *)) { while( L != NULL) { (*pfun)(L); L = L->next; } }

Prvi argument ove funkcije je lista, a drugi argument ove funkcije je pokazivač na funkciju, koja se primijenjuje na sve elemente liste. To može biti bilo koja void funkcija kojoj je argument tipa Node *. Definiramo li funkciju: void printNode(Node *n) { printf("%d\n", n->elem);}

tada poziv funkcije: list_for each(L, printNode);

ispisuje sadržaj cijele liste Kada je potrebno izvršiti obilazak liste od kraja prema glavi liste, ne može se primijeniti iterativni postupak. U tom slučaju se može koristiti rekurzivna funkcija reverse_list_for_each(). void reverse_list_for_each(LIST L, void (*pfun)(Node *)) { if (L == NULL) return; reverse_list_for_each(L->next, pfun); (*pfun)(L);

254

}

Obilazak liste je nužan i kada treba odrediti posljednji čvor liste. To vrši funkcija last_node(). Node *last_node(LIST L) {/*vraća pokazivač na krajnji čvor liste*/ if(L == NULL) return NULL; while ( L->next != NULL) L = L->next; return L; }

Broj elemenata koji sadrži lista daje funkcija num_list_elements(). int num_list_elements(LIST L) { int num = 0; while ( L != NULL) { num++; L = L->next; } return num; /* broj elemenata liste */ }

18.2.6 Traženje elementa liste Često je razlog za obilazak liste traženje elementa liste. U slučaju kada je element liste prostog skalarnog tipa može se koristiti funkciju find_list_element(). /* funkcija: find_list_element * ispituje da li lista sadrži element x * Argumenti: * x – element kojeg se traži * List – pokazivač na glavu liste * Vraća: * pokazivač na čvor koji sadrži x, ili NULL ako x nije u listi */ Node *find_list_element(LIST L, elemT x) { while( L != NULL && p->elem != x ) L = L->next; return L; }

Pretraživanje liste ima složenost O(n), gdje je n broj elemenata liste.

18.2.7 Dodavanje čvora na kraju liste Dodavanje čvora na kraju liste vrši se na sljedeći način: Ako lista još nije formirana, tj. ako je List==NULL, koristi se postupak opisan u funkciji add_front_node(). Ako je List != NULL, tada 1. Obilaskom liste odredi se pokazivač krajnjeg čvora liste. Taj pokazivač, nazovimo ga p, ima karakteristiku da je p->next == NULL.

255

2. Zatim se p->next postavi na vrijednost pokazivača čvora kojeg se dodaje u listu. 3. Pošto dodani čvor predstavlja novi kraj liste njega se zaključuje s NULL . Ovaj postupak je realiziran funkcijom add_back_node(); /* funkcija: add_back_node * --------------------* Dodaje čvor na kraj liste * Argumenti: * pList - pokazivač na pokazivač liste * n – pokazivač na čvor koji se dodaje u listu */ void add_back_node(LIST *pList, Node *n) { if(n == NULL) /* Izvršava se samo ako je return; /* alociran čvor. if(*pList == NULL) { /* Ako lista nije formirana *pList = n; /* iniciraj pokazivač n->next = NULL; } else { LIST p = *pList; while ( p->next != NULL) /* 1. odredi krajnji čvor p = p -> next; p ->next = n; /* 2. dodaj novi čvor n->next = NULL; /* 3. označi kraj liste } }

*/ */ */ */

*/ */ */

Ovu funkciju se koristi po istom obrascu kao i funkciju add_front_node(), tj. novi element (x) se dodaje naredbom: add_back_node(&List, newNode(x));

18.2.8 Umetanje i brisanje čvora unutar liste Postupak umetanja ili brisanja čvora n ilustrira slika 18.3

Slika 18.3 Umetanje i brisanje čvora unutar vezane liste Brisanje čvora koji slijedi iza čvora “p” ( na slici, to je čvor “n”) vrši se naredbama:

256

n = p->next; p->next = n->next; freeNode(n);

/* odredi sljedeći */

Ako treba izbrisati čvor “n”, potrebno je odrediti čvor “p”, koji mu prethodi. p = /* odredi čvor koji prethodi čvoru n*/ p->next = n->next; freeNode(n);

Umetanje čvora “n” iza čvora “p” se provodi naredbama: n->next = p->next; p->next = n;

Umetanje čvora “n” iza čvora “x” provodi se tako da se najprije odredi čvor “p” koji prethodi čvoru “x”, a zatim se provodi prethodni postupak. Operaciju kojom se određuje čvor koji prethodi čvoru “n” realizira se funkcijom get_previous_node(), koja vraća pokazivač na prethodni čvor, ili NULL ako ne postoji prethodni čvor. Node *get_previous_node(LIST List, Node *n ) { Node *t, *pre; t = pre = List; /* start od glave */ while( (t != n) /* dok ne pronađeš n */ && (t->next != NULL )) { pre = t; /* pamti prethodni */ t = t->next ; } return (pre); /* vrati prethodni */ }

Sada se postupak brisanja čvora može realizirati funkcijom delete_node(). Njome se iz liste briše čvor “n”. void delete_node(LIST *pList, Node *n) { if(*pList == NULL || n == NULL) return; if(*pList == n) { /* ako n pokazuje glavu */ *pList = n->next; /* odstrani glavu */ } else { Node *pre = get_previous_node(*pList, n ); pre->next = n->next; } freeNode(n); /* dealociraj čvor */ }

18.2.9 Brisanje čvora na kraju liste Brisanje čvora na kraju liste je jednako komplicirana operacija kao i brisanje unutar liste, jer se i u ovom slučaju mora odrediti čvor koji mu prethodi. To je realizirano funkcijom delete_back_node(). /* Funkcija: delete_back_node

257

* odstranjuje čvor na kraju liste * Argumenti: * pList - pokazivač na pokazivač liste. */ void delete_back_node(LIST *pList) { Node *pre, back; if (*pList == NULL) return; back = pre = *pList; while(back->next != NULL ) { pre = back; back = back->next ; } if(back == *pList) *pList == NULL; else pre->next = NULL; freeNode(back);

/* /* /* /* /*

/* pre – prethodni /* back – krajnji

*/ */

/* start od glave */ /* pronađi kraj liste*/ /* zapamti prethodni */

ako je krajnji = glava */ napravi praznu listu */ inače */ prethodni postaje krajnji*/ dealociraj čvor */

}

Primjer: U program testlist.c testiraju se prije opisane funkcije. void printNode(Node *n) { if(n) printf( "%d ", n->elem ); } void printList(LIST List) { if( L == NULL ) printf( "Lista je prazna\n" ); else list_for_each(L, printNode); printf( "\n" ); } int main( ) { LIST List; int i; /* obvezno iniciraj listu */ List = NULL ; /* formiraj listu s 10 cijelih brojeva */ for( i = 0; i < 10; i++ ) { add_front_node(&List, newNode(i)); printList(List); } /* izbriši prednji i stražni element */ delete_front_node(&List); delete_back_node(&List); printList(List); if(find_list_element(List, 5) != NULL) printf( "pronadjen element :%d\n", 5 );

258

if(find_list_element(List, 9) == NULL) printf( "Nije pronadjen element :%d\n", 9 ) ;

}

add_back_node(&List, newNode(9)); printList(List); delete_all_nodes(&List) ; printList(List); return 0;

Nakon izvršenja dobije se ispis: 0 1 0 2 1 0 3 2 1 0 4 3 2 1 0 5 4 3 2 1 0 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 8 7 6 5 4 3 2 1 pronadjen element :5 Nije pronadjen element :9 8 7 6 5 4 3 2 1 9 Lista je prazna

18.3 Što može biti element liste Funkcije koje su u prethodnom poglavlju korištene za rad s vezanom listom mogu se, u neizmijenjenom obliku, primijeniti jedino na listu cijelih brojeva. Sada će biti pokazano kako se te funkcije mogu prilagoditi za rad s listama koje sadrže proizvoljan tip podataka. Uzmimo za primjer da u listi treba zapisati podatke o studentima: 1) njihovo ime i 2) ocjenu. To se može realizirati na dva načina: Prvi je način da se čvor liste formira tako da sadrži više različitih tipova podataka, primjerice: typedef struct snode StudentNode; typedef StudentNode *STUDENTLIST; typedef struct snode { StudentNode *next; char *ime; int ocjena; } StudentNode;

Drugi je način da se element liste definira strukturom koja ima više članova, primjerice: typedef struct student_info { char *ime; int ocjena; }Student; struct snode

259

{ StudentNode *next; Student elem;

/* pokazuje na slijedeći čvor */ /* element liste tipa Student */

};

U oba slučaja lista se formira istim postupkom kao i lista stringova, jedino je potrebno modificirati funkcije newNode(), freeNode(), Find() i Print(). Primjerice, funkcija newNode() u ovom slučaju ima dva argumenta: ime i ocjenu, a realizira se na sljedeći način. Prvi način

Drugi način

StudentNode *newNode(char *ime, int ocjena) { StudentNode *novi = malloc(sizeof(StudentNode)); if(novi != NULL) { novi->ime = strdup(ime); novi->ocjena = ocjena; } return novi; }

StudentNode *newNode(char *ime, int ocjena) { StudentNode *novi = malloc(sizeof(StudentNode)); if(novi != NULL) { novi->elem.ime = strdup(ime); novi->elem.ocjena = ocjena; } return novi; }

18.4 Lista sa sortiranim redoslijedom elemenata U dosadašnjim primjerima podaci su bili svrstani u listi redoslijedom kojim su i uneseni u listu. Često je pak potrebno da podaci u listi budu u sortiranom redoslijedu. To se može postići na dva načina. Prvi je način da se izvrši sortiranje liste, a drugi je način da se već pri samom unošenju podataka ostvari sortirani redoslijed elemenata. S obzirom da je namjena liste da služi kao kolekcija u koju se često unose i odstranjuju elementi, ovaj drugi pristup se češće koristi. Izrada liste s podacima o imenu i ocjeni studenta je tipičan primjer korištenja liste u kojem je poželjno imati sortiran redoslijed elemenata. Sada će biti prikazana izvedba modula za manipuliranje s listom studenata. Prema uputama za formiranje modula, iznesenim u poglavlju 9, modul za listu studenata će se formirati od sljedećih datoteka: 1. datoteka specifikacije modula ("studentlist.h") sadrži deklaracije funkcija i struktura koje se koriste za rad s listom. 2. datoteka implementacije ("studentlist.c") sadrži definicije varijabli i implementaciju potrebnih funkcija. 3. datoteka primitivnih funkcija za manipuliranje listom ("listprim.c") koje su definirane u prethodnom poglavlju. Ova datoteka će se koristiti isključivo kao #include datoteka u datoteci "studentlist.c". Sve funkcije iz ove datoteke su označene prefiksom static, što znači da će biti vidljive samo u datoteci "studentlist.c". 4. Testiranje modula se provodi programom studlist-test.c. Datoteka specifikacije –"studentlist.h" #ifndef _STUDENTLIST_H_ #define _STUDENTLIST_H_ /*

sortirana lista za evidenciju studenata */

typedef struct stnode StudentNode; typedef StudentNode *STUDENTLIST;

260

struct stnode { StudentNode *next; char *ime; int ocjena; }; void StudentL_insertSorted (STUDENTLIST *pList, char *ime, int ocjena); /* U listu umeće podatke o studentu: ime i ocjenu * Ako ime već postoji, zamjenuje se vrijednost ocjene. * Lista je uvijek sortirana prema imenu studenta */ StudentNode *StudentL_find(STUDENTLIST L, char *s); /* Traži čvor liste u kojem je ime jednako stringu s * Vraća pokazivač na čvor, ili NULL ako nije pronađen string */ void StudentL_deleteNode(STUDENTLIST *pList, StudentNode *n); /* Odstranjuje čvor liste na kojeg pokazuje n */ void StudentL_delete(STUDENTLIST *pList) ; /* Odstranjuje sve čvorove liste */ int StudentL_size(STUDENTLIST List); /* Vraća broj elemanate liste */ void StudentL_print(STUDENTLIST L ); /* Ispisuje sadržaj liste */ #endif

Sve deklaracije su napisane unutar makro naredbi #ifndef _STUDENTLIST_H_ #define _STUDENTLIST_H_ ........ deklaracije ..... #endif

To osigurava da se u jednoj izvornoj datoteci samo jedan put može umetnuti h-datoteka. Čvor liste je opisan strukturom StudentNode, a tip pokazivač na taj čvor je nazvan STUDENTLIST. Imena svih funkcija počinju prefiksom StudentL_ , a ostatak imena je u skladu s imenom ekvivalentnih primitivnih funkcija koje su definirane u prethodnom poglavlju. Datoteka implementacije – "studentlist.c" #include #include #include "studentlist.h" static StudentNode *newNode(char *ime, int ocjena) { StudentNode *n = malloc(sizeof(StudentNode)); if(n != NULL) { n->ime=strdup(ime);

261

n->ocjena = ocjena; } return n; } static void freeNode(StudentNode *n) { if(n) { free(n->ime); free(n); } } #define LIST STUDENTLIST #define Node StudentNode #include "listprim.c" #undef LIST #undef Node int { }

StudentL_size(STUDENTLIST List) return num_list_elements(List);

void StudentL_deleteNode(STUDENTLIST *pList, StudentNode *n) { delete_node(pList, n); } void StudentL_delete(STUDENTLIST *pList) { delete_all_nodes(pList); } void StudentL_insertSorted (STUDENTLIST *pList, { StudentNode *tmp = *pList; StudentNode *pre = NULL; StudentNode *n; int cmp;

char *ime, int ocjena)

if (!*pList) { /* ako je lista prazna, formiraj prvi čvor */ n= newNode (ime, ocjena); n->next=NULL; *pList = n; return; } /* nađi zadnji čvor (i njemu prethodni) u kojem je ime manje ili jednako zadanom imenu, ili kraj liste */ cmp = strcmp(ime, tmp->ime); while ((tmp->next) && (cmp > 0)) { pre = tmp; tmp = tmp->next; cmp = strcmp(ime, tmp->ime); } /* ako ime već postoji, zamijeni ocjenu if(cmp == 0) { tmp->ocjena = ocjena; return; }

*/

262

/*inače dodaj novi čvor */ n= newNode (ime, ocjena); n->next=NULL;

}

/* ako je dosegnut kraj liste, dodaj na kraj liste */ if ((tmp->next == NULL) && (cmp > 0)) { tmp->next = n; return; } /*ili umetni iza prethodnog*/ if (pre != NULL) { pre->next = n; n->next = tmp; } /*ili stavi na početak liste ako je prethodni == NULL*/ else { n->next = *pList; *pList = n; }

StudentNode *StudentL_find(STUDENTLIST L, char *ime) { while( L != NULL && strcmp(L->ime, ime) ) L = L->next; return L; } void PrintStInfo(StudentNode *n ) { printf( "Ime:%s, ocjena:%d \n", n->ime, n->ocjena ); } void StudentL_print(STUDENTLIST L ) { if( L == NULL ) printf( "Lista je prazna\n" ); else list_for_each(L, PrintStInfo); printf( "\n" ); }

Izvedba svih funkcija je izvršena po poznatom obrascu, jedino je potrebno objasniti algoritam za sortirano umetanje čvorova liste. On glasi: Algoritam: Sortirano umetanje čvora liste (ime,ocjena) Ako je lista prazna, Formiraj čvor s zadanim imenom i ocjenom i stavi ga na početak liste inače Nađi zadnji čvor (i njemu prethodni) u kojem je ime leksikografski manje ili jednako zadanom imenu, ili kraj liste Ako čvor s zadanim imenom već postoji, Zamijeni ocjenu inače Formiraj čvor s zadanim imenom i ocjenom. Ako je dosegnut kraj liste Dodaj na kraj liste

263

inače Umetni iza prethodnog ili stavi na početak liste ako je prethodni == NULL Testiranje modula se provodi programom studlist_test.c. /* Datoteka: studlist_test.c */ #include #include #include "studentlist.h" int main( ) { STUDENTLIST List, n; int i; List = NULL ; StudentL_insertSorted StudentL_insertSorted StudentL_insertSorted StudentL_insertSorted

(&List, (&List, (&List, (&List,

"Bovan "Radic "Marin "Bovan

Marko", 5); Jure", 2); Ivo", 3); Marko", 2);

printf("Lista ima %d elemenata\n", StudentL_size(List)); StudentL_print(List ); n = StudentL_find(List, "Bovan Marko"); if(n != NULL) StudentL_deleteNode(&List, n); StudentL_print(List); StudentL_delete(&List) ; }

return 0;

Program se kompilira komandom: c:> cl studlist_test.c

studentlist.c

Nakon izvršenja, dobije se sljedeći ispis: Lista ima Ime:Bovan Ime:Marin Ime:Radic

3 elemenata Marko, ocjena:2 Ivo, ocjena:3 Jure, ocjena:2

Ime:Marin Ivo, ocjena:3 Ime:Radic Jure, ocjena:2

Zadatak: Objasnite zašto u prethodnom ispisu lista početno ima tri elementa, iako je u programu četiri puta primijenjena funkcija StudentL_insertSorted(). Zadatak: Napišite program u kojem korisnik unosi rezultate ispita (ime i ocjenu studenta) u sortiranu listu. Unos završava kada se otkuca “prazno ime” ili ocjena 0. Nakon toga treba

264

rezultate ispita upisati u tekstualnu datoteku, na način da se u jednom retku ispiše redni broj, ime i ocjena studenta.

18.5 Implementacija ADT STACK pomoću linearne liste U poglavlju 16 izvršena je implementacija ADT STACK pomoću niza elemenata. Stog se može efikasno implementirati i pomoću linearne liste. Uzmemo li da je specifikacija ADT STACK ista kao i specifikacija ADT za implementaciju pomoću niza u datoteci "stack.h", koja je opisana u poglavlju 16, implementacija se može napraviti na način kako je opisano u datoteci "stack-list.c". Sada STACK predstavlja pokazivač na strukturu stack, koja ima samo jedan element, pokazivač na čvor linearne liste. Taj pokazivač je nazvan top, i on pokazuje na glavu liste. Operacija Push() je implementirana kao umetanje elementa na glavu liste, a pop() kao brisanje elementa s glave liste. /* Datoteka: stack-list.c */ /* Implementacija ADT STACK pomoću vezane liste */ #include #include #include "stack.h" /* typedef int stackElemT; /* typedef struct stack *STACK;

definiran u stack.h*/ definiran u stack.h*/

struct node { stackElemT elem; struct node *next; }; struct stack { struct node *top; }; static void stack_error(char *s) { printf("\nGreska: %s\n", s); exit(1); } STACK stack_new(void) {/*alocira ADT STACK*/ STACK S = malloc(sizeof(struct stack)); if(S != NULL) S->top = NULL; return S; } void stack_free(STACK S) {/*dealocira ADT STACK*/ struct node *n; assert(S != NULL); if( !stack_empty(S)) for (n = S->top; n != NULL; n = n->next) free(n); } int stack_empty(STACK S)

265

{/* vraća != 0 ako je stog prazan*/ assert(S != NULL); return ( S->top==NULL); } unsigned stack_count(STACK S) {/*vraća broj elemenata na stogu*/ unsigned num = 0; struct node *n; assert(S != NULL); if( !stack_empty(S)) for (n = S->top; n != NULL; num++; return num; }

n = n->next)

stackElemT stack_top(STACK S) {/*vraća vrijednost elementa na vrhu stoga*/ assert(S != NULL); return S->top->elem; } stackElemT stack_pop(STACK S) {/* Skida čvor s vrha stoga */ stackElemT el; struct node *n; assert(S != NULL); if (stack_empty(S)) stack_error("Stog je prazan"); n = S->top; el = n->elem; S->top = n->next; free(n); return el; } void stack_push(STACK S, stackElemT el) {/* Ubacuje čvor na vrh stoga*/ struct node *n; assert(S != NULL); n = malloc(sizeof(struct node)); if (n != NULL) { n->elem = el; n->next = S->top; S->top = n; } else printf(" Nema dovoljno memorije!\n"); }

Testiranje ove implementacije se provodi s programom stack-test.c, koji je opisan u poglavlju 16. Jedina razlika je da se u u tom programu umjesto uključenja datoteke "stack-arr.c" uključi datoteka "stack-list.c". Zadatak: Napišite ADT imena STRSTACK za stog na kojeg će se stavljati stringovi. Vodite računa da treba alocirati i dealocirati memoriju koju zauzima string. Napišite program za testiranje ADT STRSTACK.

266

18.6 Implementacija ADT QUEUE pomoću vezane liste Red se može jednostavno implementirati i pomoću vezane liste. Za implementacija reda pomoću vezane liste koristit će se prethodna specifikaciju ADT-a QUEUE (danu u datoteci "queue.h"). U ovoj implementaciji QUEUE predstavlja pokazivač na strukturu queue, koja ima dva člana: front i back. To su pokazivači na prednji i stražnji element liste. Operacijom put() stavlja se element na kraj liste, a operacijom get() skida se element s glave liste.

Slika 18.4 Operacije s listom koju se koristi kao red za čekanje /* Datoteka: queue-list.c */ /* Queue #include #include #include #include

realiziran pomoću vezane liste*/ "queue.h"

/* typedef int queueElemT; */ /* typedef struct queue *QUEUE; */ struct node { queueElemT elem; struct node *next; }; struct queue { struct node * front; struct node * back; }; QUEUE queue_new(void) { /*alocira ADT QUEUE*/ QUEUE Q = malloc(sizeof(struct queue)); if(Q != NULL){ Q->front = Q->back = NULL; } return Q; } void queue_free(QUEUE Q) {/*dealocira ADT QUEUE*/ struct node *n; assert(Q != NULL); while ((n = Q->front) != NULL ) { Q->front = n ->next; free(n); }

267

free(Q); } int queue_empty(QUEUE Q) {/* vraća != 0 ako je red prazan*/ assert(Q != NULL); return Q->front == NULL; } int queue_full(QUEUE Q) { /* vraća != 0 ako je red popunjen*/ /* uvijek možemo dodati element u listu */ return 0; } void queue_put(QUEUE Q, queueElemT el) {/* stavlja element el u red Q*/ struct node * n; assert(Q != NULL); n = malloc(sizeof(struct node)); if (n != NULL) { n->elem = el; n->next = NULL; if (queue_empty(Q)) Q->front = n; else Q->back->next = n; Q->back = n; } } queueElemT queue_get(QUEUE Q) {/* vraća element iz reda Q*/ queueElemT el; struct node * n; assert(Q != NULL); n = Q->front; el = n->elem; Q->front = n->next; if (Q->front == NULL) Q->back = NULL; free(n); return el; } unsigned queue_count(QUEUE Q) {/*vraća broj elemenata reda*/ unsigned num = 0; struct node *n; assert(Q != NULL); if( !queue_empty(Q)) for (n = Q->front; n != NULL; num++; return num; }

n = n->next)

void queue_print(QUEUE Q) { int i; struct node *n; assert(Q != NULL); n = Q->front;

268

if( Q->front == NULL ) printf( "Red je prazan\n" ); else do { printf( "%d, ", n->elem ); n = n->next; } while( n != NULL ); printf( "\n" ); }

Testiranje ove implementacije se provodi s programom queue-test.c, opisanim u pogavlju 16, tako da se u njemu umjeto uključenja datoteke "queue-arr.c" uključi datoteka "queuelist.c".

18.7 Dvostruko vezana lista Ukoliko se čvor liste formira tako da sadrži dva pokazivača, next - koji pokazuje na sljedeći čvor i prev - koji pokazuje na prethodni čvor, dobije se dvostruko vezana lista. Realizira se pomoću sljedeće strukture podataka: typedef int elemT; typedef struct node Node; typedef Node *DLIST; struct node { elemT elem; Node *next; Node *prev; }

/* element liste */ /* pokazuje na sljedeći čvor */ /* pokazuje na prethodni čvor */

Slika 18.5 Dvostruko vezana lista

Veze među čvorovima ilustrira slika 18.5. Karakteristike prikazane liste su: 1. Pokazivač next krajnjeg elementa i pokazivač prev glave liste su jednaki NULL. 2. Ovakvu listu se može iterativno obilaziti u oba smjera, od glave prema kraju liste (korištenjem pokazivača next) i od kraja prema početku liste (korištenjem pokazivača prev). 3. Umetanje unutar liste i brisanje iz liste se provodi jednostavnije i brže nego kod jednostruko vezane liste, jer je u svakom čvoru poznat pokazivač na prethodni čvor. 4. U odnosu na jednostruko vezanu listu, dvostruko vezana lista zauzima više memorije (zbog pokazivača prev) .

269

5. Ako se vrši umetanje i brisanje čvorova samo na početku liste, tada je bolje rješenje koristiti jednostruko vezanu listu. Kroz dva primjera bit će pokazano kako se implementira dvostruko vezana lista. Primjer:

Prikana

je

implementacija

funkcija

za

umetanje

čvora

na

glavi

liste

(dlist_add_front_node) i odstranjivanje čvora na glavi liste (dlist_delete_front_node). Uočite da je potrebno izvršiti nešto više operacija nego kod

jednostruko vezane liste. void dlist_add_front_node(DLIST *pList, Node *n) { if(n != NULL) /* izvrši samo ako je alociran čvor */ { n->next = *pList; n->prev = NULL; if(*pList != NULL) (*pList)->prev = n; *pList = n; } } void dlist_delete_front_node(DLIST *pList) { Node *tmp = *pList; if(*pList != NULL) { *pList = (*pList)->next; if(*pList != NULL) (*pList)->prev = NULL; freeNode(tmp); } }

Primjer:

Prikazana

je

implementacija

funkcija

za

brisanje

čvora

unutar

liste

(dlist_delete_node). Uočite da se ova operacija izvršava efikasnije nego kod primjene

jednostruko vezane liste. void dlist_delete_node(DLIST *pList, Node *n) { if(*pList == NULL || n == NULL) return; if(*pList == n) { /* ako n pokazuje glavu */ *pList = n->next; /* odstrani glavu */ *plist->prev = NULL; } else { n->prev->next = n->next; if(n->next != NULL) n->next->prev = n->prev; } freeNode(n); /* dealociraj čvor */ }

Zadatak: Napišite funkciju za brisanje i umetanje čvora na kraju liste. Za testiranje implementacije koristite isti program kao kod jednostruko vezane liste.

270

Napomena: Funkcije za formiranje čvora, brisanje liste i traženje elementa liste su iste kao kod jednostruko vezane liste.

18.8 Generički dvostrani red - ADT DEQUEUE Dvostruka se lista može iskoristiti za efikasnu implementaciju dvostranog reda (eng. double ended queue) u obliku ADT DEQUEUE. Temeljne operacije s dvostranim redom - DEQUEUE su: front() - vraća element na glavi liste push_front(el) - stavlja element el na glavu liste pop_front() - skida element s glave liste back() - vraća element s kraja liste push_back(el) - stavlja element el na kraj liste pop_back() - skida element s kraja liste find(el) - vraća pokazivač na traženi element ili null ako element nije u listi delete(el) - briše element el ako postoji u listi for_each(fun) - primjenjuje funkciju fun na sve elemente liste size() - vraća broj elemenata u listi empty() - vraća nenultu vrijednost ako je red prazan, inače vraća 0 Cilj je izvršiti generičku implementaciju ADT DQUEUE, tako da se on može primijeniti na bilo koji tip elemenata reda. Zbog toga će se u implementaciji ADT-a koristiti sljedeće strukture podataka: typedef struct dnode DNode; struct dnode { void *elem; DNode *prev; DNode *next; }; typedef struct dequeue Dequeue; typedef struct dequeue *DEQUEUE; typedef int (*CompareFuncT)(void *, void *); typedef void *(*CopyFuncT)(void *); typedef void (*FreeFuncT)(void *); struct dequeue { DNode *front; DNode *back; int size; CompareFuncT CompareElem; CopyFuncT CopyElem; FreeFuncT FreeElem; };

/*pokazivač prednjeg čvora liste */ /*pokazivač stražnjeg čvora liste */ /*broj elemenata u listi */ /*funkcija za usporedbu elemenata */ /*funkcija za kopiranje i alociranje*/ /*funkcija za dealociranje elementa */

Čvor liste je opisan strukturom DNode. Sadrži dva pokazivača koji pokazuju na prethodni i sljedeći čvor liste te pokazivač na element liste koji je deklariran kao void*. Tom pokazivaču se može pridijeliti adresa bilo koje strukture podataka, ali void pokazivači ne sadrže

271

informaciju o operacijama koje se mogu provoditi s podacima na koje oni pokazuju. Zbog toga će biti potrebno da korisnik definira tri pokazivača na funkcije; o o o

CompareElem - pokazivač funkcije za usporedbu dva elementa (poput strcpy), CopyElem - pokazivač funkcije za alociranje i kopiranje elementa (poput strdup) i FreeElem - pokazivač funkcije za dealociranje memorije koju element liste zauzima

(popot free). Ovi se pokazivači bilježei u strukturi Dequeue, koja je temelj za definiranje ADT DEQUEUE. Tip ovih funkcija je definiran s tri typedef definicije. U structuri Dequeue bilježe se i pokazivači na prednji (front) i stražnji element liste (back) te broj elemenata u listi Sadržaje ove strukture se određuje pri inicijalizaciji ADT-a funkcijom (size). dequeue_new(). Ta i ostale funkcije opisane su u datoteci "dequeue.h". Datoteka specifikacije - "dequeue.h": /* Datoteka: dequeue.h * Specifikacija ADT DEQUEUE */ #ifndef _DEQUEUE_H_ #define _DEQUEUE_H_ typedef struct dequeue *DEQUEUE; typedef int (*CompareFuncT)(void *, void *); typedef void *(*CopyFuncT)(void *); typedef void (*FreeFuncT)(void *); DEQUEUE dequeue_new(CompareFuncT Compare, CopyFuncT Copy, FreeFuncT Free); /* Konstruktor ADT DEQUEUE * Argumenti: * Free - pokazivač funkcije za dealociranje elementa liste * Copy - pokazivač funkcije za alociranje elementa liste * Compare - pokazivač funkcije za usporedbu elemenata liste * vraća 0 ako su elementi jednaki, * 0 ako je prvi element veći od drugoga * Ako su svi pokazivači funkcija jednaki nuli, * tada se podrazumijeva se da je element liste cijeli broj. * Primjer korištenja: * Za rad s listom stringova konstruktor je * DEQUEUE d = dequeue_new(strcmp,strdup, free); * Za rad s listom cijelih brojeva konstruktor je * DEQUEUE d = dequeue_new(0,0,0); * i tada se void pokazivači tretiraju kao cijeli brojevi */ void dequeue_free(DEQUEUE d); /* Destruktor DEQUEUE d */ int dequeue_size(DEQUEUE d); /* Vraća veličunu DEQUEUE d*/ int dequeue_empty(DEQUEUE d); /* Vraća 1 ako je DEQUEUE d prazan, inače vraća 0*/

272

void *dequeue_front(DEQUEUE d); /* Vraća pokazivač elementa na glavi DEQUEUE d * ili cijeli broj ako lista sadrži cijele brojeve */ void dequeue_push_front(DEQUEUE d, void *el); /* Stavlja element el na glavu DEQUEUE d*/ void dequeue_pop_front(DEQUEUE d); /* Skida element s glave DEQUEUE d*/ void *dequeue_back(DEQUEUE d); /* Vraća pokazivač elementa s kraja DEQUEUE d * ili cijeli broj ako lista sadrži cijele brojeve */ void dequeue_push_back(DEQUEUE d, void *el); /* Stavlja element el na kraj DEQUEUE d*/ void dequeue_pop_back(DEQUEUE d); /* Skida element s kraja DEQUEUE d*/ void *dequeue_find(DEQUEUE d, void *el); /* Vraća pokazivač elementa liste koji je jednak elementu el */ int dequeue_delete(DEQUEUE d, void *el); /* Briše element el. Ako postoji vraća 1, inače vraća 0 */ void dequeue_for_each(DEQUEUE d, void (*func)(void *)); /* Primjenjuje funkciju func na sve elemente DEQUEUE d*/ #endif

Implementacija ADT DEQUEUE je opisana u datoteci "dequeue.c" . /* Datoteka: dequeue.c * Implementacija ADT DEQUEUE */ #include #include #include #include "dequeue.h" typedef struct dnode DNode; struct dnode { void *elem; DNode *prev; DNode *next; }; typedef struct dequeue Dequeue; struct dequeue { DNode *front; DNode *back; int size; CompareFuncT CompareElem; CopyFuncT CopyElem; FreeFuncT FreeElem;

273

}; static int CompareInternal(void *a, void *b) { if((int)a > (int)b) return 1; else if((int)a < (int)b) return -1; else return 0; } DEQUEUE dequeue_new( CompareFuncT Compare, CopyFuncT Copy, FreeFuncT Free) { DEQUEUE d=(DEQUEUE) malloc(sizeof(Dequeue)); if(d ) { d->front = d->back = NULL; d->size = 0; d->CopyElem = Copy; d->FreeElem = Free; if(Compare == NULL) d->CompareElem = CompareInternal; else d->CompareElem = Compare; } return d; } /* dvije pomoćne funkcije - make_node i free_node*/ static DNode *make_node(DEQUEUE d, void *el) {/* stvara čvor koristeći CopyElem funkciju */ DNode *n=(DNode *) malloc(sizeof(DNode)); if(n) { n->prev = n->next = NULL; if(d->CopyElem != NULL) n->elem = d->CopyElem(el); else n->elem = el; } return n; } static void free_node(DEQUEUE d, DNode *n) {/* dealocira čvor koristeći FreeElem funkciju */ if(n) { if(d->FreeElem != NULL) d->FreeElem(n->elem); free(n); } } void dequeue_free(DEQUEUE d) { DNode *tmp, *node; assert(d); node = d->front; while (node) { tmp = node; node = node->next; free_node(d, tmp);

274

}

} free(d);

int dequeue_size(DEQUEUE d) { assert(d); return d->size; } int dequeue_empty(DEQUEUE d) { assert(d); return d->size==0; } void *dequeue_front(DEQUEUE d) { assert(d); if(d->front) return d->front->elem; else return NULL; } void dequeue_push_front(DEQUEUE d, void *elem) { DNode *new_node; assert(d); new_node = make_node(d, elem); if(new_node == NULL) return; if (d->front) { new_node->next = d->front; d->front->prev = new_node; d->front = new_node; } else { d->front = d->back = new_node; } d->size ++; } void dequeue_pop_front(DEQUEUE d) { DNode *old; assert(d); if (d->front) { old = d->front; d->front = d->front->next; if(d->front == NULL) d->back = NULL; else d->front->prev = NULL; free_node(d, old); d->size --; } } void* dequeue_back(DEQUEUE d) { assert(d);

275

}

if(d->back) return d->back->elem; else return NULL;

void dequeue_push_back(DEQUEUE d, void * elem) { DNode *new_node; DNode *last; assert(d); new_node = make_node(d, elem); if(new_node == NULL) return; if (d->back) { last = d->back; last->next = new_node; new_node->prev = last; d->back = new_node; } else { d->front = d->back = new_node; } d->size++; } void dequeue_pop_back(DEQUEUE d) { DNode *old; assert(d); if (d->back) { old = d->back; d->back = d->back->prev; if(d->back == NULL) d->front = NULL; else d->back->next = NULL; free_node(d, old); d->size--; } } int dequeue_delete(DEQUEUE d, void * elem) { DNode *tmp; assert(d); tmp = d->front; while (tmp) { /*prvo pronađi element*/ if ((*d->CompareElem)(tmp->elem, elem) != 0) tmp = tmp->next; /*element u čvoru tmp*/ else if(tmp == d->front) { dequeue_pop_front(d); break; } else if(tmp == d->back){ dequeue_pop_back(d); break; } else { if (tmp->prev) tmp->prev->next = tmp->next; if (tmp->next) tmp->next->prev = tmp->prev; free_node(d, tmp);

276

d->size--; break;

} } return tmp != NULL; } void *dequeue_find(DEQUEUE d, void * elem) { DNode* node; assert(d); node = d->front; while (node){ if((*d->CompareElem)(node->elem, elem) == 0) return node->elem; node = node->next; } return NULL; } void dequeue_for_each(DEQUEUE d, void (*func)(void *)) { DNode* node; assert(d); node = d->front; while (node) { DNode *next = node->next; (*func) (node->elem); node = next; } }

Testiranje DEQUEUE se provedi programom dequeue-teststr.c. U njemu korisnik proizvoljno umeće ili briše stringove u dvostranom redu. Datoteka: dequeue-teststr.c #include #include #include #include #include "dequeue.h" void upute(void) { printf ("Izbornik:\n" " 1 Umetni ime sprijeda \n" " 2 Umetni ime straga \n" " 3 Odstrani sprijeda\n" " 4 Odstrani straga\n" " 0 Kraj\n"); } void print_elem(void *el) {printf("%s ", (char*)el);} void dequeue_print(DEQUEUE D) {

277

}

if(!dequeue_empty(D)) dequeue_for_each(D, print_elem);

int main() { int izbor; char elem[255]; DEQUEUE D = dequeue_new(strcmp, strdup, free); upute(); printf("? "); scanf("%d", &izbor); while (izbor != 0) { switch(izbor) { case 1: printf("Otkucaj ime: "); scanf("\n%s", elem); dequeue_push_front(D, elem); printf("%s ubacen u red.\n", elem); dequeue_print(D); break; case 2: printf("Otkucaj ime: "); scanf("\n%s", elem); dequeue_push_back(D, elem); printf("%s ubacen u red.\n", elem); dequeue_print(D); break; case 3: if (!dequeue_empty(D)) { printf("Bit ce %s odstranjen sprijeda.\n", (char *) dequeue_front(D)); dequeue_pop_front(D); } dequeue_print(D); break; case 4: if (!dequeue_empty(D)) { printf("Bit ce %s odstranjen straga.\n", (char *)dequeue_back(D)); dequeue_pop_back(D); } dequeue_print(D); break; default: printf("Pogresan Izbor.\n\n"); upute(); break; } printf("\n? "); scanf("%d", &izbor);

} if(dequeue_find(D, "ivo")) {

278

dequeue_delete(D, "ivo"); dequeue_print(D);

}

} dequeue_free(D); return 0;

Uočite da operacije traženja imaju složenost O(n), a sve ostale operacije imaju složenost O(1). Zadatak: Prethodni primjer testiranja dvostranog reda, koji sadrži stringove, promijenite tako da se njime testira dvostrani red koji sadrži cijele brojeve. Koristite konstruktor oblika: DEQUEUE d = dequeue_new(0,0,0);

Zadatak: Prethodni primjer testiranja dvostranog reda, koji sadrži stringove, promijenite tako da se njime testira dvostrani red koji sadrži realne brojeve tipa double. U tom slučaju morate definirati funkcije void FreeFunc(double* pd); /*dealocira memoriju koju zauzima broj tipa double*/ double *CopyFunc(double* pd); /*vraća pokazivač na kopiju alociranog broja *pd

*/

int CompareDouble(double* pd1, double* pd1); /* Vraća 0 ako je *pd1 == *pd2 * Vraća 1 ako je *pd1 > *pd2 * Vraća -1 ako je *pd1 < *pd2 */

i koristiti konstruktor oblika; DEQUEUE d = dequeue_new(FreeFunc, Copy Func, CompareFunc);

Zadatak: Generički pristup koji je korišten za definiranje ADT DEQUEUE može se iskoristiti za definiranje ADT koji služi za rad sa skupovima. Definirajte ADT SET kojim se, kao i u matemetici, može raditi sa skupovima pomoću sljedećih operacija: ADT SET Empty(S) size(S) insert(S, x) member(S, x) delete(S, x) for_each(S, fun) intersection(S1, S2) union(S1, S2) difference(S1, S2)

- vraća nenultu vrijednost ako S prazan skup - vraća broj elemenata u skupu S, tj. vraća |S| - stavlja element x u skup S, ako x ∉ S. Nakon toga je x ∈ S - vraća true ako je x ∈ S - briše element x ako postoji u skupu S - primjenjuje funkciju fun na sve elemente skupa S - vraća skup S koji je jednak presjeku skupova S1 ∩ S2 - vraća skup S koji je jednak uniji skupova S1 ∪ S2 - vraća skup S koji je jednak razlici skupova S1 \ S2

Za implementaciju ovih operacija dovoljno je koristiti jednostruko vezanu listu. Zadatak: Često se kolekcija elemenata, koja, za razliku od skupa, može sadržavati više istovrsnih elemenata, naziva vreća (eng. BAG). Realizirajte ADT BAG pomoću vezane liste.

279

Svakom elementu liste pridodajte atribut kojeg se obično naziva referentni izbroj (eng. referent count). Primjerice, možete za čvor liste koristiti sljedeću strukturu: struct bag_node { int ref_count; void *element; struct bag_node *next; }

Referentni izbroj pokazuje koliko je istih elemenata u vreći. Realizirajte generički ADT BAG koji podržava sljedeće operacije: ADT BAG empty(B) size(B) insert(B, x)

find(B, x) delete(B, x)

- vraća nenultu vrijednost ako je vreća prazna - vraća broj elemenata u vreći - stavlja element x u vreću B po sljedećem algoritmu: ako u vreći već postoji element vrijednoti x, tada uvećavaj x.ref_count inače, kopiraj i alociraj element x u vreću, postavi x.ref_count na vrijednost 1. - vraća vrijednost x.ref_count koji znači koliko ima istovrsnih elemenata vrijednosti x, ili 0 ako element x nije u vreći - briše element x ako postoji u vreći, po sljedećem algoritmu: smanji x.ref_count za jedan. ako je x.ref_count jednak nuli, tada dealociraj memoriju koju zauzima x vrati x.ref_count

18.9 Zaključak Vezana lista je dinamička strukutura. Povećava se i smanjuje prema zahtjevima korisnika. Liste su kolekcije sa “sekvencijalnim pristupom”, za razliku od nizova koje se koristi kao kolekcije sa “slučajnim pristupom”. Apstraktno, liste predstavljaju kolekciju elemenata. Operacije umetanja i brisanja elemenata kolekcije provode se jednostavnije s vezanom listom, nego je to slučaj s nizovima. Umetanje i brisanje elementa unutar jednostruko vezane liste je relativno spora operacija jer se u njoj mora odrediti i položaj elementa koji prethodi mjestu umetanja. Znatno je brže umetanje elemenata na glavi liste. Vezane liste se mogu slikovito usporediti s vlakom kojem su vagoni povezani jedan za drugim. Iz jednog vagona se može prijeći samo u susjedni vagon. Vagoni se mogu dodavati tako da se spoje na postojeću kompoziciju ili da se umetnu unutar kompozicije vlaka. Dodavanje vagona je jednostavna operacija, a umetanje vagona unutar kompozicije je složena operacija. Kada je nužno vršiti često umetanje i brisanje čvorova unutar liste, tada je povoljno koristiti dvostruko vezanu listu. Korištenjem tehnike definiranja apstraktnih tipova pokazano je da se pomoću liste mogu efikasno realizirati apstraktni tipovi STACK, QUEUE i DEQUEUE. U implementaciji ADT DEQUEUE pokazano je kako se mogu stvarati generičke kolekcije podataka.

280

19 Razgranate strukture - stabla

Naglasci: • binarno stablo • stablo s aritmetičkim izrazima • leksički i sintaktički analizator aritmetičkih izraza • višesmjerna stabla • hrpa i prioritetni redovi

19.1 Definicija stabla Stablo (eng. tree) je apstraktna struktura podataka koja se četo koristi za predstavljanje hijerarhijskih odnosa, primjerice, slika 19.1 prikazuje hijerarhiju porodičnog stabla. Struktura stabla je potpuno određena čvorovima stabla i vezama između čvorova. Čvorovi stabla sadrže neki podatak, a veze određuju hijerarhijske odnose, prema sljedećem pravilu: 1. svaki čvor (osim jednog) ima samo jednog prethodnika, 2. svaki čvor ima određen broj slijednika.

Slika 19.1 Porodično stablo Ako nije ograničen broj slijednika, stablo se naziva općenito stablo (eng. general tree). Prikazano je na slici 19.2. Ako čvorovi stabla imaju maksimalno k slijednika, stablo se naziva kstupanjsko stablo. U specijalnom slučaju, kada elementi stabla imaju maksimalno 2 slijednika, stablo se naziva binarno stablo. korijen

dubina 0

a

1

b

2

d

3

visina stabla

c e

f

g i

h

x

unutarnji čvor

y

vanjski čvor (list)

j

Slika 19.2 Strukturalni prikaz općenitog stabla

281

Jedinstveni čvor koji nema prethodnika naziva se korijen (eng. root) stabla. Čvorovi koji nemaju slijednika nazivaju se listovi stabla (eng. leaf) ili vanjski čvorovi, svi ostali čvorovi su unutarnji čvorovi, a predstavljaju podstabla (eng. subtree). Slijednik čvora se naziva dijete (eng. child), a prethodnik čvora se naziva roditelj (eng. parent). Ako više čvorova ima istog roditelja oni su jedan drugome braća (ili sestre) (eng. siblings). U binarnom stablu čvor može imati dvoje djece koja se nazivaju lijevo dijete (eng. left child) i desno dijete (eng. right child). Kada je važan redoslijed djece, stablo se naziva uređeno stablo (eng ordered tree). Uzmemo li niz čvorova n1, n2, . . . , nk na način da je ni roditelj od ni+1 for 1 ≤ i < k, tada ovaj niz tvori stazu od čvora n1 do čvora nk. Duljina staze je jednaka broju čvorova u stazi umanjeno za jedan (od čvora do njega samog vodi staza nulte duljine). Visina čvora u stablu jednaka je duljini najdulje staze od čvora do lista (visina stabla je jednaka visini korijena stabla). Dubina čvora je jednaka je duljini staze od korijena stabla do tog čvora. Ona određuje razinu čvora. Puno ili popunjeno k-stupanjsko stablo je stablo u kojem svi listovi imaju istu dubinu, a svaki čvor ima svih k grana. U takvom stablu, na dubini 0 postoji 1 (k0 ) čvor, na dubini 1 postoji k1 čvorova, na dubini 2 postoji k2 čvorova..., pa ukupan broj unutarnjih čvorova Nu u stablu visine h iznosi: h

N u = 1 + k 1 + k 2 + .. + k h −1 = ∑ k i = i =0

k h −1 k −1

(1)

i maksimalni broj vanjskih čvorova (listova) iznosi kh.

19.2 Binarno stablo Najčešće korišteni oblik stabla je binarno stablo. Definira se rekurzivno: Definicija: Binarno stablo je: 1. prazno stablo, ili 2. sastoji se od čvora koji se naziva korijen i dva čvora koji se nazivaju lijevo i desno dijete, a oni su također binarna stabla (podstabla).

Slika 19.3 (a) Puno i (b) potpuno binarno stablo

282

Ukupan broj unutarnjih čvorova punog binarnog stabla dobije se iz izraza (1). Tada je k=2 i Nu=2h-1. Broj vanjskih čvorova (listova) jednak je Nv = 2h. Makimalni broj čvorova u punom stablu, visine h, jednak je N = Nu+ Nu = 2h+1-1. Duljina staze od korijena do čvora jednaka je visini stabla. Kod punog stabla visina iznosi h = log2(N+1)-1. Kaže se da je binarno stablo potpuno 1 (eng. complete binary tree) ako je to puno stablo ili ako od tog stabla može nastati puno stablo dodavanjem čvorova na na donjoj razini s desna. Ova stabla su ilustrirana na slici 19.3. Kasnije ćemo vidjeti da se potpuna stabla mogu efikasno implementirati pomoću nizova.

19.2.1 Programska implementacija binarnog stabla Čvorovi binarnog stabla se najčešće opisuju samoreferentnim dinamičkim strukturama, primjerice u C jeziku čvor binarnog stabla se može definirati strukturom tnode: typedef char elemT; typedef struct tnode { elemT elem; struct tnode * left; struct tnode * right; } Tnode, *TREE;

Ova struktura sadrži neki element i dva pokazivača: left pokazuje na lijevo dijete, a right pokazuje na desno dijete. Ponekad se ovoj strukturi dodaje i pokazivač na roditelja. Pokazivači služe uspostavljanju veza među čvorovima. Slika 19.4 prikazuje veze među čvorovima jednog stabla.

Slika 19.4 Dva načina prikaza binarnog stabla: a) strukturalni prikaz i b) prikaz pomoću pokazivača na strukturu tnode. U strukturalnom prikazu krugovi označavaju unutarnje čvorove, a pravokutnici označavaju listove stabla Binarno stablo se može prikazati i tekstualnim zapisom, prema algoritmu prefiksne notacije stabla, na sljedeći način: Algoritam: Prefiksna notacija stabla 1. Ako čvor predstavlja prazno stablo (NULL pokazivač), tada zapiši prazne zagrade (): 2. Ako čvor predstavlja list stabla, tada zapiši element lista. 3. Ako čvor predstavlja unutarnji čvor, tada unutar zagrada zapiši element čvora te lijevi i desni čvor. 1

Neki autori puno stablo nazivaju potpuno stablo, a potpuno stablo nazivaju "skoro potpuno stablo".

283

Za stablo sa slike 19.4 vrijedi zapis: (A (B () D) (C E F)

ili

(A (B () D) (C E F) ).

)

Ovaj oblik notacije se naziva prefiksna notacija jer se apstraktno može smatrati da element unutarnjeg čvora stabla predstavlja prefiksni operator koji djeluje na svoju djecu. Prefiksna notacija se često koristi za zapis aritmetičkih izraza. Ona se vrši na način da se u zagradama najprije zapiše operator, a zatim dva operanda. Operand može biti broj ili izraz. Ako je operand izraz, ponovo se primjenjuje isto pravilo. Primjerice, izraz infiksne notacije 8*(7+3) ima prefiksnu notaciju ( * 8 (+ 3 7) ). Općenito, svaki se aritmetički izraz može napisati u prefiksnoj notaciji. Zbog ovog svojstva aritmetički izrazi se mogu pohraniti u binarnom stablu. Primjer je prikazan na slici 19.5.

infiksna notacija = (7 – 3) * (4 + 5) prefiksna notacija = ( * (- 7 3) (+ 4 5))

Slika 19.5 Binarno stablo aritmetičkog izraza Programski se stablo se može formirati pomoću funkcije make_tnode(). TREE make_tnode (elemT elem, TREE left, TREE right) { TREE t = malloc(sizeof(Tnode)); if(t) { t->left = left; t->right = right; t->elem = elem;} return t; }

Argumenti funkcije su vrijednost elementa čvora (elem) i pokazivači na lijevo i desno dijete (left, right). Funkcija vraća pokazivač na formirani čvor, ili NULL ako se ne može izvršiti alokacija memorije. Kada se formira vanjski čvor stabla (list), tada se vrijednost argumenata left i right postavlja na vrijednost NULL. U svim ostalim čvorovima bar jedan od ova dva pokazivača mora biti različit od NULL. Primjer: Stablo sa slike 19.4 može se formirati sljedećim naredbama: /* 1. formiraj listove */ TREE l1 = make_tnode('7', TREE l2 = make_tnode('3', TREE l3 = make_tnode('4', TREE l4 = make_tnode('5',

NULL,NULL); NULL,NULL); NULL,NULL); NULL,NULL);

/* 2.formiraj podstabla */ TREE t1 = make_tnode('-', l1, l2); TREE t2 = make_tnode('+', l3, l4); /* 3. formiraj korijena stabla */

284

TREE t = make_tnode('*', t1, t2);

Varijabla t je pokazivač na korijen stabla. Ostale varijable su pomoćne pokazivačke varijable za formiranje listova i podstabala. Isto se može ostvariti jednom naredbom: TREE t = make_tnode('*', make_tnode('-', make_tnode('7', make_tnode('3', make_tnode('+', make_tnode('4', make_tnode('5', );

NULL,NULL) ), NULL,NULL) )), NULL,NULL), NULL,NULL))

Uočite da se u ovom slučaju poziv funkcije make_tnode() obavlja prema prije opisanoj prefiksnoj notaciji stabla. U prethodnom primjeru korisnik obavlja sve operacije formiranja stabla. Kasnije će biti pokazano kako se može automatizirati postupak formiranja stabla. Prazno stablo se formira naredbom: TREE

t = NULL;

pa funkcija int tree_is_empty(TREE t) {return t == NULL;}

vraća nulu ako je stablo prazno, ili nenultu vrijednost ako je stablo nije prazno. Korisna je i funkcija tree_is_leaf() kojom se određuje da li čvor predstavlja list stabla. int tree_is_leaf(TREE t) {return !(t->left || t->right);} tree_is_leaf()vraća nulu ako čvor t nije list stabla, ili nenultu vrijednost ako je čvor t list

stabla. Rekurzivna definicija stabala čini da je većinu operacija nad stablom najlakše definirati rekurzivno. Primjer: Funkcija tree_size(t) vraća broj čvorova binarnog stabla. int tree_size(TREE t) { if(tree_is_empty(t)) return 0 ; else return 1 + tree_size(t->left) + tree_size(t->right); }

Primjer: Funkcija print_prefiks() ispisuje prefiksnu notaciju stabla koje sadrži aritmetički izraz. Realizirana je prema rekurzivnom algoritmu prefiksne notacije stabla. void print_prefiks (TREE t) { if(t == NULL) {printf(" () "); return;} if(tree_is_leaf(t)) {printf("%c ",t->elem); return;} printf("( %c ",t->elem); print_prefiks (t->left); print_prefiks (t->right); printf(" )");

285

}

Zadatak: Testirajte primjenu funkcije print_prefilks() u programu: /* Datoteka: print_nodes.c int main() { TREE t = make_tnode('*', make_tnode('-', make_tnode('7', NULL,NULL), make_tnode('3', NULL,NULL)), make_tnode('+', make_tnode('4', NULL,NULL), make_tnode('5', NULL,NULL)) ); print_prefiks (t); }

return 0;

19.2.2 Obilazak binarnog stabla Obilazak stabla je postupak kojim se na sve čvorove stabla primjenjuje neka operacija. Kroz čvor se smije "proći" više puta, ali se operacija nad čvorom izvršava samo jedan put. Općeniti obrazac za rekurzivni obilazak binarnog stabla, počevši od čvora N (koji nije prazan), temelji se na tri operacije: (L) rekurzivno obiđi lijevo stablo. Nakon ove operacije ponovo si u čvoru N. (D) rekurzivno obiđi desno stablo. Nakon ove operacije ponovo si u čvoru N. (P) procesiraj čvor N. Ove tri operacije se mogu izvršiti bilo kojim redoslijedom. Ako se vrši (L) prije (D) tada je obilazak s lijeva na desno (eng. left-right traversal), u suprotnom je obilazak s desna na lijevo (right-left traversal). Prema redoslijedu operacija (L) (D) (P), razlikuju se tri načina obilaska stabla: Obilazak stabla Redoslijed operacija Pre-order ili prefiks (P) (L) (D) Post-order ili postfiks (L) (D) (P) In-order ili infiks (L) (P) (D) Slika 19.6 prikazuje stazu obilaska stabla za sva tri načina.

286

Slika 19.6 Tri načina obilaska stabla (za obilazak s lijeva na desno). Usmjerene crtkane linije pokazuju redoslijed obilaska čvorova Obilazak stabla se programski može realizirati funkcijom tree_traverse(): enum torder {PREORDER,INORDER,POSTORDER}; void tree_traverse(TREE t, int order, void (*visit)(TREE)) { if (t != NULL){ if (order == PREORDER) visit(t); tree_traverse(t->left, order, visit); if (order == INORDER) visit (t); tree_traverse(t->right, order, visit); if (order == POSTORDER) visit(t); } }

Funkciji tree_traverse() prvi je argument pokazivač čvora stabla. Drugi argument je vrijednost iz skupa {PREORDER, INORDER, POSTORDER}, kojom se određuje način obilaska stabla. Treći argument je pokazivač na tzv. visit funkciju kojoj je argument pokazivač čvora na koji ta funkcija djeluje. Primjerice, "visit" funkcija print_char() ispisuje vrijednost elementa čvora. void print_char(TREE t) { printf("%c ",t->elem); }

Zadatak: Funkcije print_char() i tree_traverse() uvrstite u prethodni program "print_nodes.c", a u funkciji main(), umjesto naredbe print_prefiks(), napišite naredbe: printf("\nPREORDER: "); tree_traverse(t, PREORDER, print_el); printf("\nINORDER: "); tree_traverse(t, INORDER, print_el); printf("\nPOSTORDER: "); tree_traverse(t, POSTORDER, print_el);

Nakon izvršenja programa dobije se ispis: PREORDER: * - 7 3 + 4 5 INORDER: 7 - 3 * 4 + 5 POSTORDER: 7 3 - 4 5 + *

Ovaj primjer pokazuje da se pomoću binarnog stabla aritmetički izrazi lako pretvaraju u prefiksni, infiksni i postfiksni oblik.

19.2.3 Brisanje stabla Brisanje stabla je operacija kojom se dealociraju svi čvorovi stabla, koje time postaje prazno stablo. Ovu operaciju treba izvesti tako da se najprije dealociraju listovi stabla, a tek onda unutarnji čvorovi. Slika 19.4 pokazuje da se to može izvesti postorder obilaskom stabla. Operacija brisanja stabla je implementirana u funkciji tree_delete(). TREE tree_delete( TREE t ) { if( t != NULL ) { tree_delete( t->left ); tree_delete( t->right );

287

free( t ); } return NULL; }

Kod primjene ove funkcije treba uvijek voditi računa da se ona koristi u obliku t = tree_delete(t);

jer na taj način t postaje jednako NULL, što označava prazno stablo.

19.2.4 Vrednovanje aritmetičkog izraza Gotovo svi moderni kompilatori i interpreteri, u prvoj fazi kompiliranja, pretvaraju izvorni kôd u oblik koji je podesan za optimiranje kôda i generiranje strojnog kôda ili interpretiranje kôda. Praksa je pokazala da je najbolji način da se izvorni kod najprije pretvori u jedan oblik "sintaktičkog" stabla. Dio tog sintaktičkog stabla, kojim se bilježi aritmetičke izraze, sličan je ovdje opisanom binarnom stablu. U prethodnim primjerima, zbog jednostavnosti, element čvora je bio tipa char. Ako se pak želi formirati stablo koje će sadržavati realne brojeve, tada treba izmijeniti definiciju tipa elementa čvora. Kod aritmetičkih izraza potrebno je da čvor sadrži ili znak operatora ili realni broj. To se može ostvariti tipom koji je unija elemenata tipa char (ili int) i tipa double. Koristit će se definicija: typedef union elemT { int op; double num; } elemT; typedef struct tnode { elemT elem; struct tnode * left; struct tnode * right; } Tnode, *TREE;

Za formiranje čvora koristit će se funkcije make_opnode() i make_numnode(). TREE make_opnode (int op, TREE left, TREE right) { /* formira unutarnji čvor koji sadrži operator op*/ /* vraća pokazivač na taj čvor, ili NULL ako je greška*/ TREE t = malloc(sizeof(Tnode)); if(t) { t->left = left; t->right = right; t->elem.op = op;} return t; } TREE make_numnode (double num) { /* formira vanjski čvor koji sadrži realni broj - num*/ /* vraća pokazivač na taj čvor, ili NULL ako je greška */ TREE t = malloc(sizeof(Tnode)); if(t) { t->left = NULL; t->right = NULL; t->elem.num = num;} return t; }

Programski jezici Lisp i Sheme koriste prefiksnu notaciju za zapis svih svojih konstrukcija, a pohrana i izračunavanje vrijednosti izraza (tzv. evaluacija) vrši se obilaskom binarnog stabla. Koristi se sljedeći algoritam:

288

Algoritam: Vrednovanje aritmetičkog izraza koji je zapisan u binarnom stablu Temeljni slučaj: Ako čvor sadrži operand, vrati vrijednost operanda Pravilo rekurzije: Ako čvor sadrži operator, rekurzivno dobavi lijevi i desni operand, izvrši operaciju koju određuje operator i vrati rezultat Ovaj algoritam je implementiran u funkciji evaluate(). double evaluate(TREE t) { /* vraća vrijednost izraza koji je u stablu t*/ double x,y; int op; if(tree_is_leaf(t)) return t->elem.num;

/* ako je t list stabla /* vrati vrijednost operanda /* inače,

*/ */ */

op = t->elem.op; /* dobavi operator */ x = evaluate(t->left); /* dobavi lijevi operand */ y = evaluate(t->right); /* dobavi desni operand */ switch (op) { /* izračunaj vrijednost izraza*/ case '+': return x+y; case '-': return x-y; case '*': return x*y; case '/': return (y == 0)? 0 : x/y; default: printf("\nGreska"); return 0; } }

Primjer: U programu evaluate.c, pokazana je primjena funkcije evaluate() za proračun vrijednosti prefiksnog izraza (*(- 7.5 3)(+ 4 5.1)). Program koristi funkcije i definicije koje su definirane u datotekama "prefix_tree.h" i "prefix_tree.c". /***************************************************************/ /* Datoteka: prefix_tree.h */ /***************************************************************/ #ifndef _PREFIX_TREE_H_ #define _PREFIX_TREE_H_ typedef union elementip { int op; double num; }elemT; typedef struct tnode { elemT elem; struct tnode * left; struct tnode * right; } Tnode, *TREE; int tree_is_leaf(TREE t); void print_prefiks (TREE t); TREE make_opnode (int op, TREE left, TREE right); TREE make_numnode (double num); double evaluate(TREE t); #endif /***************************************************************/

289

/* Datoteka: prefix_tree.c */ /***************************************************************/ #include #include #include "prefix_tree.h" int tree_is_leaf(TREE t) {return !(t->left || t->right);} void print_prefiks (TREE t) { if(t == NULL) {printf(" () "); return;} if(tree_is_leaf(t)) {printf("%f ",t->elem.num); printf("( %c ",t->elem.op); print_prefiks (t->left); print_prefiks (t->right); printf(" )"); }

return;}

TREE make_opnode (int op, TREE left, TREE right) { TREE t = malloc(sizeof(Tnode)); if(t) { t->left = left; t->right = right; t->elem.op = op;} return t; } TREE make_numnode (double num) { TREE t = malloc(sizeof(Tnode)); if(t) { t->left = NULL; t->right = NULL; t->elem.num = num;} return t; } TREE tree_delete( TREE t ) { if( t != NULL ) { tree_delete( t->left ); tree_delete( t->right ); free( t ); } return NULL; } double evaluate(TREE t) { double x,y; int op; if(tree_is_leaf(t)) /* operand*/ return t->elem.num; /*else analyse operator*/ op = t->elem.op; x = evaluate(t->left); y = evaluate(t->right); switch (op) { case '+': return x+y; case '-': return x-y;

290

case '*': return x*y; case '/': return (y==0)? 0 : x/y; default: printf("\nGreska"); return 0; }

}

/************************************************************/ /* Datoteka: evaluate.c */ /************************************************************/ #include #include #include "prefix_tree.c" int main() { TREE t = make_opnode('*', make_opnode('-', make_numnode(7.5), make_numnode(3)), make_opnode('+', make_numnode(4), make_numnode(5.1)) ); printf("\nVrijednost prefiksnog izraza:\n"); print_prefiks(t); printf("\njednaka je: %f", evaluate(t)); }

return 0;

Program se kompilira komandom: c:> cl evaluate.c prefix_tree.c

Nakon izvršenja programa "evaluate.exe" dobije se ispis: Vrijednost prefiksnog izraza: ( * ( - 7.500000 3.000000 )( + 4.000000 5.100000 jednaka je: 40.950000

) )

19.3 Interpreter prefiksnih izraza Sada će biti pokazano kako se realizira interpreter prefiksnih izraza koje unosi korisnik pomoću tipkovnice ili iz datoteke.

19.3.1 Dobavi-vrednuj-ispiši Rad interpretera se obično opisuje tzv. dobavi-vrednuj-ispiši petljom (eng. read-eval-print loop), koja je definirana sljedećim algoritmom: Algoritam : Interpreter s dobavi-vrednuj-ispiši petljom Ponavljaj: 1. Dobavi naredbu – dobavi izvorni kod naredbe i interno ga spremi u prikladnu podatkovnu strukturu. Ako je primljen zahtjev za kraj rada, završi petlju. 2. Vrednuj naredbu – izvrši operacije na internoj podatkovnoj strukturi, koje rezultiraju nekom vrijednošću. Primjerice, obilaskom binarnog stabla izračunaj vrijednost aritmetičkog izraza. 3. Ispiši rezultat.

291

U prethodnoj sekciji je pokazano kako se vrednuje i ispisuje rezultat aritmetičkog izraza koji je zapisan u binarnom stablu. Sada će biti pokazano kako se realizira "dobavi" operacija, tj. kako se od korisnika dobavlja naredba te kako se vrši leksička i sintaktička analiza izvornog kôda naredbe, koja rezultira binarnom stablom aritmetičkog izraza. U leksičkoj i sintaktičkoj analizi koristit će se specifikacije u BNF notaciji, koja je opisana u poglavlju 6. U BNF notaciji gramatička se pravila zapisuju u obliku produkcije: A:α gdje je A neterminalni simbol, a α predstavlja niz terminalnih ili neterminalnih simbola X1 X2...Xn . Ako je simbol Xi neterminalni simbol, zapisuje se kurzivom. Na taj se način razlikuje od terminalnog simbola. Ako je simbol Xi opcioni, što znači da ne mora biti u produkciji, zapisuje sa sufiksom opt, tj. Xopt. Ako je neterminalni simbol A definiran s više alternativnih produkcija A : α1 , A : α2 ... A : αn., koristi se notacija: A : α1 | α2 | ...| αn U gramatičkim analizama opcioni simbol se tretira kao neterminalni simbol koji je definiran produkcijom: Xopt : X | ε gdje ε označava "praznu" produkciju. Kaže se da je Xopt definiran ili kao X ili kao prazna produkcija (ε-produkcija).

19.3.1 Leksička analiza prefiksnih aritmetičkih izraza Razmotrimo jedan prefiksni izraz, s realnim brojevima, koji je napisan u više linija teksta: (* (- 3.7 35) (/ 8 (+ 9.1 -5) ) ) $

# komentar # ....... # kraj unosa

Ovaj izraz se može pročitati na sljedeći način: "pomnoži razliku 3.7 i 35 i kvocijent od 8 i zbroja 9.1 i -5". Nakon ovog izraza slijedi znak $. On označava kraj unosa. U prikazanom prefiksnom aritmetičkom izrazu su zastupljene sljedeće leksičke i gramatičke kategorije: leksem (iz ulaznog toka) (, ), 3.7

35

+, *, $,

-, /, EOF

-5

razmak, tab, nova linija # .......

značaj zagrade za grupiranje izraza realni broj

token - gramatički terminalni simbol '( ' ')'

vrijednost tokena ( ili atribut tokena) '(' ')'

NUM

numerička vrijednost realnog broja '+', '-', '*', '/' '$' ili EOF

aritmetički operatori

OPERATOR

oznak kraja unosa (EOF kod datoteka) separatori (bijeli znakovi: '\n', '\t', '\n' ) komentar (počinje

QUIT

292

znakom # )

Leksička analiza je proces kojim se sekvencijalno u nizu ulaznih znakova prepoznaje neki leksem i gramatički simbol koji on predstavlja, tj. token. Primjerice, leksem "37.5" predstavlja token NUM. Ako je potrebno, uz token se bilježi i njegova vrijednost, koja predstavlja atribut tokena. Leksički analizator se može jednostavno implementirati pomoću funkcije getToken() i sljedećih deklaracija: /**************************************************************/ /* datoteka: prefix_lex.h */ /**************************************************************/ #ifndef _PREFIX_LEX_H_ #define _PREFIX_LEX_H_ /* tokeni (terminalni simboli) */ #define NUM 255 #define OPERATOR 256 #define QUIT 257 /* globalne varijable*/ extern FILE *input = stdin; /* ulazni tok */ extern elemT tokenval; /* vrijednost tokena */ extern char lexeme[]; /* leksem tokena */ int getToken(); /* Pri svakom pozivu funkcija vraća sljedeći token iz ulaznog toka, ili 0, ako je registriran nepredviđeni znak. Vrijednost tokena i leksem tokena se zapisuju u globalne varijable tokenval i lexeme. */ #endif

Implementacija funkcije getToken() je u datoteci "prefix_lex.c". /**************************************************************/ /* datoteka prefix_lex.c */ /**************************************************************/ #include #include #include #include "lexan.h" FILE *input = stdin; elemT tokenval; char lexeme[64];

/* ulazni tok */ /* vrijednost tokena */ /* leksem tokena */

int getToken() { int ch; while(1) { ch = getc(input);

293

/* preskoči bijele znakove */ if(ch == ' ' || ch =='\t' || ch =='\n' || ch =='\r') continue; /* zapamti prvi znak */ lexeme[0]=(char)ch; lexeme[1]='\0';

}

}

if (isdigit(ch)) { /* dobavi broj */ int i = 1; /* prva znamenka je u lexeme[0]*/ ch = getc(input); while(isdigit(ch) && i cl prefix_int.c prefix_tree.c prefix_lex.c

Program "prefix_int.exe" se može koristiti na dva načina. Prvi je način da se program pozove bez argumenata komandne linije. Tada program čeka da korisnik unese aritmetički izraz ili znak za kraj unosa ($) s tipkovnice. Drugi je način da se program pozove s argumentom koji je ime datoteke u kojoj je zapisano više prefiksnih izraza, primjerice, komandom c:> prefix_int pr.txt

analizira se tekstualna datoteka "pr.txt". Ako ona ima sljedeći sadržaj: (* 8 9) (* 8 (+ 8 9)) (+ 8 (+ 8 9))

program ispisuje rezultat u obliku: ( * 8.000000 9.000000 ) =72.000000 ( * 8.000000 ( + 8.000000 9.000000 =136.000000 ( + 8.000000 ( + 8.000000 9.000000 =25.000000

) ) ) )

Primijetite da u datoteci "pr.txt" nije korišten znak $, koji pri unosu s tipkovnice predstavlja naredbu za kraj unosa, jer program prihvaća i znak za kraj datoteke (EOF) kao naredbu za kraja unosa. Zadatak: Napišite interpreter infiksnih izraza, za kojeg vrijedi sljedeća gramatika: Naredba : Izraz ENTER ; Izraz : Clan '+' Izraz | Clan '-' Izraz | Clan Clan : Faktor '*' Clan | Faktor '/' Clan | Faktor Faktor : NUM | - NUM | ( Izraz ) Tokeni su: ENTER - znak nove linije NUM - realni broj +,-,*,/ - operatori Interpreter očekuje da korisnik otkuca aritmetički izraz u infiksnom obliku, primjerice 67.8*(7-9) i da pritisne , tj znak nove linije. Nakon toga interpreter ispisuje rezultat. U analizi ovog zadatka prvo treba primijetiti da prethodna gramatika nije LL(1) jer u drugom i trećem pravilu postoje zajednički lijevi prefiksi. Zbog toga, treba koristiti modificiranu gramatiku: Naredba : Izraz ENTER Izraz : Clan IzrazOptopt IzrazOpt : '+' Izraz | '-' Izraz Clan : Faktor ClanOptopt ClanOpt: '*' Clan

304

| '/' Clan Faktor : NUM | - NUM | ( Izraz ) Ovo je LL(1) gramatika i za nju se može napisati rekurzivni silazni parser. Primjerice, funkcija parsera za pravilo Clan ima oblik: TREE Clan () { TREE t2, t1; t1 = Factor(); if(next_token == '*' || next_token = '/') int tok = next_token; t2 = ClanOpt(); return make_tnode(tok, t1, t2); } else return t1; }

{

TREE ClanOpt() { if(is_next_token('*')) { match('*'); return Clan (); } else if(is_next_token('/')) { match('/'); return Clan (); } else syn_error("greška") }

Napišite ostale funkcije parsera i izvršite potrebne izmjene u leksičkom analizatoru (funkcija getToken() treba vraćati poseban token za sve operatore i za znak nove linije).

19.4 Stabla s proizvoljnim brojem grana Ako čvorovi stabla imaju više djece, kao u M-stupanjskom stablu tada se oni mogu opisati samoreferentnom strukturom Node koja sadrži niz od M pokazivača na djecu čvora. #define NUM_CHILD M typedef int elemT: typedef struct _node { void * data; struct _node * child[NUM_CHILD]; } Node;

Čvor se formira pomoću funkcije makeNode(): Node * makeNode( elemT elem, Node *N0, Node *N1,..., Node *Nm) { Node *n=(Node *) malloc(sizeof(Node)); if(n) { n->elem = elem; n->child[0] = N0;

305

n->child[1] = N1; ..... n->child[m] = Nm; } return n; }

Ako podstabla imaju različite stupnjeva, zovemo ih višesmjerna stabla (eng. multiway tree). U tom slučaju može se koristiti prethodna struktura tako da se svi child[i] pokazivači postave jednakim nuli za i-smjerni čvor. Primjerice, dvosmjerni čvor bi formirali naredbom: makeNode( elem, N0, N1, NULL,..,NULL);

Loša strana ovakvog pristupa je da se troši memorija za pokazivače nepostojećih čvorova. Taj se problem može riješiti na dva načina. Prvi je da se i niz child alocira dinamički te da se u strukturi Node bilježi broj alociranih čorova. Tada struktura Node može biti ovakva: typedef struct _node { elemT elem; int num_child; struct _node **child; } Node;

/*broj alociranih čvorova*/ /*pokazivač na niz pokazivača*/

Drugi način za stvaranje čvorova višesmjernog stabla je da se koriste dva pokazivača. Prvi, nazvat ćemo ga first_children, pokazuje prvo dijete čvora, a drugi, nazvat ćemo ga next_sibling pokazuje na listu braće prvog djeteta. typedef struct _node { elemT elem; struct _node *first_child; struct _node *next_sibling; } Node;

Dakle, next_sibling je pokazivač glave jednostruko vezane liste, koja sadrži djecu čvora osim prvog djeteta. Kraj liste se uvijek označava NULL pokazivačem. Za stvaranje čvorova i uspostavljanje veza među njima mogu poslužiti sljedeće funkcije: Node *makeLeaf(elemT elem) { /*stvaranje lista*/ Node *n= (Node*)malloc(sizeof(Node)); if(n) { n->first_child = n->next_sibling = 0; n->elem = elem; } return n; } Node * make1ChildNode(elemT elem, Node *N1) { /*čvor s jednim djetetom*/ Node *n=makeLeaf(elem); if(n) n->first_child = N1; return n; } Node * make2ChildNode(elemT elem, Node *N1, Node *N2) { /*čvor s dva djeteta*/ Node *n=makeLeaf(elem); if(n) {

306

n->first_child = N1; N1->next_sibling = N2; N2->next_sibling = 0; } return n; } Node * make3ChildNode(elemT elem, Node *N1, Node *N2, Node *N3) { /*čvor s tri djeteta*/ Node *n=makeLeaf(elem); if(n) { n->first_child = N1; N1->next_sibling = N2; N2->next_sibling = N3; N3->next_sibling = 0; } return n; } itd.

Veze čvora zapisane u Node *child[3];

Veze čvora zapisane u Node *first-child Node *next-sibling

1 /|\ / | \ / | \ 2 3 4 / \ | 5 6 7 / \ 8 9

1 / / / 2---3---4 / / 5---6 7 / 8---9

a)

b)

Ekvivalentno binarno stablo

c)

1 / 2 / \ 5 3 \ \ 6 4 / 7 / 8 \ 9

Slika 19.9 Tri načina zapisa stabla kojem čvorovi imaju promjenjljivi broj djece. a) pokazivači čvorova su u nizu child[], b) pokazivači čvorova su first_child i elementi liste next_sibling, c) ekvivalentno binarno stablo koje se dobije iz drugog oblika rotiranjem liste čvorova braće za 45o u smjeru okretanja kazaljke sata. Važno je primijetiti da zapis stabla pomoću first_child i next_sibling čvorova ima ekvivalentan zapis pomoću binarnog stabla. To je pokazano na slici 19.9. Stablo je oformljeno naredbama: Node Node Node Node

*N1 *N2 *N3 *N0

= = = =

make2ChildNode make1ChildNode make2ChildNode make3ChildNode

(7, (4, (2, (1,

makeLeaf(8), makeLeaf(9)); N1); makeLeaf(5), makeLeaf(6)); N3, makeLeaf(3), N2);

Ekvivalentno binarno stablo se dobije iz višesmjernog stabla (first_child-next_sibling tipa) "rotiranjem" veza next_sibling liste za 45o u smjeru okretanja kazaljke sata.

307

Definicija: Višesmjerno stablo T se može predstaviti odgovarajućim binarnim stablom B. Ako su {n1,..., nk} čvorovi T, a {n'1,..., n'k} čvorovi B, tada čvor nk odgovara čvoru n'k. Oni imaju isti sadržaj. Ako je nk korijen od T, tada je n'k korijen od B. Veze među čvorovima su: • Ako je nl prvo dijete od nk, tada je n'l lijevo dijete od n'k. (Ako nk nema djece, tada n'k nema lijevo dijete.) • Ako je ns sljedeći neposredno desni brat od nk, tada je n's desno dijete od n'k. Bez obzira na ovu ekvivalentnost, sva tri opisana oblika implementacije višesmjernog stabla imaju različiti apstraktni značaj pa se i koriste u različitim primjenama. Zapis s nizom pokazivača djece često se koristi kod izrade sintaktičkih stabala, a zapis s first_child i next_sibling pokazivačima je uobičajen kod interpretetera jezika LISP i SCHEME. Algoritmi za obilazak višesmjernog stabla slični su algoritmima binarnog stabla, i lako se definiraju rekurzivno: Algoritam: visina stabla kojem je korijen n tree_height(n) = Ako je n != 0 tada vrati 0, inače vrati 1 + MAX(tree_height (child1), … tree_height (childn) ) Algoritam: veličina stabla kojem je korijen n tree_size(n) = Ako je čvor n != 0 tada vrati 0, inače vrati 1 + tree_size(child1) + … + tree_size(childn) Algoritam: obilazak stabla - postorder postorder (n) = Ako je čvor n != 0 tada za svako dijete od n pozovi postorder(dijete). Posjeti čvor. Algoritam: obilazak stabla - preorder preorder (n) = Ako je čvor n != 0 tada Posjeti čvor n, zatim za svako dijete od n pozovi postorder(dijete). Primjer: funkcija postorder() vrši postorder obilazak stabla i na čvorove primjenjuje funkciju (*visit)(). void postorder(Node * n, void (*visit)(Node *)) { Node * c; if (n == NULL) return; c = n->first_child; while (c != NULL) { postorder(c, visit); c = c->next_sibling; } visit(n); }

Stablo, kojem je pokazivač korijena t, možemo izbrisati naredbom postorder(t, free);. Primjer: funkcija tree_size() vrši postorder obilazak stabla i vraća broj čvorova stabla. int tree_size(Node * n) { /* postorder obilazak */

308

Node * c; int m; if (n == NULL) return 0; m = 1; c = n->first_child; while (c != NULL) { m += tree_size(c); c = c->next_sibling; } return m; }

Primjer: funkcija print_prefiks()vrši preorder obilazak stabla i ispisuje prefiksnu notaciju stabla. void print_prefiks (Node * n) { Node *x; if(n == NULL) { printf(" () "); return;} if(isLeaf(n)) { printf("%d ",n->data); return; } printf("( %d ",n->data); x = n->first_child; while(x != 0) { print_prefiks (x); x = x->next_sibling; } printf(" )"); }

Ako se ova funkcija primijeni na (print_prefiks(N0);), dobije se ispis: ( 1 ( 2 5 6

) 3 ( 4 ( 7 8 9

stablo

koje

je

prikazano

na

slici

19.9

) ) )

Pitanje: Zašto kod višesmjernih stabala nema smisla "inorder" obilazak stabla? Obilazak stabla može biti i proizvoljan. Tada obično korisnik ispituje sadržaj čvora i na temelju toga određuje u kojem smjeru će dalje obilaziti stablo.

19.5 Prioritetni redovi i hrpe U prethodnim odjeljcima stabla su programski implementirana pomoću pokazivača i samoreferentnih struktura. Sada će biti pokazan primjer u kojem se stabla mogu efikasno implementirati pomoću nizova. Analizirat će se posebni tipovi stabala, odnosno nizova, koji se nazivaju hrpa i pomoću kojih će biti realizirani tzv. prioritetni redovi (eng. priority queue). Najjednostavnije kazano, prioritetni red je skup podataka u kojem se svakom elementu skupa pridružuje oznaka prioriteta . Prioritetni redovi se često koriste. Evo dva primjera: Primjer 1: Pacijenti ulaze u čekaonicu, te iz nje odlaze liječniku na pregled. Prvi na redu za liječnika nije onaj koji je prvi ušao, već onaj čije je stanje najteže. Primjer 2: U računalu više procesa čeka u redu za izvršenje. Redoslijed kojim se izvršavaju procesi određen je prioritetom procesa. Procesor uzima iz reda program s najvećim prioritetom, te ga izvodi. U odnosu na obične redove, koji sadrže neki element x, prioritetni redovi sadrže elemente koji imaju dva atributa: ključ(x) i vrijednost(x). Ključ simbola određuje njegov prioritet. Najčešće se uzima da najveći prioritet ima simbol s najmanjim ključem, ali ponekad se uzima i obrnuto, da najveći prioritet ima simbol s najvećim ključem.

309

Kod prioritetnog reda važne su samo dvije operacije : ubaci element i izbaci element s najmanjim ključom (ili alternativno, s najvećim ključem). Ovdje ćemo pokazati izvedbu apstraktnog dinamičkog tipa PQUEUE koji je definiran na sljedeći način: ADT PQUEQUE insert(Q, x)

- ubacuje element x u red Q.

delete_min(Q)

- vraća najmanji element reda i izbacuje ga iz reda Q. Operacija nije definirana ako Q ne sadrži ni jedan element, tj ako je size(Q) = 0.

size(Q)

- vraća broj elemenata u prioritetnom redu Q.

Ovaj ADT se može jednostavno realizirati pomoću linearne sortirane liste. U tom slučaju operacija delete_min(), u kojoj se pronalazi i izbacuje prvi element u listi, ima vremensku složenost O(1). Operacija insert() mora ubaciti novi element na “sortirano mjesto”. To znači da u prosjeku treba obići bar pola sortirane liste, pa je vremenska složenost operacije insert() O(n). Efikasnija implementacija se može napraviti pomoću hrpe (eng. heap). Hrpa je naziv za potpuno binarno stablo koje je zapisano pomoću niza elemenata, prema sljedećim pravilima: 1. Pravilo rasporeda: Potpuno stablo s N čvorova, koji sadrže elemente tipa , može se bilježiti u nizu elemenata, na indeksima i = 1,2,..N-1, prema sljdedećem pravilu: Ako element indeksa i predstavlja čvor stabla, tada - element indeksa 2*i+1 predstavlja lijevo dijete čvora, - element indeksa 2*i+2 predstavlja desno dijete čvora, - element indeksa (i-1)/2 predstavlja roditelja čvora, 2. Pravilo hrpe: Ključ roditalja nekog čvora uvijek je manji od ključa tog čvora, pa je najmanji ključ u korijenu stabla. 13

21

16

24

65

31

26

32

19

69

13

21

16

24

31

19

69

65

26

32

Slika 19.10 Potpuno binarno stablo i ekvivalentni niz hrpe. Na slici 19.10 prikazano je potpuno binarno stablo i ekvivalentni niz hrpe. Uočimo: - indeks namanjeg elementa (koji je u korijenu stabla) jednak je i = 0. - indeks kranjeg elementa je N-1. To je krajnji desni list stabla. Kapacitet niza treba biti veći od broja elemenata niza. Zbog toga je povoljno da se niz realizira kao dinamički niz koji se može povećavati (realocirati) ako, pri operaciji umetanja, broj elemenata niza dosegne kapacitet niza.

310

Postupno će biti opisana implementacija ADT PQUEUE na temelju specifikacije koja je opisana u datoteci "pq.h". /* Datoteka: pqueue.h */ /* Specifikacija ADT PQUEUE */ typedef struct pq_elem { int key; void *val; } elemT;

/* element prioritetnog reda /* ključ za postavljanje prioriteta /* vrijednost elementa (bilo što)

*/ */ */

PQUEUE pqueue_new(int capacity) /* Konstruktor ADT-a. * Argument: capacity - određuje početni kapacitet hrepe * Vraća: pokazivač ADT-a */ void pqueue_free(PQUEUE pQ) /*Destruktor objekta pQ*/ unsigned pqueue_size(PQUEUE pQ); /*Vraća broj elemenata u redu pQ*/ void pqueue_insert(PQUEUE pQ, elemT x); /* Umeće element na kojeg pokazuje px u red * Argumenti: pQ - red * x - element kojeg se umeće */ elemT pqueue_delete_min(PQUEUE pQ); /* Briše najmanji element iz reda PQ * Vraća: element koji je izbrisan */

Implementacija ADT PQUEUE je opisana u datoteci "pq.c". /* Datoteka: pq.c */ #include #include #include #include #include "pq.h" typedef struct pq { int capacity; int N; elemT *elements; } PQueue, *PQUEUE;

/* kapacitet PQUEUE */ /* broj umetnutih elemenata */ /* niz elemenata reda */

PQUEUE pqueue_new(int capacity) { PQUEUE pq = (PQUEUE)malloc(sizeof(PQueue)); if(pq) { pq->N=0; pq->capacity=capacity; pq->elements = malloc(sizeof(elemT)*capacity); if(!pq->elements) exit(1); } return pq;

311

} void pqueue_free(PQUEUE pQ) { /*Destruktor ADT-a*/ assert(pQ); free(pQ->elements); free(pQ); }

19.5.1 Umetanje elementa u prioritetni red Ako želimo dodati element u prioritetni red možemo ga dodati na kraj niza. To je ekvivalentno dodavanju krajnjeg desnog lista u stablu (prikazan crtkano na slici 19. 10). Ta operacija ima za posljedicu da se N uveća za jedan, ali i da možda stablo ne zadovoljava pravilo hrpe. Da bi se zadovoljilo pravilo hrpe koristi se postupak, ilustriran na slici 19.11, opisan u sljedećem algoritmu: Algoritam: Umetanje elementa x u hrpu 1. Element iza krajnjeg elementa niza se tretira kao prazan (to je sljedeći krajnji desni list stabla). Podrazumijeva se da je kapacitet niza veći od broja elemenata u nizu. 2. Ako je ključ roditelja praznog čvora manji od ključa elementa x, tada se element x upisuje u prazni čvor. Time postupak završen. 3. Ako je ključ roditelja praznog čvora veći od ključa elementa x, tada se element roditelja kopira u prazni čvor, a čvor u kojem je bio roditelj se uzima kao prazni čvor. Postupak se ponavlja korakom 2. 13

17

21

16

17 24

65

31

26

32

19

69

13

21

16

24

31

19

69

65

26

32

Slika 19.11 Stablo sa slike 19.10 prilikom dodavanja ključa 17. Strelice pokazuju koje elemente treba pomaknuti da bi se otvorilo mjesto za novi element. Prethodni algoritam je primijenjen u funkciji pqueue_insert(). U toj funkciji se najprije provjerava da li je popunjen kapacitet reda. Ako je popunjen, dvostruko se povećava kapacitet reda. Nakon toga se vrši umetanje elementa prema prethodnome algoritmu. void pqueue_insert(PQUEUE pQ, elemT x) { int i; assert(pQ); /* provjeri kapacitet reda i povećaj ga ako je N>=kapacitet*/ if ( pQ->N >= pQ->capacity-1 ) { pQ->capacity *= 2; /*udvostruèi kapacitet hrpe*/ pQ->elements = (elemT *) realloc(pQ->elements, sizeof(elemT)*pQ->capacity); if(!pQ->elements) exit(1); }

312

/* umetni element x i povećaj N */ i = pQ->N++; while (i) { int parent = (i-1)/2; if (x.key > pQ->elements[parent].key) break; pQ->elements[i].key = pQ->elements[parent].key; i = parent; } pQ->elements[i] = x; }

19.5.2 Brisanje elementa iz prioritetnog reda Brisanje elementa iz prioritetnog reda se vrši samo na jedan način: briše se element s minimalnim ključem pomoću funkcije delete_min(). Minimalni element se nalazi u korijenu stabla, odnosno u hrpi na indeksu 0. Ako ga izbrišemo tada je korijen prazan i u njega treba zapisati neki drugi element. Najzgodnije je u njega zapisati krajnji element niza i smanjiti veličinu hrpe. Problem je što tada možda nije zadovoljeno pravilo hrpe. Dovođenje niza u stanje koje zadovoljava pravilo hrpe vrši se funkcijom heapify(), prema sljedećem algoritmu: Algoritam: uređenje hrpe kada vrh hrpe nije u skladu s pravilom hrpe 1. Započni s indeksom koji predstavlja vrh hrpe. Spremi taj element u varijablu x. Nadalje se uzima da je vrh hrpe "prazan". 2. Analiziraj djecu praznog čvora i odredi koje dijete je manje. 3. Ako je ključ od x manji od ključa manjeg djeteta spremi x u prazan čvor i završi, inače upiši element manjeg djeteta u prazan čvor i postavi da čvor manjeg djeteta bude prazan čvor. Ponovi korak 2. void heapify(elemT *elements,int vrh, int N) { int min_child, i = vrh; /*zapamti element kojem tražimo mjesto u hrpi */ elemT x = elements[i]; /* (i) označava "prazni" element*/ while (i < N/2) { int left = min_child = 2*i+1; int right = left + 1; /* Prvo odredi indeks manjeg djeteta - min_child */ if ( (left < N-1) && (elements[left].key > elements[right].key) ) min_child = right; /* Ako je min_child manji od x, upiši ga na prazno mjesto (i), inače break */ if ( x.key < elements[min_child].key) break; elements[i] = elements[min_child]; i = min_child; } /* konačno stavi x na prazno mjesto*/ elements[i] = x; }

Sada je implementacija funkcije pqueue_delete_min() jednostavna. u njoj je najprije zapamćen minimalni element, koji je na indeksu 0, u varijabli minimum. Zatim je krajnji element upisan na početak niza, a veličina hrpe je smanjena. Slijedi poziv funkcije heapify(), kojom se uređuje hrpa, ako je poremećeno pravilo hrpe. Postupak brisanja elementa je ilustriran na slici 19.12.

313

elemT pqueue_delete_min(PQUEUE pQ) { elemT minimum; assert(pQ); assert( pQ->N > 0 ); /* minimum je u korijenu (indeks 0) */ minimum = pQ->elements[0]; /* zadnji element prebaci u korijen i smanji veličinu hrpe*/ pQ->elements[0] = pQ->elements[--pQ->N]; /* preuredi hrpu od vrha prema dnu (vrh je na indeksu 0) */ heapify(pQ->elements, 0, pQ->N); return minimum; } 13 minumum

16

16

X > 16 17

24

65

21

26

32

19

31

X = 31

X > 19

17

16

24

69

65

21

26

32

19

x

17

69

24

65

21

26

19

31

69

32

Slika 19.12 Primjer kako se izvršava operacija delete_min(). Iz reda se odstranjuje ključ 13 koji je na vrhu hrpe. Strelice pokazuju koje elemente treba pomaknuti da bi se otvorilo mjesto za element s kraja hrpe ( x=31). Testiranje se provodi programom "pq-test.c". U programu se najprije 10 slučajno generiranih brojeva upisuje u prioritetni red pQ, a zatim se briše element po element i ispisuje vrijednost ključa. /* Datoteka: pq-test.c*/ /* 1. Generira se 10 slučajnih brojeva i sprema u pqueue * 2. Briše iz pqueue i ispisuje element po element */ #include #include #include #include #include "pq.h" int main() { int i; elemT elem; PQUEUE pQ =pqueue_new(20); printf("Slucajno generirani brojevi:\n"); for(i=0; i A[i] > A[i+1]*/ int i; /*1. stvari hrpu od postojeceg niza */

315

for(i = N/2-1; i >= 0; i--) heapify( A, i, N );

}

/*2. zamijeni minimum s posljednjim elementom i uredi hrpu */ for(i = N-1; i>0; i-- ) { elemT tmp = A[i]; A[i]=A[0]; A[0]= tmp; heapify( A, 0, i); }

Ova verzija heapsort() funkcije sortira nizove od veće prema manjoj vrijednosti. Ako bi trebali suprotan raspored elemenata tada treba izmijeniti funkciju heapify(), tako da ona stvara hrpu s najvećim elementom na vrhu hrpe. Izmjena je jednostavna - treba u naredbama usporedbe zamijeniti operatore < i >. Vremenska složenost heapsort metode je O(nlog2n) i to u najgorem slučaju, jer se u njoj funkcija heapify() poziva maksimalno 3n/2 puta. Brzina izvršenja u prosječnom slučaju je nešto sporija nego kod primjene funkcije quicksort(), ali postoje slučajevi kada heapsort() daje najveću brzinu izvršenja. Zadatak: Napišite verziju ADT-a za prioritetni red PQ kojem je na vrhu hrpe element s maksimalnim ključem. ADT PQ insert(Q, x)

- ubacuje element x u red Q.

delete_max(Q,)

- vraća najveći element reda i izbacuje ga iz reda Q Operacija nije definirana ako Q ne sadrži ni jedan element, tj ako je size(Q) = 0.

size(Q)

- vraća broj elemenata u prioritetnom redu Q.

19.6 Zaključak Stabla omogućuju jednostavnu apstrakciju različitih podatkovnih struktura, od slike porodičnog stabla do jezičkih sintaktičkih i semantičkih veza. U sljedećem poglavlju bit će pokazano da stabla omogućuju stvaranje općih podatkovnih struktura tipa tablice, rječnika i skupova. Stabla se najčešće koriste u obliku binarnog stabla. Pokazano je da se višesmjerna stabla mogu prikazati ekvivalentnim binarnim stablima. Pokazana su dva načina programske implementacije stabla. U prvoj se stablo realizira pomoću samoreferentnih struktura koje sadrže pokazivače, a u drugoj se stablo realizira pomoću prostih nizova. Korištenje stabala je uvijek povezano uz neku primjenu za koju se definira potreban skup operacija. Pokazana je potpuna realizacija interpretera prefiksnih aritmetičkih izraza, a pokazano je i kako se može napraviti interpreter prefiksnih izraza. Izneseni su temelji sintaktičke analize jednostavnih jezičkih konstrukcija koje zadovoljavaju LL(1) tip gramatike. Pokazano je kako se realizira jednostavni leksički analizator i kako se iz BNF notacije gramatike realizira rekurzivno silazni parser. Opisana je izvedba apstraktnog tipa podataka PQUEUE kojom se stvaraju prioritetni redovi. Korištena je struktura tipa hrpe i pomoću nje je realizirana metoda sortiranja koja se naziva heapsort.

316

20 Strukture za brzo traženje podataka

Naglasci: • Tablice simbola i rječnici • Hash tablica • Otvorena hash tablica • Zatvorena hash tablica • BST - binarno stablo traženja • RBT - crveno-crno stablo

20.1 Tablice simbola i rječnici Tablice simbola i rječnici imaju sličnu karakteristiku, a to je da svaki simbol ima neku ključnu oznaku (ključ) i vrijednost (ili definiciju) koja opisuje značaj simbola. Može se i reći da je tablica simbola skup parova . Ključ je najčešće tipa string, ali može biti i numerička vrijednost. Tri su temeljne operacije koje definiraju ADT tablice (ili rječnika): ADT TABLE insert (T, k, v) find (T, k) delete(T, k)

- umeće simbol, ključa k i vrijednosti v u tablicu T, ako već nije u T. - vraća vrijednost simbola ako u T postoji ključ k, - iz tablice T briše simbol kojem je ključ k, ako postoji .

Brzina ovih operacija ovisna je o tome koja struktura podataka je upotrijebljena za tablicu simbola. Za implementaciju tablice moguće je koristiti nizove i liste. To ćemo pokazati primjerom. Primjer: Implementacija tablice pomoću niza. Simbol opisuje struktura: typedef struct symbol { char *key; /* ključ simbola */ void *val; /* pokazivač vrijednost simbola }Symbol;

*/

Tablica se sastoji od niza simbola : typedef struct _table { Symbol *array; /* alocirani niz simbola */ int N; /* broj simbola u nizu */ int M; /* kapacitet alociranog niza */ } *TABLE

Primjer funkcije za traženje:

317

Symbol *find(TABLE T, char *key) { /* vraća pokazivač simbola ili NULL */ int i; for (i = 0; i < T->N; i++) if(!strcmp(T->array[i].key, key); return &T->array[i]; return NULL; } /*sami napišite funkcije insert() i delete()*/

Zbog jednostavnosti uzeto je da je ključ tipa string i da je vrijednost simbola označana pokazivačem tipa void * (dakle, pokazivačem na bilo koji tip podatka). Primjer: Implementacija tablice pomoću vezane liste se može temeljiti na sljedećim strukturama: typedef struct symbol { struct symbol *next char * key; void * val; }Symbol;

/* veza u listi */ /* ključ simbola */ /* pokazivač vrijednost simbola

*/

typedef struct _table { Symbol *list; /* tablica je lista*/ } *TABLE Symbol *find(TABLE T, char * key) { Symbol *L = T->list; while( L != NULL) { if (!strcmp(L->key, key)) return L; L = L->next; } return NULL; }

U oba slučaja kompleksnost algoritma traženja je O(n) pa ovakova rješenja mogu zadovoljiti samo za implementaciju manjih tablica. Kada je potrebno imati veće tablice cilj je smanjiti kompleksnost na O(log n) ili čak na O(1). Ta poboljšanja se mogu ostvariti pomoću tzv. "hash" tablica i specijano izvedenih stabala za traženje (BST, AVL, RED-BLACK, B-tree). Neke od ovih struktura upoznat ćemo u ovom poglavlju.

20.2 Hash tablica Ideju hash2 tablice se može opisati na slijedeći način. Kada u telefonskom imeniku tražimo neko ime, najprije moramo pronaći stranicu na kojoj je zapisano to ime, a zatim pregledom po stranici dolazimo do traženog imena. Iskustvo pokazuje da više vremenu treba za traženje stranice, nego za traženje imena na stranici. Zbog toga se kod telefonskih imenika grupe stranica označavaju slovima abecede, kako bi se brže pronašla željena stranica. Na sličan način podaci u hash tablici se grupiraju u više skupova (ili buketa). U kojem skupu se nalaze podaci, određuje se iz ključa simbola pomoću posebne funkcije koja se naziva hash-funkcija. 2

eng. hash – znači podjelu na manje dijelove, koji su otprilike jednake veličine

318

Definicija: Funkcija hash(k,m) raspodjeljuje argument k u jedan od m skupova, sa statistički uniformnom razdiobom. Funkcija vraća vrijednost iz intervala i ∈ [0..m-1]. Ta vrijednost označava da string pripada i-tom skupu. Postoji više izvedbi hash-funkcije koje zadovoljavaju navedenu definiciju. Za rad sa stringovima ovdje će biti korišten sljedeći oblik hash-funkcije: unsigned hash(char *s, unsigned m) { /* Argument: s – string * m - maksimalni broj grupa * Vraća: hash vrijednost iz intervala [0..m-1] */ unsigned x=0; while(*s != '\0') { s = *s + 31 * x; k++; } return x % m; }

Može se primijetiti sličnost ove funkcije s funkcijom rand(), koja je opisana u poglavlju 11. Tada je slučajni broj xi , iz intervala [0..n-1], bio generiran iz prethodne vrijednost xi-1, prema izrazu: xi = (a * xi-1 + b) % m gdje su a i b konstante, koje su određene uvjetom da se broj generira sa statistički uniformnom razdiobom. U slučaju prikazane hash-funkcije ova se zakonitost primjenjuje kumulativno za sve elemente stringa (a=31, b=*s). Kada je ključ cjelobrojnog tipa tada se može koristiti funkcija unsigned hash(int k, unsigned M) { return k > 0? k % M : - k % M; }

20.2.1 Otvorena hash tablica Sada će biti opisana izvedba tablice simbola, koja se naziva otvorena hash-tablica (eng. Open Hashing ili Separate Chaining). Ilustrirana je na slici 20.1. Otvorena hash tablica se realizira kao niz koji sadrži pokazivače na liste simbola koje imaju istu hash vrijednost. Te liste se nazivaju buketi (eng. bucket).

319

Slika 20.1 Hash tablica s vezanom listom Operacije se provode prema sljedećem pravilu: insert(k,v) find (k)

Umetni simbol na glavu liste bucket[hash(k, m)]. Traži simbol ključa k u listi bucket[hash(k, m)]. Vraća

simbol ili NULL

delete(k)

Odstrani simbol iz liste bucket[hash(k, m)].

Element niza buckets[i] je pokazivač i-te liste. Početno su svi pokazivači jednaki NULL. Kada u tablici treba tražiti ili unijeti simbol kojem je ključno ime key, to se vrši u listi kojoj je pokazivač jednak buckets[hash(key,m)]. Slijedi opis implementacije tablice, prema specifikaciji ADT TABLE iz datoteke "table.h". Specifikacija za generičku hash tablicu /*Datoteka: table.h */ /*Apstraktni tip TABLE */ typedef struct _table *TABLE; typedef struct _symbol *SYMBOL; */ typedef typedef typedef typedef

int (*CompareFuncT)(void *, void *); void *(*CopyFuncT)(void *); void (*FreeFuncT)(void *); unsigned (*HashFuncT)(void *, unsigned);

/* Primjer za string /* /* /* /*

strcmp() strdup() free() hash()

*/ */ */ */

TABLE table_new(unsigned m, CompareFuncT f, HashFuncT h); /* Konstruktor tablice * Argument: m - veličina tablice * f - pokazivač funkcije za poredbu ključa (kao strcmp) * h - pokazivač hash funkcije (kao hash_str) * Ako se umjeto pokazivača f i h upiše 0 * podrazumjeva se rad s ključem tipa int * Vraća: pokazivač tablice ili NULL ako se ne može oformiti tablica */ void table_set_alloc(TABLE T, CopyFuncT copykey, FreeFuncT freekey, CopyFuncT copyval, FreeFuncT freeval); /* Postavlja pokazivače funkcija za alociranje ključa i vrijednosti * Argumenti: copykey - pokazivač funkcije za alociranje ključa * freekey - pokazivač funkcije za dealociranje ključa * copyval - pokazivač funkcije za alocira vrijednosti

320

* */

freeval - pokazivač funkcije za dealocirane vrijednosti

void table_free(TABLE T); /* Briše tablicu - oslobađa memoriju*/ int table_insert(TABLE T, void *k, void *v); /* Umeće simbol , u tablicu simbola, ako već nije u tablici. * Argumenti: T - tablica simbola (rječnik) * k - ključ simbola * v - pokazivač vrijednosti simbola * Vraća: 1 ako je umetanje uspješno, inaće vraća 0 */ SYMBOL table_find(TABLE T, void *k); /* Traži simbol poznatog ključa k u tablici simbola T * Argumenti: T – tablica simbola (rječnik) * k – ključ simbola * Vraća: pokazivač simbola, * ili NULL ako simbol nije pronađen */ void *table_symbol_value(SYMBOL S); /* Vraća pokazivač vrijednosti simbola S * ili cijeli broj, ako je vrijednost cijeli broj */ void *table_symbol_key(SYMBOL s); /* Vraća pokazivač ključa simbola S * ili cijeli broj, ako je ključ cijeli broj */ int table_delete(TABLE T, void *k); /* Traži simbol poznatog ključa k i briše ga iz tablice * Argumenti: T – tablica simbola (rjecnik) * k – ključ simbola * Vraća: 1 ako je simbol izbrisan ili 0 ako simbol nije u tablici */ unsigned table_size(TABLE T); /* Vraća broj elemenata u tablici */

Pokazivači ključa i vrijednosti simbola su tipa void *. Na taj način specifikacija se može koristiti za implementaciju generičkih tablica, za bilo koji tip ključa i vrijednosti simbola. Za potpunu generičku primjenu ADT-a, nakon inicijalizacije treba registrirati funkcije koje se koriste za dinamičko alociranje i dealociranje memorije. To se vrši funkcijom table_set_aloc(). Njeni argumenti su pokazivači na funkcije za alociranje i dealociranje ključa i vrijednosti simbola. Ovo pokazivači na funkcije se koriste i definiraju na isti način koji je opisan kod ADT DEQUEUE u poglavlju 18. Zbog zahtjeva za generičkim operacijama potrebno je prilagoditi i deklaracije hash funkcija. U datoteci "hash.c" definirane su hash funkcije za rad sa stringovima (hash_str) i cijelim brojevima (hash_int): /* Datoteka: hash.c */ unsigned hash_str(void

*key, unsigned M)

321

{/* Vraća hash vrijednost iz intervala [0..M-1] za string key */ unsigned hashval=0; char *s = key; while(*s != '\0') { hashval = *s + 31 * hashval; s++; } return hashval % M; } unsigned hash_int(void *p, unsigned M) {/* Vraća hash vrijednost iz intervala [0..M-1] za cjeli broj p */ /* Prvi argument će biti int, iako je deklariran void *p */ int k = (int)p; return k > 0? k % M : - k % M; }

Prema specifikacji ADT TABLE, tablica T, čiji simboli imaju ključ i vrijednost tipa string, inicijalizira se naredbama: TABLE T = new_table(1023, strcmp, hash_str); table_set_alloc(T, strdup, free, strdup, free);

Veličina tablice se odabire proizvoljno. Poželjno je da m bude prosti broj. Ako je ključ cjelobrojnog tipa inicijalizacija se vrši samo jednom naredbom. Primjerice, tablicu veličine m=1023, kojoj su ključ i vrijednost tipa int inicijalizira se naredbom: TABLE T = new_table(1023, 0, 0);

To znači da u implementaciji ADT-a treba predvidjeti automatsko postavljanje funkcije usporedbe ključeva i hash funkcije za rad s cijelim brojevima, kada su drugi i treći argument jednaki nuli. Pored apstraktnog objekta TABLE, specificiran je apstraktni objekt SYMBOL. Ideja je sljedeća: želimo da su simboli tablice dostupni korisniku. Simboli imaju dva atributa: ključ i vrijednost. Ako želimo pronaći vrijednost (ili ključ) simbola iz tablice, koristimo najprije funkciju table_find(), koja vraća tip SYMBOL, a zatim pomoću funkcije table_symbol_value() ili table_symbol_key(), iz poznatog simbola, dobijemo pokazivač vrijednosti ili ključa simbola. Primjerice, nakon naredbe char *v

= table_symbol_value(table_find(T, "xxx"));

varijabla v pokazuje string koji je vrijednost simbola koji ima ključ "xxx". Ako ključ "xxx" nije u tablici varijabla v sadrži NULL pokazivač. Ako simbol sadrži ključ i vrijednost cjelobrojnog tipa, tada se umjesto pokazivača radi s cijelim brojevima,primjerice int v

= (int) table_symbol_value(table_find(T, 123));

Implementacije ovih funkcija i objekata tipa TABLE i SIMBOL mogu biti različite. Sada će biti pokazana implementacije otvorene hash tablice, a zatim će biti pokazan drugi tip implementacije koji se naziva zatvorena hash tablica. Implementacija otvorene hash tablice

322

/* Datoteka htable1.c */ /* Otvorena hash tablica */ #include #include #include #include "table.h" typedef struct _symbol { struct _symbol *next; void *key; void *val; }Symbol; typedef struct _table { unsigned M; unsigned N; Symbol **bucket; CompareFuncT compare; HashFuncT hash; CopyFuncT copy_key; CopyFuncT copy_val; FreeFuncT free_key; FreeFuncT free_val; } Table, *TABLE;

/* /* /* /*

/* /* /* /* /* /* /* /* /*

čvor liste simbola pokazivač vezane liste pokazivač ključa simbola pokazivač vrijednosti simbola

*/ */ */ */

veličina tablice */ broj simbola u tablici */ niz pokazivača na listu simbola */ funkcije usporedbe kljuca */ hash funkcija */ funkcija alociranja ključa */ funkcija alociranja vrijednosti */ funkcija dealociranja ključa */ funkcija dealociranja vrijednosti*/

static int compareInternal(void *a, void *b) { /* usporedba dva integera a i b*/ if( (int)a > (int)b ) return 1; else if( (int)a < (int)b ) return -1; else return 0; } static unsigned hash_int(void *p, unsigned M) {/* Vraća hash vrijednost iz intervala [0..M-1] za cjeli broj p */ /* prvi argument će biti int iako je deklariran void *p */ int k = (int)p; return k > 0? k % M : - k % M; } TABLE table_new(unsigned M, CompareFuncT compare, HashFuncT hash) { TABLE T = (TABLE) malloc(sizeof(Table)); if (T == NULL) return NULL; T->M = M; T->N = 0; T->bucket = (Symbol **) calloc(M, sizeof(Symbol *) ); T->hash = hash ? hash : hashInternal; T->compare = compare? compare : compareInternal; T->free_key = T->free_val = NULL; T->copy_key = T->copy_val = NULL; return T; } void table_set_alloc(TABLE T, CopyFuncT copykey, FreeFuncT freekey, CopyFuncT copyval, FreeFuncT freeval) { T->copy_key = copykey; T->copy_val = copyval;

323

}

T->free_key = freekey; T->free_val = freeval;

static void free_node(TABLE T, Symbol *n) { assert(T); if(n == NULL) return; /* oslobodi sadržaj čvora: key, val i sam čvor*/ if(T->free_key) T->free_key(n->key); if(T->free_val) T->free_val(n->val); free(n); } static Symbol *new_node(TABLE T, void *key, void *val) { Symbol *np; assert(T); np = (Symbol *)malloc(sizeof(Symbol)); if (np == NULL ) return NULL; np->key =(T->copy_key)? T->copy_key(key) : key; np->val =(T->copy_val)? T->copy_val(val) : val; return np; } void table_free(TABLE T) { unsigned i; assert(T); for(i=0; iM; i++) { Symbol *p, *t; p = T->bucket[i]; /* glava liste */ while (p != NULL) { /* briše listu iz memorije */ t = p; p = p->next; free_node(T, t); } } free(T->bucket); free(T); } unsigned table_size(TABLE T) { assert(T); return T->N; } SYMBOL table_find(TABLE T, void *key) { Symbol *p; assert(T); p = T->bucket[T->hash(key, T->M)]; while( p != NULL) { if (T->compare(key, p->key) == 0) return (void *) p; /* pronađen simbol */ p = p->next; } return NULL; /* nije pronađen */ } void *table_symbol_value(SYMBOL S)

324

{ }

if(S) return S->val; else return NULL;

void *table_symbol_key(SYMBOL S) { if(S) return S->key; else return NULL; } int {

}

table_insert(TABLE T, void *key, void *val) SYMBOL s; unsigned h; assert(T); s = table_find(T, key); if (s == NULL) { /* ako ne postoji ključ Symbol *np = new_node(T, key, val); if (np == NULL) return 0; h = T->hash(key, T->M); np->next = T->bucket[h]; T->bucket[h] = np; T->N++; return 1: } return 0;

*/

int table_delete(TABLE T, void *key) { Symbol *p , *prev; unsigned h = T->hash(key, T->M); assert(T); prev = 0; p = T->bucket[h]; while (p && T->compare(p->key, key)) { prev = p; p = p->next; } if (!p) return 0; /* p - čvor koji brišemo, prev - prethodni čvor*/ if (prev) prev->next = p->next; else T->bucket[h] = p->next; /* glava buketa */ --T->N; free_node (T, p); return 1; } Testiranje se provodi programom hash_test.c. U njemu se formira tablica simbola, i u nju se

unosi nekoliko simbola. Zatim se provjerava da li se simbol "while" nalaze u tablici. Zatim se taj simbol briše i ponovo provjerava da li je u tablici. /* Datoteka: hash_test.c*/ #include #include #include #include "table.h" #include "hash.c" int main( ) { char str[20]; SYMBOL s;

325

TABLE T = table_new(127, strcmp, hash_str); table_set_alloc(T, strdup, free, strdup, free); table_insert(T, table_insert(T, table_insert(T, table_insert(T,

"if", "naredba selekcije"); "while", "petlja"); "for", "petlja"); "printf", "C-funkcija");

strcpy(str, "while"); s=table_find(T, str); if(s) printf("\n%s -> %s", str, table_symbol_value(s)); else printf("\n%s -> nije u tablici", str); /*izbriši simbol i provjeri da li je u tablici*/ table_delete(T, str); s = table_find(T, str); if(s) printf("\n%s -> %s", str, table_symbol_value(s)); else printf("\n%s -> nije u tablici\n", str); table_free(T); return 0; }

Nakon izvršenja programa dobije se ispis: while -> petlja while -> nije u tablici

Hash-tablica s vezanom listom omogućuje realiziranje tablice simbola u kojoj se vrlo brzo izvršavaju operacije traženja i umetanja u tablicu. Kada se radi s vrlo velikim skupom podataka, može se uzeti da u prosječnom slučaju obje operacije imaju složenost O(1). Samo u najgorem slučaju, kada svi ključevi imaju ustu vrijednost, složenost iznosi O(n). Do tog zaključka se dolazi sljedećom analizom složenosti. Uzmimo da je u tablici s m buketa pohranjeno n simbola. Definirajmo faktor popunjenosti α (eng. load faktor) kao omjer α = n/m. On pokazuje koliko prosječno ima simbola u jednom buketu. Ukupno vrijeme za pretraživanje tablice, uključujući vrijeme za proračun hash funkcije, iznosi O(1+α). Ako uzmemo da je broj buketa m barem proporcionalan broju simbola u tablici n, dobije se n = cm= O(m), i iz toga α = n/m=O(m)/m=O(1). Dakle, pretraživanje u prosjeku ima složenost O(1).

20.2.2 Zatvorena hash tablica Kada otprilike znamo koliko će elemenata sadržavati tablica, tada se ona može efikasno realizirati kao niz koji sadržava simbole, a ne pokazivače na bukete simbola. Ideja je da se simbol upiše u niz na indeksu koji je određen s hash vrijednošću. Pošto više simbola može imati istu hash vrijednost, ta pozicija može biti zauzeta s prethodno upisanim simbolom. U tom slučaju, koji se naziva kolizija, ispituje se da li se simbol može upisati na lokacijama koje sukscesivno, ili po nekom drugom zakonu, slijede iza te lokacije. Na isti način se vrši traženje u tablici. Niz, koji sadrži ovakvu tablicu, mora imati kapacitet m veći od broja simbola n, pa je faktor popunjenosti zatvorene tablice uvijek manji od jedinice (α = n/m htable[(i)].key) (T->htable[(i)].val) (T->htable[(i)].status (T->htable[(i)].status (T->htable[(i)].status (T->htable[(i)].status (T->htable[(i)].status (T->htable[(i)].status -1

*/ */ */ */ */ */ */ */ */

== 0) = 0) == 1) = 1) == -1) = -1)

static int compareInternal(void *a, void *b) { if( (int)a > (int)b ) return 1; else if( (int)a < (int)b ) return -1; else return 0; } static unsigned hashInternal(void *p, int m) {/* daje hash za integer*/ int k = (int)p; return (k > 0)? (k % m) : (-k % m); } static unsigned prime_list[] = { /*niz prostih brojeva*/ 7u, 11u, 13u, 17u, 19u, 23u, 29u, 31u, 53u, 97u, 193u, 389u, 769u, 1543u, 3079u, 6151u, 12289u, 24593u, 49157u, 98317u, 196613u, 393241u, 786433u, 1572869u, 3145739u, 6291469u, 12582917u, 25165843u, 50331653u, 100663319u, 201326611u, 402653189u, 805306457u, 1610612741u, 3221225473u, 4294967291u }; static unsigned get_next_prime(unsigned M)

329

{ /*vraca sljedeci prosti broj*/ int i, len = sizeof(prime_list)/sizeof(unsigned); for(i=0; i= M) return prime_list[i]; } return prime_list[len-1]; } TABLE table_new(unsigned M, CompareFuncT compare, HashFuncT hash) { TABLE T = (TABLE) malloc(sizeof(Table)); if (!T) return NULL; T->M = get_next_prime(M); T->N = 0; T->htable = (Symbol *)calloc(T->M, sizeof(Symbol) ); T->hash = hash ? hash : hashInternal; T->compare = compare? compare : compareInternal; T->free_key = T->free_val = NULL; T->copy_key = T->copy_val = NULL; return T; } void table_set_alloc(TABLE T, CopyFuncT copykey, FreeFuncT freekey, CopyFuncT copyval, FreeFuncT freeval) { T->copy_key = copykey; T->copy_val = copyval; T->free_key = freekey; T->free_val = freeval; } void table_free(TABLE T) { unsigned i; short status; assert(T); for(i = 0; i < T->M; i++) { if(FULL(i)) { if(T->free_key) (*T->free_key)(KEY(i)); if(T->free_val) (*T->free_val)(VAL(i)); } } free(T->htable); free(T); } int find_sym_idx(TABLE T, void *key) { unsigned i = 0; unsigned idx = T->hash(key, T->M); while (1) { if(EMPTY(idx)) return NOT_FOUND; if(FULL(idx) && T->compare(KEY(idx), key) == 0) return idx; /*pronaðen simbol*/ i++; idx = (idx + (2*i-1)) % T->M; }

330

return NOT_FOUND; } SYMBOL table_find(TABLE T, void *key) { int i; assert(T); i = find_sym_idx (T, key); if(i != NOT_FOUND) return &T->htable[i]; else return NULL; /* nije pronaðen */ } void *table_symbol_value(SYMBOL s) { if(s) return s->val; else return NULL; } void *table_symbol_key(SYMBOL s) { if(s) return s->key; else return NULL; } int table_insert(TABLE T, void *key, char *val) { unsigned idx, i = 0; assert(T); if(T->N >= T->M/2) return 0; /*ovdje primijeni rehash funkciju*/ idx = T->hash(key, T->M); for(;;) /* probaj sljedeći index*/ { if(!FULL(idx)) { KEY(idx) = (T->copy_key)? T->copy_key(key) : key; VAL(idx) = (T->copy_val)? T->copy_val(val) : val; SET_FULL(idx); T->N++; return 1; } else if(T->compare(KEY(idx), key) == 0) { return 0; /* već postoji simbol*/ } i++; idx = (idx + (2*i-1)) % T->M; } return 0; } int table_delete(TABLE T, void *key) { int i; assert(T); i = find_sym_idx (T, key) ; if(i != NOT_FOUND && !(DELETED(i))) { if(T->free_key) T->free_key(KEY(i)); if(T->free_val) T->free_val(VAL(i)); SET_DELETED(i); --T->N;

331

return 1; } return 0;

/* nije pronađen */

} unsigned table_size(TABLE T) { assert(T); return T->N; }

Testiranje ovog ADT provodimo s istim programom kojim je testirana prethodna implementacija otvorene hash tablice. Zadatak: Usporedite brzinu izvršena otvorene i zatvorene hash tablice u programu table_timer_test.c. U tom programu su pomoću funkcije clock() mjeri vrijeme izvršenja petlje u kojoj se umeću i brišu cjelobrojni slučajno odabrani ključevi simbola. Petlja se ponavlja 1000000 puta. /*Datoteka: table_timer_test.c*/ #include #include #include #include #include #include "table.h" int main() { float sec; long start = 0L, end = 0L; int maxnum, maxrand, status, i; int val= 9; int key; TABLE T; /* procesiraj 100000 slučajnih brojeva */ maxnum = 100000; maxrand = 1501; printf("maxnum = %d\n", maxnum); T = table_new(maxrand*2, 0, 0); start = clock (); for (i = maxnum; i>0; i-- ){ key = rand() % maxrand; if ( table_find(T,(void *)key) ) { status = table_delete(T,(void *)key); if (!status) printf("fail: status = %d\n", status); } else { status = table_insert(T, (void *)key,(void *) val); if (!status) printf("fail: status = %d\n", status); } } end = clock (); sec=(float)(end-start)/(float)CLOCKS_PER_SEC; printf("\n %d kljuceva procesirano u %f ms",maxnum, sec*1000);

332

}

table_free(T); return 0;

Napomena: Otvorenu hash tablicu inicijalizirajte na veličinu maxrand/2, a zatvorenu tablicu na veličinu maxrand*2. Mijenjajte veličinu maxrand od 127 do 10000. Primjetit ćete da se operacije s otvorenom hash tablicom, brže izvršavaju od operacija s zatvorenom tablicom. Zadatak: U prethodnoj implementaciji zatvorene hash tablice metodom kvadratičnog ispitivanja izvršte sljedeću izmjenu: Ukoliko broj simbola u tablice n premaši vrijednost m/2, treba udvostručiti vrijednost tablice. Na taj način tablica može primiti proizvoljan broj simbola. Za tu svrhu koristite funkciju table_rehash(T): TABLE table_rehash(TABLE T ) /* Vraća pokazivač tablice koja ima dvostuko veći kapacitet * od ulazne tablice T, i koja sadrži simbole it T */ { unsigned int i; TABLE Tnew; /* Formiraj uvećanu tablicu i umetni vrijednosti iz T*/ Tnew = table_new( get_next_prime(2*T->M)); for( i=0; iM; i++ ) if(T->htable[i].key != NULL ) table_insert( Tnew, T->htable[i].key, T->htable[i].val ); free(T->htable ); free(T); return Tnew; }

Analizom programerske prakse vidljivo je da se znatno više koristi otvorene tablice. One daju znatno bolju efikasnost kod visoke popunjenosti tablice.

20.3 BST - binarno stablo traženja BST je kratica za naziv binarno stablo traženja (eng. binary search tree). Temeljna karakteristika binarnog stabla traženja je da uvijek sadrži sortirani raspored čvorova. Jedan od elemenata BST čvora predstavlja jedinstveni ključ. Usporedbom vrijednosti ključa određuje se red kojim se umeću ili pretražuju čvorovi stabla, prema sljedećoj rekurzivnoj definiciji. Definicija: Binarno stablo traženja (BST) je: 1. Prazno stablo, ili 2. Stablo koje se sastoji od čvora koji se naziva korijen i dva čvora koji se nazivaju lijevo i desno dijete, a oni su također binarna stabla (podstabla). Svaki čvor sadrži ključ čija vrijednost je veća od vrijednosti ključa čvora lijevog podstabla, a manja ili jednaka vrijednosti ključa čvora desnog podstabla.

333

a)

b)

Slika 20.2 Sortirano binarno stablo, koje nastaje kad je redosljed unošenja elemenata: a) 6, 9, 3, 8, 5, 2. b) 3, 2, 5, 6, 8, 9. Najljeviji čvor sadrži najmanji ključ (2), a najdesniji čvor sadrži najveći ključ (9). Strelice pokazuju INORDER obilazak stabla. Slika 20.2a) prikazuje binarno stablo u kojem su elementi uneseni redom: 6, 9, 3, 8, 5, 2, prema prethodnoj definiciji BST. Strelice pokazuju da se INORDER obilaskom stabla dobije sortirani raspored elemenata(2, 3, 5, 6, 8, 9). Slika 20.2b) prikazuje binarno stablo u kojem su elementi uneseni redom: 3,2,5,6,8,9. Iako stabla sa slika 20.2a) i 20.3b) sadrže iste elemente, oblik stabala je različite jer oblik ovisi o redoslijedu elemenata. Najveća visina stabla nastaje kada se unose elementi u sortiranom rasporedu, a najmanja kada se elementi unose po slučajnom uzorku. U najboljem slučaju dobije se potpuno stablo. Tada je potreban minimalan broj operacija za prijeći stazu od korijena do lista, bilo za traženje ili unošenje elementa u stablo. Nešto kompleksnija statistička analiza pokazuje da se u najboljem i u prosječnom slučaju vrši O(log2 n) operacija. U najgorem slučaju, kod sortiranog unosa, vrši se O(n) operacija, jer stablo degenerira u "kosu" listu. Može se zaključiti da BST predstavlja manje efikasno rješenje za izradu tablica ili rječnika od hash tablice koji nudi efikasnost O(1). Ipak, BST nudi neke mogućnosti koje se ne može ostvariti hash tablicom. To se u prvom redu odnosi na formiranje dinamičkih sortiranih skupova u kojima se lako određuje raspored elemenata. Za tu svrhu definira se skup operacija: ADT BSTREE insert (T, k, v) find (T, k) delete(T, k) minimum(T) maximum(T) predecessor(T, x) succcessor(T, x)

- umeće simbol, kojem je ključ k i vrijednost v u T, ako već nije u T. - vraća simbol ako u T postoji simbol ključa k, ili NULL - briše simbol kojem je ključ k, ako postoji u T. - vraća simbol iz T koji ima minimalnu vrijednost ključa. - vraća simbol iz T koji ima maksimalnu vrijednost ključa. - vraća simbol iz T koji prethodi simbolu x - vraća simbol iz T koji prethodi simbolu x

Ove operacije definiraju ADT BSTREE, kojem je specifikacija operacija u C jeziku zapisana u datoteci "bst.h". #ifndef #define

_RBT_BST_H_ _RBT_BST_H_

/* odstrani komentar ako zelis raditi s RED-BLACK stablom*/ /* #define RED_BLACK */ typedef struct _symbol *SYMBOL; typedef struct bs_tree *BSTREE;

334

typedef int (*CompareFuncT)(void *, void *); typedef void *(*CopyFuncT)(void *); typedef void (*FreeFuncT)(void *); BSTREE bst_new(CompareFuncT compare); /* Stvara ADT BSTREE. * Argument: compare - pokazivač funkcije za usporedbu dva ključa * Ako je NULL, uzima se da stablo sadrži cijele brojeve * Vraća: pokazivač ADT-a */ void bst_set_alloc(BSTREE T, CopyFuncT copykey, FreeFuncT freekey, CopyFuncT copyval, FreeFuncT freeval); /* Postavlja funkcije za alociranje (kopiranje) i dealociranje simbola * Argumenti: T - ADT stablo (mora biti formiran) * copykey - pokazivač funkcije za kopiranje ključa * copyval - pokazivač funkcija za kopiranje vrijednosti * freekey - pokazivač funkcija za dealociranje ključa * freeval - pokazivač funkcija za dealociranje vrijednosti */ void bst_free(BSTREE T); /* Dealocira cijelo stablo T*/ int bst_size(BSTREE T); /* Vraća broj simbola u stablu T*/ int bst_insert(BSTREE T, void * key, void *val); /* Umeće simbol u stablo T, ali samo ako ključ key nije u stablu * Argumenti: T - ADT stablo (mora bit formiran) * key - pokazivač ključa (ili cijeli broj) * val - pokazivač vrijednosti (ili cijeli broj) * Vraća: 1 ako je izvršeno umetanje, inače vraća 0 */ int bst_delete(BSTREE T, void * key); /* Briše simbol koji ima ključ key, iz stabla T * Argumenti: T - ADT stablo (mora bit formiran) * key - pokazivač ključa (ili cijeli broj) * Vraća: 1 ako je izvršeno brisanje, inače vraća 0 */ SYMBOL bst_find(BSTREE T, void * key); /* Vraća pokazivač simbola, ako ključ postoji */ void *bst_symbol_value(SYMBOL S); /* Vraća pokazivač vrijednosti simbola S (ili cijeli broj)*/ void *bst_symbol_key(SYMBOL s); /* Vraća pokazivač ključa simbola S (ili cijeli broj)*/ SYMBOL bst_minimum(BSTREE T); /* Vraća pokazivač simbola koji ima minimalni ključa u stablu T*/ SYMBOL bst_maximum(BSTREE T); /* Vraća pokazivač simbola koji ima najveæi ključ u stablu T*/

335

SYMBOL bst_succesor(SYMBOL S); /* Vraća pokazivač simbola koji slijedi iza simbola S * PRE: S mora biti pokazivač čvora stabla */ SYMBOL bst_predecessor(SYMBOL S); /* Vraća pokazivač simbola koji prethodi simbolu S * PRE: S mora biti pokazivač čvora stabla */ #endif

Primijetite da je na početku ove datoteke, unutar komentara definirano: #define RED_BLACK

Ako se primijeni ova definicija tada će se navedene operacije provoditi po algoritmu za tzv. crveno-crna stabla (eng. red-black trees). Ideja i algoritmi za crveno-crna stabla bit će opisani u sljedećem odjeljku. Ukratko, kod crveno-crnog stabla svakom se čvoru pridjeljuje boja (eng. color). Taj se podatak koristi u algoritmima za uravnoteženje stabla, s ciljem da sve staze stabla, i u najgorem slučaju, budu dostupne sa složenošću O(log2 n). Prije nego se objasni postupak izgradnje crveno-crnog stabla neće se analizirati dio kôda koji je napisan unutar #ifdef RED_BLACK ........ #endif

/* kod za RED-BLACK stabla */

Implementacija ADT BSTREE je u dana u datoteci "bst.c". Najprije je izvršeno definiranje struktura za implementaciju ADT BSTREE. Prva struktura, struct _simbol, opisuje čvor binarnog stabla. Također je s typedef definiran i sinonim za ovu strukturu imena Node. Definirana su tri pokazivača: na lijevo dijete, na desno dijete i na roditelja. Pokazivač na roditelja je potreban za implementaciju operacija successor() i predecessor(). Ako ove operacije nisu potrebne, tada se može izvršiti implementacija ADT-a i bez pokazivača na roditelja. Simboli su određeni ključem (key) i vrijednošću (val). U ovoj implementaciji key i val su definirani tipom void *, što znači da će se dinamičkim alociranjem memorije moći registirati simbole bilo kojeg tipa ključa i vrijednosti. Primijetimo da se u strukturi Node opciono bilježi "boja" čvora u članu Color, koji može imati samo dvije pobrojane vrijednosti: RED i BLACK. Za potpunu generičku implementaciju pri inicijalizaciji ADT-a bit će potrebno registrirati funkcije koje se koriste za dinamičko alociranje i dealociranje memorije. Pokazivači na ove funkcije zajedno s pokazivačem korijena stabla se bilježe u strukturi bs_tree. Tip pokazivača na ovu strukturu - BSTREE služi za označavanje ADT-a. /* Datoteka: bst.c * Implementacija bst ili red-black stabla (ako je definirano RED_BLACK) */ #include #include #include #include "bst.h" typedef enum { BLACK, RED } nodeColor; typedef struct _symbol Node;

336

struct _symbol { struct _symbol *left; struct _symbol *right; struct _symbol *parent; void * key; void * val; #ifdef RED_BLACK nodeColor color; #endif }; struct bs_tree { int N; Node *root; CompareFuncT compare; CopyFuncT copy_key; CopyFuncT copy_val; FreeFuncT free_key; FreeFuncT free_val; };

/* /* /* /* /*

/* /* /* /* /* /* /*

lijevo dijete desno dijete roditelj ključ simbola vrijednost simbola boja (BLACK, RED)

funkcija funkcija funkcija funkcija funkcija

za za za za za

*/ */ */ */ */ */ */

usporedbe ključa alociranje ključa alociranje vrijednosti dealociranje ključa dealociranje vrijednosti

*/ */ */ */ */

Slika 20. 3. Standardni i prošireni oblik binarnog stabla. Kod standardnog oblika listovi nemaju djece i pokazuju na NULL. Kod proširenog oblika stabla svi su podaci smješteni u unutarnjim čvorovima. Listovi ne sadrže podatke već su to virtualni čvorovi na koje pokazuje jedinstveni NIL pokazivač. Zbog lakše izvedbe algoritama koristit će se prošireni oblik binarnog stabla, u kojem se svi podaci nalaze u unutarnjim čvorovima, a vanjski čvorovi su tzv. NIL čvorovi (slika 20.2), tj. čvorovi na koje pokazuje NIL pokazivač. Roditelj korijena stabla također je NIL čvor. Vanjski čvorovi ne sadrže podatke, oni služe jedino kao graničnici u nekim algoritmima, pa se za vezu s vanjskim čvorovima koristi jedinstveni pokazivač - NIL - koji pokazuje na jedan globalni čvor kojeg se naziva sentinel. Sentinel pokazuje na samog sebe, jer mu se svi pokazivači (left, right i parent) inicijaliziraju na vrijednost NIL. To je ostvareno definicijama: #define NIL &sentinel

/* svi listovi su NIL sentinel */

#ifdef RED_BLACK static Node sentinel = { NIL, NIL, NIL, 0, 0, BLACK}; #else static Node sentinel = { NIL, NIL, NIL, 0, 0}; #endif

Zbog preglednijeg zapisa programa koristit će se sljedeće definicije: #define

Left(x)

(x)->left

337

#define #define #define #define #define #define

Right(x) Parent(x) Color(x) Key(x) Val(x) Root(x)

(x)->right (x)->parent (x)->color (x)->key (x)->val (x)->root

Prve operacije koje treba definirati su operacije kojima se inicijalizira ADT i kojima se briše objekt ADT iz memorije. Funkcija bst_new() prima za argument pokazivač na funkciju za usporedbu ključeva. Ako je ovaj argument jednak nuli postavlja se interna funkcija CompareInternal() kojo se vrši usporedba cijelih brojeva. Korijen stabla je početno prazan i njemu se pridjeljuje vrijednost NIL pokazivača. static int CompareInternal(void *a, void *b) { /*funkcija usporedbe cijelih brojeva*/ if( (int)a > (int)b ) return 1; else if( (int)a < (int)b ) return -1; else return 0; } BSTREE bst_new(CompareFuncT compare) { BSTREE T =(BSTREE)malloc(sizeof(struct bs_tree)); if(T){ Root(T) = NIL; T->N = 0; T->compare = compare? compare : compareInternal; T->free_key = T->free_val = NULL; T->copy_key = T->copy_val = NULL; } return T; } void bst_set_alloc(BSTREE T, CopyFuncT copykey, FreeFuncT freekey, CopyFuncT copyval, FreeFuncT freeval) { /*registriraj funkcije za kopiranje i dealociranje */ T->copy_key = copykey; T->copy_val = copyval; T->free_key = freekey; T->free_val = freeval; }

Dvije interne funkcije newNode() i freeNode() služe za formiranje i dealociranje novog čvora, Funkcija deletaAll() , koristeći "postorder" obilazak stabla, briše sve čvorove. Nju koristi javna funkcija bst_free() kojom se dealocira cijelo stablo. static Node *newNode(BSTREE T, void * key, void *val, Node *parent) { Node *x = (Node *) malloc (sizeof(Node)); if (x == 0) return 0; Left(x) = NIL; Right(x) = NIL; Key(x) = (T->copy_key)? T->copy_key(key) : key; Val(x) = (T->copy_val)? T->copy_val(val) : val; Parent(x) = parent; #ifdef RED_BLACK

338

Color(x) = RED; #endif return x; } static void freeNode(BSTREE T, Node *n) { if(n != NIL && n != NULL) { if(T->free_key) T->free_key(n->key); if(T->free_val) T->free_val(n->val); free(n); } } static void deletaAll(BSTREE T, Node *n ) {/* dealocira čvorove */ if( n != NIL && n != NULL) { deletaAll(T, Left(n)); deletaAll(T, Right(n)); freeNode(T, n); } } void bst_free(BSTREE T) {/* dealocira cijelo stablo*/ assert(T); deletaAll(T, Root(T)); free(T); }

Najednostavnija je operacija size() koja vraća broj simbola u stablu: int bst_size(BSTREE T) { assert (T); return T->N; }

Operacija traženja se jednostavno implementira funkcijom bst_find(). Polazi se od korijena stabla i ispituje da li čvor sadrži ključ koji je veći, manji ili jednak argumentu key. Ako je jednak, traženje je završeno i funkcija vraća pokazivač čvora. Ako je argument key veći od ključa u čvoru, traženje se nastavlja u desnom stablu, inače se traženje nastavlja u lijevom čvoru. Traženje se ponavlja sve do čvor ne pokazuje NIL. Ako se ne pronađe traženi ključ funkcija vraća NULL. SYMBOL bst_find(BSTREE T, void * key) {/* Vraca pokazivač simbola ako pronađe key, ili NULL */ Node *n; assert(T); n = Root(T); while(n != NIL) { int cmp = T->compare(key, Key(n)); if(cmp == 0) return n; else if(cmp > 0) n = Right(n); else /*(cmp < 0)*/ n = Left(n); }

339

return NULL; }

Funkcija bst_find() vraća vrijednost tipa SYMBOL. Iako u implementaciji SYMBOL predstavlja pokazivač čvora, tom se čvoru ne može pristupiti jer je implementacija skrivena od korisnika. Za dobivanje ključa i vrijednosti simbola definirane su funkcije bst_symbol_key() i bst_symbol_value(). void *bst_symbol_value(SYMBOL s) { if(s) return s->val; else return NULL; } void *bst_symbol_key(SYMBOL s) { if(s) return s->key; else return NULL; }

Pronalaženje maksimalne vrijednosti ključa je trivijalno; počevši od korijena stabla ispituje se desni čvor, sve do lišća stabla, jer najdesniji čvor ima maksimalni ključ. Kod traženja najmanjeg elementa ispituje se lijevi čvor jer najljeviji čvor ima minimalni ključ. SYMBOL bst_maximum(BSTREE T) {/* vraća simbol s najvećim ključem u sortiranom stablu T */ Node *n; assert(T); n = T->root; while (Right(n) != NIL) n = Right(n); return (n == NIL)? NULL : n; } SYMBOL bst_minimum(BSTREE T) { /* vraća simbol s minimalnim ključem u sortiranom stablu T*/ Node *n; assert(T); n = T->root; while (Left(n) != NIL) n = Left(n); return (n == NIL)? NULL : n; }

Slijedi opis funkcija bst_predecessor(x) i bst_successor(x). Sljednik čvora x (successor) je čvor s najmanjim ključem koji je veći od Key(x). Analogno tome je definiran prethodnik čvora x (predecessor) kao čvor s najvećim ključem koji je manji od Key(x). SYMBOL bst_succesor(SYMBOL x) { /* Vraća sljednika od čvora x * ili NULL ako sljednik ne postoji*/ /* PRE: x mora biti čvor stabla */ Node *y; if(x == NIL || x == NULL) return NULL; if((y = Right(x)) != NIL){ while (Left(y) != NIL) y = Left(y); return y; /* minimum desnog stabla od x*/ } y = Parent(x); while (y != NULL && x == Right(y)) { x = y;

340

y = Parent(y); } return y; }

Funkcija successor(x) vrši dvije analize. Prvo ako, čvor x ima desno dijete, znači da postoji veća vrijednost u desnoj stazi. Nju se dobije kao minimum(Right(x)). U drugom slučaju, ako je Right(x) == NULL, tada treba unatrag analizirati roditelja čvora i tražiti njegovo lijevo dijete (ukoliko je x desno dijete). Na analogan način se implementira funkcija predecessor(x). SYMBOL bst_predecessor(SYMBOL x) {/* Vraća prethodnika od čvora x ili NULL ako ne postoji*/ /* PRE: x mora biti čvor stabla */ Node * y; if(x == NIL || x == NULL) return NULL; if ((y = Left(x)) != NIL) { while (Right(y) != NIL) y = Right(y); return y; /* maksimum lijevog stabla od x*/ } else { y = Parent(x); while(y != NULL && x == Left(y)) { x = y; y = Parent(y); } return(y); } }

Umetanje novog simbola (čvora) stabla se vrši prema sljedećem algoritmu: 1) Analiziraj stablo od korijena do lista. Ako je zadani ključ manji od ključa u čvoru, analiziraj čvor lijevo, inače analiziraj čvor desno. Ako je zadani ključ jednak ključu u čvoru, prekini ispitivanje, jer se simbol već postoji. Ako dođeš do lista, zapamti njegovog roditelja i prekini ispitivanje. 2) Alociraj novi čvor s zadanim ključem, vrijednošću i roditeljem. 3) Ako je roditelj jednak NULL novi čvor postaje korijen stabla inače postaje lijevo ili desno dijete, ovisno o tome da li je ključ bio manji ili veći od ključa u čvoru roditelja. Ovaj algoritam je implementiran u funkciji bst_insert(). int bst_insert(BSTREE T, void * key, void *val) {/*Vraća 1 ako je umetnut simbol u stablo T, ili 0*/ Node *parent = 0; Node *x = Root(T); while (x != NIL) { int cmp = T->compare(key, Key(x)); if(cmp == 0) /*ako postoji ključ završi i vrati 0*/ return 0; parent = x; if(cmp > 0) x = Right(x); else /*(compare(key, Key(parent)); if(cmp < 0) Left(parent) = x; else Right(parent) = x; } else { Root(T) = x; } #ifdef RED_BLACK insertFixup(T,x); #endif T->N++; return 1; }

Operacija brisanja čvora je implementirana u funkciji bst_delete(). Treba uočiti tri slučaja: 1) čvor koji treba izbrisati je list 2) čvor koji treba izbrisati ima samo jedno dijete 3) čvor koji treba izbrisati ima dvoje djece Ilustrirani su na slici 20.4. Prva dva slučaja su jednostavna: čvor odstranjujemo na način da roditelju čvora pridijelimo pokazivač na dijete čvora (koje može biti NIL), a zatim dealociramo čvor. U trećem slučaju čvor ima dva djeteta. Njega možemo odstraniti, tako da u njega upišemo sadržaj čvora s prvim većim ključem (to je čvor s najmanjim ključem u desnom podstablu), a da zatim odstranimo taj prvi veći čvor. Pošto je prvi veći čvor najmanji čvor u desnom podstablu, on ne može imati lijevo dijete, pa se njega briše jednostavno, kao u prva dva slučaja.

Slika 20.4 Tri slučaja brisanja čvora (podebljeno je prikazan čvor koji se briše)

342

int bst_delete(BSTREE T, void * key) { Node *x, *y, *z; z = bst_find(T, key); if (!z) return 0; if (Left(z) == NIL || Right(z) == NIL) { /* y ima NIL djecu - mozeš ga premostiti */ y = z; } else { /* ako postoje oba djeteta - nađi prvog većeg */ /* to je minimalni s desna - on ima samo jedno dijete */ y = Right(z); while (Left(y) != NIL) y = Left(y); } /* y ćemo kasnije odstraniti */ /* y ima jedno dijete, spremi to dijete u x */ x = (Left(y) != NIL)? Left(y) : Right(y); /* odstrani y iz lanca roditelja */ /* tako da x postane lijevo ili desno dijete od Parent(y)*/ Parent(x) = Parent(y); if (Parent(y) == NIL) Root(T) = x; else if (y == Left(Parent(y))) Left(Parent(y)) = x; else Right(Parent(y)) = x; if (y != z) { /*zamijeni sadržaj y i z*/ void *tmp = Key(z); Key(z) = Key(y); Key(y) = tmp; tmp = Val(z); Val(z) = Val(y); Val(y) = tmp; } #ifdef RED_BLACK if (Color(y) == BLACK) deleteFixup (T,x); #endif freeNode(T, y); T->N--; return 1; }

Testiranje ADT BSTREE se može provesti programom "bst_test.c". Demonstrira se umetanje, traženje i brisanje simbola s cjelobrojnim ključem, te ispis u sortiranom rasporedu od manjeg prema većem ključu i obrnuto. /*Datoteka: bst_test.c*/ #include #include #include #include #include "bst.h" int main() { int maxnum, ct, status; int key, val= 9; SYMBOL s; BSTREE T = bst_new(0);

343

/* procesiraj 100 slučajnih brojeva */ maxnum = 100; printf("maxnum = %d\n", maxnum);

}

for (ct = maxnum; ct; ct-- ) { key = rand() % 19 + 1; if ( bst_find(T,(void *)key) ) { status = bst_delete(T,(void *)key); if (!status) printf("fail: status = %d\n", status); } else { status = bst_insert(T, (void *)key,(void *) val); if (!status) printf("fail: status = %d\n", status); }

printf("\nU stablu ima %d simbola.\n", bst_size(T)); s = bst_minimum(T); while(s) { printf("%d ", (int)bst_symbol_key(s) ); s = bst_succesor(s); }

}

printf("\nReverzni ispis:\n"); s = bst_maximum(T); while(s){ printf("%d ", (int)bst_symbol_key(s) ); s = bst_predecessor(s); } bst_free(T); return 0;

Nakon izvršenja programa dobije se ispis: U stablu ima 8 simbola. 1 5 6 7 12 16 17 19 Reverzni ispis: 19 17 16 12 7 6 5 1

20.4 Crveno-crna stabla Crveno-crno stablo (eng. red-black tree) je modificirano binarno stablo traženja kojem svaki čvor ima jedan dodatni atribut: može biti crven ili crn. Crveno-crno stablo je određeno sljedećim svojstvima: Definicija: Crveno-crno stablo je binarno stablo sa svojstvima: 1. Svaki čvor je ili crn ili crven. 2. Korijen je crn 3. Svaki list (NIL) je crn. 4. Ako je čvor crven, njegova oba djeteta su crni (to znači da roditelj crvenog čvora ne smije biti crven). 5. Svaka prosta staza od nekog čvora do listova sadrži jednak broj crnih čvorova.

344

Posljedica ove definicije je a) da ni jedna staza od korijena do NIL čvorova nije duplo dulja od ostalih staza i b) visina crveno-crnog stabla s n čvorova iznosi najviše 2log2(n+1). Ovo svojstvo se lako dokazuje. Dokaz: Prvo ćemo dokazati da bilo koje podstablo čvora x sadrži barem 2bh(x)-1 unutarnjih čvorova. Funkcija bh(x) daje broj crnih čvorova u bilo kojoj stazi od čvora x (ne uključujući x) do listova. Ako je visina 0, x je list (NIL) - tada ima 2bh(x)-1 = 20-1= 0 unutarnjih čvorova. Razmotrimo sada unutarnji čvor s dva djeteta. Njegova visina je bh(x) ili bh(x)-1 ovisno o tome da li je dijete crveno ili crno. Pošto je to dijete od x ima manju visinu od x, možemo zaključiti da dijete ima barem 2bh(x)-1-1 unutarnjih čvorova, pa podstablo od x ima barem (2bh(x)-1-1)+ (2bh(x)-1-1)+1 = 2bh(x)-1 unutarnjih čvorova, čime je dokazan početni stav. Ako je visina stabla h, a prema svojstvu 4 bar pola čvorova mora biti crno, tada bh visina korijena mora biti barem h/2, pa broj čvorova ispod korijena iznosi n ≥ 2h/2-1. Logaritmiraući ovaj izraz dobijemo da je h ≤ 2 log2(n+1). Ovaj dokaz pokazuje da operacije traženja u crveno-crnom stablu uvijek ima složenost O(logn). Postavlja se pitanje, kako izvršiti operacije umetanja i brisanja, uz uvjet da se uvijek zadrži crveno-crno svojstvo stabla. Ideja je jednostavna, nakon umetanja čvora (i nakon brisanja čvora) treba provjeriti raspored crnih i crvenih čvorova. Ako raspored ne zadovoljava prethodnu definiciju svojstava crveno-crnog stabla, treba izvršiti transformaciju stabla. Transformacije stabla se vrše pomoću dva temeljna postupka - lijeve i desne rotacije čvorova. Ti postupci su ilustrirani na slici 20.5. Primjetite da nakon obje rotacije raspored čvorova ostaje nepromijenjen: Ključ(A) < Ključ(B) < Ključ (C), dakle rotacije mijenjaju oblik stabla, ali ne mijenjaju "inorder" raspored čvorova.

Slika 20.5 Desna i lijeva rotacija čvorova Rotacije čvorova sa slike 20.5 vrše se pomoću funkcija rotateLeft() i rotateRight(). #ifdef RED_BLACK static void rotateLeft(BSTREE T, Node *x) { Node *y; /* rotira čvor x u lijevo */ assert (x); assert(Right(x)); y = Right(x); /* veza s Right(x) */ Right(x) = Left(y); if (Left(y) != NIL) Parent(Left(y)) = x; /* poveži s Parent(y) */ if (y != NIL) Parent(y) = Parent(x); if (Parent(x)) { if (x == Left(Parent(x))) Left(Parent(x)) = y; else

345

}

Right(Parent(x)) = y; } else { Root(T) = y; } /* poveži x i y */ Left(y) = x; Parent(x) = y;

static void rotateRight(BSTREE T, Node *x) { Node *y; /* rotira čvor x u desno */ assert (x); y = Left(x); /* poveži s Left(x) */ Left(x) = Right(y); if (Right(y) != NIL) Parent(Right(y))= x; /* poveži s Parent(y) */ if (y != NIL) Parent(y) = Parent(x); if (Parent(x)) { if (x == Right(Parent(x))) Right(Parent(x)) = y; else Left(Parent(x)) = y; } else { Root(T) = y; } /* poveži x i y */ Right(y) = x; Parent(x) = y; } #endif

Ove funkcije će biti korištene u funkciji insertFixup(), kojom se ispravlja ravnoteža stabla nakon umetanja čvora, i u funkciji deleteFixup(), kojom se ispravlja ravnoteža stabla nakon brisanja čvora. Funkcija insertFixup() se poziva na završetku funkcije bst_insert(), a funkcija deleteFixup() se poziva na završetku funkcije bst_delete(). Najprije će biti opisan algoritam koji se koristi u funkciji insertFixup(). Uravnoteženje stabla nakon umetanja čvora Prilikom umetanja čvora, u funkciji bst_insert(), uvijek se umeće crveni čvor. Ako je i roditelj umetnutog čvora crven, nastaje neuravnoteženo stablo jer više nije zadovoljeno svojstvo 4 iz definicije crveno-crnog stabla. Ta se neuravnoteženost ispravlja pozivom funkcije insertFixup(). Slike 20.6 i 20.7 omogućuju objašnjenje algoritma iz ove funkcije. Ukoliko umetnuti čvor nije korijen stabla, funkcija se izvršava unutar petlje sve dok je roditelj umetnutog čvora crven. Analizira se i ispravlja uravnoteženost stabla. Prvo se analiziraju ujaci umetnutog čvora: (Left(Parent(Parent(x))) i Right(Parent(Parent(x))). Ako su oni crveni, dovoljno je promijeniti boju čvorova, kao na slici 20.6, a ako su crni tada se vrši jednostruka ili dvostruka rotacija, uz promjenu boje čvorova, prema slici 20.7. U analizi nas ne zanimaju djeca umetnutog čvora, jer ako je roditelj umetnutog čvora crven, tada je, prema svojstvu 4, sigurno da su mu djeca crna.

346

Slika 20.6 Prvi slučaj analize umetanja čvora x. Crni čvorovi su prikazani tamnije. Nije zadovoljeno svojstvo 4 jer su crveno obojani i x i njegov roditelj. Ujak od x, odnosno y=Right(Parent(Parent(x))) je također crven. Svako podstablo A,B,C,D i E ima crni korijen i jednaku crnu-visinu. Nisu nužne rotacije već samo izmjena boje čvorova. Nakon prikazane promjene boje čvorova while petlja nastavlja s čvorom "Novi x" i ispituje mogući problem, tj. da li je njegov roditelj crven. #ifdef RED_BLACK static void insertFixup(BSTREE T, Node *x) { /* stvara Red-Black ravnotežu nakon umetanja čvora x */ /* podrazumjeva se da je Color(x) = RED; */ while (x != Root(T) && Color(Parent(x)) == RED) { if (Parent(x) == Left(Parent(Parent(x)))) { Node *y = Right(Parent(Parent(x))); if (Color(y) == RED) {/* ujak je RED - slučaj 1 */ Color(Parent(x)) = BLACK; Color(y) = BLACK; Color(Parent(Parent(x))) = RED; x = Parent(Parent(x)); } else { /* ujak je BLACK - slučaj 2*/ if (x == Right(Parent(x))) { /* postavi x za lijevo dijete */ x = Parent(x); rotateLeft(T, x); } /* oboji i rotiraj - slučaj 3 */ Color(Parent(x)) = BLACK; Color(Parent(Parent(x)))= RED; rotateRight(T, Parent(Parent(x))); } } else {/* slika u zrcalu prethodnog slučaja*/ Node *y = Left(Parent(Parent(x))); if (y->color == RED) {/* ujak je RED - slučaj 1 */ Color(Parent(x)) = BLACK;

347

y->color = BLACK; Color(Parent(Parent(x))) = RED; x = Parent(Parent(x)); } else { /* ujak je BLACK - slučaj 2*/ if (x == Left(Parent(x))) { x = Parent(x); rotateRight(T, x); } /* oboji i rotiraj - slučaj 3 */ Color(Parent(x)) = BLACK; Color(Parent(Parent(x))) = RED; rotateLeft(T, Parent(Parent(x))); }

} } Color(Root(T))= BLACK; } #endif

z y D

u

Slučaj 2 A

rotateLeft(u)

B z

x

A

u

y

y

v

A

rotateRight(z)

A

B

B C

v x

rotateLeft(u)

z

u

x

C

u

C

D

v

x

v

B D

z C

Slučaj 3

D

Slučaj 3 v y A Slučaj 2

rotateRight(z)

z x B

u

D C

Slika 20.7 Drugi i treći slučaj analize umetanja čvora x. Sada su nužne jednostruke i dvostruke rotacije. Svako podstablo A,B,C,D i E ima crni korijen i jednaku crnu-visinu. Slovom x je označen čvor koji se analizira unutar while petlje (to je umetnuti čvor), a y označava njegovog lijevog ili desnog ujaka. Uočite da se slučaj 2 i 3 razlikuju od slučaja 1 po boji ujaka. U slučaju 2 vrši se rotacija Parent(x) i dobije se slučaj 3. Zatim se vrši rotacija Parent(Parent(x) u konačni oblik. Vremensku složenosti umetanja čvora procjenjujemo na sljedeći način:

348

1. Funkcija bst_insert() se izvršava u O(lg n) vremena jer se mjesto za umetanje čvora traži u uravnoteženom stablu. 2. U funkciji insertFixup() svaka iteracija uzima O(1) vremena, i pri svakoj iteraciji se pomičemo dvije razine naviše. Pošto ima O(log2n) razina, vrijeme izvršenja insertFixup() je također O(log2n) Ukupno, umetanje u crveno-crno stablo ima vremensku složenost O(log2n). Uravnoteženje stabla nakon brisanja čvora Nešto kompliciraniji zahtjevi za uravnoteženjem stabla se javljaju kada se u funkciji bst_delete() izbriše čvor y. Problem nastaje ako se izbriše crni čvor jer tada može nastati slučaj da su čvor i njegovo dijete crveni. To je u suprotnosti sa svojstvom 3 crveno-crnog stabla. Zbog toga se na kraju funkcije bst_delete(T, y) poziva funkcija deleteFixup(T,x) koja vrši uravnoteženje stabla. Čvor x je jedino dijete izbrisanog čvora y (možda jednak NIL). Unutar ove funkcije se prvo analizira boja od x, sa sljedećim posljedicama: 1. Ako je boja od x crvena, ili ako je x korijen stabla, tada se ne izvršava petlja, već se jedino boja od x promijeni u crnu boju. U tom slučaju nije potrebno analizirati uravnoteženost stabla, jer ako smo izbacili crni čvor (y), sada sada povratili crni čvor (x) Time smo osigurali da je crna-visina nepromijenjena. Pošto smo izbacili i crveni čvor (x) osigurali smo se da neće biti dva crvena čvora u odnosu dijete-roditelj. 2. Ako je boja od x crna, i ako x nije korijen stabla izvršava se petlja u kojoj se analizira uravnoteženost stabla. Ideja je da tražimo uzlaznom stazom,od čvora x, crveni čvor koji bi mogli pretvoriti u crni čvor. Na taj način bi se održala ravnoteža crnih čvorova (jer je prije izbrisan crni čvor y). Slika 20. 8 pokazuje četiri slučaja kod kojih treba izvršiti promjenu boje i/ili izvršiti potrebne rotacije u stablu, pri traženju tog crvenog čvora. Analiziraju se brat od x, to je čvor w, i roditelj od čvora x. Moguće je 8 slučajeva, 4 kad je brat desno od roditelja i 4 kad je brat lijevo od roditelja. Na slici 20.8 prikazana su 4 slučaja kada je brat desno dijete roditelja x. Slučajeve razlikujemo po boji djece od w. Čvor w je zgodan za analizu jer ne može biti NIL, inače bi imali odstupanje od svojstva 5 za čvor Parent(x) koji je i roditelj od w. (Ako je x crne boje, on mora imati brata, jer staza od Parent(x) do lista, na lijevo i na desno mora imati bar jedan crni čvor koji nije NIL).

349

d

b a

x

d

B

A

c

D

b

x

d c

F

w e

D

c

d

F

E

Novi w

C

B

A

F

E

s

a

x

Slučaj 3

e

D

C

b

c

C

s

b

B

A

F

E D

a

E

d

B

c

C

s

a

A

B

Slučaj 2

e

D

b

Novi w

w

c

C

e

Novi x d

B

a

A

F

E

x

s

a

A

b

Slučaj 1

e

C

x

w

D

e

E

b

x A

s w

d

B C

c

s

d

a

e

S'

D

b

Slučaj 4 E

F

A

c

B

e

S'

a

C

F

E D

F Novi x = Root(T)

Slika 20.8 Slučajevi koji se obrađuju u while-petlji funkcije deleteFixup().Crni čvorovi su prikazani tamnije, crveni čvorovi s zadebljanim rubom,a ostali čvorovi, koji mogu biti ili crni ili crveni prikazani su s tanjim rubom i označeni slovima s i s'. Slova A,B,C,.. označavaju podstabla. Petlja se ponavlja jedino u drugom slučaju #ifdef RED_BLACK static void deleteFixup(BSTREE T, Node *x) { /* stvara Red-Black ravnotežu nakon brisanja čvora*/ while (x != Root(T) && Color(x) == BLACK) { if (x == Left(Parent(x))) { Node *w = Right(Parent(x)); if (Color(w) == RED) { /*slučaj 1*/ Color(w) = BLACK; Color(Parent(x)) = RED; rotateLeft(T, Parent(x)); w = Right(Parent(x)); } if (Color(Left(w)) == BLACK && Color(Right(w)) == BLACK) { Color(w) = RED; /* slučaj 2*/

350

x = Parent(x); } else { if (Color(Right(w)) == BLACK) { Color(Left(w)) = BLACK; /*slučaj 3*/ Color(w) = RED; rotateRight(T, w); w = Right(Parent(x)); } Color(w) = Color(Parent(x)); /*slučaj 4*/ Color(Parent(x)) = BLACK; Color(Right(w)) = BLACK; rotateLeft(T, Parent(x)); x = Root(T); } } else {/*slika u zrcalu (zamijeni lijevi desni)*/ Node *w = Left(Parent(x)); if (Color(w) == RED) { Color(w) = BLACK; Color(Parent(x)) = RED; rotateRight(T, Parent(x)); w = Left(Parent(x)); } if (Color(Right(w)) == BLACK && Color(Left(w)) == BLACK) { Color(w) = RED; x = Parent(x); } else { if (Color(Left(w)) == BLACK) { Color(Right(w)) = BLACK; Color(w) = RED; rotateLeft(T, w); w = Left(Parent(x)); } Color(w) = Color(Parent(x)); Color(Parent(x)) = BLACK; Color(Left(w)) = BLACK; rotateRight(T, Parent(x)); x = Root(T); } } } Color(x) = BLACK; } #endif

Slučaj 1: w je crven

o o o

w mora imati crnu djecu (prema svojstvu 5) Postavi boju w crno i Parent(x) crveno (to kasnije može značiti kraj ispitivanja) Lijevo rotiraj Parent(x)

351

o o

Novi brat od x je bio dijete od w prije rotacije, pa mora bit crn Nastavi razmatrati stanje prelaskom na slučajeve 2,3 ili4, u kojima će se, ovisno o položaju Novog w odrediti da li se Parent(x), koji je crven, pretvara u crni čvor.

Slučaj 2: w je crn i oba djeteta od w su crna

[Napomena: Čvor nepoznate boje označen tankim rubom i slovom s] Promijeni boju od w u crveno Pomakni promatranje na viši čvor tako da Parent(x) postane Novi x Ponovi iteraciju. Iteracija prestaje ako je Novi x = Parent(x) crven (to je uvijek slučaj ako smo došli iz slučaja 1) Tada se Novi x konačno oboji u crno. Kraj! Primjetite da je zadržan broj crnih čvorova u stazi od x i u stazi od w. U obje staze najprije je smanjen broj crnih čvorova, a zatim je na kraju dodan crni čvor. o o o

Slučaj 3: w je crn, lijevo dijete od w je crveno, a desno dijete od w je crno

o o o o

Pomijeni boju od w u crveno i boju lijevog djeteta od w u crno Desno rotiraj w Novi w, tj. novi brat od x je crn s crvenim desnim djetetom Proslijedi analizu na slučaj 4

Slučaj 4: w je crn, lijevo dijete od w je crno, a desno dijete od w je crveno

[Napomena: Dva čvora nepoznate boje označeni su slovima s i s'.] o o o o

Postavi boju w jednaku boji Parent(x) Postavi boju Parent(x) crno i boju desnog djeteta od w crno Zatim lijevo rotiraj Parent(x) Pošto je jedan crveni čvor pretvoren u crni (Right(w)), cilj je ispunjen. Završi petlju na način da postaviš da je x jednak Root(T). Kraj!

Vremensku složenost ovih operacija određuje složenost tri procesa:

352

1. Složenost bst_delete() je O(log2n), jer se traženje vrši u zravnoteženom stablu. 2. Složenost deleteFixup() je određena slučajem 2 jer se jedino u njemu može javiti potreba za više iteracije. U svakoj iteraciji se analiza podiže jednu razina više, pa je maksimalno moguće O(log2n) iteracija 3. Slučajevi 1, 3 i 4 imaju samo 1 rotaciju, pa ukupno ne može biti više od 3 rotacije. Ukupna vremenska složenost je O(log2n) i to s malim konstantnim faktorom. Upravo brzina operacije brisanja daje crveno-crnom stablu malu prednost nad drugim metodama uravnoteženja stabla (primjerice, AVL metoda, u najgoram slučaju, kod brisanja vrši O(log2n) rotacija). Zadatak: Napišite specijaliziranu verziju ADT binarnog stabla koja se od ADT BSTREE razlikuje po tome da simboli nisu određeni parom , već samo sadrže ključ. Rezultirajući ADT nazovite imenom MULTISET, čime se aludira na skup podataka koji može sadržavati više istih objekata. Definirajte sve operacije koje postoje kod BST, ali promijenite imena operacija tako da sva imena počinju prefiksom mset_. Testirajte ADT MULTISET na problemu sortiranja niza pomoću stabla traženja. Koristite algoritam za sortiranje stablom koji se obično naziva Treesort, a sastoji se od sljedećih koraka. Problem: Sotiraj niz A koji sadrži od n objekta tipa T, tako da vrijedi A[i] < A[i+1] Algoritam: Inicijaliziraj ADT MULTISET imena Skup, za tip podataka T Ponavljaj za sve elemente niza A[i], i=0,1, n-1 mset_insert(Skup, A[i]) SYMBOL s ← mset_minimum(Skup); Ponavljaj za i=0,1, n-1 A[i] ← mset_symbol_key(s) s ← mset_sucessor(s) Kraj! Primjetite da umetanje jednog podatka u stablo traje O(log2n). Pošto ukupno postoji n podataka, unos u stablo traje O(n log2n). Ispis iz stabla traje O(n). Zaključujemo da sortiranje stablom (treesort) ima vremensku složenost O(n log2n) + O(n) = O(n log2n). Teorijski ovo je isti rezultat kao kod Mergesort ili Heapsort metode, ili kao prosječni slučaj složenosti kod Quicksort metode. U praksi, zbog manjeg konstantnog faktora, preferira se Quicksort metoda.

353

Literatura

[1] American National Standards Institute: American national standard for information systems—Programming language - C, ANSI X3.159-1989, 1989. [2] International Standard ISO/IEC 9899: Programming Language C, 1999. [3] B. W. Kernighan and D. M. Ritchie: The C programming language (2nd ed.), Englewood Cliffs, NJ: Prentice -Hall, 1988. [4] D. M. Ritchie: The development of the C language. Second ACM HOPL Conference, Cambridge, MA. 1993. [5] R. Sedgewick: Algorithms in C, Addison-Wesley, 1998. [6] M. A. Weiss: Data Structures and Algorithm Analysis in C, Addison-Wesley, 1997. [7] T. H. Cormen, C. E. Leiserson and R. L. Rivest: Introduction to Algorithms, MIT Press, 1990. [8] D. E. Knuth: Sorting and Searching, volume 3 of The Art of Computer Programming, Addison-Wesley, 1973. [9] A. V. Aho, J. E. Hopcroft and J. D. Ullman: Data Structures and Algorithms, AddisonWesley, 1983. [10] A. V. Aho, J. E. Hopcroft and J. D. Ullman: Compilers, Addison-Wesley, 1985. [11] E. Horowitz and S. Sahni: Fundamentals of Computer Algorithms, Computer Science Press, 1978. [12] N. Wirth: Algorithms and Data Structures, Prentice Hall, 1986. [13] A. M. Berman: Data Structures via C++, Oxford Press, 1997. [13] W. J. Collins: Data Structures and Standard Template Library, McGraw-Hill, 2003.

354

Dodatak

Dodatak A - Elementi dijagrama toka

355

Dodatak B - Gramatika C jezika Sintaktička i leksička pravila C-jezika su zapisana na sljedeći način: Neterminalni simboli su zapisani kurzivom. Terminalni simboli su zapisani na isti način kao u ciljnom jeziku Opcioni simboli su označeni indeksom opt (Simbolopt ili Simbolopt). Pravila imaju oblik: neterminalni-simbol : niz-terminalnih-i-neterminalnih-simbola Alternativna pravila su zapisana u odvojenim redovima. Iznimno, kada su zapisana u jednom retku, odvojena su okomitom crtom Sintaksa deklaracija i definicija C jezika kompilacijaska-jedinica: definicija-funkcije deklaracija kompilacijaska-jedinica deklaracija kompilacijaska-jedinica definicija-funkcije definicija-funkcije: deklaracija-specifikatoraopt deklarator lista-deklaracijaopt složena-naredba lista-deklaracija: deklaracija lista-deklaracija deklaracija deklaracija: deklaracija-specifikatora lista-init-deklaratoraopt; deklaracija-specifikatora: specifikacija-klase-spremanja deklaracija-specifikatoraopt specifikacija-tipa deklaracija-specifikatoraopt specifikacija-kvalifikacije deklaracija-specifikatoraopt specifikacija-klase-spremanja : auto | register | static | extern | typedef specifikacija-tipa: void | char | short | int | long float | double | signed | unsigned struct-ili-union-specifikator enum-specifikator typedef-ime specifikacija-kvalifikacije: const | volatile struct-ili-union-specifikator: struct-ili-union identifikatoropt { struct-lista-deklaracija } struct-ili-union identifikator struct-ili-union: struct | union struct-lista-deklaracija: struct-deklaracija struct-lista-deklaracija struct-deklaracija lista-init-deklaratora: init-deklarator lista-init-deklaratora, init-deklarator init-deklarator: deklarator deklarator = inicijalizacija struct-deklaracija: lista-specifikatora lista-struct-deklaratora; lista-specifikatora: specifikacija-tipa lista-specifikatoraopt

356

specifikacija-kvalifikacije lista-specifikatoraopt lista-struct-deklaratora: struct-deklarator lista-struct-deklaratora , struct-deklarator struct-deklarator: deklarator deklaratoropt : konstanti-izraz enum-specifikator: enum identifikatoropt { enumerator-lista } enum identifikator enumerator-lista: enumerator enumerator-lista , enumerator enumerator: identifikator identifikator = konstanti-izraz deklarator: pokazivačopt direktni-deklarator direktni-deklarator: identifikator (deklarator) direktni-deklarator [ konstanti-izrazopt ] direktni-deklarator ( parametari-funkcije ) direktni-deklarator ( lista-identifikatoraopt ) pokazivač: * lista-specifikacija-kvalifikacijeopt * lista-specifikacija-kvalifikacijeopt pokazivač lista-specifikacija-kvalifikacije: specifikacija-kvalifikacije lista-specifikacija-kvalifikacije specifikacija-kvalifikacije parametari-funkcije: lista-parametara lista-parametara , ... lista-parametara: deklaracija-parametra lista-parametara , deklaracija-parametra deklaracija-parametra: deklaracija-specifikatora deklarator deklaracija-specifikatora apstraktni-deklaratoropt lista-identifikatora: identifikator lista-identifikatora , identifikator inicijalizacija: izraz-pridjele { lista-inicijalizacije } { lista-inicijalizacije , } lista-inicijalizacije: inicijalizacija lista-inicijalizacije , inicijalizacija ime-tipa: lista-specifikatora apstraktni-deklaratoropt apstraktni-deklarator: pokazivač pokazivačopt direktni-apstraktni-deklarator direktni-apstraktni-deklarator: ( apstraktni-deklarator ) direktni-apstraktni-deklaratoropt [konstanti-izrazopt ] direktni-apstraktni-deklaratoropt ( parametari-funkcijeopt ) typedef-ime: identifikator

357

Sintaksa naredbi naredba: naredbeni izraz složena-naredba naredba-selekcije naredba-iteracije naredba-skoka označena-naredba naredbeni izraz : izrazopt ; složena-naredba : { lista-deklaracijaopt niz-naredbiopt } lista-deklaracija : deklaracija lista-deklaracija deklaracija niz-naredbi: naredba niz-naredbi naredba

naredba-iteracije: while ( izraz ) naredba do naredba while ( izraz ) ; for ( izrazopt ; izrazopt ; izrazopt ) naredba naredba-selekcije: if ( izraz ) naredba if ( izraz ) naredba else naredba switch ( izraz ) naredba naredba-skoka : goto identifikator ; continue; break; return izrazopt ; označena-naredba: identifikator: naredba case konstanti-izraz : naredba default : naredba

Sintaksa izraza primarni-izraz: identifikator konstanta literalni-string (izraz ) postfiks-izraz: primarni-izraz postfiks-izraz [ izraz ] postfiks-izraz ( lista-argumenata-opt ) postfiks-izraz . identifikator postfiks-izraz -> identifikator postfiks-izraz ++ postfiks-izraz – ( ime-tipa ) { lista-inicijalizacije } ( ime-tipa ) { lista-inicijalizacije , } lista-argumenata: izraz-pridjele lista-argumenata , izraz-pridjele unarni-izraz: postfiks-izraz ++ unarni-izraz -- unarni-izraz unarni-operator cast-izraz sizeof unarni-izraz sizeof ( ime-tipa ) unarni-operator: & | * | + | - | ~ | ! cast-izraz: unarni-izraz ( ime-tipa ) cast-izraz multiplikativni-izrazr: cast-izraz multiplikativni-izrazr * cast-izraz multiplikativni-izrazr / cast-izraz multiplikativni-izrazr % cast-izraz aditivni-izraz: multiplikativni-izraz aditivni-izraz + multiplikativni-izraz aditivni-izraz - multiplikativni-izraz posmačni-izraz: aditivni-izraz posmačni-izraz > aditivni-izraz

relacijski-izraz: posmačni-izraz relacijski-izraz < posmačni-izraz relacijski-izraz > posmačni-izraz relacijski-izraz = posmačni-izraz izraz-jednakosti: relacijski-izraz izraz-jednakosti == relacijski-izraz izraz-jednakosti != relacijski-izraz AND-izraz: izraz-jednakosti AND-izraz & izraz-jednakosti XOR-izraz: AND-izraz XOR-izraz ^ AND-izraz OR-izraz: XOR-izraz OR-izraz | XOR-izraz logički-AND-izraz: OR-izraz logički-AND-izraz && OR-izraz logički-OR-izraz: logički-AND-izraz logički-OR-izraz || logički-AND-izraz uvjetni-izraz: logički-OR-izraz logički-OR-izraz ? expr : uvjetni-izraz izraz-pridjele: uvjetni-izraz unarni-izraz operator-pridjele izraz-pridjele operator-pridjele: = | *= | /= | %= | += | -= | = | &= | ^= | |= izraz: izraz-pridjele izraz , izraz-pridjele konstantni-izraz: uvjetni-izraz

358

Regularna gramatika za zapis literalnih konstanti konstanta: integer-konstanta float-konstanta char-konstanta enum-konstanta literalni-string integer-konstanta: 0 decimalna-konstanta int-sufiksopt oktalna-konstanta int-sufiksopt hexadecimalna-konstanta int-sufiksopt decimalna-konstanta: nenulta-znamenka decimalna-konstanta znamenka oktalna-konstanta: 0oktalna_znamenka oktalna-konstanta oktalna-namenka heksa-konstanta: 0xheksa-znamenka 0Xheksa-znamenka heksa-konstanta heksa-znamenka int-sufiks: unsigned-sufiks long-sufiksopt long-sufiks unsigned-sufiksopt unsigned-sufiks: u | U long-sufiks: l | L float-konstanta: floating-konstanta float-sufiksopt float-sufiks: L | l | F | f floating-konstanta: frakciona-konstanta exponent frakciona-konstanta niz-znamenki eksponent frakciona-konstanta: niz-znamenki.niz-znamenki .niz-znamenki niz-znamenki. eksponent: e predznak niz-znamenki E predznak niz-znamenki e niz-znamenki E niz-znamenki predznak:

+|-

nenulta-znamenka: 1|2|3|4|5|6|7|8|9 znamenka : 0|nenulta-znamenka heksa-znamenka: znamenka |A|B|C|D|E|F|a|b|c|d|e|f oktalna-znamenka : 0|1|2|3|4|5|6|7 niz-znamenki: znamenka niz-znamenki znamenka char-konstanta: 'char' L 'char' char: reg-char escape-sequence reg-char: bilo koji ASCII znak osim apostrofa ('), backslash(\) i znaka za novu liniju. escape-sekvenca: \' | \" | \\ | \dd | \ddd | \xd | \xddd | \a | \b | \n | \r | \t (d je znamenka) enum-konstanta: identifikator

| | | |

\d \xdd \f \v

literalni_string: "" "char-sekvenca" L"char-sekvenca" char-sekvenca: char char-sekvenca char Napomena: Znak navodnika(") se ne može direktno unositi u string, već kao escape-sekvenca \". Prefiks L označava prošireni znakove (wchar_t)

359

Klučne riječi C - jezika auto break case char const continue default do

double else enum extern float for goto if

int long register return short signed sizeof static

struct switch typedef union unsigned void volatile while

Tip operatora

Operator

Asocijativnost

primarni postfiks –unarni prefiks – unarni multiplikativni aditivni posmačni relacijski jednakost bitznačajni "i" ekskluzivni "ili" bitznačajni "ili" logički "i" logički "ili" ternarni uvjetni op. pridjela vrijednosti zarez

[] . -> () ++ -++ -- & * + - ~ ! sizeof cast */% +> < > = == != & ^ | && || ?: = *= /= %= += -= = &= ^= |= ,

s lijeva na desno s lijeva na desno s desna na lijevo s lijeva na desno s lijeva na desno s lijeva na desno s lijeva na desno s lijeva na desno s lijeva na desno s lijeva na desno s lijeva na desno s lijeva na desno s lijeva na desno s desna na lijevo s desna na lijevo s lijeva na desno

Prioritet i asocijativnost operatora

360

Dodatak C - Standardna biblioteka C jezika Standardna biblioteka C jezika definirana je dokumentom: American national standard for information systems - Programming language C, ANSI X3.159-1989, American National Standards Institute. 1989. Sadrži niz korisnih funkcija, konstanti (makro naredbi), typedef tipova i globalnih varijabli (pr. stdin, stdout). Deklaracija tih objekata zapisana je u datotekama koje se nazivaju zaglavlja ili h-datoteke (eng. headers), a koriste se kao početne #include datoteke u gotovo svakom izvornom programu. Funkcionalna podjela temeljne definicije rad s ulazno izlaznim uređajima i datotekama radnje s znakovima i stringovima alociranje memorije matematičke funkcije datum i vrijeme promjenljivi broj argumenata globalni skokovi asertacija prihvat signala stanja računalnog procesa prihvat i dojava pogreške minimalne i maksimalne vrijednosti tipova lokalizacija znakovnih zapisa sučelje s operativnim sustavom

h-datoteke (zaglavlja) , , , , , , , , ,

Slijedi opis funkcija definiranih standardom. Za svaku funkciju navodi se deklaracija, opis argumenata funkcije i tip vrijednosti koju funkcija vraća.

C 1 U zaglavlju , definiran je niz funkcija za rad s ulazno/izlaznim tokovima. Svakom toku je pridijeljen pokazivač na strukturu FILE koja je također definirana u . Standardni ulaz, standardni izlaz i standardni tok dojave greške se automatski inicijaliziraju pri pokretanju programa, a njihov pokazivač na strukturu FILE je u globalnim varijablama: FILE *stdin; FILE *stdout; FILE *stderr;

/* pokazivač toka standardnog ulaza */ /* pokazivač toka standardnog izlaza */ /* pokazivač toka dojave greške */

Iniciranje pokazivača datotečnih tokova vrši se pomoću funkcije fopen(). Kada se završi rad s datotekom treba zatvoriti njen tok pomoću funkcije fclose(). U zaglavlju definirana je i simbolička konstanta EOF koja služi kao oznaka kraja datoteke. Otvaranje i zatvaranje toka

fopen FILE *fopen(const char *name, const char *mode);

361

fopen() otvara tok za datoteku imena name. Ako je operacija uspješna, funkcija vraća pokazivač toka, a ako je neuspješna tada vraća NULL. String mode označava način na koji će

biti otvorena datoteka. Prvi znak tog stringa mora biti 'r', 'w', ili 'a', što označava da se datoteka otvara za čitanje, pisanje ili dopunjavanje. Ako se nakon prvog znaka napiše znak '+' tada se datoteka otvara za čitanje i pisanje. Dodatno se može napisati i znak 'b'. To je oznaka da se na MS-DOS sustavima datoteka tretira kao binarna datoteka. U suprotnom datoteka se otvara u tekstualnom modu. MS-DOS /Windows sustavi znak za kraj retka '\n' u tekstualnoj datoteci pretvaraju u dva znaka '\r\n', također se znak Ctrl-Z (ASCII vrijednost 26) tretira kao oznaka kraja datoteke. Ove pretvorbe ne postoje na Unix sustavima.

freopen FILE *freopen(const char *name, const char *mode, FILE *fp); freopen() služi kao i fopen(), ali na način da se datoteka imena name poveže s postojećim pokazivačem toka fp. Tipično se koristi za redirekciju sa standardnih tokova,

primjerice, naredbom freopen("output.log", "w", stdout);

budući izlaz, koji generiraju funkcije koje djeluju na stdin (primjerice putchar i printf), bit će preusmjeren u datoteku output.log.

fclose int fclose(FILE *fp);

Funkcija fclose() zatvara tok fp. U slučaju greške vraća EOF, inače vraća 0. Funkcija fclose() se automatski primjenjuje na sve otvorene tokove pri normalnom završetku programa.

fflush int fflush(FILE *fp);

Ako je otvoren tok fp, funkcijom fflush() svi se podaci iz privremenog datotečnog spremnika zapišu u datoteku. Funkcija vraća 0, ili EOF ako nastupi greška. Ulazno izlazne opracije

getchar, getc, fgetc int getchar(); int getc(FILE *fp); int fgetc(FILE *fp); Funkcija getc() vraća sljedeći dobavljivi znak iz toka fp, ili EOF ako je kraj datoteke. Obično EOF ima vrijednost -1, kako bi se razlikovao od ostalih znakova. Zbog toga ova funkcija vraća tip int, a ne tip char. Funkcija getchar() je ekvivalentna getc(stdin). Funkcija fgetc() je ekvivalentna getc(), ali može biti implementirana kao makro naredba.

putchar, putc, fputc int putchar(int c);

362

int putc(int c, FILE *fp); int fputc(int c, FILE *fp);

Funkcija putc() upisuje znak c u tok fp. Funkcija putchar(c) je ekvivalentna putc(c, stdout). Funkcija fputc() je ekvivalentna putc(),ali može biti implementirana kao makro naredba. Sve tri funkcije vraćaju upisani znak, ili EOF u slučaju greške.

printf, fprintf int printf(const char *format, ...); int fprintf(FILE *fp, const char *format, ...);

Funkcija fprintf() upisuje tekstualno formatirane argumente u tok fp. Tri točkice označavaju listu argumenata. Format se određuje u stringu format, koji sadrži znakove koji se direktno upisuju na izlazni tok i specifikatore formatiranog ispisa argumenata. printf(format, …) je ekvivalentno fprintf(stdout, format, …).

Specifikator formata se sastoji od znaka %, iza kojeg slijede oznake za širinu i preciznost ispisa te tip argumenta, u sljedećem obliku: %[prefiks][širina_ispisa][. preciznost][veličina_tipa]tip_argumenta Specifikator formata mora započeti znakom % i završiti s oznakom tipa argumenta. Sva ostala polja su opciona (zbog toga su napisana unutar uglatih zagrada). U polje širina_ispisa zadaje se minimalni broj kolona predviđenih za ispis vrijednosti. Ako ispis sadrži manji broj znakova od zadane širine ispisa, na prazna mjesta se ispisuje razmak. Ako ispis sadrži veći broj znakova od zadane širine, ispis se proširuje. Ako se u ovo polje upiše znak * to znači da će se broj kolona indirektno očitati iz slijedećeg argumenta funkcije, koji mora biti tipa int. Polje prefiks može sadržavati jedan znak koji ima sljedeće značenje: -

Ispis se poravnava prema lijevoj granici ispisa određenog poljem širina_ispisa. (inače se poravnava s desne strane) U prazna mjesta se upisuje razmak + Pozitivnim se vrijednostima ispisuje i '+' predznak. razmak Ako je vrijednost positivna, dodaje se razmak prije ispisa (tako se može poravnati kolone s pozitivnim i negativnim brojevima). 0 Mjesta razmaka ispunjuju se znakom 0. # Alternativni stil formatiranja

Polje . preciznost određuje broj decimalnih znamenki iza decimalne točke kod ispisa realnog broja ili minimalni broj znamenki ispisa cijelog broja ili maksimalni broj znakova koji se ispisuje iz nekog stringa. Ovo polje mora započeti znakom točke, a iza nje se navodi broj ili znak *, koji znači da će se preciznost očitati iz slijedećeg argumenta tipa int. Ukoliko se ovo polje ne koristi, tada se podrazumijeva da će realni brojevi biti ispisani s maksimalno šest decimalnih znamenki iza decimalne točke. Polje tip_argumenta može sadržavati samo jedan od sljedećih znakova znakova: c d, i e, E f

Argument se tretira kao int koji se ispisuje kao znak iz ASCII skupa. Argument se tretira kao int, a ispisuje se decimalnim znamenkama. Argument je float ili double, a ispis je u eksponentnom formatu. Argument je float ili double, a ispis je prostom decimalnom formatu. Ako je prefiks # i

363

g, G o p s u x, X % n

preciznost .0, tada se ne ispisuje decimalna točka. Argument je float ili double, a ispis je prostom decimalnom formatu ili u eksponencijalnom formatu, ovisno o tome koji daje precizniji ispis u istoj širini ispisa. Argument je unsigned int, a ispisuje se oktalnim znamenkama. Argument se tretira kao pokazivač tipa void *, pa se na ovaj način može ispisati adresa bilo koje varijable. Adresa se obično ispisuje kao heksadecimalni broj. Argument mora biti literalni string odnosno pokazivač tipa char *. Argument je unsigned int, a ispisuje se decimalnim znamenkama. Argument je unsigned int, a ispisuje se heksadecimalnim znamenkama. Ako se zada prefiks # , ispred heksadecimalnih znamenki se ispisuje 0x ili 0X. Ne odnosi se na argument, već znači ispis znaka % Ništa se ne ispisuje, a odgovarajući argument mora biti pokazivač na int, u kojega se upisuje broj do tada ispisanih znakova

Polje veličina_tipa može sadržavati samo jedan znak koji se upisuje neposredno ispred oznake tipa. h

Pripadni argument tipa int tretira se kao short int ili unsigned short int.

l

Pripadni argument je long int ili unsigned long int.

L

Pripadni argument realnog tipa je long double.

Funkcije printf() i fprintf() vraćaju broj ispisanih znakova. Ako nastupi greška, vraćaju negativan broj.

scanf, fscanf int scanf(const char *format, ...); int fscanf(FILE *fp, const char *format, ...);

Funkcijom fscanf() dobavljaju se vrijednosti iz tekstualnog toka fp po pravilima pretvorbe koji su određeni u stringu format. Te vrijednosti se zapisuju na memorijske lokacije određene listom pokazivačkih argumenata. Lista argumenata (...) je niz izraza odvojenih zarezom, čija vrijednost predstavlja adresu postojećeg memorijskog objekta. Tip objekta mora odgovarati specifikaciji tipa prethodno zapisanog u stringu format. scanf(format, …) je ekvivalentno fscanf(stdin, format, …).

String format se formira od proizvoljnih znakova i specifikatora formata oblika: %[prefiks][širina_ispisa][veličina_tipa]tip_argumenta Ako se pored specifikatora formata navedu i proizvoljni znakovi tada se očekuje da i oni budu prisutni u ulaznom tekstu (osim tzv. bijelih znakova: razmak, tab i nova linija) . Osim u slučaju specifikatora %c, %n, %[, i %% , za sve pretvorbe se podrazumijeva da se iz ulaznog teksta odstrane bijeli znakovi. Jedini opcioni prefiks je znak '*' koji znači da se pretvorena vrijednost ne dodjeljuje ni jednom argumentu. U polju širina_ispisa zadaje se maksimalni broj kolona predviđenih za dobavu vrijednosti.

364

Polje veličina_tipa može sadržavati samo jedan znak koji se upisuje neposredno ispred oznake tipa pokazivačkog argumenta. To su sljedeći znakovi: h

Pripadni argument tipa int tretira se kao short int ili unsigned short int.

l

Pripadni argument je long int ili unsigned long int.

L

Pripadni argument realnog tipa je long double.

Polje tip_argumenta može sadržavati samo jedan od sljedećih znakova: c

Dobavlja se jedan znak, a argument je tipa char *. Ako je definirano polje širina_unosa, tada se unosi onoliko znakova kolika je vrijednost širina_unosa. d Dobavlja se cijeli broj, a argument je tipa int * ( ili short int * ili long int *, ako je zadana veličina tipa h ili l). i Dobavlja se cijeli broj, a argument je tipa int * ( ili short int * ili long int *, ako je zadana veličina tipa h ili l). Pretvorba se vrši i ako je broj zapisan oktalno (počinje znamenkom 0) ili heksadecimalno (počinje s 0x ili 0X), e, E, f, Dobavlja se realni broj. Argument je tipa float * ili double *. g, G o Dobavlja se cijeli broj zapisan oktalnim znamenkama. Argument je tipa unsigned int* (ili unsigned short int * ili unsigned long int *, ako je zadana veličina tipa h ili l). p Dobavlja se pokazivač, a argument mora biti tipa void **. s Dobavlja se string koji ne sadrži bijele znakove. Argument mora biti tipa char *. u Dobavlja se cijeli broj, a argument je tipa unsigned int * ( ili unsigned short int * ili unsigned long int *, ako je zadana veličina tipa h ili l). x, X Dobavlja se cijeli broj zapisan heksadecimalno. Argument je tipa unsigned int* Argument je tipa unsigned int* (ili unsigned short int * ili unsigned long int *, ako je zadana veličina tipa h ili l). % Ne odnosi se na argument, već znači dobavu znaka % n Ne vrši se pretvorba. Odgovarajući argument mora biti tipa int *. Upisuje se broj do tada učitanih znakova [ Za format oblika %[...] dobavlja se string koji sadrži znakove koji su unutar zagrada, prema sljedećem obrascu: %[abc] znači da se dobavlja string koji može sadržavati znakove a,b ili c. %[a-d] znači da se dobavlja string koji može sadržavati znakove a,b,c ili d. %[^abc] znači da se dobavlja string koji može sadržavati sve znakove osim a,b ili c. %[^a-d] znači da se dobavlja string koji može sadržavati znakove sve znakove osim a,b,c ili d. Argument je tipa char *.

Funkcije scanf()i fscanf() vraćaju broj uspješno izvršenih pretvorbi (bez %n i %*). Ako se ne izvrši ni jedna konverzija, zbog dosegnutog kraja datoteke, vraća se vrijednost EOF.

gets, fgets char *fgets(char *buf, int n, FILE *fp); char *gets(char *buf);

Funkcije fgets() čita liniju teksta (koja završava znakom '\n') iz toka fp, i sprema taj tekst (uključujući '\n' i zaključni znak '\0') u znakovni niz buf. Veličina tog niza je n znakova. Ako u liniji ima više od n-2 znakova, tada neće biti dobavljeni svi znakovi. Funkcije gets() je ekvivalentna funkciji fgets(), ali isključivo služi za dobavu stringa s tipkovnice.

365

Ona ne prenosi znak nove linije i ne vrši kontrolu broja unesenih znakova. Obje funkcije vraćaju pokazivač na dobavljeni string ili NULL ako je greška ili kraj datoteke.

puts, fputs int puts(char *str); int fputs(char *str, FILE *fp); Funkcija fputs() ispisuje string str u tok fp. Funkcija puts() ispisuje string str na stdout i dodaje znak nove linije. Obje funkcije vraćaju pozitivnu vrijednost ili EOF ako

nastane greška.

fread size_t fread(void *buf, size_t elsize, size_t n, FILE *fp);

Funkcija fread() iz toka fp čita blok veličine n x elsize bajta i upisuje u spremnik buf. Za dobavu n elemenata nekog tipa, elsize treba biti jednak sizeof(tip elementa). Kod čitanja znakova je elsize = 1. Funkcija vraća broj dobavljenih elemenata ili EOF u slučaju greške.

fwrite size_t fwrite(void *buf, size_t elsize, size_t n, FILE *fp);

Funkcija fwrite() upisuje u tok fp blok veličine n x elsize bajta iz spremnika buf. Za upis n elemenata nekog tipa, elsize treba biti jednak sizeof(tip elementa). Kod upisa n znakova elsize = 1. Funkcija vraća broj upisanih elemenata ili EOF u slučaju greške.

ungetc int ungetc(int c, FILE *fp);

Funkcija ungetc() umeće u tok fp znak c, tako da će pri sljedećem čitanju taj znak biti prvi očitan. Ova operacija se može obaviti samo nakon operacije čitanja iz toka. Funkcija vraća znak c ili EOF u slučaju greške. Pozicioniranje toka Mjesto na kojem se vrši čitanje/pisanje u tok naziva se trenutna pozicija toka (eng. stream position). U radu s binarnim datotekama može se kontrolirati i postavljati trenutnu poziciju toka.

ftell long int ftell(FILE *fp);

Funkcija ftell() vraća long int vrijednost koja je jednaka trenutnoj poziciji toka fp (kod binarnih datoteka to je pomak u bajtima od početka datoteke). Ako se radi s velikom datotekama, u kojima pozicija može biti veće od long int, tada treba koristiti funkciju fgetpos().

fseek, rewind int fseek(FILE *fp, long int pos, int from); void rewind(FILE *fp);

366

Funkcija fseek() postavlja trenutnu poziciju toka fp na poziciju pos, relativno prema vrijednosti from argumenta, koji može imati tri vrijednosti: SEEK_SET (početak datoteke) , SEEK_CUR (trenutna pozicija) i SEEK_END (kraj datoteke). Argument pos može biti negativna vrijednost. Funkcija vraća 0, ako je operacija uspješna. Ako je argument from jednak SEEK_SET, nova_pozicija = pos; Ako je argument from jednak SEEK_CUR, nova_pozicija = trenutna_pozicija + pos; Ako je argument from jednak SEEK_END nova_pozicija = pozicija_kraja_datoteke + pos; Nova pozicija mora biti veća ili jednaka nuli, i može biti veća od trenutne pozicije. Funkcija rewind(fp) postavlja poziciju na 0, što je ekvivalentno fseek(fp,0,SEEK_SET). Ovu funkciju se može koristiti i s tekstualnim datotekama.

fgetpos, fsetpos int fgetpos(FILE *fp, fpos_t *pos); int fsetpos(FILE *fp, const fpos_t *pos);

Funkcija fgetpos() zapisuje trenutnu poziciju toka fp u fpos_t objekt na kojega pokazuje pos. Funkcija fsetpos() postavlja trenutnu poziciju toka fp na vrijednost fpos_t objekta na kojeg pokazuje pos (to mora biti vrijednost prethodno dobivena funkcijom fgetpos). Obje funkcije vraćaju 0 ako je operacija uspješna. Kontrola ulazno/izlaznog spremnika

setbuf, setvbuf void setbuf(FILE *fp, char *buf); void setvbuf(FILE *fp, char *buf, int mode, size_t size);

Pomoću ovih funkcija postavlja se korisnički definirani spremnik buf kao spremnik ulazno/izlaznih operacija. Primjenjuju se prije poziva ulazno/izlaznih operacija na otvoreni tok fp. Kod funkcije setbuf() veličina spremnika mora biti jednaka BUFSIZE (definiran u stdio.h). Ako je buf==NULL tada se ne koristi spremnik. Kod funkcije setvbuf() veličina spremnika se postavlja argumentom size, a način korištenja spremnika se postavlja argumentom mode, koji može imati tri predefinirane vrijednosti: _IONBF _IOFBF _IOLBF

Ne koristi se spremnik, već se vrši neposredan pristup datoteci. Spremnik se koristi potpuno, tj. spremnik se prazni tek kada je popunjen. Spremnik se koristi djelomično (uvijek se prazni kada se ispisuje znak nove linije '\n' ).

Dojava i prihvat greške

feof int feof(FILE *fp);

Funkcija feof() vraća nenultu vrijednost ako je tok u poziciji kraj datoteke, inače vraća 0.

367

ferror int ferror(FILE *fp);

Funkcija ferror() vraća nenultu vrijednost ako je nastala greška u radu s tokom, inače vraća 0.

clearerr void clearerr(FILE *fp);

Funkcija clearerr() briše indikatore greške ili kraj datoteke za tok fp.

perror void perror(const char *prefix);

Funkcija perror() ispisuje poruku na stderr o trenutno nastaloj greški. Tip greške se bilježi u globalnoj varijabli errno. Poruka je ista kao i poruka koja se dobije pozivom funkcije strerror(errno). Drugim riječima, perror(p) je otprilike ekvivalentna pozivu fprintf(stderr, "%s: %s", p == NULL ? "" : p, strerror(errno));

Argument prefix je proizvoljni string koji se ispisuje ispred poruke. Operacije s formatiranim stringovima

sprintf, sscanf int sprintf(char *buf, const char *format, ...); int sscanf(const char *buf, const char *format, ...); Funkcije sprintf() i sscanf() su varijante od printf() i scanf() koje umjesto ulazno/izlaznog toka koriste proizvoljno odabrani string buf. Znak kraja stringa '\0' se tretira kao znak za kraj datoteke. Korisnik mora voditi računa o tome da veličina stringa buf bude dovoljno velika, da se može izvršiti sve pretvorbe formata u sprintf() funkciji.

Operacije s promjenjljivom listom argumenata

vprintf, vfprintf, vsprintf int vprintf(const char *format, va_list argp); int vfprintf(FILE *fp, const char *format, va_list argp); int vsprintf(char *buf, const char *format, va_list argp);

Ove funkcije omogućuju definiranje funkcija s promjenljivim brojem argumenata, koje imaju funkcionalnost kao printf(), fprintf() i sprintf() funkcije. Posljednji argument ovih funkcija argp je pokazivač tipa va_list. To omogućuje rad s promjenljivim brojem argumenata. U sljedećem programu definirana je funkcija za dojavu greške, koju se može koristiti za dojavu greške pri analizi izvornog programa. Ona koristi dvije globalne varijable koje sadrže ime datoteke (filename) i broj linije izvornog programa (lineno). U dojavi greške se uvijek prije izvještaja o tipu greške ispisuje to ime i broj linije. #include

368

#include extern char *filename; extern int lineno;

/* current input file name */ /* current input line number */

void error(char *msg,...) { va_list argp; va_start(argp, msg); fprintf(stderr, "%s:, line %d: ",filename, lineno); vfprintf(stderr, msg, argp); fprintf(stderr, "\n"); va_end(argp); }

Manipuliranje s datotekama

rename int rename(const char *origname, const char *newname);

Funkcija rename() vrši promjenu imena datoteke origname, operacija uspješna funkcija vraća 0, inaće vraća nenultu vrijednost.

u ime newname. Ako je

remove int remove(const char *name);

Funkcija remove() briše datoteku imena name. Ako je operacija uspješna funkcija vraća 0, inaće vraća nenultu vrijednost.

tmpfile, tmpnam FILE *tmpfile(void); char *tmpnam(char *buf);

Funkcija tmpfile() stvara privremenu datoteku i otvara je u "wb+" modu. Po izlasku iz programa ova se datoteka automatski briše. Funkcija tmpnam() generira jedinstveno ime u string buf, koji mora biti duljine L_tmpnam (predefinirana konstanta). To se ime može koristiti za stvaranje datoteke. Ako je buf==0, ime se generira u internom statičkom spremniku. Funkcija vraća pokazivač na taj spremnik.

C 2 Funkcije koje su deklarirane u uglavnom služe za rad s ASCIIZ stringovima. Pored njih, definirano je nekoliko funkcija, čije ime počinje s mem, za rad s memorijskim blokovima (nizovima bajta). Rad sa stringovima size_t strlen(const char *s)

Vraća duljinu stringa s. char *strcpy(char *s, const char *t)

Kopira string t u string s, uključujući '\0'; vraća s.

369

char *strncpy(char *s, const char *t, size_t n)

Kopira najviše n znakova stringa t u s; vraća s. Dopunja string s sa znakovima '\0', ako t ima manje od n znakova. Napomena: ako u stringu t ima n ili više znakova, tada string s neće biti zaključen s '\0'. char *strcat(char *s, const char *t)

Dodaje string t na kraj stringa s; vraća s. char *strncat(char *s, const char *t, size_t n)

Dodaje najviše n znakova stringa t na string s, i znak '\0'; vraća s. int strcmp(const char *s, const char *t)

Uspoređuje string s sa stringom t, vraća t. Usporedba je leksikografska, prema ASCII rasporedu. int strncmp(const char *s, const char *t, size_t n)

Uspoređuje najviše n znakova stringa s sa stringom t; vraća t. int strcoll(const char *s, const char *t);

Uspoređuje dva stringa s1 and s2, poput strcmp(), ali se usporedba vrši prema multinacionalnom znakovnom rasporedu (koji je određen konstantom LC_COLLATE ). Vraća t. size_t strxfrm(char *s, const char *t, size_t n);

Stvara modificiranu kopiju n znakova stringa t u stringu s (uključujući '\0') , tako da strcmp(s,t) daje istu ocjenu kao i strcoll(s,n) na originalnom stringu. Vraća broj znakova u stringu s. char *strchr(const char *s, int c)

Vraća pokazivač na prvu pojavnost znaka c u stringu s, ili NULL znak ako c nije sadržan u stringu s. char *strrchr(const char *s, int c)

Vraća pokazivač na zadnju pojavnost znaka c u stringu s, ili NULL znak ako c nije sadržan u stringu s. char *strstr(const char *s, const char *t)

Vraća pokazivač na prvu pojavu stringa t u stringu s, ili NULL ako string s ne sadrži string t. size_t strspn(const char *s, const char *t)

Vraća duljinu prefiksa stringa s koji sadrži znakove koji čine string t. size_t strcspn(const char *s, const char *t)

Vraća duljinu prefiksa stringa s koji sadrži znakove koji nisu prisutni u stringu t.

370

char *strpbrk(const char *s, const char *t)

Vraća pokazivač na prvu pojavu bilo kojeg znaka iz string t u stringu s, ili NULL ako nije prisutan ni jedan znak iz string t u stringu s. char *strerror(int n)

Vraća pokazivač na string koji se interno generira, a služi za dojavu greške u nekim sistemskim operacijama. Argument je obično globalna varijabla errno, čiju vrijednost određuje izvršenje funkcija iz standardne biblioteke. char *strtok(char *s, const char *sep) strtok() je funkcija

kojom se može izvršiti razlaganje stringa na niz leksema koji su razdvojeni znakovima-separatorima. Skup znakova-separatora se zadaje u stringu sep. Funkcija vraća pokazivač na leksem ili NULL ako nema leksema. Korištenje funkcije strtok() je specifično jer u stringu može biti više leksema, a ona vraća pokazivač na jedan leksem. Da bi se dobili slijedeći leksemi treba iznova pozvati ovu funkciju, ali s prvim argumentom jednakim NULL. Primjerice, za string char *s = "Prvi

drugi,treci";

ako odaberemo znakove separatore: razmak, tab i zarez, tada sljedeći iskazi daju ispis tri leksema (Prvi drugi i treci): char *leksem = strtoken(s, " ,\t"); while( leksem != NULL) { printf("", leksem); lexem = strtok(NULL," ,\t"); }

/* /* /* /* /*

dobavi prvi leksem */ ukoliko postoji */ ispiši ga i */ dobavi sljedeći leksem */ pa ponovi postupak */

Operacije s memorijskim blokovima (nizovima)

memcpy, memmove void *memcpy(void *dest, const void *src, size_t n); void *memmove(void *dest, const void *src, size_t n);

Ove funkcije kopiraju točno n bajta s lokacije na koju pokazuje src na lokaciju koju pokazuje dest. Ukoliko se blokovi preklapaju tada treba koristiti funkciju memmove(). Funkcije vraćaju pokazivač dest.

memcmp int memcmp(const void *p1, const void *p2, size_t n);

Uspoređuje točno n znakova s lokacija na koje pokazuju p1 i p2, na isti način kao strcnmp(), ali se usporedba ne prekida ako je dostignut znak '\0'.

memchr void *memchr(const void *p, int c, size_t n);

371

Traži prvu pojavu znaka c u n znakova bloka na koji pokazuje p. Vraća pokazivač na pronađeni znak ili NULL ako znak nije pronađen.

memset void *memset(void *p, int c, size_t n);

Postavlja n bajta bloka na koji pokazuje p na vrijednost znaka c, i vraća p.

C 3 Funkcije iz omogućuju klasifikaciju znakova te pretvorbu velikih u mala slova i obratno. Klasifikacija znakova int isupper(int c);

vraća vrijednost različitu od nule ako je znak c veliko slovo, inače vraća 0. int islower(int c);

vraća vrijednost različitu od nule ako je znak c malo slovo, inače vraća 0. int isalpha(int c);

vraća vrijednost različitu od nule ako je znak c veliko ili malo slovo, inače vraća 0. int iscntrl(int c);

vraća vrijednost različitu od nule ako je znak c kontrolni znak, inače vraća 0. int isalnum(int c);

vraća vrijednost različitu od nule ako je znak c slovo ili znamenka, inače vraća 0. int isdigit(int c);

vraća vrijednost različitu od nule ako je znak c decimalna znamenka, inače vraća 0. int isxdigit(int c);

vraća vrijednost različitu od nule ako je znak c heksadecimalna znamanka, inače vraća 0. int isgraph(int c);

vraća vrijednost različitu od nule ako je znak c tiskani znak osim razmaka, inače vraća 0. int isprint(int c);

vraća vrijednost različitu od nule ako je znak c tiskani znak uključujući razmak, inače vraća 0. int ispunct(int c);

vraća vrijednost različitu od nule ako je znak c tiskani znak osim razmaka, slova ili znamanke, inače vraća 0.

372

int isspace(int c);

vraća vrijednost različitu od nule ako je znak c razmak, tab, vert. tab, nova linija, povrat ili nova stranica, inače vraća 0. Pretvorba znaka int toupper(int c); int tolower(int c);

Funkcija toupper() pretvara malo slovo u ekvivalentno veliko slovo, ostala slova ostaju nepromijenjena. Slično, tolower() pretvara veliko slovo u ekvivalentno malo slovo.

C 4 U zaglavlju definirano je nekoliko temeljnih funkcija za alokaciju memorije, pretvorbu stringa u brojeve, manipuliranje s multibajtnim znakovnim skupom, itd. Alokacija memorije

malloc, calloc void *malloc(size_t n); void *calloc(size_t n, size_t elsize);

Funkcija malloc() alocira se n bajta slobodne memorije. Ako je alociranje uspješno funkcija vraća pokazivač na tu memoriju, u suprotnom vraća NULL pokazivač. Primjerice, naredbom double *dp = malloc(10 * sizeof(double));

dobije se pokazivač dp, koji pokazuje na niz od 10 elemenata tipa double. Funkcija calloc(n, elsize) je ekvivalentna malloc(n * elsize), uz dodatni uvjet da calloc() inicijalizira sve bitove alocirane memorije na vrijednost nula.

free void free(void *p);

Funkcija free() prima kao argument pokazivač p. Uz pretpostavku da p pokazuje na memoriju koja je prethodno alocirana funkcijom malloc(), calloc() ili realloc(), ova funkcija dealocira tu memoriju.

realloc void *realloc(void *oldptr, size_t newsize);

Funkcija realloc() vrši promjenu veličine prethodno alocirane memorije, koja je pridijeljena pokazivaču ptr, na veličinu newsize. Funkcija realloc() vraća pokazivač na tu memoriju. Vrijednost toga pokazivača može biti ista kao i vrijednost od ptr, ako memorijski alokator može prilagoditi veličinu zahtijevanog području slobodne memorije veličini newsize. Ukoliko se to ne može ostvariti funkcija realloc() alocira novo područje memorije pa u njega kopira i zatim oslobađa dio memorije na koju pokazuje ptr. Ukoliko se ne može izvršiti alokacija memorije funkcija realloc() vraća NULL. Napomena: poziv realloc(p, 0) je ekvivalentan pozivu free(p), a poziv realloc(0, n) je ekvivalentan pozivu malloc(n).

373

Pretvorba stringa u numeričku vrijednost

atoi, atol, strtol, strtoul int atoi(const char *s); long int atol(const char *s); long int strtol(const char *s, char **endp, int baza); unsigned long int strtoul(const char *s, char **endp, int baza);

Ove funkcije pretvaraju numerički string s u odgovarajuću numeričku vrijednost. Funkcija strtol() u stringu s preskače bijele znakove, pretvara s u broj i vraća vrijednost tipa long int. Ona omogućuje pretvorbu iz sustava različite baze. Ako je baza 10 tada se iz ulaznog stringa s prihvaćaju znakovi od 0 do 10, a ako je baza 16 prihvaćaju se i znakovi a-f, AF. Ako je baza manja od 10 prihvaćaju se znakovi od 0 do baza-1. Ako je baza 0, tada se koristi pravilo da oktalni brojevi počinju s nulom, a heksadecimalni s 0x ili 0X. atoi(s) je ekvivalntno strtol(s,NULL,0);

Argument endp ja pokazivač na pokazivač na char. Njega se koristi za tako da ako endp != NULL, tada strtol() sprema u *endp pokazivač na prvi znak koji nije obrađen. Ako je to znak '\0', pretvorba je uspješna. Ako je jednak s, pretvorba uopće nije izvršena. Funkcija vraća vrijednost koja je pretvoreni broj ili 0 ako pretvorba nije izvršena ili konstanta LONG_MAX ili LONG_MIN ako se broj ne može predstaviti kao long int. U slučaju prekoračenja postavlja se errno = ERANGE. Funkcija strtoul() je slična strtol() osim što vraća tip unsigned long int, ili vrijednost ULONG_MAX kod prekoračenja. Poziv atol(s) je ekvivalentan pozivu strtoul(s,NULL,0).

atof, strtod double atof(const char *s); double strtod(const char *s, char **endp);

Ove funkcije pretvaraju numerički string s u odgovarajuću numeričku vrijednost realnog broja. Funkcija strtod() u stringu s preskače bijele znakove, pretvara s u broj i vraća vrijednost tipa double. Prihvaća prosti i eksponentni zapis realnog broja. Argument endp ja pokazivač na pokazivač na char. Njega se koristi za tako da ako endp != NULL, tada strtod() sprema u *endp pokazivač na prvi znak koji nije obrađen. Ako je to znak '\0', pretvorba je uspješna. U slučaju prekoračenja vraća konstantu HUGE_VAL, i postavlja globalnu varijablu errno = ERANGE. Napomena: atof(s) je ekvivalentno strtod(s, NULL), osim što rezultat nije definiran u slučaju prekoračenja. Generator slučajnih brojeva

rand int rand(void);

Funkcija rand() vraća slučajni cijeli broj iz intervala konstanta definirana u ).

0 do RAND_MAX ( RAND_MAX je

srand

374

void srand(unsigned int seed);

Funkcija srand() postavlja početnu vrijednost seed generatora slučajnih brojeva. Sortiranje i traženje

qsort void qsort(void *a, size_t n, size_t elsize, int (*cmpfunc)());

Funkcija qsort() sortira niz a, koji ima n elemenata veličine elsize (u bajtima), prema kriteriju koji je određen funkcijom na koju pokazuje cmpfunc. Ta funkcija mora biti deklarirana u obliku: int name(const void *p1, const void *p2);

i mora vratiti cijeli broj koji je manji, veći ili jedanak nuli, ovosno o tome da li je objekt na kojeg pokazuje p1 manji, veći, ili jednak objektu na kojeg pokazuje p2.

bsearch bsearch(const void *pObj, const void *a, size_t n, size_t elsize, int (*cmpfunc)());

Funkcija bsearch() vrši binarno traženje u sortiranom nizu a, koji ima n elemenata veličine elsize (u bajtima), tražeći element koji je jednak objektu na kojeg pokazuje pObj. Pri traženju se za usporedbu koristi funkcija na koju pokazuje cmpfunc. Deklaracija te funkcije je ista kao kod qsort(). Interakcija s operativnim sustavom

getenv char *getenv(const char *name);

"Environment" sadrži postavke operativnog sustava u sistemskim varijablama (pr. path). Funkcija getenv() traži "environment varijablu" imena name i vraća njenu vrijednost u obliku stringa. Ako ne može pronaći tu varijablu, tada vraća NULL.

atexit int atexit(void (*func)(void));

Funkcija atexit() prima kao argument pokazivač na funkciju func, koja će biti pozvana pri normalnim završetku programa. Vraća 0 ako je funkcija uspješno registrirana. Može se registrirati do 32 funkcije, koje će biti pozvane u obrnutom redoslijedu od reda registriranja.

exit void exit(int status);

Funkcija exit() vrši normalni završetak programa, poziva sve funkcije koje su registrirane s atexit() funkcijom i zatvara sve tokove. Argument status se prosljeđuje operativnom sustavu. Mogu se koristiti i dvije simboličke konstante EXIT_SUCCESS i EXIT_FAILURE. Po dogovoru, status=0 (EXIT_SUCCESS) znači uspješan završetak programa.

375

system int system(const char *s);

Funkcija system() prima kao argument string koji sadrži komandu operativnog sustava. Funkcija vraća vrijednost koju vrati operativni sustav po završetku procesa.

abort void abort(void);

Funkcija abort() predstavlja zahtjev za neposrednim prekidom programa, na isti način kao da je izvršen poziv raise(SIGABRT). Cjelobrojne aritmetičke funkcije

abs, labs int abs(int x); long int abs(long int x);

Obje funkcije vraćaju apsolutnu vrijednost argumenta x..

div, ldiv div_t div(int num, int denom); ldiv_t div(long int num, long int denom);

Ove funkcije vrše dijeljenje num/denum na način da se istovremeno dobije rezultat dijeljenja i ostatak cjelobrojnog dijeljenja. Rezultat se vraća u vrijednost koja je tipa sljedeće strukture: typedef struct { int quot; int rem; }div_t;

/* rezultat dijeljenja */ /* ostatak dijeljenja */

Multibajt znakovne sekvence i stringovi Za zapis znakova po ASCII standardu koristi se tip char. Za zapis znakova se također može koristiti prošireni znakovni tip wchar_t, koji podržava 16-bitni Unicode standard, i multibajt znakovne sekvence MBCS (za jezike poput japanskog).

mblen int mblen(const char *s, size_t n);

Funkcija mblen() vraća broj znakova u stringu koji sadrži multibajt znakovne sekvence. Analizira se maksimalno n znakova.

mbtowc, wctomb int mbtowc(wchar_t dest, const char *src, size_t n); int wctomb(char *dest, wchar_t src);

376

Ove funkcije pretvaraju multibajt znakovnu sekvencu u tip wchar_t i obrnuto. Funkcija mbtowc() analizira maksimalno n bajta iz stringa src i pretvara ih u wchar_t i sprema u dest, te vraća broj bajta ili -1 ako pretvorba nije uspješna. Funkcija wctomb() pretvara prošireni znak src u multibajt sekvencu dest, i vraća broj bajta u toj sekvenci.. (Ovaj broj neće nikada biti veći od konstante MB_CUR_MAX, definirane u .)

mbstowcs, wcstombs size_t mbstowcs(wchar_t *dest, const char *src, size_t n); size_t wcstombs(char *dest, wchar_t src);

Ove funkcije vrše pretvorbu višestruke multibajt sequence i niza proširenih znakova. Funkcija mbtowcs() pretvara multibajt sekvencu src i u niz proširenih znakova i sprema u dest, ali maksimalno n znakova. Vraća broj pretvorenih znakova. Funkcija wcstombs() vrši obrnutu radnju.

C 5 Standardne matematičke funkcije su deklarirane u zaglavlju . Njihov opis je dan sljedećoj tablici: Funkcija double double double double double double double double double double double double double double double double double double double double

sin(double x); cos(double x); tan(double x); asin(double x); acos(double x); atan(double x); atan2(double y, double x); sinh(double x); cosh(double x); tanh(double x); exp(double x); log(double x); log10(double x); pow(double x, double y); sqrt(double x); ceil(double x); floor(double x); fabs(double x); ldexp(double x,int n); frexp(double x, int *exp);

double modf(double x, double *ip); double fmod(double x, double y);

Vraća vrijednost sin(x) , sinus kuta x u radijanima cos(x) , kosinus kuta x u radijanima tg(x) , tangens kuta x u radijanima arcsin(x), vraća vrijednost [-π/2, π/2], za x ∈ [-1,1]. arccos(x), vraća vrijednost [0, π], za x ∈ [-1,1]. arctg(x), vraća vrijednost [-π/2, π/2]. arctan(y / x), vraća vrijednost [-π,π]. sh(x), sinus hiperbolni kuta x ch(x) , kosinus hiperbolni kuta x th(x) , tangens hiperbolni kuta x ex , x potencija broja e = 2,781 prirodni logaritam ln(x), x>0. logaritam baze 10, log10(x), x>0. xy, potenciranje x s eksponentom y. Nastaje greška ako je x=0 i y tm_mday, p->tm_mon + 1, p->tm_year +1900, p->tm_hour, p->tm_min);

11.05.2002 14:21 */

strftime size_t strftime(char *buf, size_t bufsize, const char *fmt, const struct tm *tp);

Funkcija strftime() se koristi za formatirani ispis vremena. Format se zadaje kao kod printf() funkcije. Prvi argument je string str u koji se vrši formatirani zapis. Drugi argument (bufsize) ograničava broj znakova stringa. Treći parametar je string u kojem se zapisuje format ispisa nizom specifikatora oblika %x (kao kod printf() funkcije). Posljednji argument je pokazivač strukture tm. Funkcija vraća broj znakova u stringu ili 0 ako nije moguće generirati formatirani string. Specifikatori formata su: %a %A %b %B %c %d %H %I %j %m %M %p %S %U %w %W

kratica od tri slova za ime dana u sedmici (eng. Sun, Mon, Tue,..) puno ime dana u sedmici (eng.) kratica od tri slova za ime mjeseca (eng. Jan, Feb, Mar,...) puno ime mjeseca (eng.) kompletni zapis vremena i datuma dan u mjesecu (1..31) sat u formatu (1..24) sat u formatu (1..12) dan u godini (1..365) mjesec u godini (1..12) minute AM/PM (eng.) string koji označava jutro ili popodne sekunde broj za sedmicu u godini (1..52) - 1 određen prvom nedjeljom broj za dan u sedmici (0-nedjelja) broj za sedmicu u godini (1..52) - 1 određen prvim ponedjeljkom

379

%x %X %y %Y %Z %%

kompletni zapis datuma kompletni zapis vremena zadnje dvije znamenke godine godina u 4-znamenkastom formatu ime vremenske zone (ako postoji ) znak %

mktime time_t mktime(struct tm *tp);

Funkcija mktime() pretvara zapisa iz strukture tm u time_t format. Korisna je u tzv. kalendarskim proračunima. Kada je potrebno dodati nekom datumu n dana, tada se može upisati datum u tm strukturu, povećati član tm_mday za n, zatim pozivom mktime() se dobije time_t vrijednost koja odgovara novom datumu.

difftime double difftime(time_t t1, time_t t2);

Funkcija difftime() vraća realnu vrijednost koja je jednaka razlici vremena t1 i t1 u sekundama. clock clock_t clock(void);

Funkcija clock() služi za preciznije mjerenje vremena nego je to moguće sa prethodnim funkcijama. Ona vraća vrijednost procesorskog mjerača vremena, koji starta na početku programa, u jedinicama koje su znatno manje od sekunde (nekoliko milisekundi). Koliko je tih jedinica u jednoj sekundi određeno je konstantom CLOCKS_PER_SEC. To znači da izraz: (double)clock()/CLOCKS_PER_SEC

daje vrijednost koja je jednaka vremenu (u sekundama) od pokretanja programa.

C 7 U zaglavlju deklarirane su dvije funkcije (signal() i raise()) za prihvat i generiranje asinkronih prekida programa ili "signala". Za identificiranje nekoliko mogućih signala, u ovom zaglavlju su definirane simboličke cjelobrojne konstante sa sljedećim imenima: SIGABRT SIGFPE SIGILL SIGINT SIGSEGV SIGTERM

Signal kojeg generira funkcija abort(). Signal koji se generira kad nastane greška kod matematičkih operacija primjerice pri dijeljenju s nulom. Signal koji se generira ako se pokušava izvršiti nepostojeća ili nedozvoljena instrukcija procesora. Signal koji se generira s tipkovnice (primjerice, Ctrl-C tipkom). Signal koji se generira ako se pristupa zaštićenoj ili nepostojećoj memoriji ("segmentation violations"). Signal koji se generira kada je proces prekinut nekim vanjskim događajem.

Ovisno o implementaciji kompilatora, moguće su i dodatne definicije identifikatora signala.

380

raise int raise(int sig);

Funkcija raise() šalje izvršnom programu signal sig. signal void (*signal(int sig, void (*func)(int)))(int);

Funkcija signal() se koristi za definiranje akcije koja se treba izvršiti kada se pojavi neki signal. Ukoliko nije definirana radnja koja se vrši nakon pojave signala, prekida se program. Argument sig je signal kojeg treba prihvatiti (jedna od SIGxxx konstanti). Argument func je ili konstanta SIG_IGN (kojom se zahtijeva ignoriranje signala) ili konstanta SIG_DFL (kojom se postavlja predodređeni postupak prihvata signala) ili pokazivač na korisnički definiranu funkciju koja će se izvršiti pojavom signala. Ta funkcija mora imati prototip oblika void signalhandler(int sig);

Argument ove funkcije tipa int je broj signala koji se prihvaća. Funkcija signal() vraća prethodni oblik prihvata signala; SIG_DFL, SIG_IGN, ili pokazivač na funkciju. Zbog navedenih svojstava, deklaracija funkcije signal() je kompleksna. To je funkcija koja vraća pokazivač na funkciju koja prima jedan argument tipa int i vraća void. Prvi argument je tipa int, a drugi argument je pokazivač na funkciju koja prima jedan argument tipa int i vraća void. Primjer: u sljedećem programskom odsječku pokazano je kako se postavlja poziv funkcije exithandler() u slučaju pojave prekida (interrupt signal - SIGINT), ali samo ako taj signal nije prethodno ignoriran: extern void exithandler(int); if(signal(SIGINT, SIG_IGN) != SIG_IGN) signal(SIGINT, exithandler);

C 8 U zaglavlju deklarirane su funkcije setjmp() i longjmp(), pomoću kojih se može izvršiti skok u program i izvan funkcije. Mjesto u programu, na koju se vrši skok, označava se funkcijom setjmp(), koja pamti stanje stoga, registre procesora i trenutnu adresu programa u objektu tipa jmp_buf. Kasnije se s bilo kojeg mjesta u programu može skočiti na ovu poziciju pozivom funkcije longjmp(). setjmp int setjmp(jmp_buf context);

Funkcija setjmp() sprema trenutnu programsku adresu i stanje procesora u objekt context, koji je tipa jmp_buf, i vraća vrijednost 0. Kasnije, nakon poziva funkcije longjmp(), povratna vrijednost se može promijeniti. longjmp

381

void longjmp(jmp_buf context, int retval)

Funkcija longjmp() vrši skok na stanje opisano u objektu context, koji je prethodno spremljen pozivom funkcija setjmp(). Skok se vrši na mjesto gdje je prethodno pozvana funkcija setjmp(), pa se sada vrši povrat iz funkcije setjmp()s vrijednošću retval.

C 9 U zaglavlju deklarirane su dvije funkcije za lokalizirane postavke. Početno, program počinje u "C" lokalizaciji, koja se zatim može promijeniti sa setlocale() funkcijom. Lokalno-specifične informacije se dijele u nekoliko kategorija, koje se označavaju sljedećim konstantama: LC_COLLATE LC_CTYPE LC_MONETARY LC_NUMERIC LC_TIME LC_ALL

Usporedba stringova se vrši pomoću funkcija strcoll() i strxfrm() Klasiofikacija znakova pomoću funkcija iz Monetarne postavke se dobiju funkcijom localeconv() Koristi decimalnu točku u funkcijama printf(), scanf(), strtod(), itd. Lokalizirani format strftime() funkcije Unija prethodnih postavki

setlocale char *setlocale(int cat, const char *locale);

Funkcija setlocale() ima dva argumenta. Prvi argument je oznaka kategorije koja se postavlja, a drugi parametar locale je string za oznaku lokalizacije. Ako taj string je jednak "C", tada se koristi predodređena lokalizacija. Prazni string "" također označava predodređenu lokalizaciju. Sve ostale oznake su specifične za pojedinu implementaciju kompilatora. Funkcija vraća pokazivač na string koji sadrži prethodnu locale postavku. Ako se setlocale() pozove s locale == NULL tada funkcija vraća trenutnu postavku. localeconv struct lconv *localeconv(void);

Funkcija localeconv() vraća pokazivač na strukturu lconv koja sadrži lokalno-specifične informacije. Ta struktura je otprilike definirana ovako: struct lconv { char *decimal_point; char *thousands_sep; char *grouping; char *int_curr_symbol; char *currency_symbol; char *mon_decimal_point; char *mon_thousands_sep; char *mon_grouping; char *positive_sign, *negative_sign; char int_frac_digits; char frac_digits; char p_cs_precedes, p_sep_by_space; char n_cs_precedes, n_sep_by_space; char p_sign_posn, n_sign_posn;

382

}; decimal_point je oznaka koja se koristi za decimalni zarez. thousands_sep je separator koji se koristi između grupe znamenki grouping je string koji definira veličinu grupe (primjerice "\3" označava da se ponavlje

grupa od 3 znaka ). Ostali članovi opisuju monetarno-specifične informacije. Ukratko, int_curr_symbol i currency_symbol su verzije (internacionalne i lokalne) za lokalnu valutu, mon_decimal_point je decimalna točka, mon_thousands_sep i mon_grouping dopisuju grupiranje znamenki (analogno s thousands_sep i grouping), positive_sign i negative_sign su znakovi pozitivnog i negativnog predznaka, int_frac_digits i frac_digits opisuju broj decimalnih znamenki koje se prikazuju. Ostali članovi opisuju oznaku valute i indikatore predznaka.

C 10 U zaglavlju su definirani makro naredbe pomoću kojih se omogućuje definiranje funkcija s promjenjljivim brojem parametara. Koristi se ideja da se argumentima neke funkcije pridijeli lista pokazivača koja ima apstraktni tip va_list. U tu svrhu koristi se makro va_start. Zatim se iz ove liste mogu dobiti svi argumenti pomoću makroa va_arg. Na kraju rada, unutar iste funkcije, treba pozvati makro va_end. va_start va_start(va_list argp, Lastarg); va_start inicijalizira argp tako da se njime mogu dohvatiti argumenti. Lastarg je ime posljednjeg fiksnog argumenta funkcije.

va_arg argtype va_arg(va_list argp, argtype); va_arg dobavlja vrijednost sljedećeg argumenta koji je tipa argtype. Tip argtype se specificira na isti način kako se definira argument sizeof operatora. Tip mora odgovarati tipu sljedećeg argumenta.

va_end va_end(va_list argp); va_end označava da je završen pristup promjenjljivoj listi argumenata.

Primjer: u sljedećem programu definirana je funkcija miniprintf(), kojom je pokazano kako je implementirana printf() funkcija. #include #include void miniprintf(const char *format, ...) { va_list argp;

383

const char *p; char tmpbuf[25]; int i; va_start(argp, format);

}

for(p = format; *p != '\0'; p++) { if(*p != '%') { putchar(*p); continue; } switch(*++p) { case 'c': i = va_arg(argp, int); putchar(i); break; case 'd': i = va_arg(argp, int); sprintf(tmpbuf, "%d", i); fputs(tmpbuf, stdout); break; case 'o': i = va_arg(argp, int); sprintf(tmpbuf, "%o", i); fputs(tmpbuf, stdout); break; case 's': fputs(va_arg(argp, char *), stdout); break; case 'x': i = va_arg(argp, int); sprintf(tmpbuf, "%x", i); fputs(tmpbuf, stdout); break; case '%': putchar('%'); break; } } va_end(argp);

C 11 U zaglavlju definirano je nekoliko tipova i makro naredbi. NULL size_t

Makro koji označava konstantu za nul pokazivač (vrijednost mu je 0 ili (void *)0). Cjelobrojni unsigned tip koji se koristi za označavanje veličine memorijskog objekta. ptrdiff_t Cjelobrojni tip koji označava vrijednosti koji nastaju kod oduzimanja pokazivača. wchar_t Tip “wide character” koji može imati znatno veći interval vrijednosti od tipa char. Koristi se za multinacionalni skup znakova (Unicode). offsetof() Makro kojim se računa pomak (eng. offset) u bajtima nekog elementa strukture, primjerice offsetof(struct tm, tm_year).

Korištenje ovih tipova osigurava prenosivost programa.

C 12 U zaglavlju definar je makro assert, koji omogućuje testiranje pograma.

384

void assert(int test_izraz)

Ako je test_izraz jednak nuli, tada assert(test_izraz)

šalje na stdderr poruku, poput ove: Assertion failed: test_izraz, file filename, line nnn

i vrši poziv funkcije abort(), čime se prekida izvršenje programa. Ime izvorne datoteke (filename) i broj linije u kojoj je assert, su dobijeni pretprocesorskih makroa: __FILE__ i __LINE__. Ako se pri kompiliranju definira makro NDEBUG (s bilo kojom vrijednošću) tada se ignorira makro assert.

C 13 U zaglavlju deklarirana je globalna varijable errno u kojoj se bilježi kôd greške koja nastaje pri korištenju funkcija standardne biblioteke. Također, definirane su simboličke konstante EDOM and ERANGE koje označavaju kôd pogreške kod matematičkih operacija.

C 14 U zaglavlju definirane su simboličke konstante standardnih tipova. To su: CHAR_BIT CHAR_MAX CHAR_MIN INT_MAX INT_MIN LONG_MAX LONG_MIN SCHAR_MAX SCHAR_MIN SHRT_MAX SHRT_MIN UCHAR_MAX UINT_MAX ULONG_MAX USHRT_MAX MB_LEN_MAX

koje označavaju interval

broj bitova u tipu char maksimalna vrijednost char tipa maksimalna vrijednost char tipa maksimalna vrijednost int tipa minimalna vrijednost int tipa maksimalna vrijednost long tipa minimalna vrijednost long tipa maksimalna vrijednost signed char tipa minimalna vrijednost signed char tipa maksimalna vrijednost short tipa minimalna vrijednost short tipa maksimalna vrijednost unsigned char tipa maksimalna vrijednost unsigned int tipa maksimalna vrijednost unsigned long tipa maksimalna vrijednost unsigned short tipa broj bajta u multibajt znakovnoj sekvenci

C 15 U zaglavlju definirane su simboličke konstante koje označavaju implementaciju realnih brojeva s pomičnom točkom. To su: FLT_RADIX FLT_MANT_DIG FLT_DIG FLT_MIN_EXP FLT_MIN_10_EXP FLT_MAX_EXP

FLT_ROUNDS DBL_MANT_DIG DBL_DIG DBL_MIN_EXP DBL_MIN_10_EXP DBL_MAX_EXP

LDBL_MANT_DIG LDBL_DIG LDBL_MIN_EXP LDBL_MIN_10_EXP LDBL_MAX_EXP

385

FLT_MAX_10_EXP FLT_MAX FLT_EPSILON FLT_MIN

DBL_MAX_10_EXP DBL_MAX DBL_EPSILON DBL_MIN

LDBL_MAX_10_EXP LDBL_MAX LDBL_EPSILON LDBL_MIN

FLT_RADIX je baza “floating-point” modela (pr. 2, 16). FLT_ROUNDS je konstanta koja

pokazuje kako se zaokružuje rezultat pri zbrajanju: 0 ako je prema 0, 1 ako je prema najbližoj vrijednosti, 2 ako je prema +∞, 3 ako je –∞, i -1 znači da nije definirano. Ostali makroi daju svojstva tipova: float (FLT_), double (DBL_), i long double (LDBL_). MANT_DIG je broj znamenki (baze FLT_RADIX) u mantisi. DIG daje približnu preciznost u ekvivalentnoj bazi 10. MIN_EXP i MAX_EXP daju maksimalni i minimalni eksponent (MIN_10_EXP i MAX_10_EXP daju njihov ekvivalent u bazi 10). MIN i MAX daju minimalnu i maksimalnu vrijednost realnog broja. EPSILON je razlika između 1.0 i sljedećeg većeg broja.

C 16 U zaglavlju definirani su makroi za zamjenu operatora koji možda nisu implementirani na nekom mikro računalima. To se sljedeće definicije: #define #define #define #define #define #define #define #define #define #define #define

and && and_eq &= bitand & bitor | compl ~ not ! not_eq != or || or_eq |= xor ^ xor_eq ^=

C 17 U zaglavlju definirane su gotovo sve funkcije za rad s znakovima i stringovima koji su tipa wchar_t. Obično se ASCII znakove naziva prostim znakovima, a znakove tipa wchar_t proširenim znakovima (eng. wide character). Evo kako se inicijalizira prošireni znakovni tip i string: wchar_t c = L'A'; wchar_t *s = L"Hello";

Pored wchar_t tipa definiran wint_t, integralni tip koji može sadržavati vrijednost wchar_t tipa, te makro WEOF kao oznaka za kraj datoteke. Operacije sa stringovima proširenih znakova size_t wcslen(const wchar_t *s); wchar_t *wcscpy(wchar_t *dest, const wchar_t *src); wchar_t *wcscat(wchar_t *dest, const wchar_t *src); wchar_t *wcsncpy(wchar_t *dest, const wchar_t *src, size_t n); wchar_t *wcsncat(wchar_t *dest, const wchar_t *src, size_t n); int wcscmp(const wchar_t *s1, const wchar_t *s2); int wcsncmp(const wchar_t *s1, const wchar_t *s2, size_t n); int wcscoll(const wchar_t *s1, const wchar_t *s2); size_t wcsxfrm(wchar_t *dest, const wchar_t *src, size_t n);

386

wchar_t *wcschr(const wchar_t *s, wchar_t c); wchar_t *wcsrchr(const wchar_t *s, wchar_t c); wchar_t *wcsstr(const wchar_t *s, const wchar_t *pat); size_t wcsspn(const wchar_t *s, const wchar_t *set); size_t wcscspn(const wchar_t *s, const wchar_t *set); wchar_t *wcspbrk(const wchar_t *s, const wchar_t *set);

Ove funkcije (wcsxxx) su ekvivalentne funkcijama za rad s ASCII stringovima (strxxx), osim što se umjesto pokazivača na char koristi pokazivač na wchar_t, a broj n se intepretira kao broj wchar_t znakova. Operacije s nizovima proširenih znakova wchar_t *wmemcpy(wchar_t *dest, const wchar_t *src, size_t n); wchar_t *wmemmove(wchar_t *dest, const wchar_t *src, size_t n); int wmemcmp(const wchar_t *p1, const wchar_t *p2, size_t n); wchar_t *wmemchr(const wchar_t *p, wchar_t c, size_t n); wchar_t *wmemset(wchar_t *p, wchar_t c, size_t n);

Ove funkcije (wmemxxx) su ekvivalentne funkcijama za rad s ASCII nizovima (memxxx), osim što se umjesto pokazivača na char koristi pokazivač na wchar_t, a broj n se intepretira kao broj wchar_t znakova. Pretvorba stringa proširenih znakova u numeričku vrijednost long int wcstol(const wchar_t *s, wchar_t **endp, int base) unsigned long int wcstoul(const wchar_t *s, wchar_t **endp, int base); double wcstod(const wchar_t *s, wchar_t **endp);

Ove funkcije (wcsxxx) su ekvivalentne funkcijama za rad s ASCII stringovima (strxxx), osim što se umjesto pokazivača na char koristi pokazivač na wchar_t, Pretvorba vremena size_t wcsftime(wchar_t *buf, size_t bufsize, const wchar_t *format, const struct tm *tp); wcsftime() izvršava operaciju analognu izvršenju strftime().

Rastav stringa proširenih znakova na lekseme wchar_t *wcstok(wchar_t *s, const wchar_t *sep, wchar_t **state);

Funkcija wcstok() vrši rastav stringa s ne lekseme koji su odvojeni znacima separatora (sep) analogno funkciji strtok(), osim što je temeljni tip wchar_t, i dodan je treći argument state, koji je pokazivač na objekt tipa wchar_t *; wcstok() koristi ovaj objekt za pohranu stanja između uzastopnih poziva funkcije. Ulazno izlazne operacije s proširenim znakovima

getwchar, getwc, fgetwc

387

wint_t getwchar(void); wint_t getwc(FILE *fp); wint_t fgetwc(FILE *fp);

Ove funkcije čitaju znakove iz toka fp ili stdin (implicitno se vrši pretvorba multibajtznakovnih sekvenci, kao da je pozvana funkcija mbrtowc).Ako je kraj datoteke funkcije vraćaju WEOF. Funkcionalnost im je ista kao kod funkcija getchar(), getc(), i fgetc().

putwchar, putwc, fputwc wint_t putwchar(wchar_t c); wint_t putwc(wchar_t c, FILE *fp); wint_t fputwc(wchar_t c, FILE *fp);

Ove funkcije upisuju wchar_t znakove u toka fp ili stdin (implicitno se vrši pretvorba multibajt-znakovnih sekvenci, kao da je pozvana funkcija mbrtowc). Funkcionalnost im je ista kao kod funkcija putchar(), putc(), i fputc().

wprintf, fwprintf int wprintf(const wchar_t *, ...); int fwprintf(FILE *fp, const wchar_t *, ...);

Ove funkcije su ekvivalentne funkcijama printf() i fprintf(), osim što se u tok zapisuje multibajt znakovna sekvenca, kao da je pozvan fputwc(). U format stringu specifikatori %c i %s i dalje znače da se očekuje prosti znakovi, a da bi se ispisali prošireni znakovi treba koristiti specifikatore %lc i %ls.

wscanf, fwscanf int wscanf(const wchar_t *, ...); int fwscanf(FILE *fp, const wchar_t *, ...);

Ove funkcije su ekvivalentne funkcijama scanf() i fscanf(), osim što se format string tretira kao niz proširenih znakova, a tok koji se očitava tretira se kao multibajt znakovni niz. U format stringu specifikatori %c, %s i %[ znače da se očekuje prosti znakovi, a da bi se unijeli prošireni znakovi treba koristiti specifikatore %lc , %ls i %l[.

fgetws, fputws wchar_t *fgetws(wchar_t *, int, FILE *fp); int fputws(const wchar_t *, FILE *fp);

Ove funkcije služe čitanju ili zapisu linije teksta analogno funkcijama fgets() i fputs().

ungetwc wint_t ungetwc(wint_t c, FILE *fp);

Funkcija ungetwc() vraća prošireni znak c natrag u ulazni tok fp, analogno ungetc() funkciji.

swprintf, swscanf

388

int swprintf(wchar_t *buf, size_t bufsize, const wchar_t *format, ...); int swscanf(const wchar_t *buf, const wchar_t *format, ...);

Funkcija swprintf() generira string buf, maksimalne veličine bufsize, a funkcija swscanf() dobavlja podatke iz stringa buf, prema zadanom formatu, analogno funkcijama sprintf() i sscanf().

vwprintf, vfwprintf, vswprintf int vwprintf(const wchar_t *format, va_list argp); int vfwprintf(FILE *fp, const wchar_t *format, va_list argp); int vswprintf(wchar_t *buf, size_t bufsize, const wchar_t *format, va_list argp);

Ove funkcije su analogne funkcijama vprintf(), vfprintf(), and vsprintf(). Argument vswprint() bufsize omogućuje kontrolu maksimalne duljine stringa kao kod swprintf().

fwide int fwide(FILE *fp, int mode);

Svaki tok ima "orijentaciju" koja pokazuje da li se on koristi s normalnim ili multibajt znakovima (pomoću funkcija iz ove sekcije) Početno je tok “neorijentiran”, ali se nakon prve upotrebe prebacuje u "bajt-orijentirani" ili "prošireno-orijentirani" mod obrade teksta. Funkcijom fwide() može se postaviti orijentacija toka fp, tako da se argument mode postavi na vrijednost veću od nule za "prošireno-orijentirani" mod, ili na vrijednost manju od nule za "bajt-orijentirani" mod. Funkcija vraća vrijednost trenutne orijentacije (0 znači da je tok neorijentiran). Dodatne pretvorbe

btowc, wctob wint_t btowc(int c); int wctob(wint_t wc); Funkcija btowc() pretvara normalni znak c u prošireni znak. Funkcija wctob() pretvara prošireni znak wc u normalni znak. Vraća znak ili EOF ako pretvorba nije moguća.

mbrlen size_t mbrlen(const char *s, size_t n, mbstate_t *state);

Funkcija mbrlen() je slična funkciji mblen(). Njome se može dobiti i duljinu prekinute multibajt sekvence. Početni se dio referira u objektu state, a na preostali dio pokazuje pokazivač s. Vraća vrijednost kao funkcija mbrtowc().

mbrtowc, wcrtomb size_t mbrtowc(wchar_t *dest, const char *src, size_t n, mbstate_t *state); size_t wcrtomb(char *dest, wchar_t src, mbstate_t *state);

389

Ove su funkcije slične funkcijama mbtowc() i wctomb(), osim što mogu obraditi i dio multibajt sekvence koja je prekinuta, uz uvjet da je stanje pretvorbe zabilježeno u objektu na kojeg pokazuje state. Funkcija mbrtowc() pretvara multibajt sekvencu src u proširen znak u *dest i vraća broj bajta na koje pokazuje src koji tvore ispravnu multibajt sekvencu, ili 0 ako src==NULL, ili -1 ako nastane greška, ili -2 ako nije pronađena kompletna multbajt sekvenca (upotrebljena za *state). Funkcija wcrtomb() pretvara prošireni znak src u multibajt sekvencu dest i vraća broj bajta zapisanih u dest, ili -1 ako nastane greška.

mbsrtowcs, wcsrtombs size_t mbsrtowcs(wchar_t *dest, const char **srcp, size_t n, mbstate_t *state); size_t wcsrtombs(char *dest, const wchar_t **srcp, size_t n, mbstate_t *state);

Ove su funkcije slične funkcijama mbtowcs() i wctombs(), osim što mogu obraditi i dio multibajt sekvence koja je prekinuta, uz uvjet da je stanje pretvorbe zabilježeno u objektu na kojeg pokazuje state. String koji se pretvara prenosi se po referenci *srcp, kako bi se mogao ažurirati da pokazuje na preostali dio nepretvorenog stringa. Ako je broj n nedovoljan (kao broj proširenih znakova za mbsrtowcs() ili bajta za wcsrtombs()) za kapacitet odredišnog stringa rezultata, *srcp se postavlja tako da pokazuje na nepretvoreni ulaz, a *state se ažurira da odrazi prekinuto stanje pretvorbe.

mbsinit int mbsinit(const mbstate_t *p);

Funkcija mbsinit() vraća nenultu vrijednost ako je objekt stanja na kojeg pokazuje p u početnom stanju, ili ako je p==NULL.

C 18 U zaglavlju deklarirano je nekoliko funkcija za klasificiranje i pretvorbu znakova tipa wchar_t, analognih funkcijama iz . Klasifikacija proširenih znakova int int int int int int int int int int int

iswupper(wint_t c); iswlower(wint_t c); iswalpha(wint_t c); iswdigit(wint_t c); iswalnum(wint_t c); iswxdigit(wint_t c); iswspace(wint_t c); iswpunct(wint_t c); iswprint(wint_t c); iswgraph(wint_t c); iswcntrl(wint_t c);

Funkcije (iswxxx) su analogne funkcijana (isxxx), osim što je argument ovih funkcija tipa wint_t.

390

Dodatne funkcije za klasifikacija proširenih znakova wctype_t wctype(const char *classname); int iswctype(wint_t c, wctype_t classtok);

Funkcija wctype() prihvaća argument classname u kojem se indicira klasifikacija i vraća token tipa wctype_t (definiran je u ). Funkcija wctype() prihvaća stringove: "upper", "lower", "alpha", "digit", "alnum", "xdigit", "space", "punct", "print", "graph", i "cntrl" (koji odgovaraju predefiniranoj klasifikaciji ), plus korisnički definirani string za klasifikaciju. Funkcija iswctype() prihvaća argumente znak c i token classtok koji je prethodno dobiven funkcijom wctype(), te vraće nenultu vrijednost ako znak ne pripada klasifikaciji Funkcije za pretvorbu proširenih znakova i stringova wint_t towupper(wint_t c); wint_t towlower(wint_t c);

Ove funkcije su ekvivalentne funkcijana toupper() i tolower(), za normalne znakove. wctrans_t wctrans(const char *convname); wint_t towctrans(wint_t c, wctrans_t convtok);

Funkcija wctrans() prihvaća argument convname u kojem se indicira znakovna pretvorba i vraća token tipa wctrans_t koji se koristi za izvršenje pretvorbe (wctrans_t je definiran u ). Funkcija towctrans() vrši pretvorbu proširenog znaka c prema tokenu convtok koji je prethodno dobiven funkcijom wctrans(), i vraća pretvoreni prošireni znak. Funkcija wctrans() prihvaća stringove "toupper" i "tolower" (koji označavaju predefinirani način pretvorbe), plus korisnički definirani string za pretvorbu.

391

Index

#define, 47, 186 #elif, 190 #else, 190 #endif, 190 #if, 190 #ifdef, 190 #ifndef, 190 #include, 186 #pragma pack, 177 #undef, 189 , 380 , 368 , 381 , 381 , 382 , 381 , 378 , 373 , 377 , 376 , 379 , 380 , 357 , 369 , 365 , 374 , 382 , 386 abort(), 372 abs(), labs(), 372 adresni operator &, 50 ADT, 208 apstrakcija, 12 apstraktni dinamički tipovi, 208 argumenti komandne linije, 157 aritmetički izrazi, 72 ASCII, 39 asctime(), 374 asocijativnost i prioritet operatora, 73 assert, 210, 380 atexit(), 371 atof(), 370 atoi(), atol(), 370 automatske varijable, 114 bajt, 7

biblioteke funkcija, 120 binarne i tekstualne datoteke, 193 binarni brojevni sustav, 18 binarno pretraživanje, 229 binarno stablo, 278, 279 bit, 7 bit polja strukture, 179 bitznačajni operatori, 75 BNF notacija, 289 Booleova logika, 16 break, 100 brojevni sustavi, 17 bsearch, 371 bsearch(), 232 calloc(), 369 centralna procesorska jedinica, 8 char, 43 clearerr(), 364 clock(), 376 continue, 100 COUNTER, 209 ctime(), 374 De Morganov teorem, 16 decimalni brojevni sustav, 17 deklaracija funkcije, 27 deklaracija varijable, 44 dereferencirani pokazivač, 53 difftime(), 376 digitalna logika, 14 dinamičke matrice, 167 dinamičko alociranje memorije, 159 disjunkcija, 15 div(), ldiv(), 372 doseg identifikatora, 113 double, 43 do-while petlja, 99 dvostruko vezana lista, 266 egzistencijalni kvantifikator, 14 eksterne varijable, 117 ekvivalencija, 15 enum, 180 errno, 381 exit(), 371 fclose(), 195, 358 feof(), 363

392

ferror(), 364 fflush(), 195, 358 fgetc(), 358 fgetpos(), fsetpos(), 363 fgets(), 361 FIFO, 218 FILE, 192 float, 43 fopen(), 194, 357 for petlja, 99 fprintf(), 359 fputc(), 358 fputs(), 362 fread(), 201, 362 free(), 369 freopen(), 358 fscanf(), 360 fseek(), 204, 362 ftell(), 203, 362 funkcije, 64 fwrite(), 201, 362 generator slučajnih brojeva, 156 getchar(), getc(), 358 getenv, 371 gets(), 361 globalne varijable, 116 gmtime(), 374 goto, 90 hash tablica, 316 heksadecimalni brojevni sustav, 20 identifikatori, 82 if, 91 if-else, 92 implikacija, 15 inicijalizacija varijable, 55 INORDER, 284 int, 43 Integrirana razvojna okolina, 22, 28 intepreter, 22 interpreter prefiksnih izraza, 288 invertor, 16 izjavna logika, 14 izlazne jedinice, 8 izrazi, 72 izvedivi program, 22 izvorni program, 6 jednodimenzionalni nizovi, 103 klasifikacija znakova, 368 kodiranje, 34 kodiranje realnih brojeva, 37 kodiranje znakova, 39 komentar, 83 kompilator, 21

komplement dvojke, 35 komplement jedinice, 35 konjunkcija, 15 korijen stabla, 279 kule Hanoia, 226 leksemi, 84 LIFO, 213 linker, 22 lista stringova, 258 listovi stabla, 279 literalne konstante, 82 LL(1) analiza, 293 localeconv(), 378 localtime(), 374 lokalne varijable, 114 long, 43 longjmp(), 377 LSB, 7 makefile, 33 makro, 187 malloc(), 369 memchr(), 367 memcmp(), 367 memcpy(), 367 memmove(), 367 memset(), 368 merge sort, 240 metajezik, 84 mktime(), 376 MSB, 7 multibajt znakovne sekvence, 372 naredbe, 88 negacija, 15 neterminalni simboli, 85 nibl, 7 nizovi, 103 nizovi znakova, 146 obilazak stabla, 283 oktalni brojevnim sustav, 20 općenito stablo, 278 operativni sustav, 9 operator indirekcije, 51 parser, 294 perror(), 364 petlje, 97 pobrojani tip, 180 podijeli pa vladaj, 228 pointeri, 51 pokazivači, 51 postfiksna notacija izraza, 215 POSTORDER, 284 povezivač, 22 poziv funkcije, 26

393

pozivna funkcija, 26 pozvana funkcija, 26 predikat, 14 PREORDER, 284 pretprocesor, 186 pretvorba tipova, 79 pretvorba znaka, 369 printf(), 48, 359 privremene datoteke, 206 produkcije C-jezika, 85 program, 6 proste naredbe, 88 prototip funkcije, 27 punjač, 22 putchar(), putc(), 358 puts(), 362 qsort(), 371 quicksort, 243 računalo, 6 Računarski algoritam, 9 radna memorija, 8, 41 raise(), 376 RAM, 8 rand(), 156, 370 realloc(), 369 red (queue), 218 referenca, 44 rekurzivne funkcije, 223 rekurzivno silazni parser, 294 relacijski i logički izrazi, 74 remove(), 206, 365 rename(), 206, 365 repna rekurzija, 231 rewind(), 203, 362 rezidentni programi, 9 riječ, 7 rječnici, 314 ROM, 8 samoreferentne strukture, 247 scanf(), 53, 360 selekcijsko sortiranje, 237 semantičke akcije, 297 separatori, 82 setbuf(), setvbuf(), 363 setjmp(), 377 setlocale(), 378 short, 43 signal(), 377 sintaksa i leksika, 82 sintaktički analizator, 294 sizeof, 46 sklop-I, 16 sklop-ILI, 16

složeni operatori, 77 složenost algoritama, 233 sortirane liste, 258 sortiranje, 237 sortiranje umetanjem, 239 sprintf(), 364 srand(), 156, 370 sscanf(), 364 stablo (tree), 278 statičke globalne varijable, 118 stderr, 192 stdin, 192 stdout, 192 Stog ADT (stack), 213 strcmp(), 150 strcpy(), 149 strdup(), 162 strftime(), 375 string, 146 string operator, 188 strlen(), 147 strojni jezik, 21 strtod(), 370 strtol(), strtoul(), 370 struktura, 171 switch-case, 95 system(), 372 tablice simbola, 314 terminalni simboli, 84 ternarni uvjetni izraz, 78 Tic-Tac-Toe, 121 tijelo funkcije, 26, 66 time(), 374 tipovi podataka, 43 tmpfile(), 365 tmpnam(), 365 točka operator, 172 tokeni, 84 tokovi, 192 typedef, 81 ulazne jedinice, 8 ungetc(), 362 union, 178 univerzalni kvantifikator, 14 unsigned, 43 unutarnji čvorovi - podstabla, 279 uvlačenje redova, 93 va_arg, 379 va_end, 379 va_start, 379 vanjska memorija, 8 vanjski čvorovi, 279 veliki-O notacija, 233

394

vezana lista, 248 Visual Studio, 28 višedimenzionalni nizovi, 111 void, 53 void funkcije, 68 vprintf(), vfprintf(), vsprintf(), 364 vrijeme i datum, 181

while petlja, 97 zaglavlje funkcije, 26, 66 zakon asocijacije, 16 zakon distribucije, 16 zakon idempotentnosti, 16 zakon komutacije, 15 znakovni ulaz/izlaz, 198

395