You are on page 1of 133

37.

DINAMIČKE
STRUKTURE PODATAKA

Do sada smo pretežno koristili


takve strukture podataka čija je
veličina uvijek bila tačno
poznata u trenutku njihovog
kreiranja. U slučaju običnih
nizova, njihova veličina morala
je biti poznata još u vrijeme
pisanja programa, dok je
dinamička alokacija memorije
omogućavala odgodu zadavanja
veličine niza do trenutka
njegovog kreiranja. Međutim, u
oba slučaja, prilikom kreiranja
veličina niza je morala biti
zadana. S druge strane,
dinamičke strukture podataka
mijenjaju svoju veličinu tokom
rada: dodavanjem novih
podataka u dinamičku strukturu
njihova veličina raste, a
vađenjem podataka iz
dinamičke strukture njihova
veličina se smanjuje.

Dinamičke strukture podataka


ne postoje kao ugrađeni tipovi
podataka u jeziku C++, ali se
mogu relativno lako napraviti
vještim korištenjem pokazivača
i dinamičke alokacije memorije.
Mada se za kreiranje
dinamičkih struktura podataka
ne moraju koristiti klase (što
omogućava njihovo kreiranje i u
jeziku C koji ne poznaje klase),
pogodnost enkapsulacije i
sakrivanja informacija koju
pružaju klase razlog su što se u
jeziku C++ dinamičke strukture
podataka realiziraju gotovo
isključivo kao klase. Stoga
ćemo se i mi odlučiti za takav
pristup. Treba napomenuti da se
mnoge od stanadrdnih
dinamičkih struktura podataka
već nalaze implementirane u
vidu generičkih klasa u
standardnim bibliotekama
jezika C++. Među njima su
najpoznatije generičke klase
“list”, “stack”, “queue”,
“deque”, “set”,
“multiset”, “map”,
“multimap” i još neke druge.
Upotrebu ovih klasa lako je
savladati, pa se zainteresirani
čitatelji i čitateljice upućuju na
raznovrsnu širu literaturu koja
obrađuje standardnu biblioteku
jezika C++ (primjere upotrebe
nekih od ovih klasa biće
navedeni i u ovom tekstu). Za
edukativne svrhe znatno je
korisnije da probamo da sami
razvijemo neke od sličnih klasa,
pa ćemo tako i ovdje učiniti. U
neku ruku, generička klasa
“vector” (odnosno generička
klasa “AdaptivniNiz” koju
smo sami razvili) na neki način
također predstavlja dinamičku
strukturu podataka, s obzirom
da njene metode “resize” i
“push_back” (odnosno
“Redimenzioniraj” i
“DodajNaKraj”)
omogućavaju promjenu veličine
same strukture podataka.
Međutim, mehanizam promjene
veličine ovih struktura podataka
zasniva se na realokaciji i
kopiranju čitavog sadržaja
strukture podataka prilikom
svake realokacije, što je prilično
neefikasno. U ovom poglavlju
ćemo razmotriti fleksibilnije
načine za realiziranje
dinamičkih struktura podataka,
koji nisu zasnovani na
realokaciji.

Principe na kojima se
zasnivaju dinamičke strukture
podataka najlakše je objasniti na
primjeru dinamičke
implementacije apstraktnog tipa
podataka poznatog pod nazivom
stek ili stog (engl. stack), o
kojem smo već govorili.
Podsjetimo se da je stek
kontejnerska struktura podataka
zasnovana na LIFO (Last In
First Out) principu, odnosno
podatak koji se posljednji
stavlja na stek ujedno je i
podatak koji se prvi skida sa
steka. U osnovi implementacije
gotovo svih dinamičkih
struktura podataka leže
specijalne pomoćne strukture
podataka nazvane čvorovi (engl.
nodes) o kojima smo već
govorili ranije. To su strukture
podataka tipa strukture ili klase
koja obavezno među svojim
atributima sadrži jedan ili više
pokazivača (ili referenci) na
neku instancu iste strukture ili
klase. Na primjer, deklaracija
jednog čvora mogla bi izgledati
recimo ovako (atribut
“element” čuva vrijednost
pohranjenu u čvoru, dok atribut
“veza” predstavlja pokazivač
na neki drugi čvor, odnosno
predstavlja “vezu” sa drugim
čvorom):

struct Cvor {
int element;
Cvor *veza;
};

Razmotrimo sada kako nam


čvorovi mogu pomoći za
dinamičku realizaciju steka.
Osnovna ideja sastoji se u tome
da svaki put kada želimo da
dodamo novi element na stek,
dinamički kreiramo novi čvor, u
njegov atribut namijenjen za
čuvanje vrijednosti smjestimo
željeni element, a atribut za
vezu postavimo tako da
pokazuje na prethodno kreirani
čvor (tj. na čvor koji čuva
prethodni element na steku). Da
bismo znali gdje se nalazi
prethodno kreirani čvor, u
svakom trenutku moramo
pamtiti pokazivač na posljednji
kreirani čvor, tj. pokazivač na
vrh steka (npr. u atributu klase
steka koji ćemo ponovo nazvati
“gdje_je_vrh”, ali će on
ovaj put biti pokazivač, a ne
cjelobrojni indeks, kao što smo
imali prilikom statičke
implementacije steka). U
slučaju da je kreirani čvor prvi
čvor (tj. kada dodajemo element
na prazan stek), njegov atribut
veze postavljamo na 0, čime
označavamo da on nema svog
prethodnika. Također, atribut
“gdje_je_vrh” će sadržavati
0 kada je stek prazan, tj. kada
nema niti jednog kreiranog
čvora. Sljedeća slika prikazuje
kako će izgledati stanje u
memoriji nakon smještanja 5
elemenata na stek (npr.
elemenata sa vrijednostima 5, 2,
7, 6, i 3):
Vidimo da se zaista zauzeće
memorije povećava kako
dodajemo nove elemente na
stek, jer se pri tome kreiraju
novi čvorovi. Uz ovakvu
organizaciju, skidanje
elemenata sa steka je također
jednostavno. Naime, dovoljno
je prosto izbrisati iz memorije
čvor na koji pokazuje pokazivač
“gdje_je_vrh”, a pokazivač
“gdje_je_vrh” preusmjeriti
na adresu koju sadrži atribut
veze čvora na koji je prethodno
pokazivač “gdje_je_vrh”
pokazivao (tj. čvora koji je
upravo obrisan).

Nakon ovog teoretskog uvoda,


možemo preći na samu
dinamičku implementaciju
generičke klase “Stek”. S
obzirom da nam čvorovi trebaju
samo za internu implementaciju
ove klase, najprirodnije je čvor
deklarirati kao “ugniježdenu”
strukturu unutar privatne sekcije
klase “Stek”. Čvor ćemo
realizirati kao strukturu sa
konstruktorom, jer je na taj
način moguće inicijalizaciju
sadržaja čvora obaviti odmah po
njegovom kreiranju, što
pojednostavljuje
implementaciju (takva struktura
je, u suštini, klasa čiji su svi
elementi javni, što nam ne
smeta, s obzirom da je čitav
čvor “zapakovan” unutar
privatne sekcije klase “Stek”, i
nije vidljiv izvan nje). Sada bi
deklaracija generičke klase
“Stek” mogla izgledati recimo
ovako:

template <typename
Tip>
class Stek {
struct Cvor {
Tip element;
Cvor *veza;
Cvor(const Tip
&element, Cvor *veza)
:
element(element),
veza(Veza) {}
};
Cvor *gdje_je_vrh;
void Unisti();
void Kopiraj(const
Stek &s);
public:
Stek() :
gdje_je_vrh(0) {}
Stek(const Stek
&s) { Kopiraj(s); }
~Stek()
{ Unisti(); }
Stek &operator
=(const Stek &s);
bool Prazan()
const { return
gdje_je_vrh == 0; }
void Stavi(const
Tip &element) {
gdje_je_vrh =
new Cvor(element,
gdje_je_vrh);
}
Tip Skini();
};

Radi kompletnosti, u ovoj


