nav_dugme codeBlog codeBlog
  • početna Početna stranica
  • Sačuvani članci Sačuvani članci
  • Članci
     (spisak)
  • Kontakt
Povratak na vrh stranice

Info & povezani članci Info o članku - dugme

Info

trejler_sat Datum objave: 18.01.2021.

trejler_olovka Poslednja izmena: 11.03.2023.

trejler_dokument Jezici: C++

trejler_teg_narandzasti Težina: 8/10

C++
algoritam
strukture podataka
nizovi
lista
operatori
teorija
tutorijali
saveti

Povezani članci

Strukture podatakaUvod u objektno orijentisano programiranjeTutorijal - Implementacija struktura podataka u programskom jeziku JavaScriptAVL Stablo - Implementacija - 1. deo - Osnovna strukturaOperacije sa bitovima u programskom jeziku CIzuzeci u programiranjuŠablonske niske u programskim jezicimaPostfiksna notacija - kako računari računaju?Izbor prvog programskog jezikaASCII, Unicode i UTF - Predstavljanje znakova na računarimaKako napraviti syntax highlighterGNU/Linux - 1. deo - Uvod
Svi članci
Programming isn't about what you know; it's about what you can figure out.
Chris Pine

Tutorijal: Implementacija jednostruko ulančane liste u programskom jeziku C++

Facebook LinkedIn Twitter Viber WhatsApp E-mail
zoom_plus zoom_minus bookmark
početna > Članci > Saveti

Uvod

Pošto smo se upoznali sa strukturama podataka i osnovama objektno orijentisanog programiranja, prikazaćemo i implementaciju jednostruko ulančane liste.

Ulančana lista je jedna od najosnovnijih (i najčešće korišćenih) linearnih struktura podataka, sačinjena je od čvorova koji se ređaju jedan za drugim, a budući da u članku prikazujemo "školsku" implementaciju jednostruko ulančane liste, * svaki čvor će obuhvatati dva podatka:

  • celobrojnu vrednost
  • pokazivač na sledeći čvor
Šema - početna
Slika 1. - Šema jednostruko ulančane liste.

Implementiraćemo metode za obavljanje sledećih operacija:

  • pronalaženje vrednosti elementa na određenom indeksu
  • umetanje elementa (na proizvoljno zadatu poziciju)
  • uklanjanje elementa (sa proizvoljno zadate pozicije)
  • ispis liste (u proizvoljnom formatu)

Da bi sve funkcionisalo kako dolikuje, program ćemo osmisliti tako da proverava da li se traženi indeks nalazi u granicama liste (u svim metodama u kojima je takva provera potrebna), a vodićemo računa i o ostalim bitnim detaljima.

* U drugačijim okolnostima, čvorovi ulančane liste (kao i čvorovi mnogih drugih struktura podataka), mogu sadržati, i najčešće sadrže, dodatne podatke (u praksi, liste niski, liste slogova i druge slične strukture, pojavljuju se češće nego liste celobrojnih vrednosti).

U člancima o implementaciji AVL stabla u programskom jeziku Java, upoznali smo se sa time kako se 'obični int čvorovi' mogu proširiti tako da obuhvate dodatne podatke, što se lako može uraditi i sa čvorovima ulančane liste, međutim, kako i dolikuje za sam početak, bavićemo se "školskim" primerom liste koja koristi čvorove sa dva podatka ....

Implementacija

U članku o strukturama podataka, već ste imali priliku da vidite kako izgleda (skoro upotpunjena) implementacija steka, ali, neke delove implementacije smo ipak izostavili zarad bolje preglednosti.

Ovoga puta, definisaćemo sve neophodne metode, i (takođe), budući da u radu sa listama program može pokušati da pristupi elementu koji je izvan granica strukture (što nije slučaj sa stekovima), predvidećemo odgovarajuće izuzetke za takve vanredne situacije.

Kratko objašnjenje vezano za izuzetke nalazi se na kraju ovog članka, a ako vas tema izuzetaka u programiranju zanima u još većoj meri, posvetili smo joj zaseban članak.

Osnovna struktura čvora

Osnovnu strukturu čvora definisaćemo na sledeći način (već poznat od ranije):

		
/* ---------------------------------- */
// Čvor jednostruko ulančane liste:
/* ---------------------------------- */
struct Cvor {
	int         vrednost;
	struct Cvor *sledeci;
};
		
	
Slika 2. - Struktura čvora koju ćemo koristiti u implementaciji liste.

Čvor je definisan preko sloga (struct), tj. nije definisan preko klase, što, s obzirom na jednostavnost tražene strukture čvora, smatramo racionalnim pristupom.

Ukoliko postoji potreba za skladištenjem dodatnih podataka, struktura Cvor se lako može prepraviti, uvođenjem dodatnog pokazivača (ili reference).

Na primer:

			
struct Cvor {
	int                  vrednost;
	struct Cvor          *sledeci;
	struct DodatniPodaci *podaci;
};
			
		

(Naravno, u nekom 'konkretnijem' primeru, ubačeni slog bi tipično nosio prikladno ime, koje bolje opisuje prirodu dodatih podataka.)

Osnovna struktura liste

Klasa Lista može se (praktično) definisati na sledeći način:

		
/* --------------------------------------------- */
// Osnovna struktura jednostruko ulančane liste:
/* --------------------------------------------- */
class Lista {
public:
	
	int brojElemenata;

private:

	struct Cvor *prvi;
	struct Cvor *poslednji;
	struct Cvor *trenutni;
};
		
	
Slika 3. - Osnovna struktura klase Lista (preko svega nekoliko polja, zapisaćemo sve neophodne podatke).

