238
UNIVERZITET FAKULTET INFORMACIONIH TEHNOLOGIJA NOVI SAD - SREMSKA KAMENICA 2015. #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+

malbaski – algoritmi i strukture podataka

Embed Size (px)

Citation preview

Page 1: malbaski – algoritmi i strukture podataka

UNIVERZITET

FAKULTET INFORMACIONIH TEHNOLOGIJA

NOVI SAD - SREMSKA KAMENICA

2015.

#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+

Page 2: malbaski – algoritmi i strukture podataka

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

Page 3: malbaski – algoritmi i strukture podataka

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

Page 4: malbaski – algoritmi i strukture podataka

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

Page 5: malbaski – algoritmi i strukture podataka

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

Page 6: malbaski – algoritmi i strukture podataka

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

Page 7: malbaski – algoritmi i strukture podataka

Dušan Malbaški - Algoritmi i strukture podataka

7

Page 8: malbaski – algoritmi i strukture podataka

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

8

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

Page 9: malbaski – algoritmi i strukture podataka

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

9

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.

Page 10: malbaski – algoritmi i strukture podataka

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

10

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

Page 11: malbaski – algoritmi i strukture podataka

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

11

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

Page 12: malbaski – algoritmi i strukture podataka

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

12

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

Page 13: malbaski – algoritmi i strukture podataka

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

13

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

Page 14: malbaski – algoritmi i strukture podataka

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

14

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

Page 15: malbaski – algoritmi i strukture podataka

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

15

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

Page 16: malbaski – algoritmi i strukture podataka

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

16

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.

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

Slika 2.1.

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

beskonačna traka

upisno-čitajuća glava upravljački blok

Page 17: malbaski – algoritmi i strukture podataka

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

17

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

Page 18: malbaski – algoritmi i strukture podataka

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