klasi smo predvidjeli i
destruktor, konstruktor kopije i
preklopljeni operator dodjele, s
obzirom da se radi o klasi koja
dinamički alocira memoriju.
Destruktor odnosno konstruktor
kopije samo pozivaju privatne
metode “Unisti” odnosno
“Kopiraj” respektivno, koje
su definirane kao posebne
metode zbog činjenice da će isti
postupci biti korišteni i unutar
operatorske funkcije koja
implementira preklopljeni
operator dodjele.
Implementacija metoda
“Prazan” i “Stavi” je posve
trivijalna, tako da smo njihovu
implementaciju izveli odmah
unutar deklaracije klase. Nešto
je složenija implementacija
metode “Skini”:

template <typename
Tip>
Tip
Stek<Tip>::Skini() {
if(gdje_je_vrh ==
0) throw "Stek je
prazan!\n";
Tip element =
gdje_je_vrh->element;
Cvor *prethodni =
gdje_je_vrh->veza;
delete
gdje_je_vrh;
gdje_je_vrh =
prethodni;
return element;
}

Implementacija ove metode


traži izvjesnu pažnju, jer nakon
što obrišemo čvor pozivom
operatora “delete”,
pokazivač na njega postaje
viseći pokazivač, i bilo kakav
pristup sadržaju memorije na
koji on pokazuje veoma je
rizičan (i zabranjen, po
standardu). Zbog toga je, prije
nego što obrišemo sam čvor,
potrebno pokupiti u pomoćne
promjenljive sve informacije
koje on sadrži a koje su nam
potrebne i nakon njegovog
brisanja (u navedenom
primjeru, to su pomoćne
promjenljive “element” i
“prethodni”).

Nakon što je implementirana


metoda “Skini”
implementacija destruktora
(bolje rečeno, funkcije “Unisti”
koja se poziva iz destruktora)
postaje veoma jednostavna.
Naime, da bismo obrisali stek,
dovoljno je u petlji pozivati
metodu “Skini” sve dok stek
ne postane prazan (pri tome,
povratni rezultat koji vraća
metoda “Skini” prosto
ignoriramo):

template <typename
Tip>
void
Stek<Tip>::Unisti() {
while(!Prazan())
Skini();
}

Da nismo implementirali
destruktor, korisnik klase
“Stek” bi bio dužan da uvijek
sam isprazni stek prije nego što
odgovarajuća instanca klase
“Stek” prestane postojati. U
suprotnom bi došlo do curenja
memorije.

Funkcija “Kopiraj”, koja je


iskorištena za realizaciju
konstruktora kopije i
preklopljenog operatora
dodjele, relativno je složena, i
zahtijeva izvjesne dosjetke.
Naime, ova funkcija treba da
kreira doslovnu kopiju sadržaja
steka, čiji elementi nisu
kompaktno zapisani unutar
jedne cjeline (npr. unutar nekog
niza) nego su razbacani svuda
po memoriji, i međusobno
uvezani pokazivačima. Pri tome
je otežavajuća okolnost
činjenica da zbog načina kako
su usmjerene veze između
čvorova, čvorovima steka koji
se kopira možemo pristupiti
samo u obrnutom redoslijedu u
odnosu na redoslijed njihovog
kreiranja. Stoga nam ne
preostaje ništa drugo nego da
čvorove kopije steka kreiramo
upravo u onom redoslijedu
kojim možemo pristupati
čvorovima steka koji se kopira,
a da veze ostvarene
pokazivačima usmjerimo tako
da se zadrži logička struktura
veze između čvorova. Drugim
riječima, kopija steka iz
prethodnog primjera (sa
elementima 5, 2, 7, 6, i 3)
stvorena konstruktorom kopije
u memoriji će izgledati ovako
(uz pretpostavku da se čvorovi
smještaju u memoriju saglasno
redoslijedu kreiranja):

Primijetimo da je memorijska
slika steka kreiranog pozivom
metode “Kopiraj”
reflektirana kao slika u ogledalu
memorijske slike izvornog
steka, ali to ništa ne mijenja
njegovu funkcionalnost. U
skladu sa opisanim
objašnjenjem, konstruktor
kopije klase “Stek” mogao bi
izgledati ovako:

template <typename
Tip>
void
Stek<Tip>::Kopiraj(co
nst Stek &s) {
Cvor *prethodni,
*tekuci =
s.gdje_je_vrh;
gdje_je_vrh = 0;

while(tekuci != 0)
{
Cvor *novi = new
Cvor(tekuci->element,
0);
if(gdje_je_vrh
== 0) gdje_je_vrh =
novi;
else prethodni-
>veza = novi;
tekuci = tekuci-
>veza;
prethodni =
novi;
}
}

Analiza ove metode zahtijeva


izvjesnu koncentraciju, s
obzirom da se u svakom
trenutku koriste tri pokazivača
“tekuci”, “novi” i
“prethodni”, koji redom
pokazuju na čvor izvornog steka
koji se upravo kopira,
novokreirani čvor kopije steka i
prethodno kreirani čvor kopije
steka. Da bi se bolje razumio
rad ove metode, potrebno je na
konkretnom primjeru proći kroz
njeno tijelo, i crtati kakvo je
stanje memorije u svakom
trenutku. Veoma je važno da
čitatelj odnosno čitateljica
samostalno prođu kroz ovaj
postupak, jer njegovo
razumijevanje predstavlja ključ
za razumijevanje svih ostalih
dinamičkih struktura podataka.
Kao interesantnu ilustraciju
činjenice da fizički raspored
čvorova u memoriji uopće nije
važan za funkcioniranje
struktura podataka zasnovanih
na čvorovima, navedimo i to da
u slučaju da na kopirani stek
dodamo novi element (npr. 8),
njegova slika u memoriji će vrlo
vjerovatno (uz pretpostavku da
se novokreirani čvor smješta iza
prethodno kreiranih čvorova)
izgledati ovako:
Preklopljeni operator dodjele
za klasu “Stek” treba obaviti
još složeniji zadatak nego
konstruktor kopije, jer se
dodjela uvijek vrši nad
objektom koji već postoji.
Efikasna izvedba operatora
dodjele trebala bi iskopirati
elemente izvornog steka u
odredišni stek, pri čemu bi u
odredišni stek trebala dodati
nove čvorove u slučaju da
odredišni stek sadrži manje
elemenata od izvornog steka,
odnosno ukloniti suvišne
čvorove u slučaju da odredišni
stek sadrži više elemenata od
izvornog steka. Ovako izvedena
realizacija preklopljenog
operatora dodjele bila bi dosta
složena (čitatelj odnosno
čitateljica mogu pokušati izvesti
ovakvu realizaciju kao veoma
korisnu vježbu). Znatno
jednostavnije (ali i manje
efikasno) rješenje je prosto
uništiti čitav odredišni stek, a
zatim ga iznova rekreirati kao
kopiju izvornog steka, na isti
način kao u konstruktoru kopije.
Kako je dodjela međusobna
dodjela objekta tipa “Stek”
operacija koja se vjerovatno
neće vršiti često, ovakvo manje
efikasno rješenje može posve
zadovoljiti. Stoga
implementacija operatorske
funkcije za operator dodjele za
klasu “Stek” može izgledati
ovako:
template <typename
Tip>
Stek<Tip>
&Stek<Tip>::operator
=(const Stek<Tip> &s)
{
if(&s == this)
return *this;
Unisti();
Kopiraj(s);
return *this;
}

Obratimo pažnju na prvu liniju


tijela ove funkcije. Podsjetimo
se da je svrha testiranja
“&s == this” sprečavanje
problema koji bi mogli nastati
ukoliko bi se neki objekat klase
“Stek” pokušao dodijeliti sam
sebi (bilo direktno, bilo
indirektno putem referenci).

