You are on page 1of 10

Kolegij: Programiranje 2

Materijali za online vježbe

Pokazivači i povezane liste

Prisjetimo se za početak što je zapravo povezana lista. Povezana lista je uređen skup podataka u kojem
svaki podatak čuva informaciju o lokaciji sljedećeg elementa. Samim time, svaki element ima dvije
komponente: podatak i vezu. Podatkovni dio čuva podatke za obradu, a veza je pokazivač koji je
usmjeren na sljedeći element u listi. Povezana lista ima i pokazivač koji je usmjeren na prvi element u
listi i to je zapravo ime liste ili glava vezane liste. Elementi povezane liste zovu se čvorovi. Važno je
također napomenuti da su čvorovi u listi samoreferencijalni što znači da svaki čvor sadrži pokazivač na
čvor istoga tipa.

Budući da je na predavanjima već objašnjen koncept povezanih lista, prisjetimo se samo kratko koje su
razlike između linearne i povezane liste. Linearna lista je lista u kojoj svaki element ima jedinstvenog
nasljednika. Najpoznatiji primjer linearne liste s kojim ste se već susretali jest polje. Polja je
jednostavno koristiti, ali ona nisu učinkovita kada je potrebno umetati podatke u sredinu polja ili kada je
potrebno brisati podatke iz polja. Iz tog razloga postoje povezane liste kod kojih je umetanje i brisanje
učinkovito, ali one također imaju nedostatak – sortiranje i pretraživanje nije učinkovito jer se zbog
fizičke razdvojenosti podataka ne mogu se koristiti algoritmi sortiranja i pretraživanja.

Kako funkcionira rad s povezanim listama, najbolje ćemo ilustrirati kroz sljedeći primjer.

Primjer 1
Potrebno je napisati program:
ULAZ: Unijeti N prirodnih brojeva, gdje N nije unaprijed poznat niti se može ograničiti.
IZLAZ: Ispisati brojeve obrnutim redoslijedom.

Zašto je u ovom zadatku nužno koristiti povezanu listu? Zašto ne bismo koristili dinamičku alokaciju
memorije te na taj način kreirali polje u kojem korisnik odredi koja je veličina polja? Zato što postoji
uvjet da N nije unaprijed poznat niti se može ograničiti. To zapravo znači da niti samom korisniku ne
možemo ograničiti broj unosa tj. koliko brojeva želi unijeti pa program treba omogućiti unos dokle god
korisnik to želi. Kreiranje dinamičkog polja npr. veličine 10.000 vrlo je neučinkovito jer s jedne strane
korisnik može unijeti samo 3 prirodna broja pa smo rezervirali memoriju za 9.997 prirodnih brojeva
previše te time neučinkovito upravljali memorijom, a isto tako korisnik može unijeti više od 10.000
prirodnih brojeva pa onda nemamo dovoljno memorije. Budući da N ne može biti ograničen odozgo, ne
možemo niti alocirati memoriju za elemente unaprijed. Iz tog se razloga povezane liste čine elegantnim
rješenjem jer se memorija alocira i koristi točno u trenutku kada je potrebna. Ono što želimo povezanim
listama postići jest da se za svaki broj koji je unesen, posebno alocira memorijski prostor, no to za
sobom povlači da elementi neće biti na uzastopnim memorijskim lokacijama pa ih moramo međusobno
povezati pomoću pokazivača koji pokazuju na sljedeće elemente.

Pretpostavimo da korisnik želi unijeti sljedeće brojeve: 23, 31, 21, 15, 55. Tada moramo
napraviti sljedeću dinamičku strukturu koja se generira pomoću pokazivača:
U toj strukturi, svaki element treba biti slog koji će, osim unesene vrijednosti, sadržavati pokazivač na
sljedeći uneseni element. Kako bismo to programski riješili?

Slog koji nam treba da bismo riješili problem izgleda ovako:

struct cvor {
int vrijendost;
cvor *veza;
};

Kao što vidite, koristili smo strukturu tipa cvor koja sprema dvije vrijednosti – vrijednost tj. ono
što korisnik unese (u ovom slučaju prirodni broj), te *veza koja je zapravo pokazivač na sljedeći
element u povezanoj listi. Ovdje možete primijetiti da je pokazivač *veza tipa cvor što znači da su
čvorovi u listi samoreferencijalni, što je već ranije spomenuto.

