Sunteți pe pagina 1din 17

Structuri de date implementate dinamic

Structurile dinamice de date sunt date structurate ale căror componente


se aloca în mod dinamic. Alocarea dinamica a componentelor structurii impune
un mecanism prin care o nouă componentă introdusă este legată în succesiune
logică de corpul structurii deja format până atunci. Rezultă că este necesar ca
fiecare componentă, pe lângă informația propriu-zisa pe care o deține, trebuie
sa conțină și o informație de legătura cu componenta cu care se leagă logic în
succesiune.
Datele alocate dinamic sunt memorate într-o zonă de memorie dedicată
numită HEAP a cărui dimensiune este variabilă, în funcție de necesități.
In HEAP, structura respectivă va avea zone alocate componentelor sale
în locurile găsite disponibile, care nu se succed întotdeauna în ordinea în care
este realizată înlănțuirea logică.
In funcție de tipul legăturilor realizate între componente, există
următoarele tipuri de structuri:
- liste simplu înlănțuite liniare și circulare;
- liste dublu înlănțuite liniare și circulare;
- stive;
- cozi;
- arbori;
- rețele.

4. Liste liniare simplu înlănțuite

Lista este o mulțime de date alocate dinamic, având un număr variabil de


elemente de același tip, între care există o anumită relație de ordine. Tipul
elementelor este, de obicei, definit de utilizator.
Elementele unei liste se mai numesc și noduri. Dacă între nodurile unei
liste există o singură relație de ordine, atunci lista este simplu înlănțuită.

4.1 Prezentarea structurii. Implementare


Relația de ordine de la listele liniare simplu înlănțuite o reprezintă, de
obicei, relația de succesor, adică fiecare nod conține o adresă către următorul
nod al listei. În astfel de liste există numai un nod care nu are succesor (ultimul
nod) și numai un nod care nu e succesorul nimănui (primul nod sau capul listei).
Prin convenție, variabila care va memora adresa primului nod se va numi baza.
O astfel de listă ar putea fi reprezentată în modul următor:
info1 adr2 info2 adr3 … infon NULL

adr1 adr2 adrn


baza
Fig. 4.1

unde:
- info1, info2, …, infon reprezintă informația memorată în fiecare dintre
cele n noduri;
- adr1, adr2, …, adrn sunt adresele din memorie ale nodurilor listei.

Declararea unui nod, care conține informații de tip oarecare tip_date, se


realizează prin următoarea secvență de instrucțiuni:

struct nod {
tip_date info;
nod *next;
};
nod *baza;

Prin această declarație s-a definit o structură cu doi membri:


- un membru de tip oarecare denumit aici tip_date, care conține informația
utilă denumită info și
- o variabilă de tip pointer care indică, de asemenea, către un obiect de tip
nod.
Pointerul denumit baza va păstra în permanență adresa ultimului nod
introdus.
Această construcție devine mult mai ușor de înțeles dacă ne întoarcem la
exemplul prezentat anterior, care descria posibilitățile de amplasare în sala de
spectacol a excursioniștilor. Considerând cel de-al doilea mod de amplasare,
fiecare membru al grupului poate fi asimilat cu o variabilă de tip nod, care poartă
două informații: numele său, ce poate fi asimilat componentei info și biletul din
buzunar, pe care este scris locul următorului excursionist, acesta putând fi
asimilat cu componenta next.

Listele înlănțuite reprezintă cele mai simple structuri dinamice de date.


Aceste structuri rezolvă problema menținerii unor liste ale căror componente
pot fi adăugate sau șterse la dorință.
In figura 4.2 este prezentată o listă înlănțuită formată dintr-un singur
pointer, denumit baza și trei noduri.
baza

z y x

Fig. 4.2

4.2 Operații specifice