Kako su realizacije
konstruktora kopije i
preklopljenog operatora dodjele
za klasu “Stek” relativno
složene, često se u literaturi i
praksi susreću razne
implementacije klase “Stek” u
kojima su konstruktor kopije i
preklopljeni operator dodjele
samo deklarirani ali ne i
implementirani, čime se
eksplicitno zabranjuje kopiranje
i međusobno dodjeljivanje
primjeraka ove klase. Ipak, na
taj način onemogućavamo
prenošenje objekata tipa
“Stek” po vrijednosti u
funkcije, i što je još gore,
vraćanje objekata tipa “Stek”
kao rezultata iz funkcije.
Međutim, ukoliko nam to nije
potrebno, takvo polurješenje
može sasvim zadovoljiti. U
svakom slučaju, i takvo
polurješenje je mnogo bolje od
rješenja koje se zasniva na tome
da konstruktor kopije i
preklopljeni operator dodjele ne
deklariramo nikako. U tom
slučaju bi se koristili
podrazumijevani konstrukor
kopije i podrazumijevani
operator dodjele, a već smo
detaljno govorili o tome kakve
probleme ovo uzrokuje kad god
klasa sadrži pokazivače na
dinamički alocirane objekte
(pogotovo u kombinaciji sa
destruktorom).

Treba još napomenuti da su


prethodno opisane realizacije
konstruktora kopije i
preklopljenog operatora dodjele
dosta neefikasne, s obzirom da
se vrši kopiranje čitave interne
strukture steka. Moguće je
napraviti mnogo efikasnija
rješenja, zasnovana na brojanju
referenciranja, o čemu smo već
govorili. Naravno, brojanje
referenciranja samo po sebi
dovodi do plitkih kopija, koje
možemo izbjeći kreiranjem
duboke kopije kada se ustanovi
da je to neophodno. Na primjer,
duboku kopiju možemo kreirati
unutar metoda “Stavi” ili
“Skini” ukoliko primijetimo
da više primjeraka klase
“Stek” dijeli istu skupinu
čvorova. Dalje, moguće je da
svaki čvor posjeduje svoj
vlastiti brojač referenciranja,
tako da je moguće da više
različitih primjeraka klase
“Stek” dijele neke zajedničke
čvorove u memoriji. Na taj
način možemo dobiti vrlo
efikasne implementacije
konstruktora kopije i
preklopljenog operatora
dodjele, kod kojih je broj
kopiranja zaista sveden na
najnužniji minimum, i to samo
u slučaju izričite potrebe. Ovdje
smo samo izložili osnovne
ideje, dok su same realizacije
ovih ideja prilično složene, i na
ovom mjestu ih nećemo
izlagati.
Stek je izuzetno važna
struktura podataka za praktične
primjene, s obzirom da se
mnogi važni algoritmi za
rješavanje praktičnih problema
zasnivaju na upotrebi steka.
Stoga, standardna biblioteka
“stack” jezika C++ sadrži
istoimenu generičku klasu, koja
je veoma slična po
funkcionalnosti klasi “Stek”
koju smo upravo razvili.
Generička klasa “stack”
sadrži metode “empty” i
“push”, koje su po
funkcionalnosti identične kao
metode “Prazna” i “Stavi”
klase “Stek”. Umjesto metode
“Skini”, klasa “stack”
koristi dvije metode bez
parametara nazvane “top” i
“pop”. Metoda “top” vraća
kao rezultat element koji se
nalazi na vrhu steka, ali ga ne
skida sa steka, dok metoda
“pop” skida element sa vrha
steka i ne vraća nikakav
rezultat. Stoga bi se primjer koji
stavlja na stek brojeve 5, 2, 7, 6
i 3 a zatim ih skida sa steka i
ispisuje (čime dobijamo ispis u
obrnutom poretku) korištenjem
standardne generičke klase
“stek” mogao napisati ovako:

stack<int> s;
s.push(5);
s.push(2);
s.push(7);
s.push(6);
s.push(3);
while(!s.empty()) {
cout << s.top() <<
endl;
s.pop();
}

Metoda “top” zapravo kao


rezultat vraća referencu na
objekat koji se nalazi na vrhu
steka, što omogućava izmjenu
objekta na vrhu steka
konstrukcijama poput
“s.top() = 4” bez potrebe
da prvo skidamo element sa
steka, a da zatim stavljamo novi
element na stek. Međutim, to je
ujedno i pomalo opasno. Naime,
ukoliko deklariramo referencu
koju vežemo za rezultat vraćen
iz metode “top”, takva
referenca će postati ilegalna
(divlja) ukoliko nakon toga
pozivom metode “pop”
uklonimo taj element sa steka.
Vjerovatnoća da učinimo ovako
nešto nije prevelika, ali ipak
treba ukazati na moguće izvore
eventualnih problema.

Većina dinamičkih struktura


podataka realizira se na sličnom
principu kao i upravo razvijena
klasa “Stek”. Zbog toga ćemo,
prije nego što razmotrimo
realizacije drugih dinamičkih
struktura podataka, razmotriti
još neke aspekte realizacije
klase “Stek”, koji će kasnije
biti primjenljivi i na sve ostale
dinamičke strukture podataka.
Na prvom mjestu, primijetimo
da smo u prikazanom primjeru
razvili stek čiji čvorovi pamte
kopije objekata koji se stavljaju
na stek. Ti objekti također mogu
biti primjerci neke klase (npr.
moguće je napraviti stek čiji su
elementi tipa “Student”).
Međutim, čuvanje kopija čitavih
instanci klasa unutar čvorova
često nije poželjno. Naime,
instance složenijih klasa mogu
zauzimati mnogo memorijskog
prostora, pa bi se njihovim
kopiranjem u čvorove
neracionalno trošio memorijski
prostor, jer bi se isti podaci
čuvali na dva mjesta (u samoj
instanci klase, i u njenoj kopiji
unutar čvora). Racionalnije
rješenje je u čvoru čuvati samo
pokazivač na instancu klase. Na
primjer, pretpostavimo da
imamo deklaraciju poput
Stek<Student> s;

gdje je “Student” neka klasa


čije instance čuvaju podatke o
studentima (alternativno,
umjesto klase “Stek” koju smo
sami razvili, možemo koristiti i
standardnu klasu “stack”, bez
bitnijeg utjecaja na stvari na
koje želimo ukazati), i neka je
“neki_student” neka
instanca klase “Student”. U
ovakvom slučaju bi, nakon
naredbe poput
s.Stavi(neki_student)
;

informacija o istom studentu


bila pohranjena na dva mjesta: u
promjenljivoj
“neki_student” i u
novostvorenom čvoru steka
“s”. Međutim, nije nikakav
problem deklarirati stek čiji će
čvorovi čuvati adrese objekata
tipa “Student”, a ne same
objekte tog tipa:
Stek<Student*> s;

U ovom slučaju bismo


stavljanje podataka o studentu
na stek ostvarili putem naredbe

s.Stavi(&neki_student
);

Adresni operator u navedenom


primjeru je neophodan, s
obzirom da na steku zapravo
čuvamo adresu promjenljive
“neki_student”. Također,
ne smijemo zaboraviti da će
metoda “Skini” umjesto samog
objekta tipa “Student” kao
rezultat vratiti pokazivač, koji
treba ili dereferencirati, ili na
vraćeni rezultat primijeniti
operator indirektnog pristupa “-
>”. U svakom slučaju, ovako se
mnogo racionalnije troši
memorija.

Moguće je napraviti
kontejnersku klasu koja se
sintaksno koristi kao da čuva
objekte, a zapravo čuva
pokazivače na njih.
Razmotrimo, na primjer,
sljedeću klasu nazvanu
“IndirektniStek”:

template <typename
Tip>
class Stek {
struct Cvor {
Tip *pok;
Cvor *veza;
Cvor(Tip *pok,
Cvor *veza) :
pok(pok), veza(veza)
{}
};
Cvor *gdje_je_vrh;
public:
IndirektniStek() :
gdje_je_vrh(0) {}
bool Prazan()
const { return
gdje_je_vrh == 0; }
void Stavi(Tip
&element) {
gdje_je_vrh =
new Cvor(&element,
gdje_je_vrh); }
const Tip
&Skini();
};

template <typename
Tip>
const Tip
&IndirektniStek<Tip>:
:Skini() {
if(gdje_je_vrh ==
0) throw "Stek je
prazan!\n";
Tip *pok =
gdje_je_vrh->pok;
Cvor *prethodni =
gdje_je_vrh->veza;
delete
gdje_je_vrh;
gdje_je_vrh =
prethodni;
return *pok;
}

Sa ovako napisanom klasom


mogli bismo imati konstrukcije
poput

IndirektniStek<Studen
t> s;
...

s.Stavi(neki_student)
;

