Sunteți pe pagina 1din 175

Academia de Studii Economice din Bucureşti

Facultatea de Cibernetică, Statistică şi Informatică


Informatică Economică
Catedra de Informatică Economică

Structuri de date
- material didactic pentru ID -

Prof. univ. dr. Ion IVAN

Cristian-Eugen CIUREA Mihai- Laurenţiu DOINEA

Acest material are la bază lucrarea


Structuri de date , coordonatori Ion IVAN, Marius POPA, Paul POCATILU
 publicată la Editura ASE, Bucureşti, 2008,
 premiată de Academia Română cu diploma Tudor Tănăsescu în data de 16 dec. 2010

Bucureşti
2011
Titlul cursului: Structuri de date
Introducere: Disciplina Structuri de date se studiază în Academia de
Studii Economice din Bucureşti, în cadrul Facultăţii de Cibernetică,
Statistică şi Informatică Economică şi se adresează studenţilor din anul 3,
secţia Informatică Economică.
Disciplina Structuri de date are ca obiectiv prezentarea tuturor structurilor
de date cu  proprietăţile, avantajele şi dezavantajele utilizării lor, în vederea utilizării lor
eficiente. Cunoaşterea structurilor de date le permite programatorilor să privească procesul de
dezvoltare software ca proces de alocare şi nivelare de resurse. Evoluţia tehnicilor de
 programare determină realizarea unor produse program caracterizate prin complexitate
ridicată şi prin consumuri reduse de resurse.  Într-un context mai larg, structurile de date se
constituie ca resurse la dispoziţia programatorilor, care prin diversitate influenţează hotarâtor
calitatea programelor.

În cadrul  procesului de instruire, se utilizează ca resurse suplimentare


materialele puse la dispoziţie de biblioteca facultăţii, atât în cadrul sălii
de lectură, cât şi prin serviciul de împrumut. Laboratoarele Catedrei de
Informatică Economică sunt disponibile   pentru studiu individual, pe
toată durata programului de lucru, atunci când nu se desfăşoară activităţi
didactice.

Evaluarea cunoştinţelor se realizează astfel:


se elaborează un proiect care are ca obiectiv utilizarea eficientă a
structurilor de date –  20%
 20% din punctaj;
studenţii trebuie să ofere o serie de soluţii pentru 5 probleme în timpul
semestrului pentru care primesc 10% din punctaj;
 pentru activitatea depusă în vederea realizării de lucrări practice la disciplina Structuri de
date se obţine 20% din p unctaj;
examen –  50%
 50% din punctaj.

În vederea asimilării cât mai eficiente şi a unei uşoare înţelegeri a


informaţiilor prezentate în acest material, următorul graf de parcurgere a
disciplinei reflectă interconectarea tuturor structurilor de date prezentat e.
Obiectivul este reprezentat de conturarea nivelului conceptual, astfel încât
să fie evidenţiată corelaţia dintre structurile de date discutate şi realizarea
unei abordări progresive pentru a înlesni înţelegerea şi a sublinia utilitatea.

2
Articole

Masive Liste simple şi


Liste simple
Fişiere
unidimensionale duble

Masive
Stive şi cozi
multidimensionale

Arbori binari de
Matrice rare
căutare

Arbori B

Fig. 1. Graful de parcurgere a disciplinei Structuri de date

3
CUPRINS

1. Masivele – structuri de date omogene ş i contigue


1.1. Masive unidimensionale
1.2. Masive bidimensionale
1.3. Operaţii cu masive bidimensionale
1.4. Masive multidimensionale

2. Matrice rare
2.1. Concepte de bază
2.2. Memorarea matricelor rare
2.3. Determinarea gradului de umplere al unei matrice rare
2.4. Adunarea, scăderea şi transpunerea
2.5. Înmulţirea şi inversarea matricelor rare

3. Articolul – structură de date neomogenă şi contiguă


3.1. Structuri de date poziţionale
3.2. Structuri de date ierarhizate
3.3. Vectori de structuri şi structuri de vectori

4. Fişiere
4.1. Structuri de date externe
4.2. Criterii de clasificare a fişierelor
4.3. Fişiere secvenţiale
4.4. Fişiere secvenţial –  indexate
4.5. Fişierele bazei de date

5. Listele –  structuri dinamice necontigue


5.1. Consideraţii privind structurile de date de tip listă
5.2. Lista simplu înlănţuită
5.3. Lista circulară simplu înlănţuită
5.4. Operaţii cu liste liniare simplu înlănţuite
5.5. Liste dublu înlănţuite

6. Stive şi cozi
6.1. Consideraţii privind structurilor de date de tip stivă şi coadă
6.2. Caracteristicile structurilor de date de tip stivă
6.3. Operaţii de bază cu stive
6.4. Evaluarea expresiilor matematice cu ajutorul stivei şi cozii

7. Arbori binari şi arbori de căutare


7.1. Structura de date de tip arborescent
7.2. Transformarea arborilor oarecare în arbori binari
7.3. Arbori binari de căutare
7.4. Aplicaţii care utilizează structura de date de tip arbore binar de căutare

4
8. Arbori B
8.1. Arbori B. Definiţie. Proprietăţi.
8.2. Operaţii de bază într -un arbore B
8.3. Algoritmii C++ pentru inserarea unei valori de cheie într-un arbore B
8.4. Algoritmii C++ pentru ştergerea unei valori de cheie intr -un arbore B

5
1. Masivele –  structuri de date omogene si contigue

Obiectivele unităţii de învăţare

După studierea acestei unităţi de învăţare, studenţii îşi vor însuşi cunoştinţe
teoretice şi vor deprinde abilităţi practice  pentru utilizarea masivelor
unidimensionale si bidimensionale. Studenţii vor aprofunda lucrul cu
vectori şi matrice, fiind capabili să definească, iniţializeze şi să refere elementele acestora.

1.1 Masive unidimensionale

Sunt puţine programele în care nu apar definite masive unidimensionale. Problemele de


ordonare a şirurilor, de calcul a indicatorilor statistici medie şi dispersie, programele pentru găsirea
elementului minim şi multe altele presupun stocarea valorilor numerice ale şirurilor în zona de
memorie care în mod folcloric le numim vectori. Ceea ce de fapt se recunoaşte sub numele de vector
este în realitate o structură de date omogene.

Prin definirea:

int x[10];

se specifică:
 x  – o dată compusă din 10 elemente;
elementul  –  de tip întreg;
 primul element  –  x [0];
al doilea element  –  x [1];
...  –  . . .
al zecelea element  –  x [9].

Lungimea zonei de memorie ocupată de variabila x se determină conform relaţiei:


n
lg(x;.) lg (x[i] ; int) (1.1)
i 1

Dacă funcţia:

lg(a; b)

se defineşte pentru tipurile standard prin:

lg (x; b) = lg(.; b) = k (1.2)

unde:
k  – lungimea standard alocată la compilare pentru o dată de tipul b;
lg(x0 , x1 , x2 , . . . ., xn ; b) = lg (x0; b) + lg (x1; b) + . . . +lg(xn; b) =
*
= lg(.; b) + lg (.; b) + lg (.; b) = k b + k b + . . . k b = n k b

rezultă că:
*
lg(x, int) = 10 2 bytes

6
Modul grafic de reprezentare a alocării memoriei pentru elementele vectorului  x,
conduce la:

x[0] x[1] x[2] x[8] x[9]

Figura 1.1 Alocarea memorie pentru vectorul x

Pentru că elementele sunt de acelaşi tip, respectiv:

lg(x[0], . ) = lg(x[1], . ) = . . . = lg(x[9], .) (1.3)

dacă se notează:

= adr(x[i])
= adr (x[i+1])
- = lg (x[i],.)

se observă că:

dist (x[i], x[i+1]) = lg(x[i], int) = 2 bytes (1.4)

întrucât elementele ocupă o zonă de memorie contiguă.


Se definesc funcţiile:
 succ( )  – desemnează succesorul unui element într -un şir;
 pred( )  – desemnează predecesorul unui element într -un şir astfel:
 succ(x[i]) = x [i+1];
 pred(x[i]) = x [i-1].

Aceste funcţii au priorităţile:

 succ(pred(x[i])) = succ(x[i-1]) = x[i]


 pred(succ(x[i])) = pred(x[i+1]) = x[i]

Deci, funcţiile succ( ) şi pred( ) sunt una inversă celeilalte.


Pentru extremităţile vectorului:

 pred(x[0]) =
 succ(x[9]) =
 succ (  ) = pred (  ) =

Direct, se observă că:

 succ (x[i]) = x [i]


 pred (x[i]) = x [i]
 succ m (x[i]) = x[i+m]
n
 pred (x [i]) = x[i-n]
m n
 succ (pred (x[i])) = x[i-n+m]
 pred n (succ m (x[i])) = x[i+m – n]

7
Dacă elementul  x[i] este considerat reper, adresele celorlalte elemente se calculează folosind
deplasarea faţă de elementul reper.
Funcţia deplasare:

depl(a;b)

va permite calculul deplasării lui a faţă de b. Astfel:

depl(x[i+k], x[i]) = (i+ k – i) * lg (., int) = k * lg(., int)