Da pojasnimo:

  • pokazivač na prvi čvor u listi (polje Prvi), čuva se kao zasebno polje (pre svega zarad iteratora * koji će biti implementiran u nastavku)
  • pokazivač na poslednji element (polje Poslednji), omogućava (znatno) lakše dodavanje novih čvorova
  • pomoćni pokazivač na čvor kome se trenutno pristupa (polje Trenutni), ** koristi se u metodama za dodavanje i uklanjanje elemenata, kao i u metodi za pristup elementu preko indeksa

* Iterator je naziv za funkciju (tj. metodu) koja redom prolazi kroz elemente liste.

** Praktično: pokazivač Trenutni je "radni pokazivač", koji će biti korišćen za "sve i svašta".

Pored navedenih privatnih polja, koristićemo i jedno javno polje (odeljak public na slici #3): čuvaćemo kao podatak i broj elemenata liste (pre svega zarad provere granica liste, pri pretrazi elemenata preko indeksa).

Konstruktor(i)

Za potrebe kreiranja novih objekata klase Lista, predvideli smo dva konstruktora:

1) Konstruktor za kreiranje prazne liste:

Konstruktor 1
Slika 4. - Konstruktor #1 kreira strukturu liste, ali, pokazivači ne pokazuju na čvorove, jer čvorova (još uvek) nema.

2) Konstruktor koji odmah u listu dodaje prvi element:

Konstruktor 2
Slika 5. - Konstruktor #2 kreira strukturu liste i prvi čvor, na koji pokazuju pokazivači Prvi i Poslednji.

Konstruktore ćemo implementirati na sledeći način:

		
public:

// Konstruktor koji se poziva ukoliko
// se ne preda vrednost prvog čvora:
Lista() {
	this->prvi          = NULL;
	this->poslednji     = NULL;
	this->brojElemenata = 0;
}

// Konstruktor koji se poziva ukoliko
// se preda vrednost prvog čvora:
Lista(int n) {
	struct Cvor *c;
	
	// Kreiranje novog čvora:
	c           = new Cvor();
	c->vrednost = n;
	c->sledeci  = NULL;
	
	// Inicijalno usmeravanje pokazivača:
	this->prvi          = c;
	this->poslednji     = c;
	this->brojElemenata = 1;
}
		
	
Slika 6. - Konstruktori klase Lista.

Prvi konstruktor je jasan sam po sebi, dok, kada je u pitanju drugi konstruktor (iako je situacija poznata od ranije), nikako nije zgoreg da se podsetimo na princip kreiranja novih čvorova.

Budući da su polja klase Lista: Prvi i Poslednji, samo pokazivači na slog tipa Cvor, potrebno je kreirati konkretan slog na koji će navedeni pokazivači pokazivati (što smo u dosadašnjim člancima/u primerima iz C-a, tipično obavljali preko funkcije malloc, ali, ovoga puta listu implementiramo u C++-u, i zato ćemo koristiti poziv new Cvor()).

Sličan motiv biće (naravno) prisutan i u metodi za dodavanje elemenata, a samostalno vođenje računa o dodeli memorije novim elementima i oslobađanju memorije pri uklanjanju elemenata ("memory management"), jedna je od najbitnijih odlika programskih jezika C i C++ u opštem smislu.

Pronalaženje vrednosti na određenom indeksu - preko metode

Za pronalaženje elementa na određenom indeksu, bilo da funkcija treba da vrati vrednost (što će biti varijacija koju ćemo prikazati u nastavku), ili referencu na element (i takvu funkciju ćemo takođe prikazati, i koristiti kao pomoćnu funkciju u drugim funkcijama), potrebno je, pažljivo i dosledno:

  • krenuti od prvog elementa liste *
  • prelaziti na susedne elemente preko pokazivača Sledeci, sve dok se ne pronađe element čiji je indeks zadat.

* Napomena: uvek (!) moramo imati sačuvan pokazivač na prvi element liste.

Navedeni algoritam je prilično jednostavan za implementaciju, uz dve bitne napomene:

  • za "šetanje" kroz listu, praktično je obavezno koristiti zaseban pomoćni pokazivač, * da bi se 'očuvao' pokazivač koji je vezan za prvi element liste
  • u slučaju prekoračenja granica liste (ukoliko je traženi indeks manji od 0 ili veći od gornje granice), potrebno je reagovati na adekvatan način (u implementaciji kojom se bavimo u članku, "uhvatićemo" i obraditi izuzetak)

* U implementaciji koju prikazujemo, u pitanju je pokazivač sa nazivom Trenutni.

Pošto smo 'pretresli' sve preduslove, sledi programski kod:

		
public:

int pronalazenjeVrednosti(int indeks) {
	// Provera indeksa (u smislu: da li se
	// predata vrednost nalazi u granicama liste):
	if (indeks < 0 || indeks >= this->brojElemenata) {
		std::string s = "Greška pri pronalaženju vrednosti.\n";
		s += "Indeks " + std::to_string(indeks) + " je van granica liste.";
		throw s;
	}

	// Pronalaženje vrednosti čvora
	// koji odgovara unetom indeksu:
	
	this->trenutni = this->prvi;
	
	int i = 0;
	
	while (i < indeks) {
		trenutni = trenutni->sledeci;
		i++;
	}
	
	return trenutni->vrednost;
}
		
	
Slika 7. - Metoda za pronalaženje vrednosti elementa čiji se indeks predaje kao argument.
  • prvi if kreira izuzetak ukoliko traženi indeks nije u granicama liste
  • ako nije došlo do izuzetka, pomoćni pokazivač se usmerava * na prvi element liste
  • obavljaju se uzastopni prelasci na 'sledeći' čvor, sve dok se ne pronađe element čiji je indeks zadat
  • na kraju, ispisuje se vrednost pronađenog elementa

Preko sličnog postupka, može se pronaći i referenca na element čiji se indeks zadaje kao ulazna vrednost.

* Kada kažemo da je određeni čvor 'usmeren' na drugi čvor, mislimo na to da pokazivač na (prvom) čvoru pokazuje na drugi čvor.