Asupra unei liste liniare simplu înlănțuite se pot efectua următoarele
operații specifice acestui tip de structură.
- crearea listei;
- parcurgerea listei în scopul efectuării unor anumite operații;
- inserarea unui nod într-o poziție impusă;
- ștergerea unui anumit nod.

Crearea unei liste


Să presupunem că ne propunem să implementăm o listă nouă. Pentru
aceasta trebuie să definim, în primul, rând structura unui nod al listei:

struct nod {
char data;
nod *next;
};
nod *baza, *temp;

Pentru memorarea adresei ultimului element vom utiliza pointerul baza. In


această situație, lista nu conține încă nici o componentă și deci vom inițializa
pointerul baza la valoarea NULL:

baza = NULL;

Pentru a introduce o componentă în listă, aceasta trebuie în primul rând


creată. O astfel de componentă poate fi creată cu ajutorul funcției new. Această
funcție are rolul de a genera o variabilă dinamică de un anume tip și de a returna
adresa acesteia.
baza baza

a temp b temp

temp = new(nod) temp ->data = ‘x’ x

baza baza

c d
temp temp

temp->next = baza x baza = temp


x

Fig. 4.3

Instrucțiunea

temp = new(nod);

creează un nou nod a cărui adresă este memorată în variabila de tip pointer
temp.
Situația creată este prezentată în figura 4.3a. Pasul următor constă în
atribuirea unei valori noii componente:

temp -> data = ’x’;

Rezultatul acestei atribuiri este prezentat în figura 4.3b. Celălalt câmp al


nodului creat, denumit temp -> next, este destinat pentru memorarea adresei
componentei dinamice următoare. Deoarece această componentă nu există
încă, în câmpul menționat va fi adusă valoarea NULL. Această operație se
realizează prin executarea instrucțiunii

temp -> next = baza;

Situația creată este prezentată în figura 4.3c. Ultima operație necesară


pentru completarea secvenței este memorarea adresei acestui nod în pointerul
baza.

baza = temp;

Rezultatul obținut fiind prezentat în figura 4.3d. Secvența descrisă


reprezintă, de fapt, un algoritm general pentru înserarea unei componente la
începutul unei liste.
Figura 4.4 prezintă evoluția situației la repetarea secvenței anterioare,
pentru introducerea celei de a doua componente a listei.
baza

baza
temp x
temp = new(nod)
temp
x
a
d b

baza baza
x x
temp -> data = ’y’ temp->next = baza
temp temp

c y y
d

baza
e x
baza = temp
temp

Fig. 4.4

Așadar, repetarea ciclică a secvenței

temp = new(nod);
temp -> data = ’valoare’;
temp -> next = baza;
baza = temp;

face posibilă introducerea la începutul listei a oricâte componente dorim.

Traversarea unei liste.


Această operație este necesară în situația în care dorim să regăsim un
nod al listei sau să executăm unele operații asupra nodurilor acesteia, de
exemplu, în scopul ordonării acestora după un criteriu impus sau a afișării
informației conținută în nodurile listei.
Pentru a găsi un procedeu convenabil de regăsire a unei componente a
listei să observăm că, dacă un pointer p conține adresa unei componente, după
executarea atribuirii

p = p -> next;

p va conține adresa componentei următoare. Această instrucțiune poate fi


repetată până când p devine NULL, situație care indică faptul că am ajuns la
sfârșitul listei. Prezentăm în continuare, algoritmul pentru afișarea informației
conținută în nodurile unei liste, organizat sub forma unei funcții C++:

void afiseaza() {
nod *p;
while (p) {
cout << p -> data << ” ”;
p = p -> next;
}
cout << endl;
}

Inserarea unui nod într-o poziție impusă


Presupunem că dorim să înserăm un nod nou într-o listă existentă. Pentru
a face posibilă inserarea unei componente în mijlocul listei trebuie să avem un
pointer către componenta care va precede noua componentă. Astfel, să
presupunem că avem o listă care conține nodul indicat de pointerul tom, iar noi
dorim să înserăm un nou nod, indicat de pointerul dick după elementul tom. O
funcție care realizează această operație este următoarea:

