You are on page 1of 139

Programiranje II

Beleške za predavanja

Smer Informatika
Matematički fakultet, Beograd

Predrag Janičić i Filip Marić

2010.
2
Sadržaj

I Osnovi algoritmike 7
1 Algoritmi i rešavanje problema 9
1.1 Algoritmi — intuitivni i formalni pojam . . . . . . . . . . . . . . 9
1.2 Rešavanje problema uz pomoć računara . . . . . . . . . . . . . . 10

2 Ispitivanje ispravnosti programa 13


2.1 Neformalno ispitivanje ispravnosti programa . . . . . . . . . . . . 13
2.2 Formalno ispitivanje ispravnosti programa . . . . . . . . . . . . . 14
2.2.1 Formalna specifikacija . . . . . . . . . . . . . . . . . . . . 14
2.2.2 Razvoj programa u terminima Horovih trojki . . . . . . . 14
2.2.3 Primeri Horovih trojki . . . . . . . . . . . . . . . . . . . . 14
2.2.4 Primer dokazivanja ispravnosti sekvencijalnog programa . 15
2.2.5 Dokazivanje ispravnosti programa koji sadrže petlje i metoda
induktivne invarijante . . . . . . . . . . . . . . . . . . . . 15
2.2.6 Primer dokazivanja korektnosti programa koji sadrži petlju 17

3 Složenost algoritama 19
3.1 Osnove analize algoritama: ,,O“ notacija; klase složenosti . . . . 19
3.2 Red algoritma; analiza najgoreg slučaja; O notacija . . . . . . . . 20
3.3 Izračunavanje složenosti algoritama . . . . . . . . . . . . . . . . . 21
3.4 NP kompletnost . . . . . . . . . . . . . . . . . . . . . . . . . . . 26

4 Rekurzija, rekurzivne matematičke funkcije i primeri korišćenja


rekurzije 29
4.1 Rekurzivne matematičke funkcije . . . . . . . . . . . . . . . . . . 30
4.2 Matematička indukcija i rekurzija . . . . . . . . . . . . . . . . . . 30
4.3 Rekurzija u računarstvu . . . . . . . . . . . . . . . . . . . . . . . 30
4.4 Faktorijel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
4.5 Obrazac trougao . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
4.6 Kule Hanoja . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
4.7 Permutacije . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
4.8 Particionisanje . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
4.9 Uzajamna rekurzija . . . . . . . . . . . . . . . . . . . . . . . . . . 36
4.10 Nedostaci rekurzije . . . . . . . . . . . . . . . . . . . . . . . . . . 37
4 SADRŽAJ

4.11 Eliminisanje rekurzije . . . . . . . . . . . . . . . . . . . . . . . . 38

5 Fundamentalni algoritmi 39
5.1 Pretraživanje . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
5.1.1 Pronalaženje karaktera u stringu . . . . . . . . . . . . . . 39
5.1.2 Odredivanje maksimuma . . . . . . . . . . . . . . . . . . . 39
5.1.3 Linearno pretraživanje . . . . . . . . . . . . . . . . . . . . 40
5.1.4 Binarno pretraživanje . . . . . . . . . . . . . . . . . . . . 42
5.2 Sortiranje . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
5.2.1 Sortiranje selekcijom . . . . . . . . . . . . . . . . . . . . . 50
5.2.2 Sortiranje umetanjem . . . . . . . . . . . . . . . . . . . . 51
5.2.3 Babl sortiranje . . . . . . . . . . . . . . . . . . . . . . . . 52
5.2.4 Quick sort . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
5.2.5 Korišćenje sistemske implementacije quick sort-a . . . . . 56
5.3 Jednostavni numerički algoritmi . . . . . . . . . . . . . . . . . . . 58
5.3.1 Stepenovanje . . . . . . . . . . . . . . . . . . . . . . . . . 58
5.3.2 Izračunavanje vrednosti polinoma . . . . . . . . . . . . . . 59
5.3.3 Zagradivanje nula funkcije . . . . . . . . . . . . . . . . . . 59
5.3.4 Odredivanje nula funkcije . . . . . . . . . . . . . . . . . . 61

II Dinamički objekti 65
6 Pokazivači i adresna aritmetika 67
6.1 Adresna aritmetika . . . . . . . . . . . . . . . . . . . . . . . . . . 67
6.2 Višedimenzioni nizovi . . . . . . . . . . . . . . . . . . . . . . . . 68
6.3 Inicijalizacija nizova pokazivača . . . . . . . . . . . . . . . . . . . 70
6.4 Pokazivači i višedimenzioni nizovi . . . . . . . . . . . . . . . . . . 70
6.5 Pokazivači na funkcije . . . . . . . . . . . . . . . . . . . . . . . . 71

7 Dinamička alokacija memorije 75


7.1 Funkcije malloc i calloc . . . . . . . . . . . . . . . . . . . . . . 75
7.2 Funkcija realloc . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
7.3 Curenje memorije (memory leaking) . . . . . . . . . . . . . . . . 79
7.4 Druge česte greške . . . . . . . . . . . . . . . . . . . . . . . . . . 79
7.5 Fragmentisanje memorije . . . . . . . . . . . . . . . . . . . . . . 80
7.6 Implementacija primitivnog alokatora memorije . . . . . . . . . . 81
7.7 Hip . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
7.8 Veličina steka i hipa . . . . . . . . . . . . . . . . . . . . . . . . . 83
7.9 Heap u Win32 sistemima . . . . . . . . . . . . . . . . . . . . . . 84
7.10 Demonstracija rada debagera . . . . . . . . . . . . . . . . . . . . 85

8 Dinamičke strukture: liste i stabla 87


8.1 Liste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
8.1.1 Stek (LIFO lista) . . . . . . . . . . . . . . . . . . . . . . . 89
8.1.2 Red (FIFO lista) . . . . . . . . . . . . . . . . . . . . . . . 89
SADRŽAJ 5

8.1.3 Dvostruko povezane (dvostruko ulančane) liste . . . . . . 91


8.1.4 Kružne (ciklične, cirkularne) liste . . . . . . . . . . . . . . 92
8.1.5 Liste: primer . . . . . . . . . . . . . . . . . . . . . . . . . 93
8.2 Stabla . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
8.2.1 Binarna stabla . . . . . . . . . . . . . . . . . . . . . . . . 95
8.2.2 Uredena binarna stabla . . . . . . . . . . . . . . . . . . . 95
8.2.3 Izrazi u formi stabla . . . . . . . . . . . . . . . . . . . . . 101

III Principi razvoja programa 103


9 Strukturna dekompozicija i druga načela pisanja programa 105
9.1 Pisanje čitljivih programa: vizualni elementi programa . . . . . . 106
9.1.1 80 karaktera u liniji . . . . . . . . . . . . . . . . . . . . . 106
9.1.2 Broj naredbi po liniji . . . . . . . . . . . . . . . . . . . . . 107
9.1.3 Nazubljivanje/uvlačenje teksta . . . . . . . . . . . . . . . 108
9.2 Imenovanje promenljivih i funkcija . . . . . . . . . . . . . . . . . 108
9.3 Komentari . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
9.3.1 Komentari ne treba da objačnjavaju kôd . . . . . . . . . . 110
9.3.2 Komentari treba da su takvi da ih je moguće održavati . . 110
9.3.3 Komentari treba da budu koncizni . . . . . . . . . . . . . 110
9.3.4 Korišćenje specifičnih komentara . . . . . . . . . . . . . . 110
9.4 Pisanje programa: modularnost i podela na datoteke . . . . . . . 111
9.4.1 Deljenje programa u više datoteka . . . . . . . . . . . . . 112
9.4.2 Modularnost . . . . . . . . . . . . . . . . . . . . . . . . . 112
9.4.3 Kako koristiti konstante u programu . . . . . . . . . . . . 112
9.5 Pisanje programa: dizajniranje programa . . . . . . . . . . . . . 113
9.5.1 Dizajn u vidu tokovnika . . . . . . . . . . . . . . . . . . . 113
9.5.2 Funkcijski-orijentisan dizajn . . . . . . . . . . . . . . . . . 114
9.5.3 Strukturna dekompozicija . . . . . . . . . . . . . . . . . . 115
9.5.4 Strukturalni model sistema . . . . . . . . . . . . . . . . . 118

10 Programi koji se sastoje od više datoteka 121


10.1 Povezivanje . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
10.1.1 Kvalifikator static . . . . . . . . . . . . . . . . . . . . . 121
10.1.2 Kvalifikator extern . . . . . . . . . . . . . . . . . . . . . 122
10.1.3 Povezivanje više programskih datoteka . . . . . . . . . . . 122
10.2 Uključivanje datoteka . . . . . . . . . . . . . . . . . . . . . . . . 124
10.3 Uslovna uključivanja . . . . . . . . . . . . . . . . . . . . . . . . . 125

11 Uvod u prevodenje programskih jezika: poredjenje interpretera


i kompilatora; faze u prevodjenju 127
11.1 Implementacija programskih jezika . . . . . . . . . . . . . . . . . 127
11.2 Kratka istorija razvoja kompilatora . . . . . . . . . . . . . . . . . 128
11.3 Moderni kompilatori . . . . . . . . . . . . . . . . . . . . . . . . . 128
11.4 Struktura kompilatora . . . . . . . . . . . . . . . . . . . . . . . . 128
6 SADRŽAJ

11.5 Leksička analiza . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130


11.6 Sintaksna analiza . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
11.7 Primer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131

IV Socijalni aspekti informatike 133


12 Istorijski i društveni kontekst računarstva kao naučne disci-
pline; društveni značaj računara i Interneta; profesionalizam,
etički kodeks; autorska prava; intelektualna svojina, softverska
piraterija 135
12.1 Istorijski i društveni kontekst računarstva . . . . . . . . . . . . . 135
12.2 Profesionalizam i etički kodeks . . . . . . . . . . . . . . . . . . . 136
12.3 Rizici i pouzdanost računarskih sistema . . . . . . . . . . . . . . 138
12.4 Intelektualna svojina i autorska prava . . . . . . . . . . . . . . . 138
12.5 Privatnost i gradanske slobode . . . . . . . . . . . . . . . . . . . 138
12.6 Računarski kriminal . . . . . . . . . . . . . . . . . . . . . . . . . 139
12.7 Ekonomski aspekti računarstva . . . . . . . . . . . . . . . . . . . 139
Deo I

Osnovi algoritmike
Glava 1

Algoritmi i rešavanje
problema

1.1 Algoritmi — intuitivni i formalni pojam


Pojam algoritma1 pripada osnovnim pojmovima matematike. Intuitivno, al-
goritam je precizno opisana procedura za rešavanje nekog matematičkog prob-
lema u konačnom broju koraka, koja često uključuje ponavljanje neke od op-
eracija; ili šire, korak po korak odredena procedura za rešavanje problema ili
dolaženje do nekog cilja.
Pojedina operacija algoritma naziva se algoritamski korak, a odredeni re-
dosled podrazumeva preciznu informaciju o tome koji je algoritamski korak
prvi, koji je poslednji (kojim se algoritam završava), i koji algoritamski ko-
rak sledi za izabranim. Redosled izvršavanje algoritamskih koraka pri jednom
izvršavanju algoritma odreduje njegovu algoritamsku strukturu. Algoritamska
struktura može biti linijska, kada se posle jednog algoritamskog koraka može
izvršavati samo algoritamski korak koji se još nije izvršavao pri tom izvršavanju
algoritma, i ciklična, kada se posle jednog algoritamskog koraka može izvršavati
i algoritamski korak koji se već izvršavao pri tom izvršavanju algoritma. U
linijskoj algoritamkoj strukturi, dakle, svaki algoritamski korak izvršava se na-
jviše jedanput pri jednom izvršavanju algoritma, dok se kod ciklične algorita-
mske strukture jedan algoritamski korak može izvršavati i više puta pri jednom
izvršavanju algoritma.
Formalno se algoritam definiše u terminima matematičkih formalizama kao
što su Tjuringova mašina ili rekurzivne funkcije ili Markovljevi algoritmi.
Čerčova teza tvrdi (to se ne može dokazati) da je intuitivni pojam algoritma
ekvivalentan formalnom pojmu algoritma.
Izgradnja algoritma pored same konstrukcije algoritma, uključuje i dokazi-
vanje korektnosti, analiza efikasnosti i implementaciju (u nekom programskom
jeziku).
1 Ovo poglavlje zasnovano je na tekstu “Algoritmi” prof. dr Gordane Pavlović-Lažetić.
10 1 Algoritmi i rešavanje problema

Problemi koji mogu da nastanu u svakoj od ovih faza utiču na modifikaciju i


ponovno izvršenje prethodnih faza. Algoritmi se mogu zapisati u različitim no-
tacijama — prirodnim jezikom, dijagramima toka, pseudojezikom, programskim
jezikom, itd.

1.2 Rešavanje problema uz pomoć računara


Rešavanja problema uz pomoć računara podrazumeva konstruisanje (pisanje)
programa na nekom programskom jeziku, čijim izvršavanjem će se rešavati svaki
pojedinačni zadatak tog problema. Da bi se program proizveo, potrebno je
odabrati metodu za rešavanje problema, poznavati (ili razviti) algoritam za
rešavanje problema, poznavati programski jezik, uveriti se u korektnost pro-
grama njegovim izvršavanjem za razne kombinacije ulaznih podataka. Jedini
jezik na kome računar može da izvršava program jeste mašinski jezik tog računara.
To je jezik najnižeg nivoa, u kome su sve ”naredbe”(tzv. instrukcije) i po-
daci predstavljeni binarnom azbukom, i koji je zbog toga neodgovarajući za
čoveka. Da bi se program izvršavao, potrebno je imati programske alate koji će
omogućiti rad sa našim programom, od unošenja u računar, preko prevodenja na
mašinski jezik, do testiranja programa i otkrivanja grešaka. Ako su u programu
otkrivene greške, program se ”ispravlja”(ponavlja se faza pisanja programa),
a ako je potrebno, primenjuje se (ili razvija) drugi algoritam, odnosno menja
metoda rešavanja problema. Pošto se izradi korektan i efikasan program za
rešavanje zadatog problema, on se primenjuje kraće ili duže vreme, uz eventu-
alne povremene modifikacije koje odgovaraju promenama u postavci problema.
Dakle, u rešavanju problema uz pomoć računara javlja se veći broj poslova koje
treba obaviti. Poslovi koji se javljaju u procesu rešavanja problema uz pomoć
računara mogu se, koncepcijski, podeliti u nekoliko faza, koje čine životni ciklus
programa. Zivotni ciklus programa je vreme od početka razvoja programa (od
početka rešavanja problema uz pomoć računara), do kraja korišćenja programa,
a sam termin asocira na ponovljeno (ciklično) izvršavanje tih faza, što i odgovara
prirodi ovog procesa. Te faze su sledeće:

1. specifikovanje problema

2. projektovanje (eventualno sa analizom složenosti rešenja)

3. realizacija

4. testiranje (i, eventualno, dokazivanje korektnosti)

5. izrada dokumentacije

6. eksploatacija i održavanje

Specifikovanje problema je postupak opisivanja problema za koji se traži


programsko rešenje. Specifikacijom je potrebno što preciznije opisati problem,
prirodu ulaznih podataka i oblik u kome se žele rešenja — izlazni rezultati.
1.2 Rešavanje problema uz pomoć računara 11

Specifikacija programa bavi se pitanjem šta program treba da uradi, kojom


brzinom, koja mu je maksimalna dozvoljena veličina, itd. Specifikacija se može
izraziti raznim jezicima sa različitim sintaksama. Mada postoje i posebni jezici
za pisanje formalne specifikacije, ona se može (u jednostavnijim slučajevima)
izraziti i prirodnim jezikom, npr. šmestiti u z proizvod brojeva a i b, pod
pretpostavkom da su a i b — celi brojevi i a ≥ 0, b ≥ 0.

Projektovanje predstavlja razumevanje problema, izradu matematičkog mod-


ela i izbor ili razvoj/konstrukciju odgovarajućeg algoritma.

Realizacija predstavlja implementaciju, ili ostvarenje rešenja. Ona uključuje


detaljnu razradu algoritma, izbor struktura podataka i medija na kojima će se
podaci držati (sa kojih će se unositi ili na koje će se izdavati), izradu sintak-
sno ispravnog programa na izabranom programskom jeziku (programa koji je
napisan prema pravilima kojima je definisan taj jezik). Da bi se utvrdila sin-
taksna ispravnost programa, potrebno je uneti program u računar (npr. preko
terminala, korišćenjem programa — editora), prevesti program na mašinski jezik
— pomoću programa — prevodioca (pri čemu se ispituje sintaksna ispravnost)
i izvršiti ga (npr. uz pomoć programa za otkrivanje grešaka — debagera), ili ga
interpretirati (analizirati sintaksnu ispravnost programa i prevoditi ga deo po
deo uz istovremeno izvršavanje analiziranih delova). Ako se pri prevodenju pro-
grama ili njegovom interpretiranju i izvršavanju ustanovi neka greška, potrebno
je ispraviti program, ponovo ga prevesti i izvršiti.

Testiranje predstavlja, pre svega, proces utvrdivanja semantičke ispravnosti


programa, tj. uveravanja, u što je moguće većem stepenu, da program izvršava
željenu funkciju, tj. da odgovara specifikaciji, tj. da rešava postavljeni prob-
lem. Ukoliko je formalno dokazana korektnost programa, ili je program razvijen
formalnim metodama iz formalne specifikacije, za njega se može garantovati
da je semantički korektan, do na tipografske greške. Tada je testiranje maksi-
malno pojednostavljeno ili nepotrebno. U svim drugim slučajevima, testiranje
je važna faza razvoja programa. Testiranje se obavlja višestrukim izvršavanjem
programa za raznovrsne pripremljene ulazne podatke, za koje su nam poznati
očekivani rezultati. Značajno je da pripremljeni ulazni podaci budu dobro
odabrani tako da se za njihove razne kombinacije, pri raznim izvršavanjima pro-
grama, izvršavaju (testiraju) sve grane programa. Takvi ulazni podaci nazivaju
se test primeri, i oni omogućuju proveru ispravnosti u što je moguće većoj meri,
tako da program ima veliku verovatnoću ispravnosti u primeni. Ovde je bitno
naglasiti da se test primerima nikako ne može dokazati korektnost programa,
ali se možemo, u visokom stepenu, uveriti da je program ispravan. Pri testi-
ranju programa mogu se otkriti greške pri izvršavanju (npr. deljenje nulom),
koje otkriva i prijavljuje sam računar tj. njegov operativni sistem, ili semantičke
greške koje može otkriti samo čovek programer, poredenjem dobijenih rezultata
izvršavanja programa sa očekivanim rezultatima. To mogu biti greške nastale pri
pisanju programa (npr. umesto uvećanja promenljive i za 1 omaškom smo naveli
12 1 Algoritmi i rešavanje problema

uvećanje promenljive i za 10 u sintaksi C-a, i+ = 10), ali to mogu biti i greške


u algoritmu (npr. sa brojačem ciklusa, i, pošli smo od vrednosti 0 umesto od
vrednosti 1), ili greške u samom matematičkom modelu ili primenjenoj metodi
(npr. za rešavanje sistema nehomogenih algebarskih jednačina primenjena je
metoda kojom se rešava samo sistem homogenih algebarskih jednačina). Ako
je greška ustanovljena na nivou programa, potrebno je vratiti se na korak re-
alizacije. Ako je greška ustanovljena na nivou algoritma ili metode (modela),
potrebno je vratiti se na fazu projektovanja, pri čemu se ponavljaju sve naredne
faze. Pri testiranju programa pomaže izdavanje tekućih vrednosti promenljivih
na karakterističnim mestima u programu, ili testiranje ulaznih podataka (tzv.
logička kontrola podataka) da bismo se uverili da će program, kao ispravne
ulazne podatke, prihvatiti samo podatke odredenog (predvidenog) tipa, oblika
i opsega.
Debager je alat za praćenje izvršavanja programa radi otkrivanja grešaga
(bagova, eng. bugs). To je program napravljen da olakša detektovanje, lociranje
i ispravljanje grešaka u drugom programu. On omogućava programeru da ide
korak po korak kroz izvršavanje programa, prati sadržaj memorije, vrednosti
promenljivih i druge elemente programa.

Izrada dokumentacije Kada je program dobro i uspešno istestiran, pris-


tupa se izradi dobre, pregledne i detaljne dokumentacije. Dokumentacija je
neophodna za uspešno korišćenje programa, i posebno za fazu održavanja pro-
grama koja prati njegovo korišćenje. Dokumentacija treba da sadrži specifikaciju
problema, algoritam (globalni i detaljni), čitljivo napisan program (uz dobru
meru komentara), način zadavanja ulaznih podataka i način izdavanja rezultata,
značenje korišćenih imena (promenljivih, datoteka, programa, potprograma,
itd.), rezultate testiranja, test primere, uputstva za korišćenje i održavanje pro-
grama. Eksploatacija i održavanje je faza korišćenja programa koja može da
traje i duži niz godina. Održavanje programa podrazumeva izvesne modifikacije
programa u toku njegove eksploatacije, koje su posledica promena samog prob-
lema u toku vremena, ili potrebe da se kvalitet programa (prostorna i vremenska
efikasnost, primenljivost) poveća. Održavanje programa je lakše ako je program
preglednije napisan i ako je dokumentacija kompletnija, posebno zbog toga što
održavanje programa po pravilu ne vrši lice koje je program pisalo. Svaku
izmenu u programu nastalu u fazi održavanja programa potrebno je takode
dokumentovati, tj. pored održavanja programa potrebno je održavati i njegovu
dokumentaciju. Postoje alati koji olakšavaju kreiranje dokumentacije i delom
je generišu automatski (npr. Doxygen).
Glava 2

Ispitivanje ispravnosti
programa

Centralno pitanje u razvoju programa je ispitivanje njegove ispravnosti (ko-


rektnosti).1 Ukoliko neki program nije ispravan, tj. ukoliko u nekim situaci-
jama on daje pogrešan rezultat, tada taj program ne samo da je nekoristan vec
može da bude i veoma štetan. Problem neispravnih programa je tokom posled-
njih decenija postao jedno od kljucnih pitanja u mnogim poslovnim granama.
Znacaj ispravnosti softvera ogleda se ne samo u finansijskim troškovima vec,
pre, u situacijama u kojima od pojedinog programa može da zavisi ljudski život.
Takvi su, na primer, programi koji upravljaju medicinskim aparatima, saobra-
cajem, kosmickim letilicama i slicno.
Postupak pokazivanja da je program ispravan naziva se verifikacija. U razvi-
janju tehnika verifikacije programa, potrebno je najpre precizno formulisati po-
jam ispravnosti programa. Ispravnost programa pociva na pojmu specifikacije.
Specifikacija je, neformalno, opis ponašanja programa koji treba napisati.
Verifikacija može biti formalna i neformalna.
U praksi se formalni dokaz ispravnosti programa retko izvodi i to često dovodi
do neispravnog softvera i mnogih problema.

2.1 Neformalno ispitivanje ispravnosti programa


Neformalna verifikacija programa zasniva se na testiranju. Specifikacija pro-
grama je proveriva testovima ako postoji razumno ocekivanje da se eksperimen-
talno može pokazati da program zadovoljava ili ne zadovoljava zadate uslove.
Na primer, tvrdenje ,,program ima prosecno vreme izvršavanja 0.5 sekundi“
je proverivo testovima kao i tvrdenje ,,prosecno vreme izmedu dva pada pro-
grama je najmanje 8 sati sa verovatnocom 95%“. Tvrdenje ,,prosecno vreme
1 Ovo poglavlje zasnovano je na tekstu “Verifikacija programa” Milene Vujošević-Janičić i
Jelene Tomašević.
14 2 Ispitivanje ispravnosti programa

izvršavanja programa je dobro“ suviše je neodredeno da bi moglo da bude testi-


rano. Primetimo da je, na primer, tvrdenje ,,prosecno vreme izmedu dva pada
programa je najmanje 8 godina sa verovatnocom 95%“ u principu proverivo
testovima ali nije praktično izvodivo.