Pronalaženje reference na element sa zadatim indeksom

Ukoliko je potrebno da funkcija pronalazenjeElementa vrati referencu na element sa zadatim indeksom (umesto brojčane vrednosti datog elementa), dovoljno je prepraviti tip funkcije i povratnu vrednost:

		
public:

struct Cvor *pronalazenjeElementa(int indeks) {
	// Provera indeksa (u smislu: da li se
	// predata vrednost nalazi u granicama liste):
	if (indeks < 0 || indeks >= this->brojElemenata) {
		std::string s = "Greška pri pronalaženju elementa.\n";
		s += "Indeks " + std::to_string(indeks) + " je van granica liste.";
		throw s;
	}
	
	// Pronalaženje reference na čvor
	// koji odgovara unetom indeksu:

	this->trenutni = this->prvi;
	
	int i = 0;
	
	while (i < indeks) {
		trenutni = trenutni->sledeci;
		i++;
	}
	
	return trenutni;
}
		
	
Slika 8. - Metoda za pronalaženje reference na element čiji se indeks predaje kao argument.

Metodu pronalazenjeElementa koristićemo u metodama za dodavanje i uklanjanje elemenata.

Metoda pronalazenjeElementa može se (naravno) koristiti i ukoliko klasa Cvor sadrži dodatne podatke.

Očitavanje vrednosti na određenom indeksu - uz preklapanje operatora []

Pošto smo definisali metodu za pronalaženje vrednosti elementa (koji je povezan sa određenim indeksom) - može se nadalje pristupati elementima, ali, poziv funkcije pronalazenjeVrednosti ....

		
int vrednost = lista.pronalazenjeVrednosti(i);
		
	
Slika 9. - Poziv metode za pronalaženje elementa.

.... ipak (u očima većine programera), ne deluje elegantno као sledeći kod:

		
int vrednost = lista[i];
		
	
Slika 10. - Pronalaženje elementa liste preko operatora [] - poziv metode.

Doduše, o lepoti i funkcionalnosti jednog ili drugog pristupa moglo bi se diskutovati, budući da operator [] 'priziva asocijacije' na statičke nizove, u kojima operacija pristupa elementu ima složenost O(1) - što naravno nije klasa složenosti koja odlikuje algoritam za pristup elementu u ulančanim listama (zbog čega korisnici mogu biti zbunjeni).

Nedoumice se najlakše otklanjaju preko propratne dokumentacije, i stoga ćemo pred kraj članka napraviti kratak osvrt na to kako treba komentarisati programski kod (uz koju usputnu napomenu u vezi sa tim kako ne treba komentarisati programski kod). :)

U svakom slučaju, preklapanje (eng. overload) operatora [], može se obaviti na sledeći način:

		
public:

int operator [] (int n) {
	return pronalazenjeVrednosti(n);
}
		
	
Slika 11. - Preklapanje (tj. "overload") operatora [].

U definiciji metode navodi se: rezervisana reč operator, kao i sam operator koji se povezuje sa povratnom vrednošću metode.