Prije nego što idemo na rješenje zadatka, osvrnimo se na dvije operacije koje je potrebno implementirati
– dodavanje novog elementa na početak vezane liste i brisanje elementa s početka vezane liste.

Prilikom dodavanja novog elementa u listu potrebno je alocirati memorijski prostor za taj element, ali
taj prostor nije ni u kakvoj vezi s memorijskim prostorom prethodnog elementa u listi pa nam je
potrebno povezivanje pokazivačima. Primjerice, želimo na početak liste dodati broj 33. Pokazivač l,
koji predstavlja glavu povezane liste, nakon dodavanja novog elementa u listu mora sadržavati
memorijsku lokaciju (pokazivati) na upravo dodani element. No, prije toga potrebno je novi element
povezati s ostatkom liste. Ako to ne učinimo, ostatak bi liste bio izgubljen i više ne bismo znali gdje se
on u memoriji nalazi.

Povezivanje ide tako da vrijednost pokazivača l zapišemo u pokazivač u slogu novododanog elementa
liste, čime se pokazivač iz novododanog elementa usmjeruje na sljedeći element vezane liste. Tek nakon
toga možemo pokazivač l preusmjeriti tako da pokazuje na novododani element. Pogledajte sljedeću
skicu dodavanja elementa na početak vezane liste:

U prvom koraku moramo alocirati memoriju za novi čvor, nakon toga u njegov dio veze zapisati ono što
je do sada pisalo u l (a to je pokazivač – memorijska adresa na element 55 u listi), te treći korak je u l
zapisati memorijsku adresu novododanog čvora 33.

Brisanje elemenata s početka liste također ima nekoliko koraka. Recimo da želimo element 55
izbrisati iz liste. Kod brisanja elementa s početka liste pokazivač l treba preusmjeriti tako da pokazuje
na drugi element u vezanoj listi. No, prije nego što se pokazivač l preusmjeri, potrebno je njegovu
vrijednost (adresa prvog elementa u vezanoj listi) zapisati u pomoćni pokazivač kako bi se nakon
preusmjeravanja pokazivača l na slijedeći element, prostor koji je zauzimao obrisani element mogao
dealocirati. Ako taj korak preskočimo, gubimo vezu s elementom koji želimo obrisati jer ne znamo
memorijsku adresu na kojoj je taj element pohranjen pa element ostaje izgubljen u memoriji i ne
možemo ga obrisati (dealocirati memoriju). Pogledajte sljedeću skicu brisanja elementa s početka
vezane liste:
Prvo je potrebno u pomoćni pokazivač zapisati memoriju od čvora 55, nakon toga možemo glavu
vezane liste l preusmjeriti da pokazuje na sljedeći element 15, te na kraju možemo dealocirati
memoriju za element 55 jer u pomoćnom pokazivaču imamo pohranjenu adresu tog elementa.

Pokušajmo sada ovu priču pretvoriti u programski kod. Pogledajmo najprije kako izgleda pseudokod:

1. Učitaj prirodni broj N


2. Ako je N>0
Alociraj prostor za novi element liste
Upiši vrijednost u novi element liste
Poveži novi element na početak liste
3. Inače
end=true
4. Dok još postoji elemenata u listi
Ispiši vrijednost
Izbaci element iz liste

Rekli smo da ćemo čvor realizirati kao strukturu koja ima podatkovni dio vrijednost i vezu *veza
na sljedeći čvor. Nakon toga u main funkciji definiramo bool varijablu end koja će nam govoriti
kada je kraj unosa. Također, naredbom cvor *glava=0; glavu vezane liste postavljamo na 0 jer na
početku nemamo elemente u listi.

Vratimo se na pseudokod i pokušajmo riješiti prva 3 koraka – učitati prirodni broj N i napraviti unos u
povezanu listu. Kao što vidite, uvjet je da broj bude prirodan tj. N mora biti pozitivan cijeli broj.
Kada je N>0, naredbom cvor *novi = new cvor; alociramo memoriju za novi čvor. Nakon toga
u podatkovni dio zapisujemo vrijednost (prirodni broj) naredbom novi->vrijednost=N; te
povezujemo novi čvor sa zadnje dodanim čvorom pomoću naredbe novi->veza=glava;. Ostao
nam je još jedan korak, da glavu vezane liste povežemo s novim čvorom pomoću naredbe
glava=novi;. To radimo tako dugo dok korisnik ne unese nulu. Kad se to dogodi, end postaje true
te izlazimo iz do-while petlje.