18

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(qF) {

(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

Page 19: malbaski – algoritmi i strukture podataka

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

19

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

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

Page 20: malbaski – algoritmi i strukture podataka

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

20

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 s3R

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

Page 21: malbaski – algoritmi i strukture podataka

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

21

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

Page 22: malbaski – algoritmi i strukture podataka

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

22

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)

Page 23: malbaski – algoritmi i strukture podataka

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

23

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.

Page 24: malbaski – algoritmi i strukture podataka

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

24

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 bcx 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 xa, xb 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.

Page 25: malbaski – algoritmi i strukture podataka

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

25

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.

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

T

T

T

Slika 2.3.

X

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

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

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

1

1

1

1

T

T

T

Page 26: malbaski – algoritmi i strukture podataka

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

26

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 x11x što daje 1x99 itd.

Kompletan deduktivni lanac primenjen na ulaz 199 je

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

Page 27: malbaski – algoritmi i strukture podataka

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

27

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.

Page 28: malbaski – algoritmi i strukture podataka

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

28

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”

Page 29: malbaski – algoritmi i strukture podataka

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

29

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

Page 30: malbaski – algoritmi i strukture podataka

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

30

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

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

Slika 3.1.

f

Page 31: malbaski – algoritmi i strukture podataka

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

31

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

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.

T

T

Slika 3.4.

vmax=v[0]

i=0

i<n

v[i]>vmax

vmax=v[i]

i++

Slika 3.3.

Slika 3.2.

T

p

Page 32: malbaski – algoritmi i strukture podataka

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

32

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.

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

Slika 3.5.

Page 33: malbaski – algoritmi i strukture podataka

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

33

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.

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

Slika 3.6.

if-then-else

do-while-do

proces

if-then

while-do

repeat-until

sekvenca

Page 34: malbaski – algoritmi i strukture podataka

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

34

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

Page 35: malbaski – algoritmi i strukture podataka

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

35

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

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

Slika 3.7.

A

p

B

1

2

3

0

0

Page 36: malbaski – algoritmi i strukture podataka

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

36

{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

Page 37: malbaski – algoritmi i strukture podataka

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

37

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

Page 38: malbaski – algoritmi i strukture podataka

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

38

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

Page 39: malbaski – algoritmi i strukture podataka

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

39

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 xi*/

Page 40: malbaski – algoritmi i strukture podataka

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

40

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 xi < 0 staviti neg=xi, 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.

Page 41: malbaski – algoritmi i strukture podataka

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

41

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,

Page 42: malbaski – algoritmi i strukture podataka

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

42

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

Page 43: malbaski – algoritmi i strukture podataka

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

43

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 : ZR

č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 : ZR

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 : ZR

Definišimo skup istih takvih funkcija

O[g(n)] = {f(n) | (n0,c>0) (nn0) 0f(n)cg(n)}

gde je c>0 konstanta. Manje formalno, Og(n) je oznaka za skup funkcija za koje je,

počev od neke vrednosti n=n0, funkcija constg(n) gornja granica, slika 3.8.

Page 44: malbaski – algoritmi i strukture podataka

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

44

Očigledno je da za svaku funkciju f(n) koja pripada Og(n) postoji konstanta c>0

takva da važi

g(n)

f(n)limn

< c

Na primer,

n3+2n-5 On3

2log2n+1 Olog2n

32n+log2(2n) O2n

Prilikom procene gornje granice za funkciju složenosti T(n), u stvari se određuje

funkcija g(n) takva da važi

T(n) Og(n)

Ovde smo dužni da napomenemo da se umesto gornje relacije u analizi složenosti

koristi (matematički nekorektna) notacija

T(n) = Og(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:

f1O[g1] f2O[g2] f1f2O[g1g2]

fO[g] O[fg]

f1O[g1] f2O[g2] f1+f2O[g1+g2]

fO[g] gO[h] f O[h]

f+O[g] O[f+g]

O[kg] = O[g], k=const>0

O[k+g] = O[g], k=const

n0

Slika 3.8.

cg(n)

f(n)

Page 45: malbaski – algoritmi i strukture podataka

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

45

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

fO[g] kfO[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 Og, 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

nlogk n (k>1) linearno-logaritamski

n2 kvadratni

nk (k>2) stepeni

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

Page 46: malbaski – algoritmi i strukture podataka

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

46

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(ai++))

+ n (t(for) + t(printf(ai))

+ 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) (nn0) 0 cg(n)f(n)}

Page 47: malbaski – algoritmi i strukture podataka

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

47

gde je c>0 konstanta. Neformalno, g(n) je oznaka za skup funkcija za koje je,

počev od neke vrednosti n=n0, funkcija constg(n) donja granica. Ilustracija je data na

slici 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) (nn0) 0 c1g(n)f(n)c2g(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.

c1g(n)

n0

Slika 3.10.

c2g(n)

f(n)

n0

Slika 3.9.

cg(n)

f(n)

Page 48: malbaski – algoritmi i strukture podataka

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

48

Page 49: malbaski – algoritmi i strukture podataka

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

49

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

Page 50: malbaski – algoritmi i strukture podataka

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

50

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

Page 51: malbaski – algoritmi i strukture podataka

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

51

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 Aij, što je i

jednostavno i brzo. Obrada nenultih elemenata vrši se dvostrukim for ciklusom:

int i,j,v,k;

Page 52: malbaski – algoritmi i strukture podataka

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

52

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

Page 53: malbaski – algoritmi i strukture podataka

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

53

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 On2. 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 BMAX_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 On2, 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

Page 54: malbaski – algoritmi i strukture podataka

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

54

zaključiti da je složenost modifikovanog postupka u najgorem slučaju jednaka On2.

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

Aij 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

Page 55: malbaski – algoritmi i strukture podataka

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

55

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.

Page 56: malbaski – algoritmi i strukture podataka

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

56

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

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.

x1 x2 .... xn

Slika 1.1.

Page 57: malbaski – algoritmi i strukture podataka

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

57

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

Page 58: malbaski – algoritmi i strukture podataka

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

58

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

Page 59: malbaski – algoritmi i strukture podataka

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

59

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

PSQ

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 PSQ 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 xln 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

Page 60: malbaski – algoritmi i strukture podataka

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

60

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

Page 61: malbaski – algoritmi i strukture podataka

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

61

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.

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

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

sekvence.

(a)

Slika 1.2

(b)

Page 62: malbaski – algoritmi i strukture podataka

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

62

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

Page 63: malbaski – algoritmi i strukture podataka

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

63

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.

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.

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.

Slika 1.5

Slika 1.4

Page 64: malbaski – algoritmi i strukture podataka

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

64

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

Page 65: malbaski – algoritmi i strukture podataka

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

65

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.

Page 66: malbaski – algoritmi i strukture podataka

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

66

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 iI vrednost

indeksa, tada se u C-u operacija indeksiranja niza a realizuje izrazom

ai

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 xi izvršava

preliminarnim računanjem adrese elementa formulom

adr(x[i]) = adr(x[p]) + (i-p)L ..... (2.1)

Page 67: malbaski – algoritmi i strukture podataka

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

67

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 aduzina 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 imeduzina;

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

imeindeks

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

Page 68: malbaski – algoritmi i strukture podataka

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

68

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

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

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

Slika 2.1.

Page 69: malbaski – algoritmi i strukture podataka

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

69

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

adr(M[j1]...[jn]) = adr(M[p1]...[pn]) + L* mmm

n

1m

)Dpj(

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

Page 70: malbaski – algoritmi i strukture podataka

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

70

d = malloc(B*sizeof(T));

i popunjavao redom od lokacije d[0] do dn-1 gde je n aktuelna dužina niza. Stanje je

prikazano na slici 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.

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:

...

Slika 2.3.

d

(a)

... d

(b)

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

Slika 2.2.

d

Page 71: malbaski – algoritmi i strukture podataka

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

71

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.

Page 72: malbaski – algoritmi i strukture podataka

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

72

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.

Page 73: malbaski – algoritmi i strukture podataka

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

73

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 = On2

š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

Page 74: malbaski – algoritmi i strukture podataka

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

74

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

Page 75: malbaski – algoritmi i strukture podataka

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

75

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)=On2, 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

Page 76: malbaski – algoritmi i strukture podataka

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

76

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,

Uins = 1/2 )1i(1-n

1i

34 videti odeljak o linearnom traženju u tabelama

Page 77: malbaski – algoritmi i strukture podataka

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

77

odnosno

Uins = (n-1)(n+2)/4 ≈ n2/4

Shodno tome, ponovo se susrećemo sa algoritmom čija je funkcija složenosti

Tins(n)=On2 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 On2, š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.

Page 78: malbaski – algoritmi i strukture podataka

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

78

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

oznaci G) odabere prvi element podniza, aL.

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 ai=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