2.2 Formalno ispitivanje ispravnosti programa


Formalna verifikacija se ne zasniva na testiranju i eksperimentima, vec na
matematickim dokazima. Korektnost programa je dokaziva ako se u okviru
nekog formalnog deduktivnog sistema može izvesti matematicki dokaz da pro-
gram zadovoljava zadatu specifikaciju. Matematicki dokaz je izvodenje neke
formule iz skupa aksioma korišcenjem zadatih pravila izvodenja.

2.2.1 Formalna specifikacija


Formalna specifikacija problema je zapis oblika

{ϕ}P {ψ}

gde su {ϕ} i {ψ} logički izrazi, tvrdenja čija je vrednost ili tačno ili netačno, a
P je algoritam (tj. program). Trojku (ϕ, P, ψ) nazivamo Horova trojka.
Interpretacija ovog zapisa specifikacije programa je sledeća: ”Ako izvršenje
programa P počinje sa vrednostima ulaznih promenljivih (”u stanju”) koje zado-
voljavaju uslov {ϕ}, onda se garantuje da će se P završiti u konačnom vremenu
sa vrednostima programskih promenljivih (”u stanju”) koje zadovoljavaju uslov
{ψ}”. Uslov {ϕ} naziva se preduslov za algoritam (program, iskaz) P , a uslov
{ψ} naziva se postuslov (pauslov, posleuslov) za algoritam (program, iskaz) P .

2.2.2 Razvoj programa u terminima Horovih trojki


Koristeci notaciju Horovih trojki, razvoj softvera može da se razmatra kao
proces koji se sastoji iz tri dela:

• Prevodenje zahteva korisnika u specifikaciju {ϕ}P {ψ};

• Pisanje programa P koji zadovoljava konstruisanu specifikaciju;

• Dokazivanje da važi {ϕ}P {ψ}.

2.2.3 Primeri Horovih trojki


Primer 2.1. Primeri Horovih trojki:

{true}x := 12; {x = 12}

{x < 40}x := x + 1; {x = 40}


2.2 Formalno ispitivanje ispravnosti programa 15

{m = n}j := (m + n)/2; {m = j = n}

Primer 2.2. {X = 1}Y := X; {Y = 1}


U ovom primeru preduslov je da je X jednako 1, a pauslov da je Y jednako
1. Program se sastoji od naredbe dodele, vrednost promenljive Y postaje X. Ako
se naredba dodele izvršava nakon što je ispunjen preduslov i ako se izvršavanje
naredbe dodele završi, tada ce nakon izvršenja naredbe biti ispunjen i pauslov.
U ovom primeru specifikacija je zadovoljena.

Primer 2.3. {X = 1}Y := X; {Y = 2}


U ovom primeru preduslov je da je X jednako 1, a pauslov da je Y jed-
nako 2. Program se sastoji od naredbe dodele, vrednost promenljive Y postaje
X. Ako se naredba dodele izvršava nakon što je ispunjen preduslov i ako se to
izvršavanje zaustavi, tada nakon izvršenja naredbe nece biti ispunjen pauslov. U
ovom slucaju specifikacija nije zadovoljena.

2.2.4 Primer dokazivanja ispravnosti sekvencijalnog pro-


grama
Primer 2.4 (Algoritam razmene vrednosti dveju promenljivih — swap). Ovaj
primer ilustruje algoritam za koji se dokazuje da odgovara zadatoj specifikaciji.
Reč je o razmeni vrednosti dve promenljive, x i y.
Za swap algoritam sledećeg oblika:
swap : t := x; x := y; y := t,
važi

{x = X ∧ y = Y }swap{x = Y ∧ y = X}
Interpretacija ovog zapisa je da
∀X, Y, x, y{x = X ∧ y = Y }swap{x = Y ∧ y = X}.
Tačnost ovog tvrdenja sledi iz sledećeg razmatranja:
{x = X ∧ y = Y }t := x{x = X ∧ y = Y ∧ t = X}
{x = X ∧ y = Y ∧ t = X}x := y{x = Y ∧ y = Y ∧ t = X}
{x = Y ∧ y = Y ∧ t = X}y := t{x = Y ∧ y = X ∧ t = X}

2.2.5 Dokazivanje ispravnosti programa koji sadrže petlje


i metoda induktivne invarijante
Metoda induktivne invarijante je dominantan pristup formalnoj verifikaciji
programa i do sada je dao najbolje rezultate, pa cemo se u nastavku baviti samo
njom. Ova metoda verifikacije programa se cesto nazova Flojd-Horov2 (Floyd-
2 Charles Antony Richard Hoare (Tony Hoare), britanski informaticar. Roden je 1934.

godine u Kolombu (Šri Lanka), a diplomirao na univerzitetu u Oksfordu 1956. godine. Kon-
struisao je, 1960. godine, algoritam za sortiranje quicksort, jedan od najkorišcenijih algori-
tama uopšte. Razvio je tzv. Horovu logiku i formalni jezik csp za specifikovanje konkurentnih
procesa. Godine 1980. dobio je Tjuringovu nagradu za ,,svoje fundamentalne doprinose
16 2 Ispitivanje ispravnosti programa

Hoare) metoda induktivnih tvrdenja i ima svoje korene u klasicnim radovima


Goldstajna (Goldstine) i Fon Nojmana (von Neumann).
Korektnost programa P u odnosu na specifikaciju {ϕ}P {ψ} može biti:

• parcijalna korektnost — ako se P zaustavi i ako za ulazne vrednosti važi


{ϕ}, tada ce važiti tvrdenje ψ;

• totalna korektnost — P će se zaustaviti za ulazne vrednosti za koje važi


{ϕ}, i važiće tvrdenje ψ.

Ukoliko se ne naglasi drugacije, koristicemo pojam Horove trojke u smislu


parcijalne korektnosti.
Za ispitivanje ispravnosti programa posebnu pažnju potrebno je posvetiti
petljama jer se kroz njih može proci razlicit broj puta u razlicitim izvršavanjima
programa.

Definicija 1.
Svojstvo koje važi svaki put kada se ispituje da li je zadovoljen uslov ulaska u
petlju naziva se invarijanta petlje.

Svaka petlja ima puno invarijanti. Tako je, na primer, x*0=0 invarijanta
svake petlje. Medutim, korisna invarijanta je samo ona koja vodi ka pauslovu
kada uslov petlje više nije ispunjen.
Formalna verifikacija programa metodom induktivne invarijante zasniva se
na dokazu da se iz preduslova može uspostaviti invarijanta pre prvog ulaska u
petlju, zatim da svaki put kada se ulazi u petlju važi dati uslov i, na kraju, da
pri prestanku važenja uslova ulaska u petlju on vodi ka ostvarivanju pauslova
programa.
Dokaz ispravnosti programa metodom induktivne invarijante se, dakle, sas-
toji iz tri koraka:

• Uspostavljanje invarijante

• Održavanje invarijante

• Ostvarivanje željenog pauslova

Na kraju, da bi se dokazala totalna korektnost programa, treba dokazati i


da se program zaustavlja.
Formalni dokazi su interesantni jer mogu da se generišu automatski uz pomoc
racunara ili barem interaktivno u saradnji coveka sa racunarom. U oba slucaja,
formalni dokaz može da se proveri automatski, dok provera neformalnog dokaza
nije laka. Takode, kada se program i njegov dokaz istovremeno razvijaju, pro-
gramer bolje razume sam program i proces programiranja uopšte. Programeri su
zainteresovani za formalne dokaze jer metodologija dokaza utice na razmatranje
definiciji i dizajnu programskih jezika”. Poznate su sledece Horove reci: ,,Postoje dva puta za
dizajniranje softvera: jedan je napraviti ga tako jednostavnim da ocigledno nema nedostataka
i drugi - napraviti ga tako komplikovanim da nema ociglednih nedostataka.“
2.2 Formalno ispitivanje ispravnosti programa 17

preciznosti, konzistentnosti i kompletnsoti specifikacije, na jasnocu implement-


cije i konzistentnost implementacije i specifikacije. Zahvaljujuci tome dobija se
pouzdaniji softver, cak i onda kada se formalni dokaz ne izvede eksplicitno.
Za neki program može se dokazati tvrdenje ”program kao rezultat vraca
faktorijel ulazne vrednosti“. U nekim slucajevima može se dokazati tvrdenje
,,program ima prosecno vreme izvršavanja 0.5 sekundi“. Tvrdenja ,,prosecno
vreme izmedu dva pada programa je najmanje 8 sati sa verovatnocom 95%“ i
,,prosecno vreme izvršavanja programa je dobro ne mogu se formalno dokazati.

2.2.6 Primer dokazivanja korektnosti programa koji sadrži


petlju
Konstruisati algoritam koji množi dva nenegativna cela broja x i y korišćenjem
operatora sabiranja (ne i množenja) i smešta rezultat u promenljivu z. Dokazati
korektnost algoritma.
{
z = 0;
n = 0;
while (n < x)
{
z = z + y;
n = n + 1;
}
}
Navedeni algoritam P treba da zadovoljava uslov:
{x ≥ 0, y ≥ 0}P {z = x ∗ y}
Da bismo dokazali korektnost ovog algoritma, dokažimo da je relacija
{z = n ∗ y; n ≤ x}
invarijanta while petlje algoritma:
induktivna osnova: pre prvog ulaska u petlju važi
z = 0 = 0 ∗ y = n ∗ y, n=0≤x
pa su relacije z = n ∗ y i n ≤ x tačne.
induktivni korak: Pretpostavimo da je relacija z = n ∗ y; n ≤ x bila tačna
pre jednog ulaska u blok naredbi u okviru while petlja i dokažimo da će
biti tačna i na kraju izvršavanja tog bloka. Neka su n0 i z 0 vrednosti
promenljivih n i z nakon tog izvršavanja bloka naredbi. Tada važi
z 0 = z + y = n ∗ y + y = (n + 1) ∗ y = n0 ∗ y
Dakle, važi z 0 = n0 ∗ y (naglasimo da je veza z = n ∗ y induktivna pret-
postavka).
18 2 Ispitivanje ispravnosti programa

Kako je bio ispunjen uslov za ulazak u petlju, važi n < x, pa je n0 =


n + 1 ≤ x.
Dakle, važi z 0 = n0 ∗ y; n0 ≤ x, što je i trebalo dokazati.

U svakoj iteraciji, vrednost promenljive n se uvećava za 1, pa će u konačnom


broju koraka biti dostignuta vrednost x. Tada neće biti zadovoljen uslov ulaska
u while petlju i program će završiti rad. Tada će promenljiva n imati vrednost
x, a na osnovu dokazane invarijante petlje važiće:

z =n∗y =x∗y

što je i trebalo dokazati.


Glava 3

Složenost algoritama

Razmatra se

• vremenska složenost algoritma;

• prostorna (memorijska) složenost algoritma.

Primer 3.1. Izračunavanje vrednosti faktorijela prirodnog broja zahteva n poredenja


i n−1 množenje; to je ukupno 2n−1 koraka, te je vremenska složenost algoritma
linearna.

Klasu algoritama koji imaju linarnu složenost označavamo sa O(n).


Često se algoritmi ne izvršavaju isto za sve ulaze istih veličina, pa je potrebno
naći način za opisivanje i poredenje efikasnosti različitih algoritama. Anal-
iza najgoreg slučaja zasniva procenu složenosti algoritma na najgorem slučaju
(na slučaju za koji se algoritam najduže izvršava). Ta procena može da bude
varljiva, ali ne postoji bolji opšti način za poredenje efikasnosti algoritama. Čak
i kada bi uvek bilo moguće izračunati prosečno vreme izvršavanja algoritma, i
takva procena bi često mogla da bude varljiva.
Ako algoritam u najgorem slučaju zahteva an2 + bn + c koraka, onda kažemo
da je on kvadratne složenosti i da pripada klasi O(n2 ).
Aditivne i multiplikativne konstante ne utiču na klasu kojoj funkcija pripada.

Primer 3.2. Važi:

• n2 = O(n2 )

• 3 ∗ n2 + 10 = O(n2 )

3.1 Osnove analize algoritama: ,,O“ notacija;


klase složenosti
Svojstva programa:
20 3 Složenost algoritama

• zaustavljanje1
• korektnost2
• kompleksnost (složenost)
– vremenska
– prostorna (memorijska)
Primer 3.3. Izračunavanje vrednosti faktorijela prirodnog broja: n poredjenja
i n − 1 množenje; linearna složenost.
Primer 3.4. Odredjivanje najmanjeg elementa u nizu od n prirodnih brojeva:
linearna složenost.
Primer 3.5. Sortiranje niza od n prirodnih brojeva odredjivanjem najmanjeg u
tekućem nizu: n(n − 1)/2 vremenskih jedinica (kvadratna složenost).

3.2 Red algoritma; analiza najgoreg slučaja; O


notacija
Definicija 2.
Ako postoje pozitivna konstanta c i prirodan broj N takvi da za funkcije f i g
nad prirodnim brojevima važi

f (n) ≤ c · g(n) za sve vrednosti n veće od N

onda pišemo
f = O(g)
i čitamo “f je veliko ‘o’ od g”.
NOTA BENE: O nije funkcija; O označava klasu funkcija.
Aditivne i multiplikativne konstante ne utiču na klasu kojoj funkcija pripada.
Primer 3.6. Važi:
• n2 = O(n2 )
• n2 + 10 = O(n2 )
• 10 · n2 + 10 = O(n2 )
• 10 · n2 + 8n + 10 = O(n2 )
• n2 = O(n3 )
1 Tjuring je tridesetih godina dvadesetog veka dokazao da ne postoji opšti postupak kojim

se za proizvoljan program može utvrditi da li se zaustavlja (to je tzv. halting problem). Dakle,
postoje funkcije koje nisu izračunljive.
2 Videti poglavlje o korektnosti programa u skripti za kurs Programiranje I.
3.3 Izračunavanje složenosti algoritama 21

• 2n = O(2n )
• 2n + 10 = O(2n )
• 10 · 2n + 10 = O(2n )
• 2n + n2 = O(2n )
• 3n + 2n = O(3n )
• 2n + 2n n = O(2n n)
Definicija 3.
Ako je T (n) vreme izvršavanja algoritam A (čiji ulaz karakteriše prirodan broj
n), ako važi T = O(g) i ako u najgorem slučaju vrednost T (n) dostiže vrednost
c · g(n) (gde je c pozitivna konstanta), onda kažemo da je algoritam A složenosti
(ili reda) g i da pripada klasi O(g).
Često se algoritmi ne izvršavaju isto za sve ulaze istih veličina, pa je potrebno
naći način za opisivanje i poredjenje efikasnosti različitih algoritama. Anal-
iza najgoreg slučaja zasniva procenu složenosti algoritma na najgorem slučaju
(na slučaju za koji se algoritam najduže izvršava). Ta procena može da bude
varljiva, ali ne postoji bolji opšti način za poredjenje efikasnosti algoritama. Čak
i kada bi uvek bilo moguće izračunati prosečno vreme izvršavanja algoritma, i
takva procena bi često mogla da bude varljiva. Analiziranje najboljeg slučaja,
naravno, ima još manje smisla.
Primer 3.7. Algoritam za izračunavanje vrednosti faktorijela prirodnog broja
je reda n, tj. pripada klasi O(n), tj. on je linearne složenosti.
Primer 3.8. Algoritam za sortiranje niza od n prirodnih brojeva odredjivanjem
najmanjeg u tekućem nizu je reda n2 , tj. pripada klasi O(n2 ), tj. on je kvadratne
složenosti jer važi
n(n − 1) 1
≤ n2 za sve vrednosti n ≥ 1.
2 2
i u najgorem slučaju (za ovaj algoritam svi se slučajevi mogu smatrati najgorim)
dostiže vrednost koja kvadratno zavisi od n.

3.3 Izračunavanje složenosti algoritama


Primer 3.9. Neka je T (n) vreme izvršavanja algoritma A za izračunavanje
faktorijela broja n. Važi T (1) = 1 (tj. za n = 1 algoritam utroši samo jednu
vremensku jedinicu) i
T (n) = T (n − 1) + 2
(nakon izračunavanja vrednosti (n−1)! algoritam treba da izvrši još jedno pored-
jenje i još jedno množenje; smatramo da i poredjenje i množenje troše po jednu
vremensku jedinicu; možemo da smatramo i da poredjenje i množenje zajedno
22 3 Složenost algoritama

troše jednu vremensku jedinicu ili c vremenskih jedinica – to ne utiče na rezultat


koji govori o redu algoritma A). Dakle,

T (n) = T (n − 1) + 2 = T (n − 2) + 2 + 2 = . . . = T (1) + 2 + . . . + 2 =
| {z }
n−1

= 1 + 2(n − 1) = 2n + 1 = O(n) ,
pa je algoritam A reda n (tj. pripada klasi O(n), tj. on je linearne složenosti).
Zadatak 1. Ako za vreme izvršavanja T (n) algoritma A (gde n odredjuje ulaznu
vrednost za algoritam) važi T (n) = T (n − 1) + n/2 i T (1) = 1, odrediti složenost
algoritma A.
Rešenje:

n n−1 n 2 n−1 n
T (n) = T (n − 1) + = T (n − 2) + + = T (1) + + . . . + + =
2 2 2 2 2 2
2
 
1 1 n(n + 1) n +n+2
= T (1) + (2 + . . . + n) = 1 + −1 = ,
2 2 2 4
pa je algoritam A kvadratne složenosti.
Zadatak 2. Ako za vreme izvršavanja T (n) algoritma A (gde n odredjuje ulaznu
vrednost za algoritam) važi T (n + 2) = 8T (n + 1) − 15T (n) (za n ≥ 1) i T (1) =
1, T (2) = 4, odrediti složenost algoritma A.
Rešenje:
Karakteristična jednačina za navedenu homogenu rekurentntu vezu drugog
reda je
t2 = 8t − 15
i njeni koreni su t1 = 3 i t2 = 5. Opšti član niza T (n) može biti izražen u obliku

T (n) = α · tn1 + β · tn2 ,

tj.
T (n) = α · 3n + β · 5n ,
Iz T (1) = 1, T (2) = 4 dobija se sistem

α·3+β·5=1

α · 9 + β · 25 = 4
čije je rešenje (α, β) = (1/6, 1/10). Dakle, važi
1 n 1
T (n) = 3 + 5n ,
6 10
pa je algoritam A reda O(5n ).
3.3 Izračunavanje složenosti algoritama 23

(Zaista, za npr. c = 1 važi


 
1 n 1 1 1 1 1
T (n) = 3 + 5n ≤ 5n + 5n ≤ + 5n ≤ 1 · 5n = c · 5n ,
6 10 6 10 6 10
pa je T (n) = O(5n ).)
Zadatak 3. Ako za vreme izvršavanja T (n) algoritma A (gde n odredjuje ulaznu
vrednost za algoritam) važi T (n + 2) = 4T (n + 1) − 4T (n) (za n ≥ 1) i T (1) =
6, T (2) = 20, odrediti složenost algoritma A.
Rešenje: Karakteristična jednačina za navedenu homogenu rekurentntu
vezu je
t2 = 4t − 4
i njen dvostruki koren je t1 = 2. Opšti član niza T (n) može biti izražen u obliku
T (n) = α · tn1 + β · n · tn2 .
tj.
T (n) = α · 2n + β · n · 2n .
Iz T (1) = 6, T (2) = 20 dobija se sistem
α·2+β·2=6
α · 4 + β · 8 = 20
čije je rešenje (α, β) = (1, 2), pa je T (n) = 2n + 2 · n · 2n , odakle sledi da je
T (n) = O(n · 2n ).
Zadatak 4. Odrediti složenost algoritma za rešavanje problema “kule Hanoja”.
Rešenje:
Važi T (1) = 1 i T (n) = 2T (n − 1) + 1 (i T (2) = 2T (1) + 1 = 3). Iz
T (n) − 2T (n − 1) = 1 = T (n − 1) − 2T (n − 2) (za n > 2) sledi T (n) =
3T (n − 1) − 2T (n − 2). Karakteristična jednačina ove veze je t2 = 3t − 2 i njeni
koreni su 2 i 1. Iz sistema
α·2+β·1=1
α·4+β·1=3
sledi α = 1 i β = −1, pa je
T (n) = 1 · 2n + (−1) · 1n = 2n − 1 ,
što znači da je algoritam reda O(2n ).
Zadatak 5. Algoritam A izvršava se za vrednost n (n > 1) primenom istog
algoritma za vrednost n − 1, pri čemu se za svodjenje problema koristi algoritam
B za vrednost n − 1. Algoritam B izvršava se za vrednost n (n > 1) trostrukom
primenom istog algoritma za vrednost n − 1, pri čemu se za svodjenje problema
koristi algoritam A za vrednost n − 1. Algoritmi A i B se za n = 1 izvršavaju
jednu vremensku jedinicu. Izračunati vreme izvršavanja algoritma A za ulaznu
vrednost n.
24 3 Složenost algoritama

Rešenje: Neka je A(n) vreme izvršavanja algoritma A za ulaznu vrednost n


i neka je B(n) vreme izvršavanja algoritma B za ulaznu vrednost n. Na osnovu
uslova zadatka važi:
A(1) = B(1) = 1 (3.1)

A(n) = A(n − 1) + B(n − 1) (3.2)

B(n) = 3B(n − 1) + A(n − 1) (n > 1) (3.3)


Iz jednakosti (2) imamo:

B(n) = A(n + 1) − A(n) (3.4)

B(n − 1) = A(n) − A(n − 1) (3.5)


pa, uvrštavanjem u jednakost (3), za n > 1 važi:

A(n + 1) − 4A(n) + 2A(n − 1) = 0 (3.6)

Pomoću ove rekurentne veze i početnih uslova: A(1) = 1 i A(2) = A(1)+B(1) =


2, možemo odrediti vrednost A(n). Karakteristična jednačina relacije (6) je:

x2 − 4x + 2 = 0
√ √
Njeni koreni su x1 = 2 + 2 i x2 = 2 − 2, pa je opšte rešenje rekurentne
jednačine (6) oblika:
√ √
A(n) = c1 (2 + 2)n + c2 (2 − 2)n

za neke c1 , c2 ∈ R. Konstante c1 i c2 odredjujemo pomoću početnih uslova,


rešavajući sledeći sistem linearnih jednačina po c1 i c2 :
√ √
1 = A(1) = c1 (2 + 2) + c2 (2 − 2)
√ √
2 = A(2) = c1 (2 + 2)2 + c2 (2 − 2)2
√ √
Dobija se da je c1 = 14 (2 − 2), a c2 = 14 (2 + 2), pa je konačno:

1 √ √
A(n) = ((2 + 2)n−1 + (2 − 2)n−1 )
2
Zadatak 6. Problem P ima parametar n (n je prirodan broj) i rešava se pri-
menom algoritama A i B. Algoritam A rešava problem za vrednost n (n > 1).
primenom algoritma B za vrednost n − 1, pri čemu se za svodjenje problema
troši n vremenskih jedinica. Algoritam B rešava problem za vrednost n (n > 1).
primenom algoritma A za vrednost n − 1, pri čemu se za svodjenje problema
troši n vremenskih jedinica. Problem za n = 1, algoritam A rešava trivijalno za
jednu vremensku jedinicu, a algoritam B za dve vremenske jedinice. Izračunati
vreme izvršavanja algoritma A za ulaznu vrednost n.
3.3 Izračunavanje složenosti algoritama 25