Nakon toga, ostaje nam još ispis brojeva i izbacivanje elementa iz liste.

Dokle god postoji elemenata u listi, a to znači dokle god glava povezane liste pokazuje na neki element
– while(glava), definirat ćemo pomoćni pokazivač tekuci koji nam služi za kretanje po listi i pohranu
memorijske lokacije elementa kojeg želimo obrisati cvor *tekuci;. Nakon toga ispisat ćemo
podatkovni dio tj. broj naredbom cout << glava->vrijednost << '' ''; te preusmjeriti
tekuci da pokazuje na isto na što pokazuje i glava vezane liste: tekuci=glava;. Nakon što
tekuci pamti lokaciju elementa kojeg želimo obrisati, možemo glavu preusmjeriti da pokazuje na
sljedeći čvor: glava=glava->veza, te obrisati trenutni element: delete tekuci;.

Da bi bolje mogli pratiti, evo još jednom cijeli programski kod zadatka:

Pokušajte sada pokrenuti ovaj programski kod. U primjeru smo unijeli brojeve 1, 2, 3, 4 i 5,
nakon toga unijeli 0 čime smo završili unos i ispisali elemente u obrnutom redoslijedu.
Zadatak 1
Modificirajmo prethodni primjer tako da isječak koda za unos elemenata prebacimo u zasebnu funkciju.
Funkcija unos izgledala bi onda ovako:

U main funkciji potrebno je samo napraviti poziv funkcije: unos(glava, N);. Funkciji
prosljeđujemo dva parametra, glavu vezane liste i N (broj koji unosimo u listu). U skladu s time,
argumenti funkcije su adresa glave vezane liste (*&glava) te N (broj koji unosimo u listu). Pokušajte
samostalno modificirati primjer i provjerite radi li.

Zadatak 2
Modificirajmo sada zadatak 1 tako da se brojevi u listu pohranjuju redoslijedom kojim se unose. To
znači da se novi čvor sprema NA KRAJ liste, a ne kao do sada, na početak. Za to ćemo kreirati
funkciju unos2 s istim argumentima kao i funkcija unos. Alokacija memorije za novi čvor te zapis
podatkovnog djela ostaje isti:

cvor *novi=new cvor;


novi->vrijednost = N;

Budući da novi čvor dodajemo na kraj liste, vezu (pokazivač) nećemo usmjeriti na glavu, već na 0:

novi->veza = 0;

Nakon toga moramo provjeriti postoji li koji čvor u listi. Ako ne postoji, onda novi predstavlja ujedno i
glavu vezane liste jer njegovim dodavanjem u listu postaje jedini član liste:

if(glava == 0) {
glava=novi;
}

Ako u našoj listi već postoje elementi/čvorovi, onda moramo definirati pomoćni pokazivač trenutni
kojim ćemo se kretati po listi kako bi došli do kraja liste i novi čvor smjestili na kraj liste (pomoću
while petlje). Sjetite se, u povezanoj listi ne znamo memorijsku lokaciju niti jednog elementa liste,
samo znamo lokaciju glave, pa postepeno možemo doći do lokacije drugih elemenata jer svaki element
nosi informaciju o memorijskoj lokaciji sljedećeg elementa (zato nam i treba pomoćni pokazivač
trenutni).
else {
cvor *trenutni = glava;
while (trenutni->veza) {
trenutni = trenutni->veza;
}
trenutni->veza = novi;
}

Kad veza čvora postane 0, izlazimo iz while petlje te povezujemo zadnji postojeći čvor u listi s
čvorom kojeg dodajemo u listu: trenutni->veza=novi;. Pogledajte sada i cijelu funkciju.

U main funkciji potrebno je samo napraviti poziv funkcije: unos2(glava, N);. Kad pokrenete
program i unesete željene brojeve, dobit ćete ispis u redoslijedu kojem su unesene vrijednosti.