i

M

R L,M

4 1 6 7 3 9 2 G=4

i

R L

4 1 6 7 3 9 2

M

i

R L

4 1 6 7 3 9 2

M

i

R L

4 1 6 7 3 9 2

M

i

R L

4 1 3 7 6 9 2

M

i

R L

4 1 3 7 6 9 2

M

i

R L

4 1 3 2 6 9 7

M R L

2 1 3 4 6 9 7

Slika 2.4.

Page 79: malbaski – algoritmi i strukture podataka

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

79

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 aL,...,aR 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 aM i pivota aL:

tmp=a[M];a[M]=a[L];a[L]=tmp;

U nastavku se primenjuje identičan postupak za oba novoformirana podniza,

aL...aM-1 i aM+1...aR. 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);

}

Page 80: malbaski – algoritmi i strukture podataka

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

80

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) + cn

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)+cn

= (T(n-2)+T(1)+c(n-1))+T(1)+cn

= 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

T(n) = nT(1) + c

2-n

0j

j)-(n = nT(1) + c(n(n-2)-(n-2)(n-1)/2)

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) + cn

= 2(2T(n/4)+cn/2) + cn

= ...

= 2kT(n/2k) + kcn

posle k koraka. U poslednjem koraku je n=2k, odakle

T(n) = nT(1) + cnlog2 n

Kako je T(1) konstanta sledi

Page 81: malbaski – algoritmi i strukture podataka

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

81

T(n) = Onlog2 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) = nTobr

Obrada pojedinačnih elemenata niza najčešće zahteva približno konstantno vreme, u

kojem je slučaju

Ttrav(n) = O[n]

Page 82: malbaski – algoritmi i strukture podataka

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

82

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.

Page 83: malbaski – algoritmi i strukture podataka

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

83

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.

Page 84: malbaski – algoritmi i strukture podataka

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

84

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!

Page 85: malbaski – algoritmi i strukture podataka

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

85

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.

Page 86: malbaski – algoritmi i strukture podataka

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

86

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

Ulin = 1/n

n

1i

i = (n+1)/2 ≈ n/2

što znači da je vremenska kompleksnost algoritma On. 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

Ulin = 1 +

1n

1i

1-n

0i

in

11i

n

1 = 1 + (n-1)/2 = (n+1)/2 ≈ n/2

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

