Sunteți pe pagina 1din 106

Structuri de date

CURS 1

1. Algoritmii şi caracteristicile lor

Noţiunea de algoritm este folosită pentru a descrie succesiunea de paşi ce alcătuiesc o operaţie, un
procedeu de rezolvare a unei probleme.

DEFINIŢIE. Algoritmul este o mulţime finită de instrucţiuni care, dacă sunt urmate, îndeplinesc o
anumită funcţie, rezolvă o anumită problemă. Fiecare algoritm trebuie să satisfacă următoarele
cerinţe legate de :
i. Intrare (Input): există zero sau mai multe valori care sunt primite din exterior, ca date de
intrare; această mulţime se numeşte INPUT, şi are proprietatea card(INPUT)  0;
ii. Ieşire (Output): există cel puţin o valoare furnizată ca ieşire, deci card(OUTPUT)  0;
iii. Specificarea: fiecare instrucţiune trebuie să fie precisă, clară, bine specificată şi neambiguă;
iv. Finitudine: oricare ar fi valorile de intrare, algoritmul trebuie să se termine într-un număr
finit de paşi;
v. Efectivitatea: fiecare instrucţiune trebuie să fie suficient de explicită astfel încât să poată fi
efectuată manual (de către o persoană cu creionul);
vi. Corectitudinea: pentru orice set de date de intrare, algoritmul trebuie să producă întotdeauna
la ieşire rezultatul corect;
vii. Generalitatea: algoritmul trebuie să rezolve mai multe instanţe ale aceleiaşi probleme.

Un algoritm poate fi specificat (descris) în moduri diferite, două dintre acestea fiind metoda
schemei logice şi cea a utilizării limbajului pseudocod (limbaj apropriat ca sintaxă de Pascal). Vom
da în continuare câteva exemple de algoritmi şi îi vom analiza.

EXEMPLUL 1. Minimul a trei numere


Se citesc de la tastatură trei numere a, b, c şi se cere determinarea şi afişarea minimului lor
min(a,b,c).
Algoritmul care rezolvă această problemă se bazează pe relaţia matematică
min(a, b, c) = min(a, min (b, c))
iar implementarea în limbaj pseudocod arată astfel:

1
Structuri de date
1 Program Exemplul_1;
2 Begin IMPORTANT
3 Read(a,b,c);
4 min:=a;  nu orice program este şi algoritm;
5 If(min < b) Then  de obicei, un program nu respectă neapărat
6 min:=b;
7 If(min < c) Then
proprietatea de finitudine.
8 min:=c;
9 Write(min);
10 End.

Se observă că algoritmul descris mai sus are cele cinci proprietăţi enumerate la definiţia algoritmului.
Putem descrie algoritmul prezentat mai sus şi folosind o schemă. Pe baza acestor descrieri putem face
câteva observaţii:
• schema logică este una liniară, fără bucle (cicluri) deci nu există structuri repetitive;
• este folosită o variabilă suplimentară min care stochează valoarea minimă;
• dacă secvenţa de calcul al minimului a trei numere ar fi returnat valoarea corectă numai (de
exemplu) când unul din ele era zero, atunci nu era un algoritm deoarece nu era respectată
condiţia vi) din definiţie;
• funcţiile de operaţii şi de locaţii ale algoritmului propus se pot calcula elementar şi au valorile:
3  TExemplul_1(a,b,c)  5 (funcţia de operaţii)
LExemplul_1(a,b,c) = 1 (funcţia de locaţii) deoarece sunt folosite 4 variabile de
memorie şi anume a, b, c şi min.
START
IMPORTANT
CITEŞTE
O schemă logică a,b,c
 începe cu START şi se
termină cu STOP; min := a
 dacă nu are cicluri se spune că
este liniară.

F
min < b min := b

F
min < c min := c

SCRIE
min

STOP

Figura 1. Schema logică pentru calculul minimului a trei numere

2
Structuri de date
EXEMPLUL 2. CMMDC a două numere
Se citesc de la tastatură două numere naturale a şi b şi se cere calculul şi afişarea valorii
CMMDC(a, b).
Algoritmul cel mai cunoscut pentru determinarea CMMDC a două numere este cel dat încă din
antichitate de către matematicianul grec Euclid şi foloseşte proprietatea că dacă a>b atunci
CMMDC(a,b)=CMMDC(b,a)=CMMDC(a-b, b). În termenii limbajului pseudocod, el poate fi
descris astfel:
1 Program CMMDC_a_b;
2 Begin IMPORTANT
3 Read(a);
La algoritmul lui Euclid
4 Read(b);
 se presupune că numerele citite a
5 While(a<>b) Do
şi b au proprietatea că a  0
6 If(a<b) Then
şi b  0
7 b:= b - a
8 Else
9 a:=a - b
10 Write(a);
11 End.

Schema logică pentru acest algoritm este prezentată în Figura 2. Putem să observăm că această schemă
conţine cicluri (bucla While) iar decrementarea lui a (respectiv a lui b) se face într-un număr finit de paşi
(până când a devine egal cu b). În fiecare ciclu se decrementează sau a sau b cu condiţia ca ele să rămână
pozitive şi diferite între ele.
START

CITEŞTE
a, b

F
a<>b

T SCRIE
a

F T
a < b STOP

a := a - b b := b - a

Figura 2. Schema logică pentru calculul CMMDC(a, b)

Există un alt algoritm mai eficient de calcul a CMMDC(a, b). Acest algoritm a fost descoperit de J.
Stein. Algoritmul foloseşte următoarele proprietăţi:
• Dacă a şi b sunt ambele pare, atunci CMMDC(a, b)=2 CMMDC(a/2, b/2);
• Dacă a este par şi b este impar atunci CMMDC(a, b) = CMMDC(a/2, b);
• Altfel, a şi b sunt impare şi atunci CMMDC(a, b)= CMMDC(|a-b|/2, b).

3
Structuri de date
EXEMPLUL 3. Automatul de COCA COLA (după Horowitz & Sahni).
Un exemplu care reflectă diferenţa dintre un program şi un algoritm este schema de funcţionare a
automatului de COCA COLA (Figura 3). Vom face observaţia că aceasta nu descrie un algoritm
deoarece nu satisface proprietatea de finitudine
START

Există F
sticle în
automat?

Caută monede

F Cineva F
Există bani cunoscut cu
potriviţi? bani potriviţi
?
T T

Introdu moneda Împrumută bani


potriviţi

F Moneda
Apasă buton acceptată ?
restituire
monedă
T

Apasă buton şi
aşteaptă sticla

A apărut F
sticla?

Bea COCA COLA Renunţă

STOP
.
Figura 3. Schema de funcţionare a automatului de COCA COLA

2. Clase de algoritmi; algoritmi iterativi şi recursivi


Algoritmii pot fi clasificaţi după multe criterii legate de tipul schemei logice, de disponibilitatea
datelor sau dacă au sau nu apeluri explicite sau ascunse ale aceluiaşi fragment de cod chiar în
interiorul său. Vom enumera câteva dintre tipurile de algoritmi.
➢ Algoritm determinist este un algoritm al cărui comportament poate fi complet precizat în
funcţie de datele de intrare. Aceasta înseamnă că de fiecare dată când un set de date e folosit ca
intrare, algoritmul va furniza aceleaşi rezultate la ieşire. Pentru algoritmii ce folosesc stări sau

4
Structuri de date
menţin informaţii persistente între rulări consecutive, se va considera intrare (“input”) orice dată
introdusă încă de la pornirea algoritmului în starea iniţială.
Majoritatea algoritmilor uzuali sunt algoritmi determinişti. Un exemplu de algoritm nedeterminist
este cel care foloseşte în interiorul său valoarea momentului actual (dată+oră+minut+secundă) în
criteriile de alegere a unor valori. Astfel de algoritmi nedeterminişti sunt de regulă folosiţi în
generarea unor secvenţe de numere aleatoare.
➢ Algoritm on-line este un algoritm care trebuie să proceseze fiecare dată de intrare imediat ce
aceasta este introdusă, fără a avea detalii suplimentare despre ce date vor mai fi introduse.
Un astfel de algoritm este cel care citeşte de la tastatură o secvenţă de caractere ce se termină cu
tasta Enter şi returnează şirul citit. Algoritmul nu ştie exact când se va termina (când se va apăsa
tasta Enter) iar după fiecare apăsare a unui caracter acesta este automat inclus în bufferul de ieşire.
➢ Dacă algoritmul necesită citirea în avans a tuturor datelor de intrare şi numai după aceea le
procesează atunci el este de tipul off-line. De exemplu, algoritmul care sortează intern crescător n
numere, prima dată citeşte cele n numere şi numai după ce le are stocate în memorie poate începe
sortarea lor.
➢ O clasă specială de algoritmi sunt algoritmii aleatori care în interiorul lor, la un anumit pas, fac
unele alegeri aleatoare. Aceste alegeri se fac de obicei pentru a evita situaţia în care cazurile cele
mai defavorabile apar frecvent în seturile de date de intrare. De exemplu, există variante ale
algoritmului de sortare rapidă QuickSort care fac alegerea elementului pivot pe baze total aleatoare.
➢ Dacă un algoritm nu conţine apeluri recursive directe sau indirecte atunci el se numeşte iterativ.
Algoritmii iterativi conţin numai instrucţiuni de atribuire, adunare/scădere, înmulţire/împărţire,
comparaţii (eventual structurate în cicluri) şi apeluri nerecursive la alte subrutine. Dacă schema
logică a algoritmului nu conţine cicluri atunci algoritmul se numeşte liniar.
➢ Dacă în schimb algoritmul conţine apeluri recursive directe sau indirecte (adică se apelează pe
el însuşi sau apelează o altă subrutină care la rândul ei apelează algoritmul în cauză) atunci el se
numeşte recursiv. Algoritmii recursivi au câteva caracteristici şi anume:
1. Expresiile funcţiilor de locaţii şi de operaţii sunt de obicei scrise sub forma unor recurenţe
foarte asemănătoare cu funcţia recursivă implementată de algoritm ceea ce duce la expresii
de multe ori exponenţiale ale timpilor de execuţie comparativ cu situaţiile când acelaşi
algoritm poate fi scris nerecursiv însă timpul de execuţie nu este exponenţial;
2. Au avantajul că pot fi foarte uşor înţeleşi, expresia lor semănând cu cea a funcţiei ce o
implementează;
3. Au dezavantajul că fiecare apel recursiv înseamnă punerea pe stivă a argumentelor
(parametrii actuali, dacă există), şi a adresei de revenire în programul apelant (întotdeauna),

5
Structuri de date
deci se încarcă stiva în mod excesiv (câştigăm în lizibilitate dar pierdem în viteză de
execuţie şi memorie folosită).

3. Datele în algoritmi
Există câţiva termeni cu care se operează frecvent în proiectarea unui algoritm, termeni pe care îi
vom defini în rândurile următoare. Printre aceştia, amintim:
• structura de dată;
• obiectul de dată;
• tipul de dată;
• reprezentarea datei.

TIPUL DE DATĂ Este un termen ce se referă la "felurile de date" pe care o variabilă le poate avea
într-un program şi este strâns legat de implementarea într-un limbaj de programare a unei structuri
de dată. De exemplu, în limbajul Pascal, există câteva tipuri elementare de dată (real, integer, char,
file) dar şi tipuri de date compuse. În limbajul LISP, structura fundamentală este lista. O altă
posibilitate este aceea de a pune împreună tipuri de date diverse şi a le împacheta pentru a obţine un
tip nou, compus, numit structură în C şi Java (record în Pascal).

OBIECTUL DE DATĂ Prin acest termen se referă o instanţiere a unei variabile cu o valoare, deci
obiectul de dată este caracterizat prin conţinutul unei variabile şi poate avea valori dintr-o mulţime
oarecare D. De obicei, obiectul de dată ne duce cu gândul la conţinutul memoriei calculatorului.

EXEMPLU Obiectul de dată numere întregi D = {0, 1, 2, ...};


Obiectul de dată şir de caractere D = {"", "A","B",..,"AB",...};

STRUCTURA DE DATĂ Este un cvadruplet SD = {D, d, F, A} în care


D = o mulţime de domenii
d  D este un domeniu (impropriu notat la fel ca şi structura de date)
F = o mulţime de funcţii
A = o mulţime de axiome

EXEMPLU Structura de date N, mulţimea numerelor naturale SDNatno={D, d, F, A} cu:


d = NATNO
D={NATNO, BOOLEAN}
F conţine următoarele funcţii:
• ZERO() → NATNO

6
Structuri de date
ISZERO(NATNO) → BOOLEAN

SUCC(NATNO) → NATNO

ADD(NATNO, NATNO) → NATNO

EQ(NATNO, NATNO) → BOOLEAN

A conţine următoarele axiome:
() x, y  NATNO 
• ISZERO(ZERO()) : = TRUE
• ISZERO(SUCC(x)) : = FALSE
• ADD(ZERO(), x) : = x
• ADD(SUCC(x), y) : = SUCC(ADD(x, y))
• EQ(ZERO(), x) : = If ISZERO(x) Then TRUE
Else FALSE
• EQ(ZERO(), SUCC(x)) : = FALSE
• EQ(SUCC(x), SUCC(y)): = EQ(x, y)

IMPLEMENTAREA UNEI STRUCTURI DE DATĂ. Acest termen se referă la o funcţie


 : SD1 → SD2
unde SD1 şi SD2 sunt structuri de dată. De exemplu, implementarea structurii de dată număr întreg
pe 2 octeţi în limbajul Pascal este sub forma tipului de dată word, în care (ZERO()) = 0,
(SUCC(ZERO())) = 1, etc.

4. Cum scriem un algoritm


De regulă, un programator mediu scrie între 10 şi 50 linii de cod pe zi. Un programator bun însă,
scrie mult mai mult şi are nişte reguli în activitatea sa. Există câteva modele ale procesului de
proiectare software bazate pe principii solide şi care cuprind în mare toate activităţile ce au loc de-a
lungul unui astfel de proces. Vom alege un model simplu, didactic ce cuprinde cinci faze ale
proiectării unui algoritm, astfel:
• stabilirea cerinţelor;
• proiectarea algoritmului;
• analiza;
• codificarea;
• testarea.

Vom lua pe rând fiecare din aceste etape specificând câteva trăsături:
1. Stabilirea cerinţelor; trebuie să stabilim riguros cerinţele algoritmului şi care sunt datele de
intrare şi de ieşire.
2. Proiectarea algoritmului; este făcută independent de limbajul de programare; în această fază
trebuie identificate conceptele cu care se operează, proprietăţile (atributele) şi serviciile pe care
acestea trebuie să le ofere şi apoi toate acestea vor fi materializate în algoritm sub formă de

7
Structuri de date
obiecte (ex. un polinom, o matrice, un labirint); pentru fiecare obiect trebuie identificate
operaţiile efectuate asupra lui şi scrise procedurile (funcţiile) necesare. Pentru algoritmii
distribuiţi trebuie luat în calcul localizarea obiectelor, sincronizarea, mecanismele de acces şi
protecţie la date.
3. Analiza; de obicei porneşte de la întrebarea "Există şi alţi algoritmi?"; dacă răspunsul la
întrebare este afirmativ (şi de cele mai multe ori este), atunci se vor compara algoritmii între ei
în ceea ce priveşte funcţiile de operaţii, de locaţii, uşurinţa în înţelegere, etc; există un set de
metrici bine stabilite care măsoară diferiţi indicatori ai unui algoritm, metrici numite metrici
software.
4. Codificarea; presupune scrierea algoritmului într-un limbaj de programare, folosind facilităţile
acelui limbaj.
5. Verificarea; de obicei, această etapă are la rândul ei trei subfaze şi anume:
• demonstrarea corectitudinii (de cele mai multe ori se face matematic);
• testarea (rularea programului cu date de intrare diferite);
• depanarea (identificarea erorilor de proiectare şi îndepărtarea lor).

EXEMPLUL 4. Sortarea a n numere


Să se scrie o subrutină care sortează crescător un vector de n numere. O soluţie ar fi aceea care, din
întregii rămaşi nesortaţi, la fiecare pas, găseşte cel mai mic şi îl pune în următoarea poziţie a listei
sortate conform următoarei scheme:
INPUT: n, (A[i]) i=1..n
OUTPUT: (A[i]) i=1..n
For i 1 To n Do
Begin
examinează vectorul A[i] .. A[n]
şi găseşte cel mai mic, A[j]
interschimba A[j] cu A[i]
End

În limbaj pseudocod putem scrie ideea algoritmului schiţat anterior pentru această problemă după
cum urmează:

1 Procedure Sort(A,n)
2 Begin
3 For i 1 To n Do
4 Begin
5 j  i
6 For k  j+1 To n Do
7 If (A[k] < A[j]) Then
8 j  k
9 End
10 t  A[i]
11 A[i]  A[j]
12 A[j] t

8
Structuri de date
13 End
14 End

Având în faţă acest fragment în pseudocod, este simplu să îl transpunem în limbaj C (vom
presupune existenţa unui tip predefinit T al elementelor de intrare):

1 typedef T Vector[MAX_ELEM];
2
3 void Sort(Vector A, int n)
4 {
5 int nIndex1, nIndex2, nMinPos;
6 T tAux;
7 for(nIndex1=0; nIndex1<n; nIndex1++)
8 {
9 nMinPos = nIndex1;
10 for(nIndex2= nMinPos; nIndex2<n; nIndex2++)
11 if(A[nIndex2] < A[nIndex1])
12 nMinPos = nIndex2;
13 tAux = A[nIndex1];
14 A[nIndex1] = A[nMinPos];
15 A[nMinPos] = tAux;
16 }
17 }

Întrebările la care vom încerca să răspundem în legătură cu acest algoritm sunt:


• Este corect ?
• Care este eficienţa algoritmului ?

A) CORECTITUDINE
Pentru a demonstra corectitudinea algoritmului de sortare, vom demonstra următoarea
PROPOZIŢIE Procedura SORT(A, n) sortează corect o mulţime de n  1 numere, rezultatul
rămânând în A[1..n] astfel încât A[1]  A[2] ...  A[n].
DEMONSTRAŢIE Dacă n = 1 este evident că nu se execută nici o instrucţiune din bucla for,
deci vectorul rămâne sortat.
În final, să observăm că în liniile 9 - 12 se calculează indicele elementului maxim din şirul
A[i], A[i+1],...,A[n], deci, după liniile de interschimbare 13 - 15 are loc relaţia:
A[i] = min{A[k]| k = i,..., n }

ceea ce demonstrează corectitudinea algoritmului de sortare.


Pentru a demonstra corectitudinea unui algoritm iterativ trebuie realizate următoarele:
• Se determină o specificaţie pentru ieşirea algoritmului ca funcţie de mulţimea de intrare
(de exemplu sum(x,y)= x+y;
• Se determină şi se separă ciclurile algoritmului începând cu cele interioare (în cazul
ciclurilor imbricate);

9
Structuri de date
• Pentru fiecare ciclu se determină un invariant al ciclului (relaţie matematică permanent
adevărată ce are loc între variabilele ciclului) care rămâne adevărat la fiecare iteraţie şi se
identifică “progresul” variabilelor în ciclu;
• Se demonstrează faptul că proprietatea invariantă este verificată; de obicei aceasta se face
prin inducţie după numărul de iteraţii;
• Se foloseşte proprietatea invariantă pentru a demonstra că algoritmul se termină după un
număr finit de paşi;
• Se foloseşte proprietatea invariantă şi condiţia de terminare a ciclului pentru a demonstra
că algoritmul calculează corect rezultatul conform specificaţiei de la început.

B) EFICIENŢĂ
Întotdeauna, eficienţa se referă la relaţia cu alţi algoritmi (dacă există). Va trebui să stabilim
care este mai bun din punct de vedere al numărului de operaţii, motiv pentru care orice analiză
de eficienţă are la bază calculul numărului total de operaţii efectuate în cursul desfăşurării
algoritmului. Vom împărţi operaţiile în patru categorii, în funcţie de complexitatea lor, în :
• atribuiri;
• adunări / scăderi;
• înmulţiri / împărţiri;
• comparaţii.
Pentru a analiza numărul de operaţii va trebui să reprezentăm algoritmul sub formă de schemă
logică ca în Figura 4.

Acum avem toate elementele necesare pentru construirea tabelului cu frecvenţa operaţiilor
elementare.

Atribuiri Adunări Comparaţii


n + 1 (ptr i) n (ptr i) n + 1 (ptr i)
n (ptr j) n - i (ptr k) n - i (ptr k)
n – i (ptr k) n - i - 1 (ptr a[k])
B (ptr j) cu B  n-i-1
3 (ptr i)

iar prin adunarea acestora obţinem expresia funcţiei de operaţii TSort(n) dată de:

n n n
5n2 + 5n + 4
TSort (n) 4n + 2 + 2  i=1
(n − i − 1)+ 3 
i=1
(n − i)+ 
i=1
3=
2

şi
n n n
4n2 + 6n + 8
TSort (n) 4n + 2 + 
i=1
(n − i − 1)+ 3 
i=1
(n − i)+ 
i=1
3=
2

10
Structuri de date
Funcţia de locaţii (cantitatea de memorie folosită pentru variabilele locale algoritmului) va fi
LSort (n)= 4
adică este constantă, deoarece avem doar 4 variabile suplimentare (i, j, k, t).
PROCEDURE SORT(A[1:n])

i := 1

F
i <= n RETURN

T
i := i+1
j := i
A[j]:= t
k := j+1

A[i]:=A[j] k := k + 1

F
t := A[i] k <= n

F
A[k]<=A[j]

j := k

Figura 4. Schema logică pentru procedura SORT

Pentru a caracteriza algoritmii, trebuie să caracterizăm funcţia de operaţii T specifică fiecărui


algoritm. De obicei, dacă mulţimea de intrare are dimensiunea n, atunci funcţia de operaţii se
notează cu T(n), iar dacă alţi parametrii variabili intră în calcul, şi aceştia se pun între paranteze
lângă n.

11
Structuri de date

CURS 2
Relaţii de recurenţă

Aşa cum vom vedea la analiza exemplelor, în cazul algoritmilor recursivi expresiile
funcţiilor de operaţii sunt descrise folosind diverse tipuri de recurenţe. Vom arăta în continuare cum
se rezolvă câteva clase de recurenţe pentru a putea estima corect ordinul de operaţii.

1. Recurenţe liniare de ordinul 1