void insereaza_dupa(nod *tom, *dick) {


dick -> next = tom -> next;
tom -> next = dick;
}

Efectul funcției inserare_dupa este prezentat în fig. 4.6. Situația anterioară


executării procedurii este prezentată în figura 4.6a, iar cea de după executarea
procedurii în figura 4.6b. Această funcție nu va lucra corect în situația în care
tom este ultimul nod a listei.

Putem imagina o funcție care realizează înserarea unui nod dick înaintea
nodului existent tom. In această situație trebuie găsit pointerul care indică către
nodul anterior lui tom. Pentru a găsi acest pointer, pe care să-l denumim prec,
trebuie traversată lista în modul următor:

while (prec -> next != tom) {


prec = prec -> next;
}

unde valoarea inițială a pointerului prec va fi baza. La ieșirea din ciclu, prec va
indica spre nodul ce precede componenta tom.
dick

baza

tom

a
1) dick -> next = tom -> next
dick

2) tom -> next = dick

baza

tom

b
Fig. 4.6
O funcție care înserează un nod dick înaintea unui alt nod tom trebuie să
depisteze situația în care tom este primul nod al listei. Ținând seama de aceste
considerente, o funcție insereaza_inainte ar putea fi scrisă precum urmează:

void insereaza_inainte(nod *tom, nod *dick, nod *baza) {


nod *prec;
if (tom = baza) {
dick -> next = tom;
baza = dick;
{
else {
prec = baza;
while (prec -> next != tom) {
prec = prec -> next;
}
prec -> next = dick;
dick -> next = tom;
}
}

Dacă tom indică primul nod al listei (tom = baza), atunci, prin înserarea
nodului dick, acesta devine primul nod al listei. In fig. 4.7a și b este prezentată
situația înainte și după înserarea nodului dick, în cazul în care tom este prima
componentă a listei.

Dacă tom nu este prima componentă a listei, atunci lista este traversată
în scopul găsirii valorii pointerului prec. In continuare este înserat nodul dick
în fața nodului tom (figura 4.7c).

dick

a
baza

tom

dick

1) dick -> next = tom


2) baza = dick b
baza a

tom

2) dick -> next = tom


dick

c
1) prec -> next = dick
prec

tom

Fig. 4.7

Ștergerea unui nod


Operația de ștergere este foarte simplă dacă avem un pointer care să
indice către nodul precedent celui care trebuie șters. Dacă denumim acest
pointer prec, ștergerea este realizată de instrucțiunea

prec -> next = prec -> next -> next


Operația realizată este prezentată în fig. 4.8. Figura 4.8a prezintă situația
înaintea operației de ștergere, iar figura 4.8b - situația după ștergere.

prec -> next prec -> next -> next

prec

a
prec -> next = prec -> next -> next

prec

Fig. 4.8
Remarcăm notația inedită în acest context

prec -> next -> next

dar, dacă ținem seama că prec -> next este un pointer, atunci această notație
devine firească.

De obicei însă, noi avem un pointer chiar către nodul care trebuie șters,
fie acesta tom și, în acest caz, trebuie să parcurgem lista pentru a găsi valoarea
pointerului prec. Operațiile menționate sunt realizate de funcția următoare:

void sterge(nod *tom, *baza) {


nod *prec;
if (tom == baza)
baza = tom -> next;
else {
prec = baza;
while (prec -> next != tom) {
prec = prec -> next;
prec - > next = tom -> next;
}
}
}

Funcția sterge face distincție între situația în care trebuie șters primul nod
și cea în care se șterge un nod oarecare din listă.

După cum am constatat, la stocarea informației în liste înlănțuite, ultimul