svaki ključ u tabeli, dakle

Page 87: malbaski – algoritmi i strukture podataka

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

87

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

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 On, pokazaće se da je kod binarnog ona

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

Page 88: malbaski – algoritmi i strukture podataka

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

88

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.

binarno traženje

1 4 5 8 10 14 16 17 19

linearno traženje

Slika 3.2.

Page 89: malbaski – algoritmi i strukture podataka

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

89

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

1-h

0k

k2 = 2h-1-1

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

Ubin = (1/n)(e1+2e2+...+(h-1)eh-1+heh)=(1/n)(heh+

1-h

1j

1-jj2 )

= (1/n)(heh+(h-2)2h-1+1)

Uvrštavanjem vrednosti za eh dobijamo

Slika 3.3.

x5

x2

x1 x3

x8

x6 x10

x0 x4 x7 x9 x11

Page 90: malbaski – algoritmi i strukture podataka

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

90

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) = Olog2 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

Page 91: malbaski – algoritmi i strukture podataka

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

91

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

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 nn/2, tj.

vremenska složenost bila bi On2. 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 Olog2 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.

Page 92: malbaski – algoritmi i strukture podataka

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

92

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š

Page 93: malbaski – algoritmi i strukture podataka

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

93

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

Page 94: malbaski – algoritmi i strukture podataka

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

94

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

Page 95: malbaski – algoritmi i strukture podataka

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

95

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.

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.

Slika 3.4.

početak pretrage

Page 96: malbaski – algoritmi i strukture podataka

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

96

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 key0=0. Lokaciju ćemo označiti kao deleted

upisivanjem sloga sa key0=1 i key1=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

Slika 3.5.

početak pretrage

a b c d

a b d

(a)

(b)

Page 97: malbaski – algoritmi i strukture podataka

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

97

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 key0=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;

}

Page 98: malbaski – algoritmi i strukture podataka

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

98

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 key0=0 ili key0=1, odnosno,

jednim izrazom, key01.

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

Page 99: malbaski – algoritmi i strukture podataka

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

99

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

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

Slika 3.6.

j j+1

Page 100: malbaski – algoritmi i strukture podataka

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

100

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)

Page 101: malbaski – algoritmi i strukture podataka

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

101

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

Page 102: malbaski – algoritmi i strukture podataka

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

102

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

spregnute realizacije prikazana je na slici 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 nSIZE. 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

Slika 3.7.

u ovom lancu su slogovi za koje je

h(key)=i

i

Page 103: malbaski – algoritmi i strukture podataka

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

103

Nhash(q) =

1

1-k q)(1kqk

= q1

1

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

Uhash(q) = (1/q)

q

0x

hash (x)dxN = q1

1ln

q

1

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,

Uchain = 1+

1n

0i

in

1

SIZE

1 = 1+

1

1i

iSIZEn

1 n

= 1+SIZE2

1-n

Ako je n>>1, a kako je n/SIZE=q, biće

Uchain ≈ 1+q/2

Page 104: malbaski – algoritmi i strukture podataka

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

104

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.

Page 105: malbaski – algoritmi i strukture podataka

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

105

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.

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

. . .

Slika 4.1.

Slika 4.2.

Page 106: malbaski – algoritmi i strukture podataka

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

106

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

Page 107: malbaski – algoritmi i strukture podataka

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

107

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

Page 108: malbaski – algoritmi i strukture podataka

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

108

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 sCAPACITY; // 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.

Page 109: malbaski – algoritmi i strukture podataka

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

109

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

Page 110: malbaski – algoritmi i strukture podataka

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

110

}

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.

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

s

s

Slika 4.3.

pop

push

t

t

Page 111: malbaski – algoritmi i strukture podataka

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

111

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

A B 3 * + 5 H - /

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!

Page 112: malbaski – algoritmi i strukture podataka

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

112

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:

Page 113: malbaski – algoritmi i strukture podataka

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

113

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*/

Page 114: malbaski – algoritmi i strukture podataka

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

114

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.

Page 115: malbaski – algoritmi i strukture podataka

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

115

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.

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

Slika 4.5.

P0 P1 P2 P3

A0 A1 A2

A0

A1

A2

A0

A1

A0

Slika 4.4.

pozivajuća rutina potprogram

Ap

Ap