a) Considerăm relaţia de recurenţă
T(n)=aT(n-1)+b, cu a, b  R, n  N, T(0) dat.
Dacă a=1, atunci putem scrie că
T(n)=T(n-1)+b=…=T(n-k)+k*b, () 1  k  n
şi pentru k=n obţinem formula termenului general
T(n)=T(0)+n*b  (n).
Dacă a1 atunci vom căuta constanta reală s  R astfel încât să existe identitatea
T(n)+s=a(T(n-1)+s), () n  N 
 T(n)=aT(n-1)+s*(a-1) s*(a-1)=b
ceea ce conduce la valoarea
b
s= , a 1
a −1

Atunci vom avea că


T(n)+s=ak(T(n-k)+s), () 1  k  n
De unde rezultă pentru k=n că
b  b 
T(n)=-s+ an(T(0)+s)= - + an  T(0)+   (an)
a −1  a −1

b) Fie acum cazul general


T(n)=aT(n-1)+P(n),
cu T(0) dat, a  R, P  R[X] un polinom cu coeficienţi reali de grad m şi nN. Dacă a=1,
atunci putem scrie că
n
T(n)=T(n-1)+P(n)=…=T(n-k)+  P(i),
i= n-k +1
() 1  k  n

şi pentru k=n obţinem formula termenului general


n
T(n)=T(0)+ P(i) 
i=1
(nm).

1
Structuri de date
Dacă a1 atunci vom căuta polinomul QR[X] cu coeficienţi reali de grad m=deg(P) astfel
încât să existe identitatea
T(n)+Q(n)=a(T(n-1)+Q(n-1)),()nN 
 T(n)=aT(n-1)+a*Q(n-1)-Q(n)  a*Q(n-1)-Q(n) P(n)
şi cei m+1 coeficienţi ai lui Q se pot determina din sistemul liniar de m+1 ecuaţii ce rezultă din
identitatea cu polinomul nul (zero)
a*Q(n-1)-Q(n)- P(n) 0

2 Recurenţe liniare de ordinul 2


a) Considerăm relaţia de recurenţă
T(n)=aT(n-1)+bT(n-2), cu a, b  R*, n  N, T(0), T(1) daţi.
Acestei relaţii de recurenţă i se asociază o ecuaţie (numită “ecuaţie caracteristică”) de gradul 2:
y2=a*y+b
cu soluţiile y1,2. Vom distinge două cazuri în funcţie de natura rădăcinilor y1,2.
Dacă y1  y2 atunci ecuaţia are două rădăcini distincte iar expresia termenului general al lui T
va fi dată de relaţia
T(n)=A* y1n +B* y2n  O(max(| y1n |, | y2n |))
cu A şi B identificaţi din sistemul ce rezultă pentru T(0) şi T(1):
T(0)= A + B



T(1)= Ay1 + By2

Dacă y1 = y2 atunci ecuaţia are o rădăcină dublă iar expresia termenului general al lui T va fi
dată de relaţia
T(n)=(A+nB)* y1n
cu A şi B identificaţi din sistemul ce rezultă pentru T(0) şi T(1):
T(0)= A


T(1)= Ay1 + By1

b) Fie acum cazul general


T(n)=aT(n-1)+bT(n-2)+P(n),
cu T(0), T(1) daţi, a,b  R, P  R[X] un polinom cu coeficienţi reali de grad m şi nN.
Pentru rezolvarea recurenţei vom căuta polinomul Q  R[X] cu coeficienţi reali de grad
m=deg(P) astfel încât să existe identitatea
T(n)+Q(n)=a(T(n-1)+Q(n-1))+b(T(n-2)+Q(n-2)),()nN

2
Structuri de date
ceea ce înseamnă că dacă notăm E(n)=T(n)+Q(n) obţinem pentru E o recurenţă liniară de
ordinul 2 în n:
E(n)=aE(n-1)+bE(n-2), E(0)=T(0)+Q(0), E(1)=T(1)+Q(1)
pe care ştim să o rezolvăm deja.

3. Recurenţe logaritmice
a) O clasă aparte de recurenţe sunt cele de forma
 
T(n)=a* T  n   +b
 k 

cu T(1) dat, a,b  R, k  N, k>1 şi n=kp. Pentru astfel de recurenţe se poate observa că
are loc relaţia
i
 
T(n)=ai* T  ni   +b*
 k  
a
j=0
j

ceea ce pentru i=logk(n) devine


log k(n)
T(n)=nlogk(a)*T(1)+b* a
j=0
j

Dacă a1 atunci formula termenului general a lui T va fi


log k(a)
−1
T(n)=nlogk(a)*T(1) +b* n
a −1

iar dacă a=1 vom avea că


T(n)=T(1)+b*logk(n)
b) Pentru cazul general al unei recurenţe de forma
 
T(n)=a* T  n   +P(n)
 k 

cu T(1) dat, aR, kN, k>1, PR[X] un polinom de grad m, se va căuta o funcţie Q
pentru care există relaţia
    
T(n)+Q(n)=ai*  T  ni   + Q  ni    , a  1
  k    k  

ceea ce pentru i=logk(n) devine


T(n)=nlogk(a)*(T(1) +Q(1))-Q(n).
Dacă a=1 atunci recurenţa devine
 
T(n)= T  n   +P(n)
 k 

de unde obţinem că
log k (n)-1
 n 
T(n)=T(1)+ 
i=0
P   
 ki 
 

Dacă punem k=2, a=1, P(n)=c, obţinem expresia termenului general

3
Structuri de date
T(n)=T(1)+c*log2(n)  ( log2(n)).
Dacă punem k=2, a=1, P(n)=c*n+b, obţinem expresia termenului general
T(n)=T(1)+2*c*(n-1)+b*log2(n)  (n).
Dacă punem k=2, a=2, P(n)=c*n+b, luăm funcţia Q(n)=-c*n*log2(n)+b şi obţinem
expresia termenului general
T(n)=n(T(1)+b)+c*n*log2(n)-b  (n*log2(n)).

4. Recurenţe speciale
Vom exemplifica aici cum se pot rezolva prin substituţii două recurenţe care nu se încadrează în
clasele prezentate anterior şi în care coeficienţii sunt la rândul lor funcţii de n.
a) Să se rezolve recurenţa
T(n)=(n-1)T(n-1)+nT(n-2), n  N, T(0), T(1) daţi.
Să observăm că relaţia de recurenţă se poate scrie:
T(n)+T(n-1)=n(T(n-1)+T(n-2))=
=n*(n-1)*(T(n-2)+T(n-3))=…=n!*(T(1)+T(0))=n!*c
unde c=T(1)+T(0).
Deci rezultă că
T(n)+T(n-1)=n!*c
adică
T(n)=n!*c- T(n-1)=n!*c-(n-1)!*c+T(n-2)=…=
n
=c* (−1)
i=k
n −i
i! + (−1)n−k+1 T(k-1).

Pentru k=1 obţinem


n
T(n)=c* (−1)
i=1
n −i
i! + (−1)n T(0).

b) Să se rezolve recurenţa
T(n)=nT(n-1)+n, n  N, T(0) dat.
Să observăm că dacă notăm E(n)=T(n)+1 atunci relaţia de recurenţă se poate scrie:
E(n)-1=nE(n-1)
de unde obţinem că
E(n)=1+nE(n-1)=1+n(1+(n-1)E(n-2))=1+n+n(n-1)E(n-2)=…=
n
=  i!
n! n!

i=k +1
+
k!
E(k)

de unde pentru k=0 rezultă expresia termenului general

4
Structuri de date
n
E(n)=n!*  i!1 +n!*E(0)
i=1

iar expresia lui T(n) va fi:


n
T(n)=n!*  i!1 +n!*(T(0)+1)-1.
i=1

EXEMPLUL 1. Numărul de scrieri distincte ale lui xn


Fie X(n) numărul de scrieri distincte ale parantezelor rotunde în produsul a n valori identice notate cu x.
De exemplu, X(1)=X(2)=1 deoarece pentru n=1 doar E=x respectiv doar E=x*x sunt scrieri distincte
şi nu mai e nevoie de paranteze pentru calculul lui E. În schimb, pentru n=3 avem X(3)=2 deoarece
cele două posibilităţi sunt E1= x*(x*x) şi E2=(x*x)*x. Pentru n=4 avem X(4)=5, cele 5
posibilităţi fiind E1=x*(x*(x*x)), E2=x*((x*x)*x), E3=(x*x)*(x*x),
E4=(x*(x*x))*x şi E5=((x*x)*x)*x. Cu notaţiile de mai sus se cere să se demonstreze că:
n −1
a) X(n)=  X(k)X(n-k), () n2;
k =1

b) () n1, X(n)  2n-2;


1 n−1
c) X(n)=
n
C2n −2
.

DEMONSTRAŢIE: a) Pentru n2 fie E(n) produsul celor n de x. Putem observa că modul de punere a
parantezelor poate fi gândit ca un grup de n-k de x urmat de alt grup de k de x, cu 1  k  n-1,

sub forma E(n)=xn=E(n-k)E(k)= x...x


x...x
 .
n −k k

Cum pentru E(n-k) numărul de scrieri cu paranteze este X(n-k) iar pentru E(k) numărul de
scrieri cu paranteze este X(k) vom avea că pentru E(n) numărul de scrieri distincte va fi calculat în
funcţie de câte posibilităţi de grupare a lui E(n) există, adică
n −1
X(n)=  X(k)X(n-k).
k =1

b) Evident că X(1)=1 2-1, X(2)=1 20=1, X(3)=221=2, X(4)=524-2=4,


X(5)=1423=8. Presupunem afirmaţia adevărată pentru 0<k. Conform punctului a) avem că pentru
n>6:
n −1 n −1 n −1
X(n)=  X(k)X(n-k)   2k-22n-k-2=  2n-4=(n-1) 2n-4 = n −21 2n-2  2n-2
2
k =1 k =1 k =1

deoarece n5.

5
Structuri de date
c) Pentru calculul expresiei X(n) vom folosi metoda lui Euler, cunoscută şi sub numele de „metoda

funcţiei generatoare”. Fie f:D R→R o funcţie formală cu expresia f(t)= 
k =0
X(k)*tk,

 k k
X(0)=0. Observăm că f2(t)=  
k =2 i= 0
X(i)X(k-i)*tk. Dar 
i= 0
X(i)X(k-i)=X(k)=

k-1


i =1
X(i)X(k-i) şi atunci f2(t)=f(t)-t. Rezolvând ecuaţia de gradul 2 în f(t) obţinem că

f(t)1,2= 1  1 − 4t
dar cum f(0)=0 rezultă că numai f(t)= 1 - 1 − 4t
este singura soluţie
2 2

acceptabilă. Vom scrie expresia lui f(t) folosind formula generalizată a binomului lui Newton astfel:
 1   

1  1 k k
f(t)=  1 − (1 − 4t) 2
=  1 − ( −1)k
(C
4t) 1=
2  2 
   k=0 2

11  1 
  − 1... − k + 1  
= ( −1)
k=1
k+1 2k k 2  2
2 t 
k!
 2 
= tk
(2k − 2)!
k(k- 1)!(k - 1)!
k =1
k 1
= t k 
k =1
Ck2k−1−2 .

Din această expresie dacă o comparăm cu definiţia lui f(t) rezultă că


1
X(k)=
k
Ck2k−1−2 , () k  1.
EXEMPLUL 2. Divizorii numerelor naturale
Fie n un număr natural pozitiv. Să se determine dacă:
a) este prim;
b) este perfect (adică este egal cu suma divizorilor lui strict mai mici decât el).

SOLUŢIE: Mai întâi să notăm faptul că numerele 1 şi 2 sunt prime şi nu sunt perfecte. Cu aceste
observaţii, vom scrie câte o funcţie care verifică dacă numărul respectiv verifică proprietăţile cerute.