element introdus este primul accesibil. Nu este dificil să aranjăm componentele
listei astfel încât să obținem un șir în care prima componentă accesibilă este
prima componentă introdusă. Șirul este o listă de tipul celei prezentate anterior,
care conține însă un pointer suplimentar ce memorează adresa ultimei
componente, (vezi figura 4.9).

prim

z y x
ultim

Fig. 4.9

Pentru implementarea acestei structuri se utilizează doi pointeri. Un


pointer denumit prim, care memorează adresa primului nod al șirului (ultimul
introdus) și un alt pointer denumit ultim care memorează adresa ultimului nod
(primul introdus). Acest mod de organizare facilitează uneori exploatarea listei.

5. Liste circulare simplu înlănțuite

O listă circulară simplu înlănțuită se obține dintr-o listă liniară simplu


înlănțuită prin legarea ultimului nod al listei cu primul nod al acesteia (figura 5.1).

prim ultim

z y x

Fig. 5.1
O listă circulară poate fi obținută numai dintr-o structură de tip șir (figura
4.9), deoarece este necesară cunoașterea adresei ultimului nod. Închiderea
listei se realizează prin instrucțiunea:

ultim -> next = prim;

Prin acest aranjament lista poate fi parcursă perpetuu, fapt care poate
constitui un avantaj în unele aplicații.

6. Liste liniare dublu înlănțuite

Lista dublu înlănțuită este o listă în care fiecare nod are două legături: o
legătură către nodul precedent și una către nodul următor. Legătura către nodul
următor este păstrată de pointerul next iar cea către nodul anterior – de pointerul
back. Deci, un nod al listei liniare dublu înlănțuite va fi declarat în modul următor:
struct nod {
char data;
nod *next, *back;
};
nod *prim, *ultim;

Am declarat și doi pointeri prim și ultim, care vor reține adresele primului
nod și respectiv, al ultimului element al listei. In figura 6.1 este prezentată o listă
liniară dublu înlănțuită.

prim

ultim
m

Fig. 6.1

Listele liniare dublu înlănțuite permit aceleași operații de baza ca și listele


simplu înlănțuite. Diferența fața de acestea consta in faptul că, pentru fiecare
nod, se reține și adresa elementului anterior, ceea ce permite traversarea listei
în ambele direcții.
prim prim
baza

ultim ultim
a b

temp temp
baza baza

temp = new(nod) temp ->data = ‘x’ x

prim prim
prim baza
prim = temp

ultim ultim
ultim = temp
c d
temp -> next = prim
temp temp
apri baza

x x

temp -> back = ultim


Fig. 6.2

O listă liniară dublu înlănțuite permite următoarele operații:


- crearea listei;
- adăugarea unui nod la dreapta;
- adăugarea unui nod la stânga;
- adăugarea unui nod în interiorul listei;
- ștergerea unui nod din interiorul listei;
- ștergerea unui nod la dreapta unui nod dat;
- ștergerea unui nod la stânga unui nod dat;
- afișarea listei de la stânga la dreapta;
- afișarea listei de la dreapta la stânga.
O funcția C++ pentru crearea unei liste liniare dublu înlănțuite este
următoarea:

void creare(nod *prim, nod *ultim) {


prim = NULL;
ultim = NULL;
cout << ” Introduceți informația ”;
cin >> ch;
temp = new(nod);
temp -> data = ch;
temp -> next = prim;
temp -> back = ultim;
prim = temp;
ultim = temp;
}

Efectul operațiilor prevăzute în funcția creare sunt prezentate în figura 6.2.


In continuare este prezentată o funcție pentru adăugarea unui nod la dreapta
unui nod existent.

void adaug_dreapta(nod *prim, nod *ultim) {


cout << ” Introduceți informația ”;
cin >> ch;
temp =new(nod);
temp -> data = ch;
prim -> next = temp;
temp -> back = prim;
temp -> next = NULL;
ultim = temp;
}

Operațiile prevăzute anterior sunt explicate în figura 6.3.