Zadatak 3
Budući da smo rekli da je sortiranje povezane liste neučinkovito, pa čak i nemoguće, moramo voditi
računa o umetanju elemenata na pravo mjesto ako želimo imati sortiranu povezanu listu. U tu svrhu,
napravit ćemo program koji korisniku nudi izbornik u kojem može odabrati želi li unijeti element u listu,
ispisati listu ili brisati određeni element. Elemente ćemo u listu dodati na odgovarajuću poziciju tako da
je lista u svakom trenutku sortirana uzlazno. Svaku od ponuđenih opcija izbornika realizirat ćemo kao
zasebnu funkciju. Na Merlinu se nalazi kostur programa za ovaj zadatak (Zadatak 3 – kostur) koji možete
otvoriti i paralelno nadopunjavati dijelove koji ne dostaju. Kostur programa sadrži izbornik te komentare
na mjestu gdje je potrebno nadopuniti programski kod.
Kao što možete vidjeti iz isječka koda/kostura, potrebno je napraviti unos, ispis i brisanje elemenata u
povezanoj listi pa krenimo redom.

Prvo ćemo iznad main funkcije kreirati već poznatu strukturu za čvor:

struct cvor {
int podatak;
cvor *veza;
};

Nakon toga, u main funkciji, na početku, kreirat ćemo listu tako da glavu vezane liste (koja je trenutno
jedini element liste) usmjerimo na 0 pomoću naredbe:

cvor *glava=0;

Nakon toga, riješit ćemo unos elemenata u listu tako da lista u svakom trenutku bude sortirana
uzlazno. Prvo moramo alocirati memoriju za novi čvor te unijeti podatkovni dio u čvor. Budući da u
ovom trenutku ne znamo na koje mjesto treba postaviti čvor da lista bude sortirana, ne možemo niti reći
koja je veza na sljedeći element pa će veza za početak biti NULL (novi->veza=NULL).

Nakon toga, bit će nam potrebna 2 pokazivača koja će nam omogućiti kretanje kroz listu (prethodni
koji postavljamo na NULL i tekuci kojeg postavljamo na glavu vezane liste). Njih ćemo postavljati na
susjedne čvorove te između njih smjestiti novi čvor. Nakon toga pomoću while petlje obilazimo listu i
tražimo gdje spremamo novi čvor (dokle god postoji tekuci i podatkovni dio od tekuci je < x tj.
manji od elementa kojeg želimo unijeti u listu, nastavi se kretati listom). Nakon toga provjeravamo ako
ima elemenata u listi (if(prethodni)). Ako ima, smještamo novi čvor između prethodni i
tekuci, a ako nema (else opcija), onda novi čvor postaje glava. Pogledajte sada i cijelu funkciju:
void input(cvor *&glava, int x) {
cvor *novi = new cvor;
novi->podatak = x;
novi->veza = NULL;
cvor *tekuci=glava, *prethodni = NULL;
while(tekuci && tekuci->podatak < x) {
prethodni = tekuci;
tekuci = tekuci->veza;
}
if(prethodni) {
prethodni->veza = novi;
novi->veza = tekuci;
}
else{
novi->veza = glava;
glava = novi;
}
}

U main funkciji, pod case 1, potrebno je još samo dodati poziv funkcije koju smo kreirali:
input(glava,elt);

Nastavimo sada s drugom funkcijom – ispis liste. Funkcija write je jednostavna, samo je potrebno
funkciji proslijediti glavu vezane liste. Unutar funkcije imamo dva slučaja: prvi – kada nema elemenata u
listi tj. kada je glava == 0 te drugi slučaj – kada imamo elemente u listi. Kada nema elemenata u listi,
jednostavno ispišemo poruku o tome, a kada ima elemenata, potreban nam je samo jedan pomoćni
pokazivač tekuci koji služi za kretanje po listi. Njega ćemo usmjeriti na glavu vezane liste i dokle god
postoji tekuci, ispisat ćemo podatkovni dio te taj isti tekuci preusmjeriti da pokazuje na sljedeći
element i ponoviti ispis tako dugo dok ima elemenata u listi.

void write(cvor *glava) {


cout<<"Ispis elemenata liste...\n";
if (glava==0) cout<<"\nLista je prazna.\n";
else {
cvor *tekuci=glava;
while(tekuci) {
cout<<tekuci->podatak<<endl;
tekuci=tekuci->veza;
}
}
}

Možda ste se pitali zašto smo koristili pomoćni pokazivač tekuci, a ne jednostavno koristili glavu
vezane liste da bi prolazili i ispisali elemente liste? Sjetite se da do svih elemenata možemo doći samo
preko glave vezane liste. Ako nju u nekom koraku preusmjerimo na sljedeći element (kako smo u while
petlji napravili s tekuci – tekuci=tekuci->veza; ), izgubili smo vezu s prvim elementom i on
nam ostaje zaglavljen u memoriji. Zato je važno uvijek koristiti pomoćne čvorove za prolazak kroz listu,
a ne glavu vezane liste. Ona je jedina veza sa listom koju znamo.

