Documente Academic
Documente Profesional
Documente Cultură
CURS 1
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.
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
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
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
F Moneda
Apasă buton acceptată ?
restituire
monedă
T
Apasă buton şi
aşteaptă sticla
A apărut F
sticla?
STOP
.
Figura 3. Schema de funcţionare a automatului de COCA COLA
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.
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)
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).
Î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 }
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 }
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.
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
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
Structuri de date
Dacă a1 atunci vom căuta polinomul QR[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)),()nN
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
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
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
cu T(1) dat, aR, kN, k>1, PR[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
de unde obţinem că
log k (n)-1
n
T(n)=T(1)+
i=0
P
ki
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).
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)
4
Structuri de date
n
E(n)=n!* i!1 +n!*E(0)
i=1
DEMONSTRAŢIE: a) Pentru n2 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,
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
deoarece n5.
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
11 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 .
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.
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
Array Permutare
BitSet
foloseşte
derivă Array2D
derivă
Matrix
SortArray Set
Polinom
foloseşte
SMatrix
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.
Î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];
...........
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
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:
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:,
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ă:
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:
Î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:
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 }
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
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
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:
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.
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.
Î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
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:
Ultima variantă de constructor primeşte ca informaţie atât gradul cât şi vectorul ce reprezintă
permutarea:
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 }
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:
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:
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 }
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:
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:
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 }
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
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.
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:
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:
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:
2
Structuri de date
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 (-):
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 * .
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:
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:
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
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
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:
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
• CREATE() → STACK
• PUSH(T, STACK) → STACK
• DELETE(STACK) → STACK
• GETTOP(STACK) → T
• POP(STACK) → T
• ISEMTS(STACK) → BOOLEAN
() 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.
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ă.
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:
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 void main()
2 {
int a, b[10];
3
double c;
4 char d;
5 ....
6 }
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 }
1000
a 0 1002
b
1004 1004
c
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]
ListElement
[2] hea tail
d
LinkedList
Figura 3
[3]
l &= 1004 &= 1000 &= 1008
5 -1 2
hea tail
dd 3
Structuri de date
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
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
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
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
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
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 ,..)
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
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:
1 3 2 7
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
1 template<class T>
2 class DoubleLinkedList
3 {
4 DListElement *head, *tail;
5 //.. ..
6 };
4
Structuri de date
head tail
1 3 2 7
template<class T>
class DListElement
{
T data;
DListElement *next;
DListElement *prev;
public:
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();
}
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;
}
Curren
t
4 1 3 2 7
6
dn
Structuri de date
1 3 2 7 4
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
10
Structuri de date
CURS 10
1. Noţiunea de arbore
Dan rădăcină(root)
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
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).
Arborele din Fig 2.b va folosi pentru fiecare nod o structură de forma:
4 1
5 3 4
2 3 1 2
Data Link
... .
Data Link
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).
SDBTREE={D, d, F, A}
cu:
D={T, BTREE, BOOLEAN}
d=BTREE
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.
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
kN
k2
ş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 }
9
Structuri de date
23 }
24 }
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 }
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 }
13