depl (x[i], x[i+k] = (i –  k – i) * lg (., int) = -k * lg(., int)

sau:

depl(x[i+k], x[i]) = adr(x[i+k]) – adr(x[i]) (1.5)

Dacă  x reprezintă structura de date omogene în totalitatea ei, iar x[0], x[1], . . . , x[9] sunt
 părţile care o compun, din reprezentarea grafică rezultă că:

adr(x) = adr(x[0]) (1.6)

Se defineşte:

adr(a+b) = adr(a) + (b – 1) * lg( ., int) (1.7)

unde:
a  –  numele datei structurate;
b  –  poziţia elementului a cărui adresă se doreşte a fi calculată;
adr(a+b) = adr(b+a).

Contiguitatea este pusă în evidenţă prin definirea funcţiei de distanţă  DST(a, b), care
indică numărul de bytes care nu aparţin datelor a şi b, separând a de b.

a  b

Fig. 1.2. Separarea zonelor de memorie alocate pentru variabilele a şi b


Astfel, conform figurii 1.2, distanţa dintre a şi b este DST(a, b)=5 bytes. În raport cu modul de
definire a funcţiei DST( ), se observă că:

 DST(x[i], x[i+1]) = 0, i Є {0, 1, . . . , 9}


 DST(x[i], x[i]) = 0
 DST(a, a) = 0
 DST (a, b) ≥ 0

11 12
a  b c

Fig. 1.3. Separarea zonelor de memorie alocate variabilelor a, b şi c

8
 DST (a, b) ≤ DST(a, c) + DST (c, b)
11 ≤ 11 + 12 + lg (b) + 12
lg(b) + 2*12 ≥ 0
 DST (a, b) = DST (b, a)

Reprezentat ca structură arborescentă, vectorul are modelul grafic:

X0 X1 X2 X9

Fig. 1.4. Structura arborescentă a vectorului x

În programe, se specifică numărul maxim al componentelor vectorului, avându-se grijă ca în


 problemele ce se rezolvă, numărul efectiv de elemente să nu depăşească dimensiunea declarată.
De exemplu, în secvenţa:

.........
int x[10];
int n;
.........
cin >> n;
for( i=0; i<n; i++)
x[i] = 0;

se identifică următoarele inexactităţi:


- deoarece n nu este iniţializată cu o valoare cuprinsă între 0 şi 9, există posibilitatea ca în
cazul n=15, adresa calculată să fie:

adr(x+15) = adr(x) + (15-1) * lg(x, int) (1.8)

şi să cuprindă un cuvânt ce urmează cu mult mai departe de componenta x[9], cuvânt al


cărui conţinut devine zero;
- în limbajul Pascal, prin parametrii compilării se controlează expresiile indiciale, aşa fel
încât să fie incluse în intervalul definit pentru variaţie, specificat în calificatorul tip[e1 ..
e2 ],  evitându-se distrugerea necontrolată a operanzilor adiacenţi masivului
unidimensional.
Exemplul dat arată că expresia indicială aparţine intervalului [0, 9], însă limbajul C/C++
 permite definirea unor limite într-o formă mult mai flexibilă.
De exemplu, definirea:

int y[11];

 permite referirea elementelor:

. . . ., y[-7], y[-6], . . . . y[0], y[1], y[2], y[3]

adr(y) = adr(y[-7])

9
 DST(y[ -7], y[-6]) = [-6-(7)-1] * lg(. , int) = 0

şi acest vector rezultă că este contiguu.


 Numărul de componente este dat de:

adr ( y[3]) adr ( y[ 7])


1 11 (1.9)
lg(., int)

adr(y+4) = adr(y[-7] ) +[4-(-7) ]*lg(y, int) = adr(y[-7] ) + 11*lg(y, int)


 succ(y[ -5]) = y [-4]
 pred(y[-1]) = y[-2]
depl(y[-7+2], y[-7]) = 2*lg(y, int)

Deplasarea lui y[-6] faţă de elementul y[2]:

depl(y[-6], y[2]) = depl(y[2-8], y[2]) = (2-8-2)*lg(y, int) = -8*lg(y, int)


depl(y[2], y[-6]) = [2-(-6)]*lg (y, int) = 8*lg(y, int)

Se observă că:

depl(a, b) = - depl (b, a) (1.10)

sau:

depl(a, b) + depl(b, a) = 0 (1.11)

Proprietăţile masivelor unidimensionale, conduc la ideea stocării într -o zonă de memorie a


adresei unuia dintre termeni şi prin adăugarea sau scăderea unei raţii egale cu lungimea unui element,
se procedează la baleierea spre dreapta sau spre stânga a celorlalte elemente.

x [0] x[1] x[i] x[i+1]

...

adr i+adr i+1+adr

Fig. 1.5. Baleierea elementelor vectorului x

α = adr(x[0]);
α = α + i*lg(x[0], int);

conduce la situaţia în care:

cont (α) ≡ adr (x[i])  (1.12)

Datorită contiguităţii memoriei şi a regulilor de regăsire a elementelor, masivul


unidimensional nu conţine în mod distinct informaţii privind poziţia elementelor sale. Cunoscând

10
numele masivului, referirea elementelor se realizează specificând acest nume şi poziţia elementului
cuprins între paranteze drepte sub forma unei expresii indiciale.
Expresia indicială are un tip oarecare, însă înainte de efectuarea calculului de adresa are loc
conversia spre întreg, care este atribuită funcţiei int( ). Deci funcţia int( ), realizează rotunjirea în
minus a valorii expresiei indiciale.
Astfel, pentru definirea:

int z[100];

unde indicele pleacă de la 6, adresa elementului:


a
 z [ e  + cos (b) / c  ] (1.13)

se calculează astfel:

a
adr (z [ e  + cos (b) / c  ] ) = adr ( z[6] ) +
a
+ ( int (e  + cos (b) / c  )) * lg ( z[6] , int)

Funcţiile de validare a adreselor se vor defini astfel:

TRUE, dacă adr(a) [ A


i, Af ]
fp(a) =
FALSE, în caz contrar
(1.14)

TRUE, dacă a d r ( a ) [ B
i, Bf ]
gm(a) =
FALSE, în caz contrar

unde:
 fp( )  – funcţia de validare adrese în raport cu programul;
 gm( )  – funcţia de validare a adreselor în raport cu un masiv m;
 Ai  –  adresa se început a programului;
 A f   – adresa se sfârşit a programului;
 Bi  –  adresa de început a masivului m;
 B f   – adresa de sfârşit a masivului m.

Dacă programul P este format din instrucţiunile  I 1 , I 2 , …, I 100 atunci:

adr (I 1 ) = Ai
adr (I 100 ) = A f 

Dacă în programul P  este definit masivul:

int x[10];

atunci:

11
adr (x[0]) = Bi
adr (x[99]) = B f 

Se spune că este corect folosită expresia indicială ea+cos(b)/  c dacă:

 gm( x [ ea + cos (b) / c  ] ) = TRUE (1.15)

sau dacă:
a
int (e  + cos (b) / c  ) * ( lg ( z[6] , int ) < B f - Bi  (1.16)

Dacă se ia în considerare că:


a
 fp( x [ e  + cos (b) / c  ] ) = TRUE (1.17)

 pot apare situaţii în care să fie modificate alte zone decât cele asociate masivului unidimensional.
Dacă se iau în considerare reprezentări simplificate, pentr u localizarea unui element  x[i] al
unui masiv unidimensional, se foloseşte formula cunoscută:

 x[0] + x[i – 0] * lungime_element (1.18)

Atunci când se construiesc programe în care se definesc masive unidimensionale, alegerea


tipului, alegerea limitei inferioare şi a limitei superioare pentru variaţie, depind de
contextul problemei dar şi de formulele identificate prin inducţie matematică pentru
explorare, folosind structuri repetitive.
De fiecare dată, trebuie avută grijă ca numărul de componente ce rezultă la definire să fie
acoperitor pentru problemele ce se rezolvă.
Pentru stabilirea numărului maxim de componente ale vectorilor ce sunt definiţi, se consideră:
 L  – lungimea în baiţi a disponibilului de memorie;
 L p  –  lungimea în baiţi a necesarului de memorie pentru program, respectiv instrucţiuni
executabile şi alte definiri;
 M   – numărul de masive unidimensionale de tip T i, având acelaşi număr de componente care
apar în program;
 x  – numărul maxim de componente ale unui masiv.

 x = int( ( L –  L p ) / ( N * lg( . ;T i ) ) ) (1.19)

În cazul în care cele  N   masive au dimensiuni variabile d 1 , d 2 , …, d  N , condiţia de utilizare
corectă a resursei memorie este ca:

d 1 + d 2 + … +d  N  int( ( L –  L p ) / lg( . ;T i ) ) (1.20)

Astfel, se rezolvă problema acoperirii resursei de memorie.

1.2 Masive bidimensionale

Este aproape sigur că în activitatea de programare, matricele ocupă ca importanţă un loc


deosebit. Şi tot atât este de adevărat că lucrul corect cu matrice oferă rezultate rapide, cum la fel de
adevărat este că utilizarea defectuoasă reduce considerabil şansa de a obţine rezultate corecte.
Singura modalitate de a realiza programe corecte utilizând matrice, este cunoaşterea
mecanismelor de implementare a acestora în diferite limbaje de programare, precum şi studierea
 proprietăţilor ce decurg din ele.

12
Pornind de la ideea că o matrice este formată din linii, iar liniile sunt formate din elemente, se
ajunge la modelul grafic al matricei, care apare sub forma unei arborescenţe organizată pe trei nivele.

Pentru definirea:

int a[2][4];

rezultă că matricea a este formată din 2 linii, fiecare linie având câte 4 componente.

Modelul grafic pentru matricea a este:


a

a0 a1

a00 a01 a02 a03 a10 a11 a12 a13

Fig. 1.6. Structura arborescentă pe linii asociată matricei a

Modelul grafic presupune existenţa a trei nivele:


- la nivelul cel mai înalt se află întregul, matricea a;
- la nivelul imediat următor se află primele părţi în care se descompun matricea şi anume
liniile acesteia a[0] şi a[1];
- la nivelul al treilea se află elementele grupate pentru fiecare linie în parte.
O altă definiţie a matricei conduce la descompunerea în coloane şi a coloanelor în elemente.
a

C0 C1 C2 C3

a00 a10 a01 a11 a02 a12 a03 a13

Fig. 1.7. Structura arborescentă pe coloane asociată matricei a

Deci în ambele cazuri se obţin structuri arborescente, elementele de la baza arborescenţelor


reprezentând dispunerea contiguă a liniilor una în continuarea celeilalte, respectiv, a coloanelor una în
continuarea celeilalte, regăsind ambele situaţii ca modalităţi de „liniarizare a matricelor”.
Dacă se consideră o matrice A, având m linii şi n coloane, elementele fiind de tipul Ti, adresa
elementului a[i][j] se calculează fie după formula:

adr(a[i][j]) = adr(a[0][0]) + ((i –  0) * n + j ) * lg(a , T  )


i (1.21)

dacă liniarizarea se face „linie cu linie”, fie după formula:

adr(a[i][j]) = adr(a[0][0]) + (( j –  0 ) * m + i ) * lg(a , T  )


i (1.22)

dacă liniarizarea se face „coloană după coloană”.


Intuitiv, o matrice este privită luând în considerare numai primul nivel de descompunere, ca
vector de linii sau ca vector de coloane.

13
Fiecare linie sau coloană, prin descompunerea la nivelul următor, este privită ca vector de
elemente. Deci, în final o matrice este privită ca vector de vectori.
Definirea:

int a[2][4];

 pune în evidenţă exact acest lucru.


Din această descriere rezultă că:

adr(a) = adr(a[0]) = adr(a[0][0])


adr(a[i]) = adr(a[i][0])

Dacă dispunerea elementelor este linie cu linie:

 DST(a[i][n], a[i+1][0]) = 0

Dacă dispunerea elementelor este coloană după coloană:

DST(a[m][j] , a[0][j+1]) = 0

Deplasarea elementelor aij faţă de elementul akh , se calculează după relaţia:

depl (a[i][j], a[k][h]) = adr(a[k][h]) –  adr(a[i][j]) =


= adr (a[0][0]) + ((k-0)n + h) * lg(a, T i ) – (adr(a[0][i])+
+ ((i-0)n + j) *lg(a, T i ) =
= lg(a, T i )*[(k-0)n –  (i-0)n + h –  j] =
= lg(a, T  )*[(k-i)n
i + h –  j]

 Numărul de elemente al matricei n se obţine:

adr(a[m][n]) adr(a[0][0 ])
1 m * n (1.23)
lg(a, Ti )

În cazul dispunerii linie de linie:

 succ(a[i][n]) = a[i+1][0]
 pred(a[i][0]) = a [i-1][n]
 succ (a[i]) = a[i+1][0]
 pred (a[i]) = a[i-1][0]
 succ(a[i+k]) = succ (a[i])
 pred (a[i]) = pred (a[i+k])

În mod asemănător:

adr(a+i) = adr(a[0]) + (i-0)*lg(a[0])


adr (a[i] +j) = adr(a[i]) + (j-0)* lg (a[0][0])
lg(a[i]) = lg(a[i][0]) + . . .+lg(a[i][n])

sau:

lg(a[i]) = n*lg(a[i][0]) (1.24)

deci:

14
adr(a+i) = adr(a[0]) + (i-0)*n*lg(a[i][0]) (1.25)

Dacă privim matricele ca vectori de vectori, în mod corespunzător se identifică forma


grafică:

a[0] a[0][0] a[0][1] a[0][n]


a
...

a[1] a[1][0] a[1][1] a[1][n]

...

a[2] a[2][0] a[2][1] a[2][n]

...

...

a[m] a[m][0] a[m][1] a[m][n]

...

Fig. 1.8. Reprezentarea matricelor ca vectori de vectori

unde a, a[i], a[i][j] se definesc în aşa fel încât să reflecte următorul conţinut:

a[i] = adr(a[i][1]), i = 0, 1, . . . , m
a = adr (a[0])

Definirea unei matrice ca având m linii şi n  coloane, deci în total m*n elemente, şi
explorarea elementelor prin includerea unui alt număr de linii şi coloane conduce la o
localizare a elementelor însoţită de un rezultat eronat, a cărui provenienţă e necesar a fi
interpretată şi reconstituită.
Se consideră matricea:

1 2 3 4 5
6 7 8 9 10
 A 11 12 13 14 15
16 17 18 19 20
7 1 1 4 2

stocată în:

int a[5][5];

definită şi iniţializată în programul principal, iar într -o procedură se consideră că:

int b[4][4];

15
Procedura realizează însumarea elementelor diagonalelor matricei b. Întrucât transmiterea
 parametrilor se face prin adresă, punerea în corespondenţă este:

a00 a01 a02 a03 a04 a10 a11 a12 a13 a14 a20 a21

1 2 3 4 5 6 7 8 9 10 11 12

 b00 b01 b02 b03 b10 b11 b12 b13 b20 b21 b22 b23

a22 a23 a24 a30 a31 a32 a33 a34 a40 a41 a42 a43 a44

13 14 15 16 17 18 19 20 7 1 1 4 2

 b30 b31 b32 b33


Fig. 1.9. Punerea în corespondenţă a matricei a cu matricea b
Prin specificaţii se cere:
4
S a a[i, i ] (1.26)
i 0

iar prin procedură se obţine:


3
S b b[i, i ] (1.27)
i 0

În loc de Sa = 42 se obţine S b = 34.


În cazul în care structura repetitivă a procedurii efectuează corect numărul de repetări:

for (i = 0; i < 5; i + +)
S = S + b[i][i];

i = 0 localizează termenul;
adr (b[0][0] ) = adr(a[0][0]);
cont (b[0][0] ) = cont (a[0][0] ) = 1;
i = 1;
adr(b[1][1]) = adr (b[1][1] ) + (1-0)*4* lg (int) + 1*lg (int) =
= adr (b[0][0] ) +6*lg (int)
cont (b[1][1]) = 6;
i = 2;
adr (b[2][2] ) = adr (b [0][0] ) + (2-0) * 4* lg (int) + 2*lg (int) =
= adr (b[0][0]) + 11*lg (int);
cont (b[2][2] ) = 11;
i = 3;
adr (b[3][3] ) = adr (b [0][0]) + 16*lg (int);
cont (b [3][3] ) = 16;
i = 4;

16
adr (b[4] ) = adr (b[0][0} + (4-0) 4* lg*(int) + 4*lg (int) =
= adr (b[0][0] ) + 21* lg (int)
cont (b [4][4] ) = 7;

Valoarea obţinută S = 1 + 6 + 11 + 16 + 7 = 41.


Cunoaşterea modului de adresare şi a modului de transmitere a parametrilor, permite
interpretarea cu rigurozitate a oricărui rezultat.
Memorarea unei matrice numai ca formă liniarizată, neînsoţită de informaţia dedusă din
structura arborescentă, implică efectuarea calcului de adresa ori de câte ori este accesat un
element al matricei.
În cazul în care nodului rădăcină a i se rezervă o zonă de memorie, iar nodurilor a[0],
a[1], . . . , a[m] li se asociază, de asemenea, zona ce se iniţializează adecvat, regăsirea elementelor se
efectuează folosind calcule de deplasări.
De la relaţia:

adr (a[i][j] = adr (a[0][0] ) + [ (i – 0)*n + j ]*lg (int) (1.28)

se ajunge la:

adr (a [i][j] ) = adr (a [i] ) + depl( a [i][j], a [i][0]) (1.29)

În cazul în care matricea este densă sau încărcată sau are puţine elemente nule sau este
complet încărcate, dar are elemente ce conţin valori particulare, se efectuează o tratare diferenţiată.
Matricele simetrice, au proprietatea:

cont (a [i][j]) = cont (a [j][i]) (1.30)

ceea ce conduce la ideea memorării numai a elementelor de pe diagonala principală şi de sub aceasta
sau de deasupra ei.
Pentru o matrice simetrică a[n][n], cu n*n  elemente, se impune memorarea a n*(n+1) /2
dintre acestea.
Dacă sunt memorate elementele a[i][j], j ≥ i, i = 0, 1, 2, . . ., n, j=i, i+1, . . ., n, astfel:

a00 a01 ... a0n a11 ... a1n a22 ... a2n ... ann

n n-1 n-2 1

Fig. 1.10. Reprezentarea elementelor de deasupra diagonalei principale

adr(a [i][j]) = adr ([0][0]) + [(n-0) + (n-1) + . . . (n-i+1) + (j-0) ]*lg (int)
adr (a [i][j] ) = adr (a [0][0] ) + f (i, j, n) * lg (int)

 f(i, j, n) reprezintă funcţia de calcul a deplasării faţă de elementul a[i][0]  a elementului a


[i][j]. Funcţia f (i, j, n) se deduce prin inducţie.
Matricea cu linii sau coloane identice, sau cu elemente identice, permite o astfel de memorare
care conduce la o economisire a spaţiului de memorie.
Astfel, pentru matricea:

1 1 1 1
 A 1 1 1 1
1 1 1 1

17
se impun definirile:
- nume matrice;
- număr linii;
- număr coloane;
- valoarea elementului repetat, respectiv 1.
În cazul în care matricea are conţinutul:
1 2 3 4 5 0
1 2 3 4 5 0
 A
6 8 3 2 6 1
2 2 2 2 2 2

aceasta are reprezentarea:


a

a[0] a[1] a[2] a[3]

1 3 4 5 0 6 8 3 2 6 1 2 *
2
Fig. 1.11. Reprezentarea grafică a matricei A

Matricele diagonale sunt matricele în care elementele componente apar sub forma:

1 0 0 0 0
0 7 0 0 0
 A 0 0 5 0 0
0 0 0 8 0
0 0 0 0 2

În acest caz, forma de memorie adecvată, în condiţiile în care în urma prelucrării matricei A,
aceasta nu-şi schimbă structura, este vectorul.
Un alt tip matrice, cu modalităţi specifice de prelucrare, este matricea tridiagonală, care are
forma:

1 4 0 0 0 0
2 8 1 0 0 0
T  0 2 7 3 0 0
0 0 6 5 9 0
0 0 0 0 8 1

18
Elementele nenule se memorează într -un masiv unidimensional, iar accesarea lor se efectuează
după formula:

adr (t[i][ j] ) = adr (t[0][0] ) + [2* (i – 0 ) + j – 0 ]* lg (int) (1.31)

unde:
i = 0, 1, . . ., n;
 j =0, 1, pentru i = 1;
 j = i –  1, i, i + 1, pentru 1 < i < n;
 j = n –  1, n, pentru i = n.
Matricea triunghiulară are elementele nenule fie deasupra, fie sub diagonala principală, iar
stocarea în memorie este asemănătoare matricei simetrice.
Pentru matricea triunghiulară  A cu elementele nenule sub diagonala principală, adresa
elementului a[i, j] este:

adr (a[i][j] ) = adr (a [0][0] ) + [i* (i – 0) /2 + (j – 0) ] * lg (int) (1.32)

Construirea masivului unidimensional se realizează în secvenţa:

k = 0;
for (i=0; i<n; i++)
for (j=i; j<n; j++)
{
k = k + 1;
s [k] = T [i][j];
}

dacă elementele nenule sunt deasupra diagonalei principale sau în secvenţa:

k = -n*(n+1)/2;
for (i=n-1; i >= 0; i--)
for (j=i; j>=0; j--)
{
s [k] = T[i][j];
k = k –  1;
}

dacă elementele nenule sunt sub diagonala principală.


În toate cazurile, se urmăreşte atât reducerea lungimii zonei de memorie ocupată, cât şi
creşterea vitezei de acces la componente. Pentru aceasta, mai întâi matricele sunt memorate în
extenso, ca mai apoi după analiza componentelor să se deducă dacă matricele sunt simetrice sau dacă
au linii sau coloane constante, cazuri în care se fac alocări adecvate.
Operaţiile cu matrice se efectuează cu algoritmi care iau în considerare forma efectivă în care
s-a făcut memorarea, existând posibilitatea trecerii rezultatului la un alt tip de stocare în memorie.
De exemplu, dacă  A este o matrice triunghiulară cu elemente nenule sub diagonala principală
şi  B este o matrice triunghiulară cu aceeaşi dimensiune ca matricea  A, dar cu elementele nenule
deasupra diagonalei principale, în cazul adunării celor două matrice, rezultă o matrice cu toate
elementele nenule, ce este stocată după regulile acestui tip.
Ca şi în cazul masivelor unidimensionale, masivele bidimensionale se iniţializează la definire,
 prin tastarea de valori şi prin atribuire.
În secvenţa:

19
for (i=0; i<n; i++)
for (j=0; j<n; j++)
if (i = = j)
a[i][j] = 1;
else
a[i][j] = 0;

se construieşte matricea unitate prin instrucţiuni de atribuire.


În secvenţa:

int a[10][10], m, n;
…………………
for (i=0; i<n; i++)
for (j=0; j<m; j++) {
cout<<”Element ”<<i<<”,”<<j<<”:”;
cin>>a[i][j]);
}

se realizează iniţializarea unei matrice de la tastatură.

1.4. Operaţii cu masive bidimensionale

Pentru masivele bidimensionale se definesc operaţii precum:


- inserarea unei linii sau unei coloane;
- ştergerea unei linii sau a unei coloane;
- interschimbul a două linii sau a două coloane.
Se consideră clasa Matrix cu următoarele atribute:
m – numărul de linii ale matricei;
n – numărul de coloane ale matricei;
a[ ][ ] –  valorile matricei.
Metoda:

void Matrix::interschimb (int i, int j)


{
int aux;
for (int K=0; K<n; K++)
{
aux=a[i][K];
a[i][K]=a[j][K];
a[j][K]=aux;
}
}

rezolvă interschimbul dintre liniile i şi j ale masivului bidimensional a.


Ştergerea liniei  K   dintre un masiv bidimensional având m linii şi n  coloane este
efectuată de funcţia:

void Matrix::stergem(int K)
{

20
int i, j;
for (i=K-1; i<m; i++)
for (j=0; j<n; j++)
a[i][j]=a[i+1][j];
m --;
}

Liniarizarea unui masiv bidimensional este realizată prin funcţia:

void Matrix::linerat(int b[ ])
{
int i, j, K = 0;
for (i=0; i<m; i++)
for (j=0; j<n; j++)
b[K++]=a[i][j];
}

Operaţia revine a pune în corespondenţă elementele unui masiv bidimensional cu elementele


unui masiv unidimensional.

a [0] a [1] a [2]

 b

Fig. 1.12. Punerea în corespondenţă a unei matrice cu un vector 

Operaţiile cu masive uni-, bi- şi multidimensionale sunt necesare atunci când se lucrează cu
colectivităţi omogene înzestrate cu caracteristici omogene, care se supun operaţiilor de agregare prin
însemnare.
Producţiile înregistrate pe intervale de timp derivate unei mulţimi de clienţi pentru un anumit
 produs se regăsesc în modele sub forma unui masiv bidimensional cu atâtea linii câţi clienţi sunt şi
atâtea coloane câte intervale sunt luate în considerare.
Se impune realizarea unei biblioteci de funcţii pentru prelucrări cu masive multidimensionale.
În cazul matricelor care au mai puţin de 30% elemente nenule se lucrează cu matrice rare, cărora le
corespund reprezentări destinate care ameliorează gradul de ocupare.
Acest indicator este dat ca raport între elementele iniţializate sau referite şi totalul elementelor
alocate dintr-un masiv.

21
1.5. Masive multidimensionale

Utilizarea masivelor multidimensionale este impusă de tendinţa unificării grupărilor de date


omogene, după diferite criterii într -o singură mulţime, cu posibilitatea reconstituirii
apartenenţei elementelor la fiecare grupă.
Dacă într -o secţie sunt 10 muncitori, iar întreprinderea are 6 secţii şi se memorează salariile
lunare ale acestora pentru 7 ani consecutivi, intuim stocarea volumului impresionant de salarii folosind
un masiv cu 4 dimensiuni, definit prin:

int salarii[10][6][12][7];

iar elementul:

salarii [ i ][ j ][ k ][ h ]

identifică salariul muncitorului i, din secţia j, obţinut în luna k  a anului h.


Obţinerea unui nivel mediu:

 salariu[.][j][k][h]
 salariu[.][.][k][h]
 salariu[.][.][.][h]
 salariu[.][.][.][.]

sau a oricărei variante de grad de cuprindere a elementelor colectivităţii, este o problemă de:
- definire a masivelor multidimensionale, cu număr de dimensiuni mai redus cu o unitate,
cu două unităţi, cu trei şi în final cu patru şi apoi iniţializarea lor;
- efectuarea calculelor în cadrul unei secvenţe de structuri repetitive incluse.
De exemplu, pentru calculele mediilor specificate mai sus se definesc variabilele:

float mjkh_salariu[6][12][7];
float mmkh_salariu[12][7];
float mmmh_salariu[7];
float mmmm_salariu;

Variabilele de control sunt definite pe domeniile:

i [0, 9] N k   [0,11] N (1.33)


 j [0, 5] N h [0, 6] N (1.34)

Problema iniţializării se simplifică atunci când masivele multidimensionale se pun în


corespondenţă cu un masiv unidimensional, acesta din urmă fiind iniţializat în cadrul unei singure
structuri repetitive, în care variabila de control are ca domeniu :

[1, 10 * 6 * 12 * 7] N (1.35)

Modelul grafic asociat masivului multidimensional rămâne în continuare structura


arborescentă, cu deosebirea că numărul de nivele este mai mare şi depinde de numărul de dimensiuni.
Dacă la variabila unidimensională numărul nivelelor este 2, la variabila bidimensională
numărul nivelelor este 3, se deduce că la o variabila p-dimensională, numărul nivelelor este p+1.
Dacă se consideră masivul a, p-dimensional, având pentru fiecare dimensiune definirea:

22
a[n1][n2][…][np]

modelul grafic este:


a

a1 a2 … an1

a11 a12 ... a1,n2 …

a111 a112 ... a1,1,n3 …

. . . . . . . . . . . . . . . . . . . .

a11 . . . 1 a11 . . . 2 a1,1, . . . np …

Fig. 1.13. Modelul grafic al unui masiv p-dimensional

Localizarea unui element oarecare din masivul p-dimensional de tipul T i, se efectuează după
formula:

adr(a[i1 ][i2 ][ ][i p ])= adr(a[l][l][ ][0])+f(i1 ,i2 ,...,i p; n1 ,n2 ,....,n p )*lg(T  )


i (1.36)

Spre exemplificare, pentru p = 3 se obţine:

adr(a[i][j][k])=adr(a[0][0][0])+((i-0)*n2*n3+ (j-0)*n2+k)*1g(int) (1.37)

Dacă reprezentarea în memorie corespunde paralelipipedului:


a[n1][0][n3] a[n1][n2][n3]

a[n1][0][0] a[n1][n2][0]

i
a[i][n2][0]

 j

Fig. 1.14. Reprezentarea geometrică a unui masiv tridimensional


Şi masivele multidimensionale sunt tratate ca matrice rare, numărul vectorilor
"informaţionali" care conţin indicii pentru corecta localizare a valorilor nenule, sunt în număr egal cu
numărul dimensiunilor masivului de bază. Asocierea arborescenţei şi a posibilităţii de adresare,
respectă aceleaşi reguli ca în cazul masivului bidimensional.
Când se fac stocări de informaţii referitoare la un grup de elemente, este corectă abordarea în
care se specifică o serie de informaţii precum:
-  poziţia în memorie a primului element;
-  poziţia în memorie a ultimului element;
- numărul de elemente sau poziţia elementelor în substructura reală a masivului.
De exemplu, dacă într -un masiv tridimensional, în secţiunea corespunzătoare lui i= 3, avem pe
linia a doua elementele:

23
(l 0 2 0 0 4)

asociem arborescenţa:
a

a[0] a[1] a[2] ……….. a[3]

………..
a[2][0] a[2][1] 0 2 5 …….  a[2][n]

0 1 3
Fig. 1.15. Structura arborescentă a unui masiv tridimensional

Variabila a[2][1] are o structură convenabil definită, care să poată memora adresele primei şi
respectiv ultimei componente de pe nivelul imediat inferior, precum şi poziţiile elementelor nenule din
linie.
Este convenabilă şi definirea:

. 3 1 3 6 .

Fig. 1.16. Conţinutul elementului a[2][1] din masivul a

în care, prima informaţie de după adresa primei componente indică numărul componentelor memorate
la nivelul inferior.
Este de dorit ca multitudinea de informaţii suplimentare care vin să descrie structura, fie să
menţină un grad de ocupare a memoriei cât mai redus, fie să permită un acces mult mai rapid la
informaţiile dintr -un
-un masiv multidimensional.
Modul de a pune problema masivelor ca structuri de date omogene, prin folosirea de exemple,
 pentru programatori nu reprezintă dificultate în înlocuirea valorilor particulare cu parametrii care
conduc la formule generale pentru fiecare clasă de probleme în parte.

Teme de control

1. Să se citească informaţiile dintr -un-un masiv unidimensional de numere


întregi şi să se parcurgă realizându-se suma elementelor.
2. Să se construiască masivul bidimensional format din vector de vectori şi
să se iniţializeze valorile elementelor coloană cu coloană.

Bibliografie

Ion IVAN, Marius POPA, Paul POCATILU - Structuri de date, Vol. 1 si 2, Editura ASE,
Bucuresti, 2008.

24
2. Matrice rare

Obiectivele unităţii de învăţare

După studierea acestei unităţi de învăţare, studenţii îşi vor însuşi


cunoştinţe teoretice asupra noţiunii de matrice rară şi vor deprinde
abilităţi practice de lucru cu acestea. Studenţii vor fi capabili să realizeze
implementarea acestor structuri în una din cele două abordări, respectiv
trei vectori alocaţi dinamic sau liste de liste.

2.1. Concepte de bază

Matricele rare îşi găsesc aplicabilitatea în modelarea unor procese de natură


industrială, economică, tehnică, socială etc. Capitolul de faţă îşi propune să trateze
modalităţile de reprezentare în structuri de date a matricelor rare, precum şi
 principalele operaţii matriceale implementate într-un limbaj orientat pe obiecte. În final este
 prezentată o aplicaţie
aplicaţie concretă –  estimarea
 estimarea parametrilor unei regresii statistice.
În rezolvarea multor probleme de natură economică, tehnică, socială, a diverselor
 probleme de optimizare, precum şi în modelarea unor procese industriale şi tehnologice este
necesar să se determine modelul matematic care descrie funcţionarea procesului respectiv.
Descrierea acestor sisteme fizice conduce la obţinerea unor modele matematice care fie în
mod direct, prin modelare, fie prin metoda de rezolvare implică sisteme de ecuaţii algebrice
liniare sau probleme de programare liniară a căror matrice a coeficienţilor este rară (sparse),
în sensul că ponderea elementelor nenule în totalul elementelor matricei este mică.
Din punct de vedere practic trebuie remarcat faptul că analiza sistemelor mai sus
amintite conduce la obţinerea unor modele matematice de mari dimensiuni care implică
sisteme de ecuaţii algebrice liniare de mii de ecuaţii, pentru a căr or rezolvare sunt necesare
resurse mari de memorie şi timp de calcul. În multe cazuri practice, cum sunt sistemele în
timp real, timpul de calcul este o resursă critică, nefiind permis să depăşească o valoare limită.
Modelele matematice ale proceselor real e implică un număr foarte mare de variabile şi
restricţii care prezintă fenomenul de raritate  sparsity
, sparsity, adică o slabă interconectare a
elementelor sale. Luarea în consideraţie a fenomenului de raritate furnizează un nou mod de
abordare foarte eficient, ce implică în dezvoltarea aplicaţiilor informatice folosirea unor
structuri de date speciale, care să conducă la reducerea resurselor de memorie şi a timpului de
calcul.
În general, o matrice (n, n) - dimensională este rară atunci când conţine un număr mic
de elemente nenule , adică n . Cantitativ, matricele rare sunt caracterizate de ponderea
2

numărului de elemente nenule în totalul de elemente, pondere ce defineşte gradul de umplere


al matricei. În apl icaţiile curente se întâlnesc matrice rare cu grade de umplere între 0,15% şi
3%.

2.2. Memorarea matricelor rare

Se consideră matricea:

25
1 0 0 0 0
0 0 2 0 4
 A   (2.1)
0 0 0 0 0
0 1 0 0 0

Matricea  A este un exemplu de matrice rară, ea conţinând 16 elemente nule din


totalul de 20.
Se defineşte gradul de umplere, densitatea, unei matrice prin raportul dintre
numărul elementelor nenule şi numărul total al elementelor sale:

 p
G 100 (%)  
100 (2.2)
n m

unde:
 p  – numărul
 – numărul de elemente nenule;
n  – numărul
 – numărul de linii;
m  – numărul
 – numărul de coloane.
În general se acceptă că o matrice este rară dacă densitatea sa este de cel mult 3%.
Densitatea matricei A este G( A) 20% , ea fiind prezentată aici în scopul ilustrării
conceptului de matrice rară.
Structura de date clasică folosită pentru manipularea matricelor, tabloul de dimensiune
(n, m) alocat la compilare, se dovedeşte a fi ineficientă în cazul în care matricea este rară. Un
 prim avantaj este legat de folosirea neeconomică a spaţiului de memorie prin alocarea de zone
mari pentru memorarea elementelor nule, care nu sunt purtătoare de informaţie. Ocuparea
unor zone de memorie cu elemente nule nu se justifică deoarece acestea nu contribuie la
formarea rezultatului operaţiilor cu matrice, adunare, înmulţire etc., conducând totodată şi la
mărirea duratei de realizare a acestor operaţii prin ocuparea procesorului cu adunări şi
înmulţiri scalare cu zero. Acest inconvenient se manifestă cu atât mai pregnan t cu cât
dimensiunea matricei este mai mare.
Prin urmare, pentru probleme de dimensiuni mari, s- a căutat găsirea unor modalităţi de
reprezentare compactă a matricelor rare, în care să se renunţe la memorarea elementelor nule.
În acest caz este necesar ca tehnicile de memorare să încorporeze pe lângă elementele nenule
şi mijloacele de identificare a poziţiilor acestor elemente în matrice.
Sunt prezentate în continuare câteva posibilităţi de memorare compactă a matricelor
rare  MR. Se face, de asemenea, o analiză a oportunităţii folosirii fiecărei tehnici în parte, în
funcţie de densitatea matricei.
identifi care binară se bazează pe natura binară a sistemului de calcul,
 Memorarea prin identificare
constând în memorarea numai a elementelor nenule ale matricei într-o zon ă primară ZP  având având
tipul de bază corespunzător tipului elementelor matricei şi dimensiunea egală cu numărul
elementelor nenule.
Structura matricei este indicată printr -o secvenţă binară memorată într -o zonă
secundară  ZS .
Matricea A prezentată anterior se memorează astfel:
astfel:
 Zona primară:

Locaţie  1 2 3 4
Valoare 1 -2 4 -1
Fig. 2.1. Structura ZP pentru matricea A

26
 Zona secundară:

Locaţie 1 5 6 10
Valoare 1 0 0 0 0 0 0 1 0 1
Locaţie 11 15 16 20
Valoare 0 0 0 0 0 0 1 0 0 0
Fig. 2.2. Structura ZS pentru matricea A

Matricea A a fost memorată în ordinea liniilor, o altă posibilitate de memorare fiind în


ordinea coloanelor. Pentru a reduce spaţiul ocupat de zona secundară se poate implementa
soluţia dată de memorarea la nivel de bit a valorilor acesteia.
Dacă matricea  B  cu dimensiunea (m, n)  are densitatea G şi dacă tipul de bază al
matricei, respectiv tipul fiecăruia dintre elemente nenule ale matricei, este reprezentat printr -
un cuvânt de b octeţi, atunci zona primară va necesita m*n*G cuvinte de b octeţi iar zona
secundară (m*n)/(8*b) cuvinte. Numărul total de cuvinte necesare memorării matricei  B prin
intermediul celor două zone este

 DMR1 = m*n*G + (m*n)/(8*b) (2.3)

Întrucât pentru memorarea matricei în forma clasică sunt necesare  DM = m*n  cuvinte,
raportul dintre cerinţele de memorie ale structurii de mai sus şi a celei standard este:

 DMR1 1
c1 G   (2.4)
 DM  8b

În relaţia anterioară s-a considerat că memorarea zonei secundare se face la nivel de


 bit.
Considerând că elementele matricei  A sunt reale şi se reprezintă pe 4 octeţi, rezultă:

1
c1 A 0,2 0,23   (2.5)
32

ceea ce indică că memorarea matricei  A conform acestei structuri ocupă de aproximativ patru
ori mai puţină memorie decât cea standard.
Egalând c1 1 se determină limita superioară a densităţii unei matrice pentru care
această structură necesită mai puţină memorie decât cea standard:

1
Glim 1   (2.6)
8b

Pentru matricea A:

1
Gli m 1 0,96 96%   (2.7)
32

Această structură de memorare diferă de abordări prin faptul că în zona secundară este
alocată memorie şi pentru elementele nule ale matricei. Structura este mai puţin eficientă

27
 pentru matricele de mari dimensiuni foarte rare. Principala dificultate constă în complexitatea
 programelor de implementare a operaţiilor matriciale.
O altă modalitate de memorare prin identificare binară se obţine prin modificarea
informaţiilor din zona secundară. Această zonă va conţine pe jumătăţi de cuvânt
indicii de coloană a elementelor nenule din matrice, precum şi anumite informaţii de
control pentru identificarea rapidă a poziţiei elementelor nenule în matrice. Structura
 ZS  pe cuvinte este următoarea:

Tabel 2.1. Structura ZS pe cuvinte


Numărul Jumătatea stângă Jumătatea dreaptă
cuvântului
1  Numărul de linii  Numărul de coloane
2  Numărul de elemente nenule
3  Numărul de elemente nenule în linia  Numărul de elemente nenule în linia
1 2
4  Numărul de elemente nenule în linia  Numărul de elemente nenule în linia
3 4
… … …
k  Numărul de elemente nenule în linia  Numărul de elemente nenule în linia
m-1 m
k+1 Indicele de coloană al primului Indicele de coloană al celui de -al
element memorat doilea element memorat
k+2 Indicele de coloană al celui de -al etc.
treilea element memorat
… … …
 j … Indicele de coloană al ultimului
element memorat

Pentru matricea A, zona secundară  ZS  are structura din figura 2.3.

Locaţie 1 2 3 4 5 6
Valoare 4 5 4 1 2 0 1 1 3 5 2
Fig. 2.3. Structura ZS pentru matricea A

În reprezentarea din figura 2.3 s- a considerat că elementele nenule sunt reprezentate pe


4 octeţi astfel că o jumătate de cuvânt în zona secundară se reprezintă pe 2 octeţi. Prin
structura de memorare prezentată mai sus se memorează matrice a căror dimensiune maximă
este de 9999 de linii sau coloane cu numărul maxim de elemente nenule memorate egal cu 10 8
 – 1. Se face observaţia că în cazul matricelor pătrate în primul cuvânt din  ZS   se va memora
dimensiunea matricei.
 Numărul total de cuvinte necesare zonei secundare este egal cu

(5 m m * n * G) / 2   (2.8)

valoarea fiind rotunjită la cel mai mare întreg. Numărul total de cuvinte necesar memorării
unei matrice prin intermediul celor două zone ZP şi ZS  este egal cu

 DMR2 = (5 m 3 * m * n * G) / 2   (2.9)

28
Raportul dintre cerinţele de memorie ale acestei structuri de identificare binară şi a
celei standard este:

3 G 5 m
c2   (2.10)
2 2*m*n

Pentru o matrice pătrată (m=n), egalând c2 = 1 şi trecând la limită pentru m


rezultă valoarea maximă a densităţii unei matrice rare pentru care structura prezentată este
eficientă:

2 5 m
Glimm 1 0,666 66,6%   (2.11)
3 2m 2

În relaţia anterioară se ajunge la acelaşi rezultat în cazul unei matrice nepătratică


 pentru care se trece la limită pentru n şi m .
Pentru o matrice rară de dimensiune (100, 100), cu o medie de 66 elemente nenule  pe
linie, structura de mai sus necesită un total de 6600 + (5 + 100 + 6600)/2 = 9952 cuvinte, cu
0,6% mai puţin decât 10.000 cuvinte necesare pentru memorarea standard. Întrucât densitatea
elementelor nenule ale unei matrice rare este de obicei între 1% şi 3%. Structura se dovedeşte
a fi deosebit de eficientă.
 Memorarea compactă aleatoare constă în utilizarea unei zone primare  ZP , conţinând
numai elementele nenule ale matricei şi a două zone secundare conţinând indicii de linie şi de
coloană corespunzătoar e elementelor nenule.
Deoarece fiecare element nenul al matricei este identificat individual, este posibil ca
matricea să fie memorată în ordine aleatoare. Matricea A se memorează astfel:

Locaţia 1 2 3 4
Valoare 1 -2 4 -1
Indice linie 1 2 2 4
Indice coloană 1 3 5 2
Fig. 2.4. Model de memorare compactă aleatoare a matricei A

Avantajele memorării compacte aleatoare constau în faptul că noile elemente nenule


ale matricei sunt adăugate la sfârşitul zonelor de memorare fără  a afecta celelalte elemente,
 precum şi o manevrabilitate rapidă a datelor. În cazul matricelor simetrice această structură de
memorare este simplificată prin memorarea numai a elementelor nenule de deasupra
diagonalei principale, precum şi a elementelor nenule situate pe această diagonală.
 Numărul total de cuvinte necesare memorării unei matrice de dimensiune (m, n) este
în acest caz

 DMR3 = 3*m*n (2.12)

Raportul dintre cerinţele de memorie ale acestei structuri şi a celei standard este:

c3 3 G   (2.13)

Egalând relaţia anterioară cu unitatea se determină valoarea limită a densităţii matricei


rare pentru care această structură este eficientă, Glim 33,3% .

29
În structura din figura 6.4, pentru identificarea elementelor nenule ale matricei rare au
fost folosite două zone secundare corespunzătoare indicelui de linie şi de coloană. Se prezintă
în continuare o altă posibilitate de memorare în care se va utiliza o singură zonă secundară de
dimensiune egală cu numărul de elemente nenule ale matricei, conţinând simultan informaţii
asupra indicilor de linie şi de coloană.
Astfel, fiecărui element din zona primară i se ataşează în zona secundară un număr
întreg din care se determină indicii de linie şi de coloană. Dacă elementul aij 0   este
memorat în locaţia k a zonei primare atunci în zona secundară se va memora un indice agregat
ig a cărui valoare este dată de relaţia

ig = i+(j-1)*n (2.14)

unde n este numărul de coloane a matricei. Acest număr este suficient pentru iden tificarea
elementului în matrice.
Utilizând acest artificiu, matricea A se memorează astfel:

Locaţia 1 2 3 4
Valoare 1 -2 4 -1
Indice agregat, ig   1 12 22 9
Fig. 2.5. Model derivat de memorare compactă a matricei A

Pentru a regăsi indicele de linie şi de coloană al oricărui element memorat în locaţia k 


se utilizează următoarea tehnică de calcul:
- coloana  j este obţinută prin relaţia:

 j ig(k)/n (2.15)

- linia i este determinată prin relaţia:

i = ig(k) –  ( j – 1 ) n (2.16)

Avantajul acestei structuri de memorare constă în faptul că necesită mai puţină


memorie decât cea precedentă, fiind în schimb mai puţin rapidă în ce priveşte manevrarea
datelor.
 Numărul total de cuvinte necesar memorării matricei este

 DMR4 = 2*m*n*G (2.17)

Raportul dintre cerinţele de memorie ale acestei structuri şi a celei standard este:

c4 2G   (2.18)

Valoarea limită a densităţii matricei pentru care această structură este eficientă este G
= 50%.
 Memorarea compactă sistematică  presupune că elementele nenule ale unei matrice
rare sunt memorate într-o anumită ordine, respectiv pe linii sau pe coloane. În acest
caz nu este necesar să se memoreze în zonele secundare indicii de linie, respectiv de
coloană. Pentru o memorare în ordinea liniilor, ne mai sunt necesari indicii de linie, însă se
cere specificarea începutului fiecărei linii.

30
Şi în acest caz există mai multe structuri de memorare. Cea prezentată în continuare
este caracterizată prin faptul că utilizează o singură zonă secundară ZS , care conţine indicii de
coloană ale elementelor nenule din matricea considerată, precum şi elemente false care indică
începutul fiecărei linii şi sfârşitul memorării întregii matrice. De exemplu, un element zero în
 ZS  marchează prezenţa unui element fals şi acesta specifică în  ZP  numărul liniei elementelor
de la dreapta locaţiei. Sfârşitul matricei este marcat prin prezenţa în  ZP  a unui element fals cu
valoarea zero.
Pentru matricea A, memorarea în această formă este următoarea:

Locaţia 1 2 3 4 5 6 7 8
ZP 1 1 2 -2 4 4 -1 0
ZS 0 1 0 3 5 0 2 0
Fig. 2.6. Model de memorare compactă sistematică a matricei A

Pentru această structură de memorare numărul maxim de cuvinte necesar pentru a


reţine o matrice rară de dimensiune (m, n) este

 DMR5 = 2*(m*n*r+m+1) (2.19)

Raportul de memorare este:

2 * (m 1)
c5 2*G   (2.20)
m*n

Se constată că structura este eficientă  pentru memorarea matricelor rare cu o densitate


a elementelor nenule de maximum 50%.
 Memorarea cu ajutorul listelor  reprezintă o extensie a memorării compacte
aleatoare. În timpul operaţiilor de inversare a matricelor rare, noi elemente nenule
sunt continuu generate, iar altele sunt anulate şi deci structurile de memorare trebuie
să fie capabile să execute aceste modificări într -un mod eficient. De aceea structurile de
memorare bazate pe această tehnică sunt folosite pentru memorarea şi manipularea matricelor
rare de mari dimensiuni.
Structura propusă utilizează o zonă principală  ZP   pentru memorarea elementelor
nenule şi trei zone secundare:

 ZSL  –  memorarea indicilor de linie ale elementelor nenule;


 ZSC   – indicii de coloană;
 ZSU   – memorarea adresei următorului element al matricei.

Matricea A se memorează după cum urmează:

Locaţia  1 2 3 4
ZP 1 -2 4 -1
ZSL 1 2 2 4
ZSC 1 3 5 2
ZSU &2 &3 &4 NULL
Fig. 2.7. Model de memorare cu ajutorul listelor a matricei A

unde prin “&2” se înţelege adresa celei de -a doua locaţii.


Raportul dintre cerinţele de memorare ale acestei structuri şi a celei standard este:

31
c6 4*G   (2.21)

Prin urmare această structură de memorare este eficientă pentru memorarea matricelor
cu o densitate a elementelor nenule de maximum 25%.

2.3. Determinarea gradului de umplere al unei matrice rare

Pentru a deduce dacă o matrice este sau nu rară, se defineşte gradul de umplere al unei
matrice, notat cu p. În cazul în care p < 0,3*m*n, se consideră că matricea este rară.
Problema matricelor rare comportă două abordări:
- abordarea statică, în care alocarea memoriei se efectuează în faza de compilare;
aceasta presupune ca programatorul să cunoască cu o precizie bună numărul maxim al
elementelor nenule;
- abordarea dinamică, în care alocarea se efectuează în timpul execuţiei, caz în care nu
este necesară informaţia asupra numărului de elemente nenule; această abordare este
dezvoltată în partea destinată listelor.
Memorarea elementelor matricei rare, presupune memorarea indicelui liniei, a
indicelui coloanei şi, respectiv, valoarea nenulă a elementului.
Se consideră matricea:

0 0 6 0 0
7 0 0 0 0
 A   (2.22)
0 0 0 9 0
0 8 2 0 0

Gradul de umplere al matricei  A cu numărul de linii m = 4 , numărul de coloane, n=


5 şi numărul elementelor nenule k = 5 este:

5
G 0.25   (2.23)
5 4

Se definesc 3 vectori:
lin [ ]  – memorează poziţia liniei ce conţine elemente nenule;
col [ ]  – memorează poziţia coloanei ce conţine elemente nenule;
val [ ]  – memorează valoarea nenulă a elementelor.

Vectorii se iniţializează cu valorile:

Tabel 2.2. Valorile iniţiale ale vectorilor LIN, COL şi VAL


LIN COL VAL
1 3 6
2 1 7
3 4 9
4 2 8
4 3 2

32
Pentru efectuarea calculelor cu matrice rare definite în acest fel, un rol important îl au
vectorii  LIN, COL, iar pentru matricele rare rezultat se definesc vectori cu un număr de
componente care să asigure şi stocarea noilor elemente ce apar.
Astfel, pentru adunarea matricelor rare definite prin:

Tabel 2.3. Valorile matricei rare A


LIN_A COL_A VAL_A
1 1 -4
2 2 7
4 4 8

şi
Tabel 2.4. Valorile matricei rare B
LIN_B COL_B VAL_B
1 1 4
2 2 -7
3 2 8
4 1 5
4 3 6

rezultatul final se stochează în vectorii:

Tabel 2.5. Valorile matricei rare rezultat C


LIN_C COL_C VAL_C
1 1 0
2 2 0
3 2 8
4 1 5
4 3 6
4 4 8
? ? ?
? ? ?

Vectorii LIN_C, COL_C şi VAL_C au un număr de componente definite, egal cu:

 DIM (LIN_A) + DIM (LIN_A) (2.24)

unde DIM() este funcţia de extragere a dimensiunii unui masiv unidimensional:


Astfel, dacă:

int a[n-m]; (2.25)

atunci:

 DIM (a) = n - m+1 (2.26)

Fiind abordată problematica matricelor rare, în mod natural se produce eliminarea


elementelor nenule, obţinându-se în final:

33
Tabel 2.6. Conţinutul final al matricei rare C
LIN_C COL_C VAL_C
3 2 8
4 1 5
4 3 6
4 4 8
? ? ?
? ? ?
? ? ?
? ? ?

Prin secvenţe de program adecvate, se face diferenţa între definirea unui masiv
 bidimensional şi componentele iniţializate ale acestora, cu care se operează pentru rezolvarea
unei probleme concrete. Din punct de vedere al nivelului de umplere, tabelul 2.6 descrie o
matrice rară cu un grad de umplere egal cu

12
G= *100 = 37.5 % (2.27)
32

Situaţia evidenţiază ineficienţă în utilizarea spaţiului de memorie alocat.


De exemplu, vectorii  LIN_A şi LIN_B au 3, respectiv 5 componente în utilizare, dar la
definire au rezervate zone de memorie ce corespund pentru câte 10 elemente. Rezultă că
vectorul  LIN_C  trebuie definit cu 20 componente încât să preia şi cazul în care elementele
celor două matrice rare au poziţii disjuncte.
Din punct de vedere al criteriului minimizării spaţiului ocupat, această abordare nu
este eficientă deoarece presupune în cele mai multe situaţii alocarea de spaţiu care nu este
utilizat. Pentru a atinge acest obiectiv, implementarea unei clase asociate matricei rare va
defini vectori alocaţi dinamic, iar operaţiile aritmetice vor genera vectori rezultat cu grad de
umplere egal cu 100%.
În cazul operaţiilor de înmulţire sau inversare, este posibil ca matricele rezultat să nu
mai îndeplinească cerinţa de matrice rară.
În acest scop, se efectuează calculele cu matrice rezultat complet definite şi numai
după efectuarea calculelor se analizează gradul de umplere şi dacă acesta este redus, se trece
la reprezentarea matricei complete ca matri ce rară.
Funcţiile full( ) şi rar( ), au rolul de a efectua trecerea la matricea completă, respectiv
la matricea rară.
Funcţia full( ) conţine secvenţa:

for( i = 0; i < n; i++)


a [LIN_a[i]] [COL_a[i]] = val_a[i];

ce descrie iniţializarea elementelor matricei pe baza valorilor din vectori, iar funcţia rar( )
conţine secvenţa:

k =1;
for( i = 0; i < m; i++)
for( j = 0; j < n; j++)
if (a[i][j] != 0)

34
{
LIN_a[k] = i;
COL_a[k] = j;
val_a[k] = a[i][j];
k = k + i;
}

În cazul în care gradul de umplere nu este suficient de mic astfel încât matricea să fie
considerată rară, pentru memorare se utilizează o structură arborescentă care conţine pe
nivelul al doilea poziţiile elementelor nenule, iar pe ni velul al treilea valorile.
Astfel matricei:

7 3 5 0 0
0 2 4 8 0
 A   (2.28)
0 0 9 0 5
6 8 9 8 1

îi corespunde reprezentarea din figura 2.8.

a[0] a[1] a[2] a[3]

1 2 3 2 3 4 3 5 -1

7 3 5 2 4 8 9 5 6 … 1

Fig. 2.8. Model grafic al matricei A

Se elaborează convenţii asupra modului de stabilire a lungimii vectorului de poziţii,


fie prin indicarea la început a numărului de componente iniţializate, fie prin definirea unui
simbol terminal.
De asemenea. în cazul considerat s- a adoptat convenţia ca liniile complete să fie
marcate cu simbolul -1, fără a mai specifica poziţiile elementelor nenule, care sunt de fapt
termenii unei progresii aritmetice.
Liniarizarea masivelor bidimensionale conduce la ideea suprapunerii acestora peste
vectori. Deci, punând în corespondenţă elementele unei matrice cu elementele unui vector, se
 pune problema transformării algoritmilor, în aşa fel încât operând cu elementele vectorilor să
se obţină rezultate corecte pe ntru calcule matriceale.
Astfel, considerând matricea:

1 2 3 4 5
 A 6 7 8 9 10   (2.29)
11 12 13 14 15

35
 prin punerea în corespondenţă cu elementele vectorului b, să se obţină interschimbul între
două coloane oarecare k şi j ale matricei.

a00 a01 a02 a03 a04 a10 a11 a20 a21 a22 a23 a24
1 2 3 4 5 6 7 … 11 12 13 14 15
 b0 b1 b2 b3 b4 b5 b6 b10 b11 b12 b13 b14
Fig. 2.9. Punerea în corespondenţă a matricei A cu vectorul b

Dacă matricea are  M linii şi N coloane şi elemente de tip int , atunci adresa elementului
a[i][j] este dată de relaţia

adr(a[i][j]) = adr(a[0][0] ) + ( (i-0 ) * N+j ) * 1g(int) (2.30)

iar din modul în care se efectuează punerea în corespondenţă a matricei  A  cu vectorul b,
rezultă:

adr(b[0]) = adr(a[0][0]) (2.31)

Pentru o matrice liniarizată, adresa elementului a[i][j] în cadrul vectorului este dată de relaţia

adr(a[i][j]) = adr(b[0] )+( (i-0) * N+j ) * lg(int) = adr(b[(i-0) * N+j]) (2.32)

Dacă se consideră problema interschimbării valorilor coloanelor  j şi k   pentru o matrice


liniarizată atunci secvenţa de înlocuire a coloanelor 

for( i = 0; i < M; i++)


{
c = a[i][j];
a[i][j] = a[i][k];
a[i][k] = c;
}

este înlocuită prin secvenţa:

for( i = 0; i < M; i++)


{
c = b[(i-0) * N+j];
b [(i-0) * N+j] = b[(i-0) * N+k];
b[(i-0) * N+k] = c;
}

Transformarea algoritmilor de lucru cu masive bidimensionale în algoritmi de lucru cu


masive unidimensionale este benefică deoarece nu se mai impune cerinţa de transmitere ca
 parametru a dimensiunii efective a numărului de linii, dacă liniarizarea se face pe coloane,
respectiv a numărului de coloane, dacă liniarizarea se face pe linii.
În cazul matricelor rare, aceeaşi problemă revine la interschimbarea valorilor de pe
coloana a treia dintre elementele corespondente ale coloanelor k  şi  j cu posibilitatea inserării
unor perechi şi, respectiv, ştergerii altora.

36
Pentru generalizare, un masiv n-dimensional rar, este reprezentat prin n + 1 vectori,
fiecare permiţând identificarea coordonatelor elementului diferit de zero, iar ultimul stocând
valoarea acestuia.
În cazul în care se construieşte o matrice booleană ce se asociază matricei rare, o dată
cu comprimarea acesteia se dispun elementele nenule într-un vector. Punerea în
corespondenţă a elementelor vectorului are loc în acelaşi moment cu decomprimarea matricei
 booleene şi analiza acesteia.
De exemplu, matricei :

8 0 0 0 4 0
3 3 0 0 0 0
 A   (2.33)
0 0 0 1 7 0
5 6 9 0 0 0

se asociază matricea booleană:

1 0 0 0 1 0
1 1 0 0 0 0
 B   (2.34)
0 0 0 1 1 0
1 1 1 0 0 0

care prin compactare, ocupă primii 3 octeţi ai unei descrieri, urmaţi de componentele
vectorului:

C = ( 8, 4, 3, 3, 1, 7, 5, 6, 9) (2.35)

Compactarea este procedeul care asociază un bit fiecărei cifre din forma liniarizată a
matricei B.

2.4. Adunarea, scăderea şi transpunerea

Prin prisma caracterului dinamic al modului de alocare a memoriei şi a caracteristicilor


unice ale matricelor rare , adunarea acestor structuri de date presupune parcurgerea unei serii
 paşi:
- determinarea numărului de elemente nenule ale matricei sumă; din punctul de vedere
al operanzilor sunt definite două situaţii de realizare a sumei, cu elemente comune şi
cu elemente distincte; în cazul sumei a două elemente comune, se verifică dacă suma
acestora este zero, caz în care rezultatul nu este reţinut în matricea rară generată;
- alocarea memoriei corespunzătoare acestui număr pentru cele trei masive
unidimensionale;
-  parcurgerea celor două matrice pe linii sau pe coloane şi determinarea sumei.
Prin elemente comune au fost desemnate valorile caracterizate prin indici de linie şi de
coloană care sunt indentice în ambele matrice.
Pentru implementare s-a folosit suprascrierea operatorilor, tehnică ce oferă o mai mare
 putere de sugestie operaţiilor matr iceale implementate. Este prezentat în continuare operatorul
care implementează operaţia de adunare, structurată conform paşilor prezentaţi mai sus.

37
MatriceRara MatriceRara::operator +(MatriceRara & MR)
{
/* se determina dimensiunea matricei rezultat */

// se simuleaza suma si se contorizeaza numarul


// de sume zero si numarul de sume nonzero

MatriceRara rezMR;

if((this->m!=MR.m)||(this->n!=MR.n))
return rezMR;

int nrsz = 0, nrsnz = 0;


int i = 0, j = 0;
while((i<this->dim)&&(j<MR.dim))
{
if(this->linii[i]<MR.linii[j])
i++;
else
if(this->linii[i]>MR.linii[j])
 j++;
else
if(this->coloane[i]<MR.coloane[j])
i++;
else
if(this->coloane[i]>MR.coloane[j])
 j++;
else
if(this->valori[i]+MR.valori[j])
{
nrsnz++;
i++;
 j++;
}
else
{
nrsz++;
i++;
 j++;
}
}

int rezdim = this->dim+MR.dim-nrsnz-2*nrsz;


rezMR.dim = rezdim;
rezMR.m = this->m;
rezMR.n = this->n;

rezMR.coloane = new int[rezdim];


rezMR.linii = new int[rezdim];

38
rezMR.valori = new double[rezdim];

// se determina suma elementelor

int k=i=j=0;

while((i<this->dim)&&(j<MR.dim))
{
if(this->linii[i]<MR.linii[j])
{
rezMR.linii[k] = this->linii[i];
rezMR.coloane[k] = this->coloane[i];
rezMR.valori[k] = this->valori[i];
i++;
k++;
}

else
if(this->linii[i]>MR.linii[j])
{
rezMR.linii[k] = MR.linii[j];
rezMR.coloane[k] = MR.coloane[j];
rezMR.valori[k] = MR.valori[j];
k++;
 j++;
}
else
if(this->coloane[i]<MR.coloane[j])
{
rezMR.linii[k] = this->linii[i];
rezMR.coloane[k] = this->coloane[i];
rezMR.valori[k] = this->valori[i];
i++;
k++;
}
else
if(this->coloane[i]>MR.coloane[j])
{
rezMR.linii[k] = MR.linii[j];
rezMR.coloane[k] = MR.coloane[j];
rezMR.valori[k] = MR.valori[j];
k++;
 j++;
}
else
if(this->valori[i]+MR.valori[j])
{
rezMR.linii[k] = MR.linii[j];
rezMR.coloane[k] = MR.coloane[j];

39
rezMR.valori[k] = this-
>valori[i]+MR.valori[j];
k++;
 j++;
i++;
}
else
{
i++;
 j++;
}
}
if(i<this->dim)
for(;i<dim;i++,k++)
{
rezMR.linii[k] = this->linii[i];
rezMR.coloane[k] = this->coloane[i];
rezMR.valori[k] = this->valori[i];
}
if(j<MR.dim)
for(;j<MR.dim;j++,k++)
{
rezMR.linii[k] = MR.linii[j];
rezMR.coloane[k] = MR.coloane[j];
rezMR.valori[k] = MR.valori[j];
}

return rezMR;
}

Pentru a minimiza numărul de parcurgeri ale celor două matrice rare, în acest caz sunt
necesare doar două parcurgeri, în metoda prezentată se porneşte de la ipoteza că matricea rară
este generată prin parcurgerea pe linii a matricei iniţiale. Acest lucru asigură o ordine între
elementele matricei rare şi permite identificare mai eficientă a elementelor comune. De
asemenea, este implementată o parcurgere simultană a celor două matrice. Elementele curente
din cele două matrice sunt analizate în ordine prin prisma valorii liniei şi a coloanei. În cazul
în care elementele se găsesc pe linii diferite, elementul care are valoarea liniei mai mică este
adăugat la rezultat şi se trece la următorul element din matricea respectivă. Dacă elementele
curente din cele două matrice prezintă aceeaşi valoare pentru linie, atunci se compară valoarea
coloanelor. Pentru elementele comune, se analizează rezultatul sumei şi se memorează doar
valorile nenule.
Implementarea operatorului de scădere este absolut similară cel ui de adunare, singura
diferenţă fiind aceea că în cazul elementelor comune se calculează diferenţa lor, în locul
adunării. O altă abordare, este dată de utilizarea sumei, negând anterior valorile matricei ce se
scade. Prin utilizarea operatorului MatriceRara MatriceRara::operator *(double valoare) ce
 permite înmulţirea matricei cu o valoare dată, scăderea se realizează prin supraîncărcarea
operatorului -.

40
MatriceRara MatriceRara::operator *(double valoare)
{
MatriceRara rezMR;
if(valoare)
{
rezMR = *this;
for(int i=0;i<rezMR.dim;i++)
rezMR.valori[i]*=valoare;
}
return rezMR;
}
MatriceRara MatriceRara::operator -(MatriceRara &MR)
{
return ((*this)+(MR*-1));
}

Transpunerea matricelor rare, prin intermediul operatorului !, este similară celei


efectuate pe structură tablou, constând în inversarea indicilor de linie şi coloană între ei.

MatriceRara MatriceRara::operator !()


{
MatriceRara rezMR= *this;
/* metoda transpune matricea */
for(int i=0;i<dim;i++)
{
int temp = rezMR.linii[i];
rezMR.linii[i] = rezMR.coloane[i];
rezMR.coloane[i] = temp;
}
return rezMR;
}

O altă metodă de a realiza transpunerea este dată de inversarea pointerilor pentru


masivele de întregi reprezentând liniile, respectiv coloanele elementelor nenule

MatriceRara MatriceRara::operator !()


{
MatriceRara rezMR= *this;
int * temp = rezMR.coloane;
rezMR.coloane = rezMR.linii;
rezMR.linii = temp;
return rezMR;
}

41
2.5. Înmulţirea şi inversarea matricelor rare

Pentru înmulţirea  matricei rare  A, (m, l) dimensională, cu matricea rară  B, (l, n)
dimensională, se utilizează procedura standard, având în vedere că metoda  getValoareElement 
şi operatorul (i,j) permit accesul direct la elementele matricei rare.
Pentru a genera matricea rezultat, ca şi în cazul operaţiilor de adunare şi scădere, este
nevoie să se determine, anterior efectuării produsului, numărul de elemente ale rezultatului.
Acest lucru se realizează prin simularea produsului şi contorizarea numărului de valori
nenule.

MatriceRara MatriceRara::operator *(MatriceRara &MR)


{
MatriceRara rezMR;

if(this->n!=MR.m)
return rezMR;

/* se determina numarul de elemente ale rezultatului */


int rezdim=0;

for(int i=0;i<this->m;i++)
for(int j=0;j<MR.n;j++)
{
double val = 0;
for(int k=0;k<this->n;k++)
{
val+=this->getValoareElement(i,k)*MR.getValoareElement(k,j);
}
if(val) rezdim++;
}

rezMR.dim = rezdim;
rezMR.m = this->m;
rezMR.n = MR.n;

rezMR.coloane = new int[rezdim];


rezMR.linii = new int[rezdim];
rezMR.valori = new double[rezdim];

int l = 0;

for(i=0;i<this->m;i++)
for(int j=0;j<MR.n;j++)
{
double val = 0;
for(int k=0;k<this->n;k++)
{
val+=this->getValoareElement(i,k)*MR.getValoareElement(k,j);
}
if(val)

42
{
rezMR.linii[l] = i;
rezMR.coloane[l] = j;
rezMR.valori[l] = val;
l++;
}
}

return rezMR;
}

Prin analiza acestui operator se constată că matricea rezultată păstrează structura de


matrice rară.
Pentru implementarea operatorului de inversare  s-a folosit algoritmul lui Krâlov.
Acesta constă în parcurgerea unui număr de paşi egal cu dimensiunile matricei:

1
1: A1 = A  p1 tr(A1 ) B1 I  p1 * A1
1
1
2: A2 = A * B1  p 2 tr(A 2 ) B2 = I –  p2 * A2
2
...
1
n-1: An-1 = A * Bn-2  p n 1 tr(A n 1 ) Bn 1 I  p n 1 * A n 1
n 1

1
n: An = A* Bn-1  p n tr(A) Bn I  p n * A n
n

Prin tr(A) se înţelege urma matricei  A, suma elementelor diagonale, iar  I  reprezintă
matricea unitate de aceeaşi dimensiune cu matricea  A. Aceste elemente sunt implementate în
clasa MatriceRară prin intermediul metodelor Unitate(int) şi Urma().

MatriceRara MatriceRara::Unitate(int n)
{
MatriceRara rezMR;
if(n>0)
{
rezMR.n=rezMR.m=rezMR.dim = n;
rezMR.coloane = new int[n];
rezMR.linii = new int[n];
rezMR.valori = new double[n];

for(int i=0;i<n;i++)
{
rezMR.coloane[i] = i;
rezMR.linii[i] = i;
rezMR.valori[i] = 1;
}

43
return rezMR;
}

double MatriceRara::Urma()
{
double rez = 0;
if(this->m==this->n)
{
for(int i=0;i<this->m;i++)
rez+=this->getValoareElement(i,i);
}
return rez;
}

Krâlov a demonstrat că după parcurgerea celor n  paşi,  Bn este o matrice identic nulă.
De aici rezultă inversa matricei  A:

 A-1 = pn * Bn-1 (6.36)

Se prezintă în continuare operatorul de inversare a matricelor rare care implementează


algoritmul prezent.

MatriceRara MatriceRara::Inversa()
{
MatriceRara tempMR, rezMR;

MatriceRara unitateMR = MatriceRara::Unitate(this->m);

MatriceRara initialaMR = *this;

if(initialaMR.m==initialaMR.n)
{
double p = initialaMR.Urma() ;
rezMR = initialaMR - (unitateMR*p);

for(int k=2;k<initialaMR.m;k++)
{
tempMR = initialaMR*rezMR;
p = (1.0/(double)k)*tempMR.Urma();
rezMR = tempMR - (unitateMR * p);
}

tempMR = initialaMR*rezMR;
p = (1.0/(double)k)*tempMR.Urma();
rezMR = rezMR*(1.0/p);

}
return rezMR;
}

44
Avantajele acestui algoritm constau în simplitatea implementării şi precizia
rezultatelor, datorată folosirii unui număr redus de operaţii de împărţire.

Teme de control

1. Să se verifice dacă o matrice ale cărei dimensiuni şi elemente au fost


citite de la tastatură îndeplineşte condiţiile de memorare ca o matrice rară .
2. Pentru o matrice rară reprezentată cu ajutorul a trei masive
unidimensionale să se realizeze conversia la un masiv bidimensional şi să se
 justifice algoritmul.

Bibliografie

Ion IVAN, Marius POPA, Paul POCATILU - Structuri de date, Vol. 1 si 2, Editura ASE,
Bucuresti, 2008.

45
3. Articolul  – structură de date neomogenă şi contiguă

Obiectivele unităţii de învăţare

După studierea acestei unităţi de învăţare, studenţii îşi vor însuşi cunoştinţe
teoretice şi vor deprinde abilităţi practice de lucru cu articole ca structuri de
date neomogene si contigue.

3.1. Structuri de date poziţionale

Există o diversitate de date cu care se caracterizează indivizii unei colectivităţi. După


efectuarea unei analize detaliate, se conchide că un număr q de caracteristici sunt suficiente
 pentru descrierea elementelor colectivităţii. În continuare numim şablon de descriere o
anumită ordine în care sunt înşirate caracteristicile, ordine absolut necesară a fi respectată la
descrierea fiecărui element al colectivităţii.
Fie caracteristicile C1, C2, ..., Cq  dispuse în ordinea care se constituie în şablonul
asociat descrierii indivizilor colectivităţii considerate.
De exemplu, pentru descrierea materialelor existente în stoc, se consideră
caracteristicile:
C1  –  numele materialului;
C2  – unitatea de măsură;
C3  –  cantitatea existentă în stoc la începutul perioadei;
C4  –  preţul unitar;
C5  – data ultimei aprovizionări;
C6  – intrările de materiale;
C7  – ieşirile de materiale;
C8  –  codul materialului;
C9  –  stocul final.
deci q = 8. Şablonul pentru introducerea datelor are elementele:

C 8 C 1 C 2 C 4 C 3 C 5 C 6  C 7   (3.1)

Caracteristica C 9 nu e necesară pentru că rezultă din calcule.


Aceste caracteristici sunt poziţionale, în sensul că C 8 este prima caracteristică în
şablon, C 3 este a 5- a caracteristică în şablon, iar C 7  este ultima caracteristică a şablonului.
La introducerea datelor, şirurile de constante se constituie în câmpuri. Astfel, se
identifică câmpul pentru codul materialului, format dintr -un număr fix de cifre, câmpul
numele materialului, ce are un număr de caractere care nu depăşesc o valoare prestabilită etc.
Şablonul se descrie prin tabelul 3.1, unde lungimea este calculată ca număr de
caractere.

Tabel 3.1. Şablon de descriere a câmpurilor 


Caracteristică Natură dată  Lungimea
C8 –  cod material număr  5
C1 –  nume material şir caractere 20
C2 – unitate măsură şir caractere 3
C3 – cantitate existentă în stoc număr  6
C4 –  preţ unitar  număr real  4+2

46
C5 – data ultimei aprovizionări şir caractere 9
C6 – intrări număr  6
C7 – ieşiri număr  6
C9 –  stoc final număr  9

Se observă că cele opt caracteristici diferă ca natură, ca lungime a şirului de simboluri


ce compun câmpurile şi ocupă în cadrul şablonului poziţii distincte.
Datele grupate în şablon cu poziţia fiecăreia specificată, formează o structură de date
de tip articol.
Modelul grafic asociat articolului este prezentat în figura 3.1.

nume articol

 primul element. al 2-lea elem. ultimul elem.


din şablon din şablon ... din şablon

Fig. 3.1. Model grafic asociat şablonului de articol

Se observă că pe ultimul nivel se află date elementare specificate prin nume şi tip.
Structura, în ansamblul ei, are un nume şi se defineşte distinct, ceea ce urmează reprezentând
componentele de la nivelul al doilea.

typedef struct material


{
int cod;
char nume[20];
char um[3];
float preţ;
int cont;
int intrări;
int ieşiri;
};

lg(material) = lg(cod) + lg(nume) + lg(um) + + lg(pret) + lg(cont) + lg(intrari) + lg(iesiri)


+ (3.2)

În cazul în care compilatorul face o alocare neoptimală reprezintă numărul biţilor


impuşi de alinierea cerută de fiecare câmp precedent care are un alt tip decât întreg sau
caracter. În cazul în care are loc optimizare în faza de compilare, devine nul.
n 1
adr(câmpi ) adr(câmp1 ) lg(câmp j)   (3.3)
 j 1

unde:

47
1 reprez int ă baitul cerut de alinierea câmpului  j 1
 j (3.4)
0 în cazul în care câmpj 1 nu cere alinierea

Dacă datele de la nivelul al doilea le regăsim sub numele de câmpuri elementare, la


nivelul superior data este numită dată de grup.
În unele situaţii fiecărui nivel i se asociază un număr, numit număr de nivel. Pentru
nivelele inferioare se asociază numere naturale mai mari decât cel asociat nivelelor
superioare. Unui nivel i se ataşează un număr sau un interval de numere. Descrierea în orice
situaţie rămâne poziţională.
Astfel, pentru exemplul dat se folosesc descrieri cu numere de nivel:

01 material
02 cod
02 nume
02 um
02 preţ 
02 cantitate
02 intrări
02 ieşiri
01 .................

Terminarea descrierii structurii, este marcată de apariţia unui număr de nivel care
delimitează începutul altei structuri, sau o instrucţiune de program, proprie limbajului, care
vine să marcheze începutul unei alte secvenţe program.
O altă descriere este:

7 material
8 cod
9 nume
10 um
8 preţ 
10 cantitate
9 intrări
9 ieşiri
4 ..........................

unde, pentru primul nivel se utilizează numerele 4, 5, 6 sau 7, iar pentru al doilea nivel, sunt
utilizate numerele 8, 9 sau 10.
La acest nivel de descriere a articolelor putem observa că o dată elementară este un
articol cu câmpuri de acelaşi tip.
Astfel:

typedef struct a
{
int b;
};

typedef struct c
{

48
int c1;
int c2;
int c3;
int c4;
int c5;
};

şi

a x;
c w,y;

conduc la aceleaşi rezervări de zone de memorie pentru  x şi y ca definiţiile:

int u;
int t[5],v[5];

adr(c4) = adr(c1) + lg(c1) + lg(c3) = adr(c1) + (4-1) * lg(int) (3.5)

iar

adr(v[4]) = adr(v[1]) + (4-1) * lg(int) (3.6)

Câmpurile unui articol se numesc membrii articolului, iar referirea lor se face folosind
operatorul de referire „.”.
În exemplul considerat, w şi  y sunt variabile de tip c, iar t  şi v sunt masive, fiecare
având 5 elemente. Prin w.C 4 se referă câmpul C 4 al variabilei w, iar prin y.C 1 se referă câmpul
C 1 al variabilei y.
Dacă se ia în considerare tipul de variabilă articol material şi se face definirea:

material m;

m.cod, m.preţ  sunt referiri ale câmpurilor ce alcătuiesc variabila m definită ca  având tipul
material .
Se observă că articolul este un tip de dată generic, pe care prin forma de definire a
câmpurilor îl putem folosi pentru a obţine tipuri derivate de date.
Astfel, material şi C sunt tipuri derivate de date corespunzătoare tipului gene ric struct .
Definirea acestui tip pune în evidenţă:
- componentele, câmpurile care-l alcătuiesc;
- natura câmpurilor;
-  poziţia câmpurilor.
Altfel spus, se evidenţiază structura sau şablonul datelor.
Variabilele definite pentru un tip derivat ocupă atâta memorie cât rezultă din lungimile
câmpurilor însumate şi corectate cu alinierea şi au în componenţă exact aceleaşi elemente
folosite la descrierea tipului, ca membri ai fiecărei variabile.
Descrierea unui tip derivat nu înseamnă rezervare de memorie, ci stocare de informaţie
 privind elementele care îl definesc.
Definirea variabilelor de tipul derivat descris anterior, efectuează rezervare de
memorie.

49
3.2. Structuri de date ierarhizate

Datele de tip articol sunt ierarhizate pe două sau mai multe nivele. Pentru a obţine
descrierea mai multor nivele este necesar ca la fiecare nivel inferior să fie posibilă enumerarea
de date elementare sau date de grup. Datele de grup e necesar să fie descrise deja înainte de a
le folosi, dar în unele cazuri particulare descrierea l or se efectuează ulterior folosirii.
Astfel, se definesc tipurile:

struct data
{
int zi;
int luna;
int an;
};

struct adresa
{
data data_stabilirii;
char strada[30];
int numar;
char telefon[8];
char oras[30];
};

struct persoana
{
data data_nasterii ;
adresa locul_nasterii;
int varsta;
data data_angajarii;
adresa adresa_actuala;
};

Definirea ulterioară specificării datelor de grup, se face ca în exemplul:

01 persoana
02 data_stabilirii
03 zi
03 luna
03 an
02 locul_nasterii
03 data_nasterii
04 zi
04 luna
04 an
03 strada
03 numar
03 telefon
03 oras

50
02 varsta
02 data_angajarii
03 zi
03 luna
03 an
02 adresa_actuala
03 data_stabilirii
04 zi
04 luna
04 an
03 strada
03 numar
03 telefon
03 oras

Oricare ar fi modalitatea de descriere a structurii arborescente, aceasta trebuie să


rămână în continuare neambiguă.
Dacă se acceptă ca definiţie a lungimii elementului de la nivelul i:
ni
lg( x , i ) ( y ,  j , i 1)
 j 0
  (3.7)

unde prin x j înţelegem elementul y j de pe nivelul i+1, astfel încât:

cont(x) ‚ adr(y1 ) (3.8)

rezultă că adresa unui element  y j de pe nivelul i aparţinând datei de grup k  dintr-o structură de
tip articol, se obţine prin relaţia:
k  1  j 1
adr   y  j , 0 adr   y1 , i lg  y k  , i k  lg  y h , i k 
k  1 h 1 (3.9)

De exemplu, pentru articolul definit prin:

struct student
{
char nume[21];
data data_nasterii;
data data_admiterii;
};

adresa câmpului data_admiterii.an se obţine urmărind modelul grafic din figura 3.2

51
student

nume data_nasterii data_admiterii

zi luna an zi luna an

Fig. 3.2. Modelul grafic al structurii articolului student

şi după formula:

adr(student.data_admiterii.an,3)=adr(student.nume,2)+
+ lg(student.nume,2)+ 1+
+ lg(student.data_nasterii,2)+ 2+
+ lg(student.data_nasterii.zi)+ 3+
+ lg(student.data_nasterii.luna)+ 4+
+ lg(student.nume,2)+ 21+ 1+4+4+4+
+ 2+4+ 3+4+ 4=41+1=42 (3.10)

 pentru că 2 = 3 = 4 = 0.
Dacă se consideră punctul operator ca orice alt operator, atunci construcţia:

a.b.c.d (3.11)

este o expresie care se evaluează ca orice altă expresie aritmetică, logică sau raţională. O
astfel de expresie o numim expresie referenţială.
Expresia referenţială se evaluează de la stânga la dreapta, pentru că în final trebuie să
se obţină o adresă. Referirea oricărui operand se face fie prin numele său, dacă abordarea este
la nivel de limbaj, fie prin adresă, dacă analiza este din punctul de vedere al programatorului
interesat să cunoască mecanismele proprii execuţiei programelor.
Interpretarea expresiei 3.11 este:
d  este membru în structura de tip articol c
c este membru în structura de tip articol b
b este membru în structura de tip articol a
ceea ce ca model grafic îi corespunde:

adr(a.b.c.d) = adr(a.b) + adr(b.c) + adr(c.d)  –  2*adr(a)


adr(a.b.c.d) = adr(a) + adr(a.b) + adr(b.c) + adr(c.d) –  3*adr(a)
adr(a.b.c.d) = adr(a) + [adr(a.b) - adr(a)] + [adr(b.c) –  adr(a)]+[adr(c.d)-adr(a)]
(3.12)

Punerea corect în corespondenţă a baiţilor ocupaţi de o structură de tip articol cu


câmpurile acesteia, mai ales în cazul în care există diferenţe generate de alocarea neoptimizată
a memoriei şi de apariţia variabilelor k , permite interpretarea riguroasă a conţinutului fiecărui

52
câmp. Problematica devine cu atât mai importantă cu cât variabilele de tip articol se utilizează
 pentru partiţionarea memoriei alocate unor buffere utilizate în operaţii de intrare/ieşire.
 Neoptimizarea prelucrărilor este benefică cu condiţia ca interpretarea grupului de baiţi
să fie controlată de programator, chiar dacă citirea lor are un alt şablon decât cel utilizat la
scriere.

3.3. Vectori de structuri şi structuri de vectori

Vectorii de structurii sunt masive omogene, în sensul că fiecare element al masivului


nu diferă de celălalt, chiar dacă în alcătuirea lor intră câmpuri de naturi diferite.
Construcţia:

typedef struct student


{
char nume[30];
int varsta;
char facult[20];
int an_studiu;
};
student x[20];

defineşte un vector  x având 20 de componente; fiecare componentă este un articol ce conţine


câmpurile nume, facult, an_studiu . Modelul grafic al acestui tip de dată derivat este
reprezentat în figura 3.3.

x[0] x[1] x[2] ··· x[14] ··· x[19]

nume varsta facult an_studiu ··· nume varsta facult an_studiu ···

Fig. 3.3. Modelul grafic asociat structurii vectorului de articole x

Masivul  x este unul unidimensional omogen, pentru că toate cele 20 de componente


au aceeaşi structură.
Referirea vârstei corespunzătoare studentului al cincisprezecelea dintr- o grupă se
realizează prin:

x[14].varsta

În cazul în care câmpurile unui articol sunt elemente ale unui masiv unidimensional,
definim o structură de vectori. De exemplu:

53
typedef struct student
{
char nume[30];
int note[10];
};
student x;

Dacă se doreşte aflarea notei a 4 -a a studentului x, referirea se efectuează prin:

x.nota[3]

Se definesc vectori de structuri care conţin vectori, respectiv vectori de structuri de


vectori:

stud y[20];

Pentru a referi nota a 5-a a studentului al 17-lea se face referirea:

y[16].nota[4]

Lucrurile iau amploare dacă se definesc structuri de matrice şi matrice de structuri.


Astfel, dacă într -o secţie sunt 20 de muncitori şi întreprinderea are 30 de secţii şi
 pentru fiecare muncitor trebuie cunoscut: timpul lucrat, salariul orar, numele, definind:

typedef struct muncitor


{
char nume[20];
long int salariu;
int ore;
};
long int salariu_total;
muncitor muncit[30][20];

se calculează salariul total pentru muncitorul 15 din secţia a 4 -a astfel:

 salariu_total: = muncit[3][14].salariu*muncit[3][14].ore; (3.13)

Dacă matricea este privită ca vectori de vectori, extensiile sunt făcute pentru masivele
tridimensionale ca vectori de matrice sau matrice de vectori, iar pentru masivele cu patru
dimensiuni, ca masive bidimensionale de masive bidimensionale, sau vectori de masive
tridimensionale sau masive tridimensionale de vectori.
Generalizările decurg din posibilitatea de a construi structuri de structuri şi uneori de a
introduce aspectul recursiv al descrierii. Secvenţa:

typedef int a[5];


typedef a b[5];
b c[5];

54
corespunde descrierii:

int c[5][5][5];

iar:

typedef int x[5][5];


x y[5][5];

corespunde descrierii:

int z[5][5][5][5];

Referirea elementelor se efectuează după aceleaşi reguli, specificând valori între


 paranteze pătrate, în număr egal cu dimensiunea atribuită masivului, ca de exemplu:

c[i][j][k] (3.14)

 y[i][j][k][h] (3.15)

Atributele sunt comutative în raport cu operatorul de referire. Un element din


vectorul de structură se referă prin:

nume_vector[i].nume_membru (3.16)

iar un element din structura de vectori se referă prin:

nume_structură.nume_membru[i]  (3.17)

Se consideră spre exemplificare următorul program care gestionează acţionarii unei


societăţi comerciale.

#include <windows.h>
#include <process.h>
#include <ctype.h>
#include <stdio.h>
#include <conio.h>
#include <string.h>

struct data
{
int da_year,da_day,da_mon;
};

struct adresa
{
char str[25];
int nr;
char bloc[3];

55
char sc;
int ap;
int sect;
long codp;
char loc[25];
char jud[20];
};

struct ID
{
char seria[3];
long nr;
data dataE;
int circa;
};

struct actionar
{
char cod[6];
char nume[30];
struct adresa adr;
data dataN;
struct ID BI;
char reprez[6];
int nr_act;
};

struct ind
{
char cheie[6];
long poz;
};

struct nod
{
char sir[6];
nod *st;
nod *dr;
};

ind index[1000];

char *mes[40]={
"Data trebuie sa fie numerica!",
"Ziua trebuie sa fie intre 1 si 31!",
"Luna trebuie sa fie intre 1 si 12!",
"Anul trebuie sa fie intre 1900 si 2001!",
"",
"Data trebuie sa fie cuprinsa intre 0 si 1!",
"Serie incorecta!"

56
};

long lg_fis()
{
long lung;
FILE *pf;
pf=fopen("actionar.dat","rb");
fseek(pf, 0, SEEK_END);
lung=ftell(pf);
fclose(pf);
return lung;
}

void insnod(nod *& rad,char *s,int &vb)


{
if(rad==NULL)
{
rad=new nod;
strncpy(rad->sir,s,6);
rad->st=rad->dr=NULL;
}
else if(strncmp(s,rad->sir,6) < 0) insnod(rad->st,s,vb);
else if(strncmp(s,rad->sir,6) > 0) insnod(rad->dr,s,vb);
else vb=1;
}

void del_arb(struct nod *T)


{
if (T!=NULL)
{
del_arb(T->st);
del_arb(T->dr);
delete T;
}
}

int indexare()
{
FILE *pf;
actionar act;

if ((pf=fopen("actionar.dat","rb"))==NULL)
{
printf("Fiserul cu date despre actionari nu exista!");
getch();
exit(1);
}
int k=0;
long pozitie;
while(fread(&act,sizeof(struct actionar),1,pf))

57
{
pozitie=ftell(pf)-sizeof(struct actionar);
strncpy(index[k].cheie,act.cod,6);
index[k].poz=pozitie;
k++;
}
fclose(pf);
return k;

void sortare(int n)
{
struct ind temp;
for(int i=0;i<n-1;i++)
for(int j=i;j<n;j++)
if(strcmp(index[i].cheie,index[j].cheie)>0)
{
temp=index[i];
index[i]=index[j];
index[j]=temp;
}
}

int cautare(int n,int &poz,char *key)


{
int s,m,d;
s=0;
d=n;
while(s<=d)
{
m=(s+d)/2;
if (!strncmp(key,index[m].cheie,6))
{
poz=m;
return m;
}
else
if (strncmp(key,index[m].cheie,6)<0)
d=m-1;
else
s=m+1;
}
return -1;
}

void sortareN()
{
struct actionar act,act1;

58
FILE *pf,*pt;
if((pf=fopen("actionar.dat","rb+"))==NULL)
{
printf("Fisierul cu date despre actionari nu exista!");
getch();
return;
}
pt=fopen("temp.dat","wb");
int vb,na=lg_fis()/sizeof(struct actionar);
for(int i=0;i<na;i++)
{
fread(&act,sizeof(struct actionar),1,pf);
fwrite(&act,sizeof(struct actionar),1,pt);
}
fclose(pt);
fclose(pf);
pt=fopen("temp.dat","rb+");
do
{
vb=0;
fseek(pt,0,0);
for(int i=0;i<na-1;i++)
{
fread(&act,sizeof(struct actionar),1,pt);
fread(&act1,sizeof(struct actionar),1,pt);
if (stricmp(act.nume,act1.nume)>0 )
{
fseek(pt,ftell(pt)-2*sizeof(struct actionar),0);
fwrite(&act1,sizeof(struct actionar),1,pt);
fwrite(&act,sizeof(struct actionar),1,pt);
vb=1;
}
fseek(pt,ftell(pt)-sizeof(struct actionar),0);
}
}
while(vb==1);
fclose(pt);
}

void eroare(int cod)


{
printf("\a%s",mes[cod-1]);
Sleep(500);
}

void valid_nr_l(long &val,int cod)


{
int i,VB=0;
do
{

59
VB=0;

char tab[255];
fflush(stdin);
gets(tab);
if (strlen(tab)==0)
VB=1;
else
{
i=0;
while(VB==0&&i<strlen(tab))
{
if(isdigit(tab[i]))
i++;
else
VB=1;
}
}
if(VB==0)
sscanf(tab,"%ld",&val);
else
eroare(cod);
}
while(VB!=0);
}

void valid_nr_f(float &val,int cod)


{
int i,VB=0;
do
{
VB=0;

char tab[255];
fflush(stdin);
gets(tab);
if (strlen(tab)==0)
VB=1;
else
{
i=0;
while(VB==0&&i<strlen(tab))
{
if(isdigit(tab[i]))
i++;
else
VB=1;
}
}
if(VB==0)

60
sscanf(tab,"%f",&val);
else
eroare(cod);
}
while(VB!=0);
}

void valid_nr_i(int &val,int cod)


{
int i,VB=0;
do
{
VB=0;

char tab[255];
fflush(stdin);
gets(tab);
if (strlen(tab)==0)
VB=1;
else
{
i=0;
while(VB==0&&i<strlen(tab))
{
if(isdigit(tab[i]))
i++;
else
VB=1;
}
}
if(VB==0)
sscanf(tab,"%d",&val);
else
eroare(cod);
}
while(VB!=0);
}

void valid_ab_sb(char val[],int cod)


{
int i,VB=0;
do
{
VB=0;
char tab[255];
fflush(stdin);
gets(tab);
if (strlen(tab)==0||strlen(tab)!=2)
VB=1;
else

61
{
i=0;
while(VB==0&&i<2)
{
if(isalpha(tab[i]))
i++;
else
VB=1;
}
}
if(VB==0)
strncpy(val,tab,3);
else
eroare(cod);
}
while(VB!=0);
}

void sortareA()
{
struct actionar act,act1;
FILE *pf,*pt;
if((pf=fopen("actionar.dat","rb+"))==NULL)
{
printf("Fisierul cu date despre actionari nu exista!");
getch();
return;
}
pt=fopen("temp.dat","wb");
int vb,na=lg_fis()/sizeof(struct actionar);
for(int i=0;i<na;i++)
{
fread(&act,sizeof(struct actionar),1,pf);
fwrite(&act,sizeof(struct actionar),1,pt);
}
fclose(pt);
fclose(pf);
pt=fopen("temp.dat","rb+");
do
{
vb=0;
fseek(pt,0,0);
for(int i=0;i<na-1;i++)
{
fread(&act,sizeof(struct actionar),1,pt);
fread(&act1,sizeof(struct actionar),1,pt);
if ((unsigned)act.nr_act<(unsigned)act1.nr_act)
{
fseek(pt,ftell(pt)-2*sizeof(struct actionar),0);
fwrite(&act1,sizeof(struct actionar),1,pt);

62
fwrite(&act,sizeof(struct actionar),1,pt);
vb=1;
}
fseek(pt,ftell(pt)-sizeof(struct actionar),0);
}
}
while(vb==1);
fclose(pt);
}
void creare()
{
int v;
char c,key[6];
FILE *pf;
actionar act;
nod *RAD=NULL;
if ((pf=fopen("actionar.dat","rb"))!=NULL)
{
printf("ATENTIE\n");
printf("Fiserul cu date despre actionari exista!\n");
printf("Datele existente se vor pierde!\n");
printf("Doriti sa-l rescrieti?[D/N]: ");
do
c=getch();
while(c!='D'&&c!='N');
if (c=='N')
{
return;
}
}
if ((pf=fopen("actionar.dat","wb"))==NULL)
{
printf("Eroare la creare!");
exit(1);
}
else
{
printf("Cod: ");
while(scanf("%6s",key)!=0)
{
v=0;
insnod(RAD,key,v);
if (v)
{
printf("\n\aArticolul cu aceasta cheie exista!");
Sleep(500);
fflush(stdin);
continue;
}
strncpy(act.cod,key,6);

63
fflush(stdin);
printf("Nume: ");
gets(act.nume);
fflush(stdin);
printf("Adresa actionarului\n");
printf("\tStrada: ");
gets(act.adr.str);
printf("\tNumarul: ");
valid_nr_i(act.adr.nr,1);
printf("\tBloc: ");
scanf("%s",act.adr.bloc);
fflush(stdin);
printf("\tScara: ");
scanf("%c",&act.adr.sc);
printf("\tApartament: ");

valid_nr_i(act.adr.ap,1);
printf("\tSector: ");
valid_nr_i(act.adr.sect,1);
printf("\tCod postal: ");
valid_nr_l(act.adr.codp,1);
fflush(stdin);
printf("\tLocalitatea: ");
gets(act.adr.loc);
printf("\tJudetul: ");
gets(act.adr.jud);
do
{
printf("Data nasterii\n");
printf("\tZiua:");
valid_nr_i(act.dataN.da_day,1);
if(act.dataN.da_day<1||act.dataN.da_day>31)
eroare(2);
}
while(act.dataN.da_day<1||act.dataN.da_day>31);
printf("\tLuna:");
do
{
valid_nr_i(act.dataN.da_mon,1);
if(act.dataN.da_mon<1||act.dataN.da_mon>12)
eroare(3);
}
while(act.dataN.da_mon<1||act.dataN.da_mon>12);
printf("\tAnul:");
do
{
valid_nr_i(act.dataN.da_year,1);
if(act.dataN.da_year<1900||act.dataN.da_year>2001)
eroare(4);
}

64
while(act.dataN.da_year<1900||act.dataN.da_year>2001);
printf("Buletin de identitate\n");
printf("\tSeria:");
valid_ab_sb(act.BI.seria,7);
printf("\tNumarul:");
valid_nr_l(act.BI.nr,1);
printf("Data eliberarii\n");
printf("\tZiua:");

do
{
valid_nr_i(act.BI.dataE.da_day,1);
if(act.BI.dataE.da_day<1||act.BI.dataE.da_day>31)
eroare(2);
}
while(act.BI.dataE.da_day<1||act.BI.dataE.da_day>31);

printf("\tLuna:");
do
{
valid_nr_i(act.BI.dataE.da_mon,1);
if(act.BI.dataE.da_mon<1||act.BI.dataE.da_mon>12)
eroare(3);
}
while(act.BI.dataE.da_mon<1||act.BI.dataE.da_mon>12);

printf("\tAnul: ");
do
{
valid_nr_i(act.BI.dataE.da_year,1);
if(act.BI.dataE.da_year<1900||act.BI.dataE.da_year>2001)
eroare(4);
}
while(act.BI.dataE.da_year<1900||act.BI.dataE.da_year>2001);
printf("\tCirca de politie: ");

valid_nr_i(act.BI.circa,1);
printf("Numarul de actiuni detinute: ");

valid_nr_i(act.nr_act,1);

printf("\n");
fwrite(&act,sizeof(struct actionar),1,pf);
printf("Cod: ");
}
}
del_arb(RAD);
fclose(pf);
}

65
void creare_date()
{
char c;
float ksoc,pfn,cota;
long int val_A;
FILE *pd;
if ((pd=fopen("date.dat","rb"))!=NULL)
{
printf("ATENTIE\n");
printf("Fiserul cu date despre profit exista!\n");
printf("Datele existente se vor pierde!\n");
printf("Doriti sa-l rescrieti?[D/N]: ");
do
c=getch();
while(c!='D'&&c!='N');
if (c=='N')
{
return;
}
}
if ((pd=fopen("date.dat","wb"))==NULL)
{
printf("Eroare la creare!");
getch();
return;
}
printf("DATE PRIVIND CAPITALUL SOCIAL SI PROFITUL\n");
printf("Valoarea unei actiuni:");
valid_nr_l(val_A,1);
fwrite(&val_A,sizeof(long int),1,pd);
printf("Capital Social:");
valid_nr_f(ksoc,1);
fwrite(&ksoc,sizeof(float),1,pd);
printf("Profit net:");
valid_nr_f(pfn,1);
fwrite(&pfn,sizeof(float),1,pd);
do
{
printf("Cota din profit repartizata la dividende [subunitara]:");
scanf("%f",&cota);
if (cota>1||cota <0)
eroare(6);
}
while(cota>1||cota <0);
fwrite(&cota,sizeof(float),1,pd);
fclose(pd);
}

void afis_date(const actionar &act)


{

66
printf("\n\nCod: %s\n",act.cod);
printf("Nume: "); puts(act.nume);
printf("Adresa actionarului\n");
printf("\tStrada:"); puts(act.adr.str);
printf("\tNumarul: %d\n",act.adr.nr);
printf("\tBloc: %s ",act.adr.bloc);
printf("Scara: %c Apartament: %d Sector:
%d\n",act.adr.sc,act.adr.ap,act.adr.sect);
printf("\tCod postal: %ld\n",act.adr.codp);
printf("\tLocalitatea: ");puts(act.adr.loc);
printf("\tJudetul: "); puts(act.adr.jud);
printf("Data nasterii:
%d/%d/%d\n",act.dataN.da_day,act.dataN.da_mon,act.dataN.da_year);
printf("Buletin de identitate\n");
printf("\tSeria: %s",act.BI.seria);
printf(" Numarul: %ld\n",act.BI.nr);
printf("\tData eliberarii:
%d/%d/%d\n",act.BI.dataE.da_day,act.BI.dataE.da_mon,act.BI.dataE.da_year);
printf("\tCirca de politie: %d\n",act.BI.circa);

printf("Numarul de actiuni detinute: %u\n",act.nr_act);


printf("\n");

void cons()
{
int n;
char key[6];
int vb,i,c;
actionar act;
n=indexare();
sortare(n);
FILE *pf=fopen("actionar.dat","rb");
while(printf("\nCod actionar:"),(c=scanf("%6s",key))!=0)
{
vb=cautare(n,i,key);
if(vb==-1)
{
printf("\n\aArticolul cu aceasta cheie nu exista!\n");
Sleep(500);
fflush(stdin);
}
else
{
fseek(pf,index[i].poz,0);
fread(&act,sizeof(struct actionar),1,pf);
afis_date(act);
getch();
}

67
}
fclose(pf);
}

void modif()
{
int n;
char key[6];
int vb,i,nr;
actionar act;
n=indexare();
sortare(n);
FILE *pf=fopen("actionar.dat","rb+");
while(printf("\nCod actionar:"),scanf("%6s",key)!=EOF)
{
vb=cautare(n,i,key);
if(vb==-1)
{
printf("\n\aArticolul cu aceasta cheie nu exista!");
Sleep(500);
}
else
{
fseek(pf,index[i].poz,0);
fread(&act,sizeof(struct actionar),1,pf);
afis_date(act);
printf("Noul numar:");
valid_nr_i(nr,1);
act.nr_act=nr;
fseek(pf,index[i].poz,0);
fwrite(&act,sizeof(struct actionar),1,pf);
}
}
fclose(pf);
}
void listA()
{
FILE *pt,*ptxt;
unsigned int tot_a=0;
long int val_a;
float ksoc;
actionar act;
sortareA();
FILE *pd=fopen("date.dat","rb");
fread(&val_a,sizeof(long int),1,pd);
fread(&ksoc,sizeof(float),1,pd);
fclose(pd);
pt=fopen("temp.dat","rb");
while(fread(&act,sizeof(struct actionar),1,pt))
tot_a+=act.nr_act;

68
rewind(pt);
ptxt=fopen("lista1.txt","w");
fprintf(ptxt,"\n\n\n\t\tLista actionarilor dupa numarul de actiuni\n");
fprintf(ptxt,"\t\t******************************************\n\n");
fprintf(ptxt,"**********************************************************
*************\n");
fprintf(ptxt,"* Cod * Nume Prenume * Numar * Ponderea
actiunilor*\n");
fprintf(ptxt,"* * * actiuni * in capitalul social*\n");
fprintf(ptxt,"**********************************************************
*************\n");
while(fread(&act,sizeof(struct actionar),1,pt))
{
fprintf(ptxt,"*%6s *%-30s*%8u *%18.3f
%%*\n",act.cod,act.nume,act.nr_act,(float)(unsigned)act.nr_act*val_a/ksoc*100);

fprintf(ptxt,"**********************************************************
*************\n");
}
fprintf(ptxt,"**********************************************************
*************\n");
fclose(pt);
fclose(ptxt);
}
void listD()
{
FILE *pt,*ptxt;
SYSTEMTIME data;
float pfn,cota;
float div;
actionar act;
sortareA();
unsigned int tot_a=0;
FILE *pd=fopen("date.dat","rb");
fseek(pd,sizeof(long int)+sizeof(float),0);
fread(&pfn,sizeof(float),1,pd);
fread(&cota,sizeof(float),1,pd);
fclose(pd);
pt=fopen("temp.dat","rb");
while(fread(&act,sizeof(struct actionar),1,pt))
tot_a+=act.nr_act;
div=(pfn*cota)/tot_a;
rewind(pt);
ptxt=fopen("lista2.txt","w");
GetSystemTime(&data);
fprintf(ptxt,"\n\t\t\t\tDividende cuvenite actionarilor la data
%d.%d.%d\n",data.wDay,data.wMonth,data.wYear);
fprintf(ptxt,"\t\t\t\t************************************************\n\n")
;
fprintf(ptxt,"Profit net:%.0f lei\n",pfn);

69
fprintf(ptxt,"Cota de profit repartizata la dividende:%.2f\n",cota);
fprintf(ptxt,"Total actiuni:%u\n",tot_a);
fprintf(ptxt,"Dividende per actiune:%.2f lei/act\n\n",div);
fprintf(ptxt,"**********************************************************
**************************************************************\n");
fprintf(ptxt,"* Nume Prenume * Data * Adresa
*Buletin de * Numar * Dividende *\n");
fprintf(ptxt,"* * nasterii * *identitate *
actiuni* *\n");
fprintf(ptxt,"**********************************************************
**************************************************************\n");
while(fread(&act,sizeof(struct actionar),1,pt))
{
fprintf(ptxt,"*%-30s*%2d.%2d.%d*Str:%-29s Nr:%-5d*%-2s
%8ld*%8u*%11.2f
*\n",act.nume,act.dataN.da_day,act.dataN.da_mon,act.dataN.da_year,act.adr.str,act.ad
r.nr,act.BI.seria,act.BI.nr,act.nr_act,(unsigned)act.nr_act*div);
fprintf(ptxt,"* * *Bl:%c%c%c Sc:%c Ap:%-3d
Sect:%-2d Cod p:%-8ld * * *
*\n",act.adr.bloc[0],act.adr.bloc[1],act.adr.bloc[2],act.adr.sc,act.adr.ap,act.adr.sect,act.a
dr.codp);
fprintf(ptxt,"* * *Loc:%-20s * *
* *\n",act.adr.loc);

fprintf(ptxt,"**********************************************************
**************************************************************\n");
}
fprintf(ptxt,"**********************************************************
**************************************************************\n");
fclose(pt);
fclose(ptxt);
}
void listAB()
{
FILE *pt,*ptxt;

actionar act;

sortareN();
pt=fopen("temp.dat","rb");
ptxt=fopen("lista3.txt","w");
fprintf(ptxt,"\n\n\n\t\t\tLista actionarilor in ordine alfabetica\n");
fprintf(ptxt,"\t\t\t***************************************\n\n");
fprintf(ptxt,"\t\t**************************************************\n");
fprintf(ptxt,"\t\t* Nr. * Nume Prenume * Numar *\n");
fprintf(ptxt,"\t\t* crt. * * actiuni *\n");
fprintf(ptxt,"\t\t**************************************************\n");
int i=0;
while(fread(&act,sizeof(struct actionar),1,pt))
{

70
fprintf(ptxt,"\t\t*%6d *%-30s*%8u *\n",++i,act.nume,act.nr_act);

fprintf(ptxt,"\t\t**************************************************\n");
}
fprintf(ptxt,"\t\t**************************************************\n");
fclose(pt);
fclose(ptxt);
}
void do_sterg()
{
int n,c;
int vb,i;
actionar act;
n=indexare();
sortare(n);
char key[6];
FILE *pf=fopen("actionar.dat","rb");
while(printf("\nCod actionar:"),scanf("%6s",key)!=0)
{
vb=cautare(n,i,key);
if(vb==-1)
{
printf("\n\aArticolul cu aceasta cheie nu exista!\n");
Sleep(500);
fflush(stdin);
continue;
}
else
{
fseek(pf,index[i].poz,0);
fread(&act,sizeof(struct actionar),1,pf);
afis_date(act);
printf("Doriti sa stergeti acest articol?[D/N]: ");
do
{
c=getch();
}
while(c!='D'&&c!='N');
if (c=='N')
{
continue;
}
}
//rescriere fisier
FILE *ptmp=fopen("tmp.dat","wb");
rewind(pf);
for(int j=0;j<index[i].poz/sizeof(struct actionar);j++)
{
fread(&act,sizeof(struct actionar),1,pf);
fwrite(&act,sizeof(struct actionar),1,ptmp);

71
}
fread(&act,sizeof(struct actionar),1,pf);
while(fread(&act,sizeof(struct actionar),1,pf))
fwrite(&act,sizeof(struct actionar),1,ptmp);
fclose(pf);
fclose(ptmp);
unlink("actionar.dat");
rename("tmp.dat","actionar.dat");
n=indexare();
sortare(n);
pf=fopen("actionar.dat","rb");
}
fclose(pf);
}
void adaug()
{
int v;
char key[6];
actionar act;
nod *RAD=NULL;
FILE *pf;
if ((pf=fopen("actionar.dat","rb+"))==NULL)
{
printf("Fiserul cu date despre actionari nu exista!");
return;
}
else
{
strncpy(act.reprez," ",6);
while(fread(&act,sizeof(struct actionar),1,pf))
insnod(RAD,act.cod,v);
//fseek(pf,0,2);
printf("Cod:\n");
while(scanf("%6s",key)!=0)
{
v=0;
insnod(RAD,key,v);
if (v)
{

printf("\n\aArticolul cu aceasta cheie exista!\n");


Sleep(500);
fflush(stdin);
continue;
}
strncpy(act.cod,key,6);
fflush(stdin);
printf("Nume: ");
gets(act.nume);
fflush(stdin);

72
printf("Adresa actionarului\n");
printf("\tStrada: ");
gets(act.adr.str);
printf("\tNumarul: ");
valid_nr_i(act.adr.nr,1);
printf("\tBloc: ");
scanf("%s",act.adr.bloc);
fflush(stdin);
printf("\tScara: ");
scanf("%c",&act.adr.sc);
printf("\tApartament: ");
valid_nr_i(act.adr.ap,1);
printf("\tSector: ");
valid_nr_i(act.adr.sect,1);
printf("\tCod postal: ");
valid_nr_l(act.adr.codp,1);
fflush(stdin);
printf("\tLocalitatea: ");
gets(act.adr.loc);
printf("\tJudetul: ");
gets(act.adr.jud);
do
{
printf("Data nasterii\n");
printf("\tZiua:");
valid_nr_i(act.dataN.da_day,1);
if(act.dataN.da_day<1||act.dataN.da_day>31)
eroare(2);
}
while(act.dataN.da_day<1||act.dataN.da_day>31);

printf("\tLuna:");
do
{
valid_nr_i(act.dataN.da_mon,1);
if(act.dataN.da_mon<1||act.dataN.da_mon>12)
eroare(3);
}
while(act.dataN.da_mon<1||act.dataN.da_mon>12);

printf("\tAnul:");
do
{
valid_nr_i(act.dataN.da_year,1);
if(act.dataN.da_year<1900||act.dataN.da_year>2001)
eroare(4);
}
while(act.dataN.da_year<1900||act.dataN.da_year>2001);
printf("Buletin de identitate\n");
printf("\tSeria:");

73
valid_ab_sb(act.BI.seria,7);
printf("\tNumarul:");
valid_nr_l(act.BI.nr,1);
printf("Data eliberarii\n");
printf("\tZiua:");
do
{
valid_nr_i(act.BI.dataE.da_day,1);
if(act.BI.dataE.da_day<1||act.BI.dataE.da_day>31)
eroare(2);
}
while(act.BI.dataE.da_day<1||act.BI.dataE.da_day>31);

printf("\tLuna:");
do
{
valid_nr_i(act.BI.dataE.da_mon,1);
if(act.BI.dataE.da_mon<1||act.BI.dataE.da_mon>12)
eroare(3);
}
while(act.BI.dataE.da_mon<1||act.BI.dataE.da_mon>12);
printf("\tAnul: ");
do
{
valid_nr_i(act.BI.dataE.da_year,1);
if(act.BI.dataE.da_year<1900||act.BI.dataE.da_year>2001)
eroare(4);
}
while(act.BI.dataE.da_year<1900||act.BI.dataE.da_year>2001);
printf("\tCirca de politie: ");

valid_nr_i(act.BI.circa,1);
printf("Numarul de actiuni detinute: ");

valid_nr_i(act.nr_act,1);

printf("\n");
fwrite(&act,sizeof(struct actionar),1,pf);
printf("Cod: ");
}
}
del_arb(RAD);
fclose(pf);
}

void men_fis()
{
int rasp,cont=1;
while(cont)
{

74
do
{
printf("MENIUL FISIERE\n\n");
printf("1.CREARE FISIER ACTIONARI\n");
printf("2.CREARE FISER CU DATE PRIVIND PROFITUL\n" );
printf("3.ADAUGARE ACTIONARI\n");
printf("4.MODIFICARE NUMAR DE ACTIUNI\n");
printf("5.STERGERE ACTIONARI DIN FISIER\n");
printf("6.REVENIRE LA MENIUL PRINCIPAL\n\n");
printf("Optiunea:");
valid_nr_i(rasp,5);//scanf("%d",&rasp);
}
while(rasp<1||rasp>6);
switch(rasp)
{
case 1:creare();break;
case 2:creare_date();break;
case 3:adaug();break;
case 4:modif();break;
case 5:do_sterg();break;
case 6:cont=0;break;
}
}
}
void men_sit()
{
int rasp,cont=1;
while(cont)
{
do
{
printf("MENIUL SITUATII DE IESIRE\n\n");
printf("1.LISTA ACTIONARILOR IN ORDINEA NUMARULUI DE
ACTIUNI\n");
printf("2.LISTA ACTIONARILOR IN ORDINE
ALFABETICA\n");
printf("3.LISTA DIVIDENDELOR CUVENITE FIECARUI
ACTIONAR\n");
printf("4.CONSULTARE FISIER DUPA COD ACTIONAR\n");
printf("5.REVENIRE LA MENIUL PRINCIPAL\n\n");
printf("Optiunea:");
valid_nr_i(rasp,5);//scanf("%d",&rasp);
}
while(rasp<1||rasp>5);
switch(rasp)
{
case 1:listA();break;
case 2:listAB();break;
case 3:listD();break;
case 4:cons();break;

75
case 5:cont=0;break;
}
}
}
void main()
{
int rasp,cont=1;
while(cont)
{
do
{
printf("MENIUL PRINCIPAL\n\n");
printf("1.FISIERE\n");
printf("2.SITUATII DE IESIRE\n");
printf("3.TERMINARE PROGRAM\n\n");
printf("Optiunea:");
valid_nr_i(rasp,5);//scanf("%d",&rasp);
}
while(rasp<1||rasp>3);
switch(rasp)
{
case 1:men_fis();break;
case 2:men_sit();break;
case 3:cont=0;break;
}
}
}

Programatorul trebuie să realizeze un echilibru între creşterea numărului de


dimensiuni, reducerea gradului de umplere şi complexitatea expresiilor asociate calculelor de
deplasare, pentru a localiza fiecare element al structurii pe care o defineşte. Acest echilibru
conduce în final la reducerea duratei de prelucrare şi la obţinerea lizibilităţii bune a
 programului.

Teme de control

1. Să se implementeze conceptul de matrice rară şi operaţia de adunare


folosind un vector de structuri.
2. Pentru structura carte, să se definească atributele corespunzătoare
necesare reprezentării unei biblioteci structurate pe categorii, folosind
masive multidimensionale.

Bibliografie

Ion IVAN, Marius POPA, Paul POCATILU - Structuri de date, Vol. 1 si 2, Editura ASE,
Bucuresti, 2008.

76
4. Fişiere
Obiectivele unităţii de învăţare

După studierea acestei unităţi de învăţare, studenţii îşi vor însuşi cunoştinţe
teoretice şi vor deprinde abilităţi practice de lucru fişiere binare si text .
Studenţii vor învăţa lucrul cu funcţii de acces la resursele memorate pe
suporturile externe de date.

4.1. Structuri de date externe

Masivele, listele şi arborii binari, sunt structuri de date interne, prin faptul că se află în
memoria internă a unui sistem de calcul. În momentul în care aceste structuri sunt stocate în
memoria externă –  pe discuri, benzi magnetice, dischete, compact-discuri acestea sunt numite
 structuri de date externe.
Memoria internă a unui calculator este imaginată ca un şir ordonat de baiţi. Fiecare
 bait se defineşte prin adresă şi prin conţinut. Memoria externă este imaginată, de asemen ea, ca
un şir de baiţi, cu deosebirea că pentru calculul adresei sunt luate în considerare elementele
specifice modului de structurare a suportului.
Astfel, atunci când suportul este organizat în piste, cilindri şi problematica accesului
este rezolvată prin poziţionarea pe aceste unităţi de acces, adresarea efectuându -se luând în
calcul elemente structurale.
În plus, particularităţile de desfăşurare în plan fizic a operaţiilor de intrare/ieşire
determină analiza distinctă a raportului dintre semnificaţia instrucţiunilor I/O din programe şi
operaţiile efective de citire/scriere de pe suportul extern.
Presupunând că pentru un sistem de calcul modulele care realizează citiri/scrieri
operează cu conţinutul unor buffere de lungime  L, tot timpul aceste funcţii furnizează
informaţii asupra începutului zonei ce este explorată, lungimea acesteia fiind rezultatul logicii
de organizare a datelor.
Programatorul are acces direct sau indirect la zona de memorie tampon prin
intermediul adresei sale de început sau prin int ermediul unei alte zone de memorie definită în
 programul său, zonă în care este copiat conţinutul bufferului.

5 adresa de început
 buffer

6 Operaţii de
Memoria c i t i r e / s c r i e r e

externă
1 4 3

zona de memorie
definită de
u t i l i z a t o r

Fig. 4.1. Operaţii de intrare/ieşire

77
Dinamica operaţiilor de intrare/ieşire determină actualizarea variabilei pointer   care
delimitează partea de început a subzonei din buffer ce este copiată în zona de memorie
definită de utilizator.
În cazul funcţiilor de citire/scriere a unui caracter, variabila pointer se modifică cu o
unitate, marcând exact schimbul de informaţii în dialogul om –  calculator .
În cazul citirilor/scrierilor cu format delimitatorul acceptat este analizat, iar algoritmul
de punere în corespondenţă este astfel proiectat încât acesta nu este integrat în setul
informaţiilor.

1 5 CR 1 2 0 CR 1 CR 1 CR

algoritm algoritm algoritm algoritm


conversie conversie conversie conversie
alfanumeric- alfanumeric- alfanumeric- alfanumeric-
întreg întreg întreg real

variabila A variabila B variabila C variabila D

Fig. 4.2. Punerea în corespondenţă a valorilor citite

Şirurile de caractere sunt delimitate prin CR , iar algoritmul de parcurgere a


 bufferului determină un astfel de conţinut al variabilei pointer încât este transmis ca parametru
funcţiilor de conversie începutul fiecărei subzone de buffer care începe după CR .
Modul cum este concretizat CR ca delimitator de sfârşit de şir şi modul cum este
definit descriptorul de format imprimă structurii de parametri ai funcţiilor de conversie
anumite particularităţi.
Se observă că dinamica variabilei pointer este influenţată de tipul operaţiei de
intrare/ieşire. Acest aspect explică de ce este necesară efectuarea avansului acestuia, când se
alternează citiri cu format cu citiri fără format. La operaţii neomogene există modalităţi
neomogene de definire şi de tratare a delimitatorilor de sfârşit de şir ca rezultat al activării
tastei CR .
Ceea ce pare simplu în cazul structurilor de date interne, nu devine mai complicat în
cazul structurilor de date externe, atât timp cât sunt clarificate chestiunile legate de variabilele
 pointer asociate bufferelor şi de faptul că o citire fizică efectivă nu înseamnă neapărat o citire
logică, iar la scriere se întâmplă acelaşi lucru. Prin operaţia logică, înţelegem acţiunea ce
corespunde unei apelări de funcţie citire/scriere din program.
De exemplu, pentru zona de lungime minimă  L = 256 octeţi, ce este scrisă/citită la o
singură operaţie fizică efectivă pe/de pe suport, la scrierea pe suportul extern a trei variabile
de tip articol, A, B şi C  cu:

lg(A) = 120 octeţi


lg(B) = 110 octeţi
lg(C) = 200 octeţi

78
variabila pointer permite preluarea datelor din structura  A, se majorează cu 120, preia datele
din structura  B şi realizează o scriere fizică pe suport. Variabila pointer este apoi reiniţializată
şi va prelua datele structurii C  după care efectuează a doua scriere fizică. În acest caz celor
trei scrieri logice le-au corespuns două scrieri fizice.
Există diferite modalităţi de realizare a operaţiilor de citire/scriere după cum se
folosesc sau nu factori de blocare, se definesc sau nu elemente de regăsire a informaţiilor.
Structurile de date externe nu diferă mult de structurile de date interne. Apar unele
complicaţii care nu sunt majore de altfel, prin aceea că volumul datelor este foarte mare şi
elementele repetitive abundă, ceea ce conduce la ideea că structurile de date externe sun t
 privite ca structuri de structuri de date interne dispuse pe suport de memorie externă.
Pentru a realiza regăsirea într -un volum de date de dimensiuni remarcabile, este
necesară organizarea, sistematizarea datelor şi crearea acelor elemente indispensabil e
localizării, adresării.
Structurile de date externe sunt contigue, formate din elemente dispuse în continuarea
celorlalte şi necontigue, distanţa dintre elemente fiind o variabilă aleatoare a cărei lege de
repartiţie este identificabilă, dar care necesită memorarea distanţelor, întrucât nu se
construieşte un mecanism de generare cu repetare a acestora.
Structurile de date externe se regăsesc în cele mai multe cazuri sub denumirea de
 fişiere. Când ating un nivel de structurare prin adrese suficient de dezvoltat, se formează
fişiere interdependente, iar în cazul unor structuri mai complexe se regăsesc sub denumirea de
baze de date .
În cazul în care conţinutul de lungime  L este tratat distinct, se ia în discuţie conceptul
de înregistrare fizică. Se por neşte de la faptul că într -un buffer, de regulă sunt stocate datele
ce corespund unei structuri de tip articol , ce se recunosc sub denumirea de înregistrare logică
sau articol logic. Scopul este de a face deosebirea între modul în care se dispun informaţii le
 pe suportul fizic şi modul în care sunt gândite organizările de date în raport cu prelucrările
 particulare.
Introducerea factorilor de blocare vine să complice lucrurile, dar să îmbunătăţească
indicele de folosire a zonelor de memorie.
Dacă:

 L’ = lg (structura de date de tip articol) L (4.1)

şi dacă există:

 L
k  (4.2)
 L'

unde parantezele drepte înseamnă partea întreagă a expresiei, raportul k reprezintă o expresie


mai simplificată a factorului de blocare.
De exemplu, dacă definim o structură de tip articol, ce conţine câmpuri ce conduc la o
lungime de 80 octeţi şi L = 256 octeţi:

256
k  3,2 3 (4.3)
80

79
În cazul în care k’ = 1, pe cei 256 baiţi ai bufferului este încărcat un articol, ce este în
fişier. Deci fişierul conţine în final n articole fizice şi tot n articole logice, gradul de încărcare
cu informaţie utilă fiind:

n 80 10 (4.4)
 g 1 100 100 30%
n 256 32

În cazul în care k’ = 2:

n/2 m 160 (4.5)


 g 2 100
n 256

unde:
0, daca n e numar  par 
m
1, daca n e numar impar 

Pentru k’ = 3:

n3 m 240
 g 3 100 (4.6)
n 256

Se observă că:

 g 3  g 2 g 1   (4.7)

Se vorbeşte de factorul de blocare optim care se stabileşte pentru fiecare tip de


memorie externă şi lungime de structură de date de tip articol, ce urmează a fi memorată în
fişier.
În continuare, luând în considerare numai aspectele care ţin de modul de stocare a
informaţiei, strict dependentă de aplicaţia programatorului, se fac următoarele specificaţii:
- se consideră fişierul ca structură de date contiguă dacă informaţiile utile sunt dispuse
unele în continuarea celorlalte, fără baiţi care să le separe;
- se consideră fişierul ca structură de date regulat necontiguă dacă între toate articolele
sau între grupuri de articole, având număr fix, există baiţi nefolosiţi, în acelaşi număr;
există posibilitatea de a construi o formulă de calcul a adresei articolului k , pornind de
la adresa altui articol j;
- se consideră structuri de date necontigue fişierele ale căror articole sunt dispuse unele
faţă de celelalte, la distanţe care sunt variabile aleatoare.
Trecerea de la memoria internă la memoria externă, ia în considerare modul de
organizare al fiecărui suport. Dacă la nivelul memoriei interne aceasta este privită ca un
vector, în cazul suportur ilor externe de informaţie organizate pe piste baiţii sunt priviţi ca
având dispunerea asemeni elementelor unei matrice. Linia indică pista pe care se află baitul,
iar coloana indică poziţia baitului pe pistă.
Organizarea pe sectoare determină luarea în co nsiderare a unei matrice
tridimensionale. Pentru fiecare suport realizatorii pun la dispoziţie formulele de calcul ale
adreselor cu luarea în considerare a elementelor de structură a suportului fizic.
Problema fragmentării informaţiilor determină stocarea de date necesare localizării
 părţii ce se continuă într -o altă zonă a suportului.

80
Pentru simplificarea prezentării se consideră un suport extern S , căruia i se asociază un
model matriceal de dispunere a baiţilor. Baitul bij reprezintă baitul al  j-lea, aflat pe pista i a
suportului. Suportul are n piste, iar pe o pistă se află m baiţi.
Pentru început se presupune că baiţii au aceeaşi destinaţie, respectiv de a memora
informaţii utile. Nu există baiţi care stochează informaţii privind structura suportului şi modul
de ocupare a acestuia.

4.2. Criterii de clasificare a fişierelor

Şi în cazul clasificării fişierelor, asemenea datelor interne, există o multitudine de


criterii, fiecare fişier fiind clasificat cu unul sau mai multe atribute ce corespund criteriilor de
clasificare.
a) Criteriul lungimii articolelor ce alcătuiesc fişierul le împarte în:
- fişiere cu articole de lungime fixă –  sunt formate din elemente având aceeaşi
lungime; fişierele sunt asemănătoare vectorilor ca structuri de date interne;
elementele sunt omogene şi sunt dispuse unele în continuarea celorlalte;
- fişierele cu articole de lungimi diferite, dar cunoscute –  se consideră m tipuri de
articole, având fiecare lungimea l 1 , l 2 , …, l m; fişierul conţine aceste elemente
dispuse într-o anumită ordine sau în ordine oarecare; în ultima situaţie este
necesară memorarea de informaţii care să permită identificarea tipurilor de
articole şi lungimea acestora;
- fişiere cu articole de lungime diferită, dar necunoscută – ceea ce se cunoaşte este
legat de faptul că lungimile articolelor se află cuprinse între două limite: lungimile
articolelor sunt variabile aleatoare, aparţinând unui interval definit; în mod
obligatoriu, primul câmp conţine lungimea articolului.
 b) Criteriul informaţiilor ce definesc regulile referitoare la dispunerea elementelor,
împarte fişierele în:
- fişiere având ca singur mod de dispunere poziţia articolelor;
- fişiere cu elemente sortate după un câmp numit cheie a articolului, în funcţie de
care se face reperarea în fişier.
c) Criteriul informaţiilor de localizare a articolelor care alcătuiesc fişierul;
- fişiere care nu conţin informaţii asupra poziţiei articolelor –  singura modalitate de
a selecta un articol este parcurgerea tuturor articolelor care îl preced;
- fişiere care au definite zone ce conţin informaţii referitoare la adresele unor grupe
şi subgrupe de articole –  pentru a identifica un anumit element se localizează
grupul şi apoi subgrupul de articole; o dată identificat subgrupul, selectarea
elementului căutat este rezultatul parcurgerii articol de articol până la găsirea
respectivului element;
- fişiere în care elementele conţin informaţii ce permit conturarea de liste înlănţuite
sau arbori pe suporţi de memorie externă –  complexitatea legăturilor dintre
articolele fişierului determină un volum de informaţie privind adresele articolelor
cu care un element intră într -o anumită relaţie.
d) Criteriul operaţiilor ce sunt efectuate de fişiere determină gruparea acestora în:
- fişiere destinate scrierii datelor;
- fişiere destinate citirii datelor;
- fişiere destinate efectuării operaţiilor de actualizare.
Oricare dintre fişierele unei grupe, în raport cu scopurile prelucrării îşi modifică
atributele. De exemplu, un fişier care se creează este destinat scrierii datelor. La

81
consultare acel aşi fişier aparţine grupei a doua, iar dacă înaintea consultării se
efectuează actualizări, acelaşi fişier aparţine celei de a treia grupe.
e) Criteriul gestiunii fişierelor ia în considerare existenţa unui sistem de funcţii de
 prelucrare care asigură efectuarea operaţiilor specifice lucrului cu fişiere, numit
sistem de gestiunea fişierelor. Acest criteriu împarte mulţimea fişierelor în:
- fişiere care sunt prelucrate prin apelarea de funcţii ale unui sistem de gestiune – 
rezolvă întreaga problematică legată de organizarea şi accesul la date; funcţiilor
sistemului de gestiune a fişierelor le corespund instrucţiuni; parametrii
instrucţiunilor devin parametrii reali ai funcţiilor sistemului de gestiune;
 programatorul abordează întreaga problematică numai din punct de vedere logic al
rezolvării problemei sale;
- fişiere pentru care sunt definite funcţii primitive de realizare a operaţiilor
elementare cu datele destinate suporturilor externe  –   programatorului îi revine
sarcina să gestioneze totalitatea informaţiilor  necesare regăsirii elementelor ce
alcătuiesc fişierul; pentru programator, fişierul este o masă amorfă de baiţi, având
conţinut şi poziţii; el efectuează transferuri de baiţi din zone de memorie spre
suportul extern, indicând adrese acestor zone, lungimea zonei şi un mod de
reparare a fişierului; există situaţii în care se efectuează lucru direct în bufferul
asociat fişierului cu care se lucrează; în acest caz fişierul apare ca o resursă iar
gestiunea acestei resurse este lăsată integral la dispoziţia prog ramatorului;
- fişiere care se construiesc şi se utilizează folosind instrucţiunile limbajului de
asamblare  –   programatorul defineşte totalitatea condiţiilor pentru efectuarea
operaţiilor cu fişiere; sunt definite şi încărcate etichetele care sunt scrise; se
definesc bufferele şi se gestionează; se calculează adresele de pe suportul extern
unde vor fi scrise datele; programatorul preia în programele sale tot ceea ce
efectuează funcţiile primitive sau sistemul de gestiune a fişierelor; programul în
care apar f işierele gestionate de programator nu apelează alte funcţii decât cele
scrise de acesta şi foloseşte numai instrucţiuni ale limbajului de asamblare, cel
mult seturi de macroinstrucţiuni.
f) Criteriul organizării fişierelor, ia în considerare existenţa sau inexistenţa unor
informaţii de regăsire a datelor, după cum urmează:
- fişierele cu organizarea secvenţială - succesiune contiguă de articole fără existenţa
unor elemente de informare, altele decât delimitatorii de început şi de sfârşit; dacă
se ia în considerare similitudinea acestui mod de organizare cu înregistrarea pe o
casetă a şlagărelor unei formaţii rock, avem imaginea clară a tuturor posibilităţilor
de lucru cu fişierele secvenţiale; accesul la un şlagăr presupune audierea celor
care-l preced; ştergerea unui şlagăr, altul decât cel de la sfârşit presupune
existenţa a două casete; înregistrarea unui nou şlagăr, se face numai după ultimul
şlagăr anterior;
- fişierele însoţite de informaţii de tip index –  presupun sortarea articolelor după
chei şi împărţirea acestora în submulţimi; punerea în corespondenţă a elementelor
din submulţimi cu adresele fizice determină realizarea într -un fişier a unei mulţimi
de subfişiere secvenţiale; cu ajutorul indecşilor se identifică subfişierul secvenţial
şi articolul căutat este preluat după ce a fost localizat prin parcurgerea articol după
articol a subfişierului; operaţiile de actualizare, vizează ştergerea de articole,
adăugarea de articole la sfârşitul fişierului sau subfişierelor, modificarea de
câmpuri, rescrier ea de articole şi inserarea de articole; subfişierele sunt masive
unidimensionale contigue, formate din articole; ştergerea este o operaţie de
dezactivare a elementelor, fără realizarea deplasării celorlalte elemente cu o
 poziţie spre stânga, deci fizic ştergerea unui articol nu se produce, operaţia fiind

82
numai la nivel logic, în sensul că accesul la date este precedat de un test asupra
caracterului activ sau neactiv al articolului; inserarea de articole, pentru a menţine
criteriul ordonării elementelor care a determinat împărţirea mulţimii în
submulţimi de articole, presupune realizarea de liste înlănţuite; articolul inserat
este dispus într- o zonă disponibilă numită folcloric, zona de depăşire.
- fişiere ale căror articole sunt dispuse aleator pe suport –   cunoscându-se
dimensiunile fişierului se alocă o zonă pe suportul extern printr -o operaţie numită
 preformare; datele sunt puse în corespondenţă printr -un procedeu oarecare cu
adresele unde vor fi scrise; rezultă că procedeul de punere în corespondenţă este
cu atât mai performant cu cât el realizează o dispunere mai uniformă a articolelor
în zona rezervată; există posibilitatea ca folosind algoritmi de randomizare să se
obţină elemente ce intră în calculul adresei fizice a începutului zonei din suportul
extern în care se scrie un articol; pornind de la imposibilitatea să se genereze
numere diferite care să îndeplinească condiţia de apartenenţă la subintervale, pe
măsură ce se scriu articole în fişiere, are loc fărâmiţarea zonei rezervate; se
construiesc funcţii pentru gestionarea articolelor care au aceeaşi adresă fizică prin
calcul; aceste fişiere cu organizarea aleatoare, permit efectuarea întregii game de
operaţii, cu condiţia ca mecanismul de obţinere a adresei fizice să se menţină
neschimbat; fişierul organizat aleator apare ca o structură necontiguă în care
fiecare element este localizat pe baza unei formule, având ca parametru valoarea
unui câmp ce intră în structura articolului, câmp numit cheia articolului;
numeroasele lucrări care prezintă limbajul COBO L, redau imagini sugestive ale
modului în care se implementează filozofia fiecărui mod de organizare a
fişierelor, deci realitatea este alta, dacă se are în vedere organizarea matriceală a
suportului şi modul static în multe cazuri de alocare a memoriei ex terne.
g) Criteriul suportului unde este stocat fişierul împarte fişierele în:
- fişiere pe cartele perforate;
- fişiere pe bandă perforată;
- fişiere pe bandă magnetică;
- fişiere pe suport optic;
- fişiere pe disc;
- fişiere pe dischete;
- fişier în memoria internă.
Prelucrările moderne, neîngrădite de restricţiile lipsei de memorie internă, au condus
la citirea unui fişier ca un singur articol foarte mare şi prelucrarea acestuia în memorie. Logic
apare o singură instrucţiune de citire, deci apelarea o singură dată a funcţiei în realitate au loc:

 L f  
cf   m  (4.8)
 L

citiri fizice, unde:


cd - citiri fizice din fişier;
 L f  - lungimea fişierului, dată în baiţi;
 L - lungimea unei înregistrări fizice la o citire;
m - variabila booleană ce este 0, dacă Lf  este divizibil prin L şi 1 în caz contrar.
h) Criteriul privind modul de efectuare a operaţiilor de intrare/ieşire grupează fişierele
în:
- fişiere de tip  stream în care datele la scriere sau la citire sunt câmpuri elementare
sau alte tipuri structuri de date, constituite într- un şir, cu indicarea formatului după

83
care se fac mai întâi convenţiile şi apoi se stabilesc lungimile şirurilor care vor fi
scrise pe suport; fişierul apare ca o succesiune de elemente oarecare ale căror
 poziţii sunt deduse, dacă se iau în calcul informaţiile pe care le oferă descriptorii
de format şi locul pe care fiecare element îl ocupă în lista de parametri ai funcţiei
apelate la scriere; este de dorit ca aceleaşi liste de variabile de la scriere să fie
utilizate şi la apelul funcţiilor de citire, iar descriptorii de format să se menţină
aceiaşi pentru a oferi succes prelucrărilor;
- fişiere de tip record  în care datele sunt caracterizate prin lungime şi adresă de
început; articolele au lungime fixă sau variabilitatea lungimilor este în cadrul unei
mulţimi formate din câteva elemente; operaţiile de lucru cu fişierele de acest tip
nu sunt în general preceda te sau urmate de conversii; operaţiile ce preced
calculele sunt lăsate la dispoziţia programatorului;
De exemplu, fişierul stocurilor de materiale este:
- un fişier de tip record;
- are organizare indexată;
- se află pe disc;
- este gestionat cu funcţiile unui sistem de gestiune a fişierelor;
- are articole de lungime fixă;
- se efectuează toate operaţiile de actualizare pe el;
- conţine informaţii asupra poziţiei anumitor articole;
- articolele sunt identificate după codul materialului care joacă rol de cheie de
articol;
- fiecare material are o cheie unică;
- fişierul este sortat crescător.
Astfel, un fişier oarecare este caracterizat înscriind oricare dintre informaţiile ce
rezultă din criteriile specificate.

4.3. Fişiere secvenţiale

Fie mulţimea de elemente  E 1 , E 2 , …, E n  oarecare ce corespund structurilor de date
SD1 , SD2 , …, SDn definite într-un program P .
Elementele  E i, i = 1, 2, …, n, sunt şiruri de baiţi caracterizate printr -un conţinut
rezultat din iniţializarea structurilor de date SDi, i = 1, 2, …, n. Pe un suport extern se alocă
elementelor E 1 , E 2 , …, E n zone de memorie de lungime:

lg SDi lg (4.9)

fişierul având în final lungimea:


n
 L f   n lg lg SDi (4.10)
i 1

De obicei, lg(α) = 2. Cei doi baiţi ataşaţi fiecărui element vor conţine lungimea
articolului:

cont  lg SDi
  (4.11)

84
Fişierul este caracterizat printr -o adresă a articolelor. Astfel:

adr  E 1 A lg (4.12)

unde,  A reprezintă adresa fizică a primului bait a primului articol căruia i s -a ataşat în faţă
lg(α)  baiţi pentru a memora lungimea articolului respectiv:

i 1
adr  E i  A  cont   j lg i (4.13)
 j 1

sau:
i 1
adr  E i  A lg SDi i lg (4.14)
 j 1

unde, β k  reprezintă grupul de baiţi de lungime lg(α) ataşat elementului E k  în fişier. Dacă:

lg SD1 lg SD2 ... lg SDn (4.15)

atunci:

cont  1 cont  2 ... cont  n


(4.16)

caz în care, în eticheta de început a fişierului se specifică tipul articolului lungime fixă. De
asemenea, se memorează şi lungimea, iar lg(β k )  devine zero.
Adresa unui element oarecare  E i, este obţinută prin relaţia:
i 1
adr  E i  A lg SD j (4.17)
 j 1

Cu fişierele secvenţiale se efectuează următoarele proceduri:


- crearea unui fişier secvenţial constă în dispunerea elementelor  E 1 , E 2 , …, E n  pe
suport în această ordine;
- consultarea integrală sau parţială a fişierului baleiază din aproape în aproape
elementele fişierului şi se prelucrează cele care prezintă interes;
- adăugarea elementului  E m se efectuează la adresa:

adr  E m adr  E n lg S n lg (4.18)

ceea ce pune în evidenţă că adăugarea se face numai la sfârşitul fişierului;


- interclasarea a două fişiere;
- sortarea fişierelor, care constă în obţinerea unui fişier în care articolele au o astfel
de dispunere încât pentru un câmp  x există relaţia:

cont (E 1.x) > cont (E 2.x) > … > cont (E n.x) (4.19)

dacă sortarea s-a făcut descrescător, sau:

85
cont (E 1.x) cont (E 2.x) … cont (E n.x) (4.20)

dacă sortarea s-a făcut crescător.


Întrucât se lucrează în cele mai multe cazuri cu fişierul în totalitatea lui, nu este
 justificată memorarea de informaţii pentru anumite articole.
Explorarea fişierelor secvenţiale corespunde unei structuri de date contigue, asemeni
unui vector de structură sau a unei înşiruiri de diferite structuri, cu posibilitatea calculului
adresei unui element oarecare:

adr (suc (E  ))


i = adr (E 
 )
i + lg (SD )
i + lg (   ) (4.21)

adr (pred (E  ))


i = adr (E  ) – 
i  lg (SDi-1) –  lg (   ) (4.22)

Se spune că fişierul este închis dacă:

lim  suc m  E i  E n (4.23)


m

Se spune că fişierul este deschis dacă:

lim  pred m  E i E 1 (4.24)


m

O astfel de abordare determină continuarea prelucrării chiar dacă există tentative de a


închide un fişier deja închis sau de a deschide un fişier deja deschis.
Apare problema privind parametrii funcţiei de deschidere. Dacă sunt luaţi în
considerare parametrii primei deschideri, problema este rezolvată, tentativele de deschidere a
unor fişiere deja deschise fiind inefective.
Întrucât există formule de calcul pentru adresele fiecărui element din submulţimea  E 1 ,
 E 2 , …, E n nu se justifică construirea unui vector a adreselor în fişier a1 , a2 , …, an pentru aceste
elemente.
Sistemele de operare evoluate gestionează închiderea fişierelor la închiderea execuţiei
 programelor.

4.4. Fişiere secvenţial –  indexate

Se consideră o mulţime de elemente  E 1 , E 2 , …, E n generate după structura SD şi un


câmp x astfel încât:

cont (E 1.x) cont (E 2.x) … cont (E n.x) (4.25)

deci, şirul elementelor este sortat crescător după câmpul  x, care joacă rol de cheie a articolelor
în structura SD de generare.
Elementele de sortare vor fi memorate, fiecare ocupând o zonă de lungime:

lg SD lg (4.26)

86
unde, γ reprezintă o zonă în care este memorată o adresă fizică de pe suport, ţinând seama de
structurarea contiguă a suportului.
Fişierul are un fond informaţional de lungime:

 L f   n lg lg SD (4.27)

Se construieşte mulţimea perechilor:

(cont(E i.x).a )
i (4.28)

a cheilor şi adreselor articolelor ce intră în componenţa fişierului. Se calculează:


n
cont  E i . x  x (4.29)
2 i 1

şi rezultă o împrăştiere suficient de mare care să reducă şansele găsirii unei formule de calcul
a adresei fizice a unui articol, folosind strict ca informaţie cheia acestuia.
Dacă se acceptă ideea utilizării unui arbore binar cu 4 noduri terminale, mulţimea
elementelor  E 1 , E 2 , …, E n este împărţită în 4 subşiruri, având un număr aproape identic de
elemente, reţinând adrese şi cheile elementelor ce delimitează fiecare subşir de articole:

(cont(E kj.x), akj ),  j= 1, 2, 3, 4; k = 1, 2, …, n ( 4.30)

unde k 1 k 2 k 3 k 4.


Perechile de delimitare ale începutului de subşir, se obţin astfel:
- pentru primul subşir:

Q1 cont  E 1 . x , a1 (4.31)

- pentru al doilea subşir:

Q2 cont  E 1 m . x , am 1
(4.32)

- pentru al treilea subşir:

Q3 cont  E 1 2m . x , a2m 1


(4.33)

- pentru al patrulea subşir:

Q4 cont  E 1 3m . x , a3m 1


(4.34)

Perechile pentru delimitarea sfârşitului pentru fiecare din cele 4 subşiruri sunt:

87
 P 1 cont  E m . x , am
 P 2 cont  E 2 m . x , a2 m
(4.35)
 P 3 cont  E 3m . x , a3m
 P 4 cont  E n . x , an

S-a considerat că fiecare subşir are m elemente, cu excepţia ultimului şir care are 1, 2
sau 3 elemente mai puţine.
Setului de date i se asociază arborele binar:

(Q1, P4)

(Q1, P2) (Q3, P4)

(Q1, P1) (Q2, P2) (Q3, P3) (Q4, P4)

Fig. 4.3. Structura setului de date i

Dacă, de exemplu, fişierul sortat este organizat secvenţial şi se doreşte citirea


articolului penultim, care are cheia 7233 , în mod normal trebuie să fie citite cele n-2 articole
care îl preced. Dacă însă fişierul este înzestrat cu această informaţie pe care o conţine arborele
 binar cu 4 noduri terminale, inspectarea n odului rădăcină permite vizualizarea faptului că
articolul căutat cu cheia 7233 este în fişier, adică:

cont (E 1.x) ≤ 7233 ≤ cont (E n.x) (4.36)

Întrucât:

cont (E n .x) cont (E i .x) (4.37)


7233
2

rezultă că se parcurge pe nivelul inferior, nodul din dreapta.


Întrucât:

cont (E 2n 1 .x) cont (E n .x)


7233
2 (4.38)

rezultă că se parcurge pe nivelul inferior, nodul din dreapta.


Odată ajungând pe ultimul nivel al arborescenţei, se reţin adresele a3m+1 şi an şi se
 baleiază secvenţial subfişierul delimitat astfel. Deci, se vor baleia mai puţin ¼ din totalul
elementelor fişierului.
Pentru construirea arborilor binari asociaţi există numeroşi algoritmi de partiţionare a
fondului informaţional. Important este ca numărul căutărilor secvenţiale să fie cât mai redus.

88
Trebuie realizat un echilibru între numărul de nivele ale arborelui binar şi numărul
subfişierelor, deoarece creşterea numărului de subfişiere conduce la creşterea duratei de acces
la acestea din cauza  parcurgerii unui număr
număr prea mare de noduri în arborele binar.binar.
În cazul în care se doreşte inserarea unui articol  E  j într-un astfel de fişier se identifică
 poziţia lui, astfel încât:
încât:

cont (E k 
k .x) cont (E  j.x) … cont (E k+1
k+1.x) (4.39)

În acest caz, articolul ca atare este memorat într- o zonă rezervată a fişierului, legătura
cu celelalte articole efectuându-se prin:

cont  k  ad r  E  j
(4.40)
cont   j ad r  E k  1

ce corespunde unei inserări de elemente într -o listă.


La un număr mare de inserări, parcurgerea listelor reduce performanţa programului de
exploatare a fişierului secvenţial –  indexat. Acestea justifică trecerea la reorganizarea
fişierului. Prin reorganizare se înţelege crearea unui nou fişier, în care elementele se dispun
într-o aceeaşi zonă fără a mai exista informaţii de legătură, ca în cazul în care ar fi dispersate
 pe suport.
La reorganizare, se construieşte un nou binar al indecşilor. Utilizarea arborilor binari
are aici numai un caracter exemplificativ. Tipul de arbore depinde în pr incipal de împrăştierea
cheilor în intervalul pe care sunt definite. Informaţiile privind arborele asociat tabelei de
indecşi se stochează pe suport şi face parte din fişier.
Pentru o parcurgere mai rapidă, în cazul în care este posibil, informaţiile afere nte
structurii arborescente a indecşilor se încarcă în memoria internă şi se lucrează cu ele folosind
funcţiile de parcurgere ale unui arbore.
În limbajele precum C şi C++, fiecare programator implementează algoritmi proprii
 pentru organizarea secvenţial – indexată,
– indexată, iar prin comparaţia comportamentului lor statistic cu
alţi algoritmi existenţi, sunt dezvoltaţi sau abandonaţi.

4.5. Fişierele bazei de date

Din punctul de vedere al modului de structurare a datelor, bazele de date reprezintă o


formă mai generală de reprezentare a datelor, care reflectă caracteristici ale elementelor
omogene ce definesc mai multe mulţimi.
Fie mulţimile  M 1 , M 2 , ..., M k k,  formate fiecare din n1 , n2 , ..., n k  elemente, aşa fel alese
încât caracterizarea completă a unui element  x j (M 1 ) este efectivă dacă sunt prezentate
datele d  j1 , d  j2 , ..., d  jk , unde d  jk  (M k k )  .
În plus, fiecare mulţime are elementele structurare după o anumită regulă. Punerea în
corespondenţă a elementelor celor k  mulţimi, conduc în numeroase situaţii, ca unui element
din mulţimea  M i să-i corespundă mai multe elemente din mulţimea  M  j  sau mai multor
elemente din mulţimea M i să le corespundă un singur element din mulţimea  M h.
Se observă că necesitatea de a separa informaţiile în fişiere distincte este dată în
 principal din dorinţa de a diminua redundanţa redundanţa dintr -un fişier, pe de o parte, şi pentru a permite
noi facilităţi de exploatare a structurilor de date, pe de altă parte.

89
În continuare, se consideră un exemplu clas ic, foarte frecvent utilizat în descrierea
 principiilor şi fundamentelor
fundamentelor de realizare a bazelor de date.
date.
Fie mulţimea M 1 a persoanelor dintr- o localitate, care se află într -una din relaţiile:

 x este vecin cu y
 x este fiul lui y
 x este tatăl lui y
 x este soţia lui y
 x este soţul lui y

Un individ al colectivităţii, este descris prin:


- nume;
- adresă;
- raport cu alţi indivizi, respectiv vecini, rude;
- tip automobil/marca;
- culoare automobil,
- an cumpărare;
- loc de muncă.
Fie mulţimea M 2 mulţimea automobilelor, în care elementele se află în relaţiile:

marca x este produsă de y


marca x are capacitate z
capacitatea z are consumul specific w

Un autoturism este descris prin:


- nume producător;
- model;
- culoare;
- an fabricaţie;
- capacitate;
- tip combustibil;
- caracteristici motor;
- consum la 100 km.
Fie M 3 mulţimea locurilor de muncă, formată din elemente caracterizate prin:
- nume instituţie;
- capital social;
- tip instituţie;
- număr salariaţi;
- nume compartiment;
- nume meserii acceptate la compartimentul respectiv;
- nume lucrători din compartiment.
Fie  M 4 mulţimea impozitelor care se aplică mijloacelor de transport, formată din
elementele:
- limita inferioară a capacităţii cilindrice;
- limita superioară a capacităţii cilindrice;
- impozit;
- taxa CASCO pentru primii 3 ani de funcţionare;
- taxa CASCO pentru maşinile cu vechime cuprinsă între 4 –  10 10 ani;
- taxa CASCO pentru maşinile cu vechime mai mare mar e de 10 ani.

90
Deşi se discută foarte mult, cea mai costisitoare etapă din activitatea de manipulare a
 bazelor de date o r eprezintă încărcarea acestora.
În cazul de faţă, datele nu suferă o uzură morală rapidă pentru că vizează indivizii
dintr-un cartier sau bloc. În cazul în care fenomenul are o dinamică accelerată, se observă că
la terminarea încărcării 60 –   80% din date sunt uzate moral şi baza de date practic este
inoperantă.
În cazul considerat, cele patru mulţimi conduc la date ce alimentează baza de date şi se
trage concluzia că în continuare ea este complet încărcată.
Pentru a vedea puterea de prelucrare pe care software-ul bazei de date o are,
exemplificăm câteva cereri:
- listarea locurilor de muncă ale membrilor familiei lui x;
- listarea proprietarilor de autoturisme cu culoarea  z ;
- listarea tuturor celor care lucrează la locul de muncă w şi au autoturisme pentru care
 plătesc taxa CASCO cuprinsă în intervalul [a, b];
- există o relaţie între capitalul social al firmelor şi uzura morală a autoturismelor pe
care le au salariaţii lor?
- listarea proprietarilor care plătesc un impozit mai mare dec ât C  lei şi taxa CASCO
mai mare decât D lei;
- există meseriaşi de tipul  z  care au maşini de tipul u absolut noi şi care au vecini în
aceeaşi situaţie?
Dacă toate datele despre un individ, sunt înregistrate într -o structură cuprinzătoare,
care conţine elementele de descriere ale celor patru mulţimi, se observă o multitudine de
repetări, ceea ce conduce de fapt la regruparea informaţiilor. Cele 4 mulţimi nu sunt un dat, ci
sunt rezultatul unei analize a modului în care sunt sistematizate şi concentrate datele.
Pentru elementele din mulţimea M 1 se asociază arborescenţa:

 persoana x

familie vecini

vecinul vecinul vecinul vecinul


soţie copii din faţă din spate din stânga din dreapta

copilul Y1 copilul Y10

Fig. 4.9. Arborescenţa asociată elementelor mulţimii M1

Pentru elementele mulţimii M 2 se asociază arborescenţa din figura 4.10.

91
Producător 

Marca 1 Marca 2 … Marca n

Model Culoare An fabric.

Fig. 4.10. Arborescenţa asociată elementelor mulţimii M2

Şi pentru elementele mulţimii M 3 şi M 4 se asociază, de asemenea, arborescenţe.


Deci, dacă fiecare mulţime este concretizată în câte un fişier, articolele în cadrul
fiecărui fişier sunt legate între ele, funcţie de structura arborescenţei asociate.
Baza de date presupune atât legături între articolele fiecărui fişier, cât şi legături între
fişiere.
Se ridică întrebarea: pentru a răspunde la toate solicitările, este necesară constituirea
unor structuri de adrese; toate structurile de adrese sunt create de la început sau acestea se
creează pe măsură ce necesităţile de prelucrare impun acest lucru?
În fişierul persoanelor, câmpul corespunzător autoturismului conţine şi adresa
articolului ce corespunde mărcii şi culorii autoturismului.
Dacă interesează listarea proprietarilor de maşini de culoare  z se, procedează astfel:
- se construieşte şirul:

S = (s1 , s2 … sn ) (4.41)

unde  si este adresa articolului pentru descrierea autoturismului al cărui proprietar este
 persoana ai din fişierul de persoane.
- din fişierul autoturismelor se extrage subşirul:

 P = (p1 , p2 , … pn ) (4.42)

al adreselor articolelor ce corespund autoturismelor de culoare  z :

 P ∩ S = (sk1 , sk2 , … skm ) (4.43)

ceea ce înseamnă că persoanele ale căror înregistrări ocupă poziţiile k 1 , k 2 , … k m cu maşini de
culoare z şi afişarea:

cont (ref (k  )


i . nume), i = 1, 2, …, m (4.44)

conduce la tabelarea numelor de persoane care posedă maşini de culoare  z .


Interogarea unei baze de date revine la constituirea mai multor şiruri, din fiecare
mulţime câte unul. Numărul de şiruri maxim este de regulă egal cu numărul mulţimilor de
articole definite.

92
Astfel, dacă se doreşte listarea tuturor celor care au locul de muncă w şi au autoturism
 pentru care plătesc o taxă CASCO cuprinsă în intervalul [a, b] , se procedează astfel:
- se construieşte şirul adreselor persoanelor care au locul de muncă w:

( γ  j = const (w.adresa_persoana j )), j = 1, 2, … n (4.45)

- se construieşte şirul capacităţilor cilindrice:

(C 1 , C 2 , … C h ) (4.46)

al autoturismelor, însoţite de adresele articolelor din fişierul asociat mulţimii  M 2, ce


corespund autoturismelor pentru capacităţile identificate:

m1 m2 … me  (4.47)

De regulă, e ≥ h, întrucât există mai multe mărci de maşini cu aceeaşi capacitate


cilindrică.
Prin analiza conţinutului fişierului  M 4, rezultă că autoturismele pentru care se plăteşte
o taxă CASCO cuprinsă în intervalul [a, b] , sunt cele care au capacităţile cuprinse între:

[  1 , 1 ] (4.48)

[  2 , 2 ] (4.49)

Aceste limite conduc la filtrarea elementelor mulţimii C 1 , C 2 , … C h, astfel încât rezultă
submulţimea adreselor:

m’ = {m1’, m2’, … m p’} (4.50)

de regulă p < e.
Se construieşte şirul de adrese:

u = (γi 0 ≤ 1 k; cont (γ.i.adresa_autoturism) m’)  (4.51)

Scrierea elementelor şirului:

(cont (γu->nume_persoana)), u = 1, 2, …, w (4.52)

rezolvă cererea formulată iniţial.


În exemplul dat, s-a procedat la utilizarea de variabile pointer. Pentru a face deosebire
între pointerii folosiţi la referirea elementelor din memoria internă şi pentru a evita confuziile
ce apar folosind conceptul de pointer spre fişier, întrucât este deja concentrat tipul de dată
 FILE . În continuare se utilizează conceptul de pointer extern.
Fiind dat un suport extern al căror baiţi au adresa cuprinsă între valorile [A,B]∩N , A
 B şi A, B N , variabila v se spune că este pointer extern dacă şi numai dacă:

cont (v)  [A, B] ∩ N (4.53)

 Poziţionarea citirii este de fapt atribuirea sau modificarea conţinutului unei variabile
de tip pointer extern, aşa fel încât ea să conţină adresa baitului de unde începe citirea/scrierea.

93
În enunţul problemei,  M 1 , M 2 , M 3 şi  M 4 reprezintă fişiere, adică variabile pointer care
sunt iniţializate cu adresele baiţilor de unde începe alocarea memoriei externe pentru fiecare
din cele patru fişiere. Dacă se are în vedere că fişierul are şi o etichetă de început de lungime,
cont(M  )
i + reprezintă adresa primului articol din fişierul M i.
Presupunând că fişierului  M i i se alocă o zonă contiguă, cuprinsă între adresele [A ,i
i , rezultă că:
 B ]

cont (M  )
i = Ai  (4.54)

cont (  )
i = Bi –  lg (SD )
i (4.55)

unde i reprezintă adresa ultimului element de structură SDi al fişierului M i care are o etichetă
de sfârşit de fişier de lungime .
Revenind la fişierele interdependente, fişierele înlănţuite, se observă că structurile de
date ce se asociază fişierelor, pe lângă informaţiile utile date de programator, se definesc încă
multe câmpuri de tip pointer extern care asigură traversările prin structura externă pe timpul
execuţiei.
Folosind definirea pointerilor extern, locul de muncă w este identificat prin pointerul
extern pw ce corespunde adresei articolului respectivului loc de muncă.
Mulţimea m1 , m2 , … me, reprezintă valori ale variabilei pointer extern r , asociat
fişierului M 2:

m= r  j cont (r  j->capacitate) C 1 , C 2 , … C h  , i j nrr (M 2 ) (4.56)

unde nrr( ) este o funcţie care defineşte numărul de articole existent într -un fişier:

nrr : F 1 , F 2 , … F k   N (4.57)

unde F este mulţimea fişierelor, date prin valoarea iniţială a pointerilor lor externi:

m’ = mi cont (m j-> capacitate) [  1 , 1 ] U [  2 , 2 ] (4.58)

Selectarea elementelor reprezintă construirea de şiruri de adrese şi apoi prin operaţii


de reuniune, intersecţie, se obţin şirurile de elemente care prezintă rezolvarea problemei.
Utilizarea bazelor de date reprezintă un domeniu al informaţiei aplicate. Construirea
de sisteme de gestiune a bazelor de date este o preocupare de mare importanţă pentru toţi
realizatorii de software, iar elementele prezentate definesc doar o serie de trăsături generale
ale filozofiei bazelor de date.
Fiecărui mecanism particular îi corespund reguli precise de definire, iniţializare şi
modificare a pointerilor externi ce se asociază fiecărui articol.
De menţionat că în spatele oricărei cereri de informaţie furnizată de utilizatorul bazei
de date, se află pointeri externi, care în unele cazuri sunt deja iniţializaţi şi se folosesc ca
atare, iar în alte cazuri, sunt numai definiţi şi îşi încarcă conţinutul în funcţie de cerere.
Există situaţii când pentru a rezolva o problemă, se definesc noi pointeri externi şi se
activează proceduri de iniţializare, în concordanţă cu problema de rezolvat. În acest caz se
creează noi legături între elemente, cu noi posibilităţi de selectare a informaţiilor din baza de
date.
Statistic, diversitatea de fişiere face ca utilizarea unora să fie în anumite cazuri mai
eficiente decât a altora. Experienţa fiecărui programator este cea care hotărăşte tipul de fişier
cu care se lucrează pentru fiecare aplicaţie.

94
La alegerea tipului de fişier cu care se lucrează, concură o serie de factori din care se
enumără:
- numărul de elemente care alcătuiesc fişierul;
- tipul prelucrărilor: integrale, după o cheie, prin selecţie;
- frecvenţa şi tipul operaţiilor de actualizare;
- durata impusă de prelucrare şi necesitatea sortării datelor;
- timpul disponibil pentru elaborarea programelor;
- costuri de elaborare şi utilizare;
- sistemul de calcul disponibil;
- sistemele de gestiune a fişierelor cunoscute şi disponibile.
Alegerea tipului de fişier şi comportamentul algoritmilor implementaţi pentru
regăsirea informaţiilor, sunt rezultatul unei analize statistice a datelor, înregistrate prin
urmărirea în timp a comportamentului la prelucrare.

Teme de control

1. Să se memoreze într-un fişier text informaţiile necesare încărcării a două


matrice rare şi să se listeze rezultatul înmulţirii dintre acestea.
2. Pentru un masiv unidimensional de elemente articol de tip  piesă, să se
listeze într-un fişier binar   toate piesele care nu sunt în conformitate cu
anumite caracteristici citite de la tastatură.

Bibliografie

Ion IVAN, Marius POPA, Paul POCATILU - Structuri de date, Vol. 1 si 2, Editura ASE,
Bucuresti, 2008.

95
5. Listele –  structuri dinamice necontigue

Obiectivele unităţii de învăţare

După studierea acestei unităţi de învăţare, studenţii îşi vor însuşi cunoştinţe
teoretice şi vor deprinde abilităţi practice de lucru cu liste simplu şi dublu
înlănţuite. Studenţii vor învăţa lucrul cu pointeri şi operaţiile pe liste
simple şi duble, liniare şi circulare.

5.1. Consideraţii privind structurile de date de tip listă

O listă liniară (numită şi listă înlănţuită -”Linked List”) este o colecţie de n>=0
elemente x[1], … x[n] toate de un tip oarecare, numite noduri între care există o relaţie de
ordine determinată de poziţia lor relativă. Ea este deci o mulţime eşalonată de elemente de
acelaşi tip având un număr arbitrar de elemente. Numărul n al nodurilor se numeşte lungimea
listei. Dacă n=0, lista este vidă. Dacă n>=1, x[1] este primul nod iar x[n] este ultimul nod.
Pentru 1<k<n, x[k] este precedat de x[k- 1] şi urmat de x[k+1].
Acest tip de structură de date se aseamănă cu o structură standard: tipul tablou cu o
singură dimensiune (vector), ambele structuri conţinând elemente de acelaşi tip iar
între elemente se poate stabili o relaţie de ordine. Una dintre deosebiri constă în
numărul variabil de elemente care constituie lista liniară, dimensiunea acesteia nu trebuie
declarată şi deci cunoscută anticipat (în timpul compilării) ci se poate modifica dinamic în
timpul execuţiei programului, în funcţie de necesităţi. Astfel utilizatorul nu trebuie să fie
 preocupat de posibilitatea depăşirii unei dimensiuni estimate iniţial, singura limită fiind
mărimea zonei heap din care se solicită memorie pentru noile elemente ale listei liniare. Un
vector ocupă în memorie un spaţiu continuu de memorie, pe când elementele unei liste simplu
înlănţuite se pot găsi la adrese nu neapărat consecutive de memorie.
O altă deosebire avantajează vectorii, deoarece referirea unui element se face prin
specificarea numărului de ordine al respectivului element, pe când accesul la elementele unei
liste liniare se face secvenţial, pornind de la capul listei (adresa primului nod al listei) până la
ultimul element al ei, ceea ce măreşte uneori considerabil timpul de acces la un anumit
element. Pentru o listă liniară este obligatoriu să existe o variabilă, declarată în timpul
compilării, denumită cap de listă care să păstreze adresa primului element al li stei. Pierderea
acestei valori va duce la imposibilitatea accesării elementelor listei liniare.
Pentru implementarea dinamică a unei liste liniare, folosind pointeri, nodurile listei
vor fi structuri ce conţin două tipuri de informaţie:
- câmpurile ce conţi n informaţia structurală a nodului
- câmpurile ce conţin informaţia de legătură, ce vor conţine pointeri la nodurile listei.
Înlănţuirea secvenţială a elementelor unei liste se face utilizând variabile de tip
 pointer, care specifică adresa de memorie a eleme ntelor adiacente. Fiecare nod are un
 predecesor şi un succesor, mai puţin elementele prim şi ultim dacă lista nu este
circulară.
Listele înlănţuite cu un singur câmp de legătură se numesc liste simplu înlănţuite
(legătura indică următorul element din listă). Fiecare nod conţine un pointer ce conţine adresa
nodului următor din listă.

96
INFO

URM  NULL

Fig. 5.1. Lista simplu înlănţuită

Ultimul element poate conţine ca adresă de legătură fie constanta NULL fie constanta
0 (indicând astfel că ultimul nod nu are nici un succe sor).
Tipul unui nod într-o listă simplu înlănţuită se poate defini folosind o declaraţie de
forma:

class Lista;
class ElementLista
{ TINFO info;
ElementLista *urm;
public:
ElementLista(int val=0);
friend class Lista;
};

Listele înlănţuite cu 2 câmpuri de legătură se numesc liste dublu înlănţuite (o legătură


indică nodul precedent iar cealaltă nodul succesor).

 NULL PREC

INFO

URM  NULL

Fig. 5.2. Lista dublu înlănţuită

 Nodurile unei liste dublu înlănţuite au tipul definit după cum urmează, point erul urm
definind relaţia de succesor pentru nodurile listei, iar prec pe cea de predecesor:

class Lista;
class ElementLista
{ ElementLista *prec;
TINFO info;
ElementLista *urm;
public:
ElementLista(int val=0);
friend class Lista;
};

În situaţia în care se realizează o închidere a înlănţuirilor, pierzându-se astfel noţiunile


de început şi de sfârşit ale unei liste liniare, se obţine o listă circulară  simplu sau dublu
înlănţuită.
Într-o listă circulară simplu înlănţuită toate nodurile   sunt echivalente, fiecare nod
având un următor şi fiecare la rândul său constituind următorul unui alt nod. Acest
considerent este valabil şi pentru listele circulare dublu înlănţuite extinzându -se şi asupra
relaţiei de precedenţă care există între nodurile listei.

97
Relativ la o poziţie K din cadrul unei liste, putem defini următoarele tipuri de operaţii:
- accesul la cel de-al  K -lea nod al listei pentru examinare sau modificare;
- ştergerea nodului aflat pe poziţia  K  din cadrul listei;
- inserarea unui nou nod înainte de, respectiv după nodul  K .
Limitând efectuarea acestor trei operaţii la primul sau la ultimul nod al unei liste
liniare simplu înlănţuite se obţin structurile de date derivate de tip  stivă şi coadă. În
ceea ce priveşte stiva, aceasta constituie o listă simplu înlănţuită în care toate inserările
şi suprimările de noduri se efectuează la un singur capăt al acesteia, numit vârful stivei.
Stivele se mai numesc structuri listă de tip LIFO,  Last-In-First-Out , adică ultimul-introdus-
 primul-suprimat sau liste de tip  pushdown.
Relativ la structura de date de tip coadă, trebuie specificat că elementele noi vor fi
inserate la un capăt al acesteia, numit  spate, iar suprimările de noduri vor avea loc la celălalt
capăt, numit  faţă. Din acest motiv, cozile se mai numesc şi liste de tip FIFO,  first-in-first-aut ,
adică liste de tip primul-venit-primul-servit .
Prezenţa mai multor înlănţuiri într -un acelaşi nod conferă mai multă flexibilitate
structurilor de date de tip listă, acestea devenind aşa-numitele multiliste, nodurile acestora
aparţinând în acelaşi timp la mai multe liste înlănţuite simple.

5.2. Lista simplu înlănţuită

Într-o astfel de listă există întotdeauna un nod care nu mai are succesor, precum şi un
nod care nu este succesorul nici unui alt nod, aceste noduri constituind capetele listei simplu
înlănţuite. Într -o primă variantă de gestionare a acestui tip de listă v om apela la doi pointeri
 prim şi ultim pentru a referiri nodurile terminale ale listei.

INFO

URM  NULL

 prim ultim

Fig. 5.3. Lista simplu înlănţuită marcată de pointerii prim şi ultim

Dacă specificarea începutului listei prin pointerul  prim  este obligatorie, utilizarea
 pointerului de sfârşit ultim este opţională, însă eficientă atunci când se doreşte crearea
listei în ordine naturală printr -o inserţie a noilor noduri la sfârşitul acesteia. În practică,
 parcurgerea întregii liste pentru a determina ultima poziţie a acesteia este o soluţie ineficientă,
convenabilă fiind cunoaşterea, cu ajutorul pointerului ultim, a nodului terminal al acesteia. În
 prezenţa lui ultim, secvenţa de program care inserează un nod la sfârşitul unei liste liniare şi
concomitent îl actualizează pe ultim este următoarea:

void Lista::inserare_sfarsit(TINFO val)


{
ElementLista* ptr=new ElementLista(val);
if (ptr==NULL)
{ cout<<"Eroare alocare spatiu la inserare";
return;
}
Ultim->urm=ptr;

98
Ultim=ptr;
if (Prim==NULL) Prim=Ultim;
cout<<"Inserare la sfarsitul listei cu succes!\n";
}

Se observă că după adăugarea unui nou element la sfârşitul listei, se actualizează


 pointerul Ultim, care va păstra adresa noului element ultim al listei.
Dacă elementul adăugat la sfârşitul listei   este primul element al listei (caz în care
 pointerul Prim conţine valoarea NULL), vom actualiza şi valoarea pointerului Prim,
elementul adăugat fiind în acelaşi timp primul şi ultimul element al listei.
Dacă se doreşte crearea unei liste prin inserţia noi lor noduri la începutul acesteia, se va
apela metoda inserare_inceput , aceasta fiind valabilă şi în cazul unei liste vide.

void Lista::inserare_inceput(TINFO val)


{
ElementLista* ptr=new ElementLista(val);
if (ptr==NULL)
{ cout<<"Eroare alocare spatiu la inserare";
return;
}
if(Prim==NULL) Ultim=ptr;
ptr->urm=Prim;
Prim=ptr;
cout<<"Inserare la inceputul listei cu succes!\n";
}

Dacă elementul adăugat la începutul listei este primul element al listei (caz în care
 pointerul Prim conţine valoarea NULL), vom actualiza şi valoarea pointerului Ultim,
elementul adăugat fiind în acelaşi timp ultimul şi primul element al listei.
Operaţia de inserare a unui nod într -o listă nu se reduce numai la cele două situaţii
 prezentate, ci implică de asemenea posibilitatea inserării acestuia într -o poziţie oarecare a
listei, şi anume, după sau înaintea unui anumit nod specificat prin referinţa sa spec. Schematic
cele două situaţii sunt prezentate în figurile următoare, observându-se o oarecare dificultate la
operaţia de inserţie a noului nod înaintea celui specificat.

spec

Fig. 5.4. Inserţia unui nod după un nod specificat

99
void Lista::inserare_dupa_un_element(TINFO X, TINFO cheie)
{
ElementLista* q, *spec;
for(spec=Prim;spec&&spec->info!=cheie;spec=spec->urm);
if(spec)
{
q=new ElementLista(X);
if (q==NULL)
{
cout<<"Eroare alocare spatiu la inserare";
return;
}
if(spec==Ultim) Ultim=q;
q->urm=spec->urm;
spec->urm=q;
cout<<"Inserare dupa element cu succes!\n";
}
else cout<<"Nu exista cheia specificata!\n";
}

spec X

INFO

Fig. 5.5. Inserarea unui nod înaintea unui nod specificat

Inserarea noului nod înaintea celui referit prin  spec se implementează ca o inserare
după acesta, noul nod preluând conţinutul câmpului info al acestuia, în care apoi se va
memora informaţia X .

void Lista::inserare_inaintea_unui_elem(TINFO X,TINFO cheie)


{
ElementLista* q, *spec;
for(spec=Prim;spec&&spec->info!=cheie;spec=spec->urm);
if(spec)
{
q=new ElementLista(X);

100
if (q==NULL)
{
cout<<"Eroare alocare spatiu la inserare";
return;
}
if(spec==Ultim) Ultim=q;
q->info=spec->info;
q->urm=spec->urm;
spec->urm=q;
spec->info=X;
cout<<"Inserare inaintea unui element cu succes!\n";
}
else cout<<"Nu exista cheia specificata!\n";
}

Se pune problema creării unei liste înlănţuite ordonate după câmpul info, caz în care
inserarea unui nod nou nu trebuie să afecteze relaţia de ordonare existentă. Vom traversa lista
utilizând doi pointeri consecutivi şi vom identifică astfel poziţia de inserare a noului nod.
Inserarea noului nod se va face dupa nodul referit de pointerul q1.
Cei doi pointeri q1 şi q2 avansează simultan de -a lungul listei până când valoarea q1 -
info devine mai mare sau egală cu valoarea de inserat  X . Inserarea noului nod va avea loc între
nodurile referite de q1 şi q2.

void Lista::inserare_ordonata(TINFO X)
{
ElementLista *q1=NULL,*q2;
ElementLista* ptr=new ElementLista(X);
if (ptr==NULL)
{
cout<<"Eroare la alocare spatiu pentru inserare";
return;
}
for(q2=Prim;q2&&q2->info<X;q1=q2,q2=q2->urm);
if(q2==Prim)
{
if(Prim==NULL) Ultim=ptr;
ptr->urm=Prim;
Prim=ptr;
}
else
{
if(q2==NULL) Ultim=ptr;
q1->urm=ptr;
ptr->urm=q2;
}
cout<<"Inserare ordonata cu succes!\n";
}

101
Identificarea unui nod în cadrul unei liste se poate efectua aplicând metoda de căutare
liniară, aceasta presupunând parcurgerea elementelor listei, nod cu nod, fie până se
localizează nodul dorit, fie până la sfârşitul listei dacă elementul căutat este in existent.

/* caută nodul X în cadrul listei şi returneză


pointerul spre nodul identificat şi 0 în caz
contrar */

ElementLista *Lista::cautare(TINFO X)
{
ElementLista *q;
for(q=Prim;q&&q->info!=X;q=q->urm);
if(q)
return(q);
else
return(0);
}

Să se ofere o implementare a  problemei concordanţei, inserarea unui nou nod


realizându-se la începutul listei. În final, lista va conţine toate “cuvintele”  X  distincte şi
numărul de apariţii ale acestora.

#include<iostream.h>
class Lista;
class ElementLista
{ int info,contor;
ElementLista *urm;
public:
ElementLista(int val=0);
friend class Lista;
};
class Lista
{
protected:
ElementLista *Ultim, *Prim;
public:
Lista()
{ Prim = NULL;
Ultim = new ElementLista();
}
~Lista();
void traversare();
void inserare(int);
};

ElementLista::ElementLista(int val)
{

102
info=val;
contor=1;
urm=NULL;
}

Lista::~Lista()
{
ElementLista* ptr=Prim;
while (Prim)
{
Prim=Prim->urm;
delete ptr;
ptr=Prim;
}
}

void Lista::traversare()
{
ElementLista* p=Prim;
if (p==NULL)
cout << "\n Lista este vida! \n";
else
while (p!=NULL)
{
cout<<p->info<<" numar de aparitii: "<<p->contor<<"\n";
p=p->urm;
}
cout<<"\n";
}

void Lista::inserare(int X)
{
ElementLista *p;
for(p=Prim;p&&p->info!=X;p=p->urm);
if(p)
p->contor++;
Else
{
p=new ElementLista(X);
p->urm=Prim;
Prim=p;
}
}

void main()
{
Lista Listamea;
int cheie;
cout<<"Introduceti cheia de inserat:";
cin>>cheie;

103
while(cheie)
{ Listamea.inserare(cheie);
cout<<"Introduceti cheia de inserat:";
cin>>cheie;
}
cout<<"Inserare incheiata!\n";
cout<<"\nTraversare lista:\n";
Listamea.traversare();
}

O îmbunătăţire substanţială a procesului de căutare poate avea loc prin aplicarea aşa -
numitei metode de căutare cu reordonare. Ori de câte ori un cuvânt se caută şi se localizează
în listă, el va fi mutat la începutul acesteia, astfel încât la proxima apariţie să fie găsit imediat.
Cu alte cuvinte, lista se reordonează după fiecare căutare finalizată cu succes. Dacă un nod nu
este găsit în listă, atunci e l va fi inserat la începutul acesteia.
q2 q1  prim
3

INFO X

URM  NULL

2 1 fanion
 prim

Fig. 5.6. Mutarea nodul X la începutul listei

void Lista::inserare(int X)
{
ElementLista *q1, *q2=NULL, *p;
for(q1=Prim;q1&&q1->info!=X;q2=q1,q1=q1->urm);
if(q1)
{
q1->contor++;
if(q2)
{
q2->urm=q1->urm;
q1->urm=Prim;
Prim=q1;
}
}
else
{
p=new ElementLista(X);
p->urm=Prim;
Prim=p;
}
}

104
Se propune eliminarea unui anumit nod, referit prin q1, din cadrul unei liste liniare
înlănţuite. Pentru aceasta vom folosi un pointer auxiliar, q2, care indică predecesorul
elementului ce urmează să fie suprimat .

void suprima (TNOD *q);


{
ElementLista *q1=NULL,*q2;
for(q2=Prim;q2&&q2->info!=X;q1=q2,q2=q2->urm);
if(q2)
{
if(q2==Prim) Prim=q2->urm;
if(q2==Ultim) Ultim=q1;
if(q1) q1->urm=q2->urm;
delete(q2);
cout<<Suprimare cu succes!\n";
}
Else
cout<<"Nodul nu exista!\n";
}

5.3. Lista circulară simplu înlănţuită

Listele circulare sunt liste înlănţuite ale căror înlănţuiri se închid, în aceste condiţii
dispărând noţiunea de început şi de sfârşit al listei. Pentru gestiunea unei astfel de lista, vom
 păstra un pointer spre „ultimul” element al listei.

# include <iostream.h>
# define TINFO int
class Lista;
class ElementLista
{ TINFO info;
ElementLista *urm;
public:
ElementLista(int val=0);
friend class Lista;
};

class Lista
{
protected:
ElementLista *Ultim;
public:
Lista()
{ Ultim = NULL;}
~Lista();
void traversare();
void inserare_sfarsit(TINFO);
void inserare_inaintea_unui_elem(TINFO,TINFO);

105
ElementLista *cautare(TINFO);
void suprimare(TINFO);
};

ElementLista::ElementLista(TINFO val)
{
info=val;
urm=NULL;
}

Lista::~Lista()
{
ElementLista* ptr;
if(Ultim!=NULL)
{ while (Ultim->urm!=Ultim)
{
ptr=Ultim->urm;
Ultim->urm=Ultim->urm->urm;
delete ptr;
}
delete Ultim;
}
}

void Lista::traversare()
{
ElementLista* ptr=Ultim;
if (Ultim==NULL)
cout << "\n Lista este vida! \n";
else
{ while (ptr->urm!=Ultim)
{
ptr=ptr->urm;
cout << ptr->info<<" ";
}
cout << ptr->urm->info<<" ";
cout<<"\n";
}
}

void Lista::inserare_sfarsit(TINFO val)


{
ElementLista* ptr=new ElementLista(val);
if (ptr==NULL)
{
cout<<"Eroare la alocare spatiu pentru inserare";
return;
}
if(Ultim)
{

106
ptr->urm=Ultim->urm;
Ultim->urm=ptr;
Ultim=ptr;
}
else
{
Ultim=ptr;
ptr->urm=ptr;
}
cout<<"Inserare sfarsit cu succes!\n";
}

void Lista::inserare_inaintea_unui_elem(TINFO X,TINFO cheie)


{
ElementLista* ptr, *q1=Ultim, *q2;
if(Ultim==NULL)
{
cout<<"Lista vida! Inserare fara succes!\n";
return;
}
for(q2=Ultim->urm;q2!=Ultim&&q2->info!=cheie;q1=q2,q2=q2-
>urm);
if(q2->info==cheie)
{
ptr=new ElementLista(X);
if (ptr==NULL)
{
cout<<"Eroare alocare spatiu la inserare";
return;
}
q1->urm=ptr;
ptr->urm=q2;
cout<<"Inserare cu succes!\n";
}
else cout<<"Nu exista cheia specificata!\n";
}

ElementLista *Lista::cautare(TINFO X)
{
ElementLista *ptr;
if(Ultim==NULL) return(0);
for(ptr=Ultim->urm;ptr!=Ultim&&ptr->info!=X;ptr=ptr-
>urm);
if(ptr->info==X)
return(ptr);
else
return(0);
}

void Lista::suprimare(TINFO X)

107
{
ElementLista *q1=Ultim, *q2;
if(Ultim==NULL)
{
cout<<"Lista vida! Suprimare fara succes!\n";
return;
}
for(q2=Ultim->urm;q2!=Ultim&&q2->info!=X;q1=q2,q2=q2-
>urm);
if(q2->info==X)
{
if(q2->urm==q2)
{
Ultim=NULL;
delete(q2);
}
else
{
if(q2==Ultim) Ultim=q1;
q1->urm=q2->urm;
delete(q2);
}
cout<<"Suprimare cu succes!\n";
}
else
cout<<"Nodul nu exista!\n";
}

void main()
{
Lista Listamea;
int opt;
TINFO valoare,cheie;
do
{
cout<<"\n Optiuni de lucru cu lista:";
cout<<"\n 1 - Afisare lista";
cout<<"\n 2 - Inserare element la sfarsitul listei";
cout<<"\n 3 - Inserare element inaintea unui elem specificat";
cout<<"\n 4 - Cautarea unui element specificat";
cout<<"\n 5 - Suprimarea unui element";
cout<<"\n 9 - Terminare lucru \n\n";
cout<<"Introduceti optiunea dorita:";
cin>>opt;
switch(opt)
{
case 1:
{
cout<<"Traversare lista:";
Listamea.traversare();

108
break;
}
case 2:
{
cout<<"Introduceti elementul de inserat:";
cin>>valoare;
Listamea.inserare_sfarsit(valoare);
break;
}
case 3:
{
cout<<"Introduceti elementul de inserat:";
cin>>valoare;
cout<<"Introduceti elem inaintea caruia
inseram:";
cin>>cheie;

Listamea.inserare_inaintea_unui_elem(valoare,cheie);
break;
}
case 4:
{
cout<<"Introduceti elementul cautat:";
cin>>valoare;
if(Listamea.cautare(valoare))
cout<<"Elementul a fost gasit!\n";
else
cout<<"Elementul nu exista in lista!\n";
break;
}
case 5:
{
cout<<"Introd elem pe care doriti sa-l
suprimati:";
cin>>valoare;
Listamea.suprimare(valoare);
break;
}
case 9: break;
default:
cout<<"\n Nu exista optiunea! \n ";
}
}while (opt!=9);
}

5.4. Operaţii cu liste liniare simplu înlănţuite

Parcurgerea listei liniare simplu înlănţuite corespunde funcţiei de extragere a adresei


elementului succesor şi de referire a membrilor acestuia.

109
Pentru tipărirea conţinutului elementelor matricei rare, cu număr necunoscut de
elemente nenule, funcţia de parcurgere, construită recursiv este:

parcurgere (pg)
{
while (pg->poz != NULL)
{
tipareste (pg->i, pg->j, pg->val);
parcurgere (pg->poz);
}
}

Considerând lista ca mulţimea de perechi de forma (Z, ) de tip TL, w = adr( Z 1  )
conţine adresele de regăsire a elementelor listei.

parcurgere (adr (w))


{
while (w != NULL)
{
tipareste (w, z);
parcurgere (ref (w), );
}
}

Ştergerea unui element ( Z k , k ) al listei

Înainte de ştergere lista este:

Zk-1 k Zk k+1 Zk+1 k+2 ....

Fig. 5.7. Lista înainte de ştergere

După efectuarea ştergerii:

Zk-1 k+1 Zk k+1 Zk+1 k+2 ....

Fig. 5.8. Lista după de ştergerea elementului

stergere ( w, w1, Zk  )


{
while (ref (w). != NULL || ref (w).Z != Zk )
{
w1 = ref (w, );
stergere (w1, w, Z k )
}

110
if( ref (w1).Z = =Zk )
ref (w). = ref (w1). ;
}

Concatenarea a două liste

Concatenarea a două liste (Z, ) şi (U,  ), reprezintă ca ultimul element al primei liste
să-şi schimbe valoarea din NULL a lui n cu adresa elementului (U 1 , 1 ).
Deci: ref( Z n ). n = adr[ (U 1 , 1 )]
Funcţia care efectuează concatenarea listelor este:

concatenare((Z, ), (U, ))
{
while (ref(Z). != NULL)
{
concatenare ((ref(Z). , ),(U,Z));
}
ref(Z). = adr((U, ))
}

Modelul grafic al concatenării:

( Z1, 1) ( Z2, 2) ( Z3, 3) ( Zn, n)

...

...
 NULL

( U1, 1) ( U2, 2) ( U3, 3) ( Um, m)

Fig. 5.9. Modelul grafic al concatenării listelor 

Lista concatenată are ca prin element (Z 1 , 1 ), iar ca ultim element (U m , m ).
Fizic, lista (U, ) nu s-a modificat. Conservând (Z, 1 ) şi (U, 1 ) se lucrează cu cele
două liste, ca şi cum concatenarea nu s -a executat. Totuşi lista concatenată, pentru operaţia de
 parcurgere se comportă ca o listă cu m + n componente.

Modificarea unui element al listei

Fie lista (Z  ,i  ),


i i = 1, 2, ..., n . Pentru înlocuirea unei valori a cu valoarea b, trebuie
mai întâi găsit elementul k  pentru care: cont (Z k )  = a , după care se realizează atribuirea:  Z k  =
b;
În toate cazurile, parcurgere, şterge re, concatenare, modificare, disciplina de
 parcurgere este de la primul element către ultimul, First In –  First Out .

modificare (w)
{

111
if (ref (w). z != a)
modificare (ref (w). );
else
ref (w). z = b;
}

Copierea unei liste

Fie lista (Z  j ,  j ), j = 1, 2, ..., n . Se pune problema obţinerii unei liste: (Z ’  j , ’  j ), j = 1, 2,
’ 
... , n, astfel încât: cont(Z  j ) = cont(Z  j ), pentru j = 1, 2, ..., n.

copiere_lista (w, u)
{
while (ref (w). != NULL)
{
ref (u).Z = ref (w).Z;
alocare (v);
ref (u). = v;
copiere_lista (ref (w). ), v);
}
ref (u). = NULL;
}

Inserarea unui element în listă

Se spune că o listă este ordonată crescător dacă: cont ( Z j ) cont ( Z j 1 ) , pentru orice
 j = 1, 2, …, n –  1.
A insera un element într-o listă, înseamnă mai întâi a găsi o valoare k Є {1, 2, …, n}
astfel încât: cont ( Zk  ) a cont ( Zk  1 ) sau cont ( Zk  ) a cont ( Zk  1 ) , după cum lista
este ordonată crescător sau descrescător. În aceste condiţii, inserarea elementului a, înseamnă
conform modelului grafic, a trece de la configuraţia:

(Zk , k ) (Zk+1, k+1)

... ...

( , )

Fig. 5.10. Configuraţia înainte de inserare nodului în interiorul listei

la configuraţia:

112
(Zk , adr( )) (Zk+1, k+1)

... ...

( , adr (Zk+1))

Fig. 5.11. Configuraţia după inserarea nodului în interiorul listei

Există cazuri particulare de inserare în care elementul este poziţionat fie la începutul
listei, fie la sfârşitul acesteia, operaţia numindu-se adăugare.
Dacă elementele a şi b vor fi inserate la începutul, respectiv, la sfârşitul listei, se trece
de la configuraţia:
(Z1, 1) (Zn, n)

...  NULL ...

a a
( , ) ( , )

Fig. 5.12. Configuraţia înainte de inserarea nodului la un capăt al listei

la configuraţia:

(Z1, 1) (Zn, adr( ))

... ...

 b  b  NULL
( ,adr ( Z1) ( , )

Fig. 5.13. Configuraţia după inserarea nodului la un capăt al listei

Interschimbul între două elemente ale listei

Interschimbul nu se realizează fizic, zonele ce corespund celor două elemente


modificându-şi doar adresele de referire a elementelor.
Modelul grafic al listei înainte de interschimbul elementelor (Z k ,  k  ) şi (Z  j ,  j ) este:

113
(Zk-1, k-1) (Zk , k ) (Zk+1, k+1)

... ...

(Z j-1,  j-1) (Z j,  j) (Z j+1,  j+1)

... ...

Fig. 5.14. Modelul grafic al listei înainte de interschimbul nodurilor

După efectuarea interschimbului, legăturile dintre componente sunt:

(Zk , k ) (Zk+1, k+1)

... ...

(Zk-1, k-1)

(Z j,  j)

... ...

(Z j-1,  j-1) (Z j+1,  j+1)

Fig. 5.15. Modelul grafic al listei după interschimbul nodurilor 

Funcţia pentru efectuarea interschimbului, realizează atribuirile:

k-1 = adr( Z  j ) (10.41)

 j-1 = adr( Z k ) (10.42)

 j = adr( Z k+1 ) (10.43)

k= adr( Z  j+1 ) (10.44)

ceea ce înseamnă că la un moment dat sunt gestionate şase adrese de variabile de tip TL, ale
elementelor ce se interschimbă, precum şi a elementelor adiacente.

Sortarea elementelor unei liste

Fiind dată o structură de date de tip listă (Z  j ,  j ), j = 1, 2, ..., n , funcţia de sortare
transformă această structură de date într -o nouă listă (Z’k , ’k ), k = 1, 2, ..., n  astfel încât
oricărui k Є [1, n] ∩ N  îi corespunde un  j Є [1, n] ∩ N  şi numai unul aşa încât: cont( Z’ k  ) =
cont( Z’  j ) şi cont ( Zk  ) cont ( Zk  1 ) pentru orice k = 1, 2, …, n –  1.
Funcţia de sortare apelează la rândul ei funcţia de interschimb a două elemente
adiacente, până când în final se obţine ordinea crescătoare sau descrescătoare a
termenilor Z i din lista iniţială.
Un exemplu simplu de sortare, fără a lua în considerare multitudinea de tehnici este:

114
sortare (w)
{
k = 1;
while (k != 0)
{
k = 0;
while (ref (w). != NULL)
{
if (ref (w).Z > ref (w).ref( ).Z)
{
k = 1;
interschimb (w, w. );
}
}
}
}

5.5. Liste dublu  înlănţuite

Spre deosebire de listele simplu înlănţuite care permit parcurgerea de la primul


element spre ultimul alocat dinamic, listele dublu înlănţuite realizează şi drumul invers,
 permiţând şi parcurgerea de la ultimul element către primul element.
Modelul grafic al listei dublu înlănţuite este:

(Z j-1,  j-1,  j-1) (Z j ,  j,  j ) (Z j+1,  j+1,  j+1)

... ...

Fig. 5.16. Model grafic al listei dublu înlănţuite

sau:

... ...

Fig. 5.17. Model grafic al listei dublu înlănţuite

Lista dublu înlănţuită este de fapt formată din două liste (Z  j ,  j ) şi (U  j ,  j ) cu
 proprietăţile:
adr ( Z j ) adr ( U j ) cont ( Z j ) cont ( U j )
adr (  j ) adr (  j ) lg (  j )
cont ( Z j ,  j ) adr ( Z j 1)
cont ( Z j ,  j ) adr ( Z j 1)
cont (  j 1 ) cont (  j 1 )

115
Şi cu listele dublu înlănţuite se efectuează operaţii precum:
- inserarea unui element;
- adăugarea unui element;
- ştergerea unui element;
- inversarea a două elemente;
- ştergerea listei;
-  parcurgerea într-un sens şi în sensul opus;
- transformarea listei în listă circulară dublu înlănţuită.

Un exemplu de creare, inserare, căutare, parcurgere şi ştergere a unei liste dublu


înlănţuite, este următorul program:

# include <iostream.h>
# define TINFO int
class ListaDubluInlan;

class ElementLista
{ TINFO info;
ElementLista *pred, *suc;
public:
ElementLista(int val=0);
friend class ListaDubluInlan;
};

class ListaDubluInlan
{
protected:
ElementLista *Prim;
public:
ListaDubluInlan()
{ Prim = NULL;}
~ListaDubluInlan();
void traversare_inainte();
void inserare_inceput(TINFO);
void inserare_inaintea_unui_elem(TINFO,TINFO);
void inserare_dupa_elem(TINFO,TINFO);
ElementLista *cautare(TINFO);
void suprimare(TINFO);
};

ElementLista::ElementLista(TINFO val)
{
info=val;
pred=suc=NULL;
}

ListaDubluInlan::~ListaDubluInlan()
{
ElementLista* ptr;

116
while (Prim)
{
ptr=Prim;
Prim=Prim->suc;
delete ptr;
}
}

void ListaDubluInlan::traversare_inainte()
{
ElementLista* ptr=Prim;
if (Prim==NULL)
cout << "\n Lista este vida!";
else
while (ptr)
{
cout << ptr->info<<" ";
ptr=ptr->suc;
}
cout<<"\n";
}

void ListaDubluInlan::inserare_inceput(TINFO val)


{
ElementLista* ptr=new ElementLista(val);
if (ptr==NULL)
{
cout<<"Eroare la alocare spatiu pentru inserare";
return;
}
ptr->pred=NULL;
ptr->suc=Prim;
if(Prim)
Prim->pred=ptr;
else
Prim=ptr;
cout<<"Inserare la inceput cu succes!\n";
}

void ListaDubluInlan::inserare_inaintea_unui_elem(TINFO X,TINFO cheie)


{
ElementLista* ptr, *p;
if(Prim==NULL)
{
cout<<"Lista vida! Inserare fara succes!\n";
return;
}
for(p=Prim;p&&p->info!=cheie;p=p->suc);
if(p)
{

117
ptr=new ElementLista(X);
if (ptr==NULL)
{
cout<<"Eroare alocare spatiu la inserare";
return;
}
if(p==Prim)
{
ptr->pred=NULL;
ptr->suc=p;
p->pred=ptr;
Prim=ptr;
}
else
{
ptr->pred=p->pred;
ptr->suc=p;
p->pred->suc=ptr;
p->pred=ptr;
}
cout<<"Inserare inainte element cu succes!\n";
}
else cout<<"Nu exista cheia specificata!\n";
}

void ListaDubluInlan::inserare_dupa_elem(TINFO X,TINFO cheie)


{
ElementLista* ptr, *p;
if(Prim==NULL)
{
cout<<"Lista vida! Inserare fara succes!\n";
return;
}
for(p=Prim;p&&p->info!=cheie;p=p->suc);
if(p)
{
ptr=new ElementLista(X);
if (ptr==NULL)
{
cout<<"Eroare alocare spatiu la inserare";
return;
}
ptr->suc=p->suc;
ptr->pred=p;
if(p->suc)
p->suc->pred=ptr;
p->suc=ptr;
cout<<"Inserare dupa element cu succes!\n";
}
else cout<<"Nu exista cheia specificata!\n";

118
}

ElementLista *ListaDubluInlan::cautare(TINFO X)
{
ElementLista *ptr;
for(ptr=Prim;ptr&&ptr->info!=X;ptr=ptr->suc);
if(ptr)
return(ptr);
else
return(0);
}

void ListaDubluInlan::suprimare(TINFO X)
{
ElementLista *ptr;
if(Prim==NULL)
{
cout<<"Lista vida! Suprimare fara succes!\n";
return;
}
for(ptr=Prim;ptr&&ptr->info!=X;ptr=ptr->suc);
if(ptr)
{
if(ptr==Prim)
{
Prim=Prim->suc;
if (Prim) Prim->pred=NULL;
}
else
{
ptr->pred->suc=ptr->suc;
if(ptr->suc)
ptr->suc->pred=ptr->pred;
}
delete(ptr);
cout<<"Suprimare cu succes!\n";
}
else
cout<<"Nodul nu exista!\n";
}

void main()
{
ListaDubluInlan Listamea;
int opt;
TINFO valoare,cheie;
do
{
cout<<"\n Optiuni de lucru cu lista:";
cout<<"\n 1 - Afisare lista";

119
cout<<"\n 2 - Inserare element la inceputul listei";
cout<<"\n 3 - Inserare element inaintea unui elem specificat";
cout<<"\n 4 - Inserare dupa un element specificat";
cout<<"\n 5 - Cautarea unui element specificat";
cout<<"\n 6 - Suprimarea unui element";
cout<<"\n 9 - Terminare lucru \n\n";
cout<<"Introduceti optiunea dorita:";
cin>>opt;
switch(opt)
{
case 1:
}
cout<<"Traversare lista:";
Listamea.traversare_inainte();
break;
}
case 2:
{
cout<<"Introduceti elementul de inserat:";
cin>>valoare;
Listamea.inserare_inceput(valoare);
break;
}
case 3:
{
cout<<"Introduceti elementul de inserat:";
cin>>valoare;
cout<<"Introduceti elem inaintea caruia inseram:";
cin>>cheie;
Listamea.inserare_inaintea_unui_elem(valoare,cheie);
break;
}
case 4:
{
cout<<"Introduceti elementul de inserat:";
cin>>valoare;
cout<<"Introduceti elementul dupa care inseram:";
cin>>cheie;
Listamea.inserare_dupa_elem(valoare,cheie);
break;
}
case 5:
{
cout<<"Introduceti elementul cautat:";
cin>>valoare;
if(Listamea.cautare(valoare))
cout<<"Elementul a fost gasit!\n";
else
cout<<"Elementul nu exista in lista!\n";
break;

120
}
case 6:
{
cout<<"Introd elem pe care doriti sa-l suprimati:";
cin>>valoare;
Listamea.suprimare(valoare);
break;
}
case 9: break;
default:
cout<<"\n Nu exista optiunea! \n ";
}
}while (opt!=9);
}

Teme de control

1. Sa se realizeze un program C++ pentru crearea si afisarea unei liste


circulare simplu înlănţuite.
2. Sa se realizeze un program C++ pentru crearea unei liste simplu
înlănţuite şi ştergerea elementului de pe poziţia “k”.

Bibliografie

Ion IVAN, Marius POPA, Paul POCATILU - Structuri de date, Vol. 1 si 2, Editura ASE,
Bucuresti, 2008.

121
6. Stive şi cozi
Obiectivele unităţii de învăţare

După studierea acestei unităţi de învăţare, studenţii îşi vor însuşi


cunoştinţe teoretice şi vor deprinde abilităţi practice de lucru cu stive
şi cozi, reprezentând cazuri particulare de liste simple.

6.1. Consideraţii privind structurilor de date de tip stivă şi coadă

en gleză, este o structură de tip LIFO (Last In First Out


O stivă, stack în limba engleză, O ut = ultimul
intrat primul ieşit) şi este un caz particular al listei liniare în care toate inserările
(depunerile –în engleză  push) şi ştergerile (sau extragerile -în engleză  pop) (în general
orice acces) sunt făcute la unul din capetele listei, numit vârful stivei. Acest nod poate fi citit,
 poate fi şters sau
sau în faţa lui se poate insera un nou
nou nod care devine
devine cap de stivă.
stivă.

push pop

Fig. 6.1. Mecanismul de stivă

Pentru înţelegerea mecanismului unei stive, se poate folosi reprezentarea manevrării


într-un depou a vagoanelor de cale ferată sau a unui vraf de farfurii din care putem depune şi
extrage doar în şi din vârful vrafului. Stivele sunt legate şi de algoritmii   recursivi, la care
salvarea variabilelor dintr-o funcţie care se apelează recursiv se face având la bază un
mecanism de tip stivă.
O stivă poate fi implementată ca o listă liniară pentru care operaţiile de acces
(inserare, ştergere, accesare element) sunt restricţionate astfel:
- inserarea se face doar în faţa primului element al listei, capul listei;
- accesarea respectiv
respectiv ştergerea acţionează doar asupra primului element al listei.
O stivă poate fi implementată folosind o structură de date statică sau dinamică.
În abordarea statică, stiva poate fi organizată pe un spaţiu de memorare de tip tablou
unidimensional.
În abordarea dinamică, implementarea unei stive se poate face folosind o structură de
tip listă liniară simplu înlănţuită în care inserarea se va fac e tot timpul în capul listei iar
ştergerea de asemenea se va face în capul listei.

122
O coadă (în engleză queue) este o structură de tip FIFO (First In First Out = Primul
venit primul servit), în care toate inserările se fac la un capăt al ei (numit capul co zii)
iar ştergerile (extragerile) (în general orice acces) se fac la celălalt capăt (numit sfârşitul
cozii). În cazul cozii, avem nevoie de doi pointeri, unul către primul element al cozii (capul
cozii), iar altul către ultimul său element (sfârşitul cozii). Există şi o variantă de coadă
circulară, în care elementele sunt legate în cerc, iar cei doi pointeri, indicând capul şi sfârşitul
cozii, sunt undeva pe acest cerc.

push pop

Fig. 6.2. Mecanismul de coadă

Se face o analogie cu o cale ferată pe un singur sens sau cu obişnuita coadă la un


ghişeu oarecare la care primul venit este şi primul servit.
Pentru gestiunea unei cozi sunt necesari doi indicatori:
- un indicator care indică primul element ce urmează a fi scos;
- un indicator care indică unde va fi pus următorul element adăugat la coadă.
Într-o abordare statică, o coadă poate fi implementată folosind un spaţiu de memorare
de tip tablou unidimensional. În acest caz, adăugări şi ştergeri repetate în coadă deplasează
conţinutul cozii la dreapta, faţă de începutul vectorului. Pentru evitarea acestei deplasări, o
soluţie este ca fiecare operaţie de extragere din coadă să fie acompaniată de deplasarea la
stânga a conţinutului cozii cu o poziţie. În acest caz, primul element care urmează să fie
eliminat din coadă va fi întotdeauna pe prima poziţie, indicatorul care să indice elementul ce
urmează să fie scos pierzându -şi utilitatea. Dezavantajul acestei soluţii îl reprezintă
necesitatea deplasării
deplasării întregului set de elemente din coadă rămase cu o poziţie.
Într-o abordare dinamică, o coadă poate fi implementată folosind o listă liniară simplu
înlănţuită la care operaţiile de acces sunt restricţionate corespunzător. Există două abordări:
- una în care adăugarea se face tot timpul la începutul listei liniare simplu înlănţuit e iar
extragerea se face de la sfârşitul listei
- cea de a doua, în care adăugarea se face la sfârşitul listei iar extragerea se face din
capul listei.
Pentru implementarea unei cozi, vom folosi aspectele tratate în capitolul de liste
liniare.

6.2. Caracteristicile structurilor de date de tip stivă

Caracteristicile unei stive sunt puse în evidenţă prin pri n comparaţie cu structura de date de
tip listă:
- în timp ce pentru gestionarea listei se vehiculează cu variabile de tip T p, care stochează
adresa componentei (Z 1 , 1 ) a listei, componenta numită cap de listă, în cazul stivei
este memorată adresa ultimei componente (Z' n , θ' n ), numită vârful stivei;
- în timp ce cont cont(θ n ) = NULL în cazul listei, cont (θ' 1 ) = NULL în cazul stivei;
- în timp ce succ (Z  j ) = Z  j+1 în cazul listei, în cazul stivei  succ(Z’  j )=Z’  j-1;
- în timp ce pred (Z  j ) = Z  j-1 în cazul listei, în cazul stivei pred(Z’  j )=Z’  j+1;

123
- în tip ce parcurgerea în cazul listei este de la (Z 1, θ1) spre (Zn, θm), în cazul stivei,
 parcurgerea este de la (Z'n, θ'n) spre ( Z'1, θ'1 );
- în timp ce disciplina lucrului cu elementele listei este primul venit  –   primul servit
(FIFO), în cazul stivei regula de vehiculare a elementelor, este ultimul venit  –  primul
  primul
servit (LIFO).
Se observă că stiva este o listă inversă. Totuşi, multe dintre tehnicile de rezolvare a
 problemelor vehiculează
vehiculează stive şi nu liste, datorită disciplinei de gestionare a elementelor.
Regulile de creare şi accesare la nivel de stivă, prevăd că fiecărui element care se
adaugă, i se alocă o zonă de memorie şi acest element devine vârf al stivei.
Există procese în care informaţiile se stochează în ordinea opusă momentului
 prelucrării, iar odată folosită, aceasta devine necesară. Stocând informaţiile
informaţiil e într -o stivă, după
utilizarea informaţiilor conţinute în vârful stivei, zona de memorie alocată este eliberată
(dealocată), vârful stivei mutându-se cu o componentă spre baza stivei.
Baza stivei este considerat elementul (Z' 1, '1). În funcţie de cerinţele de prelucrare, se
obţine o fluctuaţie a poziţiei vârfului stivei, ceea ce creează o imagine mai bună pentru
dinamica prelucrării datelor. Astfel, stivele stau la baza gestionării adreselor parametrilor ce
se transmit în funcţii, implementării mecanismelor de recursivitate din limbaje de programare.

În general, ideea de stivă sugerează prelucrări care au un caracter simetric. De


exemplu, se consideră o prelucrare completă privind:

A. încărcarea componentelor C 1  pentru disponibilizarea de resurse ale unui sistem de


calcul;
B. încărcarea componentei C 2 care este destinată lansării în execuţie a programului
utilizatorului;
C. efectuarea deschiderii
deschiderii de fişiere în programul utilizatorului – C 3;
D.  prelucrări de date
date şi afişări de rezultate
rezultate – C 4;
E. închiderea fişierelor;
F. ieşirea din programul utilizatorului sub supravegherea componentei
componentei C 3;
G. ieşirea din lucru sub componenta C 2;
H. dezactivarea resurselor sistemului de calcul prin componenta C 1.
Se construieşte stiva din figura 11.3.

vârf de stivă adr (C4) (Z'4, '4)

adr (C3)
(Z'3, '3)

adr (C2) (Z'2, '2)

adr (C1) (Z'1, '1)


NULL baza activă
Fig. 6.3. Structura unei stive

124
După parcurgerea pasului D, vârfului stivei coboară la (Z' 3, θ'3) şi componenta (Z'4,
θ'4) este dealocată, deci distrusă.
Folosind informaţiile stocate în pasul C, se efectuează ştergerea fişierelor (pasul E).
Vârful stivei se coboară la componenta (Z'2, θ'2), iar componenta (Z'3, θ'3) este eliberată.
e
vârf de stivă

a
baza activă
NULL
Fig. 6.4. Structura unei stive pentru inversarea caracterelor unui şir 

Se execută pasul F, care este simetricul pasului C şi apoi vârful coboară spre (Z' 1, θ'1)
şi se activează componentele C 1 pentru eliberarea resurselor pas simetric pasului A.
Astfel, de exemplu, dacă dorim să inversăm poziţia elementelor unui şir {a, b, c, d, e},
 prin crearea unei stive care să conţină aceste elemente, cu modelul grafic din figura 11.4 şi
 prin apelarea unei funcţii de parcurgere cu imprimarea elementelor identificate, se obţine
exact şirul { e, d, c, b, a }.
În cazul calculării lui n!, se foloseşte formula de recurenţă:  P(n) = P(n-1) * n, cu P(1)
= 1 , se creează o stivă care conţine expresiile ce sunt evaluate cu valorile descrescătoare ale
lui n.
Se construieşte stiva care are în informaţia utilă, de la bază spre vârf: n, n –   1, n –  2,
…, 3, 2, 1 .
Valoarea iniţială a funcţiei recursive  P(1) = 1, permite prin parcurgerea stivei de la
vârf spre bază, obţinerea rând pe rând a rezultatelor:  P(2), P(3),…,P(n –  2), P(n –  1 ), P(n) şi
întâlnindu-se NULL la baza stivei, procesul se încheie.

6.3. Operaţii de bază cu stive

Ştergerea unei stive, revine la dealocarea componentelor care formează stiva. Se


realizează prin parcurgerea integrală a stivei, cu dealocarea fiecărui vârf.
Adăugarea unui element în stivă, se face de regulă cu mutarea poziţiei vârfului stivei
 pe componenta adăugată.
Ştergerea unui element din stivă, se efectuează de regulă asupra elementului din vârful
stivei, aceasta coborând pe componenta ce îl precede.
Parcurgerea stivei, se efectuează de la vârf către bază.
Operaţiile de inserare, de concatenare, de sortare a stivelor, nu diferă mult de aceleaşi
operaţii din interiorul unei liste, dar frecvenţa lor de utilizare este mult mai redusă.

125
Programul următor, exemplifică unele dintre operaţiile care se execută cu structuri de
date de tip stivă.

#include <iostream.h>
#include <malloc.h>

class elementstiva
{
public:
int info;
elementstiva *prec;
elementstiva()
{
prec=NULL;
}
};

class stiva
{

public:
elementstiva *vs; //varful stivei

stiva()
{
vs=NULL;
}
void sterg()
{
elementstiva* aux=vs;
if(vs == NULL) cout<<"\n Stiva este vida!";
else
{
aux = vs->prec;
free(vs);
while(aux !=NULL)
{
vs=aux;
aux=aux->prec;
delete vs;
}
vs = NULL;
}

}//
void tipar()
{
elementstiva* aux;
if(vs == NULL) cout<<"\n Stiva este vida!";

126
else
{
aux = vs;
while(aux !=NULL)
{
cout<<aux->info;
aux=aux->prec;
}
}

void inserare(int el)


{
elementstiva *aux;
aux = new elementstiva();
aux->info = el;
aux->prec = vs;
vs = aux;

}//

int extrag()
{
elementstiva* aux;
int el;
if (vs == NULL) return -1;
else
{
aux = vs->prec;
el = vs->info;
delete vs;
vs = aux;
return el;
}

};

void main()
{
stiva st;
char opt;
int x,el;

do
{
cout<<"\n Optiuni de lucru cu stiva ";
cout<<"\n P - Tiparire stiva";

127
cout<<"\n A - Adaugare element în stiva";
cout<<"\n E - Extragere element din stiva";
cout<<"\n T - Terminare lucru";
cin>>opt;
switch(opt)
{
case 'a':
cout<<"element:";cin>>el;
st.inserare(el);
break;
case 'e':
{
x = st.extrag();
if (x==-1)
cout<<"\n Stiva este vida!";
else
cout<<"\n Element extras"<<x;
}
break;
case 's':
st.sterg();
break;
case 'p':
st.tipar();
break;
default:
cout<<"\n Nu exista optiunea!";
}
}while (opt!='t');
}

Pe baza meniului afişat prin program, se activează funcţiile în orice ordine, iar situaţia
de stivă vidă, este marcată în mod corespunzător. Crearea stivei se realizează prin adăugări
succesive, care presupun alocări dinamice de memorie pentru elementele componente.
Procedura de ştergere eliberează toată zona de memorie alocată stivei, iar funcţia de extragere,
eliberează zona de memorie ocupată de vârful stivei şi iniţializează cu adresa noului vârf al
stivei, variabila VS.

6.4. Evaluarea expresiilor matematice cu ajutorul stivei şi cozii

Pentru evaluarea expresiilor matematice există diferiţi algoritmi. Unul dintre aceştia
foloseşte ca structură principală de date stiva.
Acest algoritm presupune rearanjarea expresiei într- o anumită formă astfel încât
ordinea operaţiilor să fie clară şi evaluarea să necesite o singură parcurgere a expresiei. Pentru
aceasta se poate folosi forma poloneză, inventată de matematicianul de origine poloneză Jan
Lukasiewicz. Acesta presupune scrierea operatorilor înaintea operanzilor. Această formă mai
are o variantă numită scrierea poloneză inversă în care operatorii sunt scrişi în urma
operanzilor.

128
Tabel 6.1. Forme ale scrierii unei expresii matematice
Expresia în forma Expresia în forma
Expresia matematică
poloneză (scriere poloneză inversă
(scriere infixată)
prefixată) (scriere postfixată)
4+5 +45 45+
4+5*5 +4*55 455*+
4*2+3 +*423 42*3+

4+2+3 ++423 42+3+


4 * (2 + 3) *4+23 423+*

După cum se vede din tabelul 6.1, ordinea operanzilor nu se schimbă, ei găsindu -se în
aceeaşi ordine ca în expresia matematică.
Forma poloneză inversă are avantaje faţă de scrierea prefixată sau infixată:
- ordinea în care se efectuează operaţiile este clară;
-  parantezele nu mai sunt necesare;
- evaluările sunt uşor de efectuat cu ajutorul calculatorului.
Un algoritm de transformare din expresie matematică în scriere postfixată a fost
dezvoltat de către Edsger Dijkstra (algoritmul macazului al lui Dijkstra –   Dijkstra
Shunting Algorithm). Acest algoritm utilizează o stivă în care sunt păstraţi operatorii şi din
care sunt eliminaţi şi transferaţi în scrierea postfixată. Fiecare operator are atribuită o ierarhie
după cum este prezentat în tabelul 6.2.

Tabel 6.2. Ierarhia operatorilor


Operator Ierarhie
([{ 1
)]} 2
+- 3
*/ 4

Algoritmul este:
se iniţializează stiva şi scrierea postfixată;
atât timp cât nu s- a ajuns la sfârşitul expresiei matematice:
- se citeşte următorul element din expresie;
- dacă este valoare se adaugă în scrierea postfixată;
- dacă este „(” se introduce în stivă;
- dacă este „)” se transferă elemente din stivă în scrierea postfixată până la „(”;
- altfel:
 atât timp cât ierarhia operatorului din vârful stivei este mai mare
ierarhia operatorului curent, se trece elementul din vârful stivei în
scrierea postfixată;
 se introduce operatorul c urent în stivă.
se trec toţi operatorii rămaşi pe stivă în scrierea postfixată.
Având expresia sub această formă, se face evaluarea ei. Algoritmul de evaluare
foloseşte tot o stivă pe care sunt păstraţi de această dată operanzii. Algoritmul este:
se iniţializează stiva;

129
atât timp cât nu s-a ajuns la sfârşitul scrierii postfixate:
- se citeşte următorul element;
- dacă este valoare se depune pe stivă;
- altfel (este operator):
 se extrage din stivă elementul y;
 se extrage din stivă elementul x;
 se efectuează operaţia x operator y;
 se depune rezultatul pe stivă;
ultima valoare care se află pe stivă este rezultatul expresiei.
De exemplu, pentru expresia în scriere postfixată: 4 8 2 3 * - 2 / +, algoritmul se
execută ca în figura 6.5:
Elementul citit Stiva

4 4

8 8
4

2 2
8
4

3 3
2
8
4

* 6
8
4

- 2
4

2 2
2
4

/ 1
4

+ 5

Fig. 6.5. Execuţia algoritmului

Programul care realizează evaluarea expresiilor matematice prin algoritmii prezentaţi


a fost realizat folosind programarea orientată pe obiecte. Astfel există o clasă numită
 stiva care are rolul de a implementa operaţiile cu stiva necesare atât la generarea expresiei în
scriere postfixată cât şi la evaluarea expresiei scrisă în această formă. Acest obiect are la bază
clasa deque (double ended que  – coadă cu 2 capete – listă dublu înlănţuită) care se găseşte în
 biblioteca standard de C++. Fiind nevoie de două tipuri de stivă (una care păstrează operanzi
şi una care păstrează valori reale) s -a construit o clasă stivă template.
Pentru păstrarea formei poloneze inverse se utilizează o structură de date numită coadă
care este implementată tot printr-un obiect care are la bază clasa deque.
În cadrul acestei clase sunt implementate funcţii care ajută la programarea
algoritmului. Există funcţii care verifică dacă primul element din coadă reprezintă o valoare
(eNumar()), extrag primul element ca valoare ( getNumar()).

130
Mai există o clasă  fisier  care are rolul de a parcurge fişierul în care se află expresie
aritmetică ce trebuie evaluată.

Teme de control

1. Să se creeze si traverseze o stiva ale carei informatii utile sunt pointeri la


un articol de tip “carte”.
2. Să se creeze si traverseze o coada ale carei informatii utile sunt articole
“student”.
3. Să se realizeze conversia din stiva de carti in lista dubla de carti.

Bibliografie

Ion IVAN, Marius POPA, Paul POCATILU - Structuri de date, Vol. 1 si 2, Editura ASE,
Bucuresti, 2008.

131
7. Arbori binari şi arbori de căutare
Obiectivele unităţii de învăţare

După studierea acestei unităţi de învăţare, studenţii îşi vor însuşi


cunoştinţe teoretice şi vor deprinde abilităţi practice de lucru cu
arbori binari şi arbori de căutare .

7.1. Structura de date de tip arborescent

Construirea unei structurii de date de tip arborescent porneşte de la problema pe care


o avem de rezolvat.
Pentru găsirea soluţiei ecuaţiei f ( x ) 0 pentru x [ a, b ] se efectuează cu metode
de calcul numeric, dintre care cea mai simplă este aceea a înjumătăţirii intervalului.
Se calculează:
c ( a  b ) / 2
Dacă:
f ( a ) * f ( c ) 0
înseamnă că rădăcina x [ a, c ) , în caz contrar x (c, b ] .
 Noul subinterval este şi el divizat. Astfel, avem imaginea unei modalităţi de lucru
ierarhizată pe atâtea niveluri câte sunt necesare obţinerii unei precizii pentru soluţia ecuaţiei
f ( x ) 0 . Se asociază acestor niveluri reprezentarea grafică din figura 12.1.

[ a, b ]

a+b a+b
[a , ] ( ,b]
2 2

3a + b 3a + b a+b a+b a + 3b a + 3b
[a , ] ( , ] ( , ] ( ,b]
4 4 2 2 4 4
Fig. 7.1. Reprezentarea grafică asociată divizării intervalelor 

Pentru evaluarea expresiei aritmetice:


(a b c d ) ( x  y )
e
c d 
 prin modul de efectuare a calculelor conduce la reprezentarea din figura 7.2.

132
*

e
/

c d
+

x y
+
+

c d
a b

Fig. 7.2. Reprezentarea arborescentă asociată evaluării expresiei

Pentru regăsirea cărţilor într -o bibliotecă, în fişierul tematic cotele se structurează pe


nivele, ca în exemplul din figura 7.3.

 bibliotec
 biblioteca
a
1xxxxx : 7xxxxx

matematica calculatoare economie


1xxxxx : 23xxxx 24xxxx : 51xxxx 52xxxx : 7xxxxx

algebra geometrie hardware software teorie software


1xxxxx / 1 98xxx
98 xxx / 24xxxx / 420
42 0 xxx / 52xxxx / 681x
68 1xxx
xx /
197xxx 23xxxx 419xxx 51xxxx 680xxx 7xxxxx
Fig. 7.3. Structura asociată fişierului de cote ale cărţilor 

Reprezentarea în memorie a informaţiilor care au multiple legături între ele, trebuie să


fie astfel făcută, încât să poată realiza parcurgerea completă a zonelor sau localizarea unui
element din structură.
Se observă că în arborescenţă există un nod numit rădăcină sau părinte. Acesta are
descendenţi.
descendenţi. Fiecare descendent poate fi la rândul său părinte şi în acest caz are descendenţi.
Arborele binar este caracterizat prin aceea că, orice nod al său are un singur părinte şi
maxim doi descendenţi. Fiecare nod este reprezentat prin trei informaţii şi anume:
 Z  – informaţia
 – informaţia utilă care face obiectul prelucrării, ea descriind elementul sau
fenomenul asociat nodului;
 – informaţia
 – informaţia care localizează nodul părinte;
 –  informaţia (informaţiile) care localizează nodul (nodurile) descendent
(descendente).
Alocarea dinamică determină ca şi să fie variabile aleatoare, ale căror legi de
repartiţie sunt necunoscute. Valorile lor sunt stocate în zona de memorie, formând împreună

133
cu variabila  Z structura de date asociată unui nod, structură ce corespunde tripletului (Z j,  j,
 j) asociat nodului  j dintr-un graf de formă arborescentă binară.
Volumul informaţiilor destinate localizării nodurilor, depinde de obiectivele urmărite
şi de rapiditatea cu care se doreşte localizarea unui anumit nod. Cu cât diversitatea
 prelucrărilor este mai mare, cu atât se impune stocarea mai multor informaţii de
localizare.

Z j-1  j-1
 j-1

Z j  j  j  j adr (Z j - 1 )

 j adr (Z j 1 )

Z j + 1  j+1  j-1

Fig. 7.4. Modelul grafic asociat tripletului (Z j,  j, j)

Dacă tripletul (Z j,  j, j) ce corespunde modelului grafic din figura 7.4 permite
 baleierea de sus în jos, sau de jos în sus, pe o ramură a arborelui. Pentru a parcurge pe
orizontală arborele, este necesară atât o informaţie suplimentară de localizare  j , care

corespunde nodului vecin din dreapta de pe ace laşi nivel, cât şi informaţia  j   pentru
localizarea nodului din stânga de pe acelaşi nivel.
Un astfel de arbore se descrie prin: ( Z j ,  j,  j,  j,  j )
Descrierea completă a unui arbore, presupune crearea mulţimii tripletelor sau
cvintuplelor asociate nodurilor.
De exemplu, arborescenţei asociate evaluării expresiei: e = a + b + c  prezentate în
figura 7.5 îi corespunde alocarea şi iniţializarea zonelor de memorie din figura 7.6.
=

+
e

c
+

a b
Fig. 7.5. Structura arborescentă asociată evaluării expresiei e

134
= null

+ e null

c null +

a null b null

Fig. 7.6. Alocarea şi iniţializarea zonelor de memorie


 pentru implementarea
implementarea structurii arborescente
arborescente

Acestor reprezentări grafice, trebuie să le corespundă algoritmi pentru încărcarea


adreselor, întrucât a construi forma liniarizată a structurii arborescente, înseamnă în
 primul rând, a aloca zone de memorie şi în al doilea rând, a iniţializa membrii
structurii cu adrese care să permită reconstruirea corectă a înlănţuirii.
Se observă că pentru iniţializarea corectă, este necesară existenţa la un moment dat a
 pointerilor spre trei zone
zone de memorie alocate
alocate dinamic şi anume:
anume:
 pp  –  pointerul
 pointerul spre zona
zona alocată nodului
nodului precedent (părinte);
(părinte);
 pc  –  pointerul
 pointerul spre zona
zona asociată nodului curent;
 pd  –  pointerul
 pointerul spre zona
zona asociată primului
primului descendent;
Construirea nodului ( Zj,  j,  j ) revine la efectuarea iniţializărilor:
Zj,  j,

ref  (pc) . Z  j  valoare_ citita;


ref  (pc) .  j  pp;
ref  (pc) .  j  pd;
 pp  pc;
 pc  pd;
new (pd);
După efectuare secvenţei se face trecerea la pasul următor. Folosind simbolurile, orice
structură arborescentă se reprezintă prin:
 N mulţimea nodurilor 
 A mulţimea arcelor 
De exemplu, arborescenţa dată pentru calculul ex presiei se reprezintă prin:
 N = { e , = , a, +, b, + , c }
şi
 A = { (a+) , (b+) , (c+) , (+, =) , (=, e) }
Se observă că pentru fiecare element al mulţimii
mulţi mii N, se alocă dinamic zona de memorie
care conţine şi informaţiile  j,  j . La alocare este posibilă numai iniţializarea zonei Z j.
Odată cu alocarea se iniţializează şi un vector de pointeri, cu adresele corespunzătoare
corespunzătoare
zonelor de memorie puse în corespondenţă cu nodurile. În continuare, preluând elementele
mulţimii A, are loc completarea câmpurilor  j şi  j . De exemplu, pentru arborele binar din

135
figura 12.7 construirea se efectuează prin alocarea a 7 zone de memorie cu structura
( Z j ,  j,  j )  pentru nodul din mulţimea  N { A, B, C, D, E, F, G } şi se iniţializează
vectorul de pointeri pp[i] după cum urmează:
 pp [1] adr (ZA)
 pp [2] adr (ZB)
 pp [3] adr (ZC)
 pp [4] adr (ZD)
 pp [5] adr (ZE)
 pp [6] adr (ZF)
 pp [7] adr (ZG)
Baleierea mulţimii:
A { (AB), (AC), (BD), (BE), (CF), (CG) } revine la efectuarea atribuirilor:
ref (pp[1] ).  pp[2]
ref (pp[2] ).  pp[4]
ref (pp[3] ).  pp[6]
ref (pp[4] ). ref (pp[5] ). ref (pp[6] ). ref (pp[7] ).  NULL  (12.11)
ref (pp[2] ).  pp[1]
ref (pp[4] ).  pp[2]
ref (pp[3] ).  pp[1]
ref (pp[6] ).  pp[3]
ref (pp[7] ).  pp[3]
ref (pp[1] ). ref (pp[1] ).  NULL

B C

D
E F G
Fig. 7.7. Arbore binar

Pentru arborii binari echilibraţi, există posibilitatea ca după alocarea memoriei să se


descrie elementele mulţimii A, după care iniţializările câmpurilor θ j şi γ j să se facă prin
apelarea unei funcţii. Problematica devine mai simplă, dacă arborele binar este
complet, adică are n niveluri la bază 2 (n-1) elemente descendente, fără a fi părinte, noduri
terminale.
În programele C/C++, pentru implementarea structurilor de date necontigue, arborele
 binar se defineşte prin următorul tip de bază derivat.

struct arbore
{
int valoare;
arbore *precedent;
arbore *urmator;

136
};
class arb
{
arbore *rad;
};

struct arbore_oarecare
{
int valoare;
arbore_oarecare **fii; //lista fiilor unui nod
int nrfii; //nr de fii ai nodului
};
class arb_oarecare
{
arbore_oarecare *rad;
};

struct arbore_binar
{
int valoare;
arbore_binar *stanga;
arbore_binar *dreapta;
};
class arb_binar
{
arbore_binar *rad;
};

7.2. Transformarea arborilor oarecare în arbori binari

Aplicaţiile concrete, asociază unor obiecte, subansamble sau procese, structuri de tip
arborescent care nu sunt binare, astfel încât se conturează ideea ca arborii binar sunt cazuri
 particulare de arbori, mai ales prin frecvenţa cu care sunt identificaţi în practică. Mecanismele
de realizare şi de utilizare a arborilor binar îi fac practice, deşi în realitate au frecvenţa d e
apariţie scăzută.
Apare problema transformării unei structuri arborescente oarecare într -o structură
arborescentă binară, problema rezolvabilă prin introducerea de noduri fictive. Astfel, fiind
dată arborescenţa din figura 7 .8, transformarea ei în struct ură arborescentă binară, revine la
introducerea nodurilor fictive, x, y, u, v, w, rezultând arborele din figura 7.9.

137
A

B C D

e f g h i j k i

Fig. 7.8. Structură arborescentă oarecare


A

B x

e u C D

f g h y i
v

 j w

k l

Fig. 7.9. Structură arborescentă cu noduri fictive

Arborele oarecare, are un număr de noduri   mai mic decât arborele binar, nodurilor
fictive corespunzându-le zone de memorie structurate (NULL, γ j, θj).
Alocarea dinamică presupune, ca în zona [D i, Df ] prin dealocare să apară goluri, adică
zone libere ce sunt realocate altor variabile. Este necesară o gestionare a acestor zone şi
 periodic trebuie să se efectueze o realocare prin reorganizare, aşa fel încât să dispară golurile
rezultate în procesul de alocare- dealocare multiplă. De exemplu, pentru un program P, este
necesară alocarea a 3 zone de memorie de 1500, 2000, 4000 baiţi, ce corespund arborilor
 binar A, B şi C.
Alocarea este efectuată iniţializând variabilele pointer  p A, p B şi pC  prin apelul succesiv
al funcţiei alocare( ), (pas 1).
Dealocarea după un timp a arborelui binar B, determină apariţia unui gol între zonele
ocupate de variabilele A şi C, (pas 2).

138
Alocarea unei zone de memorie pentru arborii binari D (3000 baiţi) şi E (1000 baiţi),
revin la a dispune pe D în continuarea lui C şi a intercala arborele E între A şi C, în “golul”
rezultat din dealocarea lui E, rămânând între E şi C un “gol” de 300 baiţi, (pas 3).
Dacă se păstrează informaţiile privind modul de iniţializare a variabilelor pointer care
stochează adresele nodurilor rădăcină a arborilor A, E, C şi D, este posibilă glisar ea
informaţiilor în aşa fel încât să dispară “golul” dintre E şi C. Nu s -a luat în considerare însăşi
necontiguitatea din interiorul fiecărui arbore.
În practică, apare problema optimizării dispunerii variabilelor dinamice, dar şi cea a
alegerii momentului în care dispersia elementelor atinge un astfel de nivel, încât zona pentru
alocare dinamică este practic inutilizabilă şi trebuie reorganizată.

7.3. Arbori binari de căutare

Un arbore binar de căutare este un arbore binar care are proprietatea că prin
 parcurgerea în inordine a nodurilor se obţine o secvenţă monoton crescătoare a cheilor
(cheia este un câmp ce serveşte la identificarea nodurilor în cadrul arborelui). Câmpul
cheie este singurul care prezintă interes din punct de vedere al operaţiilor care se pot efectua
asupra arborilor de căutare.
Principala utilizare a arborilor binari de căutare este regăsirea rapidă a unor informaţii
memorate în cheile nodurilor. Pentru oric e nod al unui arbore de căutare, cheia acestuia are o
valoare mai mare decât cheile tuturor nodurilor din subarborele stâng şi mai mică decât cheile
nodurilor ce compun subarborele drept.

20

10 25

7 22 26

Fig. 7.10. Arbore binar de căutare

Structura de date folosită pentru descrierea unui nod al unui arbore binar de căutare va
fi următoarea:

struct arbore_binar
{
int cheie;
arbore_binar *stanga;
arbore_binar *dreapta;
};
class arb_binar
{
arbore_binar *rad;
};

139
Rădăcina arborelui binar de căutare va fi definită în felul următor: arb *Radacina =
 NULL;
Se observă ca fiecare nod este compus din cheia asociată şi din informaţiile de
legătură care se referă eventualii fii.
Aşa cum le spune şi numele, arborii binari de căutare sunt folosiţi pentru regăsirea
rapidă a informaţiilor memorate în cheile nodurilor. De aceea căutarea unui nod cu o anumită
valoarea a cheii este o operaţie deosebit de importantă.
Căutarea începe cu nodul rădăcină al arborelui prin compararea valorii cheii căutate cu
valoarea cheii nodului curent. Dacă cele două valori coincide, căutarea s -a încheiat cu succes.
Dacă informaţia căutată este mai mică decât cheia nodului, căutarea se continuă în
subarborele stâng. Dacă cheia căutată este mai mare decât valoarea cheii nodului, căutarea se
reia pentru subarborele drept.
Crearea unui arbore binar de căutare presupune adăugarea câte unui nod la un arbore
iniţial vid. După inserarea unui nod, arborele trebuie să rămână în continuare ordonat. Din
acest motiv, pentru adăugarea unui nod se parcurge arborele începând cu rădăcina şi
continuând cu subarborele stâng sau drept în funcţie de relaţia de ordine dintre cheia nodului
şi cheia de inserat. Astfel, dacă cheia de inserat este mai mica decât cheia nodului, următorul
nod vizitat va fi rădăcina subarborelui stâng. În mod similar, dacă cheia de inserat este mai
mare decât cheia nodului, traversarea se va continua cu nodul rădăcină al subarborelui drept.
Această modalitate de traversare se continuă până în momentul în care se ajunge la un   nod
fără descendent. Acestui nod îi va fi adăugat un nod fiu cu valoarea dorită a cheii.
Aplicaţiile care utilizează arbori binari de căutare pot permite sau pot interzice, în
funcţie de filozofia proprie, inserarea în cadrul arborelui a unei chei care există deja.
Inserarea în cadrul arborelui anterior a unui nou nod cu valoarea cheii egală cu 12 conduce
către următorul arbore, figura 7.11.

20

10 25

7 12 22 26

Fig. 7.11. Arbore binar de căutare după inserarea unui nod

Maniera uzuală de inserare a unui nod într -un arbor e binar de căutare este cea
recursivă.
În practică, de cele mai multe ori căutarea şi inserarea se folosesc împreună. Astfel, în
cazul în care căutarea unei chei s -a efectuat fără succes, aceasta este adăugată la arborele
 binar de căutare.
O altă operaţie care se poate efectua asupra unui arbore binar de căutare este ştergerea
unui nod care are o anumită valoare a cheii. Dacă valoarea cheii este găsită în cadrul
arborelui, nodul corespunzător este şters. Arborele trebuie să rămân arbore de căutare şi după
ştergerea nodului.
În ceea ce priveşte nodul care va fi şters, acesta se va încadra într -una din variantele
următoare:
a) nodul nu are subarbori (fii);
 b) nodul are doar subarbore stâng;
c) nodul are doar subarbore drept;

140
d) nodul are atât subarbore stâng cât şi subarbore drept.
În cazul în care nodul nu are nici subarbore stâng dar nici subarbore drept (varianta a)
este necesară doar ştergerea nodului. Nu sunt necesare alte operaţiuni de actualizare a
arborelui.

Fig. 7.12. Arbore binar de căutare înainte de şterge rea unui nod

În figura 7.12 este prezentat un fragment al unui arbore binar de căutare din care
dorim să ştergem nodul din drea pta, iar în figura 7 .13 se prezintă acelaşi fragment de arbore
dar după ştergerea nodului dorit.

Fig. 7.13. Arbore binar de căutare după ştergerea unui nod

Pentru cazurile b şi c (nodul pe e dorim să -l ştergem are subarbore stâng sau drept), pe
lângă ştergerea nodului este necesară şi actualizarea legăturilor dintre no durile arborelui. În
figurile 7.14 şi 7.16 se prezintă câte un fragment dintr -un arbore binar de căutare din care
dorim să ştergem nodul evidenţiat. În figura 7.14, nodul pe care dorim să -l ştergem are numai
subarbore stâng iar cel din figura 7.16 are doar subarbore drept. În figurile 7.15 şi 7 .17 se pot
observa efectele operaţiei de ştergere.

Fig. 7.14. Subarbore numai cu descendent stâng

141

Fig. 7.15. Subarbore după efectuarea ştergerii

Fig. 7.16. Subarbore numai cu descendent drept

Fig. 7.17. Subarbore după efectuarea ştergerii

Pentr u aceste prime trei cazuri, actualizarea arborelui se face în felul următor: fiul
nodului care va fi şters, dacă există, va deveni fiul tatălui acestuia. Actualizarea arborelui este
urmată de ştergerea nodului din memorie.
Cazul în care nodul ce se doreşte a fi şters are atât subarbore stâng cât şi subarbore
drept necesită o tratare specială. Astfel, mai întâi se localizează fie cel mai din stânga fiu al
subarborelui drept fie cel mai din dreapta fiu al subarborelui drept. Cheile acestor noduri sunt
valoarea imediat următoare cheii nodului ce se doreşte a fi şters şi valoarea precedentă.
După suprimarea nodului dorit, arborele va trebui să rămână în continuare arbore de
căutare ceea ce înseamnă că relaţia de ordine dintre nodurile arborelui va trebui să se păstreze.
Pentru aceasta, unul din nodurile prezentate anterior va trebui adus în locul nodului care se
doreşte a fi şters după care are loc ştergerea efectivă a nodului dorit.
Determinarea celui mai din dreapta fiu al subarborelui stâng se face parcurgând
subarborelui stâng prin vizitarea numai a fiilor din dreapta. Primul nod care nu are subarbore
drept este considerat ca fiind cel mai din dreapta nod al subarborelui stâng.

142
În figura 7.18 se prezintă un fragment de arbore binar de căutare din care dorim să
suprimăm nodul evidenţiat care are doi descendenţi. Presupunem ca nodul haşurat este cel
mai din dreapta fiu al subarborelui stâng. Acest nod va lua locul nodului care se va şterge. În
figura 7.19 este reprezentat fragmentul de arbore după efectuarea operaţiei de ştergere a
nodului dorit.

… … …

Fig. 7.18. Subarbore cu doi descendenţi


… … … …

Fig. 7.19. Subarbore după efectuarea ştergerii

7.4. Aplicaţii care utilizează structura de date de tip arbore binar de căutare

Asupra arborilor binari de căutare pot fi efectuate o serie de operaţii, dintre care o
 parte sunt specifice tuturor structurilor de date compuse (adăugare element, ştergere
elemente) iar altele sunt specifice acestui tip de structură de date. De asemenea se
remarcă operaţii la nivel de element (nod), precum şi operaţii care implică întregul arbore.
Programul următor, exemplifică o modalitate de creare a unui arbore binar de căutare,
stergere noduri, traversare, numarare noduri şi de tipărire a acestuia:

#include <iostream.h>
#include <conio.h>
#include <stdio.h>

143
#include <malloc.h>
#include <string.h>

struct nod
{
int info;
nod *stg,*drt;
};

class arbbin
{
nod *rad;
int stergere(nod *&);
void stergere_nod(nod*&,int);
public:
arbbin()
{rad=NULL;};
~arbbin()
{rad=NULL;};
void traversare_srd(nod*);
void srd();
void traversare_rsd(nod *);
void rsd();
void traversare_sdr(nod *);
void sdr();
int sumaFrunze(nod*);
int sFrunza();
int numara(nod *);
int numara_nod();
void print(nod *);
void tiparire();
void salvare();
nod *inserare_nod(nod *,int );
void operator + (int);
void operator - (int);
arbbin &operator >(FILE *);
arbbin &operator <(FILE *);
void inserare_cuv(nod *& ,char*);
void insert(char *);
nod *operator [] (int);
};

nod *arbbin::inserare_nod(nod *rad,int k)


{
if (rad)
{
if (k<rad->info) rad->stg=inserare_nod(rad->stg,k);
else
if (k>rad->info) rad->drt=inserare_nod(rad->drt,k);
else printf("\nNodul exista in arbore!");

144
return rad;
}
else
{
nod *p=new nod;
p->stg=NULL;
p->drt=NULL;
p->info=k;
return p;
}
}

void arbbin::operator +(int k)


{
rad=inserare_nod(rad,k);
}

void arbbin::traversare_srd(nod *rad)


{
if (rad)
{
traversare_srd(rad->stg);
printf(" %d",rad->info);
traversare_srd(rad->drt);
}
}

void arbbin::srd()
{
nod *p;
p=rad;
traversare_srd(p);
}

void arbbin::traversare_rsd(nod *rad)


{
if (rad)
{
printf(" %d",rad->info);
traversare_rsd(rad->stg);
traversare_rsd(rad->drt);
}
}

void arbbin::rsd()
{
nod *p;
p=rad;
traversare_rsd(p);
}

145
void arbbin::traversare_sdr(nod *rad)
{
if (rad)
{
traversare_sdr(rad->stg);
traversare_sdr(rad->drt);
printf(" %d",rad->info);
}
}

void arbbin::sdr()
{
nod *p;
p=rad;
traversare_sdr(p);
}

void arbbin::print(nod *rad)


{
if (rad)
{
printf("%d",rad->info);
if((rad->stg)||(rad->drt))
{
printf("(");
print(rad->stg);
printf(",");
print(rad->drt);
printf(")");
}
}
else
printf("-");
}

void arbbin::tiparire()
{
nod *p;
p=rad;
print(p);
}

int arbbin::sumaFrunze(nod *rad)


{
if (rad)
if(!rad->stg&&!rad->drt)
return rad->info;
else
return sumaFrunze(rad->stg)+sumaFrunze(rad->drt);

146
else
return 0;
}

int arbbin::sFrunza()
{
nod *p;
p=rad;
return sumaFrunze(p);
}

int arbbin::numara(nod *rad)


{
if (rad)
return 1+numara(rad->stg)+numara(rad->drt);
else return 0;
}

int arbbin::numara_nod()
{
nod *p;
int nr;
p=rad;
nr=numara(p);
return nr;
}

void arbbin::stergere_nod(nod *&rad,int inf)


{
nod *aux;
if (!rad) printf("\nNodul nu exista in arbore!");
else
if (inf<rad->info) stergere_nod(rad->stg,inf);
else
if (inf>rad->info) stergere_nod(rad->drt,inf);
else
{
aux=rad;
if (!aux->stg)
{
rad=aux->stg;
delete(aux);
}
else
if(!aux->drt)
{
rad=aux->drt;
delete(aux);
}
else

147
rad->info=stergere(rad->stg);
}
}

int arbbin::stergere(nod *&p)


{
if (p->stg)
return stergere(p->stg);
else
{
nod *q=p;
int inf=q->info;
p=p->stg;
delete(q);
return inf;
}
}

void arbbin::operator -(int inform)


{
nod *nou;
nou=rad;
stergere_nod(nou,inform);
}

nod *arbbin::operator [] (int inf)


{
nod *aux;
aux=rad;
while(aux&&aux->info!=inf)
{
if (inf<aux->info)
aux=aux->stg;
else
if (inf>aux->info)
aux=aux->drt;
};
if (aux&&aux->info==inf)
cout<<"\nNodul cautat exista in arbore!";
else
cout<<"\nNodul cautat nu exista in arbore!";
return aux;
}

void meniu()
{

cout<<"\n 1.ADAUGARE NOD DE LA TASTATURA";


cout<<"\n 2.STERGERE NOD DE LA TASTATURA";
cout<<"\n 3.CAUTARE NOD IN ARBORE";

148
cout<<"\n 4.TRAVERSARI ARBORE-SRD(),RSD(),SDR()";
cout<<"\n 5.SUMA INFORMATIILOR DIN FRUNZE ";
cout<<"\n 6.NUMARUL NODURILOR DIN ARBORE";
cout<<"\n 7.AFISARE ARBORE";
cout<<"\n 0.TERMINARE";
}

void main()
{
arbbin arb;
int informatie;
char optiune;
arb+10;arb+7;arb+15;arb+9;arb+3;arb+8;arb+25;
meniu();
}
}

Teme de control

1. Să se scrie funcţia pentru numă rarea elementelor dintr-un arbore binar de


căutare.
2. Să se scrie funcţ ia pentru determinarea elementului minim şi elementului
maxim dintr-un arbore binar de căutare .

Bibliografie

Ion IVAN, Marius POPA, Paul POCATILU - Structuri de date, Vol. 1 si 2, Editura ASE,
Bucuresti, 2008.

149
8. Arbori B

Obiectivele unităţii de învăţare

După studierea acestei unităţi de învăţare, studenţii îşi vor însuşi cunoştinţe
teoretice şi vor deprinde abilităţi practice de lucru cu a rbori B.

8.1. Arbori B. Definiţie. Proprietăţi.

În cazul sistemelor de gestiune a bazelor de date relaţionale (SGBDR) este important


ca pe lângă stocarea datelor să se realizeze şi regăsirea rapidă a acestora. În acest scop
sunt folosiţi indecşii. Un index este o colecţie de perechi <valoare cheie, adresa articol>.
Scopul primar al unui index este acela de a facilita accesul la o colecţie de articole. Un index
se spune ca este dens dacă el conţine câte o pereche <valoare cheie, adresa articol>  pentru
fiecare articol din colecţie. Un index care nu este dens uneori este numit index rar .
Structura de date foarte des folosită pentru implementarea indecşilor este arborele de
căutare. Articolele memorate pot fi oricât de complexe, dar ele conţin un câmp numit cheie ce
serveşte la identificarea acestora. Să notăm cu C mulţimea cheilor posibile ce vor trebui
regăsite cu ajutorul arborelui de căutare. Dacă arborele de căutare este astfel construit încât
foloseşte o relaţie de ordine totală pe C, atunci vom spune că arborele de căutare este bazat pe
ordinea cheilor. Arborii de căutare, bazaţi pe ordinea cheilor, sunt de două feluri: arbori
binari de căutare (au o singură cheie asociată fiecărui nod) sau arbori multicăi de căutare (au
mai multe chei asociate fiecărui nod).
Performanţele unui index se îmbunătăţesc în mod semnificativ prin mărirea factorului
de ramificare a arborelui de căutare folosit.  Arborii multicăi de căutare sunt o
generalizare a arborilor binari de căutare. Astfel, unui nod oarecare, în loc să i se
ataşeze o singură cheie care permite ramificarea în doi subarbori, i se ataşează un număr de m
chei, ordonate strict crescător, care permit ramificarea în m + 1 s ubarbori. Numărul m diferă
de la nod la nod, dar în general pentru fiecare nod trebuie să fie între anumite limite (ceea ce
va asigura folosirea eficientă a mediului de stocare). Cele m chei ataşate unui nod formează o
 pagină. Determinarea poziţiei cheii căutate în cadrul unui nod se realizează secvenţial în cazul
 paginilor cu număr mic de chei sau prin căutare binară. Un exemplu de arbore multicăi de
căutare de ordin 3 este dat în figura 8.1.

● 50 ● 100 ●

● 20 ● 40 ● ● 70 ● 120 ● 140 ●

15 30 60 65 110 130 136 150


● ● ● ● 42 43 ● ● ● ●

7 18 26 36 58 62 69 145

Fig. 8.1. Arbore multicăi de căutare de ordin 3

Un arbore multicăi de căutare care nu este vid are următoarele proprietăţi:


fiecare nod al arborelui are structura dată în figura 8.2;

150
n P0 K 0 P1 K 1 P2 … Pn - 1 K n - 1 Pn

Fig. 8.2. Structura de nod pentru un arbore multicăi de căutare de ordin n

unde P0, P1, …, Pn sunt pointeri către subarbori şi K 0, K 1, … , K n  –  1 sunt valorile cheilor.
Cerinţa ca fiecare nod să aibă un număr de ramificaţii mai mic sau egal decât m conduce la
restricţia n ≤ m –  1.
valorile cheilor într-un nod sunt date în ordine crescătoare:  K i < K i + 1 ,
i 0, n 2
toate valorile de chei din nodurile subarborelui indicat de P i  sunt mai mici
decât valoarea cheii K i, i 0, n 1 .
toate valorile de chei din nodurile subarborelui indicat de P n  sunt mai mari
decât valoarea de cheie K n –  1.
subarborii indicaţi de Pi, i 0, n sunt de asemenea arbori multicăi de căutare.
Prima oară arborele B a fost descris de R. Bayer şi E. McCreight în 1972. Arborii B
rezolvă problemele majore întâlnite la implementarea arborilor de căutare stocaţi pe disc:
au întotdeauna toate nodurile frunză pe acelaşi nivel (cu alte cuvinte sunt
echilibraţi după înălţime);
operaţiile de căutare şi actualizare afectează puţin blocuri pe disc;
 păstrează articolele asemănătoare în acelaşi bloc pe disc;
garantează ca fiecare nod din arbore va fi plin cu un pro cent minim garantat.

Un arbore B de ordin m este un arbore multicăi de căutare şi are următoarele


 proprietăţi:

i. toate nodurile frunză sunt pe acelaşi nivel;


ii. rădăcina are cel puţin doi descendenţi, dacă nu este frunză;
m
iii. fiecare pagină conţine cel puţin 2 chei (excepţie face rădăcina care poate
avea mai puţine chei, dacă este frunză);
iv. nodul este fie frunză, fie are n + 1 descendenţi (unde n este numărul de chei
m

din nodul respectiv, cu 2 ≤ n ≤ m-1 );


v. fiecare pagină conţine cel mult m-1 chei; din acest motiv, un nod poate avea
maxim m descendenţi.
Proprietatea i menţine arborele balansat. Proprietatea ii forţează arborele să se ramifice
devreme. Proprietatea iii ne asigură că fiecare nod al arborelui este cel puţin pe jumătate plin.
Înălţimea maximă a unui arbore B dă marginea superioară a numărului de accese la
disc necesare pentru a localiza o cheie.
Se consideră h înălţimea maximă a unui arbore B cu  N   chei, unde valoarea
indicatorului este dată de relaţia:
 N  1
h log m
2
2
Ca exemplu, pentru N = 2.00 0.000 şi m = 20, înălţimea maximă a unui arbore B de
ordin m va fi 3, pe când un arborele binar corespondent va avea o înălţime mai mare de 20.

151
8.2. Operaţii de bază într-un arbore B

Procesul de căutare într-un arbore B este o extindere a căutării într -un arbore binar.


Operaţia de căutare în arborele B se realizează comparând cheia căutată  x cu cheile
nodului curent, plecând de la nodul rădăcină. Dacă nodul curent are n chei, atunci se
disting următoarele cazuri:
ci < x < ci +1 , i 1, n  – se continuă căutarea în nodul indicat de P i;
cn < x – se continuă căutarea în nodul indicat de P n;
 x < c0 – se continuă căutarea în nodul indicat de P 0.
Lungimea maximă a drumului de căutare este dată de înălţimea arborelui. F iecare
referire a unui nod implică selecţia unui subarbore.
Arborele B suportă căutarea secvenţială a cheilor. Arborele este traversat secvenţial
 prin referirea în inordine a nodurilor. Un nod este referit de mai multe ori întrucât el conţine
mai multe chei. Subarborele asociat fiecărei chei este referit înainte ca următoarea cheie sa fie
accesata. Arborii B sunt optimi pentru accesul direct la o cheie. Pentru accesul secvenţial la o
cheie nu se obţin performanţe satisfăcătoare.
Condiţia ca toate frunzele să fie pe acelaşi nivel duce la un comportament caracteristic
arborilor B: faţă de arborii binari de căutare, arborilor B nu le este permis să crească la frunze;
ei sunt forţaţi sa crească la rădăcină.
Operaţia de inserare a unei chei în arborele B este pre cedată de operaţia de căutare. În
cazul unei căutări cu succes (cheia a fost găsită în arbore) nu se mai pune problema inserării
întrucât cheia se afla deja în arbore. Dacă cheia nu a fost găsită, operaţia de căutare se va
termina într-un nod frunză. În acest nod frunză se va insera noua cheie. Funcţie de gradul de
umplere al nodului frunză afectat, se disting următoarele cazuri:
nodul are mai puţin de m  –  1 chei; inserarea se efectuează fără să se modifice
structura arborelui ;
nodul are deja numărul maxi m de m – 1 chei; în urma inserării nodul va avea prea
multe chei, de aceea el va “fisiona”. În urma fisionării vom obţine două noduri care
se vor găsi pe acelaşi nivel şi o cheie mediană care nu se va mai găsi în nici unul din
m
cele două noduri. Cele 2 chei din stânga rămân în nodul care fisionează. Cele
m
2  din dreapta vor forma cel de- al doilea nod. Cheia mediană va urca în nodul
 părinte, care la rândul lui poate să fisioneze. Dacă şi rădăcina fisionează, atunci
cheia mediană va da naştere la o nouă rădăcină.
Se observă că procesul de inserare a unei chei garantează că fiecare nod intern va avea
cel puţin jumătate din numărul maxim de descendenţi. În urma operaţiilor de inserare arborele
va deveni mai înalt şi mai lat, figura 8.3.

Fig. 8.3. Modificările dimensionale ale unui arbore B după efectuarea inserării

152
Să considerăm un arbore B de ordin 5 (deci numărul maxim de chei dintr -un nod va fi
4). În urma inserării valorilor de cheie 22, 57, 41, 59 nodul rădăcină va fi cel din figura 8.4.

22 41 57 59
Fig. 8.4. Structura nodului rădăcină după inserarea cheilor 

În urma inserării cheii 54, nodul rădăcină va conţine prea multe chei, aşa că   el va
fisiona, figura 8.5.
cheia

22 41 54 57 59 22 41 57 59
Fig. 8.5. Fisionarea nodului rădăcină

În urma promovării cheii mediene 54 se va forma o nouă rădăcină, arborele crescând


în înălţime cu 1, figura 8.6.
a

● 54 ●
b c

22 41 57 59
Fig. 8.6. Formarea noii rădăcini a arborelui

Inserarea cheilor 33, 75, 124 nu ridică probleme, figura 8.7.


a

● 54 ●
b c

22 33 41 57 59 75 124

Fig. 8.7. Structura arborelui după inserarea cheilor 33, 75, 124

Inserarea cheii 62 însă duce la divizarea nodului c, figura 13.8.

Cheia mediana 62
va promova
c c d
57 59 62 75 124 57 59 75 124

Fig. 8.8. Fisionarea nodului c

Cheia 62 va promova în nodul rădăcină, figura 8.9.


a

● 54 ● 62 ●

b c d

22 41 57 59 75 124

Fig. 8.9. Promovarea cheii 62 în rădăcină

153
În urma inserării cheilor 33, 122, 123, 55, 60, 45, 66, 35 configuraţia arborelui va fi
cea din figura 13.10.
a
35 54 62 122

b f c d e
22 33 41 45 55 57 59 60 66 75 123 124

Fig. 8.10. Structura arborelui după inserări succesive

Inserarea cheii 56 se va face în nodul c şi acesta va fisiona, figura 8.11.


Cheia mediana 57
va promova

c c g
55 56 57 59 60 55 56 59 60
Fig. 8.11. Fisionarea nodului c

Oricum, nodul părinte a este deja plin şi nu poate primi noua cheie 57 şi pointerul
către nodul nou format f. Algoritmul de fisionare este aplicat din nou, dar de data aceasta
nodului a, figura 8.12.
Cheia mediana 57
va promova

a a h
35 54 57 62 122 35 54 62 122

Fig. 8.12. Fisionarea nodului a

Cheia mediană promovată 57 va forma noua rădăcină a arborelui B, având subarborele


stâng a şi subarborele drept h, figura 8.13.
i
57

a h
35 54 62 122

b f c g d e
22 33 41 45 55 56 59 60 66 75 123 124

Fig. 8.13. Noua configuraţie a arborelui B

Uzual, în special pentru arbori B de ordin mare, un nod părinte are suficient spaţiu
disponibil pentru a primi valoarea unei chei şi un pointer către un nod descendent. În cel mai
rău caz algoritmul de fisionare este aplicat pe întreaga înălţime a arborelui. În acest mod
arborele va creşte în înălţime, lungimea drumului de căutare crescând cu 1.
Sintetizat, algoritmul de inserare a unei valori de cheie în arbore este prezentat în
figura 8.14.

154
Adaugă noua valoare
de cheie în nodul
frunză corespunzator

OVERFLOW?

Divide nodul în două noduri


aflate pe acelaşi nivel şi
promovează cheia mediană

Fig. 8.14. Algoritmul de inserare a unei chei în arbore

Algoritmul de inserare într-un arbore B (pseudocod):


inserează noua valoare de cheie în nodul frunză corespunzător;
nodul_curent = nodul_frunza;
while( starea pentru nodul_curent este OVERFLOW  ):
- divide nodul_curent în două noduri aflate pe acelaşi nivel şi promovează cheia
mediană în nodul părinte pentru nodul_curent;
- nodul_curent = nodul părinte pentru nodul_curent.
În cel mai rău caz, inserarea unei chei noi duce la aplicarea algoritmului de fisionare
 pe întreaga înălţime a arborelui, fisionându-se h  –  1 noduri, unde h este înălţimea arborelui
înainte de inserare. Numărul total de fisionări care au apărut când arborele avea  p noduri este
de  p  –  2. Prima fisionare adaugă două noduri noi; toate celelalte fisionări produc doar un
m
1
singur nod nou. Fiecare nod are un minim de 2 chei, cu excepţia rădăcinii, care are cel
m
1  p 1 1
2
 puţin o cheie. Deci arborele cu  p noduri conţine cel puţin chei.
Probabilitatea ca o fisionare să fie necesară după inserarea unei valori de cheie este mai mică
decât :
 p 2
m divizari
1  p 1 1
2 chei
m 1
1
care este mai mică decât 1 d ivizare per 2 inserări de chei (deoarece  p 2 tinde către 0
 p 1

 pentru o valoare mare a lui  p şi  p 2   este aproximativ 1). De exemplu, pentru m = 10,
 probabilitatea apariţiei unei divizări este de 0.25. Pentru m = 100, probabilitatea divizării este
0.0204. Pentru m = 200, probabilitatea divizării este 0.0101. Cu alte cuvinte, cu cât ordinul
arborelui este mai mare, cu atât este mai mică probabilitatea ca inserarea unei valori de cheie
sa ducă la divizarea unui nod.
Operaţia de  ştergere  dintr-un arbore B este ceva m ai complicată decât operaţia de
inserare. Operaţia de ştergere se realizează simplu dacă valoarea de cheie care
urmează a fi ştearsă se află într -un nod frunză. Dacă nu, cheia va fi ştearsă logic, fiind

155
înlocuită cu o alta, vecină în inordine, care va fi ştearsă efectiv. În urma ştergerii se disting
următoarele cazuri:
m
dacă nodul conţine mai mult de 2 chei, ştergerea nu ridică probleme;
m
dacă nodul are numărul minim de chei 2 , după ştergere numărul de chei din
nod va fi insuficient. De aceea se împrumută o ch eie din nodul vecin (aflat pe
m
acelaşi nivel în arbore) dacă acesta are cel puţin 2 chei, caz în care avem de-a
face cu o  partajare. Dacă nu se poate face o partajare cu nici unul din nodurile
vecine (fiecare nod vecin are numărul minim de chei), atunci cele două noduri
vecine vor  fuziona, împrumutându-se o cheie şi din nodul părinte. Partajarea sau
fuzionarea trebuie eventual repetate şi pentru nivelurile superioare. În cazul cel
mai nefavorabil, dacă partajarea sau fuzionarea parcurg întreaga înălţime a
arborelui, se va forma o nouă rădăcină, înălţimea arborelui scăzând cu unu.
Probabilitatea de fuzionare a nodurilor scade o dată cu creşterea ordinului
arborelui B.
Să considerăm următoarea configuraţie de a rbore B de ordin 5 din figura 8.15.
a
57

b c
35 54 62 122

d f g h i
22 33 22 55 56 59 60 66 75 123 124

e
40 41 45 50
Fig. 8.15. Arbore B de ordin 5

Ştergerea valorilor de cheie 40 şi 45 din nodul e nu ridică probleme, figura 8.16


a
57

b c
35 54 62 122

d e f g h i
22 33 34 41 50 55 56 59 60 66 75 123 124

Fig. 8.16. Structura arborelui după ştergerea cheilor 40 şi 45

Ştergerea valorii de cheie 50 necesită însă partajarea între nodurile d şi e, figura 8.17.

156
a
57

valoarea de cheie 35
coboara; valoarea de
b c
cheie 34 urca
35 54 62 122

d e f g h i
22 33 34 41 50 55 56 59 60 66 75 123 124

Fig. 8.17. Partajarea între nodurile d şi e

a
57

b c
34 54 62 122

d e f g h i
22 33 35 41 55 56 59 60 66 75 123 124

Fig. 8.18. Structura arborelui după partajare

Ştergerea valorii de cheie 22 va necesita fuzionarea nodurilor d şi e, figura 8.19.


a
57

fuzionare b c
34 54 62 122

d e f g h i
22 33 35 41 55 56 59 60 66 75 123 124

Fig. 8.19. Fuzionarea nodurilor d şi e

Însă în urma fuzionării nodurilor d şi e, nodul b va conţine prea puţine valori de cheie,
aşa că vor fuziona şi nodurile b şi c, figura 8.20.
a
fuzionare
57

b c
54 62 122

d f g h i
33 34 35 41 55 56 59 60 66 75 123 124

Fig. 8.20. Fuzionarea nodurilor a, b şi c

157
Astfel, în final, arborele B va arăta astfel ca în figura 8 .21.

122
54 57 62

123 124
33 34 35 41 55 56 59 60 66 75
Fig. 8.21. Structura finală a arborelui B

Algoritmul de ştergere dintr -un arbore B, în pseudocod:


if  (valoarea de cheie care se şterge nu este într -un nod frunză) then: înlocuieşte
valoarea de cheie cu succesor / predecesor;
nodul_curent = nodul_frunza;
while (starea pentru nodul_curent este UNDERFLOW  ):
- încearcă partajarea cu unul din nodurile vecine aflate pe acelaşi nivel, via
nodul părinte;
- if (nu este posibil ) then:
1. fuzionează nodul_curent cu un nod vecin, folosind o valoare de
cheie din nodul părinte;
2. nodul_curent = părintele pentru nodul_curent.

8.3. Algoritmii C++ pentru inserarea unei valori de cheie într-un arbore B

Înainte de a prezenta algoritmii pentru căutare şi inserare într -un arbore B, să începem


mai întâi cu declaraţiile necesare  pentru implementarea unui arbore. Pentru simplicitate vom
construi întregul arbore B în memoria heap, folosind pointeri pentru a descrie structura
arborescentă. În majoritatea aplicaţiilor, aceşti pointeri vor trebui înlocuiţi cu adrese către
 blocuri sau pagini stocate într-un fişier pe disc (deci accesarea unui pointer va însemna un
acces la disc).
Un nod al arborelui B va fi descris de clasa btree_node. Pentru a fi cât mai generală în
ceea ce priveşte tipul valorii de cheie folosită va fi implementată ca fiind o clasă template.
Oricum, tipul valorii de cheie folosită va trebui să permită ordonarea cheilor; în cazul
implementării de faţă cerinţa este de a suporta testele de inegalitate strictă (<) şi de egalitate
(= =).
Pentru generalitate se vor folosi funcţii pentru testele de egalitate şi inegalitate strictă.
Declaraţiile acestora sunt următoarele:

template <typename FIRST_ARGUMENT_TYPE,


typename SECOND_ARGUMENT_TYPE,
typename RETURN_TYPE
>
struct binary_function { /** empty */ } ;

template <typename TYPE>


struct less_than : public binary_function<TYPE, TYPE, bool> {
inline bool operator()( const TYPE& first, const TYPE& second ) {
return first < second ;

158
}
} ; /** struct less_than */

template <>
struct less_than<char*> : public binary_function<char*, char*, bool> {
inline bool operator()( const char* first, const char* second ) {
return strcmp( first, second ) < 0 ;
}
} ; /** struct less_than<char*> */

template <typename TYPE>


struct equal_to : public binary_function<TYPE, TYPE, bool> {
inline bool operator()( const TYPE& first, const TYPE& second ) {
return first == second ;
}
} ; /** struct equal_to */

template <>
struct equal_to<char*> : public binary_function<char*, char*, bool> {
inline bool operator()( const char* first, const char* second ) {
return strcmp( first, second ) == 0 ;
}
} ; /** struct equal_to<char*> */

Declaraţia clasei btree_node este:

template <typename KEY_NODE_TYPE>


class btree_node {
friend class btree<KEY_NODE_TYPE> ;

public:
typedef KEY_NODE_TYPE key_node_type ;
typedef key_node_type* key_node_ptr ;
typedef btree_node<KEY_NODE_TYPE> btree_node_type ;
typedef btree_node_type* btree_node_ptr ;

typedef struct {
key_node_type key_value ;
btree_node_ptr node_ptr ;
} btree_node_tuple ;

public:
enum BTREE_NODE_STATUS {
EMPTY = 0,
UNDERFLOW,
MINIMAL,
OPERATIONAL,
FULL,
OVERFLOW

159
};

public:
btree_node( int ncapacity ) ;
virtual ~btree_node( void ) ;

BTREE_NODE_STATUS status( void ) const ;


int capacity( void ) const ;
int size( void ) const ;

key_node_type& key_value( int position ) ;


btree_node<KEY_NODE_TYPE>* child( int position ) ;

int push( const key_node_type& value, btree_node_ptr child ) ;


bool find( const key_node_type& value, int& position ) ;
int remove_at( const int position ) ;

bool is_leaf( void ) const ;

int split( btree_node_tuple* tuple ) ;

protected:
static int initialize( btree_node_ptr node ) ;
int shift2right( const int start ) ;
int shift2left( const int start ) ;

protected:
btree_node( const btree_node<KEY_NODE_TYPE>& ) ;
btree_node<KEY_NODE_TYPE>& operator =( const
btree_node<KEY_NODE_TYPE>& ) ;

protected:
less_than<key_node_type> _less_than ;
equal_to<key_node_type> _equal_to ;

protected:
key_node_ptr node_keys ;
btree_node_ptr* node_childs ;
int node_capacity ;
int node_size ;
}; /** class btree_node */

Structura btree_node_tuple va fi folosită de algoritmul de fisionare (divizare) a unui


nod. Valorile din cadrul enumerării BTREE_NODE_STATUS înseamnă:
 EMPTY   – nodul nu conţine nici o cheie ;
UNDERFLO  – nodul conţine prea puţine chei ;

 MINIMAL  – nodul conţine numărul minim de chei ;
OPERATION   – numărul de chei conţinut de nod respecta cerinţele de arbore
 AL B;

160
 FULL  – nodul conţine numărul maxim de chei ;
OVERFLOW   – nodul conţine prea multe chei.

Ordinea în care câmpurile din enumerare apar este importantă (a se vedea funcţiile de
introducere şi ştergere a unei chei din arbore). Constructorul clasei btree_node  primeşte ca
 parametru numărul maxim de chei care pot fi păstrate într -un nod (capacitatea nodului).
Operaţiile de copiere a unui nod nu sunt permise (este dezactivat constructorul de
copiere şi operatorul de atribuire). Câmpurile _ less_than şi  _equal_to sunt folosite în testele
de egalitate şi inegalitate strictă. Câmpul node_capacity  păstrează numărul maxim de chei
dintr-un nod (capacitatea nodului). Câmpul node_size menţine numărul de chei aflate la un
moment dat într-un nod. Câmpul node_keys este un vector în care vor fi păstrate valorile de
chei din nod (în implementarea de faţă, pentru implementarea mai uşoara a algoritmului de
divizare a unui nod, numărul de chei păstrate într -un nod va fi cu unu mai mare decât numărul
maxim de chei). Câmpul node_childs va păstra pointeri către descendenţi (numărul de
descendenţi ai unui nod este cu unu mai mare decât numărul de chei din acel nod).
Constructorul btree_node<KEY_TYPE>::btree_node() apelează funcţia initialize()
care iniţializează un nod al arborelui (alocă memoria necesară pentru păstrarea valorilor de
chei şi a pointerilor către descendenţi, setează numărul de chei prezente în arbore la 0):

template <typename KEY_NODE_TYPE>


btree_node<KEY_NODE_TYPE>::btree_node( int capacity ) {
this->node_capacity = capacity ;
initialize( this );
}

template <typename KEY_NODE_TYPE>


int
btree_node<KEY_NODE_TYPE>::initialize( btree_node_ptr node ) {
if( 0 = = node ) return -1 ;
node->node_keys = new key_node_type[ node->capacity() + 1 ];
if( 0 = = node->node_keys ) return -1 ;
node->node_childs = new btree_node_ptr[ node->capacity() + 2 ];
if( 0 = = node->node_childs ) {
delete []node->node_keys ;
node->node_keys = 0;
return -1 ;
}
memset(node->node_childs,0,sizeof(btree_node_ptr)*
(node->capacity() + 2 ) );
node->node_size = 0;

return 0;
}

Destructorul btree_node<KEY_TYPE>::~btree_node() eliberează memoria alocată:

template <typename KEY_NODE_TYPE>

161
btree_node<KEY_NODE_TYPE>::~btree_node( void ) {
delete []this->node_keys ;
delete []this->node_childs ;
}

Pentru a se afla numărul de chei aflate la un moment dat într -un nod se foloseşte
funcţia size():

template <typename KEY_NODE_TYPE>


inline int
btree_node<KEY_NODE_TYPE>::size( void ) const
{ return this->node_size ; }

Pentru a se determina numărul maxim de chei care pot fi păstrate într -un nod se
foloseşte funcţia capacity():

template <typename KEY_NODE_TYPE>


inline int
btree_node<KEY_NODE_TYPE>::capacity( void ) const
{ return this->node_capacity ; }

Un nod poate fi interogat pentru starea în care se află folosind funcţia  status():

template <typename KEY_NODE_TYPE>


inline btree_node<KEY_NODE_TYPE>::BTREE_NODE_STATUS
btree_node<KEY_NODE_TYPE>::status( void ) const {
if( 0 == size() ) return EMPTY ;
else if( size() < ( capacity() / 2 ) ) return UNDERFLOW ;
else if( size() == ( capacity() / 2 ) ) return MINIMAL ;
else if( size() == capacity() ) return FULL ;
else if( size() > capacity() ) return OVERFLOW ;
return OPERATIONAL ;
}

Se observă că starea nodului este funcţie de numărul de chei aflate la un moment dat
în nod. Mai exact, dacă avem un arbore B de ordin m, atunci numărul maxim de chei dintr -
m
1
un nod va fi m –  1, iar numărul minim de chei va fi 2 . Parametrul primit de constructor
va fi m – 1, deci capacitatea nodului va fi m – 1. Cum numărul de chei aflate la un moment
dat într-un nod se obţine folosind funcţia size(), vom avea:
dacă  size() returnează zero, atunci în nod nu se găseşte nici o cheie starea
nodului este EMPTY  ;
numărul minim de chei din nod este capacity()/2; deci dacă
 size()==capacity()/2, atunci nodul are numărul minim de chei starea
nodului este MINIMAL ;

162
dacă  size() < capacity() / 2 , atunci nodul are prea puţine chei starea nodului
este UNDERFLOW  ;
dacă size() == capacity(), atunci nodul are numărul maxim de chei permise
starea nodului este  FULL ;
dacă size() > capacity(), atunci nodul are prea multe chei starea nodului este
OVERFLOW .
Pentru a se determina dacă nodul este un nod frunză se foloseşte funcţia is_leaf(), care
va returna true dacă nodul este o frunză (un nod este nod frunză dacă nu are nici un
descendent):

template <typename KEY_NODE_TYPE>


bool
btree_node<KEY_NODE_TYPE>::is_leaf( void ) const {
assert( 0 != this->node_childs ) ;

// return 0 == this->node_childs[0] ;
for( int idx = 0; idx <= size(); idx++ )
if( 0 != this->node_childs[idx] ) return false ;
return true ;
}

Ca funcţii pentru interogarea valorii unei chei şi unui descendent dintr -o anume poziţie
se folosesc funcţiile key_value() şi child():

template <typename KEY_NODE_TYPE>


inline KEY_NODE_TYPE&
btree_node<KEY_NODE_TYPE>::key_value( int position ) {
if( position < 0 ||
position >= size()
){
/** signal out of bounds*/
assert( false ) ;
}

return this->node_keys[position] ;
}

template <typename KEY_NODE_TYPE>


inline btree_node<KEY_NODE_TYPE>*
btree_node<KEY_NODE_TYPE>::child( int position ) {
if( position < 0 ||
position > size()
){
/** signal out of bounds */
assert( false ) ;
return 0 ;
}

163
return this->node_childs[position] ;
}

Căutarea unei valori de cheie într -un nod se face folosind funcţia  find(). Primul
 parametru primit de funcţie este valoarea de cheie care se caută în nod. După cum valoarea de
cheie căutată se găseşte sau nu în nod,  find() va returna true  sau  false, cu menţiunea ca al
doilea parametru al funcţiei ( position, care este un parametru de ieşire) va fi setat după cum
urmează:
daca valoarea de cheie căutată se găseşte în nod, atunci  position este setat la
indexul la care se găseşte cheia în nod;
dacă valoarea de cheie căutată nu se găseşte în nod,  position va indica indexul
subarborelui în care s- ar putea găsi valoarea de cheie căutată.

template <typename KEY_NODE_TYPE>


bool
btree_node<KEY_NODE_TYPE>::find( const key_node_type& value,
int& position ) {
bool ret_value ;
position = -1 ;

if( _less_than( value, this->node_keys[0] ) ) {


position = 0 ;
ret_value = false ;
} else {
for( position = size() - 1 ;
 _less_than( value, key_value( position ) ) &&
position > 0;
position--
);

ret_value = _equal_to( value, this->node_keys[position] ) ;


if( !ret_value ) position++ ;
}

return ret_value ;
}

Inserarea unei valori de cheie în nod se realizează folosind funcţia  push(). În


implementarea de faţă inserarea valorii de cheie într -un nod se face întotdeauna specificând şi
 pointerul către subarborele din dreapta valorii de cheie (subarbore care conţine toate valorile
de cheie mai mari decât valoarea de cheie inserată). Dacă n odul este în starea OVERFLOW 
sau valoarea de cheie există deja în nod, funcţia va returna –1, fără a face nimic altceva. În
urma determinării poziţiei pe care va fi inserată valoarea de cheie, ar putea fi necesară o
deplasare către dreapta a valorilor de cheie din nod mai mari decât cea care se inserează
(deplasarea se face împreună cu pointerii către descendenţi; funcţia folosită este  shift2right()).

164
template <typename KEY_NODE_TYPE>
int
btree_node<KEY_NODE_TYPE>::push(
const key_node_type& value,
btree_node_ptr child
){
if( OVERFLOW == status() ) return -1 ;

if( EMPTY == status() ) {


this->node_keys[0] = value ;
this->node_childs[1] = child ;
this->node_size = 1 ;
return 0 ;
}

int key_position = -1 ;
if( find( value, key_position ) ) {
/** duplicate key value */
return -1 ;
}

if( key_position < size() ) shift2right( key_position ) ;


this->node_keys[key_position] = value ;
this->node_childs[key_position + 1] = child ;
this->node_size++ ;

return 0 ;
}

Funcţia shift2right() este:

template <typename KEY_NODE_TYPE>


int
btree_node<KEY_NODE_TYPE>::shift2right( const int start ) {
if( EMPTY == status() ||
start < 0 ||
start >= size()
) return -1 ;

for( int idx = size(); idx > start; idx-- ) {


this->node_keys[idx] = this->node_keys[idx - 1] ;
this->node_childs[idx + 1] = this->node_childs[idx] ;
}
return 0 ;
}

165
Pentru eliminarea unei chei dintr- o anumită poziţie se va folosi funcţia remove_at()
(practic, eliminarea presupune diminuarea cu unu a numărului de chei conţinute de nod şi o
deplasare către stânga a valorilor de cheie şi a subarborilor aflaţi în dreapta poziţiei din care se
şterge cheia).

template <typename KEY_NODE_TYPE>


int
btree_node<KEY_NODE_TYPE>::remove_at( const int position ) {
if( -1 == shift2left( position ) ) return -1 ;
this->node_size-- ;

return 0 ;
}

Funcţia shift2left() este:

template <typename KEY_NODE_TYPE>


int
btree_node<KEY_NODE_TYPE>::shift2left( const int start ) {
if( EMPTY == status() ||
start < 0 ||
start >= size()
) return -1 ;

for( int idx = start + 1; idx < size(); idx++ ) {


this->node_keys[idx - 1] = this->node_keys[idx] ;
this->node_childs[idx] = this->node_childs[idx + 1] ;
}

return 0 ;
}

Atunci când starea unui nod este de OVERFLOW , acesta se va diviza. În urma
divizării se va obţine un nod nou. Funcţia de divizare a unui nod este  split(). Parametrul
funcţiei split() este de ieşire, fiind de tipul btree_node_tuple:

typedef struct {
key_node_type key_value ;
btree_node_ptr node_ptr ;
} btree_node_tuple ;

În urma procesului de divizare a unui nod va urca în nodul părinte cheia mediană şi un
 pointer către nodul nou format. Cheia mediană va fi câmpul key_value  al structurii
btree_node_tuple. Pointerul către nodul nou format va fi câmpul node_ptr   al structurii
btree_node_tuple. Noul nod va conţine valorile de chei şi subarborii din dreapta cheii
mediane.

166
template <typename KEY_NODE_TYPE>
int
btree_node<KEY_NODE_TYPE>::split(
btree_node<KEY_NODE_TYPE>::btree_node_tuple* tuple
){
if( 0 == tuple ) return 0 ;
if( OVERFLOW != this->status() ) return 0;

int median_position = this->size() / 2;


tuple->key_value = this->key_value( median_position );

btree_node_ptr new_node = new btree_node_type( this->capacity());


if( 0 == new_node ) return -1;

for( int idx = median_position + 1;


idx < this->size() ;
idx++
) new_node->push( this->key_value( idx ), this->child( idx + 1 ) );
new_node->node_childs[0] = this->child( median_position + 1 );

this->node_size = median_position;
tuple->node_ptr = new_node;

return 0 ;
}

În continuare, va fi dată declaraţia clasei de arbore B:

template <typename KEY_TYPE>


class btree {

public:
typedef KEY_TYPE key_type ;
typedef btree_node<KEY_TYPE> btree_node_type ;
typedef btree_node_type* btree_node_ptr ;
typedef btree_node_type::btree_node_tuple btree_node_tuple ;
typedef btree<KEY_TYPE> btree_type ;
typedef btree_type* btree_ptr ;

public:
btree( int order ) ;
virtual ~btree( void ) ;

int push( const key_type& value ) ;


int remove( const key_type& value ) ;

protected:
int push_down( btree_node_tuple* tuple, btree_node_ptr current ) ;

167
int remove_down( const key_type& value, btree_node_ptr current ) ;
int replace_with_predecessor( btree_node_ptr node, int position ) ;
void restore( btree_node_ptr current, const int position ) ;
void move_left( btree_node_ptr current, const int position ) ;
void move_right( btree_node_ptr current, const int position ) ;
void combine( btree_node_ptr current, const int position ) ;

private:
btree_node_ptr root ;
int order ;
} ; /** class btree */

Clasa btree este o clasă template după tipul valorii de cheie. La fel ca la clasa
btree_node au fost folosite o serie de typedef- uri în cadrul clasei (codul este mai uşor de scris
/ citit dacă tipul btree_node<KEY_TYPE> se redefineşte ca fiind btree_node_type). Nodul
rădăcină al arborelui B este dat de câmpul root . Ordinul arborelui B este dat de câmpul order .
Constructorul btree<KEY_TYPE>::btree()  primeşte ca parametru ordinul arborelui şi
setează rădăcina arborelui la 0.

template <typename KEY_TYPE>


btree<KEY_TYPE>::btree( int order ) {
this->order = order ;
this->root = 0 ;
}

Destructorul clasei btree<KEY_TYPE>::~btree() va elibera spaţiul de memorie ocupat


de arbore.

template <typename KEY_TYPE>


btree<KEY_TYPE>::~btree( void ) {
clear( this->root ) ;
this->root = 0 ;
}

template <typename KEY_TYPE>


void btree<KEY_TYPE>::clear( btree_node_ptr node ) {
if( 0 == node ) return ;
if( !node->is_leaf() )
for( int idx = 0 ; idx <= node->size(); idx++ )
clear( node->child( idx ) ) ;
delete node ;
}

Funcţia de inserare a unei valori de cheie în arbore este  push(). Dacă arborele nu are
nici o valoare de cheie inserată (adică rădăcina arborelui este 0 arborele este gol), atunci
inserarea valorii de cheie este simplă: se construieşte rădăcina arborelui cu valoarea de cheie

168
care se inserează. Daca arborele nu este gol, atunci se inserează cheia în arbore recursiv
folosind funcţia push_down(), pornind de la nodul rădăcină. Funcţia push_down() va returna 1
dacă în urma procesului de inserare recursivă a valorii de cheie nodul rădăcină a fost divizat
(cheia şi un pointer către subarborele drept al cheii vor fi conţinute de câmpurile variabilei
tuple). În acest caz înălţimea arborelui creşte cu unu, formându -se o nouă rădăcină cu
valoarea de cheie dată de câmp ul kei_value al variabilei tuple; subarborele stâng va fi vechea
rădăcină a arborelui, iar subarborele drept va fi dat de câmpul node_ptr  al variabilei tuple.

template <typename KEY_TYPE>


int btree<KEY_TYPE>::push( const key_type& value ) {
if( 0 == this->root ) {
this->root = new btree_node_type( this->order - 1 ) ;
if( 0 == this->root ) return -1 ;

this->root->push( value, (btree_node_ptr)0 ) ;


return 0 ;
}

btree_node_tuple tuple ;
tuple.key_value = value ;
tuple.node_ptr = 0 ;

if( push_down( &tuple, this->root ) ) {


btree_node_ptr new_root = new btree_node_type( this->order - 1 );
if( 0 == new_root ) return -1 ;

new_root->push( tuple.key_value, tuple.node_ptr ) ;


new_root->node_childs[0] = this->root ;
this->root = new_root ;
}

return 0 ;
}

Funcţia  push_down() de inserare recursivă în arbore primeşte ca parametri nodul


curent current în care se încearcă inserarea, şi, împachetat în variabila tuple, valoarea de cheie
care se inserează. Iniţial, la primul apel, câmpul node_ptr  al variabilei tuple va avea valoarea
0. În cadrul algoritmului de inserare se va căuta valoarea de cheie în nodul curent. Dacă
aceasta există deja în nod (arbore), funcţia de inserare nu va face nimic şi va returna – 1.
Altfel, variabila key_position va indica:
dacă nodul curent este frunză, key_position indică locul în care se va insera valoarea
de cheie;
dacă nodul curent nu este o frunză, key_position va indica subarborele în care va trebui
să se facă inserarea.
Dacă nodul curent este frunză, inserarea este simplă: se apelează doar metoda  push()
de inserare a unei chei într-un nod (la inserarea într-un no d frunză întotdeauna câmpul
node_ptr   al parametrului tuple va fi 0). Dacă nodul curent nu este frunză, atunci va trebui
urmărit dacă în urma inserării recursive în nodul curent a urcat o valoare de cheie. Daca în
nodul curent a urcat o valoare de cheie (metoda  push_down() a returnat valoarea 1) atunci:

169
câmpurile parametrului tuple vor conţine valoarea de cheie care a urcat şi un pointer
către subarborele drept corespunzător cheii ;
în nodul curent vor trebui inserate key_value şi node_ptr   indicate de câmpurile
 parametrului tuple.
În final, se verifică starea în care se află nodul curent. Dacă starea acestuia este de
OVERFLOW , atunci înseamnă ca acesta conţine prea multe chei şi va trebui divizat. Pentru
aceasta se foloseşte metoda  split() a nodului care va primi ca parametru adresa lui tuple. În
urma apelului metodei  split() conţinutul câmpurilor key_value şi node_ptr  ale variabilei tuple
vor fi actualizate pentru a indica valoarea de cheie care va urca în nodul părinte al nodului
curent şi pointerul către subarborele drept; în acest caz metoda  push_down()  va returna 1
 pentru a indica faptul că în nodul părinte vor trebui folosite câmpurile variabile tuple. Dacă
starea nodului nu este OVERFLOW , metoda push_down() va returna 0.

template <typename KEY_TYPE>


int btree<KEY_TYPE>::push_down(
btree_node_tuple* tuple,
btree_node_ptr current
){
if( 0 == current ) return 0 ;

int key_position ;
bool duplicate_key = current->find( tuple->key_value, key_position) ;
if( duplicate_key ) {
/** signal duplicate value */
return -1 ;
}

if( current->is_leaf() ) {
current->push( tuple->key_value, tuple->node_ptr );
} else {
if( push_down( tuple, current->child( key_position ) ) )
current->push( tuple->key_value, tuple->node_ptr);
}

if( btree_node_type::OVERFLOW == current->status() ) {


current->split( tuple ) ;
return 1 ;
}

return 0 ;
}

8.4. Algoritmii C++ pentru ştergerea unei valori de cheie  intr-un arbore B

Funcţia de ştergere a unei valori de cheie dintr -un arbore B este remove(). Intern este
folosită funcţia recursivă remove_down(). Dacă în urma ştergerii valorii de cheie nodul
rădăcină nu mai are nici o valoare de cheie (starea nodului rădăcină este  EMPTY ), atunci noua

170
rădăcină a arborelui este subarborele stâng al vechii rădăcini. Vechea rădăcină se elimină din
memorie.

template <typename KEY_TYPE>


int btree<KEY_TYPE>::remove( const key_type& value ) {
remove_down( value, this->root ) ;

if( btree_node_type::EMPTY == this->root->status() ) {


btree_node_ptr old_root = this->root ;
this->root = this->root->child( 0 ) ;

delete old_root ;
}

return 0 ;
}

Funcţia de ştergere recursivă a unei valori de cheie din arbore este remove_down().
Parametrii primiţi de funcţie sunt valoarea de cheie care se şterge şi un pointer către nodul
curent. Dacă la un moment dat nodul curent este 0, atunci înseamnă că valoarea   de cheie
solicitată a fi ştearsă nu se găseşte în arbore. Altfel, funcţie de tipul nodului unde cheia este
găsită avem următoarele cazuri:
dacă valoarea de cheie se găseşte într -un nod frunză, atunci ştergerea înseamnă
apelarea metodei remove() a nodului frunză pentru eliminarea cheii aflată în poziţia
dată de variabila position ;
dacă valoarea de cheie a fost găsită într -un nod care nu este nod frunza, atunci
valoarea de cheie care trebuie ştearsă va fi înlocuită, în implementarea de faţă, cu
valoarea de cheie care o precede (se poate demonstra că valoarea de cheie care o
 precede se găseşte într -un nod frunză). După ce se face înlocuirea folosind funcţia
replace_with_predecessor(), se va continua algoritmul de ştergere, dar de data
aceasta se va solicita ş tergerea valorii de cheie cu care s- a făcut înlocuirea.
Se observă că dacă nodul curent este valid (nu este 0) şi valoarea de cheie care se
caută nu se găseşte în nodul curent, atunci variabila  position va fi poziţionată de metoda  find()
a nodului ca fiind poziţia subarborelui care este posibil să conţină valoarea de cheie.
În finalul funcţiei, dacă nodul curent nu este frunză (este un nod părinte care are
descendenţi care au fost afectaţi), se testează starea nodului rădăcină pentru subarborele pe
care s-a efectuat coborârea în procesul de ştergere. Dacă starea acestui nod este
UNDERFLOW (conţine prea puţine valori de cheie), atunci va fi apelată funcţia restore() care
va restaura proprietăţile de arbore B.

template <typename KEY_TYPE>


int btree<KEY_TYPE>::remove_down(
const key_type& value,
btree_node_ptr current
){
if( 0 == current ) {
/** signal value not found */

171
return 0 ;
}

int position ;
if( current->find( value, position ) ) {
if( current->is_leaf() ) current->remove_at( position ) ;
else {
replace_with_predecessor( current, position ) ;
remove_down(current->key_value(position ),
current->child( position)) ;
}
} else remove_down( value, current->child( position ) ) ;

if( !current->is_leaf() &&


btree_node_type::UNDERFLOW = = current->child(position)->status() )
restore( current, position ) ;

return 0 ;
}

Funcţia de înlocuire a unei valori de cheie cu valoarea de cheie care o precede este
replace_with_succesor(). Parametrii primiţi de funcţie sunt: node, un pointer către nodul care
conţine valoarea de cheie care se înlocuieşte şi  position care dă poziţia su barborelui (în nodul
indicat de node) care conţine valoarea de cheie care precede valoarea de cheie care se
înlocuieşte.

template <typename KEY_TYPE>


int btree<KEY_TYPE>::replace_with_predecessor(
btree_node_ptr node,
int position
){
if( position > node->size() ) return 0 ;

btree_node_ptr leaf = node->child( position ) ;


while( !leaf->is_leaf() ) leaf = leaf->child( leaf->size() ) ;
node->node_keys[position] = leaf->key_value( leaf->size() - 1 ) ;

return 1 ;
}

Funcţia care asigură restaurarea proprietăţilor de arbore B este restore(). Parametrii


funcţiei sunt: un pointer către un nod părinte ( current ) şi o poziţie ( position) care indică un
subarbore al nodului current  (nodul rădăcină al acestui subarbore conţine  prea puţine valori
de cheie). Funcţia folosită în implementarea de faţă este cumva orientată către stânga, în
sensul că mai întâi se uită la nodul vecin din stânga pentru a lua o valoare de cheie, folosind
nodul vecin din dreapta numai dacă nu găseşte suf iciente valori de cheie în nodul din stânga.
Paşii care sunt necesari sunt ilustraţi în figura 8.22.

172
template <typename KEY_TYPE>
void btree<KEY_TYPE>::move_left(
btree_node_ptr current,
const int position
){
btree_node_ptr node = current->child( position - 1 ) ;
node->push(
current->key_value( position - 1 ),
current->child( position )->child( 0 )
);

node = current->child( position ) ;


current->node_keys[position - 1] = node->key_value( 0 );
node->node_childs[0] = node->node_childs[1] ;
node->shift2left( 0 ) ;
node->node_size-- ;
}

template <typename KEY_TYPE>


void btree<KEY_TYPE>::move_right(
btree_node_ptr current,
const int position
){
btree_node_ptr node = current->child( position ) ;
node->shift2right( 0 ) ;
node->node_childs[1] = node->child( 0 ) ;
node->node_keys[0] = current->key_value( position - 1 ) ;
node->node_size++ ;
node = current->child( position - 1 ) ;
current->node_keys[position - 1] = node->key_value(node->size()-1);
current->child(position)->node_childs[0]=node->child(node->size());
node->node_size-- ;
}

template <typename KEY_TYPE>


void btree<KEY_TYPE>::combine(
btree_node_ptr current,
const int position )
{
btree_node_ptr rnode = current->child( position ) ;
btree_node_ptr lnode = current->child( position - 1 ) ;

lnode->push(current->key_value( position - 1 ), rnode->child( 0 ));


for( int idx = 0; idx < rnode->size(); idx++ )
lnode->push( rnode->key_value( idx ), rnode->child( idx + 1 ) );

current->remove_at( position - 1 ) ;
delete rnode ;
}

174

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