Page 116: malbaski – algoritmi i strukture podataka

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

116

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.

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.

Slika 4.6.

lokalne promenljive, argumenti, adresa povratka, prethodni bp

bp

sp

stek frejm

ka nižim adresama

Page 117: malbaski – algoritmi i strukture podataka

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

117

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

Page 118: malbaski – algoritmi i strukture podataka

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

118

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! = 10! = 1

2! = 21! = 2

3! = 32! = 6

4! = 43! = 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

Page 119: malbaski – algoritmi i strukture podataka

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

119

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

1i

i ako je n>0

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

Page 120: malbaski – algoritmi i strukture podataka

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

120

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

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

Page 121: malbaski – algoritmi i strukture podataka

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

121

= 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

T(n) ≈ n618.15

1

odakle sledi da je T(100) ≈3.51020. 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 On.

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

Page 122: malbaski – algoritmi i strukture podataka

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

122

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

Page 123: malbaski – algoritmi i strukture podataka

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

123

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

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

prateća rekurzija

Page 124: malbaski – algoritmi i strukture podataka

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

124

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

Page 125: malbaski – algoritmi i strukture podataka

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

125

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

Page 126: malbaski – algoritmi i strukture podataka

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

126

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

Page 127: malbaski – algoritmi i strukture podataka

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

127

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.

Slika 4.8.

0

x

1

y

2 c-1

. . .

getPos putPos

0

x

1

y

2 c-1

. . .

getPos putPos

z

Page 128: malbaski – algoritmi i strukture podataka

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

128

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

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.

Slika 4.9.

getPos putPos pun red prazan red

cirkularni red

Slika 4.10.

pun red prazan red

Page 129: malbaski – algoritmi i strukture podataka

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

129

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;

Page 130: malbaski – algoritmi i strukture podataka

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

130

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

Page 131: malbaski – algoritmi i strukture podataka

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

131

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.

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.

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

Slika 4.12.

. . . last

first

Slika 4.11.

sadržaj

elementa pokazivač na sledeći

Page 132: malbaski – algoritmi i strukture podataka

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

132

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

Page 133: malbaski – algoritmi i strukture podataka

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

133

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

je na slici 4.13.

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.

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

Slika 4.13.

. . . last

first

n newNode

Slika 4.12.

. . . last

first

n

Page 134: malbaski – algoritmi i strukture podataka

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

134

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;

}

Slika 4.14.

. . . last

first

n pos tmp

Page 135: malbaski – algoritmi i strukture podataka

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

135

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.

Č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

Slika 4.15.

. . . last

first

n p>p0 p=p0 p<p0

Page 136: malbaski – algoritmi i strukture podataka

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

136

č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

Page 137: malbaski – algoritmi i strukture podataka

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

137

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:

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

Slika 4.16.

. . .

. . .

Page 138: malbaski – algoritmi i strukture podataka

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

138

dodavanje s leva

pristup krajnjem desnom elementu

uklanjanje krajnjeg desnog elementa

dodavanje s desna

što je prikazano na slici 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.

Operacije su nešto sporije nego kod reda, jer treba ažurirati dva pokazivača, a ne

jedan. Inače, odvijaju se sasvim analogno.

Slika 4.18.

. . . right

left

n

Slika 4.17.

. . .

Page 139: malbaski – algoritmi i strukture podataka

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

139

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.

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

Slika 4.19.

. . .

tekući element

Page 140: malbaski – algoritmi i strukture podataka

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

140

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

Page 141: malbaski – algoritmi i strukture podataka

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

141

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.

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

Slika 4.20.

Page 142: malbaski – algoritmi i strukture podataka

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

142

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

Page 143: malbaski – algoritmi i strukture podataka

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

143

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.

Page 144: malbaski – algoritmi i strukture podataka

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

144

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:

Slika 4.21

file

sort

sort

sort

sort

F$0

F$1

F$2

faza disperzije

F$0

F$1

F$2

merge file faza mešanja

Page 145: malbaski – algoritmi i strukture podataka

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

145

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

Page 146: malbaski – algoritmi i strukture podataka

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

146

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

Page 147: malbaski – algoritmi i strukture podataka

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

147

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 isActivei jednak je 1 ako u odgovarajućoj segmentnoj datoteci F$i

ima još elemenata koji nisu prepisani u izlaznu datoteku. Ako je isActivei 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

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