prim prim
baza
prim = temp
ultim
ultim x

a b
d d
temp = new(nod)
temp temp
baza baza

x temp -> data = ch y

1) prim -> next = temp


prim
baza

ultim
x y

c 2) temp -> back = prim


d temp
baza

prim temp -> next = NULL


baza

x y
ultim
d
d ultim = temp
temp
baza

Fig. 6.3

7. Liste circulare dublu înlănțuite

Pentru a elimina inconvenientele structurilor anterioare, care presupun


verificări speciale pentru prima componentă, se pot utiliza listele dublu
înlănțuite. Pentru realizarea acestora, fiecare nod va conține doi pointeri, unul
indicând către nodul anterior, iar celălalt către nodul următor. Definirea nodurilor
unei liste circulare poate fi realizată cu ajutorul declarației:

struct nod {
char data;
nod *prec, *suc;
};
unde prec este pointerul către nodul precedent iar suc este pointerul către nodul
următor.
Simetria listei poate fi completată prin legarea primei și ultimei
componente, obținându-se astfel o listă circulară dublu înlănțuită (figura 7.1a).

baza

a
baza

Fig. 7.1

Funcțiile pentru manipularea listelor circulare sunt substanțial simplificate


dacă definim lista vidă ca un inel cu o singură componentă închisă pe ea însăși
(fig. 7.1b).
Pentru a introduce un nod dick după un nod tom se poate utiliza
următoarea funcție:

void insert_dupa(nod *tom, nod *dick) {


dick -> suc = tom -> suc;
dick -> pred = tom;
tom -> suc -> prec = dick;
tom -> suc = dick;
}

Operațiile efectuate sunt explicate în figura 7.2


dick

tom

dick
dick -> suc = tom -> suc

tom
om

dick -> pred = tom

dick

tom
omt

2) tom -> suc = dick


1) tom -> suc -> prec = dick

c
Fig. 7.2

8. Stive

O stivă este o listă cu acces la un singur capăt, numit “vârful” stivei.


Singurele operații permise într-o stivă sunt inserare în prima poziție și ștergere
din prima poziție (eventual și citire din prima poziție). Aceste operații sunt
denumite, în mod tradițional „push” (pune pe stivă) și “pop” (scoate din stivă).
Pentru a efectua astfel de operații, nu mai este necesar să se specifice poziția
din listă, aceasta fiind implicită. O stivă mai este numită și listă LIFO („Last In
First Out‟), deoarece ultimul element pus este primul care va fi extras din stivă.
Operațiile care pot fi efectuate pe o stivă sunt:
- inițializare stivă vidă (initSt)
- test stivă vidă (emptySt)
- pune un obiect în stivă (push)
- extrage obiectul din vârful stivei (pop)
- obține valoarea elementului din vârful stivei, fără scoatere din stivă (top)

9. Cozi

O coadă („Queue"), numită și listă FIFO ("First In First Out") este o listă la
care adăugarea se face pe la un capăt (de obicei la sfârșitul cozii), iar extragerea
se face de la celălalt capăt (de la începutul cozii). Ordinea de extragere din
coadă este aceeași cu ordinea de introducere în coadă, ceea ce face utilă o
coadă în aplicațiile unde ordinea de servire este aceeași cu ordinea de sosire:
procese de tip "vânzător - client" sau "producător - consumator". In astfel de
situații coada de așteptare este necesară pentru a acoperi o diferență temporară
între ritmul de servire și ritmul de sosire, deci pentru a memora temporar cereri
de servire (mesaje) care nu pot fi încă prelucrate.
Operațiile care pot fi efectuate pe o „coadă" sunt:
- inițializare coadă (initQ);
- test coadă goală (emptyQ) ;
- adăugare un obiect la coadă (addQ, insQ, enqueue);
- scoatere un obiect din coadă (delQ, dequeue);
- citire a unor noduri din coada.

S-ar putea să vă placă și