Professional Documents
Culture Documents
#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+
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ć
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
Predgovor.................................................................................................................. 3
Sadržaj ...................................................................................................................... 4
DEO I: OSNOVI TEORIJE I ANALIZE ALGORITAMA
1. Uvod ..................................................................................................................... 8
1.1 Empirijska definicija algoritma ........................................................................ 8
1.2 Sintetička definicija algoritma.........................................................................11
2. Algoritamski sistemi.............................................................................................15
2.1. Tjuringove mašine .........................................................................................15
2.2. Normalni algoritmi Markova .........................................................................23
3. Analiza algoritamskih struktura ............................................................................28
3.1. Graf toka programa........................................................................................30
3.2. Strukturna teorema.........................................................................................34
3.2.1. Strukturna teorema i algoritamski sistemi................................................38
3.3. Metoda sukcesivne dekompozicije .................................................................38
3.4. Složenost (kompleksnost) algoritama.............................................................41
DEO II: STRUKTURE PODATAKA
1 Osnovni pojmovi ...................................................................................................49
1.1. Definicija strukture podataka .........................................................................55
1.2. Operacije nad strukturama podataka ..............................................................57
1.3 Klasifikacija struktura podataka ......................................................................60
2. Niz .......................................................................................................................65
2.1. Fizička realizacija niza...................................................................................67
2.1.1. Fizička realizacija statičkog niza .............................................................67
2.1.2. Metoda linearizacije................................................................................68
2.1.3. Fizička realizacija dinamičkog niza.........................................................69
2.2.Sortiranje niza.................................................................................................71
2.2.1. Metoda izbora (Selection Sort)................................................................72
2.2.2. Metoda izmene (Standard Exchange Sort, Bubble Sort) ..........................73
2.2.3. Metoda umetanja (Insertion Sort) ............................................................75
2.2.4. Quicksort ................................................................................................77
2.3. Redosledna obrada niza .................................................................................81
3. Slog i tabela..........................................................................................................82
3.1. Slog ...............................................................................................................82
3.2 Tabela.............................................................................................................85
3.2.1. Linearno traženje ....................................................................................86
3.2.2. Binarno traženje......................................................................................87
3.2.3. Redosledna obrada tabele........................................................................91
3.3. Rasuta ili heš tabela .......................................................................................91
3.3.1. Fizička realizacija heš tabele ...................................................................92
3.3.2. Kolizija ključeva i rukovanje sinonimima................................................94
3.3.3. Analiza otvorenog adresiranja ...............................................................102
3.3.4. Analiza lančanja....................................................................................103
4. Poludinamičke strukture .....................................................................................104
4.1. Stek .............................................................................................................104
4.1.1. Fizička realizacija steka ........................................................................107
4.1.2. Prevođenje izraza ..................................................................................110
4.1.3. Programski stek.....................................................................................114
4.1.4. Rekurzija ..............................................................................................117
4.2. Red..............................................................................................................124
4.2.1. Sekvencijalna realizacija reda ...............................................................127
4.2.2. Spregnuta realizacija reda .....................................................................130
4.2.3. Red sa prioritetima................................................................................135
4.2.4. Ukratko o primenama reda ....................................................................137
4.3. Dek..............................................................................................................137
4.4. Sekvenca .....................................................................................................139
4.4.1. Fizička realizacija sekvence ..................................................................140
4.4.2. Sortiranje datoteke ................................................................................142
5. Liste ...................................................................................................................148
5.1. Jednostruko spregnuta lista ..........................................................................148
5.1.1. Jednostavna lista ...................................................................................150
5.1.2. Jednostruko spregnuta lista sa ključevima .............................................156
5.1.3. Sortiranje jednostruko spregnute liste....................................................160
5.2.Dvostruko spregnuta lista..............................................................................161
5.2.1. Dvostruko spregnuta lista sa navigacijom..............................................163
5.2.2. Obrada dvostruko spregnute liste ..........................................................173
5.2.3. Demonstracioni program.......................................................................174
5.3. Multilista .....................................................................................................179
6. Stablo .................................................................................................................182
6.1. Orijentisano stablo .......................................................................................182
6.2. Stablo kao struktura podataka ......................................................................185
6.3. N-arno stablo ...............................................................................................187
6.3.1. Fizička realizacija n-arnog stabla ..........................................................189
6.3.2. Binarno stablo.......................................................................................191
6.3.3. Binarno stablo pristupa..........................................................................201
6.3.4. Balansiranje binarnog stabla..................................................................212
6.3.5. AVL stablo ...........................................................................................216
6.3.6. Stohastičko stablo .................................................................................225
6.4. Uopšteno (generalisano) stablo ....................................................................234
6.4.1. Fizička realizacija uopštenog stabla.......................................................236
Literatura................................................................................................................238
Dušan Malbaški - Algoritmi i strukture podataka
7
Dušan T. Malbaški - Algoritmi i strukture podataka
1. UVOD
Definisati pojmove nikad nije lako. Što je pojam bliži fundamentalnom,
definisati ga sve je teže i teže. Šta je skup? A broj? Tačka? Lepota? Pravda? O
definiciji vremena, sv. Avgustin, hrišćanski teolog i filozof, piše:
"Si nemo ex me quaerat, scio; si quaerenti explicare velim, nescio". (Ako me
niko ne pita, znam; ako hoću da objasnim nekom ko pita - ne znam).
Jedan od najosnovnijih pojmova u računarstvu jeste pojam algoritma, do te
mere fundamentalan da bi, možda, najbolji odgovor na pitanje "šta je to?" bio "zna
se!". Zato ne treba da čudi to što neki veoma istaknuti teoretičari, kao što je Uspenski,
smatraju da algoritme ni ne treba definisati dovođenjem u vezu sa drugim jezičkim
simbolima, nego ih valja smatrati polaznim1, i da je svaki takav pokušaj definisanja, u
stvari, samo opisivanje. Drugi pak stoje na stanovištu da algoritam ipak treba
definisati, ali samo u logičkom, a ne u formalno-matematičkom smislu. U svakom
slučaju, sam pojam algoritma je toliko bazičan da stoji tvrdnja da je "najveće otkriće u
oblasti nauke o algoritmima otkriće samog algoritma".
1
u opštoj logici takvi pojmovi nose naziv kategorije
8
Dušan T. Malbaški - Algoritmi i strukture podataka
algoritmisti koji nisu koristili posebna pomagala. Posle dosta lutanja (detalji se mogu
naći u 15 i drugde) istoričari matematike su konačno otkrili da je reč o imenu
arapskog matematičara iz IX veka - jednog od najvećih matematičara svih vremena! -
Abu Ja'far Mohammed ibn Musa al-Khowarizmi - ja (u prevodu Džafarov otac,
Mohamed, sin Musin, iz Horezma2). Transkripcija dela imena al-Khowarizmi (čita se
"Alhorezmi"), u "algoritam" nameće se sama. Navedimo nekoliko primera algoritama:
A1. Razvući šestar na dužinu zadate duži. Iz jedne krajnje tačke duži opisati luk.
Ponoviti isto za drugu krajnju tačku, tako da se lukovi seku. Spojiti lenjirom krajnje
tačke duži sa presečnom tačkom lukova.
A2. Sabrati x i y. Zbir podeliti sa z.
A3. (Određivanje S = a1+ ... +an)
1. Postaviti vrednost S na nulu i preći na sledeći korak.
2. Postaviti vrednost i na 1 i preći na sledeći korak.
3. Dodati ai na tekuću vrednost S i preći na sledeći korak.
4. Povećati i za 1 i preći na sledeći korak.
5. Ako je i manje ili jednako n vratiti se na korak 3; u suprotnom završiti.
Algoritam je, međutim, i ovo:
A4.
1. Ubaciti monetu od 1 eur u odgovarajući prorez
2. Sačekati da se upali zeleno svetlo
3. Ako se želi crna kafa pritisnuti dugme A; ako se želi bela kafa pritisnuti dugme
B.
Kako se vidi, primeri su dosta različiti, ali ipak imaju određene zajedničke osobine.
Prvo, svugde se susreću neke akcije (dejstva, operacije) i to u strogo zadatom
redosledu. Dalje, ta sekvenca dejstava je svrsishodna, tj. vodi ka nekakvom cilju.
Dejstva, složena u algoritamske korake, diskretna su i izvode se konačan broj puta,
sve do dobijanja rezultata. I još, svi algoritmi imaju neke ulazne podatke: da bi se
konstruisao jednakostranični trougao /A1/ mora se poznavati dužina stranice; da bi se
odredila vrednost (x+y)/z /A2/ neophodno je poznavati vrednosti x, y i z; u cilju
izvršavanja algoritma A3 mora se poći od zadavanja vrednosti n i a1, ..., an. Čak i
algoritam A4 polazi od nekih podataka, ali se oni ne zadaju spolja nego su ugrađeni u
2
Horezm je bila veoma napredna država u donjem toku Amu-Darje. Uništile su je, kao i mnogo toga,
primitivne horde Džingis-kana.
9
Dušan T. Malbaški - Algoritmi i strukture podataka
njegov opis. Sve u svemu, može se kao prvo određenje pojma algoritma prihvatiti da
je to pravilo koje objašnjava proces transformacije ulaznih podataka u traženi rezultat.
Pojam algoritma, očigledno, sasvim je blizak pojmovima recept, postupak ili
procedura. Dakle, svaki algoritam je pravilo, ali obrnuto ne mora da važi: saobraćajna
pravila nisu algoritmi, kao ni pravilo "čast svakom-veresija nikom!".
Činjenica da se algoritam sastoji od dejstava i da vodi ka rezultatu neposredno
ukazuje na potrebu postojanja izvršioca, i to takvog koji može mehanički, samo na
osnovu opisa algoritma, a bez uplitanja korisnika (onog kome je rezultat potreban),
bez ikakve kreativnosti, da produkuje traženi rezultat. To, pak, zahteva da postoji
način komunikacije, tj. jezik, na kojem se algoritam zadaje, a čija je glavna
karakteristika ta da ga izvršilac razume i to u potpunosti. Taj jezik, nazvan jezikom
algoritma, nema neku unapred zadatu, kanoničku formu: algoritam se može zadati u
vidu uređenog skupa rečenica (svi navedeni primeri su takvi), ali i kao poznati blok
dijagram, kao sekvenca iskaza na formalnom jeziku, programskom ili
kvaziprogramskom, kao kolekcija crteža (kod geometrijskih konstrukcija na primer),
kao i na druge načine; jedino na čemu se insistira je to da izvršilac bude u stanju da,
na osnovu opisa i samo na osnovu njega, proizvede rezultat. Vratimo se, za trenutak,
na ulazne podatke. Jasno je da ne može svaki podatak da igra ulogu ulaznog podatka u
ma koji algoritam: zato govorimo o dozvoljenim i nedozvoljenim ulaznim podacima.
Dozvoljeni ulazni podaci su oni koje algoritam može uopšte da interpretira kao takve:
dozvoljeni ulazni podaci za A2 mogu biti bilo koje trojke realnih brojeva (uključujući
čak i z=0, kada se kaže da je na dotičnu trojku algoritam neprimenljiv, iako je ona
dozvoljena). Nedozvoljeni ulazni podaci obuhvataju vrednosti koje algoritam u
principu ne može da prihvati kao ulaz; za isti algoritam to bi mogla da bude, recimo,
trojka (zeleno, crveno, plavo). Imajući sve ovo u vidu u mogućnosti smo da
empirijsku definiciju algoritma (često korišćen naziv je intuitivna definicija)
preciziramo na sledeći način:
Algoritam je pravilo, formulisano na nekom jeziku, koje jednoznačno definiše
redosled operacija neophodan za transformaciju dozvoljenih ulaznih podataka
u traženi rezultat.
Napominjemo da metod kojim je dobijena gornja definicija nosi naziv "analitički".
Odlikuje se time što se termin ("jezički simbol") što se definiše dovodi u vezu sa
grupom drugih jezičkih simbola koji se smatraju poznatim i koji izražavaju njegove
bitne odlike. Tako se dolazi do strukture definicije: novi termin (algoritam) - šira
10
Dušan T. Malbaški - Algoritmi i strukture podataka
3
Kvadrat je-pravougaonik-čije su sve četiri stranice jednake dužine. Klika je-regularan graf-sa n
čvorova stepena n-1
4
Empirijski (leksički) način je u suštini induktivan
5
razlika između „običnog“ simbola i leksičkog simbola (tzv. tokena) je u tome što token može da se
sastoji od iše pojedinačnih simbola, npr „pqr“, pri čemu se smatra da je token nerazloživ, tj. atomaran
11
Dušan T. Malbaški - Algoritmi i strukture podataka
6
Ali i "Hajdn i Bečki klasici su Mocart". Ne gubimo iz vida da se radi o čisto sintaksnim pojmovima
7
Umesto izraza "zakon" koristi se i "pravilo" koji smo izbegli da ne bi došlo do konfuzije jer se
poslednji sreće u empirijskoj definiciji algoritma i nema identično značenje
8
Takav pristup, slaganje intuitivnih pojmova u uređene n-torke, dosta je uobičajen u računarskim
naukama i služi svrsi - razumljivosti - ali samo kada se ne zloupotrebljava
12
Dušan T. Malbaški - Algoritmi i strukture podataka
13
Dušan T. Malbaški - Algoritmi i strukture podataka
9
treći algoritamski sistem su rekurzivne funkcije kojima se nećemo baviti
14
Dušan T. Malbaški - Algoritmi i strukture podataka
2. ALGORITAMSKI SISTEMI
Sintetičkom definicijom algoritma uspevaju se formalizovati pojmovi “ulaznih
podataka” i “traženog rezultata” korišćeni u empririjskoj definiciji. Takođe,
formalizuje se i veza između njih, koja treba da bude ostvarena izvršavanjem
algoritma. Ova veza govori o tome kakav izlaz (tj. koju reč ili skup reči iz izlaznog
alfabeta) produkuje izvršenje algoritma nad zadatom rečju ulaznog alfabeta. Nažalost,
sintetička definicija nema formalizovan odgovor na pitanje kako se implementira
alfabetski operator, tj. kako izgleda postupak transformacije ulazne reči u izlaznu.
Traženi odgovor pružaju algoritamski sistemi. Obradićemo dva od njih: Tjuringovu
mašinu i Markovljeve normalne algoritme.
15
Dušan T. Malbaški - Algoritmi i strukture podataka
samo dva osnovna tipa: tzv. specijalnu Tjuringovu mašinu (sa jednom trakom) i
univerzalnu Tjuringovu mašinu (ali bez ulaženja u detalje, u drugom slučaju).
Specijalna Tjuringova mašina (u daljem tekstu Tjuringova mašina) sastoji se
od tri funkcionalne celine: beskonačne trake, upisno-čitajuće glave i upravljačkog
bloka, slika 2.1.
upravljački blok
upisno-čitajuća glava
beskonačna traka
Slika 2.1.
16
Dušan T. Malbaški - Algoritmi i strukture podataka
10
postoje varijeteti kod kojih upisno-čitajuća glava može i da se ne pomeri
17
Dušan T. Malbaški - Algoritmi i strukture podataka
početka (krajnjeg levog slova) reči . Očigledno, konfiguracija u celosti opisuje stanje
čitave Tjuringove mašine.
Neka se u nekom trenutku na traci nalazi niz slova (reč) a1a2 ... an. Neka je,
dalje, upravljački blok u stanju q, a upisno-čitajuća glava iznad slova ai, što znači da
je tekuća konfiguracija (q, a1, a2, ..., an, i). Tada
(q, ai) = (p, x, R)
znači "promeniti stanje u p, umesto ai upisati na traku x i pomeriti upisno-čitajuću
glavu za jedno mesto udesno". Po izvršenju, nova konfiguracija je
(p, a1, ... ai-1xai+1 ... an, i+1).
Rad Tjuringove mašine prikazaćemo na kvazi-programskom jeziku zasnovanom na
sintaksi programskog jezika C. Beskonačnu traku ćemo predstaviti beskonačnim
nizom cell čiji su indeksi celi brojevi. Na početku rada upisno-čitajuća glava nalazi se
iznad pozicije sa indeksom 0, gde je i prvi simbol ulazne reči. Neka je ceo broj i
tekuća pozicija upisno-čitajuće glave, tako da je na početku rada i=0. Označimo sa
cell[i] simbol koji se nalazi na poziciji i. Neka važi L,R. Rad Tjuringove
mašine može se predstaviti sledećim ciklusom:
q = q0; i = 0;
while(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
18
Dušan T. Malbaški - Algoritmi i strukture podataka
skupa F11? Odgovor na pitanje je negativan! Dakle, halting problem kod Tjuringove
mašine spada u klasu neodlučivih problema.
Na kraju, jedna napomena vezana za skup ulaznih slova . Svrha njegovog
izdvajanja iz je nominacija dozvoljenih ulaznih simbola. Ako je, na primer, = 0,
1 tada na početku rada traka može da sadrži samo reči iz skupa *, odnosno nizove
nula i jedinica.
Primer.
Neka je data Tjuringova mašina T = (K, , , , q0, F) gde je
K = s1, s2, s3
= 0, 1, , B ( je prazno slovo!)
= 0, 1
q0 = s1
F = s3
i gde je prelazna funkcija zadata sledećom funkcionalnom shemom:
0 1 B
s1 s20R s21R - -
s2 s21R s20R - s3R
s3 - - - -
11
te, tako, produkovati rezultat
19
Dušan T. Malbaški - Algoritmi i strukture podataka
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).
20
Dušan T. Malbaški - Algoritmi i strukture podataka
12
Bolje reći računar se ponaša kao univerzalna Tjuringova mašina jer je ovo drugo i hronološki i
pojmovno stariji koncept
21
Dušan T. Malbaški - Algoritmi i strukture podataka
ak i B i neka je skup stanja K = q1, ..., qm. Neka su L i R oznake za pomeraj. Kodne
grupe se definišu kao nizovi različitog broja nula omeđeni dvema jedinicama prema
sledećem rasporedu:
R 101
L 1001
B 10001
a1 100001 - 4 nule
a2 10000001 - 6 nula
.
.
.
ak 10...01 - 2(k+1) nula
q1 1000001 - 5 nula
q2 100000001 - 7 nula
.
.
.
qm 10...01 - 2(m+1)+1 nula
22
Dušan T. Malbaški - Algoritmi i strukture podataka
Slika 2.2.
23
Dušan T. Malbaški - Algoritmi i strukture podataka
13
Inače, postoji i uopšteni normalni algoritam Markova koji ne podrazumeva nikakav poseban
redosled smena, te zato dozvoljava definisanje nedetermističkih algoritama koji nadilaze nivo ovog
teksta.
24
Dušan T. Malbaški - Algoritmi i strukture podataka
značenje "transformisati reč X primenom smene i". Konačno, predikat K(i) ima
vrednost ako je i završna smena i u suprotnom. Reč na koju se primenjuje
algoritam označena je sa X.
T
1
T
1
T
1
1
Slika 2.3.
Sa dijagrama se vidi da se, pre svega, smene vrše u strogo određenom redosledu, a
zatim i da postupak može da se završi na dva načina: ili se nailazi na neku od završnih
smena, ili se pak prolazi kroz svih n pokušaja, a da se ne izvrši ni jedna smena. Osim
navedenog postoji i jednostavniji prikaz normalnog algoritma: smene se urede u
sekvencu i primenjuju cirkularno od prve do poslednje; zatim se opet prelazi na prvu i
tako dalje sve dok se ne naiđe na završnu smenu ili se prođe cela sekvenca bez
primene ijedne od njih (videti primer).
Značaj normalnih algoritama je njihova univerzalnost koja proističe iz tzv.
principa normalizacije koji glasi
Za svaki algoritam može se konstruisati funkcionalno ekvivalentan normalni
algoritam
koji je nedokaziv zbog neodređenosti pojma "svaki algoritam". Neka je zadat alfabet
A i normalni algoritam N. Tada se za N kaže da je normalni algoritam u alfabetu A ako
prihvata ulaz iz skupa slova A*, daje izlaz iz istog skupa i u svim smenama sadrži
samo slova iz tog alfabeta. Postoji, međutim, niz slučajeva u kojima nije moguće
25
Dušan T. Malbaški - Algoritmi i strukture podataka
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
26
Dušan T. Malbaški - Algoritmi i strukture podataka
27
Dušan T. Malbaški - Algoritmi i strukture podataka
14
u naučnim krugovima koristi se manje pežorativan naziv “kompozitno programiranje”
28
Dušan T. Malbaški - Algoritmi i strukture podataka
15
takve programe pežorativno zovu "špageti kod".
29
Dušan T. Malbaški - Algoritmi i strukture podataka
Slika 3.1.
2. Predikat je čvor koji odgovara algoritamskom koraku logičkog testa, tj. realizuje
grananje diktirano rezultatom izračunavanja vrednosti odgovarajuće logičke funkcije.
Ima ulazni stepen 1, a izlazni stepen 2 (slika 3.2). U simbol se unosi oznaka logičke
funkcije.
16
grane su prikazane strelicama
30
Dušan T. Malbaški - Algoritmi i strukture podataka
Slika 3.2.
3. Kolektor je pomoćni čvor koji ne izvodi nikakvu obradu nego služi samo za
povezivanje dve grane koje se stiču na istom mestu. Ima ulazni stepen 2, a izlazni
stepen 1 (slika 3.3).
Slika 3.3.
vmax=v[0]
i=0
i<n
T
T
i++ v[i]>vmax
vmax=v[i]
Slika 3.4.
31
Dušan T. Malbaški - Algoritmi i strukture podataka
Slika 3.5.
Podgraf18 grafa toka programa nosi naziv potprogram (ne treba ga mešati sa
pojmom potprograma u programskim jezicima!). Potprogram koji je pravilan nosi
naziv pravilan potprogram. Od tri vrste čvorova što sačinjavaju graf toka jedino
procesi sami za sebe predstavljaju pravilne potprograme jer predikati i kolektori ne
zadovoljavaju uslove 2 odnosno 1 respektivno.
Prost program je program kojem odgovara graf toka takav da nijedan od
njegovih pravilnih potprograma nema više nego jedan čvor (a radi se o procesu, što je
17
u ovom smislu put je skup grana g1,...gm takav da grana gi+1 počinje u čvoru u kojem se završava
grana gi, za i=1,2,...
18
podgraf datog grafa jeste struktura koja se dobija uklanjanjem pojedinih čvorova i svih grana
povezanih sa njima
32
Dušan T. Malbaški - Algoritmi i strukture podataka
proces
sekvenca if-then-else
if-then
do-while-do
while-do
repeat-until
Slika 3.6.
33
Dušan T. Malbaški - Algoritmi i strukture podataka
svim programskim jezicima nastalim posle paskala, što znači da upravljačke strukture
u programskim jezicima (posle fortrana i kobola) nisu nastale iskustveno, nego imaju
teorijsku podlogu. Dodatnih upravljačkih struktura u strukturiranim programskim
jezicima tipa paskala i C-a nema mnogo, a uvedene su iz praktičnih razloga, da bi
programski kôd bio kraći i razumljiviji.
Podsećanja radi, prvih 6 osnovnih prostih programa u programskom jeziku C
su:
izraz; (proces)
naredba1 naredba2 (sekvenca)
if(izraz) naredba
while(izraz) naredba
do naredba while(izraz);
if(izraz) naredba1 else naredba2
Zapazimo da moćni ciklus for u C-u nije prost program!
Analizirati baze strukturiranih programa znači izučavati podskupove
navedenog skupa prostih programa pomoću kojih se mogu realizovati svi pravilni
programi - zato, uostalom, i govorimo o bazi strukturiranih programa. Problem
određivanja bar jedne baze odavno je rešen Strukturnom teoremom čije poznavanje
spada u obavezni deo obrazovanja svakog ko želi da ga smatraju programerom.
34
Dušan T. Malbaški - Algoritmi i strukture podataka
ciklusa označićemo sve čvorove grafa toka programa osim kolektora rednim
brojevima 1, 2, ... pri čemu je način pridruživanja proizvoljan, te nema gubljenja
opštosti. Okolini pridružujemo, takođe bez uticaja na opštost dokaza, oznaku 0.
Funkcija veze definiše se pomoću kvadratne logičke matrice prelaza čijoj svakoj vrsti
i koloni sa istom oznakom odgovara po jedan označeni čvor grafa toka, uključujući i
okolinu. Dakle, ako graf ima k čvorova tipa procesa i predikata, matrica prelaza je
reda (k+1)x(k+1). Elementi matrice su ili logičke konstante ili predikati sadržani u
čvorovima-predikatima. Prilikom izvršenja programa čvor koji se obrađuje posle
čvora sa oznakom i u matrici prelaza je onaj koji ima vrednost T u koloni koja se seče
sa vrstom i. Ako za matricu prelaza uvedemo oznaku L tada prethodno znači da se
posle obrade čvora i prelazi na onaj čvor j za koji važi 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.
1
A
p 2
B 3
Slika 3.7.
Matrica prelaza je
0 1 2 3
0 T
1 T
2 p p
3 T
Lako je pokazati da se primenom matrice prelaza L funkcija prelaza realizuje
programskim segmentom
35
Dušan T. Malbaški - Algoritmi i strukture podataka
36
Dušan T. Malbaški - Algoritmi i strukture podataka
19
inače, uobičajen postupak kod semantičke analize upravljačkih struktura
37
Dušan T. Malbaški - Algoritmi i strukture podataka
38
Dušan T. Malbaški - Algoritmi i strukture podataka
39
Dušan T. Malbaški - Algoritmi i strukture podataka
2.1. /*odrediti vrednost neg prvog negativnog; ako takvih nema neg dobija vrednost
0*/
3.1. /*ako je neg < 0 prikazati njegovu vrednost; u suprotnom izdati poruku o
nepostojanju negativnih elemenata u nizu*/
U trećoj fazi treba razraditi praktično samo komentar 2.1 tako da je krajnji produkt
razvoja programa, koji se vrlo direktno prevodi na strukturirani programski jezik
sledeći:
2.1.1. /*neg = 0*/
2.1.2. /*dok je 0 <= i < n i neg=0 ponavljati: ako je 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.
40
Dušan T. Malbaški - Algoritmi i strukture podataka
41
Dušan T. Malbaški - Algoritmi i strukture podataka
mera efikasnosti treba da bude funkcija koja zavisi od konkretnih ulaznih podataka,
što opet otvara pitanje opisivanja ulaza. Pre svega, više ulaznih podataka niukoliko ne
znači da će izvršenje trajati duže: u numeričkoj analizi postoje algoritmi koji uopšte
nemaju ulazne podatke20, a itekako mogu da traju. Kada analiziramo do sada rečeno,
uočićemo da sve ukazuje na potrebu uvođenja apstraktne mere efikasnosti, kako
vremenske tako i prostorne. Mera (vremenske i prostorne) efikasnosti algoritma nosi
naziv složenost ili kompleksnost.
Inače, svakom programeru je poznato, nedokazivo ali veoma čvrsto, pravilo
recipročnosti utroška memorije i vremena izvršenja (engl. space-time tradeoff) koje
kaže da se u cilju ubrzavanja programa mora žrtvovati memorijski prostor i vice
versa. Za ovo pravilo ne postoji dokaz jer se mogu napraviti ekvivalentni programi P1
i P2 takvi da je npr. P2 i sporiji i zauzima više memorije, no tada P2 ili ispunjava neke
dodatne zahteve (recimo, u pogledu robustnosti) ili naprosto ne valja! Ipak, određena
veza između vremena i kapaciteta je ustanovljena. U 19 navodi se teorema koja
glasi: za svaku Tjuringovu mašinu sa više traka postoji takav broj k za koji kapacitet
M i vreme T zadovoljavaju nejednačine M 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
42
Dušan T. Malbaški - Algoritmi i strukture podataka
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.
43
Dušan T. Malbaški - Algoritmi i strukture podataka
cg(n)
f(n)
n0
Slika 3.8.
Očigledno je da za svaku funkciju f(n) koja pripada Og(n) postoji konstanta c>0
takva da važi
f(n)
lim <c
n g(n)
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
44
Dušan T. Malbaški - Algoritmi i strukture podataka
45
Dušan T. Malbaški - Algoritmi i strukture podataka
46
Dušan T. Malbaški - Algoritmi i strukture podataka
gde je c>0 konstanta. Neformalno, g(n) je oznaka za skup funkcija za koje je,
počev od neke vrednosti n=n0, funkcija constg(n) donja granica. Ilustracija je data na
slici 3.9.
f(n)
cg(n)
n0
Slika 3.9.
gde su c1 i c2 pozitivne konstante. Za funkciju T(n) koja pripada skupu [g(n)] kaže
se da je reda veličine g(n). Ilustracija je data na slici 3.10.
c2g(n)
f(n)
c1g(n)
n0
Slika 3.10.
47
Dušan T. Malbaški - Algoritmi i strukture podataka
48
Dušan T. Malbaški - Algoritmi i strukture podataka
1. OSNOVNI POJMOVI
21
od novolatinske reči entitas sa značenjem biće u filozofskom smislu
49
Dušan T. Malbaški - Algoritmi i strukture podataka
22
u ovom kontekstu, model predstavlja homomorfnu (uprošćenu) sliku originala
23
relevantnim u odnosu na domen problema
50
Dušan T. Malbaški - Algoritmi i strukture podataka
Burbakiju, struktura kao pojam prosto skup snabdeven nekim uređenjem, dolazimo do
pojma strukture podataka, kao uređenog skupa podataka, i to uređenog u skladu sa
proizvoljnim brojem kriterijuma.
Posebno, kada je u pitanju računar kao medijum za skladištenje i obradu
podataka, njihovo uređivanje odn. organizovanje ne može se izbeći, ako ni zbog čega
drugog, a ono zbog toga što je računar konačni automat u kojem je svaki podatak
strogo definisan i po tipu i po mestu memorisanja. Podaci u nekom programu mogu
biti skalarni ili pak složeni u vektore, matrice, slogove ili pak u komplikovanije
strukture o kojima će biti reči u ovoj knjizi.
U ranim fazama razvoja računarstva, kada su se problemi koji su na njima bili
rešavani odnosili isključivo na matematičko-tehničke proračune, potreba za
uređivanjem podataka svodila se na svega nekoliko jednostavnih struktura: na skalare,
vektore, matrice i ređe na njihova tro- i višedimenzionalna uopštenja. Prelaskom na
nenumeričke probleme, a osobito uključivanjem računara u informacione sisteme
pojavila se potreba za složenijim, pa i daleko složenijim strukturama. Razlog za to leži
u činjenici da se isti podaci mogu strukturirati na više načina i da odabrana struktura
neposredno utiče na kvalitet procesiranja u smislu brzine rada programa i njegove
pouzdanosti, utroška memorijskog prostora i pogodnosti za održavanje, neki put do te
mere da neka rešenja uopšte nije moguće implementirati ako se ne obezbedi
adekvatna organizacija podataka.
Primer 1.1.
Uticaj izbora strukture podataka na konstrukciju algoritma, njegovu složenost i
utrošak memorijskog prostora programa demonstriraćemo na jednostavnom primeru
(1) pristupa svakom pojedinačnom elementu, odnosno (2) obrade nenultih elemenata
double matrice A sa v vrsta i k kolona. Pretpostavimo da se obrada elemenata obavlja
algoritmom obraditi čija je složenost konstantna.
Najjednostavnije rešenje je svakako upotreba gotovih sredstava koje nudi
programski jezik. Matricu A definišemo rezervisanjem memorijskog prostora u fazi
kompilacije:
double A[VMAX][KMAX];
Pristup elementima obavlja se dvostrukim indeksiranjem, tj. izrazom Aij, što je i
jednostavno i brzo. Obrada nenultih elemenata vrši se dvostrukim for ciklusom:
int i,j,v,k;
51
Dušan T. Malbaški - Algoritmi i strukture podataka
for(i=0;i<v;i++)
for(j=0;j<k;j++)
if(!A[i][j]) obraditi A[i][j];
Inače, postupak obrade je školski primer algoritma sa složenošću 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.
52
Dušan T. Malbaški - Algoritmi i strukture podataka
25
da je u pitanju paskal, postupak bi bio u osnovi isti, ali bi zahtevao nešto više izmena u kodu
53
Dušan T. Malbaški - Algoritmi i strukture podataka
54
Dušan T. Malbaški - Algoritmi i strukture podataka
nego odgovarajući algoritmi. Ako su A1,..,An algoritmi koji operišu nad strukturom
podataka S, tada izmena u algoritmu Ai ne mora da utiče na oblik ostalih, dok
modifikacija strukture podataka S u opštem slučaju zahteva promene u više
algoritama, a počesto i u svim.
26
Cirkularna definicija pojma: pojam se definiše preko samog sebe.
55
Dušan T. Malbaški - Algoritmi i strukture podataka
x1 x2 .... xn
Slika 1.1.
56
Dušan T. Malbaški - Algoritmi i strukture podataka
57
Dušan T. Malbaški - Algoritmi i strukture podataka
58
Dušan T. Malbaški - Algoritmi i strukture podataka
struktura podataka prazna. Naime, ako u datom trenutku u strukturi podataka nema
elemenata, svaki pokušaj pristupa i uklanjanja dovodi do greške te je, zbog toga,
neophodno raspolagati odgovarajućim sredstvom za predupređivanje takve situacije.
Kada je zadata eksplicitno, operacija se obično zove empty ili isEmpty.
Pored ovih, osnovnih, operacija navešćemo još neke karakteristične operacije.
To su
Pretraživanje. Operacija kojom se izdvajaju svi elementi koji zadovoljavaju
zadati kriterijum; za razliku od traženja, rezultat pretraživanja može da bude
nijedan, jedan ili više elemenata; primer bi mogao da bude pretraživanje
strukture podataka sa podacima o automobila i kriterijum „boja je crna“.
Određivanje veličine. Jednostavna operacija kojom se određuje broj
elemenata skupa S.
Redosledna obrada. Obrada svih elemenata strukture podataka po nekom
redosledu. Primer: štampanje sadržaja neke datoteke.
Sortiranje. Uređivanje elemenata strukture po nekom kriterijumu.
Kopiranje sadržaja jedne strukture podataka u drugu.
Spajanje dve ili više struktura podataka u jednu.
Razlaganje strukture podataka na dve ili više.
S obzirom na to da se gotovo svaka operacija može realizovati na više načina, a u
svrhu poštovanja principa skrivanja informacija27, operacije se opisuju specifikacijom
baziranom obično na tzv. Horovim (Hoare28) tripletima. Horov triplet je, neformalno,
konstrukt oblika
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
59
Dušan T. Malbaški - Algoritmi i strukture podataka
60
Dušan T. Malbaški - Algoritmi i strukture podataka
(a)
(b)
Slika 1.2
61
Dušan T. Malbaški - Algoritmi i strukture podataka
(a)
(b)
Slika 1.3
62
Dušan T. Malbaški - Algoritmi i strukture podataka
Slika 1.4
Slika 1.5
63
Dušan T. Malbaški - Algoritmi i strukture podataka
64
Dušan T. Malbaški - Algoritmi i strukture podataka
2. NIZ
65
Dušan T. Malbaški - Algoritmi i strukture podataka
Nizovi kod kojih operacije uklanjanja i dodavanja nisu definisane nose naziv statički
nizovi, a oni kod kojih jesu zovu se dinamički nizovi. S obzirom na činjenicu da se
statički i dinamički nizovi različito realizuju, treba biti u stanju da se prepozna o kojoj
se od dve vrste nizova radi. Ovo se postiže analizom programa u kojem se dati niz
koristi. Naime, odluka o tome da li neku strukturu podataka u programu treba
realizovati kao statički ili kao dinamički niz donosi se na bazi sledećeg pravila:
ako se u toku izvršenja programa broj elemenata niza ne menja, tada je u
pitanju statički niz; u suprotnom, radi se o dinamičkom nizu.
Inače, nizovi koji odgovaraju višedimenzionalnim promenljivim iz starih programskih
jezika nisu izgubili na značaju: oni se i dalje intenzivno koriste, samo što se u
modernim jezicima tretiraju kao podvrsta nizova koju zovemo multiindeksne
strukture. Njih definišemo rekurzivno, na bazi niza skalara koji se zove vektor:
1. Vektor je multiindeksna struktura reda 1.
2. Multiindeksna struktura reda k >1 je niz Nk=(S(Nk),r(Nk)) u kojem su elementi
skupa S(Nk) multiindeksne strukture reda k-1.
S obzirom na čestu primenu, pored multiindeksnih struktura reda 1 (vektora) i
multiindeksne strukture reda 2 imaju posebno ime - matrice.
Glavna operacija kod nizova je operacija pristupa. U principu, moguće je
realizovati sve tri vrste pristupa - pristup prema poziciji, traženje, pa i navigaciju - s
tim što navigacija ima manji značaj od prve dve. Pristup se u svim slučajevima
obavlja osloncem na mehanizam indeksiranja. U logičkom smislu, indeks je neka
vrednost iz linearno uređenog, konačnog skupa indeksa I. Ako je 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)
66
Dušan T. Malbaški - Algoritmi i strukture podataka
gde funkcija adr iznačava adresu, a L jeste dužina svakog elementa niza u
bajtovima29. Iz formule direktno sledi odgovor na pitanje zašto je minimalna vrednost
indeksa u C-u uvek 0? Kako vidimo, u opštem slučaju, svako indeksiranje zahteva tri
operacije: sabiranje, oduzimanje (p od i) i množenje. U slučaju da je p=0 oduzimanja
više nema i indeksiranje se ubrzava, što je kod C-a od najveće važnosti. Dakle,
indeksiranje C-ovog niza T 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.
29
upravo primena ove formule jeste razlog zbog kojeg svi elementi istog niza moraju biti iste dužine
67
Dušan T. Malbaški - Algoritmi i strukture podataka
Slika 2.1.
30
i veoma slično u paskalu
68
Dušan T. Malbaški - Algoritmi i strukture podataka
gde je T tip elemenata matrice y. I ovde se može zapaziti da maksimalni broj vrsta
MAX_V ne učestvuje u računanju adrese, i to je razlog zbog kojeg pri prenosu
standardno realizovane matrice u funkciju nije potrebno navoditi prvu dimenziju, ali
drugu već jeste (i to važi za sve programske jezike)!
Uopštimo ovu formulu na mulitindeksnu strukturu M sa n indeksa i dozvolimo
da se svaki indeks i kreće u rasponu pi do ki gde je pi najmanja vrednost i-tog indeksa
(u C-u je 0), a ki njegova najveća vrednost. Ako je L veličina svakog elementa u
bajtovima, adresa elementa M[j1]...[jn] se direktno izračunava po formuli
n
adr(M[j1]...[jn]) = adr(M[p1]...[pn]) + L* ( j m p m)D m
m 1
31
tako je u C-u i u paskalu
32
tako je u fortranu
69
Dušan T. Malbaški - Algoritmi i strukture podataka
d = malloc(B*sizeof(T));
i popunjavao redom od lokacije d[0] do dn-1 gde je n aktuelna dužina niza. Stanje je
prikazano na slici 2.2.
d
d[0] d[1] ... d[n-1]
Slika 2.2.
d
... (a)
d
... (b)
Slika 2.3.
70
Dušan T. Malbaški - Algoritmi i strukture podataka
int j;
if(n==B) realloc(d,(B+=deltaB)*sizeof(T));
for(j=n++;j>i;j--) d[j]=d[j-1];
d[i] = value;
Sledeći segment ukloniće iz niza element na indeksu i:
int j;
for(j=i,n--;j<n;j++) d[j]=d[j+1];
Posle izvesnog broja uklanjanja blok koji je dodeljen nizu može postati slabo
popunjen. Da bi se memorijom upravljalo efikasno, u slučajevima kada popunjenost
memorijskog prostora padne ispod neke granice, blok se može smanjiti (ovo, inače,
nije obavezno, za razliku od proširenja):
realloc(d,(B-=deltaB)*sizeof(T));
33
Liste, o kojima će kasnije biti reči, sortiraju se praktično istim metodama.
71
Dušan T. Malbaški - Algoritmi i strukture podataka
4 9 8 2 6
2 9 8 4 6
2 4 8 9 6
2 4 6 9 8
2 4 6 8 9
Slika 2.4.
72
Dušan T. Malbaški - Algoritmi i strukture podataka
73
Dušan T. Malbaški - Algoritmi i strukture podataka
74
Dušan T. Malbaški - Algoritmi i strukture podataka
for(chn=1,i=n-1;chn&&(i>0);i--)
for(j=chn=0;j<i;j++)
//los redosled? da: izmena mesta
if(a[j]>a[j+1]) {tmp=a[j];a[j]=a[j+1];a[j+1]=tmp;chn=1;}
}
Promenljiva chn u funkciji je, ustvari, indikator postojanja izmena. Kako se vidi, ona
dobija vrednost 1 ako se izvrši bar jedna izmena mesta; ukoliko chn ima vrednost 0 na
kraju nekog prolaza, to je znak da izmena nije bilo i da procedura treba da se završi.
Broj poređenja u jednom prolazu za podniz dužine i iznosi i-1. Prema tome,
ukupni broj poređenja dobija se kao
Ububble(n) = (n-1)+(n-2)+...+2+1 = n(n-1)/2 ≈ n2/2
odakle, uz pretpostavku da je vremenska složenost srazmerna broju upoređivanja,
sledi da je funkcija složenosti Tbubble(n)=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.
75
Dušan T. Malbaški - Algoritmi i strukture podataka
34
videti odeljak o linearnom traženju u tabelama
76
Dušan T. Malbaški - Algoritmi i strukture podataka
odnosno
Uins = (n-1)(n+2)/4 ≈ n2/4
Shodno tome, ponovo se susrećemo sa algoritmom čija je funkcija složenosti
Tins(n)=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.
77
Dušan T. Malbaški - Algoritmi i strukture podataka
L M R
2 1 3 4 6 9 7
Slika 2.4.
78
Dušan T. Malbaški - Algoritmi i strukture podataka
79
Dušan T. Malbaški - Algoritmi i strukture podataka
gde pomoćna funkcija _qsort vrši preuređenje niza. Promenljive _G koja sadrži
vrednost pivota prilikom preuređenja i _tmp koja posreduje pri zameni mesta,
realizovane su kao statičke globalne promenljive da se ne bi opterećivao stek pri
rekurzivnim pozivima.
Analiza kompleksnosti metode quicksort nešto je složenija nego u prethodnim
slučajevima. Dajemo verziju iz 10. Neka je T(n) vremenska kompleksnost sortiranja
niza sa n elemenata. Jedno preuređenje deli taj niz na dva dela: prvi ima k elemenata,
a drugi n-k elemenata, pri čemu se zanemaruje činjenica da pivot ne ulazi u
podnizove, jer to ne utiče na krajnji rezultat i to stoga što nas interesuje samo red
funkcije složenosti. Pošto jedan prolaz kojim se niz preuređuje ima linearnu funkciju
složenosti oblika cn gde je c konstanta, važi
T(n) = T(k) + T(n-k) + 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
n -2
T(n) = nT(1) + c (n - j) = nT(1) + c(n(n-2)-(n-2)(n-1)/2)
j 0
odnosno T(n) = O[n2]. Dakle, tek u najgorem slučaju quicksort se ponaša kao
prethodne metode izbora, izmene i umetanja. U najboljem slučaju pak, kada svako
preuređenje deli niz na dva jednaka dela, važi
T(n) = 2T(n/2) + 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
80
Dušan T. Malbaški - Algoritmi i strukture podataka
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
81
Dušan T. Malbaški - Algoritmi i strukture podataka
3. SLOG I TABELA
Slog ili zapis (u C-u „struktura“) pripada, zajedno sa nizom, prvom višem
nivou organizacije podataka u odnosu na skalar. Kako skalar smatramo za najnižu,
nesvodivu strukturu - dakle atom - Virt za nizove i slogove koristi termin molekuli. I
slog je u više programske jezike uvršten rano, još 1959. u programski jezik kobol.
Statički ili dinamički niz čiji su elementi slogovi nazivamo tabelom. Prema ovom
shvatanju, tabela je izvedena struktura podataka no, s obzirom na učestanost primene i
način pristupa i obrade, zaslužuje poseban tretman. U ovom poglavlju opisaćemo i
tzv. rasutu ili heš tabelu koja, po definiciji, nije tabela (niz slogova), ali se primenjuje
u sličnim situacijama.
3.1. SLOG
Kao što niz objedinjava istovrsne elemente, tako i slog objedinjava raznovrsne
elemente, što znači da je u pitanju heterogena struktura. Poput statičkog niza, slog je
statička struktura kod koje je osnovni način pristupa - pristup prema poziciji. Na
ovom se, međutim, sličnosti između niza i sloga završavaju jer im je ponašanje
potpuno različito. U prethodnom poglavlju videli smo da je niz linearno uređen skup
elemenata koji imaju istu semantiku, ali koji su međusobno semantički nezavisni -
svaki element niza je po značenju zaokružena celina. Kod sloga je situacija sasvim
drukčija. Sa semantičke tačke gledišta, svaki element niza opisuje poseban entitet, a
niz kao celina opisuje skup entiteta. S druge strane, slog kao celina opisuje entitet, a
svaki njegov element daje opis neke od osobina tog entiteta, tako da su elementi sloga
semantički nesamostalni i samo svi zajedno opisuju neku jedinicu posmatranja. U
gnoseološkom smislu, slog je blizak skalaru i može se shvatiti kao njegovo
semantičko uopštenje: dok skalar, kada ima značenje, predstavlja trivijalno
jednostavan opis entiteta sa samo jednom osobinom, slog daje opis entiteta sa više
osobina te, pošto se različite osobine najčešće opisuju podacima različitih vrsta
(tipova), heterogenost sloga postaje neminovna. Semantička nesamostalnost
elemenata sloga prepoznaje se već i po tome što se za njih upotrebljava poseban
termin - polja. Inače, osobina pôlja da se uvek odnose na isti entitet i da samo zajedno
daju sliku tog entiteta zove se semantička konvergencija.
82
Dušan T. Malbaški - Algoritmi i strukture podataka
35
Programski jezici paskal i C imaju tipove čije ponašanje podseća na ponašanje sloga sa
promenljivom strukturom (varijabilni slog u paskalu, odnosno unija u C-u), ali to nisu osnovne
strukture podataka niti imaju široku upotrebu.
83
Dušan T. Malbaški - Algoritmi i strukture podataka
Slika 3.1
Ako se u izvornom programu pojavi izraz d.x za pristup polju x sloga d, prevodilac će
postupiti ovako: ako se d nalazi na adresi A0 jednostavnim linearnim traženjem
pronaćiće se naziv polja „x“ i pročitati iz treće kolone ofset jednak 8. Iz ovih podataka
formiraće se adresa odgovarajuće memorijske lokacije izrazom
adr(d.x) = A0 + 8
36
postoji i kod niza!
84
Dušan T. Malbaški - Algoritmi i strukture podataka
3.2. TABELA
Po definiciji, tabela je niz čiji su elementi slogovi. Pritom, taj niz može biti
statički i tada je tabela statička, ili dinamički kada govorimo o dinamičkoj tabeli.
Najvažnije operacije nad tabelom jesu
sortiranje
traženje, tj. pristup po ključu i
redosledna obrada elemenata tabele
i izvode se na isti način u statičkim i dinamičkim tabelama. Operacije nad tabelom
objasnićemo na uopštenom primeru tabele sa n slogova tipa
typedef struct {
int key;
T info;
} Entry;
gde polje key predstavlja ključ, a polje info nekog tipa T preostali sadržaj elementa.
Neka je tabela definisana kao statički ili dinamički niz tipa Entry i dužine n i neka je
naziv odgovarajućeg tipa Table. Činjenica da smo ključ definisali kao int nema uticaja
na opštost razmatranja, pod pretpostavkom da u skupu ključeva postoji linearno
uređenje.
U principu, sortiranje tabele vrši se metodama koje su identične metodama
sortiranja niza, uz trivijalnu razliku koja se sastoji u tome da se sortiranje vrši po
vrednosti nekog ili nekih polja sloga. Najčešće, to polje jeste ključ (naravno, ako ga
ima). Shodno tome, nećemo se zadržavati na ovoj operaciji: sve što treba uraditi jeste
proučiti ranije opisane metode sortiranja niza i sintagmu „vrednost elementa“
zameniti sintagmom „polje sloga“ ili „ključ“.
Traženje u tabeli jeste postupak određivanja indeksa elementa sa zadatom
vrednošću ključa, nazvanom argument traženja, a sa krajnjim ciljem pribavljanja
podataka iz polja info traženog sloga. Traženje ima dva ishoda, oba regularna (!): tzv.
85
Dušan T. Malbaški - Algoritmi i strukture podataka
š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
n -1
1 1 n 1
Ulin = 1 + n i 1 n i = 1 + (n-1)/2 = (n+1)/2 ≈ n/2
i 0 i 1
86
Dušan T. Malbaški - Algoritmi i strukture podataka
Nlin = n
gde smo sa N označili broj upoređivanja kod neuspešnog traženja. Vremenska
složenost je i u ovom slučaju On.
87
Dušan T. Malbaški - Algoritmi i strukture podataka
binarno traženje
1 4 5 8 10 14 16 17 19
linearno traženje
Slika 3.2.
88
Dušan T. Malbaški - Algoritmi i strukture podataka
x5
x2 x8
x1 x3 x6 x10
x0 x4 x7 x9 x11
Slika 3.3.
2 k
= 2h-1-1
k 0
h-1
= (1/n)(heh+(h-2)2 +1)
Uvrštavanjem vrednosti za eh dobijamo
89
Dušan T. Malbaški - Algoritmi i strukture podataka
Ubin = h+h/n+(2h-1)n
Kako je, u opštem slučaju, 2h-1 n 2h-1 vrednost h može se izraziti kao najmanji ceo
broj koji nije manji od log2 (n+1), tj.
h = log2 (n+1) = log2 (n+1) +
gde je 0 < 1. Dakle,
Ubin = log2 (n+1)+ (1+1/n) - 2(n+1)-1/n
Ako je n>>1 sledi
Ubin ≈ log2 n - (2-)
Konačno, lako se pokazuje da u intervalu (0,1) važi 0,91 2- 1, što znači da bi
dosta precizna procena prosečnog broja upoređenja pri uspešnom traženju bila
Ubin ≈ (log2 n) - 1
Kako je funkcija složenosti Tbin(n) srazmerna broju upoređenja zaključujemo da je,
kod uspešnog traženja,
Tbin(n) = 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
90
Dušan T. Malbaški - Algoritmi i strukture podataka
91
Dušan T. Malbaški - Algoritmi i strukture podataka
92
Dušan T. Malbaški - Algoritmi i strukture podataka
tabele i načinu određivanja adrese iz ključa. Pre svega, fizička realizacija heš tabele je
sekvencijalna, pošto se u suprotnom ne bi moglo izvesti preslikavanje ključa na
adresu direktnim izračunavanjem. Neka je tip sloga u heš tabeli
typedef struct {
K key;
T info;
} Entry;
gde je K tip ključa sloga (najčešće je u pitanju string). Ako se očekuje intenzivna
primena operacija dodavanja i uklanjanja, tabela treba da bude na hipu, te bi definicija
odgovarajućeg tipa bila
typedef Entry* Hashtable;
Posmatrajmo heš tabelu t definisanu sa
Hashtable t;
i realizovanu sa
t = malloc(SIZE*sizeof(Entry));
gde je SIZE veličina memorije rezervisane za heš tabelu, izražena brojem elemenata.
Zadatak heš funkcije h je, dakle, da na bazi polja key sloga generiše indeks iz
segmenta 0 do SIZE-1 elementa (fizičkog) niza koji realizuje heš tabelu. Taj indeks se
pojavljuje u ulozi relativne adrese u okviru rezervisane zone memorije. U idealnom
slučaju heš funkcija je bijekcija, tj. svakoj mogućoj vrednosti ključa odgovara jedna i
samo jedna vrednost adrese (tj. indeksa niza tipa Hashtable). Međutim, u praksi je
situacija uvek veoma daleko od idealne, te se biunivoka heš funkcija ima shvatiti kao
redak (i nevažan) slučaj. Razlog za to je činjenica da bi se u idealnom slučaju morao
rezervisati memorijski prostor koji pokriva sve moguće vrednosti ključeva od kojih u
stvarnim primenama može da bude zastupljen tek vrlo mali deo. Ako bi, na primer,
ključ bio samo dvobajtni string, za tabelu bi već trebalo predvideti oko 65000 lokacija
(što je jednostavno neprihvatljivo). Zato treba tražiti druga rešenja.
Ideja heširanja je stara ideja, te nije čudo što za izbor heš funkcije postoji
veliki broj varijanata. Najpopularnije se zasnivaju na ideji da se, polazeći od ključa,
generiše neki broj između 0 i SIZE-1 algoritmom sličnim nekom od algoritama za
generisanje pseudoslučajnih brojeva, pri čemu raspodela tako dobijenih brojeva (tj.
indeksa odn. relativnih adresa) treba da bude što bliža uniformnoj. Treba podvući da u
ovim postupcima, u stvari, nema nikakve „slučajnosti“, stoga što za istu vrednost
93
Dušan T. Malbaški - Algoritmi i strukture podataka
ključa heš funkcija mora da generiše isti indeks. Otud i engleski naziv „heš“ koji, u
smislu u kojem se koristi ovde, znači „razbacivanje“.
Broj i raznovrsnost metoda za heširanje impresivni su, pa ćemo, stoga
prikazati samo jednu, predviđenu za ključeve koji su stringovi (11). Ideja je da se
pojedinačni znaci u stringu tretiraju kao celobrojne vrednosti i međusobno se sabiraju
da bi na kraju rezultat bio sveden na traženi segment operacijom deljenja po modulu:
unsigned h(char* key) {
unsigned x=0;
while(*key) x+=*key++;
return x%SIZE;
}
94
Dušan T. Malbaški - Algoritmi i strukture podataka
Entry t0 = {″″};
.................
for(i=0;i<SIZE;i++) t[i]=t0; //upisati na sve lokacije slog sa praznim kljucem
Najpoznatija grupa metoda za rukovanje sinonimima poznata je pod imenom
„otvoreno adresiranje“. Zajedničko za sve metode otvorenog adresiranja jeste ideja
ako je lokacija na koju treba smestiti novi slog zauzeta, tada se traži novo
mesto na rastojanju koje je izračunljivo; ako je i to, novo, mesto zauzeto
postupak se ponavlja.
Među metodama otvorenog adresiranja najprostija je tzv. metoda linearnog
sondiranja (engl. linear probing). Ovde se, za slučaj kolizije, mesto za upis traži na
sledećoj lokaciji, pa na sledećoj itd. U slučaju da se stigne do kraja tabele prelazi se na
početak. Dakle, ako je i adresa već zauzete lokacije, nova se traži izrazom
i = (i+1)%SIZE
Šema linearnog sondiranja prikazana je na slici 3.4 (slobodne lokacije su šrafirane).
Postupak se završava kada se naiđe na slobodnu lokaciju (sa praznim ključem) ili
kada se stigne do lokacije sa kojom je počela pretraga, što znači da je tabela
popunjena.
početak pretrage
Slika 3.4.
95
Dušan T. Malbaški - Algoritmi i strukture podataka
početak pretrage
(a) a b c d
(b) a b d
Slika 3.5.
96
Dušan T. Malbaški - Algoritmi i strukture podataka
97
Dušan T. Malbaški - Algoritmi i strukture podataka
98
Dušan T. Malbaški - Algoritmi i strukture podataka
dodavanja tako i vreme traženja. Na slici 3.6. prikazana je heš tabela sa jednim
elementom, na lokaciji j. Neka je p=1/SIZE verovatnoća da heš funkcija generiše
pojedinačnu adresu i neka je ona uvek ista za sve adrese (što je i svrha dobre heš
funkcije).
j j+1
Slika 3.6.
Verovatnoća da će novi slog biti smešten na bilo koju od slobodnih lokacija iznosi p,
sa izuzetkom lokacije j+1. Naime, ova lokacija biće popunjena ako heš funkcija
generiše adresu j+1, ali i ako generiše lokaciju j koja je već zauzeta, pa se linearnim
sondiranjem opet pronalazi lokacija j+1! Dakle, verovatnoća da se pri prvom
sledećem dodavanju zauzme lokacija j+1 je 2p, dvaput veća nego verovatnoća
zauzimanja ostalih lokacija. Po zauzimanju ove lokacije, verovatnoća da se zauzme
lokacija j+2 iznosi 3p itd. Razlog za ovakvo ponašanje tabele je očigledno u činjenici
da se slobodne lokacije traže sukcesivno, odnosno, da je mehanizam prolaska kroz
tabelu izraz
i = (i+1)%SIZE
koji se koristi u funkcijama getItem i putItem. Izvesno, ne naročito značajno,
poboljšanje postiže se ako se rastojanje između lokacija-kandidata za upis poveća od
1 na neku vrednost inc>1, tako da bude
i = (i+inc)%SIZE
pri čemu su vrednosti inc i SIZE uzajamno proste da bi se pri upisu uračunale sve
lokacije.
Značajnije poboljšanje u odnosu na pojavu nagomilavanja pruža metoda tzv.
kvadratnog sondiranja. Kod ove metode, rastojanje pri traženju je promenljivo i
vezano je za prethodni broj pokušaja. Konkretno, neka je i0 adresa dobijena
heširanjem
i0 = h(key)
Ako se traženje mora nastaviti, ono se obavlja u k pokušaja tako da bude
ik = (i0 + k2)%SIZE k=1,2,...
99
Dušan T. Malbaški - Algoritmi i strukture podataka
100
Dušan T. Malbaški - Algoritmi i strukture podataka
if(!strcmp(t[i].key,arg)) return i;
}
return -1;
}
I još jedna napomena: zbog numeričkih problema vezanih za performansu heširanja
opšta je preporuka (koje se svi pridržavaju) da
veličina rezervisanog memorijskog prostora SIZE treba da bude prost broj.
Dakle, ako smo procenili da za tabelu treba 1000 lokacija, umesto tog iznosa
opredelićemo se, recimo, za 997.
Konačno, ne sme se izgubiti iz vida da je realizacija heš tabele sekvencijalna,
što neminovno otvara problem prepunjenosti. Ako heš tabela nije statička, tada do
stanja prepunjenosti (overflow) itekako može doći. Pošto se u tim situacijama program
ne sme zaustaviti sa porukom o grešci (jer nije u pitanju nikakva greška), u sklopu
realizacije tabele mora se predvideti i funkcija za rekonfiguraciju tabele. U pitanju je
jednostavna (ali ne i brza) funkcija koja će zauzeti veći memorijski prostor. Ovde C-
ova funkcija realloc nije od pomoći jer se, zbog heširanja, slogovi iz stare tabele
moraju iznova upisati u novu u kojoj se neće naći na istim lokacijama. Štaviše, za
pokretanje rekonfiguracije nije dobro čekati da se tabela popuni. Naime, performansa
heš tabele najdirektnije zavisi od tzv. koeficijenta popunjenosti koji predstavlja odnos
broja zauzetih lokacija i veličine memorijskog prostora SIZE. Običaj je da se funkcija
za rekonfiguraciju uključi ne kada je taj koeficijent jednak 100% (tj. popunjena
tabela), nego kada pređe određenu granicu (tipično 70% - 80%).
Problemi vezani za rekonfigurisanje zbog prepunjenosti, pa i za koliziju
ključeva, mogu se rešavati i posebnom sekvencijalno-spregnutom realizacijom heš
tabele. Metoda nosi naziv metoda lančanja. Po cenu usporavanja operacija, ovom
realizacijom se u potpunosti izbegava ponovno formiranje tabele, a sinonimima se
rukuje tako što se svi slogovi nalaze na hipu, pri čemu su sinonimi međusobno
spregnuti. Sama heš tabela sadrži samo pokazivač na lanac slogova koji su sinonimi.
Tako, element tabele na adresi i sadrži pokazivač na lanac slogova čija je zajednička
karakteristika da heš funkcija h primenjena na njihove ključeve daje adresu i. Iako je
ova struktura jednostavna i fleksibilna, osnovne operacije su usporene, kako zbog
rukovanja hipom, tako i zbog sporosti pristupa lancima sinonima (o ponašanju ovih
101
Dušan T. Malbaški - Algoritmi i strukture podataka
u ovom lancu su
slogovi za koje je
h(key)=i
Slika 3.7.
102
Dušan T. Malbaški - Algoritmi i strukture podataka
k -1 1
Nhash(q) = kq
k 1
(1 q) =
1 q
Da bismo procenili broj pokušaja pri uspešnom traženju, moramo se podsetiti na
postupak upisivanja. Prilikom upisivanja u tabelu u kojoj je bilo m zauzetih lokacija,
upisu je prethodilo neuspešno traženje pre dodavanja u tabeli sa faktorom
popunjenosti x=m/SIZE. Kako su slogovi dodavani, tako je faktor popunjenosti rastao
do vrednosti q. Ako je broj slogova u tabeli mnogo veći od 1, sumu možemo
aproksimirati integralom pa se dobija
q
1 1
Uhash(q) = (1/q) N hash (x)dx = ln
x 0
q 1 q
Da bi čitalac stekao osećaj o performansi heš tabele posmatraćemo običnu tabelu sa,
recimo, 500 elemenata. Uspešno linearno traženje proizvoljnog sloga zahteva u
proseku 250 pokušaja. Ako bi se tih 500 slogova našlo u heš tabeli sa faktorom
popunjenosti 80% (što je blizu gornjoj granici tolerancije), broj pokušaja iznosio bi
svega oko 2!
103
Dušan T. Malbaški - Algoritmi i strukture podataka
4. POLUDINAMIČKE STRUKTURE
4.1. STEK
Stek (engl. stack, u prevodu naređana gomila) jedna je od najčešće korišćenih
struktura podataka uopšte, prvenstveno zahvaljujući njegovoj ulozi u komunikaciji sa
potprogramima. Definiše se kao uređeni par
L = (S(L),r(L))
sa sledećim osobinama:
struktura je linearna
pristupa se isključivo prvom elementu
uklanja se isključivo prvi element
dodaje se isključivo ispred prvog elementa.
Šema steka prikazana je na slici 4.1.
104
Dušan T. Malbaški - Algoritmi i strukture podataka
...
Slika 4.1.
Očigledno, prvi element steka igra najvažniju ulogu, jer se sve tri osnovne operacije
obavljaju nad njim. Prvi element ima poseban naziv - vrh steka (engl. top). Poslednji
element takođe ima ime: zove se dno steka (engl. bottom), iako nema posebnu ulogu i
ne razlikuje se od ostalih elemenata. Pošto se sve tri operacije izvršavaju nad istim -
prvim - elementom, stek se ponaša kao da je u strukturi “vidljiv” samo vrh, pa se
često grafički prikazuje kao na slici 4.2.
Slika 4.2.
Glavna osobina steka, čak osobina zbog koje stek i postoji, proističe iz činjenice da će
element koji je, u vremenu, kasnije dodat u stek napustiti stek ranije. Radi se o osobini
koja se zove Last In First Out, ili skraćeno LIFO. Ključna reč za stek je, dakle,
akronim LIFO. Iza ove osobine krije se, ustvari, tzv. mehanizam privremeno
prekinutog posla. Zamislimo da je u toku rad na nekom poslu koji se prekida da bi se
obavio neki drugi posao, koji se takođe prekida da bi se obavio neki treći, pa četvrti
posao. Po završetku četvrtog posla nastavlja se sa trećim, po njegovom završetku sa
drugim i, najzad, sa prvim. Jasno je da se prilikom privremenog prekida bilo kojeg od
navedenih poslova njegovo stanje mora zapamtiti, da bi kasnije mogao da se nastavi
od tačke prekida. Lako zaključujemo da se stanja prekinutih poslova uspostavljaju
redosledom obrnutim od onog u kojem su zapamćeni prilikom pekidanja. Zanimljivo
je da je ovakva situacija česta i u životu i u programiranju (setimo se poziva
potprograma), čak mnogo češća nego što bi se to na prvi pogled moglo zaključiti.
Izuzetno čestu primenu steka ilustruje još i to što osnovne operacije imaju
posebna, opšteprihvaćena, imena. Operacija pristupa kod steka po pravilu nosi naziv
top. Operacija uklanjanja zove se pop, a operacija dodavanja push. U praksi (recimo u
105
Dušan T. Malbaški - Algoritmi i strukture podataka
106
Dušan T. Malbaški - Algoritmi i strukture podataka
37
Uobičajeno je da se stanje u koje prazan stek ulazi pri pokušaju očitavanja praznog steka ili
uklanjanja zove underflow.
107
Dušan T. Malbaški - Algoritmi i strukture podataka
38
Ranije pomenutom programskom steku, koji je heterogen, posvetićemo poseban odeljak.
108
Dušan T. Malbaški - Algoritmi i strukture podataka
stk->t=-1;
}
kada je s u statičkoj memoriji, odnosno
void create(Stack* stk,int capacity) {
stk->c=capacity;
stk->t=-1;
stk->s=malloc(capacity*sizeof(T));
}
kada je niz s na hipu.
Ako su elementi steka na hipu, mora se dodati i funkcija čiji je zadatak da
oslobodi hip:
void destroy(Stack* stk) {
free(stk->s);
}
Funkcije za proveru da li je stek prazan (isEmpty), odnosno popunjen (isFull)
realizovaćemo sledećim kôdom:
int isEmpty(const Stack* stk) {
return stk->t<0;
}
109
Dušan T. Malbaški - Algoritmi i strukture podataka
}
Operaciju uklanjanja izvešćemo onako kako se obično realizuje u praksi, a to
je da odgovarajuća funkcija kao rezultat vrati uklonjeni element, dok je samo
uklanjanje bočni efekat. Bez obzira na to gde se nalazi sadržaj steka, u statičkoj
memoriji ili na hipu, uklanjanje ima sledeći kôd:
T pop(Stack* stk) {
return stk->s[stk->t--];
}
Konačno, operacija push za dodavanje elementa u stek takođe je ista za obe verzije:
void push(Stack* stk,T item) {
stk->s[++stk->t]=item;
}
Operacije očitavanja i dodavanja šematski su prikazane na slici 4.3.
pop s
push s
Slika 4.3.
110
Dušan T. Malbaški - Algoritmi i strukture podataka
nikakve veze, što u velikoj meri otežava izradu algoritma za opšti slučaj. Već kod
jednostavnog izraza tipa navedenog, algoritam bi otpočeo fizički poslednjom
operacijom (deljenje), zatim bi izvršio prethodnu (oduzimanje), potom nekako
označio u originalnom izrazu da je ovaj deo obrađen i uklonio ga, pa prešao na drugi
kraj izraza i izvršio množenje i na kraju pronašao u sredini operaciju sabiranja te
izvršio i nju39. Pitanje je kako bi u opštem slučaju izgledao ovakav algoritam? U
praksi, tj. kod realnih kompajlera realnih programskih jezika, postoje razna rešenja, ali
se pokazuje da sva ona, na jedan ili drugi način, koriste stek.
Najpoznatiji način za prevođenje izraza (koristi ga većina kompajlera, mada ne
i većina kompajlera za C) jeste korišćenje tzv. poljske notacije, koja je dobila ime po
čuvenom poljskom matematičaru Janu Lukasiewitz-u. Postoje dve verzije poljske
notacije: prefiksna i postfiksna (inverzna poljska) notacija. Da bi se podvukla razlika,
obična notacija nosi naziv infiksna notacija. Posmatrajmo infiksni izraz sa jednim
binarnim operatorom, a op b, gde je op binarni operator. U prefiksnoj verziji, prvo se
navodi operator, pa onda levi, a zatim desni operand. U postfiksnoj notaciji prvo se
navodi levi operand, zatim desni i na kraju operator. U sledećoj tabeli prikazana su
četiri osnovna binarna aritmetička operatora u sve tri forme:
U nastavku ćemo razmatrati postfiksnu, tj. inverznu poljsku notaciju. Pre svega,
redosled izvršavanja operacija određen je opštim pravilom po kojem prvo stupa u
dejstvo operator ispred kojeg postoji operand (za unarne operatore) ili oba operanda
(za binarne) sa već izračunatim vrednostima. Pri tome izraz se skenira sleva u desno.
Podizraz čija je vrednost izračunata uklanja se iz izraza i zamenjuje izračunatom
vrednošću. Na primer, izraz u postfiksnoj formi
AB3*+5H-/
računao bi se po sledećoj šemi:
1. pomnožiti B i 3
39
Kada se još doda da kod nesusednih operatora ne važi prioritet, zamešateljstvo postaje potpuno!
111
Dušan T. Malbaški - Algoritmi i strukture podataka
112
Dušan T. Malbaški - Algoritmi i strukture podataka
i postfix[i] stek
0 8 8
1 4 4,8
2 3 3,4,8
3 * 12,8
4 + 20
5 5 5,20
6 1 1,5,20
7 - 4,20
8 / 5
113
Dušan T. Malbaški - Algoritmi i strukture podataka
while(!isEmpty(&stk)&&(priority(top(&s))>=priority(infix[j])))
postfix[++n]=pop(&stk);
push(&stk,infix[j];
break;
case znak ‘)’: /*izvuci iz steka sve operatore izmedju leve i desne zagrade i
upisati ih u izlazni niz postfix*/
while(top(&stk)!=’(‘) postfix[++n]=pop(&stk);
pop(&stk); //ukloniti znak ‘(‘ iz steka
} //kraj switch
while(!isEmpty(&stk) postfix[++n]=pop(&stk);
114
Dušan T. Malbaški - Algoritmi i strukture podataka
Ap
Ap
Slika 4.4.
P0 P1 P2 P3
A0 A1 A2
A2
A1 A1
Slika 4.5. A0 A0 A0
115
Dušan T. Malbaški - Algoritmi i strukture podataka
bp
lokalne
promenljive,
argumenti, stek frejm
adresa povratka,
prethodni bp
sp
40
U algoritamskim (imperativnim) jezicima rekurzija se prvi put pojavila u jeziku algol 60, koji je bio
daleko ispred svog vremena. Paskal je direktni naslednik algola, što u velikoj meri važi i za C.
116
Dušan T. Malbaški - Algoritmi i strukture podataka
4.1.4. Rekurzija
Korišćenje programskog steka za memorisanje stanja potprograma stvorilo je
uslove za implementaciju rekurzivnih potprograma, tj. potprograma koji pozivaju
sami sebe. Naime, ako se vratimo na sliku 4.5 videćemo da je svejedno da li su rutine
P0, P1, P2 i P3 različite ili je u pitanju ista rutina, jer svaki poziv stvara novi stek
frejm, nezavisan od prethodnih. Ovo, inače, nije bilo moguće kod direktnog
povezivanja jer je u okviru memorijskog prostora potprograma postojala jedna jedina
lokacija za smeštanje adrese povratka.
Rekurzivni potprogrami sintaksno se ne razlikuju od drugih potprograma.
Međutim, njihova struktura je specifična, imajući u vidu da se sukcesivni pozivi istog
potprograma ne mogu produžavati unedogled - rekurzija se negde mora zaustaviti.
Načelno, rekurzivni potprogram treba da ima dve komponente koje se različito
raspoređuju, saobrazno konkretnom slučaju. Da bismo objasnili način funkcionisanja
rekurzivne funkcije posmatraćemo školski primer računanja faktorijela. Rekurzivni
sistem za ovu svrhu ima sledeći izgled:
0! = 1
41
precizinje, podiže, s obzirom na smer širenja steka
117
Dušan T. Malbaški - Algoritmi i strukture podataka
n! = n(n-1)!
Neka se funkcija za rekurzivno računanje faktorijela zove fct. Sada se rekurzivni
sistem pretvara u
fct(0) = 1
fct(n) = n*fct(n-1)
Pomenute komponente rekurzivne funkcije su:
korak rekurzije; to je osnovni blok koji se izvršava pri svakoj rekurziji, pri
čemu se uslovi izvršavanja menjaju; u našem slučaju to je fct(n)=n*fct(n-1)
pravilo zaustavljanja; u matematici to je baza rekurzije, u programiranju to je
blok fct(0)=1.
Još nešto: kada matematički razvijamo rekurzivni sistem, počinjemo od baze i
ponavljamo rekurzivnu formulu dok se ne dostigne granica; kod rekurzivnih funkcija
u programiranju, postupak se odvija u suprotnom smeru i završava bazom koja igra
ulogu kriterijuma zaustavljanja. Rekurzivna funkcija za računanje faktorijela je
long fct(int n) {
return (n>0) ? n*fct(n-1) : 1;
}
Rekurzivni sistem za računanje npr. 4! odvijao bi se ovako:
0! = 1
1! = 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
118
Dušan T. Malbaški - Algoritmi i strukture podataka
Evo i funkcije:
long fct(int n) {
long p;
if(!n) return 1;
else for(p=1;n>1;n--) p*=n;
return p;
}
Iterativno rešenje je fizički duže i deluje pomalo nezgrapno u odnosu na rekurzivno.
Ali je bolje! Naime, dobro je poznato da fizički obim kôda izražen brojem naredbi
nije ni u kakvoj korelaciji sa brzinom izvršenja, pa počesto ni sa obimom zauzete
memorije. S druge strane, svaki rekurzivni poziv fct zahteva pripremu steka sa
prenosom parametra i adrese povratka, te sve to zajedno može da traje duže od
iterativne varijante. Tome treba dodati i cenu elegancije rekurzivnih funkcija: one su,
po pravilu, nejasne i podložne greškama, tako da zahtevaju vrlo iscrpno testiranje. Sve
u svemu, u odgovoru na pitanje “iterativno ili rekurzivno”, Virt u 3 navodi da se
“rekurzivna rešenja koriste za rekurzivne probleme”, što samo na prvi pogled deluje
trivijalno, jer pravi smisao jeste “koristiti iterativno rešenje ako postoji”. Videćemo da
kod struktura podataka, naročito nelinearnih, postoji niz postupaka za koje iterativna
119
Dušan T. Malbaški - Algoritmi i strukture podataka
rešenja jednostavno ne postoje. Jedan primer smo već razmatrali: quicksort je tipičan
rekurzivni problem.
Iterativna i rekurzivna funkcija za računanje faktorijela ne razlikuju se u
tolikoj meri da bi rekurzivno rešenje unapred bilo odbačeno kao neprihvatljivo sporo.
U oba slučaja vremenska kompleksnost je reda 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
120
Dušan T. Malbaški - Algoritmi i strukture podataka
= 2[T(n - 3) + T(n - 4) + b] + b
2[T(n - 4) + T(n - 4) + b] + b
= 22T(n - 4) + 2b + b
= 22[T(n - 5) + T(n - 6) + b] + 2b + b
≥ 23T(n – 6) + (22 + 21 + 20)b
...
2kT(n – 2k) + (2k-1 + 2k-2 + . . . + 21 + 20)b
= 2kT(n – 2k) + (2k – 1)b
Baza rekurzije biće dostignuta kada je n–2k=2, odakle k=(n-2)/2. Prema tome,
T(n) ≥ 2(n – 2)/2 T(2) + [2(n - 2)/2 – 1]b
= (b+a)2(n–2)/2 – b
= [(b+a)/2]*2n/2 – b
Dakle,
T(n)= 2n.
Drugim rečima, radi se o eksponencijalnoj složenosti sa kojom se današnji računari
još uvek ne mogu nositi! Štaviše, poznato je da se T(n) za veće vrednosti n može
aproksimirati sa
1
T(n) ≈ 1.618 n
5
odakle sledi da je T(100) ≈3.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
121
Dušan T. Malbaški - Algoritmi i strukture podataka
122
Dušan T. Malbaški - Algoritmi i strukture podataka
123
Dušan T. Malbaški - Algoritmi i strukture podataka
_qsort(a,L,M-1);
L=M+1;
}
}
Podvucimo da dalja eliminacija rekurzivnih poziva više nije moguća, jer preostali
poziv _qsort više nije poslednja aktivnost u funkciji (iza nje je naredba L=M+1).
4.2. RED
Na prvi pogled, struktura sa nazivom red (engl. queue, čita se „kjū“) tek
neznatno se razlikuje od steka: u pitanju je samo mesto dodavanja. Međutim, odmah
treba naglasiti da je red struktura koja se po ponašanju potpuno razlikuje od steka i
koristi se u sasvim drugom kontekstu. Red se definiše kao uređeni par
F = (S(F),r(F))
sa sledećim osobinama:
struktura je linearna
pristupa se isključivo prvom elementu
uklanja se isključivo prvi element
dodaje se isključivo iza poslednjeg elementa.
Kako vidimo, ovde postoje dva karakteristična elementa: prvi koji se zove još i
početak ili čelo reda i poslednji što nosi nativ kraj reda ili začelje. Red je homogena
struktura. Po osobinama operacija dodavanja i uklanjanja vidimo da se ponašanje reda
svodi na činjenicu da će element koji je ranije ušao u red, ranije biti i uklonjen. Ova
glavna osobina reda nosi naziv FIFO od akronima First In First Out. Ovakvo
ponašanje ispoljava red ispred šaltera, te se kao ključna reč za strukturu reda može
prihvatiti čekanje (što sa ponašanjem steka, očigledno, nema nikakve veze). Šema
izvođenja osnovnih operacija prikazana je na slici 4.7.
...
Slika 4.7.
124
Dušan T. Malbaški - Algoritmi i strukture podataka
Operacija pristupa obično se zove front ili ima prefiks get, operacija dodavanja ima
prefiks put ili append, a operacija uklanjanja prefiks remove ili delete. Operacije
dodavanja i uklanjanja zovu se još i enqueue i dequeue. Pored njih, a s obzirom na to
da su operacije uklanjanja i dodavanja definisane, postoje i operacije provere da li je
red prazan, odnosno da li je red pun. To su operacije isEmpty i isFull. Isto kao i stek, i
red se mora kreirati (operacija create) te, ako koristi hip, mora se i uništiti (operacija
destroy). Ni red nema mnogo dodatnih operacija: one na koje se nailazi su pražnjenje
reda clear i operacija određivanja broja elemenata (dužine) reda, size. Specifikacija
tipičnih operacija je (T je tip elemenata reda)
//prototip: void create(Queue* que);
//parametri: que je adresa reda
//preduslov: -
//postuslov: red je kreiran
//rezultat: -
125
Dušan T. Malbaški - Algoritmi i strukture podataka
126
Dušan T. Malbaški - Algoritmi i strukture podataka
... x y
getPos putPos
0 1 2 c-1
... x y z
getPos putPos
Slika 4.8.
127
Dušan T. Malbaški - Algoritmi i strukture podataka
Kao što je predviđeno u deskriptoru, oznaka getPos odnosi se na indeks čela, a putPos
na mesto iza začelja koje je predviđeno za upis pri izvođenju operacije dodavanja.
Kako se vidi na istoj slici, na donjem crtežu, već posle jednog dodavanja elementa
npr. z, poslednja raspoloživa lokacija u nizu je zauzeta, te bi novi pokušaj dodavanja
rezultovao porukom o prepunjenosti memorijskog prostora! To, očigledno, nije tačno
jer na drugom kraju reda ima mesta, samo se ona ne mogu popunjavati elementarnim
algoritmom za dodavanje. Da bi se ovaj problem rešio, red se realizuje cirkularno
(kružno) tako da se, kada se stigne do kraja memorijskog prostora, sa dodavanjem
nastavlja na početku (naravno, ako ima mesta). Inače, cirkulisanje po nizu realizuje se
primenom operacije ostatka pri deljenju %. Umesto direktnog povećanja nekog
indeksa i za jedan operacijom i++, cirkularno uvećanje obezbeđuje se operacijom
i=(i+1)%c. Cirkularno rešenje prikazano je na slici 7.12 levo.
Nažalost, ovo nije dovoljno jer se pojavljuje novi problem: (stvarno) popunjen
red ne može se razlikovati od praznog reda jer u oba slučaja važi getPos==putPos.
Ova situacija prikazana je na slici 4.9 u sredini i desno..
cirkularni red
Slika 4.9.
Za ovaj problem ima više rešenja, a ovde ćemo opisati jedno, u svoje vreme prilično
originalno. Ideja se sastoji u tome da se žrtvuje jedna lokacija u koju nije dozvoljen
upis, što znači da je memorijski prostor reda za jednu lokaciju veći od stvarnog
kapaciteta. Na taj način ostvaruje se razlika između praznog i punog reda. Dok je kod
praznog reda i dalje getPos==putPos, pun red prepoznaje se po tome što su pozicije
getPos i putPos susedne (u cirkularnom smislu), slika 4.10.
Slika 4.10.
128
Dušan T. Malbaški - Algoritmi i strukture podataka
129
Dušan T. Malbaški - Algoritmi i strukture podataka
item=que->q[que->getPos];
que->getPos=(que->getPos+1)%que->c;
return item;
}
Dodavanje elementa vrši se na indeksu putPos, posle čega se vrednost putPos
cirkularno uvećava za 1:
void putItem(Queue* que,T item) {
que->q[que->putPos]=item;
que->putPos=(que->putPos+1)%que->c;
}
Funkcija za računanje dužine reda ima sledeći oblik:
int size(const Queue* que) {
return (que->putPos>=que->getPos) ?
que->putPos-que->getPos :
que->c+que->putPos-que->getPos;
}
Na kraju, pražnjenje reda je vrlo jednostavno:
void clear(Queue* que) {
que->putPos=que->getPos;
}
Napominjemo da se problem nerazlikovanja praznog i punog reda pri
cirkularnoj realizaciji može rešiti i drukčije: praćenjem dužine reda. U deskriptor
treba uneti dodatno polje, recimo int n koje uvek sadrži aktuelnu dužinu reda. Pri
kreiranju i pražnjenju, polje n postavlja se na vrednost 0. Prilikom svakog uklanjanja,
vrednost polja se smanjuje za 1, a prilikom dodavanja n se povećava za 1. U stanju
praznog reda važi n=0, a za pun red n=c, pri čemu je c sada stvarni kapacitet reda (ne
uvećava se za 1 pri kreiranju). Funkcija size sada vraća vrednost polja n. Inače, cena
ovakve realizacije je izvesno produženje operacija uklanjanja i dodavanja i to zbog
ažuriranja vrednosti n. Programsku realizaciju prepuštamo čitaocu.
130
Dušan T. Malbaški - Algoritmi i strukture podataka
sadržaj
pokazivač na sledeći
elementa
Slika 4.11.
first
last ...
Slika 4.12.
131
Dušan T. Malbaški - Algoritmi i strukture podataka
132
Dušan T. Malbaški - Algoritmi i strukture podataka
n
first
last ...
Slika 4.12.
Dodavanje novog čvora na kraj reda obavlja funkcija putItem sa sledećim kôdom:
void putItem(Queue* que,T item) {
Node *newNode;
newNode=malloc(sizeof(Node));
newNode->item=item;
newNode->next=NULL;
if(que->last==NULL) que->first=que->last=newNode; //dodavanje u prazan red
else que->last=que->last->next=newNode;
que->n++;
}
Prvo se formira novi čvor i na njega usmerava pokazivač newNode. Naredbom if
proverava se specijalni slučaj kada je novododati čvor jedini, a tada se na njega
usmeravaju oba pokazivača iz deskriptora, first i last. Ako red nije bio prazan, na novi
čvor usmeravaju se pokazivač iz bivšeg začelja i pokazivač last. Dužina reda
povećava se za 1. Šema dodavanja za opšti slučaj kada novododati čvor nije jedini
data je na slici 4.13.
n
newNode
first
last ...
Slika 4.13.
133
Dušan T. Malbaški - Algoritmi i strukture podataka
jednostavnija jer se red ne može napuniti, te ona vraća uvek vrednost 0. Određivanje
dužine reda funkcijom size svodi se na očitavanje polja n.
int isEmpty(const Queue* que) {
return que->first==NULL;
}
n tmp pos
first
last ...
Slika 4.14.
134
Dušan T. Malbaški - Algoritmi i strukture podataka
Slika 4.15.
135
Dušan T. Malbaški - Algoritmi i strukture podataka
čvora mora uključiti i prioritet. Pioritet ćemo opisati int poljem priority koje, inače, ne
sme biti deo informacionog sadržaja, jer služi za zadavanje mesta elementa, a ne
njegovu obradu.
typedef struct node {
int priority;
T item;
struct node* next;
} Node;
Uključivanje prioriteta ima najveći uticaj na operaciju dodavanja koja se bitno
razlikuje od verzije kod običnog reda:
//priority je prioritet novog cvora
void putItem(Queue* que, T item, int priority) {
Node *newNode, *pos,*prev;
newNode=malloc(sizeof(Node)); newNode->priority=priority;
newNode->item=item; newNode->next=NULL;
que->n++;
if(que->first==NULL) {que->first=que->last=newNode; return;} //red bio prazan
pos=que->first;
while((pos!=NULL)&&(pos->priority<=priority)) {prev=pos;pos=pos->next;}
if(pos==que->first) {
newNode->next=que->first;que->first=newNode;} //umetanje ispred prvog
else {
prev->next=newNode; newNode->next=pos; //umetanje u sredinu ili na kraj
if(newNode->next==NULL) que->last=newNode; //umetnuti je poslednji
}
}
U prvom bloku naredbi formira se pokazivač na novi čvor, newNode i povećava
brojač čvorova n za 1. Zatim se proverava rubni slučaj kada je red pre dodavanja bio
prazan. Tada se novi čvor uključuje tako što se na njega usmeravaju i pokazivač first i
pokazivač last, te se funkcija završava.
U slučaju da red nije bio prazan, prvo se traži njegovo buduće mesto, a u
skladu sa prioritetom. U ciklusu while traži se prvi element sa manjim prioritetom, da
bi se novi dodao ispred njega. Kada se mesto pos pronađe, proveravaju se dva moguća
136
Dušan T. Malbaški - Algoritmi i strukture podataka
slučaja: umetanje ispred prvog i umetanje negde u sredini, odnosno na kraj. Konačno,
ako se novi element nađe na kraju reda, potrebno je ažurirati pokazivač last. Pomoćni
pokazivač prev služi za to da u svakom trenutku pokaže na čvor koji prethodi pos, jer
se njegov pokazivač next mora usmeriti na novododati čvor (videti sl. 4.15).
4.3. DEK
Struktura podataka nazvana dek (engl. deque, akronim od Double Ended
Queue, red sa dva kraja) predstavlja uopštenje steka i reda, uključujući i ostale
moguće strukture kod kojih se osnovne operacije vrše na krajevima. Osobine deka
čine, na određeni način, sintezu osobina steka i reda. Logičku strukturu definišemo
kao uređeni par
DK = (S(DK),r(DK))
sa digrafom oblika prikazanog na slici 4.16:
...
...
Slika 4.16.
Kao što vidimo, struktura je bilinearna i simetrična, nema „prvog“ niti „poslednjeg“,
pa govorimo o levom i desnom kraju deka. Osnovne operacije su:
pristup krajnjem levom elementu
uklanjanje krajnjeg levog elementa
137
Dušan T. Malbaški - Algoritmi i strukture podataka
dodavanje s leva
pristup krajnjem desnom elementu
uklanjanje krajnjeg desnog elementa
dodavanje s desna
što je prikazano na slici 4.17
...
Slika 4.17.
Dek je struktura koja ima prevashodno teorijsku ulogu, dok se u praksi ređe
primenjuje, upravo zbog toga što objedinjuje karakteristike steka i reda, a videli smo
da su njihove oblasti primene primene potpuno disjunktne.
Fizička struktura deka slična je fizičkoj strukturi reda, jer, kao i red, dek ima
dva pokretna kraja. Može biti sekvencijalna i spregnuta.
Kod sekvencijalne realizacije javlja se problem lažne prepunjenosti i rešava
cirkularnom strukturom što, kao i kod reda, dovodi do problema nerazlikovanja punog
i praznog deka. Problem se rešava uopštenjem rešenja kod reda, gde se mora uzeti u
obzir činjenica da dek cirkuliše u oba smisla, a ne samo u jednom kao red, te su detalji
rešenja, u stvari, duplirani.
Spregnuta realizacija deka takođe se razlikuje od spregnute realizacije reda po
tome što se mora poštovati simetričnost krajeva. U tom smislu, svaki čvor deka
snabdeva se sa dva pokazivača: jedan pokazuje na levog suseda, a drugi na desnog.
Prikazana je na slici 4.18.
n
left
...
right
Slika 4.18.
Operacije su nešto sporije nego kod reda, jer treba ažurirati dva pokazivača, a ne
jedan. Inače, odvijaju se sasvim analogno.
138
Dušan T. Malbaški - Algoritmi i strukture podataka
4.4. SEKVENCA
Sekvenca je poludinamička kontejnerska struktura koja se definiše kao uređeni
par
D=(S(D),r(D))
sa osobinama:
struktura je linearna
dozvoljen je pristup svakom elementu, po pravilu navigacijom
ukloniti se mogu samo svi elementi odjednom
element se dodaje na kraju sekvence
sekvenca je homogena
Ponašanje sekvence prikazano je na slici 4.19.
tekući element
...
Slika 4.19.
Osnovni način pristupa sekvenci jeste navigacija, što znači da, kada je sekvenca u
upotrebljivom stanju, mora biti definisan tzv. tekući element. Prilikom pristupa ne
navodi se pozicija, nego se operacija obavlja implicitno, nad tekućim elementom.
Tekući element određuje se markerom tekućeg elementa koji ukazuje na njegovu
poziciju u sekvenci. Pritom, markerom tekućeg elementa može se markirati i mesto
iza aktuelnog poslednjeg elementa. U tom stanju sekvence pristup se ne može izvesti,
ali je moguće izvršiti operaciju dodavanja (jedino tada je i moguće).
Slično ostalim poludinamičkim (i dinamičkim) strukturama, sekvenca se
koristi samo ako je prethodno dovedena u početno stanje. Operacija kreiranja, između
ostalog, ima za zadatak da definiše poziciju tekućeg elementa, bez koje sekvenca nije
u radnom stanju. Postoje dve varijante operacije pristupa:
pristup u svrhu čitanja informacionog sadržaja elementa
pristup u svrhu izmene informacionog sadržaja elementa
Operacija dodavanja, kako je rečeno, izvodi se isključivo tako što se element dodaje
na kraj sekvence, što znači da marker tekućeg elementa mora da pokazuje na mesto
139
Dušan T. Malbaški - Algoritmi i strukture podataka
iza kraja. Ako to nije slučaj dodavanje se automatski zamenjuje operacijom izmene
informacionog sadržaja.
Posebnu grupu operacija čine operacije vezane za rukovanje tekućim
elementom. U načelu, te operacije ne menjaju sadržaj sekvence, ali menjaju njeno
stanje pošto je stanje sekvence definisano njenom strukturom i sadržajem, ali i
pozicijom tekućeg elementa. U ovu grupu operacija spadaju:
operacija promene pozicije tekućeg elementa
operacija očitavanja pozicije tekućeg elementa
operacija provere da li je marker tekućeg elementa postavljen iza poslednjeg
elementa (uslov za dodavanje u sekvencu)
operacija vraćanja markera na početak sekvence
Dodatne operacije nad sekvencom obuhvataju:
zatvaranje sekvence
uništavanje sekvence
proveru da li je sekvenca prazna
određivanje dužine sekvence (može biti izražena brojem elemenata ili u
bajtovima)
redoslednu obradu sekvence
sortiranje sekvence
U načelu, sekvenca se može realizovati u operativnoj memoriji ili se nalaziti na
nekom drugom medijumu, ali daleko najvažniji slučaj je primena sekvence za
realizaciju tokova (tok, engl. stream), najčešće datoteka. U nastavku, korišćenje
sekvence demonstriraćemo na primeru (binarnih) datoteka u programskom jeziku C.
140
Dušan T. Malbaški - Algoritmi i strukture podataka
sličnom pokazivačima. Razlika u sprezanju blokova je u tome što pokazivači nisu deo
bloka, nego se nalaze u posebnim, pomoćnim strukturama, koje održava operativni
sistem. Ne ulazeći u pojedinosti jer, u zavisnosti od operativnog sistema (čak od
verzije operativnog sistema) postoji čitav niz tehnika, idejno rešenje može se prikazati
kao na slici 4.20.
Slika 4.20.
Inače, ovakva realizacija i jeste razlog zbog kojeg se ne može ukloniti proizvoljan
element, jer bi u tom slučaju oni koji se nalaze iza njega morali biti pomereni, a
pojavila bi se i potreba za premeštanjem elemenata iz bloka u blok, što bi - sve
zajedno - bilo više nego sporo!
Operacija kreiranja pojavljuje se, po pravilu, u tri varijante:
kreiranje prazne datoteke; ako datoteka ne postoji kreira se inicijalno prazna;
ako postoji zatečeni sadržaj se uništava;
kreiranje bez promene sadržaja; ako datoteka nije prazna njen sadržaj ostaje;
marker se postavlja na početak datoteke
kreiranje sa dopunjavanjem; ako datoteka ne postoji kreira se inicijalno
prazna; ako postoji, njen sadržaj se ne menja, a marker se postavlja na kraj
U programskom jeziku paskal, na primer, procedure za kreiranje su redom rewrite,
reset i append. U programskom jeziku C, kreiranje izvodi jedna jedina funkcija,
fopen, gde se način kreiranja zadaje kao argument.
Operacije pristupa i dodavanja u C-u su realizovane kroz dve funkcije:
funkcija fread služi za čitanje sadržaja tekućeg elementa; druga funkcija, fwrite, služi
za upis ili dodavanje. Naime, ako je tekući element neki od aktuelnih elemenata, novi
141
Dušan T. Malbaški - Algoritmi i strukture podataka
sadržaj biće upisan preko starog, tako da se u tom slučaju fwrite ponaša kao funkcija
pristupa. Ako je marker tekućeg elementa iza poslednje aktuelne pozicije, novi sadržaj
biće upisan u novi element, na kraju, što znači da u ovim okolnostima fwrite igra
ulogu operacije dodavanja. Obe ove operacije u svim programskim jezicima praćene
su automatskim pomeranjem markera tekućeg elementa na sledeću poziciju.
Za uklanjanje svih elemenata obično ne postoji posebna operacija, nego se ono
obavlja rekreiranjem inicijalno prazne datoteke.
Upravljanje markerom se u C-u realizuje kroz sledeće funkcije: funkcija fseek
pomera marker na poziciju koja se izračunava iz parametara; očitavanje položaja
markera vrši se funkcijom ftell; provera da li je marker na kraju datoteke obavlja se
funkcijom feof; vraćanje na početak datoteke izvodi se funkcijom rewind.
Kada datoteka više nije potrebna u programu, treba je zatvoriti funkcijom
fclose. Ovo je obavezna radnja da bi se obavile neke završne akcije u nadležnosti
operativnog sistema. Operacija (fizičkog) uništavanja datoteke realizovana je kao
funkcija remove. Za proveru da li je datoteka prazna ne postoji posebna funkcija, nego
se koristi marker: postavi se na kraj datoteke i proveri pozicija: ako je pozicija 0
datoteka je prazna.
Ni za određivanje veličine datoteke u C-u nema posebne funkcije, nego se i to
postiže pomeranjem i očitavanjem položaja markera (videti primer u sledećem
odeljku o sortiranju datoteke).
S obzirom na to da je datoteka sekvenca, a sekvenca linearna struktura,
redosledna obrada vrši se tako što se marker postavi na početnu poziciju (funkcija
rewind), a zatim sledi ciklus while unutar kojeg se čita element funkcijom fread, a
zatim obrađuje. Pošto funkcija fread vraća kao rezultat broj učitanih elemenata, kraj
obrade (tj. kraj ciklusa while) indikovan je vraćenom vrednošću jednakom 0.
142
Dušan T. Malbaški - Algoritmi i strukture podataka
143
Dušan T. Malbaški - Algoritmi i strukture podataka
file
sort F$0
sort F$1
faza
disperzije sort F$2
sort
F$0
F$1
Slika 4.21
144
Dušan T. Malbaški - Algoritmi i strukture podataka
//FAZA DISPERZIJE
for(i=0;i<m;i++) {
//kreiranje segmenta
n=fread(segmentArray,sizeof(Record),segmentSize,file); //citanje segmenta
bubbleSort(segmentArray,n); //sortiranje segmenta
fwrite(segmentArray,sizeof(Record),n,segments[i]); //upis segmenta
145
Dušan T. Malbaški - Algoritmi i strukture podataka
146
Dušan T. Malbaški - Algoritmi i strukture podataka
147
Dušan T. Malbaški - Algoritmi i strukture podataka
5. LISTE
148
Dušan T. Malbaški - Algoritmi i strukture podataka
149
Dušan T. Malbaški - Algoritmi i strukture podataka
item next
Slika 5.1.
n
first ...
Slika 5.2.
Deskriptor sadrži, pre svega, pokazivač na prvi čvor, a pored njega (obično) sadrži i
aktuelnu dužinu (broj elemenata), u oznaci n:
typedef struct node {
T item;
150
Dušan T. Malbaški - Algoritmi i strukture podataka
151
Dušan T. Malbaški - Algoritmi i strukture podataka
152
Dušan T. Malbaški - Algoritmi i strukture podataka
return lst->n;
}
Očitavanje sadržaja item čvora na poziciji position obaviće funkcija
T* getItem(const SimpleList* lst, int position) {
Node *curr; int pos;
for(curr=lst->first,pos=0;pos<position;pos++) curr=curr->next;
return &(curr->item);
}
Pokazivač curr (uobičajena skraćenica od current) postavlja se na prvi čvor liste
preko njegove adrese koja se nalazi u deskriptoru, curr=lst->first. Zatim se prati
pokazivač next svakog čvora i istovremeno prebrojavaju čvorovi kojima je
pristupljeno brojačem pos. Onog trenutka kada pos dostigne vrednost zadate pozicije
position, prolaz kroz listu se završava i rezultat isporučuje kao vrednost polja item
čvora na koji pokazuje pokazivač curr, tj. adresu curr->item. Rezultat je adresa zato
da bi se ostavila mogućnost za izmenu polja item izrazom
*getItem(lst,position)=nova_vrednost_item;
Na ovom primeru najbolje se vidi da je pristup prema poziciji kod liste sporiji nego
kod niza. Ako pretpostavimo da su verovatnoće pristupa svim pozicijama međusobno
jednake i jednake 1/n tada je srednji broj pokušaja prilikom pristupa
n
U= i/n = (n+1)/2 ≈ n/2
i 1
Dakle, vremenska složenost je 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:
n
first ...
prev curr
Slika 5.3.
153
Dušan T. Malbaški - Algoritmi i strukture podataka
prev=curr=lst->first;
for(prev=curr=lst->first,pos=0;pos<position;pos++) {prev=curr; curr=curr->next;}
if(pos==0) lst->first=curr->next; //uklanjanje prvog
else prev->next=curr->next; //uklanjanje cvora koji nije prvi
itm=curr->item;
lst->n--;
free(curr);
return itm;
}
Da bi se moglo izvršiti prevezivanje prethodnika čvora za uklanjanje na njegovog
sledbenika, kroz listu se prolazi pomoću dva pokazivača: curr i prev (od engl.
previous). Tokom prolaska kroz listu pokazivač curr pokazuje na element kojem se
upravo pristupa, a prev na njegovog prethodnika. Dok se pokazivačem curr prolazi
kroz listu, brojač pos odbrojava pozicije sve dok se ne stigne do pozicije position na
kojoj se nalazi čvor za uklanjanje. Kada se do te pozicije stigne, vrši se prevezivanje
tako što se pokazivač next iz čvora curr kopira u pokazivač next njegovog prethodnika
preko pokazivača prev. Rubni slučaj o kojem se mora voditi računa jeste uklanjanje
čvora sa pozicije 0 (dakle, prvog po redu). U tom slučaju, njegov pokazivač next
kopira se u polje first deskriptora. Zatim se čvor oslobađa funkcijom free, smanjuje
brojač elemenata list->n i polje item uklonjenog elementa vraća kao rezultat.
Dodavanje u listu jednostavno je pošto, po pretpostavci, može da se doda na
bilo koje mesto. Da bi se izbegao prolazak kroz čvorove novi čvor dodaje se isped
prvog, slika 5.4.
newNode
n
first ...
Slika 5.4.
154
Dušan T. Malbaški - Algoritmi i strukture podataka
newNode->item=item;
newNode->next=lst->first;
lst->first=newNode;
lst->n++;
}
Konačno, funkcija za pražnjenje liste veoma podseća na istu funkciju kod spregnuto
realizovanog reda:
void clear(SimpleList* lst) {
Node *curr=lst->first,*tmp;
while(curr) {
tmp=curr;
curr=curr->next;
free(tmp);
}
lst->first=NULL;
lst->n=0;
}
Prvo, ako je lista već prazna funkcija se odmah završava. Ako nije, u nastavku se
pokazivačem curr sukcesivno pristupa čvorovima. Pre nego što se pređe na sledeći
čvor operacijom curr=curr->next pokazivač curr memoriše se u privremenoj
promenljivoj tmp. Kada se izvrši prelaz na sledeći, čvor na koji pokazuje tmp
oslobađa se funkcijom free. Na kraju, polje deskriptora first postavlja se na NULL, a
broj čvorova na vrednost 0.
Redosledna obrada jednostavne liste je jedna od zamki u koje mogu da
upadnu neiskusni programeri. Na prvi pogled, sve je jednostavno:
for(position=0;position<size(&lst);position++) obraditi getItem(&lst, position);
gde je lst lista koja se obrađuje. Međutim, ako se zna kako funkcioniše pristup prema
poziciji (tj. funkcija getItem), jasno je da će ovakvom rešenju programer pribeći samo
u krajnjem očajanju: naime, prilikom svakog pristupa funkcija kreće od početka liste,
tražeći zadatu poziciju, a to se, jednostavno, ne radi. Dobro pripremljen softver za
jednostavnu listu treba da sadrži i posebnu funkciju za redoslednu obradu. Takve
funkcije - inače, operacija te vrste zove se iterator - imaju za zadatak da obezbede
sukcesivno pristupanje elementima na pozicijama 0,1,2... bez vraćanja na početak.
155
Dušan T. Malbaški - Algoritmi i strukture podataka
156
Dušan T. Malbaški - Algoritmi i strukture podataka
dodaje na proizvoljnom mestu nego na mestu koje je određeno veličinom ključa i koje
prethodno treba naći. Pošto prvi slučaj umnogome liči na jednostavnu listu,
pozabavićemo se listom koja je u svakom trenutku sortirana po, recimo, rastućoj
vrednosti ključa. Definicije tipa čvora i tipa same liste su
typedef struct node {
int key;
T item;
struct node* next;
} Node;
typedef struct {
int n;
Node *first;
} KeyedList;
Funkcije za kreiranje, proveru da li je lista prazna, određivanje broja elemenata i
pražnjenje liste su iste kao kod jednostavne liste:
void create(KeyedList* lst) {
lst->first=NULL;
lst->n=0;
}
157
Dušan T. Malbaški - Algoritmi i strukture podataka
free(tmp);
}
lst->first=NULL;
lst->n=0;
}
Funkcija za pristup na bazi ključa (tj. za traženje) bazira se na linearnom traženju45:
polazi se od prvog čvora i redom traži čvor u kojem se ključ poklapa sa argumentom
traženja arg. Postupak se završava kada se čvor nađe ili kada se stigne do čvora u
kojem je ključ veći od argumenta traženja, u kojem slučaju to znači da je traženje
neuspešno. Prosečan broj upoređivanja u oba slučaja je približno n/2, gde je n broj
čvorova (videti odeljak 3.2.1).
T* getItem(const KeyedList* lst,int arg) {
Node *curr=lst->first;
while((curr->next)&&(curr->key<arg)) curr=curr->next;
return (curr->key==arg) ? &(curr->item) : NULL;
}
Funkcija vraća pokazivač na polje item nađenog čvora (NULL ako nije nađen). Ovo
zato da bi se to polje moglo modifikovati. Zapazimo da je ključ u čvoru nedostupan za
modifikovanje jer bi to moglo da dovede do dupliranja ključeva, što se ne sme
dogoditi.
Uklanjanje čvora vrši se funkcijom
int removeItem(KeyedList* lst,int key) {
Node *curr,*prev;
prev=NULL; curr=lst->first;
while(curr&&(curr->key<key)) {prev=curr; curr=curr->next;} //trazenje cvora
if(!curr||(curr->key!=key)) return 0; //nije nadjen
if(!prev) lst->first=curr->next; //cvor je bio prvi
else prev->next=curr->next; //cvor nije bio prvi
free(curr);
lst->n--;
return 1;
45
Binarno traženje u listi bilo bi nedopustivo sporo, jer nema indeksiranja
158
Dušan T. Malbaški - Algoritmi i strukture podataka
}
Funkcija vraća vrednost 1 ako je uklanjanje uspešno i 0 u suprotnom (kada čvor za
uklanjanje nije nađen). U ciklusu while prate se istovremeno markirani čvor curr i
njegov prethodnik prev, sve dok se ne naiđe na čvor sa ključem koji je veći ili jednak
argumentu traženja, odnosno dok se ne stigne do kraja liste. Sledećom naredbom if
proverava se da li dostignuti čvor ima traženi ključ ili ga nema i u potonjem slučaju
funkcija se završava neuspehom. Ako je čvor nađen postoje dve alternative: ako se
uklanja prvi čvor treba ažurirati polje first liste, a ako čvor nije prvi treba ažurirati
polje next njegovog prethodnika prev. Čvor se briše funkcijom free i broj čvorova
smanjuje za 1.
Funkcija za dodavanje putItem je posebna, s obzirom na to da joj je zadatak ne
samo dodavanje novog čvora nego i održavanje stanja sortiranosti liste:
int putItem(KeyedList* lst, int key, T item) {
Node *newNode,*curr,*prev;
prev=NULL; curr=lst->first;
while(curr&&(curr->key<key)) {prev=curr; curr=curr->next;} //trazenje mesta
if(curr&&(curr->key==key)) return 0; //dupliran kljuc
newNode=malloc(sizeof(Node)); //formiranje novog cvora
newNode->key=key; newNode->item=item;
if(!prev) {newNode->next=lst->first; lst->first=newNode;} //novi cvor je prvi
else {newNode->next=curr;prev->next=newNode;} //novi cvor nije prvi
lst->n++;
return 1;
}
Prvo se ciklusom while traži mesto na kojem će se naći novi čvor, s obzirom na
veličinu njegovog ključa (parametar key). Kada se ciklus završi mora se proveriti da li
čvor sa ključem key već postoji u listi u kojem se slučaju od dodavanja odustaje i
funkcija vraća rezultat 0. Ako je mesto za novi čvor nađeno i ključ nije dupliran, prvo
se formira novi čvor koji treba dodati. Zatim se proveravaju dva moguća slučaja: ako
je novododati čvor prvi u listi ažurira se polje first, a ako nije ažurira se polje next
čvora prev koji prethodi novododatom. Povećava se broj čvorova n i vraća kôd
uspešnosti jednak 1.
159
Dušan T. Malbaški - Algoritmi i strukture podataka
160
Dušan T. Malbaški - Algoritmi i strukture podataka
n
first A B ... NE!
n
first A B ... DA
Slika 5.5.
161
Dušan T. Malbaški - Algoritmi i strukture podataka
...
Slika 5.6.
162
Dušan T. Malbaški - Algoritmi i strukture podataka
Slika 5.7.
Pokazivač left pokazuje na levog suseda, a right na desnog. Polje item tipa T
predstavlja informacioni sadržaj čvora. Na ovom primeru pokazaćemo još jednu
tehniku primenljivu na sve spregnute strukture sa deskriptorom (ne samo na ovu vrstu
dvostruko spregnute liste). Ako se osvrnemo na realizaciju jednostruko spregnute liste
iz prethodnih primera, videćemo da se u deskriptoru nalazi pokazivač first na početak
liste. Ovaj pokazivač se po formatu razlikuje od čvora, što realizaciju čini unekoliko
neuniformnom. Tehnika koju ćemo prikazati, umesto pokazivača na levi i desni kraj
liste sadržaće kvazi-čvorove, po formatu iste kao i čvorovi liste. Ovi kvazielementi
nose naziv sentineli (od engleskog sentinel=stražar), po tipu su čvorovi i čine neku
vrstu graničnika između kojih se nalaze čvorovi liste. Pri tom, levi sused levog
sentinela je NULL pokazivač, kao i desni sused desnog sentinela. Upotreba sentinela
pojednostavljuje realizaciju funkcija liste, jer se sentineli programski tretiraju isto kao
i čvorovi. Na slici 5.8 prikazana je realizacija dvostruko spregnute liste sa
navigacijom, uz upotrebu sentinela.
n
backupCurrent
current
* leftSentinel ...
rightSentinel *
Slika 5.8.
163
Dušan T. Malbaški - Algoritmi i strukture podataka
164
Dušan T. Malbaški - Algoritmi i strukture podataka
165
Dušan T. Malbaški - Algoritmi i strukture podataka
backupCurrent 0 n
-
current
* leftSentinel
rightSentinel *
Slika 5.9.
166
Dušan T. Malbaški - Algoritmi i strukture podataka
završetku lread pomera jedno mesto ulevo; ako je na tom mestu konstanta RIGHT
marker će se pomeriti za jedno mesto udesno; ako je argument direction jednak
CURRENT tekući element ostaje isti. Skrećemo pažnju i na primenu standardne
funkcije memcpy koja na memorijsku lokaciju zadatu adresom prvog argumenta šalje
sadržaj memorijske lokacije čija je adresa drugi argument, dok treći argument
označava broj bajtova koji se kopiraju.
Funkcija lwrite ponaša se istovetno, s tom razlikom što se informacioni sadržaj
upisuje u tekući element:
int lwrite(DoubleList* lst,void* item, int itemsize, int direction) {
if(isSentinel(lst)) return 0;
memcpy(&(lst->current->item),item,itemsize);
switch(direction) {
case RIGHT: lst->current=lst->current->right; break;
case LEFT: lst->current=lst->current->left;
}
return 1;
}
S obzirom na to da je sve isto kao kod lread, osim smera razmene podataka, na ovoj
funkciji nećemo se više zadržavati.
Operacija uklanjanja čvora, removeItem, realizovana je funkcijom koja uklanja
tekući element:
int removeItem(DoubleList* lst) {
Node *tmp;
if(isSentinel(lst)) return 0;
tmp=lst->current;
lst->current->left->right=lst->current->right;
lst->current->right->left=lst->current->left;
lst->current=(lst->current->right!=&lst->rightSentinel) ? lst->current->right : lst-
>current->left;
free(tmp);
lst->n--;
return 1;
}
167
Dušan T. Malbaški - Algoritmi i strukture podataka
current
Slika 5.10.
168
Dušan T. Malbaški - Algoritmi i strukture podataka
if(isEmpty(lst)) {
newNode->left=&lst->leftSentinel; //lista bila prazna
newNode->right=&lst->rightSentinel;
lst->leftSentinel.right=lst->rightSentinel.left=newNode;}
else switch(direction){
case LEFT:
newNode->right=lst->current;
newNode->left=lst->current->left;
lst->current->left->right=newNode;
lst->current->left=newNode;
break;
case RIGHT:
newNode->left=lst->current;
newNode->right=lst->current->right;
lst->current->right->left=newNode;
lst->current->right=newNode;
}
lst->current=newNode;
lst->n++;
return 1;
}
U funkciji se prvo proverava da li je u nepraznoj listi marker tekućeg elementa na
sentinelu, u kojem slučaju se odustaje od operacije sa rezultatom 0. Ako to nije slučaj,
formira se novi čvor i, u zavisnosti od mesta dodavanja, novi čvor vezuje levo
odnosno desno od tekućeg. Novi čvor postaje tekući, dužina liste povećava se za 1 i
vraća kôd uspešnosti jednak 1. Na slici 5.11 prikazana je šema dodavanja, za slučaj da
se dodaje levo od tekućeg (direction=LEFT). Dodavanje desno od tekućeg izvodi se
analogno.
current
Slika 5.11.
169
Dušan T. Malbaški - Algoritmi i strukture podataka
Pažljiv čitalac može postaviti pitanje „šta se dešava ako (greškom) parametar
direction dobije vrednost koja nije ni LEFT ni RIGHT“? Odgovor nije trivijalan i
vezan je čak za stil programiranja. U jezicima tipa paskala obaveza programera koji
realizuje listu bila bi da ugradi proveru te okolnosti, i da obezbedi da se konstatuje
greška. Programski jezik C, u principu, nije takav: zbog brzine (a provera traje!),
egzotične omaške se jednostavno ne uzimaju u obzir i odgovornost za korektnu
upotrebu funkcija liste prepušta se programeru koji ih koristi. To je razlog zbog kojeg
u funkciji putItem, u naredbi switch nema treće default varijante (dakle, ni LEFT ni
RIGHT) u kojoj bi se rad funkcije prekinuo sa porukom o grešci.
Upravljanje pozicijom tekućeg elementa obavlja se primenom dve funkcije,
lseek i ltell čiji je oblik ponovo inspirisan odgovarajućim funkcijama kod datotečnog
tipa u C-u.
int lseek(DoubleList* lst, int offset, int base) {
Node *nod; int i=0;
switch(base) {
case LEFT: nod=lst->leftSentinel.right; break;
case RIGHT: nod=lst->rightSentinel.left; break;
case CURRENT: if(isSentinel(lst)) return 0; else nod=lst->current;
}
if(offset>0) while((i++<offset)&&nod->left&&nod->right) nod=nod->right;
else while((i-->offset)&&nod->left&&nod->right) nod=nod->left;
if(!(nod->left&&nod->right)) return 0; //ako je sentinel vrati 0
lst->current=nod;
return 1;
}
Funksija lseek postavlja pokazivač current na čvor koji je zadat parametrima base i
offset. Pozicija tekućeg elementa zadaje se relativno, u odnosu na tri moguće baze:
može se računati u odnosu na levi kraj, u odnosu na desni kraj i u odnosu na trenutni
tekući element. U prvom slučaju parametar base treba postaviti na vrednost
simboličke konstante LEFT, u drugom na vrednost RIGHT i za slučaj da se nova
pozicija računa u odnosu na tekući, na vrednost CURRENT. Parametar offset
predstavlja rastojanje od odabrane baze izraženo brojem čvorova. Primeri za listu lst:
lseek(&lst,0,LEFT) //postavljanje na levi kraj
170
Dušan T. Malbaški - Algoritmi i strukture podataka
171
Dušan T. Malbaški - Algoritmi i strukture podataka
172
Dušan T. Malbaški - Algoritmi i strukture podataka
173
Dušan T. Malbaški - Algoritmi i strukture podataka
174
Dušan T. Malbaški - Algoritmi i strukture podataka
typedef char T;
int main()
{
DoubleList lst; T itm; int i;
printf("KREIRANA LISTA");
create(&lst);
showDoubleList(&lst);
printf("\n\nUPISANI a,b,c,d,e,f");
putItem(&lst,'a',RIGHT);
175
Dušan T. Malbaški - Algoritmi i strukture podataka
putItem(&lst,'b',RIGHT);
putItem(&lst,'c',RIGHT);
putItem(&lst,'d',RIGHT);
putItem(&lst,'e',RIGHT);
putItem(&lst,'f',RIGHT);
showDoubleList(&lst);
176
Dušan T. Malbaški - Algoritmi i strukture podataka
printf("\nCurrent: %d",ltell(&lst));
printf("\n\nSORTIRANJE LISTE");
clear(&lst);
// upis slucajno odabranih znakova u listu
for(i=0;i<20;i++) putItem(&lst,65+rand()%26,RIGHT);
printf("\nPolazna lista: ");
showDoubleList(&lst);
insertionSort(&lst);
printf("\nSortirana lista: ");
showDoubleList(&lst);
printf("\n\nLISTA ISPRAZNJENA");
clear(&lst);
showDoubleList(&lst);
printf("\n\n");
return 0;
}
Izlaz koji generiše demosntracioni program treba da izgleda ovako:
KREIRANA LISTA
SADRZAJ LISTE: Prazna. Size: 0
PROMENJEN TEKUCI U x
SADRZAJ LISTE: b c x a Current: 2
177
Dušan T. Malbaški - Algoritmi i strukture podataka
UPISANI a,b,c,d,e,f
SADRZAJ LISTE: a b c d e f Current: 5
SORTIRANJE LISTE
Polazna lista:
SADRZAJ LISTE: P H Q G H U M E A Y L N L F D X F I R C Current: 19
Sortirana lista:
SADRZAJ LISTE: A C D E F F G H H I L L M N P Q R U X Y Current: 0
178
Dušan T. Malbaški - Algoritmi i strukture podataka
LISTA ISPRAZNJENA
SADRZAJ LISTE: Prazna. Size: 0
5.3. MULTILISTA
Multilista ili višestruka lista je struktura podataka koja se dobija
superpozicijom dve ili više jednostruko ili dvostruko spregnutih listi sastavljenih od
čvorova koji čine podskup istog skupa. Osnovna namena multiliste jeste da obezbedi
efikasno pretraživanje, uz ostale standardne operacije nad listama. Shodno tome,
glavna vrsta pristupa je pristup po informacionom sadržaju, kako prema ključu, tako i
prema vrednostima pôlja koja nisu ključ. Kako ta polja moraju da se definišu unapred,
običaj je da se ona nazivaju sekundarnim ključevima. Na slici 5.12 prikazana je šema
jedne multiliste.
Slika 5.12.
179
Dušan T. Malbaški - Algoritmi i strukture podataka
ML = (S(ML),r1,...,rn)
gde je S(ML) skup elemenata, dok su r1,...,rn binarne relacije u tom skupu takve da
svaki uređeni par (si,ri), 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
180
Dušan T. Malbaški - Algoritmi i strukture podataka
181
Dušan T. Malbaški - Algoritmi i strukture podataka
6. STABLO
a
b
e
c d
f
Slika 6.1.
Čvor a ima ulazni stepen 0 i izlazni stepen 2. Čvor b ima ulazni stepen 1 i izlazni
stepen 2. Niz čvorova i grana x1v1x2v2x3v3...xnvnxn+1 takav da grana vi predstavlja ili
par (xi,xi+1) ili par (xi+1,xi) nosi naziv lanac dužine n. U digrafu sa slike lanci su
abefgd (dužina:4), abef(dužina:3), bdgf (dužina:3) ac (dužina:1) ili ca (dužina:1).
Očigledno, dužina lanca jednaka je broju grana u lancu i za 1 manja od broja čvorova.
Put dužine n je skup čvorova i grana x1v1x2v2x3v3...xnvnxn+1 u kojem je vi=(xi,xi+1).
182
Dušan T. Malbaški - Algoritmi i strukture podataka
Zapaža se da se put može shvatiti kao lanac u kojem se poštuje orijentacija svake
grane. Među lancima koje smo naveli osobine puta imaju abef i ac. Digraf je slabo
povezan ako je svaki par čvorova povezan bar jednim lancem. Digraf sa slike jeste
slabo povezan.
Na osnovu navedenih definicija možemo oformiti i definiciju orijentisanog
stabla (u daljem tekstu izostavljaćemo reč „orijentisano“). Dakle, stablo je digraf za
koji važi:
1. postoji tačno jedan čvor sa ulaznim stepenom 0 (koji se zove koren)
2. svi ostali čvorovi imaju ulazni stepen 1
3. digraf je slabo povezan.
b c
d e f g
h
i j k l
Slika 6.2.
Na slici 6.2 prikazano je jedno stablo čiji koren je čvor a. S obzirom na to da se ovde
bavimo isključivo konačnim digrafovima, u stablu moraju postojati i neki čvorovi sa
izlaznim stepenom 0. Čvor sa izlaznim stepenom 0 nosi naziv list. Čvorovi e, g, h, i, j,
k i l su listovi stabla sa slike 6.2. U uvodnom delu pominjali smo termine prethodnik i
sledbenik za čvorove koji su povezani granom od prethodnika do sledbenika. Za
stabla je uobičajeno da se umesto ovog para termina koriste respektivno nadređeni i
podređeni čvor. Na primer, čvor a je nadređen čvorovima b i c, koji su mu podređeni.
Uočimo još jednu osobinu stabla koja je od izuzetne važnosti za odgovarajuće
strukture podataka: iz korena vodi put u svaki od preostalih čvorova stabla. Broj
čvorova na najdužem putu u stablu nosi naziv visina stabla. Najduži putevi (koji,
uzgred, uvek počinju u korenu, a završavaju se u listu) u stablu sa slike su putevi
abdh, acfi, acfj, acfk i acfl, što znači da je visina ovog stabla jednaka 4. Pod redom
stabla podrazumeva se najveći izlazni stepen čvora u njemu (u našem stablu to je čvor
f, tako da je ono reda 4).
183
Dušan T. Malbaški - Algoritmi i strukture podataka
Na slici 6.2 primećuje se još jedna opšta, formalno dokaziva, osobina stabla:
svaki čvor stabla zajedno sa svojim podređenim, njihovim podređenim itd. do nivoa
lista čine strukturu koja je opet stablo. Neki čvor x u stablu sa svim svojim
podređenim čvorovima, njihovim podređenim itd. nosi naziv podstablo generisano
čvorom x.
Hijerarhijski nivo čvora u stablu jeste dužina puta od korena do tog čvora.
Hijerarhijski nivo korena je uvek 0. Hijerarhijski nivo čvora b sa slike je 1, čvora e je
2, a čvorova npr. i i j je 3.
Digraf tipa stabla ima još čitav niz različitih osobina, no nisu sve u vezi sa
stablom kao strukturom podataka. Sledeće dve osobine su za stablo kao strukturu
podataka značajne. Prvo, kompletno stablo reda n je stablo u kojem svi čvorovi osim
listova imaju izlazni stepen n. Drugo, puno stablo je stablo u kojem su putevi od
korena do proizvoljnog lista iste dužine. Zapazimo da su ove dve osobine, kako se to
kaže, ortogonalne što znači da su međusobno nezavisne. Na slici 6.3 levo prikazano je
kompletno stablo reda 3 koje nije puno; desno se nalazi puno stablo koje nije
kompletno.
Slika 6.3.
Slika 6.4.
184
Dušan T. Malbaški - Algoritmi i strukture podataka
46
u sledećim odeljcima biće data i alternativna definicija stabla
185
Dušan T. Malbaški - Algoritmi i strukture podataka
podređeni itd. U datom stanju, mesta za podređene ne moraju sva biti zauzeta: neki
element može imati prvog podređenog, trećeg podređenog itd., dok je pozicija broj 2
nezauzeta. Na slici 6.5 prikazano je jedno takvo stablo. Za svaki element podređeni
broj 1 (ili levi podređeni) odgovara odnosu "majka", a podređeni broj 2 (desni
podređeni) odnosu "otac".
Apolon
Leta Zevs
Slika 6.5.
Sledeći primer genealoškog stabla ilustruje slučaj kada neki od podređenih ne postoji
(prema grčkoj mitologiji, Atena je rođena iz Zevsovog bedra, te nema majku):
Atena
Zevs
Rea Kron
Slika 6.6.
Kod generalisanog stabla, podređeni na logičkom nivou čine skup, što znači da je
njihov međusobni raspored proizvoljan. Na slici 6.7 prikazano je stablo u kojem
nadređeni element i njegove podređene vezuje relacija "biti subdirektorijum".
My Files
Slika 6.7.
186
Dušan T. Malbaški - Algoritmi i strukture podataka
X X X
A B B A A B
Slika 6.8.
187
Dušan T. Malbaški - Algoritmi i strukture podataka
Osnovna vrsta pristupa kod n-arnog stabla jeste pristup prema informacionom
sadržaju. Neka je k0 argument traženja (tj. ključ traženog elementa). Neka je k(e) ključ
elementa e. Traženje uvek počinje od korena. Ako je ključ korena jednak argumentu
traženja postupak je završen. Ako nije, na bazi nekog kriterijuma vezanog za k0 i
koren bira se podređeni element i za njega se postupak ponavlja. Traženje se uspešno
završava kada se pristupi elementu sa ključem k0.Traženje se neuspešno završava
kada se stigne do lista. Neka je sub(k0,e) funkcija koja odražava kriterijum izbora
podređenog u postupku traženja i koja za rezultat vraća odabrani element podređen
elementu e, ili pak specijalnu vrednost koja dogovorno označava da takvog elementa
nema. Na slici 6.9 prikazan je opšti postupak traženja u n-arnom stablu.
ekoren
da
e= element nije pronađen
ne
da
k(e)=k0 element je pronađen
ne
esub(k0,e)
Slika 6.9.
188
Dušan T. Malbaški - Algoritmi i strukture podataka
ponašanje algoritma. Očigledno, broj poređenja pri traženju ne može biti veći od
visine stabla h. Kako je
h
N= n i -1
= (nh-1)/(n-1)
i 1
sledi
h = logn[1+N(n-1)]
te, kada je N>>n>1,
max(Tn(N)) = O[lognN]
što znači da, za slučaj perfektno balansiranog n-arnog stabla, algoritam traženja spada
u najbrže moguće.
Kada su u pitanju operacije uklanjanja odnosno dodavanja elementa, uopšteni
algoritmi ne postoje, jer se razlikuju u zavisnosti od konkretne vrste n-arnog stabla.
Ono što je, međutim, zajedničko za sve njih, jeste činjenica da se u svakom trenutku
element može i dodati i ukloniti, ali tako da definiciona ograničenja - red n i uređenje
u skupu podređenih - ne budu narušena.
Slika 6.10.
189
Dušan T. Malbaški - Algoritmi i strukture podataka
B C D
E F G H
Slika 6.11.
deskriptor
B * C * * * D *
E * * * F * * * G * * * H * * *
Slika 6.12.
190
Dušan T. Malbaški - Algoritmi i strukture podataka
B C
D E
F G
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
A B C D E F G
Slika 6.13.
191
Dušan T. Malbaški - Algoritmi i strukture podataka
i n-i-1
Slika 6.14.
tj.
192
Dušan T. Malbaški - Algoritmi i strukture podataka
N 1
1
U(N) =
N2
[ N iU(i) (N - i - 1)U(N - i - 1)]
i 0
47
tipičan primer rekurzivnog problema koji zahteva rekurzivno rešenje
193
Dušan T. Malbaški - Algoritmi i strukture podataka
B C
D E F
G H I
Slika 6.15.
Međutim, i za čvor B važi isto: pre nego što se obradi, mora se obraditi njegovo levo
podstablo, te se i adresa čvora B odlaže u istu pomoćnu strukturu S. Pristupa se čvoru
D, pa se i on odlaže u pomoćnu strukturu S, jer ima levo podstablo. Tako se dolazi do
čvora G koji nema levo podstablo i taj čvor je prvi čvor koji se obrađuje. Pošto čvor G
nema desno podstablo, treba obraditi čvor koji je njemu nadređen, a to je čvor D čija
194
Dušan T. Malbaški - Algoritmi i strukture podataka
195
Dušan T. Malbaški - Algoritmi i strukture podataka
196
Dušan T. Malbaški - Algoritmi i strukture podataka
Ako bi stablo izgledalo kao na slici 6.15, a tip T bio char, tj.
typedef char T;
uz funkciju za obradu čvora
void printItem(Node* x) {
printf("%c ",x->item);
}
koja prikazuje polje item na ekranu, tada bi poziv
inorder(&bt,printItem);
gde je promenljiva bt tipa BinTree, generisao izlaz oblika
GDHBAECIF
Obilazak binarnog stabla koristi se i prilikom njegovog pražnjenja (brisanja).
Pritom, najpogodnije je koristiti obilazak s dna ka vrhu jer element koji se upravo
uklanja, u trenutku uklanjanja nema podređene (ostale metode bi zahtevale
privremeno memorisanje jednog od podstabala). Funkcija clear za brisanje stabla ima
sledeći oblik:
//Preduslov: -
//Postuslov: stablo bt je prazno
void clearNode(Node* x) {
free(x);
}
void clear(BinTree* bt) {
_postorder(bt->root,clearNode);
bt->root=NULL;
}
197
Dušan T. Malbaški - Algoritmi i strukture podataka
A 0
statusno polje
B 1 C 0
D 0 * E 1 F 0*
pokazivač prošivka
* G 1 * H 1 * I 1
Slika 6.16.
198
Dušan T. Malbaški - Algoritmi i strukture podataka
0 A 0
0 B 1 0 C 0
0 D 0 1 E 1 0 F 0*
leva prošivka
* G 1 1 H 1 1 I 1 desna prošivka
Slika 6.17.
199
Dušan T. Malbaški - Algoritmi i strukture podataka
tehnika koja će biti primenjena samo tada kada se očekuje intenzivna primena
postupka obilaska s leva u desno.
Među postupcima obilaska binarnog stabla postoji još jedan, manje pominjan
postupak, a to je obilazak binarnog stabla po nivoima. Odvija se tako što se prvo
obradi koren (nivo 0), zatim njegovi neposredni podređeni (nivo 1), pa njihovi
neposredno podređeni (nivo 2) itd. Postupak podseća na rekurziju, samo što posrednik
pri ovoj vrsti „rekurzije“ nije stek nego red. Osnova algoritma je sledeća:
1. upisati u red koren stabla
2. ponavljati sledeće: pročitati element na početku reda; ukloniti ga; ako je
pročitani element neprazan (nije NULL) obraditi ga i u red ubaciti levog i
desnog podređenog
3. postupak završiti kada se red isprazni.
Realizacija postupka obilaska po nivoima ima sledeći oblik:
void traverseByLevel(BinTree *bst,void (*visit)(Node* x)) {
Queue que; Node *node;
createQueue(&que);
node=bst->root; putItemQueue(&que,node);
while(!isEmptyQueue(&que)) {
node=frontQueue(&que); removeItemQueue(&que);
if(node) {
visit(node);
putItemQueue(&que,node->left);
putItemQueue(&que,node->right);
}
}
}
Funkcija traverseByLevel koristi spregnuto realizovani red que (videti odeljak 4.2.2).
Da bi se izbeglo dupliranje identifikatora, nazivima funkcija koje koristi red dodat je
sufiks Queue tako da su promenjeni u createQueue, isEmptyQueue, frontQueue,
removeItemQueue i putItemQueue. Parametar visit je, kao i u prethodnim primerima,
funkcija koja vrši obradu elementa. Ilustrovaćemo primenu obilaska stabla po nivoima
obradom koja predstavlja prikaz sadržaja stabla na ekranu. Svaki element stabla
200
Dušan T. Malbaški - Algoritmi i strukture podataka
201
Dušan T. Malbaški - Algoritmi i strukture podataka
algoritma pristupa na bazi ključa (dakle, traženja). Binarno stablo pristupa odlikuje se
sledećim definicionim osobinama:
pre svega, stablo je binarno
elementi su snabdeveni ključevima
za svaki element stabla važi: ključevi elemenata u njegovom levom podstablu
manji su od njegovog ključa, dok su ključevi u desnom podstablu veći.
Na slici 6.18 prikazano je jedno binarno stablo pristupa. Smatraćemo da su ključevi
celobrojni, što nema uticaja na opštost razmatranja, jer sve što je potrebno da bi se
elementi smestili u binarno stablo pristupa jeste da ključevi mogu da se urede u
sekvencu, po rastućim vrednostima. Takođe, na slici su prikazani samo ključevi, a
informacioni sadržaj se podrazumeva.
50
30 80
20 40 60 90
10 35 55 67 95
62 75
70
Slika 6.18.
202
Dušan T. Malbaški - Algoritmi i strukture podataka
može naći samo u desnom podstablu, a ako je manji samo u njegovom levom
podstablu.
ekoren
da
k(e)= element nije pronađen
ne
da
k(e)=k0 element je pronađen
ne
< >
k0:k(e)
eleft(e) eright(e)
Slika 6.19.
Korišćene oznake iste su kao na slici 6.9, s tim da se oznaka left(e) odnosi na levog
podređenog za element e, a oznaka right(e) isto za desnog. Oznaka se i dalje odnosi
na „prazan“ element koji znači da prethodna primena left ili right nije dala rezultat. U
(spregnutoj) fizičkoj realizaciji, to će uvek značiti da je pokazivač dobijen primenom
left ili right jednak NULL. Takođe oznake < i > treba shvatiti uopšteno, kao “ispred”
odn. “iza” u smislu poretka ključeva. Dakle, traženje počinje od korena. Ako je
argument traženja k0 jednak ključu tekućeg elementa, traženje je uspešno završeno.
Ako je argument traženja manji prelazi se u levo podstablo, a ako je veći u desno i
postupak se ponavlja. Ako se stigne do lista traženje je završeno neuspehom.
Neka je binarno stablo pristupa definisano na sledeći način:
typedef struct node {
K key;
T item;
struct node *left,*right;
} Node;
typedef struct {
Node *root;
} BinSearchTree;
Funkcija traženja, sa imenom getItem zamišljena je tako da vrati pokazivač na polje
item traženog čvora (čiji je ključ parametar key) ili vrednost NULL ako čvor nije
nađen. Lokalna promenljiva curr služi za prolazak kroz stablo.
203
Dušan T. Malbaški - Algoritmi i strukture podataka
204
Dušan T. Malbaški - Algoritmi i strukture podataka
50 100
30 40 55 80
20 40 60 90
10 35 55 67 95
62 75
70
Slika 6.20.
50
30 80
20 40 60 90
10 35 47 55 67 95
15 62 75
dodati elementi 70
Slika 6.21.
205
Dušan T. Malbaški - Algoritmi i strukture podataka
Node *curr,**insPoint;
curr=bst->root; insPoint=&(bst->root);
while(curr) {
if(key<curr->key) {insPoint=&(curr->left); curr=curr->left;}
else if(key>curr->key) {insPoint=&(curr->right); curr=curr->right;}
else {curr->item=item; return 0;} //kljuc dupliran; promeniti sadrzaj
}
*insPoint=malloc(sizeof(Node));
(*insPoint)->key=key; (*insPoint)->item=item;
(*insPoint)->left=(*insPoint)->right=NULL;
return 1;
}
Pokazivač curr koristi se za prolazak kroz stablo. Uočimo promenljivu insPoint koja
je pokazivač na tip Node*. Njen zadatak je da obezbedi adresu lokacije u koju treba
upisati pokazivač na novi element. Na početku izvršavanja funkcije, insPoint sadrži
adresu polja root u deskriptoru. U nastavku, pre prelaza u levo podstablo čvora curr, u
insPoint se upisuje adresa polja curr->left, a ukoliko se prelazi u desno podstablo u
insPoint se upisuje adresa polja curr->right. Na taj način postiže se cilj, da u svakom
trenutku upis u *insPoint pokazivača na novi čvor označava povezivanje novododatog
elementa sa čvorom curr. U trenutku kada curr dobije vrednost NULL, promenljiva
insPoint sadrži adresu polja u koje treba upisati pokazivač na novi čvor. Konačno, ako
čvor sa ključem key već postoji u stablu, menja se samo sadržaj polja item i funkcija
se završava uz povratnu vrednost 0. Kôd iza naredbe while predstavlja formiranje
novog čvora i upis u stablo na mesto određeno ranije vrednošću *insPoint. U slučaju
da je stablo prošireno novim čvorom, funkcija vraća vrednost 1. Inače, funkciju smo
mogli realizovati i tako što ćemo, u slučaju dupliranja ključeva, zabraniti upis (i toga
ima u praksi). Sve što treba uraditi je promena treće linije naredbe if iz
else {curr->item=item; return 0;}
u
else return 0;
Uklanjanje čvora iz binarnog stabla pristupa nije trivijalan postupak, upravo
zbog očuvanja BST invarijante. Naime, kada se čvor za uklanjanje pronađe, postavlja
se pitanje šta dalje? U opštem slučaju „prevezivanje“ njegovih podređenih čvorova na
206
Dušan T. Malbaški - Algoritmi i strukture podataka
njegov nadređeni nije moguće ako uklonjeni čvor ima oba podređena, prosto zato što
u nadređenom nema mesta za tri pokazivača, slika 6.22. Za rešavanje ove poteškoće
postoje dva osnovna postupka
1. logičko brisanje i
2. Hibardova metoda.
nadređeni
uklanja se
Slika 6.22.
207
Dušan T. Malbaški - Algoritmi i strukture podataka
30 30
* 20 * 40 *
uklanja se
* 10 *
* 35 * uklanja se
Slika 6.23.
Problem, u stvari, nastaje kada element d ima oba podređena. U tom slučaju postupak
je sledeći:
pronaći element e čiji je ključ prvi manji od ključa elementa d (alternativa:
prvi veći); taj element ne može imati desnog podređenog zbog invarijante BST
prepisati ključ i informacioni sadržaj tog elementa u element d, a zatim
osloboditi element e, pri čemu je prevezivanje njegovog (eventualnog)
podređenog sada moguće.
Element čiji je ključ prvi manji (prvi veći) od ključa elementa d dobija se
jednostavnim postupkom:
1. preći u levo podstablo elementa d (alternativa: desno); ovde su elementi čiji su
ključevi manji (alternativa: veći) od ključa elementa d
2. pratiti desne pokazivače (alternativa: leve) sve dok se ne dođe do čvora koji
nema desnog (alternativa: levog) podređenog; to je traženi element.
Postupak za slučaj da element koji se uklanja (čvor sa ključem 80) ima oba podređena
prikazan je na slici 6.24.
208
Dušan T. Malbaški - Algoritmi i strukture podataka
50
80
75
60
* 55 * 67
* 62 * 75 *
briše se
* 70 *
Slika 6.24.
Funkcija za uklanjanje, pod imenom removeItem ima za parametre sâmo stablo i ključ
čvora koji se uklanja. Vraća vrednost 1 ako je uklanjanje uspelo, a vrednost 0 ako
nije, tj. ako čvora za uklanjanje nema u stablu.
Preduslov: -
Postuslov: ako postoji, cvor sa kljucem key je uklonjen
Rezultat: 1 ako je uklanjanje uspesno ili 0 ako trazenog cvora nema
int removeItem(BinSearchTree* bst, K key) {
Node *curr, *loc, **delPoint;
//trazenje cvora
curr=bst->root; delPoint=&(bst->root);
while(curr)
if(key<curr->key) {delPoint=&(curr->left); curr=curr->left;}
else if(key>curr->key) {delPoint=&(curr->right); curr=curr->right;}
else break; //nadjen
if(!curr) return 0; //nije nadjen
if(!curr->left) *delPoint=curr->right; //nema levog podredjenog
else if(!curr->right) *delPoint=curr->left; //nema desnog podredjenog
else { //element ima oba podredjena
loc=curr; //memorisati mesto elementa predvidjenog za brisanje
curr=curr->left; //preci u levo podstablo
while(curr->right) {delPoint=&(curr->right); curr=curr->right;} //trazenje prvog
manjeg
209
Dušan T. Malbaški - Algoritmi i strukture podataka
210
Dušan T. Malbaški - Algoritmi i strukture podataka
bst->root=_putItem(bst->root,key,item);
}
Postupak je neznatno izmenjen u odnosu na iterativnu varijantu, utoliko što u slučaju
da element sa ključem key već postoji, neće biti prekida već će se bez izmene ključa
upisati novi informacioni sadržaj item. Razlog za ovu izmenu je taj što, u slučaju već
postojećeg ključa, nije moguće na jednostavan način prekinuti sa izvršavanjem uz
poruku o neuspehu. Ostavljamo čitaocu da analizira ponašanje ove funkcije.
Rekurzivna varijanta uklanjanja čvora takođe je zasnovana na istom opštem
pristupu (Hibardovom) kao i iterativna. Verzija koju navodimo takođe se razlikuje od
iterativne po tome što, za slučaj da čvora za brisanje nema u stablu, ne generiše
nikakav rezultat.
//rekurzivno uklanjanje
//Preduslov: -
//Postuslov: uklonjen cvor node, ako postoji
Node* _removeItem(Node* node, K key) {
if(!node) return NULL;
if(key<node->key) node->left=_removeItem(node->left,key);
else if(key>node->key) node->right=_removeItem(node->right,key);
else { //cvor nadjen
Node *temp;
if(!node->left) {temp=node->right; //nema levog podredjenog
free(node);
node=temp;}
else if(!node->right) {temp=node->left; //nema desnog podredjenog
free(node);
node=temp;}
else { //ima oba podredjena
temp=node->left; //pronaci najveci u levom podstablu
while(temp->right) temp=temp->right;
node->key=temp->key; node->item=temp->item;
node->left=_removeItem(node->left,temp->key);
}
}
211
Dušan T. Malbaški - Algoritmi i strukture podataka
return node;
}
void removeItem(BinSearchTree* bst, K key) {
bst->root=_removeItem(bst->root,key);
}
U prvom delu funkcije traži se čvor koji treba ukloniti, pri čemu se na stek smeštaju
adrese svih čvorova na putu od korena do čvora za brisanje. Kada se (tj. ako se) čvor
pronađe, sledi provera broja podređenih. Ako čvor za uklanjanje nema više od jednog
podređenog, taj podređeni se vraća kao rezultat da bi na višem nivou rekurzije bio
dodeljen nadređenom uklonjenog čvora, ostvarujući tako prevezivanje. U slučaju da
čvor za uklanjanje ima oba podređena, kao i u iterativnoj varijanti, traži se najveći u
levom podstablu, njegov ključ i sadržaj prepisuju se u čvor za brisanje, a na levo
podstablo primenjuje se rekurzivno uklanjanje tog, najvećeg.
Iako fizički kratke, rekurzivne varijante su, u stvari, nešto sporije od
iterativnih, a glavni razlog je utrošak vremena na pripremu steka prilikom rekurzivnih
poziva. S druge strane, rekurzivne verzije imaju važnu primenu za slučajeve kada
dodavanje i uklanjanje imaju dodatne radnje vezane, recimo, za balansiranje (videti
sledeće odeljke), a koje, sa svoje strane, zahtevaju da na raspolaganju bude čitav put
od korena do čvora za dodavanje-brisanje.
212
Dušan T. Malbaški - Algoritmi i strukture podataka
h--;}
return hmax;
}
unsigned height(BinTree* bt) {
return _height(bt->root);
}
Ideja se sastoji u tome da se u svakom koraku prati rastojanje h čvora koji se obrađuje
od korena. Na početku, za vrednosti h i maksimalnog rastojanja hmax stavlja se 0.
Potom se obilazi stablo, određuje h za svaki čvor te, ako je tekuća vrednost h veća od
hmax, promenljiva hmax povećava se na tu, veću vrednost. Na kraju rekurzije,
najveće rastojanje od korena hmax jednako je visini stabla. Ovde lako prepoznajemo
klasični algoritam određivanja maksimuma, s tom specifičnošću što se prolaz kroz sve
čvorove stabla obavlja nekom od rekurzivnih procedura (u našem primeru to je
postorder). U realizaciji valja uočiti da svaki rekurzivni poziv podrazumeva prelazak
na podređenog, što znači da se tekuća vrednost h povećava za 1. Na mestu gde se u
rekurzivnoj funkciji _postorder nalazi obrada čvora, ustvari se prelazi na nadređeni
čvor, što ovde znači smanjenje h za 1 (naredba h--;). Naredbom
if(++h>hmax) hmax=h;
ažurira se vrednost najvećeg konstatovanog rastojanja u toku obilaska. Da bi se u toku
pojedinačnih koraka rekurzije očuvale vrednosti h i hmax, one su realizovane kao
statičke lokalne promenljive. Konačno, prva linija koda
if(!h) hmax=0;
obezbeđuje da se prilikom svakog otpočinjanja rekurzije, vrednost statičke
promenljive hmax vrati na nulu. Otpočinjanje nove rekurzije prepoznaje se po tome
što je tada tekuća visina h obavezno jednaka 0. Inače, i ovde važi napomena da
zamenom segmenta
_height(node->left);
_height(node->right);
ciklusom sa n pristupa svim podređenim, postupak određivanja visine može da se
proširi na stabla reda većeg od 2.
Uopšte uzev, što je visina stabla veća, algoritmi manipulacije stablom su
sporiji i obrnuto. Najgori slučaj je uvek binarno stablo degenerisano u linearnu
213
Dušan T. Malbaški - Algoritmi i strukture podataka
strukturu u kojoj svaki element ima samo jednog podređenog, kada su svi algoritmi
reda O[N]. Najbolji slučaj je kada, za zadati broj elemenata, stablo ima najmanju
visinu, a to je slučaj kada je stablo (perfektno) balansirano: tada su algoritmi reda
O[log2N]. Prema tome, ako hoćemo brze algoritme, stablo treba da bude balansirano,
ili bar blisko balansiranom.
Postoje dva opšta pristupa rešavanju problema balansiranja stabla: nezavisnim
procedurama (off-line balansiranje) i dinamičko (on-line) balansiranje.
Off-line balansiranje podrazumeva da se softver za upravljanje binarnim
stablom snabdeva posebnim potprogramom čiji je zadatak da već postojeće stablo
dovede u balans. Nažalost, algoritmi za balansiranje stabla nisu jednostavni i
generalno nisu reda O[log2N], te bi to značilo da utrošak vremena za balansiranje
praktično anulira prednosti koje stablo pruža u pogledu osnovnih operacija. Posebna
procedura za balansiranje ima još jedan nedostatak: primenjuje se na poziv, u trenutku
kada se zaključi da se, zbog dodavanja i uklanjanja, stablo udaljilo od balansa. Ovo
pak usporava klijentski softver i to u nepredvidivim vremenskim trenucima, ako se
procedura uključuje automatski. Posebne procedure za balansiranje imaju smisla kada
je stablo koncipirano tako da se, s vremena na vreme, mora rekonfigurisati, recimo za
stabla koja imaju logičko brisanje. Pošto se takva stabla i inače moraju
rekonfigurisati, u proceduru za rekonfigurisanje ugrađuje se i segment za balansiranje.
Standardni pristup održavanju performanse na visokom nivou zahtevao bi da
se balansiranje vrši dinamički, tj. da se stablo održava u balansiranom stanju.
Nažalost, ovo bitno posložnjava i usporava operacije uklanjanja i dodavanja, tako da
se ponovo ugrožava glavna prednost stabla - brzina osnovnih operacija. U svrhu
usklađivanja protivurečnih zahteva - balansiranost i brze operacije uklanjanja i
dodavanja - formiraju se postupci koji imaju za cilj da približe stablo balansu.
Postupaka ima više, i svi su dosta efikasni, u smislu da zadržavaju osnovne operacije
na nivou vremenske kompleksnosti od 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
214
Dušan T. Malbaški - Algoritmi i strukture podataka
rotaciju lakše programski realizovati nego objasniti. Funkcije imaju za parametar čvor
oko kojeg se vrši rotacija, a za rezultat vraćaju pokazivač na čvor koji je zamenio
mesto sa x. U slučaju leve rotacije to je njegov desni podređeni, a u slučaju desne
rotacije njegov levi podređeni.
//rotate left
Node* rotL(Node* x) {
Node *node = x->right;
x->right = node->left;
node->left = x;
return node;
}
//rotate right
Node* rotR(Node* x) {
Node *node = x->left;
x->left = node->right;
node->right = x;
return node;
}
rotL(x)
z
x rezultat: z
x
y z
y D
C
A B C D
A B
Slika 6.25.
Smisao obe varijante rotacije vidi se na slikama 6.25 i 6.26. Naime, rotacijom čvora
ulevo skraćuje se desno podstablo čvora z koji je nasledio čvor x na mestu korena
podstabla, a produžava se levo podstablo. Prilikom rotiranja udesno situacija je
obrnuta. Jenom rečju, rotacijom se može promeniti visina čitavog podstabla
generisanog čvorom x, zadržavajući pritom invarijantu BST. Neka je, na primer,
215
Dušan T. Malbaški - Algoritmi i strukture podataka
rotR(x)
y
x rezultat: y
x
y z
A z
A B C D B
C D
Slika 6.26.
216
Dušan T. Malbaški - Algoritmi i strukture podataka
48
U daljem tekstu termin “balans” odnosiće se na visinski balans
217
Dušan T. Malbaški - Algoritmi i strukture podataka
//rotate left
Node* leftRotate(Node* x) {
Node *y = x->right;
x->right = y->left;
y->left = x;
//azurirati visinu
x->h=max(geth(x->left),geth(x->right))+1;
y->h=max(geth(y->left),geth(y->right))+1;
return y;
}
//rotate right
Node* rightRotate(Node* x) {
Node *y = x->left;
x->left = y->right;
y->right = x;
//azurirati visinu
x->h=max(geth(x->left),geth(x->right))+1;
y->h=max(geth(y->left),geth(y->right))+1;
218
Dušan T. Malbaški - Algoritmi i strukture podataka
return y;
}
U svrhu skraćivanja izvornog kôda, dodate su tri makrodirektive: makrodirektiva
max(A,B) za određivanje veće od vrednosti A i B, makrodirektiva geth za određivanje
visine čvora i makrodirektiva getBalance za računanje faktora balansa (trebaće u
nastavku). Direktiva geth očitava polje h, ali samo ako pokazivač NODE pokazuje na
čvor, dok u suprotnom vraća vrednost NULL. Visina čvora određuje se kao veća od
visina levog i desnog podređenog uvećana za 1 da bi se uračunao i sam čvor.
Makrodirektiva getBalance vraća faktor balansa ako čvor nije NULL, odnosno
vrednost 0 u suprotnom.
Prilikom umetanja novog čvora u stablo, do disbalansa može doći kod
neposrednog nadređenog, ali i kod nadređenog višeg nivoa, pod uslovom da se nalazi
na putu od korena do novododatog čvora. To pak znači da funkcija za dodavanje mora
čuvati u memoriji taj put, a to opet sugeriše da je prirodno rešenje rekurzivna funkcija.
Dakle, osnovu za operaciju dodavanja činiće rekurzivna varijanta funkcije putItem
binarnog stabla pristupa, opisana na kraju odeljka 6.3.3. Rekurzivna funkcija _putItem
za dodavanje u AVL stablo odvija se u sledećim fazama:
u prvoj, pronalazi se mesto za dodavanje čvora standardnom rekurzivnom
varijantom metode (ako element sa zadatim ključem već postoji, menja mu se
polje item i postupak se završava)
u drugoj fazi vrši se visinsko balansiranje čvorova u smeru ka korenu; pošto se
radi o rekurzivnoj funkciji, na steku se nalazi kompletan put (adrese čvorova)
od čvora nadređenog novododatom do korena.
Ako je dodavanje izazvalo disbalans u nekom čvoru, taj čvor mora biti negde na putu
do korena, što znači da će razrešavanje rekurzije ranije ili kasnije dovesti do tog
čvora. Neka je a novododati čvor. Neka je z čvor u kojem je detektovan disbalans, y
njegov neposredni podređeni, a x neposredni podređeni čvora y. Čvorovi x, y i z
moraju se nalaziti na putu ka korenu, te prema tome i na susednim frejmovima steka.
Moguća su četiri slučaja:
1. y je levi podređeni z i x je levi podređeni y (slučaj levi-levi)
2. y je levi podređeni z i x je desni podređeni y (slučaj levi-desni)
3. y je desni podređeni z i x je desni podređeni y (slučaj desni-desni)
4. y je desni podređeni z i x je levi podređeni y (slučaj desni-levi)
219
Dušan T. Malbaški - Algoritmi i strukture podataka
levi-levi
z y
y t4 x z
rightRotate(z)
x t3 t1 t2 t3 t4
t1 t2
Slika 6.27a
levi-desni
z z
y t4 x t4
leftRotate(y)
t1 x y t3
t2 t3 t1 t2
rightRotate(z) y z
t1 t2 t3 t4
Slika 6.27b
desni-desni
z y
t1 y z x
leftRotate(z)
t2 x t1 t2 t3 t4
t3 t4
Slika 6.27c
220
Dušan T. Malbaški - Algoritmi i strukture podataka
desni-levi
z z
t1 y t1 x
rightRotate(y)
x t4 t2 y
t2 t3 t3 t4
leftRotate(z) z y
t1 t2 t3 t4
Slika 6.27d
221
Dušan T. Malbaški - Algoritmi i strukture podataka
node->item=item;
return node; //u slucaju dupliranja kljuca promeniti polje item i zavrsiti
}
//azurirati visinu
node->h = max(geth(node->left), geth(node->right)) + 1;
//izracunati faktor balansa
int balance = getBalance(node);
//ako je na cvoru node disbalans, postoje 4 slucaja
//levi-levi
if (balance > 1 && key < node->left->key)
return rightRotate(node);
//desni-desni
if (balance < -1 && key > node->right->key)
return leftRotate(node);
//levi-desni
if (balance > 1 && key > node->left->key) {
node->left = leftRotate(node->left);
return rightRotate(node);
}
//desni-levi
if (balance < -1 && key < node->right->key) {
node->right = rightRotate(node->right);
return leftRotate(node);
}
//zavrsiti
return node;
}
Radi jednostavnosti korišćemka, kao i do sada, zatvorićemo ovu funkciju u omotač
oblika
void putItem(AVLTree* avlt,K key,T item) {
avlt->root=_putItem(avlt->root,key,item);
}
222
Dušan T. Malbaški - Algoritmi i strukture podataka
49
uočiti razliku u odnosu na dodavanje!
223
Dušan T. Malbaški - Algoritmi i strukture podataka
node=temp;
} else { //ima oba podredjena
temp=node->left; //pronaci najveci u levom podstablu
while(temp->right) temp=temp->right;
node->key=temp->key;
node->item=temp->item;
node->left=_removeItem(node->left,temp->key);
}
}
//ako je stablo imalo samo jedan cvor zavrsii
if(!node) return NULL;
//azurirati visinu tekuceg cvora
node->h=max(geth(node->left),geth(node->right))+1;
//izracunati faktor balansa
int balance=node ? geth(node->left)-geth(node->right) : 0;
//ako je na cvoru disbalans postoje 4 slucaja
//levi-levi
if(balance<-1 && getBalance(node->left)>=0) return rightRotate(node);
//levi-desni
if(balance>1 && getBalance(node->left)<0) {
node->left=leftRotate(node->left);
return rightRotate(node);
}
//desni-desni
if(balance<-1 && getBalance(node->right)<=0) return leftRotate(node);
//desni-levi
if(balance<-1 && getBalance(node->right)>0) {
node->right=rightRotate(node->right);
return leftRotate(node);
}
return node;
}
224
Dušan T. Malbaški - Algoritmi i strukture podataka
225
Dušan T. Malbaški - Algoritmi i strukture podataka
226
Dušan T. Malbaški - Algoritmi i strukture podataka
x->left=node;
setSize(node); setSize(x);
return x;
}
//rotiranje udesno
Node* rotateRight(Node* node) {
Node *x=node->left;
node->left=x->right;
x->right=node;
setSize(node); setSize(x);
return x;
}
Za određivanje veličine podstabla generisanog čvorom (nalazi se u polju n), koristimo
makrodirektivu getSize, jer pokazivač može biti i NULL u kojem slučaju se smatra da
je pomenuta veličina jednaka 0. Za ažuriranje broja čvorova u takvom podstablu
koristimo makrodirektivu setSize koja zbraja veličine levog i desnog podstabla i
dodaje 1 da bi bio uračunat i koren.
S S S
E X E X E X
C R C R C G
H G R
G H H
S G
G X E S
E R C R X
C H H
Slika 6.28
227
Dušan T. Malbaški - Algoritmi i strukture podataka
hijerarhiji i to tako da, ako je G levi podređeni, tada se vrši desna rotacija, a kada je
desni podređeni, rotacija je obrnuta. Prilikom izvođenja rotacije, kao što i piše u
izvornom kodu, mora se podešavati veličina novog podstabla, a razlog se jasno vidi na
slici.
Rekurzivna funkcija za korensko dodavanje u stohastičko stablo izgleda ovako:
//Dodavanje u koren podstabla
Node* putRoot(Node* node,K key,T item) {
if(!node) {
node=malloc(sizeof(Node));
node->key=key; node->item=item;
node->left=node->right=NULL;
node->n=1;
return node;
}
//trazenje mesta za dodavanje
if(key==node->key) node->item=item; //duplikat kljuca
else if (key<node->key) {
node->left=putRoot(node->left,key,item);
node=rotateRight(node);}
else {
node->right=putRoot(node->right,key,item);
node=rotateLeft(node);}
return node;
}
Sve dok se ne naiđe na list, funkcija ostvaruje rekurzivni prolaz kroz stablo. Kada je
dostignut list, formira se novi čvor i u povratnoj fazi rekurzije vrši se rotacija tako da
ako je novi čvor dodat na mestu desnog podređenog rotacija je ulevo, a u suprotnom
rotacija je udesno. Tako se nastavlja sve do korena postabla. Krajnji rezultat jeste da
se novi čvor nalazi na mestu korena podstabla na koje je funkcija putRoot primenjena.
Funkcija uzima u obzir i slučaj kada se u podstablu već nalazi element sa zadatim
ključem key (duplikat ključa): u tom slučaju samo se upisuje novi sadržaj u polje item.
228
Dušan T. Malbaški - Algoritmi i strukture podataka
229
Dušan T. Malbaški - Algoritmi i strukture podataka
već opisano. Posle korenskog dodavanja, funkcija putRoot vraća kontrolu funkciji
_putItem koja nastavlja povratnu fazu standardnim postupkom.
Odluka o tome hoće li biti prelaska na korensko dodavanje donosi se prilikom
prelaska na niži nivo stabla u prvoj fazi traženja mesta za dodavanje50. Odluka se
donosi stohastički na bazi verovatnoće da novododati čvor bude koren nekog
podstabla. Neka je x čvor koji treba dodati. Neka je u postupku traženja mesta za
dodavanje algoritam stigao do čvora a, što znači da će se x sigurno naći u podstablu
generisanom čvorom a. Neka je broj čvorova u tom podstablu m (u funkciji se broj
čvorova odeđuje makrodirektivom getSize). Kada bi redosled dodavanja bio slučajan,
čvor x bi u tom podstablu bio koren (umesto a) sa verovatnoćom 1/(m+1)51. Da bi se
postigao efekat slučajnog dodavanja, pri nailasku na čvor a treba sa verovatnoćom
1/(m+1) aktivirati funkciju putRoot koja će izvršiti pomenuti zadatak. Mehanizam za
postizanje ovog cilja jesu pseudoslučajni brojevi koji se u programskom jeziku C
dobijaju primenom funkcije rand(). Funkcija rand generiše uniformno raspodeljene
celobrojne pseudoslučajne brojeve u nekom rasponu [0,RAND_MAX], gde je
RAND_MAX simbolička konstanta. S obzirom na uniformnost raspodele ovih brojeva,
vrednost
rand()%(m+1)
biće jednaka nuli sa verovatnoćom 1/(m+1), a to je tražena verovatnoća potrebna za
uključivanje korenskog dodavanja. Konačno, u slučaju da element sa zadatim ključem
već postoji u stablu (dupliran ključ), samo se upisuje novi sadržaj u polje item.
Funkcija _putItem zatvorena je u funkciju-omotač putItem koja se poziva iz
klijenta. Funkcija vraća vrednost 1 ako je stablo prošireno novim elementom, odnosno
0 ako je ključ bio dupliran, a to se ostvaruje jednostavnim poređenjem broja čvorova u
stablu pre i posle primene funkcije _putItem.
//Rekurzivno dodavanje u stablo
//Preduslov: -
//Postuslov: dodat cvor; ako je vec postojao izmenjeno polje item
//Rezultat: 1 ako je cvor dodat; 0 ako je vec postojao
int putItem(RandomTree* rt,K key,T item) {
int sizeBefore;
50
uočiti da se na korensko dodavanje prelazi samo jednom
51
podstablo ima m zatečenih elemenata kojima se dodaje element x
230
Dušan T. Malbaški - Algoritmi i strukture podataka
sizeBefore=getSize(rt->root);
rt->root=_putItem(rt->root,key,item);
return getSize(rt->root)-sizeBefore; //provera da li je bilo dodavanja
}
Ponašanje stohastičkog stabla ilustrovaćemo (ne i analizirati!) tabelom u kojoj
su dati visina i prosečan broj poređenja pri uspešnom traženju52, a za stohastičko
stablo koje je generisano sukcesivnim dodavanjem elemenata sa ključevima u
rastućem poretku. Inače, to bi kod standardnog binarnog stabla pristupa rezultovalo
najgorim slučajem - stablom degenerisanim u linearnu strukturu:
broj visina srednji
čvorova broj
pristupa
10 6 3,7
100 12 6.9
1000 24 12,7
5000 34 16,0
10000 36 16,7
30000 39 19,2
50000 40 19,6
100000 41 20,5
Čak i bez posebne analize, očigledno je da sa naglim rastom broja čvorova visina
stabla i srednji broj pristupa imaju vrlo umeren rast koji je za vrlo veliko stablo
gotovo zanemarljiv.
Operacija uklanjanja čvora iz stohastičkog stabla oslanja se na tzv. spajanje
(engl. join) kojom se od dva binarna stabla pristupa dobija jedno. Uslov za operaciju
join jeste da su ključevi u jednom stablu veći od svih ključeva u drugom. Neka su L i
R dva binarna stabla pristupa sa korenima respektivno a i b, takva da su ključevi u
stablu R veći od ključeva u stablu L. Da bi se očuvala BST invarijanta, moguća su dva
načina spajanja:
1. učiniti stablo R desnim podstablom najvećeg čvora u L, koji se dobija
praćenjem desnih pokazivača u L; koren novog stabla postaje a
2. učiniti stablo L levim podstablom najmanjeg čvora u R, koji se dobija
praćenjem levih pokazivača u R; koren novog stabla postaje b.
Dva načina spajanja prikazana su na slici 6.29.
52
podaci su dobijeni jednostavnim eksperimentom
231
Dušan T. Malbaški - Algoritmi i strukture podataka
a b
L R
R L
Slika 6.29
Stohastičko uklanjanje uzima u obzir obe varijante, s tim što se konkretna varijanta
bira na probabilističkoj osnovi, a na osnovu veličine stabala L i R. U načelu, algoritam
je sasvim jednostavan: polazi se od korena i prati desni (levi) pokazivač sve dok se ne
stigne do čvora koji ga nema. Za taj čvor vezuje se koren drugog stabla. Ipak, funkcija
koju ćemo koristiti biće rekurzivna jer se posle spajanja mora ažurirati polje n (broj
čvorova) u svakom čvoru i to unatrag. Funkcija sa nazivom join za parametre ima
korene dva stabla i ima sledeći oblik:
//Spajanje dva stabla
//Preduslov: kljucevi u stablu b su veci od kljuceva u stablu a
//Postuslov: stabla spojena. Koren je ili a ili b
//Rezultat: koren novog stabla
Node* join(Node* a,Node* b) {
if(!a) return b;
if(!b) return a;
if(rand()%(getSize(a)+getSize(b))<getSize(a)) {
a->right = join(a->right,b);
setSize(a);
return a;
} else {
b->left = join(a,b->left);
setSize(b);
return b;
}
232
Dušan T. Malbaški - Algoritmi i strukture podataka
}
Rezultat funkcije je koren novog stabla dobijenog spajanjem. Algoritam funkcioniše
tako što se praćenje desnih pokazivača u stablu L i levih pokazivača u stablu R odvija
simultanim postupcima, kao da se „utrkuju“ koji će pre stići do pokazivača NULL. To
„napredovanje“ ka čvoru NULL odvija se tako što se u svakom koraku rekurzije sa
verovatnoćom N(L)/(N(L)+N(R)) prelazi na desnog podređenog u stablu L, a sa
verovatnoćom N(L)/(N(L)+N(R)) prelazi na levog podređenog u stablu R. Ako je
“pobednik” krajnji desni čvor stabla L, koren stabla R vezuje se za njega. Obrnuto,
ako je u pitanju krajnji levi čvor stabla R, tada se koren stabla L vezuje za njega. U
povratnoj fazi rekurzije ažuriraju se veličine podstabala generisanih elementima na
putu do čvora ispod kojeg je dodato drugo stablo.
Slika 6.30
233
Dušan T. Malbaški - Algoritmi i strukture podataka
} else if(key<node->key )
node->left=_removeItem(node->left,key);
else
node->right=_removeItem(node->right,key);
setSize(node);
return node;
}
I ovu funkciju zatvorićemo u omotač removeItem tako da, na osnovu upoređivenja
veličine stabla pre i posle pokušaja uklanjanja, vrati kôd uspešnosti.
//Uklanjanje cvora
//Preduslov: -
//Postuslov: uklonjen cvor ako ga ima
//Rezultat: 1 ako je cvor uklonjen; 0 ako nije postojao
int removeItem(RandomTree* rt,K key) {
int sizeBefore;
sizeBefore=getSize(rt->root);
rt->root=_removeItem(rt->root,key);
return sizeBefore-getSize(rt->root); //provera da li je bilo uklanjanja
}
234
Dušan T. Malbaški - Algoritmi i strukture podataka
C
B C D
C
E C
F G H
C X
Slika 6.31
Imena elemenata su redom A, B, C itd. Identifikator elementa npr. F bio bi put ABF.
Element X dodat je ispod elementa D i predstavlja list. Kada se uklanja element B, sa
njim se uklanja podstablo sastavljeno od B, E, F i G. Pošto je osnovni način pristupa
navigacija, operacija se uvek odvija nad tekućim elementom. Ako je potrebno
pristupiti nekom elementu koji nije tekući, prethodno se mora proglasiti za tekući, pri
čemu operacija redefinisanja tekućeg elementa zahteva da se zada identifikator novog
tekućeg elementa. Kada je identifikator putanja postoje dva načina: jedan je da se
zada tzv. apsolutna putanja, tj. put od korena do novog tekućeg elementa. Drugi način
jeste da se mesto novog tekućeg odredi u odnosu na trenutni tekući (tzv. relativna
putanja). Ovaj drugi način zahteva da se uopšteno stablo snabde operacijom prelaska
na nadređeni element, koja je po matematičkoj prirodi funkcija, jer element ne može
imati više od jednog nadređenog. Neka je F tekući element i neka je potrebno
proglasiti H za novi tekući element. Ako se mesto novog tekućeg zadaje apsolutnom
putanjom, argument odgovarajuće operacije bio bi A-D-H. U slučaju da se koristi
235
Dušan T. Malbaški - Algoritmi i strukture podataka
pokazivač na
sledeći u listi
informacioni
sadržaj
pokazivač na
početak liste
podređenih
Slika 6.32.
236
Dušan T. Malbaški - Algoritmi i strukture podataka
deskriptor
koren A *
tekući
B C * D *
E * F * G * * H * *
Slika 6.33
Inače, postoji još jedan pristup realizaciji uopštenog stabla, nazvan prirodnom
realizacijom, jer se svaka veza iz logičke strukture direktno realizuje pokazivačem u
fizičkoj strukturi. Ovaj način realizacije nije naročito pogodan zbog zahteva da svi
čvorovi stabla budu istog tipa, što bi bilo u koliziji sa promenljivim brojem
pokazivača u čvoru. Kod prirodne realizacije, broj podređenih se, zato, mora
ograničiti, čime se, u stvari, realizacija prevodi u realizaciju n-arnog stabla.
Ograničavanje reda stabla može da izazove prekoračenje, pri čemu je situacija kod
stabla znatno rizičnija nego u drugim slučajevima sekvencijalno realizovane
promenljive strukture i to zato što do prekoračenja može doći u svakom čvoru
ponaosob. To je razlog zbog kojeg prirodna realizacija nije osobito popularna.
237
Dušan T. Malbaški - Algoritmi i strukture podataka
LITERATURA
238