Page 148: malbaski – algoritmi i strukture podataka

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

148

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

Page 149: malbaski – algoritmi i strukture podataka

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

149

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.

Page 150: malbaski – algoritmi i strukture podataka

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

150

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.

Lista je prikazana na slici 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;

Slika 5.2.

. . . first

n

Slika 5.1.

item next

Page 151: malbaski – algoritmi i strukture podataka

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

151

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

Page 152: malbaski – algoritmi i strukture podataka

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

152

//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) {

Page 153: malbaski – algoritmi i strukture podataka

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

153

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

U =

n

1i

i/n = (n+1)/2 ≈ n/2

Dakle, vremenska složenost je On, dok je kod niza ona konstantna i iznosi O1.

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:

T removeItem(SimpleList* lst, int position) {

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

Slika 5.3.

. . . first

n

prev curr

Page 154: malbaski – algoritmi i strukture podataka

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

154

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.

Funkcija za dodavanje izgleda ovako:

void putItem(SimpleList* lst, T item) {

Node *newNode;

newNode=malloc(sizeof(Node));

Slika 5.4.

. . . first

n

newNode

Page 155: malbaski – algoritmi i strukture podataka

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

155

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.

Page 156: malbaski – algoritmi i strukture podataka

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

156

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

Page 157: malbaski – algoritmi i strukture podataka

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

157

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;

Page 158: malbaski – algoritmi i strukture podataka

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

158

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

Page 159: malbaski – algoritmi i strukture podataka

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

159

}

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.

Page 160: malbaski – algoritmi i strukture podataka

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

160

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.

Page 161: malbaski – algoritmi i strukture podataka

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

161

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

Slika 5.5.

A B . . . first

n

NE!

A B . . . first

n

DA

Page 162: malbaski – algoritmi i strukture podataka

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

162

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.

Slika 5.6.

. . .

Page 163: malbaski – algoritmi i strukture podataka

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

163

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.

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.

Slika 5.8.

. . . leftSentinel

n

rightSentinel

current

*

*

backupCurrent

Slika 5.7.

item right left

Page 164: malbaski – algoritmi i strukture podataka

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

164

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

Page 165: malbaski – algoritmi i strukture podataka

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

165

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

Page 166: malbaski – algoritmi i strukture podataka

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

166

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

leftSentinel

0

rightSentinel

current

*

*

-

Slika 5.9.

n backupCurrent

Page 167: malbaski – algoritmi i strukture podataka

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

167

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;

}

Page 168: malbaski – algoritmi i strukture podataka

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

168

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.

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;

Slika 5.10.

current

Page 169: malbaski – algoritmi i strukture podataka

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

169

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.

Slika 5.11.

current

Page 170: malbaski – algoritmi i strukture podataka

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

170

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

Page 171: malbaski – algoritmi i strukture podataka

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

171

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

}

Page 172: malbaski – algoritmi i strukture podataka

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

172

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.

Page 173: malbaski – algoritmi i strukture podataka

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

173

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 position0, 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 Tchar.