iako se na steku ne bi čuvala


kopija sadržaja promjenljive
“neki_student”, već samo
njena adresa. Također, metoda
“Skini” bi kao rezultat dala
sadržaj promjenljive stavljene
na stek, a ne pokazivač, bez
obzira što se na steku čuva
samo pokazivač. Ovo je
ostvareno uzimanjem adrese i
dereferenciranjem unutar samih
izvedbi metoda “Stavi” i
“Skini”, tako da se o tome ne
mora brinuti korisnik klase
“IndirektniStek”, već o
tome vodi računa sama klasa.

U prikazanoj implementaciji,
radi jednostavnosti prikaza i
uštede u prostoru, nismo
definirali destruktor,
konstruktor kopije i operator
dodjele (mada treba voditi
računa da su “jednostavnost” i
“ušteda u prostoru” često samo
dobra isprika za pravi argument
– lijenost). Međutim, ovdje je
važno da uočimo izvjesne
detalje. Prvo, formalni
parametar “element” u
metodi “Stavi” deklariran je
kao referenca, i to na
nekonstantni objekat. Prilično je
razumljivo zbog čega je
korištena referenca. Da nismo
koristili referencu, odnosno da
se koristi prenos parametra po
vrijednosti, formalni parametar
“element” bio bi kopija
stvarnog parametra
proslijeđenog u metodu, tako da
bi adresni operator “&” uzeo
adresu te kopije, a ne objekta
koji je prenesen u metodu (u
slučaju prenosa po referenci,
formalni parametar
“element” i stvarni parametar
faktički predstavljaju isti
objekat). Međutim, razmotrimo
zbog čega nismo koristili
referencu na konstantni objekat,
kao što obično radimo. Na prvi
pogled, mogli bismo koristiti
referencu na konstantni objekat,
s obzirom da nigdje ne
mijenjamo sadržaj referiranog
objekta. Prvi, i manje bitan
razlog, je što se pokazivačima
na nekonstantne objekte (kakav
je pokazivač “pok” deklariran
unutar čvora) ne smiju
dodjeljivati adrese konstantnih
objekata, jer bi se onda putem
takvog pokazivača mogao
promijeniti sadržaj konstantnog
objekta (inače, skup pravila
koja određuju koje su dodjele
zabranjene zbog činjenice da bi
njihova primjena mogla dovesti
do promjene sadržaja
konstantnih objekata poznata su
pod nazivom pravila o
konzistenciji konstantnosti).
Ovo bi se moglo lako riješiti
tako što bismo sam pokazivač
“pok” deklarirali kao
pokazivač na konstantan
objekat (što sasvim ima smisla,
jer stek nikada ne mijenja
sadžaj objekta kojeg čuva).
Drugi, mnogo bitniji razlog
zbog kojeg formalni parametar
“element” nije referenca na
konstantni objekat leži u
činjenici da na taj način stvarni
parametar mora biti l-vrijednost
(npr. neka promjenljiva) a ne
proizvoljan izraz, s obzirom da
se referenca na nekonstantni
objekat ne može vezati za
privremene objekte koji nastaju
kao rezultat izračunavanja
izraza. Međutim, zbog čega smo
uveli ovo ograničenje?
Pretpostavimo da smo dozvolili
da se kao stvarni argument
upotrijebi proizvoljan izraz.
Tada bi se na steku pohranila
adresa privremenog objekta koji
sadrži izračunati izraz.
Međutim, ovaj privremeni
objekat se automatski uništava
čim se završi kompletno
izvršavanje izraza unutar kojeg
je stvoren, tako da će se nakon
toga na steku čuvati viseći
pokazivač! Ilustrirajmo ovo na
konkretnom primjeru.
Pretpostavimo da konstruktor
klase “Student” prima kao
parametre ime i prezime
studenta, kao i broj indeksa.
Tada bi, ukoliko bi parametar
metode “Stavi” bila referenca
na konstantni objekat, sljedeća
naredba bila sasvim sintaksno
ispravna:

s.Stavi(Student("Pero
Perić", 1234));

Posljedice ove naredbe mogle


bi biti fatalne. Poziv
“Student("Pero Perić", 
1234)” kreira privremeni
propisno inicijalizirani bezimeni
objekat tipa “Student” koji se
dalje prosljeđuje metodi
“Stavi”, koja uzima njegovu
adresu i upisuje je na stek.
Međutim, taj privremeni objekat
prestaje postojati odmah po
završetku prikazane naredbe,
nakon čega će se na steku
nalaziti viseći pokazivač.
Deklariranjem formalnog
parametra metode “Stavi”
kao reference na nekonstantni
objekat, ovakve konstrukcije
nisu dozvoljene. Naime, metodi
“Stavi” će se kao parametri
moći proslijediti samo već
postojeći objekti, koji će
nastaviti svoje postojanje i
nakon poziva metode “Stavi”.
Iz ovoga treba izvući sljedeću
pouku: nikada i ni po koju
cijenu ne smijemo u
kontejnerskim strukturama
podataka čuvati adrese
privremenih objekata!

Obratimo pažnju na još jedan


detalj. Metoda “Skini” prije
nego što vrati rezultat, vrši
dereferenciranje pokazivača (da
bi zaista vratila objekat na koji
pokazivač pokazuje), i vraća
kao rezultat konstantnu
referencu na taj objekat. Na taj
način izbjegavamo suvišno
kreiranje privremenog objekta
koji će biti vraćen kao rezultat
iz funkcije i kopiranje
dereferenciranog pokazivača u
privremeni objekat. Naime,
pošto pokazivač koji
dereferenciramo pokazuje na
konkretan objekat koji
najvjerovatnije postoji (jer
takvog smo ga stavili na stek,
osim ako ga nismo u
međuvremenu uništili, što
svakako ne bismo trebali raditi),
prosto vratiti kao rezultat
referencu na njega. Kvalifikator
“const” obezbjeđuje da se
tako vraćena vrijednost neće
moći koristiti sa lijeve strane
operatora dodjele.

U jeziku C++ se savjetuje da


se umjesto “zločestih”
pokazivača koriste “manje
zločeste” reference gdje god je
to moguće. Time se, pored
neznatno drugačije sintakse
(poneka mrska zvjezdica
manje), smanjuje mogućnost
grešaka u implementaciji, jer je
sa referencama teže “zabrljati”
nego sa pokazivačima. Stoga
bismo, u skladu sa modernim
trendovima u C++
programiranju, u čvorovima
klase “IndirektniStek”
umjesto pokazivača na objekte
trebali čuvati reference na
objekte. Ovakva izvedba klase
“IndirektniStek”
izgledala bi ovako:

template <typename
Tip>
class Stek {
struct Cvor {
Tip &ref;
Cvor *veza;
Cvor(Tip &ref,
Cvor *veza) :
ref(ref), veza(veza)
{}
};
Cvor *gdje_je_vrh;
public:
IndirektniStek() :
gdje_je_vrh(0) {}
bool Prazan()
const { return
gdje_je_vrh == 0; }
void Stavi(Tip
&element) {
gdje_je_vrh =
new Cvor(element,
gdje_je_vrh); }
const Tip
&Skini();
};

template <typename
Tip>
const Tip
&IndirektniStek<Tip>:
:Skini() {
if(gdje_je_vrh ==
0) throw "Stek je
prazan!\n";
Tip &ref =
gdje_je_vrh->ref;
Cvor *prethodni =
gdje_je_vrh->veza;
delete
gdje_je_vrh;
gdje_je_vrh =
prethodni;
return ref;
}

Verzije klase
“IndirektniStek” koje
čuvaju pokazivače odnosno
reference na objekte
funkcionalno su potpuno
identične, i na programeru je da
izabere stil koji mu se više
sviđa. Međutim, ove dvije
verzije ipak nisu funkionalno
identične sa klasom “Stek” čiji
čvorovi čuvaju čitave kopije
objekata koji se stavljaju na
stek. Naime, pretpostavimo da
smo “gurnuli nekog studenta na
stek” naredbom poput
s.Stavi(neki_student)
;

i da smo nakon toga, a prije


“skidanja studenta sa steka”,
promijenili sadržaj promjenljive
“neki_student”. Prilikom
skidanja sa steka, u slučaju kada
stek čuva čitavu kopiju instance
klase “Student”, kao rezultat
će biti vraćena vrijednost
objekta “neki_student”
kakva je bila u trenutku
smještanja na stek (jer stek čuva
kopiju svih informacija iz
objekta “neki_student”
kakve su bile u tom trenutku).
Međutim, u slučaju kad stek
čuva samo pokazivače ili
reference na studente koji se
smještaju na stek, prilikom
skidanja sa steka biće vraćena
modificirana vrijednost
promjenljive
“neki_student”, jer se na
steku nije ni čuvao njen sadržaj,
već samo njena adresa
(upakovana u formu pokazivača
ili reference). Još gora situacija
nastaje ukoliko iz bilo kojeg
razloga promjenljiva
“neki_student” prestane
postojati prije trenutka skidanja
odgovarajućeg elementa sa
steka, recimo zbog izlaska iz
vidokruga unutar kojeg je
promjenljiva
“neki_student” definirana.
U tom slučaju, kao rezultat će
biti vraćen fantomski objekat,
odnosno dereferencirani viseći
pokazivač! Ovo je još jedan od
primjera problema vlasništva,
na koji smo u više navrata
ukazivali (naime, ovakav stek
nije vlasnik objekata koji su na
njemu smješteni, pa ne može
utjecati na to šta se sa njima
dešava za vrijeme dok se
formalno “čuvaju” na steku). U
brojnim primjenama se sadržaj
promjenljivih poput
“neki_student” neće
mijenjati između trenutka
smještanja na stek i trenutka
skidanja sa steka, tako da
korištenje steka koji čuva samo
pokazivače ili reference može
zadovoljiti. Vidimo da je
potrebno dobro razmisliti prije
nego što donesemo odluku da li
ćemo koristiti stek koji čuva
samo pokazivače (ili reference)
na objekte, ili čitave kopije
objekata. Ukoliko se ipak
odlučimo za korištenje
pokazivača, možda je najbolje
uopće ne koristiti klase kao što
je klasa “IndirektniStek”,
već koristiti klasu “Stek” (ili,
još bolje, standardnu klasu
“stack”) kojoj ćemo
eksplicitno kao tip podataka
koji se smještaju na stek
deklarirati pokazivački tip (kao
što smo na početku ilustrirali), i
eksplicitno koristiti adresni
operator i dereferenciranje. Na
taj način, program jasnije
odražava samu namjeru
programera, i smanjuje se
mogućnost pogrešne
interpretacije.
Pored steka,kandidata
izvrsnog red predstavlja za
dinamičku
Podsjetimo implementaciju.
se da za razliku od
steka koji
strukturu predstavlja
podataka apstraktnu
zasnovanu
na LIFO
predstavlja principu,
apstraktnu red
strukturu
podataka
(First In zasnovanu
First Out) na FIFO
principu,
odnosno
ušao u podatakkoji
red, prvi izlazi je
iz prvi
reda.
Čitatelju
shvatili ili čitateljki
način na koji radi
koji su
dinamička
ne bi implementacija
trebalo da reda,
predstavlja
nikakav problem daklasu
kreiraju generičku samostalno
“Red”
koja realizira reda.
implementaciju dinamičku
Zbog
činjenice
dodaju na da
jedanse u
kraj,reda podaci
skidaju
sa
vezu drugog
između kraja (tj. početka),
čvorova je bolje
ostvariti
na tako
sljedeći Kako da veze pokazuju
čvor sea ovakvo ne na
prethodni.
vezivanje može ostvariti,u
najbolje
konstruktoru je ilustrirano
kopije za klasu
“Stek”.
potrebno Naravno,
da klasa pri tome
“Red” je
čuva
pokazivač
Na taj način,na vađenje
prvi čvorelemenata
u lancu.
iz reda
ekvivalentno principijelno skidanju je
elemenata
olakšalo sa steka.elemenata
ubacivanje Da bi se u
red,
pokazivačpametnona je čuvati
posljednji čvor (ui
suprotnom
petljom proći bismo
kroz čitav morali
lanac
počev
nađemo od prvog
poziciju čvora
posljednjeg da
čvora kojeg trebamo
novokreiranim povezatikoji
čvorom sa
sadrži element koji
Interesantno je ubacujemo).
da se
konstruktor
dodjele za kopije
klasu “Red” i operator
mogu
izvesti
nego znatno
zada kod jednostavnije
klasukreiranja
“Stek”, s
obzirom
čvorovima izvornog kopije
reda
možemo
redoslijedom pristupati
kojim tačno onim
trebamo
kreirati
ne u čvoroveporetku
obrnutom kopije(kao reda,koda
steka).
davati Na ovom mjestu nećemo
dinamičku
implementaciju
nego ćemo to klase čitatelju
ostaviti Red,
ili
Kao čitateljici
što je kaorečeno,
već korisnu ovovježbu.
ne bi
trebao
problem, da bude
pogotovoshvatili nikakav
onima kako koji
su
radi u potpunosti
konstruktor
“Stek”. Također, kopije klase
oni kojima
implementacija
predstavljala ove klasemnoge
problem, bude
ideje mogu klase
implementacije preuzeti
“Lista”, iz
koja
kasnijećeu ovom
biti objašnjena
poglavlju. nešto
S obziromkorisna
veoma da je redstruktura
također
podataka,
implementiran onkao jesastavni
također
dio
standardne
+. biblioteke
Odgovarajuća jezika C+
generička
klasa
zove sekoja implementira
“queue”, a nalazi red
se u
istoimenoj
metode se zovubiblioteci.
istovjetno Njene
kao i
kod klase
“empty”, “stack”,
“push”, odnosno
“top” i
“pop”,
ista kao čija
kod je funkcionalnost
klase “stack”,
samo što
suprotne se vrh
strane reda
u nalazi na
odnosu sa
stek. Stoga će sljedeća sekvenca
instrukcija

queue<int> q;
q.push(5);
q.push(2);
q.push(7);
q.push(6);
q.push(3);
while(!q.empty()) {
cout << q.top() <<
endl;
q.pop();
}
ispisati
5, 2, 7, na6 iekran
3 slijedu brojeva
(svaki novom
redu).
dohvata Naime,
element metoda “top”
kojiposljednji,
je prvi
stavljen
kao u
u slučajured (a ne
steka). Također,
metoda
koji je prvi stavljen u red.element
“pop” uklanja
Stek strukture
važne i red spadaju u veoma
podataka, koje
se mnogo
algoritmima. koriste
Međutim, u raznim
obje ove
strukture
ograničene, podataka su prilično
s obziromelemenata
da je u
njima
moguće dodavanje
samo na kraj, a
uzimanje
moguće podataka
samo sa je (jednog
kraja također
ili
li drugog,
se o zavisnoiliod redu).
steku toga radiU
primjenama
stekovi u kojima
ili redovi, se
oni setakokoriste
koriste
upravo
to nena takav način,
predstavlja da
bitno
ograničenje
treba da (npr. ukoliko
pristupamo nam
elementu
koji
smo nije posljednji
stavili u nekuelement koji
strukturu
podataka,
nam ustvari toi zapravo
nije govori
potreban da
stek,
nego
podataka nekakoja to druga struktura
omogućava). S
druge
potrebne strane,i čestofleksibilnije
su nam
dinamičke
Stek i red, strukture
na način podataka.
kako su
implementirani
predstavljaju specijalan ovdje,
slučaj
dinamičkih
koje se nazivajustruktura podataka
jednostruko
povezane
linked liste
lists), s (engl.
obzirom single
da se
sastoje
je svaki od niza
čvor čvorova
povezan saod kojih
jednim
susjednim
napraviti i čvorom.
znatno Moguće je
fleksibilniju
jednostruko
omogućava povezanu
dodavanje listu,novih
koja
elemenata
unalisti, kao na
i proizvoljno
pristup mjesto
elementima
U proizvoljnoj
nastavku ćemo poziciji
razviti ujednu
listi.
verziju
pruža generičke
upravo klaseovakve
koja
mogućnosti.
klase, koju ćemo Deklaracija
nazvati ove
prosto
“Lista”, izgleda ovako:

template <typename
Tip>
class Lista {
struct Cvor {
Tip element;
Cvor *veza;
Cvor(const Tip
&element, Cvor *veza)
:
element(element),
veza(veza) {}
};
Cvor *pocetak,
*kraj, *aktuelni;
int velicina,
aktuelni_indeks;
public:
Lista() :
pocetak(0), kraj(0),
velicina(0) {}
Lista(const Lista
&lista);
~Lista();
Lista &operator
=(const Lista
&lista);
bool Prazna()
const { return
velicina == 0; }
int Duzina() const
{ return velicina; }
void
DodajNaPocetak(const
Tip &element);
void
DodajNaKraj(const Tip
&element);
void Umetni(int
indeks, const Tip
&element);
void Izbaci(int
indeks);
Tip &operator []
(int indeks);
void
Ponavljaj(void(*akcij
a)(Tip &));
};
Ostavimo
implementacione zadetalje sada
po
strani,
sadrži i pogledajmo
interfejs samo šta
klase.konstruktor,
Na prvom
mjestu,
konstruktor tu su
kopije, destruktor i
preklopljeni
koje operator
treba klasa
da posjeduje dodjele,
svaka
pristojna
dinamičku koja koristi
Dalje, tu sualokaciju memorije.
metode “Prazna”
iispituju
“Duzina”,
da li koje
je respektivno
lista prazna,
odnosno
elemenata koliko
(klase lista
“Stek” sadržii
“Red”
“Duzina”, nisu posjedovale
zbog činjenice metodu
da u
primjenama
stekovi u
i redovi kojima
ne se koriste
treba znati
koliko
odnosno u nekom
red sadrži trenutku stek
elemenata).
Metode “DodajNaPocetak”
inovi“DodajNaKraj” dodaju
odnosno element
kraj liste, nadok početak
metodau
“Umetni”
listu umeće
na “Izbaci” element
proizvoljnuizbacuje poziciju.
Metoda
liste element na navedenoj iz
poziciji.
operator Tu je i preklopljeni
indeksiranja “[]” koji
će omogućiti
elementima liste dapristupamo
proizvoljnim nao
isti način
elementima kao da
niza. se radi
Konačno,
predviđena
metoda je
“Ponavljaj” i interesantna
koja
obavlja
pokazivačem akciju
na definiranu
funkciju koji
joj
nad jesvakim
prenesen kao parametar
elementom liste.
Način upotrebe
objašnjen kasnije. ove metode biće
Pređimo sada na
implementacione detalje. Rad
klase opisuju atributi
“pocetak”, “kraj” i
“velicina” koji respektivno
sadrže pokazivače na početak
odnosno kraj liste, kao i broj
elemenata u listi, te atribute
“aktuelni” i
“aktuelni_indeks”, čija će
uloga biti uskoro razjašnjena, a
pomoću kojih se postižu
izvjesni trikovi koji bitno
povećavaju efikasnost klase
(vidjećemo da se u načelu sve
moglo postići samo pomoću
atributa “pocetak”, ali uz
gubitak efikasnosti). Veze
između čvorova ćemo realizirati
tako da svaki čvor sadrži
pokazivač na sljedeći čvor.
Stoga bi se dodavanje elementa
na početak liste principijelno
moglo realizirati na isti način
kao i stavljanje elemenata na
stek. Tako bismo i radili da je
atribut “pocetak” jedini
atribut koji opisuje rad liste.
Međutim, ukoliko dodajemo
čvor na početak prazne liste,
kreirani čvor je ujedno i prvi i
posljednji, tako da je u tom
slučaju potrebno također
inicijalizirati pokazivač “kraj”
da pokazuje na isti element
(istom prilikom je potrebno
inicijalizirati i atribute
“aktuelni” i
“aktuelni_indeks”, čija će
uloga postati jasna kasnije).
Također je pri svakom
dodavanju novog čvora
potrebno povećati atribut
“velicina” za 1. Stoga bi
metoda “DodajNaPocetak”
mogla izgledati ovako:

template <typename
Tip>
void
Lista<Tip>::DodajNaPo
cetak(const Tip
&element) {
pocetak = new
Cvor(element,
pocetak);
if(velicina == 0)
{
aktuelni = kraj
= pocetak;
aktuelni_indeks
= 0;
}
else
aktuelni_indeks++;
velicina++;
}
S obzirom da u atributu
“velicina” vodimou
evidenciju
listi, o broju
implementacija čvorova
metodei
“Duzina”
izvedena je trivijalna,
je unutar je deklaracije
klase.
metodu Interesantno
“Duzina” damogli
smo
napisati i bez uvođenja
“velicina”, tako štoatributa
bismo
prosto
“pocetak”krenuli od
i listi, pokazivača
prebrojali koliko
čvorova
sve dok ima
ne u
dođemo prateći
do čvoraveze
iza
kojeg ne slijedi
Konkretnije, niti jedan čvor.
metodu
“Duzina”
ovako: mogli smo napisati

template <typename
Tip>
int
Lista<Tip>::Duzina()
{
int brojac(0);
for(Cvor *pok =
pocetak; pok != 0;
pok = pok->Veza)
brojac++;
return Brojac;
}

Međutim, nije ni potrebno


govoriti da je mnogo efikasnije
voditi informaciju o broju
čvorova u nekom atributu nego
ih svaki put brojati.
Implementacija metode
“Prazna” je također trivijalna,
a bila bi trivijalna i da nismo
uveli atribut “velicina”
(lista je prazna ako je pokazivač
“pocetak” jednak nuli).
Dodavanje
kraj liste u novog elementa
slučaju prazne na
liste
istovjetno
elementa na je
njen dodavanju
početak. U
svim
potrebno ostalim
jegakreiratislučajevima,
novi čvor,
povezati
čvorom sa
u listi, posljednjim
i posljednji.
proglasiti
novokreirani
Na taj način, čvor za
implementacija
metode “DodajNaKraj”
mogla bi izgledati ovako:

template <typename
Tip>
void
Lista<Tip>::DodajNaKr
aj(const Tip
&element) {
if(velicina == 0)
DodajNaPocetak(elemen
t);
else {
kraj = kraj-
>veza = new
Cvor(element, 0);
velicina++;
}
}
Po
efikasnosti,cijenu
mogli smosmanjene
proći i
bez
kraj pokazivača “kraj”,
listeod početka jer
moguće i pronaćije
polazeći
veze između čvorova. prateći
U tom
slučaju, implementacija
“DodajNaKraj” metode
mogla bi
izgledati ovako:

template <typename
Tip>
void
Lista::DodajNaKraj(co
nst Tip &element) {
if(velicina == 0)
DodajNaPocetak(elemen
t);
else {
Cvor *pok;
for(pok =
pocetak; pok->veza !=
0; pok = pok->veza);
pok->veza = new
Cvor(element, 0);
velicina++;
}
}
Uz pretpostavku
metodu “Izbaci” da uklanja
koja imamo
element
trivijalan.izDovoljno
liste, destruktor
je izbacivatije
jedan
dok po
se jedan
lista element
ne iz liste,
isprazni.
Najlakše
prvi je stalno
element (tj. izbacivati
element sa
indeksom
konvenciju 0 ukoliko
da usvojimo
indeksiranje
elemenata počinjenizova):
u slučaju običnih od nule, kao

template <typename
Tip>
Lista<Tip>::~Lista()
{
while(!Prazna())
Izbaci(0);
}
S obzirom da
metodu imamo napisanu
“DodajNaKraj”,
konstruktor
preklopljenog kopije i izvedba
operatora dodjele
su
Potrebnotakođer
je proći jednostavni.
kroz cijelu
izvornu
na listu i
kraj odredišnesvaki čvor
liste dodati
(upotrebno
slučaju
dodjele, prethodno je
obrisati postojeću izvornu listu):

template <typename
Tip>
Lista<Tip>::Lista(co
nst Lista &lista) :
pocetak(0), kraj(0),
velicina(0) {
for(Cvor *pok =
lista.pocetak; pok !=
0; pok = pok->veza)

DodajNaKraj(pok-
>element);
}

template <typename
Tip>
Lista<Tip>
&Lista<Tip>::operator
=(const Lista<Tip>
&lista) {
if(&lista ==
this) return *this;
while(!Prazna())
Izbaci(0);
for(Cvor *pok =
lista.pocetak; pok !=
0; pok = pok->veza)
DodajNaKraj(pok-
>element);
return *this;
}
Implementaciju
funkcije za operatorske
preklapanje
operatora
ćemo indeksiranja
objasniti “[]”
prije
implementacije
“Umetni” imetode
“Izbaci”, metoda
jer na
će
se ove dvije
implementaciju oslanjati
ove operatorske
funkcije.
da se Najjednostavniji
realizira ova način
operatorska
funkcija
početka je
liste da krećući
prateći od
veze
pronađemo
rednim brojem,čvor sa
i da iz zadanim
njega
očitamo
Primijetimo traženi
da je element.
potrebno
vratiti
nađeni kao rezultat
element, referencu
jer će na
jedino
tako biti moguće
indeksiranja nađe da se rezultat
sa kaolijeve
strane
je operatora
moguće u dodjele,
slučaju što
običnih
nizova:

template <typename
Tip>
Tip
&Lista<Tip>::operator
[](int indeks) {
if(indeks < 0 ||
indeks >= velicina)
throw "Indeks izvan
opsega!\n";
Cvor *pok =
pocetak;
for(int i = 0; i <
indeks; i++) pok =
pok->veza;
return pok-
>element;
}
Međutim, moguće je postići
mnogo veću efikasnost.
Zamislimo, na primjer, da smo
prvo trebali da pristupimo 50-
tom elementu liste, a nakon toga
52-gom elementu. Uz prethodnu
implementaciju, prilikom
pristupa 52-gom elementu
ponovo bismo potragu za
čvorom koji sadrži ovaj element
krenuli od početka liste, iako se
ovaj čvor nalazi svega dva
čvora ispred 50-tog čvora kojem
smo maločas pristupali! Ovim
se nameće ideja da je pri
svakom pristupu nekom čvoru
pametno čuvati njegovu adresu i
redni broj u nekim atributima. U
slučaju da je indeks čvora
kojem želimo da pristupimo
veći od indeksa posljednjeg
čvora kojem smo pristupali (tj.
ako se traženi čvor nalazi ispred
posljednjeg čvora kojem smo
pristupali), potragu za
njegovom lokacijom možemo
započeti upravo od posljednjeg
čvora kojem smo pristupali, a
ne od početka liste. Međutim, u
slučaju da je indeks čvora
kojem želimo da pristupamo
manji od indeksa posljednjeg
čvora kojem smo pristupali,
potraga se mora započeti od
početka liste (jer nema načina
da na osnovu adrese čvora
saznamo adresu njegovog
prethodnika). Stoga su uvedeni
već pomenuti atributi
“aktuelni” i
“aktuelni_indeks” koji
redom čuvaju adresu i redni
broj posljednjeg čvora kojem se
pristupalo. Uvođenjem ovih
atributa, implementacija
operatorske funkcije za operator
“[]” mogla bi izgledati ovako:

template <typename
Tip>
Tip
&Lista<Tip>::operator
[](int indeks) {
if(indeks < 0 ||
indeks >= velicina)
throw "Indeks izvan
opsega!\n";
if(indeks <
aktuelni_indeks) {
aktuelni_indeks
= 0; aktuelni =
pocetak;
}
for(;
aktuelni_indeks <
indeks;
aktuelni_indeks++)
aktuelni =
aktuelni->veza;
return aktuelni-
>element;
}

Ovim trikom se zaista može


mnogo dobiti na efikasnosti,
naročito u situacijama kada
elementima liste često
pristupamo u rastućem
redoslijedu indeksa.
Umetanje
listu je novih elemenata
naročito interesantno. u
Umetanje
početak novog na
odnosno elementa
kraj na
liste
svodi
metode se“DodajNaPocetak”
na već napisane
odnosno
Međutim, “DodajNaKraj”.
posebnoumetanje je
interesantno
elemenata razmotriti
na proizvoljno
mjesto
početak unutar
liste liste
niti koje nijekraj.
njen niti
Poznato
elemenata je usred
da jeniza ubacivanje
veoma
neefikasan
prethodno postupak,
sve elemente jer je
niza
koji
želimoslijededaiza pozicije
ubacimo na novi
koju
element
jedno potrebno
mjesto pomjeriti
naviše, da bi za
se
stvorilo
element prazno
koji mjesto da
želimo za
ubacimo.
vremena, Ovim
pogotovose troši mnogo
ukoliko je
potrebno
elemenata pomjeriti
niza. mnogo
Međutim,
ubacivanje
liste može elemenata
se izvesti unutar
znatno
efikasnije,
trošenje uz mnogo Naime,
vremena. manje
dovoljno je kreirati novi čvor
koji
umećemo, sadržia element
zatim koji
izvršiti
uvezivanje
novokreirani pokazivača
čvor logičkitakodođe
da
na
može svoje mjesto.
izvesti Sveefikasno.
veoma ovo se
Posmatrajmo,
koja sadrži na primjer,
brojeve 3, 6, 7, 2listu
i 5,
učvorova
tom u poretku.
ovoj listi Raspored
možemo
prikazati sljedećom slikom:

Pretpostavimo dalje da je
između drugog i trećeg
elementa potrebno ubaciti novi
element, čija je vrijednost 8.
Nakon kreiranja novog čvora,
dovoljno je povezati pokazivače
tako da dobijemo situaciju kao
na sljedećoj slici:

Vidimo da nikakva premještanja


elemenata u memoriji nisu
potrebna (zahvaljujući činjenici
da je redoslijed elemenata
definiran vezama između
čvorova, a ne njihovim fizičkim
rasporedom). Na osnovu ove
ideje, implementacija metode
Umetni mogla bi se izvesti
ovako (parametar Indeks
predstavlja redni broj elementa
ispred kojeg ubacujemo novi
element):

template <typename
Tip>
void
Lista<Tip>::Umetni(in
t indeks, const Tip
&element) {
if(indeks == 0)
DodajNaPocetak(elemen
t);
else if(indeks ==
velicina)
DodajNaKraj(element);
else {
operator []
(indeks - 1);
aktuelni->veza =
new Cvor(element,
aktuelni->veza);
velicina++;
}
}

Ovdje smo eksplicitno pozvali


operatorsku funkciju za
operator “[]” sa ciljem da nam
pronađe adresu čvora koji
prethodi čvoru koji umećemo
(nađena adresa biće zapamćena
u atributu “aktuelni”).
Brisanje listielemenata
slučaju može se u
također
izvesti
slučaju mnogo efikasnije
nizova. Da nego
bismo u
uklonili
potrebno neki
je element
sve elemente iz niza,
niza
koji se nalaze
izbacujemo iza elementa
pomjeriti za koji
jedno
mjesto
slučaju unazad.
liste, Međutim,
dovoljna je malau
igra
povezujusa čvorove.
pokazivačima Tako, koji
na
primjer,
elementi da3, bismo
6, 7, iz iliste
2 5 čiji su
izbacili
četvrti
prepravitielement,
pokazivače dovoljno
tako da je
se
dobije
slika: sljedeća memorijska
Četvrti čvor na ovaj način više
nije unutar “lanca”, pa ga
slobodno možemo obrisati, da
ne troši memoriju. Vidimo da ni
ovdje nije potrebno vršiti
nikakvo premještanje elemenata
u memoriji, tako da je i
operacija brisanja veoma
efikasna. Ova ideja iskorištena
je u realizaciji metode
“Izbaci”, koja kao parametar
zahtijeva redni broj čvora koji
se uklanja iz liste.
Implementacija je nešto
složenija, zbog činjenice da
treba razlikovati tri slučaja.
Naime, nije teško zaključiti da
se postupak brisanja čvora koji
se nalazi na samom početku
odnosno na samom kraju liste
razlikuje od postupka brisanja
čvora koji nije granični čvor (tj.
nije niti prvi niti posljednji
čvor). Bez obzira na sve, nije
teško shvatiti kako ova metoda
radi:

template <typename
Tip>
void
Lista<Tip>::Izbaci(in
t indeks) {
if(velicina == 0)
throw "Lista je
prazna!\n";
velicina--;
Cvor *za_brisanje;
if(indeks == 0) {
za_brisanje =
pocetak;
aktuelni =
pocetak =
za_brisanje->veza;
aktuelni_indeks
= 0;
}
else {
operator []
(indeks - 1);
za_brisanje =
aktuelni->veza;
aktuelni->veza =
za_brisanje->veza;
if(indeks ==
velicina) kraj =
aktuelni;
}
delete
za_brisanje;
}
Ostala je
metode još implementacija
“Ponavljaj”. Ova
metoda
čitavu prosto prolazi
listu, i funkciju kroz
na svaki element
primjenjuje
parametrom “Akcija”: zadanu

template <typename
Tip>
void
Lista<Tip>::Ponavljaj
(void(*akcija)(double
&)){
for(Cvor *pok =
pocetak; pok != 0;
pok = pok->veza)
akcija(pok-
>element);
}

Da bismo vidjeli kako se ova


metoda može korisno
upotrijebiti. Često je potrebno
obaviti istu akciju nad svim
elementima liste zaredom.
Pretpostavimo, na primjer, da
želimo da ispišemo sve
elemente u listi realnih brojeva
“lista” razdvojene
razmacima. Naravno, jedna
mogućnost je da koristimo for
petlju i preopterećeni operator
indeksiranja “[]” kao u slučaju
da koristimo niz:

for(int i = 0; i <
lista.Duzina(); i++)
cout << lista[i] << "
";

Međutim, druga mogućnost je


da definiramo pomoćnu
funkciju (nazovimo je npr.
“Ispisi”) koja ispisuje
vrijednost svog parametra, na
primjer

void Ispis(double
&element) {
cout << element <<
" ";
}

a zatim primijenimo metodu


“Ponavljaj” prosljeđujući
joj funkciju “Ispisi” kao
parametar:

lista.Ponavljaj(Ispis
i);
Ovakav jer
efektniji, pristup
se je mnogo
akcija izvodi
neposredno
liste kojima nad elementima
se pristupa
sekvencijalno, prateći
pokazivače,
pozivanjem bez potrebe
operatorske za
funkcije
svakako zatroši
operator “[]”, bez
vrijeme, što
obzira
funkcija štoznatno
je ova optimizirana
operatorska
pamćenjem
posljednjeg pozicije
elementa i indeksa
kojem
smo
mnogo pristupali.
stvari sa Pored
listom toga,
bi se
moglo raditi
“Ponavljaj” pomoću
čak i da metode
u listi
nismo
operator uopće
“[]”. implementirali
Može čega se
postaviti
funkcija pitanje zbog
koja seu prenosi kao
parametar
“Ponavljaj” metodu
svoj parametar
prihvata
nekonstantnipo referenci
objekat). (i tosmo
Ovo na
uradili
metoda da bismo“Ponavljaj”
omogućili da
eventualno
elementima obaviZamislimo,
liste. izmjene nad na
primjer,
udvostručimo da sve želimo
elemente dau
listi.
svakakoJedno mogućepetlje:
korištenje rješenje je

for(int i = 0; i <
lista.Duzina(); i++)
lista[i] *= 2;

Međutim, alternativno rješenje


je da definiramo pomoćnu
funkciju (nazovimo je npr.
“Dupliraj”) koja
udvostručava vrijednost svog
parametra, na primjer

void Dupliraj(double
&element) {
element *= 2;
}

a zatim primijenimo metodu


“Ponavljaj” prosljeđujući
joj funkciju “Dupliraj” kao
parametar:
lista.Ponavljaj(Dupli
raj);
Metode
koje poput
prolaze “Ponavljaj”
kroz sve elemente
neke
akciju strukture
nad obavljajući
njima obično neku
se
nazivaju iteratorske
metode iteratori. metode ili
Pored osobine
imati ikakve da ne moramo
apriorne
informacije
koje treba o broju elemenata
smjestiti, lijepa
osobina
podataka liste
je kao
činjenica strukture
da je
umetanje
proizvoljno elemenata
mjesto kao nai
izbacivanje
proizvoljnog elemenata
mjesta iz mnogo
listi sa
efikasnije
nizova. negoilustracije
Radi pri korištenju
ćemo
prikazati
brojeva primjer
koji se sortiranja
unose sa
tastature
njihovim “u hodu”, uodnosno
smještanjem listu po
na
pravo
njihovom mjesto
unosu. odmah
Prikazani
isječak
skupine programa
brojeva, zahtijeva
pri čemu unos
unos
nule
svakogprekida
broja, unos.
traži Nakon
se unosa
mjesto u
listi na koji
ubačen, bi ončega
nakon trebao
se dapoziva
bude
metoda
ubaci na“Umetni”
pravo da se Ovaj
mjesto. broj
postupak
načinu konceptualno
kako bi čovjek jesortirao
sličan
spisak
svaki podataka
podatak od kojih na
zapisan je
posebnom listu papira (u
suštini, ovoumetanjem
sortiranja je zapravo ovarijanta
kojem
smo govorili
sortiranju): u poglavlju o

Lista<double> lista;
for(;;) {
double broj;
cout << "Unesi
broj (0 za kraj): ";
cin >> broj;
if(broj == 0)
break;
for(int i = 0; i <
lista.Duzina() &&
lista[i] < broj; i+
+);
lista.Umetni(i,
broj);
}
cout << "Sortirani
spisak brojeva glasi:
";
lista.Ponavljaj(Ispi
si);
Lista je jedna
najviše od najkorisnijih
korištenih strukturai
podadaka
programerskim u problemima.
brojnim
Kako je lista
struktura mnogo
podataka i općenitija
od steka i
od reda
vidjeli, (koji
mogu se, kao
smatrati što
kao smo
njene
podvarijante),
“Red” je, u klase “Stek”
slučaju da nami
problem
zahtijeva koji
njihovu rješavamo
upotrebu,
moguće implementirati
jednostavno pomoću veoma
klase
“Lista”.
implementacijaJedna od
moglamogućih bi
izgledati ovako:

template <typename
Tip>
class Stek {
Lista<Tip> lista;
public:
bool Prazan() {
return
lista.Prazna(); }
void Stavi(const
Tip &element)
{ lista.DodajNaPoceta
k(element); }
Tip Skini() {
double element =
lista[0];
lista.Izbaci(0);
return element;
}
};

template <typename
Tip>
class Red {
Lista<Tip> lista;
public:
bool Prazan() {
return
lista.Prazna(); }
void Ubaci(const
Tip &element)
{ lista.DodajNaKraj(e
lement); }
Tip Izvadi() {
double element =
lista[0];
lista.Izbaci(0);
return element;
}
};

Ove implementacije su zaista u


toj mjeri jednostavne da ne
traže nikakva dopunska
objašnjenja.
Najveći
jednostruko povezanenedostatak
liste je
činjenica
čvorova da je veza
ostvarena između
samo u
jednom smjeru (od prethodnom
ka
je sljedećem
da pristup čvoru).
elementimaPosljedica
liste
može
ukoliko bitise veoma neefikasan
elementima ne
pristupa
indeksa. u rastućem
Na primjer, poretku
ukoliko
prvo
100-tom trebamo
elementu, da a nakon
pristupimo
toga
99-tom
pronašli elementu,
99-ti element da bismo
moramo
krenuti
neposrednood početka
prethodiliste, iako on
100-tom
elementu
pristupali! kojem
Ovaj smo upravo
nedostatak
može se čvorove
proširiti izbjeći tako liste što
takoćemoda
sadrže
prethodni dvai napokazivača,
sljedeći na
čvor.
Dinamička
zasnovana struktura
na podataka
ovakvim
čvorovima
povezana nazivailise dvostruko
lista dvostruko
spregnuta
linked lista
list). (engl.
Sljedeća double
slika
ilustrira kako
organizacija bi mogla
čvorova izgledati u
dvostruko spregnutoj listi:
Dvostruko povezana lista je
mnogo efikasnija dinamička
struktura podataka od
jednostruko povezane liste.
Čitaocima koji su shvatili
implementaciju jednostruko
povezane liste ne bi trebao da
bude nikakav problem da
sastave implementaciju
dvostruko povezane liste.

You might also like