1 public class Exemplul_11{


2 public static boolean isPrim(int n)
3 // verifică dacă numărul pozitiv n este prim
4 {
5 boolean isAPrimNumber=true;
6 if(n<=2){
7 return true;
8 }
9 for(int i=2;(i<n/2)&&(isAPrimNumber);i++){
10 if(n%i==0){
11 isAPrimNumber=false;
12 }
13 return isAPrimNumber;
14 }

6
Structuri de date
15 public static boolean isPerfect(int n)
16 // verifică dacă numărul pozitiv n este perfect
17 {
18 long int sum=1;
19 if(n<=2){
20 return false;
21 }
22 for(int i=2;i<n/2;i++){
23 if(n%i==0){
24 sum+=i;
25 }
26 return (sum==n);
27 }
28 // …
29 };

Atât funcţia isPrim cât şi isPerfect parcurg numerele naturale de la 2 la n/2 deci ordinul de
operaţii este O(n).

7
Structuri de date

CURS 3

▪ Pentru a putea lucra cu colecţii de obiecte de acelaşi fel se defineşte


clasa Array;
▪ Clasa SortArray se foloseşte pentru a menţine toate elementele din
vector sortate;
▪ Pe baza clasei SortArray se implementează clasele Permutare şi Set
(mulţime) iar apoi clasa Polinom, fiecare corespunzătoare unui anumit
tip de dată din matematică; pentru toate acestea se definesc şi
implementează cele mai uzuale metode algebrice;
▪ Analog se defineşte clasa Matrix corespunzătoare matricilor
bidimensionale şi sunt implementate câteva operaţii cu ele;
▪ Pentru o mai bună eficienţă în memorare sunt explicate două clase
speciale: BitSet (mulţimi de biţi) şi SMatrix(matrici rare);
▪ În final sunt tratate câteva considerente legate de adresarea în matrici.

Array Permutare

BitSet
foloseşte
derivă Array2D

derivă

Matrix

SortArray Set

Polinom
foloseşte

SMatrix

1 STRUCTURA DE DATĂ VECTOR UNIDIMENSIONAL (ARRAY)

Matricea unidimensională (vectorul) este cea mai simplă structură de dată după tipurile elementare de
date. Ea poate fi definită ca o mulţime de locaţii consecutive de memorie. Această definiţie se referă
însă la implementarea ei (reprezentarea în memorie). Trebuie făcută distincţia între această reprezentare
şi structura de dată respectivă. Intuitiv, matricea este o mulţime de perechi (indice, valoare) şi aici ne
referim la matricea unidimensională. Puţine limbaje de programare au implementat suport pentru

1
Structuri de date
matrici multidimensionale. În memorie, o matrice este stocată sub forma unor locaţii consecutive,
adresabile printr-o valoare întreagă numită index sau indice.

A → A[1] A[2] A[3] ...... A[n]

În unele limbaje de programare, indicii iau valori constrânse. De exemplu în Pascal indicii pot varia
între orice limite numere întregi (depinde de capacitatea de memorare a calculatorului), însă în C, limita
inferioară a unei matrici este întotdeauna 0 (indicele primului element). Descriem mai jos modalităţile
de declarare şi adresare ale unei matrici uni şi bidimensionale în Pascal şi C;

Pascal C
{declarare} /* declarare */
Var vect : Array[l_inf .. l_sup] Of T vect [nr_elem];
T ; T mat[nr_elem1][nr_elem2];
Mat : Array[ l_inf1 .. l_sup1, .........
l_inf2 ..l_sup2] Of T; /* adresare */
........... var1 = vect[k];
{atribuire} var2 = mat[i][j];
var1:=vect[k]; .........
var2:=mat[i,j];
...........

Trebuie să observăm diferenţa dintre matrice şi tipul record (structură):


 tipul record conţine un număr prestabilit de câmpuri cu tipuri posibil diferite (structură eterogenă),
al căror tip, nume şi număr sunt cunoscute în momentul compilării;
 tipul array conţine un număr maxim prestabilit de elemente de acelaşi tip (structură omogenă).

Pentru a putea gestiona şiruri de elemente de un tip oarecare T memorate consecutiv vom introduce
structura de dată vector unidimensional. Ea poate fi declarată

SDVector<T>=(D, d, F, A),

cu

D={NATNO, BOOLEAN, T
i =0
i
}

d= T
i =0
i

F={=, [ ], getBase, getData, getLength, setBase, setLength, visit}

Pentru a implementa un tip de dată Array corespunzător structurii de vector unidimensional vom folosi
o clasă C++ capabilă să gestioneze matrici de orice tipuri de elemente (deci nu numai de întregi).
Pentru aceasta este nevoie de o facilitate a limbajului C++ şi anume folosirea claselor template (engl.
Templates=modele). Iată cum arată prototipul (interfaţa) clasei Array:

2
Structuri de date
1 template <class T>
2 class Array{
3 protected:
4 T* data;
5 unsigned int base;
6 unsigned int length;
7 public:
8 Array();
9 Array(unsigned int newLength, unsigned int newBase=0);
10 Array(Array const& array);
11 ~Array();
12 Array& operator=(Array const& array);
13 T const& operator[](unsigned int position) const;
14 T& operator[](unsigned int position);
15 T const* getData() const;
16 unsigned int getBase() const;
17 unsigned int getLength() const;
18 void setBase(unsigned int newBase);
19 void setLength(unsigned int newLength);
20 void visit(void (*f)(T&));
21 };

Instrucţiunea de pe linia 1 specifică faptul că declaraţia ce urmează (adică cea a clasei Array) foloseşte
un template de clasă cu numele T. Aceasta înseamnă că putem defini obiecte de tip Array ca vectori de
orice tip (ex. de întregi, şiruri de caractere) prin simpla lor declarare ca Array<int> respectiv
Array<char*>.

Liniile 8-20 declară ceea ce se numeşte „interfaţa” clasei Array, adică un set de funcţii oferite de ea.
Dacă primele 4 funcţii sunt constructori şi respectiv destructorul clasei, metodele declarate la liniile 12-
20 sunt de fapt „interfaţa” originală. Ele nu reprezintă altceva decât funcţiile din cadrul mulţimii F care
definesc vectorul unidimensional ca structură de date.
O să luăm pe rând fiecare membru şi atribut şi o să le descriem.
Liniile 4-6 specifică singurele atribute ale clasei, care sunt în secţiunea de atribute protejate (vizibile
numai pentru clasa curentă şi cele derivate din ea):
▪ data este un pointer la o zonă de memorie cu elemente de tip T;
▪ length este un întreg fără semn care specifică lungimea zonei pointate de data;
▪ base indică indicele primului element din data (de ex. pentru vectori gen Pascal base va fi 1,
pentru vectori gen C sau Java base va fi 0).

Pe linia 8 este declarat un constructor implicit fără nici un parametru, care iniţializează cu 0 toate cele
trei atribute şi a cărui definiţie (cod) este următoarea:

3
Structuri de date
1 template <class T>
2 Array<T>::Array():
3 data(new T[0]), base(0), length(0){}

Constructorul de pe linia 9 ţine cont de valorile celor doi parametri daţi ca argumente:

1 template <class T>


2 Array<T>::Array(unsigned int newLength, unsigned int newBase):
3 data(new T[newLength]), base(newBase), length(newLength){}

Linia 10 conţine declaraţia unui constructor de copiere ce are ca parametru o referinţă constantă (adică
nemodificabilă în constructor) la un obiect de tip Array şi construieşte o copie a acestuia în obiectul
curent. Înainte de a construi însă copia, testează ca nu cumva obiectul curent să fie tocmai cel sursa iar
dacă cele două diferă atunci se efectuează copierea:,

1 template <class T>


2 Array<T>::Array(Array<T> const& array)
3 // constructor de copiere
4 {
5 if(this!=&array)
6 {
7 delete []data;
8 length = array.length;
9 base = array.base;
10 data = new T[length];
11 for(unsigned int i = 0; i < length; i++)
12 data[i] = array.data[i];
13 }
14 }

Destructorul clasei Array este simplu de implementat şi el testează dacă obiectul pointat de data a fost
alocat (adică este nenul) iar în caz afirmativ dealocă zona de memorie respectivă:

1 template <class T>


2 Array<T>::~Array()
3 {
4 if (data)
5 delete []data;
6 }

Asemănător constructorului de copiere este implementată şi supraîncărcarea operatorului de atribuire


(declarat pe linia 12 în corpul clasei) şi care arată astfel:

1 template <class T>


2 Array& Array<T>::operator=(Array const& array){
3 if(this!=&array)
4 {
5 delete []data;
6 length = array.length;

4
Structuri de date
7 base = array.base;
8 data = new T[length];
9 for(unsigned int i = 0; i < length; i++)
10 data[i] = array.data[i];
11 }
12 return *this;
13 }

Dar vectorul este o secvenţă de elemente de acelaşi tip memorate în locaţii consecutive de memorie,
motiv pentru care cea mai importantă proprietate a lui este că poate accesa direct orice element
cunoscându-i poziţia (indexul) în vector. Aceasta este realizată în clasa Array de cele două fucţii care
supraîncarcă operatorul de indexare []. De ce avem nevoie de două funcţii? Pentru că prima (linia 14
din declaraţia clasei) este folosită atunci când valoarea indexată este o valoare dreaptă într-o expresie (r-
value) şi deci nu se modifică valoarea ei iar cea de-a doua (linia 15 din declaraţia clasei) este folosită
când valoarea indexată apare în stânga unei expresii (este o l-value) şi poate fi modificată. Codul
corespunzător supraîncărcării celor doi operatori este următorul:
1 template <class T> //supraincarcare r-value
2 T const& Array<T>::operator[](unsigned int position) const
3 {
4 unsigned int const offset=position-base;
5 if(offset>length)
6 cout<< "invalid position";
7 return data[offset];
8 }
9 template <class T> //supraincarcare l-value
10 T& Array<T>::operator[](unsigned int position)
11 {
12 unsigned int const offset=position-base;
13 if(offset>length)
14 cout<<"invalid position";
15 return data[offset];
16 }
Pe liniile 15-17 din declaraţia clasei sunt declarate trei funcţii care returnează valori nemodificabile
(constante) corespunzătoare celor trei date private ale clasei. Se observă că numele metodelor încep cu
get şi sunt urmate de numele datei la care se face acces. Astfel de metode poartă numele de accesori.
Definiţiile lor sunt arătate mai jos:
1 template <class T>
2 T const* Array<T>::getData() const
3 {
4 return data;
5 }
6
7 template <class T>
8 unsigned int Array<T>::getBase() const
9 {
10 return base;

5
Structuri de date
11 }
13 template <class T>
14 unsigned int Array<T>::getLength() const
15 {
16 return length;
17 }

Pe linia 20 din declaraţia clasei apare o metodă mai puţin obişnuită şi anume o funcţie care parcurge
toate elementele efectuând o anumită operaţie cu/pe ele. Metoda în cauză se numeşte iterator şi are ca
parametru o un pointer la o funcţie care are ca parametru o referinţă la un obiect de tip T şi returnează
void. Codul iteratorului este următorul:

1 template <class T>


2 void Array<T>::visit(void (*f)(T&))
3 {
4 for(unsigned int i=0;i<length;i++)
5 f(data[i]);
6 }

În fine, după ce am văzut cum funcţionează membrii accesor, pe liniile 18-19 din declaraţia clasei puteţi
observa alte doi membrii speciali care încep cu set şi care modifică valoarea datelor protejate length şi
base. Aceste metode se numesc modificatori deoarece numai prin intermediul lor putem modifica “din
afară” valorile membrilor privaţi sau protejaţi ai unei clase. Ca regulă de proiectare a unei structuri de
date cu modificatori, numele acestora va începe cu prefixul set urmat de numele membrului modificat.
Codul celor doi modificatori este următorul:
1 template <class T>
2 void Array<T>::setLength(unsigned int newLength)
3 {
4 length=newLength;
5 }
6
7 template <class T>
8 void Array<T>::setBase(unsigned int newBase)
9 {
10 base=newBase;
11 }

Pe baza celor declarate în clasa Array putem de exemplu declara şi lucra cu un obiect de tip Array ca
în fragmentul următor:

1 void f(int& k) // declararea functiei vizitator f


2 {
3 cout <<k<<endl; // afiseaza valoarea pe ecran
4 }
5 void main() //functia principala
6 {
7 Array<int> a(10); // declararea unui vector de int

6
Structuri de date
8 for(int i=0;i<10;i++)
9 a[i]=2*i; //initializarea valorilor
10 for(i=0;i<a.getLength();i++)
11 cout<<a[i]<<endl; //afisarea lor
12 cout<<endl;
13 a.visit(f); // apel de iterator
14 }

2 VECTORUL UNIDIMENSIONAL SORTAT


După cum am văzut în clasa Array, structura de date vector unidimensional are ca interfaţă (set de
funcţii) cele de acces la membri protejaţi (getBase, getLength şi getData), cele de acces la un element pe
baza indexului (operatorul [ ]) şi modificatorii setBase şi setLength. Aşa cum am văzut la clasa Array,
vectorul unidimensional memora consecutiv elemente de tip T. Dacă în plus faţă de proprietăţile de la
Array punem condiţia ca elementele să fie memorate sortat atunci vom defini o nouă structură de date
şi anume vectorul unidimensional sortat iar clasa ce implementează funcţiile (metodele) ei o vom numi
SortArray şi este dată în cele ce urmează:

1 template <class T>


2 class SortArray:public Array<T>
3 {
4 public:
5 int findValue(T const& val);
6 SortArray& addValue(T const& val);
7 SortArray& removeValue(T const& val);
8 SortArray& merge(SortArray<T> const& sa);
9 SortArray & operator+(T const& val);
10 SortArray & operator+(SortArray const& sa);
11 SortArray & operator-(T const& val);
12 SortArray & operator-(SortArray const& sa);
13 };

Prima metodă findValue caută binar în vectorul sortat după valoarea dată val şi returnează poziţia
acestuia (dacă l-a găsit) sau base-1 (adică un indice în afara domeniului valid) în caz contrar:
1 template <class T>
2 int SortArray<T>::findValue(T const& val)
3 { // cautare binara in vector sortat
4 int left=0, right=length-1, mid;
5 while(left<=right)
6 {
7 mid=(left+right)>2;
8 if(data[mid]<val)
9 left=mid+1;
10 else
11 if(data[mid]>val)
12 right=mid-1;
13 else //gasit data[mid]==val

7
Structuri de date
14 return mid;
15 }
16 return base-1; // nu s-a gasit valoarea
17 }

Membrul addValue inserează o valoare nouă exact pe poziţia corespunzătoare ei (vezi Figura 1) aşa
încât păstrează sortată mulţimea de elemente de tip T din Array.Va trebui să remarcăm aici faptul că se
construieşte o nouă zonă de memorie în care sunt copiate toate valorile noi, deci funcţia de operaţii
pentru addValue va fi TaddValue(n)=O(n).

3 4 7 12 20 21 3 4 7 8 12 20 21

Figura 1 Inserarea unei valori în vectorul sortat

1 template <class T>


2 SortArray<T>& SortArray<T>::addValue(T const& val)
3 {
4 T* newData= new T[length+1];
5 for(unsigned int i=0;(i<length)&&(data[i]<val);i++)
6 newData[i]=data[i];
7 newData[i++]=val;
8 for(;i<length+1;i++)
9 newData[i]=data[i-1];
10 length++;
11 delete []data;
12 data=newData;
13 return *this;
14 }

Complementară metodei de inserare valoare nouă este removeValue, cea care şterge valoare din
vectorul sortat (ca în Figura 2). După cum se poate observa din cod procedeul este asemănător cu cel
de la addValue (se realizează o copie în memorie cu noile valori) deci eforul de calcul va fi asemănător
TremoveValue(n)=O(n).

3 4 7 8 12 20 21 3 4 7 12 20 21

Figura 2 Ştergerea unei valori din vectorul sortat


1 template <class T>
2 SortArray<T>& SortArray<T>::removeValue(T const& val)
3 {
4 int foundPosition=findValue(val);
5 if(foundPosition>=base) // s-a gasit in vector

8
Structuri de date
6 {
7 T* newData= new T[length-1];
8 for(unsigned int i=0;i<foundPosition;i++)
9 newData[i]=data[i];
10 for(;i<length-1;i++)
11 newData[i]=data[i+1];
12 length--;
13 delete []data;
14 data=newData;
15 }
16 return *this;
17 }

Pentru a interclasa (reuni) valorile celor doi vectori (cu menţiunea că se iau în considerare şi valorile
multiple motiv pentru care nu este o reuniune de mulţimi) vom folosi metoda merge:

1 template <class T>


2 SortArray<T>& SortArray<T>::merge(SortArray<T> const& sa)
3 {
4 if(sa!=this)
5 {
6 T* newData= new T[length+sa.length]; //aloca spatiu nou
7 unsigned int index1=0, index2=0, index3=0;
8 while((index1<length)&&(index2<sa.length))
9 if(data[index1]<sa.data[index2])
10 newData[index3++]=data[index1++];
11 else
12 newData[index3++]=sa.data[index2++];
13 while(index1<length)
14 newData[index3++]=data[index1++];
15 while(index2<sa.length)
16 newData[index3++]=sa.data[index2++];
17 delete []data;
18 data=newData;
19 length+=sa.length;
20 }
21 return *this;
22 }

Cum limbajul C++ oferă o mare flexibilitate vom supraîncărca operatorii + şi – pentru a putea insera
respectiv şterge atât valori de tip T cât şi a aduna/scădea doi vectori sortaţi. Codul care supraîncarcă
aceştia este dat în continuare şi se bazează exclusiv pe metodele addValue şi removeValue deja
implementate.

1 template <class T>


2 SortArray<T>& SortArray<T>::operator+(T const& val)
3 { // insereaza valoare in vectorul sortat curent
4 return addValue(val);
5 }

9
Structuri de date
6
7 template <class T>
8 SortArray<T>& SortArray<T>::operator+(SortArray const& sa)
9 { // insereaza succesiv toate valorile lui sa in vectorul
10 // sortat curent
11 for(unsigned int b=0; b< sa.length; b++)
12 addValue(sa[sa.base+b]);
13 return *this;
14 }
15
16 template <class T>
17 SortArray<T>& SortArray<T>::operator-(T const& val)
18 { // elimina (daca exista) valoarea val din vectorul sortat
19 // curent
20 return removeValue(val);
21 }
22 template <class T>
23 SortArray<T>& SortArray<T>::operator-(SortArray const& sa)
24 {// elimina succesiv toate valorile lui sa in vectorul
25 // sortat curent
26 for(unsigned int b=0; b< sa.length; b++)
27 removeValue(sa[sa.base+b]);
28 return *this;
29 }

Un exemplu de utilizare a structurii SortArray tocmai implementate este descris în fragmentul de cod ce
urmează:
1 void f(int& k) // functia vizitator
2 {
3 cout <<k<<endl;
4 }
5
6 void main() //functia principala
7 {
8 SortArray<int> sa; // vector sortat de intregi, initial vid
9 for(int i=0;i<5;i++)
10 sa.addValue(i); // insereaza valori in vectorul sortat
11 for(i=0;i<sa.getLength();i++)
12 cout<<sa[i]<<endl; // afiseaza componenta vectorului
13 sa=sa + 7; // aduna valoarea 7 mentinand vectorul sortat
14 sa.addValue(9);//insereaza valoarea 9 mentinand vectorul sortat
15 for(i=0;i<sa.getLength();i++)
16 cout<<sa[i]<<endl; // afiseaza continutul vectorului
17 cout <<"Remove"<<endl;
18 sa=sa-3; //elimina valoarea 3 mentinand vectorul sortat
19 sa.removeValue(9); //analog cu valoarea 9
20 for(i=0;i<sa.getLength();i++)
21 cout<<sa[i]<<endl; // afiseaza continutul vectorului
22 sa.visit(f);
23 }

10
Structuri de date

11
Structuri de date

CURS 4
1. Permutări
Permutările sunt structuri algebrice larg folosite în algoritmi. Posibilitatea implementării unei structuri
de date care să gestioneze permutările devine astfel o necesitate. Cea mai simplă structură de date care
gestionează o permutare de n elemente este vectorul de n întregi.
De aceea vom construi în clasa Permutare structura de date necesară lucrului cu aceste tipuri de date.

1 #include “array.h” // vom folosi clasa Array


2 class Permutare
3 {
4 protected:
5 Array<unsigned int> data; //vectorul ce memorează întregii
6 unsigned int nGrad; // gradul permutării
7 public:
8 Permutare(unsigned int const);
9 Permutare(Permutare& const);
10 Permutare(Array<unsigned int> const&, unsigned int grad);
11 Permutare& setIdentic();
12 Permutare& setReverse();
13 Permutare& operator=(Permutare const&);
14 Permutare& operator*(Permutare const&); //compunere
15 unsigned int operator[](unsigned int const&);
16 Permutare& operator++(); // succesorul
17 friend ostream& operator<<(ostream& os, Permutare&);
18 Permutare& getInverse() const;
19 unsigned int isValid() const;
20 unsigned int isIdentic() const;
21 unsigned int isReverse() const;
22 void display() const;
23 };

În liniile 5-6 sunt declaraţi membrii protejaţi ai unei permutări şi anume vectorul de întregi fără semn şi
gradul permutării (numărul de elemente).
După cum se poate vedea în liniile 8-10 sunt declarate trei variante de constructor.
Primul constructor este cel implicit la care se specifică doar gradul şi care iniţial iniţializează permutarea
cu cea identică şi are ordinul de operaţii O(n):

1 Permutare::Permutare(unsigned int const nrNewGrad):


2 nGrad(nrNewGrad), data(nrNewGrad,1)
3 {
4 for(unsigned int nrIndex=1; nrIndex<=nrGrad;nrIndex++)
5 data[nrIndex]=nrIndex;
6 }

1
Structuri de date
Cel de-al doilea constructor este un constructor de copiere care verifică în linia 3 dacă nu cumva
obiectul curent este tocmai cel după care se copiază. Dacă primeşte ca parametru un alt obiect atunci
copiază datele acestuia în obiectul curent:

1 Permutare::Permutare(Permutare const& perm)


2 {
3 if(this!=&perm)
4 {
5 nrGrad=perm.nrGrad;
6 data=perm.data;
7 }
8 }

Ultima variantă de constructor primeşte ca informaţie atât gradul cât şi vectorul ce reprezintă
permutarea:

1 Permutare::Permutare(Array<unsigned int> const& array,


2 unsigned int nrNewGrad)
3 {
4 nrGrad=nrNewGrad;
5 data=array;
6 }

Odată ce am văzut cum se pot apela constructorii de permutări vom trece să vedem implementările
celorlalte metode ce ţin exclusiv de teoria permutărilor. Funcţiile setIdentic şi setReverse setează
valorile din permutarea curentă corespunzător permutării identice respectiv permutării inverse, deci
aceste funcţii modifică valoarea permutării curente.

1 Permutare& Permutare::setIdentic()
2 {
3 for(unsigned int nrIndex=1; nrIndex<=nrGrad;nrIndex++)
4 data[nrIndex]=nrIndex;
5 return *this;
6 }
7
8 Permutare& Permutare::setReverse()
9 {
10 for(unsigned int nrIndex=1; nrIndex<=nrGrad;nrIndex++)
11 data[nrIndex]=nrGrad-nrIndex+1;
12 return *this;
13 }

Următoarele trei metode sunt supraîncărcări ai operatorilor de atribuire, compunere şi indexare. La fel
ca la constructorul de copiere, operatorul de atribuire verifică dacă nu cumva este vorba de o atribuire a
valorii proprii şi numai în caz contrar realizează o atribuire de valori (folosind aşa cum se vede în linia 6,
supraîncărcarea operatorului de atribuire pentru Array):

2
Structuri de date
1 Permutare& Permutare::operator=(Permutare& const perm)
2 {
3 if(this!=&perm)
4 {
5 nrGrad=perm.nrGrad;
6 data=perm.data;
7 }
8 return *this;
9 }

Vom folosi operatorul de înmulţire * pentru a realiza compunerea a două permutări:

1 Permutare& Permutare::operator*(Permutare& const perm) //compunere


2 {
3 for(unsigned int nrIndex=1; i<=nrGrad;i++)
4 data[nrIndex]=perm[data[nrIndex]];
5 return *this;
6 }

Astfel, dacă notăm cu Sn mulţimea tuturor permutărilor de grad n şi ,Sn atunci  =* .
Operatorul de indexare supraîncărcat în continuare foloseşte deja supraîncărcarea indexării de la Array
şi testează mai întâi limitele indicelui nrIndex pasat ca parametru:

1 unsigned int Permutare::operator[](unsigned int const& nrIndex)


2 {
3 if((nrIndex>=1)&&(nrIndex<=nrGrad))
4 return data[nrIndex];
5 cout << "Eroare acces indice "<< nrIndex<< endl;
6 return 0;
7 }

Mai complicat de implementat este operatorul de incrementare ++ care generează permutarea succesor
în lanţul de permutări sortate în ordine lexicografică. Din punct de vedere teoretic, ideea are la bază
faptul că pe mulţimea Sn a tuturor permutărilor de grad n există o relaţie de ordine totală < astfel încât
între două permutări ,Sn are loc relaţia:

<  [()1kn|(i)=(i) () 1i<k şi (k)<(k)].

Pentru o permutare dată p, procedura următoare generează succesoarea ei în ordine lexicografică:


1 Permutare& Permutare::operator++()// succesorul permutarii
2 {
3 unsigned int nrIndex=nrGrad, nrAux, nrSec, nrTemp;
4 while((data[nrIndex]< data[nrIndex-1])&&(nrIndex > 1))
5 nrIndex--;
6 if(nrIndex>1) //exista succesor
7 {
8 nrAux=nrGrad;
9 while(data[nrIndex-1]>data[nrAux]) // gaseste elementul
10 //cel mai mic mai mare decat ultimul

3
Structuri de date
11 nrAux--;
12 nrTemp=data[nrIndex-1]; //interschimba elementele de pe
13 data[nrIndex-1]=data[nrAux]; // poz. nrIndex-1 si nrAux
14 data[nrAux]=nrTemp;
15 for(nrAux=nrIndex;nrAux<=nrGrad;nrAux++)//resorteaza
16 //subsecventa terminala
17 for(nrSec=nrAux+1;nrSec<=n; nrSec++)
18 if(data[nrAux]>data[nrSec])
19 {
20 nrTemp=data[nrAux];
21 data[nrAux]=data[nrSec];
22 data[nrSec]=nrTemp;
23 }
24 }
25 return *this;
26 }

De exemplu, pentru permutarea p=(2, 1, 4, 6, 5, 3) S6 succesorul ei este p'=(2, 1,


5, 3, 4, 6). Ideea de calcul a succesorului este următoarea:
a) se determină cel mai lung şir descrescător de la sfârşitul permutării, fie acesta pk, pk+1, ...,
pn cu pk-1 < pk > pk+1 > ... > pn ; în cazul nostru, pentru p avem că 4 < 6 > 5 > 3;
b) se determină în secvenţa pk, pk+1, ..., pn elementul pj astfel încât j {k,...,n} şi pj-
1 > pk-1 > pj ; în cazul nostru avem 5 > 4 >3;
c) se interschimbă pk-1 cu pj-1 , adică 4 cu 5;
d) se sortează crescător elementele de pe poziţiile k,k+1,..,n , adică 6, 4, 3 şi se obţine 3, 4, 6.

Neîntâlnită până acum în curs, supraîncărcarea operatorului << de scriere în flux(engl:stream) de ieşire este
declarată ca funcţie prieten (engl:friend) tocmai pentru că ea poate fi apelată din afara obiectului curent.
Deci ea este o funcţie externă, care nu e membru al clasei Permutare dar are acces la datele
private/protejate din clasă. Codul ei apelează funcţia membru display a clasei Permutare, este simplu
şi şablonul ei va fi reprodus pentru fiecare clasă studiată în continuare:

1 ostream& operator<<(ostream& ostr, Permutare& perm)


2 {
3 perm.display();
4 return ostr;
5 }
Funcţia membru getInverse calculează şi returnează într-o permutare nouă valoarea inversei permutării
curente:

1 Permutare& Permutare::getInverse() const


2 {
3 Permutare* perm=new Permutare(*this);
4 for(unsigned int nrIndex=1; nrIndex<nrGrad;nrIndex++)
5 perm[data[nrIndex]]=nrIndex;

4
Structuri de date
6 return *perm;
7 }

Membrii isIdentic şi isReverse testează dacă permutarea curentă este cumva permutarea identică
respectiv permutarea inversă şi returnează 1 în caz afirmativ şi 0 în caz contrar:
1 unsigned int Permutare::isIdentic() const
2 {
3 for(unsigned int nrIndex=1; nrIndex<nrGrad;nrIndex++)
4 if(data[nrIndex]!=nrIndex)
5 return 0; //false
6 return 1; //true
7 }
8
9 unsigned int Permutare::isReverse() const
10 {
11 for(unsigned int nrIndex=1; nrIndex<nrGrad;nrIndex++)
12 if(data[nrIndex]!=nrGrad+1-nrIndex)
13 return 0; //false
14 return 1; //true
15 }

Se poate observa la membrii anteriori faptul că linia de declaraţie a lor este urmată de cuvântul rezervat
const, ceea ce înseamnă că ei nu modifică sub nici o formă membrii privaţi/protejaţi ai clasei
Permutare.
Pentru a putea valida o permutare (adică faptul că ea este o funcţie bijectivă pe mulţimea {1,2,…,n})
avem nevoie de o funcţie membru care testează acest lucru:

1 unsigned int Permutare::isValid() const


2 {
3 Array<unsigned int> tempArray(nrGrad, 1); // array temporar
4 for(unsigned int nrIndex=1; nrIndex<nrGrad;nrIndex++)
5 tempArray[nrIndex]=0; // iniţializează cu 0 valorile
6 for(nrIndex=1; nrIndex<nrGrad;nrIndex++)
7 if((data[nrIndex]>=1)&&(data[nrIndex<=nrGrad]))
8 {
9 tempArray[data[nrIndex]]++;
10 }
11 else
12 {
13 return 0; // nu este valida permutarea
14 }
15 for(nrIndex=1; nrIndex<nrGrad;nrIndex++)
16 if(tempArray[nrIndex]!=1)
17 return 0; // nu exista sau e duplicat
18 return 1; //true
19 }

5
Structuri de date
Funcţia printValue nu este membru al clasei însă este apelată de membrul display pentru a afişa
conţinutul permutării. Ea are ca parametru un întreg pozitiv şi îl afişează la fluxul standard de ieşire
cout:
1 void printValue(unsigned int nrPos)
2 {
3 cout<<nrPos<<"\t";
4 }

Aşa cum probabil sunteţi obişnuiţi de la Array şi clasa Permutare are un iterator care procesează
secvenţial valorile din vectorul suport al permutării. Acest membru se numeşte display şi are codul
următor:
1 void Permutare::display()
2 {
3 data.visit(printValue);
4 cout<<endl;
5 }

2. STRUCTURA DE DATĂ MATRICE BIDIMENSIONALĂ

Matricea bidimensională joacă un rol important în algebră şi geometrie vectorială motiv pentru care un
rol important este acordat acestei structuri de date. Ea va fi gândită exact aşa cum o tratează orice
calculator atunci când la compilare se cunosc deja dimensiunile ei (numărul de linii şi cel de coloane) cu
menţiunea că ne vom folosi de un obiect de tipul Array deja tratat. Clasa Array2D pe care o vom
scrie ca suport pentru matricele bidimensionale va privi o matrice cu m linii şi n coloane ca pe un grid,
adică vector unidimensional de m x n elemente grupate în m pachete (linii) succesive de câte n valori
(coloane) ca în Figura 1.

n n
elemente elemente
............ ..............
.
Linia 1 Linia 2 Linia i Linia m

Figura 1. Memorarea matricelor bidimensionale


Iată cum arată clasa ce implementează matricea bidimensională văzută simplu ca un grid cu
numberOfRows linii si numberOfColumns coloane, fără alte proprietăţi:

1 #include "array.h" // foloseste clasa Array


2 template<class T>
3 class Array2D
4 {
5 protected:
6 unsigned int numberOfRows;

6
Structuri de date
7 unsigned int numberOfColumns;
8 Array<T> array; // suportul de memorare
9 public:
10
11 class Row //foloseste clasa Row intern
12 {
13 Array2D &array2D; // referinta la un obiect de tip Array2D
14 unsigned int const row; // numarul liniei curente
15 public:
16 Row(Array2D &_array2D, unsigned int _row):
17 array2D(_array2D), row(_row){}// funcţie inline
18 T& operator[](unsigned int column) const
19 {
20 return array2D.select(row, column); // funcţie inline
21 }
22 };// sfârşitul clasei Row
23
24 Array2D(unsigned int _numberOfRows, unsigned int
25 _numberOfColumns):numberOfRows(_numberOfRows),
26 numberOfColumns(_numberOfColumns),
27 array(_numberOfRows*_numberOfColumns){} //funcţie inline
28 ~Array2D():numberOfRows(0), numberOfColumns(0)
29 {
30 array.setLength(0);//functie inline
31 }
32 T& select(unsigned int nrRow, unsigned int nrColumn);
33 Row operator[](unsigned int nrRow);
34 };