void showDoubleList(DoubleList* lst) {

char item;

printf("\nSADRZAJ LISTE: ");

if(isEmpty(lst)) printf("Prazna. Size: %d",size(lst));

else {

Page 174: malbaski – algoritmi i strukture podataka

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

174

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

Page 175: malbaski – algoritmi i strukture podataka

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

175

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

Page 176: malbaski – algoritmi i strukture podataka

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

176

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

Page 177: malbaski – algoritmi i strukture podataka

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

177

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

Page 178: malbaski – algoritmi i strukture podataka

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

178

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

Page 179: malbaski – algoritmi i strukture podataka

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

179

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.

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

Slika 5.12.

Page 180: malbaski – algoritmi i strukture podataka

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

180

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), siS(ML), i=1,...,n, čini jednostruko ili dvostruko spregnutu

listu. Uslov siS(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

Page 181: malbaski – algoritmi i strukture podataka

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

181

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

Page 182: malbaski – algoritmi i strukture podataka

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

182

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.

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

a b

c d

e

f

g

Slika 6.1.

Page 183: malbaski – algoritmi i strukture podataka

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

183

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.

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

a

c b

d f g e

Slika 6.2.

i j k l h

Page 184: malbaski – algoritmi i strukture podataka

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

184

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.

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.

Slika 6.3.

Page 185: malbaski – algoritmi i strukture podataka

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

185

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

Page 186: malbaski – algoritmi i strukture podataka

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

186

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

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

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

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

Slika 6.7.

My Files

Pictures Projects Music Reports

Classical Rock Pascal CPP Java

Slika 6.6.

Atena

Zevs

Kron Rea

Slika 6.5.

Apolon

Leta Zevs

Febe Kejo Kron Rea

Page 187: malbaski – algoritmi i strukture podataka

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

187

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.

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.

Slika 6.8.

X

A B

X

B A

X

A B

Page 188: malbaski – algoritmi i strukture podataka

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

188

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.

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

ne

ne

da

da

Slika 6.9.

ekoren

e= element nije pronađen

k(e)=k0 element je pronađen

esub(k0,e)

Page 189: malbaski – algoritmi i strukture podataka

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

189

ponašanje algoritma. Očigledno, broj poređenja pri traženju ne može biti veći od

visine stabla h. Kako je

N =

h

1i

1-in = (nh-1)/(n-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.

Slika 6.10.

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

Page 190: malbaski – algoritmi i strukture podataka

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

190

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

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

Slika 6.11.

A

B C D

E F G H

A

C * * * B * D *

Slika 6.12.

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

deskriptor

Page 191: malbaski – algoritmi i strukture podataka

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

191

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.

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.

Slika 6.13.

A

B C

D

F

E

G

A 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

Page 192: malbaski – algoritmi i strukture podataka

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

192

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.

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

podstablu ima i (i0) 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

U(N) =

1-N

0i

(i) /NU(N)

tj.

i n-i-1

Slika 6.14.

Page 193: malbaski – algoritmi i strukture podataka

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

193

U(N) =

1

02

1)]-i-1)U(N-i-(NiU(i)N[N

1 N

i

iz čega, posle jednostavnih transformacija, dobijamo

U(N) = 1 +

1

12

jU(j)N

2 N

j

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

U(N) = 2N

1((N2-1)U(N-1)+2N-1)

Ako sa Hn označimo sumu

Hm = 1 + 1/2 + 1/3 + 1/4 +...+1/m

gornja formula razvija se u

U(N) = 2N

1N HN - 3

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

Page 194: malbaski – algoritmi i strukture podataka

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

194

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.

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

Slika 6.15.

A

B

D

G H

C

E F

I

Page 195: malbaski – algoritmi i strukture podataka

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

195

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;

Page 196: malbaski – algoritmi i strukture podataka

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

196

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

}

Page 197: malbaski – algoritmi i strukture podataka

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

197

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

G D H B A E C I F

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;

}

Page 198: malbaski – algoritmi i strukture podataka

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

198

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.

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

Slika 6.16.

A 0

B 1 C 0

D 0 E * 1 F 0 *

G * 1 H * 1 I * 1

statusno polje

prošivka pokazivač

Page 199: malbaski – algoritmi i strukture podataka

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

199

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.

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

Slika 6.17.

A 0

B 1 C 0

D 0 E 1 F 0 *

G * 1 H 1 I 1 desna prošivka

0

0

0

0

1

1

0

1 leva prošivka

Page 200: malbaski – algoritmi i strukture podataka

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

200

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

Page 201: malbaski – algoritmi i strukture podataka

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

201

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

Page 202: malbaski – algoritmi i strukture podataka

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

202

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.

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)

50

80

90

95

60

55 67

62 75

70

30

40 20

10 35

Slika 6.18.

Page 203: malbaski – algoritmi i strukture podataka

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

203

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

podstablu.

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.

ne

ne

da

da

Slika 6.19.

ekoren

k(e)= element nije pronađen

k(e)=k0 element je pronađen

k0:k(e)

eright(e) eleft(e)

> <

Page 204: malbaski – algoritmi i strukture podataka

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

204

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 ON, dok je u

najboljem slučaju balansiranog stabla najviše Olog2N. U prosečnom slučaju

vremenska kompleksnost je takođe Olog2N.

Page 205: malbaski – algoritmi i strukture podataka

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

205

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.

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

50

80

90

95

60

55 67

62 75

70

30

40 20

10 35

Slika 6.21.

47

15

dodati elementi

50

80

90

95

60

55 67

62 75

70

30

40 20

10 35

Slika 6.20.

40

100

55