U main funkciji moramo samo još pozvati funkciju write pod opcijom case 2:

write(glava);
Za kraj nam je ostala još samo funkcija koja briše traženi element iz liste. Kao i kod unosa, imat ćemo
dva pomoćna pokazivača tekuci i prethodni. Prvo moramo prolaziti kroz listu i pronaći element
koji želimo obrisati (pomoću pokazivača tekuci). Kada ga pronađemo, pokazivač prethodni
postavimo na isto mjesto (prethodni=tekuci; ), te tekuci pomaknemo za jedno mjesto
(tekuci=tekuci->veza; ). Time smo osigurali da nakon brisanja elementa ne izgubimo vezu s
ostatkom liste. Nakon toga provjeravamo ima li u listi jednog ili više čvorova. Ako je samo jedan
(prethodni== NULL; ) tada glavu preusmjerimo na sljedeći element i brišemo tekuci. Ako je više
elemenata u listi (else opcija), tada preusmjerimo da tekuci pokazuje na isto na što pokazuje i
prethodni i brišemo tekuci. Na kraju još tekuci postaje NULL kako ne bi ostao neusmjeren
pokazivač. Pogledajte cijelu funkciju:

void brisi(cvor *&glava, int x) {


cvor *tekuci=glava, *prethodni = NULL;
while(tekuci) {
if(tekuci->podatak == x)
break;
prethodni = tekuci;
tekuci = tekuci->veza;
}
if(tekuci) {
if(prethodni == NULL)
glava = glava->veza;
else
prethodni->veza = tekuci->veza;
delete tekuci;
tekuci = NULL;
}
}

Potrebno je ponovno napraviti poziv funkcije pod opcijom case 3:

brisi(glava, elt);

Na kraju, prije izlaska iz programa potrebno je i obrisati sve elemente iz liste:

cvor *tekuci=glava;
cvor *temp;
while(tekuci){
temp=tekuci;
tekuci=tekuci->veza;
delete temp;
}
glava=tekuci;

Pokušajte sada pokrenuti program i testirajte rade li sve funkcionalnosti.


Zašto vezana lista?
Do sada ste već vidjeli koje su to karakteristike i prednosti povezanih lista pa ćemo zaključiti ovo
gradivo sljedećom tablicom koja sistematizira karakteristike polja, struktura i povezanih lista:

POLJE STRUKTURA (struct) VEZANA LISTA


• broj elemenata je • broj elemenata je • broj elemenata NIJE unaprijed zadan
unaprijed zadan unaprijed zadan (svaki pojedini element liste se
(alocira/dealocira se (alocira/dealocira se kao zasebno alocira/dealocira)
kao cjelina) cjelina) • elementi se ne moraju nalaziti na
• elementi se nalaze na • elementi se nalaze na uzastopnim memorijskim lokacijama
uzastopnim uzastopnim (umjesto toga svaki element liste
memorijskim memorijskim lokacijama sadrži pokazivač na sljedeći element
lokacijama • elementi su različitih liste, a vrijednost pokazivača u
• svi elementi su istog tipova i naziva zadnjem elementu liste je 0 ili NULL)
tipa i istog naziva (adresiraju se po nazivu) • elementi su istog tipa i pristupa im se
(adresiraju se po uz pomoć pokazivača (nema indeksa)
indeksu) • lista sadrži pomoćni element (glava
vezane liste)

Zadatak za vježbu:
Napišite program koji u jednostruko povezanu listu pohranjuje podatke o predmetima koji se predaju na
nekom fakultetu: šifru predmeta, naziv predmeta, broj sati predavanja tjedno i broj sati vježbi tjedno.
Napišite funkciju za unos novog predmeta tako u svakom trenutku lista bude sortirana silazno prema šifri
predmeta. Funkcija treba vratiti 1 ako je zapis uspješno dodan u listu, a ako nije, treba vratiti 0. Prototip
funkcije treba biti: int ubaci (cvor &glava, cvor *noviPredmet). Interakcija korisnika s
programom odvija se sve dokle on to želi. Nakon svakog unosa, ispisuje se proširena lista. Na kraju
izvršite dealokaciju.

You might also like