You are on page 1of 238

UNIVERZITET

FAKULTET INFORMACIONIH TEHNOLOGIJA

#include <stdio.h>
#include <stdlib.h>
void procArrays(const int* a,const in
int j; double s;
for(j=0;j<n;j++) c[j]= a[j]+b[j];
for(s=0,j=0;j<n;j++) s*= c[j]*a[2*j+

NOVI SAD - SREMSKA KAMENICA


2015.
Dr Dušan T. Malbaški

ALGORITMI I STRUKTURE PODATAKA

Recenzenti
Dr Miroslav Hajduković, red. prof., Fakultet tehničkih nauka, Novi Sad
Dr Aleksandar Kupusinac, docent, Fakultet tehničkih nauka, Novi Sad

Izdavač
Univerzitet Educons
Fakultet informacionih tehnologija
Vojvode Putnika 87, Sremska Kamenica

Za izdavača
Prof. dr Aleksandar Andrejević

CIP - Каталогизација у публикацији


Библиотека Матице српске, Нови Сад

004.421(075.8)
004.422.6(075.8)

МАЛБАШКИ, Душан
Algoritmi i strukture podataka [Elektronski izvor] /
Dušan T. Malbaški. - Sremska Kamenica : Univerzitet
Educons,
Fakultet informacionih tehnologija, 2015. - 1 elektronski
optički disk (CD-ROM) : tekst, slika ; 12 cm

Bibliografija.

ISBN 978-86-87785-61-8

a) Алгоритми b) Структуре података


COBISS.SR-ID 295004167
PREDGOVOR
Računar, kao sistem, funkcioniše tako što izvršava programe, odnosno „radi šta mu
se kaže“. Program, pak, kao što piše u naslovu čuvene knjige Niklausa Virta ima
samo dve komponente: algoritam i strukturu podataka. U zavisnosti od programske
paradigme, algoritam i struktura podataka mogu biti jasno razgraničeni, ali se mogu
i prožimati. Svejedno, uvek su tu.
Kao što se ne može zamisliti fizičar koji ne zna Njutnove zakone, tako se
podrazumeva da svako ko želi da bude informatičar mora da raspolaže iscrpnim
poznavanjem fundamenta - algoritama i struktura podataka.
Ovaj udžbenik namenjen je prvenstveno studentima Fakulteta informacionih
tehnologija Univerziteta EDUCONS, kao literatura za istoimeni predmet. Namenjen
je i svakom drugom ko želi da stekne, odnosno, proširi znanje iz ovih oblasti.
Knjiga se sastoji iz dva dela. U prvom se razmatraju osnovi teorije i analize
algoritama, dok se drugi bavi osnovnim strukturama podataka. Deo o algoritmima
oslanja se na literaturnu jedinicu 17 (deo koji je pisao autor) uz dopune i izmene.
Kompletan materijal zasnovan je na korišćenju programskog jezika C, čije
poznavanje predstavlja preduslov za razumevanje izloženog.

U Novom Sadu, januara 2015.


Autor
SADRŽAJ

Predgovor.................................................................................................................. 3
Sadržaj ...................................................................................................................... 4
DEO I: OSNOVI TEORIJE I ANALIZE ALGORITAMA
1. Uvod ..................................................................................................................... 8
1.1 Empirijska definicija algoritma ........................................................................ 8
1.2 Sintetička definicija algoritma.........................................................................11
2. Algoritamski sistemi.............................................................................................15
2.1. Tjuringove mašine .........................................................................................15
2.2. Normalni algoritmi Markova .........................................................................23
3. Analiza algoritamskih struktura ............................................................................28
3.1. Graf toka programa........................................................................................30
3.2. Strukturna teorema.........................................................................................34
3.2.1. Strukturna teorema i algoritamski sistemi................................................38
3.3. Metoda sukcesivne dekompozicije .................................................................38
3.4. Složenost (kompleksnost) algoritama.............................................................41
DEO II: STRUKTURE PODATAKA
1 Osnovni pojmovi ...................................................................................................49
1.1. Definicija strukture podataka .........................................................................55
1.2. Operacije nad strukturama podataka ..............................................................57
1.3 Klasifikacija struktura podataka ......................................................................60
2. Niz .......................................................................................................................65
2.1. Fizička realizacija niza...................................................................................67
2.1.1. Fizička realizacija statičkog niza .............................................................67
2.1.2. Metoda linearizacije................................................................................68
2.1.3. Fizička realizacija dinamičkog niza.........................................................69
2.2.Sortiranje niza.................................................................................................71
2.2.1. Metoda izbora (Selection Sort)................................................................72
2.2.2. Metoda izmene (Standard Exchange Sort, Bubble Sort) ..........................73
2.2.3. Metoda umetanja (Insertion Sort) ............................................................75
2.2.4. Quicksort ................................................................................................77
2.3. Redosledna obrada niza .................................................................................81
3. Slog i tabela..........................................................................................................82
3.1. Slog ...............................................................................................................82
3.2 Tabela.............................................................................................................85
3.2.1. Linearno traženje ....................................................................................86
3.2.2. Binarno traženje......................................................................................87
3.2.3. Redosledna obrada tabele........................................................................91
3.3. Rasuta ili heš tabela .......................................................................................91
3.3.1. Fizička realizacija heš tabele ...................................................................92
3.3.2. Kolizija ključeva i rukovanje sinonimima................................................94
3.3.3. Analiza otvorenog adresiranja ...............................................................102
3.3.4. Analiza lančanja....................................................................................103
4. Poludinamičke strukture .....................................................................................104
4.1. Stek .............................................................................................................104
4.1.1. Fizička realizacija steka ........................................................................107
4.1.2. Prevođenje izraza ..................................................................................110
4.1.3. Programski stek.....................................................................................114
4.1.4. Rekurzija ..............................................................................................117
4.2. Red..............................................................................................................124
4.2.1. Sekvencijalna realizacija reda ...............................................................127
4.2.2. Spregnuta realizacija reda .....................................................................130
4.2.3. Red sa prioritetima................................................................................135
4.2.4. Ukratko o primenama reda ....................................................................137
4.3. Dek..............................................................................................................137
4.4. Sekvenca .....................................................................................................139
4.4.1. Fizička realizacija sekvence ..................................................................140
4.4.2. Sortiranje datoteke ................................................................................142
5. Liste ...................................................................................................................148
5.1. Jednostruko spregnuta lista ..........................................................................148
5.1.1. Jednostavna lista ...................................................................................150
5.1.2. Jednostruko spregnuta lista sa ključevima .............................................156
5.1.3. Sortiranje jednostruko spregnute liste....................................................160
5.2.Dvostruko spregnuta lista..............................................................................161
5.2.1. Dvostruko spregnuta lista sa navigacijom..............................................163
5.2.2. Obrada dvostruko spregnute liste ..........................................................173
5.2.3. Demonstracioni program.......................................................................174
5.3. Multilista .....................................................................................................179
6. Stablo .................................................................................................................182
6.1. Orijentisano stablo .......................................................................................182
6.2. Stablo kao struktura podataka ......................................................................185
6.3. N-arno stablo ...............................................................................................187
6.3.1. Fizička realizacija n-arnog stabla ..........................................................189
6.3.2. Binarno stablo.......................................................................................191
6.3.3. Binarno stablo pristupa..........................................................................201
6.3.4. Balansiranje binarnog stabla..................................................................212
6.3.5. AVL stablo ...........................................................................................216
6.3.6. Stohastičko stablo .................................................................................225
6.4. Uopšteno (generalisano) stablo ....................................................................234
6.4.1. Fizička realizacija uopštenog stabla.......................................................236
Literatura................................................................................................................238
Dušan Malbaški - Algoritmi i strukture podataka

7
Dušan T. Malbaški - Algoritmi i strukture podataka

1. UVOD
Definisati pojmove nikad nije lako. Što je pojam bliži fundamentalnom,
definisati ga sve je teže i teže. Šta je skup? A broj? Tačka? Lepota? Pravda? O
definiciji vremena, sv. Avgustin, hrišćanski teolog i filozof, piše:
 "Si nemo ex me quaerat, scio; si quaerenti explicare velim, nescio". (Ako me
niko ne pita, znam; ako hoću da objasnim nekom ko pita - ne znam).
Jedan od najosnovnijih pojmova u računarstvu jeste pojam algoritma, do te
mere fundamentalan da bi, možda, najbolji odgovor na pitanje "šta je to?" bio "zna
se!". Zato ne treba da čudi to što neki veoma istaknuti teoretičari, kao što je Uspenski,
smatraju da algoritme ni ne treba definisati dovođenjem u vezu sa drugim jezičkim
simbolima, nego ih valja smatrati polaznim1, i da je svaki takav pokušaj definisanja, u
stvari, samo opisivanje. Drugi pak stoje na stanovištu da algoritam ipak treba
definisati, ali samo u logičkom, a ne u formalno-matematičkom smislu. U svakom
slučaju, sam pojam algoritma je toliko bazičan da stoji tvrdnja da je "najveće otkriće u
oblasti nauke o algoritmima otkriće samog algoritma".

1.1 EMPIRIJSKA DEFINICIJA ALGORITMA


Bez pretenzija na arbitriranje, navodimo detaljnije drugi prilaz - prosto zato
što prvi (po Uspenskom) i ne zahteva nikakva dodatna objašnjenja. Osnovni problem
sa kojim se susrećemo prilikom pokušaja da se formuliše definicija algoritma je
opredeljivanje za neki od filozofskih pristupa definiciji uopšte. Imajući u vidu
pomenutu "opštepoznatost", od prilaza ponuđenih u 14 biramo, za početak, tzv.
empirijski (naziva se i leksičkim), koji podrazumeva prikupljanje činjenica i stavova o
definiendumu (pojmu koji se definiše), njihovo klasifikovanje, formulisanje
funkcionalnih osobina i konačno, uopštavanje sa ciljem da se definicijom obuhvate
sve osobine uočenih grupa (klasa).
Interesantno, već sam termin "algoritam" dugo je predstavljao enigmu. Knut
(D.Knuth, 15) navodi da se u Webster-ovom rečniku ova reč po prvi put pojavila tek
1957. godine. Do tada važio je stari izraz "algorisam (algorism)" koji se odnosio na
izvođenje aritmetičkih operacija nad arapskim brojevima, što potiče iz srednjeg veka
kada su se razlikovali abacisti koji su za dotične operacije koristili abakus i

1
u opštoj logici takvi pojmovi nose naziv kategorije

8
Dušan T. Malbaški - Algoritmi i strukture podataka

algoritmisti koji nisu koristili posebna pomagala. Posle dosta lutanja (detalji se mogu
naći u 15 i drugde) istoričari matematike su konačno otkrili da je reč o imenu
arapskog matematičara iz IX veka - jednog od najvećih matematičara svih vremena! -
Abu Ja'far Mohammed ibn Musa al-Khowarizmi - ja (u prevodu Džafarov otac,
Mohamed, sin Musin, iz Horezma2). Transkripcija dela imena al-Khowarizmi (čita se
"Alhorezmi"), u "algoritam" nameće se sama. Navedimo nekoliko primera algoritama:
A1. Razvući šestar na dužinu zadate duži. Iz jedne krajnje tačke duži opisati luk.
Ponoviti isto za drugu krajnju tačku, tako da se lukovi seku. Spojiti lenjirom krajnje
tačke duži sa presečnom tačkom lukova.
A2. Sabrati x i y. Zbir podeliti sa z.
A3. (Određivanje S = a1+ ... +an)
1. Postaviti vrednost S na nulu i preći na sledeći korak.
2. Postaviti vrednost i na 1 i preći na sledeći korak.
3. Dodati ai na tekuću vrednost S i preći na sledeći korak.
4. Povećati i za 1 i preći na sledeći korak.
5. Ako je i manje ili jednako n vratiti se na korak 3; u suprotnom završiti.
Algoritam je, međutim, i ovo:
A4.
1. Ubaciti monetu od 1 eur u odgovarajući prorez
2. Sačekati da se upali zeleno svetlo
3. Ako se želi crna kafa pritisnuti dugme A; ako se želi bela kafa pritisnuti dugme
B.
Kako se vidi, primeri su dosta različiti, ali ipak imaju određene zajedničke osobine.
Prvo, svugde se susreću neke akcije (dejstva, operacije) i to u strogo zadatom
redosledu. Dalje, ta sekvenca dejstava je svrsishodna, tj. vodi ka nekakvom cilju.
Dejstva, složena u algoritamske korake, diskretna su i izvode se konačan broj puta,
sve do dobijanja rezultata. I još, svi algoritmi imaju neke ulazne podatke: da bi se
konstruisao jednakostranični trougao /A1/ mora se poznavati dužina stranice; da bi se
odredila vrednost (x+y)/z /A2/ neophodno je poznavati vrednosti x, y i z; u cilju
izvršavanja algoritma A3 mora se poći od zadavanja vrednosti n i a1, ..., an. Čak i
algoritam A4 polazi od nekih podataka, ali se oni ne zadaju spolja nego su ugrađeni u

2
Horezm je bila veoma napredna država u donjem toku Amu-Darje. Uništile su je, kao i mnogo toga,
primitivne horde Džingis-kana.

9
Dušan T. Malbaški - Algoritmi i strukture podataka

njegov opis. Sve u svemu, može se kao prvo određenje pojma algoritma prihvatiti da
je to pravilo koje objašnjava proces transformacije ulaznih podataka u traženi rezultat.
Pojam algoritma, očigledno, sasvim je blizak pojmovima recept, postupak ili
procedura. Dakle, svaki algoritam je pravilo, ali obrnuto ne mora da važi: saobraćajna
pravila nisu algoritmi, kao ni pravilo "čast svakom-veresija nikom!".
Činjenica da se algoritam sastoji od dejstava i da vodi ka rezultatu neposredno
ukazuje na potrebu postojanja izvršioca, i to takvog koji može mehanički, samo na
osnovu opisa algoritma, a bez uplitanja korisnika (onog kome je rezultat potreban),
bez ikakve kreativnosti, da produkuje traženi rezultat. To, pak, zahteva da postoji
način komunikacije, tj. jezik, na kojem se algoritam zadaje, a čija je glavna
karakteristika ta da ga izvršilac razume i to u potpunosti. Taj jezik, nazvan jezikom
algoritma, nema neku unapred zadatu, kanoničku formu: algoritam se može zadati u
vidu uređenog skupa rečenica (svi navedeni primeri su takvi), ali i kao poznati blok
dijagram, kao sekvenca iskaza na formalnom jeziku, programskom ili
kvaziprogramskom, kao kolekcija crteža (kod geometrijskih konstrukcija na primer),
kao i na druge načine; jedino na čemu se insistira je to da izvršilac bude u stanju da,
na osnovu opisa i samo na osnovu njega, proizvede rezultat. Vratimo se, za trenutak,
na ulazne podatke. Jasno je da ne može svaki podatak da igra ulogu ulaznog podatka u
ma koji algoritam: zato govorimo o dozvoljenim i nedozvoljenim ulaznim podacima.
Dozvoljeni ulazni podaci su oni koje algoritam može uopšte da interpretira kao takve:
dozvoljeni ulazni podaci za A2 mogu biti bilo koje trojke realnih brojeva (uključujući
čak i z=0, kada se kaže da je na dotičnu trojku algoritam neprimenljiv, iako je ona
dozvoljena). Nedozvoljeni ulazni podaci obuhvataju vrednosti koje algoritam u
principu ne može da prihvati kao ulaz; za isti algoritam to bi mogla da bude, recimo,
trojka (zeleno, crveno, plavo). Imajući sve ovo u vidu u mogućnosti smo da
empirijsku definiciju algoritma (često korišćen naziv je intuitivna definicija)
preciziramo na sledeći način:
 Algoritam je pravilo, formulisano na nekom jeziku, koje jednoznačno definiše
redosled operacija neophodan za transformaciju dozvoljenih ulaznih podataka
u traženi rezultat.
Napominjemo da metod kojim je dobijena gornja definicija nosi naziv "analitički".
Odlikuje se time što se termin ("jezički simbol") što se definiše dovodi u vezu sa
grupom drugih jezičkih simbola koji se smatraju poznatim i koji izražavaju njegove
bitne odlike. Tako se dolazi do strukture definicije: novi termin (algoritam) - šira

10
Dušan T. Malbaški - Algoritmi i strukture podataka

klasa u koju spada (pravilo) - osobine po kojima se razlikuje od ostalih predstavnika


iste klase3.
Empirijska definicija algoritma sasvim je prihvatljiva za opšte razumevanje
pojma i dovoljna je kao podloga za svakodnevnu praksu. S druge strane, glavno
područje primene algoritma per se - programiranje - u velikoj meri je egzaktna
delatnost, te stoga ne treba da čudi to što se stručnjaci za ovu oblast nisu zadržali
isključivo na tako širokom određenju i što je znatan napor uložen u dobijanje
preciznije formulacije. Pri tom, postavljaju se kako pitanje samog koncepta algoritma
tako i egzistencije algoritma za rešavanje svakog, unapred zadatog problema. Ni jedno
od navedenih pitanja nije do danas potpuno rešeno, ali saznanja nadilaze empirijsku
definiciju u datom obliku.

1.2 SINTETIČKA DEFINICIJA ALGORITMA


Znatno određenija, mada i dalje ne sasvim egzaktna, definicija dobija se - što
je i za očekivanje - uz pomoć tzv. normativnog pristupa definisanju i to prevashodno
na bazi Raslovog (Russel) shvatanja tzv. sintetičke metode prema njegovim
"Principima matematike" (videti 14). Normativni pristup, za razliku od empirijskog,
podrazumeva propisivanje, ergo nametanje, značenja pojma i po prirodi je
deduktivan4. Po Raslovom shvatanju "Matematička definicija se sastoji u ukazivanju
na takvu utvrđenu relaciju prema jednom utvrđenom terminu u kojoj može da stoji
samo jedan termin; ovaj termin onda definišu utvrđena relacija i utvrđeni termin".
Glavni nedostatak empirijske definicije jeste oslanjanje na pojmove koji su
visoko intuitivni i za koje možemo reći samo “zna se šta je to”. Najuočljiviji su
pojmovi “ulazni podatak” i traženi rezultat”. Sintetička definicija algoritma jeste način
da se (bar) ova dva pojma formalizuju.
Osnova za sintetičku definiciju algoritma je apstraktni alfabet, onaj isti koji se
susreće i kod formalnih teorija. Apstraktni alfabet, u algoritamskoj terminologiji,
predstavlja konačan skup leksičkih simbola5 koji nemaju nikakvu semantiku što će,

3
Kvadrat je-pravougaonik-čije su sve četiri stranice jednake dužine. Klika je-regularan graf-sa n
čvorova stepena n-1
4
Empirijski (leksički) način je u suštini induktivan
5
razlika između „običnog“ simbola i leksičkog simbola (tzv. tokena) je u tome što token može da se
sastoji od iše pojedinačnih simbola, npr „pqr“, pri čemu se smatra da je token nerazloživ, tj. atomaran

11
Dušan T. Malbaški - Algoritmi i strukture podataka

dakle, reći da mogu odgovarati ma kakvim jedinicama posmatranja: simbolima u


užem smislu, subjektima, stvarima (objektima), događajima ili misaonim
kategorijama. Očigledno, jedina suštastvena razlika u odnosu na formalne teorije je
zahtev za konačnošću alfabeta. Alfabeti (kratkoće radi, izostavljamo reč "apstraktni")
su npr. X = , , ,  ili Y = Bečki, klasici, Hajdn, Mocart, Betoven, i , su,
(prazan simbol, blenk ili blanko). Reč dužine n je niz od n simbola iz datog alfabeta.
Dužina reči može da bude i 0, kada govorimo o praznoj reči. Primeri reči iz alfabeta
A su , , (prazna reč) i ; njihove dužine su respektivno 5, 2, 0 i 3. Reč
dužine 15 u alfabetu Y je: Bečki klasici su Hajdn i Mocart i Betoven6. Skup reči
nekog alfabeta W označavaćemo sa W*. Uobičajeno je da ako W eksplicitno ne sadrži
praznu reč za oznaku stavljamo W+.
Neka su X i Y dva, ne obavezno različita, alfabeta. Tada je alfabetski ili
alfabetski operator svaka relacija (dakle jednoznačno ili višeznačno preslikavanje)
reči iz alfabeta X u reč(i) alfabeta Y, odnosno G  X* x Y*. Ako je G alfabetski
operator tada se alfabet X naziva ulaznim, a alfabet Y izlaznim alfabetom, dok su sa
X* i Y*, podsetimo, označeni skupovi reči iz pomenutih alfabeta. U slučaju da se
ulazni i izlazni alfabet poklapaju, kaže se da je alfabetski operator definisan nad tim
alfabetom. Domen (oblast definisanosti) alfabetskog operatora je skup reči nad kojima
je on definisan, dok je skup reči koje se nalaze u drugonavedenim komponentama
uređenih parova njegov kodomen. Uz pomoć ovih nekoliko pojmova formuliše se
sintetička definicija algoritma:
 Algoritam je uređena četvorka (X, Y, G, Z), gde su X i Y respektivno ulazni i
izlazni alfabet, G  X* x Y* alfabetski operator, a Z konačni skup zakona7
kojima se G zadaje. Skup Z nazvaćemo kodeks.
Iako ova definicija deluje određenije, "matematičkije", od prethodne, sa takvim
zaključkom ne treba žuriti. Naime, prve tri komponente uređene četvorke jesu
formalizovane, ali četvrta to ni u kojem slučaju nije8. Lako se zapaža da manjkavost
leži u komponenti "konačni skup zakona" koja je tipično intuitivna. Nažalost, ova

6
Ali i "Hajdn i Bečki klasici su Mocart". Ne gubimo iz vida da se radi o čisto sintaksnim pojmovima
7
Umesto izraza "zakon" koristi se i "pravilo" koji smo izbegli da ne bi došlo do konfuzije jer se
poslednji sreće u empirijskoj definiciji algoritma i nema identično značenje
8
Takav pristup, slaganje intuitivnih pojmova u uređene n-torke, dosta je uobičajen u računarskim
naukama i služi svrsi - razumljivosti - ali samo kada se ne zloupotrebljava

12
Dušan T. Malbaški - Algoritmi i strukture podataka

četvrta, komponenta je i najvažnija, jer odgovara na pitanje “kako” (a ne samo “šta”),


što i jeste suština pojma algoritma.
Dva algoritma su ekvivalentna po izvršenju ako se slažu odgovarajući
alfabetski operatori i kodeks po kojima se oni izvršavaju. Drugim rečima, algoritmi A1
= (X1, Y1, G1, Z1) i A2 = (X2, Y2, G2, Z2) su ekvivalentni po izvršenju ako je, u
matematičkom smislu, A1 = A2 (zbog ovog neki autori, umesto termina "ekvivalentni
po izvršenju", kažu prosto "jednaki"). Očigledno, ovakvi algoritmi mogu se
razlikovati, eventualno, po načinu na koji su zadati zakoni izvršavanja, odn. po jeziku
na kojem su formulisani, što znači da se, de facto, radi o istom algoritmu. Dva
algoritma su funkcionalno ekvivalentna ako se razlikuju samo po kodeksu, tj. ako za
iste ulazne podatke daju isti izlaz, pri čemu se do vrednosti izlaza dolazi na drugi
način. Za gornji primer to bi značilo da se u uređenim četvorkama mogu razlikovati
samo Z1 i Z2. Takođe, smatra se da su algoritmi koji su ekvivalentni po izvršenju
istovremeno i funkcionalno ekvivalentni. Primera za funkcionalno ekvivalentne
algoritme ima bezbroj jer je jedna od osnovnih osobina algoritma ta da za gotovo
svaki problem postoji više algoritama rešavanja. Setimo se samo mnoštva algoritama
za sortiranje koji su svi funkcionalno ekvivalentni. Čak i za tako jednostavan
algoritam kakav je npr. /A3/ lako se konstruiše funkcionalno ekvivalentan koji,
umesto da sabira od a1 do an, operaciju izvodi u suprotnom redosledu: od an ka a1. Na
pitanje ekvivalencije algoritama vratićemo se u poglavlju o analizi algoritamskih
struktura.
Kako je već pomenuto, algoritmi mogu biti deterministički kada se za isti ulaz
uvek dobija isti izlaz (tj. alfabetski operator je funkcija), ali i stohastički za koje to ne
važi. Konačno, algoritmi mogu biti samoizmenljivi. Njihova karakteristika je to što
redosled izvođenja operacija može da zavisi od istorijata izvršenja, odn. od prethodno
izvršenih koraka. U daljem tekstu, bavićemo se uglavnom determinističkim
algoritmima. Algoritam (X, Y, G, Z) je deterministički ako je G funkcija, tj. ako važi
G: X*  Y*.
Vratimo se na pitanje sintetičke definicije algoritma. Kao što je rečeno, ni ona
nije strogo egzaktna jer se postavlja pitanje univerzalnog načina za formulisanje
zakona iz kodeksa. Pitanje se može preformulisati u "postoje li sredstva kojima bi se
zadao bilo koji algoritam ili bar algoritam koji mu je funkcionalno ekvivalentan?".
Odgovor na ovo pitanje još nije dat, osim na nivou (doduše krajnje verovatnih)

13
Dušan T. Malbaški - Algoritmi i strukture podataka

hipoteza. Formalizmi (sredstva) kojima se zadaju algoritmi nose naziv algoritamski


sistemi i ima ih više. U daljem ćemo nešto detaljnije razmotriti dva od tri najpoznatija
sistema: Tjuringovu mašinu i Markovljeve (normalne) algoritme9. Treba odmah
podvući da su, bez obzira na to što deluju veoma različito, sva tri sistema
ekvivalentna, odn. postoje formalni postupci kojima se prikaz algoritma u jednom
algoritamskom sistemu može prevesti na prikaz u drugom.
Primena algoritamskih sistema na precizan prikaz algoritama zasnovana je na
tzv. osnovnoj hipotezi teorije algoritama koja glasi
 svaki algoritam može se predstaviti algoritamskim sistemom.
Radi se o hipotezi (a ne npr. o teoremi) jer je ovaj stav nedokaziv. Inače, navedena
tvrdnja sme se proglasiti za hipotezu, jer se može zamisliti primer kojim bi ona mogla
biti opovrgnuta: dovoljno bi bilo pronaći nešto što zadovoljava našu predstavu o
algoritmu (a i njegove intuitivne definicije), a što se ne može predstaviti
algoritamskim sistemom. Iako je takva situacija krajnje neverovatna, ona nam ipak ne
dozvoljava da idemo dalje od hipoteze.

9
treći algoritamski sistem su rekurzivne funkcije kojima se nećemo baviti

14
Dušan T. Malbaški - Algoritmi i strukture podataka

2. ALGORITAMSKI SISTEMI
Sintetičkom definicijom algoritma uspevaju se formalizovati pojmovi “ulaznih
podataka” i “traženog rezultata” korišćeni u empririjskoj definiciji. Takođe,
formalizuje se i veza između njih, koja treba da bude ostvarena izvršavanjem
algoritma. Ova veza govori o tome kakav izlaz (tj. koju reč ili skup reči iz izlaznog
alfabeta) produkuje izvršenje algoritma nad zadatom rečju ulaznog alfabeta. Nažalost,
sintetička definicija nema formalizovan odgovor na pitanje kako se implementira
alfabetski operator, tj. kako izgleda postupak transformacije ulazne reči u izlaznu.
Traženi odgovor pružaju algoritamski sistemi. Obradićemo dva od njih: Tjuringovu
mašinu i Markovljeve normalne algoritme.

2.1. TJURINGOVE MAŠINE


Ovaj algoritamski sistem postavio je engleski matematičar Alen Tjuring
(Turing) godine 1936. polazeći od ideje izvršioca algoritma. Ranije smo podvukli da
je izvršilac dužan da, pridržavajući se kodeksa, a na osnovu ulaznog i izlaznog
alfabeta realizuje alfabetski operator, tj. da dobije traženi izlaz, i to postupajući
mehanički. Postavlja se pitanje šta je to što je u stanju da izvede traženu
transformaciju na čisto mehanički način? Naravno, to je mašina, ali ne realna koja
raspolaže ograničenim resursima, nego apstraktna mašina, dakle mašina čiji su resursi
neograničeni jer tako zahteva definicija algoritma (kada bismo govorili o računaru to
bi bio računar sa beskonačnim kapacitetom memorije). Zadatak takve mašine bio bi
da prerađuje nekakav ulaz (u najširem smislu reči) u izlaz, što se u sintetičkoj
definiciji algoritma svodi na transformaciju reči iz ulaznog alfabeta u reč(i) iz
izlaznog alfabeta, sve u skladu sa kodeksom. Pri tom, Tjuringova mašina ne pravi
razliku između ulaznog i izlaznog alfabeta što ne utiče na opštost jer smo uvek u
mogućnosti da ova dva alfabeta zamenimo njihovom unijom. Na taj način, osnovni
zadatak Tjuringove mašine je transformacija reči nekog alfabeta u drugu reč iz istog
alfabeta, prema svrsi alfabetskog operatora G, a saobrazno kodeksu Z. Alfabet nad
kojom se vrše transformacije nosi naziv spoljašnji alfabet.
Postoji čitav niz varijeteta Tjuringove mašine (zato je u naslovu i napisana
množina) i gotovo svaki od njih može da se matematički opiše na više načina, no
važno je napomenuti da su svi oni ekvivalentni, tako da ćemo u ovom tekstu obraditi

15
Dušan T. Malbaški - Algoritmi i strukture podataka

samo dva osnovna tipa: tzv. specijalnu Tjuringovu mašinu (sa jednom trakom) i
univerzalnu Tjuringovu mašinu (ali bez ulaženja u detalje, u drugom slučaju).
Specijalna Tjuringova mašina (u daljem tekstu Tjuringova mašina) sastoji se
od tri funkcionalne celine: beskonačne trake, upisno-čitajuće glave i upravljačkog
bloka, slika 2.1.

upravljački blok
upisno-čitajuća glava

beskonačna traka

... B a1 a2 a3 ... an-1 an B B ...

Slika 2.1.

Beskonačna traka je podeljena na segmente (ćelije) u kojima se nalaze pojedinačna


slova spoljašnjeg alfabeta što čine reč i koja igra ulogu radne memorije. Zadatak
upisno-čitajuće glave je, kako i samo ime sugeriše, upisivanje slova na traku odnosno
čitanje slova sa nje. Upisno-čitajuća glava ima mogućnost pomeranja u oba smera,
levo i desno. Upravljački blok inicira upis-čitanje i pomeranje upisno-čitajuće glave.
Odlikuje se time što može (bolje rečeno mora) biti u nekom od apstraktnih stanja.
Skup stanja upravljačkog bloka čini tzv. unutrašnji alfabet. Među stanjima postoji
tačno jedno koje nosi naziv početno stanje; Tjuringova mašina se nalazi u tom stanju
na početku izvršavanja zadatka.
Mašina, uopšte, funkcioniše na sledeći način: traka na početku rada sadrži reč
iz spoljašnjeg alfabeta čija je dužina konačna (reč a1, a2, ..., an sa slike 1.1), a koja je
sa obe strane ograničena beskonačnim nizovima praznih ćelija, u oznaci B. Upisno-
čitajuća glava nalazi se iznad prvog slova s leva a1, a upravljački blok je u početnom
stanju. Mašina radi u vremenski diskretnim pokretima (engl. move) gde se svaki
pokret sastoji od 3 razdela:
1. Menja se stanje upravljačkog bloka (u opštem slučaju, stanje može ostati isto,
no to se smatra ipak trivijalnom promenom stanja)
2. Upisuje se slovo koje nije B u ćeliju nad kojom se nalazi upisno-čitajuća glava
(nazovimo je aktuelnom ćelijom) i

16
Dušan T. Malbaški - Algoritmi i strukture podataka

3. Glava se pomera za jedno mesto ulevo ili udesno10.


Odluka o načinu promene stanja donosi se na osnovu trenutnog stanja upravljačkog
bloka i slova koje se nalazi u aktuelnoj ćeliji. U skupu stanja nalazi se najmanje jedno
koje je završno (finalno). Kada se mašina nađe u finalnom stanju (tačnije, ako se nađe
jer to nije obavezno, a tada je u pitanju postupak koji se ne završava) ona se
zaustavlja. Reč omeđena praznim ćelijama (tj. ćelijama čiji je sadržaj B) predstavlja
rezultat obrade. Odmah treba skrenuti pažnju na dve pojedinosti. Naime, zapaža se da
upis simbola B nije dozvoljen, tj. ćelija se ne može "isprazniti", što bi moglo da
navede na pogrešan zaključak da izlazna reč ne može biti kraća od ulazne. Situacija,
međutim, nije takva zato što postoji suptilna, ali veoma važna, razlika između prazne
ćelije (ćelije sa sadržajem B) i praznog slova koje se ni po čemu ne razlikuje od
ostalih slova, kao što se 0 pri sabiranju ne razlikuje od ostalih brojeva ili, recimo,
neutralni element grupe od ostalih. Prazno slovo se, dakle, legalno upisuje u ćeliju što
efektivno znači skraćivanje izlazne reči za jedno slovo. Drugo što treba notirati je
činjenica da se ne garantuje zaustavljanje Tjuringove mašine (odn. dostizanje nekog
od završnih stanja), što se, u suštini, svodi na problem konačnosti algoritma.
Formalno, Tjuringova mašina definiše se kao uređena šestorka (K, , , , q0,
F) čije su komponente:
 K je konačan skup stanja upravljačkog bloka (unutrašnji alfabet)
  je spoljašnji alfabet koji sadrži i B (praznu ćeliju), a po potrebi i prazno
slovo
   ( \ B) je podskup skupa  koji ne sadrži B i koji nosi naziv skup
ulaznih slova
 : ((K x )  K x ( \ B) x L, R je funkcija prelaza koja ne mora biti
definisana na celom domenu. L i R su specijalni simboli koji označavaju
kretanje upisno-čitajuće glave (L = ulevo, R = udesno)
 q0  K je početno stanje
 F  K je skup završnih stanja.
Pod konfiguracijom Tjuringove mašine podrazumevamo uređenu trojku (q, , i) gde
je q tekuće stanje,  aktuelna reč na traci, a i udaljenost upisno-čitajuće glave od

10
postoje varijeteti kod kojih upisno-čitajuća glava može i da se ne pomeri

17
Dušan T. Malbaški - Algoritmi i strukture podataka

početka (krajnjeg levog slova) reči . Očigledno, konfiguracija u celosti opisuje stanje
čitave Tjuringove mašine.
Neka se u nekom trenutku na traci nalazi niz slova (reč) a1a2 ... an. Neka je,
dalje, upravljački blok u stanju q, a upisno-čitajuća glava iznad slova ai, što znači da
je tekuća konfiguracija (q, a1, a2, ..., an, i). Tada
(q, ai) = (p, x, R)
znači "promeniti stanje u p, umesto ai upisati na traku x i pomeriti upisno-čitajuću
glavu za jedno mesto udesno". Po izvršenju, nova konfiguracija je
(p, a1, ... ai-1xai+1 ... an, i+1).
Rad Tjuringove mašine prikazaćemo na kvazi-programskom jeziku zasnovanom na
sintaksi programskog jezika C. Beskonačnu traku ćemo predstaviti beskonačnim
nizom cell čiji su indeksi celi brojevi. Na početku rada upisno-čitajuća glava nalazi se
iznad pozicije sa indeksom 0, gde je i prvi simbol ulazne reči. Neka je ceo broj i
tekuća pozicija upisno-čitajuće glave, tako da je na početku rada i=0. Označimo sa
cell[i] simbol koji se nalazi na poziciji i. Neka važi L,R. Rad Tjuringove
mašine može se predstaviti sledećim ciklusom:
q = q0; i = 0;
while(qF) {
(q,a,) = (q, cell[i]);
cell[i] = a;
i = (==R) ? i+1 : i-1;
}
Očigledno, centralno mesto u Tjuringovoj mašini zaizima funkcija prelaza . Funkcija
prelaza zadaje se tzv. funkcionalnom shemom - tablicom čije vrste odgovaraju
stanjima iz skupa K, kolone slovima spoljašnjeg alfabeta , a elementi uređenim
trojkama koje odgovaraju vrednostima funkcije prelaza (ako su definisane).
Jedan od najinteresantnijih problema vezanih za Tjuringovu mašinu (i
programiranje uopšte) jeste tzv. halting problem (problem zaustavljanja). Radi se o
pitanju „da li se može unapred zaključiti hoće li proizvoljna Tjuringova mašina za
zadatu ulaznu reč završiti rad“, tj. hoće li upravljački blok doći u neko od stanja iz

18
Dušan T. Malbaški - Algoritmi i strukture podataka

skupa F11? Odgovor na pitanje je negativan! Dakle, halting problem kod Tjuringove
mašine spada u klasu neodlučivih problema.
Na kraju, jedna napomena vezana za skup ulaznih slova . Svrha njegovog
izdvajanja iz  je nominacija dozvoljenih ulaznih simbola. Ako je, na primer,  = 0,
1 tada na početku rada traka može da sadrži samo reči iz skupa *, odnosno nizove
nula i jedinica.
Primer.
Neka je data Tjuringova mašina T = (K, , , , q0, F) gde je
K = s1, s2, s3
 = 0, 1, , B ( je prazno slovo!)
 = 0, 1
q0 = s1
F = s3
i gde je prelazna funkcija  zadata sledećom funkcionalnom shemom:

0 1  B
s1 s20R s21R - -
s2 s21R s20R - s3R
s3 - - - -

Zadatak ove mašine je da prihvati ulazne nizove koji se sastoje od slova 0 i 1 i da


zameni sve nule jedinicama i obrnuto, osim za prvi simbol koji ostaje nepromenjen.
Neka se na ulazu nalazi niz 11100 što znači da je početna konfiguracija (s1,
11100, 0). Kako je s1 početno stanje i 1 aktuelno slovo, prvo što se izvršava je prelaz
s11  s21R
posle kojeg će stanje biti promenjeno u s2, na traku biti upisano slovo 1 i biti izvršen
pomeraj udesno, tako da konfiguracija postaje (tekuća pozicija je, preglednosti radi,
podvučena)
(s2, 11100, 1).
Sledeći prelaz je prelaz
s21  s20R

11
te, tako, produkovati rezultat

19
Dušan T. Malbaški - Algoritmi i strukture podataka

koja menja konfiguraciju u


(s2, 10100, 2).
Postupak se nastavlja prema sledećem rasporedu:

(s2, 10100, 2) s21  s20R (s2, 10000, 3)


(s2, 10000, 3) s20  s21R (s2, 10010, 4)
(s2, 10010, 4) s20  s21R (s2, 10011, 5).

U ovom trenutku upisno-čitajuća glava nalazi se iznad prazne ćelije (B) tako da je
sledeći prelaz
s2B  s3R
i, s obzirom na to da je s3 završno stanje, rad se zaustavlja. Rezultat je
10011
što je efektivno isto što i 10011 jer je  prazno slovo (koje se tokom rada mora tretirati
eksplicitno, kao simbol, da bi mašina mogla uopšte da funkcioniše).

Veza između specijalne Tjuringove mašine i intuitivnog pojma algoritma


prilično je vidljiva no, nažalost, identičnost je nedokaziva. Neka je A = (X, Y, G, Z)
sintetički definisan algoritam, a T = (K, , , , q0, F) Tjuringova mašina. Tada se
ulazni alfabet A može interpretirati kao , dok je  = X  Y. Ovo poslednje može da
zbuni; međutim ako se A zameni sa A' = (X, X  Y, G, Z) dobija se funkcionalno
ekvivalentan algoritam jer višak slova u novoj izlaznom alfabetu nema nikakvog
uticaja ni na alfabetski operator G niti na kodeks Z, jer je skup vrednosti operator G
ostao isti. Kodeks Z ostvaruje se putem (K, , q0, F) dok cela Tjuringova mašina
realizuje operator G. Sve ovo ne znači da je pred nama egzaktan dokaz ekvivalencije
algoritma i Tjuringove mašine i to zato što ostaje otvoreno pitanje da li se svaki
kodeks Z može ostvariti putem (K, , q0, F). Sve što se može iskazati je modifikovani
oblik osnovne hipoteze teorije algoritama:
 Svaki algoritam može se zadati u obliku funkcionalne sheme specijalne Tjuringove
mašine.
Inače, pored ove verzije specijalne Tjuringove mašine postoji još niz
varijanata koje su, uočimo to, međusobno ravnopravne (ekvivalentne), tako da je sa
stanovišta algoritamskih sistema teorijski svejedno koja se razmatra. Najčešće razlike

20
Dušan T. Malbaški - Algoritmi i strukture podataka

koje se pojavljuju su mogućnost da se upisno-čitajuća glava po upisu ne pomeri


(dakle, pored pomeraja L i R postoji još jedan, npr. N, koji znači da glava ostaje na
istom mestu); druga uočljiva razlika je vezana za zaustavljanje mašine: naime, umesto
skupa završnih stanja, zaustavljanje se može izvesti i tako što do njega dolazi ako se
konfiguracija ponovi dva puta uzastopce. Najzad, postoje i Tjuringove mašine sa dve
ili više traka.
Drugi osnovni oblik Tjuringove mašine je univerzalna Tjuringova mašina.
Nastala je kao odgovor na pitanje da li može da se definiše Tjuringova mašina koja ne
bi, poput specijalne, izvršavala samo jedan algoritam, nego bi mogla da se primeni na
sve algoritme. Pitanje nikako nije stvar običnog uopštenja jer se radi o direktnom putu
ka računaru, tako da je univerzalna mašina sa stanovišta računarstva najvažnija vrsta
Tjuringovih mašina. Odgovor leži u uvođenju mogućnosti da se, pored ulaznih slova,
menja, odn. zadaje i funkcionalna shema. Prema tome, osnovna karakteristika
univerzalne Tjuringove mašine je izmenljivost kako spoljašnjeg alfabeta, tako i
unutrašnjeg alfabeta i funkcije prelaza. Njena konstrukcija zasniva se na ideji
oponašanja (simulacije) različitih specijalnih Tjuringovih mašina, a tehnika kojom se
to postiže je kodiranje kako spoljašnjeg alfabeta, tako i kompletne funkcionalne
sheme pri čemu su sami kodni simboli univerzalne mašine fiksirani, ali se
pridruživanje vrši na različite načine u zavisnosti od specijalne mašine koja se
oponaša. Ono što je možda najvažnije kod ovog tipa mašine je činjenica da se različite
funkcionalne sheme takođe memorišu na traci, tako da svaka takva shema nije ništa
drugo do program, a sama univerzalna Tjuringova mašina ponaša se, u stvari, kao
računar12! Time je teorijski potkrepljena čuvena fon Nojmanova (von Neumann)
koncepcija da između podataka u užem smislu reči i instrukcija za manipulisanje tim
podacima nema razlike.
Skupovi kodnih simbola mogu se razlikovati i može ih biti dva ili više. Ne
ulazeći u detalje, dajemo ilustraciju jednog od mogućih načina kodiranja.
Pretpostavićemo da skup kodnih simbola univerzalne Tjuringove mašine sadrži samo
dva simbola, 0 i 1 (dakle, baš kao i računar) i prikazati jedan od mogućih postupaka
za kodiranje spoljašnjeg alfabeta , unutrašnjeg alfabeta odn. skupa stanja K i
funkcionalne sheme odn. funkcije . Neka se spoljašnji alfabet sastoji od slova a1, ...,

12
Bolje reći računar se ponaša kao univerzalna Tjuringova mašina jer je ovo drugo i hronološki i
pojmovno stariji koncept

21
Dušan T. Malbaški - Algoritmi i strukture podataka

ak i B i neka je skup stanja K = q1, ..., qm. Neka su L i R oznake za pomeraj. Kodne
grupe se definišu kao nizovi različitog broja nula omeđeni dvema jedinicama prema
sledećem rasporedu:

R 101
L 1001
B 10001
a1 100001 - 4 nule
a2 10000001 - 6 nula
.
.
.
ak 10...01 - 2(k+1) nula
q1 1000001 - 5 nula
q2 100000001 - 7 nula
.
.
.
qm 10...01 - 2(m+1)+1 nula

Očigledno, kodne reči se biunivoko preslikavaju na simbole bilo koje specijalne


Tjuringove mašine jer su i spoljašnji alfabet i skup stanja dati sasvim uopšteno.
Primenom gornjeg načina kodiranja prerada reči bcadc u reč bcdcc specijalne
Tjuringove mašine sa  = a, b, ..., z odvijala bi se na bazi ulazne reči univerzalne
Tjuringove mašine oblika

10000001 1000000001 100001 100000000001 1000000001


(b) (c) (a) (d) (c)

dok bi odgovarajući program imao sledeći vid:


1000001 10000001 1000001 101 (q1b  q1bR)
1000001 1000000001 1000001 1000000001 101 (q1c  q1cR)
1000001 100001 100000001 100001 101 (q1a  q2aR)

22
Dušan T. Malbaški - Algoritmi i strukture podataka

100000001 100000000001 10000000001 1000000001 1001 (q2d  q3cL)


10000000001 100001 1000000000001 1000000000001 1001 (q3a  q4dL)
gde je q1 = 1000001 početno, a q4 = 1000000000001 završno stanje ili jedno od
završnih stanja, ako ih ima više.

2.2. NORMALNI ALGORITMI MARKOVA


Normalni algoritmi Markova (prema sovjetskom naučniku A.A. Markovu,
1954.) po osnovnoj ideji srodni su Tjuringovim mašinama zbog toga što je prilaz
direktan, odnosno baziran na odvijanju algoritma. Glavna razlika je u uglu
posmatranja: dok Tjuringove mašine tretiraju algoritme sa stanovišta izvršioca,
normalni algoritmi ih tretiraju sa stanovišta izvršavanja. Iz aspekta programera ovo je
sigurno najprirodniji algoritamski sistem stoga što normalni algoritam nije ništa drugo
do opšti, kanonički oblik bilo kojeg algoritma. Štaviše, upadljiva je sličnost između
normalnih algoritama i Strukturne teoreme (videti dalje) koja se bavi ne samo
algoritmima nego i njihovom implementacijom na računaru.
Prvi korak na putu ka kanoničkom obliku algoritma je standardizacija
alfabetskog operatora G. U tom smislu Markov je uočio da se transformacija ulazne
reči iz skupa X* u reč iz Y*, a posredstvom G, može realizovati u etapama uz
primenu samo dve vrste elementarnih operatora:
1. operatora obrade čiji je zadatak da tekući oblik reči preradi u sledeći koji vodi
(ili ne vodi) konačnom rešenju i
2. upravljačkih operatora što, na osnovu osobina tekuće reči, odlučuju o tome
koji će operator obrade biti odabran za narednu transformaciju.
Za prikaz redosleda izvršavanja operatora obrade diktiranog upravljačkim
operatorima najpogodniji je grafički metod jer, kao što kaže stara mudrost, "jedna
slika vredi hiljadu reči". Od nekoliko varijanata opredeljujemo se za onu koja je
najbliža programerima: blok dijagrame algoritama (organigrame) i to u
najjednostavnijem obliku, dovoljnom za teorijsko razmatranje, a to je onaj koji
obuhvata samo pomenuta dva konkretna operatora, koji se prikazuju simbolima sa
slike 2.2 gde su prikazani respektivno.

Slika 2.2.

23
Dušan T. Malbaški - Algoritmi i strukture podataka

Operatori obrade (procesiranja) transformišu međureči jednu u drugu;


operatori odluke sa obavezno dva izlaza daju informaciju o sledećem koraku.
Operator obrade se, u opštem slučaju, predstavlja kao smena (zamena).
Izvršava se nad rečju. Ako je tekuća reč iz nekog alfabeta A označena sa X tada smena

znači "prvu podreč (čitano sleva)  reči X zameniti sa ; ako to nije moguće ignorisati
operaciju". Na primer, ako se na reč 4923746592367 primeni smena 923  0 dobija
se reč 40746592367. Svrha operatora odluke je da obezbedi informaciju o tome da li
je smena    izvodljiva ili nije, odn. da li je  podreč tekuće reči. Niz reči X1, X2,
... , Xn koji se dobija primenom tačno određenog redosleda smena nosi naziv
deduktivni lanac, što već po semantici nagoveštava postojanje odgovarajuće formalne
teorije. Tako, na primer, dvostruka primena smene bcx na reč abcabca produkuje
deduktivni lanac
abcabca - axabca - axaxa.
Pod normalnim algoritmom Markova podrazumeva se strogo uređeni niz operatora
smene i odluka o njihovoj primeni. Suština definicije je u sintagmi "strogo uređeni
niz" jer različiti nizovi smena mogu da vode ka različitim rezultatima. Ako se na reč
xyzyx primene redom smene xa, xb dobija se reč ayzyb; ako se redosled smena
promeni u x b , x a rezultat je byzya13. Kao što kod Tjuringovih mašina postoje
završna stanja, tako i kod normalnih algoritama postoje završne smene po čijem se
izvođenju smatra da je procedura završena. Označavaju se, obično, tako što se iza
simbola  dopisuje tačka, to jest smena
 . 
označava kraj obrade. Posebno, smena   , koja može biti i završna, ima značenje
"zameniti prvu sleva praznu podreč sa ", to jest "postaviti  na početak reči. Za
praznu reč upotrebljavaćemo oznaku .
Blok dijagram normalnog algoritma Markova prikazan je na slici 2.3. Jasnoće
radi, primenjene su tri metaformule. Neka je i, i=1,...,n smena. Tada predikat P(i ,X)
ima vrednost  ako je i primenljiva na reč X, a  u suprotnom. Dalje, X = i(X) ima

13
Inače, postoji i uopšteni normalni algoritam Markova koji ne podrazumeva nikakav poseban
redosled smena, te zato dozvoljava definisanje nedetermističkih algoritama koji nadilaze nivo ovog
teksta.

24
Dušan T. Malbaški - Algoritmi i strukture podataka

značenje "transformisati reč X primenom smene i". Konačno, predikat K(i) ima
vrednost  ako je i završna smena i  u suprotnom. Reč na koju se primenjuje
algoritam označena je sa X.

P(1,X) X = 1(X) K(1)


T 

 T
1

P(2,X) X = 2(X) K(2)


T 

 T
1

P(n,X) X = n(X) K(n)


T 

 T
1
1

Slika 2.3.

Sa dijagrama se vidi da se, pre svega, smene vrše u strogo određenom redosledu, a
zatim i da postupak može da se završi na dva načina: ili se nailazi na neku od završnih
smena, ili se pak prolazi kroz svih n pokušaja, a da se ne izvrši ni jedna smena. Osim
navedenog postoji i jednostavniji prikaz normalnog algoritma: smene se urede u
sekvencu i primenjuju cirkularno od prve do poslednje; zatim se opet prelazi na prvu i
tako dalje sve dok se ne naiđe na završnu smenu ili se prođe cela sekvenca bez
primene ijedne od njih (videti primer).
Značaj normalnih algoritama je njihova univerzalnost koja proističe iz tzv.
principa normalizacije koji glasi
 Za svaki algoritam može se konstruisati funkcionalno ekvivalentan normalni
algoritam
koji je nedokaziv zbog neodređenosti pojma "svaki algoritam". Neka je zadat alfabet
A i normalni algoritam N. Tada se za N kaže da je normalni algoritam u alfabetu A ako
prihvata ulaz iz skupa slova A*, daje izlaz iz istog skupa i u svim smenama sadrži
samo slova iz tog alfabeta. Postoji, međutim, niz slučajeva u kojima nije moguće

25
Dušan T. Malbaški - Algoritmi i strukture podataka

definisati normalni algoritam funkcionalno ekvivalentan nekom algoritmu u alfabetu


A. Zato se definiše proširenje alfabeta A, odn. skup B  A tako što se dodaju još neka
slova. U tom slučaju normalni algoritam N konstruiše se u skupu B, ali tako da za sve
ulaze iz A* generiše iste izlaze kao i algoritam koji se normalizuje. Za takav normalni
algoritam kaže se da je definisan nad alfabetom A. Princip normalizacije, naravno,
odnosi se upravo na ovakve postupke.

Primer (prema 16). Jedna od osnovnih rekurzivnih funkcija nosi naziv funkcija-
naslednik. To je funkcija jednog celobrojnog argumenta koja za rezultat daje sledeći
po veličini ceo broj. Ako je x ceo broj tada je funkcija-naslednik, u oznaci z’ efektivno
jednaka z+1. Funkcija-naslednik može se izračunati sledećim normalnim algoritmom
Markova (zadat je u pojednostavljenom obliku, kao sekvenca smena):

0y . 1 8y . 9 x5  5x 3x  3y
1y . 2 9y  y0 x6  6x 4x  4y
2y . 3 y . 1 x7  7x 5x  5y
3y . 4 x0  0x x8  8x 6x  6y
4y . 5 x1  1x x9  9x 7x  7y
5y . 6 x2  2x 0x  0y 8x  8y
6y . 7 x3  3x 1x  1y 9x  9y
7y . 8 x4  4x 2x  2y  x

pri čemu su smene, kratkoće radi, smeštene po kolonama. Da bi realizovao funkciju-


naslednik algoritam na ulazu može da primi samo nizove cifara i prazno slovo na
početku, što znači da je osnovni alfabet A = 0, 1, ..., 9. S druge strane, u smenama
se pojavljuju još i slova x i y, što znači da je algoritam definisan nad alfabetom A. Sa
praznim slovom na ulazu algoritam se neće završiti jer se dobija beskonačni niz x, xx,
xxx, ... Neka je na ulazu reč 199. Primena algoritma na ovu reč počinje poslednjom
smenom x i daje x199. Zatim se primenjuje smena x11x što daje 1x99 itd.
Kompletan deduktivni lanac primenjen na ulaz 199 je

199, x199, 1x99, 19x9, 199x, 199y, 19y0, 1y00, 200.

26
Dušan T. Malbaški - Algoritmi i strukture podataka

Imajući u vidu princip normalizacije i ranije navedene oblike osnovne


hipoteze teorije algoritama, poslednja se očigledno može formulisati i ovako
 Svaki algoritam može se predstaviti kao normalni algoritam Markova
iz čega neposredno sledi tvrđenje da su Tjuringove mašine i normalni algoritmi
Markova međusobno ekvivalentni načini za zadavanje istog pojma - algoritma.
Vredno je napomenuti da za ovo tvrđenje postoji i formalni dokaz.

27
Dušan T. Malbaški - Algoritmi i strukture podataka

3. ANALIZA ALGORITAMSKIH STRUKTURA


Pored teorije algoritama koja se prevashodno bavi egzistencijom algoritamskih
rešenja postoji još jedan, ne manje važan, aspekt nauke o algoritmima: analiza
algoritama u okviru koje se tretiraju problemi njihove strukture i realizacije. Ta
problematika obuhvata izučavanje specifičnih algoritamskih struktura, zatim pitanja
korektnosti i efikasnosti i konačno postupke za konstrukciju algoritama pod zadatim
uslovima.
U ovom poglavlju osvrnućemo se na algoritamske strukture i to kroz tzv.
strukturirano programiranje koje je prva zaokružena metodologija za analizu i
izgradnju algoritama. Odmah se skreće pažnja da se, umesto izraza "algoritam" koristi
termin "program" što, strogo gledano, nije sasvim korektno s obzirom na to da se ne
razmatraju tipovi podataka. Treba, međutim, prihvatiti činjenicu da je u kontekstu
strukturiranog programiranja naziv "program" odavno usvojen kao takav i svi ga, bez
razlike, upotrebljavaju, tako da bi pokušaj promene termina izazvao nepotrebnu
konfuziju.
Strukturirano programiranje predstavlja istorijski najstariji (uspešan) pokušaj
da se u postupak razvoja programa unese neki red, odnosno da se programiranje
fundira kao inženjerska delatnost. Nastalo je kao glavna, iako ne i jedina, posledica
rešavanja tzv. softverske krize kraja šezdesetih godina prošlog veka, koja se ogledala u
pojavi potpunog nesklada između hardverskih mogućnosti računara i povećanih
potreba korisnika s jedne i primitivne tehnologije izrade softvera s druge strane.
Naime, osnovna odlika tehnologije izrade softvera pre pojave strukturiranog
programiranja sastojala se u tome da nekakve posebne tehnologije nije ni bilo, pa
stoga za tadašnji pristup razvoju softvera neki autori čak upotrebljavaju izraz
"haotično programiranje"14. Počeci strukturiranog programiranja sežu negde u drugu
polovinu šezdesetih godina kada je formulisana Strukturna teorema Boehm-a i
Jacopinija 1966. godine i kada je Dajkstra (E.W. Dijkstra) objavio čuvenu raspravu
"Goto Statement Considered Harmful" u Communications of the ACM, godine 1968.
Pritom, od posebnog interesa je upravo ovo poslednje jer je Dajkstra ukazao na glavni
razlog neupravljivosti složenih softverskih paketa - programske skokove kojima su
obilovali programi pisani na u to doba glavnim jezicima, fortranu i kobolu.

14
u naučnim krugovima koristi se manje pežorativan naziv “kompozitno programiranje”

28
Dušan T. Malbaški - Algoritmi i strukture podataka

Analizirajući uticaj naredbi skoka (goto nije jedina) on je pokazao da je čitljivost i


razumljivost programa obrnuto proporcionalna broju skokova i sledstveno, da je
verovatnoća postojanja greške u direktnoj korelaciji sa tim brojem. Takvo tvrđenje u
potpunom je skladu sa činjenicom da prilikom rešavanja bilo kojeg problema čovek
rezonuje sistematski, odnosno da ga rešava sekvencijalno, korak po korak, a ne
haotično, preskačući sa jednog potproblema na drugi i vraćajući se na već urađeno.
Programi sa velikim brojem skokova razvijani su upravo na ovaj drugi način te su
stoga obilovali greškama, teško su se pisali, još teže testirali, a skoro nikako
modifikovali i dopunjavali po zahtevu naručioca.15
Osnovne postavke i sredstva strukturiranog pristupa programiranju bili su
relativno brzo definisani, naročito radovima Dajkstre, Hoara (C.A.R. Hoare) i Dala
(O.J. Dahl) (knjiga "Structured Programming" iz 1972. po kojoj je čitava
metodologija dobila ime) i pojavom programskog jezika paskal (Niklaus Virt, 1969.).
Metode i tehnike koje čine strukturirano programiranje intenzivno su se razvijale i u
narednih desetak do petnaest godina predstavljale su jedinu tehnologiju za sistematsku
proizvodnju softvera. U poslednjih desetak godina strukturirano programiranje nije
više dominirajuća generalna paradigma prepustivši primat novijem pristupu: objektno
orijentisanom programiranju. Iz toga se, međutim, ne bi smeo izvesti zaključak da je
strukturirano programiranje zastarelo (programerski svet je inače sklon takvim
preterivanjima) jer deduktivni način razmišljanja prilikom izrade programa nije se
promenio niti je u izgledu da se to desi u skoroj budućnosti. Takođe, upravljačke
strukture koje se koriste u objektnom programiranju (sekvence, selekcije i ciklusi) su
iste one koje su definisane u strukturiranom pristupu.
Strukturirano programiranje bilo je čvrsto vezano za tehniku razvoja programa
tzv. sukcesivnom dekompozicijom (top-down program development), koja se izvodi
postepenom detaljizacijom programa od grube šeme do nivoa spremnog za
kompilaciju. Ova veza bila je toliko čvrsta da se tehnika sukcesivne dekompozicije
izjednačavala sa metodologijom u celini, što je pogrešno, naročito u novije vreme
kada sama tehnika više ne predstavlja fundament za izgradnju softverskih sistema,
dok je strukturirani način razmišljanja i dalje u potpunosti aktuelan. Da bi se napravila
razlika između strukturiranog programiranja i tehnike sukcesivne dekompozicije
dajemo definiciju strukturiranog programiranja 18:

15
takve programe pežorativno zovu "špageti kod".

29
Dušan T. Malbaški - Algoritmi i strukture podataka

 Pod strukturiranim programiranjem podrazumeva se skup tehnika za razvoj


programskih modula koje koriste strogo definisane grupe upravljačkih
struktura i struktura podataka.

3.1. GRAF TOKA PROGRAMA


Graf toka programa je kvazi-digraf čiji čvorovi odgovaraju naredbama,
njihovim delovima ili pak grupama. To je struktura slična blok dijagramu algoritma
(organigramu) koja pokazuje redosled izvršavanja njegovih naredbi. Takođe, ulazne i
izlazne grane16 grafa toka programa obično se vezuju za samo jedan čvor, tj. ulazna
grana ne izlazi ni iz jednog čvora niti izlazna grana ulazi u neki čvor. Kaže se da one
ulaze iz okoline odnosno izlaze u nju. Dakle, za razliku od blok dijagrama algoritma,
graf toka programa nema čvorova tipa START i STOP, a takođe se ne pravi razlika
između operacija obrade i raznih operacija ulaza-izlaza. Graf toka programa, u stvari,
ima samo tri vrste čvorova:
1. Proces ili funkcionalni čvor koji predstavlja operaciju transformacije (obrade)
podataka. Zamenjuje operacije obrade, ulaza-izlaza i poziva podalgoritma u blok
dijagramu. Funkcionalni čvor u grafu toka programa može da zameni i neku
podstrukturu ako je to od interesa za analizu. Ima i ulazni i izlazni stepen jednak 1.
Prikazuje se pravougaonikom u koji se upisuje neka identifikacija operacije (slika
3.1).

Slika 3.1.

2. Predikat je čvor koji odgovara algoritamskom koraku logičkog testa, tj. realizuje
grananje diktirano rezultatom izračunavanja vrednosti odgovarajuće logičke funkcije.
Ima ulazni stepen 1, a izlazni stepen 2 (slika 3.2). U simbol se unosi oznaka logičke
funkcije.

16
grane su prikazane strelicama

30
Dušan T. Malbaški - Algoritmi i strukture podataka


Slika 3.2.

3. Kolektor je pomoćni čvor koji ne izvodi nikakvu obradu nego služi samo za
povezivanje dve grane koje se stiču na istom mestu. Ima ulazni stepen 2, a izlazni
stepen 1 (slika 3.3).

Slika 3.3.

Primer. Funkcija maxEl služi za određivanje vrednosti maksimalnog elementa realnog


niza v sa n elemenata:
double maxEl(double v[], int n) {
double vmax; int i;
for(maxv=v[0],i=0;i<n;i++) if(v[i]>vmax) vmax=v[i];
return vmax;
}
Ovom programu odgovara graf toka prikazan na slici 3.4.

vmax=v[0]
i=0


i<n

T
T
i++ v[i]>vmax

vmax=v[i]

Slika 3.4.

31
Dušan T. Malbaški - Algoritmi i strukture podataka

Na slici su (neuobičajeno) prikazani svi detalji procesa i predikata, a u svrhu lakšeg


razumevanja.
Teorija procedurnog programiranja zasnovana je na pojmu pravilnog
programa. Pravilan program je program čiji graf toka zadovoljava sledeća tri uslova:
1. Postoji tačno jedna ulazna grana.
2. Postoji tačno jedna izlazna grana.
3. Kroz svaki čvor prolazi najmanje jedan put17 od ulazne do izlazne grane (ovaj
uslov sprečava postojanje beskonačnih ciklusa i izolovanih grupa čvorova).
Odmah treba napomenuti da pravilni programi ne čine nekakvu posebnu,
specijalizovanu klasu programa, za razliku od "običnih" programa. Ako se bliže
analiziraju uslovi koje oni zadovoljavaju, lako se dâ primetiti da je baš pravilan
program "običan" a da oni programi koji ne zadovoljavaju gornja tri uslova
predstavljaju posebne, najčešće besmislene, slučajeve, poput onog na slici 3.5, čija je
jedina smislena osobina to da zadovoljava definiciju grafa toka programa i ništa više.

Slika 3.5.

Podgraf18 grafa toka programa nosi naziv potprogram (ne treba ga mešati sa
pojmom potprograma u programskim jezicima!). Potprogram koji je pravilan nosi
naziv pravilan potprogram. Od tri vrste čvorova što sačinjavaju graf toka jedino
procesi sami za sebe predstavljaju pravilne potprograme jer predikati i kolektori ne
zadovoljavaju uslove 2 odnosno 1 respektivno.
Prost program je program kojem odgovara graf toka takav da nijedan od
njegovih pravilnih potprograma nema više nego jedan čvor (a radi se o procesu, što je

17
u ovom smislu put je skup grana g1,...gm takav da grana gi+1 počinje u čvoru u kojem se završava
grana gi, za i=1,2,...
18
podgraf datog grafa jeste struktura koja se dobija uklanjanjem pojedinih čvorova i svih grana
povezanih sa njima

32
Dušan T. Malbaški - Algoritmi i strukture podataka

lako zaključiti). Prosti programi su od ključne važnosti za strukturiranu analizu jer


imaju tu osobinu da se ne mogu dekomponovati na delove koji su za sebe pravilni
programi, sa izuzetkom čvorova-procesa. Značaj prostih programa leži u činjenici da
su upravljačke strukture (naredbe) procedurnih programskih jezika uglavnom prosti
programi.
Skup prostih programa čijom se superpozicijom može realizovati bilo koji
pravilan program nosi naziv baza strukturiranih programa. Izučavanje sastava baza
strukturiranih programa jedan je od najznačajnijih zadataka strukturiranog
programiranja kao metodologije, te se u tom smislu
 strukturirani program definiše kao program sastavljen od skupa prostih
programa iz zadate baze.
Zapazimo da je definicija strukturiranog programa relativna, odnosno zavisi od baze:
program koji je strukturiran u odnosu na jednu bazu ne mora imati tu osobinu za neku
drugu bazu. Baze strukturiranih programa koje su od značaja svode se na proste
programe sa jednim do četiri čvora. Takvih programa ima ukupno 15, ali kao
kandidati za članove baza izdvajaju se oni koji imaju bar jedan funkcionalni čvor
(proces), jer samo takvi programi vrše obradu podataka. Pomenutu grupu čine prosti
programi prikazani na slici 3.6.

proces

sekvenca if-then-else

if-then
do-while-do

while-do

repeat-until
Slika 3.6.

Nazivi osnovnih prostih programa tradicionalno potiču iz paskalske terminologije.


Lako se može uočiti da se prvih 6 osnovnih prostih programa, u vidu naredbi, sreću u

33
Dušan T. Malbaški - Algoritmi i strukture podataka

svim programskim jezicima nastalim posle paskala, što znači da upravljačke strukture
u programskim jezicima (posle fortrana i kobola) nisu nastale iskustveno, nego imaju
teorijsku podlogu. Dodatnih upravljačkih struktura u strukturiranim programskim
jezicima tipa paskala i C-a nema mnogo, a uvedene su iz praktičnih razloga, da bi
programski kôd bio kraći i razumljiviji.
Podsećanja radi, prvih 6 osnovnih prostih programa u programskom jeziku C
su:
izraz; (proces)
naredba1 naredba2 (sekvenca)
if(izraz) naredba
while(izraz) naredba
do naredba while(izraz);
if(izraz) naredba1 else naredba2
Zapazimo da moćni ciklus for u C-u nije prost program!
Analizirati baze strukturiranih programa znači izučavati podskupove
navedenog skupa prostih programa pomoću kojih se mogu realizovati svi pravilni
programi - zato, uostalom, i govorimo o bazi strukturiranih programa. Problem
određivanja bar jedne baze odavno je rešen Strukturnom teoremom čije poznavanje
spada u obavezni deo obrazovanja svakog ko želi da ga smatraju programerom.

3.2. STRUKTURNA TEOREMA


Strukturna teorema formulisana je faktički pre izbijanja softverske krize u
članku "Flow Diagrams, Turing Machines and Languages with only Two Formation
Rules" iz 1966. godine, čiji su autori bili Corrado Boehm i Giuseppe Jacopini.
Teorema se bavi trima osnovnim strukturama: sekvencom, selekcijom tipa if-then-else
i ciklusom tipa while-do. Suština strukturne teoreme je u tome da se svaki pravilan
program može realizovati superpozicijom ova tri prosta programa, a neposredna
posledica da za izradu pravilnih (tj. običnih) programa nisu potrebne naredbe skoka.
Dokaz strukturne teoreme izvešćemo postepeno počevši od ključnog
problema: dokaza da se prelazak sa jednog čvora grafa toka programa na sledeći može
realizovati programskim segmentom koji ne sadrži ništa drugo osim sekvence i
ciklusa tipa while-do. Programski segment kojim se definiše prelazak sa datog čvora
na sledeći, pa stoga i redosled izvršavanja odgovarajućih operacija, zove se funkcija
veze. Da bismo dokazali da se funkcija veze može ostvariti samo pomoću selekcije i

34
Dušan T. Malbaški - Algoritmi i strukture podataka

ciklusa označićemo sve čvorove grafa toka programa osim kolektora rednim
brojevima 1, 2, ... pri čemu je način pridruživanja proizvoljan, te nema gubljenja
opštosti. Okolini pridružujemo, takođe bez uticaja na opštost dokaza, oznaku 0.
Funkcija veze definiše se pomoću kvadratne logičke matrice prelaza čijoj svakoj vrsti
i koloni sa istom oznakom odgovara po jedan označeni čvor grafa toka, uključujući i
okolinu. Dakle, ako graf ima k čvorova tipa procesa i predikata, matrica prelaza je
reda (k+1)x(k+1). Elementi matrice su ili logičke konstante ili predikati sadržani u
čvorovima-predikatima. Prilikom izvršenja programa čvor koji se obrađuje posle
čvora sa oznakom i u matrici prelaza je onaj koji ima vrednost T u koloni koja se seče
sa vrstom i. Ako za matricu prelaza uvedemo oznaku L tada prethodno znači da se
posle obrade čvora i prelazi na onaj čvor j za koji važi Li,j=T. Zapazimo da iz
pravilnosti programa sledi da se u svakoj vrsti u trenutku kada se dospe do
odgovarajućeg čvora pri izvršavanju mora naći tačno jedan element sa vrednošću T,
tako da je naredni čvor za izvršavanje određen jednoznačno. Na slici 3.7 prikazan je
primer jednostavnog grafa toka programa i njegove matrice prelaza.

1
A

p 2

B 3

Slika 3.7.

Matrica prelaza je
0 1 2 3
0  T  
1   T 
2  p  p
3 T   
Lako je pokazati da se primenom matrice prelaza L funkcija prelaza realizuje
programskim segmentom

35
Dušan T. Malbaški - Algoritmi i strukture podataka

{j=0; while(!L[n,j]) j++;} ... (3.1)


gde je n tekući čvor, a j na kraju izvršenja ima vrednost oznake čvora koji se obrađuje
posle čvora n. U skladu sa sintaksom programskog jezika C elementi matrice prelaza
L su 0 umesto  i 1 umesto T. Uočimo da se programski segment (3.1) sastoji od
jedne sekvence i jednog ciklusa. Kratkoće tradi, obeležićemo funkciju koja je
realizovana segmentom (3.1) sa next, tj. next(n) daje oznaku čvora koji se obrađuje po
završetku obrade čvora sa oznakom n. Na osnovu ovog zaključka jednostavnim
postupkom dokazuje se
Teorema 1 (Strukturna teorema). Svaki pravilan program može se transformisati u
ekvivalentan, formalno strukturiran program uz korišćenje tri osnovne upravljačke
strukture: sekvence, selekcije i ciklusa.
Dokaz. Za dokaz teoreme koristićemo konstruktivni pristup, tj. teoremu ćemo
dokazati tako što ćemo za proizvoljan graf toka konstruisati pomenuti ekvivalentni
program. Pri dokazivanju koristićemo uvedene oznake čvorova i funkciju next za koju
smo (sic!) dokazali da se može realizovati samo pomoću sekvence i ciklusa while-do.
Pritom, a ponovo bez gubljenja opštosti, pretpostavićemo da prvi čvor koji se izvršava
nosi oznaku 1. Kanonička forma programa reaizovanog samo pomoću navedene tri
upravljačke strukture (tri osnovna prosta programa) ima sledeći izgled
{
n=1; //1 je oznaka prvog cvora koji se izvrsava
while(n!=0) //0 je oznaka okoline
if(n==1) {obraditi cvor n; n=next(n);}
else if(n==2) {obraditi cvor n; n=next(n);}
else if(n==3) {obraditi cvor n; n=next(n);}
...
else if(n==k) {obraditi cvor n; n=next(n);}
}
gde je k ukupan broj označenih čvorova, ne računajući okolinu 0. Pritom, "obraditi
čvor n" u slučaju da je čvor proces znači izvršiti predviđenu trasnsformaiciju, a ako je
predikat znači izračunati njegovu logičku vrednost, pri čemu za obradu nije potrebna
nijedna od tri upravljačke strukture.
Strukturna teorema ima bar dve važne posledice:
 za programsku realizaciju pravilnih programa nisu potrebne naredbe skoka

36
Dušan T. Malbaški - Algoritmi i strukture podataka

 osnovni prosti programi tipa sekvence, selekcije if-then-else i ciklusa while-do


čine bazu strukturiranih programa.
Pošto sada raspolažemo bar jednom bazom, moguće je ekvivalentnim
transformacijama osnovnih prostih programa izgraditi i druge baze. Ako se ima u vidu
da se prost program
if(p) A else B
može transformisati u ekvivalentnu sekvencu oblika
{
if(p) A
if(!p) B
}
koja sadrži samo sekvencu i if-then sledi zaključak da
 prosti programi tipa sekvence, selekcije if-then i ciklusa while-do takođe čine
bazu strukutiranih programa.
Na sličan način može se ciklus while-do zameniti u bazi ciklusom repeat-until itd.
Interesanrtno je uočiti da dve pomenute baze nisu minimalne, tj. redundantne
su u smislu da se iz njih može isključiti jedna upravljačka struktura, a da i dalje ostanu
baze strukturiranih programa. Kako se selekcija
if(p) A else B
može zameniti sekvencom oblika
{
q=p; r=!p;
while(q) {A q=!q;}
while(r) {B r=!r;}
}
sledi zaključak da selekcija i ciklus while-do čine bazu strukturiranih programa (koja
je, ovaj put, minimalna, tj. neredundantna). Slično, ciklus while-do može se izvesti iz
sekvence i selekcije if-then korišćenjem rekurzivne relacije19
while(p)A  if(p) {A while(p)A}
čije razrešenje uvek vodi ka sekvenci promenljive dužine.
Vratimo se, za trenutak, na bazu sekvenca-selekcija-iteracija. S obzirom na
važnost Strukturne teoreme, često se pod pojmom "strukturirani program"

19
inače, uobičajen postupak kod semantičke analize upravljačkih struktura

37
Dušan T. Malbaški - Algoritmi i strukture podataka

podrazumeva program koji je strukturiran upravo u odnosu na tu, redundantnu, bazu.


Inače, programski jezici su, po pravilu, redundantni tj. sadrže više od te tri (ili pak
dve) upravljačke strukture. Tako, recimo, paskal i C imaju tri iteracije (while, repeat
odn. do-while i for) i tri selekcije (if-then-else odn. if-else, if-then odn. if i case odn.
switch). Osim njih, u C-u su skokovi break i continue u standardnoj upotrebi. Ono što
je, međutim, zajedničko za sve strukturirane jezike jeste striktno izbegavanje naredbe
goto. Ovo ne treba da čudi jer bi programi pisani samo uz pomoć upravljačkih
struktura iz minimalne baze bili preobimni i vrlo komplikovani.

3.2.1. Strukturna teorema i algoritamski sistemi


Ovo poglavlje završavamo vrlo važnim zaključcima opšteg tipa koji povezuju
Strukturnu teoremu sa algoritamskim sistemima. Pošto smo već naveli da su svi
algoritamski sistemi međusobno ekvivalentni, uspostavićemo vezu između
Markovljevih normalnih algoritama i grafova toka programa, s obzirom na to da je
ova veza sasvim očigledna. Na osnovu prikaza Markovljevog normalnog algoritma
(slika 1.3) zaključujemo da normalni algoritam predstavlja pravilan program, jer
zadovoljava sva tri definiciona uslova: ima tačno jednu ulaznu granu, tačno jednu
izlaznu granu i kroz svaki čvor prolazi bar jedan put koji povezuje ulaznu granu sa
izlaznom. Iz ovog, a na osnovu osnovne hipoteze teorije algoritama, sledi
fundamentalni zaključak
 svaki algoritam može se prikazati na programskom jeziku čije upravljačke
strukture obuhvataju bar jednu bazu strukturiranih programa,
i još
 svaki algoritam se može prikazati na programskom jeziku koji sadrži bar
sekvencu i ciklus while-do (alternativa: sekvencu i if-then).

3.3. METODA SUKCESIVNE DEKOMPOZICIJE


Metoda sukcesivne (hijerarhijske) dekompozicije koristi se tokom izrade
programa, sa ciljem da se obezbedi da njegov primarni oblik bude strukturiran, te da
se ne zahtevaju naknadne intervencije. Ova metoda dugo je zauzimala centralno
mesto u strukturnom pristupu programiranju zahvaljujući svojoj prirodnosti i izrazitoj
jednostavnosti. Među njenim promoterima nalaze se neka od najistaknutijih imena u
oblasti računarskih nauka: Dajkstra, Virt, Hoar, ... Popularnost metode sukcesivne
dekompozicije bila je tolika da su je mnogi poistovetili sa čitavom paradigmom

38
Dušan T. Malbaški - Algoritmi i strukture podataka

strukturiranog programiranja što nije opravdano, pogotovo danas, kada sukcesivna


dekompozicija kao globalna metoda ustupa mesto modernijim.
Suštinu metode predstavlja postepeni prelaz od opšteg opisa budućeg
programa ka njegovoj realizaciji, organizovan tako da se u fazama vrši sve veća
detaljizacija. Sam postupak opisao je J. Dujmović 18 na sledeći način:
1. Formulisati problem u obliku pogodnom za računarsko rešavanje.
2. Formulisati osnovnu ideju algoritamskog rešenja.
3. Napisati osnovne komponente programskog rešenja u vidu niza komentara.
4. Izdvojiti pogodnu manju celinu (iskazanu u vidu komentara) i razložiti je na
detaljnije programske zahteve.
5. Ponavljati korak (4) dok se ne dobiju programski zahtevi koji su dovoljno
jednostavni da se mogu realizovati kao programski segmenti na nekom
pseudojeziku.
6. Odabrati neki od programskih zahteva i realizovati ga na pseudojeziku
koristeći pri tome jedino upravljačke strukture iz određene usvojene baze. Na
početku programa dati definicije potrebnih struktura (tipova) podataka.
7. Sistematski ponavljati korak (6) dok god je to moguće i pri tome povećavati
nivo detaljisanja kako za programe tako i za podatke koje program obrađuje.
8. Na kraju, dobijeni program na pseudojeziku prevesti u program na nekom od
konkretnih jezika.
Primena navedenog postupka garantuje, između ostalog, da u programu neće biti
skokova jer se lako zapaža da se rešenje "širi" u vidu stabla između čijih čvorova
postoje samo veze tipa nadređeni-podređeni.
Kao primer, razmotrićemo program za određivanje vrednosti prvog po redu
negativnog elementa niza X=(x1, ..., xn) koji izdaje rezultat 0 ako takvog elementa
nema. U prvoj fazi dekompozicije daje se globalni opis rešenja:
1. /*učitati n i niz X*/
2. /*odrediti vrednost prvog negativnog*/
3. /*prikazati rezultat*/
U sledećem koraku razrađuju se (posebno i nezavisno!) delovi prethodnog opisa:

1.1. /*učitati n*/


1.2 /*dok je 0 <= i < n učitavati sledeći element xi*/

39
Dušan T. Malbaški - Algoritmi i strukture podataka

2.1. /*odrediti vrednost neg prvog negativnog; ako takvih nema neg dobija vrednost
0*/

3.1. /*ako je neg < 0 prikazati njegovu vrednost; u suprotnom izdati poruku o
nepostojanju negativnih elemenata u nizu*/
U trećoj fazi treba razraditi praktično samo komentar 2.1 tako da je krajnji produkt
razvoja programa, koji se vrlo direktno prevodi na strukturirani programski jezik
sledeći:
2.1.1. /*neg = 0*/

2.1.2. /*dok je 0 <= i < n i neg=0 ponavljati: ako je xi < 0 staviti neg=xi, a u
suprotnom povećati i za 1*/
Na osnovu ovog jednostavnog primera možemo zaključiti ponešto i o prednostima, ali
i nedostacima metode sukcesivne dekompozicije. Pre svega, nezavisna razrada delova
programa, pored toga što eliminiše skokove i čini kôd razumljivim smanjujući tako
verovatnoću greške omogućava i timski rad na projektu. Takođe, zbog uniformnosti
tehnologije, za očekivanje je da timovi produkuju module koji su slični po stilu, te se
relativno jednostavno mogu povezati u celinu (nažalost, samo algoritamski, ali ne i
prema strukturama podataka). Metoda, međutim ima i nedostataka, a najvažniji su:
 Problem kompatibilnosti: kako je sukcesivna dekompozicija bazirana na
razradi algoritma može se desiti da se strukture podataka kojima operišu
pojedini moduli međusobno znatno razlikuju, tako da je module teško spojiti u
celinu.
 Problem složenih softverskih sistema: za kompleksne softverske sisteme
veoma je teško, ako ne i nemoguće definisati postupke na visokim (početnim)
nivoima dekompozicije. Kako, na primer, opisati jednim komentarom zadatak
operativnog sistema?
 Kako izvršiti modifikaciju programa (tačnije proširivanje njegovih funkcija)
kada su osnovne funkcije opisane na visokim nivoima dekompozicije? Izmena
na visokom nivou dekompozicije zahtevala bi preuređenje pseudokoda i
odgovarajućeg konkretnog programa kroz veći broj nivoa i bila bi vrlo
zametan posao.

40
Dušan T. Malbaški - Algoritmi i strukture podataka

 Kako iskoristiti ranije napisane i testirane delove programa za izradu drugih?


Vrlo teško, jer je program razvijen sukcesivnom dekompozicijom kompaktna
celina.

Navedeni problemi (i još neki) su tako ozbiljni da sukcesivna dekompozicija ne bi bila


u stanju da nam pomogne da razvijamo programske sisteme današnje kompleksnosti
sa desetinama megabajta kôda te je, stoga, zamenjena modernijim tehnikama (to je
trenutno objektna paradigma). Ipak, ne može se reći da je potpuno zastarela jer se
manje celine kôda i dalje mogu razvijati na ovaj način uz sve navedene prednosti.

3.4. SLOŽENOST (KOMPLEKSNOST) ALGORITAMA


Složenost ili kompleksnost je osobina algoritama koja nije definiciona i nije
inherentna, ali je u praksi - a ne zaboravimo da je "praksa" u oblasti algoritama
programiranje - generalno jedna od najvažnijih. Ko god je pisao program ili pravio
(možda i neračunarski) algoritam dobro zna da za rešenje svakog algoritamski rešivog
problema postoji više, a najčešće mnogo, algoritama. Svi su oni funkcionalno
ekvivalentni, ali uz različit utrošak resursa izvršioca. Inače, složenost i efikasnost
algoritama nisu sasvim isti pojmovi: složenost, onako kako će biti definisana u ovom
odeljku, predstavlja meru njegove efikasnosti. Polazeći od računara, kao tipičnog
izvršioca algoritma, zaključujemo da efikasnost programa zavisi od utroška
računarskih resursa, a to su
 vreme i
 memorija.
Utrošak vremena i memorije nije sasvim jednostavno ni definisati a kamoli proceniti,
jer zavise od konkretnog računara: vreme izvršenja i memorijski zahtevi zavise i od
drugih činilaca osim samog programa: hardvera, operativnog sistema, čak i trenutnog
stanja računara. Usled toga, umesto efikasnosti programa, daleko je celishodnije
razmatrati efikasnost algoritma. Dakle, umesto vremena odn. memorijskog prostora,
uvode se njihove mere. Nažalost, ni ovo nije univerzalno rešenje, jer problemi nastaju
kada za vreme-prostor treba izabrati meru. Demonstriraćemo to na vremenu izvršenja
koje je više u fokusu posmatranja od memorijskog prostora. Kako odrediti meru za
vreme izvršenja algoritma? Kao prva ideja nameće se broj izvršenih algoritamskih
koraka izvršenih u datom prolazu kroz algoritam. Ovo pak stvara nove poteškoće, jer
u različitim prolazima, broj izvršenih koraka može biti i jeste različit! Shodno tome,

41
Dušan T. Malbaški - Algoritmi i strukture podataka

mera efikasnosti treba da bude funkcija koja zavisi od konkretnih ulaznih podataka,
što opet otvara pitanje opisivanja ulaza. Pre svega, više ulaznih podataka niukoliko ne
znači da će izvršenje trajati duže: u numeričkoj analizi postoje algoritmi koji uopšte
nemaju ulazne podatke20, a itekako mogu da traju. Kada analiziramo do sada rečeno,
uočićemo da sve ukazuje na potrebu uvođenja apstraktne mere efikasnosti, kako
vremenske tako i prostorne. Mera (vremenske i prostorne) efikasnosti algoritma nosi
naziv složenost ili kompleksnost.
Inače, svakom programeru je poznato, nedokazivo ali veoma čvrsto, pravilo
recipročnosti utroška memorije i vremena izvršenja (engl. space-time tradeoff) koje
kaže da se u cilju ubrzavanja programa mora žrtvovati memorijski prostor i vice
versa. Za ovo pravilo ne postoji dokaz jer se mogu napraviti ekvivalentni programi P1
i P2 takvi da je npr. P2 i sporiji i zauzima više memorije, no tada P2 ili ispunjava neke
dodatne zahteve (recimo, u pogledu robustnosti) ili naprosto ne valja! Ipak, određena
veza između vremena i kapaciteta je ustanovljena. U 19 navodi se teorema koja
glasi: za svaku Tjuringovu mašinu sa više traka postoji takav broj k za koji kapacitet
M i vreme T zadovoljavaju nejednačine M  kT i (ako je l dužina ulaza, a T < ) T 
kM+l.
S obzirom na činjenicu da se, u praksi, vremenu ipak pridaje veća važnost
nego memorijskom prostoru u daljem ćemo se koncentrisati na vremensku složenost
algoritama. Videli smo da vreme kao pokazatelj utroška resursa nije lako ni definisati,
pa je tim pre teško naći zadovoljavajuću meru - zadovoljavajuću u smislu da odražava
stvarni utrošak s jedne i da se uopšte može izvršiti procena s druge strane. Rešenje je
u proučavanju trenda, tj. analizi asimptotskog ponašanja algoritma pri povećavanju
obima polaznih podataka (namerno se koristi izraz "polazni" umesto "ulazni" jer
podaci ne moraju biti učitani, nego se mogu delom ili u celosti generisati u okviru
samog algoritma). Osnova za procenu vremenske složenosti je tzv. dimenzija
problema definisana kao broj polaznih podataka 9. Do ove veličine se dolazi
uglavnom lako s obzirom na to da je analiza asimptotska te se polazni podaci čiji je
broj nepromenljiv ne uzimaju u obzir, osim za trivijalne, tj. konstantne algoritme, a i
kod njih se prikazuju kao konstanta bez konkretne vrednosti. Tako, na primer,
algoritam koji obrađuje jedan niz dužine k ima dimenziju k; algoritam koji manipuliše
jednom matricom sa k vrsta i m kolona ima dimenziju n=kxm. U daljem tekstu

20
recimo, algoritam za numeričko određivanje rešenja nelinearne jednačine

42
Dušan T. Malbaški - Algoritmi i strukture podataka

prihvatićemo (apstraktnu) dimenziju prosto kao ceo nenegativan broj n. Vrednost


dimenzije nije određena jednoznačno i zavisi od konkretnog problema. Na primer, za
algoritam koji manipuliše matricom može se prihvatiti dimenzija n=kxm ili, ako je
matrica kvadratna samo n gde je n red matrice. Štaviše, u slučaju potrebe, možemo
odustati od skalarne dimenzije n i za nju prihvatiti uređenu s-torku (n1,...,ns) gde su
n1,...,ns celi nenegativni brojevi. U daljem tekstu koristićemo skalarnu dimenziju kao
meru obima polaznih podataka.
Neka je, dakle, n dimenzija problema, Z skup celih nenegativnih brojeva, a R
skup realnih brojeva. Tada se pod vremenskom složenošću (engl. time complexity)
podrazumeva nenegativna, monotono neopadajuća funkcija
T : ZR
čiji argument ima semantiku dimenzije, a vrednost semantiku vremena izvršenja. I
prostornu složenost definisaćemo na isti način, kao nenegativnu, monotono
neopadajuću funkciju
M : ZR
U daljem ćemo se baviti vremenskom složenošću za koju ćemo, kratkoće radi,
koristiti izraz složenost. Tačnu vrednost funkcije T(n) za zadatu dimenziju n moguće
je izračunati samo kod sasvim jednostavnih algoritama no, na sreću, u najvećem broju
realnih slučajeva od interesa je analizirati asimptotsko ponašanje T(n) za velike
vrednosti n imajući u vidu činjenicu da je T(n) monotono neopadajuća funkcija.
Asimptotska procena složenosti može se vršiti na tri načina: preko gornje
granice, preko donje granice i kombinacijom. Najpopularniji je prvi način.
Analiza gornje granice složenosti obavlja se na bazi tzv. O-notacije (engl.
Big-Oh notation). Neka je g(n) monotono neopadajuća funkcija.
g : ZR
Definišimo skup istih takvih funkcija

O[g(n)] = {f(n) | (n0,c>0) (nn0) 0f(n)cg(n)}

gde je c>0 konstanta. Manje formalno, Og(n) je oznaka za skup funkcija za koje je,
počev od neke vrednosti n=n0, funkcija constg(n) gornja granica, slika 3.8.

43
Dušan T. Malbaški - Algoritmi i strukture podataka

cg(n)

f(n)

n0
Slika 3.8.

Očigledno je da za svaku funkciju f(n) koja pripada Og(n) postoji konstanta c>0
takva da važi
f(n)
lim <c
n  g(n)
Na primer,
n3+2n-5  On3
2log2n+1  Olog2n
32n+log2(2n)  O2n
Prilikom procene gornje granice za funkciju složenosti T(n), u stvari se određuje
funkcija g(n) takva da važi
T(n)  Og(n)
Ovde smo dužni da napomenemo da se umesto gornje relacije u analizi složenosti
koristi (matematički nekorektna) notacija
T(n) = Og(n),
no ta praksa je toliko raširena da je nećemo izbegavati ni u ovoj knjizi, naravno
znajući pravo njeno značenje. Za O notaciju važe neki opšti zakoni:
f1O[g1]  f2O[g2]  f1f2O[g1g2]
fO[g]  O[fg]
f1O[g1]  f2O[g2]  f1+f2O[g1+g2]
fO[g]  gO[h]  f O[h]
f+O[g]  O[f+g]
O[kg] = O[g], k=const>0
O[k+g] = O[g], k=const

44
Dušan T. Malbaški - Algoritmi i strukture podataka

O[k] = O[1], k=const>0


fO[g]  kfO[g], k=const
Primetimo da se od funkcije g teorijski ne zahteva da bude striktno pozitivna.
Međutim, lako se zapaža da, na osnovu definicije Og, ako postoji neka vrednost n1
iza koje je g stalno negativna, tj.
(n>n1)  (g(n)<0)
tada je
O[g] = .
Postoji nekoliko karakterističnih oblika funkcije g(n) prema kojima se vrši
klasifikacija algoritama po tzv. redu složenosti. Ti specifični oblici sa nazivima
odgovarajućih klasa algoritama dati su u sledećoj tabeli:
g(n) Tip algoritma
const konstantan
logk n (k>1) logaritamski
n linearan
nlogk n (k>1) linearno-logaritamski
n2 kvadratni
k
n (k>2) stepeni
n
k (k>1) eksponencijalni
n! faktorijelni
Uočimo da su tipovi algoritama, dati u tabeli, poređani po rastućem redu složenosti,
kao i činjenicu da su poslednja dva (eksponencijalni i faktorijelni) na današnjem
nivou razvoja tehnologije još uvek van domašaja mogućnosti računara. Da bi čitalac
stekao jasnu sliku o razlikama između klasa algoritama iz gornje tabele navodimo
neke konkretne brojke:
n log2n n nlog2n n2 n3 2n n!
10 3,3 10 33,2 100 1000 1024 3,6 106
100 6,6 100 664 104 106 1,3 1030 9,3 10157
500 9,0 500 4483 2,5 105 1,3 108 3,2 10150 1,2 101134
1000 10,0 1000 9966 106 109 1,1 10301 4 102567

Primer. U okviru ovog primera demonstriraćemo izračunavanje reda složenosti


relativno jednostavnog algoritma za uvećavanje vrednosti elemenata niza za jedinicu.

45
Dušan T. Malbaški - Algoritmi i strukture podataka

Skrećemo pažnju na činjenicu da se određivanje reda složenosti izvodi direktno iz


programa. Neka je dat program P1 oblika
int main() {
int a[100], i, n;
scanf(“%d”,&n);
for(i=0;i<n;i++) scanf(“%d”,a+i);
for(i=0;i<n;i++) a[i]++;
for(i=0;i<n;i++) printf(“%d “,a[i]);
}
koji ima za zadatak da učita n elemenata niza a, uveća svaki za 1 i ispiše rezultat na
ekranu. Neka je sa t označeno trajanje dela naredbe ili funkcije. Tada je ukupno
trajanje izvršavanja programa
tP1(n) = t(scanf(n)) + n (t(for) + t(scanf(a+i)))
+ n (t(for) + t(ai++))
+ n (t(for) + t(printf(ai))
+ t(inicijalizacija i terminiranje programa).
gde t(for) predstavlja trajanje obrade upravljačkog dela C-ovog ciklusa for. Primetimo
da su sva vremena označena sa t(naredba) konstantna ili vrlo približno konstantna u
odnosu na n. Složenost programa P1 je
TP1(n) = K1+nK2+nK3+nK4+K5 = A+Bn
gde su K1,...,K5, A=K1+K5 i B=K2+K3+K4 konstante veće od 0. Primenom osobina O,
dobija se
O[A+Bn] = O[Bn] = O[n].
Dakle,
TP1(n)  O[n]
ili, u uobičajenoj notaciji,
TP1(n) = O[n]
što znači da program P1 ima osobine linearnog algoritma.
Analiza donje granice zasnovana je na tzv. -notaciji. Slično gornjoj granici,
donja granica određena je skupom funkcija

[g(n)] = {f(n) | (n0,c>0) (nn0) 0 cg(n)f(n)}

46
Dušan T. Malbaški - Algoritmi i strukture podataka

gde je c>0 konstanta. Neformalno, g(n) je oznaka za skup funkcija za koje je,
počev od neke vrednosti n=n0, funkcija constg(n) donja granica. Ilustracija je data na
slici 3.9.

f(n)

cg(n)

n0
Slika 3.9.

Najbolja, ali i najkomplikovanija, procena dobija se kombinacijom dveju prethodnih i


nosi naziv -notacija. Skup g(n) definiše se kao skup funkcija oblika

[g(n)] = {f(n) | (n0,c1,c2>0) (nn0) 0 c1g(n)f(n)c2g(n)}

gde su c1 i c2 pozitivne konstante. Za funkciju T(n) koja pripada skupu [g(n)] kaže
se da je reda veličine g(n). Ilustracija je data na slici 3.10.

c2g(n)

f(n)
c1g(n)

n0
Slika 3.10.

47
Dušan T. Malbaški - Algoritmi i strukture podataka

48
Dušan T. Malbaški - Algoritmi i strukture podataka

1. OSNOVNI POJMOVI

„Algorithms + Data Structures = Programs“ jeste naslov čuvene knjige


Niklausa Virta, tvorca paskala (i još mnogo čega u sferi programiranja). Teško je naći
koncizniju i tačniju ilustraciju značaja struktura podataka koje, kako se vidi, Virt
stavlja u istu ravan sa algoritmom i programom, fundamentalnim pojmovima u
računarstvu. Algoritam i struktura podataka čine (procedurni) program i imaju se
shvatiti kao avers i revers na moneti: ako nedostaje bilo koji od ova dva dela, moneta
prestaje da bude - moneta.
Sam pojam podatka je najosnovniji pojam u računarstvu uopšte: sve što se
nalazi u memoriji jeste podatak, čak i instrukcije mašinskog programa jer - kako je
zapazio Dajkstra (egzekutor naredbe goto) - program nije ništa drugo do uputstvo
računaru šta treba da radi. Izvorni kod programa pisanog na bilo kojem programskom
jeziku je samo uređen skup ulaznih podataka drugog programa - kompajlera ili
asemblera. U nastavku bavićemo se samo podacima u užem smislu reči, dakle
podacima kao vrednostima.
S obzirom na značaj pojma podatka ne čudi da postoji velik broj više ili manje
formalnih definicija ovog pojma, zavisnih i od oblasti u kojima se koncept koristi jer
računarstvo, naravno, nije jedina takva oblast. Sve one, međutim, na neki način su
povezane sa percepcijom sveta, što znači da je podatak u suštini gnoseološka
(saznajna) kategorija. Prema [1] "podaci odgovaraju diskretnim, zapisanim
činjenicama o fenomenima, iz kojih se izvlače informacije o svetu", pri čemu odmah
zapažamo da se pojmovi podatka i informacije ne poklapaju: po Langeforsu,
informacijom se smatra povećanje znanja koje se izvodi iz podataka. Na primer, kada
kažemo "ispod Savskog mosta teče reka Sava" to očigledno nije informacija, jer
pomenuti podatak ne povećava ničije znanje. Ako bi se, kojim slučajem, ispostavilo
da ispod Savskog mosta teče reka Dunav ili uopšte ne teče nikakva reka, to bi već bila
informacija.
Prikupljanjem podataka o nekoj jedinici posmatranja ili, kako se to još kaže,
entitetu21 stiče se saznanje o njegovim esencijalnim osobinama, tj. stvara se slika tog
entiteta. Činjenica da neki zapis predstavlja podatak tada i samo tada kada se odnosi

21
od novolatinske reči entitas sa značenjem biće u filozofskom smislu

49
Dušan T. Malbaški - Algoritmi i strukture podataka

na konkretnu osobinu konkretnog entiteta, kao i da je podatak promenljiv u vremenu


sublimirane su u još jednoj, nešto formalnijoj, definiciji podatka:
 podatak je uređena četvorka (naziv_entiteta, naziv_osobine, vrednost_osobine,
vreme)
Očigledno, vrednost 52 nije podatak jer se ne zna na šta se odnosi: da li su to nečije
(čije?) godine, telesna masa ili broj cipela; u kojem vremenskom trenutku je izvršena
opservacija? Da li je 52 broj? Da li je prikazan u dekadnom brojevnom sistemu?
Pomenuta slika entiteta, naravno, ne sadrži sve moguće podatke o njemu, jer je
to jednostavno nemoguće (neka čitalac pokuša da nabroji sve podatke o običnoj
olovci uključujući i egzotične poput električne otpornosti ili gustine). To, međutim,
nije ni potrebno jer nisu sve osobine entiteta relevantne za konkretno razmatranje ili,
kako se to još kaže, nisu relevantne za dati domen problema. Opisivanjem entiteta
pomoću osobina koje su relevantne za dati domen problema, dobija se model tog
entiteta u datom domenu problema22. Model osobe koja studira na nekom fakultetu
neizostavno sadrži i osobinu "broj indeksa"; model te iste osobe u informacionom
sistemu grada tu osobinu nema, jer u tom domenu problema nije relevantna. Do
(informacionog) modela nekog entiteta dolazi se posredstvom jednog od najmoćnijih
oruđa ljudskog intelekta – apstrakcije. Apstrahovanjem svodimo entitet na model koji
se sastoji od relevantnih osobina, dok se one koje su irelevantne za dati domen
problema jednostavno zanemaruju. Samim tim, uočavaju se sličnosti između
pojedinačnih entiteta te, uz zanemarivanje nebitnih razlika entiteti se mogu
klasifikovati. Na taj način – da parafraziramo primer iz 2 - od jedne velike crvene
jabuke, jedne žute jabuke, jedne nagrižene jabuke i jedne male jabuke dolazi se do
pojma „četiri jabuke“ i odmah do klase „jabuka“. Daljim apstrahovanjem od četiri
jabuke“, „četiri olovke“, „četiri čoveka“ itd. dolazimo do apstraktnog pojma „četiri“, a
od brojeva 4, 12, 345, 1326 do pojma „prirodan broj“. I tako dalje.
Model entiteta, dakle, može se shvatiti kao skup podataka o njegovim
relevantnim osobinama23. Entiteti koji imaju isti model (tj. iste skupove relevantnih
osobina) čine klasu entiteta. S druge strane, ako se kao cilj prikupljanja podataka o
entitetima postavi obrada tih podataka (a sve u svrhu sticanja novih informacija) ne
može se zaobići zahtev da ti podaci budu uređeni. Konačno, budući da je, prema

22
u ovom kontekstu, model predstavlja homomorfnu (uprošćenu) sliku originala
23
relevantnim u odnosu na domen problema

50
Dušan T. Malbaški - Algoritmi i strukture podataka

Burbakiju, struktura kao pojam prosto skup snabdeven nekim uređenjem, dolazimo do
pojma strukture podataka, kao uređenog skupa podataka, i to uređenog u skladu sa
proizvoljnim brojem kriterijuma.
Posebno, kada je u pitanju računar kao medijum za skladištenje i obradu
podataka, njihovo uređivanje odn. organizovanje ne može se izbeći, ako ni zbog čega
drugog, a ono zbog toga što je računar konačni automat u kojem je svaki podatak
strogo definisan i po tipu i po mestu memorisanja. Podaci u nekom programu mogu
biti skalarni ili pak složeni u vektore, matrice, slogove ili pak u komplikovanije
strukture o kojima će biti reči u ovoj knjizi.
U ranim fazama razvoja računarstva, kada su se problemi koji su na njima bili
rešavani odnosili isključivo na matematičko-tehničke proračune, potreba za
uređivanjem podataka svodila se na svega nekoliko jednostavnih struktura: na skalare,
vektore, matrice i ređe na njihova tro- i višedimenzionalna uopštenja. Prelaskom na
nenumeričke probleme, a osobito uključivanjem računara u informacione sisteme
pojavila se potreba za složenijim, pa i daleko složenijim strukturama. Razlog za to leži
u činjenici da se isti podaci mogu strukturirati na više načina i da odabrana struktura
neposredno utiče na kvalitet procesiranja u smislu brzine rada programa i njegove
pouzdanosti, utroška memorijskog prostora i pogodnosti za održavanje, neki put do te
mere da neka rešenja uopšte nije moguće implementirati ako se ne obezbedi
adekvatna organizacija podataka.
Primer 1.1.
Uticaj izbora strukture podataka na konstrukciju algoritma, njegovu složenost i
utrošak memorijskog prostora programa demonstriraćemo na jednostavnom primeru
(1) pristupa svakom pojedinačnom elementu, odnosno (2) obrade nenultih elemenata
double matrice A sa v vrsta i k kolona. Pretpostavimo da se obrada elemenata obavlja
algoritmom obraditi čija je složenost konstantna.
Najjednostavnije rešenje je svakako upotreba gotovih sredstava koje nudi
programski jezik. Matricu A definišemo rezervisanjem memorijskog prostora u fazi
kompilacije:
double A[VMAX][KMAX];
Pristup elementima obavlja se dvostrukim indeksiranjem, tj. izrazom Aij, što je i
jednostavno i brzo. Obrada nenultih elemenata vrši se dvostrukim for ciklusom:
int i,j,v,k;

51
Dušan T. Malbaški - Algoritmi i strukture podataka

for(i=0;i<v;i++)
for(j=0;j<k;j++)
if(!A[i][j]) obraditi A[i][j];
Inače, postupak obrade je školski primer algoritma sa složenošću On2. Međutim,
problem koji će i ne naročito iskusan programer odmah uočiti jeste veličina
memorijskog prostora koju treba da rezerviše kompajler, tj. zadavanje VMAX i
KMAX. Ako su te dve veličine prihvatljive (ma šta to značilo) – problem je rešen.
No, šta biva ako su VMAX i KMAX preveliki, na primer reda veličine 1000? Bez
obzira na to što savremeni računari raspolažu velikom operativnom memorijom,
ovakvo rešenje (rezervisanje milion double lokacija) u normalnim uslovima je vrlo
rizično zbog načina rukovanja operativnom memorijom i činjenice da se matrica
nalazi u statičkoj memoriji programa24. Dodatna poteškoća može da bude i
nemogućnost apriorne procene maksimalnog broja vrsta odnosno kolona matrice.
Jedno od mogućih rešenja jeste da se matrica realizuje tako što će sadržati
samo aktuelne elemente i neće zahtevati preliminarno rezervisanje memorije u fazi
kompilacije. Drugim rečima, za matricu će se zauzeti ne više
VMAX x KMAX x sizeof(double) bajtova, nego v x k x sizeof(double) bajtova. Ovo
će se postići smeštanjem matrice u dinamičku zonu memorije (hip, heap), koji je veći
od statičke memorije, ali i omogućuje da se memorijski blok zauzme (i oslobodi!) u
toku izvršenja programa. To će, međutim, uticati na način realizacije matrice A. Prvo,
A neće biti definisana kao matrica, nego kao dvostruki pokazivač, u skladu sa
mehanizmom pokazivača u programskom jeziku C:
double **A;
Sada je, međutim, neophodno obezbediti da se, u toku izvršenja programa, eksplicitno
zauzme odgovarajući memorijski prostor na hipu:
int i,j,v,k;
.................
A = malloc(v*sizeof(double*));
for(i=0;i<v;i++) A[i] = malloc(k*sizeof(double));
Zahvaljujući specifičnoj vezi pokazivača i nizova u programskom jeziku C, pristup
pojedinačnim elementima i segment za obradu svih nenultih elemenata ostali bi

24
Autor je pokušao da definiše matricu 1000x1000; C kompajler je preveo program, ali je njegovo
pokretanje izazvalo havariju.

52
Dušan T. Malbaški - Algoritmi i strukture podataka

nepromenjeni25. Ovakva realizacija, pre svega, ublažava problem zauzeća memorije


jer se zauzima samo onoliko prostora koliko matrica stvarno zauzima. Pored toga, hip
ima daleko veći kapacitet od statičke memorije. Ipak, cena postoji, a čini je upravo
opisani segment za formiranje matrice u toku izvršenja programa: ako je matrica
velika (dakle v i k su veliki, a ne samo V_MAX i K_MAX) taj segment itekako može
da potraje!
Posvetimo, sada, nešto više pažnje procesu obrade svih nenultih elemenata
matrice. Ako je matrica velika tada dvostruki ciklus u navedenom algoritmu može da
postane usko grlo, s obzirom na to da mu je složenost On2. S druge strane, poznato
je iz prakse da su velike matrice po pravilu retke, tj. najveći broj njihovih elemenata
jednak je 0. Ukoliko je to slučaj i kod matrice A, možemo iskoristiti tu osobinu da
bismo prvo uštedeli memorijski prostor, a zatim i ubrzali postupak obrade nenultih
elemenata. Ideja se sastoji u tome da se memorišu samo oni elementi koji su različiti
od 0 i to u posebnoj strukturi podataka koja više nije matrica nego niz slogova oblika
typedef {
int vrsta, kolona;
double vrednost;
} Element;
gde su vrsta i kolona oznake vrste i kolone elementa, a vrednost njegova vrednost
(koja nije 0). Umesto matrice A podatke ćemo smestiti u niz B oblika
double BMAX_P;
gde je MAX_P procenjeni maksimalni broj elemenata matrice A koji su različiti od 0.
Naravno, i rešenje sa korišćenjem hipa je moguće, a tada bismo matricu B formirali
ovako:
double *B; int p;
B = malloc(p*sizeof(Element));
gde je p stvarni broj elemenata matrice A koji su različiti od 0. Postupak obrade
nenultih elemenata se pojednostavljuje i obavlja jednim ciklusom
for(i=0;i<p;i++) obraditi B[i].vrednost;
Složenost ovog postupka nije nužno manja od On2, ali u zavisnosti od broja nenultih
elemenata originalne matrice A može da se spusti ispod tog nivoa, tako da možemo

25
da je u pitanju paskal, postupak bi bio u osnovi isti, ali bi zahtevao nešto više izmena u kodu

53
Dušan T. Malbaški - Algoritmi i strukture podataka

zaključiti da je složenost modifikovanog postupka u najgorem slučaju jednaka On2.


Nažalost, i ovo rešenje ima cenu (u inženjerskim discplinama uvek je tako). Cena se
sastoji u sporijem pristupu pojedinačnim elementima. Umesto jednostavnog izraza
Aij koristila bi se funkcija
double getAij(double B[], int p, int i, int j) {
int m;
for(m=0;m<p;m++) if((B[m].vrsta==i)&&(B[m].kolona==j)) return B[m].vrednost;
return 0;
}
To nije sve. Problem može da nastane je ako se javi potreba za menjanjem vrednosti
elemenata matrice A. U originalnoj izvedbi to se postiže jednostavnim izrazom
A[i][j]=x, gde je x nova vrednost. U modifikovanoj varijanti element prvo treba
pronaći u nizu B, pa onda izmeniti njegovo polje vrednost na x. Ali - šta ako je
vrednost bila prethodno jednaka 0, a x je različito od 0, ili obrnuto, ako je vrednost
bila različita od 0, a x je jednako 0? U tim slučajevima odgovarajući element treba da
bude dodat u niz B odnosno uklonjen iz njega, za šta je potrebno formirati
odgovarajuće funkcije.
Ovde prekidamo dalju razradu rešenja, jer je cilj razmatranja bio da se ilustruje
uticaj algoritma na strukturu podataka i vice versa. Ono što treba istaći jeste da od
nabrojanih varijanata nijedna nije apsolutno najbolja. Osnovna varijanta obezbeđuje
brzo očitavanje i promenu vrednosti pojedinačnih elemenata, dok je obrada svih
nenultih elemenata sporija, a utrošak memorijskog prostora može da bude
neprihvatljivo velik. Varijanta sa hipom štedi memoriju, ali troši vreme na formiranje
matrice. Memorisanje samo nenultih elemenata skraćuje vreme njihove obrade, ali
produžava procedure ažuriranja vrednosti pojedinačnih elemenata.
Sve ovo, najzad, ukazuje na činjenicu da se struktura podataka mora
projektovati u skladu sa jasno definisanim zahtevima i uz pažljiv izbor adekvatnih
metoda. Uopšte, kada se projektuje programski sistem, osnovno pitanje koje se
postavlja jeste: šta izabrati prvo, algoritam ili strukturu podataka? Odgovor daje Virt u
3: s obzirom na to da se algoritam odvija nad strukturom podataka, a ne obrnuto, ona
je ta koja se prvo definiše jer „pre nego što izvedete operacije nad nekim objektima
morate definisati njih“. Još je jedan razlog za navedeno tvrđenje, a on leži u činjenici
da strukture podataka (ako su dobro projektovane) znatno ređe podležu modifikaciji

54
Dušan T. Malbaški - Algoritmi i strukture podataka

nego odgovarajući algoritmi. Ako su A1,..,An algoritmi koji operišu nad strukturom
podataka S, tada izmena u algoritmu Ai ne mora da utiče na oblik ostalih, dok
modifikacija strukture podataka S u opštem slučaju zahteva promene u više
algoritama, a počesto i u svim.

1.1. DEFINICIJA STRUKTURE PODATAKA


Za strukturu podataka egzistira više definicija, manje ili više formalnih,
baziranih na konceptima skupa i relacije, ali i na drugim. Potpuno formalna definicija
postoji (radi se o tzv. apstraktnom tipu podataka), ali ona nadilazi naše potrebe i
zahteva primenu sofistikovanog matematičkog aparata. U ovom tekstu, opredelićemo
se za definiciju koja nije potpuno formalna (ali je bar jednostavna), a koja se oslanja
na matematičke strukture, odnosno na skupove snabdevene relacijama (po Burbakiju).
U opštem slučaju, strukturu podataka predstavlja skup snabdeven jednom ili
više relacija, praznih, binarnih ili reda većeg od dva. Ovde se odmah postavlja pitanje
šta su elementi pomenutog skupa? U krajnjoj liniji to su svakako podaci, ali kakvi?
Još od vremena nastanka paskala, matrica se definiše kao niz čiji su elementi nizovi
podataka, dakle kao linearno uređen skup čiji su elementi opet strukture podataka!
Shodno tome, strukturu podataka morali bismo definisati kao uređen skup struktura
podataka što bi bila cirkularna definicija pojma26, a to nije dozvoljeno ni u
elementarnoj logici. Cirkularne definicije se, u matematici, obično razrešavaju
rekurzijom i to tako što se polazi od atomarnih (nesvodivih) pojmova i definicija gradi
na njima. Atomarni pojam za strukturu podataka jeste skalar. Pod skalarom ćemo
podrazumevati podatak koji nije u relaciji ni sa čim. U programskim jezicima skalari
su realizovani baznim tipovima (int, double, enum itd.) u koje se negde (paskal)
uključuju i stringovi, a negde (C) ne. Polazeći od skalara, možemo sačiniti sledeću
(rekurzivnu) definiciju strukture podataka:
Definicija 1.1
1. Skalar je struktura podataka.
2. Ako su S1,...,Sk strukture podataka i S=S1,...,Sk tada je uređena n+1-orka
(S,r1,...,rn) gde su r1,...,rn relacije u skupu S, takođe struktura podataka.
3. Struktura podataka se može obrazovati samo konačnim brojem primena stavki
1 i 2 ove definicije.

26
Cirkularna definicija pojma: pojam se definiše preko samog sebe.

55
Dušan T. Malbaški - Algoritmi i strukture podataka

Uočimo da ovu definiciju zadovoljava i (neuređen) skup podataka koji interpretiramo


kao skup snabdeven praznom relacijom. Dodajmo još i to da su, u opštem slučaju, i
skup S i sve relacije promenljivi u vremenu, što znači da elementi strukture podataka
mogu da se menjaju, ali i da se dodaju i uklanjaju. Štaviše, ovo je normalna situacija,
jer strukturu podataka treba shvatiti dinamički, kao nešto što se neprestano menja bilo
dodavanjem ili uklanjanjem elemenata bilo promenom vrednosti skalara koji je
sačinjavaju.
Relacije kojima se uređuje struktura podataka mogu biti jednostavne poput
binarne relacije kojom se povezuju elementi niza, ali mogu biti i semanički bogate
kao što bi bila relacija što povezuje element „student“ sa elementom „fakultet“. Kod
binarnih relacija element koji (u smislu relacije) prethodi datom nosi naziv
prethodnik, a element koji se nalazi iza datog zove se sledbenik. Formalno, ako se u
nekoj binarnoj relaciji r nalazi uređeni par (a,b) tada je a prethodnik b, odnosno b je
sledbenik a, sve u smislu relacije r.
Među fundamentalnim strukturama podataka preovlađuju one koje se mogu
predstaviti uređenim parom (S,r) gde je r binarna relacija. Kako je pomenuti uređeni
par istovremeno i jedna od definicija orijentisanog grafa (tzv. digrafa) bez višestrukih
grana, to se digraf koristi kao model takve strukture. Kod digrafa (S,r) elementi skupa
S zovu se čvorovi digrafa, a elementi relacije nose naziv grane digrafa. Kod digrafa
koji opisuje neku strukturu podataka, uobičajeno je da se čvorovi prikažu
pravougaonicima, a grane usmerenim linijama tako da se uređeni par (a,b) prikazuje
linijom sa strelicom okrenutom od elementa a ka elementu b. Na slici 1.1 prikazan je
digraf koji pripada nizu (x1,...,xn).

x1 x2 .... xn

Slika 1.1.

Napomenimo, na kraju, da paralelno sa strukturama podataka egzistira još jedan


srodan pojam, poznat svakom programeru - tip podataka. Radi se, u suštini, samo o
različitim modelima iste stvari. Dok je struktura podataka, kao model zasnovana na
topološkom pristupu - skupovima i relacijama - tip podataka se bazira na skupovima i
funkcijama.

56
Dušan T. Malbaški - Algoritmi i strukture podataka

1.2. OPERACIJE NAD STRUKTURAMA PODATAKA


Kao što je sugerisano u prethodnom odeljku, struktura podataka dobija smisao
samo u sprezi sa algoritmom, odnosno operacijama što se izvršavaju nad njom.
Štaviše, ako bi se iz razmatranja isključila ova operaciona komponenta, neke od
fundamentalnih struktura podataka ne bismo mogli da razlikujemo. Izvršavanje
operacije nad strukturom podataka u opštem slučaju izaziva promenu njenog stanja i
generiše rezultat (analogno operaciji u C-u koja ima bočni efekat).
Broj i vrsta operacija koje se mogu izvršavati nad strukturom podataka nisu
ograničeni - svaki program može se shvatiti kao globalna operacija nad globalnom
strukturom podataka - tako da bi se svaki pokušaj potpune klasifikacije operacija
neizostavno završio neuspehom. Umesto toga, izdvojićemo nekoliko karakterističnih
operacija koje će biti predmet razmatranja u sklopu opisa osnovnih struktura
podataka.
Prvu i najvažniju grupu čine osnovne operacije nad strukturama podataka. U
pitanju su četiri operacije: operacija pristupa, operacije uklanjanja i dodavanja i
najzad operacija provere da li je struktura podataka prazna (tj. da li je skup S iz
definicije 1.1 prazan skup).
Operacijom pristupa izdvaja se, markira, element strukture, a u svrhu
očitavanja ili izmene njegovog sadržaja. Da bi se pristupilo nekom elementu strukture
podataka neophodno je da postoji način zadavanja tog elementa. U odnosu na način
zadavanja, razlikujemo tri vrste pristupa:
1. Pristup prema poziciji; element kojem se pristupa određen je pozicijom u
strukturi podataka; tipičan slučaj je pristup elementu niza u kojem se pozicija
zadaje indeksiranjem. Inače, pozicija elementa kojem se pristupa može se
zadati eksplicitno što je upravo opisani slučaj niza; pozicija se može zadati i
implicitno, što znači da se podrazumeva, a tipičan slučaj je struktura sa
nazivom stek o kojoj će biti reči u nastavku. Treba podvući da pristup prema
poziciji ne postoji kod svake strukture podataka, jer postoje i takve strukture
kod kojih nije moguće definisati poziciju elementa.
2. Pristup prema ključu ili traženje pretpostavlja da se u okviru sadržaja elementa
nalazi podatak koji je unikatan za taj element. Takav podatak nosi naziv ključ
(recimo JMBG građanina il broj indeksa studenta). Traženje podrazumeva
zadavanje ključa elementa kojem se pristupa, a rezultat je ostvaren pristup ili

57
Dušan T. Malbaški - Algoritmi i strukture podataka

informacija da elementa sa takvim ključem nema u strukturi podataka. Za


razliku od pristupa prema poziciji, pristup prema ključu po pravilu zahteva
više od jednog pokušaja.
3. Posebna vrsta pristupa, često primenjivana kod struktura podataka na disku
jeste tzv. navigacija. Tehnika navigacije izgrađena je oko koncepcije tzv.
tekućeg elementa koji mora biti definisan u svakom trenutku (u suprotnom
struktura podataka nije operativna). Radi se o posebno označenom elementu
čija se adresa memoriše na posebnoj, pomoćnoj lokaciji. U okviru funkcije
pristupa podrazumeva se da se pristupa upravo tekućem elementu. Po pravilu,
strukture podataka snabdevene mehanizmom navigacije raspolažu operacijama
koje menjaju oznaku tekućeg elementa bez uticaja na sadržaj strukture
podataka. Tipičan slučaj jeste datotečni tip u C-u gde se funkcijama pristupa
fread i fwrite ne prosleđuju nikakvi podaci o elementu kojem se pristupa, jer
se podrazumeva pristup tekućem elementu (kojeg, inače, postavlja funkcija
fopen). Funkcije fseek i ftell služe za rukovanje tekućim elementom bez
izmene sadržaja datoteke.
Uklanjanje (brisanje, isključivanje) elementa je operacija koja je u čvrstoj vezi sa
pristupom, jer akciji uklanjanja mora da prethodi uspešan pristup. Napomenimo
odmah da postoje strukture podataka kod kojih ova operacija nije definisana (tzv.
statičke strukture), zatim strukture za koje, kao i kod pristupa, postoje restrikcije u
pogledu uklanjanja i, najzad, strukture kod kojih se ova operacija odvija bez
ograničenja. U svetlu definicije 1.1, uklanjanje elementa e iz strukture podataka
podrazumeva njegovo isključivanje iz skupa S, kao i isključivanje iz svih relacija
uređenih torki koje sadrže e. Pored toga, kod većine struktura uklanjanje elementa
zahteva i dodatne, završne radnje koje imaju za cilj uspostavljanje definicionih
osobina date strukture podataka ako su narušene brisanjem elementa e i pomenutih
torki.
Dodavanje elementa u strukturu podataka je, kao i uklanjanje, dozvoljeno
samo kod dela (doduše velikog dela) struktura podataka. Operacija podrazumeva
proširivanje skupa S novim elementom, dodavanje novih torki koje sadrže taj element
te, kao i kod uklanjanja, završne radnje koje treba da uspostave definicione osobine
strukture.
Kod svih struktura podataka kod kojih je definisana operacija uklanjanja, mora
postojati, eksplicitno ili ugrađena u druge operacije, operacija provere da li je

58
Dušan T. Malbaški - Algoritmi i strukture podataka

struktura podataka prazna. Naime, ako u datom trenutku u strukturi podataka nema
elemenata, svaki pokušaj pristupa i uklanjanja dovodi do greške te je, zbog toga,
neophodno raspolagati odgovarajućim sredstvom za predupređivanje takve situacije.
Kada je zadata eksplicitno, operacija se obično zove empty ili isEmpty.
Pored ovih, osnovnih, operacija navešćemo još neke karakteristične operacije.
To su
 Pretraživanje. Operacija kojom se izdvajaju svi elementi koji zadovoljavaju
zadati kriterijum; za razliku od traženja, rezultat pretraživanja može da bude
nijedan, jedan ili više elemenata; primer bi mogao da bude pretraživanje
strukture podataka sa podacima o automobila i kriterijum „boja je crna“.
 Određivanje veličine. Jednostavna operacija kojom se određuje broj
elemenata skupa S.
 Redosledna obrada. Obrada svih elemenata strukture podataka po nekom
redosledu. Primer: štampanje sadržaja neke datoteke.
 Sortiranje. Uređivanje elemenata strukture po nekom kriterijumu.
 Kopiranje sadržaja jedne strukture podataka u drugu.
 Spajanje dve ili više struktura podataka u jednu.
 Razlaganje strukture podataka na dve ili više.
S obzirom na to da se gotovo svaka operacija može realizovati na više načina, a u
svrhu poštovanja principa skrivanja informacija27, operacije se opisuju specifikacijom
baziranom obično na tzv. Horovim (Hoare28) tripletima. Horov triplet je, neformalno,
konstrukt oblika
PSQ
gde su P i Q predikati, a S segment kôda. P nosi naziv preduslov (engl. precondition),
a Q se zove postuslov (engl. postcondition). Konstrukt PSQ interpretiramo sa
„ako segment S počinje u trenutku kada je peduslov P tačan, po završetku segmenta
mora biti tačan i postuslov Q“. Pored preduslova i postuslova, specifikacija sadrži
prototip funkcije, opis parametara i opis rezultata. Na primer, specifikacija funkcije y
za računanje xln x izgledala bi ovako:

27
Princip skrivanja informacija (Information Hiding Principle, Parnas, 1972.). Princip po kojem treba
sakriti detalje realizacije jer oni ne smeju da utiču na klijentski softver.
28
C.A.R. Hoare (1934.-), jedan od pionira računastva

59
Dušan T. Malbaški - Algoritmi i strukture podataka

//prototip: double y(double x);


//parametri: x je argument funkcije
//preduslov: x>0
//postuslov: -
//rezultat: vrednost x ln x
Neformalno, crtica kod postuslova znači „nevažno“. Funkciju sort za sortiranje
celobrojnog niza a sa n elemenata specifikovali bismo na sledeći način:
//prototip: void sort(int a[],int n);
//parametri: a je niz, n je duzina niza
//preduslov: -
//postuslov: niz a je sortiran u neopadajucem poretku.
U skladu sa principom skrivanja informacija, svaka C funkcija koja zadovoljava
specifikaciju može biti prihvaćena kao realizacija. Umesto specifikacije date
odvojeno, u praksi se često funkcija (potprogram) snabdeva zaglavljem što predstavlja
komentar sa opisom preduslova, postuslova, parametara i rezultata. U nastavku ćemo
koristiti oba pristupa, osim kada je opis funkcije dat obimnijim tekstom.

1.3 KLASIFIKACIJA STRUKTURA PODATAKA


Strukture podataka klasifikuju se na više načina, od kojih izdvajamo sledeća
četiri:
 prema nivou apstrakcije
 prema mestu memorisanja
 prema tipu relacije
 prema ograničenjima u pogledu izvršavanja osnovnih operacija
 prema nameni
Prema nivou apstrakcije, razlikujemo logičke strukture podataka i njihovu
fizičku realizaciju (neki je nazivaju fizičkom strukturom podataka). Logička struktura
podataka jeste nivo apstrakcije koji je nezavisan od načina realizacije u programu.
Zasnovana je na pojmovima skupa, relacije i operacije te, očigledno, predstavlja
pojam matematičke prirode. Definicija 1.1 odnosi se upravo na logičku strukturu
podataka. Fizička realizacija ili fizička struktura podataka odgovara konkretnoj
programskoj realizaciji logičke strukture u memoriji. Odnos između logiške strukture
podataka i njene fizičke realizacije je „jedan prema više“, odnosno ista logička

60
Dušan T. Malbaški - Algoritmi i strukture podataka

struktura može se realizovati na više načina (i to po pravilu!). Primer za to smo već


imali, a to je matrica iz primera 1.1. U primeru su navedene čak tri realizacije iste
logičke strukture - double matrice sa v vrsta i k kolona.
U načelu, fizička realizacija strukture podataka može biti dvojaka:
sekvencijalna i spregnuta. Kod sekvencijalne realizacije za memorisanje se unapred
odvaja (neprekinuta) zona memorije u koju se smeštaju elementi, slika 1.2a. U odnosu
na spregnutu realizaciju, sekvencijalna ima tu prednost da se operacije izvršavaju
brže, često znatno brže. Nedostatak sekvencijalne realizacije leži u činjenici da je
zona memorije ograničenog kapaciteta, te da se ovakva struktura može napuniti, tako
da više nema mesta za dodavanje elemenata. Ovo, inače, zahteva da se sekvencijalno
realizovana struktura podataka koja ima operaciju dodavanja mora snabdeti
operacijom provere da li je memorija dodeljena strukturi popunjena (operacija se
obično zove full ili isFull).
Spregnuta fizička realizacija eliminiše problem prepunjenosti tako što se
elementi smeštaju na hip, a susedstvo elemenata postiže se sprezanjem, slika 1.2b.
Ako su u logičkoj strukturi elementi a i b susedni i a prethodi b, tada se u spregnutoj
realizaciji elementi a i b nalaze negde na hipu, a u okviru elementa a postoji
pokazivač ne element b. Dok s jedne strane spregnuta realizacija praktično isključuje
mogućnost prepunjenosti, s druge strane sve operacije se usporavaju zbog upravljanja
hipom.

(a)

(b)

Slika 1.2

Podvucimo, najzad, da postoji i kombinacija ovih dveju realizacija (sekvencijalno-


spregnuta) koja se pojavljuje kod jedne od osnovnih struktura podataka, tzv.
sekvence.

61
Dušan T. Malbaški - Algoritmi i strukture podataka

Prema mestu memorisanja strukture podataka dele se na strukture podataka


koje su u operativnoj memoriji, tzv. operativne strukture podataka i one koje se
nalaze na periferijskim memorijama (tipično - datoteke na disku) i koje se
tradicionalno zovu masovne strukture podataka. Radi se o strukturama podataka koje
se bitno razlikuju po svim parametrima, do te mere da su načini njihove upotrebe u
programu sasvim različiti. Prvo, operativne strukture čine organski deo programa, što
nije slučaj kod masovnih struktura. Životni vek operativnih struktura (promenljive
različitih tipova) sadržan je u životnom veku programa, tj. one nastaju zajedno sa
programom ili kasnije i nestaju zajedno sa programom ili ranije. Masovne strukture su
u tom smislu nezavisne: mogu se stvoriti iz programa ili uništiti iz programa ili ni
jedno ni drugo. Takođe, nad istom masovnom strukturom po pravilu operišu različiti
programi, tj. više njih. Ne treba zanemariti ni drugu razliku između ovih struktura, a
ona se ogleda u brzini pristupa koja je kod masovnih struktura manja za nekoliko
redova veličine. Konačno, masovne strukture predviđene su za memorisanje veće
količine podataka (otuda naziv „masovne“), mada je to jedina od tri navedene osobine
koja nije obavezno ispunjena.
Podela struktura podataka prema tipu relacije odnosi se na one strukture koje
se opisuju skupom elemenata i jednom, binarnom relacijom, dakle strukture oblika
(S,r). Prema ovom kriterijumu razlikujemo
 linearne strukture
 strukture tipa stabla i
 mrežne strukture.
Ovu klasifikaciju najlakše je objasniti posredstvom pripadajućih digrafa. Linearna
struktura je struktura kod koje svaki element osim tačno jednog ima tačno jednog
prethodnika i svaki element osim tačno jednog ima tačno jednog sledbenika.
Prikazana je na slici 1.3a. Linearna struktura ima i cikličku varijantu (slika 1.3b) kod
koje svaki element ima tačno jednog prethodnika i tačno jednog sledbenika.

(a)

(b)

Slika 1.3

62
Dušan T. Malbaški - Algoritmi i strukture podataka

Strukture tipa stabla, za razliku od linearnih, dozvoljavaju da elementi imaju i više od


jednog sledbenika. Tipično stablo prikazano je na slici 1.4. Na slici uočavamo da
stablo ima tačno jedan element bez prethodnika (tzv. „koren stabla“). Svi ostali imaju
tačno jednog prethodnika, dok broj sledbenika može biti 0 ili više. Strogu definiciju
stabla ostavićemo za odgovarajuće poglavlje.

Slika 1.4

Mrežne strukture podataka su najopštije po pitanju broja prethodnika i sledbenika


svakog elementa. Ni jedna ni druga stavka nisu ograničene, kao što se vidi sa slike
1.5.

Slika 1.5

Prema ograničenjima u pogledu izvršavanja osnovnih operacija strukture podataka


dele se na
 Statičke strukture kod kojih operacije uklanjanja i dodavanja nisu definisane,
tj. strukture kod kojih su i skup elemenata S i sve relacije nepromenljivi u
vremenu; provera da li je struktura prazna nije potrebna.
 Poludinamičke strukture kod kojih su sve četiri operacije definisane, ali u
pogledu pristupa, uklanjanja i dodavanja postoje ograničenja: nije moguć
pristup svakom elementu ili se ne može ukloniti svaki element ili se novi
element ne može dodati na bilo kojem mestu ili sve to zajedno.

63
Dušan T. Malbaški - Algoritmi i strukture podataka

 Dinamičke strukture podataka kod kojih nema posebnih ograničenja u pogledu


izvršavanja operacija pristupa, uklanjanja i dodavanja.
Prema nameni, strukture podataka klasifikujemo na
 kontejnerske strukture koje su namenjene samo za čuvanje podataka; glavna
karakteristika im je da je u svakom trenutku dozvoljen pristup svim podacima
 strukture specijalne namene

64
Dušan T. Malbaški - Algoritmi i strukture podataka

2. NIZ

Nizovi su, uz skalare, najpoznatije i najstarije strukture podataka, dobro


poznate svakom ko se susretao sa bilo kojim algoritamskim jezikom. Pod imenom
„višedimenzionalne promenljive“ pojavljuju se već u najstarijem višem programskom
jeziku, fortranu (1957) i od tada se intenzivno koriste u gotovo svakom programu. U
starim programskim jezicima, višedimenzionalne promenljive sa jednom, dve ili više
dimenzija tretirale su se kao različite familije struktura podataka, prvenstveno
zahvaljujući činjenici da je preovlađujuća oblast primene računara u to vreme bilo
matematičko modelovanje, tj. numerička analiza. Višedimenzionalne promenljive
sastojale su se od skalarnih elemenata kojima se pristupalo pomoću jednog, dva ili
više indeksa, baš kao što je to slučaj kod matematičkih struktura vektora (jedan
indeks) ili matrice (dva indeksa).
Pojavom novih struktura podataka (npr. sloga) i njihovim uvrštavanjem u
programske jezike, došlo je do promene pogleda na višedimenzionalne strukture.
Naime, ako element višedimenzionalne promenljive ne mora obavezno da bude
skalar, nego može da bude proizvoljnog tipa (npr. slog), sledi logično pitanje „zašto
matrica ne bi bila jednodimenzionalna promenljiva čiji su elementi opet
jednodimenzionalne promenljive, a trodimenzionalna promenljiva bila
jednodimenzionalna promenljiva čiji su elementi matrice“? Na taj način, došlo se do
jedinstvene (jednodimenzionalne) strukture koja nosi naziv - niz (engl. array).
Osnovna osobina svih nizova je da raspolažu mehanizmom indeksiranja, tj.
pristupa elementu preliminarnim izračunavanjem njegove relativne adrese u okviru
niza. Druga osnovna osobina, bez koje indeksiranje ne bi bilo moguće, jeste
homogenost koja znači da su svi elementi niza istog tipa što - a to je, ustvari, važno -
podrazumeva da svaki od njih zauzima memorijsku lokaciju iste dužine.
Logičku strukturu niza definisaćemo kao uređeni par A = (S(A),r(A)) gde je S
skup elemenata, a r binarna relacija, sa sledećim osobinama:
1. struktura A je linearna i homogena tj. elementi skupa S(A) su istovrsni
2. dozvoljen je pristup svakom elementu i to indeksiranjem
3. operacije dodavanja i uklanjanja mogu, ali ne moraju biti definisane.

65
Dušan T. Malbaški - Algoritmi i strukture podataka

Nizovi kod kojih operacije uklanjanja i dodavanja nisu definisane nose naziv statički
nizovi, a oni kod kojih jesu zovu se dinamički nizovi. S obzirom na činjenicu da se
statički i dinamički nizovi različito realizuju, treba biti u stanju da se prepozna o kojoj
se od dve vrste nizova radi. Ovo se postiže analizom programa u kojem se dati niz
koristi. Naime, odluka o tome da li neku strukturu podataka u programu treba
realizovati kao statički ili kao dinamički niz donosi se na bazi sledećeg pravila:
 ako se u toku izvršenja programa broj elemenata niza ne menja, tada je u
pitanju statički niz; u suprotnom, radi se o dinamičkom nizu.
Inače, nizovi koji odgovaraju višedimenzionalnim promenljivim iz starih programskih
jezika nisu izgubili na značaju: oni se i dalje intenzivno koriste, samo što se u
modernim jezicima tretiraju kao podvrsta nizova koju zovemo multiindeksne
strukture. Njih definišemo rekurzivno, na bazi niza skalara koji se zove vektor:
1. Vektor je multiindeksna struktura reda 1.
2. Multiindeksna struktura reda k >1 je niz Nk=(S(Nk),r(Nk)) u kojem su elementi
skupa S(Nk) multiindeksne strukture reda k-1.
S obzirom na čestu primenu, pored multiindeksnih struktura reda 1 (vektora) i
multiindeksne strukture reda 2 imaju posebno ime - matrice.
Glavna operacija kod nizova je operacija pristupa. U principu, moguće je
realizovati sve tri vrste pristupa - pristup prema poziciji, traženje, pa i navigaciju - s
tim što navigacija ima manji značaj od prve dve. Pristup se u svim slučajevima
obavlja osloncem na mehanizam indeksiranja. U logičkom smislu, indeks je neka
vrednost iz linearno uređenog, konačnog skupa indeksa I. Ako je iI vrednost
indeksa, tada se u C-u operacija indeksiranja niza a realizuje izrazom
ai
koji obezbeđuje pristup elementu niza a (levi operand) čiji je indeks i (desni operand).
Uočimo da se, uopšte uzev, indeks ne poklapa sa pozicijom. Naime, pozicija u nizu je
uvek 1, 2, 3, ..., dok indeks u C-u može biti i 0, a u paskalu čak i negativan. Ono što je
bitno jeste da između skupa indeksa i skupa pozicija mora postojati biunivoko
(uzajamno jednoznačno) preslikavanje koje indeks direktno preslikava na poziciju, i
obrnuto. Pretpostavimo da je skup indeksa nekog niza x dat sa p,p+1,...,k, gde je p
najmanja, a k najveća vrednosti indeksa. Tada se operacija indeksiranja xi izvršava
preliminarnim računanjem adrese elementa formulom
adr(x[i]) = adr(x[p]) + (i-p)L ..... (2.1)

66
Dušan T. Malbaški - Algoritmi i strukture podataka

gde funkcija adr iznačava adresu, a L jeste dužina svakog elementa niza u
bajtovima29. Iz formule direktno sledi odgovor na pitanje zašto je minimalna vrednost
indeksa u C-u uvek 0? Kako vidimo, u opštem slučaju, svako indeksiranje zahteva tri
operacije: sabiranje, oduzimanje (p od i) i množenje. U slučaju da je p=0 oduzimanja
više nema i indeksiranje se ubrzava, što je kod C-a od najveće važnosti. Dakle,
indeksiranje C-ovog niza T aduzina realizuje se formulom
adr(a[i]) = adr(a[0]) + i*sizeof(T)
u kojoj se može zapaziti još jedna pojedinost: mehanizam indeksiranja ne zahteva
poznavanje dužine niza i to je razlog zbog kojeg prilikom prenosa niza kao argumenta
u funkciju, ni u jednom programskom jeziku ne treba navoditi njegovu dužinu.

2.1. FIZIČKA REALIZACIJA NIZA


Videli smo da je indeksiranje osnovni mehanizam za rukovanje nizovima, bez
obzira na programski jezik. S druge strane, jedini način da se obezbedi da formula
(2.1) bude primenljiva jeste sekvencijalna realizacija. Stoga se svi nizovi, statički i
dinamički, realizuju sekvencijalno, tj. odvaja se kompaktna zona memorije i u nju se
smeštaju elementi tako da se relacija susedstva u logičkoj strukturi realizuje
smeštanjem susednih elemenata na susedne lokacije.

2.1.1. Fizička realizacija statičkog niza


Statički niz se može realizovati na dva načina: standardno, korišćenjem
sredstava programskog jezika (tj. korišćenjem tipa niza) ili na hipu. Preovladava prvi
način, osim u situacijama kada je niz prevelik, kao što je to diskutovano u primeru
1.1. U C-u, standardnim načinom niz se definiše iskazom
tip imeduzina;
gde je duzina konstantni izraz na osnovu kojeg kompajler (dakle, u fazi prevođenja)
rezerviše memorijski prostor za niz. Stvarna dužina niza (nepromenljiva u toku jednog
izvršenja programa!) ne može biti veća od rezervisanog memorijskog prostora, što je
osigurano činjenicom da operacije uklanjanja i dodavanja koje menjaju dužinu niza
kod statičkih nizova nisu definisane. Elementima niza pristupa se indeksiranjem, tj.
izrazom oblika
imeindeks

29
upravo primena ove formule jeste razlog zbog kojeg svi elementi istog niza moraju biti iste dužine

67
Dušan T. Malbaški - Algoritmi i strukture podataka

pri čemu je stvar prevodioca da obezbedi primenu formule 2.1.


Drugi način jeste realizacija statičkog niza na hipu, primenom mehanizma
dinamičke dodele memorije. U C-u30 to se postiže korišćenjem pokazivača i funkcija
malloc i free za upravljanje hipom. Kod statičkih nizova, ovaj postupak omogućuje da
niz zauzima tačno onoliko memorije koliko ima elemenata, tj. da nema viška
memorijskog prostora, što je slučaj kod prethodnog postupka. Statički niz a tipa T i
dužine n (gde n može da bude po tipu i promenljiva) realizuje se putem pokazivača:
T *a; int n;
.................
a = malloc(n*sizeof(T));
a elementima se pristupa indeksnim izrazom oblika ai gde je i indeks. Ne treba
zaboraviti da se blok na hipu mora osloboditi, kada niz a više nije potreban, pozivom
free(a).

2.1.2. Metoda linearizacije


Metoda linearizacije ("pakovanje") jeste specijalna metoda za realizaciju
statičkih multiindeksnih struktura koncipirana tako da se omogući preliminarno
računanje adrese elementa posebnom formulom koja je uopštenje formule (2.1).
Primenjuje se kod standardne izvedbe nizova, što će reći da je primenjuje kompajler.
Inače, zastupljena je kod svih procedurnih programskih jezika, čak i kod fortrana. Cilj
je, naravno ubrzati pristup elementima multiindeksne strukture. Posmatrajmo matricu
y sa MAX_V vrsta i MAX_K kolona čiji su elementi tipa T. Metoda linearizacije
predviđa da se matrica y smešta u (jednodimenzionalnu!) operativnu memoriju tako
što se seče po vrstama, odnosno upisuje u susedne blokove memorije vrsta po vrsta.

y[0] y[1] ....... y[MAX_V-1]

Slika 2.1.

Pošto je ukupna dužina u bajtovima jedne vrste jednaka MAX_K*sizeof(T), lako je


pokazati da se adresa elementa matrice y[i][j] može direktno izračunati
linearizacionom formulom
adr(y[i][j]) = adr(y[0][0]) + (i*MAX_K+j)*sizeof(T)

30
i veoma slično u paskalu

68
Dušan T. Malbaški - Algoritmi i strukture podataka

gde je T tip elemenata matrice y. I ovde se može zapaziti da maksimalni broj vrsta
MAX_V ne učestvuje u računanju adrese, i to je razlog zbog kojeg pri prenosu
standardno realizovane matrice u funkciju nije potrebno navoditi prvu dimenziju, ali
drugu već jeste (i to važi za sve programske jezike)!
Uopštimo ovu formulu na mulitindeksnu strukturu M sa n indeksa i dozvolimo
da se svaki indeks i kreće u rasponu pi do ki gde je pi najmanja vrednost i-tog indeksa
(u C-u je 0), a ki njegova najveća vrednost. Ako je L veličina svakog elementa u
bajtovima, adresa elementa M[j1]...[jn] se direktno izračunava po formuli
n
adr(M[j1]...[jn]) = adr(M[p1]...[pn]) + L*  ( j m  p m)D m
m 1

U slučaju da se multiindeksna struktura seče po horizontali (u slučaju matrice po


vrstama31), faktor Dm računa se rekurzivno po formuli
Dm = (km+1-pm+1+1)Dm+1 Dn=1, m=n-1,n-2,...,1
Ako se multiindeksna struktura seče po vertikali32 (kod matrice to bi bile kolone)
faktor Dm računa se kao
Dm = (km-1-pm-1+1)Dm-1 D1=1, m=2,3,...,n
Napomenimo, na kraju, da je linearizacija u programskom jeziku C obavezna, tj. čim
definišemo neku matricu p standardnom definicijom, npr.
double p[20][30];
ona će u memoriju biti smeštena shodno šemi na slici 2.1, a indeksirani izraz p[i][j]
računaće se po formuli
adr(p[i][j]) = adr(p[0][0]) + (i*30+j)*sizeof(double)

2.1.3. Fizička realizacija dinamičkog niza


Dinamički niz moguće je realizovati samo na hipu i to zato što je neophodno
obezbediti istovremeno i promenljivu dužinu tokom izvršenja programa i primenu
indeksiranja. Neka je d dinamički niz tipa T. Zbog potrebe dodavanja elemenata na
hipu se rezerviše blok koji je veći nego što niz u datom trenutku zahteva. Neka je B
veličina tog bloka izražena brojem elemenata niza. Dakle, niz d realizovao bi se
inicijalno na sledeći način:
T *d; int B;

31
tako je u C-u i u paskalu
32
tako je u fortranu

69
Dušan T. Malbaški - Algoritmi i strukture podataka

d = malloc(B*sizeof(T));
i popunjavao redom od lokacije d[0] do dn-1 gde je n aktuelna dužina niza. Stanje je
prikazano na slici 2.2.

d
d[0] d[1] ... d[n-1]

Slika 2.2.

Da bi se održala mogućnost indeksiranja, prilikom dodavanja na nekom mestu niza


koje nije na kraju, elementi što će slediti novododati element moraju prethodno biti
pomereni za po jedno mesto prema kraju niza, kao što je prikazano na slici 2.3a.

d
... (a)

d
... (b)

Slika 2.3.

Iz istog razloga, pri uklanjanju elementa njegovi sledbenici moraju se pomeriti za po


jedno mesto ka početku memorijskog prostora, slika 2.3b.
Postoji još jedan problem vezan za fizičku realizaciju dinamičkog niza.
Naime, primarni memorijski blok zauzet funkcijom malloc može da se popuni
primenom operacije dodavanja. Kada se čitav blok popuni, sledeći zahtev doveo bi
niz u stanje prepunjenosti (engl. overflow) i program bi se morao zaustaviti. Da se to
ne bi desilo (jer ne sme da se desi!), pri pokušaju dodavanja u pun niz njegov
memorijski prostor mora se povećati. Pritom, nije dobra praksa povećavati za samo
jednu lokaciju, jer realokacija traje dugo i u slučaju sukcesivnog dodavanja u pun niz
nepotrebno bi se trošilo vreme. Umesto toga, memorija se odmah povećava za neki
segment deltaB (izražen, recimo, brojem elemenata) koji može da primi više
elemenata, tako da se sledeća eventualna realokacija odlaže. U C-u povećanje
memorijskog bloka za niz postiže se funkcijom realloc, dakle
realloc(d,(B+=deltaB)*sizeof(T));
Sledeći segment dodaće vrednost value u dinamički niz d dužine n, na mestu i:

70
Dušan T. Malbaški - Algoritmi i strukture podataka

int j;
if(n==B) realloc(d,(B+=deltaB)*sizeof(T));
for(j=n++;j>i;j--) d[j]=d[j-1];
d[i] = value;
Sledeći segment ukloniće iz niza element na indeksu i:
int j;
for(j=i,n--;j<n;j++) d[j]=d[j+1];
Posle izvesnog broja uklanjanja blok koji je dodeljen nizu može postati slabo
popunjen. Da bi se memorijom upravljalo efikasno, u slučajevima kada popunjenost
memorijskog prostora padne ispod neke granice, blok se može smanjiti (ovo, inače,
nije obavezno, za razliku od proširenja):
realloc(d,(B-=deltaB)*sizeof(T));

2.2. SORTIRANJE NIZA


Malo je algoritamskih problema koji su tako temeljito razmatrani kao što je
sortiranje linearnih struktura podataka, prvenstveno nizova i listi. Nekoliko generacija
programera oštrilo je imaginaciju na tom problemu, te kao rezultat imamo na desetine
postupaka među kojima ima i vrlo sofistikovanih. Razlog je, barem u početku, bila
relativna dugotrajnost postupka (zbog sporosti računara). Kasnije, kada je brzina
sortiranja prešla u drugi plan ostao je još jedan motiv: relativna kompleksnost
problema, dovoljna da predstavlja izazov i ne toliko velika da bi zahtevala obimna,
nezanimljiva i nerazumljiva rešenja.
Prvo, pod sortiranjem linearne strukture podrazumeva se preuređivanje njenih
elemenata u skladu sa zadatim kriterijumom. Taj kriterijum obično se vezuje bilo za
vrednosti elemenata, ako su skalarni, bilo za vrednost ključa, ako postoji. Bez
gubljenja na opštosti, koncentrisaćemo se na problem sortiranja niza33. Pošto metode
ne zavise od toga da li se niz sortira po vrednosti elemenata ili po ključu
posmatraćemo slučaj celobrojnog niza a=a0, a1, ... an-1 čija je dužina n. Zadatak će biti
da se elementi preurede (tj. sortiraju) u neopadajućem poretku. U okviru ovog odeljka,
razmotrićemo četiri najpoznatije metode, tri jednostavne, razumljive i nešto manje
kvalitetne i još jednu, čuveni quicksort, kojom se pročuo jedan od pionira računarstva
C.A.R. Hoare još 1962.

33
Liste, o kojima će kasnije biti reči, sortiraju se praktično istim metodama.

71
Dušan T. Malbaški - Algoritmi i strukture podataka

2.2.1. Metoda izbora (Selection Sort)


Metoda izbora spada u najstarije metode sortiranja. Zasnovana je na
jednostavnom postupku sukcesivnog određivanja minimalnog elementa u
podnizovima niza a. Posmatrajmo celobrojni niz oblika
4 9 8 2 6
Prolaskom kroz ceo niz odredićemo njegov najmanji element, a to je element 2 na
indeksu 3. Pošto je on najmanji u nizu, mesto mu je na početku, pa ćemo izvršiti
međusobnu zamenu tog elementa i elementa na indeksu 0 (element 4). Dolazimo do
niza
2 9 8 4 6
Element 2 je sada na svom mestu, te ga više ne obrađujemo. U sledećem prolazu
posmatramo podniz koji počinje na indeksu 1
9 8 4 6
i na njega primenjujemo isto postupak. Kako je u ovom podnizu najmanji element 4
on će zameniti mesta sa elementom 9 na najmanjem indeksu u podnizu. Ukupni
rezultat biće
2 4 8 9 6
gde su sada prva dva elementa na svom mestu i dalje se ne obrađuju. Dalje bismo
nastavili sa podnizom 8 9 6 koji počinje na indeksu 2 i ponovili postupak. Postupak
se završava kada je podniz u kojem se određuje minimum sveden na jedan element.

4 9 8 2 6

2 9 8 4 6

2 4 8 9 6

2 4 6 9 8

2 4 6 8 9
Slika 2.4.

72
Dušan T. Malbaški - Algoritmi i strukture podataka

Postupak je prikazan na slici 2.4. U opštem slučaju, algoritam se može prikazati


sledećim stavkama:
 u svakom prolazu posmatra se podniz ai, ai+1,...,an-1, i=0,1,...,n-1
 u podnizu se određuje najmanji element i on menja mesto sa elementom ai.
Odgovarajuća funkcija na C-u izgleda ovako:
void selectionSort(int a[],int n) {
int i,j,ndxmin,tmp;
for(i=0;i<n;i++) {
//odredjivanje indeksa ndxmin najmanjeg u podnizu
for(ndxmin=i,j=i+1;j<n;j++) if(a[ndxmin]>a[j]) ndxmin=j;
//zamena mesta a[i] i a[ndxmin], ako se ne poklapaju
if(i!=ndxmin) {tmp=a[ndxmin];a[ndxmin]=a[i];a[i]=tmp;}
}
}
Složenost algoritama za sortiranje obično se meri brojem upoređenja u zavisnosti od
dužine niza. Lako je uveriti se da u prvom prolazu (kada se traži najmanji element u
celom nizu), broj poređenja iznosi n-1. U drugom prolazu podniz se skraćuje za 1 te je
broj poređenja n-2, i tako dalje, do dužine podniza 1. Prema tome, ukupni broj
poređenja iznosi
Usel(n) = 1+2+3+...+n-1 = n(n-1)/2 ≈ n2/2
Pod pretpostavkom da je vremenska složenost Tsel(n) proporcionalna broju poređenja,
sledi da je
Tsel = On2
što znači da je funkcija složenosti algoritma sortiranja metodom izbora reda n2.

2.2.2. Metoda izmene (Standard Exchange Sort, Bubble Sort)


Metoda izmene je približno istog kvaliteta kao i prethodna. Zasnovana je na
sledećoj iterativnoj šemi:
 u svakom prolazu posmatra se podniz a0,a1,...,ai, i=n-1,n-2,...,1, pri čemu se
počinje od celog niza
 upoređuju se parovi aj i aj+1, (j=0,...,i-1); ako nisu u dobrom poretku
razmenjuju mesta

73
Dušan T. Malbaški - Algoritmi i strukture podataka

 na kraju prolaza i se smanjuje (tj. podniz se skraćuje) za 1


 postupak se završava ili kada je i=0 ili ako u jednom prolazu nije bilo ni jedne
zamene mesta
Posmatrajmo ponovo niz
4 9 8 2 6
U prvom prolazu porede se 4 i 9 (nulti i prvi element niza). Pošto su u dobrom
poretku odmah se prelazi na sledeći par, 9 i 8 (prvi i drugi element). Oni nisu dobro
raspoređeni, te menjaju mesta generišući rezultat
4 8 9 2 6
Sada se upoređuje sledeći par, tj. elementi sa indeksima 2 i 3. Ni oni nisu u dobrom
redosledu te menjaju mesta
4 8 2 9 6
Konačno, upoređuju se elementi sa indeksima 3 i 4 te, kako nisu u dobrom redosledu,
i oni menjaju mesta, čime se prolaz završava. Na kraju prolaza stanje je sledeće:
4 8 2 6 9
Lako je uočiti da se u završnom stanju posle jednog prolaza, na poslednjem mestu
obavezno pojavljuje najveći element, što znači da jedan prolaz rezultuje stanjem u
kojem je najveći element podniza na svom mestu. Ovo podseća na isplivavanje
mehura na vrh posude sa penušavom tečnošću i otuda opštepoznati naziv za ovu
metodu - bubble sort, od reči bubble koja znači "mehur". Pošto je najveći element na
svom (poslednjem) mestu u sledećem prolazu ga isključujemo i gornji postupak
ponavljamo za podniz
4 8 2 6
Na kraju drugog prolaza niz će biti transformisan u
4 2 6 8
gde je ponovo najveći element (8) na svom mestu. Podniz se sada skraćuje na
4 2 6
i postupak ponavlja. Sortiranje se završava ili time što se podniz skraćuje na jedan
element, ali i ako u toku jednog prolaza ne dođe do izmene, jer su u tom slučaju
elementi podniza već u odgovarajućem poretku koji se više ne bi menjao. Uzimajući u
obzir oba kriterijuma za završetak, funkcija za sortiranje izgledala bi ovako:
void bubbleSort(int a[],int n) {
int i,j,chn,tmp; //chn=0: nije bilo zamena u prolazu, zavrsiti

74
Dušan T. Malbaški - Algoritmi i strukture podataka

for(chn=1,i=n-1;chn&&(i>0);i--)
for(j=chn=0;j<i;j++)
//los redosled? da: izmena mesta
if(a[j]>a[j+1]) {tmp=a[j];a[j]=a[j+1];a[j+1]=tmp;chn=1;}
}
Promenljiva chn u funkciji je, ustvari, indikator postojanja izmena. Kako se vidi, ona
dobija vrednost 1 ako se izvrši bar jedna izmena mesta; ukoliko chn ima vrednost 0 na
kraju nekog prolaza, to je znak da izmena nije bilo i da procedura treba da se završi.
Broj poređenja u jednom prolazu za podniz dužine i iznosi i-1. Prema tome,
ukupni broj poređenja dobija se kao
Ububble(n) = (n-1)+(n-2)+...+2+1 = n(n-1)/2 ≈ n2/2
odakle, uz pretpostavku da je vremenska složenost srazmerna broju upoređivanja,
sledi da je funkcija složenosti Tbubble(n)=On2, odnosno da je reda n2, kao i kod
metode izbora. Metoda izmene poseduje izvesnu prednost u odnosu na prethodnu, jer
se može završiti i pre nego što se obave svi prolazi, u slučaju da u jednom prolazu nije
bilo izmena. Štaviše, u posebnom slučaju, kada je niz već sortiran, postupak se
završava posle samo jednog prolaza, odnosno posle n-1 poređenja. Zaključak je da se,
po pitanju složenosti, metode izbora i izmene razlikuju utoliko što je broj poređenja
kod metode izmene u najgorem slučaju jednak broju kod metode izbora. Red funkcije
složenosti kod metode izmene je, dakle, negde između O[n2] i O[n] no, da ne bi bilo
nesporazuma, moramo napomenuti da, u konkretnim situacijama, razlika između ove
dve metode nije preterano impresivna.

2.2.3. Metoda umetanja (Insertion Sort)


Metoda umetanja ima više varijanata među kojima je najpoznatija (mada ne i
najbolja) tzv. metoda linearnog umetanja. Ideja je u tome da se u svakom prolazu
uočava elemet ai sa indeksom i (i=1,2,...,n-1) i da se poredi redom sa prethodnicima.
Sve dok je element ai manji od prethodnika oni menjaju mesta. Drugim rečima,
element ai se potiskuje unazad sve dok se ne naiđe na manji. S obzirom na to da se
polazi od drugog po redu elementa (elementa sa indeksom 1) u svakom trenutku niz je
podeljen na dva dela: prvi deo (niži indeksi) sadrži već sortirane elemente, dok se u
drugom delu nalaze elementi koje tek treba potisnuti. Postupak se završava kada se
stigne do elementa sa indeksom n-1. Neka je niz koji treba sortirati oblika
4 9 8 2 6

75
Dušan T. Malbaški - Algoritmi i strukture podataka

Počinje se sa elementom na indeksu 1, tj. sa 9. On se poredi sa prethodnikom te, pošto


nije manji, ne dolazi do izmene mesta i prolaz je završen. Sledeći element je 8. On se
poredi sa elementom 9 te, kako nisu u dobrom poretku, menja sa njim mesta, tako da
niz dobija oblik
4 8 9 2 6
Uočimo da je na kraju prolaza deo niza 4 8 9 sortiran, dok se drugi, nesortirani deo
sastoji od elemenata 2 i 6. Sledeći element je 2. On se prvo poredi sa 9 i menja sa njim
mesta dajući niz
4 8 2 9 6
Element 2 se ponovo poredi sa prethodnikom i menja sa njim mesta:
4 2 8 9 6
Sledi novo poređenje elementa 2 sa prethodnikom i ponovna razmena mesta
2 4 8 9 6
čime je prolaz završen. Ostaje još poslednji element 6. Poređenjem sa prethodnicima
on će biti potisnut između elemenata 4 i 8 dajući sortiran niz
2 4 6 8 9
Funkcija za sortiranje ima sledeći oblik:
void insertionSort(int a[],int n) {
int i,j,tmp;
for(i=1;i<n;i++)
//potiskivanje elementa na korektnu poziciju
for(j=i;(j>0)&&(a[j]<a[j-1]);j--) {tmp=a[j];a[j]=a[j-1];a[j-1]=tmp;}
}
Analiza složenosti u ovom slučaju nešto je komplikovanija jer broj poređenja
elementa koji se potiskuje sa njegovim prethodnicima nije konstantan, pošto se
završava kada se naiđe na prvi manji. Zato ćemo poći od pretpostavke da je prilikom
potiskivanja elementa sa indeksom i unazad verovatnoća da se naiđe na manji element
sve vreme ista. Pošto deo niza kroz koji se dati element potiskuje ima dužinu i, lako je
izračunati da je srednji broj poređenja pri nalaženju mesta za potiskivani element
jednak (i+1)/234, te sledi da je, u proseku,
n -1
Uins = 1/2  ( i  1)
i 1

34
videti odeljak o linearnom traženju u tabelama

76
Dušan T. Malbaški - Algoritmi i strukture podataka

odnosno
Uins = (n-1)(n+2)/4 ≈ n2/4
Shodno tome, ponovo se susrećemo sa algoritmom čija je funkcija složenosti
Tins(n)=On2 reda n2. Ipak, kada se uporede izrazi za broj poređenja ove i prethodne
dve metode, zapaža se da je metoda umetanja po broju poređenja u proseku dvaput
brža od prethodnih, što svakako nije zanemarljivo.

2.2.4. Quicksort
Ovu metodu formulisao je C.A.R. Hoare godine 1962. i time se (mada ne
samo time) kvalifikovao za ulazak u sve enciklopedije računarstva. Quicksort, za
razliku od prethodnih metoda, spada u sofistikovane metode sa funkcijom složenosti
koja je manjeg reda od On2, što je uvrštava u grupu najbržih metoda za sortiranje. Iz
osnovne metode razvijen je čitav niz podvarijanata, tako da se Quicksort može
smatrati familijom metoda. U osnovi, ideja je jednostavna: uočava se jedan element
niza, takozvani pivot. Niz se preuređuje tako što se deli na dva dela: prvi deo sadrži
elemente manje od pivota, a drugi elemente koji su veći. Posmatrajmo niz
4 1 6 7 3 9 2
Najjednostavniji postupak za izbor pivota jeste proglasiti za pivot prvi član niza.
Dakle, za pivot biramo element 4. Postupkom koji ćemo naknadno objasniti niz se
preuređuje u
2 1 3 4 6 9 7
gde se uočava da se levo od pivota 4 nalaze elementi koji su manji od njega, a desno
oni koji su veći. Napominjemo da uopšte nije obavezno da podnizovi levo i desno od
pivota budu iste dužine. Štaviše, u ekstremnim slučajevima moglo bi da se dogodi da
je pivot na prvom ili na poslednjem mestu! U nastavku, primenjuje se isti postupak
deljenja na oba ovako dobijena podniza, što ukazuje na činjenicu da programska
realizacija mora biti bazirana na rekurziji.
Algoritama za preuređivanje podniza u skladu s napred navedenim uslovima
ima više. Pošto su praktično istog kvaliteta, prikazaćemo samo jedan (prema 8).
Neka je
4 1 6 7 3 9 2
podniz dobijen u nekoj fazi deljenja originalnog niza a. Neka je podniz omeđen
indeksima L i R, što znači da je indeks elementa 4 u ulaznom nizu jednak L, a indeks
elementa 2 jednak R.

77
Dušan T. Malbaški - Algoritmi i strukture podataka

U prvom koraku bira se pivot. Najjednostavnija varijanta je da se za pivot (u


oznaci G) odabere prvi element podniza, aL.
L,M R
G=4
4 1 6 7 3 9 2
i
L M R
4 1 6 7 3 9 2
i
L M R
4 1 6 7 3 9 2
i
L M R
4 1 6 7 3 9 2
i
L M R
4 1 3 7 6 9 2
i
L M R
4 1 3 7 6 9 2
i
L M R
4 1 3 2 6 9 7
i

L M R
2 1 3 4 6 9 7

Slika 2.4.

Tokom svakog prolaza, element sa indeksom i (i=L+1,L+2,...,R), poredi se sa


pivotom G=4. Indeks M u svakom trenutku definiše granicu između levog podniza
(elementi manji od pivota) i desnog podniza (elementi veći od pivota ili jednaki
njemu). Prvo se, dakle, element ai=1 poredi sa pivotom. Kako je ovaj element manji
od pivota, mesto mu je u levom podnizu, a smešta se tako što se granica M pomera za
1 i element na granici menja mesto sa posmatranim. U ovom slučaju, radi se o istom
elementu te je zamena mesta trivijalna (element menja mesto sa samim sobom).
Prelazi se na sledeći element, 6. Pošto je on veći od pivota, treba da ostane sa desne

78
Dušan T. Malbaški - Algoritmi i strukture podataka

strane granice M, te ne dolazi ni do kakvih izmena. Isto se dešava i sa sledećim


elementom 7. Sledeći element 3 manji je od pivota i nalazi se sa pogrešne strane
granice M. Stoga se M povećava za 1 i element 3 menja mesto sa elementom na
indeksu M, tj. elementom 6. Element 9 je na dobroj strani, tako da se dolazi do
poslednjeg elementa 2 na indeksu R. Pošto se on nalazi na pogrešnoj strani granice M,
vrednost M se povećava za 1 i element 2 menja mesto sa elementom na tom indeksu,
tj. elementom 7. Pošto više nema nepregledanih elemenata, prolaz je završen.
Poslednja stvar koju treba uraditi jeste da element na indeksu M i element na indeksu
L (pivot) međusobno zamene mesta. Lako se uočava da je na kraju prolaza podniz
podeljen na dva dela: u jednom, levo od pivota (L do M-1), nalaze se elementi koji su
manji od njega, a desno (M+1 do R) nalaze se elementi koji su veći od pivota.
Programski segment koji izvršava preuređenje podniza aL,...,aR ima sledeći
izgled:
for(M=L,G=a[L],i=L+1;i<=R;i++)
if(a[i]<G){tmp=a[++M];a[M]=a[i];a[i]=tmp;}
//zamena mesta aM i pivota aL:
tmp=a[M];a[M]=a[L];a[L]=tmp;
U nastavku se primenjuje identičan postupak za oba novoformirana podniza,
aL...aM-1 i aM+1...aR. Ovo se programski realizuje kao rekurzivni poziv iste
funkcije za preuređenje. Postupak se završava kada se dužine svih podnizova svedu na
jedan. Kompletan postupak ima sledeći oblik:
static int _tmp,_G; //da se ne bi zauzimao stek
void _qsort(int a[],int L,int R) {
int M; register int i;
if(L>=R) return;
for(M=L,_G=a[L],i=L+1;i<=R;i++)
if(a[i]<_G){_tmp=a[++M];a[M]=a[i];a[i]=_tmp;}
_tmp=a[M];a[M]=a[L];a[L]=_tmp;
_qsort(a,L,M-1); _qsort(a,M+1,R);
}
void quickSort(int a[],int n) {
_qsort(a,0,n-1);
}

79
Dušan T. Malbaški - Algoritmi i strukture podataka

gde pomoćna funkcija _qsort vrši preuređenje niza. Promenljive _G koja sadrži
vrednost pivota prilikom preuređenja i _tmp koja posreduje pri zameni mesta,
realizovane su kao statičke globalne promenljive da se ne bi opterećivao stek pri
rekurzivnim pozivima.
Analiza kompleksnosti metode quicksort nešto je složenija nego u prethodnim
slučajevima. Dajemo verziju iz 10. Neka je T(n) vremenska kompleksnost sortiranja
niza sa n elemenata. Jedno preuređenje deli taj niz na dva dela: prvi ima k elemenata,
a drugi n-k elemenata, pri čemu se zanemaruje činjenica da pivot ne ulazi u
podnizove, jer to ne utiče na krajnji rezultat i to stoga što nas interesuje samo red
funkcije složenosti. Pošto jedan prolaz kojim se niz preuređuje ima linearnu funkciju
složenosti oblika cn gde je c konstanta, važi
T(n) = T(k) + T(n-k) + cn
Najgori slučaj jeste slučaj u kojem se za pivot uvek bira najmanji element, tako da su
podnizovi dužine 1 odnosno n-1. Najbolji slučaj je slučaj kada je pivot, po veličini,
uvek u sredini niza (tj. predstavlja medijanu) jer su tada dužine oba podniza prilbližno
n/2. U najgorem slučaju imamo
T(n) = T(1)+T(n-1)+cn
= (T(n-2)+T(1)+c(n-1))+T(1)+cn
= T(n-2)+2T(1)+c(n-1+n)
= T(n-3)+3T(1)+c(n-2+n-1+n)
Lako se dokazuje da važi
n -2
T(n) = nT(1) + c  (n - j) = nT(1) + c(n(n-2)-(n-2)(n-1)/2)
j 0

odnosno T(n) = O[n2]. Dakle, tek u najgorem slučaju quicksort se ponaša kao
prethodne metode izbora, izmene i umetanja. U najboljem slučaju pak, kada svako
preuređenje deli niz na dva jednaka dela, važi
T(n) = 2T(n/2) + cn
= 2(2T(n/4)+cn/2) + cn
= ...
= 2kT(n/2k) + kcn
posle k koraka. U poslednjem koraku je n=2k, odakle
T(n) = nT(1) + cnlog2 n
Kako je T(1) konstanta sledi

80
Dušan T. Malbaški - Algoritmi i strukture podataka

T(n) = Onlog2 n
tj. u najboljem slučaju algoritam je linearno-logaritamski te, prema tome, za red bolji
od prethodnih. Inače, pokazuje se da je u prosečnom slučaju (ni najgorem ni
najboljem) funkcija složenosti opet linearno-logaritamska.
Analiza kompleksnosti jasno ukazuje na činjenicu da je glavni problem kod
metode quicksort izbor pivota. U slučaju da je u svakoj fazi pivot medijana, dobijaju
se najbolji rezultati, dok izbor najmanjeg (ili najvećeg) za pivota vodi ka najgorem
slučaju. Pošto se u algoritam ne sme ugrađivati segment za određivanje medijane jer
bi vremenski poništio prednosti same metode, za kratku procenu pivota koriste se
razni postupci. Bez ulaženja u pojedinosti, nabrojaćemo par ideja:
 za pivot se bira element iz sredine podniza
 pivot se bira slučajnim izborom, tako što se generatorom pseudoslučajnih
brojeva zadaje njegov indeks
 formira se uzorak od tri elementa, prvog, poslednjeg i jednog iz sredine
podniza, te od te tri vrednosti odabere ona koja je u sredini
 uzima se slučajni uzorak elemenata i za pivot bira medijana tog uzorka

2.3. REDOSLEDNA OBRADA NIZA


Redosledna obrada (engl. traversing) niza je vrlo jednostavan postupak obrade
elemenata redosledom kojim su smešteni u niz (recimo, štampanje elemenata).
Redosledna obrada odvija se primenom brojačkog ciklusa koji poseduje svaki
programski jezik. Neka je a niz sa n elemenata. Redosledna obrada elemenata a izvodi
se ciklusom for oblika
for(i=0;i<n;i++) obraditi a[i]
za šta je potrebno n iteracija. Ako je Tobr vremenska složenost operacije obrade
svakog elementa (i ista je za svaki element), tada je vremenska složenost redosledne
obrade
Ttrav(n) = nTobr
Obrada pojedinačnih elemenata niza najčešće zahteva približno konstantno vreme, u
kojem je slučaju
Ttrav(n) = O[n]

81
Dušan T. Malbaški - Algoritmi i strukture podataka

3. SLOG I TABELA

Slog ili zapis (u C-u „struktura“) pripada, zajedno sa nizom, prvom višem
nivou organizacije podataka u odnosu na skalar. Kako skalar smatramo za najnižu,
nesvodivu strukturu - dakle atom - Virt za nizove i slogove koristi termin molekuli. I
slog je u više programske jezike uvršten rano, još 1959. u programski jezik kobol.
Statički ili dinamički niz čiji su elementi slogovi nazivamo tabelom. Prema ovom
shvatanju, tabela je izvedena struktura podataka no, s obzirom na učestanost primene i
način pristupa i obrade, zaslužuje poseban tretman. U ovom poglavlju opisaćemo i
tzv. rasutu ili heš tabelu koja, po definiciji, nije tabela (niz slogova), ali se primenjuje
u sličnim situacijama.

3.1. SLOG
Kao što niz objedinjava istovrsne elemente, tako i slog objedinjava raznovrsne
elemente, što znači da je u pitanju heterogena struktura. Poput statičkog niza, slog je
statička struktura kod koje je osnovni način pristupa - pristup prema poziciji. Na
ovom se, međutim, sličnosti između niza i sloga završavaju jer im je ponašanje
potpuno različito. U prethodnom poglavlju videli smo da je niz linearno uređen skup
elemenata koji imaju istu semantiku, ali koji su međusobno semantički nezavisni -
svaki element niza je po značenju zaokružena celina. Kod sloga je situacija sasvim
drukčija. Sa semantičke tačke gledišta, svaki element niza opisuje poseban entitet, a
niz kao celina opisuje skup entiteta. S druge strane, slog kao celina opisuje entitet, a
svaki njegov element daje opis neke od osobina tog entiteta, tako da su elementi sloga
semantički nesamostalni i samo svi zajedno opisuju neku jedinicu posmatranja. U
gnoseološkom smislu, slog je blizak skalaru i može se shvatiti kao njegovo
semantičko uopštenje: dok skalar, kada ima značenje, predstavlja trivijalno
jednostavan opis entiteta sa samo jednom osobinom, slog daje opis entiteta sa više
osobina te, pošto se različite osobine najčešće opisuju podacima različitih vrsta
(tipova), heterogenost sloga postaje neminovna. Semantička nesamostalnost
elemenata sloga prepoznaje se već i po tome što se za njih upotrebljava poseban
termin - polja. Inače, osobina pôlja da se uvek odnose na isti entitet i da samo zajedno
daju sliku tog entiteta zove se semantička konvergencija.

82
Dušan T. Malbaški - Algoritmi i strukture podataka

Osnovna operacija kod sloga jeste operacija pristupa i to pristupa prema


poziciji. I ovde se, kao i kod niza, adresa polja kojem se pristupa određuje
preliminarno, u fazi prevođenja. Međutim, pošto su elementi sloga heterogeni, oni
imaju različitu dužinu, te indeksiranje nije moguće. Umesto toga, poljima sloga
pristupa se prema nazivu koji ima sve osobine identifikatora. Za razliku od indeksa, u
skupu naziva polja ne postoji linearno uređenje, tj. za dati naziv polja ne može se
odrediti „sledeći“ (što je kod indeksa niza, naravno, moguće). Tako na primer, u slogu
datum poljima se pristupa preko naziva dan, mesec i godina, pri čemu ni za jedan od
tri naziva ne postoji ni „sledeći“ niti „prethodni“.
Rukovanje slogom zbog heterogenosti nije moguće ostvariti drugim
sredstvima osim onih koja su ugrađena u programski jezik. Primerice, prve verzije
fortrana nisu podržavale tip sloga i jedini način da se opiše entitet sa više osobina bio
je da se svaka osobina opiše posebnom promenljivom što je bilo krajnje neefikasno i
podložno greškama.
Da bi se dala što vernija slika sloga u svetlu do sada rečenog, njegovu logičku
strukturu definisaćemo drukčije od logičke strukture niza. Prvo, treba uočiti da su sva
polja sloga međusobno ravnopravna i svako je povezano sa svakim drugim vezom
jednake logičke čvrstoće. To za posledicu ima odustajanje od binarne relacije u skupu
polja koja bi neka od polja dovodila u međusobnu vezu, a neka ne. Umesto toga,
logičku strukturu sloga opisaćemo jednom jedinom torkom u kojoj se nalaze sva polja
sloga. Formalno, logička struktura sloga je uređeni par
R = (S(R),r(R))
gde je
S(R) = {x1,...,xn}
r(R) = {(x1,...,xn)}
i (i≠j)(xi≠xj), i,j=1,..,n. Neformalno, relacija r je reda n, i sastoji se od jedne jedine
uređene n-torke u koju ulaze svi elementi skupa S(R). Slog koji opisuje datum ima
logičku strukturu (dan,mesec,godina,(dan,mesec,godina)). Pritom, elementi
skupa S(R) su heterogeni, dozvoljen je pristup svakom polju preko naziva, a
dodavanje i uklanjanje polja nije definisano35.

35
Programski jezici paskal i C imaju tipove čije ponašanje podseća na ponašanje sloga sa
promenljivom strukturom (varijabilni slog u paskalu, odnosno unija u C-u), ali to nisu osnovne
strukture podataka niti imaju široku upotrebu.

83
Dušan T. Malbaški - Algoritmi i strukture podataka

Kako smo rekli, tip sloga i odgovarajuće operacije obezbeđuje programski


jezik, što znači da se neporsredna realizacija prepušta kompajleru. Fizička realizacija
sloga je, po pravilu, sekvencijalna. Za polja sloga odvaja se kompaktan, nepromenljiv
segment memorije u kojem se nalaze polja smeštena redosledom kojim su navedena u
definiciji. Glavna operacija kod sloga je operacija pristupa prema poziciji, koja se
izvodi posebnim operatorom sa nazivom selektor, a čija je oznaka u svim
programskim jezicima “.”. Dakle, ako je s slog, a p njegovo polje, pristup tom polju u
C-u ostvaruje se izrazom
s.p
Adresu lokacije određuje kompajler u fazi prevođenja. Kako su polja sloga u načelu
različitog tipa adresa polja p ne može se odrediti matematičkim izrazom kao kod niza.
Umesto toga, u procesu prevođenja, kompajler održava posebnu tabelu vezanu za tip
datog sloga koja se zove deskriptor36. Sadržaj deskriptora čine podaci o slogu (između
ostalog broj pôlja), kao i podaci o svakom polju ponaosob (recimo, tip polja, naziv
polja i relativna adresa (ofset) početka polja u odnosu na adresu samog sloga). Na slici
3.1 prikazano je pojednostavljeno idejno rešenje dela deskriptora sloga sa podacima o
tački u Dekartovom koordinatnom sistemu: polja su oznaka tipa char koje sadrži
oznaku tačke, te x i y tipa double koja odgovaraju apscisi i ordinati. Pretpostavimo da
tip char zauzima jedan bajt, a tip double 8 bajtova.

tip polja naziv polja ofset


char oznaka 0
double x 8
double y 16

Slika 3.1

Ako se u izvornom programu pojavi izraz d.x za pristup polju x sloga d, prevodilac će
postupiti ovako: ako se d nalazi na adresi A0 jednostavnim linearnim traženjem
pronaćiće se naziv polja „x“ i pročitati iz treće kolone ofset jednak 8. Iz ovih podataka
formiraće se adresa odgovarajuće memorijske lokacije izrazom
adr(d.x) = A0 + 8

36
postoji i kod niza!

84
Dušan T. Malbaški - Algoritmi i strukture podataka

Po pravilu, nad istovrsnim slogovima definisana je operacija dodele s1=s2


kojom se vrednosti svih polja sloga s2 kopiraju u odgovarajuča polja sloga s1.
Zbog nepostojanja uređenja u skupu naziva polja, ne postoji poseban
mehanizam (tipa brojačkog ciklusa kod niza) kojim bi se obavljala redosledna obrada
svih polja u slogu.

3.2. TABELA
Po definiciji, tabela je niz čiji su elementi slogovi. Pritom, taj niz može biti
statički i tada je tabela statička, ili dinamički kada govorimo o dinamičkoj tabeli.
Najvažnije operacije nad tabelom jesu
 sortiranje
 traženje, tj. pristup po ključu i
 redosledna obrada elemenata tabele
i izvode se na isti način u statičkim i dinamičkim tabelama. Operacije nad tabelom
objasnićemo na uopštenom primeru tabele sa n slogova tipa
typedef struct {
int key;
T info;
} Entry;
gde polje key predstavlja ključ, a polje info nekog tipa T preostali sadržaj elementa.
Neka je tabela definisana kao statički ili dinamički niz tipa Entry i dužine n i neka je
naziv odgovarajućeg tipa Table. Činjenica da smo ključ definisali kao int nema uticaja
na opštost razmatranja, pod pretpostavkom da u skupu ključeva postoji linearno
uređenje.
U principu, sortiranje tabele vrši se metodama koje su identične metodama
sortiranja niza, uz trivijalnu razliku koja se sastoji u tome da se sortiranje vrši po
vrednosti nekog ili nekih polja sloga. Najčešće, to polje jeste ključ (naravno, ako ga
ima). Shodno tome, nećemo se zadržavati na ovoj operaciji: sve što treba uraditi jeste
proučiti ranije opisane metode sortiranja niza i sintagmu „vrednost elementa“
zameniti sintagmom „polje sloga“ ili „ključ“.
Traženje u tabeli jeste postupak određivanja indeksa elementa sa zadatom
vrednošću ključa, nazvanom argument traženja, a sa krajnjim ciljem pribavljanja
podataka iz polja info traženog sloga. Traženje ima dva ishoda, oba regularna (!): tzv.

85
Dušan T. Malbaški - Algoritmi i strukture podataka

uspešno traženje čiji je ishod pomenuti indeks i neuspešno traženje sa ishodom


„element sa traženim ključem ne postoji u tabeli“.

3.2.1. Linearno traženje


Linearno traženje je najjednostavniji postupak i istovremeno najopštiji, jer ne
podrazumeva nikakve posebne osobine tabele. Ako arg označava argument traženja i
ako je t tabela veličine n u kojoj se traži, tada je procedura traženja iterativan postupak
kojim se arg sukcesivno poredi sa ključevima elemenata tabele; postupak se završava
ili kada se pronađe element čiji ključ ima vrednost arg (uspešno traženje) ili kada se
pregleda cela tabela (neuspešno traženje). U svrhu ilustracije, napravićemo funkciju
int linSearch(Table t, int arg) koja će za rezultat vratiti indeks traženog elementa u
slučaju uspešnog traženja ili vrednost -1 ako traženje nije uspešno:
int linSearch(Table t, int n, int arg) {
int i;
for(i=0;i<n;i++) if(t[i].key==arg) return i;
return -1;
}
U svrhu analize performanse algoritma linSearch učinićemo uobičajenu pretpostavku
da je verovatnoća da se traži neki element ista za sve elemente i iznosi 1/n.
Vremenska složenost postupka srazmerna je broju upoređivanja, tj. broju provera
jednakosti == u naredbi if. Pod navedenim uslovima, očekivani broj upoređivanja kod
uspešnog traženja iznosi
n
Ulin = 1/n  i = (n+1)/2 ≈ n/2
i 1

što znači da je vremenska kompleksnost algoritma On. Do ovog izraza možemo doći
na još jedan način, koristeći opštiji postupak. Rezonovaćemo ovako: u slučaju
uspešnog traženja neophodno je najmanje jedno upoređenje i to upravo ciljnog sloga.
Pored tog upoređenja može biti još 0,1,...,n-1 upoređenje, zavisno od toga koliko
slogova prethodi nađenom. Verovatnoća za sve ove slučajeve je, po pretpostavci ista i
jednaka 1/n, tako da je srednji broj upoređenja
n -1
1 1 n 1
Ulin = 1 +  n i  1  n  i = 1 + (n-1)/2 = (n+1)/2 ≈ n/2
i 0 i 1

U slučaju neuspešnog traženja, broj upoređivanja je tačno n, jer se mora proveriti


svaki ključ u tabeli, dakle

86
Dušan T. Malbaški - Algoritmi i strukture podataka

Nlin = n
gde smo sa N označili broj upoređivanja kod neuspešnog traženja. Vremenska
složenost je i u ovom slučaju On.

3.2.2. Binarno traženje


Binarno traženje je postupak koji je znatno brži od linearnog: dok je
vremenska složenost linearnog traženja On, pokazaće se da je kod binarnog ona
Olo2 n, što ovaj postupak svrstava u najbrže algoritme. Za razliku od linearnog
traženja koje je uvek moguće, binarno traženje može se obaviti samo pod uslovom da
je tabela sortirana po vrednosti ključa (rastućoj ili opadajućoj, svejedno). Bez
gubljenja opštosti, pretpostavićemo da je tabela t u kojoj se traži binarnim postupkom
sortirana u rastućem redosledu ključeva.
Procedura se odvija na sledeći način: argument traženja, umesto sa prvim po
redu elementom upoređuje se sa elementom na polovini tabele. Ako je argument
traženja manji od ključa elementa na polovini tabele, sasvim je sigurno da se traženi
element mora nalaziti u prvoj polovini tabele, jer se iza elementa na polovini nalaze
oni sa još većim vrednostima ključa. Obrnuto, ako je argument traženja veći od ključa
elementa na polovini tabele, ciljni element može se naći samo u drugom delu tabele.
Na taj način, već posle jednog upoređenja, neizvesnost u pogledu mesta traženog
elementa smanjuje se u dvostrukom iznosu. Posle prvog upoređivanja nastavlja se na
isti način: pristupa se elementu na polovini odabrane polovine i njegov ključ se poredi
sa argumentom traženja, sa istim ishodom kao u prethodnoj fazi. Kako se vidi,
segment u kojem se mora nalaziti traženi element se u svakoj fazi skraćuje na
polovinu prethodne dužine: u početku to je ceo niz, pa njegova polovina, pa četvrtina
itd. Postupak se zaustavlja kada se element nađe ili kada se dužina podniza u kojem se
on mora nalaziti svede na 1.
Šema binarnog traženja prikazana je na slici 3.2 gde je, komparacije radi,
prikazano i linearno traženje. Zbog jednostavnosti, na slikama elementa nalaze se
samo ključevi. Prikazan je proces traženja elementa sa ključem 14.

87
Dušan T. Malbaški - Algoritmi i strukture podataka

binarno traženje

1 4 5 8 10 14 16 17 19

linearno traženje

Slika 3.2.

Programska izvedba funkcije za binarno traženje izgrađena je oko dve interne


promenljive koje se odnose na indekse: promenljiva low koja u svakoj fazi sadrži
donju granicu indeksa posmatranog segmenta tabele i promenljiva high koja sadrži
gornju granicu. Na početku važi low=0 i high=n-1. Traženje počinje elementom koji
se nalazi na indeksu i=(low+high)/2, tj. koji se nalazi na polovini niza. Ako se
ustanovi da element na indeksu i ima ključ jednak argumentu traženja arg, postupak
je završen. Ako je taj ključ manji od arg segment treba skratiti na low do i-1; ako je
ključ elementa na indeksu i veći od arg, segment se skraćuje na i+1 do high. Postupak
se završava ili kada se element nađe (tada je rezultat njegov indeks) ili kada postane
low>high, što znači da je traženje neuspešno (tada funkcija vraća vrednost -1).
Funkcija ima sledeći oblik:
int binSearch(Table t, int n, int arg) {
int i, low=0, high=n-1;
while(low<=high) {
i = (low+high)/2;
if(t[i].key==arg) return i;
if(t[i].key<arg) low = i+1; else high = i-1;
}
return -1;
}
U cilju određivanja izraza za srednji broj upoređenja kod binarnog traženja,
poslužićemo se tzv. binarnim stablom traženja, digrafom koji pokazuje sve moguće
redoslede pristupa pri traženju. Pošto je u pitanju digraf koji ne predstavlja strukturu
podataka, čvorove ćemo označiti uobičajenim kružićima. Na slici 3.3 prikazano je
jedno takvo stablo koje odgovara sortiranoj tabeli sa elementima čiji su ključevi
rastućim redom x0, x1, ... ,x10, x11.

88
Dušan T. Malbaški - Algoritmi i strukture podataka

x5

x2 x8

x1 x3 x6 x10

x0 x4 x7 x9 x11

Slika 3.3.

U korenu stabla (koren je čvor od kojeg traženje počinje, a u stablu se prepoznaje po


tome što nema ulaznih grana) nalazi se čvor x5 koji odgovara elementu sa polovine
tabele, tj. onaj kojim traženje počinje. Za njegovo nalaženje potreban je jedan
pokušaj, tj. jedno upoređenje. Sledeća dva čvora stabla odgovaraju dvama elementima
x2 i x8 za čije nalaženje su potrebna dva pokušaja. Na sledećem nivou nalaze se
elementi x1 x3, x6 i x10 za koje je potrebno tri upoređenja itd. Na poslednjem nivou
nalaze se tzv. listovi stabla, tj. elementi što nemaju izlaznih grana. To su elementi x0,
x4, x7, x9 i x11 za koje je potrebno 4 poređenja. Uočimo odmah da je u slučaju
neuspešnog traženja neophodno izvesti sva poređenja do listova, što je za posmatrani
primer jednako 4. Neka je h najveći broj upoređenja potreban za uspešno traženje.
Broj h (zove se visina stabla) jednak je broju čvorova između korena i lista koji je
najudaljeniji od korena, uračunavajući i sam koren i list. Neka je ej broj elemenata do
kojih se stiže posle j upoređenja za j=1,...,h. Očigledno je
e1=1=20 e2=21 e3=22 ... ei=2i-1 ... eh-1=2h-2
Ukupan broj elemenata do kojih se stiže sa 1,2,...,h-1 upoređenja iznosi
h -1

2 k
= 2h-1-1
k 0

te je eh=n-2h-1+1. Srednji broj upoređenja pri uspešnom traženju, a pod pretpostavkom


da je verovatnoća traženja ista za sve elemente i jednaka 1/n iznosi
h -1
Ubin = (1/n)(e1+2e2+...+(h-1)eh-1+heh)=(1/n)(heh+  j2 j-1 )
j1

h-1
= (1/n)(heh+(h-2)2 +1)
Uvrštavanjem vrednosti za eh dobijamo

89
Dušan T. Malbaški - Algoritmi i strukture podataka

Ubin = h+h/n+(2h-1)n
Kako je, u opštem slučaju, 2h-1  n 2h-1 vrednost h može se izraziti kao najmanji ceo
broj koji nije manji od log2 (n+1), tj.
h = log2 (n+1) = log2 (n+1) + 
gde je 0   < 1. Dakle,
Ubin = log2 (n+1)+ (1+1/n) - 2(n+1)-1/n
Ako je n>>1 sledi
Ubin ≈ log2 n - (2-)
Konačno, lako se pokazuje da u intervalu (0,1) važi 0,91  2-  1, što znači da bi
dosta precizna procena prosečnog broja upoređenja pri uspešnom traženju bila
Ubin ≈ (log2 n) - 1
Kako je funkcija složenosti Tbin(n) srazmerna broju upoređenja zaključujemo da je,
kod uspešnog traženja,
Tbin(n) = Olog2 n
što bi značilo da binarno traženje spada u red najbržih algoritama. Slučaj neuspešnog
traženja ne razlikuje se mnogo, jer zahteva najviše h upoređivanja, pa se može
proceniti sa log2 (n+1) odnosno, ako je n>>1 sa log2 n.
Razlika između linearnog traženja u tabeli i binarnog traženja (kada je ono
moguće odn. kada je tabela sortirana) velika je i ubrzano raste sa povećanjem broja
elemenata. U sledećoj tablici ilustrujemo ovu razliku:

n Ulin Ubin
10 5 2.3
50 25 4.6
100 50 5.6
500 250 8.0
1000 500 9.0
5000 2500 11.3
10000 5000 12.3

90
Dušan T. Malbaški - Algoritmi i strukture podataka

3.2.3. Redosledna obrada tabele


Redosledna obrada tabele vrši se u skladu sa dve vrste zahteva. Jedan slučaj
jeste redosledna obrada u kojoj je konkretan redosled kojim se slogovi obrađuju
proizvoljan. Primer takve obrade bila bi tabela u čijim se slogovima nalazi neko
numeričko polje, a obrada predstavlja određivanje zbira tih numeričkih podataka.
Pošto je konkretan redosled nevažan, ovu vrstu redosledne obrade realizujemo na isti
način na koji se obrađuju obični nizovi, dakle jednim brojačkim ciklusom sa ukupnom
vremenskom složenošću On.
Sasvim je drukčija situacija kada obradu treba vršiti u skladu sa nekim
kriterijumom koji utiče na konkretan redosled obrade. Takav je, recimo, slučaj
redosledne obrade slogova, ali redosledom saobraznim rastućoj veličini ključa. Ako bi
se obrada vršila uzastopnim linearnim traženjem shodno rastućoj vrednosti ključa i
ako bi se obrađivali svi slogovi, tada bi ukupan broj poređenja bio približno nn/2, tj.
vremenska složenost bila bi On2. Znatno skraćenje ukupnog vremena obrade postigli
bismo time što bismo tabelu održavali sortiranom po traženom redosledu. U tom
slučaju, slogovi bi se obrađivali upravo u onom redosledu u kojem se nalaze u tabeli,
te bi ukupan broj pristupa bio n, što znači da je odgovarajući algoritam za klasu manje
složen nego onaj koji podrazumeva linearno traženje. Cena ovog je, naravno, vezana
za potrebu sortiranja, tako da se odluka donosi prvenstveno na osnovu učestanosti
redosledne obrade.

3.3. RASUTA ILI HEŠ TABELA


U odeljcima 3.2.1 i 3.2.2 videli smo kako se realizuje traženje u tabeli i
zaključili da je opšti postupak, linearno traženje, reda složenosti n, što u slučaju da se
postupak obavlja intenzivno može da bude premnogo. Binarno traženje spada u
najbrže algoritme uopšte, sa složenošću Olog2 n; štaviše, u 11 je pokazano da
među algoritmima traženja koji se baziraju na upoređivanju ne postoje oni sa manjom
složenošću (tj. brži) od binarnog traženja. Nažalost, conditio sine qua non za binarno
traženje jeste uslov da tabela mora biti sortirana, što, prvo, zahteva dodatno,
nezanemarljivo vreme i drugo nije uvek moguće, jer neke primene tabele mogu da
zahtevaju redosled koji se ne poklapa sa redosledom sortiranja po ključu.

91
Dušan T. Malbaški - Algoritmi i strukture podataka

Ako, međutim, odustanemo od algoritma zasnovanog na sukcesivnom


upoređivanju, pokazuje se da rešenje za brz postupak traženja ipak postoji. U osnovi,
traženje je algoritam koji se može specifikovati na sledeći način:
 na osnovu zadatog argumenta traženja arg odrediti adresu odgovarajućeg
sloga
gde se, kako vidimo, ne zahteva da osnovni mehanizam bude sukcesivno upoređivanje
ključeva sa argumentom traženja. U matematičkom smislu, traženje možemo prikazati
kao primenu neke funkcije h koja, na osnovu vrednosti argumenta traženja, generiše
adresu na kojoj se nalazi odgovarajući slog, tj.:
adr = h(arg)
gde je adr relativna adresa sloga sa ključem arg. Kod običnih tabela, funkcija h
realizuje se algoritamski kao linearno ili binarno traženje.
Rasuta ili heš tabela (engl. scattered table ili hash table) jeste struktura
podataka kod koje se adresa traženog sloga određuje direktnom primenom
preslikavanja h skupa ključeva na skup adresa slogova. Strogo formalno, heš tabela
nije tabela u smislu u kojem je definisana u odeljku 3.2 jer ona nije niz. Najbliža
svojstvima heš tabele bila bi definicija u kojoj se heš tabela definiše kao (neuređen)
skup slogova snabdeven operacijom h (što se, inače, zove heširanje). Dakle,
prihvatićemo da se logička struktura heš tabele definiše kao uređeni par
H = (S(H),)
gde su elementi skupa S(H) slogovi iste vrste, a  prazan skup (sa smislom prazne
relacije). Pristup se obavlja traženjem, putem heš funkcije h, koja preslikava skup
ključeva K na skup S(H):
h : K  S(H)
Pristupiti se može svakom elementu, a definisane su i operacije dodavanja i uklanjanja
i to bez ograničenja, tako da heš tabele spadaju u dinamičke strukture podataka.
Uočimo, odmah, da je glavna (i praktično jedina) svrha heš tabela da obezbede
brz pristup po ključu, sa idejom da se adresa traženog elementa izračunava iz ključa,
analogno indeksiranju gde se adresa elementa izračunava iz indeksa.

3.3.1. Fizička realizacija heš tabele


Teorijski, heš tabela bila bi idealna za traženje po ključu: ona, naime,
isključuje upoređivanje, jer se pristup traženom slogu obavlja u jednom pokušaju. U
stvarnosti, situacija nije tako idilična, zahvaljujući ograničenjima fizičke strukture heš

92
Dušan T. Malbaški - Algoritmi i strukture podataka

tabele i načinu određivanja adrese iz ključa. Pre svega, fizička realizacija heš tabele je
sekvencijalna, pošto se u suprotnom ne bi moglo izvesti preslikavanje ključa na
adresu direktnim izračunavanjem. Neka je tip sloga u heš tabeli
typedef struct {
K key;
T info;
} Entry;
gde je K tip ključa sloga (najčešće je u pitanju string). Ako se očekuje intenzivna
primena operacija dodavanja i uklanjanja, tabela treba da bude na hipu, te bi definicija
odgovarajućeg tipa bila
typedef Entry* Hashtable;
Posmatrajmo heš tabelu t definisanu sa
Hashtable t;
i realizovanu sa
t = malloc(SIZE*sizeof(Entry));
gde je SIZE veličina memorije rezervisane za heš tabelu, izražena brojem elemenata.
Zadatak heš funkcije h je, dakle, da na bazi polja key sloga generiše indeks iz
segmenta 0 do SIZE-1 elementa (fizičkog) niza koji realizuje heš tabelu. Taj indeks se
pojavljuje u ulozi relativne adrese u okviru rezervisane zone memorije. U idealnom
slučaju heš funkcija je bijekcija, tj. svakoj mogućoj vrednosti ključa odgovara jedna i
samo jedna vrednost adrese (tj. indeksa niza tipa Hashtable). Međutim, u praksi je
situacija uvek veoma daleko od idealne, te se biunivoka heš funkcija ima shvatiti kao
redak (i nevažan) slučaj. Razlog za to je činjenica da bi se u idealnom slučaju morao
rezervisati memorijski prostor koji pokriva sve moguće vrednosti ključeva od kojih u
stvarnim primenama može da bude zastupljen tek vrlo mali deo. Ako bi, na primer,
ključ bio samo dvobajtni string, za tabelu bi već trebalo predvideti oko 65000 lokacija
(što je jednostavno neprihvatljivo). Zato treba tražiti druga rešenja.
Ideja heširanja je stara ideja, te nije čudo što za izbor heš funkcije postoji
veliki broj varijanata. Najpopularnije se zasnivaju na ideji da se, polazeći od ključa,
generiše neki broj između 0 i SIZE-1 algoritmom sličnim nekom od algoritama za
generisanje pseudoslučajnih brojeva, pri čemu raspodela tako dobijenih brojeva (tj.
indeksa odn. relativnih adresa) treba da bude što bliža uniformnoj. Treba podvući da u
ovim postupcima, u stvari, nema nikakve „slučajnosti“, stoga što za istu vrednost

93
Dušan T. Malbaški - Algoritmi i strukture podataka

ključa heš funkcija mora da generiše isti indeks. Otud i engleski naziv „heš“ koji, u
smislu u kojem se koristi ovde, znači „razbacivanje“.
Broj i raznovrsnost metoda za heširanje impresivni su, pa ćemo, stoga
prikazati samo jednu, predviđenu za ključeve koji su stringovi (11). Ideja je da se
pojedinačni znaci u stringu tretiraju kao celobrojne vrednosti i međusobno se sabiraju
da bi na kraju rezultat bio sveden na traženi segment operacijom deljenja po modulu:
unsigned h(char* key) {
unsigned x=0;
while(*key) x+=*key++;
return x%SIZE;
}

3.3.2. Kolizija ključeva i rukovanje sinonimima


Nažalost, činjenica da heš funkcije nisu bijekcije stvara i glavni problem kod heš
tabela: tzv. koliziju ključeva. Ova pojava znači da međusobno različiti ključevi mogu
da daju istu adresu, tj. može da se dogodi da bude h(k1)=h(k2) iako je k1k2. Slogovi
sa različitim ključevima za koje heš funkcija generiše istu adresu nose naziv sinonimi.
Postojanje sinonima je nepovoljno i neizbežno. Pošto bi sinonimi morali da se nađu
na istoj adresi što je, naravno, nemoguće, svaka heš tabela mora se snabdeti
mehanizmom za rukovanje sinonimima. Problem se svodi na sledeće: u tabelu treba
upisati slog s sa ključem k, ali se na adresi h(k) već nalazi neki slog. Gde smestiti slog
s? Prvi problem koji, u ovom kontekstu, mora biti rešen jeste kako uopšte ustanoviti
da je neka lokacija u heš tabeli već zauzeta. Najjednostavnije rešenje pruža kontekst u
kojem među potencijalnim vrednostima ključa ima neka koja nikada ne može biti
ključ (kod stringa to bi bio, recimo, prazan string). U takvim slučajevima prethodna
priprema memorijske zone za heš tabelu obuhvata njeno popunjavanje
pseudoslogovima čiji su ključevi prazni stringovi. Lokacija koja je zauzeta prepoznaje
se po tome što slog koji se na njoj nalazi ima ključ koji nije prazan string. U slučaju
da u skupu ključeva nema zabranjenih vrednosti, svaki slog se proširuje binarnim
statusnim poljem čija jedna vrednost znači „slog je aktuelan“, a druga „slog nije
aktuelan“. Memorijski prostor se u početku popunjava pseudoslogovima sa statusom
„nije aktuelan“, a zauzeta lokacija prepoznaje se po statusnom polju „jeste aktuelan“.
Evo segmenta za pripremu memorijskog prostora heš tabele t definisane napred (uz
pretpostavku da je polje key string):

94
Dušan T. Malbaški - Algoritmi i strukture podataka

Entry t0 = {″″};
.................
for(i=0;i<SIZE;i++) t[i]=t0; //upisati na sve lokacije slog sa praznim kljucem
Najpoznatija grupa metoda za rukovanje sinonimima poznata je pod imenom
„otvoreno adresiranje“. Zajedničko za sve metode otvorenog adresiranja jeste ideja
 ako je lokacija na koju treba smestiti novi slog zauzeta, tada se traži novo
mesto na rastojanju koje je izračunljivo; ako je i to, novo, mesto zauzeto
postupak se ponavlja.
Među metodama otvorenog adresiranja najprostija je tzv. metoda linearnog
sondiranja (engl. linear probing). Ovde se, za slučaj kolizije, mesto za upis traži na
sledećoj lokaciji, pa na sledećoj itd. U slučaju da se stigne do kraja tabele prelazi se na
početak. Dakle, ako je i adresa već zauzete lokacije, nova se traži izrazom
i = (i+1)%SIZE
Šema linearnog sondiranja prikazana je na slici 3.4 (slobodne lokacije su šrafirane).
Postupak se završava kada se naiđe na slobodnu lokaciju (sa praznim ključem) ili
kada se stigne do lokacije sa kojom je počela pretraga, što znači da je tabela
popunjena.

početak pretrage

Slika 3.4.

Traženje u heš tabeli sa linearnim sondiranjem izvodi se algoritmom sličnim


dodavanju. Posle transformacije ključa u adresu, proverava se da li se ključ sloga na
dobijenoj adresi poklapa sa argumentom traženja. Ako se poklapa, postupak je
završen. Ako se ne poklapa nastavlja se sa linearnim sondiranjem sve dok se slog ne
pronađe ili dok se ne stigne do nezauzete lokacije ili lokacije sa kojom je počela
pretraga, a to znači da je traženje neuspešno.
Od osnovnih operacija najveći problem je uklanjanje. Čini se da je rešenje
jednostavno - proglasiti lokaciju za nezauzetu - ali to ne bi funkcionisalo. Naime,
pražnjenje lokacije (u našem primeru upis sloga sa praznim ključem) pokvarilo bi
algoritam za traženje. Posmatrajmo heš tabelu na slici 3.5.

95
Dušan T. Malbaški - Algoritmi i strukture podataka

početak pretrage

(a) a b c d

(b) a b d

Slika 3.5.

Neka su slogovi a, b, c i d sinonimi. Ako su bili upisivani u tabelu redom a-b-c-d


stanje strukture bilo bi kao na slici 3.5a. Pretpostavimo da se u nekom trenutku ukloni
element c tako što se lokacija proglasi za praznu, tako da tabela dobije oblik sa slike
3.5b. Ako bi se posle toga izdao upit za traženje elementa d koji nije uklonjen,
postupak bi se zaustavio nailaskom na lokaciju na kojoj je bio element c i rezultat -
pogrešan! - bio bi neuspešno traženje. Problem se rešava tako što se uvode dve vrste
nezauzetih lokacija: jednu vrstu čine lokacije koje su inicijalno prazne (u njih nikada
nije bilo upisivano ništa), a drugu lokacije u kojima su bili slogovi koji su uklonjeni.
Ovo se postiže korišćenjem dve nedozvoljene vrednosti ključa: jedna označava
inicijalno praznu lokaciju (nazovimo je empty), a druga lokaciju sa koje je izbrisan
slog (nazovimo je deleted). Operacijom uklanjanja sloga lokacija se proglašava za
deleted. Pri traženju, empty lokacije zaustavljaju proces, što nije slučaj sa deleted
lokacijama koje i dalje učestvuju u linearnom sondiranju. Prilikom dodavanja sloga,
traži se prva empty ili deleted lokacija i u nju se upisuje novi slog. Ako se, umesto
specijalnih vrednosti ključa, koristi statusno polje, ono sada ima tri vrednosti:
„aktuelna lokacija“, „inicijalno prazna lokacija“ i „neaktuelna lokacija“. Iako
jednostavno, ovo rešenje može ozbiljno da uspori sve algoritme, jer kada lokacija
jednom izgubi status inicijalno prazne, ona ga više ne može povratiti te, bez obzira na
sadržaj, učestvuje u traženju produžavajući tako postupak. Ilustrovaćemo, sada,
metodu linearnog sondiranja na primeru heš tabele sa slogovima tipa Entry (opisan
ranije) čiji je ključ string key. Za označavanje inicijalno prazne lokacije koristićemo
prazan string, tj. slog za čiji ključ važi key0=0. Lokaciju ćemo označiti kao deleted
upisivanjem sloga sa key0=1 i key1=0 kod kojeg se string što čini ključ sastoji od
jednog znaka sa kodom 1. Pošto su znaci sa kodovima 0 i 1 upravljački znaci, malo je

96
Dušan T. Malbaški - Algoritmi i strukture podataka

moguće da se pojave u sklopu stringa što predstavlja ključ. Traženje elementa po


argumentu traženja arg obavlja se funkcijom getItem:
//trazenje elementa po kljucu
int getItem(char* arg) {
int probe, i;
probe=h(arg);
if(!strcmp(t[probe].key,arg)) return probe;
else {
for(i=(probe+1)%SIZE; *t[i].key&&(i!=probe); i=(i+1)%SIZE)
if(!strcmp(t[i].key,arg)) return i;
}
return -1;
}
Prvo se pomoću heš funkcije h odredi početna lokacija probe. Ako slog na toj lokaciji
ima traženi ključ, postupak je završen i funkcija vraća kao izlaz indeks tog sloga u
tabeli. Ako to nije slučaj, vrši se linearno sondiranje brojačem i: sve dok se ne naiđe
na inicijalno praznu lokaciju (kod koje je key0=0) ili dok se ne dođe do početne
lokacije (probe), traži se slog sa ključem arg te, ako se nađe, njegov indeks vraća se
kao rezultat. Ako se pregled završi neuspehom funkcija vraća rezultat -1. Funkcija
removeItem uklanja element čiji je ključ arg. Uklanjanju prethodi traženje: ako je
traženje uspešno, na mesto ključa sloga na odgovarajućoj lokaciji upisuje se string čiji
je sadržaj znak sa kodom 1 koji označava neaktuelnu (deleted) lokaciju i vraća
vrednost 0 koja označava uspešno završenu operaciju. U slučaju da slog za brisanje
nije nađen, funkcija vraća rezultat -1
//uklanjanje elementa
int removeItem(char* arg) {
int i;
if((i=getItem(arg))!=-1) {
t[i].key[0]=1;
t[i].key[1]='\0';
return 0;
} else return -1;
}

97
Dušan T. Malbaški - Algoritmi i strukture podataka

Dodavanje elementa item realizovano je funkcijom putItem. S obzirom na to da se u


tabeli sme naći najviše jedan element sa datim ključem, dodavanju mora da prethodi
neuspešno traženje. Ukoliko element sa ključem item.key već postoji, dodavanje se ne
može izvršiti i funkcija se završava sa kodom uspešnosti -2. Ako to nije slučaj prvo se
određuje mesto elementa pomoću heš funkcije, pa ako je lokacija slobodna za upis
element se dodaje i funkcija završava uz indeks novododatog elementa kao rezultat.
Ako je lokacija zauzeta nastavlja se sa traženjem slobodne lokacije sve dok se takva
ne nađe ili dok se ne dođe ponovo do početne lokacije što znači da je tabela puna
(funkcija tada vraća rezultat -1). Podsetimo da „slobodna lokacija“ u ovom slučaju
znači ili inicijalno prazna lokacija ili lokacija tipa deleted. Za slobodnu lokaciju, s
obzirom na dva specijalna ključa koja smo uveli, važi key0=0 ili key0=1, odnosno,
jednim izrazom, key01.
//dodavanje elementa
int putItem(Entry item) {
int i, probe;
//ako kljuc vec postoji zavrsiti sa rezultatom -2;
if(getItem(item.key)!=-1) return -2;
probe = h(item.key);
if(*t[probe].key<=1) {
t[probe]=item;
return probe;
} else {
for(i=(probe+1)%SIZE; i!=probe; i=(i+1)%SIZE)
if(*t[i].key<=1) {
t[i]=item;
return i;
}
return -1;
}
}
Metoda heširanja sa linearnim sondiranjem stvara još jednu poteškoću što nosi naziv
nagomilavanje (engl. pile-up ili clustering). Nagomilavanje se ispoljava kao
tendencija stvaranja dugačkih sekvenci zauzetih lokacija, što produžava kako vreme

98
Dušan T. Malbaški - Algoritmi i strukture podataka

dodavanja tako i vreme traženja. Na slici 3.6. prikazana je heš tabela sa jednim
elementom, na lokaciji j. Neka je p=1/SIZE verovatnoća da heš funkcija generiše
pojedinačnu adresu i neka je ona uvek ista za sve adrese (što je i svrha dobre heš
funkcije).

j j+1

Slika 3.6.

Verovatnoća da će novi slog biti smešten na bilo koju od slobodnih lokacija iznosi p,
sa izuzetkom lokacije j+1. Naime, ova lokacija biće popunjena ako heš funkcija
generiše adresu j+1, ali i ako generiše lokaciju j koja je već zauzeta, pa se linearnim
sondiranjem opet pronalazi lokacija j+1! Dakle, verovatnoća da se pri prvom
sledećem dodavanju zauzme lokacija j+1 je 2p, dvaput veća nego verovatnoća
zauzimanja ostalih lokacija. Po zauzimanju ove lokacije, verovatnoća da se zauzme
lokacija j+2 iznosi 3p itd. Razlog za ovakvo ponašanje tabele je očigledno u činjenici
da se slobodne lokacije traže sukcesivno, odnosno, da je mehanizam prolaska kroz
tabelu izraz
i = (i+1)%SIZE
koji se koristi u funkcijama getItem i putItem. Izvesno, ne naročito značajno,
poboljšanje postiže se ako se rastojanje između lokacija-kandidata za upis poveća od
1 na neku vrednost inc>1, tako da bude
i = (i+inc)%SIZE
pri čemu su vrednosti inc i SIZE uzajamno proste da bi se pri upisu uračunale sve
lokacije.
Značajnije poboljšanje u odnosu na pojavu nagomilavanja pruža metoda tzv.
kvadratnog sondiranja. Kod ove metode, rastojanje pri traženju je promenljivo i
vezano je za prethodni broj pokušaja. Konkretno, neka je i0 adresa dobijena
heširanjem
i0 = h(key)
Ako se traženje mora nastaviti, ono se obavlja u k pokušaja tako da bude
ik = (i0 + k2)%SIZE k=1,2,...

99
Dušan T. Malbaški - Algoritmi i strukture podataka

Drugim rečima, ako se ustanovi kolizija ključeva, novi pokušaji će biti na


rastojanjima 1, 4, 9, ... od prvobitne lokacije i0. Kvadratno sondiranje u velikoj meri
eliminiše problem nagomilavanja, ali ima jednu manu: kada se traži nova lokacija ik
kvadratnim sondiranjem neće biti obuhvaćene sve lokacije, tj. dogodiće se da za neku
vrednost k važi ik=i0, a da prethodno nisu isprobane sve lokacije.
Još jedna ideja je da se, umesto da inkrement bude konstantan ili vezan za broj
prethodnih pokušaja, njegova vrednost veže za argument traženja. Najpoznatija
metoda kod koje se primenjuje ova tehnika nosi naziv slučajno sondiranje. Kod
slučajnog sondiranja, pri dodavanju (upisu) novog sloga postupa se na sledeći način:
ako je lokacija određena heširanjem, tj. primenom h(arg) popunjena, tada se aktivira
sekundarno heširanje nekom drugom funkcijom h’ i računa
inc = h’(arg)
a onda, kako pri dodavanju tako i pri traženju, koristi izraz
i = (i+inc)%SIZE
Podvucimo da funkcija h’ treba da bude različita od funkcije h. U našem primeru,
funkcija h’ mogla bi da bude
h’(key) = 1+*key%(SIZE-1) //ne sme biti 0!
Korišćenjem sekundarnog heširanja rastojanje na kojem se traži slobodno mesto
vezuje se za ključ sloga koji se dodaje ili traži i nije uvek isto, čime se pojava
nagomilavanja u velikoj meri predupređuje. Ilustracije radi, pokazaćemo kako bi
izgledala funkcija getItem uz korišćenje slučajnog sondiranja:
//sekundarno hesiranje
unsigned hsec(char* arg) {
return 1+*arg%(SIZE-1);
}

int getItem(char* arg) {


int probe, i;
probe=h(arg);
if(!strcmp(t[probe].key,arg)) return probe;
else {
inc = hsec(arg);
for(i=(probe+1)%SIZE; *t[i].key&&(i!=probe); i=(i+inc)%SIZE)

100
Dušan T. Malbaški - Algoritmi i strukture podataka

if(!strcmp(t[i].key,arg)) return i;
}
return -1;
}
I još jedna napomena: zbog numeričkih problema vezanih za performansu heširanja
opšta je preporuka (koje se svi pridržavaju) da
 veličina rezervisanog memorijskog prostora SIZE treba da bude prost broj.
Dakle, ako smo procenili da za tabelu treba 1000 lokacija, umesto tog iznosa
opredelićemo se, recimo, za 997.
Konačno, ne sme se izgubiti iz vida da je realizacija heš tabele sekvencijalna,
što neminovno otvara problem prepunjenosti. Ako heš tabela nije statička, tada do
stanja prepunjenosti (overflow) itekako može doći. Pošto se u tim situacijama program
ne sme zaustaviti sa porukom o grešci (jer nije u pitanju nikakva greška), u sklopu
realizacije tabele mora se predvideti i funkcija za rekonfiguraciju tabele. U pitanju je
jednostavna (ali ne i brza) funkcija koja će zauzeti veći memorijski prostor. Ovde C-
ova funkcija realloc nije od pomoći jer se, zbog heširanja, slogovi iz stare tabele
moraju iznova upisati u novu u kojoj se neće naći na istim lokacijama. Štaviše, za
pokretanje rekonfiguracije nije dobro čekati da se tabela popuni. Naime, performansa
heš tabele najdirektnije zavisi od tzv. koeficijenta popunjenosti koji predstavlja odnos
broja zauzetih lokacija i veličine memorijskog prostora SIZE. Običaj je da se funkcija
za rekonfiguraciju uključi ne kada je taj koeficijent jednak 100% (tj. popunjena
tabela), nego kada pređe određenu granicu (tipično 70% - 80%).
Problemi vezani za rekonfigurisanje zbog prepunjenosti, pa i za koliziju
ključeva, mogu se rešavati i posebnom sekvencijalno-spregnutom realizacijom heš
tabele. Metoda nosi naziv metoda lančanja. Po cenu usporavanja operacija, ovom
realizacijom se u potpunosti izbegava ponovno formiranje tabele, a sinonimima se
rukuje tako što se svi slogovi nalaze na hipu, pri čemu su sinonimi međusobno
spregnuti. Sama heš tabela sadrži samo pokazivač na lanac slogova koji su sinonimi.
Tako, element tabele na adresi i sadrži pokazivač na lanac slogova čija je zajednička
karakteristika da heš funkcija h primenjena na njihove ključeve daje adresu i. Iako je
ova struktura jednostavna i fleksibilna, osnovne operacije su usporene, kako zbog
rukovanja hipom, tako i zbog sporosti pristupa lancima sinonima (o ponašanju ovih

101
Dušan T. Malbaški - Algoritmi i strukture podataka

lanaca videti u poglavlju o jednostruko spregnutim listama). Šema sekvencijalno-


spregnute realizacije prikazana je na slici 3.7.

u ovom lancu su
slogovi za koje je
h(key)=i

Slika 3.7.

3.3.3. Analiza otvorenog adresiranja


Ponašanje heš tabele sa stanovišta performanse komplikovano je, imajući u
vidu da se dva dominantna faktora - stvarna raspodela ključeva u tabeli i kvalitet heš
transformacije - ne mogu opisati jednostavnim izrazima. Da bismo pojednostavili
analizu učinićemo neke pretpostavke:
 heš funkcija h generiše adrese po uniformnoj raspodeli
 prilikom upisivanja-traženja slogova svako sondiranje je uniformno, tj.
verovatnoća izbora bilo koje lokacije uvek je ista, nezavisno od istorijata
prethodnih pokušaja u slučaju kolizije (dakle, podleže binomnoj raspodeli)
 broj slogova u tabeli je, kako se to kaže, dovoljno velik
Mera performanse biće broj pokušaja prilikom traženja. Počećemo sa neuspešnim
traženjem. Neka je tabela veličine SIZE i neka je u datom trenutku broj zauzetih
slogova nSIZE. Pre svega, pokazuje se da je osnovni parametar u modelu tzv.
koeficijent popunjenosti tabele q izražen odnosom n i SIZE:
q = n/SIZE
Verovatnoća da će se prilikom prvog pokušaja dohvatiti zauzeta lokacija iznosi q, a da
će biti pogođena prazna lokacija iznosi 1-q. Verovatnoća da se neuspešno traženje
završi posle tačno dva pokušaja iznosi q(1-q). Verovatnoća da će se neuspešno
traženje završiti posle tačno k pokušaja iznosi qk-1(1-q). Prema tome, očekivani broj
pokušaja pri neuspešnom traženju iznosi

102
Dušan T. Malbaški - Algoritmi i strukture podataka


k -1 1
Nhash(q) =  kq
k 1
(1  q) =
1 q
Da bismo procenili broj pokušaja pri uspešnom traženju, moramo se podsetiti na
postupak upisivanja. Prilikom upisivanja u tabelu u kojoj je bilo m zauzetih lokacija,
upisu je prethodilo neuspešno traženje pre dodavanja u tabeli sa faktorom
popunjenosti x=m/SIZE. Kako su slogovi dodavani, tako je faktor popunjenosti rastao
do vrednosti q. Ako je broj slogova u tabeli mnogo veći od 1, sumu možemo
aproksimirati integralom pa se dobija
q
1 1
Uhash(q) = (1/q) N hash (x)dx = ln
x 0
q 1 q

Da bi čitalac stekao osećaj o performansi heš tabele posmatraćemo običnu tabelu sa,
recimo, 500 elemenata. Uspešno linearno traženje proizvoljnog sloga zahteva u
proseku 250 pokušaja. Ako bi se tih 500 slogova našlo u heš tabeli sa faktorom
popunjenosti 80% (što je blizu gornjoj granici tolerancije), broj pokušaja iznosio bi
svega oko 2!

3.3.4. Analiza lančanja


Neka je broj slogova u heš tabeli n. S obzirom na to da se slogovi ne nalaze u
samoj tabeli, nego u lancima vezanim za elemente u tabeli, faktor popunjenosti
q=n/SIZE može biti i veći od 1. Ako su slogovi uniformno distribuirani, ima smisla
poći od pretpostavke da su svi lanci u proseku iste dužine koja je, u tom slučaju,
jednaka q. Imajući u vidu da se traženje u pojedinačnim lancima vrši algoritmima koji
su u osnovi isti kao kod obične tabele (umesto povećavanja indeksa prate se
pokazivači), odmah zaključujemo da je prosečan broj pokušaja pri neuspešnom
traženju
Nchain = q
Za uspešno traženje upotrebićemo pristup korišćen u 3.2.1 (alternativno rešenje).
Prvo, za uspešno traženje neophodan je bar jedan pristup i to pristup traženom slogu.
Njemu može da prethodi 0,1,..,n-1 pristup slogovima koji su u istom lancu, za šta je
verovatnoća 1/SIZE. Dakle,
1 n 1 1 1 n 1
n -1
Uchain = 1+ 
SIZE i0 n
 i = 1+ 
n  SIZE i 1
i = 1+
2  SIZE
Ako je n>>1, a kako je n/SIZE=q, biće
Uchain ≈ 1+q/2

103
Dušan T. Malbaški - Algoritmi i strukture podataka

4. POLUDINAMIČKE STRUKTURE

Opšta odlika poludinamičkih struktura je ta da su osnovne operacije - pristup,


dodavanje i uklanjanje elementa - doduše dozvoljene, ali samo uz poštovanje
određenih restrikcija u pogledu pozicije elementa kojem se pristupa ili koji se uklanja,
odnosno mesta na koje se element dodaje. Ova ograničenja nisu posledica samo
objektivnih okolnosti, na primer fizičke realizacije. Ona se uvode prvenstveno zbog
toga da bi se strukturi podataka nametnulo određeno ponašanje. Najpoznatije
poludinamičke strukture su
 stek,
 red,
 dek i
 sekvenca.
Stek, red i dek su strukture specijalne namene, dok je sekvenca tipična kontejnerska
struktura.

4.1. STEK
Stek (engl. stack, u prevodu naređana gomila) jedna je od najčešće korišćenih
struktura podataka uopšte, prvenstveno zahvaljujući njegovoj ulozi u komunikaciji sa
potprogramima. Definiše se kao uređeni par
L = (S(L),r(L))
sa sledećim osobinama:
 struktura je linearna
 pristupa se isključivo prvom elementu
 uklanja se isključivo prvi element
 dodaje se isključivo ispred prvog elementa.
Šema steka prikazana je na slici 4.1.

104
Dušan T. Malbaški - Algoritmi i strukture podataka

...

Slika 4.1.

Očigledno, prvi element steka igra najvažniju ulogu, jer se sve tri osnovne operacije
obavljaju nad njim. Prvi element ima poseban naziv - vrh steka (engl. top). Poslednji
element takođe ima ime: zove se dno steka (engl. bottom), iako nema posebnu ulogu i
ne razlikuje se od ostalih elemenata. Pošto se sve tri operacije izvršavaju nad istim -
prvim - elementom, stek se ponaša kao da je u strukturi “vidljiv” samo vrh, pa se
često grafički prikazuje kao na slici 4.2.

Slika 4.2.

Glavna osobina steka, čak osobina zbog koje stek i postoji, proističe iz činjenice da će
element koji je, u vremenu, kasnije dodat u stek napustiti stek ranije. Radi se o osobini
koja se zove Last In First Out, ili skraćeno LIFO. Ključna reč za stek je, dakle,
akronim LIFO. Iza ove osobine krije se, ustvari, tzv. mehanizam privremeno
prekinutog posla. Zamislimo da je u toku rad na nekom poslu koji se prekida da bi se
obavio neki drugi posao, koji se takođe prekida da bi se obavio neki treći, pa četvrti
posao. Po završetku četvrtog posla nastavlja se sa trećim, po njegovom završetku sa
drugim i, najzad, sa prvim. Jasno je da se prilikom privremenog prekida bilo kojeg od
navedenih poslova njegovo stanje mora zapamtiti, da bi kasnije mogao da se nastavi
od tačke prekida. Lako zaključujemo da se stanja prekinutih poslova uspostavljaju
redosledom obrnutim od onog u kojem su zapamćeni prilikom pekidanja. Zanimljivo
je da je ovakva situacija česta i u životu i u programiranju (setimo se poziva
potprograma), čak mnogo češća nego što bi se to na prvi pogled moglo zaključiti.
Izuzetno čestu primenu steka ilustruje još i to što osnovne operacije imaju
posebna, opšteprihvaćena, imena. Operacija pristupa kod steka po pravilu nosi naziv
top. Operacija uklanjanja zove se pop, a operacija dodavanja push. U praksi (recimo u

105
Dušan T. Malbaški - Algoritmi i strukture podataka

asemblerskom jeziku) često se pojavljuje poseban oblik operacije pristupa-uklanjanja,


tzv. destruktivno očitavanje, koje predstavlja pristup praćen uklanjanjem (tj.
kombinacija top i pop). Asemblerska instrukcija POP predstavlja destruktivno
očitavanje.
Stek je, u suštini, vrlo jednostavna struktura i nema mnogo operacija, ne zato
što se ne bi mogle smisliti, nego zato što za njima nema potrebe. Osim top, pop i push,
neophodna je i operacija isEmpty provere da li u steku ima elemenata ili je prazan.
Operacija je filter za izvršavanje top i pop, jer se praznom steku ne može pristupiti,
niti se iz njega može ukloniti element. Ovim operacijama pridružuje se, zbog
sekvencijalnog načina realizacije, i operacija isFull provere da li je stek popunjen.
Ova pak operacija je filter za izvršavanje operacije push koja je izvodljiva samo ako u
steku ima mesta. Ponekad stek snabdevamo operacijom pražnjenja kojom se iz njega
uklanjaju svi elementi, mada ju je moguće izvesti i uzastopnom primenom operacije
pop. Operacija određivanja broja elemenata u steku moguća je, ali nije potrebna. S
obzirom na restrikcije, operacije traženja, redosledne obrade, sortiranja i sl.
besmislene su. Sve u svemu, standardni repertoar operacija nad stekom čine top, pop,
push, isEmpty i isFull. Dajemo njihove specifikacije, uz pretpostavku da je T tip
elemenata steka:
//prototip: T top(const Stack* stk);
//parametri: stk je adresa steka
//preduslov: stek je kreiran i nije prazan
//postuslov: -
//rezultat: element sa vrha steka

// prototip: T pop(Stack* stk);


//parametri: stk je adresa steka
//preduslov: stek je kreiran i nije prazan
//postuslov: element sa vrha steka je uklonjen
//rezultat: element koji je bio na vrhu steka pre uklanjanja

// prototip: void push(Stack* stk,T item);


//parametri: stk je adresa steka; item je element koji se dodaje
//preduslov: stek je kreiran i nije pun

106
Dušan T. Malbaški - Algoritmi i strukture podataka

//postuslov: element item je dodat na vrh steka


//rezultat: -

//prototip: int isEmpty(const Stack *stk);


//parametri: stk je adresa steka
//preduslov: stek je kreiran
//postuslov: -
//rezultat: 1 ako je stek prazan, 0 u suprotnom

//prototip: int isFull(const Stack *stk);


//parametri: stk je adresa steka
//preduslov: stek je kreiran
//postuslov: -
//rezultat: 1 ako je stek pun, 0 u suprotnom
U vezi sa logičkom strukturom steka treba još napomenuti da stek može biti
homogen (elementi skupa S(L) su iste vrste), ali da postoje i heterogeni stekovi gde
pojedinačni elementi nisu istovrsni. Jedna važna klasa stekova, tzv. programski stek,
je heterogen.

4.1.1. Fizička realizacija steka


U načelu - ali samo u načelu - stek se može realizovati i sekvencijalno i
spregnuto. Međutim, za razliku od većine drugih struktura podataka, stek nije
kontejnerska struktura, tj. ne služi za skladištenje podataka u svrhu obrade. Njegova
uloga je da posluži kao pomoćna struktura za privremeno čuvanje podataka na
principu LIFO, što znači da je ono što se od steka očekuje pre svega brzina i to je
razlog zbog kojeg se stek realizuje sekvencijalno. Zbog postojanja operacije
dodavanja push, sekvencijalna realizacija nosi sa sobom rizik prepunjenosti, kada je
memorijska zona popunjena do kraja i kada primena operacije push nije moguća.
Inače, pokušaj upisa u pun stek izaziva stanje koje se zove overflow37. Stek se
sekvencijalno realizuje na dva načina:

37
Uobičajeno je da se stanje u koje prazan stek ulazi pri pokušaju očitavanja praznog steka ili
uklanjanja zove underflow.

107
Dušan T. Malbaški - Algoritmi i strukture podataka

 niz se smešta u statičku memoriju i ne može mu se menjati veličina; stanje


prepunjenosti (overflow) rezultuje havarijskim prekidom programa
 interni niz sa elementima steka nalazi se na hipu; realizacija na hipu pruža
mogućnost da se u slučaju stanja overflow stek rekonfiguriše tako što se taj niz
povećava pomoću funkcije realloc. Napominjemo da su obe realizacije u
upotrebi.
Pošto realizacije nisu komplikovane, prikazaćemo ih obe. Pretpostavićemo da je stek
homogen38, tj. da su svi njegovi elementi tipa T. Pošto pun opis steka, pored
elemenata koji se nalaze u nizu s, sadrži i kapacitet c, kao i indeks vrha steka t,
pridružićemo mu deskriptor oblika
typedef struct {
int c;
int t;
T sCAPACITY; // CAPACITY je konstanta
} Stack;
ako je interni niz s u statičkoj memoriji, odnosno
typedef struct {
int c;
int t;
T *s;
} Stack;
ako je niz s na hipu.
Stek spada u strukture podataka koje nisu apriori u upotrebljivom stanju, nego
se moraju pre prve primene dovesti u početno stanje. Jasno je da je u tom stanju stek
prazan, te sledi da funkcija za kreiranje steka sa sadržajem u statičkoj memoriji treba
da postavi polje c na vrednost konstante CAPACITY (dužina niza s) i da indeks vrha
steka postavi na vrednost -1 koja znači da je stek prazan. Ako je niz s na hipu,
funkcija za kreiranje prvo treba da ga stvori pomoću malloc. Funkcija za kreiranje
steka imala bi oblik
void create(Stack* stk) {
stk->c=CAPACITY;

38
Ranije pomenutom programskom steku, koji je heterogen, posvetićemo poseban odeljak.

108
Dušan T. Malbaški - Algoritmi i strukture podataka

stk->t=-1;
}
kada je s u statičkoj memoriji, odnosno
void create(Stack* stk,int capacity) {
stk->c=capacity;
stk->t=-1;
stk->s=malloc(capacity*sizeof(T));
}
kada je niz s na hipu.
Ako su elementi steka na hipu, mora se dodati i funkcija čiji je zadatak da
oslobodi hip:
void destroy(Stack* stk) {
free(stk->s);
}
Funkcije za proveru da li je stek prazan (isEmpty), odnosno popunjen (isFull)
realizovaćemo sledećim kôdom:
int isEmpty(const Stack* stk) {
return stk->t<0;
}

int isFull(const Stack* stk) {


return stk->t==stk->c-1;
}
Primetimo da se u oba slučaja parametar stk prenosi po adresi. Razlog za to je
uniformnost primene svih funkcija, jer se sve pozivaju sa argumentom koji je adresa
steka. U suprotnom bi se neke funkcije pozivale sa argumentom koji je promenljiva, a
neke (one koje menjaju stanje steka) sa argumentom koji je adresa steka, što bi
otežavalo korišćenje. Pored toga, ako je sadržaj steka u statičkoj memoriji, prenosom
po adresi izbegava se kopiranje celog niza s.
Operacija pristupa top vrlo je jednostavna: ona vraća element sa vrha bez
menjanja stanja steka. Operacija izgleda ovako:
T top(const Stack* stk) {
return stk->s[stk->t];

109
Dušan T. Malbaški - Algoritmi i strukture podataka

}
Operaciju uklanjanja izvešćemo onako kako se obično realizuje u praksi, a to
je da odgovarajuća funkcija kao rezultat vrati uklonjeni element, dok je samo
uklanjanje bočni efekat. Bez obzira na to gde se nalazi sadržaj steka, u statičkoj
memoriji ili na hipu, uklanjanje ima sledeći kôd:
T pop(Stack* stk) {
return stk->s[stk->t--];
}
Konačno, operacija push za dodavanje elementa u stek takođe je ista za obe verzije:
void push(Stack* stk,T item) {
stk->s[++stk->t]=item;
}
Operacije očitavanja i dodavanja šematski su prikazane na slici 4.3.

pop s

push s

Slika 4.3.

4.1.2. Prevođenje izraza


Prilikom izračunavanja vrednosti izraza ubedljivo najveću poteškoću pri
formalizaciji predstavlja redosled izvršenja operacija koji se, u opštem slučaju,
niukoliko ne poklapa sa redosledom navođenja operacija i operanada. Razlog je u
tome što se operacije izvode u sekvenci koja je diktirana zagradama i prioritetom
operatorâ, a ne njihovom pozicijom u izrazu. Posmatrajmo izraz
x*y+(z-t/w)
Prioritet operatora kod većine programskih jezika nalaže da se prvo izvrše operacije u
zagradama, zatim množenje i deljenje te, konačno, sabiranje i oduzimanje. Očigledno,
između navođenja operatora odn. operanada i redosleda izvršavanja operacija nema

110
Dušan T. Malbaški - Algoritmi i strukture podataka

nikakve veze, što u velikoj meri otežava izradu algoritma za opšti slučaj. Već kod
jednostavnog izraza tipa navedenog, algoritam bi otpočeo fizički poslednjom
operacijom (deljenje), zatim bi izvršio prethodnu (oduzimanje), potom nekako
označio u originalnom izrazu da je ovaj deo obrađen i uklonio ga, pa prešao na drugi
kraj izraza i izvršio množenje i na kraju pronašao u sredini operaciju sabiranja te
izvršio i nju39. Pitanje je kako bi u opštem slučaju izgledao ovakav algoritam? U
praksi, tj. kod realnih kompajlera realnih programskih jezika, postoje razna rešenja, ali
se pokazuje da sva ona, na jedan ili drugi način, koriste stek.
Najpoznatiji način za prevođenje izraza (koristi ga većina kompajlera, mada ne
i većina kompajlera za C) jeste korišćenje tzv. poljske notacije, koja je dobila ime po
čuvenom poljskom matematičaru Janu Lukasiewitz-u. Postoje dve verzije poljske
notacije: prefiksna i postfiksna (inverzna poljska) notacija. Da bi se podvukla razlika,
obična notacija nosi naziv infiksna notacija. Posmatrajmo infiksni izraz sa jednim
binarnim operatorom, a op b, gde je op binarni operator. U prefiksnoj verziji, prvo se
navodi operator, pa onda levi, a zatim desni operand. U postfiksnoj notaciji prvo se
navodi levi operand, zatim desni i na kraju operator. U sledećoj tabeli prikazana su
četiri osnovna binarna aritmetička operatora u sve tri forme:

sabiranje oduzimanje množenje deljenje


infiksno A+B A-B A*B A/B
prefiksno +AB -AB *AB /AB
postfiksno AB+ AB- AB* AB/

U nastavku ćemo razmatrati postfiksnu, tj. inverznu poljsku notaciju. Pre svega,
redosled izvršavanja operacija određen je opštim pravilom po kojem prvo stupa u
dejstvo operator ispred kojeg postoji operand (za unarne operatore) ili oba operanda
(za binarne) sa već izračunatim vrednostima. Pri tome izraz se skenira sleva u desno.
Podizraz čija je vrednost izračunata uklanja se iz izraza i zamenjuje izračunatom
vrednošću. Na primer, izraz u postfiksnoj formi
AB3*+5H-/
računao bi se po sledećoj šemi:
1. pomnožiti B i 3

39
Kada se još doda da kod nesusednih operatora ne važi prioritet, zamešateljstvo postaje potpuno!

111
Dušan T. Malbaški - Algoritmi i strukture podataka

2. sabrati A sa rezultatom iz prethodne tačke


3. oduzeti H od 5
4. podeliti rezultat iz tačke 2 sa rezultatom iz tačke 3
Vidimo da bi u infiksnoj formi izraz imao oblik
A + B * 3 / (5 - H) .......... (4.1)
Prvo što se zapaža je važna činjenica da postfiksna notacija ne zahteva korišćenje
zagrada! Pored toga, izraz se skenira linearno, s leva u desno, a ne „na preskok“ kako
je to bio slučaj kod uvodnog primera. Posmatrajmo, na primer, postfiksni podizraz
AB3*+. Iza prva dva simbola (koji nisu operatori) sledi simbol 3 koji takođe nije
operator, a iza njega simbol * koji jeste operator te može biti primenjen na prethodna
dva operanda. Dakle, operand A treba privremeno odložiti da bi bio upotrebljen tek
kada se pojavi još jedan operator. Već sam termin „privremeno odložiti“ navodi na
pomisao o upotrebi steka. Osnovna ideja je: odložiti operand sve dok se ne steknu
uslovi za primenu operacije nad njim.
Neka su operatori i operandi izraza smešteni u niz postfix dužine n i neka je stk
stek koji je prilagođen tipu elemenata tog niza. Algoritam za izračunavanje vrednosti
izraza ima sledeći uopšten oblik:
for(i=0;i<n;i++)
switch(postfix[i]) {
case operand: push(&stk,postfix[i]);
break;
case unarni_operator: X = pop(&stk);
Z = postfix[i] X;
push(&stk,Z);
break;
case binarni_operator: X = pop(&stk);
Y = pop(&stk);
Z = Y postfix[i] X;
push(&stk,Z);
}
vrednost_izraza = pop(&stk);
Ako se ovaj algoritam primeni na izraz (4.1) uz vrednosti, na primer, A=8, B=4 i H=1
ostvariće se sledeći rezultati:

112
Dušan T. Malbaški - Algoritmi i strukture podataka

i postfix[i] stek
0 8 8
1 4 4,8
2 3 3,4,8
3 * 12,8
4 + 20
5 5 5,20
6 1 1,5,20
7 - 4,20
8 / 5

Najvažnije od svega je zapaziti da se niz postfix skenira samo jednom i to s leva u


desno, bez preskakanja ili povratka na već pregledano, u svega nekoliko algoritamskih
koraka.
Pored svih navedenih prednosti, postfiksna notacija ima i jedan očigledan
nedostatak: ljudi je ne upotrebljavaju. Neka čitalac zamisli programski jezik koji bi za
ispisivanje izraza zahtevao postfiksnu notaciju i neka razmisli o pouzdanosti tako
napisanih programa (na stranu psihička stabilnost programera)! Srećom, i za ovaj
problem postoji lek u vidu algoritma za automatsku transformaciju infiksnog izraza u
postfiksni oblik. I taj algoritam koristi stek i takođe se odvija u jednom prolazu.
Pretpostavićemo da se polazni izraz u infiksnoj formi nalazi u nizu infix dužine m čiji
su elementi prilagođeni prikazu operanada i operacija. Neka je priority funkcija koja
za dati operator vraća vrednost njegovog prioriteta, a stk stek prilagođen elementima
niza infix. Rezultat primene algoritma je postfiksna forma realizovana kao niz postfix
dužine n. Idejno rešenje izgleda ovako:
n=-1;
for(j=0;j<m;j++)
switch(infix[j]) {
case operand: postfix[++n]=infix[j]; break; //preneti operand u izlazni niz
case znak ‘(‘: push(&stk,infix[j]); break;
case operator: /*izvuci iz steka sve operatore istog ili viseg nivoa
i upisati ih u izlazni niz postfix*/

113
Dušan T. Malbaški - Algoritmi i strukture podataka

while(!isEmpty(&stk)&&(priority(top(&s))>=priority(infix[j])))
postfix[++n]=pop(&stk);
push(&stk,infix[j];
break;
case znak ‘)’: /*izvuci iz steka sve operatore izmedju leve i desne zagrade i
upisati ih u izlazni niz postfix*/

while(top(&stk)!=’(‘) postfix[++n]=pop(&stk);
pop(&stk); //ukloniti znak ‘(‘ iz steka
} //kraj switch
while(!isEmpty(&stk) postfix[++n]=pop(&stk);

4.1.3. Programski stek


Pozivanje potprograma predstavlja jedan od ključnih mehanizama u
algoritamskim jezicima, procedurnim i objektnim. Šta se, u stvari, dešava na mestu
gde je pozvan neki potprogram? Odgovor je poznat svima koji su se susretali sa
programiranjem: pozivajuća rutina se privremeno prekida i prelazi se na izvršavanje
potprograma, a kada se on završi nastavlja se sa izvršenjem pozivajuće rutine od
mesta na kojem je prekinuta. Osnovno pitanje u ovom kontekstu jeste „gde se nalazi
podatak o tom mestu“, gde „mesto“ u ovom slučaju znači adresu od koje se nastavlja
izvršenje pozivajuće rutine (tzv. adresa povratka).
U starim programskim sistemima poput fortrana i kobola potprogrami su imali
sopstveni memorijski prostor namenjen čuvanju vrednosti parametara i lokalnih
promenljivih. Adresa povratka bila je deo tog memorijskog prostora i prilikom poziva,
neposredno pre nego što će početi izvršavanje potprograma, pozivajuća rutina je na tu
lokaciju smeštala adresu povratka. Na slici 4.4. prikazan je način pozivanja tako
organizovanih potprograma.

114
Dušan T. Malbaški - Algoritmi i strukture podataka

pozivajuća rutina potprogram

Ap

Ap

Slika 4.4.

U sklopu poziva potprograma, adresa povratka Ap smešta se, recimo, na početak


memorijskog prostora potprograma (tako je bilo kod verzije IV fortrana). Izvršavanje
potprograma okončavalo se indirektnim skokom na adresu Ap, što je prikazano
isprekidanim linijama. Lokalne promenljive i argumenti smeštani su u memorijski
prostor potprograma. Metod direktnog povezivanja bio je veoma nefleksibilan
(između ostalog, iz jasnih razloga potprogram nije mogao da pozove samog sebe,
rekurzivnim pozivom) i napušten je prelaskom sa kompozitnog na strukturirano
programiranje, tj. napuštanjem fortrana i kobola kao dominantnih programskih jezika.
Daleko bolji mehanizam predviđa da se adresa povratka izdvoji u posebnu,
pomoćnu strukturu iz koje se čita prilikom povratka u pozivajuću rutinu. Posmatrajmo
programski sistem na slici 4.5. Rutina P0 poziva rutinu P1 koja poziva P2 koja poziva
P3.

P0 P1 P2 P3

A0 A1 A2

A2
A1 A1
Slika 4.5. A0 A0 A0

Prilikom svakog poziva, adresa povratka upisuje se u pomoćnu strukturu, a po


završetku potprograma čita se iz te strukture redosledom obrnutim od redosleda
upisa. Vidimo da se od te pomoćne strukture očekuje LIFO osobina, tj. radi se o

115
Dušan T. Malbaški - Algoritmi i strukture podataka

steku. Uvođenje steka kao posrednika bilo je značajno poboljšanje u odnosu na


direktno povezivanje, a čija je prva - i ne najvažnija - posledica bila pojava
rekurzivnih funkcija40. Od vremena uvođenja, uloga interne memorije potprograma
stalno se smanjivala i prebacivala na stek, sve dok interna memorija nije u potpunosti
ukinuta. U modernim programskim jezicima na steku se nalaze, pored adrese povratka
i sve lokalne promenljive potprograma, vrednosti svih argumenata korišćenih pri
pozivu, kao i rezultat (ako ga ima). Jednom rečju, na steku je snimljeno kompletno
stanje potprograma, tako da se uvek može restaurisati kada se izvršavanje
potprograma privremeno prekine. Taj posrednički stek nazvaćemo programski stek
(engl. call stack).
Pre svega, na programskom steku nalaze se vrednosti argumenata i lokalnih
promenljivih, a nalazi se i adresa povratka, što znači da je programski stek heterogen,
jer ti podaci nisu istog tipa. Osim LIFO osobine programski stek nema mnogo
zajedničkih osobina sa homogenim stekovima. Pre svega, na njemu se u opštem
slučaju nalaze podaci o stanjima različitih potprograma, a ti podaci razlikuju se i po
broju i po tipu što znači da za svaku takvu grupu podataka (tzv. stek-frejm, engl. stack
frame) moraju biti označene dve pozicije: početak i kraj. Za ovo se koriste dve adrese:
adresa vrha steka, sp (stack pointer) i adresa početka stek-frejma koji pripada
poslednjem pozivu, bp (base pointer). Podaci koji se nalaze između sp i bp opisuju
stanje potprograma i sadrže lokalne promenljive, adresu povratka i vrednosti
argumenata. Pored ovog, skrećemo pažnju na to da se stek, kako trenutno stoje stvari,
proteže od viših adresa ka nižim, slika 4.6.

bp
lokalne
promenljive,
argumenti, stek frejm
adresa povratka,
prethodni bp
sp

ka nižim Slika 4.6.


adresama

40
U algoritamskim (imperativnim) jezicima rekurzija se prvi put pojavila u jeziku algol 60, koji je bio
daleko ispred svog vremena. Paskal je direktni naslednik algola, što u velikoj meri važi i za C.

116
Dušan T. Malbaški - Algoritmi i strukture podataka

Napominjemo još i to da je prikazano rešenje idejno, a da se detalji razlikuju od


sistema do sistema (ima čak i programskih stekova koji su realizovani spregnuto) i da
podležu izmenama. U modernom programiranju, statička memorija programa
konsekventno gubi na značaju u korist steka s jedne i hipa s druge strane, tako da
programi pisani na programskom jeziku java uopšte ne poseduju statičku memoriju!
Po završetku potprograma, stek pointer se spušta41 na početak frejma, a bp na početak
prethodnog frejma memorisan u posmatranom frejmu, tako da je kompletan frejm
označen kao slobodan. Ovo je, inače, razlog zbog kojeg lokalne promenljive
potprograma doslovno nestaju po njegovom završetku.
Najzad, izmeštanje interne memorije iz potprograma na stek rezultuje i
uštedom u njenom utrošku. S jedne strane, potprogrami postaju fizički manji jer u
svom adresnom prostoru sadrže samo kôd, a s druge lokalni podaci zauzimaju
memoriju (tj. deo steka) samo dok su neophodni, tj. dok je potprogram aktivan. Kada
se potprogram završi prostor na steku se oslobađa i ostaje na raspolaganju za druge
pozive potprograma.

4.1.4. Rekurzija
Korišćenje programskog steka za memorisanje stanja potprograma stvorilo je
uslove za implementaciju rekurzivnih potprograma, tj. potprograma koji pozivaju
sami sebe. Naime, ako se vratimo na sliku 4.5 videćemo da je svejedno da li su rutine
P0, P1, P2 i P3 različite ili je u pitanju ista rutina, jer svaki poziv stvara novi stek
frejm, nezavisan od prethodnih. Ovo, inače, nije bilo moguće kod direktnog
povezivanja jer je u okviru memorijskog prostora potprograma postojala jedna jedina
lokacija za smeštanje adrese povratka.
Rekurzivni potprogrami sintaksno se ne razlikuju od drugih potprograma.
Međutim, njihova struktura je specifična, imajući u vidu da se sukcesivni pozivi istog
potprograma ne mogu produžavati unedogled - rekurzija se negde mora zaustaviti.
Načelno, rekurzivni potprogram treba da ima dve komponente koje se različito
raspoređuju, saobrazno konkretnom slučaju. Da bismo objasnili način funkcionisanja
rekurzivne funkcije posmatraćemo školski primer računanja faktorijela. Rekurzivni
sistem za ovu svrhu ima sledeći izgled:
0! = 1

41
precizinje, podiže, s obzirom na smer širenja steka

117
Dušan T. Malbaški - Algoritmi i strukture podataka

n! = n(n-1)!
Neka se funkcija za rekurzivno računanje faktorijela zove fct. Sada se rekurzivni
sistem pretvara u
fct(0) = 1
fct(n) = n*fct(n-1)
Pomenute komponente rekurzivne funkcije su:
 korak rekurzije; to je osnovni blok koji se izvršava pri svakoj rekurziji, pri
čemu se uslovi izvršavanja menjaju; u našem slučaju to je fct(n)=n*fct(n-1)
 pravilo zaustavljanja; u matematici to je baza rekurzije, u programiranju to je
blok fct(0)=1.
Još nešto: kada matematički razvijamo rekurzivni sistem, počinjemo od baze i
ponavljamo rekurzivnu formulu dok se ne dostigne granica; kod rekurzivnih funkcija
u programiranju, postupak se odvija u suprotnom smeru i završava bazom koja igra
ulogu kriterijuma zaustavljanja. Rekurzivna funkcija za računanje faktorijela je
long fct(int n) {
return (n>0) ? n*fct(n-1) : 1;
}
Rekurzivni sistem za računanje npr. 4! odvijao bi se ovako:
0! = 1
1! = 10! = 1
2! = 21! = 2
3! = 32! = 6
4! = 43! = 24
Funkcija fct dejstvuje u suprotnom smeru:
1. otpočinje se sa fct(4) i stiže do koraka 4*fct(3); na ovom mestu izvršavanje
fct(4) se privremeno prekida, stanje se smešta na programski stek i otpočinje
sa izvršavanjem fct(3)
2. na istom mestu, ovog puta 3*fct(2), izvršavanje se ponovo prekida, stanje
smešta na stek i otpočinje sa izvršavanjem fct(2)
3. ponovo isto: na mestu 2*fct(1) - prekid i prelazak na fct(1)
4. još jednom isto: na mestu 1*fct(0) - prekid i prelazak na fct(0)
5. konačno kraj rekurzije: fct(0) vraća rezultat 1

118
Dušan T. Malbaški - Algoritmi i strukture podataka

6. sada se nastavlja sa prekinutom fct(1), ona se završava i rezultat 1*1=1


ostavlja na steku
7. završava se fct(2), vraća se rezultat 2*1=2
8. završava se fct(3), vraća se rezultat 3*2=6
9. završava se fct(4); rezultat 4*6=24.
Opšte osobine rekurzivnih funkcija (potprograma) mogu se svesti na dve očigledne:
 fizički (po broju naredbi) su kratke
 elegantne su (i u programiranju ima estetike!)
Da bismo se uverili u ovo posmatrajmo tzv. iterativnu izvedbu faktorijela. Iterativne
verzije nose taj naziv jer, umesto rekurzivnih poziva, za formiranje rezultata koriste
cikluse. Za naš primer, iterativna verzija je bazirana na matematičkoj formuli
n! = 1 ako je n=0
n
n! =  i ako je n>0
i 1

Evo i funkcije:
long fct(int n) {
long p;
if(!n) return 1;
else for(p=1;n>1;n--) p*=n;
return p;
}
Iterativno rešenje je fizički duže i deluje pomalo nezgrapno u odnosu na rekurzivno.
Ali je bolje! Naime, dobro je poznato da fizički obim kôda izražen brojem naredbi
nije ni u kakvoj korelaciji sa brzinom izvršenja, pa počesto ni sa obimom zauzete
memorije. S druge strane, svaki rekurzivni poziv fct zahteva pripremu steka sa
prenosom parametra i adrese povratka, te sve to zajedno može da traje duže od
iterativne varijante. Tome treba dodati i cenu elegancije rekurzivnih funkcija: one su,
po pravilu, nejasne i podložne greškama, tako da zahtevaju vrlo iscrpno testiranje. Sve
u svemu, u odgovoru na pitanje “iterativno ili rekurzivno”, Virt u 3 navodi da se
“rekurzivna rešenja koriste za rekurzivne probleme”, što samo na prvi pogled deluje
trivijalno, jer pravi smisao jeste “koristiti iterativno rešenje ako postoji”. Videćemo da
kod struktura podataka, naročito nelinearnih, postoji niz postupaka za koje iterativna

119
Dušan T. Malbaški - Algoritmi i strukture podataka

rešenja jednostavno ne postoje. Jedan primer smo već razmatrali: quicksort je tipičan
rekurzivni problem.
Iterativna i rekurzivna funkcija za računanje faktorijela ne razlikuju se u
tolikoj meri da bi rekurzivno rešenje unapred bilo odbačeno kao neprihvatljivo sporo.
U oba slučaja vremenska kompleksnost je reda On. Postoji, međutim, jedan drugi
primer gde je razlika između iterativne i rekurzivne verzije tolika da se rekurzivna
verzija uopšte ne može koristiti! Radi se o funkciji za određivanje Fibonačijevih42
brojeva. Fibonačijev niz43 definiše se sa
F(0) = 0
F(1) = 144
F(n) = F(n-1) + F(n-2), n>1
Nekoliko članova Fibonačijevog niza bili bi
0, 1, 1, 2, 3, 5, 8, 13, 21, 34 ...
Fibonačijev niz pokazuje mnoštvo zanimljivih osobina i u vezi je sa tzv. „zlatnim
presekom“, što nije od interesa za ovo izlaganje. Elem, rekurzivna funkcija za
računanje Fibonačijevih brojeva deluje vrlo privlačno:
long F(int n) {
return (n<=1) ? n : F(n-1)+F(n-2);
}
Za manje vrednosti n funkcija će generisati rezultat u razumnom vremenu. Međutim,
ako pozovemo F(100) odjednom se dešavaju čudne stvari: funkcija kreće sa
izvršavanjem, ali nikako da se završi! Da bismo razjasnili razlog za dugotrajan
postupak procenićemo vremensku kompleksnost. S obzirom na svrhu analize
procenićemo donju granicu kompleksnosti, tj. n. Prvo
T(n) = a za n1
T(n) = T(n – 1) + T(n – 2) + b za n>1
gde su a i b konstante. Ako razvijemo T(n) sledi
T(n) = T(n - 1) + T(n - 2) + b
≥ T(n - 2) + T(n-2) + b
= 2T(n - 2) + b

42
Leonardo Pisano Bigollo znan kao Fibonacci (oko 1170 – oko 1250), čuveni italijanski matematičar
43
Fibonači je, kažu, do ovog niza došao proučavajući razmnožavanje kunića.
44
Neki usvajaju F(0)=F(1)=1, što nije od naročite važnosti za ovo razmatranje

120
Dušan T. Malbaški - Algoritmi i strukture podataka

= 2[T(n - 3) + T(n - 4) + b] + b
 2[T(n - 4) + T(n - 4) + b] + b
= 22T(n - 4) + 2b + b
= 22[T(n - 5) + T(n - 6) + b] + 2b + b
≥ 23T(n – 6) + (22 + 21 + 20)b
...
 2kT(n – 2k) + (2k-1 + 2k-2 + . . . + 21 + 20)b
= 2kT(n – 2k) + (2k – 1)b
Baza rekurzije biće dostignuta kada je n–2k=2, odakle k=(n-2)/2. Prema tome,
T(n) ≥ 2(n – 2)/2 T(2) + [2(n - 2)/2 – 1]b
= (b+a)2(n–2)/2 – b
= [(b+a)/2]*2n/2 – b
Dakle,
T(n)= 2n.
Drugim rečima, radi se o eksponencijalnoj složenosti sa kojom se današnji računari
još uvek ne mogu nositi! Štaviše, poznato je da se T(n) za veće vrednosti n može
aproksimirati sa
1
T(n) ≈ 1.618 n
5
odakle sledi da je T(100) ≈3.51020. Ilustracije radi, ako bi svaki poziv trajao samo
jednu mikrosekundu, na rezultat bi trebalo pričekati oko 10 miliona godina. Iterativna
varijanta ne deluje naročito privlačno:
long F(int n) {
long F0,F1,F2; int i;
if(n<=1) return n;
else {
for(F0=0,F1=1,i=2; i<=n; i++) { F2=F1+F0; F0=F1; F1=F2;}
return F2;
}
}
ali se lako dokazuje da je vremenska složenost ove varijante svega On.
Pomenimo, na kraju, i jednu specijalnu vrstu rekurzije, tzv. prateću rekurziju
(engl. tail recursion), koja omogućuje izvesnu uštedu u memoriji na steku (ne i uštedu

121
Dušan T. Malbaški - Algoritmi i strukture podataka

u vremenu). Rekurzivne funkcije sa pratećom rekurzijom imaju osobinu da je


samoaktiviranje poslednja aktivnost u sklopu njihovog izvršenja. Odmah skrećemo
pažnju na to da rekurzivno samoaktiviranje ne mora da bude i fizički poslednja
naredba: sve što je potrebno jeste da se iza rekurzivnog poziva ne dešava ništa.
Posmatrajmo strukturu jedne takve funkcije. Rekurzivni poziv smestili smo
fizički na kraj, ali samo u svrhu lakšeg razumevanja. Takođe, pravilo zaustavljanja
predstavili smo jednom naredbom if:
Tip rekFunkcija(T1 p1,T2 p2,...,Tn pn) {
if(uslov) {
blok_naredbi
rekFunkcija(a1,a2,...,an);
}
}
Funkcija se završava kada uslov više nije ispunjen, a vidimo da je samoaktiviranje
rekFunkcija naredba koja se izvršava poslednja. Prilikom rekurzivnog poziva stvoriće
se novi stek-frejm i u njega uneti odgovarajuće vrednosti lokalnih promenljivih i
adresa povratka, iako se te vrednosti nigde ne koriste, jer se po izvršenju rekurzivnog
poziva ne dešava ništa. Lako možemo zapaziti da je gornji kôd ništa drugo nego još
jedan poziv funkcije rekFunkcija, ovog puta sa argumentima a1,a2,...,an. Pošto u
nastavku nema promena stanja, ovaj rekurzivni poziv možemo zameniti još jednim
izvršavanjem funkcije rekFunkcija sa izmenjenim vrednostima argumenata.
Prerađeni, ekvivalentni, kôd izgledao bi ovako:
Tip rekFunkcija(T1 p1,T2 p2,...,Tn pn) {
PONOVI: if(uslov) {
blok_naredbi koji koristi p1,...,pn
p1=a1; p2=a2; ...; pn=an;
goto PONOVI;
}
}
odnosno, posle eliminisanja naredbe goto:
Tip rekFunkcija(T1 p1,T2 p2,...,Tn pn) {
while(uslov) {
blok_naredbi koji koristi p1,...,pn

122
Dušan T. Malbaški - Algoritmi i strukture podataka

p1=a1; p2=a2; ...; pn=an;


}
}
Kao primer, preradićemo rekurzivnu funkciju _qsort koja učestvuje u realizaciji
quicksort-a tako da eliminišemo poslednji rekurzivni poziv. Prvo, funkcija je imala
oblik
void _qsort(int a[],int L,int R) {
int M; register int i;
if(L>=R) return;
for(M=L,_G=a[L],i=L+1;i<=R;i++)
if(a[i]<_G){_tmp=a[++M];a[M]=a[i];a[i]=_tmp;}
_tmp=a[M];a[M]=a[L];a[L]=_tmp;
_qsort(a,L,M-1); _qsort(a,M+1,R);
}
Radi lakšeg praćenja, prvo ćemo je prikazati sa strukturom iz uvodnog dela (može se i
bez toga):
void _qsort(int a[],int L,int R) {
int M; register int i;
if(L<R) {
for(M=L,_G=a[L],i=L+1;i<=R;i++)
if(a[i]<_G){_tmp=a[++M];a[M]=a[i];a[i]=_tmp;}
_tmp=a[M];a[M]=a[L];a[L]=_tmp;
_qsort(a,L,M-1);
prateća rekurzija
_qsort(a,M+1,R);
}
}
Eliminisanjem prateće rekurzije na napred opisani način, dobijamo
void _qsort(int a[],int L,int R) {
int M; register int i;
while(L<R) {
for(M=L,_G=a[L],i=L+1;i<=R;i++)
if(a[i]<_G){_tmp=a[++M];a[M]=a[i];a[i]=_tmp;}
_tmp=a[M];a[M]=a[L];a[L]=_tmp;

123
Dušan T. Malbaški - Algoritmi i strukture podataka

_qsort(a,L,M-1);
L=M+1;
}
}
Podvucimo da dalja eliminacija rekurzivnih poziva više nije moguća, jer preostali
poziv _qsort više nije poslednja aktivnost u funkciji (iza nje je naredba L=M+1).

4.2. RED
Na prvi pogled, struktura sa nazivom red (engl. queue, čita se „kjū“) tek
neznatno se razlikuje od steka: u pitanju je samo mesto dodavanja. Međutim, odmah
treba naglasiti da je red struktura koja se po ponašanju potpuno razlikuje od steka i
koristi se u sasvim drugom kontekstu. Red se definiše kao uređeni par
F = (S(F),r(F))
sa sledećim osobinama:
 struktura je linearna
 pristupa se isključivo prvom elementu
 uklanja se isključivo prvi element
 dodaje se isključivo iza poslednjeg elementa.
Kako vidimo, ovde postoje dva karakteristična elementa: prvi koji se zove još i
početak ili čelo reda i poslednji što nosi nativ kraj reda ili začelje. Red je homogena
struktura. Po osobinama operacija dodavanja i uklanjanja vidimo da se ponašanje reda
svodi na činjenicu da će element koji je ranije ušao u red, ranije biti i uklonjen. Ova
glavna osobina reda nosi naziv FIFO od akronima First In First Out. Ovakvo
ponašanje ispoljava red ispred šaltera, te se kao ključna reč za strukturu reda može
prihvatiti čekanje (što sa ponašanjem steka, očigledno, nema nikakve veze). Šema
izvođenja osnovnih operacija prikazana je na slici 4.7.

...

Slika 4.7.

124
Dušan T. Malbaški - Algoritmi i strukture podataka

Operacija pristupa obično se zove front ili ima prefiks get, operacija dodavanja ima
prefiks put ili append, a operacija uklanjanja prefiks remove ili delete. Operacije
dodavanja i uklanjanja zovu se još i enqueue i dequeue. Pored njih, a s obzirom na to
da su operacije uklanjanja i dodavanja definisane, postoje i operacije provere da li je
red prazan, odnosno da li je red pun. To su operacije isEmpty i isFull. Isto kao i stek, i
red se mora kreirati (operacija create) te, ako koristi hip, mora se i uništiti (operacija
destroy). Ni red nema mnogo dodatnih operacija: one na koje se nailazi su pražnjenje
reda clear i operacija određivanja broja elemenata (dužine) reda, size. Specifikacija
tipičnih operacija je (T je tip elemenata reda)
//prototip: void create(Queue* que);
//parametri: que je adresa reda
//preduslov: -
//postuslov: red je kreiran
//rezultat: -

//prototip: void destroy(Queue* que);


//parametri: que je adresa reda
//preduslov: red je kreiran
//postuslov: red je uništen
//rezultat: -

//prototip: T front(const Queue* que);


//parametri: que je adresa reda
//preduslov: red je kreiran i nije prazan
//postuslov: -
//rezultat: element sa čela reda

// prototip: T removeItem(Queue* que);


//parametri: que je adresa reda
//preduslov: red je kreiran i nije prazan
//postuslov: element sa čela reda je uklonjen
//rezultat: element koji je bio na čelu reda pre uklanjanja

125
Dušan T. Malbaški - Algoritmi i strukture podataka

// prototip: void putItem(Queue* que,T item);


//parametri: que je adresa reda; item je element koji se dodaje
//preduslov: red je kreiran i nije pun
//postuslov: element item je dodat na začelje reda
//rezultat: -

//prototip: int isEmpty(const Queue *que);


//parametri: que je adresa reda
//preduslov: red je kreiran
//postuslov: -
//rezultat: 1 ako je red prazan, 0 u suprotnom

//prototip: int isFull(const Queue *que);


//parametri: que je adresa reda
//preduslov: red je kreiran
//postuslov: -
//rezultat: 1 ako je red pun, 0 u suprotnom

//prototip: int size(const Queue *que);


//parametri: que je adresa reda
//preduslov: red je kreiran
//postuslov: -
//rezultat: broj elemenata (dužina) reda

//prototip: void clear(Queue *que);


//parametri: que je adresa reda
//preduslov: red je kreiran
//postuslov: red je prazan
//rezultat: -
Fizička realizacija reda može biti sekvencijalna ili spregnuta, ali za razliku od
steka, spregnuta realizacija nije samo teorijska mogućnost, nego i realna praksa.
Počećemo od sekvencijalne realizacije.

126
Dušan T. Malbaški - Algoritmi i strukture podataka

4.2.1. Sekvencijalna realizacija reda


Svrha sekvencijalne realizacije jeste da obezbedi brze funkcije kao
implementaciju operacija. S obzirom na to da je red struktura koja se mora realizovati
programskim sredstvima (programski jezici ne podržavaju red), počećemo od
deskriptora koji programer mora da napravi sam. Kao i kod steka, elementi reda mogu
biti smešteni u niz koji je u statičkoj memoriji ili pak u niz koji je na hipu. Pošto je
razlika minimalna (i videli smo kako to izgleda kod steka), opisaćemo samo ovo
drugo rešenje. Deskriptor je oblika
typedef struct {
int c, getPos,putPos;
T *q;
} Queue;
gde je:
 T tip elemenata reda (tj. tip informacionog sadržaja)
 c dužina niza u koji se smeštaju elementi
 q niz sa elementima koji je na hipu
 getPos mesto (indeks niza q) sa kojeg se čita element (dakle, indeks čela reda)
 putPos mesto (indeks niza q) na koje se upisuje element pri dodavanju;
skrećemo pažnju da je to mesto pozicija iza trenutnog začelja
Elementi se ređaju od nižih indeksa ka višim (sic!), tako da se očitava i briše
prvi zauzeti element, a uklanja poslednji. Ova realizacija ima ozbiljnu manjkavost
koja bi, ako je ne uklonimo, bila dovoljna da se odustane od čitave ideje. Radi se o
pojavi tzv. "lažne prepunjenosti" reda. Posmatrajmo stanje reda prikazano na slici 4.8
gore.
0 1 2 c-1

... x y

getPos putPos

0 1 2 c-1

... x y z

getPos putPos

Slika 4.8.

127
Dušan T. Malbaški - Algoritmi i strukture podataka

Kao što je predviđeno u deskriptoru, oznaka getPos odnosi se na indeks čela, a putPos
na mesto iza začelja koje je predviđeno za upis pri izvođenju operacije dodavanja.
Kako se vidi na istoj slici, na donjem crtežu, već posle jednog dodavanja elementa
npr. z, poslednja raspoloživa lokacija u nizu je zauzeta, te bi novi pokušaj dodavanja
rezultovao porukom o prepunjenosti memorijskog prostora! To, očigledno, nije tačno
jer na drugom kraju reda ima mesta, samo se ona ne mogu popunjavati elementarnim
algoritmom za dodavanje. Da bi se ovaj problem rešio, red se realizuje cirkularno
(kružno) tako da se, kada se stigne do kraja memorijskog prostora, sa dodavanjem
nastavlja na početku (naravno, ako ima mesta). Inače, cirkulisanje po nizu realizuje se
primenom operacije ostatka pri deljenju %. Umesto direktnog povećanja nekog
indeksa i za jedan operacijom i++, cirkularno uvećanje obezbeđuje se operacijom
i=(i+1)%c. Cirkularno rešenje prikazano je na slici 7.12 levo.
Nažalost, ovo nije dovoljno jer se pojavljuje novi problem: (stvarno) popunjen
red ne može se razlikovati od praznog reda jer u oba slučaja važi getPos==putPos.
Ova situacija prikazana je na slici 4.9 u sredini i desno..

putPos getPos pun red prazan red

cirkularni red

Slika 4.9.

Za ovaj problem ima više rešenja, a ovde ćemo opisati jedno, u svoje vreme prilično
originalno. Ideja se sastoji u tome da se žrtvuje jedna lokacija u koju nije dozvoljen
upis, što znači da je memorijski prostor reda za jednu lokaciju veći od stvarnog
kapaciteta. Na taj način ostvaruje se razlika između praznog i punog reda. Dok je kod
praznog reda i dalje getPos==putPos, pun red prepoznaje se po tome što su pozicije
getPos i putPos susedne (u cirkularnom smislu), slika 4.10.

pun red prazan red

Slika 4.10.

128
Dušan T. Malbaški - Algoritmi i strukture podataka

Kreiranje i destruisanje reda obavlja se funkcijama create i destroy čije smo


specifikacije naveli ranije:
void create(Queue* que,int capac) {
que->c=capac+1; --stvarna duzina je za 1 veca od deklarisanog kapaciteta
que->getPos= que->putPos= 0;
que->q=malloc(que->c*sizeof(T));
}

void destroy(Queue* que) {


free(que->q);
}
Vidimo da je parametar funkcije create stvarni kapacitet reda, dok se, u okviru
kreiranja, zauzima jedno mesto više, što je inače nevidljivo za onog ko koristi red, a u
skladu sa principom skrivanja informacija.
Funkcije isEmpty i isFull koje su predstavljale problem, sada to više nisu, kao što
vidimo iz kôda:
int isEmpty(const Queue* que) {
return que->getPos==que->putPos;
}

int isFull(const Queue* que) {


return que->getPos==(que->putPos+1)%que->c;
}
Funkcija front vraća kao rezultat element sa čela reda, ne menjajući, pritom, njegovo
stanje:
T front(const Queue* que) {
return que->q[que->getPos];
}
Funkcija za uklanjanje elementa, po ustaljenoj praksi, vraća kao rezultat uklonjeni
element. Samo pak uklanjanje obavlja se cirkularnim uvećavanjem vrednosti getPos
za 1, čime se oslobađa mesto na kojem je bio čeoni element.
T removeItem(Queue* que) {
T item;

129
Dušan T. Malbaški - Algoritmi i strukture podataka

item=que->q[que->getPos];
que->getPos=(que->getPos+1)%que->c;
return item;
}
Dodavanje elementa vrši se na indeksu putPos, posle čega se vrednost putPos
cirkularno uvećava za 1:
void putItem(Queue* que,T item) {
que->q[que->putPos]=item;
que->putPos=(que->putPos+1)%que->c;
}
Funkcija za računanje dužine reda ima sledeći oblik:
int size(const Queue* que) {
return (que->putPos>=que->getPos) ?
que->putPos-que->getPos :
que->c+que->putPos-que->getPos;
}
Na kraju, pražnjenje reda je vrlo jednostavno:
void clear(Queue* que) {
que->putPos=que->getPos;
}
Napominjemo da se problem nerazlikovanja praznog i punog reda pri
cirkularnoj realizaciji može rešiti i drukčije: praćenjem dužine reda. U deskriptor
treba uneti dodatno polje, recimo int n koje uvek sadrži aktuelnu dužinu reda. Pri
kreiranju i pražnjenju, polje n postavlja se na vrednost 0. Prilikom svakog uklanjanja,
vrednost polja se smanjuje za 1, a prilikom dodavanja n se povećava za 1. U stanju
praznog reda važi n=0, a za pun red n=c, pri čemu je c sada stvarni kapacitet reda (ne
uvećava se za 1 pri kreiranju). Funkcija size sada vraća vrednost polja n. Inače, cena
ovakve realizacije je izvesno produženje operacija uklanjanja i dodavanja i to zbog
ažuriranja vrednosti n. Programsku realizaciju prepuštamo čitaocu.

4.2.2. Spregnuta realizacija reda


Red se može realizovati i sprezanjem elemenata koji su na hipu a, za razliku
od steka, spregnuta implementacija se koristi i u praksi. Ovaj način realizacije

130
Dušan T. Malbaški - Algoritmi i strukture podataka

praktično eliminiše eventualnu prepunjenost, jer se smatra da na hipu ima dovoljno


mesta. Elementi koji su na hipu proširuju se pokazivačem tako da se unutar svakog
elementa nalazi pokazivač na sledeći, tj. relacija susedstva ostvaruje se softverski.
Izgled takvog elementa (zovu ga i čvor) prikazan je na slici 4.11.

sadržaj
pokazivač na sledeći
elementa

Slika 4.11.

U deskriptoru se nalazi pokazivač na čelo reda sa zadatkom da obezbedi pristup


samom redu. Pored njega, efikasna implementacija podrazumeva da se u deskriptoru
nalazi i pokazivač na začelje reda. Razlog je operacija dodavanja koja bi, da nema tog
pokazivača, zahtevala prethodni prolazak kroz ceo red da bi se pronašao poslednji
element, jer njegov pokazivač treba usmeriti na novododati element. Šema spregnute
realizacije reda prikazana je na slici 4.12.

first
last ...

Slika 4.12.

Pokazivač first u deskriptoru pokazuje na čelo reda (zbog pristupa i uklanjanja), a


pokazivač last na začelje (zbog dodavanja). Ukoliko postoji potreba računanja dužine
reda, pravilo je da se u deskriptoru nađe polje, recimo n, u kojem se prati dužina, da bi
se izbegao prolazak kroz ceo red u svrhu prebrojavanja.
Čvor reda (slika 4.11) prikazaćemo kao tip Node gde je informacioni sadržaj
prikazan kao polje item tipa T:
typedef struct node {
T item; //sadrzaj elementa
struct node* next; //pokazivac na sledeci
} Node;
Deskriptor reda je slog tipa
typedef struct {
int n; //duzina reda

131
Dušan T. Malbaški - Algoritmi i strukture podataka

Node *first,*last; //pokazivaci na celo i zacelje


} Queue;
Funkcija create za kreiranje reda neznatno se razlikuje od verzije za sekvencijalnu
realizaciju, jer nema potrebe za parametrom capac što predstavlja kapacitet:
void create(Queue* que) {
que->first=que->last=NULL;
que->n=0;
}
Za funkcijom destroy nema potrebe, mada se može napraviti i to tako što će, recimo,
isprazniti red. Očitavanje čela reda bez menjanja njegove sadržine obavlja funkcija
front čiji je zadatak u spregnutoj verziji da, preko pokazivača first deskriptora,
pristupi prvom po redu elementu i vrati informacioni sadržaj item:
T front(const Queue* que) {
return que->first->item;
}
Funkcija removeItem treba da ukloni element sa čela reda i da vrati njegov
informacioni sadržaj.
T removeItem(Queue* que) {
T itm; Node *tmp;
itm=que->first->item;
tmp=que->first;
if((que->first=que->first->next)==NULL) que->last=NULL; //NULL: red je prazan
free(tmp);
que->n--;
return itm;
}
Prva naredba memoriše polje item prvog čvora, jer to treba da bude vraćeno kao
rezultat. Potom se u privremenom pokazivaču tmp memoriše pokazivač na prvi čvor.
Naime, element sa čela ne sme se odmah osloboditi jer prethodno pokazivač first iz
deskriptora mora biti postavljen na vrednost next elementa sa čela. Sledi provera da li
je ovim uklanjanjem red ispražnjen, pa ako jeste i pokazivač last na poslednji element
postavlja se na NULL. Tek posle toga oslobađa se element sa čela, preko pokazivača
tmp. Najzad smanjuje se tekući broj elemenata n i kao rezultat vraća ranije

132
Dušan T. Malbaški - Algoritmi i strukture podataka

memorisani informacioni sadržaj itm uklonjenog elementa. Šema uklanjanja prikazana


je na slici 4.13.

n
first
last ...

Slika 4.12.

Dodavanje novog čvora na kraj reda obavlja funkcija putItem sa sledećim kôdom:
void putItem(Queue* que,T item) {
Node *newNode;
newNode=malloc(sizeof(Node));
newNode->item=item;
newNode->next=NULL;
if(que->last==NULL) que->first=que->last=newNode; //dodavanje u prazan red
else que->last=que->last->next=newNode;
que->n++;
}
Prvo se formira novi čvor i na njega usmerava pokazivač newNode. Naredbom if
proverava se specijalni slučaj kada je novododati čvor jedini, a tada se na njega
usmeravaju oba pokazivača iz deskriptora, first i last. Ako red nije bio prazan, na novi
čvor usmeravaju se pokazivač iz bivšeg začelja i pokazivač last. Dužina reda
povećava se za 1. Šema dodavanja za opšti slučaj kada novododati čvor nije jedini
data je na slici 4.13.

n
newNode
first
last ...

Slika 4.13.

Funkcija za proveru da li je red prazan, isEmpty jednostavna je kada se uzme u obzir


da je u praznom redu polje first deskriptora jednako NULL. Funkcija isFull još je

133
Dušan T. Malbaški - Algoritmi i strukture podataka

jednostavnija jer se red ne može napuniti, te ona vraća uvek vrednost 0. Određivanje
dužine reda funkcijom size svodi se na očitavanje polja n.
int isEmpty(const Queue* que) {
return que->first==NULL;
}

int isFull(const Queue* que) {


return 0;
}
int size(const Queue* que) {
return que->n;
}
Funkcija clear za pražnjenje („čišćenje“) reda samo je malo složenija. Pokazivač pos
ima zadatak da sukcesivno pokazuje na čvorove reda prateći pokazivač next u
svakom. Pri svakom pomeranju pos njegova vrednost se prvo memoriše u
privremenom pokazivaču tmp, da bi se posle pomeranja pos na sledeći čvor prethodni
obrisao preko tmp. Na kraju, pokazivači first i last postavljaju se na NULL, a dužina
reda n na vrednost 0. Proces pražnjenja reda prikazan je na slici 4.14.
void clear(Queue* que) {
Node *pos=que->first,*tmp;
while(pos!=NULL) {
tmp=pos;
pos=pos->next;
free(tmp);
}
que->first=que->last=NULL;
que->n=0;
}

n tmp pos
first
last ...

Slika 4.14.

134
Dušan T. Malbaški - Algoritmi i strukture podataka

4.2.3. Red sa prioritetima


Spregnuta realizacija reda pruža uslove za implementaciju još jedne zanimljive
i dosta korišćene strukture podataka - reda sa prioritetima. U ovoj strukturi
elementima se dodeljuje prioritet u pogledu pristizanja na čelo: umesto strogog
redosleda FIFO, upravo pristigli element doćiće na čelo reda ne samo u skladu sa
trenutkom upisa, nego i saobrazno prioritetu. Elementi sa većim prioritetom stićiće na
čelo pre onih sa manjim, čak i ako su u red upisani kasnije. Po pravilu, prioritet je ceo
broj veći ili jednak nuli, s tim da je najviši prioritet 0, a što je veća oznaka prioriteta,
tim je prioritet niži.
Strogo gledano, red sa prioritetima nije red jer ne poseduje jedinstvenu FIFO
osobinu, ali se može posmatrati kao skup redova u kojem svi elementi istog reda
imaju isti prioritet i unutar tog reda važi FIFO. Prilikom dodavanja u red sa
prioritetima novi element ne dodaje se na začelje, nego mu se traži mesto unutar
podreda sa istim prioritetima i smešta se na poslednje mesto u tom podredu. Na taj
način, u globalnom redu nalazi se prvo podred sa najvišim prioritetom, zatim onaj sa
prvim nižim itd. Sve operacije osim dodavanja rade onako kako bi radile u običnom
redu.
Red sa prioritetima implementira se spregnuto i to zbog dodavanja. Naime,
ako bi se novi element dodavao na kraj reda, prilikom pristupa i uklanjanja, a u
opštem slučaju, morao bi se pregledati ceo red da bi se pronašao element kojem se
pristupa. Umesto toga, pri dodavanju, a na osnovu prioriteta novog čvora, traži se
odgovarajući podred i dodaje na kraj tog podreda. Dodavanje čvora sa prioritetom p0
šematski je prikazano na slici 4.15.

p>p0 p=p0 p<p0


n
first
last ...

Slika 4.15.

Čvorovi sa prioritetom p0 prikazani su šrafirano. Uočimo da p>p0 znači da je oznaka


prioriteta p manja od oznake prioriteta p0. Inače, ovo se radi zbog toga da bi najviši
prioritet (tj. 0) bio fiksiran, a ne promenljiv od slučaja do slučaja.
Kako smo već pomenuli, operacije u sklopu reda sa prioritetima se gotovo ne
razlikuju od istih kod običnog reda. Minorna razlika je u tome što se u definiciju

135
Dušan T. Malbaški - Algoritmi i strukture podataka

čvora mora uključiti i prioritet. Pioritet ćemo opisati int poljem priority koje, inače, ne
sme biti deo informacionog sadržaja, jer služi za zadavanje mesta elementa, a ne
njegovu obradu.
typedef struct node {
int priority;
T item;
struct node* next;
} Node;
Uključivanje prioriteta ima najveći uticaj na operaciju dodavanja koja se bitno
razlikuje od verzije kod običnog reda:
//priority je prioritet novog cvora
void putItem(Queue* que, T item, int priority) {
Node *newNode, *pos,*prev;
newNode=malloc(sizeof(Node)); newNode->priority=priority;
newNode->item=item; newNode->next=NULL;
que->n++;
if(que->first==NULL) {que->first=que->last=newNode; return;} //red bio prazan
pos=que->first;
while((pos!=NULL)&&(pos->priority<=priority)) {prev=pos;pos=pos->next;}
if(pos==que->first) {
newNode->next=que->first;que->first=newNode;} //umetanje ispred prvog
else {
prev->next=newNode; newNode->next=pos; //umetanje u sredinu ili na kraj
if(newNode->next==NULL) que->last=newNode; //umetnuti je poslednji
}
}
U prvom bloku naredbi formira se pokazivač na novi čvor, newNode i povećava
brojač čvorova n za 1. Zatim se proverava rubni slučaj kada je red pre dodavanja bio
prazan. Tada se novi čvor uključuje tako što se na njega usmeravaju i pokazivač first i
pokazivač last, te se funkcija završava.
U slučaju da red nije bio prazan, prvo se traži njegovo buduće mesto, a u
skladu sa prioritetom. U ciklusu while traži se prvi element sa manjim prioritetom, da
bi se novi dodao ispred njega. Kada se mesto pos pronađe, proveravaju se dva moguća

136
Dušan T. Malbaški - Algoritmi i strukture podataka

slučaja: umetanje ispred prvog i umetanje negde u sredini, odnosno na kraj. Konačno,
ako se novi element nađe na kraju reda, potrebno je ažurirati pokazivač last. Pomoćni
pokazivač prev služi za to da u svakom trenutku pokaže na čvor koji prethodi pos, jer
se njegov pokazivač next mora usmeriti na novododati čvor (videti sl. 4.15).

4.2.4. Ukratko o primenama reda


Red ima široku i raznovrsnu primenu. Najfrekventniji slučaj jeste red koji se
formira ispred tastature jer, bez obzira na to sa kojim programom se komunicira
putem tastature, uneti podaci moraju biti prihvaćeni u strogo FIFO poretku. Red se
pojavljuje i kao posrednik prilikom asinhronog prenosa podataka, kada predajnik i
prijemnik ne rade istom brzinom, pa se poruke moraju privremeno memorisati da ne
bi bile izgubljene. Redove sa prioritetima susrećemo u sklopu operativnih sistema,
gde se u njih smeštaju procesi koji čekaju na resurse (pri čemu treba podvući da se to
ne odnosi na procesor čiji mehanizam upravljanja nije tako jednostavan). Redove sa
prioritetima ili bez njih srećemo, recimo, kod mrežnih štampača gde se zahtevi za
štampanje slažu u redove. Konačno, redovi se vrlo intenzivno primenjuju u
programskim sistemima za digitalnu simulaciju.

4.3. DEK
Struktura podataka nazvana dek (engl. deque, akronim od Double Ended
Queue, red sa dva kraja) predstavlja uopštenje steka i reda, uključujući i ostale
moguće strukture kod kojih se osnovne operacije vrše na krajevima. Osobine deka
čine, na određeni način, sintezu osobina steka i reda. Logičku strukturu definišemo
kao uređeni par
DK = (S(DK),r(DK))
sa digrafom oblika prikazanog na slici 4.16:

...
...

Slika 4.16.

Kao što vidimo, struktura je bilinearna i simetrična, nema „prvog“ niti „poslednjeg“,
pa govorimo o levom i desnom kraju deka. Osnovne operacije su:
 pristup krajnjem levom elementu
 uklanjanje krajnjeg levog elementa

137
Dušan T. Malbaški - Algoritmi i strukture podataka

 dodavanje s leva
 pristup krajnjem desnom elementu
 uklanjanje krajnjeg desnog elementa
 dodavanje s desna
što je prikazano na slici 4.17

...

Slika 4.17.

Dek je struktura koja ima prevashodno teorijsku ulogu, dok se u praksi ređe
primenjuje, upravo zbog toga što objedinjuje karakteristike steka i reda, a videli smo
da su njihove oblasti primene primene potpuno disjunktne.
Fizička struktura deka slična je fizičkoj strukturi reda, jer, kao i red, dek ima
dva pokretna kraja. Može biti sekvencijalna i spregnuta.
Kod sekvencijalne realizacije javlja se problem lažne prepunjenosti i rešava
cirkularnom strukturom što, kao i kod reda, dovodi do problema nerazlikovanja punog
i praznog deka. Problem se rešava uopštenjem rešenja kod reda, gde se mora uzeti u
obzir činjenica da dek cirkuliše u oba smisla, a ne samo u jednom kao red, te su detalji
rešenja, u stvari, duplirani.
Spregnuta realizacija deka takođe se razlikuje od spregnute realizacije reda po
tome što se mora poštovati simetričnost krajeva. U tom smislu, svaki čvor deka
snabdeva se sa dva pokazivača: jedan pokazuje na levog suseda, a drugi na desnog.
Prikazana je na slici 4.18.

n
left
...
right

Slika 4.18.

Operacije su nešto sporije nego kod reda, jer treba ažurirati dva pokazivača, a ne
jedan. Inače, odvijaju se sasvim analogno.

138
Dušan T. Malbaški - Algoritmi i strukture podataka

4.4. SEKVENCA
Sekvenca je poludinamička kontejnerska struktura koja se definiše kao uređeni
par
D=(S(D),r(D))
sa osobinama:
 struktura je linearna
 dozvoljen je pristup svakom elementu, po pravilu navigacijom
 ukloniti se mogu samo svi elementi odjednom
 element se dodaje na kraju sekvence
 sekvenca je homogena
Ponašanje sekvence prikazano je na slici 4.19.

tekući element

...

Slika 4.19.

Osnovni način pristupa sekvenci jeste navigacija, što znači da, kada je sekvenca u
upotrebljivom stanju, mora biti definisan tzv. tekući element. Prilikom pristupa ne
navodi se pozicija, nego se operacija obavlja implicitno, nad tekućim elementom.
Tekući element određuje se markerom tekućeg elementa koji ukazuje na njegovu
poziciju u sekvenci. Pritom, markerom tekućeg elementa može se markirati i mesto
iza aktuelnog poslednjeg elementa. U tom stanju sekvence pristup se ne može izvesti,
ali je moguće izvršiti operaciju dodavanja (jedino tada je i moguće).
Slično ostalim poludinamičkim (i dinamičkim) strukturama, sekvenca se
koristi samo ako je prethodno dovedena u početno stanje. Operacija kreiranja, između
ostalog, ima za zadatak da definiše poziciju tekućeg elementa, bez koje sekvenca nije
u radnom stanju. Postoje dve varijante operacije pristupa:
 pristup u svrhu čitanja informacionog sadržaja elementa
 pristup u svrhu izmene informacionog sadržaja elementa
Operacija dodavanja, kako je rečeno, izvodi se isključivo tako što se element dodaje
na kraj sekvence, što znači da marker tekućeg elementa mora da pokazuje na mesto

139
Dušan T. Malbaški - Algoritmi i strukture podataka

iza kraja. Ako to nije slučaj dodavanje se automatski zamenjuje operacijom izmene
informacionog sadržaja.
Posebnu grupu operacija čine operacije vezane za rukovanje tekućim
elementom. U načelu, te operacije ne menjaju sadržaj sekvence, ali menjaju njeno
stanje pošto je stanje sekvence definisano njenom strukturom i sadržajem, ali i
pozicijom tekućeg elementa. U ovu grupu operacija spadaju:
 operacija promene pozicije tekućeg elementa
 operacija očitavanja pozicije tekućeg elementa
 operacija provere da li je marker tekućeg elementa postavljen iza poslednjeg
elementa (uslov za dodavanje u sekvencu)
 operacija vraćanja markera na početak sekvence
Dodatne operacije nad sekvencom obuhvataju:
 zatvaranje sekvence
 uništavanje sekvence
 proveru da li je sekvenca prazna
 određivanje dužine sekvence (može biti izražena brojem elemenata ili u
bajtovima)
 redoslednu obradu sekvence
 sortiranje sekvence
U načelu, sekvenca se može realizovati u operativnoj memoriji ili se nalaziti na
nekom drugom medijumu, ali daleko najvažniji slučaj je primena sekvence za
realizaciju tokova (tok, engl. stream), najčešće datoteka. U nastavku, korišćenje
sekvence demonstriraćemo na primeru (binarnih) datoteka u programskom jeziku C.

4.4.1. Fizička realizacija sekvence


Budući da je datoteka najtipičniji oblik sekvence, pozabavićemo se njenom
fizičkom realizacijom. Najizrazitija specifičnost datoteke (i svih ostalih vrsta tokova)
leži u činjenici da ona nije deo programa. Program koristi datoteku, ali njome
upravlja operativni sistem, preciznije njegov podsistem sa engleskim nazivom file
system.
Fizička struktura datoteke je vrlo specifična i izvodi se kombinacijom
sekvencijalne i spregnute metode. Naime, elementi se sekvencijalno smeštaju u
blokove konstantne dužine, ali se blokovi distribuiraju po disku i sprežu tehnikom

140
Dušan T. Malbaški - Algoritmi i strukture podataka

sličnom pokazivačima. Razlika u sprezanju blokova je u tome što pokazivači nisu deo
bloka, nego se nalaze u posebnim, pomoćnim strukturama, koje održava operativni
sistem. Ne ulazeći u pojedinosti jer, u zavisnosti od operativnog sistema (čak od
verzije operativnog sistema) postoji čitav niz tehnika, idejno rešenje može se prikazati
kao na slici 4.20.

Slika 4.20.

Inače, ovakva realizacija i jeste razlog zbog kojeg se ne može ukloniti proizvoljan
element, jer bi u tom slučaju oni koji se nalaze iza njega morali biti pomereni, a
pojavila bi se i potreba za premeštanjem elemenata iz bloka u blok, što bi - sve
zajedno - bilo više nego sporo!
Operacija kreiranja pojavljuje se, po pravilu, u tri varijante:
 kreiranje prazne datoteke; ako datoteka ne postoji kreira se inicijalno prazna;
ako postoji zatečeni sadržaj se uništava;
 kreiranje bez promene sadržaja; ako datoteka nije prazna njen sadržaj ostaje;
marker se postavlja na početak datoteke
 kreiranje sa dopunjavanjem; ako datoteka ne postoji kreira se inicijalno
prazna; ako postoji, njen sadržaj se ne menja, a marker se postavlja na kraj
U programskom jeziku paskal, na primer, procedure za kreiranje su redom rewrite,
reset i append. U programskom jeziku C, kreiranje izvodi jedna jedina funkcija,
fopen, gde se način kreiranja zadaje kao argument.
Operacije pristupa i dodavanja u C-u su realizovane kroz dve funkcije:
funkcija fread služi za čitanje sadržaja tekućeg elementa; druga funkcija, fwrite, služi
za upis ili dodavanje. Naime, ako je tekući element neki od aktuelnih elemenata, novi

141
Dušan T. Malbaški - Algoritmi i strukture podataka

sadržaj biće upisan preko starog, tako da se u tom slučaju fwrite ponaša kao funkcija
pristupa. Ako je marker tekućeg elementa iza poslednje aktuelne pozicije, novi sadržaj
biće upisan u novi element, na kraju, što znači da u ovim okolnostima fwrite igra
ulogu operacije dodavanja. Obe ove operacije u svim programskim jezicima praćene
su automatskim pomeranjem markera tekućeg elementa na sledeću poziciju.
Za uklanjanje svih elemenata obično ne postoji posebna operacija, nego se ono
obavlja rekreiranjem inicijalno prazne datoteke.
Upravljanje markerom se u C-u realizuje kroz sledeće funkcije: funkcija fseek
pomera marker na poziciju koja se izračunava iz parametara; očitavanje položaja
markera vrši se funkcijom ftell; provera da li je marker na kraju datoteke obavlja se
funkcijom feof; vraćanje na početak datoteke izvodi se funkcijom rewind.
Kada datoteka više nije potrebna u programu, treba je zatvoriti funkcijom
fclose. Ovo je obavezna radnja da bi se obavile neke završne akcije u nadležnosti
operativnog sistema. Operacija (fizičkog) uništavanja datoteke realizovana je kao
funkcija remove. Za proveru da li je datoteka prazna ne postoji posebna funkcija, nego
se koristi marker: postavi se na kraj datoteke i proveri pozicija: ako je pozicija 0
datoteka je prazna.
Ni za određivanje veličine datoteke u C-u nema posebne funkcije, nego se i to
postiže pomeranjem i očitavanjem položaja markera (videti primer u sledećem
odeljku o sortiranju datoteke).
S obzirom na to da je datoteka sekvenca, a sekvenca linearna struktura,
redosledna obrada vrši se tako što se marker postavi na početnu poziciju (funkcija
rewind), a zatim sledi ciklus while unutar kojeg se čita element funkcijom fread, a
zatim obrađuje. Pošto funkcija fread vraća kao rezultat broj učitanih elemenata, kraj
obrade (tj. kraj ciklusa while) indikovan je vraćenom vrednošću jednakom 0.

4.4.2. Sortiranje datoteke


Postupak sortiranja datoteke je, po definiciji, isto što i sortiranje niza -
uređivanje elemenata po nekom kriterijumu - ali u pogledu realizacije postavlja bitno
različite zahteve. Naime, videli smo da je pri sortiranju nizova glavna akcija promena
mesta elementima, što važi za sve metode pa i za one koje nismo bili u mogućnosti da
prikažemo. Upravo ova operacija stvara najveće teškoće prilikom sortiranja struktura
koje nisu u operativnoj memoriji (tj. datoteka) jer pojedinačna zamena mesta
elementima koji su npr. na disku toliko je dugotrajna da se jednostavno ne sme

142
Dušan T. Malbaški - Algoritmi i strukture podataka

primenjivati. S druge strane, međusobna zamena mesta pri sortiranju ne može se


izbeći, jer svako preuređivanje samo po sebi znači razmenu pozicija.
Da bi se ova dva međusobno sukobljena zahteva uskladila, datoteka se sortira
kombinacijom tzv. internog sortiranja (sortiranja u operativnoj memoriji) segmenata
datoteke i prepisivanja čitavih segmenata na disk. Najjednostavnija metoda za ovo,
tzv. eksterno, sortiranje zove se sortiranje mešanjem. Ideja je jednostavna: podeliti
datoteku na manje delove (segmente) koji mogu da stanu u operativnu memoriju,
zatim sortirati svaki od njih nekom od ranije opisanih metoda i konačno zapisati
segment u pomoćnu datoteku na disku. Faza formiranja pojedinačnih sortiranih
segmenata zove se faza disperzije (engl. dispersion phase). U drugoj fazi, tzv. fazi
mešanja (engl. merge phase), segmenti se kombinuju u jednu, sortiranu, datoteku.
Broj segmenata m na koje se deli originalna datoteka je projektni parametar i zove se
red mešanja. Sortiranje koje koristi m segmenata nosi naziv mešanje po m putanja (m-
way merge).
Ilustrovaćemo sortiranje mešanjem na primeru datoteke file čiji slogovi imaju
sasvim pojednostavljen tip
typedef struct {
int key;
} Record;
gde je key ključ po kojem se datoteka sortira. Red mešanja označićemo sa m. Opšti
postupak je
 podeliti datoteku na m segmenata
 svaki segment sortirati u operativnoj memoriji (recimo metodom bubbleSort) i
upisati ga u posebnu datoteku. Imena tih, pomoćnih, datoteka biće F$0, F$1,...;
time se završava faza disperzije
 kombinovati datoteke F$0,F$1,... u jedinstvenu, sortiranu, datoteku koja se
upisuje na mesto početne datoteke file; kombinovanje se izvodi tako što se
čitaju slogovi svih m pomoćnih datoteka, a zatim se najmanji od njih upisuje
na izlaz; iz pomoćne datoteke iz koje potiče taj najmanji slog čita se sledeći i
postupak se ponavlja; postupak (faza mešanja) završava se kada su sve
pomoćne datoteke pročitane.
Opšti postupak prikazan je na slici 4.21.

143
Dušan T. Malbaški - Algoritmi i strukture podataka

file

sort F$0

sort F$1

faza
disperzije sort F$2

sort

F$0

F$1

faza merge file


mešanja F$2

Slika 4.21

Funkcija za sortiranje koristiće ranije opisanu funkciju bubbleSort u kojoj su


parametri prilagođeni tipu Record elemenata datoteke. Prilagođena funkcija ima
sledeći izgled:
void bubbleSort(Record a[],int n) {
int i,j,chn; Record tmp; //chn=0: nije bilo zamena u prolazu; zavrsiti
for(chn=1,i=n-1;chn&&(i>0);i--)
for(j=chn=0;j<i;j++)
if(a[j].key>a[j+1].key) {tmp=a[j];a[j]=a[j+1];a[j+1]=tmp;chn=1;}
}
Da bi se datoteka mogla podeliti na m segmenata potrebno je, pre svega, odrediti
njenu veličinu u bajtovima. Poznato je da standardni C nema gotovu funkciju za tako
nešto, nego se koristi kombinacija fseek i ftell:

144
Dušan T. Malbaški - Algoritmi i strukture podataka

long fileSize(FILE* file) {


long fpos,fsize;
fpos=ftell(file); //memorisati trenutnu poziciju
fseek(file,0,SEEK_END); //postaviti marker na poslednji bajt
fsize=ftell(file); //velicina je jednaka udaljenosti markera od pocetka
fseek(file,fpos,SEEK_SET); //vratiti marker u zateceno stanje
return fsize;
}
Sada je na redu sama funkcija za sortiranje. Prvo dajemo njen kôd, a zatim slede
objašnjenja:
void mergeSort(FILE* file, int m) {
long tmp,segmentSize;
Record *segmentArray; FILE** segments; char segmentName[6];
Record *filter; int* isActive;
int i,n,imin,keymin,keymax;

tmp=fileSize(file)/sizeof(Record); //velicina fajla u slogovima


segmentSize = tmp/m+tmp%m; //duzina jednog segmenta u slogovima
segmentArray=malloc(segmentSize*sizeof(Record)); //formiranje segmentnog niza
segments=malloc(m*sizeof(FILE*)); //formiranje niza segmentnih fajlova
//formiraje segmentnih fajlova
for(i=0;i<m;i++) {
sprintf(segmentName,"F$%d",i); //imena segmenata: F$0, F$1, F$2 ...
segments[i]=fopen(segmentName,"wb+");
}
rewind(file);

//FAZA DISPERZIJE
for(i=0;i<m;i++) {
//kreiranje segmenta
n=fread(segmentArray,sizeof(Record),segmentSize,file); //citanje segmenta
bubbleSort(segmentArray,n); //sortiranje segmenta
fwrite(segmentArray,sizeof(Record),n,segments[i]); //upis segmenta

145
Dušan T. Malbaški - Algoritmi i strukture podataka

//odredjivanje najveceg u datoteci (treba za fazu mesanja)


if(i==0) keymax=segmentArray[n-1].key;
else if(segmentArray[n-1].key>keymax) keymax=segmentArray[n-1].key;
}
//FAZA MESANJA
rewind(file);
for(i=0;i<m;i++) rewind(segments[i]);
filter=malloc(m*sizeof(Record)); isActive=malloc(m*sizeof(int));
//ucitati prvi slog iz svakog od segmenata
for(i=0;i<m;i++) isActive[i]=fread(&filter[i],sizeof(Record),1,segments[i]);
do {
//odrediti indeks najmanjeg
imin=-1; keymin=keymax;
for(i=0;i<m;i++)
if(isActive[i]&&(filter[i].key<=keymin)) keymin=filter[imin=i].key;
//upisati najmanji u fajl
if(imin>-1) {
fwrite(&filter[imin],sizeof(Record),1,file);
isActive[imin]=fread(&filter[imin],sizeof(Record),1,segments[imin]);
}
} while(imin>-1);
//OSLOBADJANJE MEMORIJE
for(i=0;i<m;i++) {
fclose(segments[i]);
sprintf(segmentName,"F$%d",i);
remove(segmentName);
}
free(segmentArray); free(segments);
free(filter); free(isActive);
}
Na početku se određuje veličina segmenata izražena brojem slogova u datoteci,
umesto bajtovima i formira se niz segmentArray u koji se smeštaju pojedinačni
segmenti u svrhu sortiranja. Potom se formira niz segments koji će sadržati datotečne

146
Dušan T. Malbaški - Algoritmi i strukture podataka

promenljive što se pridružuju pojedinačnim segmentima, te otvaraju segmentne


datoteke čija se imena F$0, F$1,... formiraju standardnom funkcijom sprintf.
Pošto su segmentne datoteke spremne, pristupa se njihovom punjenju,
odnosno fazi disperzije. U fazi disperzije čita se iz ulazne datoteke file segment koji se
smešta u niz segmentArray. Niz se interno sortira funkcijom bubbleSort i tako sortiran
smešta u odgovarajuću segmentnu datoteku F$i. Usput se određuje i najveći ključ u
datoteci koji će biti potreban u fazi mešanja. Time se završava faza disperzije.
Faza mešanja počinje formiranjem niza filter sa elementima tipa Record. Svaki
element niza vezuje se za jednu segmentnu datoteku i sadrži sledeći slog te datoteke
koji još nije prepisan na izlaz. Elementi niza isActive takođe se vezuju za segmentne
datoteke. Element isActivei jednak je 1 ako u odgovarajućoj segmentnoj datoteci F$i
ima još elemenata koji nisu prepisani u izlaznu datoteku. Ako je isActivei jednak 0,
to znači da je segmentna datoteka F$i obrađena i da više ne učestvuje u procesu
mešanja. U ciklusu do-while određuje se indeks imin najmanjeg elementa u nizu filter
(za šta je potreban najveći element datoteke, keymax određen u fazi disperzije) i taj
element se upisuje u izlaznu datoteku file. Potom se iz odgovarajuće segmentne
datoteke F$imin čita sledeći element ako ga ima, što se ustanovljuje indikatorom
isActiveimin. Postupak mešanja završava se kada više nema elemenata u
segmentnim datotekama, a indikator za to je vrednost imin jednaka -1. Na kraju se
oslobađa zauzeti prostor na hipu i brišu segmentne datoteke standardnom funkcijom
remove.
Pored ove, postoje i druge varijante sortiranja mešanjem, ali sve one bazirane
su na istoj ideji: sva premeštanja elemenata moraju biti obavljena u operativnoj
memoriji i nikako na disku.

147
Dušan T. Malbaški - Algoritmi i strukture podataka

5. LISTE

Pored nizova, liste su najpoznatije i najčešće korišćene kontejnerske strukture


podataka. Podsetimo, kontejnerske strukture podataka su strukture opšte namene čiji
je jedini zadatak čuvanje podataka. Ima ih više vrsta, a zajednička ključna reč za sve
njih je fleksibilnost. Dozvoljavaju sve tri vrste pristupa bilo kojem elementu,
dodavanje na bilo kojem mestu i uklanjanje bilo kojeg elementa. Mogu se sortirati,
obrađivati redosledno, pretraživati, kopirati, spajati i razlagati. Postoje tri osnovna
oblika liste:
 jednostruko spregnuta lista
 dvostruko spregnuta lista
 multilista ili višestruka lista
među kojima se ispiču jednostruko i dvostruko spregnuta lista. Fizička realizacija svih
vrsta listi je isključivo spregnuta.

5.1. JEDNOSTRUKO SPREGNUTA LISTA


Jednostruko spregnuta lista najtipičniji je predstavnik ove grupe struktura
podataka i gotovo da nema ozbiljnije aplikacije, a da u njoj nije zastupljena, čak u više
varijanata. Jednostruko spregnuta lista definiše se kao struktura
L = (S(L),r(L))
sa sledećim osobinama:
 struktura je linearna
 dozvoljen je pristup svakom elementu, pri čemu su zastupljene sve tri vrste
pristupa: prema poziciji, prema ključu i navigacijom
 dozvoljeno je uklanjanje bilo kojeg elementa
 element se može dodati na bilo kojem mestu u listi.
Kako vidimo, mogućnosti manipulacije koje pruža jednostruko spregnuta lista veoma
su široke. Jedini mehanizam koji kod jednostruko spregnute liste ne postoji je
indeksiranje, što ipak ne znači da nema pristupa prema poziciji - on je samo sporiji.
Fleksibilnost jednostruko (uostalom i dvostruko) spregnute liste ima za
posledicu i to što karakteristike osnovnih operacija, pa čak i njihov repertoar, variraju

148
Dušan T. Malbaški - Algoritmi i strukture podataka

u zavisnosti od namene liste. Na primer, operacija dodavanja elementa može da se


pojavi u bar dve varijante koje se, čak, međusobno isključuju:
 ako je redosled elemenata u listi potpuno proizvoljan, novi element dodaje se
na početak, jer je to najbrže
 ako je lista sortirana dodaje se na mesto koje je određeno ključem novog
elementa
Dalje, ako se očekuje intenzivna redosledna obrada, lista treba da bude snabdevena
mehanizmom navigacije, dok u suprotnom navigacija samo usporava osnovne
operacije i nije potrebna. Ako navigacija postoji, tada se u deskriptoru mora pratiti
pozicija tekućeg elementa i to ugraditi u operacije koje podrazumevaju pristup,
dodavanje i uklanjanje (a to su skoro sve operacije). Ako navigacije nema,
implementacija pomenutih operacija biće drukčija, jer nema tekućeg elementa. I tako
dalje. Jednom rečju, lista se mora projektovati u skladu sa unapred postavljenim
zahtevima, te nije čudno što se u složenijim programima pojavljuje više različitih
vrsta listi: sortiranih ili nesortiranih, sa navigacijom ili bez nje, sortiranih sa
navigacijom, sortiranih bez navigacije itd. U programskim jezicima lista se pojavljuje
kao deo pratećeg softvera i u tim implementacijama projektanti se trude da ona bude
univerzalna, što je neki put dobro, ali neki put i nije, jer su univerzalna rešenja uvek
komplikovanija i sporija od namenskih.
Pošto i sama specifikacija operacija zavisi od namene liste i značajno varira,
razmotrićemo tri karakteristična primera realizacije liste (od mnoštva mogućih):
 tzv. jednostavnu listu,
 listu sa navigacijom i
 listu sa ključevima.
Jednostavna lista je primer liste u kojoj je osnovna operacija pristup prema poziciji.
Lista sa navigacijom snabdevena je mehanizmom navigacije, a elementi trećeg
primera liste imaju ključeve, pa je glavna operacija traženje po ključu. Takođe su
moguće i sve kombinacije po dve, a može se napraviti i neka vrsta univerzalne liste
koja bi bila kombinacija sve tri navedene. S obzirom na to da se dvostruko spregnuta
lista razlikuje od jednostruko spregnute upravo po pitanju navigacije, listu sa
navigacijom ostavićemo za primer dvostruko spregnute liste.

149
Dušan T. Malbaški - Algoritmi i strukture podataka

5.1.1. Jednostavna lista


Kao što i naziv sugeriše, jednostavna lista (engl. simple list) je varijanta
jednostruko spregnute liste sa uprošćenom strukturom i rudimentarnim repertoarom
operacija. Osnovne osobine su joj:
 redosled elemenata (čvorova) liste je proizvoljan
 osnovni način pristupa je pristup prema poziciji
 ne postoji mehanizam navigacije
Činjenica da je redosled čvorova u listi proizvoljan neposredno utiče na realizaciju
operacije dodavanja. Naime, ako je svejedno gde se novi čvor dodaje, on će biti dodat
na početak liste, suprotno prvom utisku, jer dodavanje na svakom drugom mestu
zahteva seriju nepotrebnih pristupa čvorovima koji prethode tom mestu.
Pristup prema poziciji je jednostavan: polazeći od pokazivača na prvi čvor,
prolazi se kroz listu praćenjem pokazivača u čvorovima i broje čvorovi kojima je
pristupljeno. Kada taj broj dostigne zadatu oznaku pozicije, pristup je završen.
Očigledno, osim toga što je jednostavan, pristup prema poziciji je izrazito spor.
S obzirom na spregnuti način realizacije, čvor jednostavne liste sastoji se iz
dva dela: prvi je informacioni sadržaj čvora koji ćemo predstaviti poljem item tipa T,
dok je drugi pokazivač next na sledeći čvor, slika 5.1.

item next

Slika 5.1.

Lista je prikazana na slici 5.2.

n
first ...

Slika 5.2.

Deskriptor sadrži, pre svega, pokazivač na prvi čvor, a pored njega (obično) sadrži i
aktuelnu dužinu (broj elemenata), u oznaci n:
typedef struct node {
T item;

150
Dušan T. Malbaški - Algoritmi i strukture podataka

struct node* next;


} Node;
typedef struct {
int n;
Node *first;
} SimpleList;
Lista se mora kreirati, a specifikacija odgovarajuće operacije je
//prototip: void create(SimpleList* lst);
//parametri: lst je adresa liste
//preduslov: -
//postuslov: lista je kreirana i prazna je
//rezultat: -
Provera da li je lista prazna:
// prototip: int isEmpty(SimpleList* lst);
//parametri: lst je adresa liste
//preduslov: -
//postuslov: -
//rezultat: 1 ako je lista prazna; 0 u suprotnom
Očitavanje polja item iz čvora koji se nalazi na poziciji position:
// prototip: T* getItem(const SimpleList* lst, int position);
//parametri: lst je adresa liste; position je pozicija čvora u listi (između 0 i n-1)
//preduslov: lista nije prazna i važi 0positionn-1
//postuslov: -
//rezultat: adresa polja item čvora na poziciji position
Operacija uklanjanja ima za parametar poziciju position na kojoj se nalazi uklonjeni
čvor. Rezultat uklanjanja je polje item uklonjenog elementa:
//prototip: T removeItem(SimpleList* lst, int position);
//parametri: lst je adresa liste; position je pozicija elementa za uklanjanje
//preduslov: lista nije prazna i važi 0positionn-1
//postuslov: čvor na poziciji position je uklonjen iz liste
//rezultat: polje item uklonjenog čvora
Kako je rečeno, osnovna osobina jednostavne liste je ta da je redosled čvorova
proizvoljan. Stoga se novi element dodaje na početak liste.

151
Dušan T. Malbaški - Algoritmi i strukture podataka

//prototip: void putItem(SimpleList* lst, T item);


//parametri: lst je adresa liste; item je polje item novog čvora
//preduslov: -
//postuslov: na početku liste nalazi se novi čvor sa poljem item
//rezultat: -
Da bi se listom moglo upravljati, neophodno je raspolagati njenom dužinom, tj.
brojem elemenata. Uočimo da aktuelna dužina liste učestvuje kao preduslov u dve
operacije: očitavanja (getItem) i uklanjanja (removeItem). Specifikacija operacije
određivanja broja čvorova jednostavna je:
//prototip: int size(const SimpleList* lst);
//parametri: lst je adresa liste
//preduslov: -
//postuslov: -
//rezultat: broj čvorova (dužina) liste
Standardna je praksa da se svaka, pa i jednostavna lista snabde operacijom pražnjenja:
//prototip: void clear(SimpleList* lst);
//parametri: lst je adresa liste
//preduslov: -
//postuslov: lista je prazna
//rezultat: -
Programska realizacija navedenih operacija prilično je jednoznačna i nije
naročito komplikovana (zato se lista i naziva jednostavnom). Poćićemo od
najjednostavnijih operacija koje ne zahtevaju praktično nikakva objašnjenja:
void create(SimpleList* lst) {
lst->first=NULL;
lst->n=0;
}

int isEmpty(const SimpleList* lst) {


return lst->first==NULL;
}

int size(const SimpleList* lst) {

152
Dušan T. Malbaški - Algoritmi i strukture podataka

return lst->n;
}
Očitavanje sadržaja item čvora na poziciji position obaviće funkcija
T* getItem(const SimpleList* lst, int position) {
Node *curr; int pos;
for(curr=lst->first,pos=0;pos<position;pos++) curr=curr->next;
return &(curr->item);
}
Pokazivač curr (uobičajena skraćenica od current) postavlja se na prvi čvor liste
preko njegove adrese koja se nalazi u deskriptoru, curr=lst->first. Zatim se prati
pokazivač next svakog čvora i istovremeno prebrojavaju čvorovi kojima je
pristupljeno brojačem pos. Onog trenutka kada pos dostigne vrednost zadate pozicije
position, prolaz kroz listu se završava i rezultat isporučuje kao vrednost polja item
čvora na koji pokazuje pokazivač curr, tj. adresu curr->item. Rezultat je adresa zato
da bi se ostavila mogućnost za izmenu polja item izrazom
*getItem(lst,position)=nova_vrednost_item;
Na ovom primeru najbolje se vidi da je pristup prema poziciji kod liste sporiji nego
kod niza. Ako pretpostavimo da su verovatnoće pristupa svim pozicijama međusobno
jednake i jednake 1/n tada je srednji broj pokušaja prilikom pristupa
n
U=  i/n = (n+1)/2 ≈ n/2
i 1

Dakle, vremenska složenost je On, dok je kod niza ona konstantna i iznosi O1.
Funkcija za uklanjanje samo je malo složenija. Zadatak joj je da pronađe čvor
na zadatoj poziciji i da ga isključi iz liste podešavanjem pokazivača next prethodnog
elementa, kao što se vidi na slici 5.3:

n
first ...

prev curr
Slika 5.3.

T removeItem(SimpleList* lst, int position) {


T itm; Node *prev,*curr; int pos;

153
Dušan T. Malbaški - Algoritmi i strukture podataka

prev=curr=lst->first;
for(prev=curr=lst->first,pos=0;pos<position;pos++) {prev=curr; curr=curr->next;}
if(pos==0) lst->first=curr->next; //uklanjanje prvog
else prev->next=curr->next; //uklanjanje cvora koji nije prvi
itm=curr->item;
lst->n--;
free(curr);
return itm;
}
Da bi se moglo izvršiti prevezivanje prethodnika čvora za uklanjanje na njegovog
sledbenika, kroz listu se prolazi pomoću dva pokazivača: curr i prev (od engl.
previous). Tokom prolaska kroz listu pokazivač curr pokazuje na element kojem se
upravo pristupa, a prev na njegovog prethodnika. Dok se pokazivačem curr prolazi
kroz listu, brojač pos odbrojava pozicije sve dok se ne stigne do pozicije position na
kojoj se nalazi čvor za uklanjanje. Kada se do te pozicije stigne, vrši se prevezivanje
tako što se pokazivač next iz čvora curr kopira u pokazivač next njegovog prethodnika
preko pokazivača prev. Rubni slučaj o kojem se mora voditi računa jeste uklanjanje
čvora sa pozicije 0 (dakle, prvog po redu). U tom slučaju, njegov pokazivač next
kopira se u polje first deskriptora. Zatim se čvor oslobađa funkcijom free, smanjuje
brojač elemenata list->n i polje item uklonjenog elementa vraća kao rezultat.
Dodavanje u listu jednostavno je pošto, po pretpostavci, može da se doda na
bilo koje mesto. Da bi se izbegao prolazak kroz čvorove novi čvor dodaje se isped
prvog, slika 5.4.

newNode

n
first ...

Slika 5.4.

Funkcija za dodavanje izgleda ovako:


void putItem(SimpleList* lst, T item) {
Node *newNode;
newNode=malloc(sizeof(Node));

154
Dušan T. Malbaški - Algoritmi i strukture podataka

newNode->item=item;
newNode->next=lst->first;
lst->first=newNode;
lst->n++;
}
Konačno, funkcija za pražnjenje liste veoma podseća na istu funkciju kod spregnuto
realizovanog reda:
void clear(SimpleList* lst) {
Node *curr=lst->first,*tmp;
while(curr) {
tmp=curr;
curr=curr->next;
free(tmp);
}
lst->first=NULL;
lst->n=0;
}
Prvo, ako je lista već prazna funkcija se odmah završava. Ako nije, u nastavku se
pokazivačem curr sukcesivno pristupa čvorovima. Pre nego što se pređe na sledeći
čvor operacijom curr=curr->next pokazivač curr memoriše se u privremenoj
promenljivoj tmp. Kada se izvrši prelaz na sledeći, čvor na koji pokazuje tmp
oslobađa se funkcijom free. Na kraju, polje deskriptora first postavlja se na NULL, a
broj čvorova na vrednost 0.
Redosledna obrada jednostavne liste je jedna od zamki u koje mogu da
upadnu neiskusni programeri. Na prvi pogled, sve je jednostavno:
for(position=0;position<size(&lst);position++) obraditi getItem(&lst, position);
gde je lst lista koja se obrađuje. Međutim, ako se zna kako funkcioniše pristup prema
poziciji (tj. funkcija getItem), jasno je da će ovakvom rešenju programer pribeći samo
u krajnjem očajanju: naime, prilikom svakog pristupa funkcija kreće od početka liste,
tražeći zadatu poziciju, a to se, jednostavno, ne radi. Dobro pripremljen softver za
jednostavnu listu treba da sadrži i posebnu funkciju za redoslednu obradu. Takve
funkcije - inače, operacija te vrste zove se iterator - imaju za zadatak da obezbede
sukcesivno pristupanje elementima na pozicijama 0,1,2... bez vraćanja na početak.

155
Dušan T. Malbaški - Algoritmi i strukture podataka

Algoritam obrade prosleđuje se funkciji kao parametar-funkcija. Funkcije koje


realizuju sukcesivni pristup obično se zovu traverse:
void traverse(SimpleList* lst, void (*pDoIt)(T* pItem)) {
Node *curr=lst->first;
do {
(*pDoIt)(&(curr->item));
} while((curr=curr->next)!=NULL);
}
Drugi parametar pDoIt je, ustvari, pokazivač na funkciju koja realizuje obradu
pojedinačnog polja item svakog čvora. Ciklusom do-while pristupa se redom
čvorovima liste i svaki od njih se prosleđuje kao argument funkciji *pDoIt čiji
algoritam nije poznat u fazi prevođenja traverse, nego će biti prosleđen kao argument
pri pozivu. Funkcija traverse kao drugi argument može da primi svaku funkciju koja
ima prototip
void imeFunkcije(T*);
sa parametrom koji je pokazivač na podatak tipa T. Da bismo ilustrovali primenu
traverse pretpostavimo da je polje item tipa char, tj. da je Tchar. Definišimo
funkciju
void printItem(T* pItem) {
printf("\n%c",*pItem);
}
koja na ekranu prikazuje sadržaj polja item. Tada se pozivom
if(!isEmpty(&lst)) traverse(&lst, printItem);
na ekranu štampa sadržaj liste, ako nije prazna.

5.1.2. Jednostruko spregnuta lista sa ključevima


Namena ove vrste liste jeste da, uz fleksibilnost koju ima svaka lista, omogući
traženje i obradu po ključu. Realizacija osnovnih operacija zavisi od učestanosti
redosledne obrade po sortiranim ključevima. Ako se ne očekuje česta redosledna
obrada po sortiranim ključevima, nove čvorove treba dodavati na početak liste, kao i u
slučaju jednostavne liste. Pre svake obrade listu treba sortirati. Obrnuto, ako je
pomenuta obrada česta, operaciju dodavanja treba realizovati tako da se lista održava
sortiranom, po cenu produženog vremena dodavanja. Drugim rečima, novi čvor se ne

156
Dušan T. Malbaški - Algoritmi i strukture podataka

dodaje na proizvoljnom mestu nego na mestu koje je određeno veličinom ključa i koje
prethodno treba naći. Pošto prvi slučaj umnogome liči na jednostavnu listu,
pozabavićemo se listom koja je u svakom trenutku sortirana po, recimo, rastućoj
vrednosti ključa. Definicije tipa čvora i tipa same liste su
typedef struct node {
int key;
T item;
struct node* next;
} Node;

typedef struct {
int n;
Node *first;
} KeyedList;
Funkcije za kreiranje, proveru da li je lista prazna, određivanje broja elemenata i
pražnjenje liste su iste kao kod jednostavne liste:
void create(KeyedList* lst) {
lst->first=NULL;
lst->n=0;
}

int isEmpty(const KeyedList* lst) {


return lst->first==NULL;
}
int size(const KeyedList* lst) {
return lst->n;
}

void clear(KeyedList* lst) {


Node *curr=lst->first,*tmp;
while(curr) {
tmp=curr;
curr=curr->next;

157
Dušan T. Malbaški - Algoritmi i strukture podataka

free(tmp);
}
lst->first=NULL;
lst->n=0;
}
Funkcija za pristup na bazi ključa (tj. za traženje) bazira se na linearnom traženju45:
polazi se od prvog čvora i redom traži čvor u kojem se ključ poklapa sa argumentom
traženja arg. Postupak se završava kada se čvor nađe ili kada se stigne do čvora u
kojem je ključ veći od argumenta traženja, u kojem slučaju to znači da je traženje
neuspešno. Prosečan broj upoređivanja u oba slučaja je približno n/2, gde je n broj
čvorova (videti odeljak 3.2.1).
T* getItem(const KeyedList* lst,int arg) {
Node *curr=lst->first;
while((curr->next)&&(curr->key<arg)) curr=curr->next;
return (curr->key==arg) ? &(curr->item) : NULL;
}
Funkcija vraća pokazivač na polje item nađenog čvora (NULL ako nije nađen). Ovo
zato da bi se to polje moglo modifikovati. Zapazimo da je ključ u čvoru nedostupan za
modifikovanje jer bi to moglo da dovede do dupliranja ključeva, što se ne sme
dogoditi.
Uklanjanje čvora vrši se funkcijom
int removeItem(KeyedList* lst,int key) {
Node *curr,*prev;
prev=NULL; curr=lst->first;
while(curr&&(curr->key<key)) {prev=curr; curr=curr->next;} //trazenje cvora
if(!curr||(curr->key!=key)) return 0; //nije nadjen
if(!prev) lst->first=curr->next; //cvor je bio prvi
else prev->next=curr->next; //cvor nije bio prvi
free(curr);
lst->n--;
return 1;

45
Binarno traženje u listi bilo bi nedopustivo sporo, jer nema indeksiranja

158
Dušan T. Malbaški - Algoritmi i strukture podataka

}
Funkcija vraća vrednost 1 ako je uklanjanje uspešno i 0 u suprotnom (kada čvor za
uklanjanje nije nađen). U ciklusu while prate se istovremeno markirani čvor curr i
njegov prethodnik prev, sve dok se ne naiđe na čvor sa ključem koji je veći ili jednak
argumentu traženja, odnosno dok se ne stigne do kraja liste. Sledećom naredbom if
proverava se da li dostignuti čvor ima traženi ključ ili ga nema i u potonjem slučaju
funkcija se završava neuspehom. Ako je čvor nađen postoje dve alternative: ako se
uklanja prvi čvor treba ažurirati polje first liste, a ako čvor nije prvi treba ažurirati
polje next njegovog prethodnika prev. Čvor se briše funkcijom free i broj čvorova
smanjuje za 1.
Funkcija za dodavanje putItem je posebna, s obzirom na to da joj je zadatak ne
samo dodavanje novog čvora nego i održavanje stanja sortiranosti liste:
int putItem(KeyedList* lst, int key, T item) {
Node *newNode,*curr,*prev;
prev=NULL; curr=lst->first;
while(curr&&(curr->key<key)) {prev=curr; curr=curr->next;} //trazenje mesta
if(curr&&(curr->key==key)) return 0; //dupliran kljuc
newNode=malloc(sizeof(Node)); //formiranje novog cvora
newNode->key=key; newNode->item=item;
if(!prev) {newNode->next=lst->first; lst->first=newNode;} //novi cvor je prvi
else {newNode->next=curr;prev->next=newNode;} //novi cvor nije prvi
lst->n++;
return 1;
}
Prvo se ciklusom while traži mesto na kojem će se naći novi čvor, s obzirom na
veličinu njegovog ključa (parametar key). Kada se ciklus završi mora se proveriti da li
čvor sa ključem key već postoji u listi u kojem se slučaju od dodavanja odustaje i
funkcija vraća rezultat 0. Ako je mesto za novi čvor nađeno i ključ nije dupliran, prvo
se formira novi čvor koji treba dodati. Zatim se proveravaju dva moguća slučaja: ako
je novododati čvor prvi u listi ažurira se polje first, a ako nije ažurira se polje next
čvora prev koji prethodi novododatom. Povećava se broj čvorova n i vraća kôd
uspešnosti jednak 1.

159
Dušan T. Malbaški - Algoritmi i strukture podataka

Konačno, funkcija za redoslednu obradu traverse gotovo je identična verziji iz


jednostavne liste:
void traverse(KeyedList* lst,void (*pDoIt)(int key,T* pItem)) {
Node *curr=lst->first;
do {
(*pDoIt)(curr->key,&(curr->item));
} while((curr=curr->next)!=NULL);
}
Jedina razlika je u prototipu funkcije za obradu čvora pDoIt koja sada ima dva
parametra: ključ key i sadržaj item, tako da traverse prima za funkciju-parametar
svaku funkciju sa prototipom
void imeFunkcije(int,T*);

5.1.3. Sortiranje jednostruko spregnute liste


Za sortiranje jednostruko spregnute liste ne postoje posebne metode, nego se
koriste neki od postupaka za sortiranje niza. Pri tom, treba imati u vidu da nisu sve
metode sortiranja niza pogodne za jednostruko spregnutu listu, budući da lista nema
pogodan način za pristup proizvoljnoj poziciji u jednom pokušaju, niti mogućnost
prolaska u oba smera. Pri izboru efikasne metode za sortiranje moraju se uzeti u obzir
sledeća ograničenja:
 striktno izbegavati metode koje zahtevaju pristup prema poziciji; ova stavka,
recimo, eliminiše quicksort kao efikasnu metodu za sortiranje jednostruko
spregnute liste
 izbegavati metode koje zahtevaju kretanje po listi u oba smera; metoda
umetanja je upravo takva metoda
 prilikom izrade funkcije za sortiranje ni pod kojim uslovima ne vršiti izmenu
mesta čvorovima; dovoljno je razmenjivati njihov sadržaj.
Manje iskusni ili nebrižljivi programeri prenebregavaju treću stavku koja je očigledna
- ako se obrati pažnja. Naime, izmena mesta čvorovima prvo traje, a drugo
posložnjava algoritam sortiranja, iako se identičan efekat postiže prostom razmenom
sadržaja čvorova, bez promene strukture liste. Slika 5.5 objašnjava sve.

160
Dušan T. Malbaški - Algoritmi i strukture podataka

n
first A B ... NE!

n
first A B ... DA

Slika 5.5.

Od razmatranih metoda za sortiranje niza, pokazuje se da su najpogodnije metoda


izbora i bubblesort. Modifikacije koje treba izvršiti da bi se prilagodile jednostruko
spregnutoj listi, minimalne su i lako se unose. Kao primer, navešćemo metodu
bubblesort prilagođenu jednostavnoj listi, uz pretpostavku da je tip T polja item takav
da dozvoljava poređenje. Ako to nije slučaj, sve što treba uraditi jeste pronaći u
informacionom sadržaju polje po kojem se sortira i uneti ga u algoritam.
Modifikovanu metodu bubblesort navodimo bez posebnih objašnjenja, jer je sve
objašnjeno u odeljku o sortiranju niza.
void bubbleSort(SimpleList* lst) {
T tmp; Node* curr; int i,j,chn; //chn=0: nije bilo zamena u prolazu; zavrsiti
for(chn=1,i=lst->n-1;chn&&(i>0);i--)
for(curr=lst->first,j=chn=0;j<i;curr=curr->next,j++)
if(curr->item>curr->next->item)
{tmp=curr->item;curr->item=curr->next->item;curr->next->item=tmp;chn=1;}
}

5.2. DVOSTRUKO SPREGNUTA LISTA


Dvostruko spregnuta ili simetrična lista je, i formalno i suštinski, struktura
koja je vrlo bliska jednostruko spregnutoj listi. Definiše se kao uređeni par
DL = (S(DL),r(DL))
sa sledećim osobinama:
 struktura je bilinearna, što znači da se relacija r može razbiti na dva dela, r1 i
r2, za koje važi (a) (S(DL),r1) je linearna struktura i (b) (a,b)r1(b,a)r2;
odgovarajući digraf prikazan je na slici 5.6
 sve ostale osobine iste su kao kod jednostruko spregnute liste

161
Dušan T. Malbaški - Algoritmi i strukture podataka

...

Slika 5.6.

Doslovno sve što je rečeno o jednostruko spregnutoj listi, važi i za dvostruko


spregnutu listu: fleksibilna je, omogućuje sve tri vrste pristupa, dodavanje i uklanjanje
elemenata obavljaju se bez ograničenja, obrađuje se redosledno, može biti sortirana i
ne mora. Osnovna prednost dvostruko spregnute liste u odnosu na jednostruko
spregnutu jeste mogućnost kretanja (tj. sukcesivnog pristupa elementima) u oba
smera, osobina koju jednostruko spregnuta lista ne poseduje. U prethodnom odeljku
mogli smo primetiti da kod jednostruko spregnute liste sve akcije moraju otpočinjati
od prvog elementa jer je samo on dostupan direktno iz deskriptora. Tako, kada treba
npr. ukloniti neki element, njegovo prolanaženje mora početi od deskriptora, pri čemu
se mora pratiti i njegov prethodnik da bi se mogli ažurirati pokazivači. Ovo je
posebno nepogodno u slučaju da listu treba snabdeti mehanizmom navigacije, gde se
prati tekući element. Ako treba ukloniti tekući element, u jednostruko spregnutoj listi
moralo bi se ponovo krenuti od početka da bi se pronašao njegov prethodnik čiji
pokazivač treba da se ažurira. Dvostruko spregnuta lista je pogodnija za navigaciju jer
se iz tekućeg elementa može direktno pristupiti i njegovom pethodniku i njegovom
sledbeniku, bez potrebe traženja. Dakle, osnovna prednost dvostruko spergnute liste u
odnosu na jednostruko spregnutu je veća pogodnost za navigaciju, a cena toga su
nešto (ne mnogo) sporije operacije zbog potrebe održavanja dva, a ne jednog
pokazivača.
Pored toga, dvostruko spregnuta lista omogućuje primenu nekih metoda
sortiranja koje su nepogodne kod jedostruko spregnute liste. Naime, u prethodnom
odeljku pomenuli smo da su postupci sortiranja koji zahtevaju prolazak kroz listu
unazad (npr. metoda umetanja) izrazito nepogodni za primenu kod jednostruko
spregnute liste. Pošto dvostruko sprenuta lista dozvoljava prolazak u oba smera, takve
vrste ograničenja kod nje nema, što ćemo demonstrirati u nastavku.
S obzirom na to da je glavna prednost dvostruko spregnute liste pogodnost za
navigaciju, fizičku realizaciju prikazaćemo upravo na tom primeru.

162
Dušan T. Malbaški - Algoritmi i strukture podataka

5.2.1. Dvostruko spregnuta lista sa navigacijom


Dvostruko spregnute liste svih vrsta, kao i jednostruko spregnute, realizuju se
isključivo povezivanjem pomoću pokazivača. Kod dvostruko spregnute liste u okviru
svakog čvora postoje dva pokazivača: jedan pokazuje na levog suseda, a drugi na
desnog, slika 5.7.

left item right

Slika 5.7.

Pokazivač left pokazuje na levog suseda, a right na desnog. Polje item tipa T
predstavlja informacioni sadržaj čvora. Na ovom primeru pokazaćemo još jednu
tehniku primenljivu na sve spregnute strukture sa deskriptorom (ne samo na ovu vrstu
dvostruko spregnute liste). Ako se osvrnemo na realizaciju jednostruko spregnute liste
iz prethodnih primera, videćemo da se u deskriptoru nalazi pokazivač first na početak
liste. Ovaj pokazivač se po formatu razlikuje od čvora, što realizaciju čini unekoliko
neuniformnom. Tehnika koju ćemo prikazati, umesto pokazivača na levi i desni kraj
liste sadržaće kvazi-čvorove, po formatu iste kao i čvorovi liste. Ovi kvazielementi
nose naziv sentineli (od engleskog sentinel=stražar), po tipu su čvorovi i čine neku
vrstu graničnika između kojih se nalaze čvorovi liste. Pri tom, levi sused levog
sentinela je NULL pokazivač, kao i desni sused desnog sentinela. Upotreba sentinela
pojednostavljuje realizaciju funkcija liste, jer se sentineli programski tretiraju isto kao
i čvorovi. Na slici 5.8 prikazana je realizacija dvostruko spregnute liste sa
navigacijom, uz upotrebu sentinela.

n
backupCurrent
current

* leftSentinel ...
rightSentinel *

Slika 5.8.

163
Dušan T. Malbaški - Algoritmi i strukture podataka

Na slici se vidi izvesno preimućstvo tehnike sentinela u pogledu uniformnosti: svaki


čvor liste ima i levog i desnog suseda, te su u tom pogledu svi čvorovi identični.
Polje n sadrži broj čvorova u listi. Polje current je pokazivač (nije kvazi-čvor
poput sentinela), koji pokazuje na tekući element za navigaciju. Polje backupCurrent
je polje u kojem se, po potrebi, memoriše trenutna vrednost pokazivača current (uloga
će biti objašnjena kasnije). Konačno, polja leftSentinel i rightSentinel jesu levi i desni
sentinel respektivno. Na slici su zvezdicama označemi NULL pokazivači. Definicija
tipa Node čvorova liste ima sledeći oblik:
typedef struct node {
T item;
struct node* left;
struct node* right;
} Node;
gde je item informacioni sadržaj tipa T, a left i right su pokazivači respektivno na
levog i desnog suseda. Definicija tipa liste (tj. deskriptora) sa nazivom DoubleList ima
oblik
typedef struct {
int n;
Node leftSentinel;
Node rightSentinel;
Node *current;
Node *backupCurrent;
} DoubleList;
Polje n sadrži aktuelnu dužinu liste. Polja leftSentinel i rightSentinel su levi i desni
sentinel (uočiti tip!). Polje current je pokazivač na tekući element, dok je
backupCurrent polje u kojem se privremeno može odložiti pokazivač na tekući
element. Ova operacija odlaganja sa pripadajućom operacijom restaurisanja mogu da
posluže u slučajevima kada listu treba obraditi (recimo štampati sadržaj), a zatim
restaurisati početno stanje.
Repertoar operacija može se formirati na mnogo načina: na primer, mogu se
realizovati pojedinačne operacije pristupa levom odn. desnom kraju i odgovarajuće
operacije prelaska na levog odn. desnog suseda. U ovom primeru opredelićemo se za
drukčiji repertoar koji je inspirisan operacijama kod tipa datoteke u C-u, upravo zato

164
Dušan T. Malbaški - Algoritmi i strukture podataka

što je upravljanje datotekom na neki način paradigma za mehanizam navigacije.


Podelićemo operacije u nekoliko grupa:
 operacija create kreiranja liste, koja mora postojati i koja dovodi listu u
početno stanje prazne liste
 operacije čitanja i upisa u listu, lread i lwrite
 operacije uklanjanja i dodavanja, removeItem i putItem
 operacije vezane za upravljanje pozicijom tekućeg elementa, lseek i ltell
 indikatori koji daju informaciju o veličini liste (size), o tome da li je lista
prazna (isEmpty) i o tome da li je trenutna pozicija tekućeg elementa na
sentinelu (isSentinel)
 operacija clear za pražnjenje liste
 operacije saveCurrent i restoreCurrent za odlaganje i restaurisanje pozicije
tekućeg elementa
 u svrhu ilustracije mogućnosti dvostruko spregnute liste u repertoar ćemo
uključiti i operaciju insertionSort za sortiranje liste metodom umetanja (za
koju smo konstatovali da je nepogodna kod jednostruko spregnute liste).
Operacija create za kreiranje liste ima za zadatak da pripremi listu za upotrebu.
void create(DoubleList* lst) {
lst->leftSentinel.right=&lst->rightSentinel;
lst->leftSentinel.left=lst->rightSentinel.right=NULL;
lst->current=lst->rightSentinel.left=&lst->leftSentinel;
lst->n=0;
}
Zapazimo da u početnom stanju, preciznije u stanju prazne liste, desni pokazivač
levog sentinela pokazuje na desni sentinel i takođe levi pokazivač desnog sentinela
pokazuje na levi sentinel (tj. pokazivači nisu NULL). Ovo zato što će sukcesivna
primena operacije uklanjanja na kraju, kada više nema čvorova, listu dovesti upravo u
takvo stanje. Levi pokazivač levog sentinela i desni pokazivač desnog sentinela
dobijaju vrednost NULL koja se neće menjati (čak bismo ih mogli ostaviti i
nedefinisanim). Pokazivač tekućeg elementa postavlja se na adresu levog sentinela
(mogli smo ga postaviti i na adresu desnog, bez uticaja na realizaciju ostalih
operacija). Aktuelna dužina liste je, naravno, nula. Početno stanje liste prikazano je na
slici 5.9.

165
Dušan T. Malbaški - Algoritmi i strukture podataka

backupCurrent 0 n

-
current

* leftSentinel
rightSentinel *
Slika 5.9.

Operacije čitanja i upisa (ne dodavanja!) informacionog sadržaja napravljene su po


ugledu na odgovarajuće funkcije fread i fwrite kod datoteke u C-u. Obe funkcionišu
samo ako postoji tekući element (tj. ako pokazivač current ne pokazuje na sentinel).
Funkcija lread za čitanje informacionog sadržaja tekućeg elementa izgleda ovako:
int lread(DoubleList* lst,void* item, int itemsize, int direction) {
if(isSentinel(lst)) return 0;
memcpy(item,&(lst->current->item),itemsize); //kopiranje informacionig sadrzaja
switch(direction) {
case RIGHT: lst->current=lst->current->right; break; //pomeriti udesno
case LEFT: lst->current=lst->current->left; //pomeriti ulevo
}
return 1;
}
Funkcija vraća vrednost 0 ako čitanje nije uspelo, a to je slučaj kada pokazivač
tekućeg elementa pokazuje na sentinel, ili zato što je pomeren na tu poziciju ili zato
što je lista prazna. U suprotnom slučaju, funkcija vraća rezultat 1. Generički (void)
pokazivač item sadrži adresu memorijske zone u koju će biti kopiran informacioni
sadržaj tekućeg elementa. Parametar itemsize sadrži veličinu u bajtovima (dobija se
pomoću sizeof) pomenute memorijske zone. Za funkcije lread i lwrite vezuju se i tri
simboličke promenljive: RIGHT jednaka 1, LEFT jednaka -1 i CURRENT jednaka 0,
definisane sa
#define LEFT -1
#define RIGHT 1
#define CURRENT 0
One određuju šta će se dogoditi posle učitavanja sadržaja tekućeg elementa. Ako se na
mestu parametra direction nađe vrednost LEFT, marker tekućeg elementa se po

166
Dušan T. Malbaški - Algoritmi i strukture podataka

završetku lread pomera jedno mesto ulevo; ako je na tom mestu konstanta RIGHT
marker će se pomeriti za jedno mesto udesno; ako je argument direction jednak
CURRENT tekući element ostaje isti. Skrećemo pažnju i na primenu standardne
funkcije memcpy koja na memorijsku lokaciju zadatu adresom prvog argumenta šalje
sadržaj memorijske lokacije čija je adresa drugi argument, dok treći argument
označava broj bajtova koji se kopiraju.
Funkcija lwrite ponaša se istovetno, s tom razlikom što se informacioni sadržaj
upisuje u tekući element:
int lwrite(DoubleList* lst,void* item, int itemsize, int direction) {
if(isSentinel(lst)) return 0;
memcpy(&(lst->current->item),item,itemsize);
switch(direction) {
case RIGHT: lst->current=lst->current->right; break;
case LEFT: lst->current=lst->current->left;
}
return 1;
}
S obzirom na to da je sve isto kao kod lread, osim smera razmene podataka, na ovoj
funkciji nećemo se više zadržavati.
Operacija uklanjanja čvora, removeItem, realizovana je funkcijom koja uklanja
tekući element:
int removeItem(DoubleList* lst) {
Node *tmp;
if(isSentinel(lst)) return 0;
tmp=lst->current;
lst->current->left->right=lst->current->right;
lst->current->right->left=lst->current->left;
lst->current=(lst->current->right!=&lst->rightSentinel) ? lst->current->right : lst-
>current->left;
free(tmp);
lst->n--;
return 1;
}

167
Dušan T. Malbaški - Algoritmi i strukture podataka

Na početku se proverava da li je marker tekućeg elementa current na sentinelu, pa ako


jeste operacija se završava neuspehom (kôd 0). Pokazivač tmp postavlja se na tekući
element, da bi se kasnije odgovarajuća memorija oslobodila preko njega. Potom se
ažuriraju pokazivači, kako je prikazano na slici 5.10. Posle ažuriranja mora se
definisati novi tekući element i to je desni sused bivšeg tekućeg, osim u slučaju kada
je desni sused sentinel kada novi tekući postaje levi sused. Aktuelni broj čvorova n
smanjuje se za 1 i vraća kôd uspešnosti jednak 1.

current

Slika 5.10.

Na primeru uklanjanja može se uočiti prednost korišćenja sentinela, koja se sastoji u


tome što nema potrebe za proverom rubnih slučajeva uklanjanja krajnjeg levog i
krajnjeg desnog čvora. Čitaocu se preporučuje da napravi listu u kojoj se u
deskriptoru nalaze pokazivači na levi i desni kraj, te da se uveri u to da se tada ovi
slučajevi moraju posebno proveravati.
Dodavanje čvora u listu je specifična operacija jer u dobro napravljenoj listi
postoje dva dodavanja: dodavanje ispred tekućeg i dodavanje iza tekućeg. U našem
primeru obe vrste dodavanja realizovaćemo jednom funkcijom putItem u kojoj će
parametrom direction biti određeno o kojoj se vrsti dodavanja radi. Simboličke
konstante LEFT i RIGHT koje mogu biti argumenti determinišu dodavanje: u prvom
slučaju dodaje se levo od tekućeg, a u drugom desno od njega. U oba slučaja
novododati element postaje tekući. I kod dodavanja mogu se primetiti prednosti
korišćenja sentinela, jer postoji samo jedan, neizbežan, specijalni slučaj: dodavanje u
praznu listu. Drugih rubnih situacija nema, što ne bi bio slučaj da se u deskriptoru
nalaze obični pokazivači na levi i desni kraj.
int putItem(DoubleList* lst,T item,int direction) {
Node *newNode;
if(!isEmpty(lst)&&isSentinel(lst)) return 0;
newNode=malloc(sizeof(Node));
newNode->item=item;

168
Dušan T. Malbaški - Algoritmi i strukture podataka

if(isEmpty(lst)) {
newNode->left=&lst->leftSentinel; //lista bila prazna
newNode->right=&lst->rightSentinel;
lst->leftSentinel.right=lst->rightSentinel.left=newNode;}
else switch(direction){
case LEFT:
newNode->right=lst->current;
newNode->left=lst->current->left;
lst->current->left->right=newNode;
lst->current->left=newNode;
break;
case RIGHT:
newNode->left=lst->current;
newNode->right=lst->current->right;
lst->current->right->left=newNode;
lst->current->right=newNode;
}
lst->current=newNode;
lst->n++;
return 1;
}
U funkciji se prvo proverava da li je u nepraznoj listi marker tekućeg elementa na
sentinelu, u kojem slučaju se odustaje od operacije sa rezultatom 0. Ako to nije slučaj,
formira se novi čvor i, u zavisnosti od mesta dodavanja, novi čvor vezuje levo
odnosno desno od tekućeg. Novi čvor postaje tekući, dužina liste povećava se za 1 i
vraća kôd uspešnosti jednak 1. Na slici 5.11 prikazana je šema dodavanja, za slučaj da
se dodaje levo od tekućeg (direction=LEFT). Dodavanje desno od tekućeg izvodi se
analogno.
current

Slika 5.11.

169
Dušan T. Malbaški - Algoritmi i strukture podataka

Pažljiv čitalac može postaviti pitanje „šta se dešava ako (greškom) parametar
direction dobije vrednost koja nije ni LEFT ni RIGHT“? Odgovor nije trivijalan i
vezan je čak za stil programiranja. U jezicima tipa paskala obaveza programera koji
realizuje listu bila bi da ugradi proveru te okolnosti, i da obezbedi da se konstatuje
greška. Programski jezik C, u principu, nije takav: zbog brzine (a provera traje!),
egzotične omaške se jednostavno ne uzimaju u obzir i odgovornost za korektnu
upotrebu funkcija liste prepušta se programeru koji ih koristi. To je razlog zbog kojeg
u funkciji putItem, u naredbi switch nema treće default varijante (dakle, ni LEFT ni
RIGHT) u kojoj bi se rad funkcije prekinuo sa porukom o grešci.
Upravljanje pozicijom tekućeg elementa obavlja se primenom dve funkcije,
lseek i ltell čiji je oblik ponovo inspirisan odgovarajućim funkcijama kod datotečnog
tipa u C-u.
int lseek(DoubleList* lst, int offset, int base) {
Node *nod; int i=0;
switch(base) {
case LEFT: nod=lst->leftSentinel.right; break;
case RIGHT: nod=lst->rightSentinel.left; break;
case CURRENT: if(isSentinel(lst)) return 0; else nod=lst->current;
}
if(offset>0) while((i++<offset)&&nod->left&&nod->right) nod=nod->right;
else while((i-->offset)&&nod->left&&nod->right) nod=nod->left;
if(!(nod->left&&nod->right)) return 0; //ako je sentinel vrati 0
lst->current=nod;
return 1;
}
Funksija lseek postavlja pokazivač current na čvor koji je zadat parametrima base i
offset. Pozicija tekućeg elementa zadaje se relativno, u odnosu na tri moguće baze:
može se računati u odnosu na levi kraj, u odnosu na desni kraj i u odnosu na trenutni
tekući element. U prvom slučaju parametar base treba postaviti na vrednost
simboličke konstante LEFT, u drugom na vrednost RIGHT i za slučaj da se nova
pozicija računa u odnosu na tekući, na vrednost CURRENT. Parametar offset
predstavlja rastojanje od odabrane baze izraženo brojem čvorova. Primeri za listu lst:
lseek(&lst,0,LEFT) //postavljanje na levi kraj

170
Dušan T. Malbaški - Algoritmi i strukture podataka

lseek(&lst,0,RIGHT) //postavljanje na desni kraj


lseek(&lst,-1,CURRENT) //pomeranje tekuceg za jedno mesto ulevo
lseek(&lst,1,CURRENT) //pomeranje tekuceg za jedno mesto udesno
lseek(&lst,-10,RIGHT) //postavljanje na deseto mesto mereno s desna
U sva tri slučaja do nove pozicije tekućeg elementa dolazi se sukcesivnim praćenjem
pokazivača. Ukoliko operaciju nije moguće izvesti, funkcija lseek vraća vrednost 0.
Funkcija ltell vraća poziciju tekućeg elementa u odnosu na levi kraj liste. U vezi sa
ovom funkcijom treba uočiti da se ona izvršava relativno sporo jer mora da prođe sve
pozicije od levog kraja do tekućeg. Još nešto: pošto je 0 regularan izlaz (tekući
element je prvi s leva), za neuspešan ishod (ako je marker tekućeg na sentinelu ili je
lista prazna) predviđena je vrednost -1.
int ltell(const DoubleList* lst) {
int posOfCurr; Node *nod;
if(isEmpty(lst)||isSentinel(lst)) return -1;
for(nod=lst->leftSentinel.right,posOfCurr=0;nod!=lst->current; nod=nod->right)
posOfCurr++;
return posOfCurr;
}
Funkcije-indikatori su standardne funkcije isEmpty za proveru da li je lista prazna i
size za određivanje broja elemenata. Imaju istu ulogu kao kod jednostruko spregnute
liste i ne zahtevaju dodatna objašnjenja:
int isEmpty(const DoubleList* lst) {
return lst->n==0;
}
int size(const DoubleList* lst) {
return lst->n;
}
Poslednji, treći, indikator je funkcija isSentinel koja daje informaciju o tome da li je
marker tekućeg elementa iznad sentinela:
int isSentinel(const DoubleList* lst) {
return !((lst->current->left)&&(lst->current->right));
}

171
Dušan T. Malbaški - Algoritmi i strukture podataka

Korišćena je za realizaciju većine ostalih funkcija, a potrebna je i u klijentskom


softveru zato što uzastopna primena lread odnosno lwrite sa pomeranjem tekućeg
elementa može dovesti marker na sentinel. Pošto u tim slučajevima lista ne može da
se obrađuje, odgovarajući indikator mora postojati.
Za pražnjenje liste koristi se funkcija clear koja, u osnovi, radi isto kao kod
jednostruko spregnute liste:
void clear(DoubleList* lst) {
Node *nod=lst->leftSentinel.right,*tmp;
if(isEmpty(lst)) return;
while(nod!=&lst->rightSentinel) {
tmp=nod;
nod=nod->right;
free(tmp);
}
lst->leftSentinel.right=&lst->rightSentinel;
lst->current=lst->rightSentinel.left=&lst->leftSentinel;
lst->n=0;
}
Pokazivačem nod prolazi se kroz listu počev od levog kraja i redom se oslobađa
memorija dodeljena svakom čvoru. Na kraju, pokazivači na sentinele, pokazivač
current i polje n podešavaju se na iste vrednosti kao kod kreiranja liste (slika 5.9).
Konačno, tu su i funkcije saveCurrent i restoreCurrent koje služe za
privremeno odlaganje i restauraciju pokazivača na tekući element.
void saveCurrent(DoubleList* lst) {
lst->backupCurrent=lst->current;
}

void restoreCurrent(DoubleList* lst) {


lst->current=lst->backupCurrent;
}
Pokazivač current se pomoću saveCurrent kopira u polje backupCurrent deskriptora,
a restauriše se čitanjem sadržaja tog polja.

172
Dušan T. Malbaški - Algoritmi i strukture podataka

5.2.2. Obrada dvostruko spregnute liste


U načelu, dvostruko spregnuta lista obrađuje se kao i jednostruko spregnuta
lista, s tom razlikom što se može redosledno obrađivati u oba smera. Ono što nas, na
ovom mestu, interesuje jeste način obrade dvostruko spregnute liste sa navigacijom iz
odeljka 5.2.1..
Prvo, obrada proizvoljno odabranih elemenata obavlja se pristupom prema
poziciji koji se, sa svoje strane, realizuje preko navigacije. To znači da ako je
potrebno obraditi element liste lst na poziciji position0, prethodno taj čvor treba
proglasiti za tekući čvor primenom funkcije lseek:
T info; //promenljiva za ucitavanje informacionog sadrzaja
lseek(&lst,position,LEFT); //pozicioniranje tekuceg
lread(&lst,&info, sizeof(T),CURRENT) //marker tekuceg se ne pomera
obraditi promenljivu info
Redosledna obrada liste vrlo je jednostavna zahvaljujući načinu na koji je
realizovana funkcija lread. Lista se može obrađivati s leva u desno ili u obrnutom
smeru. Neka je, ponovo, u pitanju lista lst i promenljiva info za prihvat informacionog
sadržaja čvora. Šema obrade liste s leva u desno izgleda ovako:
T info;
lseek(&lst,0,LEFT); //postavljanje tekućeg na levi kraj liste
while(lread(&lst,&info,sizeof(T),RIGHT)) obraditi info
Uočimo da je ciklus za redoslednu obradu konstrolisan izlazom funkcije lread koji je
jednak 1 sve dok ima neobrađenih elemenata. Uočimo, takođe, da je na kraju ciklusa
tekući element pozicioniran na desni sentinel. Obrada s desna u levo analogna je:
T info;
lseek(&lst,0,RIGHT); //postavljanje tekućeg na desni kraj liste
while(lread(&lst,&info,sizeof(T),LEFT)) obraditi info
U svrhu ilustracije redosledne obrade napravićemo funkciju za prikaz sadržaja liste na
ekranu, uz pretpostavku da informacioni sadržaj liste čine znaci, tj. da je Tchar.
void showDoubleList(DoubleList* lst) {
char item;
printf("\nSADRZAJ LISTE: ");
if(isEmpty(lst)) printf("Prazna. Size: %d",size(lst));
else {

173
Dušan T. Malbaški - Algoritmi i strukture podataka

saveCurrent(lst); //memorisati poziciju tekuceg


lst->current=lst->leftSentinel.right;
while(lread(lst,&item,sizeof(char),RIGHT)) printf(" %c",item);
restoreCurrent(lst); //restaurisati poziciju tekuceg
printf(" Current: %d",ltell(lst)); //stampanje velicine liste
}
}
Uočimo primenu funkcije saveCurrent i restoreCurrent: pre štampanja liste, pozicija
tekućeg elementa se memoriše pomoću saveCurrent, a po završetku štampe restauriše
pomoću restoreCurrent, tako da postupak štampanja ostavlja listu u prvobitnom
stanju.
Kao što smo rekli, dvostruko spregnuta lista može se sortirati i nekim od
metoda koje su nepogodne za jednostruko spregnutu listu jer zahtevaju prolazak kroz
listu u oba smera. Jedna od tih metoda je metoda umetanja iz odeljka 2.2.3. Algoritam
sortiranja metodom umetanja, primenjen na dvostruko spregnutu listu razlikuje se od
verzije iz odeljka 2.2.3 samo po načinu pristupa čvorovima, jer kod liste nema
indeksiranja. Sve ostalo je isto, tako da se čitaocu preporučuje da uporedi te dve
verzije:
void insertionSort(DoubleList* lst) {
Node *first,*pI,*pJ; T tmp;
if(lst->n<2) return;
for(first=lst->leftSentinel.right,pI=first->right;pI->right;pI=pI->right)
for(pJ=pI;(pJ!=first)&&(pJ->item)<(pJ->left->item);pJ=pJ->left)
{tmp=pJ->item;pJ->item=pJ->left->item;pJ->left->item=tmp;}
lst->current=lst->leftSentinel.right;
}
Jedini dodatak kod liste je poslednja linija kôda u kojoj se za tekući element
proglašava prvi s leva.

5.2.3. Demonstracioni program


Navešćemo, na kraju, i demonstracioni program za dvostruko spregnutu listu,
da bi čitalac mogao da vidi kako se izvršavaju glavne operacije. Pretpostavka je da se
u elementima liste nalaze znaci, tj. da je Tchar.

174
Dušan T. Malbaški - Algoritmi i strukture podataka

typedef char T;
int main()
{
DoubleList lst; T itm; int i;

printf("KREIRANA LISTA");
create(&lst);
showDoubleList(&lst);

printf("\n\nUPISANI ELEMENTI a,b,c,d");


putItem(&lst,'a',LEFT);
putItem(&lst,'b',LEFT);
putItem(&lst,'c',RIGHT);
putItem(&lst,'d',RIGHT);
showDoubleList(&lst);

printf("\n\nPROMENJEN TEKUCI U x");


itm='x';
lwrite(&lst,&itm,sizeof(T),CURRENT);
showDoubleList(&lst);

printf("\n\nUKLONJENA DVA ELEMENTA");


removeItem(&lst);
removeItem(&lst);
showDoubleList(&lst);

printf("\n\nUKLONJENA JOS DVA ELEMENTA");


removeItem(&lst);
removeItem(&lst);
showDoubleList(&lst);

printf("\n\nUPISANI a,b,c,d,e,f");
putItem(&lst,'a',RIGHT);

175
Dušan T. Malbaški - Algoritmi i strukture podataka

putItem(&lst,'b',RIGHT);
putItem(&lst,'c',RIGHT);
putItem(&lst,'d',RIGHT);
putItem(&lst,'e',RIGHT);
putItem(&lst,'f',RIGHT);
showDoubleList(&lst);

printf("\n\nPOZICIONIRAN NA LEVI KRAJ");


lseek(&lst,0,LEFT);
printf("\nCurrent: %d",ltell(&lst));

printf("\n\nPOZICIONIRAN NA DESNI KRAJ");


lseek(&lst,0,RIGHT);
printf("\nCurrent: %d",ltell(&lst));

printf("\n\nPOZICIONIRAN NA POZICIJU 3 S LEVA");


lseek(&lst,3,LEFT);
showDoubleList(&lst);
printf("\nCurrent: %d",ltell(&lst));

printf("\n\nPOZICIONIRAN JEDAN ULEVO");


lseek(&lst,-1,CURRENT);
showDoubleList(&lst);
printf("\nCurrent: %d",ltell(&lst));

printf("\n\nPOZICIONIRAN DVA UDESNO");


lseek(&lst,2,CURRENT);
showDoubleList(&lst);
printf("\nCurrent: %d",ltell(&lst));

printf("\n\nPOZICIONIRAN NA ISTO MESTO");


lseek(&lst,0,CURRENT);
showDoubleList(&lst);

176
Dušan T. Malbaški - Algoritmi i strukture podataka

printf("\nCurrent: %d",ltell(&lst));

printf("\n\nSORTIRANJE LISTE");
clear(&lst);
// upis slucajno odabranih znakova u listu
for(i=0;i<20;i++) putItem(&lst,65+rand()%26,RIGHT);
printf("\nPolazna lista: ");
showDoubleList(&lst);
insertionSort(&lst);
printf("\nSortirana lista: ");
showDoubleList(&lst);

printf("\n\nLISTA ISPRAZNJENA");
clear(&lst);
showDoubleList(&lst);

printf("\n\n");
return 0;
}
Izlaz koji generiše demosntracioni program treba da izgleda ovako:
KREIRANA LISTA
SADRZAJ LISTE: Prazna. Size: 0

UPISANI ELEMENTI a,b,c,d


SADRZAJ LISTE: b c d a Current: 2

PROMENJEN TEKUCI U x
SADRZAJ LISTE: b c x a Current: 2

UKLONJENA DVA ELEMENTA


SADRZAJ LISTE: b c Current: 1

177
Dušan T. Malbaški - Algoritmi i strukture podataka

UKLONJENA JOS DVA ELEMENTA


SADRZAJ LISTE: Prazna. Size: 0

UPISANI a,b,c,d,e,f
SADRZAJ LISTE: a b c d e f Current: 5

POZICIONIRAN NA LEVI KRAJ


Current: 0

POZICIONIRAN NA DESNI KRAJ


Current: 5

POZICIONIRAN NA POZICIJU 3 S LEVA


SADRZAJ LISTE: a b c d e f Current: 3
Current: 3

POZICIONIRAN JEDAN ULEVO


SADRZAJ LISTE: a b c d e f Current: 2
Current: 2

POZICIONIRAN DVA UDESNO


SADRZAJ LISTE: a b c d e f Current: 4
Current: 4

POZICIONIRAN NA ISTO MESTO


SADRZAJ LISTE: a b c d e f Current: 4
Current: 4

SORTIRANJE LISTE
Polazna lista:
SADRZAJ LISTE: P H Q G H U M E A Y L N L F D X F I R C Current: 19
Sortirana lista:
SADRZAJ LISTE: A C D E F F G H H I L L M N P Q R U X Y Current: 0

178
Dušan T. Malbaški - Algoritmi i strukture podataka

LISTA ISPRAZNJENA
SADRZAJ LISTE: Prazna. Size: 0

5.3. MULTILISTA
Multilista ili višestruka lista je struktura podataka koja se dobija
superpozicijom dve ili više jednostruko ili dvostruko spregnutih listi sastavljenih od
čvorova koji čine podskup istog skupa. Osnovna namena multiliste jeste da obezbedi
efikasno pretraživanje, uz ostale standardne operacije nad listama. Shodno tome,
glavna vrsta pristupa je pristup po informacionom sadržaju, kako prema ključu, tako i
prema vrednostima pôlja koja nisu ključ. Kako ta polja moraju da se definišu unapred,
običaj je da se ona nazivaju sekundarnim ključevima. Na slici 5.12 prikazana je šema
jedne multiliste.

Slika 5.12.

Veze su predstavljene strelicama sa dvostrukim vrhom da bi se podvuklo da mogu biti


dvostrane ili jednostrane, tj. da odgovarajuća podlista može biti jednostruko ili
dvostruko spregnuta. Multilista na slici sastoji se od tri podliste i svaka od njih
prikazana je drukčijim stilom strelica. Jedna od njih sadrži sve čvorove liste, a druge
dve ne, što inače nije obavezno. U načelu, svaka podlista sadrži neke od čvorova, ali
praksa je takva da obično jedna od njih povezuje sve čvorove. Multilista sa slike 5.12
sastoji se od tri podliste među kojima jedna, nacrtana punim linijama, povezuje sve
elemente, a druge dve (nacrtane isprekidanim i tačka-crta linijama) ne.
U opštem slučaju, multilistom se povezuju elementi u skladu sa različitim
kriterijumima, za razliku od dvostruko spregnute liste u kojoj se elementi povezuju
prema datom kriterijumu u jednom smeru i prema suprotnom kriterijumu (dakle
negaciji istog kriterijuma) u drugom smeru. Shodno tome, definiciju multiliste
zasnovaćemo da dve ili više relacija definisanih na istom skupu elemenata. Multilista
je uređena n+1-orka

179
Dušan T. Malbaški - Algoritmi i strukture podataka

ML = (S(ML),r1,...,rn)
gde je S(ML) skup elemenata, dok su r1,...,rn binarne relacije u tom skupu takve da
svaki uređeni par (si,ri), siS(ML), i=1,...,n, čini jednostruko ili dvostruko spregnutu
listu. Uslov siS(ML) odražava činjenicu da svaka od podlisti može i ne mora da
sadrži sve elemente multiliste. Podvucimo još jednom da u praksi jedna od tih podlisti
ipak povezuje sve elemente.
Recimo, podaci o studentima mogli bi da se urede u multilistu tako da jedna
podlista povezuje sve studente, druga samo one koji imaju prosek veći od 8,0, treća
studente sa stipendijom i četvrta samofinansirajuće studente.
Pritom, svaka od podlisti, bila ona jednostruko ili dvostruko spregnuta, ima
sve osobine takve liste. Dakle, ono što nas, u ovom slučaju interesuje, jeste pitanje šta
multilista pruža kao takva, tj. u čemu su njene specifičnosti, kao celine. Kako je već
naznačeno, multilista ima za glavnu osobinu efikasno pretraživanje, po cenu toga što
su sve ostale operacije usporene. Neka su, na primer, podaci o studentima smešteni u
jednu jedinu listu dužine k. Ako treba obraditi podatke o studentima koji imaju prosek
8,0 ili više, operacija bi zahtevala pristup svim elementima (k pristupa). Pre svake
obrade, morala bi se izvršiti provera da li dati element zadovoljava taj uslov, pa ako
ga ne zadovoljava odmah preći na sledeći. Ako bismo iste podatke uredili u multilistu,
obrada bi zahtevala mnogo manje pristupa jer bi se pratila samo odgovarajuća podlista
i pristupilo bi se isključivo elementima koji zadovoljavaju pomenuti kriterijum (jer
samo oni spadaju u podlistu), a to bi zahtevalo manje ili znatno manje od k pristupa.
Isto tako, i složeniji upiti se mogu ubrzati: ako je potrebno obraditi podatke o
samofinansirajućim studentima sa prosekom preko 8,0 dovoljno je proći kroz kraću
od te dve podliste i obraditi elemente koji zadovoljavaju drugi kriterijum.
Multilista se realizuje isključivo spregnuto, pri čemu se za svaku podlistu,
čvor proširuje dodatnim pokazivačem (ili parom pokazivača ako je podlista
dvostruka). U deskriptoru se nalaze pokazivači (ili sentineli) na svaku podlistu
ponaosob. Zapazimo, da način realizacije zahteva da se broj i vrsta podlisti
(jednostruko ili dvostruko spregnuta) mora znati unapred i ne može se menjati bez
reprogramiranja.
U vezi sa operacijama takođe treba povesti računa o nekim specifičnostima.
Pre svega, osnovne operacije izvode se u svakoj podlisti na način koji smo opisali u
prethodnim poglavljima. Međutim, postoje još neke okolnosti na koje skrećemo

180
Dušan T. Malbaški - Algoritmi i strukture podataka

pažnju. Prvo, uklanjanje elementa iz neke podliste ne mora da znači i njegovo


uklanjanje iz multiliste: ako je, na primer, nekom studentu prosečna ocena pala ispod
8,0 on će biti uklonjen iz odgovarajuće podliste, ali ne i iz multiliste. I obrnuto, ako je
studentu prosečna ocena prešla limit od 8,0 on neće biti dodat u multilistu (jer je
odgovarajući čvor već u multilisti), nego će samo biti uključen u pomenutu podlistu.
Pored ovih, specifičnih operacija dodavanja i uklanjanja, multilista mora sadržati i
„pravo“ dodavanje gde se pojavljuje novi čvor u uključuje u sve podliste u kojima
treba da se nađe. Isto važi i za „pravo“ uklanjanje gde se čvor briše iz multiliste, pri
čemu se moraju podesiti sve podliste u koje je uklonjeni čvor bio uključen.
Na kraju, nekoliko napomena u vezi sa strukturom podlisti. Pomenuto je da
podlista može (čak svaka ponaosob) biti jednostruko ili dvostruko spregnuta.
Razmotrimo, ipak, kako to izgleda u praksi. Prvo, jedna od podlisti (nazovimo je
primarna podlista) povezuje sve elemente. Ona može biti jednostruko ili dvostruko
spregnuta. Sasvim drukčije stoji stvar sa ostalim podlistama. Naime, ako treba
ukloniti čvor iz primarne podliste („pravo“ uklanjanje), moraju se ažurirati i sve
podliste u kojima se nalazi taj čvor. To bi, pak, značilo da odgovarajući algoritam
treba da izvrši prevezivanje čvorova u svim tim podlistama. U slučaju da su podliste
jednostruko spregnute, to bi značilo da se svaka od njih mora pregledati od početka u
svrhu pronalaženja prethodnika i njegovog prevezivanja na sledbenik uklonjenog
elementa, što je i komplikovan i relativno dugotrajan proces. Ukoliko su te podliste
dvostruko spregnute, prevezivanje se vrši direktno iz uklonjenog elementa, jer se u
njemu nalazi i pokazivač na prethodnika. Potpuno je analogna situacija i sa
dodavanjem u primarnu podlistu. Stoga, ako se želi efikasna multilista, osnovne
preporuke bile bi
 primarna podlista treba da postoji i ona može biti jednostruko ili dvostruko
spregnuta
 ostale podliste treba da budu realizovane dvostrukim sprezanjem

181
Dušan T. Malbaški - Algoritmi i strukture podataka

6. STABLO

Stablo (engl. tree), kao struktura, odražava fundamentalni odnos hijerarhije u


najopštijem smislu reči. Svaki postupak koji ima elemente klasifikacije, kompozicije-
dekompozicije, pa i analize-sinteze ima za prirodan model upravo stablo, te stoga ne
iznenađuje velik značaj ove strukture, uopšte i u organizaciji podataka, posebno.
Naziv „stablo“ potiče od engleske reči „tree“ za koju je direktan prevod
„drvo“, što je u upotrebi kod nekih autora kao alternativni termin. Međutim, izraz
„drvo“ koristi se i za odgovarajući materijal (u engleskom za to postoji posebna reč,
„wood“), s kog razloga smatramo da je naziv „stablo“ adekvatniji, te ćemo ga, kao
takvog, usvojiti.
Stablo kao struktura podataka naslanja se na odgovarajući digraf sa nazivom
orijentisano stablo. Razmotrićemo prvo osobine digrafa tipa stabla.

6.1. ORIJENTISANO STABLO


Pre nego što damo definiciju orijentisanog stabla, moramo uvesti još neke
pojmove vezane za teoriju grafova. Prvo, ulazni (izlazni) stepen čvora je broj grana
koje ulaze u čvor (izlaze iz njega). Na slici 6.1 prikazan je jedan digraf.

a
b
e
c d
f

Slika 6.1.

Čvor a ima ulazni stepen 0 i izlazni stepen 2. Čvor b ima ulazni stepen 1 i izlazni
stepen 2. Niz čvorova i grana x1v1x2v2x3v3...xnvnxn+1 takav da grana vi predstavlja ili
par (xi,xi+1) ili par (xi+1,xi) nosi naziv lanac dužine n. U digrafu sa slike lanci su
abefgd (dužina:4), abef(dužina:3), bdgf (dužina:3) ac (dužina:1) ili ca (dužina:1).
Očigledno, dužina lanca jednaka je broju grana u lancu i za 1 manja od broja čvorova.
Put dužine n je skup čvorova i grana x1v1x2v2x3v3...xnvnxn+1 u kojem je vi=(xi,xi+1).

182
Dušan T. Malbaški - Algoritmi i strukture podataka

Zapaža se da se put može shvatiti kao lanac u kojem se poštuje orijentacija svake
grane. Među lancima koje smo naveli osobine puta imaju abef i ac. Digraf je slabo
povezan ako je svaki par čvorova povezan bar jednim lancem. Digraf sa slike jeste
slabo povezan.
Na osnovu navedenih definicija možemo oformiti i definiciju orijentisanog
stabla (u daljem tekstu izostavljaćemo reč „orijentisano“). Dakle, stablo je digraf za
koji važi:
1. postoji tačno jedan čvor sa ulaznim stepenom 0 (koji se zove koren)
2. svi ostali čvorovi imaju ulazni stepen 1
3. digraf je slabo povezan.

b c

d e f g

h
i j k l

Slika 6.2.

Na slici 6.2 prikazano je jedno stablo čiji koren je čvor a. S obzirom na to da se ovde
bavimo isključivo konačnim digrafovima, u stablu moraju postojati i neki čvorovi sa
izlaznim stepenom 0. Čvor sa izlaznim stepenom 0 nosi naziv list. Čvorovi e, g, h, i, j,
k i l su listovi stabla sa slike 6.2. U uvodnom delu pominjali smo termine prethodnik i
sledbenik za čvorove koji su povezani granom od prethodnika do sledbenika. Za
stabla je uobičajeno da se umesto ovog para termina koriste respektivno nadređeni i
podređeni čvor. Na primer, čvor a je nadređen čvorovima b i c, koji su mu podređeni.
Uočimo još jednu osobinu stabla koja je od izuzetne važnosti za odgovarajuće
strukture podataka: iz korena vodi put u svaki od preostalih čvorova stabla. Broj
čvorova na najdužem putu u stablu nosi naziv visina stabla. Najduži putevi (koji,
uzgred, uvek počinju u korenu, a završavaju se u listu) u stablu sa slike su putevi
abdh, acfi, acfj, acfk i acfl, što znači da je visina ovog stabla jednaka 4. Pod redom
stabla podrazumeva se najveći izlazni stepen čvora u njemu (u našem stablu to je čvor
f, tako da je ono reda 4).

183
Dušan T. Malbaški - Algoritmi i strukture podataka

Na slici 6.2 primećuje se još jedna opšta, formalno dokaziva, osobina stabla:
svaki čvor stabla zajedno sa svojim podređenim, njihovim podređenim itd. do nivoa
lista čine strukturu koja je opet stablo. Neki čvor x u stablu sa svim svojim
podređenim čvorovima, njihovim podređenim itd. nosi naziv podstablo generisano
čvorom x.
Hijerarhijski nivo čvora u stablu jeste dužina puta od korena do tog čvora.
Hijerarhijski nivo korena je uvek 0. Hijerarhijski nivo čvora b sa slike je 1, čvora e je
2, a čvorova npr. i i j je 3.
Digraf tipa stabla ima još čitav niz različitih osobina, no nisu sve u vezi sa
stablom kao strukturom podataka. Sledeće dve osobine su za stablo kao strukturu
podataka značajne. Prvo, kompletno stablo reda n je stablo u kojem svi čvorovi osim
listova imaju izlazni stepen n. Drugo, puno stablo je stablo u kojem su putevi od
korena do proizvoljnog lista iste dužine. Zapazimo da su ove dve osobine, kako se to
kaže, ortogonalne što znači da su međusobno nezavisne. Na slici 6.3 levo prikazano je
kompletno stablo reda 3 koje nije puno; desno se nalazi puno stablo koje nije
kompletno.

Slika 6.3.

Sa stanovišta brzine pristupa, kod stabala kao struktura podataka poželjno je da


odgovarajući digraf predstavlja puno i kompletno stablo. S druge strane, za zadati broj
čvorova k i red stabla n, nije uvek moguće formirati puno i kompletno stablo. Ono što,
međutim, jeste moguće to je formiranje tzv. perfektno balansiranog stabla.

Slika 6.4.

184
Dušan T. Malbaški - Algoritmi i strukture podataka

Perfektno balansirano stablo je, po definiciji, stablo u kojem se broj čvorova u


podstablima na istom hijerarhijskom nivou razlikuju najviše za 1. Na slici 6.4
prikazano je jedno takvo stablo.

6.2. STABLO KAO STRUKTURA PODATAKA


Struktura podataka tipa stabla nije jedinstvena struktura, jer ima dva vida koji,
osim što je pripadajući digraf stablo, nemaju mnogo sličnosti. Stoga, ni sama
definicija stabla kao strukture podataka ne može biti naročito konkretna46: pod
stablom se podrazumeva struktura podataka
T = (S(T),r(T))
za koju važi:
1. pripadajući digraf je (orijentisano) stablo
2. dozvoljen je pristup svakom elementu
3. dozvoljene su operacije uklanjanja i dodavanja, pod uslovom da ne narušavaju
definicione osobine konkretne vrste stabla.
Odmah napominjemo da, u opštem slučaju, pristup prema poziciji nije moguć prosto
zato što se, osim u specijalnim slučajevima, u stablu ne može definisati pozicija
elementa.
U digrafu tipa stabla podređeni elementi čine skup, tj. ne postoji uređenje u
smislu prvi podređeni, drugi podređeni itd. S druge strane, stablo kao struktura
podataka ne bi uopšte moglo da funkcioniše bez takvog uređenja, jer bi bilo
nemoguće formirati algoritam koji bi u sebi sadržao operaciju prelaska na neki od
podređenih elemenata datog elementa: mogli bi se realizovati samo trivijalni algoritmi
pristupa korenu ili provere da li je stablo prazno. Postoje dva pristupa rešavanju ovog
problema: uređenje u skupu podređenih elemenata bilo kojeg elementa stabla može se
zadati ili (već) na logičkom nivou ili (tek) na nivou fizičke realizacije. U zavisnosti od
odabranog pristupa, razlikujemo dve vrste stabala:
 n-arno stablo i
 uopšteno (generalisano) stablo.
Kod n-arnog stabla uređenje u skup elemenata podređenih datom uvodi se na
logičkom nivou i to tako što se mestu svakog podređenog pridružuje redni broj. Dakle,
za svaki element n-arnog stabla definisano je mesto za prvi podređeni, drugi

46
u sledećim odeljcima biće data i alternativna definicija stabla

185
Dušan T. Malbaški - Algoritmi i strukture podataka

podređeni itd. U datom stanju, mesta za podređene ne moraju sva biti zauzeta: neki
element može imati prvog podređenog, trećeg podređenog itd., dok je pozicija broj 2
nezauzeta. Na slici 6.5 prikazano je jedno takvo stablo. Za svaki element podređeni
broj 1 (ili levi podređeni) odgovara odnosu "majka", a podređeni broj 2 (desni
podređeni) odnosu "otac".

Apolon

Leta Zevs

Febe Kejo Rea Kron

Slika 6.5.

Sledeći primer genealoškog stabla ilustruje slučaj kada neki od podređenih ne postoji
(prema grčkoj mitologiji, Atena je rođena iz Zevsovog bedra, te nema majku):

Atena

Zevs

Rea Kron

Slika 6.6.

Kod generalisanog stabla, podređeni na logičkom nivou čine skup, što znači da je
njihov međusobni raspored proizvoljan. Na slici 6.7 prikazano je stablo u kojem
nadređeni element i njegove podređene vezuje relacija "biti subdirektorijum".

My Files

Reports Music Projects Pictures

Classical Rock Pascal CPP Java

Slika 6.7.

Promena redosleda podređenih nema uticaja na stanje generalisanog stabla. S obzirom


na to da se nad takvom (logičkom) strukturom ne može formulisati algoritam prelaska

186
Dušan T. Malbaški - Algoritmi i strukture podataka

sa nadređemog na podređene niti se može algoritamski pregledati skup podređenih,


uređenje se mora uvesti, s tim što je ono definisano na nivou fizičke realizacije. Time
je problem rešen, pošto svi algoritmi funkcionišu nad fizičkom strukturom.

6.3. N-ARNO STABLO


Vrsta stabla koju nazivamo „n-arnim stablom“ karakteriše se dvema posebnim
osobinama:
 u skupu podređenih svakog elementa postoji eksplicitno linearno uređenje i to
na nivou logičke strukture
 stablo je reda n, što znači da element ne može imati više od n podređenih, pri
čemu se projektovani red n ne može menjati.
Svaki element n-arnog stabla osim korena karakteriše se pozicijom u skupu elemenata
koji imaju istog nadređenog, pri čemu neke pozicije mogu biti i nezauzete (ali su
definisane). Na slici 6.8 prikazana su tri različita stabla. Kod prvog s leva redosled
elemenata podređenih elementu X je A pa B; kod stabla u sredini redosled je obrnut;
stablo na desnoj strani je reda 3, a prva pozicija u skupu podređenih je nezauzeta.
Inače, uobičajeno je da se na digrafu nezauzete pozicije ne prikazuju.

X X X

A B B A A B

Slika 6.8.

N-arno stablo može se definisati i rekurzivno: to je struktura podataka sa sledećim


karakteristikama:
 ili je prazno
 ili se sastoji od datog elementa i linearno uređenog skupa njegovih podređenih
elemenata koji su, svaki ponaosob, ponovo n-arna stabla.
Ova definicija je u skladu sa opštom definicijom strukture podataka datom u odeljku
1.1. Naime, stablo reda n može se definisati kao struktura podataka (,) ili (S,r) gde
je S=S0,...,Sn, S0(,) i r=(S0,S1),(S0,S2),...,(S0,Sn) pri čemu su S1,...,Sn takođe
n-arna stabla i skup S1,...,Sn je linearno uređen.

187
Dušan T. Malbaški - Algoritmi i strukture podataka

Osnovna vrsta pristupa kod n-arnog stabla jeste pristup prema informacionom
sadržaju. Neka je k0 argument traženja (tj. ključ traženog elementa). Neka je k(e) ključ
elementa e. Traženje uvek počinje od korena. Ako je ključ korena jednak argumentu
traženja postupak je završen. Ako nije, na bazi nekog kriterijuma vezanog za k0 i
koren bira se podređeni element i za njega se postupak ponavlja. Traženje se uspešno
završava kada se pristupi elementu sa ključem k0.Traženje se neuspešno završava
kada se stigne do lista. Neka je sub(k0,e) funkcija koja odražava kriterijum izbora
podređenog u postupku traženja i koja za rezultat vraća odabrani element podređen
elementu e, ili pak specijalnu vrednost  koja dogovorno označava da takvog elementa
nema. Na slici 6.9 prikazan je opšti postupak traženja u n-arnom stablu.

ekoren

da
e= element nije pronađen

ne

da
k(e)=k0 element je pronađen

ne

esub(k0,e)

Slika 6.9.

Algoritam pristupa - osim u ekstremnim slučajevima - izuzetno je brz čak i za stabla


reda 2 (kod kojih je najsporiji). Da bismo se uverili u to, izvršićemo pojednostavljenu
analizu vremenske kompleksnosti. Neka n-arno stablo ima N elemenata. Vremensku
kompleksnost analiziraćemo preko najgoreg i najboljeg slučaja. Najgori slučaj jeste
slučaj stabla koje je degenerisano tako da svaki element ima ne više od jednog
podređenog. U tom slučaju, algoritam pristupa poklapa se sa algoritmom pristupa kod
jednostruko spregnute liste, te odmah sledi da je
Tn(N) ≈ N/2
tj.
Tn(N) = O[N].
Najbolji slučaj jeste n-arno stablo koje je puno i kompletno, ili barem perfektno
balansirano. Pokazatelj koji ćemo koristiti je maksimalna vremenska kompleksnost,
jer bi analiza prosečnog slučaja bila komplikovana, a da pritom ne bi dala bolji uvid u

188
Dušan T. Malbaški - Algoritmi i strukture podataka

ponašanje algoritma. Očigledno, broj poređenja pri traženju ne može biti veći od
visine stabla h. Kako je
h
N= n i -1
= (nh-1)/(n-1)
i 1

sledi
h = logn[1+N(n-1)]
te, kada je N>>n>1,
max(Tn(N)) = O[lognN]
što znači da, za slučaj perfektno balansiranog n-arnog stabla, algoritam traženja spada
u najbrže moguće.
Kada su u pitanju operacije uklanjanja odnosno dodavanja elementa, uopšteni
algoritmi ne postoje, jer se razlikuju u zavisnosti od konkretne vrste n-arnog stabla.
Ono što je, međutim, zajedničko za sve njih, jeste činjenica da se u svakom trenutku
element može i dodati i ukloniti, ali tako da definiciona ograničenja - red n i uređenje
u skupu podređenih - ne budu narušena.

6.3.1. Fizička realizacija n-arnog stabla


Fizička struktura n-arnog stabla je, u opštem slučaju, obavezno spregnuta, a u
izuzetnom slučaju malog, binarnog (reda 2) i nepromenljivog stabla može biti i
sekvencijalna.
Spregnuta fizička realizacija stabla bazirana je na činjenici da je maksimalan
broj podređenih u n-arnom stablu konstantan, jednak n i ne može se menjati. Shodno
tome, format svakog čvora je isti, a sadržaj čine
 podaci (informacioni sadržaj) i
 statički niz od tačno n pokazivača na podređene elemente; u slučaju da je n
malo (tipično: n=2) umesto niza od 2 pokazivača koriste se dva polja sa
pokazivačima.
Format čvora n-arnog stabla prikazan je na slici 6.10.

item p[0] p[1] ... p[n-1]

Slika 6.10.

189
Dušan T. Malbaški - Algoritmi i strukture podataka

Polje item čini informacioni sadržaj čvora, dok su pokazivači p[0],...,p[n-1]


predviđeni za povezivanje sa podređenim čvorovima. Na taj način, definicija čvora
dobija sledeći oblik:
typedef struct node {
T item;
struct node* next[ORDER]; //ORDER mora biti konstanta!
} Node;
Deskriptor može sadržati razne podatke, ali se u njemu mora naći pokazivač na koren
stabla:
typedef struct {
Node *root;
} NaryTree;
gde je root pokazivač na koren stabla.
Na slikama 6.11 i 6.12 prikazane su respektivno logička i fizička struktura
jednog ternarnog stabla (stabla reda 3).

B C D

E F G H

Slika 6.11.

deskriptor

B * C * * * D *

E * * * F * * * G * * * H * * *

Slika 6.12.

Na slici 6.12 zvezdicama su označeni NULL pokazivači.

190
Dušan T. Malbaški - Algoritmi i strukture podataka

U izuzetnom slučaju, n-arno stablo može se realizovati i sekvencijalno, ali uz


uslove:
 da je reda 2
 da ima malo čvorova
 da je nepromenljivo.
Algoritam smeštanja pristupa jednostavan je. Odvaja se sekvencijalna zona memorije
sa lokacijama koje su na relativnim adresama 1,2,3...
1. koren stabla smešta se na lokaciju sa relativnom adresom 1.
2. ako se nadređeni element nalazi na relativnoj adresi k, tada se prvi podređeni
smešta na relativnu adresu 2k, a drugi na relativnu adresu 2k+1.
Primer sekvencijalno realizovanog stabla prikazan je na slici 6.13.

B C

D E

F G

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
A B C D E F G

Slika 6.13.

Sekvencijalna realizacija karakteriše se jednostavnim i brzim algoritmima za


manipulaciju, ali po cenu velikog utroška memorije. Naime, bez obzira na broj
elemenata, mora se odvojiti prostor za puno i kompletno stablo visine jednake
najdužem putu. Ako je visina stabla h, neophodno je unapred zauzeti 2h-1 lokacija što
će, ako je logička struktura slabo balansirana, rezultovati velikom, slabo popunjenom
memorijskom zonom.

6.3.2. Binarno stablo


Najpoznatiji oblik n-arnog stabla, korišćen i kao logička i kao fizička
struktura, jeste n-arno stablo reda 2 koje zovemo binarno stablo. Podređeni elementi
tradicionalno se zovu levi podređeni i desni podređeni. U fizičkoj strukturi uobičajeno
je da se, umesto niza od dva pokazivača na podređene, koriste dva zasebna
pokazivača koji se označavaju sa left i right.

191
Dušan T. Malbaški - Algoritmi i strukture podataka

Traženje u binarnom stablu obavlja se opštim algoritmom sa slike 6.9. Zbog


binarne strukture ovog stabla, analiza postupka traženja nešto je jednostavnija, tako da
smo u prilici da odredimo vremensku složenost algoritma za opšti (prosečan) slučaj.
Analiza je prikazana prema 3. Da bismo došli do srednjeg broja upoređenja
posmatraćemo opštu strukturu binarnog stabla sa N>0 elemenata, datu na slici 6.14.

i n-i-1

Slika 6.14.

Posmatra se koren, njegovo levo i desno podstablo. Pretpostavićemo da u levom


podstablu ima i (i0) elemenata te, sledstveno, da u desnom podstablu ima N-i-1
element. Pretpostavićemo još i da je verovatnoća (uspešnog) traženja svakog elementa
jednaka 1/N. Razlikujemo tri slučaja:
1. traži se koren; tada je broj pristupa 1 (uz verovatnoću 1/N)
2. traženi element je u levom podstablu uz verovatnoću i/N; tada je prosečan broj
upoređenja 1+U(i), gde je U(i) prosečan broj upoređenja u levom podstablu
3. traženi element je u desnom podstablu sa verovatnoćom (N-i-1)/N gde je
prosečan broj upoređenja U(N-i-1).
Pritom, treba uočiti da se prosečni brojevi upoređenja u levom i desnom podstablu
dobijaju istim postupkom kao i za celo stablo. Prema tome, za slučaj da u levom
podstablu ima i elemenata, a u desnom N-i-1 element, prosečan broj upoređenja iznosi
U(N)(i) = 1/N + i/N(1+U(i)) + (N-i-1)/N(1+U(N-i-1)
Kada ovu formulu uprosečimo za i=0,1,...,N-1 (jer su to sve moguće konfiguracije
levog i desnog podstabla) dobijamo
N -1
(i)
U(N) =  U(N)
i 0
/N

tj.

192
Dušan T. Malbaški - Algoritmi i strukture podataka

N 1
1
U(N) =
N2
 [ N  iU(i)  (N - i - 1)U(N - i - 1)]
i 0

iz čega, posle jednostavnih transformacija, dobijamo


N 1
2
U(N) = 1 +
N2
 jU(j)
j 1

Ako razvijemo gornju formulu za U(N) i U(N-1) videćemo da važi


1
U(N) = ((N2-1)U(N-1)+2N-1)
N2
Ako sa Hn označimo sumu
Hm = 1 + 1/2 + 1/3 + 1/4 +...+1/m
gornja formula razvija se u
N 1
U(N) = 2 HN - 3
N
Izraz HN nosi naziv harmonijski red, koji je divergentan, ali za koji važi
HN < 1+lnN = 1+log2N/ln2
Konačno, pošto za vremensku složenost T(N) važi približno
T(N) = T(1)U(N) gde je T(1)=const
možemo prihvatiti da je za velike vrednosti N, T(N)~log2N, tj.
T(N) = O[log2N]
Kako vidimo, vremenska složenost operacije traženja u binarnom stablu je u proseku
reda O[log2N]. Dakle, u prosečnom slučaju kada stablo nije degenerisano u listu ali ni
balansirano, algoritam traženja je logaritamski, što znači najbrži mogući!
Jedna od najznačajnijih operacija nad binarnim stablom je redosledna obrada,
nazvana obilaskom binarnog stabla (eng. binary tree traversal). Operacija ni u kom
slučaju nije trivijalna, a razlog je nelinearnost stabla. Postoji više varijanti postupka
obilaska i sve su one po prirodi rekurzivne47. Rekurzija se bazira na sledeće tri stavke:
 obrada čvora
 obrada podstabla generisanog njegovim levim podređenim (tzv. „levog
podstabla“)
 obrada podstabla generisanog njegovim desnim podređenim („desnog
podstabla“).

47
tipičan primer rekurzivnog problema koji zahteva rekurzivno rešenje

193
Dušan T. Malbaški - Algoritmi i strukture podataka

Pomenute tri stavke mogu se odvijati proizvoljnim redosledom i svaki od tih


redosleda produkuje poseban postupak. Dakle, postoje 3!=6 postupaka obilaska koje
možemo podeliti u dve dualne grupe u zavisnosti od toga da li obrada levog podstabla
prethodi obradi desnog ili je obrnuto. Običaj je da se razmatraju tri metode kod kojih
obrada levog podstabla prethodi obradi desnog, pri čemu valja zapaziti da se tri
dualne metode iz njih izvode trivijalnom zamenom „levi“ sa „desni“. Tri standardne
metode za obilazak binarnog stabla su:
1. obilazak s leva u desno (inorder traversal) kod kojeg je redosled levo
podstablo-čvor (tj. nadređeni)-desno podstablo
2. obilazak s vrha ka dnu (preorder traversal) sa redosledom čvor-levo
podstablo-desno podstablo i
3. obilazak s dna ka vrhu (postorder traversal) sa redosledom levo podstablo-
desno podstablo-čvor
Sva tri algoritma (i preostala tri dualna) počinju od korena. Posmatrajmo binarno
stablo na slici 6.15. Neka je potrebno izvršiti obilazak s leva u desno (najpopularnija
od tri metode). Polazi se od korena (koji je, zahvaljujući realizaciji, jedini čvor
dostupan iz deskriptora). Prema opisu algoritma, pre nego što se obradi koren, tj. čvor
A, mora se obraditi njegovo levo podstablo, tako da se adresa čvora A privremeno
odlaže u pomoćnu strukturu podataka S i pristupa se levom podređenom B.

B C

D E F

G H I

Slika 6.15.

Međutim, i za čvor B važi isto: pre nego što se obradi, mora se obraditi njegovo levo
podstablo, te se i adresa čvora B odlaže u istu pomoćnu strukturu S. Pristupa se čvoru
D, pa se i on odlaže u pomoćnu strukturu S, jer ima levo podstablo. Tako se dolazi do
čvora G koji nema levo podstablo i taj čvor je prvi čvor koji se obrađuje. Pošto čvor G
nema desno podstablo, treba obraditi čvor koji je njemu nadređen, a to je čvor D čija

194
Dušan T. Malbaški - Algoritmi i strukture podataka

se adresa čita (i uklanja) iz pomoćne strukture S. Posle obrade čvora D pristupa se


njegovom desnom podstablu. Pošto čvor H nema levo podstablo na njega je red da se
obradi. Time je obrađeno čitavo levo podstablo čvora B sa slike 6.14, te i ovaj čvor
dolazi na red za obradu. Njegova adresa čita se iz pomoćne strukture S. Sada je čitavo
levo podstablo čvora A obrađeno, tako da se adresa ovog čvora čita (i uklanja) iz S,
čvor A se obrađuje i pristupa se njegovom desnom podstablu. Postupak se nastavlja
sve dok algoritam ne dospe do čvora koji
 nema desno podstablo pri čemu je
 pomoćna struktura S prazna.
Obilazak s leva u desno prikazan je na slici 6.15 isprekidanom linijom. Ako
proanaliziramo postupak obrade, lako ćemo uočiti da se adrese čvorova čitaju i
uklanjaju iz pomoćne strukture S redosledom koji je obrnut redosledu unošenja u S,
što nije išta drugo do LIFO, odakle sledi da je pomoćna struktura S stek! Redosled
obrade metodom obilaska s leva u desno je, dakle,
G, D, H, B, A, E, C, I, F.
Sličnom analizom zaključili bismo da je redosled obrade s vrha ka dnu
A, B, D, G, H, C, E, F, I
a s dna ka vrhu
G, H, D, B, E, I, F, C, A.
Zapazimo još jednu karakteristiku stabla: zbog nelinearnosti strukture, prilikom
obrade, čvorovima se pristupa više puta (u ovom slučaju dva puta).
Činjenica da je postupak rekurzivan i da se obavlja posredstvom steka jasno
ukazuje na ideju da se algoritmi obilaska realizuju u obliku rekurzivnih potprograma.
Realizovaćemo ove potprograme uz pretpostavku da je definicija binarnog stabla
oblika
typedef struct node {
T item;
struct node *left,*right;
} Node;
typedef struct {
Node *root;
} BinTree;

195
Dušan T. Malbaški - Algoritmi i strukture podataka

Funkcije za obilazak s leva u desno (_inorder), s vrha ka dnu (_preorder) i sa dna ka


vrhu (_postorder) izgledaju ovako:
void _inorder(Node *node,void (*visit)(Node* x)) {
if(node) {
_inorder(node->left,visit);
visit(node);
_inorder(node->right,visit);
}
}

void _preorder(Node *node,void (*visit)(Node* x)) {


if(node) {
visit(node);
_preorder(node->left,visit);
_preorder(node->right,visit);
}
}

void _postorder(Node *node,void (*visit)(Node* x)) {


if(node) {
_postorder(node->left,visit);
_postorder(node->right,visit);
visit(node);
}
}
U sva tri slučaja, parametar void (*visit)(Node* x) je funkcija-parametar visit
(uobičajen naziv) koja obavlja obradu čvora čija je adresa (pokazivač) data sa x. Da
bismo ostvarili uniformnost, ove tri funkcije ćemo spakovati u funkcije-omotače koje
za parametar imaju ne pokazivač na čvor nego, kao i do sada, pokazivač na strukturu
podataka:
void inorder(BinTree* bt,void (*visit)(Node* x)) {
_inorder(bt->root,visit);
}

196
Dušan T. Malbaški - Algoritmi i strukture podataka

void preorder(BinTree* bt,void (*visit)(Node* x)) {


_preorder(bt->root,visit);
}

void postorder(BinTree* bt,void (*visit)(Node* x)) {


_postorder(bt->root,visit);
}

Ako bi stablo izgledalo kao na slici 6.15, a tip T bio char, tj.
typedef char T;
uz funkciju za obradu čvora
void printItem(Node* x) {
printf("%c ",x->item);
}
koja prikazuje polje item na ekranu, tada bi poziv
inorder(&bt,printItem);
gde je promenljiva bt tipa BinTree, generisao izlaz oblika
GDHBAECIF
Obilazak binarnog stabla koristi se i prilikom njegovog pražnjenja (brisanja).
Pritom, najpogodnije je koristiti obilazak s dna ka vrhu jer element koji se upravo
uklanja, u trenutku uklanjanja nema podređene (ostale metode bi zahtevale
privremeno memorisanje jednog od podstabala). Funkcija clear za brisanje stabla ima
sledeći oblik:
//Preduslov: -
//Postuslov: stablo bt je prazno
void clearNode(Node* x) {
free(x);
}
void clear(BinTree* bt) {
_postorder(bt->root,clearNode);
bt->root=NULL;
}

197
Dušan T. Malbaški - Algoritmi i strukture podataka

Funkcija poziva parametar-funkciju clearNode koja uklanja čvor.


Napomenimo, usput, da se funkcije _preorder i _postorder mogu lako
prilagoditi n-arnom stablu reda većeg od 2 tako što se segment u kojem su rekurzivni
pozivi za node->left i node->right zamenjeni ciklusom u kojem se nalaze rekurzivni
pozivi sa svakim podređenim kao parametrom.
Postupak obilaska binarnog stabla može se ubrzati, ali ne izmenom algoritma
nego promenom fizičke realizacije stabla. Naime, rezerva vremena u funkcijama za
obilazak stabla leži u primeni steka prilikom rekurzivnog poziva, jer priprema steka i
uzimanje rezultata traje. Izbegavanjem rekurzivnih poziva (pa, prema tome i
korišćenja steka), postupak bi mogao da se ubrza. Tehnika fizičke realizacije kojom se
ostvaruje ovaj cilj nosi naziv tehnika prošivki (niti) i primenjuje se za ubrzanje
obilaska s leva u desno (inorder traversal)
Ako analiziramo obilazak s leva u desno (videti sliku 6.15), lako ćemo
ustanoviti da se stek aktivira samo prilikom prelaska sa nekog elementa na viši nivo, a
to se dešava kada element nema desno podstablo, tj. kada je njegov desni pokazivač
NULL. Da bi se stek eliminisao iz postupka obilaska s leva u desno potrebno je,
umesto NULL desnog pokazivača u odgovarajuće polje right upisati pokazivač na
sledeći element u postupku obilaska. Na taj način, umesto da se adresa sledećeg čita iz
steka, ona se čita iz samog elementa. Pokazivač na sledeći element u procesu obilaska
nosi naziv prošivka ili nit (engl. thread). Na slici 6.16 prikazana je fizička realizacija
binarnog stabla sa slike 6.15 snabdevenog prošivkama.

A 0

statusno polje
B 1 C 0

D 0 * E 1 F 0*

pokazivač prošivka
* G 1 * H 1 * I 1
Slika 6.16.

Da bi ova tehnika mogla da se primeni neophodno je rešiti jednu poteškoću. Naime,


prošivka je po tipu pokazivač i to iste vrste kao i "običan" pokazivač, tako da bi ostali
algoritmi (pristup, dodavanje, uklanjanje) postali neupotrebljivi. Na primer, pošto

198
Dušan T. Malbaški - Algoritmi i strukture podataka

desni pokazivač elementa H sa slike nema vrednost NULL ispalo bi da je element B


njegov desni podređeni i nijedan algoritam osim obilaska s leva u desno zbog toga ne
bi funkcionisao. U svrhu rešavanja ovog problema, svaki čvor u fizičkoj realizaciji
morao bi biti proširen binarnim statusnim poljem vezanim za desni pokazivač gde bi
jedna vrednost tog polja indikovala da je desni pokazivač right običan pokazivač (deo
strukture stabla), a druga vrednost da se radi o prošivci. Algoritmi koji zahtevaju
pristup čvoru morali bi da imaju ugrađenu proveru statusnog polja kojom bi se
konstatovalo da li je u pitanju pokazivač ili prošivka. Druga mogućnost je da prošivku
realizujemo kao posebno polje čvora. Upravo opisana tehnika, inače, nosi naziv
tehnika jednostrukih prošivki.
Postoji još jedna tehnika prošivki bazirana na istoj ideji, nazvana tehnikom
dvostrukih prošivki. Ova tehnika predstavlja trivijalno proširenje prethodne, gde se po
istom principu tretira i levi pokazivač: ako je on jednak NULL, biće zamenjen
prošivkom na prethodni u procesu obilaska omogućujući ubrzanje obilaska u oba
smera. Za slučaj obilaska s leva u desno, tehnikom dvostrukih prošivki ubrzava se i
postupak obilaska s desna u levo. Pri tom, a iz istih razloga kao i kod tehnike
jednostrukih prošivki, čvor mora da sadrži dodatno statusno polje vezano za levi
pokazivač. Tehnika dvostrukih prošivki prikazana je na slici 6.17.

0 A 0

0 B 1 0 C 0

0 D 0 1 E 1 0 F 0*

leva prošivka
* G 1 1 H 1 1 I 1 desna prošivka

Slika 6.17.

Uopšte uzev, tehnika prošivki, ma kako privlačno delovala, nije univerzalna,


jer ozbiljno posložnjava i usporava sve algoritme osim obilaska s leva u desno odn. s
desna u levo. Prvo, algoritam pristupa mora biti modifikovan tako da može da
prepozna prošivku i drugo, algoritmi dodavanja i uklanjanja, bez obzira na konkretnu
izvedbu, takođe se moraju prilagoditi tako da se, pored podešavanja pokazivača,
ažuriraju i prošivke. Shodno tome, tehnika prošivki ima se shvatiti kao specijalna

199
Dušan T. Malbaški - Algoritmi i strukture podataka

tehnika koja će biti primenjena samo tada kada se očekuje intenzivna primena
postupka obilaska s leva u desno.
Među postupcima obilaska binarnog stabla postoji još jedan, manje pominjan
postupak, a to je obilazak binarnog stabla po nivoima. Odvija se tako što se prvo
obradi koren (nivo 0), zatim njegovi neposredni podređeni (nivo 1), pa njihovi
neposredno podređeni (nivo 2) itd. Postupak podseća na rekurziju, samo što posrednik
pri ovoj vrsti „rekurzije“ nije stek nego red. Osnova algoritma je sledeća:
1. upisati u red koren stabla
2. ponavljati sledeće: pročitati element na početku reda; ukloniti ga; ako je
pročitani element neprazan (nije NULL) obraditi ga i u red ubaciti levog i
desnog podređenog
3. postupak završiti kada se red isprazni.
Realizacija postupka obilaska po nivoima ima sledeći oblik:
void traverseByLevel(BinTree *bst,void (*visit)(Node* x)) {
Queue que; Node *node;
createQueue(&que);
node=bst->root; putItemQueue(&que,node);
while(!isEmptyQueue(&que)) {
node=frontQueue(&que); removeItemQueue(&que);
if(node) {
visit(node);
putItemQueue(&que,node->left);
putItemQueue(&que,node->right);
}
}
}
Funkcija traverseByLevel koristi spregnuto realizovani red que (videti odeljak 4.2.2).
Da bi se izbeglo dupliranje identifikatora, nazivima funkcija koje koristi red dodat je
sufiks Queue tako da su promenjeni u createQueue, isEmptyQueue, frontQueue,
removeItemQueue i putItemQueue. Parametar visit je, kao i u prethodnim primerima,
funkcija koja vrši obradu elementa. Ilustrovaćemo primenu obilaska stabla po nivoima
obradom koja predstavlja prikaz sadržaja stabla na ekranu. Svaki element stabla

200
Dušan T. Malbaški - Algoritmi i strukture podataka

praćen sadržajem njegovih neposredno podređenih prikazuje se u posebnoj liniji.


Funkcija showNodeAndChildren ima sledeći oblik:
void showNodeAndChildren(Node* x){
printf("\n%1c",x->item);
if(x->left) printf(" %1c",x->left->item); else printf(" *");
if(x->right) printf(" %1c",x->right->item); else printf(" *");
}
Kada se funkcija traverseByLevel primeni na stablo bt sa slike 6.15 naredbom
traverseByLevel(&bt,showNodeAndChildren);
dobija se izlaz
A B C
B D *
C E F
D G H
E * *
F I *
G * *
H * *
I * *
Redosled obilaska dat je u prvoj koloni, a svaki red sadrži čvor i njegova dva
podređena (simbol * označava da odgovarajućeg podređenog nema). Zapazimo da se
funkcija za obilazak po nivoima lako transformiše u oblik koji je primenljiv i stabla
reda većeg od 2. Sve što treba uraditi je da se segment
putItemQueue(&que,node->left);
putItemQueue(&que,node->right);
zameni ciklusom u kojem se svih n podređenih čvora node smešta u red.

6.3.3. Binarno stablo pristupa


U dosadašnjim razmatranjima nismo bili u mogućnosti da opišemo konkretne
algoritme pristupa, kao ni uklanjanja i dodavanja, s obzirom na to da zavise od
konkretne vrste stabla.
Binarno stablo pristupa (engl. Binary Search Tree, skraćeno BST) je sasvim
konkretna vrsta binarnih stabala namenjena, kao što ime kaže, realizaciji brzog

201
Dušan T. Malbaški - Algoritmi i strukture podataka

algoritma pristupa na bazi ključa (dakle, traženja). Binarno stablo pristupa odlikuje se
sledećim definicionim osobinama:
 pre svega, stablo je binarno
 elementi su snabdeveni ključevima
 za svaki element stabla važi: ključevi elemenata u njegovom levom podstablu
manji su od njegovog ključa, dok su ključevi u desnom podstablu veći.
Na slici 6.18 prikazano je jedno binarno stablo pristupa. Smatraćemo da su ključevi
celobrojni, što nema uticaja na opštost razmatranja, jer sve što je potrebno da bi se
elementi smestili u binarno stablo pristupa jeste da ključevi mogu da se urede u
sekvencu, po rastućim vrednostima. Takođe, na slici su prikazani samo ključevi, a
informacioni sadržaj se podrazumeva.

50

30 80

20 40 60 90

10 35 55 67 95

62 75

70

Slika 6.18.

Posmatramo li bilo koji element stabla, uočićemo da su svi ključevi u njegovom


levom podstablu manji, a ključevi u desnom podstablu veći od njegovog ključa. Ovu
definicionu osobinu binarnog stabla pristupa zvaćemo BST invarijanta.
Odmah se može uočiti da obilazak binarnog stabla pristupa s leva u desno
generiše sekvencu elemenata sortiranu po rastućoj vtrednosti ključa, dok obilazak s
desna u levo generiše to isto, po opadajućoj vrednosti ključa.
Algoritam traženja u binarnom stablu pristupa oslanja se na njegovu
definicionu osobinu vezanu za ključeve u levom i desnom podstablu. Traženje se
obavlja konkretizacijom algoritma prikazanog na slici 6.9, gde je kriterijum izbora
levog ili desnog podređenog na pristupnom putu određen odnosom između argumenta
traženja i ključa trenutno proveravanog elementa. Naime, ako je argument traženja
veći od ključa posmatranog elementa, tada se traženi element (ako je uopšte u stablu)

202
Dušan T. Malbaški - Algoritmi i strukture podataka

može naći samo u desnom podstablu, a ako je manji samo u njegovom levom
podstablu.

ekoren

da
k(e)= element nije pronađen

ne
da
k(e)=k0 element je pronađen

ne
< >
k0:k(e)
eleft(e) eright(e)

Slika 6.19.

Korišćene oznake iste su kao na slici 6.9, s tim da se oznaka left(e) odnosi na levog
podređenog za element e, a oznaka right(e) isto za desnog. Oznaka  se i dalje odnosi
na „prazan“ element koji znači da prethodna primena left ili right nije dala rezultat. U
(spregnutoj) fizičkoj realizaciji, to će uvek značiti da je pokazivač dobijen primenom
left ili right jednak NULL. Takođe oznake < i > treba shvatiti uopšteno, kao “ispred”
odn. “iza” u smislu poretka ključeva. Dakle, traženje počinje od korena. Ako je
argument traženja k0 jednak ključu tekućeg elementa, traženje je uspešno završeno.
Ako je argument traženja manji prelazi se u levo podstablo, a ako je veći u desno i
postupak se ponavlja. Ako se stigne do lista traženje je završeno neuspehom.
Neka je binarno stablo pristupa definisano na sledeći način:
typedef struct node {
K key;
T item;
struct node *left,*right;
} Node;
typedef struct {
Node *root;
} BinSearchTree;
Funkcija traženja, sa imenom getItem zamišljena je tako da vrati pokazivač na polje
item traženog čvora (čiji je ključ parametar key) ili vrednost NULL ako čvor nije
nađen. Lokalna promenljiva curr služi za prolazak kroz stablo.

203
Dušan T. Malbaški - Algoritmi i strukture podataka

T* getItem(const BinSearchTree* bst, K key) {


Node *curr=bst->root;
while(curr)
if(key<curr->key) curr=curr->left; //prelazak u levo podstablo
else if(key>curr->key) curr=curr->right; //prelazak u desno podstablo
else return &(curr->item); //nadjen! vraca se adresa polja item
return NULL;
}
Treba napomenuti još jednu, praktičnu, stvar: u funkciji za upoređivanje ključeva
korišćeni su obični relacioni operatori < i >. Može se dogoditi da su ključevi tipa koji
ne dozvoljava direktnu primenu ovih operatora (recimo stringovi). Ovo, međutim, ne
predstavlja nikakav problem jer se uvek mogu napraviti funkcije za poređenje
ključeva koje vraćaju vrednosti 0 odn. 1 prema odnosu ključeva. Recimo, može se
napraviti funkcija less za poređenje ključeva sa prototipom
int less(K key1,K key2);
koja vraća 1 ako se key1 smatra manjim od key2, a 0 u suprotnom. U tom slučaju,
opštiji oblik funkcije getItem bio bi
T* getItem(const BinSearchTree* bst, K key) {
Node *curr=bst->root;
while(curr)
if(less(key,curr->key)) curr=curr->left;
else if(less(curr->key,key)) curr=curr->right;
else return &(curr->item);
return NULL;
}
Na slici 6.20 grafički je prikazano nekoliko slučajeva traženja. Slika je
samoobjašnjavajuća. Po pitanju vremenske kompleksnosti, ovaj algoritam traženja je,
u stvari, tipičan algoritam traženja u binarnom stablu, te svi zaključci navedeni ranije
važe i ovde. Dakle, u najgorem (inače vrlo retkom) slučaju, kada se stablo sa N
elemenata degeneriše u linearnu strukturu, vremenska složenost je ON, dok je u
najboljem slučaju balansiranog stabla najviše Olog2N. U prosečnom slučaju
vremenska kompleksnost je takođe Olog2N.

204
Dušan T. Malbaški - Algoritmi i strukture podataka

50 100

30 40 55 80

20 40 60 90

10 35 55 67 95

62 75

70

Slika 6.20.

Algoritam za dodavanje elementa direktno se izvodi iz algoritma traženja. Da


bi se održala BST invarijanta, dodavanje se izvodi na mestu lista, tj. novododati
element je uvek list. Dodavanju prethodi neuspešno traženje, a novi element upisuje
se na mestu gde je neuspešno traženje završeno. Postupak je ilustrovan na slici 6.21.

50

30 80

20 40 60 90

10 35 47 55 67 95

15 62 75

dodati elementi 70

Slika 6.21.

Skrećemo pažnju da dodavanje „na mestu lista“ ne znači da se novododati element


upisuje ispod lista, nega da novododati element postaje list, što se vidi iz primera
elementa sa ključem 47 na slici. Funkcija za dodavanje, putItem, izgleda ovako:
//Preduslov: -
//Postuslov: (key,item) dodat u stablo; ako key vec postoji promenjen sadrzaj item
//Rezultat: 1 u slucaju uspeha, 0 ako vec postoji cvor sa kljucem key
int putItem(BinSearchTree* bst, K key, T item) {

205
Dušan T. Malbaški - Algoritmi i strukture podataka

Node *curr,**insPoint;
curr=bst->root; insPoint=&(bst->root);
while(curr) {
if(key<curr->key) {insPoint=&(curr->left); curr=curr->left;}
else if(key>curr->key) {insPoint=&(curr->right); curr=curr->right;}
else {curr->item=item; return 0;} //kljuc dupliran; promeniti sadrzaj
}
*insPoint=malloc(sizeof(Node));
(*insPoint)->key=key; (*insPoint)->item=item;
(*insPoint)->left=(*insPoint)->right=NULL;
return 1;
}
Pokazivač curr koristi se za prolazak kroz stablo. Uočimo promenljivu insPoint koja
je pokazivač na tip Node*. Njen zadatak je da obezbedi adresu lokacije u koju treba
upisati pokazivač na novi element. Na početku izvršavanja funkcije, insPoint sadrži
adresu polja root u deskriptoru. U nastavku, pre prelaza u levo podstablo čvora curr, u
insPoint se upisuje adresa polja curr->left, a ukoliko se prelazi u desno podstablo u
insPoint se upisuje adresa polja curr->right. Na taj način postiže se cilj, da u svakom
trenutku upis u *insPoint pokazivača na novi čvor označava povezivanje novododatog
elementa sa čvorom curr. U trenutku kada curr dobije vrednost NULL, promenljiva
insPoint sadrži adresu polja u koje treba upisati pokazivač na novi čvor. Konačno, ako
čvor sa ključem key već postoji u stablu, menja se samo sadržaj polja item i funkcija
se završava uz povratnu vrednost 0. Kôd iza naredbe while predstavlja formiranje
novog čvora i upis u stablo na mesto određeno ranije vrednošću *insPoint. U slučaju
da je stablo prošireno novim čvorom, funkcija vraća vrednost 1. Inače, funkciju smo
mogli realizovati i tako što ćemo, u slučaju dupliranja ključeva, zabraniti upis (i toga
ima u praksi). Sve što treba uraditi je promena treće linije naredbe if iz
else {curr->item=item; return 0;}
u
else return 0;
Uklanjanje čvora iz binarnog stabla pristupa nije trivijalan postupak, upravo
zbog očuvanja BST invarijante. Naime, kada se čvor za uklanjanje pronađe, postavlja
se pitanje šta dalje? U opštem slučaju „prevezivanje“ njegovih podređenih čvorova na

206
Dušan T. Malbaški - Algoritmi i strukture podataka

njegov nadređeni nije moguće ako uklonjeni čvor ima oba podređena, prosto zato što
u nadređenom nema mesta za tri pokazivača, slika 6.22. Za rešavanje ove poteškoće
postoje dva osnovna postupka
1. logičko brisanje i
2. Hibardova metoda.

nadređeni

uklanja se

Slika 6.22.

Logičko brisanje je vrlo jednostavna, ali i neefikasna procedura. Ideja se sastoji u


tome da se svaki čvor proširi binarnim statusnim poljem d koje označava da li je čvor
aktuelan ili nije. Čvor se logički briše tako što se statusno polje d postavi na vrednost
„ne važi“, pri čemu čvor i dalje ostaje u strukturi. Procedura traženja se modifikuje
tako da konsultuje statusno polje, te ako se ustanovi poklapanje ključa sa argumentom
traženja, a statusno polje ima vrednost „ne važi“, traženje se završava neuspehom.
Logičko brisanje ima bar dva ozbiljna nedostatka: prvo, stablo ima neopadajući broj
elemenata (jer nema fizičkog uklanjanja) i drugo, nevažeći čvorovi učestvuju u
traženju produžavajući pristupne puteve te, samim tim, i vreme izvršavanja. Pored
traženja, mora se modifikovati i procedura dodavanja: umesto da se postupak prekine
ako se ustanovi da u stablu već postoji element sa istim ključem, prethodno se
proverava da li taj element važi. Ako ne važi, u čvor se upisuje novi informacioni
sadržaj i statusno polje se menja na vrednost „važi“. Konačno, dobra je praksa
snabdeti stablo algoritmom za reorganizaciju (rekonfigurisanje), koji ponovo formira
stablo tako što u njega upisuje samo važeće elemente, a staru verziju briše.
Hibardova (Hibbard, 1962.) metoda znatno je pogodnija i smatra se
standardnim postupkom za uklanjanje. Odlikuje se time što u stablu, posle uklanjanja,
nema nevažećih elemenata, tj. njegov broj elemenata se zaista smanjuje za 1. Već smo

207
Dušan T. Malbaški - Algoritmi i strukture podataka

uočili da je glavni problem kod uklanjanja elementa nemogućnost prevezivanja


njegovih podređenih na nadređeni (slika 6.22). Umesto da se, kao kod logičkog
brisanja, taj problem zaobiđe, on se rešava direktno. Neka je d element koji treba
ukloniti iz stabla. Po pitanju onog što smo nazvali prevezivanje, mogu se detektovati
dva slučaja:
 element d ima manje od dva podređena (tj. jedan ili nijedan)
 element d ima oba podređena.
Odmah se zapaža da problem postoji samo u drugom slučaju. Naime, ako je broj
podređenih manji od dva, a imajući u vidu da se pokazivač nadređenog oslobađa,
prevezivanje jeste moguće (slika 6.23).

30 30

* 20 * 40 *
uklanja se
* 10 *
* 35 * uklanja se
Slika 6.23.

Problem, u stvari, nastaje kada element d ima oba podređena. U tom slučaju postupak
je sledeći:
 pronaći element e čiji je ključ prvi manji od ključa elementa d (alternativa:
prvi veći); taj element ne može imati desnog podređenog zbog invarijante BST
 prepisati ključ i informacioni sadržaj tog elementa u element d, a zatim
osloboditi element e, pri čemu je prevezivanje njegovog (eventualnog)
podređenog sada moguće.
Element čiji je ključ prvi manji (prvi veći) od ključa elementa d dobija se
jednostavnim postupkom:
1. preći u levo podstablo elementa d (alternativa: desno); ovde su elementi čiji su
ključevi manji (alternativa: veći) od ključa elementa d
2. pratiti desne pokazivače (alternativa: leve) sve dok se ne dođe do čvora koji
nema desnog (alternativa: levog) podređenog; to je traženi element.
Postupak za slučaj da element koji se uklanja (čvor sa ključem 80) ima oba podređena
prikazan je na slici 6.24.

208
Dušan T. Malbaški - Algoritmi i strukture podataka

50

80
75

60

* 55 * 67

* 62 * 75 *
briše se
* 70 *
Slika 6.24.

Funkcija za uklanjanje, pod imenom removeItem ima za parametre sâmo stablo i ključ
čvora koji se uklanja. Vraća vrednost 1 ako je uklanjanje uspelo, a vrednost 0 ako
nije, tj. ako čvora za uklanjanje nema u stablu.
Preduslov: -
Postuslov: ako postoji, cvor sa kljucem key je uklonjen
Rezultat: 1 ako je uklanjanje uspesno ili 0 ako trazenog cvora nema
int removeItem(BinSearchTree* bst, K key) {
Node *curr, *loc, **delPoint;
//trazenje cvora
curr=bst->root; delPoint=&(bst->root);
while(curr)
if(key<curr->key) {delPoint=&(curr->left); curr=curr->left;}
else if(key>curr->key) {delPoint=&(curr->right); curr=curr->right;}
else break; //nadjen
if(!curr) return 0; //nije nadjen
if(!curr->left) *delPoint=curr->right; //nema levog podredjenog
else if(!curr->right) *delPoint=curr->left; //nema desnog podredjenog
else { //element ima oba podredjena
loc=curr; //memorisati mesto elementa predvidjenog za brisanje
curr=curr->left; //preci u levo podstablo
while(curr->right) {delPoint=&(curr->right); curr=curr->right;} //trazenje prvog
manjeg

209
Dušan T. Malbaški - Algoritmi i strukture podataka

loc->key=curr->key; loc->item=curr->item; //prepisivanje


*delPoint=curr->left; //prevezivanje
}
free(curr);
return 1;
}
Na isti način kao kod funkcije putItem, pokazivač delPoint pokazuje na polje
nadređenog elementa u koje treba upisati pokazivač na podređeni u slučaju
prevezivanja. Ciklusom while traži se čvor za uklanjanje i istovremeno ažurira
odgovarajuće polje delPoint nadređenog elementa. Ako čvor za uklanjanje nije nađen,
funkcija se završava kôdom neuspeha 0. Sledeće dve linije regulišu situaciju u kojoj
čvor za uklanjanje ima jednog ili nijednog podređenog, tj. kada je prevezivanje
moguće. Poslednji segment funkcije odnosi se na slučaj kada čvor ima oba podređena.
Pokazivač loc usmerava se na čvor za uklanjanje da bi se omogućilo prepisivanje
sadržaja prvog manjeg u njega. Potom se postupkom opisanim napred traži čvor koji
ima prvi manji ključ, njegov sadržaj prepisuje se u čvor sa adresom loc i čvor sa
prvim manjim ključem se fizički uklanja (briše).
Operacije nad binarnim stablom pristupa mogu da se realizuju i rekurzivno,
pri čemu rekurzivne verzije deluju - ma šta to značilo - elegantno, jer su fizički sasvim
kratke. Rekurzivna varijanta funkcije putItem za dodavanje ima sledeći oblik:
//Preduslov: -
//Postuslov: dodat novi cvor; ako vec postoji promenjeno mu je polje item
Node* _putItem(Node* node,K key,T item) {
if(!node) {
node=malloc(sizeof(Node));
node->key=key; node->item=item;
node->left=node->right=NULL;}
else if(key<node->key) node->left =_putItem(node->left,key,item);
else if(key>node->key) node->right=_putItem(node->right,key,item);
else node->item=item;
return node;
}
void putItem(BinSearchTree* bst,K key,T item) {

210
Dušan T. Malbaški - Algoritmi i strukture podataka

bst->root=_putItem(bst->root,key,item);
}
Postupak je neznatno izmenjen u odnosu na iterativnu varijantu, utoliko što u slučaju
da element sa ključem key već postoji, neće biti prekida već će se bez izmene ključa
upisati novi informacioni sadržaj item. Razlog za ovu izmenu je taj što, u slučaju već
postojećeg ključa, nije moguće na jednostavan način prekinuti sa izvršavanjem uz
poruku o neuspehu. Ostavljamo čitaocu da analizira ponašanje ove funkcije.
Rekurzivna varijanta uklanjanja čvora takođe je zasnovana na istom opštem
pristupu (Hibardovom) kao i iterativna. Verzija koju navodimo takođe se razlikuje od
iterativne po tome što, za slučaj da čvora za brisanje nema u stablu, ne generiše
nikakav rezultat.
//rekurzivno uklanjanje
//Preduslov: -
//Postuslov: uklonjen cvor node, ako postoji
Node* _removeItem(Node* node, K key) {
if(!node) return NULL;
if(key<node->key) node->left=_removeItem(node->left,key);
else if(key>node->key) node->right=_removeItem(node->right,key);
else { //cvor nadjen
Node *temp;
if(!node->left) {temp=node->right; //nema levog podredjenog
free(node);
node=temp;}
else if(!node->right) {temp=node->left; //nema desnog podredjenog
free(node);
node=temp;}
else { //ima oba podredjena
temp=node->left; //pronaci najveci u levom podstablu
while(temp->right) temp=temp->right;
node->key=temp->key; node->item=temp->item;
node->left=_removeItem(node->left,temp->key);
}
}

211
Dušan T. Malbaški - Algoritmi i strukture podataka

return node;
}
void removeItem(BinSearchTree* bst, K key) {
bst->root=_removeItem(bst->root,key);
}
U prvom delu funkcije traži se čvor koji treba ukloniti, pri čemu se na stek smeštaju
adrese svih čvorova na putu od korena do čvora za brisanje. Kada se (tj. ako se) čvor
pronađe, sledi provera broja podređenih. Ako čvor za uklanjanje nema više od jednog
podređenog, taj podređeni se vraća kao rezultat da bi na višem nivou rekurzije bio
dodeljen nadređenom uklonjenog čvora, ostvarujući tako prevezivanje. U slučaju da
čvor za uklanjanje ima oba podređena, kao i u iterativnoj varijanti, traži se najveći u
levom podstablu, njegov ključ i sadržaj prepisuju se u čvor za brisanje, a na levo
podstablo primenjuje se rekurzivno uklanjanje tog, najvećeg.
Iako fizički kratke, rekurzivne varijante su, u stvari, nešto sporije od
iterativnih, a glavni razlog je utrošak vremena na pripremu steka prilikom rekurzivnih
poziva. S druge strane, rekurzivne verzije imaju važnu primenu za slučajeve kada
dodavanje i uklanjanje imaju dodatne radnje vezane, recimo, za balansiranje (videti
sledeće odeljke), a koje, sa svoje strane, zahtevaju da na raspolaganju bude čitav put
od korena do čvora za dodavanje-brisanje.

6.3.4. Balansiranje binarnog stabla


Na osnovu procene kompleksnosti algoritama nad binarnim stablom, lako
zaključujemo da je osnovni uticajni faktor - visina stabla h. Algoritam za određivanje
visine stabla (i to ma kojeg binarnog, pa i n-arnog, stabla) jednostavan je i ne razlikuje
se suštinski od klasičnog algoritma za određivanje najvećeg elementa nekog uređenog
skupa.
//odredjivanje visine stabla
unsigned _height(Node *node) {
static unsigned h=0,hmax;
if(!h) hmax=0; //nova rekurzija
if(node) {
if(++h>hmax) hmax=h;
_height(node->left);
_height(node->right);

212
Dušan T. Malbaški - Algoritmi i strukture podataka

h--;}
return hmax;
}
unsigned height(BinTree* bt) {
return _height(bt->root);
}

Ideja se sastoji u tome da se u svakom koraku prati rastojanje h čvora koji se obrađuje
od korena. Na početku, za vrednosti h i maksimalnog rastojanja hmax stavlja se 0.
Potom se obilazi stablo, određuje h za svaki čvor te, ako je tekuća vrednost h veća od
hmax, promenljiva hmax povećava se na tu, veću vrednost. Na kraju rekurzije,
najveće rastojanje od korena hmax jednako je visini stabla. Ovde lako prepoznajemo
klasični algoritam određivanja maksimuma, s tom specifičnošću što se prolaz kroz sve
čvorove stabla obavlja nekom od rekurzivnih procedura (u našem primeru to je
postorder). U realizaciji valja uočiti da svaki rekurzivni poziv podrazumeva prelazak
na podređenog, što znači da se tekuća vrednost h povećava za 1. Na mestu gde se u
rekurzivnoj funkciji _postorder nalazi obrada čvora, ustvari se prelazi na nadređeni
čvor, što ovde znači smanjenje h za 1 (naredba h--;). Naredbom
if(++h>hmax) hmax=h;
ažurira se vrednost najvećeg konstatovanog rastojanja u toku obilaska. Da bi se u toku
pojedinačnih koraka rekurzije očuvale vrednosti h i hmax, one su realizovane kao
statičke lokalne promenljive. Konačno, prva linija koda
if(!h) hmax=0;
obezbeđuje da se prilikom svakog otpočinjanja rekurzije, vrednost statičke
promenljive hmax vrati na nulu. Otpočinjanje nove rekurzije prepoznaje se po tome
što je tada tekuća visina h obavezno jednaka 0. Inače, i ovde važi napomena da
zamenom segmenta
_height(node->left);
_height(node->right);
ciklusom sa n pristupa svim podređenim, postupak određivanja visine može da se
proširi na stabla reda većeg od 2.
Uopšte uzev, što je visina stabla veća, algoritmi manipulacije stablom su
sporiji i obrnuto. Najgori slučaj je uvek binarno stablo degenerisano u linearnu

213
Dušan T. Malbaški - Algoritmi i strukture podataka

strukturu u kojoj svaki element ima samo jednog podređenog, kada su svi algoritmi
reda O[N]. Najbolji slučaj je kada, za zadati broj elemenata, stablo ima najmanju
visinu, a to je slučaj kada je stablo (perfektno) balansirano: tada su algoritmi reda
O[log2N]. Prema tome, ako hoćemo brze algoritme, stablo treba da bude balansirano,
ili bar blisko balansiranom.
Postoje dva opšta pristupa rešavanju problema balansiranja stabla: nezavisnim
procedurama (off-line balansiranje) i dinamičko (on-line) balansiranje.
Off-line balansiranje podrazumeva da se softver za upravljanje binarnim
stablom snabdeva posebnim potprogramom čiji je zadatak da već postojeće stablo
dovede u balans. Nažalost, algoritmi za balansiranje stabla nisu jednostavni i
generalno nisu reda O[log2N], te bi to značilo da utrošak vremena za balansiranje
praktično anulira prednosti koje stablo pruža u pogledu osnovnih operacija. Posebna
procedura za balansiranje ima još jedan nedostatak: primenjuje se na poziv, u trenutku
kada se zaključi da se, zbog dodavanja i uklanjanja, stablo udaljilo od balansa. Ovo
pak usporava klijentski softver i to u nepredvidivim vremenskim trenucima, ako se
procedura uključuje automatski. Posebne procedure za balansiranje imaju smisla kada
je stablo koncipirano tako da se, s vremena na vreme, mora rekonfigurisati, recimo za
stabla koja imaju logičko brisanje. Pošto se takva stabla i inače moraju
rekonfigurisati, u proceduru za rekonfigurisanje ugrađuje se i segment za balansiranje.
Standardni pristup održavanju performanse na visokom nivou zahtevao bi da
se balansiranje vrši dinamički, tj. da se stablo održava u balansiranom stanju.
Nažalost, ovo bitno posložnjava i usporava operacije uklanjanja i dodavanja, tako da
se ponovo ugrožava glavna prednost stabla - brzina osnovnih operacija. U svrhu
usklađivanja protivurečnih zahteva - balansiranost i brze operacije uklanjanja i
dodavanja - formiraju se postupci koji imaju za cilj da približe stablo balansu.
Postupaka ima više, i svi su dosta efikasni, u smislu da zadržavaju osnovne operacije
na nivou vremenske kompleksnosti od Olog2N.
Ključnu ulogu u održavanju stabla u balansiranom stanju ima operacija sa
nazivom rotiranje. Ona omogućuje rearanžiranje čvorova, ali tako da invarijanta BST
ostaje u važnosti. Termin „rotacija“ odnosi se na rotaciju proizvoljnog podstabla oko
nekog čvora. Na slici 6.25 prikazana je tzv. leva rotacija oko čvora x, a na slici 6.26
prikazana je desna rotacija oko istog čvora. Funkcije rotL za levu rotaciju i rotR za
desnu rotaciju oko čvora x su veoma jednostavne, tako da ne čudi što se kaže da je

214
Dušan T. Malbaški - Algoritmi i strukture podataka

rotaciju lakše programski realizovati nego objasniti. Funkcije imaju za parametar čvor
oko kojeg se vrši rotacija, a za rezultat vraćaju pokazivač na čvor koji je zamenio
mesto sa x. U slučaju leve rotacije to je njegov desni podređeni, a u slučaju desne
rotacije njegov levi podređeni.
//rotate left
Node* rotL(Node* x) {
Node *node = x->right;
x->right = node->left;
node->left = x;
return node;
}

//rotate right
Node* rotR(Node* x) {
Node *node = x->left;
x->left = node->right;
node->right = x;
return node;
}

rotL(x)
z
x rezultat: z
x
y z
y D
C
A B C D
A B

Slika 6.25.

Smisao obe varijante rotacije vidi se na slikama 6.25 i 6.26. Naime, rotacijom čvora
ulevo skraćuje se desno podstablo čvora z koji je nasledio čvor x na mestu korena
podstabla, a produžava se levo podstablo. Prilikom rotiranja udesno situacija je
obrnuta. Jenom rečju, rotacijom se može promeniti visina čitavog podstabla
generisanog čvorom x, zadržavajući pritom invarijantu BST. Neka je, na primer,

215
Dušan T. Malbaški - Algoritmi i strukture podataka

visina podstabla D sa slike 6.25 levo jednaka t, dok su visine podstabala A, B i C


manje ili jednake t-2, što čini visinu cele strukture jednakom t+2. Po izvedenoj
rotaciji ulevo, visina desnog podstabla novog korena z postaje t, a njegovog levog
podstabla najviše t. Shodno tome, visina cele strukture (sa čvorom z) sa t+2 smanjuje
se na t+1.

rotR(x)
y
x rezultat: y
x
y z
A z

A B C D B
C D
Slika 6.26.

6.3.5. AVL stablo


Jedna od najpoznatijih varijanata binarnog stabla pristupa sa automatskim
rekonfigurisanjem u svrhu održavanja približnog balansa jesu tzv. AVL stabla,
nazvana po autorima (G. Adelson-Velskij i E.M.Landis, 1962.). I samo AVL stablo
može se realizovati u više varijanata od kojih ćemo se opredeliti za onu opisanu u [12]
i [13], uz izvesne modifikacije. Ova vrsta stabla ne garantuje perfektni balans, ali zato
garantuje da će se stablo održavati u tzv. visinskom balansu (engl. height-balance).
Podsetimo se: stablo je perfektno balansirano ako se podstabla na istom
hijerarhijskom nivou po broju elemenata razlikuju najviše za 1. Visinski balansirano
stablo se definiše kao stablo u kojem za svaki čvor važi da se visine njegovih
podstabala razlikuju najviše za 1. Očigledno, perfektno balansirano stablo je
istovremeno i vidsinski balansirano, dok obrnuto ne važi. Oba stabla na slici 6.3 su
visinski balansirana, ali nijedno nije perfektno balansirano. Stablo na slici 6.4 je
perfektno balansirano.
Svrha AVL stabla jeste da se obezbedi dinamički visinski balans, tj. da stablo
u svakom trenutku ima osobinu (nazovimo je AVL invarijanta) da se visina levog i
desnog podstabla ma kojeg čvora razlikuju najviše za 1. Osnovna ideja kojom se ovo
postiže kod AVL stabala jeste da se svaki čvor proširi poljem koje obezbeđuje
informaciju o balansiranosti podstabla generisanog tim čvorom. Postoji više načina da

216
Dušan T. Malbaški - Algoritmi i strukture podataka

se ovo realizuje, no oni se ne razlikuju suštinski, tako da je dovoljno navesti jedno


rešenje, a iz njega se lako izvode slična. Rešenje je sledeće: svaki čvor proširuje se
jednim poljem h koje u svakom trenutku sadrži visinu podstabla generisanog tim
čvorom. Ako su left i right levi i desni pokazivač čvora x, AVL invarijanta zahteva da
za svaki takav čvor važi
|x.left.h-x.right.h|  1
Razlika x.left.h-x.right.h nosi naziv faktor balansa čvora x, u oznaci bf(x). Saobrazno
tome, AVL invarijanta može se prikazati i kao
|bf(x)|  1
za svaki čvor x u AVL stablu. Očigledno, kada je bf(x)>1 to znači da je potrebno
"skratiti" levo podstablo, a kada je bf(x)<-1 isto treba uraditi sa desnim podstablom.
Pritom, usklađivanje visine levog i desnog podstabla ne sme narušiti definicionu
osobinu binarnog stabla pristupa opisanu BST invarijantom. Operacija kojom se
postižu oba cilja - podešavanje visine podstabala i očuvanje BST invarijante - jeste
upravo rotacija. Programska definicija AVL stabla sa informacionim sadržajem item
ima sledeći oblik:
typedef struct node {
K key;
T item;
struct node *left,*right;
int h;
} Node;
typedef struct {
Node *root;
} AVLTree;
Razlika u odnosu na obično binarno stablo pristupa jeste u polju h čvora koje u
svakom trenutku sadrži visinu podstabla generisanog njime.
Da bi se AVL stablo moglo održavati u balansiranom stanju treba, pre svega,
ustanoviti koje su to operacije što mogu da naruše balans48. Odgovor je jednostavan:
to su operacije dodavanja i uklanjanja. Ostale operacije poput kreiranja, pristupa,
obilaska itd. iste su kao kod običnog binarnog stabla pristupa.

48
U daljem tekstu termin “balans” odnosiće se na visinski balans

217
Dušan T. Malbaški - Algoritmi i strukture podataka

Počećemo sa operacijom dodavanja. U idejnom smislu, operacija dodavanja u


AVL stablo realizuje se superpozicijom algoritma dodavanja u BST stablo (zbog BST
invarijante) i dela za uspostavu balansa (zbog AVT invarijante). Odmah uočavamo da
dodavanje novog čvora (setimo se: uvek na mestu lista) može da izazove disbalans
kod neposredno nadređenog čvora, ali i kod čvora na višem nivou, što odmah ukazuje
na potrebu da odgovarajuća funkcija bude rekurzivna. Kao što smo videli, disbalans
se uklanja operacijom rotiranja koja čuva BST invarijantu. Da bi se operacije
rotiranja uskladile sa AVL stablom potrebno ih je podesiti tako da po izvedenoj
rotaciji i visina (tj. polje h) bude prilagođeno novom rasporedu čvorova. Izmene su
jednostavne i samoobjašnjavajuće;
#define max(A,B) (((A)>(B))?(A):(B))
#define geth(NODE) ((!(NODE))?0:((NODE)->h))
#define getBalance(NODE) ((NODE)?(geth(NODE->left)-geth(NODE->right)):0)

//rotate left
Node* leftRotate(Node* x) {
Node *y = x->right;
x->right = y->left;
y->left = x;
//azurirati visinu
x->h=max(geth(x->left),geth(x->right))+1;
y->h=max(geth(y->left),geth(y->right))+1;
return y;
}

//rotate right
Node* rightRotate(Node* x) {
Node *y = x->left;
x->left = y->right;
y->right = x;
//azurirati visinu
x->h=max(geth(x->left),geth(x->right))+1;
y->h=max(geth(y->left),geth(y->right))+1;

218
Dušan T. Malbaški - Algoritmi i strukture podataka

return y;
}
U svrhu skraćivanja izvornog kôda, dodate su tri makrodirektive: makrodirektiva
max(A,B) za određivanje veće od vrednosti A i B, makrodirektiva geth za određivanje
visine čvora i makrodirektiva getBalance za računanje faktora balansa (trebaće u
nastavku). Direktiva geth očitava polje h, ali samo ako pokazivač NODE pokazuje na
čvor, dok u suprotnom vraća vrednost NULL. Visina čvora određuje se kao veća od
visina levog i desnog podređenog uvećana za 1 da bi se uračunao i sam čvor.
Makrodirektiva getBalance vraća faktor balansa ako čvor nije NULL, odnosno
vrednost 0 u suprotnom.
Prilikom umetanja novog čvora u stablo, do disbalansa može doći kod
neposrednog nadređenog, ali i kod nadređenog višeg nivoa, pod uslovom da se nalazi
na putu od korena do novododatog čvora. To pak znači da funkcija za dodavanje mora
čuvati u memoriji taj put, a to opet sugeriše da je prirodno rešenje rekurzivna funkcija.
Dakle, osnovu za operaciju dodavanja činiće rekurzivna varijanta funkcije putItem
binarnog stabla pristupa, opisana na kraju odeljka 6.3.3. Rekurzivna funkcija _putItem
za dodavanje u AVL stablo odvija se u sledećim fazama:
 u prvoj, pronalazi se mesto za dodavanje čvora standardnom rekurzivnom
varijantom metode (ako element sa zadatim ključem već postoji, menja mu se
polje item i postupak se završava)
 u drugoj fazi vrši se visinsko balansiranje čvorova u smeru ka korenu; pošto se
radi o rekurzivnoj funkciji, na steku se nalazi kompletan put (adrese čvorova)
od čvora nadređenog novododatom do korena.
Ako je dodavanje izazvalo disbalans u nekom čvoru, taj čvor mora biti negde na putu
do korena, što znači da će razrešavanje rekurzije ranije ili kasnije dovesti do tog
čvora. Neka je a novododati čvor. Neka je z čvor u kojem je detektovan disbalans, y
njegov neposredni podređeni, a x neposredni podređeni čvora y. Čvorovi x, y i z
moraju se nalaziti na putu ka korenu, te prema tome i na susednim frejmovima steka.
Moguća su četiri slučaja:
1. y je levi podređeni z i x je levi podređeni y (slučaj levi-levi)
2. y je levi podređeni z i x je desni podređeni y (slučaj levi-desni)
3. y je desni podređeni z i x je desni podređeni y (slučaj desni-desni)
4. y je desni podređeni z i x je levi podređeni y (slučaj desni-levi)

219
Dušan T. Malbaški - Algoritmi i strukture podataka

Na slikama 6.27 a, b, c i d prikazani su navedeni slučajevi i način primene rotacije u


svrhu rebalansiranja (t1, t2, t3 i t4 su koreni podstabala!).

levi-levi
z y

y t4 x z
rightRotate(z)
x t3 t1 t2 t3 t4

t1 t2
Slika 6.27a

levi-desni

z z

y t4 x t4
leftRotate(y)
t1 x y t3

t2 t3 t1 t2

rightRotate(z) y z

t1 t2 t3 t4

Slika 6.27b

desni-desni
z y

t1 y z x
leftRotate(z)
t2 x t1 t2 t3 t4

t3 t4

Slika 6.27c

220
Dušan T. Malbaški - Algoritmi i strukture podataka

desni-levi

z z

t1 y t1 x
rightRotate(y)
x t4 t2 y

t2 t3 t3 t4

leftRotate(z) z y

t1 t2 t3 t4

Slika 6.27d

Rekurzivna funkcija _putItem ima sledeći oblik:


//DODAVANJE
//Preduslov: -
//Postuslov: dodat novi cvor; ako vec postoji promenjeno mu je polje item
Node* _putItem(Node* node,K key,T item) {

//formirati novi cvor


if(!node) {
node=malloc(sizeof(Node));
node->key=key;
node->item=item;
node->left=node->right=NULL;
node->h=1;
return node;
}
//pronaci mesto za upis
if (key < node->key)
node->left = _putItem(node->left, key, item);
else if (key > node->key)
node->right = _putItem(node->right, key, item);
else {

221
Dušan T. Malbaški - Algoritmi i strukture podataka

node->item=item;
return node; //u slucaju dupliranja kljuca promeniti polje item i zavrsiti
}
//azurirati visinu
node->h = max(geth(node->left), geth(node->right)) + 1;
//izracunati faktor balansa
int balance = getBalance(node);
//ako je na cvoru node disbalans, postoje 4 slucaja
//levi-levi
if (balance > 1 && key < node->left->key)
return rightRotate(node);
//desni-desni
if (balance < -1 && key > node->right->key)
return leftRotate(node);
//levi-desni
if (balance > 1 && key > node->left->key) {
node->left = leftRotate(node->left);
return rightRotate(node);
}
//desni-levi
if (balance < -1 && key < node->right->key) {
node->right = rightRotate(node->right);
return leftRotate(node);
}
//zavrsiti
return node;
}
Radi jednostavnosti korišćemka, kao i do sada, zatvorićemo ovu funkciju u omotač
oblika
void putItem(AVLTree* avlt,K key,T item) {
avlt->root=_putItem(avlt->root,key,item);
}

222
Dušan T. Malbaški - Algoritmi i strukture podataka

Kao i operacija dodavanja, operacija uklanjanja čvora je rekurzivna jer


zahteva dodatne radnje vezane za balansiranje. Neka je w čvor koji se uklanja.
Postupak se odvija u dve faze:
1. ukloniti čvor standardnim (Hibardovom) algoritmom
2. počev od w pratiti put ka korenu (put se nalazi na steku); neka je z prvi čvor sa
disbalansom, y njegov podređeni sa većom visinom, a x podređeni čvora y,
opet sa većom visinom49; izvršiti rebalansiranje podstabla sa korenom z;
nastaviti sa postupkom sve dok se ne obradi i koren celog stabla.
Rebalansiranje podstabla sa korenom u čvoru z podrazumeva 4 slučaja (slika 6.28 a,
b, c i d):
1. y je levi podređeni čvora z i x je levi podređeni čvora y (slučaj levi-levi)
2. y je levi podređeni, a x je desni podređeni (slučaj levi-desni)
3. y je desni podređeni i x je desni podređeni (slučaj desni-desni)
4. y je desni podređeni, a x je levi podređeni (slučaj desni-levi)
Pošto su akcije iste kao i kod dodavanja, odmah navodimo kôd odgovarajuće funkcije:
//rekurzivno uklanjanje
//Preduslov: -
//Postuslov: uklonjen cvor node ako postoji
Node* _removeItem(Node* node, K key) {
//izvrsiti standardno uklanjanje iz BST stabla
if(!node) return NULL;
if(key<node->key) node->left=_removeItem(node->left,key);
else if(key>node->key) node->right=_removeItem(node->right,key);
else { //cvor nadjen
Node *temp;
if(!node->left) {
temp=node->right; //nema levog podredjenog
free(node);
node=temp;
} else if(!node->right) {
temp=node->left; //nema desnog podredjenog
free(node);

49
uočiti razliku u odnosu na dodavanje!

223
Dušan T. Malbaški - Algoritmi i strukture podataka

node=temp;
} else { //ima oba podredjena
temp=node->left; //pronaci najveci u levom podstablu
while(temp->right) temp=temp->right;
node->key=temp->key;
node->item=temp->item;
node->left=_removeItem(node->left,temp->key);
}
}
//ako je stablo imalo samo jedan cvor zavrsii
if(!node) return NULL;
//azurirati visinu tekuceg cvora
node->h=max(geth(node->left),geth(node->right))+1;
//izracunati faktor balansa
int balance=node ? geth(node->left)-geth(node->right) : 0;
//ako je na cvoru disbalans postoje 4 slucaja
//levi-levi
if(balance<-1 && getBalance(node->left)>=0) return rightRotate(node);
//levi-desni
if(balance>1 && getBalance(node->left)<0) {
node->left=leftRotate(node->left);
return rightRotate(node);
}
//desni-desni
if(balance<-1 && getBalance(node->right)<=0) return leftRotate(node);
//desni-levi
if(balance<-1 && getBalance(node->right)>0) {
node->right=rightRotate(node->right);
return leftRotate(node);
}
return node;
}

224
Dušan T. Malbaški - Algoritmi i strukture podataka

Rekurzivno uklanjanja ćemo, kao i do sada, umetnuti u funkciju-omotač da bi


parametar bio ne čvor nego stablo:
void removeItem(AVLTree* avlt, K key) {
avlt->root=_removeItem(avlt->root,key);
}

6.3.6. Stohastičko stablo


Stohastičko stablo (engl. Randomized Binary Search Tree) jeste još jedan
način za ublažavanje disbalansa te, samim tim, i za poboljšanje performanse binarnog
stabla pristupa. Za razliku od AVL stabla zasnovano je na ujednačavanju ne visine
podstabala, nego njihove veličine izražene brojem elemenata. Ujednačavanje veličine
podstabala bazira se na probabilističkom pristupu, pa otud i naziv - stohastičko stablo.
Osnovna ideja oslanja se na upoređenje načina nastanka dva ekstremna
slučaja: najboljeg (perfektno balansirano stablo) i najgoreg (stablo degenerisano u
linearnu strukturu). Pokazuje se (dokaz nije trivijalan) da je verovatnoća da se od N
ulaznih podataka formira perfektno balansirano stablo daleko veća nego da se formira
linearna struktura. Štaviše - a to je baza čitavog pristupa - nasumice odabrana
sekvenca podataka rezultovaće stablom koje je relativno blisko perfektno
balansiranom i to dovoljno bliskom da se za osnovne algoritme praktično garantuje
kompleksnost reda Olog2N.
Zadatak je jednostavno formulisati (ne i rešiti): obezbediti da se algoritmi
dodavanja i uklanjanja ponašaju onako kako bi se ponašali kada bi redosled ključeva
za dodavanje-uklanjanje bio slučajan. Na primer, kada je redosled dodavanja strogo
rastući po ključu, osnovno stablo bi se degenerisalo u linearnu strukturu (najgori
slučaj). Isti algoritam bi za proizvoljno odabranu sekvencu podataka formirao stablo
relativno blisko perfektno balansiranom. Pošto na redosled dodavanja, u opštem
slučaju, nemamo uticaja, treba modifikovati algoritam dodavanja, tako da se on
ponaša kao da je redosled ključeva slučajan.
Ključna operacija za ostvarivanje simuliranog slučajnog dodavanja jeste
operacija korenskog dodavanja (engl. root insertion). Ideja je jednostavna: kod
slučajnog dodavanja u ma koje podstablo sa m čvorova, verovatnoća da novododati
čvor bude upisan na bilo kojem mestu u podstablu iznosi 1/(m+1) što, naravno, važi i
za koren podstabla. Modifikovani algoritam dodavanja zasnovan je upravo na ovom
poslednjem: prilikom traženja mesta na kojem će se naći novi čvor standardnim

225
Dušan T. Malbaški - Algoritmi i strukture podataka

algoritmom, prilikom prelaska u ma koje podstablo, otvara se mogućnost da sa


verovatnoćom 1/(m+1) novododati element preuzme ulogu korena tog podstabla.
Shodno tome, operacija dodavanja logički se sastoji iz dva dela: prvi je standardno
dodavanje na mestu lista, a drugi korensko dodavanje. Pri svakom prelasku na niži
nivo, generiše se pseudoslučajan broj (bibliotečka funkcija rand) koji je sa
verovatnoćom 1/(m+1) manji od 1. Ako to nije slučaj, nastavlja se sa standardnim
dodavanjem. Ako jeste, aktivira se operacija korenskog dodavanja kojom se novi čvor
dodaje na mestu korena podstabla.
Da bi se operacije uopšte mogle izvoditi, definicija stohastičkog stabla mora se
neznatno modifikovati (kao i kod AVL stabla), tako da svaki čvor sadrži polje n koje
je jednako broju čvorova u podstablu generisanom tim čvorom. Dakle, definicija
stohastičkog stabla ima sledeći oblik:
typedef struct node {
K key;
T item;
struct node *left,*right;
int n; //broj cvorova u podstablu generisanom datim cvorom
} Node;
typedef struct {
Node *root;
} RandomTree;
Razmotrimo, prvo, samu operaciju korenskog dodavanja. U prvoj fazi, novi
čvor se dodaje na mestu lista, kao kod standardnog postupka. U drugoj fazi, međutim,
novi element se operacijama rotacije u oba smera podiže, sve dok ne zauzme mesto
korena podstabla. Operacije rotiranja moraju se modifikovati tako da se ažurira i polje
n jer rotiranje menja veličinu generisanih podstabala:
#define getSize(NODE) ((NODE)?((NODE)->n):0)
#define setSize(NODE)
(NODE)->n=getSize((NODE)->left)+getSize((NODE)->right)+1
//rotiranje ulevo
Node* rotateLeft(Node* node) {
Node *x=node->right;
node->right=x->left;

226
Dušan T. Malbaški - Algoritmi i strukture podataka

x->left=node;
setSize(node); setSize(x);
return x;
}
//rotiranje udesno
Node* rotateRight(Node* node) {
Node *x=node->left;
node->left=x->right;
x->right=node;
setSize(node); setSize(x);
return x;
}
Za određivanje veličine podstabla generisanog čvorom (nalazi se u polju n), koristimo
makrodirektivu getSize, jer pokazivač može biti i NULL u kojem slučaju se smatra da
je pomenuta veličina jednaka 0. Za ažuriranje broja čvorova u takvom podstablu
koristimo makrodirektivu setSize koja zbraja veličine levog i desnog podstabla i
dodaje 1 da bi bio uračunat i koren.

S S S

E X E X E X

C R C R C G

H G R

G H H

S G

G X E S

E R C R X

C H H

Slika 6.28

Druga faza operacije korenskog dodavanja prikazana je na slici 6.28.


Pretpostavlja se da je u prvoj fazi čvor G dodat u podstablo generisano čvorom S, te se
nalazi na mestu jednog od listova. Da bi se čvor G doveo na mesto korena, tj. čvora S,
potrebno je izvršiti seriju rotiranja prikazanu na slici. Rotiranjem se čvor G penje u

227
Dušan T. Malbaški - Algoritmi i strukture podataka

hijerarhiji i to tako da, ako je G levi podređeni, tada se vrši desna rotacija, a kada je
desni podređeni, rotacija je obrnuta. Prilikom izvođenja rotacije, kao što i piše u
izvornom kodu, mora se podešavati veličina novog podstabla, a razlog se jasno vidi na
slici.
Rekurzivna funkcija za korensko dodavanje u stohastičko stablo izgleda ovako:
//Dodavanje u koren podstabla
Node* putRoot(Node* node,K key,T item) {
if(!node) {
node=malloc(sizeof(Node));
node->key=key; node->item=item;
node->left=node->right=NULL;
node->n=1;
return node;
}
//trazenje mesta za dodavanje
if(key==node->key) node->item=item; //duplikat kljuca
else if (key<node->key) {
node->left=putRoot(node->left,key,item);
node=rotateRight(node);}
else {
node->right=putRoot(node->right,key,item);
node=rotateLeft(node);}
return node;
}
Sve dok se ne naiđe na list, funkcija ostvaruje rekurzivni prolaz kroz stablo. Kada je
dostignut list, formira se novi čvor i u povratnoj fazi rekurzije vrši se rotacija tako da
ako je novi čvor dodat na mestu desnog podređenog rotacija je ulevo, a u suprotnom
rotacija je udesno. Tako se nastavlja sve do korena postabla. Krajnji rezultat jeste da
se novi čvor nalazi na mestu korena podstabla na koje je funkcija putRoot primenjena.
Funkcija uzima u obzir i slučaj kada se u podstablu već nalazi element sa zadatim
ključem key (duplikat ključa): u tom slučaju samo se upisuje novi sadržaj u polje item.

228
Dušan T. Malbaški - Algoritmi i strukture podataka

Osnovna procedura _putItem za dodavanje u stohastičko stablo koncipirana je


tako da koristi standardni postupak, ali sa određenom verovatnoćom da u nekom
koraku pređe na korensko dodavanje:
//Rekurzivno dodavanje u stablo
Node* _putItem(Node* node,K key,T item) {
if(!node) {
node=malloc(sizeof(Node));
node->key=key; node->item=item;
node->left=node->right=NULL;
node->n=1;
return node;
}
if(key==node->key) {node->item=item; return node;} //duplikat kljuca
if(rand()%(node->n+1)==0) return putRoot(node,key,item);
else if (key<node->key) node->left = _putItem(node->left,key,item);
else node->right=_putItem(node->right,key,item);
node->n=getSize(node->left)+getSize(node->right)+1;;
return node;
}
Funkcija _putItem odvija se u sadejstvu sa funkcijom putRoot za korensko dodavanje.
Njome počinje procedura za dodavanje, koja startuje sa korenom celog stabla. U toku
izvršenja kompletnog algoritma u prvoj fazi se mora stići do čvora ispod kojeg bi
standardnim postupkom bio dodat novi čvor. To se može ostvariti na tri načina:
 izvršavanje funkcije _putItem do kraja, bez aktiviranja putRoot
 momentalnim aktiviranjem putRoot koja takođe obezbeđuje primenu
standardnog postupka za dodavanje u prvoj fazi
 prelaskom na putRoot u toku traženja mesta za dodavanje; no tada prelazak na
funkciju putRoot ne menja nego samo nastavlja standardni postupak.
Razlika u odnosu na standardni postupak dodavanja nastaje u toku povratne faze
rekurzije. Naime, ako je, u toku prve faze, došlo do prelaska na korensko dodavanje,
tada će u povratnoj fazi novi čvor biti dodat na mestu čvora na kojem je izvršen
prelazak i to u okviru funkcije putRoot, sukcesivnom primenom rotacije, kao što je

229
Dušan T. Malbaški - Algoritmi i strukture podataka

već opisano. Posle korenskog dodavanja, funkcija putRoot vraća kontrolu funkciji
_putItem koja nastavlja povratnu fazu standardnim postupkom.
Odluka o tome hoće li biti prelaska na korensko dodavanje donosi se prilikom
prelaska na niži nivo stabla u prvoj fazi traženja mesta za dodavanje50. Odluka se
donosi stohastički na bazi verovatnoće da novododati čvor bude koren nekog
podstabla. Neka je x čvor koji treba dodati. Neka je u postupku traženja mesta za
dodavanje algoritam stigao do čvora a, što znači da će se x sigurno naći u podstablu
generisanom čvorom a. Neka je broj čvorova u tom podstablu m (u funkciji se broj
čvorova odeđuje makrodirektivom getSize). Kada bi redosled dodavanja bio slučajan,
čvor x bi u tom podstablu bio koren (umesto a) sa verovatnoćom 1/(m+1)51. Da bi se
postigao efekat slučajnog dodavanja, pri nailasku na čvor a treba sa verovatnoćom
1/(m+1) aktivirati funkciju putRoot koja će izvršiti pomenuti zadatak. Mehanizam za
postizanje ovog cilja jesu pseudoslučajni brojevi koji se u programskom jeziku C
dobijaju primenom funkcije rand(). Funkcija rand generiše uniformno raspodeljene
celobrojne pseudoslučajne brojeve u nekom rasponu [0,RAND_MAX], gde je
RAND_MAX simbolička konstanta. S obzirom na uniformnost raspodele ovih brojeva,
vrednost
rand()%(m+1)
biće jednaka nuli sa verovatnoćom 1/(m+1), a to je tražena verovatnoća potrebna za
uključivanje korenskog dodavanja. Konačno, u slučaju da element sa zadatim ključem
već postoji u stablu (dupliran ključ), samo se upisuje novi sadržaj u polje item.
Funkcija _putItem zatvorena je u funkciju-omotač putItem koja se poziva iz
klijenta. Funkcija vraća vrednost 1 ako je stablo prošireno novim elementom, odnosno
0 ako je ključ bio dupliran, a to se ostvaruje jednostavnim poređenjem broja čvorova u
stablu pre i posle primene funkcije _putItem.
//Rekurzivno dodavanje u stablo
//Preduslov: -
//Postuslov: dodat cvor; ako je vec postojao izmenjeno polje item
//Rezultat: 1 ako je cvor dodat; 0 ako je vec postojao
int putItem(RandomTree* rt,K key,T item) {
int sizeBefore;

50
uočiti da se na korensko dodavanje prelazi samo jednom
51
podstablo ima m zatečenih elemenata kojima se dodaje element x

230
Dušan T. Malbaški - Algoritmi i strukture podataka

sizeBefore=getSize(rt->root);
rt->root=_putItem(rt->root,key,item);
return getSize(rt->root)-sizeBefore; //provera da li je bilo dodavanja
}
Ponašanje stohastičkog stabla ilustrovaćemo (ne i analizirati!) tabelom u kojoj
su dati visina i prosečan broj poređenja pri uspešnom traženju52, a za stohastičko
stablo koje je generisano sukcesivnim dodavanjem elemenata sa ključevima u
rastućem poretku. Inače, to bi kod standardnog binarnog stabla pristupa rezultovalo
najgorim slučajem - stablom degenerisanim u linearnu strukturu:
broj visina srednji
čvorova broj
pristupa
10 6 3,7
100 12 6.9
1000 24 12,7
5000 34 16,0
10000 36 16,7
30000 39 19,2
50000 40 19,6
100000 41 20,5
Čak i bez posebne analize, očigledno je da sa naglim rastom broja čvorova visina
stabla i srednji broj pristupa imaju vrlo umeren rast koji je za vrlo veliko stablo
gotovo zanemarljiv.
Operacija uklanjanja čvora iz stohastičkog stabla oslanja se na tzv. spajanje
(engl. join) kojom se od dva binarna stabla pristupa dobija jedno. Uslov za operaciju
join jeste da su ključevi u jednom stablu veći od svih ključeva u drugom. Neka su L i
R dva binarna stabla pristupa sa korenima respektivno a i b, takva da su ključevi u
stablu R veći od ključeva u stablu L. Da bi se očuvala BST invarijanta, moguća su dva
načina spajanja:
1. učiniti stablo R desnim podstablom najvećeg čvora u L, koji se dobija
praćenjem desnih pokazivača u L; koren novog stabla postaje a
2. učiniti stablo L levim podstablom najmanjeg čvora u R, koji se dobija
praćenjem levih pokazivača u R; koren novog stabla postaje b.
Dva načina spajanja prikazana su na slici 6.29.

52
podaci su dobijeni jednostavnim eksperimentom

231
Dušan T. Malbaški - Algoritmi i strukture podataka

a b

L R

R L

Slika 6.29

Stohastičko uklanjanje uzima u obzir obe varijante, s tim što se konkretna varijanta
bira na probabilističkoj osnovi, a na osnovu veličine stabala L i R. U načelu, algoritam
je sasvim jednostavan: polazi se od korena i prati desni (levi) pokazivač sve dok se ne
stigne do čvora koji ga nema. Za taj čvor vezuje se koren drugog stabla. Ipak, funkcija
koju ćemo koristiti biće rekurzivna jer se posle spajanja mora ažurirati polje n (broj
čvorova) u svakom čvoru i to unatrag. Funkcija sa nazivom join za parametre ima
korene dva stabla i ima sledeći oblik:
//Spajanje dva stabla
//Preduslov: kljucevi u stablu b su veci od kljuceva u stablu a
//Postuslov: stabla spojena. Koren je ili a ili b
//Rezultat: koren novog stabla
Node* join(Node* a,Node* b) {
if(!a) return b;
if(!b) return a;
if(rand()%(getSize(a)+getSize(b))<getSize(a)) {
a->right = join(a->right,b);
setSize(a);
return a;
} else {
b->left = join(a,b->left);
setSize(b);
return b;
}

232
Dušan T. Malbaški - Algoritmi i strukture podataka

}
Rezultat funkcije je koren novog stabla dobijenog spajanjem. Algoritam funkcioniše
tako što se praćenje desnih pokazivača u stablu L i levih pokazivača u stablu R odvija
simultanim postupcima, kao da se „utrkuju“ koji će pre stići do pokazivača NULL. To
„napredovanje“ ka čvoru NULL odvija se tako što se u svakom koraku rekurzije sa
verovatnoćom N(L)/(N(L)+N(R)) prelazi na desnog podređenog u stablu L, a sa
verovatnoćom N(L)/(N(L)+N(R)) prelazi na levog podređenog u stablu R. Ako je
“pobednik” krajnji desni čvor stabla L, koren stabla R vezuje se za njega. Obrnuto,
ako je u pitanju krajnji levi čvor stabla R, tada se koren stabla L vezuje za njega. U
povratnoj fazi rekurzije ažuriraju se veličine podstabala generisanih elementima na
putu do čvora ispod kojeg je dodato drugo stablo.

Slika 6.30

Uklanjanje čvora iz stabla sada je jednostavno. Prvo se čvor pronalazi, zatim


se oslobađa funkcijom free i na kraju spajaju se njegovo levo i desno podstablo
funkcijom join, slika 6.30. Pritom i ova funkcija mora biti rekurzivno realizovana, jer
se po uklanjanju i spajanju podstabala ažuriraju veličine svih podstabala sve do
korena:
Node* _removeItem(Node* node, K key) {
if(!node) return node;
if(node->key==key) {
Node* temp=join(node->left,node->right);
free(node);
return temp;

233
Dušan T. Malbaški - Algoritmi i strukture podataka

} else if(key<node->key )
node->left=_removeItem(node->left,key);
else
node->right=_removeItem(node->right,key);
setSize(node);
return node;
}
I ovu funkciju zatvorićemo u omotač removeItem tako da, na osnovu upoređivenja
veličine stabla pre i posle pokušaja uklanjanja, vrati kôd uspešnosti.
//Uklanjanje cvora
//Preduslov: -
//Postuslov: uklonjen cvor ako ga ima
//Rezultat: 1 ako je cvor uklonjen; 0 ako nije postojao
int removeItem(RandomTree* rt,K key) {
int sizeBefore;
sizeBefore=getSize(rt->root);
rt->root=_removeItem(rt->root,key);
return sizeBefore-getSize(rt->root); //provera da li je bilo uklanjanja
}

6.4. UOPŠTENO (GENERALISANO) STABLO


Glavna karakteristika generalisanog ili uopštenog stabla (engl. general tree) je
ta da je raspored podređenih elemenata datog elementa proizvoljan i da njihov broj
nije ograničen. U tom smislu, logička struktura generalisanog stabla praktično se
podudara sa orijentisanim stablom (digrafom). I za uopšteno stablo, kao za n-arno,
postoji alternativna, rekurzivna definicija kojom se uopšteno stablo određuje kao
struktura podataka za koju važi:
 stablo je prazno ili
 stablo je uređeni par (x,P) gde je x izdvojeni element, a P jednostruko
spregnuta lista čiji su elementi međusobno različita uopštena stabla.
Kao struktura podataka, uopšteno stablo odlikuje se sledećim karakteristikama:
 radi se o strukturi podataka tipa stabla
 dozvoljen je pristup svakom elementu

234
Dušan T. Malbaški - Algoritmi i strukture podataka

 element se može dodati na bilo kojem mestu


 može se ukloniti bilo koji element.
Kako vidimo, kod uopštenog stabla (teorijski) nema nikakvih ograničenja u pogledu
izvršavanja osnovnih operacija. U praksi, međutim, osnovne operacije nad uopštenim
stablom najčešće se realizuju ovako:
 glavna vrsta pristupa je navigacija
 dodaje se na mestu lista
 pri uklanjanju, briše se čitavo podstablo generisano zadatim elementom.
Elementi uopštenog stabla prepoznaju se po jedinstvenom identifikatoru iz skupa u
kojem nema linearnog uređenja (te, stoga, nije ključ). Identifikator se obično sastoji
od imena koje ne mora biti jedinstveno i puta od korena do tog elementa. Na slici 6.31
prikazano je jedno uopšteno stablo.

C
B C D
C

E C
F G H
C X

Slika 6.31

Imena elemenata su redom A, B, C itd. Identifikator elementa npr. F bio bi put ABF.
Element X dodat je ispod elementa D i predstavlja list. Kada se uklanja element B, sa
njim se uklanja podstablo sastavljeno od B, E, F i G. Pošto je osnovni način pristupa
navigacija, operacija se uvek odvija nad tekućim elementom. Ako je potrebno
pristupiti nekom elementu koji nije tekući, prethodno se mora proglasiti za tekući, pri
čemu operacija redefinisanja tekućeg elementa zahteva da se zada identifikator novog
tekućeg elementa. Kada je identifikator putanja postoje dva načina: jedan je da se
zada tzv. apsolutna putanja, tj. put od korena do novog tekućeg elementa. Drugi način
jeste da se mesto novog tekućeg odredi u odnosu na trenutni tekući (tzv. relativna
putanja). Ovaj drugi način zahteva da se uopšteno stablo snabde operacijom prelaska
na nadređeni element, koja je po matematičkoj prirodi funkcija, jer element ne može
imati više od jednog nadređenog. Neka je F tekući element i neka je potrebno
proglasiti H za novi tekući element. Ako se mesto novog tekućeg zadaje apsolutnom
putanjom, argument odgovarajuće operacije bio bi A-D-H. U slučaju da se koristi

235
Dušan T. Malbaški - Algoritmi i strukture podataka

relativna putanja (u odnosu na F), argument bi bio up-up-D-H, gde je up oznaka


operacije prelaska na nadređeni element.
Pored osnovnih operacija, uopšteno stablo ima još neke:
 kreiranje stabla
 provera da li je stablo prazno
 brisanje celog stabla
 promena tekućeg elementa te, s njom u vezi, operacija prelaska na nadređeni
element
 dodavanje podstabla
 obilazak (tj. redosledna obrada)

6.4.1. Fizička realizacija uopštenog stabla


Standardni način realizacije uopštenog stabla je tzv. binarna realizacija. Do
ideje binarne realizacije dolazi se dosta lako, na bazi zahteva koji se postavljaju pred
uopšteno stablo. S jedne strane, broj čvorova podređenih datom nije ograničen i
promenljiv je, a s druge tip (format) svakog čvora stabla mora biti isti. U tu svrhu,
čvorovi podređeni datom uređuju se u jednostruko spregnutu listu za šta je potreban
jedan pokazivač. U nadređeni čvor upisuje se adresa početka te liste za šta je potreban
još jedan pokazivač, slika 6.32. Dakle, uz pomoć svega dva pokazivača može se
realizovati uopšteno stablo koje u potpunosti odgovara definiciji.

pokazivač na
sledeći u listi

informacioni
sadržaj
pokazivač na
početak liste
podređenih
Slika 6.32.

Binarna realizacija omogućuje da se sve, pa i neuobičajene, operacije nad uopštenim


stablom realizuju na relativno jednostavan način. Moguće je (ne i uobičajeno) dodati
bilo gde (a ne samo na mestu lista), moguće je ukloniti bilo koji element, a da pritom
ne bude pomeranja čvorova i to sve zato što je lista kao struktura dovoljno fleksibilna
da omogući prevezivanje. Što se tiče uobičajenih operacija, nabrojanih u prethodnoj
tački, realizacija je jednostavna uz kombinovanje operacija nad binarnim stablom sa
operacijama nad jednostruko spregnutom listom. Binarna realizacija stabla sa slike
6.31 prikazana je na slici 6.33. Tekući čvor je E.

236
Dušan T. Malbaški - Algoritmi i strukture podataka

deskriptor
koren A *
tekući
B C * D *

E * F * G * * H * *

Slika 6.33

Inače, postoji još jedan pristup realizaciji uopštenog stabla, nazvan prirodnom
realizacijom, jer se svaka veza iz logičke strukture direktno realizuje pokazivačem u
fizičkoj strukturi. Ovaj način realizacije nije naročito pogodan zbog zahteva da svi
čvorovi stabla budu istog tipa, što bi bilo u koliziji sa promenljivim brojem
pokazivača u čvoru. Kod prirodne realizacije, broj podređenih se, zato, mora
ograničiti, čime se, u stvari, realizacija prevodi u realizaciju n-arnog stabla.
Ograničavanje reda stabla može da izazove prekoračenje, pri čemu je situacija kod
stabla znatno rizičnija nego u drugim slučajevima sekvencijalno realizovane
promenljive strukture i to zato što do prekoračenja može doći u svakom čvoru
ponaosob. To je razlog zbog kojeg prirodna realizacija nije osobito popularna.

237
Dušan T. Malbaški - Algoritmi i strukture podataka

LITERATURA

[1] Tzichritzis D. Lochovsky F.: Data Models, Prentice-Hall, Englewood Cliffs,


1982
[2] Dahl O.J., Dijkstra E.W., Hoare, C.A.R.: Structured Programming, Academic
Press 1972
[3] Wirth N.: Algorithms + Data Structures = Programs, Prentice-Hall, Englewood
Cliffs, 1976
[4] Malbaški D., Obradović D.: Osnovne strukture podataka, Tehnički fakultet
„Mihajlo Pupin“, Zrenjanin, 1994.
[5] Berztiss A.T.: Data Structures, Theory and Practice, Academic Press, 1973
[6] Bentley J.: Programming Pearls, prevod na ruski, Radio i svjaz, 1990
[7] Lorin H.: Sorting and Sort Systems, Addison-Wesley, 1972
[8] Urošević D.: Algoritmi u programskom jeziku C, Mikro knjiga, 1996
[9] Lipschutz S.: Data Structures, McGraw-Hill, 1986
[10] http://www.cise.ufl.edu/class/cot3100fa07/quicksort_analysis.pdf
[11] Cruse R., Tondo C.L., Leung B.: Data Structures & Program Design in C,
Prentice-Hall, 1991
[12] http://www.geeksforgeeks.org/avl-tree-set-1-insertion/
[13] http://www.geeksforgeeks.org/avl-tree-set-2-deletion/
[14] Marković M.: Filozofija nauke, BIGZ, Prosveta, SKZ, Beograd, 1994.
[15] Knuth D. The Art of Computer Programming, Part One, Addison-Wesley, 1975
[16] Krinickij N.A.: Algoritmy vokrug nas, Nauka, Moskva, 1984
[17] Hotomski P., Malbaški D.: Matematička logika i principi programiranja,
Univerzitet u Novom Sadu, 2000.
[18] Dujmović J.J.: Programski jezici i metode programiranja, Naučna knjiga,
Beograd, 1990.
[19] Uspenskij V.A., Semjonov A.L.: "Teorija algoritmov: osnovnye otkrytija i
priloženija", Nauka, Moskva, 1987

238

You might also like