După cum se poate observa, în interiorul clasei Array2D am definit o nouă clasă Row corespunzătoare
unei linii din grid, vizibilă doar pentru metodele din clasa Array2D. Cum majoritatea metodelor au fost
deja definite inline, iată cum arată în continuare definiţiile metodelor select şi a operatorului de
indexare aparţinând clasei Array2D:

1 template<class T>
2 T& Array2D::select(unsigned int nrRow, unsigned int nrColumn)
3 { //functie inline
4 if(nrRow>=numberOfRows)
5 cout<< "Linia "<<nrRow<<" invalida "<<endl;
6 if(nrCol>=numberOfColumns)
7 cout<< "Coloana "<<nrColumn<<" invalida "<<endl;
8 return array[nrRow*numberOfColumns+ nrColumn]; //operatorul de
9 // indexare aplicat clasei Array
10 }
11
12 Row Array2D::operator[](unsigned int nrRow)
13 {
14 return Row(*this, nrRow); // functie inline
15 }

7
Structuri de date

Odată astfel definit suportul pentru grid, putem scrie interfaţa cu metodele ce implementează operaţiile
algebrice corespunzătoare matricilor şi anume adunarea, scăderea, înmulţirea, transpusa şi scrierea
matricilor.

1 #include "array2D.h" // folosim clasa Array2D


2 template<class T>
3 class Matrix:public Array2D<T> // moşteneşte toate metodele
4 // clasei Array2D
5 {
6 public:
7 Matrix(unsigned int _numberOfRows, unsigned int
8 _numberOfColumns):array2D<T>(_numberOfLines,_numberOfColumns){}
9 Matrix<T> operator*(Matrix<T> const & arg) const;
10 Matrix<T> operator+(Matrix<T> const & arg) const;
11 Matrix<T> operator-(Matrix<T> const & arg) const;
12 Matrix<T> transpose()const;
13 friend ostream& operator<<(ostream& ostr, Matrix<T> const &arg);
14 }; // sfârşit de clasă

După cum se poate observa pe liniile 7-8 din declaraţia clasei, constructorul este definit inline iar pe
linia 13 este declarată o funcţie (atenţie! nu e metodă a clasei) prieten care pune conţinutul matricei într-
un flux de ieşire.
Respectând întocmai algoritmul algebric vom implementa în cele ce urmează înmulţirea a două matrice:
1 Matrix<T> Matrix::operator*(Matrix<T> const & arg) const
2 {
3 if(numberOfColumns!=arg.numberOfRows)
4 cout<< "Matrici incompatibile"<<endl;
5 Matrix<T> result(numberOfRows, arg.numberOfColumns);
6 for(unsigned int nrCurrentRow=0; nrCurrentRow<numberOfRows;
7 nrCurrentRow++)
8 for(unsigned int nrCurrentColumn=0;
9 nrCurrentColumn<arg.numberOfColumns; nrCurrentColumn++)
10 {
11 T sum=0;
12 for(unsigned int nrIter=0; nrIter<numberOfColumns; nrIter++)
13 sum+=(*this)[nrCurrentRow][nrIter] * arg[nrIter][nrCurrentColumn];
14 result[nrCurrentRow][nrCurrentColumn]=sum;
15 }
16 return result;
17 }

În liniile 3-4 ne asigurăm că cele două matrici se pot înmulţi, după care în linia 5 alocăm noua matrice
rezultat şi apoi o populăm.

Adunarea şi scăderea a două matrice au aproximativ acelaşi cod şi se compun din trei etape (vezi codul
următor):

▪ validarea dimensiunilor (cele două matrici trebuie să aibă aceleaşi dimensiuni) pe liniile 3-6;

8
Structuri de date
▪ alocarea rezultatului (linia 7);
▪ adunarea elementelor corespondente (liniile 8-13).
1 Matrix<T> Matrix::operator+(Matrix<T> const & arg) const
2 {
3 if(numberOfRows!=arg.numberOfRows)
4 cout<< "Matrici incompatibile"<<endl;
5 if(numberOfColumns!=arg.numberOfColumns)
6 cout<< "Matrici incompatibile"<<endl;
7 Matrix<T> result(numberOfRows, numberOfColumns);
8 for(unsigned int nrCurrentRow=0; nrCurrentRow<numberOfRows;nrCurrentRow++)
9 for(unsigned int nrCurrentColumn=0;
10 nrCurrentColumn<numberOfColumns;nrCurrentColumn++)
11 result[nrCurrentRow][nrCurrentColumn]=
12 (*this)[nrCurrentRow][nrCurrentColumn]+
13 arg[nrCurrentRow][nrCurrentColumn];
14 return result;
15 }

Transpusa matricei se calculează relativ uşor deoarece construim o matrice nouă iar operatorul de
scriere în flux de ieşire seamănă cu cel de la Array:

1 Matrix<T> Matrix::transpose()const //nu modifică obiectul curent


2 {
3 Matrix<T> result(numberOfRows, numberOfColumns);
4 for(unsigned int nrCurrentRow=0; nrCurrentRow<numberOfRows;
5 nrCurrentRow++)
6 for(unsigned int nrCurrentColumn=0;
7 nrCurrentColumn<numberOfColumns; nrCurrentColumn++)
8 result[nrCurrentRow][nrCurrentColumn]=
9 (*this)[nrCurrentColumn][nrCurrentRow];
10 return result;
11 }
12
13 friend ostream& operator<<(ostream& ostr, Matrix<T> const &arg)
14 {
15 for(unsigned int nrCurrentRow=0; nrCurrentRow<numberOfRows;
16 nrCurrentRow++)
17 {
18 for(unsigned int nrCurrentColumn=0;
19 nrCurrentColumn<numberOfColumns;nrCurrentColumn++)
20 ostr<<arg[nrCurrentColumn][nrCurrentRow]<<"\t";
21 ostr<<endl;
22 }
23 return ostr;
24 }

9
Structuri de date

CURS 5
1. MULŢIMI

Mulţimea ca structură de date trebuie privită sub forma unui container de elemente. Teoretic, ea
seamănă foarte mult cu vectorul sortat cu deosebirea că nu poate conţine un element de mai multe ori.
Tocmai de aceea vom scrie o clasă Set (engl: mulţime) care implementează toate operaţiile cu mulţimi
bazată pe clasa SortArray deja studiată, cu amendamentul că tipul T din care provin elementele mulţimii
suportă o relaţie de ordine totală. În acest context, fiecare element al mulţimii va fi un element în
vectorul sortat SortArray. Iată cum arată declaraţia clasei Set:

1 #include "sortarray.h"
2 template<class T>
3 class Set:public SortArray<T>
4 {
5 public:
6 Set();
7 Set(Set const& set);
8 Set(T const& val);
9 unsigned int cardinal() const {return length;}
10 void setEmpty();
11 unsigned int isEmpty() const;
12 int contains(T const& val);
13 Set& add(T const& val);
14 Set& remove(T const& val);
15 Set& operator=(Set const& set);
16 Set& operator+(T const& val);
17 Set& operator-(T const& val);
18 Set& operator+(Set const& set);
19 Set& operator-(Set const& set);
20 Set& operator*(Set const& set) // intersectie
21 friend ostream& operator<<(ostream& ostr, Set const& set);
22 };
Vom lua pe rând metodele clasei set şi vom vedea cum sunt ele definite pentru a putea implementa
proprietăţile clasice ale mulţimilor. Pentru început să observăm cei trei constructori:

▪ primul iniţializează mulţimea cu mulţimea vidă;


▪ cel de-al doilea este un cosntructor de copiere;
▪ cel de-al treilea iniţializează mulţimea cu elementele unui vector primit ca parametru.

1 template<class T>
2 Set::Set()
3 {
4 length=0;

1
Structuri de date
5 base=0;
6 data= new T[0];
7 }
8
9 template<class T>
10 Set::Set(Set const& set)
11 {
12 if(this==&set)// este obiectul curent
13 return;
14 if(data)
15 delete [] data;
16 data=set.data;
17 length=set.length;
18 base=set.base;
19 }
20
21 template<class T>
22 Set::Set(T const& val)
22 {
23 if(data)
24 delete []data;
25 length=1;
26 data=new T[1];
27 data[0]=val;
28 base=0;
29 }

Metodele setEmpty şi respectiv isEmpty construiesc mulţimea vidă respectiv compară obiectul curent
cu mulţimea vidă.

1 template<class T>
2 void Set::setEmpty()
3 {
4 length=0;
5 base=0;
6 data= new T[0];
7 }
8
9 template<class T>
10 unsigned int Set::isEmpty()
11 {
12 return length==0;
13 }
Metoda contains implementează operatorul de apartenenţă iar codul ei se bazează pe metoda
findValue a clasei SortArray:

1 template <class T>


2 int Set::contains(T const& val){
3 return findValue(val);
4 }

2
Structuri de date

Funcţiile add şi remove la fel ca şi metodele ce au ca parametrii valori de tip T se bazează în


implementarea lor pe metodele addValue şi removeValue ale clasei SortArray după cum se poate
urmări în codul următor:

1 template <class T>


2 Set& Set::add(T const& val) // reuniunea cu un element
3 {
4 addValue(val);
5 return *this;
6 }
7
8 template <class T>
9 Set& Set::remove(T const& val) //scaderea unui element
10 {
11 return removeValue(val);
12 }
13
14 template <class T>
15 Set& Set::operator+(T const& val)
16 {
17 return add(val);
18 }
19
20 template <class T>
21 Set& Set::operator-(T const& val)
22 {
23 return remove(val);
24 }

Operaţiile propriu-zise de reuniune şi scădere au la bază şi ele reuniunea respectiv scăderea element cu
element aşa cum se poate urmări în fragmentul care le implementează. Ele sunt implementate sub
forma supraîncărcării operatorilor de adunare (+) respective de scădere (-):

1 template <class T>


2 Set& Set::operator+(Set const& set)
3 {
4 for(unsigned int i=0;i<set.length;i++)
5 if(findValue(set.data[i])<0)
6 addValue(set.data[i]);
7 return *this;
8 }
9
10 template <class T>
11 Set& Set::operator-(Set const& set){
12 for(unsigned int i=0;i<set.length;i++)
13 removeValue(set.data[i]);
14 return *this;
15 }

3
Structuri de date
Intersecţia este la fel de simplă şi se bazează pe eliminarea succesivă a elementelor care nu sunt găsite în
mulţimea primită ca parametru. Vom implementa intersecţia ca supraîncărcare a operatorului de
înmulţire * .

1 template <class T>


2 Set& Set::operator*(Set const& set) // intersectie
3 {
4 for(unsigned int i=0;i<length;i++)
5 if(set.findValue(data[i])<0)
6 removeValue(data[i]);
7 return *this;
8 }

Atribuirea dintre două mulţimi este şie ea posibilă pe baza supraîncărcării operatorului de atribuire.
După ce ne-am asigurat că nu atribuim tocmai obiectul current (linia 3) vom dealoca orice memorie
folosită si vom copia toate valorile noii mulţimi:

1 template <class T>


2 Set& Set::operator=(Set const& set)
3 {
4 if(this==&set)
5 return *this;
6 if(data)
7 delete [] data;
8 data=set.data;
9 length=set.length;
10 base=set.base;
11 return *this;
12 }

Scrierea unei mulţimi la fluxul de ieşire se face analog ca la clasa SortArray cu menţiunea că vom afişa
între acolade conţinutul mulţimii:

1 template <class T>


2 ostream& operator<<(ostream& ostr,
3 Set const& set)
4 {
5 ostr<<"{ ";
6 for(unsigned int i=0;i<set.length;i++)
7 ostr<<set.data[i]<<" ";
8 ostr<<"}"<<endl;
9 return ostr;
10 }

Dacă vrem să vedem rezultatul muncii de până acum cu clasa Set vom rula exemplul următor:
1 #include "set.h"
2 void main()
3 {
4 Set<unsigned int> s,g,p,k;

4
Structuri de date
5 s=s+1;
6 s=s+5;
7 s=s+8;
8 s=s+4;
9 cout<<s; // afiseaza {1,4,5,8}
10 p=s;
11 k=s;
12 g=g+2;
13 g=g+4;
14 g=g+8;
15 cout<<g; // afiseaza {2,4,8}
16 s=s+g;
17 cout << s; // afiseaza {1,2,4,5,8}
18 k=p*g;
19 cout <<p*g; // afiseaza {4,8}
20 k=k-g;
21 cout <<k; // afiseaza {2}
22 }

2. POLINOAME
O altă implementare a vectorului unidimensional sortat este în cazul operaţiilor simbolice cu polinoame.
Un polinom poate fi implementat ca un vector în care elementul de indice k indică valoarea
coeficientului termenului de grad k însă în această situaţie, polinoamele cu un mare număr de coeficienţi
nuli vor avea o cantitate mult prea mare de memorie alocată lor.

Soluţia oferită de vectorul sortat este cea a unei memorări a perechilor (grad, coeficient) ordonate
după grad, consumând astfel memorie numai pentru termenii nenuli.

De exemplu, polinomul P(x ) = 2x + 7x + x + 4 este memorat sub forma unei vector sortat
5 3 2

L=(4;(2, 5);(7, 3);(1; 2);(4, 0))


 
nr de elem. conţinutul listei
din listă
Operaţiile primitive ce se pot efectua cu polinoame sunt:
P1) adăugarea/ştergerea unui termen dintr-un polinom;
P2) adunarea/scăderea a două polinoame;
P3) înmulţirea/împărţirea a două polinoame;
P4) înmulţirea unui polinom cu o constantă.

După cum se vede avem nevoie de un tip de date special Term corespunzător termenilor memoraţi în
vector. Termenii vor avea ca şi valori coeficientul termenului de grad k şi gradul (k) al termenului de
grad k. Deoarece clasa SortArray presupune existenţa unei relaţii de ordine totale între elementele

5
Structuri de date
vectorului vom fi nevoiţi să supraîncărcăm operatorii de comparaţie pentru clasa Term. Iată cum arată
această clasă (cu toate funcţiile definite inline):

1 template<class T>
2 class Term
3 {
4 public:
5 T coef;
6 unsigned int grd;
7 Term():coef(0), grd(0){ }
8 Term(T const& newCoef, unsigned int newGrd=0): coef(newCoef)
9 {
10 grd=(newCoef==0.0)?0:newGrd;
11 }
12 Term(Term<T> const& t)
13 {
14 coef=t.coef;
15 grd=(coef==0.0)?0:t.grd;
16 }
17 unsigned int isNull() const
18 {
19 return (coef==0.0);
20 }
21 unsigned int operator <(Term<T> const& t)
22 {
23 return (grd<t.grd);
24 }
25 unsigned int operator <=(Term<T> const& t)
26 {
27 return (grd<=t.grd);
28 }
29 unsigned int operator >(Term<T> const& t)
30 {
31 return (grd>t.grd);
32 }
33 unsigned int operator >=(Term<T> const& t)
34 {
35 return (grd>=t.grd);
36 }
37 unsigned int operator ==(Term<T> const& t) // egalitate la grade
38 {
39 return (grd==t.grd);
40 }
41 unsigned int eq(Term<T> const& t)// egalitate completă
42 {
43 return ((grd==t.grd)&&(coef==t.coef));
44 }
46 Term<T>& operator=(Term<T> const& t)
47 {
48 coef=t.coef;

6
Structuri de date
48 grd=t.grd;
50 return *this;
51 }
52 };// sfârşitul clasei

După cum se poate observa în definiţia clasei Term aceasta are doar doi membrii de tip dată şi anume
gradul grd şi coeficientul coef (de tip T). În acest mod putem lucra cu polinoame de orice tip (ex de
numere reale, complexe, etc). De asemenea se remarcă faptul că singurele metode definite au fost (pe
lângă constructori) supraîncărcarea operatorilor de comparaţie.
Să vedem acuma cum arată clasa propriu-zisă Polinom:
1 #include "sortarray.h" // pentru clasa SortArray
2 #include "term.h" // pentru clasa Term tocmai definită
3 template<class T>
4 class Polinom
5 {
6 protected:
7 SortArray<Term<T>> data;
8 unsigned int grd; //gradul
9 public:
10 Polinom();
11 Polinom(Term<T> const& t);
12 ~Polinom();
13 unsigned int isNull();
14 SortArray<Term<T> > & getData();
15 unsigned int & getGrad();
16 Polinom<T>& addTerm(Term<T> const& t);
17 Polinom<T>& subTerm(Term<T> const& t);
18 Polinom<T>& addValue(T const& t1, unsigned int g);
19 Polinom<T>& subValue(T const& t1, unsigned int g);
20 Polinom<T>& operator=(Term<T> const& t);
21 Polinom<T>& operator=(Polinom<T> const& p);
22 Polinom<T>& operator+(Term<T> const& t);
23 Polinom<T>& operator+(T const& t);
24 Polinom<T>& operator+(Polinom<T> const& p);
25 Polinom<T>& operator-(Term<T> const& t);
26 Polinom<T>& operator-(T const& t);
27 Polinom<T>& operator-(Polinom<T> const& p);
28 Polinom<T>& operator*(T const& t);
29 Polinom<T>& operator*(Term<T> const & t);
30 Polinom<T>& operator*(Polinom<T> const& p);
31 T evaluate(T const& x);
32 friend ostream& operator<<(ostream& o, Polinom<T>& p);
33 };// sfârşitul clasei

În cele ce urmează sunt prezentate implementările acestor operaţii. Pentru început să vedem cum arată
constructorii şi destructorul:

7
Structuri de date
1 template<class T>
2 Polinom::Polinom():grd(0)
3 {
4 data=data+0;// se apelează construcorul Term(0)
5 }
6 template<class T>
7 Polinom::Polinom(Term<T> const& t)
8 {
9 data=t;
10 grd=t.grd;
11 }
12 template<class T>
13 Polinom::~Polinom()
14 {
15 grd=0; // aici se apelează implicit destructorul lui Array
16 }

Metoda isNull ne spune dacă polinomul curent este identic nul (are gradul şi coeficientul nul).
Metodele getData şi getGrad sunt accesorii tipici ai polinomului şi au următorul cod:

1 template<class T>
2 unsigned int Polinom::isNull()
3 {
4 return ((grd==0)&&(data[0].coef==0.0));
5 }
6 template<class T>
7 SortArray<Term<T> > & Polinom::getData()
8 {
9 return data;
10 }
11 template<class T>
12 SortArray<Term<T> > & Polinom::getGrad()
13 {
14 return grd;
15 }

La adăugarea unui termen în polinom trebuie să avem în vedere următoarele: dacă nu există deja termen
de gradul respectiv atunci se adaugă direct în SortArray; altfel, se adună cei doi coeficienţi ai gradului
respectiv şi dacă cumva rezultă valoarea nulă atunci se elimină termenul respectiv din SortArray. Codul
metodei addTerm este următorul:

1 template<class T>
2 Polinom<T>& Polinom::addTerm(Term<T> const& t)
3 {
4 int pos=data.findValue(t);
5 if(pos<0)
6 {
7 data=data+t;
8 grd=(grd<t.grd)? t.grd :grd;

8
Structuri de date
9 }
10 else
11 {
12 Term<T> & curent=data[pos];
13 T sum=curent.coef+ t.coef;
14 if(sum==0.0)//trebuie eliminat termenul
15 {
16 data=data - curent;
17 grd=data[data.getLength()-1].grd;
18 }
19 else
20 {
21 curent.coef=sum;
22 }
23 }
24 if(data.getLength()==0)// nu mai exista termeni
25 {
26 data=data+0;
27 grd=0;
28 }
29 else
30 grd=data[data.getLength()-1].grd;
31 return *this;
32 }

Pe baza acestei funcţii putem scrie imediat codul pentru metodele subTerm, addValue şi subValue:
1 template<class T>
2 Polinom<T>& Polinom::subTerm(Term<T> & t)
3 {
4 t.coef=-t.coef;
5 return addTerm(t);
6 }
7 template<class T>
8 Polinom<T>& Polinom::addValue(T const& t1, unsigned int g)
9 {
10 Term<T> t(t1,g);
11 return addTerm(t);
12 }
13 template<class T>
14 Polinom<T> & Polinom::subValue(T const& t1, unsigned int g)
15 {
16 return addValue(-t1,g);
17 }

Cel mai simplu cod pentru operatori clasei Polinom îl au cei care nu iterează membrul data şi
anume:
1 template<class T>
2 Polinom<T>& Polinom::operator=(Term<T> const& t)
3 {
4 if(data.getData())

9
Structuri de date
5 {
6 data.setLength(0); //dealocă eventualul spaţiu alocat
7 }
8 data=data+t;
9 grd=t.grd;
10 return *this;
11 }
12 template<class T>
13 Polinom<T>& Polinom::operator=(Polinom<T> const& p)
14 {
15 data=p.data;
16 grd=p.grd;
17 return *this;
18 }
19 template<class T>
20 Polinom<T>& Polinom::operator+(Term<T> const& t)
21 {
22 return addTerm(t);
23 }
24 template<class T>
25 Polinom<T>& Polinom::operator+(T const& t)
26 {
27 Term<T> trm(t);
28 return addTerm(trm);
29 }
30 template<class T>
31 Polinom<T>& Polinom::operator-(Term<T> const& t)
32 {
33 return subTerm(t);
34 }
35 template<class T>
36 Polinom<T>& Polinom::operator-(T const& t)
37 {
38 Term<T> trm(t);
39 return subTerm(trm);
40 }

Adunarea respectiv scăderea la nivel de Polinom va necesita parcurgerea tuturor termenilor polinomului
parametru şi adunarea/scăderea lor din polinomul reprezentat de obiectul curent:

1 template<class T>
2 Polinom<T>& Polinom::operator+(Polinom<T> const& p)
3 {
4 for(unsigned int nrIndex=0;nrIndex<p.data.getLength();nrIndex++)
5 addTerm(p.data[nrIndex]);
6 return *this;
7 }
8 template<class T>
9 Polinom<T> & Polinom::operator-(Polinom<T> const& p)
10 {

10
Structuri de date
11 for(unsigned int nrIndex=0;nrIndex<p.data.getLength();nrIndex++)
12 subTerm(p.data[nrIndex]);
13 return *this;
14 }

Înmulţirea polinoamelor o vom trata gradual şi anume vom defini înmulţirea cu o constantă de tip T,
apoi cu un termen şi în final vom implementa înmulţirea a două polinoame:

1 template<class T>
2 Polinom<T>& Polinom::operator*(T const& t)
3 {
4 if(t==0)// înmulţire cu constanta 0
5 {
6 data=0;
7 grd=0;
8 }
9 else
10 for(unsigned int i=0;i<data.getLength();i++)
11 data[i].coef=data[i].coef*t;
12 return *this;
13 }
14
15 template<class T>
16 Polinom<T>& Polinom::operator*(Term<T> const & t)
17 {
18 if(t.isNull()) //înmulţire cu termen nul
19 {
20 data.setLength(0);
21 data=0;
22 grd=0;
23 }
24 else
25 for(unsigned int i=0;i<data.getLength();i++)
26 {
27 data[i].coef=data[i].coef*t.coef;
28 data[i].grd =data[i].grd+t.grd;
29 }
30 return *this;
31 }
32
33 template<class T>
34 Polinom<T>& Polinom::operator*(Polinom<T> const& p)
35 {
36 if(p.isNull())
37 {
38 data=0;
39 grd=0;
40 }
41 else
42 for(unsigned int i=0;i<p.data.getLength();i++)

11
Structuri de date
43 for(unsigned int j=0;j<data.getLength();j++)
44 {
45 data[j].coef=data[j].coef*p.data[i].coef;
46 data[j].grd=data[j].grd +p.data[i].grd;
47 }
48 return *this;
49 }

Ca şi metodă utilitară pentru clasa Polinom funcţia evaluate primeşte ca parametru o constantă de tip T
şi calculează valoarea polinomului în acel punct:

1 template<class T>
2 T Polinom::evaluate(T const& x)
3 {
4 T p=0.0;
5 int curGrd;
6 for(int i=data.getLength()-1;i>=0;i--)
7 {
8 p+=data[i].coef;
9 curGrd=data[i].grd;
10 int nextGrd=(i>0)?data[i-1].grd:0;
11 while(curGrd-- > nextGrd)
12 p*=x;
13 }
14 while(curGrd-->0)
15 p*=x;
16 return p;
17 }

Deosebit de utilă atunci când dorim să afişăm conţinutul unui polinom este funcţia prieten de
supraîncărcare a operatorului << care afişează scrierea lui simbolică la un flux de ieşire:

1 template<class T>
2 ostream& operator<<(ostream& o, Polinom<T>& p)
3 {
4 unsigned int isFirst=1;
5 for(unsigned int i=0;i<p.data.getLength();i++)
6 {
7 if(p.data[i].coef>0)
8 o<<((isFirst)?"":"+");
9 if(p.data[i].grd==0)
10 o<<p.data[i].coef;
11 else
12 if(p.data[i].coef==1.0)
13 o<<"X^"<<p.data[i].grd;
14 else
15 if(p.data[i].coef==-1.0)
16 o<<"-X^"<<p.data[i].grd;
17 else
18 o<<p.data[i].coef<<"X^"<<

12
Structuri de date
19 p.data[i].grd;
20 isFirst=0;
21 }
22 o<<endl;
23 return o;
24 }

13
Structuri de date

CURS 6
1. MATRICI RARE
Matricile bidimensionale sunt structuri de date frecvent utilizate în algoritmi. În cazul în care
majoritatea elementele matricii sunt nenule, atunci este foarte eficientă memorarea ei sub formă de
tablou bidimensional.
Efortul de memorare este astfel de mxn  sizeof (T) . Această soluţie este inadecvată însă în cazul în care
avem de-a face cu matrici rare (engl; sparse matrix), adică matrici care au un număr foarte mare de
elemente egale cu 0. Nu există o definire clară a procentului de elemente nenule într-o matrice nenulă,
însă se acceptă că o matrice care are mai puţin de 15 elemente nenule se poate numi rară.
Interesul cu care sunt tratate matricile rare este dat de faptul că în practică matricile rare apar foarte
frecvent şi numai un procent infim din matricile cu care se lucrează, sunt populate peste 20. Totodată
matricile rare au dimensiuni foarte mari (peste 100), ceea ce face mai avantajoasă memorarea sub formă
de listă .
O metodă de memorare a matricilor rare este cea folosită de Horowitz & Sahni sub formă de listă,
fiecare element al listei conţinând trei valori, după cum urmează:
i) cele trei valori ale primului element conţin dimensiunile matricei
(număr_linii , număr_coloane şi numărul de elemente nenule din matrice);
ii) celelalte linii conţin fiecare câte o intrare (Entry) de tipul (număr_linie, număr_coloană, valoare)
asfel încît fiecare element nenul are trei locaţii de memorie folosite pentru memorare; ordinea în care
sunt memorate este cea lexicografică astfel încât se memoreză mai întâi linia apoi coloana.

De exemplu, matricea:
0 0 0 5
0 9 0 0
A=
0 0 0 3
 
4 0 1 0

va fi memorată sub forma: L = ((4,4,5), (1,4,5), (2,2,9), (3,4,5), (4,1,4), (4,3,1))

nr. de linii nr. elem0


nr. de coloane

Să vedem în continuare care este câştigul obţinut prin memorarea unei matrice rare A(n:) cu p
elemente nenule, faţă de varianta clasică, sub forma de tablou bidimensional:

- pentru cazul clasic , L1 (n ) = n 2 locaţii de memorie

1
Structuri de date
- pentru varianta cu listă , L 2 (n ) = 3(p + 1) , cu p  n .

n2 n2
Dacă p<< − 1 ceea ce este foarte posibil, deoarece, p 2  , atunci este evident avantajul variantei
3 10
sub formă de listă. În cele ce urmează se prezintă o clasă în limbajul C++ care defineşte câteva din
operaţiile ce se pot efectua cu matrice rare, şi anume;
1. calculul transpusei;
2. setarea valorii unui element (adăugare, modificare, ştergere);
3. adunarea a două matrici rare;
4. înmulţirea a două matrici rare;
5. înmulţirea unei matrici rare cu o constantă;
6. afişarea unei matrice rare.

Pentru a putea lucra cu matrici rare vom avea nevoie să definim o clasă Entry corespunzătoare intrării
în vectorul sortat. Clasa Entry va memora linia, coloana şi valoarea nenulă corespunzătoare şi va
implementa prin supraîncărcarea operatorilor o relaţie de ordine totală între valorile de tip Entry
pentru a putea vorbi de vector sortat. Iată cum arată declaraţia clasei împreună cu definiţiile inline ale
tuturor metodelor sale:

1 template<class T>
2 class Entry // corespunde unei intrări în listă de tipul
3 // (linie, coloană, valoare)
4 {
5 public:
6 unsigned int lin;
7 unsigned int col;
8 T val;
9
10 Entry(){}
11 Entry(unsigned int l, unsigned int c, T const & v):lin(l),
12 col(c), val(v)
13 {
14 if(v==0){
15 cout<< "Eroare! Valoare nula la linia "<<lin <<
16 ", coloana "<<col<<endl;
17 }
18 }
19 Entry(Entry<T> const& t)
20 {
21 if(t.val==0){
22 cout<< "Eroare! Valoare nula la linia "<<t.lin <<
23 ", coloana "<<t.col<<endl;
24 }
25 val=t.val;
26 lin=t.lin;

2
Structuri de date
27 col=t.col;
28 }
29 void setVal(unsigned int l, unsigned int c, T const & v)
30 {
31 lin=l;
32 col=c;
33 val=v;
34 if(v==0){
35 cout<< "Eroare! Valoare nula la linia "<<lin <<
36 ", coloana "<<col<<endl;
37 }
38 }
39 unsigned int isNull() const
40 {
41 return ((v==0.0));
42 }
43 unsigned int operator<(Entry<T> const& t)
44 {
45 return ((lin<t.lin)||((lin==t.lin)&&(col<t.col)));
46 }
47 unsigned int operator<=(Entry<T> const& t)
48 {
49 return ((lin<t.lin)||((lin==t.lin)&&(col<=t.col)));
50 }
51 unsigned int operator>(Entry<T> const& t)
52 {
53 return ((lin>t.lin)||((lin==t.lin)&&(col>t.col)));
54 }
55 unsigned int operator>=(Entry<T> const& t)
56 {
57 return ((lin>t.lin)||((lin==t.lin)&&(col>=t.col)));
58 }
59 unsigned int operator==(Entry<T> const& t)
60 {
61 return ((lin==t.lin)&&(col==t.col));
62 }
63 unsigned int eq(Entry<T> const& t)
64 {
65 return ((lin==t.lin)&&(col==t.col)&&(val==t.val));
66 }
67 Entry<T>& operator=(Entry<T> const& t)
68 {
69 if(t.val==0){
70 cout<< "Eroare! Valoare nula la linia "<<t.lin <<
71 ", coloana "<<t.col<<endl;
72 }
73 val=t.val;
74 lin=t.lin;
75 col=t.col;
76 return *this;

3
Structuri de date
77 }
78 };// sfârşitul clasei Entry

Se va folosi pentru memorarea matricilor rare sub formă de listă, o clasă numită SMatrix (de la sparse
matrix) în conformitate cu cele menţionate anterior. Această clasă foloseşte obiecte de tipul Entry iar
declaraţia ei împreună cu definiţiile metodelor inline este următoarea:

1 #include "sortarray.h"
2 template<class T>
3 class SMatrix
4 {
5 protected:
6 unsigned int nrLin; // numărul de linii al matricei
7 unsigned int nrCol; // numărul de coloane al matricei
8 unsigned int nrVal; // numărul de valori nenule
9 SortArray<Entry<T> > data; //valorile sunt memorate într-un
10 //vector sortat de elemente de tip Entry
11 public:
12 SMatrix(unsigned int l, unsigned int c):nrLin(l), nrCol(c),
13 nrVal(0) {}
14 ~SMatrix():nrLin(0),nrCol(0),nrVal(0){}
15 unsigned int isNull()
16 {
17 return (nrVal==0);
18 }
19 SortArray<Entry<T> >& getData()
20 {
21 return data;
22 }
23 SMatrix<T>& addEntry(Entry<T> const& t)
24 {
25 if((t.lin<=0)||(t.lin>nrLin)||(t.col<=0)||(t.col>nrCol))
26 {
27 cout<< "Eroare! dimensiuni incorecte !"<<endl;
28 return *this;
29 }
30 int pos=data.findValue(t);
31 if(pos<0)
32 {
33 data=data+t;
34 nrVal++;
35 }
36 else
37 {
38 Entry<T> & curent=data[pos];
39 T sum=curent.val+ t.val;
40 if(sum==0.0)
41 {
42 data=data - curent;
43 nrVal--;

4
Structuri de date
44 }
45 else
46 {
47 curent.val=sum;
48 }
49 }
50 return *this;
51 }
52 SMatrix<T>& subEntry(Entry<T> & t)
53 {
54 t.coef=-t.coef;
55 return addEntry(t);
56 }
57 SMatrix<T>& addValue(unsigned int l, unsigned int c,
58 T const& t1)
59 {
60 Entry<T> t(l,c,t1);
61 return addEntry(t);
62 }
63 SMatrix<T> & subValue(unsigned int l, unsigned int c,
64 const& t1)
65 {
66 Entry<T> t(l,c,-t1);
67 return addValue(t1);
68 }
69 SMatrix& reset() // elimină toate valorile
70 {
71 data.setLength(0);
72 nrVal=0;
73 }
74 SMatrix<T>& operator=(SMatrix<T> const& p)
75 {
76 data=p.data;
77 nrLin=p.nrLin;
78 nrCol=p.nrCol;
89 nrVal=p.nrVal;
90 return *this;
91 }
92 SMatrix<T>& operator+(Entry<T> const& t)
93 {
94 return addEntry(t);
95 }
96 SMatrix<T>& operator+(SMatrix<T> const& p);
97 SMatrix<T>& operator-(Entry<T> const& t)
98 {
99 return subEntry(t);
100 }
101 SMatrix<T>& operator-(SMatrix<T> const& p);
102 SMatrix<T>& operator*(T const& t);
103 friend ostream& operator<<(ostream& o, SMatrix<T>& p);

5
Structuri de date
}; // sfârşitul clasei SMatrix
După cum se poate observa şi în declaraţia clasei SMatrix, sunt câteva metode care presupun iterarea
vectorului suport data şi care nu au putut fi definite inline. Pentru început, iată codul pentru
supraîncărcarea operatorului de adunare pe matrici rare:

1 template<class T>
2 SMatrix<T>& SMatrix::operator+(SMatrix<T> const& p)
3 {
4 if((p.lin!=lin)||(p.col!=col))
5 {
6 cout<<"Eroare! nu pot aduna matrici de”
7 “ dimensiuni diferite!"<<endl;
8 return *this;
9 }
10 for(unsigned int i=0; i < p.data.getLength(); i++)
11 addEntry(p.data[i]);
12 return *this;
13 }

Analog, supraîncărcarea operatorului de scădere pe matrici iterează toate elementele vectorului sortat
data al variabilei parametru:

1 template<class T>
2 SMatrix<T>& Smatrix::operator-(SMatrix<T> const& p)
3 {
4 if((p.lin!=lin)||(p.col!=col))
5 {
6 cout<<"Eroare! nu pot aduna matrici de imensiuni diferite!"<<endl;
7 return *this;
8 }
9 for(unsigned int i=0; i < p.data.getLength(); i++)
10 subEntry(p.data[i]);
11 return *this;
12 }

Pentru înmulţirea a unei matrici rare cu o constantă de tip T vom parcurge toate elementele şi vom
înmulţi valorile corespunzătoare cu valoarea din parametru:

1 template<class T>
2 SMatrix<T>& SMatrix::operator*(T const& t)
3 {
4 if(t==0)
5 {
6 data.setLength(0);
7 nrVal=0;
8 }
9 else
10 for(unsigned int i=0; i < data.getLength(); i++)
11 data[i].val=data[i].val*t;
12 return *this;

6
Structuri de date
13 }
Aşa cum am obişnuit cu toate clasele studiate până acum, vom scrie o funcţie prieten de afişare a
matricii la un flux de ieşire de tip ostream. Rezultatul va arăta sub forma unui caroiaj dreptunghiular cu
linii şi coloane:

1 template<class T>
2 ostream& operator<<(ostream& o, SMatrix<T>& p)
3 {
4 unsigned int k=0;
5 for(unsigned int i=0; i < p.nrLin; i++)
6 {
7 for(unsigned int j=0; j < p.nrCol; j++)
8 if((k<p.data.getLength())&&(p.data[k].lin==i)
9 &&(p.data[k].col==j))
10 {
11 cout <<p.data[k++].val<<"\t";
12 }
13 else
14 {
15 cout <<0<<"\t";
16 }
17 cout<<endl;
18 }
19 return o;
20 }

7
Structuri de date

CURS 7
1. Stiva

DEFINIŢIA 1: Stiva este o listă ordonată în care toate operaţiile de inserare şi ştergere sunt
efectuate la un singur capăt numit vârful stivei (“top“ in engleză).
Dându-se o stivă S=(a1, .., an) atunci a1 este elementul de la baza stivei (“bottom” în engleză ) şi ai+1
este deasupra lui ai (1 < i  n). Intuitiv, o stivă S=(a1, .., an) poate fi desenată sub forma:

an top

.
.
.
a.2
bottom
a1

Stiva este un tip particular de listă. Particularitatea ei constă în faptul că toate operaţiile
(inserare/extragere element) se fac la un singur capăt. Dacă este să urmărim ordinea în care
introducem şi extragem elementele rezultă că totdeauna, ultimul element introdus este primul
extras, cu alte cuvinte are loc regula (in engleză) “Last In, First Out“ prescurtat LIFO, adică
"Ultimul Intrat, Primul Ieşit".
Definirea structurii de dată STACK cu elemente de tip T este următoarea:
SDSTACK={D, d, F, A}
cu:
D={T, STACK, BOOLEAN}
d=STACK

F conţine următoarele funcţii:

• CREATE() → STACK
• PUSH(T, STACK) → STACK
• DELETE(STACK) → STACK
• GETTOP(STACK) → T
• POP(STACK) → T
• ISEMTS(STACK) → BOOLEAN

A conţine următoarele axiome:

() s  STACK, i  T 
• ISEMTS(CREATE) ::= TRUE
• ISEMTS(PUSH(i,S)) ::= FALSE

1
Structuri de date
• DELETE(CREATE) ::= ERROR
• POP(PUSH(i,S)) ::= i
• GETTOP(CREATE) ::= ERROR
• GETTOP(PUSH(i,S)) ::= i
• POP(CREATE) ::= ERROR
• POP(PUSH(i,S)) ::= i

Operaţiile cele mai uzuale pe stivă – inserarea, respectiv extragerea unui element – sunt întâlnite cel
mai des cu numele de PUSH (inserare) si POP (extragere). Metoda GETTOP returnează elementul
din vârful stivei fără însă a-l extrage din stivă (spre deosebire de PUSH care-l extrage).
Deoarece vom lucra în continuare cu colecţii numărabile de elemente vom defini o clasă virtuală numită
Container care nu este altceva decât expresia acestei colecţii:

1 // fişierul "container.h"
2 class Container
3 {
4 protected:
5 unsigned int count;
6 Container():count(0){}
7 public:
8 virtual unsigned int getCount()const;
9 virtual unsigned int isEmpty()const;
10 virtual unsigned int isFull()const;
11 };// sfârşitul clasei

Containerul are un singur membru de tip dată şi anume variabila count care ne spune exact câte
elemente sunt în colecţie. Metodele getCount,isEmpty şi isFull sunt funcţii virtuale care vor fi
implementate în clasele derivate din Container. Nu vom avea explicit nici un obiect de tipul
Container ci numai obiecte derivate.
Vom scrie în continuare o clasă numită Stack ce defineşte funcţionalitatea unei stive. Acestă clasă
derivă din Container şi este folosită doar pentru a specifica interfaţa (funcţionalitatea) care trebuie
oferită de o stivă. Nu vom avea obiecte de tip Stack ci vom deriva diverse implementări de stive din
clasa Stack. Definiţia interfeţei Stack este următoarea:

1 // fisierul "stack.h"
2 #include "container.h"
3
4 template<class T>
5 class Stack:public virtual Container
6 {
7 public:
8 virtual T& getTop()const=0; //funcţie virtuală pură
9 virtual T& pop()=0; //funcţie virtuală pură
10 virtual void push(T const&)const=0; //funcţie virtuală pură
11 };

2
Structuri de date
După cum se poate observa, interfaţa Stack derivă din Container deci moşteneşte toate metodele şi
membrii dată ai acesteia. Singurele metode noi sunt getTop, pop şi push. Faptul că ele sunt
declarate urmat de expresia “=0” înseamnă că sunt metode virtuale pure, adică nu vor fi definite
(implementate) în clasa Stack ci numai eventual în clasele derivate din aceasta.
Vom defini implementarea stivei ca un vector unidimensional (Array) de capacitate fixă, într-o
clasă cu numele StackAsArray. Iată definiţia şi implementarea acestei clase:

1 //fişierul "stackasarray.h"
2 #include "stack.h"
IMPORTANT
3 #include "array.h"
4 #include "error.h" Implementarea unei stive presupune:
5 template<class T>  Un suport de memorare array;
6 class StackAsArray:public Stack  Definirea operaţiilor primitive push şi pop.
7 {
8 protected:
9 Array<T> array;
10 public:
11 StackAsArray(unsigned int size):array(size){}
12 StackAsArray(StackAsArray<T> const &s)array(s.array){}
13 ~StackAsArray(){}
14 T& getTop() const
15 {
16 if(count==0)
17 throw Error("Stiva vida", ERROR);
18 return array[count-1];
19 }
20 T& pop()
21 {
22 if(count==0)
23 throw Error("Stiva vida", ERROR);
24 return array[--count];
25 }
26 void push(T const& t)
27 {
28 if(count==array.getLength())
29 throw Error("Stiva plina", ERROR);
30 array[count++]=t;
31 }
32 unsigned int isEmpty()
33 {
34 return (count==0);
35 }
36 unsigned int isFull()
37 {
38 return (count==array.getLength());
39 }
40 }; //sfârşitul clasei StackAsArray

3
Structuri de date
În codul anterior pe liniile 17, 23 şi 29 apare o instrucţiune nouă şi anume throw. Este o facilitate a
limbajului C++ care permite ca atunci când apare o eroare ea să fie tratată separat prin “aruncarea”
(engl: throw) ei pe o stivă de erori care va fi tratată separat în corpul unei secvenţe “try”. Dacă
analizăm metoda getTop vedem că pe linia 17 dacă stiva este vidă atunci ea creează un obiect de
tip Error şi îl aruncă pe stiva de erori a programului.

2. Forma poloneză. Evaluarea expresiilor aritmetice folosind stiva


Ne propunem în continuare să tratăm evaluarea expresiilor aritmetice şi notaţia poloneză din
perspectiva utilizării stivelor.
Un subiect foarte important în construcţia compilatoarelor l-a constituit interpretarea expresiilor
aritmetice. O posibilă tratare a acestora este aceea în care ele sunt transcrise din forma normală în
forma poloneză inversă sau postfixată. Fie A op B o expresie, în care A şi B sunt la rândul lor
expresii, iar op este un operator aritmetic binar simplu (+, -, *, /, =, and, or). De exemplu 5 +2,
x AND y, i/j sunt expresii de forma A op B. Pentru simplificarea lucrurilor vom presupune că A şi
B nu conţin paranteze care să afecteze prioritatea operaţiilor. În efectuarea expresiilor aritmetice
simple, un rol important îl joacă noţiunea de prioritate a unei operaţii. O operaţie binară op1 se zice
că este mai prioritară (are prioritatea mai mare) decât operaţia op2, dacă în expresia A op1 B op2 C
se efectuează mai întâi A op1 B, iar apoi rezultatului i se va aplica operaţia op2, adică dacă ordinea
de efectuare este cea corespunzătoare expresiei (A op1 B) op2 C.
Prioritatea operaţiilor amintite este dată de tabelul următor:

Operator Prioritate
*, / 5
+, - 4
<, <>, <=, =, 3
>=, >
And 2
Or 1

Pentru a simplifica şi mai mult calculele, vom considera numai operatorii +, -, *, / care sunt formaţi
dintr-un singur caracter iar rezultatul aplicării acestor operatori unor numere este tot un număr, spre
deosebire de expresiile logice unde rezultatul este true sau false . Prioritatea acestor 4 operatori este
luată din tabelul de priorităţi anterior.
Vom defini în continuare noţiunile de formă poloneză. Fie expresia A op B unde A, B şi op au
semnificaţiile anterioare. Atunci expresia se poate scrie în 3 moduri şi anume:
i) în forma poloneză prefixată: op A B ;
ii) în forma poloneză infixată (normală): A op B ;
iii) în forma poloneză postfixată (inversă): A B op .