Rešenje:
Označimo sa an vreme izvršavanja algoritma A, a sa bn vreme izvršavanja
algoritma B za ulaznu vrednost n. Na osnovu uslova zadatka je:
a1 = 1
b1 = 2
an = bn−1 + n (n > 1)
bn = an−1 + n (n > 1)
Kako je bn−1 = an−2 + n − 1, zaključujemo da je
an = an−2 + n − 1 + n
Primenom matematičke indukcije dokažimo da za neparne vrednosti n važi
an = n(n+1)
2 .
Tvrdjenje važi za n = 1 jer je a1 = 1 = 1·2
2 .
Pretpostavimo da je tvrdjenje tačno za neki neparan broj n i dokažimo da
važi i za sledeći neparan broj - n + 2:
an+2 = bn+1 + n + 2 = an + n + 1 + n + 2
an+2 = n(n+1)2 +n+1+n+2
n(n+1)+2(n+1)+2(n+2)
an+2 = 2
(n+1)(n+2)+2(n+2)
an+2 = 2
(n+2)(n+1+2)
an+2 = 2
(n+2)(n+3)
an+2 = 2
Dakle, na osnovu principa matematičke indukcije tvrdjenje važi za sve neparne
brojeve.
Dokažimo da za parne vrednosti n važi an = n(n+1)
2 + 1.
2·3
Za n = 2 tvrdjenje je tačno: a2 = b1 + 2 = 2 + 2 = 2 + 1.
Pretpostavimo da je tvrdjenje tačno za neki paran broj n i dokažimo da je
tačno i za n + 2, tj. za sledeći paran broj:
an+2 = bn+1 + n + 2 = an + n + 1 + n + 2
an+2 = n(n+1)
2 +1+n+1+n+2
n(n+1)+2(n+1)+2(n+2)
an+2 = 2 + 1
(n+1)(n+2)+2(n+2)
an+2 = 2 +1
(n+2)(n+1+2)
an+2 = 2 +1
(n+2)(n+3)
an+2 = 2 +1
Dakle, tvrdjenje je tačno za sve parne brojeve.
Konačno imamo da je:
(
n(n+1)
2 +1 za n parno
an = n(n+1)
2 za n neparno

Teorema 1.
Rešenje rekurentne relacije

T (n) = aT (n/b) + cnk ,


26 3 Složenost algoritama

gde su a i b celobrojne konstante (a ≥ 1, b ≥ 1) i c i k pozitivne konstante je



 O(nlogb a ) , ako je a > bk
T (n) = O(nk log n) , ako je a = bk
O(nk ) , ako je a < bk

3.4 NP kompletnost
Primer 3.10. Šef protokola na jednom dvoru treba da organizuje bal za pred-
stavnike ambasada. Kralj traži da na bal bude pozvan Peru ili da ne bude pozvan
Katar (Qatar). Kraljica zahteva da budu pozvani Katar ili Rumunija (ili i Katar
i Rumunija). Princ zahteva da ne bude pozvana Rumunija ili da ne bude pozvan
Peru (ili da ne budu pozvani ni Rumunija ni Peru). Da li je moguće organizovati
bal i zadovoljiti zahteve svih članova kraljevske porodice?

Ako su p, q i r bulovske (logičke) promenljive (koje mogu imati vrednosti


true ili false, tj. > ili ⊥), navedeni problem može biti formulisan na sledeći
način: da li je zadovoljiv logički iskaz

(p ∨ ¬q) ∧ (q ∨ r) ∧ (¬r ∨ ¬p) .

Zadovoljivost navedenog iskaza može biti odredjena tako što bi bile ispitane sve
moguće interpretacije – sve moguće dodele varijablama p, q i r. Ako je u nekoj
interpretaciji vrednost datog logičkog iskaza true, onda je dati iskaz zadovoljiv.
Za izabranu, fiksiranu interpretaciju može se u konstantnom vremenu utvrditi
da li je istinitosna vrednost true. Za tri promenljive ima 2n interpretacija, pa
je red ovog algoritma za ispitivanje zadovoljivosti logičkih iskaza reda O(2n )
(algoritam je eksponencijalne složenosti). Pitanje je da li postoji algoritam koji
navedeni problem rešava u polinomijalnom vremenu.

Definicija 4.
Za algoritam sa ulaznom vrednošću n kažemo da je polinomijalne složenosti
ako je njegovo vreme izvršavanja O(P (n)) gde je P (n) polinom po n. Klasu
polinomijalnih algoritama označavamo sa P .

Definicija 5 Pojednostavljena definicija klase NP.


Ako neki problem može da se predstavi u vidu najviše eksponencijalno mnogo
instanci i ako za bilo koju instancu može da bude rešen u polinomijalnom vre-
menu, onda kažemo da problem pripada klasi N P .

Definicija 6 Formalna definicija klase NP.


Ako je U skup svih mogućih ulaznih vrednosti za neki problem odlučivanja (za
koji je potrebno dati odgovor da ili ne) i L ⊆ U skup svih ulaznih vrednosti
za koje je odgovor na problem potvrdan, onda L zovemo jezik koji odgovara
problemu.
Kažemo da nedetrministički algoritam prepoznaje jezik L ako važi: za zadate
ulazne vrednosti x, moguće je pretvoriti svaki nd-izbor koji se koristi tokom
3.4 NP kompletnost 27

izvršavanja algoritma u stvarni izbor takav da će algoritam prihvatiti x ako i


samo ako je x ∈ L.
Klasu svih problema za koje postoje nedeterministički algoritmi čije je vreme
izvršavanja je polinomijalne složenosti zovemo N P klasa.

Očigledno je da važi P ⊆ N P , ali se još uvek ne zna3 da li važi P = N P :

P = NP ?

Ako bi se pokazalo da neki problem iz klase N P nema polinomijalno rešenje,


onda bi to značilo da ne važi P = N P . Ako neki problem iz klase N P ima
polinomijalno rešenje, onda to još ne znači da važi P = N P . Za probleme iz
posebne potklase klase N P (to je klasa N P -kompletnih problema) važi da ako
neki od njih ima polinomijalno rešenje, onda važi P = N P .

Primer 3.11. Problem ispitivanja zadovoljivosti logičkih iskaza (SAT ) pripada


klasi N P , ali se ne zna da li pripada klasi P .

Definicija 7.
Za problem X kažemo da je N P -težak problem ako je svaki N P problem poli-
nomijalno svodljiv na X.

Definicija 8.
Za problem X kažemo da je N P -kompletan problem ako pripada klasi N P i
ako je N P -težak.

Teorema 2.
Ako bilo koji N P -težak problem pripada klasi P , onda važi P = N P .

Teorema 3.
Problem X je N P -kompletan ako

• X pripada klasi N P

• Y je polinomijalno svodljiv na X, gde je Y neki N P -kompletan problem.

Dakle, ako znamo da je neki problem N P -kompletan, onda na osnovu prethodne


teoreme možemo da pokažemo da su i svi N P -problemi na koje ga je moguće
svesti takodje N P -kompletni. Dugo se tragalo za pogodnim N P -kompletnim
problemom za koji bi moglo da se pronadje polinomijalno rešenje, što bi značilo
da važi P = N P . Zato je bilo važno otkriti što više N P -kompletnih prob-
lema. Postavljalo se pitanje da li uopšte postoji ijedan N P -kompletan problem
(korišćenjem prethodne teoreme može da se utvrdi da je neki problem N P -
kompletan samo ako se za neki drugi problem veće zna da je N P -kompletan).
Cook je 1971. godine dokazao (neposredno, koristeći formalizam Tjuringove
mašine) da je SAT problem N P -kompletan. U godinama koje su sledile za
3 Clay Institute for Mathematical Sciences is offering a one million dollar prize for a complete

polynomial-time SAT solver or a proof that such an algorithm does not exist (the P vs N P
problem).
28 3 Složenost algoritama

mnoge probleme je utvrdjeno da su takodje N P -kompletni (najčešće svodjen-


jem SAT problema na njih), ali ni za jedan od njih nije pokazano da pripada
klasi P , pa se još uvek ne zna da li važi P = N P (mada se veruje da ne važi).
P SP ACE je klasa problema koji mogu biti rešeni korišćenjem memorijskog
prostora koji je polinomijalna funkcija ulaza. Važi N P ⊆ P SP ACE, ali se ne
zna da li važi N P = P SP ACE (rasprostranjeno je uverenje da ne važi).
Tokom poslednje decenije, pored napora da se dokaže da ne važi P = N P ,
radi se i na ispitivanju raspodela najtežih problema u pojedinim klasama N P -
kompletnih problema. Dosta se radi i na primeni novih pristupa (npr. na pri-
meni genetskih algoritama) u efikasnijem rešavanju nekih problema u pojedinim
klasama N P -kompletnih problema.
Glava 4

Rekurzija, rekurzivne
matematičke funkcije i
primeri korišćenja rekurzije

In mathematics and computer science, recursion specifies (or constructs) a


class of objects or methods (or an object from a certain class) by defining a few
very simple base cases or methods (often just one), and then defining rules to
break down complex cases into simpler cases.
For example, the following is a recursive definition of person’s ancestors:

• One’s parents are one’s ancestors (base case);

• The parents of any ancestor are also ancestors of the person under con-
sideration (recursion step).

It is convenient to think that a recursive definition defines objects in terms


of ”previously defined”objects of the class to define.

Primer 4.1 (Recursive humour). A common joke is the following definition”of


recursion.
Recursion See ”Recursion”.
This is a parody on references in dictionaries, which in some careless cases
may lead to circular definitions. Every joke has an element of wisdom, and
also an element of misunderstanding. This is also an example of an erroneous
recursive definition of an object, the error being the absence of the termination
condition (or lack of the initial state, if to look at it from an opposite point of
view). Newcomers to recursion are often bewildered by its apparent circularity,
until they learn to appreciate that a termination condition is key.
4 Rekurzija, rekurzivne matematičke funkcije i primeri korišćenja
30 rekurzije

4.1 Rekurzivne matematičke funkcije


A function may be partly defined in terms of itself. A familiar example is
the Fibonacci sequence: F (n) = F (n − 1) + F (n − 2). For such a definition to
be useful, it must lead to values which are non-recursively defined, in this case
F (0) = 0 and F (1) = 1.
Another example is factorial. Factorial of a number, say n, is equal to the
product of all integers from 1 to n. Factorial of n is denoted by n! = 1 · 2 · 3... · n
or by 1! = 1 and n! = n · (n − 1)!, for n > 1. Eg: 10! = 1 · 2 · 3 · 4 · 5 · 6 · 7 · 8 · 9 · 10.

4.2 Matematička indukcija i rekurzija


Now recall proofs by induction.

• Show that the theorem is true for the smallest case. This can usually be
done by inspection.
⇒ basis

• Show that if the theorem is true for the basis cases, it can be extended to
include the next case. If the theorem is true for the case k = n − 1, show
that it is true for the case k=n.
⇒ Inductive hypothesis assumes that the theorem is true for the case
k = n − 1.

Similarity with recursive programming:

• The base case of a recursive method is the case that can be solved without
a recursive call.

• For the general (non-base) case, k = n, we assume that the recursive


method works for k = n − 1.

Recursion is the programming equivalent of induction.

4.3 Rekurzija u računarstvu


Recursion is an algorithmic technique where a function, in order to accom-
plish a task, calls itself with some part of the task.
Programs using recursion can be made to do all sorts of things, right from
calculating the factorial of a number, to playing complex games against human
intelligence.
In normal procedural languages, one can go about defining functions and
procedures, and ’calling’ these from the ’parent’ functions. Some languages also
provide the ability of a function to call itself. This is called recursion.
4.4 Faktorijel 31

4.4 Faktorijel

The simplest program to calculate factorial of a number is a loop with a


product variable. Instead, it is possible to give a recursive definition for Factorial
as follows:

(1) If n=1, then Factorial of n = 1

(2) Otherwise, Factorial of n = product of n and Factorial of (n − 1)

The following code fragment depicts recursion at work.

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

The important thing to remember when creating a recursive function is to


give an ’end-condition’. We don’t want the function to keep calling itself forever.
Somehow, it should know when to stop. There are many ways of doing this.
One of the simplest is by means of an ’if condition’ statement, as above. In the
above example, the recursion stops when n reaches 1. In each instance of the
function, the value of n keeps decreasing. So it ultimately reaches 1 and ends.
Of course, the above function will run infinitely if the initial value of n is less
than 1. So the function is not perfect. The n==1 condition should be changed
to n<=1.

Imagination of recursion is a bit tricky. Think of clones. Say you have a


machine to make clones of yourself, and (for lack of a better pass-time) decide
to find the factorial of a number, say 10, using your clones.
4 Rekurzija, rekurzivne matematičke funkcije i primeri korišćenja
32 rekurzije

4.5 Obrazac trougao

void triangle(unsigned int m, unsigned int n)


// Precondition: m <= n
// Postcondition: The function has printed a pattern of 2*(n-m+1)
// lines to the output stream outs. The first line contains m
// asterisks, the next line contains m+1 asterisks, and so on
// up to a line with n asterisks. Then the pattern is repeated
// backwards, going n back down to m.
/* Example output:
triangle(3, 5) will print this to cout:
***
****
*****
*****
****
***
*/

Prva varijanta:
#include<stdio.h>

void triangle(unsigned int m, unsigned int n);

main() {
triangle(3,5);
}

void triangle(unsigned int m, unsigned int n) {


unsigned int i;

if(m>n)
return;

for(i=0;i<m;i++)
printf("*");
printf("\n");

triangle(m+1, n);

for(i=0;i<m;i++)
printf("*");
printf("\n");
}
4.6 Kule Hanoja 33

4.6 Kule Hanoja

Definition: Given three posts (towers) and n disks of decreasing sizes, move
the disks from one post to another one at a time without putting a larger disk on
a smaller one. The minimum is 2n-1 moves. The ”ancient legend”was invented
by De Parville in 1884.

A solution using recursion is: to move n disks from post A to post B (1)
recursively move the top n-1 disks from post A to C, (2) move the n-th disk
from A to B, and (3) recursively move the n-1 disks from C to B. A solution
using iteration is: on odd-numbered moves, move the smallest disk clockwise.
On even-numbered moves, make the single other move which is possible.

static void tower(int n, char start, char finish, char spare) {


if (n<=1)
printf("Moving a disk from peg %c to %c",start,finish);
else
{
tower(n-1,start,spare,finish);
printf("Moving a disk from peg %c to %c",start,finish);
tower(n-1,spare,finish,start);
}
}

Calling the above method with n=4, i.e., tower(4,’A’,’C’,’B’) yields the
following result:

Moving a disk from peg A to peg B Moving a disk from peg A to peg C Moving a disk from peg B
Moving a disk from peg A to peg B Moving a disk from peg C to peg A Moving a disk from peg C
Moving a disk from peg A to peg B Moving a disk from peg A to peg C Moving a disk from peg B
Moving a disk from peg B to peg A Moving a disk from peg C to peg A Moving a disk from peg B
Moving a disk from peg A to peg B Moving a disk from peg A to peg C Moving a disk from peg B
4 Rekurzija, rekurzivne matematičke funkcije i primeri korišćenja
34 rekurzije

4.7 Permutacije

#include<stdio.h>

#define N 3 static int used[N+1]; static int p[N+1];

void permutation(int n) ;

main() {
int i;
for (i=1;i<=N;i++)
used[i]=0;
permutation(1);
}

void permutation(int n) {
int i;
if(n>N)
{
for(i=1;i<=N;i++)
printf("%d ",p[i]);
printf("\n");
}
else
for (i=1;i<=N;i++)
if (!used[i])
{
used[i]=1;
p[n]=i;
permutation(n+1);
used[i]=0;
}
}

Rezultat rada za N=3:

1 2 3 1 3 2 2 1 3 2 3 1 3 1 2 3 2 1

Druga varijanta:
4.8 Particionisanje 35

#include<stdio.h>

#define N 3 static int p[N+1];

void permutation(int n) ;

main() {
for (i=1;i<=N;i++)
p[i]=i;

permutation(1);
}

void permutation(int n) {
int i,tmp;
if(n==N)
{
for(i=1;i<=N;i++)
printf("%d ",p[i]);
printf("\n");
}
else
for (i=n;i<=N;i++)
{
tmp=p[n];
p[n]=p[i];
p[i]=tmp;

permutation(n+1);

tmp=p[n];
p[n]=p[i];
p[i]=tmp;
}
}

4.8 Particionisanje
A partition of a positive integer n is a sequence of positive integers that sum
to n. Write a program to print all partitions of n, e.g:
n=4 4 3 1 2 2 2 1 1 1 3 1 2 1 1 1 2 1 1 1 1
Rešenje:
4 Rekurzija, rekurzivne matematičke funkcije i primeri korišćenja
36 rekurzije

#include<stdio.h>

#define N 10 static int p[N];

void partition(unsigned int n);

main() {
partition(4);
}

void partition(unsigned int n) {


unsigned int i;
static unsigned int k;

if(n==0)
{
for(i=0;i<k;i++)
printf("%d ",p[i]);
printf("\n");
return;
}

for(i=n;i>0;i--)
{
p[k]=i;
k++;
partition(n-i);
k--;
}
}

4.9 Uzajamna rekurzija

So far we have only considered recursive methods that call themselves. An-
other type of recursion involves methods that cyclically call each other. This
is known as cyclical or mutual recursion. In the following example, methods A
and B are mutually recursive.
4.10 Nedostaci rekurzije 37

void A(int n) {
if (n <= 0)
return;
n--;
B(n);
}

void B(int n) {
if (n <= 0)
return;
n--;
A(n);
}

4.10 Nedostaci rekurzije

Redundantna izračunavanja Consider the Fibonacci number sequence, de-


fined as the sequence: { 0,1,1,2,3,5,8,13,...} where n is a non-negative integer.
A method to generate Fibonacci numbers can be implemented recursively.
int fib(int n) {
if(n <= 1)
return n; //base case
else
return fib(n-1) + fib(n-2);
}

Consider now the execution of this function for n=5. Note the redundant
calculations: 3 calls to fib(0), 5 calls to fib(1), and so on. Each recursive call
does more and more redundant work, resulting in exponential growth.

Cena poziva All local variables and formal parameters for the procedure are
stored on top of a stack. When a recursive call is made, new copies are pushed.
This can be very time and space consuming. In such cases, when possible,
recursion should be replaced by iterative solution.
Na primer, gore navedena funkcija fib može se zameniti iterativnom funkci-
jom koja dinamički alocira prostor (korišćenjem funkcije realloc) za elemente
Fibonačijevog niza ili iterativnom funkcijom koja ne pamti sve elemente niza do
indeksa n već samo dva prethodna:
4 Rekurzija, rekurzivne matematičke funkcije i primeri korišćenja
38 rekurzije

int fib(int n) {
int f1,f2,tmp,i;

if (n<=1)
return n;

f1 = 0;
f2 = 1;

for(i=3;i<=n;i++)
{
tmp = f2;
f2 = f1+f2;
f1 = tmp;
}

return f2;
}

4.11 Eliminisanje rekurzije


Zadatak 7. Write an iterative, non-recursive version of the following routine:
void r(int x) {
if (p(x))
a(x);
else
{
b(x);
r(f(x));
}
}
where p, a, b, and f are arbitrary functions.
Rešenje:
void r(int x) {
while(!p(x))
{
b(x);
x = f(x);
}
a(x);
}
Glava 5

Fundamentalni algoritmi

5.1 Pretraživanje

Pod pretraživanjem za dati niz elemenata (npr. data) i datu vrednost (npr. value)
podrazumevamo odredivanje indeksa elementa niza koji je jednak datoj vred-
nosti (tj. odredivanje indeksa i takvog da je data[i]==value).

5.1.1 Pronalaženje karaktera u stringu

Primer 5.1.
/* Pronalazi poslednju poziciju karaktera c u stringu s,
odnosno -1 ukoliko s ne sadrzi c */
int string_last_char(char s[], char c) {
Koristeci string_length :

for (i = string_length(s) - 1; i>0; i--)


if (s[i] == c)
return i;

return -1;
}

5.1.2 Odredivanje maksimuma

Primer 5.2. Maksimum


40 5 Fundamentalni algoritmi

#include<stdio.h> #define MAXDUZ 100

int main() {
/* Dimenzija niza,pomocna i brojacke promenljive */
int n, i, max;

/* Niz od maksimalno MAXDUZ elemenata*/


int a[MAXDUZ];

printf("Unesite dimenziju niza\n");


scanf("%d",&n);
/* n treba da bude bar 1 i najvise MAXDUZ-1 */

/* Unos clanova niza */


for(i=0; i<n; i++)
{
printf("Unesite %d. clan niza\n",i+1);
scanf("%d",&a[i]);
}

max = a[1];
for(i=2; i<n; i++)
if (a[i]>max)
max = a[i];

printf("Maksimum niza je %d ",max);


}

5.1.3 Linearno pretraživanje


Linear search is a search algorithm, also known as sequential search, that is
suitable for searching a set of data for a particular value.
It operates by checking every element of a list until a match is found. Linear
search runs in O(n). If the data are distributed randomly, on average n/2
comparisons will be needed. The best case is that the value is equal to the first
element tested, in which case only 1 comparison is needed. The worst case is
that the value is not in the list, in which case n comparisons are needed.
Here is code which determines the index of a given value in a sorted list a
between indices 0 and length (if the value is not found, -1 is returned):
int linear_search(int *data, int length, int value) {
for(int i = 0 ; i < length ; i++)
if(data[i] == value)
return i;
return -1;
}
5.1 Pretraživanje 41

Linear search can be used to search an unordered list. The more efficient
binary search can only be used to search an ordered list.

If more than a small number of searches are needed, it is advisable to use


a more efficient data structure. One approach is to sort and then use binary
searches.

Primer 5.3. Linearna pretraga

#include <stdio.h>

/* Funkcija proverava da li se dati element x nalazi


u datom nizu celih brojeva.
Funkcija vraca poziciju u nizu na kojoj je x pronadjen
odnosno -1 ukoliko elementa nema.
*/ int linearna_pretraga(int niz[], int br_elem, int x) {
int i;
for (i = 0; i<br_elem; i++)
if (niz[i] == x)
return i;
/* nikako else */

return -1;
}

main() {
/* Inicijalizacija niza moguca je i na ovaj nacin*/
int a[] = { 4, 3, 2, 6, 7, 9, 11 };

/* Da bismo odredili koliko clanova


ima niz mozemo koristiti operator sizeof */
int br_elem = sizeof(a)/sizeof(int);
int x;
int i;
printf("Unesi broj koji trazimo : ");
scanf("%d",&x);

i = linearna_pretraga(a, br_elem, x);


if (i == -1)
printf("Element %d nije pronadjen\n",x);
else
printf("Element %d je pronadjen na
poziciji %d\n",x, i);
}
42 5 Fundamentalni algoritmi

5.1.4 Binarno pretraživanje


A binary search algorithm is a technique for finding a particular value in
a linear array, by ruling out half of the data at each step, widely but not ex-
clusively used in computer science. A binary search finds the median, makes
a comparison to determine whether the desired value comes before or after it,
and then searches the remaining half in the same manner. A binary search is
an example of a divide and conquer algorithm (more specifically a decrease and
conquer algorithm) and a dichotomic search.
The most common application of binary search is to find a specific value in
a sorted list. To cast this in the frame of the guessing game, realize that we are
now guessing the index, or numbered place, of the value in the list.
The search begins by examining the value in the center of the list; because
the values are sorted, it then knows whether the value occurs before or after the
center value, and searches through the correct half in the same way.

Primer 5.4. An example of binary search in action is a simple guessing game in


which a player has to guess a positive integer selected by another player between
1 and N, using only questions answered with yes or no. Supposing N is 16 and
the number 11 is selected, the game might proceed as follows.
Is the number greater than 8? (Yes)
Is the number greater than 12? (No)
Is the number greater than 10? (Yes)
Is the number greater than 11? (No)
Therefore, the number must be 11. At each step, we choose a number right
in the middle of the range of possible values for the number. For example, once
we know the number is greater than 8, but less than or equal to 12, we know to
choose a number in the middle of the range [9, 12] (either 10 or 11 will do).
At most questions are required to determine the number, since each question
halves the search space. Note that one less question (iteration) is required than
for the general algorithm, since the number is constrained to a particular range.

Primer 5.5. Even if the number we’re guessing can be arbitrarily large, in
which case there is no upper bound N , we can still find the number in at most
O(log k) steps (where k is the (unknown) selected number) by first finding an
upper bound by repeated doubling. For example, if the number were 11, we could
use the following sequence of guesses to find it:
Is the number greater than 1? (Yes)
Is the number greater than 2? (Yes)
Is the number greater than 4? (Yes)
Is the number greater than 8? (Yes)
Is the number greater than 16? (No, N=16, proceed as above) ( We know
the number greater than 8 )
Is the number greater than 12? (No)
Is the number greater than 10? (Yes)
Is the number greater than 11? (No)
5.1 Pretraživanje 43

Primer 5.6. As one simple application, in revision control systems, it is possi-


ble to use a binary search to see in which revision a piece of content was added
to a file. We simply do a binary search through the entire version history; if
the content is not present in a particular version, it appeared later, while if it is
present it appeared at that version or sooner. This is far quicker than checking
every difference.

Primer 5.7. There are many occasions unrelated to computers when a binary
chop is the quickest way to isolate a solution we seek. In troubleshooting a
single problem with many possible causes, we can change half the suspects, see
if the problem remains and deduce in which half the culprit is; change half the
remaining suspects, and so on.

Primer 5.8. Binarno pretrazivanje


44 5 Fundamentalni algoritmi

#include<stdio.h>
#define MAXDUZ 100

int main() {
/* Dimenzija niza,pomocna i brojacke promenljive */
int n,pom,i,j;

/* Niz od maksimalno MAXDUZ elemenata*/


int a[MAXDUZ];

/* Elemet koji se trazi i pozicija


na kojoj se nalazi, ukoliko je u nizu*/
int x, pozicija;

/* Pomocne promenljive za pretragu */


int donji, gornji, srednji;

printf("Unsite dimenziju niza\n");


scanf("%d",&n);

/* Unos clanova niza */


for(i=0; i<n; i++)
{
printf("Unesite %d. clan niza\n",i+1);
scanf("%d",&a[i]);
}

/*Sortiranje*/
for(i=0; i<n-1; i++)
for(j=i+1; j<n; j++)
if(a[i]>a[j])
{
pom=a[i];
a[i]=a[j];
a[j]=pom;
}

/* Unos elementa binarne pretrage */


printf("Unesite element koji se trazi\n");
scanf("%d",&x);

donji = 0;
gornji = n-1;
pozicija = -1;

while(donji<=gornji)
{
srednji = (donji + gornji)/2;
if(a[srednji] == x)
{
pozicija = srednji;
break;
}
else
if(a[srednji] < x)
5.1 Pretraživanje 45

Primer 5.9.
46 5 Fundamentalni algoritmi

/* Binarna pretraga niza celih brojeva - rekurzivna verzija*/


#include <stdio.h>

/* Funkcija proverava da li se element x javlja unutar niza


celih brojeva a.
Funkcija vraca poziciju na kojoj je element nadjen odnosno
-1 ako ga nema.

!!!!! VAZNO !!!!!


Pretpostavka je da je niz a uredjen po velicini
*/

int binary_search(int a[], int l, int d, int x) {


/* Ukoliko je interval prazan, elementa nema */
if (l > d)
return -1;

/* Srednja pozicija intervala [l, d] */


int s = (l+d)/2;

/* Ispitujemo odnos x-a i srednjeg elementa */


if (x == a[s])
return s; /* Element je pronadjen */
else if (x < a[s])
/* Pretrazujemo interval [l, s-1] */
return binary_search(a, l, s-1, x);
else
/* Pretrazujemo interval [s+1, d] */
return binary_search(a, s+1, d, x);
}

main() {
int a[] = {3, 5, 7, 9, 11, 13, 15};
int x;
int i;

printf("Unesi element kojega trazimo : ");


scanf("%d",&x);
i = binary_search(a, 0, sizeof(a)/sizeof(int)-1, x);

if (i==-1)
printf("Elementa %d nema\n", x);
else
printf("Pronadjen na poziciji %d\n", i);
}
5.2 Sortiranje 47

Here is code which determines the index of a given value in a sorted list a
between indices left and right (if the value is not found, -1 is returned):
int binarySearch(int *data, int value, int left, int right) {
while (left <= right)
{
mid = (right-left)/2+left;
if (data[mid] == value)
return mid;
if (value < data[mid])
right = mid-1;
else
left = mid+1;
}
return -1;
}
Recursive version:
int binarySearch(int a[], int value, int left, int right) {
if (right < left)
return -1;
mid = (right-left)/2 + left;
if (a[mid] == value
return mid;
if (value < a[mid])
return binarySearch(a, value, left, mid-1);
else
return binarySearch(a, value, mid+1, right);
}
In both cases, the algorithm terminates because on each recursive call or
iteration, the range of indexes right minus left always gets smaller, and so must
eventually become negative.
Binary search is a logarithmic algorithm and executes in O(log n) time.
Specifically, 1 + / log2 n iterations are needed to return an answer. It is con-
siderably faster than a linear search. It can be implemented using recursion
or iteration, as shown above, although in many languages it is more elegantly
expressed recursively.

5.2 Sortiranje
One of the fundamental problems of computer science is ordering (ascending
or descending) a sequence of items. There’s a plethora of solutions to this
problem, known as sorting algorithms. Some sorting algorithms are simple and
intuitive, such as the bubble sort. Others, such as the quick sort are extremely
complicated, but produce lightening-fast results. Za više detalja videti http:
//linux.wku.edu/~lamonml/algor/sort/sort.html.
48 5 Fundamentalni algoritmi

Below are listed seven of the most common sorting algorithms:

• Bubble sort
• Heap sort
• Insertion sort
• Merge sort
• Quick sort
• Selection sort
• Shell sort

Neki od algoritama za sortiranje rade in-place (u mestu), tj. sortiraju zadate


elemente bez korišćenja dodatnog niza. Drugi algoritmi zahtevaju korišćenje
pomoćnog niza.
The common sorting algorithms can be divided into two classes by the com-
plexity of their algorithms. Algorithmic complexity is a complex subject that
would take too much time to explain here, but suffice it to say that there’s a
direct correlation between the complexity of an algorithm and its relative effi-
ciency. Algorithmic complexity is generally written in a form known as Big-O
notation, where the O represents the complexity of the algorithm and a value n
represents the size of the set the algorithm is run against. (videti glavu ??).
For example, O(n) means that an algorithm has a linear complexity. In
other words, it takes ten times longer to operate on a set of 100 items than it
does on a set of 10 items (10 · 10 = 100). If the complexity was O(n2 ) (quadratic
complexity), then it would take 100 times longer to operate on a set of 100 items
than it does on a set of 10 items.
The two classes of sorting algorithms are O(n2 ), which includes the bubble,
insertion, selection, and shell sorts; and O(n log n) which includes the heap,
merge, and quick sorts.
In addition to algorithmic complexity, the speed of the various sorts can
be compared with empirical data. Since the speed of a sort can vary greatly
depending on what data set it sorts, accurate empirical results require several
runs of the sort be made and the results averaged together. The empirical data
given is the average of a hundred runs against random data sets on a single-user
250MHz UltraSPARC II. The run times on your system will almost certainly
vary from these results, but the relative speeds should be the same - the selection
sort runs in roughly half the time of the bubble sort on the UltraSPARC II, and
it should run in roughly half the time on whatever system you use as well.
These empirical efficiency graphs (5.1 and 5.1) are kind of like golf - the
lowest line is the ”best”. Keep in mind that ”bestdepends on your situation -
the quick sort may look like the fastest sort, but using it to sort a list of 20
items is kind of like going after a fly with a sledgehammer.
As the graph 5.1 pretty plainly shows, the bubble sort is grossly inefficient,
and the shell sort blows it out of the water. Notice that the first horizontal line
5.2 Sortiranje 49

Slika 5.1: Rezultati za algoritme sortiranja složenosti O(n2 )

Slika 5.2: Rezultati za algoritme sortiranja složenosti O(n log n)


50 5 Fundamentalni algoritmi

in the plot area is 100 seconds - these aren’t sorts that you want to use for huge
amounts of data in an interactive application. Even using the shell sort, users
are going to be twiddling their thumbs if you try to sort much more than 10,000
data items.
On the bright side, all of these algorithms are incredibly simple (with the
possible exception of the shell sort). For quick test programs, rapid prototypes,
or internal-use software they’re not bad choices unless you really think you need
split-second efficiency.
Speaking of split-second efficiency, the O(n log n) sorts (slika 5.2) are where
it’s at. Notice that the time on this graph is measured in tenths of seconds,
instead hundreds of seconds like the O(n2 ) graph.
But as with everything else in the real world, there are trade-offs. These
algorithms are blazingly fast, but that speed comes at the cost of complexity.
Recursion, advanced data structures, multiple arrays - these algorithms make
extensive use of those nasty things.
In the end, the important thing is to pick the sorting algorithm that you
think is appropriate for the task at hand. You should be able to use the source
code on this site as a ”black box”if you need to - you can just use it, without
understanding how it works. Obviously taking the time to understand how the
algorithm you choose works is preferable, but time constraints are a fact of life.
Zna se da postoje algoritmi za sortiranje koji su složenosti O(n log n), ali za
sada se ne zna da li postoji algoritam manjeg reda: za sada nije otkriven takav
algoritam niti je dokazano da on ne može da postoji.

5.2.1 Sortiranje selekcijom

The selection sort works by selecting the smallest unsorted item remaining
in the list, and then swapping it with the item in the next position to be filled.
The selection sort has a complexity of O(n2 ).
Pros: Simple and easy to implement.
Cons: Inefficient for large lists, so similar to the more efficient insertion sort
that the insertion sort should be used in its place.
The selection sort is in the group of n2 sorts. It yields a 60% performance
improvement over the bubble sort, but the insertion sort is over twice as fast
as the bubble sort and is just as easy to implement as the selection sort. In
short, there really isn’t any reason to use the selection sort - use the insertion
sort instead.
If you really want to use the selection sort for some reason, try to avoid
sorting lists of more than a 1000 items with it or repetitively sorting lists of
more than a couple hundred items.
Below is the basic selection sort algorithm.
5.2 Sortiranje 51

void selectionSort(int numbers[], int array_size) {


int i, j;
int min, temp;

for (i = 0; i < array_size-1; i++)


{
min = i;
for (j = i+1; j < array_size; j++)
{
if (numbers[j] < numbers[min])
min = j;
}
temp = numbers[i];
numbers[i] = numbers[min];
numbers[min] = temp;
}
}

5.2.2 Sortiranje umetanjem

The insertion sort works just like its name suggests - it inserts each item
into its proper place in the final list. The simplest implementation of this
requires two list structures - the source list and the list into which sorted items
are inserted. To save memory, most implementations use an in-place sort that
works by moving the current item past the already sorted items and repeatedly
swapping it with the preceding item until it is in place.
Like the bubble sort, the insertion sort has a complexity of O(n2 ). Although
it has the same complexity, the insertion sort is a little over twice as efficient as
the bubble sort.
Pros: Relatively simple and easy to implement.
Cons: Inefficient for large lists.
The insertion sort is a good middle-of-the-road choice for sorting lists of a
few thousand items or less. The algorithm is significantly simpler than the shell
sort, with only a small trade-off in efficiency. At the same time, the insertion sort
is over twice as fast as the bubble sort and almost 40% faster than the selection
sort. The insertion sort shouldn’t be used for sorting lists larger than a couple
thousand items or repetitive sorting of lists larger than a couple hundred items.
Below is the basic insertion sort algorithm.
52 5 Fundamentalni algoritmi

void insertionSort(int numbers[], int array_size) {


int i, j, index;

for (i=1; i < array_size; i++)


{
index = numbers[i];
j = i;
while ((j > 0) && (numbers[j-1] > index))
{
numbers[j] = numbers[j-1];
j = j - 1;
}
numbers[j] = index;
}
}

5.2.3 Babl sortiranje

The bubble sort is the oldest and simplest sort in use. Unfortunately, it’s
also the slowest.
The bubble sort works by comparing each item in the list with the item next
to it, and swapping them if required. The algorithm repeats this process until
it makes a pass all the way through the list without swapping any items (in
other words, all items are in the correct order). This causes larger values to
”bubble”to the end of the list while smaller values šink”towards the beginning
of the list.
The bubble sort is generally considered to be the most inefficient sorting algo-
rithm in common usage. Under best-case conditions (the list is already sorted),
the bubble sort can approach a constant O(n) level of complexity. General-case
is an abysmal O(n2 ).
While the insertion, selection, and shell sorts also have O(n2 ) complexities,
they are significantly more efficient than the bubble sort.
Pros: Simplicity and ease of implementation.
Cons: Horribly inefficient.
A fair number of algorithm purists (which means they’ve probably never
written software for a living) claim that the bubble sort should never be used
for any reason. Realistically, there isn’t a noticeable performance difference
between the various sorts for 100 items or less, and the simplicity of the bubble
sort makes it attractive. The bubble sort shouldn’t be used for repetitive sorts
or sorts of more than a couple hundred items.
Below is the basic bubble sort algorithm.
5.2 Sortiranje 53

void bubbleSort(int numbers[], int array_size) {


int i, j, temp;

for (i = (array_size - 1); i >= 0; i--)


{
for (j = 1; j <= i; j++)
{
if (numbers[j-1] > numbers[j])
{
temp = numbers[j-1];
numbers[j-1] = numbers[j];
numbers[j] = temp;
}
}
}
}

5.2.4 Quick sort


The quick sort is an in-place, divide-and-conquer, massively recursive sort.
As a normal person would say, it’s essentially a faster in-place version of the
merge sort. The quick sort algorithm is simple in theory, but very difficult to
put into code (computer scientists tied themselves into knots for years trying to
write a practical implementation of the algorithm, and it still has that effect on
university students).
The recursive algorithm consists of four steps (which closely resemble the
merge sort):

1. If there are one or less elements in the array to be sorted, return immedi-
ately.

2. Pick an element in the array to serve as a ”pivot”point. (Usually the


left-most element in the array is used.)

3. Split the array into two parts - one with elements larger than the pivot
and the other with elements smaller than the pivot.

4. Recursively repeat the algorithm for both halves of the original array.

The efficiency of the algorithm is majorly impacted by which element is


choosen as the pivot point. The worst-case efficiency of the quick sort, O(n2 ),
occurs when the list is sorted and the left-most element is chosen. Randomly
choosing a pivot point rather than using the left-most element is recommended
if the data to be sorted isn’t random. As long as the pivot point is chosen
randomly, the quick sort has an algorithmic complexity of O(n log n).
Pros: Extremely fast.
Cons: Very complex algorithm, massively recursive.
54 5 Fundamentalni algoritmi

The quick sort is by far the fastest of the common sorting algorithms. It’s
possible to write a special-purpose sorting algorithm that can beat the quick
sort for some data sets, but for general-case sorting there isn’t anything faster.

As soon as students figure this out, their immediate implulse is to use the
quick sort for everything - after all, faster is better, right? It’s important to
resist this urge - the quick sort isn’t always the best choice. As mentioned
earlier, it’s massively recursive (which means that for very large sorts, you can
run the system out of stack space pretty easily). It’s also a complex algorithm
- a little too complex to make it practical for a one-time sort of 25 items, for
example.

With that said, in most cases the quick sort is the best choice if speed is
important (and it almost always is). Use it for repetitive sorting, sorting of
medium to large lists, and as a default choice when you’re not really sure which
sorting algorithm to use. Ironically, the quick sort has horrible efficiency when
operating on lists that are mostly sorted in either forward or reverse order -
avoid it in those situations.

Below is the basic quick sort algorithm.


5.2 Sortiranje 55

void quickSort(int numbers[], int array_size) {


q_sort(numbers, 0, array_size - 1);
}

void q_sort(int numbers[], int left, int right) {


int pivot, l_hold, r_hold;

l_hold = left;
r_hold = right;
pivot = numbers[left];
while (left < right)
{
while ((numbers[right] >= pivot) && (left < right))
right--;
if (left != right)
{
numbers[left] = numbers[right];
left++;
}
while ((numbers[left] <= pivot) && (left < right))
left++;
if (left != right)
{
numbers[right] = numbers[left];
right--;
}
}
numbers[left] = pivot;
pivot = left;
left = l_hold;
right = r_hold;
if (left < pivot)
q_sort(numbers, left, pivot-1);
if (right > pivot)
q_sort(numbers, pivot+1, right);
}

Primer 5.10. Ako se uzme da za vreme izvršavanja T (n) algoritma quicksort


važi:
T (n) = 2T (n/2) + n ,
onda na osnovu teoreme 1 važi T (n) = O(n log n). Medjutim, u najgorem
slučaju za quicksort važi T (n) = T (n − 1) + n i tada je T (n) = O(n2 ). Ipak,
za ovaj algoritam često se najgori slučaj smatra retkim (i zanemaruje), pa se
obično uzima da je složenost algoritma quicksort O(n log n).
56 5 Fundamentalni algoritmi

5.2.5 Korišćenje sistemske implementacije quick sort-a


Funkcija qsort izvršava quick sort:
void qsort( void *base, size_t num, size_t width,
int (*compare )(const void *elem1, const void *elem2 ) );
Zahteva zaaglavlje: <stdlib.h>.
Parameters:

• base: Start of target array

• num: Array size in elements

• width: Element size in bytes

• compare: Comparison function

• elem1, elem2: Pointers to two array elements

The qsort function implements a quick-sort algorithm to sort an array of


num elements, each of width bytes. The argument base is a pointer to the base
of the array to be sorted. qsort overwrites this array with the sorted elements.
The argument compare is a pointer to a user-supplied routine that compares
two array elements and returns a value specifying their relationship. qsort calls
the compare routine one or more times during the sort, passing pointers to two
array elements on each call:
compare( (void *) elem1, (void *) elem2 );
Return Value Description: the routine must compare the elements, then
return one of the following values:

• < 0 elem1 less than elem2

• 0 elem1 equivalent to elem2

• > 0 elem1 greater than elem2

The array is sorted in increasing order, as defined by the comparison func-


tion. To sort an array in decreasing order, reverse the sense of ”greater than”and
”less than”in the comparison function.

Primer 5.11.
5.2 Sortiranje 57

#include<stdio.h> #include <stdlib.h>

int poredi_brojeve(const void *i1, const void *i2) {


if (*(int*)i1 < *(int*)i2)
return -1;
else if (*(int*)i1 > *(int*)i2)
return 1;
else
return 0;
}

main() {
int i,niz[10];

for(i=0;i<10;i++)
{
niz[i]=rand()%20;
printf("%d ",niz[i]);
}

qsort(niz,10,sizeof(int),poredi_brojeve);

printf("Sortirano:\n");

for(i=0;i<10;i++)
printf("%d ",niz[i]);
}

Izlaz programa:

1 7 14 0 9 4 18 18 2 4 Sortirano: 0 1 2 4 4 7 9 14 18 18

Primer 5.12.
58 5 Fundamentalni algoritmi

/* QSORT.C: This program reads the command-line


* parameters and uses qsort to sort them. It
* then displays the sorted arguments.
*/

#include <stdlib.h> #include <string.h> #include <stdio.h>

int compare( const void *arg1, const void *arg2 );

void main( int argc, char **argv ) {


int i;
/* Eliminate argv[0] from sort: */
argv++;
argc--;

/* Sort remaining args using Quicksort algorithm: */


qsort( (void *)argv, (size_t)argc, sizeof( char * ), compare );

/* Output sorted list: */


for( i = 0; i < argc; ++i )
printf( "%s ", argv[i] );
printf( "\n" );
}

int compare( const void *arg1, const void *arg2 ) {


/* Compare strings: */
return strcmp( *(char**)arg1, *(char**)arg2);
}

Output:

[C:\code]qsort every good boy deserves favor boy deserves every favor
good

5.3 Jednostavni numerički algoritmi

5.3.1 Stepenovanje

Primer 5.13. Izracunavanje nk


5.3 Jednostavni numerički algoritmi 59

/* n>=0, k>=0 */ int stepen_sporo(int n, int k) {


int i,m=1;

for(int i=0;i<k;i++)
m *= n;

return m;
}
/* n>=0, k>=0 */ int stepen_brzo(int n, int k) {
int m;

if (k==0)
return 1;

if (k%2)
{
m = stepen_brzo(n,(k-1)/2);
return n*m*m;
}
else
{
m = stepen_brzo(n,k/2);
return m*m;
}
}

5.3.2 Izračunavanje vrednosti polinoma


Any polynomial in the form

P (x) = axn + bxn−1 + . . . dx + e

can be expressed in a form requiring fewer operations. For example:

P (x) = ax3 + bx2 + cx + d = x(x(ax + b) + c) + d

5.3.3 Zagradivanje nula funkcije


We will say that a root is bracketed in the interval (a, b) if f(a) and f(b) have
opposite signs. If the function is continuous, then at least one root must lie in
that interval (the intermediate value theorem). If the function is discontinuous,
but bounded, then instead of a root there might be a step discontinuity which
crosses zero. For numerical purposes, that might as well be a root, since the
behavior is indistinguishable from the case of a continuous function whose zero
crossing occurs in between two ”adjacent”floating-point numbers in a machine’s
60 5 Fundamentalni algoritmi

finite-precision representation. Only for functions with singularities is there the


possibility that a bracketed root is not really there, as for example

1
f (x) =
x−c

Some root-finding algorithms (e.g., bisection) will readily converge to c. Luckily


there is not much possibility of your mistaking c, or any number x close to it,
for a root, since mere evaluation of |f(x)| will give a very large, rather than a
very small, result. If you are given a function in a black box, there is no sure
way of bracketing its roots, or of even determining that it has roots.

#include <math.h> #define FACTOR 1.6 #define NTRY 50

/* Given a function func and an initial guessed range */ /* x1


to x2, the routine expands the range geometrically */ /* until a
root is bracketed by the returned values x1 and */ /* x2 (in which
case zbrac returns 1) or until the range */ /*becomes
unacceptably large (in which case zbrac returns 0).*/

int zbrac(float (*func)(float), float *x1, float *x2) {


int j;
float f1,f2;
if (*x1 == *x2)
{
printf("Bad initial range in zbrac");
return -1;
}
f1=(*func)(*x1);
f2=(*func)(*x2);
for (j=1;j<=NTRY;j++)
{
if (f1*f2 < 0.0)
return 1;
if (fabs(f1) < fabs(f2))
f1=(*func)(*x1 += FACTOR*(*x1-*x2));
else
f2=(*func)(*x2 += FACTOR*(*x2-*x1));
}
return 0;
}

Ukoliko se uz navedenu funkciju definiše funkcija funkcija1 i pozove kao u


narednoj funkciji main:
5.3 Jednostavni numerički algoritmi 61

float funkcija1(float x) {
return x*x*x-2;
}

main() {
float x1,x2;
x1=5; x2=10;

printf("x1=%f, x2=%f\n",x1,x2);
if(zbrac(funkcija1, &x1, &x2)==1)
printf("x1=%f, x2=%f\n",x1,x2);
}
dobija se rezultat:
x1=5.000000, x2=10.000000 x1=-3.000000, x2=10.000000

5.3.4 Odredivanje nula funkcije


Once we know that an interval contains a root, several classical procedures
are available to refine it. These proceed with varying degrees of speed and
sureness towards the answer. Unfortunately, the methods that are guaranteed
to converge plod along most slowly, while those that rush to the solution in the
best cases can also dash rapidly to infinity without warning if measures are not
taken to avoid such behavior. The bisection method is one that cannot fail. It
is thus not to be sneered at as a method for otherwise badly behaved problems.
The idea is simple. Over some interval the function is known to pass through
zero because it changes sign. Evaluate the function at the intervals midpoint
and examine its sign. Use the midpoint to replace whichever limit has the same
sign. After each iteration the bounds containing the root decrease by a factor
of two. If after n iterations the root is known to be within an interval of size
εn , then after the next iteration it will be bracketed within an interval of size

εn+1 = εn /2

neither more nor less. Thus, we know in advance the number of iterations
required to achieve a given tolerance in the solution,

ε0
n = log2
ε

where ε0 is the size of the initially bracketing interval, ε is the desired ending
tolerance.
Bisection must succeed. If the interval happens to contain two or more
roots, bisection will find one of them. If the interval contains no roots and
merely straddles a singularity, it will converge on the singularity.
62 5 Fundamentalni algoritmi

#include <math.h> #define JMAX 40 /* Maximum allowed number of


bisections. */

/* Using bisection, find the root of a function func */ /* known


to lie between x1 and x2. The root, returned */ /* as rtbis, will
be refined until its accuracy is +/-acc.*/

float rtbis(float (*func)(float), float x1, float x2, float xacc) {


int j;
float dx,f,fmid,xmid,rtb;
f=(*func)(x1);
fmid=(*func)(x2);
if (f*fmid >= 0.0)
{
printf("Root must be bracketed for bisection in rtbis");
return 0.0;
}
rtb = f < 0.0 ? (dx=x2-x1,x1) : (dx=x1-x2,x2); /* Orient the search so */
/* that f>0 lies at x+dx. */
for (j=1;j<=JMAX;j++)
{
fmid=(*func)(xmid=rtb+(dx *= 0.5)); /* Bisection loop. */
if (fmid <= 0.0)
rtb=xmid;
if (fabs(dx) < xacc || fmid == 0.0)
return rtb;
}
printf("Too many bisections in rtbis");
return 0.0; /* Never get here. */
}

Sa funkcijama zbrac i f1 definisanim kao u prethodnom poglavlju i funkci-


jom main definisanom kao u nastavku:
main() {
float x1,x2, root;

x1=5; x2=10;
printf("x1=%f, x2=%f\n",x1,x2);
if(zbrac(funkcija1, &x1, &x2)==1)
{
root = rtbis(funkcija1, x1, x2, 0.0001);
printf("root = %f\n",root);
}
}

dobija se sledeći rezultat


5.3 Jednostavni numerički algoritmi 63

root = 1.259872
Zaista, (1.259872)3 ≈ 1.9997664 ≈ 2.
64 5 Fundamentalni algoritmi
Deo II

Dinamički objekti
Glava 6

Pokazivači i adresna
aritmetika

6.1 Adresna aritmetika


If p is a pointer to some element of an array, then p++ increments p to point
to the next element, and p+=i increments it to point i elements beyond where it
currently does. These and similar constructions are the simples forms of pointer
or address arithmetic. C is consistent and regular in its approach to address
arithmetic; its integration of pointers, arrays, and address arithmetic is one of
the strengths of the language.
Pointers and integers are not interchangeable.

int i, *a, *b;


...
a = b; /* ispravno */
*a = i; /* ispravno */

i = a; /* neispravno */
i = (int)a; /* ispravno, ali izbegavati */
a = i; /* neispravno */
a = 0; /* ispravno */

a = a+i; /* ispravno */
a = a+b; /* neispravno */
Primer 6.1.

Zero is the sole exception: the constant zero may be assigned to a pointer,
and a pointer may be compared with the constant zero. The symbolic constant
NULL is often used in place of zero, as a mnemonic to indicate more clearly
that this is a special value for a pointer. NULL is defined in <stdio.h>. We
will use NULL henceforth.
68 6 Pokazivači i adresna aritmetika

Tests like
if (allocbuf + ALLOCSIZE - allocp >= n) { /* it fits */
and
if (p >= allocbuf && p < allocbuf + ALLOCSIZE)
show several important facets of pointer arithmetic. First, pointers may be
compared under certain circumstances. If p and q point to members of the same
array, then relations like ==, !=, <, >=, etc., work properly. For example,
p < q
is true if p points to an earlier element of the array than q does. Any
pointer can be meaningfully compared for equality or inequality with zero. But
the behavior is undefined for arithmetic or comparisons with pointers that do
not point to members of the same array. (There is one exception: the address
of the first element past the end of an array can be used in pointer arithmetic.)
Second, we have already observed that a pointer and an integer may be added
or subtracted. The construction
p + n
means the address of the n-th object beyond the one p currently points to.
This is true regardless of the kind of object p points to; n is scaled according
to the size of the objects p points to, which is determined by the declaration of
p. If an int is four bytes, for example, the int will be scaled by four. Pointer
subtraction is also valid: if p and q point to elements of the same array, and
p<q, then q-p+1 is the number of elements from p to q inclusive.

6.2 Višedimenzioni nizovi


C provides rectangular multi-dimensional arrays, although in practice they
are much less used than arrays of pointers. In this section, we will show some
of their properties. Consider the problem of date conversion, from day of the
month to day of the year and vice versa. For example, March 1 is the 60th day
of a non-leap year, and the 61st day of a leap year. Let us define two functions
to do the conversions: day_of_year converts the month and day into the day of
the year, and month_day converts the day of the year into the month and day.
Since this latter function computes two values, the month and day arguments
will be pointers:
month_day(1988, 60, &m, &d)
sets m to 2 and d to 29 (February 29th). These functions both need the same
information, a table of the number of days in each month (“thirty days hath
September ...”). Since the number of days per month differs for leap years and
non-leap years, it’s easier to separate them into two rows of a two-dimensional
array than to keep track of what happens to February during computation. The
array for performing the transformations are as follows:
6.2 Višedimenzioni nizovi 69

static char daytab[2][13] = {


{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
{0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
};
Recall that the arithmetic value of a logical expression, such as the one for
leap, is either zero (false) or one (true), so it can be used as a subscript of the
array daytab. The array daytab has to be external to both day_of_year and
month_day, so they can both use it. We made it char to illustrate a legitimate
use of char for storing small non-character integers.
daytab is the first two-dimensional array we have dealt with. In C, a two-
dimensional array is really a one-dimensional array, each of whose elements is
an array. Hence subscripts are written as
daytab[i][j] /* [row][col] */
rather than
daytab[i,j] /* WRONG */
Other than this notational distinction, a two-dimensional array can be treated
in much the same way as in other languages. Elements are stored by rows, so
the rightmost subscript, or column, varies fastest as elements are accessed in
storage order. An array is initialized by a list of initializers in braces; each row
of a two-dimensional array is initialized by a corresponding sub-list. We started
the array daytab with a column of zero so that month numbers can run from
the natural 1 to 12 instead of 0 to 11. Since space is not at a premium here,
this is clearer than adjusting the indices.
If a two-dimensional array is to be passed to a function, the parameter
declaration in the function must include the number of columns; the number
of rows is irrelevant, since what is passed is, as before, a pointer to an array
of rows, where each row is an array of 13 ints. In this particular case, it is a
pointer to objects that are arrays of 13 ints. Thus if the array daytab is to be
passed to a function f, the declaration of f would be:
f(int daytab[2][13]) { ... }
It could also be
f(int daytab[][13]) { ... }
since the number of rows is irrelevant, or it could be
f(int (*daytab)[13]) { ... }
which says that the parameter is a pointer to an array of 13 integers. The
parentheses are necessary since brackets [] have higher precedence than *.
Without parentheses, the declaration
int *daytab[13]
is an array of 13 pointers to integers. More generally, only the first dimension
(subscript) of an array is free; all the others have to be specified.
Kao što smo ranije naučili, ime jednodimenzionog niza (npr. a za int a[10])
može se tretirati kao pokazivač na prvi element niza. Ime dvodimenzionog niza
(npr.) tipa int može se tretirati kao pokazivač na pokazivač na int. Na primer,
ako je
int d[10][20];
70 6 Pokazivači i adresna aritmetika

onda je d[0] (kao i d[1], d[2], ...) pokazivač na int. Pokazivač d[0] sadrži
adresu elementa d[0][0], i, opštije, d[i] sadrži adresu elementa d[i][0].
Vrednost d je tipa int **, ona je pokazivač na pokazivač na int i sadrži adresu
pokazivača d[0].

6.3 Inicijalizacija nizova pokazivača


Consider the problem of writing a function month_name(n), which returns
a pointer to a character string containing the name of the n-th month. This is
an ideal application for an internal static array. month_name contains a private
array of character strings, and returns a pointer to the proper one when called.
This section shows how that array of names is initialized. The syntax is similar
to previous initializations:
/* month_name: return name of n-th month */
char *month_name(int n)
{
static char *name[] = {
"Illegal month",
"January", "February", "March",
"April", "May", "June",
"July", "August", "September",
"October", "November", "December"
};

return (n < 1 || n > 12) ? name[0] : name[n];


}
The initializer is a list of character strings; each is assigned to the corre-
sponding position in the array. The characters of the i-th string are placed
somewhere, and a pointer to them is stored in name[i]. Since the size of the
array name is not specified, the compiler counts the initializers and fills in the
correct number.

6.4 Pokazivači i višedimenzioni nizovi


Newcomers to C are sometimes confused about the difference between a
two-dimensional array and an array of pointers. Given the definitions
int a[10][20];
int *b[10];
then a[3][4] and b[3][4] are both syntactically legal references to a single
int. But a is a true two-dimensional array: 200 int-sized locations have been set
aside, and the conventional rectangular subscript calculation 20 * row +col
is used to find the element a[row,col]. For b, however, the definition only
allocates 10 pointers and does not initialize them; initialization must be done
6.5 Pokazivači na funkcije 71

explicitly, either statically or with code. Assuming that each element of b does
point to a twenty-element array, then there will be 200 ints set aside, plus ten
cells for the pointers. The important advantage of the pointer array is that the
rows of the array may be of different lengths. That is, each element of b need
not point to a twenty-element vector; some may point to two elements, some
to fifty, and some to none at all. Although we have phrased this discussion in
terms of integers, by far the most frequent use of arrays of pointers is to store
character strings of diverse lengths, as in the function month_name. Compare
the declaration and picture for an array of pointers:
char *name[] = { "Illegal month", "Jan", "Feb", "Mar" };
with those for a two-dimensional array:
char aname[][15] = { "Illegal month", "Jan", "Feb", "Mar" };

6.5 Pokazivači na funkcije

In C, a function itself is not a variable, but it is possible to define pointers to


functions, which can be assigned, placed in arrays, passed to functions, returned
by functions, and so on. We will illustrate this by a function that compares
command line argument. It will sort the input lines numerically instead of
lexicographically if the optional argument -n is given. Lexicographic comparison
of two strings is done by strcmp, as before; we will also need a routine numcmp
that compares two strings on the basis of numeric value and returns the same
kind of condition indication as strcmp does.

Primer 6.2. Program treba da se ponaša na sledeći način:


C:>primer
Nema elemenata za poredjenje

C:>primer 1 2
Manji je prvi element

C:>primer 12 2
Manji je prvi element

C:>primer 12 2 -n
Manji je drugi element

U nastavku je dat kôd programa.


72 6 Pokazivači i adresna aritmetika

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int poredi(char *a, char *b, int (*comp)(char *, char *));


int numcmp(char *, char *);

main(int argc, char *argv[])


{
int numeric = 0;
int rezultat;

if (argc > 3 && strcmp(argv[3], "-n") == 0)


numeric = 1; /* 1 if numeric sort */

if (argc<3)
{
printf("Nema elemenata za poredjenje\n");
return -1;
}

if (numeric)
rezultat = poredi(argv[1],argv[2],numcmp);
else
rezultat = poredi(argv[1],argv[2],strcmp);

/* isti efekat dobija se i ovako: */


/* rezultat = poredi(argv[1],argv[2],
(numeric ? numcmp : strcmp)); */
/* ili ovako: */
/* rezultat = poredi(argv[1],argv[2],
(int (*)(char*,char*))(numeric ? numcmp : strcmp)); */

if (rezultat<0)
printf("Manji je prvi element\n");
else if (rezultat==0)
printf("Dva elemenata su jednaka\n");
else
printf("Manji je drugi element\n");

return 0;
}

/* numcmp: compare s1 and s2 numerically */


int numcmp(char *s1, char *s2)
{
double v1, v2;

v1 = atof(s1);
v2 = atof(s2);
if (v1 < v2)
return -1;
else if (v1 > v2)
return 1;
6.5 Pokazivači na funkcije 73

strcmp and numcmp are addresses of functions. Since they are known to be
functions, the & is not necessary, in the same way that it is not needed before
an array name.
The use of comp in the line
((*comp)(a, b]))
is consistent with the declaration: comp is a pointer to a function, *comp is
the function, and
(*comp)(a, b)
is the call to it. The parentheses are needed so the components are correctly
associated; without them,
int *comp(char *, char *) /* WRONG */
says that comp is a function returning a pointer to an int, which is very
different.
74 6 Pokazivači i adresna aritmetika
Glava 7

Dinamička alokacija
memorije

7.1 Funkcije malloc i calloc


The functions malloc and calloc obtain blocks of memory dynamically.
They are declared in <stdlib.h>.
void *malloc(size_t n)
returns a pointer to n bytes of storage, or NULL if the request cannot be satisfied.
The memory returned by malloc() is not initialized. It can contain any random
garbage.
void *calloc(size_t n, size_t size)
returns a pointer to enough free space for an array of n objects of the specified
size, or NULL if the request cannot be satisfied. The storage is initialized to zero.
The pointer returned by malloc or calloc has the proper alignment for the
object in question, but it must be cast into the appropriate type, as in
int *ip;
ip = (int *) calloc(n, sizeof(int));

free(p) frees the space pointed to by p, where p was originally obtained


by a call to malloc or calloc. There are no restrictions on the order in which
space is freed, but it is a ghastly error to free something not obtained by calling
malloc or calloc. It is also an error to use something after it has been freed.
A typical but incorrect piece of code is this loop that frees items from a list:
for (p = head; p != NULL; p = p->next) /* WRONG */
free(p);

The right way is to save whatever is needed before freeing:


76 7 Dinamička alokacija memorije

for (p = head; p != NULL; p = q) {


q = p->next;
free(p);
}

Ne sme se koristiti nešto što je već osloboe.no, ne sme se dva puta oslobaa.ti
ista memorija.
Always check the return value of malloc() and calloc(). Never assume
that memory allocation will succeed. If the allocation fails, malloc() returns
NULL. If you use the value without checking, it is likely that your program will
immediately die from a segmentation violation (or segfault), which is an attempt
to use memory not in your address space. If you check the return value, you
can at least print a diagnostic message and terminate gracefully. Or you can
attempt some other method of recovery.

#include <stdio.h>
#include <stdlib.h>

main()
{
int n, i, *a;

printf("Unesi broj clanova niza : ");


scanf("%d", &n);

/* Nije moguce deklarisati niz od n elemenata;


no, moguce je sledece:*/
a = (int*)malloc(n*sizeof(int));

/* Mora se proveriti da li je alokacija memorije


uspesno izvrsena */
if (a == NULL)
{
printf("Nema slobodne memorije\n");
return 1;
}

/* a moze da se koristi kao obican niz */


for (i = 0; i<n; i++)
scanf("%d",&a[i]);

/* Oslobadjanje alocirane memorije */


free(a);
Primer 7.1. }
7.2 Funkcija realloc 77

7.2 Funkcija realloc

Function realloc reallocates memory blocks. Za njeno korišćenje potrebno


je uključiti zaglavlja <stdlib.h> i <malloc.h>.

void *realloc( void *memblock, size_t size );

Parameter memblock is a pointer to previously allocated memory block. Pa-


rameter size is a new size in bytes.

realloc returns a void pointer to the reallocated (and possibly moved)


memory block. The return value is NULL if the size is zero and the buffer
argument is not NULL, or if there is not enough available memory to expand
the block to the given size. In the first case, the original block is freed. In the
second, the original block is unchanged. The return value points to a storage
space that is guaranteed to be suitably aligned for storage of any type of object.
To get a pointer to a type other than void, use a type cast on the return value.
78 7 Dinamička alokacija memorije

/* REALLOC.C: This program allocates a block of memory for


* buffer and then uses _msize to display the size of that
* block. Next, it uses realloc to expand the amount of
* memory used by buffer and then calls _msize again to
* display the new amount of memory allocated to buffer.
*/

#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>

void main( void )


{
long *buffer1, *buffer2;
size_t size;

if( (buffer1 = (long *)malloc( 1000 * sizeof(long) )) == NULL )


exit( 1 );

size = 1000 * sizeof(long);


printf( "Size of block after malloc of 1000 longs: %u\n", size );

/* Reallocate and show new size: */


if( (buffer2 = realloc( buffer1, size + (1000 * sizeof( long )) ))
== NULL )
{
free(buffer1);
exit( 1 );
}

size = size + (1000 * sizeof( long ));


printf( "Size of block after realloc of 1000 more longs: %u\n",
size );

free( buffer2 );
exit( 0 );
}

Output

Size of block after malloc of 1000 longs: 4000


Size of block after realloc of 1000 more longs: 8000
7.3 Curenje memorije (memory leaking) 79

7.3 Curenje memorije (memory leaking)


Memory leaking is the situation that occurs when dynamically allocated
memory is lost to the program.
char * p;

p = (char *) malloc(1000);
....
p = (char *) malloc(5000);
Initially, 1000 bytes are dynamically allocated and the the address of those
bytes is stored in p. later 5000 bytes are dyanmically allocated and the address
is stored in p. However, the original 1000 bytes have not been returned to the
system using free.
Memory leak actually depends on the nature of the program.
Any dynamic memory that’s not needed should be released. In particular,
memory that is allocated inside loops or recursive or deeply nested function calls
should be carefully managed and released. Failure to take care leads to memory
leaks, whereby the process’s memory can grow without bounds; eventually, the
process dies from lack of memory.
This situation can be particularly pernicious if memory is allocated per input
record or as some other function of the input: The memory leak won’t be noticed
when run on small inputs but can suddenly become obvious (and embarrassing)
when run on large ones. This error is even worse for systems that must run
continuously, such as telephone switching systems. A memory leak that crashes
such a system can lead to significant monetary or other damage.
Even if the program never dies for lack of memory, constantly growing pro-
grams suffer in performance, because the operating system has to manage keep-
ing in-use data in physical memory. In the worst case, this can lead to behavior
known as thrashing, whereby the operating system is so busy moving the con-
tents of the address space into and out of physical memory that no real work
gets done.
Većina debagere detektuje da u programu postoji curenje memorije, ali ne
može da pomogne u lociranju odgovarajuće greške u kodu. Postoje specijalizo-
vani programi (memory leaks profiler) koji olakšavaju otkrivanje curenje mem-
orije.

7.4 Druge česte greške


Once free(p) is called, the memory pointed to by p is off limits. It now
”belongs”to the allocation subroutines, and they are free to manage it as they
see fit. They can change the contents of the memory or even release it from the
process’s address space! There are thus several common errors to watch out for
with free():

Accessing freed memory If unchanged, p continues to point at memory that


80 7 Dinamička alokacija memorije

no longer belongs to the application. This is called a dangling pointer. In


many systems, you can get away with continuing to access this memory,
at least until the next time more memory is allocated or freed. In many
others though, such access won’t work.
In sum, accessing freed memory is a bad idea: It’s not portable or reli-
able, and the GNU Coding Standards disallows it. For this reason, it’s a
good idea to immediately set the program’s pointer variable to NULL. If
you then accidentally attempt to access freed memory, your program will
immediately fail with a segmentation fault (before you’ve released it to
the world, we hope).

Freeing the same pointer twice This causes ”undefined behavior.”Once the
memory has been handed back to the allocation routines, they may merge
the freed block with other free storage under management. Freeing some-
thing that’s already been freed is likely to lead to confusion or crashes
at best, and so-called double frees have been known to lead to security
problems.

Passing a pointer not obtained from malloc(), calloc(), or realloc()


This seems obvious, but it’s important nonetheless. Even passing in a
pointer to somewhere in the middle of dynamically allocated memory is
bad:
free(p+10); /* Release all but first 10 elements. */
This call won’t work, and it’s likely to lead to disastrous consequences,
such as a crash. (This is because many malloc() implementations keep
”bookkeeping”information in front of the returned data. When free()
goes to use that information, it will find invalid data there. Other imple-
mentations have the bookkeeping information at the end of the allocated
chunk; the same issues apply.)

Buffer overruns and underruns Accessing memory outside an allocated chunk


also leads to undefined behavior, again because this is likely to be book-
keeping information or possibly memory that’s not even in the address
space. Writing into such memory is much worse, since it’s likely to de-
stroy the bookkeeping data.

7.5 Fragmentisanje memorije


Often, applications that are free from memory leaks but frequently allocate
and delocate dynamic memory show gradual performance degradation if they
are kept running for long periods. Finally, they crash. Why is this? Recurrent
allocation and deallocation of dynamic memory causes heap fragmentation, es-
pecially if the application allocates small memory chunks. A fragmented heap
might have many free blocks, but these blocks are small and non-contiguous.
To demonstrate this, look at the following scheme that represents the system’s
7.6 Implementacija primitivnog alokatora memorije 81

heap. Zeros indicate free memory blocks and ones indicate memory blocks that
are in use: 100101010000101010110
The above heap is highly fragmented. Allocating a memory block that con-
tains five units (i.e., five zeros) will fail, although the systems has 12 free units in
total. This is because the free memory isn’t contiguous. On the other hand, the
following heap has less free memory but it’s not fragmented: 1111111111000000
malloc() is a nightmare for embedded systems. As with stacks, figuring the
heap size is tough at best, a problem massively exacerbated by multitasking.
malloc() leads to heap fragmentation though it may contain vast amounts
of free memory, the heap may be so broken into small, unusable chunks that
malloc() fails.
In simpler systems its probably wise to avoid malloc() altogether. When
theres enough RAM allocating all variables and structures statically yields the
fastest and most deterministic behavior, though at the cost of using more mem-
ory.
When dynamic allocation is unavoidable, by all means remember that malloc()
has a return value! Fact is, it may fail, which will cause our program to crash
horribly. If were smart enough proactive enough to test every malloc() then
an allocation error will still cause the program to crash horribly, but at least we
can set a debug trap, greatly simplifying the task of finding the problem.
The programmer should concentrate on optimizing busy heap allocation
sites.
What can you do to avoid heap fragmentation? First, use dynamic memory
as little as possible. Secondly, try to allocate and de-allocate large chunks rather
than small ones. For example, instead of allocating a single object, allocate an
array of objects at once. As a last resort, use a custom memory pool.

7.6 Implementacija primitivnog alokatora mem-


orije
Let us illustrate memory allocation by writing a rudimentary storage al-
locator. There are two routines. The first, alloc(n), returns a pointer to n
consecutive character positions, which can be used by the caller of alloc for
storing characters. The second, afree(p), releases the storage thus acquired
so it can be re-used later. The routines are “rudimentary” because the calls to
afree must be made in the opposite order to the calls made on alloc. That is,
the storage managed by alloc and afree is a stack, or last-in, first-out. The
standard library provides analogous functions called malloc and free that have
no such restrictions.
The easiest implementation is to have alloc hand out pieces of a large
character array that we will call allocbuf. This array is private to alloc and
afree. Since they deal in pointers, not array indices, no other routine need know
the name of the array, which can be declared static in the source file containing
alloc and afree, and thus be invisible outside it. In practical implementations,
82 7 Dinamička alokacija memorije

the array may well not even have a name; it might instead be obtained by calling
malloc or by asking the operating system for a pointer to some unnamed block
of storage.
The other information needed is how much of allocbuf has been used. We
use a pointer, called allocp, that points to the next free element. When alloc
is asked for n characters, it checks to see if there is enough room left in allocbuf.
If so, alloc returns the current value of allocp (i.e., the beginning of the free
block), then increments it by n to point to the next free area. If there is no room,
alloc returns zero. afree(p) merely sets allocp to p if p is inside allocbuf.
#define ALLOCSIZE 10000 /* size of available space */

static char allocbuf[ALLOCSIZE]; /* storage for alloc */


static char *allocp = allocbuf; /* next free position */

char *alloc(int n) /* return pointer to n characters */


{
if (allocbuf + ALLOCSIZE - allocp >= n) { /* it fits */
allocp += n;
return allocp - n; /* old p */
} else /* not enough room */
return 0;
}

void afree(char *p) /* free storage pointed to by p */


{
if (p >= allocbuf && p < allocbuf + ALLOCSIZE)
allocp = p;
}
In general a pointer can be initialized just as any other variable can, though
normally the only meaningful values are zero or an expression involving the
address of previously defined data of appropriate type. The declaration
static char *allocp = allocbuf;
defines allocp to be a character pointer and initializes it to point to the be-
ginning of allocbuf, which is the next free position when the program starts.
This could also have been written
static char *allocp = &allocbuf[0];
since the array name is the address of the zeroth element. The test
if (allocbuf + ALLOCSIZE - allocp >= n) { /* it fits */
checks if there’s enough room to satisfy a request for n characters. If there is,
the new value of allocp would be at most one beyond the end of allocbuf.
If the request can be satisfied, alloc returns a pointer to the beginning of a
block of characters (notice the declaration of the function itself). If not, alloc
must return some signal that there is no space left. C guarantees that zero is
never a valid address for data, so a return value of zero can be used to signal
an abnormal event, in this case no space.
7.7 Hip 83

7.7 Hip
Svaki program ima prostor raspoložive memorije koju može da koristi za
vreme izvršavanja. Ovaj prostor raspoložive memorije naziva se slobodna mem-
orija ili hip. Alokacija memorije u fazi izvršavanja naziva se dinamička alokacija
memorije. Ona se postiže funkcijama kao što je malloc. Objekti koji su aloci-
rani u slobodnom memorijskom prostoru nisu imenovani. malloc ne vraća
stvarni alocirani objekat, već njegovu adresu. Preko te adrese se indirektno
manipuliše objektom. Kada završimo sa korišćenjem objekta, moramo da ek-
splicitno vratimo memoriju tog objekta u slobodnu memoriju. To se postiže
funkcijom free. free se ne poziva nad adresama objektata koji nisu alocirani
dinamički.
The heap segment provides more stable storage of data for a program; mem-
ory allocated in the heap remains in existence for the duration of a program.
The memory allocated in the heap area, if initialized to zero at program start,
remains zero until the program makes use of it. Thus, the heap area need not
contain garbage.
The heap is where dynamic memory (obtained by malloc() and friends)
comes from. As memory is allocated on the heap, the process’s address space
grows, as you can see by watching a running program with the ps command.
Although it is possible to give memory back to the system and shrink a process’s
address space, this is almost never done and this leads to memory fragmentation.
It is typical for the heap to ”grow upward.”This means that successive items
that are added to the heap are added at addresses that are numerically greater
than previous items. It is also typical for the heap to start immediately after
the data segment.
Main characteristics of the heap:

• freelist - list of free space


• on allocation - memory manager finds space and marks it as used changing
freelist;
• on deallocation - memory manager marks space as free changing freelist;
• memory fragmentation - memory fragments into small blocks over lifetime
of program;

7.8 Veličina steka i hipa


Some operating systems may have the size of the heap and stack defined
at run time. In UNIX operating system, the heap and the stack grow towards
each other. The stack grows in the up direction and the heap grows in the
down direction as indicated in the above diagram. When the stack and the
heap collide the program will crash. This is an extremely important concept for
students to understand. It is also important to understand what is going on in
the run-time stack.
84 7 Dinamička alokacija memorije

Traditionally in a process address space you would see stack growing upside
down and heap growing upwards. An application wise difference is all the mem-
ory allocated dynamically gets allocated on heap viz. malloc free, calloc etc.
The stack of an process contains stack frames( containing the return address
linkage for a function and the data required in a stack which has the qualifier
’auto’).
Although it’s theoretically possible for the stack and heap to grow into each
other, the operating system prevents that event, and any program that tries
to make it happen is asking for trouble. This is particularly true on modern
systems, on which process address spaces are large and the gap between the top
of the stack and the end of the heap is a big one. The different memory areas
can have different hardware memory protection assigned to them. The details,
of course, are hardware and operating-system specific and likely to change over
time. Of note is that both Standard C and C++ allow const items to be placed
in read-only memory.

7.9 Heap u Win32 sistemima


In its simplest form, the default heap spans a range of addresses. Some
ranges are reserved, while others are committed and have pages of memory
associated with them. In this case the addresses are contiguous, and they all
originated from the same base allocation address. In some cases the default
heap needs to allocate more memory than is available in its current reserved
address space. For these cases the heap can either fail the call that requests the
memory, or reserve an additional address range elsewhere in the process. The
default heap manager opts for the second choice.
When the default heap needs more memory than is currently available, it
reserves another 1MB address range in the process. It also initially commits
as much memory as it needs from this reserved address range to satisfy the
allocation request. The default heap manager is then responsible for managing
this new memory region as well as the original heap space. If necessary, it will
repeat this throughout the application until the process runs out of memory
and address space.
So the default heap is not really a static heap at all. In fact, it may seem
like a waste of energy to bother with managing the size of the initial heap since
the heap always behaves in the same way after initialization. Managing the
size of the default heap offers one advantage, though. It takes time to locate
and reserve a new range of addresses in a process; you can save time by simply
reserving a large enough address range initially. If you expect your application
to require more heap space than the 1MB default, reserve more address space
initially to avoid allocating another region of addresses in your process later.
Remember, each application has 2 gigabytes (GB) of address space to work
with, and requires very little physical memory to support it.
Na starim MS DOS sistemima, postojala su različita vrlo stroga ograničenja
u vezi sa veličinama memorijskih segmenata.
7.10 Demonstracija rada debagera 85

7.10 Demonstracija rada debagera


86 7 Dinamička alokacija memorije
Glava 8

Dinamičke strukture: liste i


stabla

An array is a list of elements with a fixed size, accessed by index. A more


flexible data structure is the linked list. Linked lists, stacks, queues, hash tables,
trees are all different types of data structures that can help accomodate almost
any type of data.

8.1 Liste
Linked lists are the most basic self-referential structures. Linked lists allow
you to have a chain of structs with related data.
A linked list is composed of nodes, each of which contains a piece of data
and a reference to the next node.

• A linked list can grow and shrink as much as needed.

• Adding an element to a linked list is O(1).

• Finding an element in a linked list is O(n)

So how would you go about declaring a linked list? It would involve a struct
and a pointer:
struct llnode
{
<type> data;
struct llnode *next;
};
The <type> signifies data of any type. This is typically a pointer to some-
thing, usually another struct. The next line is the next pointer to another
llnode struct. Another more convenient way using typedef:
88 8 Dinamičke strukture: liste i stabla

typedef struct list_node


{
<type> data;
struct list_node *next;
} llnode;

llnode *head = NULL;


Note that even the typedef is specified, the next pointer within the struct
must still have the struct tag! There are two ways to create the root node of
the linked list. One method is to create a head pointer and the other way is to
create a dummy node. It’s usually easier to create a head pointer. Now that we
have a node declaration down, how do we add or remove from our linked list?
Simple! Create functions to do additions, removals, and traversals (oblizak).
Additions: A sample Linked list addition function:
void add(llnode **head, <type> data_in) {
llnode *tmp;

if ((tmp = malloc(sizeof(*tmp))) == NULL) {


ERR_MSG(malloc);
(void)exit(EXIT_FAILURE);
}
tmp->data = data_in;
tmp->next = *head;
*head = tmp;
}

/* ... inside some function ... */


llnode *head = NULL;
<type> *some_data;
/* ... initialize some_data ... */

add(&head, some_data);
(Napomena: umesto (*tmp).data može da se piše tmp->data (isto važi za
bilo koji pokazivač na strukturu, dakle umesto (*a).member može da se piše
a->member).)
What’s happening here? We created a head pointer, and then sent the
address-of the head pointer into the add function which is expecting a pointer to
a pointer. We send in the address-of head. Inside add, a tmp pointer is allocated
on the heap. The data pointer on tmp is moved to point to the data_in. The
next pointer is moved to point to the head pointer (*head). Then the head
pointer is moved to point to tmp. Thus we have added to the beginning of the
list.
Removals: You traverse the list, querying the next struct in the list for the
target. If you get a match, set the current target next’s pointer to the pointer
of the next pointer of the target. Don’t forget to free the node you are removing
8.1 Liste 89

(or you’ll get a memory leak)! You need to take into consideration if the target
is the first node in the list. There are many ways to do this (i.e. recursively).
Think about it!
Traversals: Traversing list is simple, just query the data part of the node
for pertinent information as you move from next to next. What about freeing
the whole list? You can’t just free the head pointer! You have to free the list.
A sample function to free a complete list:
void freelist(llnode *head)
{
llnode *tmp;
while (head != NULL)
{
free(head->data); /* Don’t forget to free memory within the list! */
tmp = head->next;
free(head);
head = tmp;
}
}
Now we can rest easy at night because we won’t have memory leaks in our
lists!

8.1.1 Stek (LIFO lista)


A stack is a basic data structure that is used all through out programming.
Stacks are a specific kind of linked list. They are referred to as LIFO or Last
In First Out. A stack is called a LIFO (Last In First Out) to demonstrate the
way it accesses data.
A stack has two basic operations:

push puts an element on top of the stack.

pop takes an element from the top of the stack.

Stacks have specific adds and removes called push and pop. Pushing nodes
onto stacks is easily done by adding to the front of the list. Popping is simply
removing from the front of the list. It would be wise to give return values when
pushing and popping from stacks. For example, pop can return the struct that
was popped.
The idea is to think of your data as a stack of plates or books where you can
only take the top item off the stack in order to remove things from it.
We can implement a stack with a linked list.

8.1.2 Red (FIFO lista)


A queue is a basic data structure that is used throughout programming.
Queues are FIFO or First In First Out. Think of a typical (non-priority) printer
90 8 Dinamičke strukture: liste i stabla

vrh steka (i dodavanje i brisanje — push i pop)

X Y ... A NULL

novi element

novi vrh steka

Y X V ... A NULL

vrh steka nakon brisanja

X V ... A NULL

Slika 8.1: Stek

queue: The first jobs submitted are printed before jobs that are submitted after
them. You can think of it as a line in a grocery store. The first one in the line
is the first one to be served. A queue is called a FIFO (First In First Out) to
demonstrate the way it accesses data.
Red se može ilustrovati i na primereu čekaonice u kojoj svako zna ko je na
redu posle njega.
A queue has two basic operations:

add puts an element at the end of the queue.

get takes an element from the front of the queue.

Queues aren’t more difficult to implement than stacks. By creating a tail


pointer you can keep track of both the front and the tail ends of the list. So
you can enqueue onto the tail of the list, and dequeue from the front of the list!
We can implement a queue with a linked list.
8.1 Liste 91

početak reda (brisanje — get)


kraj reda (dodavanje — add)

A B ... X NULL

novi element

početak reda
novi kraj reda

A B ... X Y NULL

početak reda nakon brisanja kraj reda

B ... X Y NULL

Slika 8.2: Red

8.1.3 Dvostruko povezane (dvostruko ulančane) liste


Each element in a singly-linked list contains a single reference – a reference
to the successor (next) element of the list. As a result, deleting the head of the
linked list is easy: The new head is the successor of the old head.
However, deleting the tail of a linked list is not so easy: The new tail is the
predecessor of the original tail. Since there is no reference from the original tail
to its predecessor, the predecessor must be found by traversing the linked list
from the head. This traversal gives rise to the O(n) running time.
In a doubly-linked list , each list element contains two references–one to its
successor and one to its predecessor. There are many different variations of
doubly-linked lists.
A doubly linked list allows us to traverse the list backwards and forwards.
Each node has a reference to both the next and the previous node.
Double linked lists allow us to very quickly (O(1)) remove an element from
either the front or the back of the list. Removing the last element of a singly
linked list is O(n).
92 8 Dinamičke strukture: liste i stabla

početak dvostruko povezane liste kraj liste

...
NULL A B C ... Z NULL

Slika 8.3: Dvostruko povezana lista

8.1.4 Kružne (ciklične, cirkularne) liste


A list can šwallow its tail”by having the pointer in the tail item contain the
location of the first item, instead of NULL. Now, the position of the head is
rather arbitrary, for it is possible to get from any item to any other one. If
items were frequently added near each other, it might make sense to change the
head pointer so that it always pointed to the most recent insertion.
A list in which the last item points to the first item is said to be circular.

početak ciklične liste

A B C ... Z

Slika 8.4: Dvostruko povezana lista

In a circular list, there is in a sense no such thing as a last item, but it is


still useful to keep a tail pointer to find the last logical item–after all, it can no
longer be found by searching for a NULL pointer. It might also be useful to
maintain a variable that stores the number of active items in the list, so that
a traverse with a high index number does not waste time by going around the
circle and examining items more than once. In such a list, inserting before the
head changes the head pointer to point to the new item, and inserting after the
tail changes the tail pointer to point to the new item, but both place a new item
after the old tail and before the old head in the circle.
8.1 Liste 93

8.1.5 Liste: primer

Primer 8.1. Naredni primer ilustruje rad sa listama. Lista koja se koristi
je jednostruko povezana i za nju je implementirano više funkcija nego što je
neophodno za stek i red (npr. implentirane su funkcije i za ubacivanje na početak
i za ubacivanje na kraj liste).
94 8 Dinamičke strukture: liste i stabla

#include <stdio.h>
#include <stdlib.h>

typedef struct cvor


{
int br;
struct cvor* sl;
} CVOR;

/* Pomocna funkcija koja kreira cvor liste


sa datim sadrzajem.
Funkcija kreira cvor i postavlja mu sadrzaj
na dati broj.
Polje sl ostaje nedefinisano.
Funkcija vraca pokazivac na kreirani cvor. */

CVOR* napravi_cvor(int br)


{
CVOR* novi = (CVOR*)malloc(sizeof(CVOR));
if (novi == NULL)
{
fprintf(stderr, "Greska prilikom
alokacije memorije\n");
exit(1); /* OVO NIJE LEPO - NIJE OSLOBODJEN ALOCIRANI PROSTOR */
}
novi->br = br;
return novi;
}

/* --------------------------------------------- */

/* Ispisivanje liste : iterativna verzija */


void ispisi_listu_i(CVOR* l)
{
CVOR* t;
for (t = l; t != NULL; t=t->sl)
printf("%d ", t->br);
}

/* Ispisivanje liste : rekurzivna verzija */


void ispisi_listu_r(CVOR* l)
{
if (l != NULL)
{
printf("%d ", l->br);
ispisi_listu_r(l->sl);
}
}

/* Ispisivanje liste unatrag : rekurzivna verzija */


/* Prethodna funkcija se lako modifikuje tako da
ispisuje listu unazad */

void ispisi_listu_unazad(CVOR* l)
{
8.2 Stabla 95

8.2 Stabla
Trees are natural structures for representing certain kinds of hierarchical
data. A (rooted) tree consists of a set of nodes (or vertices) and a set of arcs (or
edges). Each arc links a parent node to one of the parent’s children. A special
root node has no parent. Every other node has exactly one parent. It is possible
to reach any node by following a unique path of arcs from the root. If arcs are
considered bidirectional, there is a unique path between any two nodes. The
simplest kind of tree is a binary tree where each parent has at most two children.
A simple binary tree involves having two types of ”next”pointers, a left and a
right pointer. You can halve your access times by splitting your data into two
different paths, while keeping a uniform data structure. But trees can degrade
into linked list efficiency. There are different types of trees, some popular ones
are self-balancing. For instance, AVL trees are a typical type of tree that can
move nodes around so that the tree is balanced without a > 1 height difference
between levels. 1

8.2.1 Binarna stabla


Until now, we’ve only looked at lists that have only one dimension”: for-
ward/backward or next/previous. Consider a structure that acts as a ”par-
ent”and has at most two čhildren”(a binary tree)
struct Node
{
int Value;
struct Node *Left;
struct Node *Right;
};
Trees are fun to use because you can easily add more children to the existing
children.

8.2.2 Uredena binarna stabla


• With the trees we’re working with, the left child always has a Value less
than or equal to the parent’s Value. The right child always has a Value
greater than the parent’s Value.
• You can always add a new child in the proper position (to the left or right
of the parent).
• The tree is always fully sorted (how)?
• The tree is easily searchable.

Creating a binary ordered tree from a random data set (also called binary
search tree):
1 Više o stablima pročitati na adresi: http://www.csse.monash.edu.au/ lloyd/tildeAlgDS/Tree/
96 8 Dinamičke strukture: liste i stabla

NULL NULL

NULL NULL NULL

NULL NULL

Slika 8.5: Stablo

• First element stored in the root node

• Compare the second element with the root node, if the new value is less
than the parent’s value, then move left (e.g. Pointer = Pointer->Left).
Otherwise go right.

• Keep on comparing new value and keep on moving (left or right) until you
reach a NULL pointer. Append the new node at that location.

Searching an ordered binary tree is just as easy as inserting something in a


tree:

1. Set a Pointer to point at the root structure.

2. If the value we’re looking for == the Pointer value, return the Pointer.
8.2 Stabla 97

17, 12, 21, 15, 5, 14


17

12 21

NULL NULL

5 15

NULL NULL NULL

inf ix : 5, 12, 14, 15, 17, 21


14 pref ix : 17, 12, 5, 15, 14, 21

postf ix : 5, 14, 15, 12, 21, 17


NULL NULL

Slika 8.6: Uredeno stablo

3. If the value we’re looking for is < the Pointer value, go left. (e.g. Pointer = Pointer->Left)
And goto (2)

4. Otherwise go right and goto (2)

5. If the Pointer is ever NULL, return NULL to indicate that the value was
not found in the tree.

Most of the tree functions can be implemented using recursion. The code is
easily readable and understandable.
98 8 Dinamičke strukture: liste i stabla

Kreiranje elementa

struct Node *Create_Node(int Value)


{
struct Node *Ptr = NULL;
Ptr = malloc(sizeof(struct Node));
assert(Ptr != NULL);
Ptr->Left = NULL;
Ptr->Right = NULL;
Ptr->Value = Value;
}

Dodavanje elementa

Iterativna verzija dodavanja elementa:

void Insert_Node(struct Node *Root, struct Node *New)


{
while(1)
{
if (New->Value <= Root->Value)
if (Root->Left == NULL)
{
Root->Left = New;
return;
}
else
Root = Root->Left;
else
if (Root->Right == NULL)
{
Root->Right = New;
return;
}
else
Root = Root->Right;
}
}

Rekurzivna verzija dodavanja elementa:


8.2 Stabla 99

void Insert_Node(struct Node *Root, struct Node *New)


{
if (New->Value <= Root->Value)
if (Root->Left == NULL)
{
Root->Left = New;
return;
}
else
Insert_Node(Root->Left, New);
else
if (Root->Right == NULL)
{
Root->Right = New;
return;
}
else

Insert_Node(Root->Right, New);
}
Napomena: umesto (*New).Value može da se piše New->Value (isto važi za
bilo koji pokazivač na strukturu, dakle umesto (*a).member može da se piše
a->member).

Pronalaženje elementa

Searching an ordered binary tree is just as easy as inserting something in a


tree:

1. Set a Pointer to point at the root structure.

2. If the value we’re looking for == the Pointer value, return the Pointer.

3. If the value we’re looking for is < the Pointer value, go left. (e.g. Pointer = Pointer->Left)
And goto (2)

4. Otherwise go right and goto (2)

5. If the Pointer is ever NULL, return NULL to indicate that the value was
not found in the tree.
100 8 Dinamičke strukture: liste i stabla

struct Node *Tree_Find(struct Node *Root,int Value)


{
if (Root == NULL)
return NULL; /* Not found */
if (Value == Root->Value)
return Root; /* Found it */
if (Value < Root->Value) /* Go left */
return Tree_Find(Root->Left, Value);
return Tree_Find(Root->Right, Value);
}

Obilazak stabla u dubinu


How do we get at the sorted content of a tree?
• We know that an ordered binary tree is fully sorted. We’d like to take
advantage of that.
• The ”least”element in the tree is at the far left.
• The ”greatest”element is at the far right.
• Our tree nodes do not point back to their parents.
• How can we start at the far left and go through each node in order?
Accessing each of the nodes of a tree in order is often called Tree Traversal
or Iterating over a Tree. We can do this in several ways:
• infix, Least to greatest: For each node, access the left node recursively,
then the node itself, then the right node recursively. (Abbreviated L-N-R)
• infix, Greatest to least: Same way except R-N-L.
• Prefix: N-L-R
• Postfix: L-R-N
Example of ordered printing (infix, Least to greatest):
void Print_Tree(struct Node *Ptr)
{
if (Ptr == NULL)
return;
Print_Tree(Ptr->Left); /* Go left */
printf("%d\n", Ptr->Value); /* Node */
Print_Tree(Ptr->Right); /* Go right */
}

Primer 8.2. Primer za vežbu: napraviti ispis elemenata stabla kao u stablu
koje prikazuje direktorijume (npr. u Windows Exploreru)
8.2 Stabla 101

Obilazak stabla u širinu


U obilasku stavla u širinu, obilazi se (i obraduje) nivo po nivo stavla.
A breadth-first traversal of a tree starts at the root of the tree. It next visits
the children, then the grand-children and so on.
The numbers indicate the order in which the nodes are visited, not the
contents of the nodes. Because children are only accessible from a parent, they
must be stored while the parent’s siblings and cousins are visited. Usually, a
queue is used to do this.

8.2.3 Izrazi u formi stabla


Matematički izrazi prirodno se mogu reprezentovati u vidu stabla. Na
primer, izraz 3*(4+5) može se reprezentovati kao stablo u čijem je korenu *,
sa levim podstablom 3, i sa desnim potomkom čvor +, koji ima potomke 4
i 5. Ispisivanjem elemenata ovog stabla u infiksnom obilasku (sa dodatnim
zagradama) dobija se izraz u uobičajenoj formi. Ispisivanjem elemenata ovog
stabla u prefiksnom obilasku dobija se izraz zapisan u takozvanoj prefiksnoj
poljskoj notaciji. Vrednost izraza reprezentovanog stablom može se izračunati
jednostavnom funkcijom.
102 8 Dinamičke strukture: liste i stabla
Deo III

Principi razvoja programa


Glava 9

Strukturna dekompozicija i
druga načela pisanja
programa

Ultimately,1 the only way a piece of software can impact the world is by doing
something useful. People learn that fact in colleges and try to make working
software. For them, the important aspects of code become, in the order of
priority, getting the task done, and getting it done correctly. As they get more
experience, they realize that there are other important aspects of software:

• It costs more to maintain and modify the software than develop the soft-
ware.

• A long living software is read more often than it is written.

• Usually, correctness of the code is correlated to the readability of the code.

All in all, we feel that the primary purpose of the code is to communicate to
the other developers within the confines of a compiler specified medium. Even
though readability is an auxiliary goal, aiming for it lets us get to the primary
goal: working code that is correct.
Normally, as soon as people start a project, they agree on a set of coding
guidelines. These coding guidelines spell out, in elaborate detail, what the
syntactic conventions are: how to use underscores, camelHump style, and how
to indent. While these guidelines are of good intentions, it ends up getting
ignored for several reasons: they do not explain why these rules make sense;
they do not say which rules are more important; they do not equip the coder
with the rationale behind the rules.
1 Zasnovano na tekstu Ramarao Kanneganti: Best Practices in Writing Readable Code
106 9 Strukturna dekompozicija i druga načela pisanja programa

9.1 Pisanje čitljivih programa: vizualni elementi


programa
Most of the code is read on the computers these days. People use their editor
or their IDE (Intergrated Development Environment) to read and modify the
code. Sometimes, they use the browser to read the code as well. People who
write good code understand that. They make it easy to read code on the
computer.
In fact, most coding guidelines are aimed at providing good readability. They
capture the principles of readability in the form of specific rules of formatting,
indenting, and naming rules. Naturally, such excessive specifications are vio-
lated. Or, even if people confirm to the rules, the intent behind the rules is
violated. That is, the underlying principles of consistency and clarity that helps
readability is violated.
Thankfully, these problems are easy to solve. These days we have tools
that makes it easy to follow formatting guidelines. We are going to provide a
configuration in the appendix. We also will explain the intent and the logic
behind the rules so that the writers can make sure the final goals are met.
One of the aspects of formatting of the code is visual appeal of the code.
It applies to spacing, grouping, and indenting of information. This issue is
important enough that some languages attach meaning to indentation (example:
python). The principle behind the visual appeal of a code is this: can you
looking at the code on the screen find what you are looking for? Can you open
a file and figure out if this is the file you need to look at? All our guidelines are
around that principle. We are going to enumerate them here.

9.1.1 80 karaktera u liniji


These days people rarely print out code. They read the code in their editor
or IDE. Even if the screen resolution is very good, people may have several
windows open such that the code may be less than 80 columns.
Aha, you say. What about using the wordwrap feature?
Two problems: One is that the reader may want to print it out. Some
printers will cut the characters beyond the specified width. The other is, some
editors may not wrap the lines. For example, a browser does not wrap lines.
Also, the wordwrap feature will not be able to do the right alignment. A struc-
tured program becomes a flattened prosaic statement. The readability of the
code is reduced.
Then, what about the lines exceeding 80 characters?
More often than not, such code is a suspect. Either the depth of the code
is too much, in which case, you are better off making it more modular, or the
indirection is too much, in which case, you are better off introducing temporary,
meaningful variables. This is a heuristic, but a darn good one.
In the rare cases where it is needed, you can go the next line and align the
argument, or operator appropriately. When in doubt, run the Unix program
9.1 Pisanje čitljivih programa: vizualni elementi programa 107

indent, and it will produce the indented program. It has gazillion options for
you to tinker to produce the kind of output you want. For Java, there is jalopy
that does all this and much more.

9.1.2 Broj naredbi po liniji


Leaving blank statements helps to focus the reader. For example, you can
club all the related statements in a visual block. On the flip side, leaving blank
lines where they are not needed is a bad idea. Excessive blank lines do not make
code more readable. In fact, if any, the linguistic affinity between the elements
get lost. Compare the following:
With blank lines:
for (i = n; i > 0; i --)
{

sq_n += 2*i - 1;
if (sq_n > MAX_VAL)
{
printf("Exceeded size");
exit(1);
}
}
Without Excessive blank lines:
for (i = n; i > 0; i --) {
sq_n += 2*i - 1;
if (sq_n > MAX_VAL) {
printf("Exceeded size");
exit(1);
}
}
Blank lines create the effect of paragraphs in the code; thus they should be
used to group all the statements that belong in a logical unit. If you can write
a comment on what the next group doing and why, then perhaps that group of
statements deserves to be together.
Excessive blank lines rob the legitimacy of the needed breaks. In addition,
they take up valuable real estate on the screen.
Along with the žeroštatement lines, we need to be concerned with more
than one statement for a line also. Conventional wisdom states that number
of statements to a line should be restricted to one. The exceptions are sim-
ple initializations and simple statements. For example, int i=10; int j=20
can be on one line. In fact, leaving those statements on one line focuses the
reader better. However, if the initializations need explanations, place them on
a separate line.
108 9 Strukturna dekompozicija i druga načela pisanja programa

9.1.3 Nazubljivanje/uvlačenje teksta


There are many religious wars about indentation. Ultimately, there are two
biggest properties you look for in any rules about indentation style: Consistency
and readability. Within these parameters, the following suggestions can be
made:
• Do not use tab characters in your code. That is, do not use tabs to indent
your files. Fortunately, these days most editors offer an option to convert
tabs into spaces as you type. Check your editor on how to turn that option
on.
Why is this important? Tabs expand to different number of spaces on
different editors. For example, you can set tab length as 4 chars in one
editor. When you open the file in another editor, it will show 8 spaces
causing misalignment.
• Align the new line with the beginning of the expression at the same level
on the previous line. i.e. Do block related statements together. Braces are
useful for compilers, but humans still rely on indentation and blank lines
to group statements together. Use as appropriate number of spaces for
indentation: I personally use 2 spaces. It means, that I can keep my lines
to 80 columns without resorting to continuation lines. Anything less than
two spaces do not provide the visual cues needed for grouping statements.
These formatting issues can best be explained by an indenting program. If
you follow these guidelines, you get these benefits.
• The reader can read most code on the modern terminals and on the paper.
• The reader will see the code as you intended (tabs to spaces).
• The reader can see the logic through the concrete structure of the code.

9.2 Imenovanje promenljivih i funkcija


For a compiler, as long as we are sticking to the language rules, it does not
matter what kind of names we use for the entities in our programs: files, classes,
objects, and variables. However, for readers, it matters how the variables are
named. They are used to communicate the concepts at a domain level. Here
are some of the do’s and don’ts.
• It is OK to name loop variables as i and j. It is a long standing convention
that treat i,j,k as loop variables.
• For common suffixes and prefixes, agree on a standard. For example, for
number, people use ”no”or ”num”or use it as prefix or suffix. For example,
number of books can be referred to as ”booknum”, ”numbooks”and some
other combinations. Establishing a common notation and stems would
help come with names that are readily understandable.
9.3 Komentari 109

• Avoid complicated Hungarian notation. You are not a type checker. It


is the job of a compiler. Use the language that is closest to what people
speak. For reference, Hungarian notation encodes type information of a
variable in the name. For example, they use ”p”as a prefix for variable
name, if the variable is a pointer. Or, š”for String. I do not believe
”ppiName”is a good variable name, as the type information is more im-
portant for a compiler than the reader of a program.

• Avoid using generic names such as User, customer. Use specific names just
as in writing prose. If a system has several kind of users, use appropriate
name as in administrator or clerk and so on. If a function computes av-
erages, name it as "computeAverages" instead of "doTask". If a boolean
variable tests if the cart is empty, call it ”isCartEmpty”instead of ”flag”.

• As far as camelHump notation (popular in Java world, where people use


Capital letters to denote word break in the name), or word_underscore
notation (popular in C world), use whichever the standard is in that lan-
guage. I personally find underscores make the word break real and visible,
but I am not going to fight the establishment on that one.

The real test is always this: If you can read the code over the phone
so that listener can comprehend it, then the naming has achieved its objec-
tive. For example, naming the procedure as a verb like šetupChannel”reads
better. Naming a function such as a = average(1,2,3) reads better than
a = computeAverage(1,2,3).

9.3 Komentari
Several good books have been written about this subject out of which I would
recommend Čode Complete”. The fundamental principle is that in general code
should convey all the meaning to the readers. However, syntactic constraints
may not make it possible. More over, unlike a book where we can recast the
matter for different audiences, we cannot write the code for different audience.
In several such cases, you can add comments to the code as an extension to the
code.
Writing comments should not provide an excuse to write poorly structured
code. Comments can only add information at a different level.

• Comments should explain the domain portion, not the code.

• Comments should tell the reader why and what you are doing it, rather
than how.

• In case the code is tricky, explain the how, and tell them explicitly why it
is tricky.

• Use commenting style that is easy to maintain.


110 9 Strukturna dekompozicija i druga načela pisanja programa

9.3.1 Komentari ne treba da objačnjavaju kôd


The worst kind of comments are the ones that explain the code. E.g.:
a = a +1 // Increment a by 1.
Well written code is self explanatory. However, code cannot explain why
something is being done. Comments can explain that part. For example, the
earlier statement may become:
a = a + 1 // Add a bonus point to the customer if he makes it here.
Thus, comments can explain the intent, the domain portion.
In some cases, we may have to explain the code. For example, if you are
implementing a complex search algorithm, you may have to explain in the com-
ments what you are doing. If your code contains unexpected or unseen effects,
you should use comments to explain that to the reader. E.g.:
isCartEmpty= true; if (! isCartEmpty) { ....} // Since other threads
may be accessing
// isCartEmpty, we need to do this test
Btw, the same principle can be stated as: Comments should not repeat
information. If it does, there is every chance of the code and comments being
out of sync, confusing the reader.

9.3.2 Komentari treba da su takvi da ih je moguće održavati


We often see comments that are formatted beautifully, often by hand. For
example, see this:
/***************************
* Author: Rama *
***************************/
If the name of the author changes, then the alignment can get messed up.
So follow a simple style of comments with using blank lines and spaces to com-
municate the ideas. Do not depend on complex formatting. You can correct the
previous code with:
// Author: Rama

9.3.3 Komentari treba da budu koncizni


Since reading any text takes time, we should make comments as brief as
possible. You can refer to other portions of the code to simplify the comments.
As much as possible, use the comments to explain the domain relating to the
requirements and use cases.

9.3.4 Korišćenje specifičnih komentara


Several experienced coders use standard markers in the code to denote cer-
tain standard concepts in the code. These markers can easily be picked out by
9.4 Pisanje programa: modularnost i podela na datoteke 111

programs such as ”grepšo that they can be acted upon. Most popular markers
are:

• TODO marker: It is used to identify the tasks yet to be done. In fact,


some IDEs like Eclipse can even show a list of TODO tasks. It is an
excellent way to keep track of things to be done in the code. Make sure
that all of the TODO is one line, so that it can be ”grepped”. It is also
a good practice not to write to many TODO’s clubbed at one place. For
example, the following is not preferred.
//TODO: Add Street name
//TODO: Add second phone number
//TODO: Add mobile phone number

Instead, the following works well. It is one line and it clubs all of them
into one TODO.
//TODO: Add second address field to the customer class.

• FIXME marker: In the code, there are several places, you may take short
cuts knowing fully well that it needs to be fixed later. In such cases, you
can use FIXME marker to denote that activity. While TODO denotes
unfinished activity, FIXME denotes code that needs fixing later on. For
example, consider the following:
//FIXME: Used only 50 chars for the name. Make it dynamic string.
char name[50];

• XXX Marker: This marker is private marker. It is generally used only for
the developer to be deleted by the time he is done. It is generally used as
note to the developer.

9.4 Pisanje programa: modularnost i podela na


datoteke
The preceding section described the ways in which you can use formatting to
communicate the program better to the readers. Programming languages have
mechanisms such as functions to create groups of statements. These functional,
object decomposition has several purposes including reuse.
The physical structure of a large program is exposed through files. Each
file again structured through functions (even in the OO world). So, to properly
structure the program we need to structure the files. Here, I am not going
to describe only the physical structure of the files. In the later section I will
describe the logical structure of the files.
112 9 Strukturna dekompozicija i druga načela pisanja programa

9.4.1 Deljenje programa u više datoteka


How do we break program into files? In most OO languages, each class gets
a file. In non OO language, each cohesive set of functions get a file. In either
case, we need to follow these guidelines.

• File length should not exceed a few hundred lines. Normal recommenda-
tion is 200 or so. However, if logic demands that the file should be larger,
try using auxiliary classes or auxiliary courses.

• Make sure each file contains at least sizable information. Excessive frag-
mentation of a program through large number of files can make it difficult
to read.

• If a group of developers are working on the project, make sure that a file is
under control of one developer. That is, to understand and work with that
file it should not take knowledge from disparate areas of the the domain
or programming. For example, if a file contains knowledge about SQL,
parsing, and numerical computing, there are not many people who know
enough about these areas to own the file.

In addition to these reasons, a proper decomposition of the code into files


help version control in the groups. Most version control systems treat files as a
unit and provide versioning at only file level.

9.4.2 Modularnost
There are several ways the application can be made modular. Files, classes,
and other structural elements help to organize the program.
One important purpose in organizing the program is to isolate the moving
parts. That is, any program contains mature code and the code that is un-
dergoing rapid transformation. It contains fixed entities and the entities that
need customizing. A good coding style separates these so that customizing,
modifying, and extending programs becomes easy.
Since customization and extensions of the code typically deals with a small
subset of the code, it is a good idea to isolate that code. Use separate functions
and files that encapsulates the changing code.

9.4.3 Kako koristiti konstante u programu


That is, unless they are cosmic constants (say π or e), people want to change
the parameters in the code. So, do not use constants in there. For example, the
following is bad.
char streetName [50];
We can correct it by:
#define FIFTY 50 char streetName[FIFTY];
9.5 Pisanje programa: dizajniranje programa 113

Still bad. What if we use the length in multiple places and we want to change
it to 75? It would read
#define FIFTY 75.
We can make it more readable by:
#define arrayLen 50 char streetName[arrayLen];
Even this can be made better by referring to the constant by its semantic
significance. For example, it can be street name length. In which case, it be
made:
#define STREETNAMELENGTH 50 streetName char[STREETNAMELENGTH];
As the next step, you collect all these constants into a separate file and
through #include mechanism to use in other parts of the code. Of course, you
should use language facilities to define constants instead of #define. So the
next version will be:
const int STREETNAMELENGTH=50 /* To be placed in a central file.
*/ char streetName[STREETNAMELENGTH]; /* Declare it where used.
*/

9.5 Pisanje programa: dizajniranje programa


Have you read any good code lately? Code that makes you feel like exploring
where you can progressively zoom into the topics you need to understand, and
glance through the topics you already understand? Or, the code that lets you
skip portions of unneeded code so that you can get to the portion you need to
read?
Well, if you have not read such code, you should read code. Reading code
is one of the best ways to write code, just like prose. Some samples of code
I liked over the years, for example, include ghostscript (A software postscript
interpreter), Scheme interpreter from Rice University, several code packages
from GNU software.
One of the most important skills that are required to write good programs
is structural decomposition. 2

9.5.1 Dizajn u vidu tokovnika


Data-flow design is concerned with designing a sequence of functional trans-
formations that convert system inputs into the required outputs. The design
is represented as data-flow diagrams. These diagrams illustrate how data flows
through a system and how the output is derived from the input through a
sequence of functional transformations. Data-flow diagrams are a useful and
intuitive way of describing a system. They are normally understandable with-
out special training, especially if control information is excluded. They show
end-to-end processing. That is, the flow of processing from when data enters
2 Delom zasnovano na pogljavlju ”Function-oriented design”knjige Ian Sommerville: Soft-

ware Engineering
114 9 Strukturna dekompozicija i druga načela pisanja programa

the system to where it leaves the system can be traced. Data-flow design is
an integral part of a number of design methods and most CASE tools support
data-flow diagram creation. Different methods may use different icons to rep-
resent data-flow diagram entities but their meanings are similar. The notation
which I use is based on the following symbols:

• Rounded rectangles represent functions which transform inputs to out-


puts. The transformation name indicates its function.
• Rectangles represent data stores. Again, they should be given a descriptive
name.
• Circles represent user interactions with the system that provide input or
receive output.
• Arrows show the direction of data flow. Their name describes the data
flowing along that path.
• The keywords and and or. These have their usual meanings as in boolean
expressions. They are used to link data flows when more than one data
flow may be input or output from a transformation.

Data-flow diagrams show functional transformations but do not suggest how


these might be implemented. A system described in this way might be imple-
mented as a single program using functions or procedures to implement each
transformation. Alternatively, it could be implemented as a number of commu-
nicating tasks.

9.5.2 Funkcijski-orijentisan dizajn


A function-oriented design strategy relies on decomposing the system into
a set of interacting functions with a centralised system state shared by these
functions. Functions may also maintain local state information but only for the
duration of their execution. Function-oriented design has been practised infor-
mally since programming began. Programs were decomposed into subroutines
which were functional in nature. In the late 1960s and early 1970s several books
were published which described top-down functional design. They specifically
proposed this as a structured design strategy (Myers, 1975; Wirth, 1976; Con-
stantine and Yourdon, 1979). These led to the development of many design
methods based on functional decomposition. Function-oriented design conceals
the details of an algorithm in a function but system state information is not
hidden. This can cause problems because a function can change the state in
a way which other functions do not expect. Changes to a function and the
way in which it uses the system state may cause unanticipated changes in the
behaviour of other functions. A functional approach to design is therefore most
likely to be successful when the amount of system state information is min-
imised and information sharing is explicit. Systems whose responses depend
on a single stimulus or input and which are not affected by input histories are
9.5 Pisanje programa: dizajniranje programa 115

naturally functionally-oriented. Many transaction-processing systems and busi-


ness data-processing systems fall into this class. In essence, they are concerned
with record processing where the processing of one record is not dependent on
any previous processing. An example of such a transaction processing system is
the software which controls automatic teller machines (ATMs) which are now
installed outside many banks. The service provided to a user is independent of
previous services provided so can be thought of as a single transaction. In this
design, the system is implemented as a continuous loop and actions are triggered
when a card is input. Functions such as Dispense_cash, Get_account_number,
Order_statement, Order_checkbook, etc. can be identified which implement
system actions. The system state maintained by the program is minimal. The
user services operate independently and do not interact with each other. An
object-oriented design would be similar to this and would probably not be sig-
nificantly more maintainable.

9.5.3 Strukturna dekompozicija


Traditional computer science and most if not all rely on the idea of modu-
larfunctionalism: that structural and functional decomposition are one and the
same. It is necessary first to understand the ideas of structural and functional
decomposition. Structural decomposition refers to the natural decomposition
into “pieces” or components of the system. For example, a cup might logically
decompose into its handle and its bowl. Alternately, we can talk about the
functional decomposition of the cup: it can be used to hold liquid, and it can be
carried. In this case, there is a clear coupling between the structural and func-
tional decompositions of the system: the handle admits the carrying function,
while the bowl is responsible for the holdingliquid part of the job. If the bowl
were to break, the handle would still work, and vice versa. Ponekad struktura
i funkcionalna podela ipak nisu isto.
Once you start reading code, you realize on basic tenet: at most your brain
will comprehend code spread over 25 or so lines. Anything beyond, your brain
will try to break into chunks. When writing prose, will you have a paragraph
stretching pages? Think of the code same way. Different programming lan-
guages offer different compositional units. The most common one is a functional
or procedural decomposition. In this model, you take a large function and break
it down into sub-functions. The reasons for decomposition is for two reasons:

• Reuse of the code: If properly decomposed, the parts of abstraction can


be reused elsewhere. For example, suppose you refer to a patient by SSN
(Social Security Number, a unique identifier in the US), then there must
be code that pulls up the patient record given SSN. That code, if isolated,
can be referred through proper function.

• Ease of understanding: Even if there is no reuse, sometimes, breaking the


code into smaller parts makes it more readable. It lets the writer code
at a uniform level, preferably that of the domain, hiding the details of
116 9 Strukturna dekompozicija i druga načela pisanja programa

exceptional logic and language objects into functions that do not clutter
the mainline code.

Normally, people do not decompose the code as an afterthought. They write


the code from top down, resulting in a code that is naturally readable, and
naturally well decomposed. Here is an example.
Consider the case of patient record system. When a patient walks in for an
appointment, here is what the system might do: First his SSN is entered. If
there is a previous history, that is pulled up. If there no history, his information
is collected and entered into the system. After that, the purpose of his visit is
entered into the system. Next, the name of the doctor is entered. His insurance
information may be verified and the name of the verifier may be noted. The
patient is advised of his rights and that fact should be noted in the system.
Suppose, we write the entire process in one function, clubbing the database
retrieval, and adding the information to the database, and validation of SSN
– the system can get unreadable. We will lose the information at the domain
level (as presented in the preceding paragraph). Here is a possible rendition of
the code:
//This code is executed when the patient visits for the first time:
Get Patent Information If the patient is not there in the system, add
him to the system. Obtain the purpose of the visit Enter the name of
the provider Verify the insurance Information Obtain the proof of
advising of the rights.
Turning it into more čode like”:
// This code gets executed when the patient visits for the first time
// Get the patient information
Patient = getPatient(getSSN());
// If the patient is not there in the system, add him to the system.
if (Patient == null) { createPatient(InputSystem.getSSN()); }
// Obtain the purpose of the visit
recordPurposeOfVisit(Patient);
//Enter the name of the providerenterProvidersName(Patient);
// Verify the insurance Information
verifyInsurance(Patient);
//Obtain the proof of advising of the rights.
adviseTheRights(Patient);
Of course, this code is not real. However, it helps you in two ways. You
can retain the comments that help us show what we are trying to do. It also
tells us more about the kind of objects we need. Here, we have two nouns:
Patient and Visit. We also may have a linguistic object like session that deals
with the current session with the patient. We also may have a glue object like
PatientRights.
With that in mind, we will redo the code.
9.5 Pisanje programa: dizajniranje programa 117

// This code gets executed when the patient visits for the first time
// Get the patient information
currentPatient = Patient.getPatient(currentSession.getSSN());
// If the patient is not there in the system, add him to the system.
if (currentPatient == null) {
currentPatient=createPatient(currentSession); }
// Obtain the purpose of the visit
currentVisit = new Visit(currentPatient);
currentVisit.recordPurpose(currentSession.getPurpose());
//Enter the name of the provider
currentVisit.recordProvider(currentSession.getProvider());
//Verify if the insurer is still current.
currentPatient.verifyInsurer(currentSession);
// Advise the rights, and record that information.
currentPatient.adviseRights(PatientRights.getRights(currentPatient),
currentSession.getOperator());

Depending on the objects you may have and the complexity of the code, this
can result in a few hundreds of lines. For example, the simple function getSSN,
may become something like:
// Get the Social Security Number from the user and validate
SSN = getInput("Enter SSN:"); if (isValid(SSN)) return SSN; for (int
i = 0; i < MAX_NUMBER_OF_TRIES, i++) {
SSN = getInput("Invalid SSN. Enter SSN:");
if (isValid(SSN)) return SSN;
} throw InvalidInput("Invalid SSN");

While this code is certainly needed, if we placed it in the main code, it would
clutter up the rest of the code. By separating it out, we have achieved our two
purposes: Reuse (in case we need to get the SSN from the user somewhere
else), and Readability. Notice that validating SSN is pushed into some other
routine to separate such non-domain logic from a domain specific constraints
(max number of times you can ask for SSN) achieves the same goals. Here are
the rules of thumb with this approach:
Have functions that are smaller than 25 lines. Best way to create new func-
tions is to abstract some part of the problem (domain, technical, or linguistic)
and provide a function for that. It always should be possible to get such an
abstraction going. Use the English description you write as you refine the code
as comments. It tells you the domain level activities that tell the reader what
you are doing. For complex logic functions, include the algorithm before the
function body and after the comment section. If the function is large, break
down into blocks, where each block is doing some unique activity. Use one line
comment describe that activity. Use appropriate formatting scheme to cut down
on excessive lines. For examples, ornate commenting scheme is not good. Plac-
ing empty lines that does not indicate some semantic separation to the reader
is not good. Also, use K&R scheme of indenting to maximize the information
118 9 Strukturna dekompozicija i druga načela pisanja programa

to lines ratio.

9.5.4 Strukturalni model sistema


As well as a data-flow model of a system, it is also useful to develop a
structural system model. This structural model shows how a function is realised
by a number of other functions which it calls. Structure charts are a graphical
way to represent this decomposition hierarchy. Like data-flow diagrams, they
are dynamic rather than static system models. They show how one function calls
others. They do not show the static block structure of a function or procedure.
A function is represented on a structure chart as a rectangle. The hierarchy
is displayed by linking rectangles with lines. Inputs and outputs (which may
be implemented either as parameters or shared variables) are indicated with
annotated arrows. An arrow entering a box implies input, leaving a box implies
output. Data stores are shown as rounded rectangles and user inputs as circles.
Converting a data-flow diagram to a structure chart is not a mechanical process.
It requires designer insight and creativity. However, there are several rules of
thumb which may be applied to help designers assess if their decomposition is
likely to be a reasonable one:

• Many systems, particularly business systems for which functional design


is most appropriate, can be considered as three-stage systems. These are
input some data, perhaps with validation and checking, process the data
then output the data, perhaps in the form of a report or perhaps to some
other file. A master file may also be updated. The first-level structure
chart may therefore have 3 or 4 functions corresponding to input, process,
master-file update and output.

• If data validation is required, functions to implement these should be


subordinate to an input function. Output formatting, printing and writing
to disk or tape should be subordinate to an output function.

• The role of functions near the top of the structural hierarchy may be to
control and coordinate a set of lower-level functions.

• The objective of the design process is to have loosely coupled, highly


cohesive components. Functions should therefore do one thing and one
thing only.

• Each node in the structure chart should have between two and seven sub-
ordinates. If there is only a single subordinate, this implies that the unit
represented by that node may have a low degree of cohesion. The compo-
nent may not be single function. A single subordinate means that another
function has been factored out. If a node has too many subordinates, this
may mean that the design has been developed to too low a level at that
stage.
9.5 Pisanje programa: dizajniranje programa 119

Three process steps, which follow these guidelines, can be identified for the
transformation process from data-flow diagram to structure chart:

• Identify system processing transformations These are the transformations


in the diagram which are responsible for central processing functions.
They are not concerned with any input or output functions such as reading
or writing data, data validation or filtering or output formatting. Group
these transformations under a single function at the first-level in the struc-
ture chart.
• Identify input transformations These are concerned with reading data,
checking it, removing duplicates, etc. These should also be grouped under
a single function at the first-level in the structure chart.
• Identify output transformations These are transformations which prepare
and format output or write it to the users screen or other device. However,
there are several rules of thumb that can be applied to help designers in
this process.
120 9 Strukturna dekompozicija i druga načela pisanja programa
Glava 10

Programi koji se sastoje od


više datoteka

10.1 Povezivanje
U slučaju da je izvorni program sačinjen od nekoliko jedinica prevodenja1 ,
svaka jedinica prevodenja se kompilira nezavisno, a tek povezivač (eng. linker)
dobijene objektne module povezuje u jedan izvršni program. U ovom poglavlju
će biti reči o vrstama povezivanja promenljivih (eng. linkage of identifiers) i
funkcije koje odreduje medusobni odnos izmedu promenljivih i funkcija defin-
isanih u različitim jedinicama prevodenja.
Jezik C razlikuje identifikatore bez povezivanja, identifikatore sa spoljašnjim
povezivanjem (eng. external linkage) i identifikatore sa unutrašnjim poveziva-
njem. Identifikatori bez povezivanja nisu vidljivi prilikom procesa povezivanja
i mogu da se ponavljaju u različitim jedinicama prevodenja. Svaka deklaracija
identifikatora bez povezivanja odreduje jedinstveni nezavisni objekat. Sve de-
klaracije identifikatora sa spoljašnjim povezivanjem u skupu jedinica prevodenja
odredjuju jedinstveni objekat, tj. sve pojave ovakvog identifikatora u različitim
jedinicama prevodenja se odnose na jedan isti objekat. Sve deklaracije iden-
tifikatora sa unutrašnjim povezivanjem u okviru jedne jedinice prevodenja se
odnose na isti objekat. Identifikatori sa unutrašnjim povezivanjem se ne mogu
koristiti kroz različite jedinice prevodenja.

10.1.1 Kvalifikator static


Kvalifikator static u programskom jeziku C ima dvojaku ulogu2 i različito
dejstvo kada se primenjuje na lokalne promenljive i kada se primenjuje na glob-
1 Iako se pod jedinicom prevodenja najčešće smatra datoteka, ovo može biti donekle nepre-

cizno. Naime, korišćenjem C pretprocesora moguće je da se više datoteka sastavi u jedinstvenu


jedinicu prevodenja.
2 Kvalifikator static ne treba poistovećivati sa statičkim životnim vekom promenljivih.
122 10 Programi koji se sastoje od više datoteka

alne promenljive ili funkcije.


Druga uloga kvalifikatora static je da naglasi da globalna promenljiva
ili funkcija ima unutrašnje povezivanje. Drugim rečima, kvalifikator static
onemogućava korišćenje promenljive ili funkcije iz drugih datoteka koje čine
program. Ukoliko se u nekoj drugoj datoteci deklariše spoljašnja promenljiva
ili funkcija istog imena, to ne dovodi do konflikta i greške u prevodenju. Os-
novna uloga unutrašnjeg povezivanja je obično u tome da neki deo koda učini
zatvorenim, u smislu da je nemoguće menjanje nekih njegovih spoljašnjih promenljivih
ili pozivanje nekih njegovih funkcija iz drugih datoteka. Time se taj deo koda
,,enkapsulira“ i čini dostupnim samo kroz preostale tačke komunikacije (pre-
ostale spoljašnje promenljive i preostale funkcije). Ovim autor koda ne želi da
onemogući druge autore da koriste deo njegovog koda, veće pre da im pomogne
da jasnije vide način njegovog korišćenja (ali naravno i da onemogući nehotične
greške).

10.1.2 Kvalifikator extern


Kvalifikator extern je u tesnoj vezi sa spoljašnjim povezivanjem i koristi se
kod programa koji se sastoje iz više jedinica prevodenja. Za razliku od funkcija,
gde smo jasno razlikovali deklaraciju (kojom se kompilator samo obaveštava o
postojanju funkcije datog imena i o njenim tipovima), od definicije (kojom se
kompilatoru saopštava celokupan kôd funkcije), u slučaju promenljivih, takva
distinkcija nije radena. Svaka deklaracija promenljive je obezbedivala i odredeni
prostor za promenljivu. U slučaju spoljašnjeg povezivanja, potrebno je da se isti
identifikator koristi za isti objekat u okviru više jedinica prevodenja. Kada bi se
koristili do sada prikazani načini deklarisanja promenljivih, prilikom prevodenja
svake jedinice, bile bi kreirane pojedinačne instance odgovarajućeg objekta i
prilikom povezivanja bi došlo do prijavljivanja greške. Umesto ovoga, koristi se
extern kvalifikator kojim se promenljiva deklariše i uvodi se njen doseg, ali se
naglašava njeno spoljašnje povezivanje i ne obezbeduje se poseban prostor za
nju, već se samo govori da je takva promenljiva (promenljiva tog tipa i imena)
definisana negde drugde (u nastavku datoteke ili u nekoj drugoj datoteci). U
okviru programa, može da postoji samo jedna prava definicija promenljive (i
postoji samo jedna takva promenljiva — koja se čuva u segmentu podataka), dok
može da postoji više (npr. u svakoj jedinici prevodenja programa koja je koristi)
extern deklaracija. Inicijalizacija promenljive moguća je samo u okviru prave
deklaracije (a ne i u okviru extern deklaracije). Za niz u extern deklaraciji
nije potrebno navoditi dimenziju.
Za funkcije se podrazumeva spoljašnje povezivanje i uz deklaracije funkcije
nema potrebe eksplicitno navoditi kvalifikator extern.

10.1.3 Povezivanje više programskih datoteka


Ako je sadržaj dve datoteke:
10.1 Povezivanje 123

#include <stdio.h>

extern int kvadrat(int n);


extern int spoljasnja_promenljiva;

main()
{
int n;
n = spoljasnja_promenljiva;
while(scanf("%d",&n)==1)
printf("\n %d na kvadrat je %d\n\n",n,kvadrat(n));
}
i
int spoljasnja_promenljiva=0;

int kvadrat(int n)
{
return n*n;
}
i ako se one zajedno kompiliraju, kompilacija će biti uspešna.
Ako je sadržaj dve datoteke:
#include <stdio.h>

extern int kvadrat(int n);


extern int spoljasnja_promenljiva;

main()
{
int n;
n = spoljasnja_promenljiva;
while(scanf("%d",&n)==1)
printf("\n %d na kvadrat je %d\n\n",n,kvadrat(n));
}
}
i
static int spoljasnja_promenljiva=0;

int kvadrat(int n)
{
return n*n;
}
i ako se one zajedno kompiliraju, kompilacija neće biti uspešna. Razlog je u
static kvalifikatoru promenljive spoljasnja_promenljiva koji odreduje njeno
unutrašnje povezivanje i time zabranjuje njeno korišćenje u okviru drugih mod-
124 10 Programi koji se sastoje od više datoteka

ula. U tom slučaju linker će prijaviti grešku sličnu sledećoj:


/tmp/ccetWPLn.o: In function ‘main’:
tmp2.c:(.text+0x5): undefined reference to ‘spoljasnja_promenljiva’
collect2: ld returned 1 exit status
Alternativno, navedeni program može biti podeljen u tri datoteke, datoteku
koja sadrži funkciju main:
#include <stdio.h>
#include "kvadrat.h"

main()
{
int n;
n = spoljasnja_promenljiva;
while(scanf("%d",&n)==1)
printf("\n %d na kvadrat je %d\n\n",n,kvadrat(n));
}
}
datoteku koja sadrži definiciju funkcije kvadrat:
#include "kvadrat.h"

int kvadrat(int n)
{
return n*n;
}
i datoteku kvadrat.h koja sadrži deklaraciju promenljive spoljasnja_promenljiva
i deklaraciju funkcije kvadrat:
int spoljasnja_promenljiva=0;
int kvadrat(int);
Prilikom kompilacije prve datoteke, uključuje se sadržaj datoteke kvadrat.h
i nema potrebe za extern deklaracijama. Slično važi i za drugu datoteku, a
povezivanjem njihovih prevedenih verzija dobija se ispravan izvršni program.

10.2 Uključivanje datoteka


File inclusion makes it easy to handle collections of #defines and declara-
tions (among other things). Any source line of the form
#include "filename"
or
#include <filename>
is replaced by the contents of the file filename. If the filename is quoted, search-
ing for the file typically begins where the source program was found; if it is
not found there, or if the name is enclosed in < and >, searching follows an
implementation-defined rule to find the file. An included file may itself contain
10.3 Uslovna uključivanja 125

#include lines. There are often several #include lines at the beginning of a
source file, to include common #define statements and extern declarations, or
to access the function prototype declarations for library functions from headers
like <stdio.h>. (Strictly speaking, these need not be files; the details of how
headers are accessed are implementation-dependent.)

#include is the preferred way to tie the declarations together for a large
program. It guarantees that all the source files will be supplied with the same
definitions and variable declarations, and thus eliminates a particularly nasty
kind of bug. Naturally, when an included file is changed, all files that depend
on it must be recompiled.

10.3 Uslovna uključivanja

It is possible to control preprocessing itself with conditional statements that


are evaluated during preprocessing. This provides a way to include code selec-
tively, depending on the value of conditions evaluated during compilation. The
#if line evaluates a constant integer expression (which may not include sizeof,
casts, or enum constants). If the expression is non-zero, subsequent lines until
an #endif or #elif or #else are included. (The preprocessor statement #elif
is like else-if.) The expression defined(name) in a #if is 1 if the name has
been defined, and 0 otherwise.

For example, to make sure that the contents of a file hdr.h are included only
once, the contents of the file are surrounded with a conditional like this:

#if !defined(HDR)
#define HDR

/* contents of hdr.h go here */

#endif

The first inclusion of hdr.h defines the name HDR; subsequent inclusions will
find the name defined and skip down to the #endif. A similar style can be used
to avoid including files multiple times. If this style is used consistently, then
each header can itself include any other headers on which it depends, without
the user of the header having to deal with the interdependence. This sequence
tests the name SYSTEM to decide which version of a header to include:
126 10 Programi koji se sastoje od više datoteka

#if SYSTEM == SYSV


#define HDR "sysv.h"
#elif SYSTEM == BSD
#define HDR "bsd.h"
#elif SYSTEM == MSDOS
#define HDR "msdos.h"
#else
#define HDR "default.h"
#endif
#include HDR
The #ifdef and #ifndef lines are specialized forms that test whether a
name is defined. The first example of #if above could have been written
#ifndef HDR
#define HDR

/* contents of hdr.h go here */

#endif
Glava 11

Uvod u prevodenje
programskih jezika:
poredjenje interpretera i
kompilatora; faze u
prevodjenju

11.1 Implementacija programskih jezika


• Various strategies depend on how much preprocessing is done before a
program can be run, and how CPU-specific the program is.
• Interpreters run a program ”as is”with little or no pre-processing, but no
changes need to be made to run on a different platform.
• Compilers take time to do extensive preprocessing, but will run a program
generally 2- to 20- times faster.
• Some newer languages use a combination of compiler and interpreter to
get many of the benefits of each.
• Examples are Java and Microsofts .NET, which compile into a virtual
assembly language (while being optimized), which can then be interpreted
on any computer.
• Other languages (such as Basic or Lisp) have both compilers and inter-
preters written for them.
• Recently, ”Just-in-Timečompilers are becoming more common - compile
code only when its used!
11 Uvod u prevodenje programskih jezika: poredjenje interpretera i
128 kompilatora; faze u prevodjenju

11.2 Kratka istorija razvoja kompilatora


• 1953 IBM develops the 701 EDPM (Electronic Data Processing Machine),
the first general purpose computer, built as a defense calculator”in the
Korean War. No high-level languages were available, so all programming
was done in assembly
• As expensive as these early computers were, most of the money companies
spent was for software development, due to the complexities of assembly.
• In 1953, John Backus came up with the idea of špeed coding”, and devel-
oped the first interpreter. Unfortunately, this was 10-20 times slower than
programs written in assembly. He was sure he could do better.
• In 1954, Backus and his team released a research paper titled ”Preliminary
Report, Specifications for the IBM Mathematical FORmula TRANslating
System, FORTRAN.”
• The initial release of FORTRAN I was in 1956, totaling 25,000 lines of
assembly code. Compiled programs ran almost as fast as handwritten
assembly! Projects that had taken two weeks to write now took only 2
hours. By 1958 more than half of all software was written in FORTRAN.

11.3 Moderni kompilatori


• Compilers have not changed a great deal since the days of Backus. They
still consist of two main components:
• The front-end reads in the program in the source languages, makes sense
of it, and stores it in an internal representation...
• ...and the back-end, which converts the internal representation into the
target language, perhaps with optimizations. The target language used
is typically an assembly language, but it is often easier to use a more
established, higher-level language.

11.4 Struktura kompilatora


Source Language

Front End

Intermediate Code

11.4 Struktura kompilatora 129

Back End

Target Language

Front End:

• Lexical Analyzer

• Syntax Analyzer

• Semantic Analyzer

• Intermetiate Code Generator

Front End:

• Code Optimizer

• Target Code Generator

Overall structure:

Source Language

Lexical Analyzer
Syntax Analyzer
Semantic Analyzer
Intermetiate Code Generator

Intermediate Code

Code Optimizer
Target Code Generator

Target Language
11 Uvod u prevodenje programskih jezika: poredjenje interpretera i
130 kompilatora; faze u prevodjenju

11.5 Leksička analiza


Lexical analysis is the processing of an input sequence of characters (such as
the source code of a computer program) to produce, as output, a sequence of
symbols called ”lexical tokens”, or just ”tokens”. For instance, lexical analyzers
(lexers) for many programming languages will convert the character sequence
123 abc into the two tokens 123 and abc. The purpose of producing these
tokens is usually to forward them as input to another program, such as a parser.
Consider the code:
if (i==j);
z=1;
else;
z=0;
endif;
This is really nothing more than a string of characters:
if_(i==j);\n\tz=1;\nelse;\n\tz=0;\nendif;
During lexical analysis phase we must divide this string into meaningful
sub-strings.
The output of our lexical analysis phase is a streams of tokens. A token is
a syntactic category. In English this would be types of words or punctuation,
such as a ”noun”, ”verb”, ”adjective”or ”end-mark”. In a program, this could
be an ”identifier”, a ”floating-point number”, a ”math symbol”, a ”keyword”,
etc
A sub-string that represents an instance of a token is called a lexeme.
For the token IDENTIFIER, possible lexemes are a, b,...
The class of all possible lexemes in a token is described by the use of a
pattern. The pattern to describe the token IDENTIFIER is a string of letters,
numbers, or underscores, beginning with a non-number. Patterns are typically
described using regular expressions.
For lexical analysis:

• Regular expressions describe tokens

• Finite automata are mechanisms to generate tokens from input stream.

lex is a program that generates lexers in the C programming language.

11.6 Sintaksna analiza


Syntax analysis is a process in compilers that recognizes the structure of
programming languages. It is also known as parsing. After lexical analysis, it
is much easier to write and use a parser, as the language is far simpler.
Context-free grammar is usually used for describing the structure of lan-
guages and BNF notation is typical to define that grammar. Grammatical to-
kens include numerical constants and literal strings and control structures such
as assignments, conditions and loops.
11.7 Primer 131

Programs or code that do parsing are called parsers.


Goal: we must determine if the input token stream satisfies the syntax of
the program
What do we need to do this?
• An expressive way to describe the syntax
• A mechanism that determines if the input token stream satisfies the syntax
description
Regular expressions don’t have enough power to express any non-trivial syn-
tax of a programming language. Syntax of programming languages is often
described by context-free grammars.
Parse Tree:
• Internal Nodes: Nonterminals
• Leaves: Terminals
• Edges:
– From Nonterminal of LHS of production
– To Nodes from RHS of production
• Captures derivation of string

Expr
Expr Op Expr Int Int 2 - 1

Yacc (yet another compiler compiler) is a program that generates parsers in


the C programming language.

11.7 Primer
• Source Code:
cur_time = start_time + cycles * 60
• Lexical Analysis:
ID(1) ASSIGN ID(2) ADD ID(3) MULT INT(60)
• Syntax Analysis:

ASSIGN
ID(1) ADD
ID(2) MULT
ID(3) INT(60)

• Sematic Analysis:
11 Uvod u prevodenje programskih jezika: poredjenje interpretera i
132 kompilatora; faze u prevodjenju

ASSIGN
ID(1) ADD
ID(2) MULT
ID(3) int2real
INT(60)

• Intermediate Code:

temp1 = int2real(60) temp2 = id3 * temp1 temp3 = id2 + temp2 id1 =


temp3

• Optimized Code :
Step 1:

temp1 = 60.0 temp2 = id3 * temp1 temp3 = id2 + temp2 id1 = temp3

Step 2:

temp2 = id3 * 60.0 temp3 = id2 + temp2 id1 = temp3

Step 3:

temp2 = id3 * 60.0 id1 = id2 + temp2

Optimized Code:

temp1 = id3 * 60.0 id1 = id2 + temp1

• Target Code Generator


Target Code:

MOVF id3, R2 MULF #60.0, R2 MOVF id2, R1 ADDF R2, R1 MOVF R1, id1
Deo IV

Socijalni aspekti
informatike
Glava 12

Istorijski i društveni
kontekst računarstva kao
naučne discipline; društveni
značaj računara i Interneta;
profesionalizam, etički
kodeks; autorska prava;
intelektualna svojina,
softverska piraterija

Technologies cannot be divorced from a social framework. Computer scien-


tists need to be educated to understand some of the complex linkages between
the social and the technical . . . computer science education should not drive
a wedge between the social and the technical.

12.1 Istorijski i društveni kontekst računarstva


Historical and Societal Context of Computing
Understanding of the impact of computer technology on society.

• prve prvih računara

• prvi računari i prve primene


12 Istorijski i društveni kontekst računarstva kao naučne discipline;
društveni značaj računara i Interneta; profesionalizam, etički
136
kodeks; autorska prava; intelektualna svojina, softverska piraterija
• epoha personalnih računara

Social Impact: The social impact of artificial intelligence is explored by ex-


amining public perceptions and the potential social implications of existing AI
technology, not only from the point of view of the responsibilities of the devel-
oper, but also from the point of view of society and how it has been influenced
by AI. To increase the student’s awareness of the public perceptions and of the
potential social implications of Artificial Intelligence.
Impact of computers–Is the technology neutral?
Društveni značaj Inteneta.
Računarstvo kao naučna disciplina.

12.2 Profesionalizam i etički kodeks


Develop a code of ethics for computing professionals by presenting codes of
ethics of other professionals.

• Odgovornost profesionalca u računarstvu Responsibility of the Computer


Professional: Personal and professional responsibility is the foundation for
discussions of all topics in this subject area. The five areas to be covered
under the responsibility of the computer professional are:

– 1) why be ethical?
– 2) major ethical models,
– 3) definition of computing as a profession, and
– 4) codes of ethics and professional responsibility for computer pro-
fessionals.

• Ethical claims can and should be discussed rationally,


• Osnovi etičke analize: Basis Skills of Ethical Analysis: Five basic skills
of ethical analysis that will help the computer science student to apply
ethics in their technical work are:

– 1) arguing from example, analogy, and counter-example,


– 2) identifying stakeholders in concrete situations,
– 3) identifying ethical issues in concrete situations,
– 4) applying ethical codes to concrete situations, and
– 5) identifying and evaluating alternative courses of action.