Pošto je operator [] "preklopljen", može se koristiti uz objekte klase Lista (onako kako smo već videli na slici #10).

Dodavanje elementa na kraj liste ("push")

Pošto koristimo nazive funkcija na srpskom jeziku, moramo biti precizni i napomenuti da, pod dodavanjem, podrazumevamo (prosto) nadovezivanje elementa na kraj liste ("push"), a ne umetanje na proizvoljnu poziciju ("splice").

Umetanje na proizvoljnu poziciju (splice), tema je jednog od narednih odeljaka.

U svakom slučaju, i ovoga puta je potrebno kreirati 'konkretan čvor' a ne samo pokazivač.

		
void dodavanje(int n) {
	struct Cvor *c;
	
	// Kreiranje novog čvora:
	c           = new Cvor();
	c->vrednost = n;
	c->sledeci  = NULL;
	
	if (this->prvi == NULL) {			
		// Dodavanje na prvu poziciju,
		// ukoliko je lista prazna:
		this->prvi      = c;
		this->poslednji = c;
	}
	else {
		// Dodavanje na poslednju poziciju,
		// ukoliko lista nije prazna:
		this->poslednji->sledeci = c;
		this->poslednji          = c;
	}
	
	this->brojElemenata++;
}
		
	
Slika 12. - Metoda za dodavanje novog elementa u listu.

Pri 'integraciji' novog čvora u ostatak strukture (if u funkciji dodavanje), razlika je (samo) u tome da li se lista "načinje" dodavanjem prvog elementa, ili se novi element nadovezuje na poslednji (postojeći) čvor.

Ukoliko je lista prazna, pokazivači Prvi i Poslednji biće usmereni na čvor koji je 'upravo kreiran'.

Nešto nalik pokretanju konstruktora koji odmah dodaje prvi element u listu (slika #5).

U suprotnom (ukoliko lista nije prazna), prvo je potrebno: povezati poslednji čvor sa novim čvorom, i potom proglasiti čvor koji je upravo dodat - za poslednji.

Dodavanje čvora
Slika 13. - Dodavanje novog elementa na kraj liste podrazumeva kreiranje novog čvora i preusmeravanje pokazivača, tako da dotadašnji poslednji element, kao i pokazivač Poslednji, pokazuju na novi čvor.

Još jednom vredi napomenuti da se pokazivač Poslednji čuva upravo zarad operacije dodavanja, jer - u suprotnom - bilo bi potrebno proći kroz celu listu.

U metodi za pronalaženje elemenata, 'prolazak' kroz celu listu je tipična/uobičajena i 'neizbežna' operacija, ali, pri dodavanju elemenata, prolazak kroz celu listu predstavlja nepotrebno 'gubljenje vremena'.

Umetanje elementa (na zadati indeks)

Kada je u pitanju umetanje elemenata na određeni indeks, razlikuju se tri situacije:

  • umetanje elementa na početak liste
  • umetanje elementa na kraj liste
  • umetanje elementa na bilo koju od preostalih pozicija

Umetanje na prvu ili poslednju poziciju su relativno jednostavni zahvati, i stoga ćemo najviše pažnje posvetiti proceduri za umetanje (novog) čvora na neku od međupozicija (postupak ćemo pojasniti preko donje slike).

Zarad ilustracije, ubacićemo novi čvor između (postojećih) čvorova 12 i 17.

U implementaciji, novi čvor se kreira preko izraza new Cvor() i povezuje se sa pokazivačem C, nakon čega se pokazivač C.Sledeci (pokazivac Sledeci na novom čvoru), usmerava na "sledeći" čvor (na slici, čvor #17).

Umetanje čvora 1
Slika 14. - Umetanje novog čvora. Pre svega, moramo kreirati konkretan novi čvor (nikako samo referencu), a pokazivač na novom čvoru potrebno je usmeriti na sledeći čvor (#17). Pokazivač Trenutni pomoći će nam da povežemo prethodni čvor (#12) sa novim (#15), pri čemu se pokazivač na čvoru #12 praktično "odvezuje" od čvora #17 (prikazano crvenom bojom).

Nakon pripremnih radnji, potrebno je: pristupiti "prethodnom" čvoru (preko polja Trenutni), i preusmeriti pokazivač Sledeci.

Na čvoru 12 (koji u primeru predstavlja prethodni čvor), pokazivač Sledeci se preusmerava, sa čvora 17 - na čvor 15 (tj. novi čvor), preko naredbe Trenutni->Sledeci = C; (plava strelica).

Novi čvor usmeren je na čvor 17 (tj. "sledeći" čvor), a čvor 12 je (pre)usmeren na novi čvor - što praktično znači da je novi čvor umetnut u listu:

Umetanje čvora 2
Slika 15. - Novi čvor (#15) je umetnut.

Implementacija podrazumeva i proveru indeksa (u smislu, 'da li je indeks u granicama liste'), nakon čega sledi traženje pozicije za novi element (uz uvažavanje pravila koja su navedena na početku odeljka):

		
void umetanje(int indeks, int n) {
	// Provera indeksa (u smislu: da li se
	// predata vrednost nalazi u granicama liste):
	if (indeks < 0 || indeks > this->brojElemenata) {
		std::string s = "Greška pri umetanju elementa " + std::to_string(n) + "\n";
		s += "Indeks " + std::to_string(indeks) + " je van granica liste.";
		throw s;
	}
	
	// Umetanje na kraj liste:
	
	if (indeks == this->brojElemenata) {
		dodavanje(n);
		return;
	}
	
	// Umetanje na prvu poziciju:
	
	struct Cvor *c = new Cvor();
	c->vrednost    = n;
	
	if (indeks == 0) {
		c->sledeci = this->prvi;
		this->prvi = c;
		return;
	}
	
	// Umetanje na neku od međupozicija:

	this->trenutni    = pronalazenjeElementa(indeks - 1);
	c->sledeci        = trenutni->sledeci;
	trenutni->sledeci = c;
}
		
	
Slika 16. - Metoda za umetanje novog elementa liste na proizvoljnu poziciju.

Osvrnimo se na najvažnije detalje implementacije:

  • ukoliko je traženi indeks van granica liste, metoda će vratiti izuzetak
  • ukoliko program pokušava da "umetne" element na poslednju poziciju, posao se jednostavno prepušta metodi za dodavanje (koja će uredno usmeriti pokazivač Poslednji na novi element)
  • umetanje čvora na prvu poziciju podrazumeva: kreiranje novog čvora (koji će biti referenciran preko pomoćnog pokazivača C), usmeravanje pokazivača C->Sledeci na prvi čvor u listi (slikovito, čvor koji je "još uvek" prvi u listi) i, na kraju, usmeravanje pokazivača Prvi, na novi čvor (this->Prvi = C;)
  • umetanje čvora na međupozicije, obavlja se preko procedure koju smo prethodno objasnili preko slika

Pravilno (pre)usmeravanje pokazivača, dodavanje i uklanjanje čvorova i sl, nisu trivijalne operacije i tipično se ne mogu lako razumeti "na prvu loptu", ali, pod uslovom da imate dovoljno prethodnog iskustva i voljni ste da se potrudite da sagledate sve delove implementacije "sa svih strana", rezultati neće izostati.

Uklanjanje elementa (na zadatom indeksu)

Kada je u pitanju uklanjanje elemenata, takođe se razlikuju tri situacije:

  • uklanjanje prvog elementa (u kom slučaju se mora pravilno * preusmeriti pokazivač Prvi)
  • uklanjanje poslednjeg elementa (u kom slučaju se mora pravilno * preusmeriti pokazivač Poslednji)
  • uklanjanje elementa koji nije prvi ili poslednji

* Osim pukog preusmeravanja pokazivača, potrebno je osloboditi i memoriju koju čvor zauzima.

Definisaćemo prvo metodu za uklanjanje prvog elementa liste:

Metoda za uklanjanje prvog elementa

		
void uklanjanjePrvi() {
	struct Cvor *c = this->prvi;

	// Preusmeravanje pokazivača:
	this->prvi = this->prvi->sledeci;
	
	// Oslobađanje memorije:
	delete c;
	
	return;			
}
		
	
Slika 17. - Metoda za uklanjanje prvog elementa liste.

U metodi prepoznajemo sledeće korake:

  • pokazivač C (pomoćni pokazivač), usmerava se na prvi element liste
  • pokazivač Prvi usmerava se na drugi element liste (koji, posle pozivanja metode uklanjanjePrvi, biva preusmeren na početak liste)
  • dosadašnji prvi element liste (čvor na koji pokazuje pokazivač C), na kraju se uklanja

Ako ste raspoloženi za šalu i volite gramatiku srpskog jezika, pogledajte kako bi prethodno navedena uputstva mogla da se uobliče (ako niste, slobodno preskočite sledeća dva pasusa):

"Pokazivač 'C' usmerava se na prvi element liste, a pokazivač 'Prvi' - na drugi, koji - posle brisanja prvobitnog prvog elementa - postaje prvi element".

Ukoliko imate mentalni sklop koji je sposoban da istovremeno: preživi naše programerske pošalice - i shvati da je zapravo u pitanju legitimno uputstvo (doduše, "ne baš" u formatu koji smatramo optimalnim :)) .... sasvim je moguće da vas čeka lepa budućnost u programiranju. :)

Metoda za uklanjanje poslednjeg elementa

Metodu za uklanjanje poslednjeg elementa, definisaćemo na sledeći način:

		
void uklanjanjePoslednji() {
	// Pronalaženje pretposlednjeg čvora:
	this->trenutni = this->prvi;
	
	for (int i = 2; i < this->brojElemenata; i++) {
		trenutni = trenutni->sledeci;			
	}

	// Uklanjanje poslednjeg čvora:
	this->poslednji = trenutni;
	Cvor *c         = trenutni->sledeci;
	delete c;
	this->poslednji->sledeci = NULL;
}
		
	
Slika 18. - Metoda za uklanjanje poslednjeg elementa liste.

U metodi za uklanjanje poslednjeg elementa prepoznajemo sledeće korake:

  • preko čvora C (pomoćni pokazivač), referencira se poslednji element liste
  • preko pokazivača Trenutni, pronalazi se pretposlednji element liste, nakon čega se adresa pretposlednjeg elementa predaje pokazivaču Poslednji
  • pokazivač Sledeci na pretposlednjem čvoru, postavlja se na vrednost NULL
  • dosadašnji poslednji čvor (koji je referenciran preko pokazivača C), na kraju se uklanja

Opšta metoda za uklanjanje elementa

Opšta procedura za uklanjanje čvora, ima četiri odeljka:

  • if grananje preko koga se proverava da li je indeks unutar granica liste
  • poziv metode uklanjanjePrvi, ukoliko je indeks 0
  • poziv metode uklanjanjePoslednji, ukoliko indeks ukazuje na poslednji element liste (indeks BrojElemenata - 1)
  • ostatak metode zadužen je za uklanjanje čvorova na "međupozicijama":

Uklanjanje čvorova na međupozicijama, ilustrovaćemo preko sledeće slike:

Uklanjanje čvora 1
Slika 19. - Uklanjanje čvora - početak operacije - Čvor #15 (označen crvenom bojom), biće uklonjen: preko pokazivača C definisaćemo pokazivač na "sledeći" element (#17), sa kojim će se povezati čvor #12 (tj. "prethodni" čvor, koji ćemo naći preko pokazivača Trenutni).
  • čvor koji se uklanja, pronalazi se preko pomoćne metode za pronalaženje elemenata i referencira se preko pomoćnog pokazivača C (za primer ćemo uzeti čvor #15)
  • preko pokazivača C (tačnije, C->Sledeci), takođe je pronađena i referenca na čvor 17, odnosno, referenca na "sledeći" čvor (koja je još bitnija za dalji tok izvršavanja programa)
  • preko pokazivača Trenutni pristupa se čvoru 12 (to jest, čvoru koji prethodi čvoru koji se uklanja)
  • pokazivač Trenutni->Sledeci usmerava se na C->Sledeci (praktično, čvor 17, ili, u opštem smislu, "sledeći" čvor)

Pošto smo se pobrinuli oko svih referenci (to jest, pošto su čvorovi 12 i 17 uspešno 'prespojeni'), može se ukloniti čvor 15 (odnosno, preko pokazivača C, može se osloboditi memorija koju čvor 15 zauzima).

Uklanjanje čvora 2
Slika 20. - Čvor #15 je uklonjen.

Dopunjena metoda za uklanjanje čvora ima sledeći oblik:

		
void uklanjanje(int indeks) {
	// Provera indeksa (u smislu: da li se
	// predata vrednost nalazi u granicama liste):
	if (indeks < 0 || indeks > this->brojElemenata) {
		std::string s = "Greška pri uklanjanju elementa.\n";
		s += "Indeks " + std::to_string(indeks) + " je van granica liste.";
		throw s;
	}
	
	// Uklanjanje elementa sa početka liste:
	if (indeks == 0) {
		uklanjanjePrvi();
		return;
	}
	
	// Uklanjanje elementa sa kraja liste;
	if (indeks == this->brojElemenata - 1) {
		uklanjanjePoslednji();
		return;
	}
	
	// Uklanjanje elementa sa neke od međupozicija:
	this->trenutni    = pronalazenjeElementa(indeks - 1);
	struct Cvor *c    = trenutni->sledeci;
	trenutni->sledeci = c->sledeci;
	delete c;		
}
		
	
Slika 21. - Metoda za uklanjanje elementa liste preko indeksa (uz korišćenje dve pomoćne metode koje smo prethodno prikazali).

Verujemo da je u ovom slučaju lakše razumeti sam kod, u odnosu na šemu koju smo prethodno videli (slike #19 i #20).

Ispis liste u proizvoljnom formatu

Ispis liste može se shvatiti kao kombinacija prolaska kroz listu i ispisivanja pojedinačnih vrednosti.

Za pretvaranje celobrojne vrednosti u nisku, koristićemo specijalizovanu metodu iz 'domaće radinosti' (koju ćemo smestiti u private odeljak klase):

		
#define ASCII_0 48
// znak '0' ima ASCII kod 48

private:

std::string intUString(int n) {
	std::string s = "", p = "";
	
	// Negativni broj svešćemo odmah na apsolutnu vrednost
	// i dodati znak "-" na početak niske
	if (n < 0) {
		s += "-";
		n = abs(n);
	}
	
	// Cifre broja očitavaju se zdesna nalevo:
	while (n > 0) {
		p += n % 10 + ASCII_0;
		n /= 10;

		// cifra jedinica broja n dobija se kao:
		// n % 10
	}
	

	// Glavni string s, popunjava se čitanjem pomoćnog
	// stringa (p) unazad i upisivanjem znakova

	int d = p.length() - 1;
	
	while (d >= 0) {
		s += p[d];
		d--;
	}
	
	return s;
}
		
	
Slika 22. - Pomoćna metoda za pretvaranje celobrojne vrednosti u nisku znakova.

Usput: postoji i efikasniji algoritam za pretvaranje celobrojnih vrednosti u niske: u svakom koraku, ispituje se ostatak pri deljenju preostale vrednosti sa 100, i potom se rezultat operacije vrednost % 100 pretražuje u pomoćnoj niski koja sadrži ispis brojeva od 0 do 99.

Sada se može definisati i metoda za ispis elemenata liste:

		
public:

std::string ispisListe() {
	std::string s = "";

	// Ispis u slučaju da je lista prazna:
	
	if (this->prvi == NULL) {
		s += "Lista je prazna.";
		return s;
	}

	// Ispis u slučaju da lista sadrži bar jedan element:
	
	struct Cvor *c = this->prvi;
	
	while (c != NULL) {
		s += intUString(c->vrednost) + ", ";
		c  = c->sledeci;
	}
	
	// Malo elegancije nije naodmet,
	// i stoga ćemo skinuti poslednji zarez	
	return s.substr(0, s.length() - 2);
}
		
	
Slika 23. - Metoda za ispis liste.

Ukoliko je lista prazna, biće ispisana prikladna poruka, dok će u suprotnom elementi liste biti ispisani redom.

Metoda za ispis svakako se može dopuniti opcijama za formatiranje (tako da se mogu birati i drugi znakovi za razdvajanje elemenata, to jest, tako da se umesto zareza može koristiti i razmak, prelazak u novi red i sl), ali, to ostavljamo vama - za vežbu. :)

Nekoliko reči o izuzecima

Zarad početnog upoznavanja sa izuzecima, uzećemo za primer situaciju sa kojom smo se susreli u toku implementacije liste: kako program treba da 'reaguje' kada funkcija pronalazenjeVrednosti pokuša da pristupi indeksu koji je manji od nule ili veći od gornje granice liste?!

U metodi za pronalaženje vrednosti čvora, na početku stoji if grananje preko koga se proverava da li je uneti indeks unutar granica liste (što omogućava da se lako zaustavi svaki pokušaj pristupa elementima koji ne zadovoljavaju uslov), međutim, šta tačno treba da vrati metoda (čiji je povratni tip int) - ukoliko program pokuša da pristupi elementu čiji je indeks van granica liste?

Metoda, teoretski, može vratiti (recimo) INT_MIN, najmanju negativnu celobrojnu vrednost koja se da zabeležiti - što se može shvatiti kao "signal" da je nešto "krenulo naopako", ali, takav pristup stvara određene probleme:

  • iako nije 'previše verovatno' u većini situacija, INT_MIN može biti vrednost nekog od čvorova
  • ceo pristup (najprostije rečeno) - nije nimalo elegantan

Jedno od rešenja može biti da metoda za pronalaženje vrednosti čvora - ukoliko dođe do greške - uopšte ne vrati celobrojnu vrednost, već - izuzetak * - što podrazumeva da se pozivajućoj funkciji vraća informacija o pojavi greške, kao i niska ili objekat ** koji se mogu "uhvatiti" (preko posebnog mehanizma u programskom jeziku), i prikazati.

* Izuzetak je zapravo 'vanredni događaj u toku izvršavanja programa', ali, termin izuzetak se takođe koristi i onda kada se misli na podatke koji se prosleđuju unutar programa - u slučaju pojave izuzetka.

** U pitanju su niske ili objekti preko kojih se (praktično) prosleđuju informacije o prirodi greške.

Kada kažemo da izuzeci zahtevaju poseban tretman, mislimo na to da ne možemo samo napisati sledeći kod (nezavisno od toga da li metoda vraća izuzetak ili ne vraća) ....

		
int vrednost = lista[i];
		
	
Slika 24. - Pristup elementu liste čiji je indeks izvan granica.

.... jer, ako je vrednost i izvan granica liste, program će "pući" na donekle nepredvidljiv način.

Ako se sledeći izuzetak definiše unutar metode za pristup elementu preko indeksa ....

		
if (indeks < 0 || indeks > this->brojElemenata) {
	throw "Uneti indeks je izvan granica liste!";
}
		
	
Slika 25. - Funkcija throw koja definiše izuzetak.

.... pri pozivu metode, potrebno je koristiti kod po sledećem obrascu:

		
try {
	int vrednost = lista[i];
}
catch (string poruka) {
	std::cout << poruka << endl;
}
		
	
Slika 26. - Blok try-catch koji omogućava prihvat i obradu izuzetaka.
  • ako se ne desi ništa nepredviđeno, metoda neće vratiti izuzetak (praktično, korisnik programa neće imati predstavu da je u izvršavanje uključen mehanizam provere)
  • u suprotnom, čim se u nekoj instrukciji unutar odeljka try pojavi greška (koja je pri tom 'uhvaćena' i prosleđena preko funkcije throw), prekida se izvršavanje i prelazi se na odeljak catch (preko koga se "uhvaćeni" izuzetak može iskoristiti za ispis poruke korisniku, ili, za neki drugi oblik debugging-a (u gornjem primeru, samo ispisujemo poruku))

Tipično se jednostavni 'tekstualni' izuzeci hvataju preko sledeće sintakse ....

		
try {
	// naredbe
} catch (const char * poruka) {
	// std::cout << poruka << endl;
}
		
	

.... ali, budući da ovoga puta samo 'ilustrujemo' principe upotrebe izuzetaka (i pri tom su poruke bile opsežnije, i formatirane na poseban način), tekstualne izuzetke smo definisali na pojednostavljen način.

U svakom slučaju, izuzeci pružaju brojne mogućnosti za "hvatanje" poruka o greškama, i stoga ćemo (kao što smo već naveli), ovoj zanimljivoj temi uskoro posvetiti više pažnje.

Za kraj, osvrnućemo se na osnovne principe za korektno navođenje komentara u programskom kodu.

Ukratko o vođenju dokumentacije

Ako se određena struktura podataka (ili bilo kakav drugi kod), kreira sa idejom o 'javnoj dostupnosti' takvog koda, pravila dobrog programiranja nalažu da kod treba da bude propraćen detaljnom dokumentacijom (ili makar osnovnim uputstvima).

Ako programski kod neće koristi drugi programeri (to jest, ako ćemo ga koristi 'mi sami'), može delovati privlačno da se 'odreknemo' pisanja komentara (jer - "znamo valjda šta smo pisali u kodu"), međutim - na osnovu ličnog i kolektivnog iskustva - i dalje preporučujemo pisanje dokumentacije (tj. komentara), za sve delove koda iole većeg obima i značaja (bez obzira na to 'ko će na kraju koristiti kod').

Iako ni najmanje ne sumnjamo u sposobnost mnogih čitalaca, da dugo pamte veće količine informacija, u praksi se vrlo često dešava da programeri - 'koji znaju šta su pisali' - zaborave šta su pisali, i stoga je više nego preporučljivo da sprečimo - uz pravilno vođenje dokumentacije - 'iznenađenja' po pitanju toga da li ćemo se za tri nedelje ili četiri meseca setiti (dovoljno brzo) čemu služi funkcija X koja poziva funkciju Y (jer pomenuta 'iznenađenja' - bojimo se da je tako - ne spadaju baš u kategoriju lepih i prijatnih iznenađenja). :)

Kada kažemo "delovi koda iole većeg obima i značaja", mislimo na to da pravilno vođenje dokumentacije svakako podleže pravilima koja se tiču i toga šta ne treba pisati.

Na primer, ukoliko funkcija namenjena sabiranju brojeva doslovno nosi naziv sabiranje i sadrži nekoliko veoma nedvosmislenih naredbi, komentari nalik sledećim komentarima: "ovo je funkcija za sabiranje brojeva" i "ovo je naredba za računanje zbira" .... nisu baš od prevelike koristi.

Pogledajmo nekoliko primera neadekvatne upotrebe komentara:

		
// ovo je funkcija za sabiranje celih brojeva:
int sabiranje(int a, int b) {
	// inicijalizacija promenljive:
	int c;
	// ovo je naredba za računanje zbira:
	c = a + b;
	// pozivajućoj funkciji se prosleđuje
	// vrednost promenljive c:
	return c;
}

int i = 1;
// petlja koja se ponavlja za sve vrednosti
// promenljive i, u rasponu od 1 do 10:
while (i <= 10) {
	// ispis trenutne vrednosti promenljive i:
	std::cout << i << " ";
	// inkrementacija promenljive i:
	++i;
}
		
	
Slika 27. - Primeri neadekvatnog (to jest, 'nepotrebnog') komentarisanja programskog koda.

Najjednostavnije: ukoliko je zaista očigledno čemu služi određeni deo koda, dodavanje suvišnih komentara, ne samo da ne povećava čitljivost koda, već (naprotiv), može se reći da se čitljivost - umanjuje.

Kako onda (nasuprot prethodnim primerima), izgleda adekvatno prokomentarisan programski kod?

Iako šira diskusija o standardima za vođenje dokumentacije tek prestoji u nekom kasnijem trenutku (u pitanju je relativno opsežna tematika koja zaslužuje zaseban članak), ukratko ćemo navesti da pravilno komentarisanje koda podrazumeva (pre svega): blok komentare ispred funkcija, linijske komentare koji označavaju odeljke koda u funkcijama (zarad lakšeg prepoznavanja), i naravno napomene na mestima na kojima su potrebna dodatna pojašnjenja (odnosno, na mestima na kojima može 'doći do zabune').

Pogledajmo nekoliko primera:

- Školski primer (komentari kakvi se pojavljuju u udžbenicima ili na sajtovima koji se bave programiranjem i sl):

		
/*
	Funkcija za proveru pripadnosti tačke pravougaoniku,
	uz korišćenje struktura Tacka i Pravougaonik, koje su
	definisane u datoteci "strukture.h"
	(tačka je definisana preko koordinata x i y,
	a pravougaonik preko koordinata x1, y1, x2 i y2.
*/

bool pripadnost_pravougaoniku(Tacka t, Pravougaonik p) {
	// Umesto da odmah proverimo da li
	// tačka pripada pravougaoniku,
	// radije ćemo prvo ispitati sve
	// uslove koji odgovaraju situacijama
	// u kojima tačka (potencijalno)
	// NE PRIPADA pravougaoniku:
	
	if (t.x < p.x1) return false;
	if (t.x > p.x2) return false;
	if (t.y < p.y1) return false;
	if (t.y > p.y2) return false;

	// Ako tačka (ipak) pripada pravougaoniku ....
	
	return true;
}
		
	
Slika 28. - Školski primer adekvatnih komentara u programskom kodu.

- Praktičan primer:

		
/**
	Provera pripadnosti tačke pravougaoniku;
	Strukture Tacka (polja: x i y) i
	Pravougaonik (polja: x1, y1, x2, y2),
	definisane su u datoteci "strukture.h"
*/

bool pripadnost_pravougaoniku(Tacka t, Pravougaonik p) {
	if (t.x < p.x1) return false;
	if (t.x > p.x2) return false;
	if (t.y < p.y1) return false;
	if (t.y > p.y2) return false;

	return true;
}
		
	
Slika 29. - Praktičan primer adekvatnih komentara u programskom kodu.

Na samom kraju, (ipak) još malo "filozofije" ....

U funkcijama za umetanje ili uklanjanje elemenata, pisali smo (iznad blokova koda), komentare nalik na "Umetanje na prvu poziciju", "Uklanjanje elementa sa kraja liste" i sl.

Sa jedne strane, može delovati da su neki od takvih komentara u neskladu sa pravilima koja smo naveli (budući da se ispod komentara nalaze mali blokovi koda ili pozivi funkcija čiji naziv jasno nagoveštava svrhu funkcije), međutim, u praktičnom smislu:

  • u pitanju su komentari u okviru članka u kome se tek upoznajemo sa implementacijom ulančane liste :)
  • u 'svakodnevnim uslovima', ukoliko funkcije imaju više od cca. 15 linija 'netrivijalnog programskog koda', takođe je sasvim primereno ukoliko se najvažnije celine označe kratkim informativnim komentarima

Iako se često smatra da "kod treba da opisuje sam sebe", tj. pojedinačne funkcije treba da budu dovoljno "kratke i jasne" da (praktično) ne zahtevaju komentare (i naravno da se sa takvim principima u potpunosti slažemo) - u praksi - ukoliko postoje iole veća 'odstupanja' između iskustva i umeća programera koji su pisali kod i programera koji čitaju kod, nastaje situacija u kojoj bi kratki komentari "ipak dobro došli" (da ne govorimo o situacijama u kojima samo deluje da je kod "jasan sam po sebi" - a zapravo nije).

Pažnja čitalaca se (najčešće) prvo usmerava na pojavu komentara u kodu (nalik tome kako na web sajtovima prvo zapažamo naslove, pa tek onda sadržaj pasusa), * i gotovo je sigurno da će neko, ko se prvi put susreće sa funkcijom, prvo pročitati nekoliko kratkih komentara (koji zajednički potcrtavaju tok izvršavanja funkcije), i tek onda se usmeriti na čitanje programskog koda - ukoliko za tim uopšte postoji potreba (drugim rečima: već smo zaključili da ne treba 'daviti' sa komentarima, ali, ukoliko smo pročitali neki komentar koji "dobro dođe", bićemo zahvalni na tome što je neko utrošio vreme da "sažme suštinu u nekoliko reči").

Kao što već rekosmo, priča o dokumentaciji zahteva malo više vremena i prostora, i time ćemo se (veoma rado) baviti nekom drugom prilikom.

* U pitanju je svojevrsno "prepoznavanje kontrasta u fotografskom smislu" (na primer: ako određena slika koju posmatramo sadrži crnu tačku na svetloj jednobojnoj pozadini, najpre ćemo zapaziti crnu tačku).

Kratak rezime ....

Na samom kraju, ostaje samo da vam preporučimo da pokušate, da samostalno implementirate ulančanu listu (ponovo), ili (zašto da ne), neku drugu strukturu podataka.

Prvi 'kandidat' bi mogla biti dvostruko ulančana lista (lista čiji čvorovi imaju i pokazivač na prethodni element), a svakako preporučujemo i samostalnu implementaciju reda za čekanje.

Zapravo, ukoliko vam vreme i druge obaveze dozvoljavaju, toplo preporučujemo da probate sve navedene implementacije, kao i to da inače "ne štedite" sebe previše kada su u pitanju obimniji programerski zadaci.

Ako zaista volite programiranje, rad na izazovnim projektima, i dalje je jedini način da zaista naučite programiranje.

Sledeća tvrdnja može vam delovati čudno, ali, duboko smo uvereni (na osnovu iskustva), da je samostalna izrada klasa preko kojih se implementiraju liste, redovi, stekovi i sl (naravno, pod uslovom da neko ko se u taj poduhvat upušta, ipak ima dovoljno iskustva i predznanja) - mnogo lakši i jednostavniji poduhvat - od pukog čitanja tekstova o implementaciji.

Možda niste još uvek u stanju da poverujete u navedene tvrdnje, ali, bar probajte da sami implementirate nešto od prikazanog i predloženog - možete se prijatno iznenaditi. :)

Autor članka Nikola Vukićević Za web portal codeblog.rs
Napomena: Tekstovi, slike, web aplikacije i svi ostali sadržaji na sajtu codeblog.rs (osim u slučajevima gde je drugačije navedeno) predstavljaju intelektualnu svojinu autora sajta codeblog.rs i zabranjeno je njihovo korišćenje na drugim sajtovima i štampanim medijima, kao i bilo kakvo drugo korišćenje u komercijalne svrhe, bez eksplicitnog pismenog odobrenja autora.
© 2020-2026. Sva prava zadržana.
Facebook LinkedIn Twitter Viber WhatsApp E-mail
početna > Članci > Tutorijal: Implementacija jednostruko ulančane liste u programskom jeziku C++
codeBlog codeBlog
Sajt posvećen popularizaciji kulture i veštine programiranja.
Napomena: Tekstovi i slike na sajtu codeblog.rs (osim u slučajevima, gde je drugačije navedeno) predstavljaju intelektualnu svojinu autora sajta codeblog.rs i zabranjeno je njihovo korišćenje na drugim sajtovima i štampanim medijima, kao i bilo kakvo drugo korišćenje u komercijalne svrhe, bez eksplicitnog odobrenja autora.
© 2020-2026. Sva prava zadržana.
Facebook - logo
Instagram - logo
LinkedIn - logo
Twitter - logo
E-mail
Naslovna
   •
Uslovi korišćenja
   •
Obaveštenja
   •
FAQ
   •
Kontakt