Page 206: malbaski – algoritmi i strukture podataka

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

206

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

Page 207: malbaski – algoritmi i strukture podataka

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

207

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.

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

Slika 6.22.

nadređeni

uklanja se

Page 208: malbaski – algoritmi i strukture podataka

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

208

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

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.

Slika 6.23.

20

10 * *

* *

30

40

35 * *

*

30

uklanja se

uklanja se

Page 209: malbaski – algoritmi i strukture podataka

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

209

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

75

Slika 6.24.

80

75 *

50

briše se

60

55 * * 67

62 * *

70 * *

Page 210: malbaski – algoritmi i strukture podataka

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

210

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

Page 211: malbaski – algoritmi i strukture podataka

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

211

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

}

}

Page 212: malbaski – algoritmi i strukture podataka

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

212

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

Page 213: malbaski – algoritmi i strukture podataka

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

213

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

Page 214: malbaski – algoritmi i strukture podataka

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

214

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

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

Page 215: malbaski – algoritmi i strukture podataka

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

215

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;

}

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,

Slika 6.25.

x

y z

A B C D

z

y

A B

C

D

x

rezultat: z

rotL(x)

Page 216: malbaski – algoritmi i strukture podataka

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

216

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.

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

Slika 6.26.

x

y z

A B C D

y

z A

B

C D

x

rezultat: y

rotR(x)

Page 217: malbaski – algoritmi i strukture podataka

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

217

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

Page 218: malbaski – algoritmi i strukture podataka

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

218

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;

Page 219: malbaski – algoritmi i strukture podataka

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

219

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)

Page 220: malbaski – algoritmi i strukture podataka

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

220

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

Slika 6.27c

z

y t1leftRotate(z)

t2

t3 t4

x

desni-desni

y

z x

t1 t4 t3 t2

Slika 6.27b

z

y t4leftRotate(y)

t1

t3 t2

x

levi-desni

x

y z

t1 t4 t3 t2

z

x t4

y

t2 t1

t3

rightRotate(z)

Slika 6.27a

z

y t4rightRotate(z)

x

t1 t2

t3

levi-levi

y

x z

t1 t4 t3 t2

Page 221: malbaski – algoritmi i strukture podataka

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

221

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 {

Slika 6.27d

z

y

t4rightRotate(y)

t1

t3 t2

x

desni-levi

x

z y

t1 t4 t3 t2

z

x t1

t2

t4 t3

y

leftRotate(z)

Page 222: malbaski – algoritmi i strukture podataka

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

222

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

}

Page 223: malbaski – algoritmi i strukture podataka

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

223

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!

Page 224: malbaski – algoritmi i strukture podataka

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

224

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;

}

Page 225: malbaski – algoritmi i strukture podataka

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

225

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

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

Page 226: malbaski – algoritmi i strukture podataka

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

226

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;

Page 227: malbaski – algoritmi i strukture podataka

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

227

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.

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

Slika 6.28

S

E

C R

H

G

X

S

E

C R

G

H

X

S

E

C G

R

H

X

S

G

E R

H

X

C

G

E

C R

H

S

X

Page 228: malbaski – algoritmi i strukture podataka

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

228

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.

Page 229: malbaski – algoritmi i strukture podataka

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

229

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

Page 230: malbaski – algoritmi i strukture podataka

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

230

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

Page 231: malbaski – algoritmi i strukture podataka

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

231

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 čvorova

visina srednji 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

Page 232: malbaski – algoritmi i strukture podataka

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

232

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;

}

b a

Slika 6.29

L

R

R

L

Page 233: malbaski – algoritmi i strukture podataka

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

233

}

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.

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;

Slika 6.30

Page 234: malbaski – algoritmi i strukture podataka

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

234

} 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

Page 235: malbaski – algoritmi i strukture podataka

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

235

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.

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

A

Slika 6.31

C C

G C E

B

F

C

C

D

H X

Page 236: malbaski – algoritmi i strukture podataka

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

236

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.

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.

Slika 6.32.

informacioni sadržaj

pokazivač na sledeći u listi

pokazivač na početak liste podređenih

Page 237: malbaski – algoritmi i strukture podataka

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

237

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.

deskriptor

A

Slika 6.33

koren *

B C * D *

E * F * G * * H * *

tekući

Page 238: malbaski – algoritmi i strukture podataka

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

238

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