• Osnovi socijalne analize: Basic Elements of Social Analysis: Five basic


elements of social analysis are:

– 1) the social context influences the development and use of technol-


ogy,
12.2 Profesionalizam i etički kodeks 137

– 2) power relations are central in all social interaction,


– 3) technology embodies the values of the developers,
– 4) populations are always diverse, and
– 5) empirical data are crucial to the design and development processes.

Ethical issues for computer professionals


A wide variety of issues from privacy and security to intellectual property,
democracy and freedom of expression, equal opportunities, globalisation, use
and misuse and many other issues.

• Ethical and Legal Issues of Data Data Protection legislation, security and
privacy of data issues.
• Ethical and Legal Issues of Information Systems Organisational and social
issues of IS, their construction and their effects. Security and privacy issues
also enter here, as do legal and issues of professional responsibility
• Professional Responsibility, Codes of Conduct

Mikro-situacije:

• A contract requires an experienced Web designer to develop an e-business


system. You have just been awarded the contract but you failed to mention
that your sole knowledge was second-hand and that you had not worked
on an e-business system before. During this contract, you decide to accept
a better-paid contract that requires you to start immediately.
• You are working as a systems designer for a company producing radiation
monitoring equipment. You feel that there are faults in the design, but
your manager rejects your concerns.
• You are an agent for a software tools producer, that has a product DoItAll.
You are being paid to advise an inexperienced small business about devel-
oping their software, so you recommend that they must purchase a DoItAll
tool.
• You are working as a contractor for a small company and have become
aware that some of their software might be unlicensed. You are also con-
cerned about the safeguard of their customers’ data. You are currently
hoping that your contract will be extended.
• You work for a software house that specialises in helping organisations to
set up their E-commerce site. You are worried about the following ethical
issues. You are aware that the client is inexperienced in E-commerce
and that the client plans to launch its publicity for its new site in time
for the Christmas market. You, as a consultant, have just been moved
on to this project. You think that the project will be too late for your
client’s Christmas marketing campaign and that, although in line with the
12 Istorijski i društveni kontekst računarstva kao naučne discipline;
društveni značaj računara i Interneta; profesionalizam, etički
138
kodeks; autorska prava; intelektualna svojina, softverska piraterija