4
Structuri de date
De exemplu, expresia 3+5 (în forma poloneza infixată) poate fi scrisă sub forma + 3 5 (forma
poloneză prefixată) sau 3 5 + (forma poloneză postfixată). O expresie E = A+B*C/D-F poate fi
scrisă sub forma poloneză postfixată astfel:
E = A B C D / * + F –
Important este modul în care, pornind de la o expresie în forma normală, să ajungem la forma
poloneză inversă şi apoi, pornind de la forma poloneză inversă să calculăm valoarea expresiei în
cazul în care A, B, C, … sunt înlocuite cu numere. Vom explica şi implementa un algoritm care
primeşte la intrare un şir de caractere care conţine o expresie matematică validă, ce poate avea ca
operaţii +, -, *, /, iar ca operatori numai litere (deci variabilele sunt unilaterale) şi generează forma
poloneză postfixată.

PRINCIPIUL de generare a expresiei este următorul:


▪ se foloseşte o stivă S în care se depune rezultatul operaţiilor interne;
▪ se citeşte rând pe rând câte un caracter din intrare; dacă el este un operand (literă), va fi scris
în şirul de ieşire; dacă este operator, atunci apar două situaţii, şi anume: dacă există în vârful
stivei un operator cu prioritate mai mare decât operatorul curent atunci acela este scos din
stiva şi afişat, operaţia repetându-se până când sau stiva e goală sau operatorul din vârful
stivei e mai puţin prioritar decât cel curent; dacă nu există un operator în vârful stivei cu
prioritatea mai mare decât cel curent, operatorul curent va fi pus pe stivă;
▪ în finalul parcurgerii expresiei de intrare (nu mai sunt caractere în şirul de intrare), se va
“goli“ stiva, scriindu-se la ieşire operatorii de pe stivă, de la vârful stivei spre bază .

De exemplu să considerăm expresia E= A / B + C * D – F. Vom schiţa în tabloul următor, evoluţia


algoritmului prezentat anterior, surprinzând configuraţia stivei, a ieşirii şi a intrării:
Următorul Stiva Ieşirea Observaţie
caracter
A Vidă A
/ / A
B / AB
+ + AB/ priorit(/)>priorit(+)
C + AB/C
* +* AB/C
D +* AB/CD
- + - AB/CD* priorit(*)>priorit(-)
F + - AB/CD*F
Gol + AB/CD*F-
Gol Gol AB/CD*F- +

Deci, forma poloneză inversă expresiei date este E = A B / C D * F - +

5
Structuri de date
Pornind de la expresia E în forma poloneză inversă, vom arăta cum se calculează valoarea expresiei
în cazul în care cunoaştem valorile variabilelor care intervin. Algoritmul are la bază următorul
principiu:
• se porneşte cu expresia în forma poloneză inversă;
• se citesc token-urile din expresie şi se prelucrează astfel:
a) dacă este un operand, se introduce pe stivă;
b) dacă este un operator, se extrag din stivă operanzii B şi A şi se efectuează operaţia A op B,
rezultatul depunându-se pe stivă;
c) operaţia continuă până la epuizarea expresiei în forma poloneză inversă; rezultatul final se
află pe stivă; dacă expresia a fost scrisă corect, atunci stiva va conţine un singur element :
rezultatul;
Pentru a implementa algoritmul anterior vom scrie două clase ce ne vor ajuta şi anume:
• Clasa Token folosită pentru a putea parsa şiruri de caractere şi a identifica operatorii şi
operanzii;
• Clasa Parser pentru a putea prelua un vector de elemente de tip Token,a-l transforma din
formă poloneză infixată în formă poloneză postfixată şi a-l evalua pentru a afla valoarea
expresiei.
Iată codul clasei Token ce realizează o analiză lexicală a unei expresii aritmetice:

1 #include "stackasarray.h"//vom folosi stiva ca vector


2 #include <string.h>
3 #define MAX_LEN 20 // lungimea maximă a unui Token=20
4
5 class Token
6 {
7 protected:
8 char data[MAX_LEN];
9 public:
10 Token(const char s[])
11 {
12 strcpy(s, data);
13 }
14 Token(const char s)
15 {
16 data[0]=s;
17 data[1]='\0';
18 }
19 Token(Token const& t)
20 {
21 strcpy(t.data, data);
22 }
23

6
Structuri de date
24 unsigned int isOperator()
25 {
26 char strOperatori[]="+-*/";
27 if(strlen(data)==1)
28 {
29 char *s=strOperatori;
30 while(s)
31 if(*s++==data[0])
32 return 1;//este operator
33 }
34 return 0; //nu este operator
35 }
36 unsigned int isOperand()
37 {
38 for(unsigned int i=0; i<strlen(data);i++)
39 if((data[i]<'0')||(data[i]>'9'))
40 return 0;// nu este numar intreg valid
41 return 1;//este un numar intreg valid
42 }
43 int getPriority()
44 {
45 char strOperatori[]="+-*/";
46 if(isOperator())
47 for(unsigned int i=0; i<strlen(strOperatori);i++)
48 if(strOperatori[i]==data[0])
49 return i;
50 return -1; //nu are prioritate
51 }
52 unsigned int getValue()
53 {
54 if(!isOperand())
55 return 0;
56 unsigned int v=0;
57 for(unsigned int i=0; i<strlen(data);i++)
58 v=v*10+(data[i]-'0');
59 return v;
60 }
61 char* getType()
62 {
63 return data;
64 }
65 }; //sfârsitul clasei Token

Odată definită clasa Token, clasa Parser va conţine doi vectori de elemente de tip Token
corespunzători reprezentării expresiei de intrare în formă infixată (normală) şi postfixată. Clasa va
avea trei metode:
• pentru parsarea şirului de caractere şi obţinerea expresiei infixate vom folosi metoda
parse2InFix;

7
Structuri de date
• pentru transformarea expresiei din formă poloneză normală în formă poloneză posfixată vom
folosi metoda inFix2PostFix;
• pentru evaluarea formei poloneze postfixate vom folosi metoda evaluatePostFix.
Clasa Parser va fi scrisă în acelaşi fişier ca şi Token. Definiţia şi metodele clasei Parser sunt
descrise în continuare:
1 class Parser
2 {
3 private:
4 Array<Token> tokensInFix;
5 Array<Token> tokensPostFix;
6 public:
7 Parser(char* s)
8 {
9 parse2InFix(s);
10 }
11 void parse2InFix(char* s)
12 {
13 char* ptr=s;
14 char strOperator[]="+-*/";
15 while(ptr)
16 {
17 char strToken[MAX_LEN];
18 int k=0;
18 if(isdigit(*ptr)) // este un operand
20 while(isdigit(*ptr))
21 if(k>=MAX_LEN)
22 throw Error("Operand prea lung",
23 ERROR);
24 else
25 strToken[k++]=*ptr++;
26 else //este un operator
27 for(unsigned int i=0; i<strlen(strOperatori);
28 i++)
29 if(strOperatori[i]==*ptr)
30 strToken[k++]=*ptr++;
31 if(k==0)
32 throw Error("Expresie invalida", ERROR);
33 else
34 strToken[k]='\0';
35 Token tk(strToken);
36 tokensInFix=tokensInFix+tk;
37 }
38 }
39
40 void inFix2PostFix()
41 {
42 StackAsArray<Token> sa;
43 for(unsigned int i=0; i<tokensInFix.getLength();i++)

8
Structuri de date
44 if(tokensInFix[i].isOperand())
45 tokensPostFix=tokensPostFix+tokensInFix[i];
46 else
47 {
48 while((!sa.isEmpty())&&
49 (sa.getTop().getPriority() >
50 tokensInFix[i].getPriority()))
51 tokensPostFix=tokensPostFix+sa.pop();
52 sa.push(tokensInFix[i]);
53 }
54 while(!sa.isEmpty())
55 tokensPostFix=tokensPostFix+sa.pop();
56 }
57
58 double operate(double operand1, double operand2,
59 Token& operator3)
60 {
61 if(operator3.isOperator())
62 switch(*(operator.getType())) //primul caracter
63 //din operator
64 {
65 case '+': return operand1+operand2;
66 case '-': return operand1-operand2;
67 case '*': return operand1*operand2;
68 case '/': if(operand2==0.0)
69 throw Error("Impartire cu zero",
70 FATAL);
71 else
72 return operand1/operand2;
73 }
74 return 0;
75 }
76
77 double evaluatePostFix()
78 {
79 StackAsArray<double> sa;
80 for(unsigned int i=0;i<tokensPostFix.getLength();i++)
81 if(tokensPostFix[i].isOperator())
82 {
83 double B=sa.pop();
84 double A=sa.pop();
85 sa.push(operate(A,B,tokensPostFix[i]));
86 }
87 else
88 sa.push(tokensPostFix[i].getValue());
89 return sa.pop();
90 }
91 }; //sfârşitul clasei parser

9
Structuri de date
OBSERVAŢII
• A fost necesară o metodă operate care are ca parametrii două numere şi un Token şi
returnează rezultatul operaţiei respective între cei doi operanzi.
• Funcţiile scrise funcţionează numai în cazul în care folosim doar operaţiile +, -, *, /, iar
operatorii şi variabilele au numai o singură literă, respectiv o singură cifră.

Rezumând, putem scrie că etapele evaluării unei expresii aritmetice sunt următoarele:
1. Parsarea expresiei (identificarea operatorilor şi a operanzilor) şi aducerea ei în Formă Poloneză
Normală (infixată);
2. Transformarea din Formă Poloneză Normală (infixată) în Formă Poloneză Postfixată;
3. Evaluarea Formei Poloneze Postfixate pe baza algoritmului folosind stiva de operatori.

3. COADA

Coada este tot o listă, dar toate inserările se fac la un capăt, numit tail (spatele cozii) şi toate
ştergerile se fac la celalalt capăt, denumit head (capul listei).

a1 a2 ....... an

head tail

Vom scrie în continuare o clasă numită Queue defineşte funcţionalitatea unei cozi. Acestă clasă
derivă din Container şi este folosită doar pentru a specifica interfaţa (funcţionalitatea) care trebuie
oferită de coadă. La fel ca la stivă, nu vom avea obiecte de tip Queue ci vom deriva diverse
implementări de cozi din clasa Queue. Definiţia interfeţei Queue este următoarea:

1 // fisierul "queue.h"
2 #include "container.h"
3 #include "error.h"
4
5 template<class T>
6 class Queue:public virtual Container
7 {
8 public:
9 virtual T& getHead() const=0; //functie virtuala pura
10 virtual void enQueue(T const&)=0; //functie virtuala pura
11 virtual T& deQueue() =0; //functie virtuala pura
12 };

După cum se poate observa, interfaţa Queue derivă din Container deci moşteneşte toate metodele şi
membrii dată ai acesteia. Singurele metode noi sunt getHead, enQueue şi deQueue. Faptul că

10
Structuri de date
ele sunt declarate urmat de expresia “=0” înseamnă că sunt metode virtuale pure, adică nu vor fi
definite (implementate) în clasa Queue ci numai eventual în clasele derivate din aceasta.
La fel ca la stivă, vom defini în continuare implementarea cozii ca un vector unidimensional (Array)
de capacitate fixă, într-o clasă cu numele QueueAsArray:
1 //fişierul "queueasarray.h"
2 #include "queue.h"
3 #include "array.h"
4
5 template<class T>
6 class QueueAsArray:public Queue
7 {
8 protected:
9 Array<T> array;
10 unsigned int head, tail;
11
12 public:
13 QueueAsArray(unsigned int size):array(size),head(0),
14 tail(size-1), count(0){}
15 QueueAsArray(QueueAsArray<T> const& q):array(q.array),
16 head(q.head), tail(q.tail), count(q.count){}
17 ~QueueAsArray(){}
18
19 T& getHead() const
20 {
21 if(count==0)
22 throw Error("Coada vida", ERROR);
23 return array[head];
24 }
25
26 void enQueue(T const& t)
27 {
28 if(count==array.getLength())
29 throw Error("Coada plina", ERROR);
30 if(++tail==array.getLength())
31 tail=0; IMPORTANT
32 array[tail]=t;
Implementarea unei cozi presupune:
33 count++;  Un suport de memorare array;
34 }  Definirea operaţiilor primitive
35 enQueue şi deQueue.
36 T& deQueue()
37 {
38 if(count==0)
39 throw Error("Coada vida", ERROR);
40 T& result=array[head];
41 if(++head==array.getLength())
42 head=0;
43 --count;
44 return result;

11
Structuri de date
45 }
46
47 unsigned int isEmpty()
48 {
49 return (count==0);
50 }
51
52 unsigned int isFull()
53 {
54 return (count==array.getLength());
55 }
56 }; //sfârşitul clasei

DEFINIŢIE: Decoada (“Deque“) este o structură de date ce mixează proprietăţile stivei şi a cozii.

Cuvântul “deque” este o prescurtare de la “double ended queue “, adică o coadă cu două terminaţii.
Ea este o coadă în care elementele pot fi adăugate sau şterse la oricare din capete. Interfaţa unei
clase Deque care defineşte structura de date decoadă este prezentată în continuare:

1 //fişierul deque.h
2 #include "queue.h"
3
4 template<class T>
5 class Deque:public virtual Queue
6 {
7 public:
8 virtual T& getHead() const=0;
9 virtual T& getTail() const=0;
10 virtual void enQueue(T const &t)
11 {
12 return enQueueTail(t);
13 }
14
15 virtual void enQueueHead(T&)=0;
16 virtual void enQueueTail(T&)=0;
17
18 T& deQueue()
19 {
20 return deQueueHead();
21 }
22
23 virtual T& deQueueHead()=0;
24 virtual T& deQueueTail()=0;
25 }; //sfârşitul clasei Deque

12
Structuri de date

CURS 8

1. Alocarea dinamică. Pointeri


În tehnicile de programare există două metode clasice de alocare a memoriei:
 ALOCAREA STATICĂ. În cazul alocării variabilelor în programe, compilatorul cunoaşte în
momentul compilării necesarul de memorie corespunzător fiecărui tip de variabilă; de exemplu,
în limbajul C, secvenţa:

1 void main()
2 {
int a, b[10];
3
double c;
4 char d;
5 ....
6 }

va informa compilatorul că trebuie să aloce următorul spaţiu de memorie:


• sizeof(int) pentru a
• 10 * sizeof(int) pentru b
• sizeof(double) pentru c
• sizeof(char) pentru d
Valorile date de macro-ul sizeof sunt cunoscute de compilator şi diferă de la o platformă la
alta. De exemplu, pentru DOS şi Windows 3.x, sizeof(int) = 2 (doi octeţi) în timp ce
pentru Windows 9x, NT,2000 şi Linux, sizeof(int) = 4 (sisteme de operare pe 32 de biţi).
Astfel, în momentul rulării secvenţei de cod anterioare pe o platformă DOS, segmentul de cod ce
conţine aceste variabile automatice va arăta astfel:
a 1000 2 octeti = 16 biţi
b 1002 .
.
. 20 octeţi
.
.
c 1022 6 octeţi

d 1028 1 octet

Adrese de memorie
Figura 1 Harta memoriei la alocarea statică

1
Structuri de date
 ALOCAREA DINAMICĂ. Aceasta foloseşte aşa-numitele variabile "pointer" care conţin adrese ale
unor zone de memorie. De exemplu, în limbajul C considerăm următoarea secvenţă:
1 void main() 1000
a 0
2 { 1002
b
int a, *b, c[10]; ??? 1004
3 c
b = &a;
4 b = c;
5 }

Instrucţiunea b = &a; va pune în b valoarea 1000:


1000
a 0 1002
b
1000 1004
c
0

iar b = c; va pune în b valoarea 1004:

1000
a 0 1002
b
1004 1004
c

Aceste declaraţii permit ca variabila b să pointeze spre o zonă de memorie ce va fi alocată


ulterior compilării şi anume în faza de execuţie (run-time). De exemplu, pentru variabila b de
mai sus, putem aloca - folosind funcţia standard malloc - o zonă de memorie de mărime egală cu
2 octeţi, adică sizeof(int):

1 b = (int*)malloc(sizeof(int)); /* în C standard */
2 b = new int; // în C++
new(b); { în Pascal }
3

Funcţia C malloc (şi toate din gama *alloc) cere sistemului de operare o zonă de memorie de
mărime egală cu 2 octeţi din zona de memorie liberă. Dacă există memorie liberă, funcţia malloc
va returna adresa zonei de memorie alocată şi va marca această zonă ca fiind folosită pentru a evita
utilizarea ei la apeluri ulterioare ale lui malloc. Dacă nu există suficientă memorie liberă în
sistem, atunci funcţia returnează valoarea NULL (= 0). De aceea, fiecare apel al funcţiei de alocare
trebuie însoţit de testul dacă valoarea returnată este nenulă. Valoarea astfel alocată în timpul
execuţiei (run-time) se zice că este alocată "dinamic". O valoare alocată dinamic poate fi dealocată
ulterior prin apelul funcţiei free (ex. free(b)) care testează dacă b pointează spre o zonă de
memorie alocată dinamic şi în caz afirmativ adaugă zona de memorie respectivă la lista de memorie
liberă a sistemului.

2
Structuri de date
2. Lista simplu înlănţuită

Vom defini o clasă pentru lista simplu înlănţuită prin structura de date asociată ei şi o vom
implementa în limbajul de programare C++.

DEFINIŢIE: O listă de elemente de tip T este o secvenţă finită de elemente ale lui T impreună cu
operaţiile:
L1 - iniţializarea listei pe mulţimea vidă;
L2 - determinarea situaţiei de lista vidă;
L3 - determinarea situaţiei de lista plină;
L4 - determinarea lungimii listei;
L5 - accesarea oricărui nod dintr-o listă nevidă;
L6 - inserarea unui nod într-o poziţie oarecare;
L7 - modificarea valorii unui nod;
L8 - ştergerea unui nod din lista nevidă.
Elementul (nodul) unei liste este de forma:
data next

Structura de date de bază necesară în lucrul cu lista înlănţuită este clasa ListElement definită în
continuare iar mărimea elementului (nodului) unei liste va fi:
sizeof(ListElement)=sizeof(T) + sizeof (void *)
O listă simplu înlănţuită va corespunde unei clase LinkedList în care vom memora doi pointeri şi
anume head şi tail corespunzător primului şi ultimului element din listă, ca în Figura 2.:
[1]

data next data next ... data next

ListElement
[2] hea tail
d
LinkedList

Figura .2 Lista simplu înlănţuită


De exemplu, dacă T este tipul întreg, lista din

Figura 3
[3]
l &= 1004 &= 1000 &= 1008

5 -1 2

hea tail
dd 3
Structuri de date

Figura 3. Structura logică a listei

4
Structuri de date
va fi memorată astfel:
1000 -1
1002 1008
head 5
=1004
1006 1000
tail 2
=1008
100A 0=NUL
L
Adrese
de memorie
Figura 4. Memorarea listei din

Figura 3.

Iată în continuare clasa necesară definirii şi implementarea principalelor operaţii legate de lista
simplu înlănţuită:
1 //fişierul linkedlist.h
2 #include "error.h"
3 IMPORTANT
4 template<class T>
Orice listă înlănţuită
5 class LinkedList; //declaraţie anticipată
 are nevoie de alocarea memoriei
6 pentru fiecare nod ;
7 template<class T>  la terminare trebuie ştearsă prin
8 class ListElement dealocarea succesivă a memoriei
anterior alocate.
9 {
10 T data;
11 ListElement* next;
12
13 public:
14 ListElement(T const& _data, ListElement<T>* _next):
15 data(_data), next(_next){}
16
17 T const& getData()const
18 {
19 return data;
20 }
21
22 ListElement *getNext()const
23 {
24 return next;
25 }
26
27 friend LinkedList<T>; // clasa LinkedList are acces la
28 // membrii privaţi ai clasei ListElement
39 };//sfârşitul clasei ListElement
30