client’s specifications, the level of safety checks in the specification seems


inadequate. You realise that the contract with the client is for a fixed
price.

12.3 Rizici i pouzdanost računarskih sistema


Reliability and safety of computer systems.
Risks, Liabilities, and Bias Considerations. Risks inherent in any software
application. Misuse and misunderstanding as well as developer bias. An em-
pirical study of the evaluation of a piece of software is included to sensitize
students to identifying and eliminating bias during the design phase. To sen-
sitize the software developer to the kinds of biases present in each of us that
could be passed on to the software we develop, and to point out innate risks in
software applications
Liability and Privacy: The kinds of risks that are intrinsic in the develop-
ment of any computer application and how these risks can be taken into account
during development are discussed. Risks include software or hardware bugs, un-
foreseen user interactions, security risks, violations of privacy, unethical uses,
and inappropriate applications of the system. Protecting data and privacy with
respect to a database management system are emphasized. To provide a clear
understanding of risks that a developer faces with respect to his or her soft-
ware, and to emphasize the importance of protecting the data from misuse or
unauthorized access
Reliability of computer systems: errors, major and minor failures.

12.4 Intelektualna svojina i autorska prava


• Intellectual property including copyright, patents, trademarks, and trade
secrets.

• The Concept of Fair Use.

• Computer Regulatory Measures/Laws

• Open source, public-ware, share-ware

• Obeveze poslodavca prema zaposlenima, obaveze zaposlenog prema poslo-


davcu.

• Zakoni različitih zemalja, uvoz/izvoz softverskih proizvoda

12.5 Privatnost i gradanske slobode


• Freedom of speech and press – censoring the Internet

• Govern and private use of personal data.


12.6 Računarski kriminal 139

• Encryption Policy
• Computers in the workplace: effects on employment, telecommuting, em-
ployee monitoring, e-mail privacy.

12.6 Računarski kriminal


• Finansijski računarski kriminal,
• zloupotreba mejla i spam

• neovlašćeni upadi u sisteme


• distribuiranje nelegalnih sadržaja itd.

12.7 Ekonomski aspekti računarstva

You might also like