5
Structuri de date
31 template<class T>
32 class LinkedList
33 {
34 ListElement<T>* head;
35 ListElement<T>* tail;
36
37 public:
38 LinkedList():head(0),tail(0){}
39
40 void Purge()
41 {
42 while(head)
43 {
44 ListElement<T>* tmp=head;
45 head=head->next;
46 delete tmp;
47 }
48 tail=0;
49 }
50
51 ~LinkedList()
52 {
53 Purge();
54 }
55
56 ListElement<T> *getHead()const
57 {
58 return head;
59 }
60
61 ListElement<T> *getTail()const
62 {
63 return tail;
64 }
65
66 unsigned int isEmpty()const
67 {
68 return (head==0);
69 }
70
71 void addAtHead(T const& item)
72 {
73 ListElement<T>* tmp=new ListElement<T>(item, head);
74 if(head==0)
75 tail=tmp;
76 head=tmp;
77 }
78
79 void addAtTail(T const& item)
80 {
81 ListElement<T>* tmp=new ListElement<T>(item, 0);

6
Structuri de date
82 if(head==0)
83 head=tmp;
84 else
85 tail->next=tmp;
86 tail=tmp;
87 }
88
89 T const& getFirst()const
90 {
91 if(head==0)
92 throw Error("Lista vida", ERROR);
93 return head->data;
94 }
95
96 T const& getLast()const
97 {
98 if(tail==0)
99 throw Error("Lista vida", ERROR);
100 return tail->data;
101 }
102
103 LinkedList(LinkedList<T> const& lList):head(0), tail(0)
104 {
105 ListElement<T> const *ptr;
106 for(ptr=lList.head;ptr!=0;ptr=ptr->next)
107 addAtTail(ptr->data);
108 }
109
110 LinkedList<T>& operator=(LinkedList<T> const& lList)
111 {
112 if(&lList!=this)
113 {
114 Purge();
115 ListElement<T> const *ptr;
116 for(ptr=lList.head;ptr!=0;ptr=ptr->next)
117 addAtTail(ptr->data);
118 }
119 return *this;
120 }
121
122 T& removeFromHead()
123 {
124 if(head==0)
125 throw Error("Lista vida", ERROR);
126 T retVal=head->data;
127 ListElement<T>* tmp=head;
128 if(head==tail)
129 tail=0;
130 head=head->next;
131 delete tmp;
132 return retVal;

7
Structuri de date
133 }
134
135 T& removeFromTail()
136 {
137 if(tail==0)
138 throw Error("Lista vida", ERROR);
139 T retVal=tail->data;
140 ListElement<T>* tmp=head;
141 if(head==tail)
142 {
143 tail=0;
144 head=0;
145 }
146 else
147 {
148 while(tmp->next!=tail)
149 tmp=tmp->next;
150 tail=tmp;
151 tmp=tail->next;
152 tail->next=0;
153 }
154 delete tmp;
155 return retVal;
156 }
157
158 void extract(T const& item)
159 {
160 ListElement<T>* ptr=head;
161 ListElement<T>* prevPtr=0;
162 while((ptr!=0)&&(ptr->data!=item))
163 {
164 prevPtr=ptr;
165 ptr=ptr->next;
166 }
167 if(ptr==0)
168 throw Error("Valoare negasita", ERROR);
169 if(ptr==head)
170 head=ptr->next;
171 else
172 prevPtr->next=ptr->next;
173 if(ptr==tail)
174 tail=prevPtr;
175 delete ptr;
176 }
177
178 void insertAfter(ListElement<T> const *arg, T const& item)
179 {
180 ListElement<T> *ptr=((ListElement<T>)*)arg;
181 if(ptr==0)
182 throw Error("Pozitie invalida in lista", ERROR);
183 ListElement<T> *tmp=new ListElement<T>(item, ptr->next);

8
Structuri de date
184 ptr->next=tmp;
185 if(tail==ptr)
186 tail=tmp;
187 }
188
189 void insertBefore(ListElement<T> const *arg, T const& item)
190 {
191 ListElement<T> *ptr=((ListElement<T>)*)arg;
192 if(ptr==0)
193 throw Error("Pozitie invalida in lista", ERROR);
194 ListElement<T> *tmp=new ListElement<T>(item, ptr);
195 if(head==ptr)
196 head=tmp;
197 else
198 {
199 ListElement<T>* prevPtr=head;
200 while((prevPtr!=0)&&(prevPtr->next!=ptr))
201 prevPtr=prevPtr->next;
202 if(prevPtr==0)
203 throw Error("Pozitie invalida in lista",
204 ERROR);
205 prevPtr->next=tmp;
206 }
207 }
208
209 T& removeAfter(ListElement<T>const *arg)
210 {
211 if(arg==0)
212 throw Error("Lista vida", ERROR);
213 if(arg->next==0)
214 throw Error("Ultim element", ERROR);
215 ListElement<T>* ptr=arg->next;
216 arg->next=ptr->next;
217 if(tail==ptr)
218 tail=arg;
219 T retVal=ptr->data;
220 delete ptr;
221 return retVal;
222 }
223 };//sfârşitul clasei LinkedList

Aşa cum am văzut la vectorul unidimensional, am avut nevoie de o variantă de listă sortată motiv
pentru care şi la lista simplu înlănţuită vom implementa o clasă care menţine sortate valorile
(presupuse comparabile) din nodurile listei.
Clasa care defineşte această listă este SortedListAsLinkedList şi spre deosebire de
SortedArray ea nu derivează din clasa LinkedList deoarece în clasa LinkedList sunt
metode care ar permite inserarea valorilor (de ex addAtHead şi addAtTail) fără a respecta o

9
Structuri de date
ordine între elemente. Pentru a compensa dezavantajul acesta vom folosi un membru privat al clasei
de tip LinkedList. Cu acestea, iată cum arată clasa SortedListAsLinkedList:
1 // fişierul SortedListAsLinkedList.h
2 #include "stack.h"
3 #include "linkedlist.h"
4
5 template<class T>
6 class SortedListAsLinkedList:public Container
7 {
8 LinkedList<T> list;
9
10 public:
11 SortedListAsLinkedList():list(),count(0){}
12
13 SortedListAsLinkedList& addValue(T const& t)
14 {
15 ListElement<T> *prevPtr=0;
16 ListElement<T> *ptr=list.getHead();
17 while((ptr!=0)&&(ptr->getData()<t))
18 {
19 prevPtr=ptr;
20 ptr=ptr->getNext();
21 }
22 if(prevPtr==0)
23 list.addAtHead(t);
24 else
25 list.insertAfter(prevPtr, t);
26 ++count;
27 }
28
29 ListElement<T>* findValue(T const& val)
30 {
31 ListElement<T> *ptr=list.getHead();
32 while((ptr!=0)&&(ptr->getData()!=val))
33 ptr=ptr->getNext();
34 return ptr;
35 }
36
37 SortedListAsLinkedList& removeValue(T const& val)
38 {
39 ListElement<T> *prevPtr=0;
40 ListElement<T> *ptr=list.getHead();
41 if(findValue(val)==0)//nu s-a gasit valoarea
42 return *this;
43 while((ptr!=0)&&(ptr->getData()<val))
44 {
45 prevPtr=ptr;
46 ptr=ptr->getNext();
47 }

10
Structuri de date
48 if(prevPtr==0)
49 list.removeFromHead();
50 else
51 list.removeAfter(prevPtr);
52 --count;
53 return *this;
54 }
55 };//sfârşitul clasei SortedListAsLinkedList

3. Stiva şi coada implementate folosind lista simplu înlănţuită

Vom scrie în continuare o clasă C++ care foloseşte interfaţa Stack şi clasa LinkedList pentru a
implementa o stivă bazată pe lista simplu înlănţuită. Principiul care stă la baza stivei memorate ca
listă simplu înlănţuită este acela că în loc de Array vom avea acum un container nou şi anume
LinkedList. Iată cum arată aceasta:

1 // fişierul stackaslinkedlist.h
2 #include "stack.h"
3 #include "linkedlist.h"
4
5 template<class T>
6 class StackAsLinkedList:public Stack
7 {
8 LinkedList<T> list;
9
10 public:
11 StackAsLinkedList():list(),count(0){}
12
13 void Purge()
14 {
15 list.Purge();
16 count=0;
17 }
18 ~StackAsLinkedList()
19 {
20 Purge();
21 }
22
23 void push(T const& t)
24 {
25 list.addAtHead(t);
26 ++count;
27 }
28
29 T& pop()
30 {
31 if(count==0)
32 throw Error("Stiva goala", ERROR);

11
Structuri de date
33 T& result=*(list.getFirst());
34 list.removeFromHead();
35 count--;
36 return result;
37 }
38 T& getTop()const
39 {
40 if(count==0)
41 throw Error("Stiva goala", ERROR);
42 return *(list.getFirst());
43 }
44 };//sfârşitul clasei

Folosirea unei stive devine astfel extrem de uşoară în programe aşa cum o arată şi fragmentul de
cod următor:

1 #include “stackaslinkedlist.h”
2 // .....
3 StackAsLinkedList<int> st;
4 st.push(4);
5 st.push(7);
6 cout<<st.pop();
7 // .....

Bazându-ne pe aceeaşi idee (memorarea elementelor folosind o listă simplu înlănţuită) vom căuta să
vedem cum arată implementarea unei structuri de date de tip coadă ca listă simplu înlănţuită.
Operaţiile asupra cozii vor fi cele de adăugare a unui element după ultimul element şi extragerea
elementului din capul cozii.

head

(şters)

Tail Introdus
Figura 4. Operaţiile pe coadă
1 //fişierul queueaslinkedlist.h
2 #include "queue.h"
3 #include "linkedlist.h"
4
5 template<class T>
6 class QueueAsLinkedList:public virtual Queue
7 {
8 protected:
9 LinkedList<T> list;
10
11 public:
12 QueueAsLinkedList():list(){}
13
14 void Purge()
15 {

12
Structuri de date
16 list.Purge();
17 count=0;
18 }
19
20 ~QueueAsLinkedList()
21 {
22 Purge();
23 }
24
25 T& getHead()const
26 {
27 if(count==0)
28 throw Error("Coada vida", ERROR);
29 return *list.getFirst();
30 }
31
32 void enQueue(T& const t)
33 {
34 list.addAtTail(t);
35 ++count;
36 }
37
38 T& deQueue()
39 {
40 if(count==0)
41 throw Error("Coada vida", ERROR);
42 T& result=*list.getFirst();
43 list.removeFromHead();
44 --count;
45 return result;
46 }
47 };//sfârşitul clasei QueueAsLinkedList

OBSERVAŢIE
Atât parcurgerea cât şi eliberarea memoriei au ca funcţie de operaţii ordinul O(n) unde n este
numărul elementelor din listă.

13
Structuri de date

CURS 9

1. Aplicaţiile listei simplu înlănţuite la aritmetica polinomială


Vom specifica în continuare un proiect de aritmetică polinomială folosind lista simplu înlănţuită ca
suport pentru memorarea polinoamelor de o singură variabilă. Vom utiliza ca informaţie în nod
coeficientul şi gradul termenului cu coeficientul nenul într-un polinom, toate acestea memorate în
ordinea descrescătoare a gradului.
De exemplu, fie polinomul de gradul 4:
P(X) = 5X4 + 2X2 - 8X + 1
Memorarea lui sub formă de listă înlănţuită este redată în figura următoare:

Figura 1. Memorarea
polinoamelor de o variabilă
P 2 2 -8 1 1 0 folosind lista simplu înlănţuită
5 4

Tipul de dată necesar memorării unui element al listei este clasa Term scrisă la implementarea
polinoamelor sub formă de vector unidimensional:

1 template<class T>
2 class Term
3 {
4 public:
5 T coef;
6 unsigned int grd;
7 // . . . . . .
8 };// sfârşitul clasei

OBSERVAŢIE
Există şi o altă variantă a implementării polinoamelor de o singură variabilă în care primul element
este doar un "tag" (reper) care nu poartă informaţie utilă iar ultimul element pointează spre primul.
De exemplu, pentru polinomul
Q(X) = 2X2 - 8X + 1
schema de memorare a lui este următoarea:

Q
2 2 -8 1 1 0

Figura 2. Memorarea polinoamelor de o variabilă folosind lista circulară

1
Structuri de date
2. Lista generalizată
O variantă mai complicată a listei simplu înlănţuite este lista generalizată, folosită de exemplu
pentru memorarea polinoamelor de mai multe variabile. Astfel, fiecare polinom are un nod "header"
care poartă informaţia referitoare la variabilă.
Să luăm de exemplu polinomul
P(x,y) = x*y = 1*x*P(y)
cu
P(y)=1*y
El poate fi memorat astfel:

P
0 X 1 1

Antet polinom 0 Y 1 1
în X

Antet polinom
în Y

Figura 3. Memorarea polinomului P(x,y)=x y


În general, un polinom P(x1, ..., xn) se scrie sub forma:
m
P(X1 , ..., X n ) =  a X P (X
i=0
i
i
1 i 2 , ..., X n )

iar reprezentarea sub formă de listă înlănţuită a lui P o vom defini recursiv, în funcţie de
reprezentarea lui Pi , astfel:
1. Polinomul nul P(X) = 0 :

0 X
P

2. Polinomul de o singură variabilă


m
P(X) = a
k =0
ik X
ik
, cu i1  i2  ...  im = 0

se reprezintă sub forma:


P
0 X ai i1 ai2 i2 ain 0
1

2
Structuri de date
3. Dacă Pi ( X 2 ,..) este reprezentarea lui Pi ( X 2 ,..) atunci reprezentarea lui
m
P(X1 , ..., X n ) = a X
i=0
i
i
1 Pi ( X 2 , ..., X n ) este

0 X am m a0 0

Pm ( X 2 ,..) P0 ( X 2 ,..)

unde Pi ( X 2 ,..) = dacă Pi ( X 2 ,..) = 1.

De exemplu, polinomul:
P(X, Y, Z) = 3X 2 (2Y 2 + Z) + 2XYZ + 6
se reprezintă într-o listă pe 5 nivele de adâncime.
Structura de date necesară memorării listei generalizate se poate scrie în limbajul C astfel:

1 template<class T>
2 class GList
3 {
4 public:
5 enum {ATOM, LIST} kind;
T value;
6 GList *down, *next;
// . . . . . .
7
};// sfârşitul clasei GList
8

kind value down next

3. Lista dublu înlănţuită

Aşa cum am văzut până acum, listele simplu înlănţuite conţin două câmpuri şi anume:
• câmpul de informaţie;
• câmpul de legătură.

Deci, există o legătură unidirecţională între elementele listei, fiecare element purtând informaţia de
legătură numai spre vecinul din dreapta. Această variantă oferă un singur mod de traversare a listei,
într-o singură direcţie.
O alternativă la lista simplu înlănţuită o constituie lista dublu înlănţuită în care fiecare nod conţine
trei câmpuri:
• câmpul de informaţie;

3
Structuri de date
• câmpul de legătură (pointer) spre următorul element din listă;
• câmpul de legătură (pointer) spre elementul precedent din listă.

Se convine ca şi în cazul listelor simplu înlănţuite, pentru ultimul nod, câmpul de legătură spre
următorul nod să fie NULL iar pentru primul nod, câmpul de legătură spre nodul precedent să fie
NULL. Un nod din lista dublu înlănţuită poate fi reprezentat astfel:

prev data next


iar o listă dublu înlănţuită L = (1, 3, 2, 7) arată astfel:

1 3 2 7

Figura 4. Un exemplu de memorare a listei dublu înlănţuite


Nodul din lista dublu înlănţuită poate fi descris în limbaj C++ cu ajutorul unei clase sub forma:

1 template<class T>
2 class DListElement
3 {
4 T data;
5 DListElement *prev, *next;
6 //.. ..
7 };

Cum scopul listei dublu înlănţuite este de a putea fi traversată în ambele sensuri, înseamnă că
trebuie să reţinem câte un pointer atât spre capul cât şi spre coada listei. În acest sens, clasa necesară
lucrului cu lista dublu înlănţuită este DoubleLinkedList şi are ca structură (vezi

Figura ) două variabile de tip pointer la DListElement:

1 template<class T>
2 class DoubleLinkedList
3 {
4 DListElement *head, *tail;
5 //.. ..
6 };

4
Structuri de date

head tail

1 3 2 7

Figura 5. Memorarea unei liste dublu înlănţuite


Clasa C++ necesară implementării lucrului cu lista dublu înlănţuită şi definiţiile principalelor
operaţii sunt redate în continuare:
//fişierul doublelinkedlist.h
#include "error.h"
template<class T>
class DoubleLinkedList; //declaraţie anticipată

template<class T>
class DListElement
{
T data;
DListElement *next;
DListElement *prev;

public:

DListElement(T const& _data, DListElement<T>* _prev,


DListElement<T>* _next):data(_data), next(_next),
prev(_prev){}

T const& getData() const


{
return data;
}

DListElement* getNext() const


{
return next;
}

DListElement* getPrev() const


{
return prev;
}

friend class DoubleLinkedList; //clasă prieten


};

template<class T>
class DoubleLinkedList
{
DListElement<T>* head;
DListElement<T>* tail;

public:

5
Structuri de date
DoubleLinkedList():head(0),tail(0){}

void Purge()
{
while(head)
{
DListElement<T>* tmp=head;
head=head->next;
delete tmp;
}
tail=0;
}

~DoubleLinkedList()
{
Purge();
}

DListElement<T> const* getHead()const


{
return head;
}

DListElement<T> const* getTail()const


{
return tail;
}

unsigned int isEmpty()const


{
return (head==0);
}

T const& getLast()const
{
if(tail==0)
throw Error("Lista vida", ERROR);
return tail->data;
}

T const& getFirst()const
{
if(head==0)
throw Error("Lista vida", ERROR);
return head->data;
}

DoubleLinkedList(DoubleLinkedList<T> const dList):head(0),


tail(0)
{
DListElement<T> const* ptr;
for(ptr=dList.head; ptr!=0; ptr=ptr->next)
addAtTail(ptr->data);
} dl
head tail

Curren
t

4 1 3 2 7

6
dn
Structuri de date

void addAtHead(T const& item)


{
DListElement<T>* tmp=new DListElement<T>(item, 0, head);
if(head==0)
tail=tmp;
else
head->prev=tmp;
head=tmp; dl
} head tail

1 3 2 7 4

void addAtTail(T const& item)


{
DListElement<T>* tmp=new DListElement<T>(item, tail, 0);
if(head==0)
head=tmp;
else
tail->next=tmp;
tail=tmp;
}

void addBefore(DListeElement<T> const *arg, T const& item)


{
DListElement<T>* ptr= (DListElement<T>*)arg;
if(ptr==0)
throw Error("Pozitie invalida", ERROR);
DListElement<T>* tmp=new DListElement<T>(item, arg->prev,arg);
if(head==ptr)
{
head=tmp;
ptr->prev=tmp;
}
else
{
DListElement<T>* prevPtr=head;
while((prevPtr!=0)&&(prevPtr->next!=ptr))
prevPtr=prevPtr->next;
if(prevPtr==0)
throw Error("Pozitie invalida", ERROR);
prevPtr->next=tmp;
}
} dl
head tail

1 3 2 7 4

ar 8
g
void addAfter(DListElement<T> const *arg, T const& item)
{

7
Structuri de date
DListElement<T>* ptr=((DListElement<T>)*)arg;
if(ptr==0)
throw Error("Pozitie invalida", ERROR);
DListElement<T> * temp=new DListElement<T>(item, ptr, ptr->next);
ptr->next=tmp;
if(tail==ptr)
tail=tmp;
}

T& removeAtHead()
{
if(head==0)
throw Error("Lista vida", ERROR);
DListElement<T> *ptr=head;
head=head->next; head tail
if(head==0)
tail=0;
else
head->prev=0;
T result=ptr->data;
delete ptr;
return result; 1 3 2 7
}

T removeAtTail()
{
if(tail==0)
throw Error("Lista vida", ERROR);
DListElement<T> *ptr=tail;
if(tail==head)//un singur element
tail=head=0; dl
else head tail
{
tail=tail->prev;
tail->next=0;
}
T result=ptr->data;
delete ptr; 1 3 2 7
return result;
}
};//sfârşitul clasei DoubleLinkedList

Lista dublu înlănţuită este utilizată în programare şi pentru memorarea matricilor rare. Structura de
date nu este o structură tipică de listă înlănţuită deoarece fiecare nod este parte a două liste şi
anume:
• lista elementelor nenule de pe aceeaşi coloană cu elementul dat;
• lista elementelor nenule de pe aceeaşi linie cu elementul dat.

Listele amintite anterior (pentru fiecare linie, respectiv pentru fiecare coloană care are elemente
nenule) sunt circulare şi au câte un nod suplimentar numit nod de antet. Fiecare nod va avea astfel
câte un pointer spre următorul element din lista coloanei sale şi câte un pointer spre următorul
element din lista liniei sale. Pentru exemplificare, să considerăm matricea rară A cu reprezentarea
în fig 7:

8
Structuri de date
0 1 0 6 0
0 0 0 1 0 

4 0 9 0 0
A= 
0 0 0 0 0
0 13 0 2 0
 
0 0 0 0 1
Vom explica în câteva cuvinte modul de memorare a matricelor rare prezentat anterior.
• lista cea mai din stânga este header-ul de linii; fiecare nod conţine următoarele câmpuri:
a) numărul liniei;
b) numărul coloanei = 0;
c) numărul de elemente nenule de pe linia respectivă;
d) pointer spre elementul de jos;
e) pointer spre lista elementelor de pe linia respectivă.
• lista cea mai de sus este header-ul de coloane; fiecare nod conţine următoarele câmpuri:
a) numărul liniei = 0;
b) numărul coloanei;
c) numărul de elemente nenule de pe coloana respectivă;
d) pointer spre lista elementelor nenule din coloană;
e) pointer spre următorul element din header-ul de coloane.
Celelalte elemente ale listei conţin şi ele 5 câmpuri şi anume:
• numărul liniei;
• numărul coloanei;
• valoarea (<> 0);
• pointer spre următorul element din lista de coloană;
• pointer spre următorul element din lista de linie.
Deci, fiecare element este "ancorat" în două liste circulare.
Un element numit "tag" (reper) este colţul din stânga sus care conţine următoarele informaţii:
• numărul liniei = 0;
• numărul coloanei = 0;
• câmp neutilizat;
• pointer spre primul element din header-ul de linie;
• pointer spre primul element din header-ul de coloană.
Structura de date ce implementează un astfel de nod va fi:

1 template<class T>
2 class SparseMatrix
3 {

9
Structuri de date
4 int lin, col;
5 T val;
6 SparseMatrix<T> *down, *next;
7 // . . . .
8 };

0 0 0 1 1 0 2 2 0 3 1 0 4 3 0 5 1

1 2 1 1 4 6
1 0 2

2 4 1
2 0 1

3 0 2 3 1 4 3 3 9

5 0 2 5 2 1 5 4 2
3

6 0 1 6 5 1

Figura 7. Reprezentarea unei matrici rare folosind liste înlănţuite


0 1 0 6 0 
0 0 0 1 0 
 
4 0 9 0 0 
A= 
0 0 0 0 0 
0 13 0 2 0 
 
0 0 0 0 1

10
Structuri de date

CURS 10

1. Noţiunea de arbore

Arborele ca structură de date a apărut ca o consecinţă a datelor inter-relaţionate şi organizate


ierarhic. O posibilă problemă care conduce în mod logic la un arbore este cea a arborelui
genealogic. Figura 1 arată o posibilă organizare a descendenţei genealogice:

Dan rădăcină(root)

Mihai Flavia Monica

Tudor Maria Mirce Elena Iulia Victoria Valeri


a a
Figura 1. Arborele
genealogic
De aici se poate vedea -în mare- care sunt trăsăturile unui arbore.
DEFINIŢIE: Arborele este o mulţime finită de unul sau mai multe noduri cu proprietăţile:
i) există un nod special denumit rădăcină ;
ii) nodurile rămase sunt partiţionate în n  0 mulţimi disjuncte T1,..,Tn , fiecare din aceste
mulţimi la rândul ei fiind un arbore. T1,..,Tn sunt numiţi şi subarbori ai rădăcinii.

OBSERVAŢIE Din definiţia recursivă de mai sus se poate observa că am introdus noţiunea de
subarbore, strâns legată de cea de arbore. De fapt, subarborele este un arbore în care rădăcina este
la rândul ei fiul cuiva, adică există cel puţin un nod deasupra rădăcinii, numit părintele ei.
După această definiţie şi din figura 1 putem să evidenţiem câteva noţiuni strâns legate de existenţa
unui arbore şi anume:
a) nodul rădăcină este nodul care nu este fiul nici unui nod;
b) fiecare alt nod din arbore este fiul unui singur nod, nod numit şi părinte; în Fig 1. nodul etichetat
“Dan” este nod rădăcină cu fiii “Mihai”, ”Flavia” şi “Monica”. Pentru “Mircea”, nodul "Mihai"
este nod părinte;
c) dacă există n  N * , astfel încât oricare nod are cel puţin n fii, atunci arborele se numeşte arbore
n-ar; pentru n = 1, obţinem o listă simplu înlănţuită, pentru n=2 obţinem arbore binar, pentru

1
Structuri de date
n=3 obţinem arbore ternar, etc. (vezi figura 2); numărul de fii ai unui nod se numeşte gradul
nodului (engl. degree).
d) fiecare arbore este compus din noduri (engl. vertex) şi arce, care unesc nodurile între ele; la
rândul lor, nodurile se împart în:
- noduri neterminale (interioare) care au cel puţin un fiu;
- noduri terminale (frunze) care nu au nici un fiu.
În figura 2.a nodurile A,B, şi E sunt neterminale iar C este o frunză.
e) arborele se poate structura pe nivele, după cum urmează:
- rădăcina se află pe primul nivel;
- dacă un nod se află pe nivelul i, atunci fiii săi se află pe nivelul i + 1 .
Astfel, în figura 2.c nodul C se află pe nivelul 1, nodurile D şi A se află pe nivelul 2,
S,P,O,M,I pe nivelul 3, şi T,L,K,U,S,Z se află pe nivelul 4.
f) nodurile fii ale aceluiaşi nod se numesc noduri gemene (engl. siblings); de exemplu în figura 2.b
nodurile 5 şi 3 sunt gemene deoarece sunt fiii nodului 4.

A 1 C
noduri
neterminale 4 1 D A
B
5 3 4 S P O M I
E T L K U G Z
2 3 1 2

C frunză b) Arbore binar c) Arbore ternar

a) Arbore unar (listă


simplu înlănţuită) Figura 2. Diferite tipuri de arbore

Un alt mod de reprezentare a unui arbore este cel folosind liste. Astfel, arborele din figura 2.c poate
fi reprezentat astfel:
(C(D(S(T)(L)(K))(P(U))(O))(A(M(G)(Z))(I)))
Regula de construcţie a listei este următoarea:
i) lista L corespunzătoare arborelui format dintr-un singur nod A este L = (A);
ii) pentru arborele T cu rădăcina a şi subarborii T1,..,Tn reprezentarea este:
LT = (a LT1 LT2 … LTn)
unde LTi este reprezentarea arborelui Ti.
Se observă că şi această definiţie este recursivă, ca de altfel şi definiţia arborelui.
O altă noţiune legată de arbore ( vezi Knuth), este cea de pădure (engl. forest).

DEFINIŢIE: Pădurea este o mulţime n  0 arbori disjuncţi.


2
Structuri de date
Implementarea arborelui sub forma unei structuri de date depinde de tipul arborelui. Dacă se ştie
despre arbore că este n-ar, atunci un nod va conţine câmpul de informaţie (data) şi n câmpuri de
legătură spre cei "cel mult n" fii , legături ce vor fi puse pe NULL dacă nu există fiul respectiv:

Data Link1 Link2 ............ ............ Linkn

Arborele din Fig 2.b va folosi pentru fiecare nod o structură de forma:

Data Left Right

iar reprezentarea lui va fi :


1

4 1

5 3 4

2 3 1 2

Figura 3. Reprezentarea arborelui din fig. 2.b


Structura în limbajul C++ care implementează arborele anterior este:
1 template<class T>
2 class BTree:public virtual Container
3 {
4 protected:
5 T data;
6 BTree<T> *left;
7 BTree<T> *right;
8 ………..
9 };
Dacă nu se ştie de la început că arborele este n-ar, atunci arborele se codifică astfel:
nodul conţine data (informaţia) şi o listă înlănţuită de pointeri spre fii.

Data Link

... .

Data Link

Pentru arborele N-ar structura de date C++ corespunzătoare este:


1 //fisierul narytree.h
2 #include "error.h"
3 #include "container.h"
4 #include "linkedlist.h"
5
6 template<class T>
7 class NaryTree:public virtual Container
8 {
9 protected:
10 T data;
11 LinkedList<NaryTree*> list;

3
Structuri de date
12 public:
13 NaryTree(T const& t):data(t), list(){}
14
15 ~NaryTree()
16 {
17 list.Purge();
18 }
19
20 T& getData()const
21 {
22 return data;
23 }
24
25 unsigned int getNumSubTree() const
26 {
27 unsigned int numSubTree=0;
28 ListElement<NaryTree*> *ptr=list.getHead();
29 while(ptr!=0)
30 {
31 ptr=ptr->getNext();
32 numSubTree++;
33 }
34 return numSubTree;
35 }
36
37 NaryTree& getSubTree(unsigned int numSubTree) const
38 {
39 unsigned int j=0;
40 ListElement<NaryTree*> *ptr=list.getHead();
41 while((j<numSubTree)&&(ptr!=0))
42 {
43 ++j;
44 ptr=ptr->getNext();
45 }
46 if(ptr==0)
47 throw Error("Indice eronat", ERROR);
48 return *ptr->getData();
49 }
50
51 void attachSubTree(NaryTree& t)
52 {
53 list.addAtTail(&t);
54 count+=t.getCount();
55 }
56
57 NaryTree& detachSubTree(unsigned int numSubTree)
58 {
59 NaryTree& subTree;
60 if(numSubTree<getNumSubTree())
61 {
62 subTree=getSubTree(numSubTree);
63 count-=subTree.getCount();
64 list.Extract(&subTree);
65 }
66 else
67 throw Error("Indice eronat", ERROR);
68 return subTree;
69 }
70 };//sfarsitul clasei NaryTree

4
Structuri de date
2. Arborele binar
Arborele binar este un tip particular de arbore care apare foarte des in algoritmică. El este
caracterizat de faptul că fiecare nod are cel mult doi fii. Definiţia ca structură de date a arborelui
este următoarea:

DEFINIŢIE: Arborele binar este o mulţime finită de noduri care este sau vidă, sau are un nod numit
rădăcină, iar restul de noduri sunt grupate în doi subarbori numiţi subarborele stâng şi
subarborele drept (left &right).

Din punct de vedere axiomatic, arborele binar poate fi definit astfel:

SDBTREE={D, d, F, A}
cu:
D={T, BTREE, BOOLEAN}
d=BTREE

F conţine următoarele funcţii:


▪ CREATE()→ BTREE
▪ ISEMPTY(BTREE)→ BOOLEAN
▪ BTREE(BTREE, T, BTREE)→ BTREE
▪ LEFT(BTREE)→ BTREE
▪ DATA(BTREE)→ T
▪ RIGHT(BTREE)→ BTREE

A conţine următoarele axiome:


() l, r  BTREE, d  T 
• ISEMPTY(CREATE())::=TRUE
• ISEMPTYBT(BTREE(l,d,r))::=FALSE
• LEFT(BTREE(l,d,r))::=l
• LEFT(CREATE())::=ERROR
• DATA(BTREE(l,d,r))::=d
• DATA(CREATE())::=ERROR
• RIGHT(BTREE(l,d,r))::=r
• RIGHT(CREATE())::=ERROR

În figura 4 sunt arătaţi câţiva arbori binari:


A
A A A A
B C
B B B
D E
C
D
a) b) c) d) e)
Figura 4. Diferiţi arbori binari

5
Structuri de date
În figura 4.a se poate vedea un caz particular de arbore binar şi anume arborele “desfăcut” (engl.
skewed) sau întins, care are aceeaşi funcţionalitate ca o listă simplu înlănţuită.
Un alt tip de arbore binar este cel din figura 4.e, şi anume un arbore binar “echilibrat”, denumit aşa
deoarece nodurile sale terminale (frunzele) se află pe ultimul sau ultimele două niveluri.

OBSERVAŢIE: Arborii din figurile 5.4.b, 5.4.c şi 5.4.d sunt, de asemenea, “echilibraţi”.

Arborii binari pot fi caracterizaţi de relaţiile între nodurile terminale şi neterminale după cum
urmează:

LEMA 1. i) Numărul maxim de noduri de pe nivelul i într-un arbore binar este 2i −1, i  1.

ii) Numărul maxim de noduri într-un arbore binar de adâncime k este 2k − 1, k  1 .

DEMONSTRAŢIE:
i) se face prin inducţie după n şi o lăsăm ca exerciţiu;
ii) folosind i), numărul maxim de noduri într-un arbore binar de adâncime k va fi:
k

2
i =1
i −1
= 2k − 1, q.e.d.

LEMA 2. Pentru orice arbore binar nevid T, dacă notăm cu n0 numărul nodurilor terminale şi cu n2
numărul nodurilor de grad 2, atunci n0 = n2 + 1.
DEMONSTRAŢIE: Fie n1 = numărul nodurilor de grad 1. Atunci, dacă n este numărul total de
noduri, cum arborele este binar, vom avea n = n0 + n1 + n2. (1).
Dacă numărăm arcele într-un arbore binar, vedem că fiecare nod, cu excepţia rădăcinii, are un arc
ce intră în el, deci n = A + 1, unde A este numărul total de arce din arbore (2). Dar, fiecare arc
provine dintr-un nod ce poate avea gradul 1 sau 2, deci A = n1 + 2n2 (3).
Din (2) şi (3) obţinem că n = n1+2n2 + 1, care, combinată cu (1) ne dă

n0 + n1 + n2 = n1 + 2n2 + 1

de unde obţinem
n0 = n2 + 1. q.e.d

LEMA 3. Dacă T este un arbore k-ar cu n noduri fiecare de mărime fixă, atunci n(k - 1) + 1 dintre
câmpurile de legătură sunt nule.

DEMONSTRAŢIE: Cum fiecare câmp de legătură nenul pointează spre un nod, iar exact o legătură
pointează spre fiecare nod cu excepţia rădăcinii, atunci numărul de legături nenule este de n - 1.
6
Structuri de date
Numărul total al legăturilor într-un arbore k-ar cu n noduri este nk. Astfel, numărul legăturilor nule
este:
nk - n + 1 = n(k - 1) + 1.
k −1
REMARCĂ: Lema 3 implică faptul că pentru un arbore k-ar cu n noduri (k  2), mai mult de
k

din legături sunt nule, deci o mare cantitate de memorie rezervată memorării legăturilor nu este
folosită. Cum
 k − 1 1
min  =
 k  2
kN
k2

şi egalitatea se realizează pentru k = 2, rezultă că arborii binari au cel mai puţin spaţiu de legătură
nul dintre toţi arborii k-ari, ceea ce justifică larga lor utilizare.
Pentru reprezentarea arborilor binari, ordinea legăturilor spre fii este importantă, motiv pentru care
ei vor purta nume distincte, şi anume fiu stâng şi fiu drept al unui nod. Structura de date va arăta
astfel:

Data
Left Right
iar în limbajul C++ declaraţia clasei va arăta astfel:
1 //fişierul btree.h
2 #include "container.h"
3
4 template<class T>
5 class BTree:public virtual Container
6 {
7 protected:
8 T data;
9 BTree<T> *left;
10 BTree<T> *right;
11
12 public:
13 BTree():left(0), right(0){}
14 BTree(T const& t):data(t), left(0), right(0)
15 {
16 count++;
17 }
18
19 void Purge();
20 ~BTree();
21 T& getData()const;
22 BTree* getLeft()const;
23 BTree* getRight()const;
24 void setLeft(BTree* l);
25 void setRight(BTree* r);
26 BTree(BTree<T>const &bt);
27 void preOrd(void (*f)(T&));
28 void inOrd(void (*f)(T&));
29 void postOrd(void (*f)(T&));
30 void breadthFirstTraversal(void (*f)(T&));
31 int compareTo(BTree<T>& bt);
32 friend int compareBTree(BTree<T>& bt1, BTree<T>& bt2);

7
Structuri de date
33 void BuildEq(unsigned int nrNoduri);
34 };//sfârşitul clasei Btree
Spre deosebire de metoda clasică de declarare a structurii de date arbore binar, varianta propusă de
noi nu foloseşte un pointer nul pentru arbore vid ci un obiect al clasei BTree reprezintă un arbore
vid numai dacă funcţia isEmpty returnează valoarea true. Vom arăta în cele ce urmează modul de
implementare a principalelor operaţii pe arbori binari. Aşa cum probabil se intuieşte, modul
recurent în care arborii binari au fost definiţi va conduce la utilizarea pe scară largă a funcţiilor
recursive. Pentru început, iată cum arată constructorii şi destructorul clasei BTree:
1 template<class T>
2 BTree::BTree(T const& t):data(t), left(0), right(0)
3 {
4 count++;
5 }
6
7 template<class T>
8 ~BTree::BTree()
9 {
10 Purge();
11 }
Prin convenţie, un arbore vid va avea membrul count (moştenit de la interfaţa Container) egal cu 0
şi implicit metoda isEmpty va returna true. Accesorii clasei BTree şi funcţiile moştenite isEmpty
şi getCount sunt definite în cele ce urmează:
1 Template<class T>
2 T& BTree::getData()const
3 {
4 return data;
5 }
6
7 template<class T>
8 BTree* BTree::getLeft()const
9 {
10 return left;
11 }
12
13 template<class T>
14 BTree* BTree::getRight()const
15 {
16 return right;
17 }
18
19 template<class T>
20 unsigned int BTree::isEmpty()
21 {
22 return (count==0);
23 }
24
25 template<class T>
26 unsigned int BTree::getCount()
27 {
28 return count;
29 }
După cum se poate urmări şi în interfaţa BTree, am declarat şi metodele modificator care setează
legăturile stângi şi drepte. Implementarea lor (care actualizează numărul de noduri) este următoarea:

8
Structuri de date
1 template<class T>
2 void BTree::setLeft(BTree const *_left)
3 {
4 if(isEmpty())
5 throw Error("Arbore vid", ERROR);
6 if(left!=0)&&(left!=_left)
7 left->Purge();
8 left=_left;
9 count=1+(left?left->getCount():0)+(right?right->getCount():0);
10 }
11
12 template<class T>
13 void BTree::setRight(BTree* _right)
14 {
15 if(isEmpty())
16 throw Error("Arbore vid", ERROR);
17 if(right!=0)&&(right!=_right)
18 right->Purge();
19 right=_right;
20 count=1+(left?left->getCount():0)+(right?right->getCount():0);
21 }

2.1. Construcţia unui arbore binar echilibrat cu n noduri


Când se cunoaşte dinainte numărul n, tehnica utilizată foloseşte următorul procedeu:
i) se porneşte cu un arbore vid;
ii) se citeşte informaţia pentru nodul rădăcină, după care se va apela recursiv procedura
n − 1
pentru determinarea subarborelui stâng cu ns =   noduri , după care se va apela
 2 

recursiv procedura de construcţie pentru subarborele drept cu nd = n - 1 - ns noduri;


iii) algoritmul se termină dacă n = 0.
Metoda care construieşte un astfel de arbore cu rădăcina în obiectul curent este BuildEq şi are
următorul cod:
1 template<class T>
2 void BTree::BuildEq(unsigned int nrNoduri)
3 {
4 unsigned int ns, nd;
5 T _data;
6 if(nrNoduri==0)
7 Purge();
8 count=nrNoduri;
9 cout<<"Data =\t";
10 cin>>_data;
11 data = _data;
12 ns = (nrNoduri - 1)/2;
13 nd = nrNoduri - ns – 1;
14 if(ns>0)
15 {
16 left= new BTree;
17 left->BuildEq(ns);
18 }
19 if(nd>0)
20 {
21 right= new BTree;
22 right->BuildEq(nd);

9
Structuri de date
23 }
24 }

2.2. Compararea a doi arbori binari


Dacă informaţia conţinută in nodul curent din primul arbore este mai mică decât cea din nodul
curent din cel de-al doilea arbore, atunci spunem că primul arbore este mai mic decât al doilea.
Dacă nodul curent din primul arbore este nul, iar nodul curent din cel de-al doilea arbore este nenul,
atunci primul arbore este mai mic decât al doilea. Criteriul se aplică recursiv subarborilor stâng şi
drept. Se va returna în final:
• 0 dacă arborii sunt egali;
• -1 dacă primul arbore este mai mic decât al doilea;
• 1 în caz contrar.
În acest scop vom scrie o funcţie prieten compareBTree care compară doi arbori şi apoi o vom
apela dintr-o metodă compareTo a clasei BTree:

1 template<class T>
2 friend int compareBTree(BTree<T> const& bt1, BTree<T> const&
3 bt2)
4 {
5 if(bt1.isEmpty())
6 if(bt2.isEmpty())
7 return 0; //egale vide
8 else
9 return -1;
10 else
11 if(bt2.isEmpty())
12 return 1;
13 else
14 if(bt1.data<bt2.data)
15 return -1;
16 else
17 if(bt1.data>bt2.data)
18 return 1;
19 else
20 {
21 int res=compareBTree(bt1->left,
22 bt2->left);
23 if(res==0)
24 return compareBTree(
25 bt1->right, bt2->right);
26 else
27 return res;
28 }
29 }
30
31 template<class T>
32 int compareTo(BTree<T> const & bt)
33 {
34 return compareBTree(*this, bt);
35 }

10
Structuri de date
2.3. Copierea unui arbore binar
Copierea unui arbore binar în altul o vom realiza cu ajutorul unui constructor de copiere:
1 template<class T>
2 BTree::BTree(BTree<T>const & bt)
3 {
4 if(bt.isEmpty())
5 ::BTree();// dacă este vid atunci se apelează
6 //constructorul simplu al clasei BTree
7 else
8 {
9 data=bt.data;
10 left= bt.left? new BTree(bt.left):0;
11 right=bt.right? new BTree(bt.right):0;
12 count= bt.getCount();
13 }
14 }

2.4. Distrugerea unui arbore binar


Aşa cum am amintit, obiectul curent există deja în memorie şi nu se poate dealoca pe sine însuşi. De
aceea, pentru distrugerea în întregime a arborelui reprezentat de acest obiect se vor distruge mai
întâi fii stâng şi drept după care se dealocă cu delete memoria ocupată de aceşti pointeri şi în final
nodul curent este marcat ca fiind liber prin setarea variabilei count pe 0. Metoda care realizează
acest lucru este Purge:
1 template<class T>
2 void BTree::Purge() //curăţă memoria ocupată de tot arborele
3 {
4 if(left!=0)
5 {
6 left->Purge();
7 delete left;
8 }
9 if(right!=0)
10 {
11 right->Purge();
12 delete right;
13 }
14 left=right=0;
15 count=0;
16 }

2.5. Traversarea în adâncime a arborilor binari (engl. depth)


Arborii binari pot fi traversaţi în adâncime şi în lăţime. La rândul ei, traversarea în adâncime se
împarte în alte trei moduri, în funcţie de ordinea de vizitare a nodurilor. Fie arborele din figura 5:

A
C O
B S T

U L D M
Figura 5. Un arbore binar la care se face traversarea în adâncime

11
Structuri de date
Există trei moduri de traversare a arborilor în adâncime:
i) traversarea în preordine, definită astfel: mai întâi se vizitează nodul rădăcină, apoi
subarborele stâng şi apoi subarborele drept.
Pentru arborele din figura 5, traversarea în preordine va da următoarea ordine de
parcurgere:
A C B S U L O T D M
În general, parcurgerea în preordine poate fi schematizată în sintagma
Root Left Right.
Metoda ce traversează arborele în preordine poate fi scrisă recursiv astfel:
1 template<class T>
2 void BTree::preOrd(void (*f)(T&))
3 {
4 f(data);
5 if(left!=0)
6 left->preOrd(f);//apel recursiv
7 if(right!=0)
8 right->preOrd(f);//apel recursiv
9 }
ii) traversarea în inordine constă în traversarea subarborelui stâng, a rădăcinii şi apoi a
subarborelui drept. Arborele din figura 5 are următoarea reprezentare în ordine:
B C U S L A O D T M (Left Root Right)
Funcţia de traversare în ordine a arborelui este următoarea:
1 template<class T>
2 void BTree::inOrd(void (*f)(T&))
3 {
4 if(left!=0)
5 left->inOrd(f);//apel recursiv
6 f(data);
7 if(right!=0)
8 right->inOrd(f);//apel recursiv
9 }
iii) traversarea în postordine constă în traversareae subarborelui stâng, asubarborelui drept
şi apoi a rădăcinii, după schema:
Left Right Root
Ca urmare a reprezentării în postordine a arborelui din figura 5 obţinem secvenţa:
B U L S C O D M T A
Funcţia de traversare în postordine a arborelui binar este următoarea:
1 template<class T>
2 void BTree::postOrd(void (*f)(T&))
3 {
4 if(left!=0)
5 left->postOrd(f);//apel recursiv
6 if(right!=0)
7 right->postOrd(f);//apel recursiv
8 f(data);
9 }

12
Structuri de date
2.6. Traversarea arborilor binari în lăţime (engl. breadth)
Traversarea arborilor binari în lăţime este foarte utilă în programe care tratează arborescenţe mari
când ştim că soluţia se află la adâncime mică faţă de rădăcină.
Această metodă de traversare foloseşte o coadă iar algoritmul este următorul:
• iniţializează coada;
• adaugă nodul rădăcină în coadă;
• cât timp sunt elemente în coadă execută:
 extrage un element din coadă;
 scrie conţinutul nodului;
 introduce în coadă fii nodului respectiv.
De exemplu, arborele din figura 5 are următoarea reprezentare în lăţime:
A C O B S T U L D M
Metoda de traversare a arborilor în lăţime este următoarea:
1 template<class T>
2 void BTree::breadthFirstTraversal(void (*f)(T&))
3 {
4 QueueAsArray<BTree*> queue;
5 if(isEmpty())
6 return; //arbore vid
7 queue.enQueue(this);
8 while(!queue.isEmpty())
9 {
10 BTree<T> *bt=queue.deQueue();
11 if(!bt->isEmpty())
12 {
13 f(bt->getData());
14 queue.enQueue(bt->left);
15 queue.enQueue(bt->right);
16 }
17 }
18 }

2.7. Analiza operaţiilor de traversare a arborilor binari


Atât traversarea în lăţime cât şi cea în adâncime presupune parcurgerea în timp constant a fiecărui
nod, deci ordinul funcţiilor de parcurgere este în fiecare caz O(n).

13

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