Sunteți pe pagina 1din 113

STRUCTURI DE DATE

Adrian CARABINEANU

Cuprins
1 Algoritmi. Notiuni generale
1.1 Exemplu de algoritm.
Sortarea prin insertie . . . . . . . .
1.2 Aspecte care apar la rezolvarea unei
probleme . . . . . . . . . . . . . . .
1.3 Timpul de executie a algoritmilor .
1.4 Corectitudinea algoritmilor . . . . .
1.5 Optimalitatea algoritmilor . . . . .
1.6 Existenta algoritmilor . . . . . . . .

5
. . . . . . . . . . . . . . .
.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

. 7
. 7
. 9
. 9
. 14

2 Tipuri de structuri de date


2.1 Generalitati . . . . . . . . . . . . . . . . . . . . .
2.2 Liste . . . . . . . . . . . . . . . . . . . . . . . . .
2.2.1 Liste alocate secvential . . . . . . . . . . .
2.2.2 Liste alocate
nlantuit . . . . . . . . . . .
2.3 Stive . . . . . . . . . . . . . . . . . . . . . . . . .
2.4 Liste de tip coada . . . . . . . . . . . . . . . .
2.5 Grafuri . . . . . . . . . . . . . . . . . . . . . . . .
2.6 Arbori binari . . . . . . . . . . . . . . . . . . . .
2.6.1 Parcurgerea arborilor binari . . . . . . . .
2.7 Algoritmul lui Huffman . . . . . . . . . . . . . . .
2.7.1 Prezentare preliminara . . . . . . . . . . .
2.7.2 Coduri prefix. Arbore de codificare . . . .
2.7.3 Constructia codificarii prefix a lui Huffman
2.7.4 Optimalitatea algoritmului Huffman . . . .

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

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

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

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

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

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

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

15
15
15
16
16
27
28
30
35
36
42
43
43
45
49

3 Tehnici de sortare
51
3.1 Heapsort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
1

CUPRINS
3.1.1 Reconstituirea proprietatii de heap
3.1.2 Constructia unui heap . . . . . . .
3.1.3 Algoritmul heapsort . . . . . . . .
3.2 Cozi de prioritati . . . . . . . . . . . . . .
3.3 Sortarea rapida . . . . . . . . . . . . . . .
3.3.1 Descrierea algoritmului . . . . . . .
3.3.2 Performanta algoritmului de sortare
3.4 Metoda bulelor (bubble method) . . . . .

. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
rapida
. . . .

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

52
54
54
57
60
60
62
64

4 Tehnici de c
autare
4.1 Algoritmi de cautare . . . . . . . . . . . . . . . . . . . . . . .
4.1.1 Algoritmi de cautare secventiala (pas cu pas) . . . . .
4.1.2 Cautarea
n tabele sortate (ordonate) . . . . . . . . . .
4.1.3 Arbori de decizie asociati cautarii binare . . . . . . . .
4.1.4 Optimalitatea cautarii binare . . . . . . . . . . . . . .
4.2 Arbori binari de cautare . . . . . . . . . . . . . . . . . . . . .
4.3 Arbori de cautare ponderati (optimali) . . . . . . . . . . . . .
4.4 Arbori echilibrati . . . . . . . . . . . . . . . . . . . . . . . . .
4.4.1 Arbori Fibonacci . . . . . . . . . . . . . . . . . . . . .
4.4.2 Proprietati ale arborilor echilibrati . . . . . . . . . . .
4.5 Insertia unui nod
ntr-un arbore echilibrat . . . . . . . . . . .
4.5.1 Rotatii
n arbori echilibrati . . . . . . . . . . . . . . . .
4.5.2 Exemple . . . . . . . . . . . . . . . . . . . . . . . . . .
4.5.3 Algoritmul de insertie
n arbori echilibrati . . . . . . .
4.6 Stergerea unui nod al unui arbore echilibrat . . . . . . . . . .
4.6.1 Algoritmul de stergere a unui nod dintr-un arbore echilibrat . . . . . . . . . . . . . . . . . . . . . . . . . . . .

65
65
66
67
71
72
76
81
86
87
89
91
91
95
98
98
98

Lista figurilor
1.1

Arbore de decizie . . . . . . . . . . . . . . . . . . . . . . . . . 11

2.1
2.2
2.3
2.4
2.5
2.6

Liste simplu si dublu


nlantuite . . . . . . . . . .
Exemple de grafuri . . . . . . . . . . . . . . . . .
Exemplu de arbore binar . . . . . . . . . . . . . .
Exemplu de arbore binar cu precizarea legaturilor
Exemplu de arbore Huffman . . . . . . . . . . . .
Construirea arborelui Huffman . . . . . . . . . . .

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

17
31
36
37
44
46

3.1

Exemplu de heap reprezentat sub forma unui arbore binar si


sub forma unui vector . . . . . . . . . . . . . . . . . . . . . . 52
3.2 Efectul functiei ReconstituieHeap . . . . . . . . . . . . . . . . 53
3.3 Model de executie a functiei ConstruiesteHeap . . . . . . . . . 55
4.1
4.2
4.3
4.4
4.5
4.6
4.7
4.8
4.9
4.10
4.11
4.12
4.13
4.14

Arbore de cautare binara . . . . . . . . . . . . . . . . . . . .


Arbori de cautare . . . . . . . . . . . . . . . . . . . . . . . .
Optimizarea lungimii drumurilor externe . . . . . . . . . . .
Stergerea radacinii unui arbore binar de cautare . . . . . . .
Arbore binar de cautare . . . . . . . . . . . . . . . . . . . .
Arbori posibili de cautare si numarul mediu de comparatii
pentru o cautare reusita . . . . . . . . . . . . . . . . . . . .
Arbori Fibonacci . . . . . . . . . . . . . . . . . . . . . . . .
Rotatie simpla la dreapta pentru re-echilibrare . . . . . . . .
Rotatie dubla la dreapta pentru re-echilibrare . . . . . . . .
Rotatie dubla la dreapta pentru re-echilibrare . . . . . . . .
Rotatie simpla la st
anga pentru re-echilibrare . . . . . . . .
Rotatie dubla la st
anga pentru re-echilibrare . . . . . . . . .
Rotatie dubla la st
anga pentru re-echilibrare . . . . . . . . .
Exemplu de insertie
ntr-un arbore echilibrat . . . . . . . . .
3

.
.
.
.
.

71
72
73
80
80

.
.
.
.
.
.
.
.
.

82
88
92
93
93
94
94
95
96

LISTA FIGURILOR
4.15
4.16
4.17
4.18
4.19
4.20
4.21

Exemplu
Exemplu
Exemplu
Exemplu
Exemplu
Exemplu
Exemplu

de
de
de
de
de
de
de

insertie
ntr-un arbore echilibrat . . . . . . . .
insertie
ntr-un arbore echilibrat . . . . . . . .
insertie
ntr-un arbore echilibrat . . . . . . . .
stergere a unui nod dintr-un arbore echilibrat
stergere a unui nod dintr-un arbore echilibrat
stergere a unui nod dintr-un arbore echilibrat
stergere a unui nod dintr-un arbore echilibrat

.
.
.
.
.
.
.

.
.
.
.
.
.
.

96
97
97
99
99
100
101

Capitolul 1
Algoritmi. Notiuni generale
De nitie preliminara. Un algoritm este o procedura de calcul bine de nita
care primeste o multime de valori ca date de intrare si produce o multime de
valori ca date de iesire.

1.1

Exemplu de algoritm.
Sortarea prin insertie

Vom considera mai


nt
ai problema sortarii (ordonarii) unui sir de n numere

ntregi a [0] , a [1] , ..., a [n 1] (ce reprezinta datele de intrare). Sirul ordonat
(de exemplu crescator) reprezinta datele de iesire. Ca procedura de calcul
vom considera procedura sortarii prin insertie pe care o prezentam
n cele ce
urmeaza:
Incep
and cu al doilea numar din sir, repetam urmatorul procedeu :
inseram numarul de pe pozitia j ,retinut
ntr-o cheie,
n subsirul deja ordonat
a [0] , ..., a [j 1] astfel
nc
at sa obtinem subsirul ordonat a [0] , ..., a [j] . Ne
oprim c
and obtinem subsirul ordonat de n elemente. De exemplu, pornind de
la sirul de numere
ntregi 7, 1, 3, 2, 5, folosind sortarea prin insertie obtinem
succesiv
7 |1325
17 |325
137 |25
1237 |5
12357
Linia verticala | separa subsirul ordonat de restul sirului. Prezentam mai jos
programul scris
n C + + pentru sortarea elementelor unui sir de 10 numere
5

CAPITOLUL 1. ALGORITMI. NOTIUNI GENERALE

ntregi:
# include <iostream.h>
void main(void)
{// datele de intrare
int a[10];
int i=0;
while(i<n)
{cout<<a[<<i<<]=; cin>>*(a+i); i=i+1;}
//procedura de calcul
for(int j=1;j<10;j++)
{int key=a[j];
//insereaza a[j]
n sirul sortat a[0,...,j-1]
i=j-1;
while((i>=0)*(a[i]>key))
{a[i+1]=a[i];
i=i-1;}
a[i+1]=key;}
//datele de iesire
for(j=0;j<n;j++)
cout<<a[<<j<<]=<<*(a+j)<<;; }
Putem modifica programul C ++ de sortare a n numere
ntregi impun
and
sa se citeasca numarul n, aloc
and dinamic un spatiu de n obiecte de tip int
si tin
and cont ca (a + i) = a[i] :
# include <iostream.h>
void main(void)
{
// datele de intrare
int n;
int i=0;
cout<<n=;
cin>>n;
int* a;
a = new int [n];
while(i<n)
{cout<<a[<<i<<]=; cin>>*(a+i); i=i+1;}
//procedura de calcul
for(int j=1;j<n;j++)
{int key=*(a+j);

1.2. ASPECTE CARE APAR LA REZOLVAREA UNEI PROBLEME 7


//insereaza a[j] in sirul sortat a[0,...,j-1]
i=j-1;
while((i>=0)*(*(a+i)>key))
{*(a+i+1)=*(a+i);
i=i-1;}
*(a+i+1)=key; }
//datele de iesire
for(j=0;j<n;j++)
cout<<a[<<j<<]=<<*(a+j)<<;;}

1.2

Aspecte care apar la rezolvarea unei


probleme

C
and se cere sa se elaboreze un algoritm pentru o problema data, care sa
furnizeze o solutie (fie si aproximativa, cu conditia mentionarii acestui lucru),
cel putin teoretic trebuie sa fie parcurse urmatoarele etape:
1) Demonstrarea faptului ca este posibila elaborarea unui algoritm pentru
determinarea unei solutii.
2) Elaborarea unui algoritm (
n acest caz etapa anterioara poate deveni
inutila).
3) Demonstrarea corectitudinii.
4) Determinarea timpului de executie al algoritmului.
5) Investigarea optimalitatii algoritmului.
Vom prezenta
n cele ce urmeaza, nu neaparat
n ordinea indicata mai
sus, aceste aspecte.

1.3

Timpul de executie a algoritmilor

Un algoritm este elaborat nu doar pentru un set de date de intrare, ci pentru


o multime de astfel de seturi. De aceea trebuie bine precizata multimea
(seturilor de date) de intrare. Timpul de executie se masoara
n functie de
lungimea n a setului de date de intrare.
Ideal este sa determinam o formula matematica pentru T (n) = timpul de
executare pentru orice set de date de intrare de lungime n. Din pacate, acest
lucru nu este
n general posibil. Din aceasta cauza ne vom limita la a evalua
ordinul de marime al timpului de executie.

CAPITOLUL 1. ALGORITMI. NOTIUNI GENERALE

Sa reluam procedura de calcul pentr algoritmul de sortare prin insertie:


//procedura de calcul .................................cost.............timp
for(int j=1;j<n;j++) ......................................c1 ................n
{int key=*(a+j); ..........................................c2 ..............n 1
//insereaza a[j] in sirul sortat a[1,...,j-1]
i=j-1; ............................................................c3 ..............n P
1
n
while((i>=0)*(*(a+i)>key))..........................c4 .............
Pn j=2 tj
{*(a+i+1)=*(a+i);........................................c5 ..........
Pn j=2 (tj 1)
i=i-1;}...........................................................c6 .......... j=2 (tj 1)
*(a+i+1)=key; }...........................................c7 ...............n 1
c1 , ..., c7 sunt costurile de timp pentru fiecare instructiune.
In coloana
timp punem de c
ate ori este repetata instructiunea; tj reprezinta numarul de
executii ale testului while (comparatia) pentru valoarea fixata j. Timpul de
executie este
T (n) = nc1 + (n 1) (c2 + c3 + c7 c5 c6 ) + (c4 + c5 + c6 )

n
X

tj . (1.1)

j=2

Timpul de executie poate sa depinda de natura datelor de intrare.


In cazul

n care vectorul de intrare este deja sortat crescator, tj = 1 (deoarece pentru


fiecare j, a[0, ..., j 1] este sortat). Timpul de executie
n acest caz (cel mai
favorabil) este
T (n) = n (c1 + c2 + c3 + c4 + c7 ) (c2 + c3 + c4 + c7 ) .

(1.2)

Daca vectorul este sortat


n sens invers (
n ordine descrescatoare) avem cazul
cel mai defavorabil. Fiecare element a [j] este comparat cu fiecare element
din a [0, ..., j 1] si astfel tj = j pentru j = 2, 3, ..., n. Cum
n
X
n (n 1)
n (n + 1)
1,
(j 1) =
,
j=
2
2
j=2
j=2

n
X

deducem ca
T (n) =



c5 c6 2
c4 c5 c6
4
+ +
n + c1 + c2 + c3 + + c7 n(c2 + c3 + c4 + c7 ) .
2
2
2
2
2
2
(1.3)

c

1.4. CORECTITUDINEA ALGORITMILOR

Timpul de executie este are ordinul de marime O (n2 ) .


In general spunem
ca timpul de executie este de ordinul O (f (n)) daca
lim

T (n)
= l, l finita.
f (n)

C
and f (n) = nk , k N spunem ca algoritmul este polinomial. Specificam
faptul ca un algoritm este considerat acceptabil daca este polinomial.

1.4

Corectitudinea algoritmilor

In demonstrarea corectitudinii algoritmilor exista doua aspecte importante


- Corectitudinea partiala: presupun
and ca algoritmul se termina
ntr-un
numar finit de pasi, trebuie demonstrat ca rezultatul este corect.
- Terminarea programului: trebuie demonstrat ca algoritmul se
ncheie
n
timp finit.
Evident, conditiile enumerate mai sus trebuie
ndeplinite pentru orice set
de date de intrare admis de problema.
Modul tipic de lucru consta
n introducerea
n anumite locuri din program
a unor invarianti, care reprezinta relatii ce sunt
ndeplinite la orice trecere
a programului prin acele locuri. De exemplu
n cazul sortarii prin insertie,
invariantii sunt urmatorii:
- dupa ecare executare a ciclului while (corespunzatoare lui i = j 1)
elementele cu indici mai mici sau egali cu j au fost sortate partial.
Ciclul for se termina odata cu ultima executare a ciclului while, c
and
j = n si c
and toate elementele sunt sortate.

1.5

Optimalitatea algoritmilor

Sa presupunem ca pentru o anumita problema am elaborat un algoritm si


am putut calcula timpul sau de executie T (n) . Este natural sa ne punem
problema daca algoritmul este executat
n timpul cel mai scurt posibil sau
exista un alt algoritm cu timpul de executie mai mic. Spunem ca un algoritm
este optim daca raportul dintre timpul sau de executie si timpul de executie
al oricarui alt algoritm care rezolva aceeasi problema are ordinul de marime
O (1) . Problema demonstrarii optimalitatii este deosebit de dificila, mai ales

10

CAPITOLUL 1. ALGORITMI. NOTIUNI GENERALE

datorita faptului ca trebuie sa consideram toti algoritmii posibili si sa aratam


ca ei au un timp de executie superior celui al algoritmului optim.

In cazul algoritmilor de sortare ne propunem sa gasim o margine inferioara


a timpilor de executie. Vom face mai
nt
ai observatia ca numarul total de
instructiuni executate si numarul de comparatii au acelasi ordin de marime.
Multimea comparatiilor poate fi vizualizata cu ajutorul arborilor de decizie.

Intr-un arbore de decizie fiecare nod este etichetat prin ai : aj pentru i si j


din intervalul 1 i, j n unde n este sa numarul de elemente din secventa
de intrare. Fiecare frunza este etichetata cu o permutare ( (1) , ..., (n)) .
Executia algoritmului de sortare corespunde trasarii unui drum elementar de
la radacina arborelui de sortare la o frunza. La fiecare nod intern este facuta o
comparatie
ntre ai si aj . Subarborele st
ang dicteaza comparatiile urmatoare
pentru ai aj iar subarborele drept dicteaza comparatiile urmatoare pentru ai > aj . C
and ajungem la o frunza algoritmul a stabilit ordonarea
a(1) a(2) ... a(n) . Pentru ca algoritmul sa ordoneze adecvat, fiecare
din cele n! permutari de n elemente trebuie sa apara
ntr-o frunza a arborelui de decizie.
In figura (1.1) prezentam arborele de decizie corespunzator
sortarii unei multimi {a1 , a2 , a3 } = {1, 2, 3} .
In functie de datele de intrare, comparatiile efectuate de program reprezinta un drum elementar
n
arborele de decizie ce uneste radacina arborelui cu o frunza. Numarul de
noduri (comparatii) dintr-un astfel de drum elementar este egal cu cel mult
h,
naltimea arborelui.
Teorema 1. Orice arbore de decizie care sorteaza n elemente are naltimea
de ordinul O (n ln n) .
Demonstrtie.
Intruc
at exista n! permutari ale celor n elemente, arborele
trebuie sa aiba n! frunze. Un arbore binar de
naltime h are cel mult 2h
frunze. Deci
n 2h ,
h log2 n! = ln n! log2 e.
Plec
and de la inegalitatea
n! >

 n n
e

obtinem
h n (ln n 1) log2 e,
adica
h = O (n ln n) .

1.5. OPTIMALITATEA ALGORITMILOR

11

Figura 1.1: Arbore de decizie

T
in
and cont de teorema mai sus enuntata, este de presupus ca un algoritm
de sortare optim are timpul de executie de ordinul O (n ln n) . Algoritmul de
sortare prin insertie, av
and timpul de executie de ordinul O (n2 ) , are toate
sansele sa nu fie optimal. Vom da
n cele ce urmeaza un exemplu de algoritm
de sortare optim si anume algoritmul de sortare prin interclasare.
Pentru a avea o imagine intuitiva a procedurii de interclasare, sa consideram ca un pachet cu n carti de joc este
mpartit
n alte 2 pachete asezate
pe masa cu fata
n sus. Fiecare din cele 2 pachete este sortat, cartea cu
valoarea cea mai mica fiind deasupra. Dorim sa amestecam cele doua subpachete
ntr-un singur pachet sortat, care sa ram
ana pe masa cu fata
n jos.
Pasul principal este acela de a selecta cartea cu valoarea cea mai mica dintre
cele 2 aflate deasupra pachetelor (fapt care va face ca o noua carte sa fie
deasupra pachetului respectiv) si de a o pune cu fata
n jos pe locul
n care
se va forma pachetul sortat final. Repetam procedeul p
ana c
and unul din
pachete este epuizat.
In aceasta faza este suficient sa luam pachetul ramas si
sa-l punem peste pachetul deja sortat
ntorc
and toate cartile cu fata
n jos.
Din punct de vedere al timpului de executie, deoarece avem de facut cel mult
n/2 comparatii, timpul de executie pentru procedura de interclasare este de
ordinul O (n) .
Procedura de interclasare este utilizata ca subrutina pentru algoritmul de

12

CAPITOLUL 1. ALGORITMI. NOTIUNI GENERALE

sortare prin interclasare care face apel la tehnica divide si stapaneste, dupa
cum urmeaza:
Divide:
Imparte sirul de n elemente ce urmeaza a fi sortat
n doua
subsiruri de c
ate n/2 elemente.
Stapaneste: Sorteaza recursiv cele doua subsiruri utiliz
and sortarea prin
interclasare.
Combina: Interclaseaza cele doua subsiruri sortate pentru a produce
rezultatul final.
Descompunerea sirului
n alte doua siruri ce urmeaza a fi sortate are loc
p
ana c
and avem de sortat siruri cu unul sau doua componente. Prezentam
mai jos programul scris
n C + +.
In program, functia sort sorteaza un
vector cu maximum 2 elemente, functia intecl interclaseaza 2 siruri sortate
iar dist implementeaza strategia divide si stapaneste a metodei studiate.
#include<iostream.h>
/****************************/
void sort(int p,int q, int n, int *a) {
int m;
if(*(a+p)>*(a+q))
{m=*(a+p); *(a+p)=*(a+q); *(a+q)=m;}}
/**********************************/
void intecl(int p, int q, int m, int n, int *a){
int *b,i,j,k;
i=p;j=m+1;k=1;
b=new int[n];
while(i<=m && j<=q)
if(*(a+i)<=*(a+j))
{*(b+k)=*(a+i);i=i+1;k=k+1;}
else
{*(b+k)=*(a+j);j=j+1;k=k+1;}
if(i<=m)
for (j=i;j<=m;j++)
{*(b+k)= *(a+j); k=k+1;}
else for(i=j;i<=q;i++)
{*(b+k)=*(a+i); k=k+1;}
k=1;
for(i=p;i<=q;i++) {*(a+i)=*(b+k); k=k+1;}}
/*************************************/
void dist(int p, int q, int n, int *a){

1.5. OPTIMALITATEA ALGORITMILOR

13

int m;
if((q-p)<=1) sort(p,q,n,a);
else
{m=(p+q)/2; dist(p,m,n,a); dist(m+1,q,n,a);intecl(p,q,m,n,a);
}}
/**************************************/
void main(void){
int n; *a,i;
cout<<n=; cin>>n;
a=new int[n];
for(i=1;i<=n;i++)
{cout<<a[<<i<<]=; cin>>*(a+i-1);}
dist(0,n-1,n,a);
for(i=1;i<=n;i++)
cout<<a[<<i-1<<]=<<a[i-1];}

In continuare sa calculam T (n), numarul aproximativ de comparatii efectuat de algoritm. Succesiv, problema se descompune
n alte doua probleme,
fiecare referindu-se la n/2 elemente. Urmeaza interclasarea elementelor, care
necesita un numar de n/2 comparatii. Avem deci
n n
+ , T (0) = 0.
T (n) = 2T
2
2
Pentru
nceput sa consideram n = 2k , k N. Rezulta (efectu
and implicit
un rationament prin inductie):




T 2k = 2T 2k1 + 2k1 = 2 2T 2k2 + 2k2 + 2k1 = ...



= 2p T 2kp + p2k1 = 2p 2T 2kp1 + 2kp1 + p2k1 =

= 2p+1 T 2kp1 + (p + 1) 2k1 = T (0) + k2k1 = k2k1 .

Pentru un numar natural oarecare n, fie k astfel


nc
at sa avem 2k n < 2k+1 .
Rezulta


(1.4)
k2k1 = T 2k T (n) < T 2k+1 = (k + 1) 2k .
Cum k = O (ln n) , din (1.4) rezulta ca
T = O (n ln n) ,
deci algoritmul de sortare prin interclasare este optim.

14

1.6

CAPITOLUL 1. ALGORITMI. NOTIUNI GENERALE

Existenta algoritmilor

Problema existentei algoritmilor a stat


n atentia matematicienilor
nca
nainte
de aparitia calculatoarelor. Un rol deosebit
n aceasta teorie l-a jucat matematicianul englez Alan Turing (1912-1954), considerat parintele inteligentei
artificiale.
Numim problema nedecidabila o problema pentru care nu poate fi elaborat un algoritm. Definirea matematica a notiunii de algoritm a permis
detectarea de probleme nedecidabile. C
ateva aspecte legate de decidabilitate
sunt urmatoarele:
- Problema opririi programelor: pentru orice program si orice valori de
intrare sa se decida daca programul se termina.
- Problema echivalentelor programelor: sa se decida pentru oricare doua
programe daca sunt echivalente (produc aceeasi iesire pentru aceleasi date
de intrare).

Capitolul 2
Tipuri de structuri de date
2.1

Generalit
ati

Structurile de date reprezinta modalitati n care datele sunt dispuse n memoria calculatorului sau sunt pastrate pe discul magnetic. Structurilor de date
sunt utilizate
n diferite circumstante ca de exemplu:
Memorarea unor date din realitate;
Instrumente ale programatorilor;
Modelarea unor situatii din lumea reala.
Cele mai des utilizate structuri de date sunt tablourile, listele, stivele,
cozile, arborii, tabelele de dispersie si grafurile.

2.2

Liste

O lista liniara este o structura de date omogena, secventiala formata din


elemente ale listei. Un element (numit uneori si nod) din lista contine o
informatie speci ca si eventual o informatie de legatura. De exemplu,
n
lista echipelor de fotbal
nscrise
ntr-un campionat, un element (ce reprezinta
o echipa) poate contine urmatoarele informatiie specifice: nume, numar de
puncte, numar de goluri
nscrise si numar de goluri primite. Fiecare din aceste
informatii poate reprezenta o cheie care poate fi utilizata pentru comparatii
si inspectii. De exemplu lu
and drept cheie numarul de puncte si golaverajul
se poate face clasificarea echipelor.
15

16

CAPITOLUL 2. TIPURI DE STRUCTURI DE DATE

Pozitia elementelor din lista defineste ordinea elementelor (primul element, al doilea, etc). Continutul listei se poate schimba prin:
- adaugarea de noi elemente la sf
arsitul listei;
- inserarea de noi elemente
n orice loc din lista;
- stergerea de elemente din orice pozitie a listei;
- modi carea unui element dintr-o pozitie data.
Printre operatiile care schimba structura listei vom considera si initializarea
unei liste ca o lista vida.
Alte operatii sunt operatiile de caracterizare. Ele nu modifica structura
listelor dar furnizeaza informatii despre ele. Dintre operatiile de caracterizare
vom mentiona
n cele ce urmeaza:
- localizarea elementului din lista care satisface un anumit criteriu;
- determinarea lungimii listei.
Pot fi luate
n considerare si operatii mai complexe, ca de exemplu:
- separarea unei liste
n doua sau mai multe subliste
n functie de
ndeplinirea
unor conditii;
- selectia elementelor dintr-o lista, care
ndeplinesc unul sau mai multe
criterii, ntr-o lista noua;
- crearea unei liste ordonate dupa valorile creascatoare sau descrescatoare
ale unei chei;
- combinarea a doua sau mai multor liste prin concatenare (alipire) sau
interclasare.
Spatiul din memorie ocupat de lista poate fi alocat
n doua moduri: prin
alocare secventiala sau prin alocare nlantuita.

2.2.1

Liste alocate secvential

In acest caz nodurile ocupa pozitii succesive


n memorie. Acest tip de alocare este
nt
alnit c
and se lucreaza cu tablouri (vectori). Avantajul alocarii
secventiale este dat de faptul ca accesul la oricare din nodurile listei este direct. Dezavantajul consta
n faptul ca operatiile de adaugare, eliminare sau
schimbare de pozitie a unui nod necesita un efort mare de calcul dupa cum
s-a vazut
n algoritmii de sortare prezentati mai
nainte.

2.2.2

Liste alocate
nl
antuit

Exista doua feluri de alocare


nlantuita: alocare simplu nlantuita si alocare
dublu nlantuita (figura 2.1). Alocarea
nlantuita poate fi efectuata static

2.2. LISTE

17

Figura 2.1: Liste simplu si dublu


nlantuite

(utiliz
and vectori) sau dinamic.
In acest din urma caz (de care ne vom ocupa

n cele ce urmeaza), se utilizeaza o zona de memorie numita HEAP (movila,


gramada).
In C + + variabilele pastrate
n HEAP (cum ar fi de exemplu
nodurile listei), se aloca atunci c
and doreste programatorul, cu ajutorul operatorului new, iar zona se elibereaza, tot la dorinta acestuia prin operatorul
delete.
In cazul alocarii dinamice, nodul unei liste este o structura care
contine alaturi de informatie si adresa nodului urmator (pentru liste simplu

nlantuite) sau adresele nodului urmator si a celui precedent


n cazul listelor
dublu
nlantuite. Dupa cum se va vedea din exemplele ce urmeaza, principala problema
n cazul operatiilor cu liste o reprezinta redefinirea legaturilor
(adreselor) c
and se sterge sau se insereaza un element.
Liste simplu
nl
antuite
Mai jos prezentam proceduri (functii) de creare (prin insertie repetata) si
parcurgere a listelor precum si procedurile de stergere (eliminare) a unui
nod si de inserare a unui nod imediat dupa alt nod ce contine o anumita
informatie.
#include<iostream.h>
//introducem structura de nod al unei liste

18

CAPITOLUL 2. TIPURI DE STRUCTURI DE DATE

struct nod {int inf; nod *adr ;};


//declararea functiilor de creare, parcurgere, eliminare si inserare
nod *creare(void);
void parcurge(nod *prim);
nod *elimina(nod *prim, int info);
nod *inserare dupa(nod* prim, int info, int info1);
//functia principala
/***********************************************/
void main(void) {
int info, info1; nod *cap;
cap=creare();
parcurge(cap);
cout<<Spuneti ce nod se elimina;
cin>>info;
cap= elimina(cap, info);
parcurge(cap);
cout<<Spuneti ce valoare se insereaza si dupa cine<<endl;
cin>>info1;
cin>>info;
inserare dupa(cap,info,info1);
parcurge(cap);}
//functia de creare a listei
/********************************************/
nod *creare(void)
{ nod *prim,*p,*q; int inf; char a;
//crearea primului element al listei
prim=new nod;
cout<< Introduceti prim->inf <<endl;
cin>>inf;
prim->inf=inf;
prim->adr=NULL;
q=prim;
/*pana cand se decide ca operatia este gata, se creaza elementele urmatoare*/
cout<<Gata?[d/n]<<endl;
cin>>a;
while (a!=d) {
p=new nod;

2.2. LISTE

19

cout<<p->inf <<endl;
cin>>inf;
/*se creaza un nou nod*/
p->inf=inf ;
p->adr=NULL;
/*se stabileste legatura dintre nodul anterior si nodul nou creat*/
q->adr=p;
q=p;
cout<<Gata?[d/n]<<endl;
cin>>a;}
return prim;}
/********************************************/
/*urmeaza procedura de parcurgere a listei*/
void parcurge(nod *cap)
{nod *q;
if(!cap) {cout<<Lista vida; return;}
cout<<Urmeaza lista<<endl;
for(q=cap;q;q=q->adr)
cout<<q->inf<< ;}
/****************************/
/*urmeaza procedura de eliminare a nodului ce contine o anumita informatie*/
nod *elimina(nod* prim, int info)
{ nod *q,*r;
/*se studiaza mai intai cazul cand trebuie eliminat primul nod*/
while((*prim).inf==info)
{q=(*prim).adr; delete prim; prim=q;}
/*se studiaza cazul cand informatia cautata nu este in primul nod*/
q=prim;
while(q->adr)
{ if(q->adr->inf==info)
/*in cazul in care a fost gasit un nod cu informatia ceruta, acesta se
elimina iar nodul anterior este legat de nodul ce urmeaza*/
{ r=q->adr; q->adr=r->adr; delete r;}
else q=q->adr;}
return prim;}
/*************************************/

20

CAPITOLUL 2. TIPURI DE STRUCTURI DE DATE

/*functia de inserare a unui nod ce contine o anumita informatie dupa


un nod ce contine o informatie data*/
nod *inserare dupa(nod*prim, int info, int info1)
{nod *c, *d;
c=prim;
while(c->adr&&(c->inf !=info)) c=c->adr;
d=new nod; d->inf=info1;
if(!c->adr)
/*daca nici-un nod nu contine informatia data, noul nod se insereaza
dupa ultimul element al listei*/
{d->adr=NULL; c->adr=d;}
/* daca au fost depistate noduri ce contin informatia data noul element
se insereaza dupa primul astfel de nod*/
else {d->adr=c->adr; c->adr=d;}
return prim;}
Ca o ilustrare a utilitatii listelor liniare simplu inlantuite vom prezenta

n cele ce urmeaza o procedura de adunare si


nmultire a doua polinoame
de o variabila. Un polinom va fi reprezentat printr-o lista, un nod al listei
corespunz
and unui monom. Informatia din nodul listei (monom) va contine
gradul monomului si coeficientul. Pentru a efectua produsul polinoamelor
vom crea cu functia prod o noua lista,
n fiecare nod al noii liste fiind produsul a doua monoame (fiecare dintre aceste monoame apartin
and altui polinom). Cu o functie numita canonic, daca doua noduri (monoame) din lista
nou creata (lista produs) au acelasi grad, coeficientul unuia din monoame
este
nlocuit cu suma coeficientilor iar coeficientul celuilalt cu 0. Cu functia
elimina stergem apoi nodurile (monoamele) care contin coeficientul 0.
Pentru a aduna doua polinoame, vom efectual mai
nt
ai concatenarea lor
(ultimul element din lista ce reprezinta primul polinom va fi legata de primul
element din lista ce reprezinta al doilea element). Aplic
and apoi functiile
canonic si elimina obtinem polinomul suma. Prezentam programul mai jos:
#include<iostream.h>
struct nod{int grad; int coef; nod *adr ;};
/************************/
//declararea functiilor utilizate
nod *creare(void);
void parcurge(nod *prim);
void canonic (nod *prim);

2.2. LISTE
nod* concatenare(nod *prim1, nod*prim2);
nod *elimina(nod* prim, int info);
nod* prod(nod *prim1, nod* prim2);
/***********************/
//functia principala
void main(void){
nod *cap, *cap1,*suma,*produs,*prim;
cap=creare();
cout<<Listeaza primul polinom<<endl;
parcurge(cap);
cap1=creare();
cout<<Listeaza al doilea polinom<<endl;
parcurge(cap1);
cout<<Produsul polinoamelor<<endl;
cout<<*****************;
produs=prod(cap,cap1);
canonic(produs);
produs=elimina(produs,0);
parcurge(produs);
prim=concatenare(cap,cap1);
cout<<Polinoamele concatenate<<endl;
parcurge(prim);
canonic(prim);
cout<<Suma polinoamelor<<endl;
suma=elimina(prim,0);
parcurge(suma);}
/**********************/
//functia de creare a unui polinom
nod *creare(void){
nod *prim,*p,*q;int coef,grad;char a;
prim=new nod;
cout<< Introduceti prim->grad<<endl;
cin>>grad;
prim->grad=grad;
cout<< Introduceti prim->coef <<endl;
cin>>coef;
prim->coef=coef;
prim->adr=NULL;

21

22

CAPITOLUL 2. TIPURI DE STRUCTURI DE DATE


q=prim;
cout<<Gata?[d/n]<<endl;
cin>>a;
while (a!=d){
p=new nod;
cout<<p->grad<<endl;
cin>>grad;
p->grad=grad;
cout<<p->coef<<endl;
cin>>coef;
p->coef=coef;
p->adr=NULL;
q->adr=p;
q=p;
cout<<Gata?[d/n]<<endl;
cin>>a;}
return prim;}
/**************************/
//functia de parcurgere a unui polinom
void parcurge(nod *cap){
nod *q;
if(!cap)cout<<Polinom vid;
return;
cout<<Urmeaza polinomul<<endl;
for(q=cap;q;q=q->adr)
cout<<+(<<(*q).coef<<)<<X<<(*q).grad;}
/*********************************/
//functia care efectueaza produsul a doua polinoame
nod* prod(nod* prim1, nod* prim2)
{nod *p,*q,*r,*cap,*s;
cap=new nod;
cap->grad=1;cap->coef=0;cap->adr=NULL;
s=cap;
for(p=prim1;p;p=p->adr)
for(q=prim2;q;q=q->adr)
{r=new nod;r->coef=p->coef*q->coef;
r->grad=p->grad+q->grad;r->adr=NULL;
s->adr=r;s=r;}

2.2. LISTE

23

return cap;}
/*************************/
/*functia care aduna coeficientii a doua monoame de acelasi grad si
atribuie valoarea 0 coeficientului unuia din monoamele adunate*/
void canonic(nod *prim){
nod *p,*q;
for (p=prim;p;p=p->adr)
{q=p->adr;
while(q) if(p->grad==q->grad)
{p->coef+=q->coef;q->coef=0;q=q->adr;}}return;}
/******************************/
/*functia care elimina monoamele ale caror coeficienti au o valoare data*/
nod *elimina(nod* prim, int info)
{nod *q,*r;
if((*prim).coef==info)
q=(*prim).adr; delete prim; prim=q;
q=prim;
while(q->adr)
if(q->adr->coef==info)
{r=q->adr; q->adr=r->adr; delete r;}
else q=q->adr;
return prim;}
/******************************/
//functia de concatenare a doua monoame
nod* concatenare(nod *prim1, nod*prim2)
nod *q,*r;
for(q=prim1;q;q=q->adr) r=q;
r->adr=prim2;
return prim1;
Liste dublu
nl
antuite

In continuare vom prezenta un program


n cadrul caruia se creeaza o lista
dublu
nlantuita, se parcurge direct si invers si se elimina nodurile ce contin
o informatie data. Structura nod va contie trei c
ampuri: unul (inf) este
informatia nodului, al doilea (urm) este pointerul care indica adresa nodului
urmator iar al treilea (ant) este pointerul care indica adresa nodului anterior.

24

CAPITOLUL 2. TIPURI DE STRUCTURI DE DATE

Vom introduce un nou tip de variabila, numit lista const


and dintr-o structura formata din doua variabile de tip nod (cele doua noduri reprezent
and
primul si ultimul element al listei dublu
nlantuite). Functia creare permite crearea unei liste dublu
nlantuite. Functia parcurg d, al carui argument este primul element al listei, permite parcurgerea directa (plec
and
de la primul element si ajung
and la ultimul) a listei. Functia parcurg i al
carei argument este ultimul nod al listei realizeaza parcurgerea listei
n sens
invers. Functia elimin d ale carei argumente sunt o variabila de tip lista
si o variabila de tip int, parcurge direct lista dubla si elimina nodurile ce
contin argumentul de tip
ntreg de c
ate ori le
nt
alneste. Proprietatea pe
care o au listele duble de a putea fi parcurse
n ambele sensuri este folosita
de functia sortin pentru a sorta prin insertie, dupa valorile din c
ampul inf,
elementele listei, valoarea
ntoarsa de functie fiind lista sortata (mai precis
primul si ultimul element al listei sortate).
#include<iostream.h>
struct nod{int inf; nod *urm; nod *ant;};
typedef struct{nod *p;nod *u;} lista;
//declararea functiilor utilizate
lista creare(void);
void parcurg i(nod*prim);
void parcurg d(nod*prim);
lista elimin d(lista list,int info);
nod*sortin(lista list);
/********************************/
//functia principala
void main(void)
{int info;
nod *prim;lista list;
list= creare();
parcurg d(list.p);
parcurg i(list.u);
cout<<Spuneti ce nod se elimina<<endl;
cout<<list.p->inf ;
cin>>info;
list=elimin d(list,info);
parcurg d(list.p);
parcurg i(list.u);
prim=sortin(list);

2.2. LISTE
cout<<Urmeaza lista sortata<<endl;
parcurg d(prim);}
/**********************************/
//functia de creare
lista creare(void)
{nod *prim,*p,*q;int inf;char a; lista val;
prim=new nod;
cout<< Introduceti prim->inf <<endl;
cin>>inf;
prim->inf=inf;prim->urm=NULL;prim->ant=NULL;
q=prim;cout<<Gata?[d/n]<<endl;
cin>>a;
while (a!=d)
{p=new nod;
cout<<p->inf <<endl;
cin>>inf;
p->inf=inf ;p->urm=NULL;p->ant=q;
q->urm=p;
q=p;
cout<<Gata?[d/n]<<endl;
cin>>a;}
val.p=prim;
if(!prim->urm) val.u=prim;
else val.u=p;
return val;}
/******************************/
//functia de parcurgere directa
void parcurg d(nod *prim)
{nod *q;
if(!prim){cout<<Lista vida;return;}
cout<<Urmeaza lista directa<<endl;
for(q=prim;q;q=q->urm)
cout<<q->inf<< ;}
/******************************/
//functia de parcurgere inversa
void parcurg i(nod *ultim)
{nod *q;
if(!ultim){cout<<Lista vida;return;}

25

26

CAPITOLUL 2. TIPURI DE STRUCTURI DE DATE


cout<<Urmeaza lista inversa<<endl;
for(q=ultim;q;q=q->ant)
cout<<q->inf<< ;}
/**********************************/
/*functia de eliminare a nodurilor ce contin o informatie data*/
lista elimin d(lista list, int info){
nod *q,*r,*p;
while(list.p->inf==info)
if (list.p==list.u) {list.p=NULL;list.u=NULL;return list;}
else
{q=list.p->urm;q->ant=NULL; delete list.p; list.p=q;}
q=list.p;
while(q->urm)
{if(q->urm==list.u)
{if (q->urm->inf==info)
{r=q->urm;
list.u=q;q->urm=NULL;delete r;}
return list;}
if(q->urm->inf==info)
{p=q;r=q->urm; q->urm=r->urm;
q->urm->ant=p; delete r;}
else q=q->urm;}
list.u=q;return list;}
/****************************/
//functia de sortare prin insertie
nod* sortin(lista list){
nod*s,*r,*p=list.p,*q;int m;
if(!list.p) cout<<Lista vida<<endl;
else
for(q=p->urm;q;q=q->urm)
{for(s=q->ant;s;s=s->ant)
{if (!s->ant) break;
if(q->inf>=s->ant->inf ) break;}
if(q->inf<s->inf )
{m=q->inf;
for(r=q;!(r==s);r=r->ant)
r->inf=r->ant->inf;s->inf=m;}}
return list.p;}

2.3. STIVE

2.3

27

Stive

Stiva este o lista pentru care singurele operatii permise sunt:


- adaugarea unui element
n stiva;
- eliminarea, vizitarea sau modificarea ultimului element introdus
n stiva.
Stiva functioneaza dupa principiul ultimul intrat primul iesit - Last In
First Out (LIFO).

In cazul alocarii dinamice (de care ne vom ocupa


n cele ce urmeaza), elementele (nodurile) stivei sunt structuri
n componenta carora intra si adresa
nodului anterior.

In programul de mai jos dam functiile push de adaugare


n stiva a unei

nregistrari si pop de extragere. Cu ajutorul lor construim o stiva folosind


functia creare.
#include<iostream.h>
struct nod{int inf; nod*ant;};
nod*stiva;
/*********************************/
//functia de adaugare in stiva a unei noi inregistrari
nod*push(nod *stiva, int info)
{nod *r;
r=new nod;
r->inf=info;
if(!stiva)r->ant=NULL;
else r->ant=stiva;
stiva=r;return stiva;}
/*********************************/
//functia de extragere din stiva a ultimului nod
nod *pop(nod*stiva)
{nod *r;
if(!stiva)
cout<<Stiva vida <<endl;
else
{r=stiva;stiva=stiva->ant;delete r;}
return stiva;}
/************************************/
/*functia de parcurgere a stivei in ordine inversa (de la ultimul nod intrat
la primul)*/
void parcurge(nod*stiva)

28

CAPITOLUL 2. TIPURI DE STRUCTURI DE DATE


{nod*r=stiva;
if(!stiva) cout<<Stiva vida<<endl;
else {cout<<Urmeaza stiva<<endl;
while(r){cout<<r->inf<< ;r=r->ant;}}
return;}
/************************************/
/*functia de creare a stivei prin adaugari si eliminari*/
nod* creare()
{char a;int info;nod*stiva;
cout<<Primul nod<<endl;
cout<<stiva->inf ;
cin >>info;
stiva=new nod;
stiva->inf=info;
stiva->ant=NULL;
a=a;
while(!(a==t)){
cout<<Urmatoarea operatie[a/e/t]<<endl;
/* t=termina; a=adauga nod; e=elimina nod;*/
cin>>a;
switch(a)
{case t: break;
case a:cout<<Stiva->inf <<endl;
cin>>info;
stiva=push(stiva,info);break;
default:stiva=pop(stiva);break;}}
return stiva;}
/******************************/
//functia principala
void main()
{stiva=creare();
parcurge(stiva);}

2.4

Liste de tip coad


a

O coada este o lista pentru care toate inserarile sunt facute la unul din capete
iar toate stergerile (consultarile, modificarile) sunt efectuate la celalalt capat.

2.4. LISTE DE TIP COADA

29

Coada functioneaza pe principiul primul intrat primul iesit - First In First


Out (FIFO).

In cazul alocarii dinamice, elementele (nodurile) cozii sunt structuri


n
componenta carora intra si adresa nodului urmator.

In programul de mai jos dam functiile pune de adaugare


n coada a unei

nregistrari si scoate de extragere. Cu ajutorul lor construim o coada folosind


functia creare. Vom reprezenta coada printr-o structura coada care contine
primul si ultimul nod al cozii.
#include<iostream.h>
//urmeaza structura de tip nod
struct nod{int inf;nod*urm;};
//urmeaza structura de tip coada
typedef struct{nod *p;nod *u;} coada;
coada c;
/***********************************/
//functia de adaugare in coada
coada pune(coada c,int n)
{nod *r;
r=new nod;r->inf=n;r->urm=NULL;
if(!c.p)
{c.p=r;c.u=r;}
else{c.u->urm=r;c.u=r;} return c;}
/*********************************/
//functia de extragere din coada
coada scoate(coada c)
{nod *r;
if(!c.p) cout<<Coada vida<<endl;
else
{cout <<Am scos <<c.p->inf<<endl;
r=c.p;c.p=c.p->urm;delete r;return c;
/***********************************/
//functia de listare (parcurgere) a cozii
void parcurge(coada c)
{nod *r=c.p;
cout<<Urmeaza coada<<endl;
while(r)
{cout<<r->inf<< ;
r=r->urm;}

30

CAPITOLUL 2. TIPURI DE STRUCTURI DE DATE


cout <<endl;}
/***********************************/
//functia de creare a cozii
coada creare()
{char a;int info;coada c;
cout<<Primul nod<<endl;
cout<<c.p->inf <<endl;
cin >>info;
c.p=new nod;
c.p->inf=info;
c.p->urm=NULL;
c.u=c.p;
a=a;
while((a==s)+(a==a)){
cout<<Urmatoarea operatie;
cout<<[a=pune nod/s=scoate/t=termina]<<endl;
cin>>a;
switch(a)
{case s: c=scoate(c);break;
case a:cout<<c.p->inf <<endl;cin>>info;
c=pune(c,info);break;
default:break;}}
return c;}
/**********************************/
//functia principala
void main()
{c=creare();
parcurge(c);}

2.5

Grafuri

Un graf orientat G este o pereche (V, E) unde V este o multime finita iar E
este o relatie binara pe V . Multimea V se numeste multimea varfurilor iar
multimea E se numeste multimea arcelor lui G ((u, v) E, u V, v V ).

Intr-un graf neorientat G = (V, E) multimea muchiilor este constituita


din perechi de v
arfuri neordonate ({u, v} E, u V, v V ) . Daca (u, v) este
un arc dintr-un graf orientat G = (V, E) spunem ca (u, v) este incident din

2.5. GRAFURI

31

sau pleaca din v


arful u si este incident n sau intra n v
arful v. Despre
(u, v) sau {u, v} spunem ca sunt incidente v
arfurilor u si v. Daca (u, v) sau
{u, v} reprezinta un arc (muchie)
ntr-un graf spunem ca v
arful v este adiacent v
arfului u. (Pentru simplitate folosim notatia (u, v) si pentru muchiile
grafurilor neorientate.)

Figura 2.2: Exemple de grafuri

In figura (2.2) prezentam un graf orientat si un graf neorientat. Adiacenta


grafurilor se pune
n evidenta cu ajutorul unei matrice de adiacenta. De
exemplu matricea de adiacenta a grafului orientat din figura este

0
1
2
3

0
0
0
0
0

1
1
1
0
1

2
0
1
0
0

3
1
0 .
0
0

0, 1, 2, 3 sunt etichetele v
arfurilor grafului considerat. Daca (u, v) {0, 1, 2, 3}
{0, 1, 2, 3} este un arc al grafului punem valoarea 1 pentru elementul aflat pe
linia u si coloana v a matricei.
In caz contrar punem 0.

32

CAPITOLUL 2. TIPURI DE STRUCTURI DE DATE


Matricea de adiacenta a grafului neorientat este

0
1
2
3

0
0
1
0
1

1
1
0
1
1

2
0
1
0
0

3
1
1 .
0
0

Daca {u, v} este o muchie a grafului punem valoarea 1 pentru elementul aflat
pe linia u si coloana v a matricei.
In caz contrar punem 0. Observam ca
matricea de adiacenta a unui graf neorientat este simetrica.
Gradul unui v
arf al unui graf neorientat este numarul muchiilor incidente
acestuia.
Intr-un graf orientat, gradul exterior al unui v
arf este numarul
arcelor care pleaca din el iar gradul interior al unui v
arf este numarul arcelor
ce intra
n el. Suma gradului exterior si a gradului interior este gradul
v
arfului.
Un drum de lungime k de la un v
arf u la un v
arf u0
ntr-un graf G = (V, E)
este un sir de v
arfuri (v0 , v1 , v2 , ..., vk1 , vk ) astfel
nc
at u = v0 , u0 = vk si
(vi1 , vi ) E pentru i = 1, 2, ..., k. Drumul contine v
arfurile v0 , v1 , ..., vk1 , vk
si muchiile(arcele) (v0 , v1 ) , (v1 , v2 ) , ..., (vk1 , vk ) .
Lungimea unui drum este numarul de muchii (arce) din acel drum. Un
drum este elementar daca toate v
arfurile din el sunt distincte.
Prezentam
n continuare un program scris
n C care stabileste, pe baza
matricei de adiacenta daca
ntre doua v
arfuri ale grafului exista un drum.
Ideea pe care se bazeaza programul este urmatoarea: daca
ntre doua v
arfuri
i si j exista un drum, atunci oricare ar fi v
arful k,
ntre i si k exista un drum
daca si numai daca (i, k) este arc al grafului sau
ntre j si k exista un drum.

In final programul afiseaza o matrice


n care elementul de pe linia i si coloana
j ia valoarea 1 daca exista un drum de la i la j si valoarea 0 daca nu exista.
Urmeaza programul:
# include <stdio.h>
# define maxcol 20
# define maxlin 20
unsigned char i,j,k,n,x[maxcol][maxlin],y[maxcol][maxlin];
void main(void)
{printf(Dimensiunea matricii n=);scanf(%d,&n);
for(i=0;i<n;i++)
for(j=0;j<n;j++)

2.5. GRAFURI

33

{printf( x[%d][%d]=,i,j);
scanf(%d,&x[i][j]);y[i][j]=x[i][j];}
j=0;while(j<n)
{
i=0;
while(i<n)
{if(y[i][j]!=0)
{k=0;
while(k<n)
{y[i][k]=y[i][k]||y[j][k]; k + +; }; };
i++;};
j++;};
for(i=0;i<n;i++)
for(j=0;j<n;j++)
printf(y[%d][%d]=%d ,i,j,y[i][j]);}
Pentru graful orientat din figura (2.2) se obtine matricea

0
1
2
3

0
0
0
0
0

1
1
1
0
1

2
1
1
0
1

3
1
0 .
0
0

In cazul grafurilor neorientate, daca x1 = xr si nici-o muchie nu este


parcursa de doua ori (adica nu apar perechi de muchii alaturate de forma
(xi xi+1 ), (xi+1 , xi ), drumul (x1 , x2 , ...., xr = x1 ) se numeste ciclu. Daca muchiile ciclului sunt arce orientate avem un ciclu
ntr-un graf orientat.
Un graf neorientat este conex daca pentru fiecare doua v
arfuri u, v exista
un drum (u, ..., v) de la u la v. Daca un graf nu este conex, atunci el are r 2
componente conexe care sunt subgrafuri conexe maximale ale lui G
n raport
cu relatia de incluziune a multimilor. Numarul de v
arfuri |V (G)| al unui graf
se numeste ordinul grafului iar numarul muchiilor |E (G)| reprezinta marimea
grafului.
Un graf (neorientat) conex care nu contine cicluri se numeste arbore. Dam

n continuare c
ateva teoreme de caracterizare a arborilor:
Teorema 2. Fie G = (V, E) un graf neorientat de ordin n 3. Urmatoarele
conditii sunt echivalente:
a) G este un graf conex fara cicluri;

34

CAPITOLUL 2. TIPURI DE STRUCTURI DE DATE

b) G este un graf fara cicluri maximal (daca i se adauga lui G o muchie,


atunci apare un ciclu);
c) G este un graf conex minimal (daca se sterge o muchie a lui G, graful
rezultat nu mai este conex).
Demonstratie.a) b). Fie G conex si fara cicluri. Fie u, v din V astfel
ca (u, v) E. Cum graful este conex, exista un drum (v, ...., u) de la v la
u. Adaug
and si muchia (u, v) , graful G0 = (V, E (u, v)) va contine ciclul
(v, ..., u, v) .
b) c) Daca G nu ar fi conex,
n virtutea proprietatii de maximalitate,
adaug
and o noua muchie ce uneste doua v
arfuri din doua componente conexe
diferite nu obtinem un ciclu, ceea ce contrazice b). Asadar G este conex. Sa
presupunem mai departe prin absurd ca e E si G00 = (V, E {e}) este
conex. Atunci exista un drum ce uneste extremitatile lui e
n G, deci G
contine un ciclu, ceea ce contrazice din nou b).
c) a). Daca G ar contine un ciclu si e ar fi o muchie a acestui ciclu,
atunci G00 = (V, E {e}) ram
ane conex, ceea ce contrazice c).
Teorema 3. Orice arbore G = (V, E) de ordinul n, are n 1 muchii.
Demonstratie. Mai
nt
ai vom demonstra ca G contine cel putin un v
arf de
gradul 1 (v
arfuri terminale, frunze). Sa presupunem prin absurd ca gradul
d (v) 2 pentru orice v V.
In acest caz sa consideram
n G un drum
D de lungime maxima si sa notam cu x o extremitate a acestui drum. Cu
presupunerea facuta, v
arful x ar avea gradul cel putin 2, deci
n virtutea maximalitatii lui D trebuie sa fie adiacent cel putin altui v
arf din D, form
andu-se
astfel un ciclu
n G. Apare o contradictie.
Acum proprietatea ca G are n 1 muchii rezulta usor prin inductie. Ea
este adevarata pentru n = 1. Presupunem ca este adevarata pentru toti
arborii de ordin cel mult n 1 si fie G un arbore de ordin n. Daca x este un
nod terminal al lui G, atunci G0 cu V (G0 ) = V {x} este si el un arbore de
ordinul n 1 si conform ipotezei de inductie va avea n 2 muchii. Rezulta
ca G are n 1 muchii.
Teorema 4. Fie G un graf de ordinul n 3. Urmatoarele conditii sunt
echivalente:
a) G este un graf conex fara cicluri;
d) G este un graf fara cicluri cu n 1 muchii;
e) G este conex si are n 1 muchii;
f ) Exista un unic drum ntre orice doua varfuri distincte ale lui G.
Demonstratie.a) d). Avem a) (b si teorema 2) d).

2.6. ARBORI BINARI

35

d) a). Presupunem prin absurd ca G nu este conex. Adaug


and un
numar de muchii care sa uneasca elemente din diverse componente conexe
ale grafului putem obtine un graf conex fara cicluri cu ordinul mai mare ca
n 1. Se ajunge la o contradictie (
n virtutea teoremei 2), deci G este conex.
a) e). Avem a) (c si teorema 2) e).
e) a). Presupunem prin absurd ca G are cicluri. Extrag
and
n mod
convenabil unele muchii, se ajunge astfel la un graf conex fara cicluri, de
ordin n cu mai putin de n1 muchii. Se obtie astfel o contradictie
n virtutea
teoremei 3.
a) f ). Rezulta imediat
n virtutea definitiilor.
Din teoremele 2 si 4 obtinem sase caracterizari diferite ale arborilor :
(a) (f ) .

2.6

Arbori binari

Ne vom ocupa
n continuare de studiul arborilor binari deoarece acestia constituie una din cele mai importante structuri neliniare
nt
alnite
n teoria
algoritmilor.
In general, structura de arbore se refera la o relatie de ramnificare la nivelul nodurilor, asemanatoare aceleia din cazul arborilor din natura.

In cele ce urmeaza vom introduce


ntr-o alta maniera notiunea de arbore.
Arborii sunt constituiti din noduri interne (puncte de ramnificare) si
noduri terminale (frunze). Fie V = {v1 , v2 , ...} o multime de noduri interne
si B = {b1 , b2 , ...} o multime de frunze. Definim inductiv multimea arborilor
peste V si B :
De nitie.
a) Orice element bi B este un arbore. bi este de asemenea radacina
unui arbore.
b) Daca T1 , ..., Tm , m 1 sunt arbori cu multimile de noduri interne si
frunze disjuncte doua cate doua iar v V este un nou nod, atunci (m + 1)
- tuplul T = hv, T1 , ..., Tm i este un arbore. Nodul v este radacina arborelui,
(v) = m este gradul arborelui iar Ti , i = 1, ..., m sunt subarbori ai lui T .
C
and reprezentam grafic un arbore radacina este deasupra iar frunzele
sunt dedesupt (figura (2.3)).
Vom preciza termenii folositi atunci c
and ne referim
n general la arbori.
Fie T un arbore cu radacina v si av
and subarborii Ti , 1 i m. Fie wi =
root (Ti ) radacina subarborelui Ti . Atunci wi este ul numarul i al lui v iar v
este tatal lui wi . De asemenea wi este fratele lui wj , i, j = 1, ..., m. Notiunea

36

CAPITOLUL 2. TIPURI DE STRUCTURI DE DATE

Figura 2.3: Exemplu de arbore binar

de descendent (ascendent) indica


nchiderea tranzitiva si reflexiva a relatiei
de fiu (tata).
Nivelul (sau adancimea) unui nod v al unui arbore T este definit astfel:
daca v este radacina lui T atunci nivel (v, T ) = 0. Daca v nu este radacina
lui T atunci pentru un anumit i, v apartine subarborelui Ti . Vom pune
nivel (v, T ) = 1 + nivel (v, Ti ). Vom omite al doilea argument din suma c
and
contextul o va cere (Ti = ) .

Inaltimea h (T ) a unui arbore T este definita dupa cum urmeaza: h (T ) =


max {nivel (b, T ) ; b este f runza lui T } .
In exemplul din figura nivel (v3 ) =
1, nivel (v4 ) = 2, nivel (b5 ) = 3 si h (T ) = 3.
Un arbore T este un arbore binar daca toate nodurile interne ale lui T sunt
radacini ale unor subarbori de gradul 1 sau 2. Un arbore T este complet daca
toate nodurile interne ale sale sunt radacini ale unor subarbori de gradul 2.
Arborele binar din figura este un arbore complet. Un arbore complet binar
cu n noduri interne are n + 1 frunze. Primul respectiv al doilea subarbore
este numit subarborele stang respectiv drept

2.6.1

Parcurgerea arborilor binari

In cazul arborilor binari, informatiile pot fi stocate


n frunze sau
n nodurile

2.6. ARBORI BINARI

37

interne. Fiecare nod al arborelui binar este o structura care contine alaturi
de informatia specifica si adresele nodurilor fiu st
ang respectiv drept (figura
(2.4)).

Figura 2.4: Exemplu de arbore binar cu precizarea legaturilor

Pentru a accesa informatiile pastrate de un arbore, trebuie sa-l exploram


(parcurgem). Cum orice arbore binar are trei componente (o radacina, un
subarbore st
ang si un subarbore drept),
n mod natural apar trei metode de
parcurgere a arborelui:
- Parcurgere
n preordine (rsd): se viziteaza radacina, se parcurge subarborele st
ang, se parcurge sub arborele drept.
- Parcurgere
n postordine (sdr): se parcurge subarborele st
ang, se
parcurge subarborele drept, se viziteaza radacina.
- Parcurgere simetric
a sau
n inordine(srd): se parcurge subarborele
st
ang, se viziteaza radacina, se parcurge subarborele drept. Aplic
and aceste
definitii arborelui din figura obtinem v1 v2 b1 v4 b4 b5 v3 b2 b3 , pentru parcurgerea

n preordine, b1 b4 b5 v4 v2 b2 b3 v3 v1 pentru parcurgerea


n postordine iar pentru
parcurgerea
n inordine b1 v2 b4 v4 b5 v1 b2 v3 b3 .
Vom prezenta
n cele ce urmeaza un program C ++ cu functiile de creare,
parcurgere si stergere a unui arbore. Pentru utilizarea nodurilor unui arbore binar a fost definita o structura cu urmatoarele c
ampuri: info - c
ampul

38

CAPITOLUL 2. TIPURI DE STRUCTURI DE DATE

informatie,
n acest program de tip
ntreg, st - adresa nodului descendent
st
ang, dr - adresa nodului descendent drept. Programul realizeaza prelucrarea arborelui prin urmatoarele functii:
a) Functia creare pentru crearea arborelui efectueaza operatiile:
- aloca memoria necesara unui nod;
- citeste informatia nodului si afiseaza mesaje de invitatie pentru crearea
nodurilor descendente (st
ang si drept);
- daca informatia introdusa este un numar
ntreg diferit de 0, se apeleaza recursiv functia pentru crearea subarborelui st
ang stoc
andu-sa adresa
de legatura pentru accesul la descendenti. Urmeaza apoi apelul recursiv
pentru crearea subarborelui drept;
- daca informatia introdusa este 0, atunci nodului i se atribuie valoarea
N U LL.
Functia
ntoarce adresa nodului creat.
b) Functia rsd pentru parcurgerea
n preordine efectueaza operatiile:
- prelucreaza (afiseaza) radacina;
- trece la descendentul st
ang apel
andu-se recursiv pentru parcurgerea
subarborelui st
ang;
- trece la descendentul drept apel
andu-se recursiv pentru parcurgerea
subarborelui drept.
c) Functia srd pentru parcurgerea
n inordine efectueaza operatiile:
- trece la descendentul st
ang apel
andu-se recursiv pentru parcurgerea
subarborelui st
ang;
- prelucreaza (afiseaza) radacina;
- trece la descendentul drept apel
andu-se recursiv pentru parcurgerea
subarborelui drept.
d) Functia sdr pentru parcurgerea
n postordine efectueaza operatiile:
- trece la descendentul st
ang apel
andu-se recursiv pentru parcurgerea
subarborelui st
ang;
- trece la descendentul drept apel
andu-se recursiv pentru parcurgerea
subarborelui drept;
- prelucreaza (afiseaza) radacina.
e) Functia sterge pentru stergerea arborelui efectueaza operatiile:
- se sterge subarborele st
ang: se apeleaza recursiv stergerea pentru descendentul st
ang;
- se sterge subarborele drept: se apeleaza recursiv stergerea pentru descendentul drept;
- se sterge nodul curent;

2.6. ARBORI BINARI

39

Urmeaza programul.
#include <iostream.h>
typedef struct nod {int info; struct nod *st; struct nod *dr;}arbore;
arbore *rad; int n;
// Se declara functiile
/***********************************/
arbore *creare();
void srd(arbore *rad);
void rsd(arbore *rad);
void sdr(arbore *rad);
void sterge(arbore *rad);
/*******************************/
// Functia principala
void main() {
cout<<Radacina=;
rad=creare();
cout<<Urmeaza arborele parcurs in inordine<<endl;
srd(rad);
cout<<Urmeaza arborele parcurs in preordine<<endl;
rsd(rad);
cout<<Urmeaza arborele parcurs in postordine<<endl;
sdr(rad);
sterge(rad);}
/*******************************/
// Functia de creare a arborelui
arbore *creare( )
{arbore *p; cin>>n;
if (n!=0)
{p=new arbore;
p->info=n;cout<<p->info<<->st ;p->st=creare( );
cout<<p->info<<->dr ;p->dr=creare( );return p;}
else
p=NULL;return p;}
/**********************************/
// Functia de parcurgere in inordine
void srd(arbore *rad)
{if (rad!=NULL)
{srd(rad->st);

40

CAPITOLUL 2. TIPURI DE STRUCTURI DE DATE

cout<<rad->info<< ;
srd(rad->dr);}}
/*********************************/
// Functia de parcurgere in postordine
void sdr(arbore *rad)
{if (rad!=NULL)
{sdr(rad->st);
sdr(rad->dr);cout<<rad->info<< ;}}
/************************************/
// Functia de parcurgere in preordine
void rsd(arbore *rad)
{if(rad!=NULL)
{cout<<rad->info<< ;
rsd(rad->st);
rsd(rad->dr);}}
/**********************************/
// Functia de stergere
void sterge(arbore *rad)
{ if (rad!=NULL){sterge(rad->st);sterge(rad->dr);
cout<<S-a sters<<rad->info<<endl;
delete rad;}};
Vom prezenta mai departe o varianta nerecursiva de traversare a unui
arbore
n inordine folosind o stiva auxiliara a. Schematic, algoritmul se
prezinta astfel:
p=radacina; a=NULL;
do {
while(p) { ap /*pune nodul p al arborelui in stiva*/;
p=p->st;}
if(!a) break;
else pa/*scoate p din stiva*/;
viziteaza p; p=p->adr;
} while(p||a);
Ideea algoritmului este sa salvam nodul curent p
n stiva si apoi sa
traversam subarborele st
ang; dupa aceea scoatem nodul p din stiva,
l vizitam
si apoi traversam subarborele drept.
Sa demonstram ca acest algoritm parcurge un arbore binar de n noduri

n ordine simetrica folosind inductia dupa n. Pentru aceasta vom demonstra

2.6. ARBORI BINARI

41

urmatorul rezultat: dupa o intrare


n ciclul do, cu p0 radacina unui subarbore cu n noduri si stiva a contin
and nodurile a[1],...,a[m], procedura va
traversa subarborele
n chestiune
n ordine simetrica iar dupa parcurgerea
lui stiva va avea
n final aceleasi noduri a[1],...,a[m]. Afirmatia este evidenta pentru n = 0. Daca n > 0, fie p=p0 nodul arborelui binar la intrarea

n setul de instructiuni al ciclului do. Pun


and p0
n stiva, aceasta devine
a[1],...,a[m],p0 iar noua valoare a lui p este p=p0 st. Acum subarborele
st
ang are mai putin de n noduri si conform ipotezei de inductie subarborele
st
ang va fi traversat
n inordine, dupa parcurgere stiva fiind a[1],...,a[m],p0 .
Se scoate p0 din stiva (aceasta devenind a[1],...,a[m]), se viziteaza p=p0 si
se atribuie o noua valoare lui p adica p=p0 dr. Acum subarborele drept
are mai putin de n noduri si conform ipotezei de inductie se va traversa
arborele drept av
and
n final
n stiva nodurile a[1],...,a[m].
Aplic
and propozitia de mai sus, se intra
n ciclul do cu radacina arborelui
si stiva vida, si se iese din ciclu cu arborele traversat si stiva vida.
Urmeaza programul.
#include <iostream.h>
typedef struct nod {int inf; nod *st; nod *dr;}arbore;
arbore *rad;int n;
typedef struct nods{arbore *inf; nods*ant;}stiva;
typedef struct{ arbore *arb; stiva *stiv;} arbstiv;
/******************************/
//Functia de punere in stiva
stiva *push(stiva*a, arbore *info)
{stiva *r;
r=new stiva;
r->inf=info;
if(!a)r->ant=NULL;
else r->ant=a;
a=r;return a;}
/*******************************/
//Functia de scoatere din stiva si de vizitare a nodului
arbstiv pop(stiva *a)
{arbstiv q;stiva *r;
if(!a)
cout<<Stiva vida <<endl;
else
{q.arb=a->inf;r=a;

42

CAPITOLUL 2. TIPURI DE STRUCTURI DE DATE


cout<<(a->inf )->inf<< ;
a=a->ant;delete r;q.stiv=a;}
return q;}
/************************************/
//Functia de creare a arborelui
arbore *creare()
{arbore *p; ;cin>>n;
if (n!=0)
{p=new arbore;
p->inf=n;cout<<p->inf<<->st ;p->st=creare( );
cout<<p->inf<<->dr ;p->dr=creare( );return p;}
else
p=NULL;return p;}
/*************************************/
//Functia principala care realizeaza traversarea in inordine
void main()
{stiva *a; arbore *p;arbstiv q;
cout<<Radacina<<endl;
p=creare();
a=NULL;
do{
while (p) {a=push(a,p);p=p->st;}
if (!a) break;
else {q=pop(a);p=q.arb;a=q.stiv;}
p=p->dr;}
while(p||a); }

2.7

Algoritmul lui Huffman

Algoritmul de codificare (compresie, compactare) Hu man poarta numele


inventatorului sau, David Huffman, profesor la MIT. Acest algoritm este
ideal pentru compresarea (compactarea, arhivarea) textelor si este utilizat
n
multe programe de compresie.

2.7. ALGORITMUL LUI HUFFMAN

2.7.1

43

Prezentare preliminar
a

Algoritmul lui Huffman apartine familiei de algoritmi ce realizeaza codific


ari
cu lungime variabil
a. Aceasta
nseamna ca simbolurile individuale (ca
de exemplu caracterele
ntr-un text) sunt
nlocuite de secvente de biti (cuvinte de cod) care pot avea lungimi diferite. Astfel, simbolurilor care
se
nt
alnesc de mai multe ori
n text (fisier) li se atribuie o secventa mai
scurta de biti
n timp ce altor simboluri care se
nt
alnesc mai rar li se
atribuie o secventa mai mare. Pentru a ilustra principiul, sa presupunem
ca vrem sa compactam urmatoarea secventa :AEEEEBEEDECDD. Cum
avem 13 caractere (simboluri), acestea vor ocupa
n memorie 13 8 = 104
biti. Cu algoritmul lui Huffman, fisierul este examinat pentru a vedea care
simboluri apar cel mai frecvent (
n cazul nostru E apare de sapte ori, D
apare de trei ori iar A, B si C apar c
ate o data). Apoi se construieste (vom
vedea
n cele ce urmeaza cum) un arbore care
nlocuieste simbolurile cu
secvente (mai scurte) de biti. Pentru secventa propusa, algoritmul va utiliza
substitutiile (cuvintele de cod) A = 111, B = 1101, C = 1100, D = 10,
E = 0 iar secventa compactata (codificata) obtinuta prin concatenare va fi
111000011010010011001010. Aceasta
nseamna ca s-au utilizat 24 biti
n loc
de 104, raportul de compresie fiind 24/104 1/4.. Algoritmul de compresie
al lui Huffman este utilizat
n programe de compresie ca pkZIP, lha, gz,
zoo, arj.

2.7.2

Coduri prefix. Arbore de codificare

Vom considera
n continuare doar codificarile
n care nici-un cuv
ant nu este
prefixul altui cuv
ant. Aceste codificari se numesc codific
ari prefix. Codificarea prefix este utila deoarece simplifica at
at codificarea (deci compactarea)
c
at si decodificarea. Pentru orice codificare binara a caracterelor; se concateneaza cuvintele de cod reprezent
and fiecare caracter al fisierului ca
n
exemplul din subsectiunea precedenta.
Decodificarea este de asemenea simpla pentru codurile prefix. Cum nici
un cuv
ant de cod nu este prefixul altuia,
nceputul oricarui fisier codificat
nu este ambiguu. Putem sa identificam cuv
antul de cod de la
nceput, sa

l convertim
n caracterul original, sa-l
ndepartam din fisierul codificat si
sa repetam procesul pentru fisierul codificat ramas.
In cazul exemplului
prezentat mai
nainte sirul se desparte
n
111 0 0 0 0 1101 0 0 10 0 1100 10 10,

44

CAPITOLUL 2. TIPURI DE STRUCTURI DE DATE

secventa care se decodifica


n AEEEBEEDECDD. Procesul de decodificare
necesita o reprezentare convenabila a codificarii prefix astfel
nc
at cuv
antul
initial de cod sa poata fi usor identificat. O astfel de reprezentare poate fi
data de un arbore binar de codificare, care este un arbore complet (adica
un arbore
n care fiecare nod are 0 sau 2 fii), ale carui frunze sunt caracterele
date. Interpretam un cuv
ant de cod binar ca fiind o secventa de biti obtinuta
etichet
and cu 0 sau 1 muchiile drumului de la radacina p
ana la frunza ce
contine caracterul respectiv: cu 0 se eticheteaza muchia ce uneste un nod cu
fiul st
ang iar cu 1 se eticheteaza muchia ce uneste un nod cu fiul drept.
In
figura (2.5) este prezentat arborele de codificare al lui Huffman corespunzator
exemplului nostru. Not
and cu C alfabetul din care fac parte simbolurile
(caracterele), un arbore de codificare are exact |C| frunze, una pentru fiecare
litera (simbol) din alfabet si asa cum se stie din teoria grafurilor, exact |C|1
noduri interne (notam cu |C| , cardinalul multimii C).

Figura 2.5: Exemplu de arbore Huffman

D
andu-se un arbore T , corespunzator unei codificari prefix, este foarte
simplu sa calculam numarul de biti necesari pentru a codifica un fisier.
Pentru fiecare simbol c C, fie f (c) frecventa (numarul de aparitii) lui
c
n fisier si sa notam cu dT (c) ad
ancimea frunzei
n arbore. Numarul de
biti necesar pentru a codifica fisierul este numit costul arborelui T si se

2.7. ALGORITMUL LUI HUFFMAN

45

calculeaza cu formula
COST (T ) =

f (c) dT (c) .

cC

2.7.3

Constructia codific
arii prefix a lui Huffman

Huffman a inventat un algoritm greedy care construieste o codificare prefix optima numita codul Huffman. Algoritmul construieste arborele corespunzator codificarii optime (numit arborele lui Huffman) pornind de jos
n sus. Se
ncepe cu o multime de |C| frunze si se realizeaza o secventa de
|C|1 operatii de fuzionari pentru a crea arborele final.
In algoritmul scris
n
pseudocod care urmeaza, vom presupune ca C este o multime de n caractere si
fiecare caracter c C are frecventa f (c). Asimilam C cu o padure constituita
din arbori formati dintr-o singura frunza.Vom folosi o stiva S formata din
noduri cu mai multe c
ampuri; un c
amp pastreaza ca informatie pe f (c), alt
c
amp pastreaza radacina c iar un c
amp suplimentar pastreaza adresa nodului
anterior (care indica un nod ce are ca informatie pe f (c0 ) cu proprietatea ca
f (c) f (c0 )). Extragem din stiva v
arful si nodul anterior (adica obiectele cu
frecventa cea mai redusa) pentru a le face sa fuzioneze. Rezultatul fuzionarii
celor doua noduri este un nou nod care
n c
ampul informatiei are f (c)+f (c0 )
adica suma frecventelor celor doua obiecte care au fuzionat. De asemenea
n
al doilea c
amp apare radacina unui arbore nou format ce are ca subarbore
st
ang, respectiv drept, arborii de radacini c si c0 . Acest nou nod este introdus

n stiva dar nu pe pozitia v


arfului ci pe pozitia corespunzatoare frecventei
sale. Se repeta operatia p
ana c
and
n stiva ram
ane un singur element. Acesta
va avea
n c
ampul radacinilor chiar radacina arborelui Huffman.
Urmeaza programul
n pseudocod. T
in
and cont de descrierea de mai sus
a algoritmului numele instructiunilor programului sunt suficient de sugestive.
n |C|
SC
cat timp (S are mai mult dec

at un nod)
{ z ALOCA-NOD( )
x stanga[z] EXTRAGE-MIN(S)
y dreapta[z] EXTRAGE-MIN(S)
f (z) f (x) +f (y)
INSEREAZA(S, z) }
returneaz
a EXTRAGE-MIN(S) .

46

CAPITOLUL 2. TIPURI DE STRUCTURI DE DATE

Figura 2.6: Construirea arborelui Huffman

In cazul exemplului deja considerat, avem f (E) = 7, f (D) = 3, f (A) =


f (B) = f (C) = 1. Putem, pentru comoditate sa etichetam frunzele nu cu
simbolurile corespunzatoare, ci cu frecventele lor.
In figura (2.6) prezentam
arborii aflati
n nodurile stivei, dupa fiecare repetare a instructiunilor din
ciclul while. Se pleaca cu o padure de frunze, ordonata descrescator dupa
frecventele simbolurilor si se ajunge la arborele de codificare.
Prezentam mai jos si programul
n C + + pentru algoritmul Huffman de
codificare prefixata.
#include<iostream.h>
#include<string.h>
typedef struct nod{char *symb; nod*st;nod*dr;} arbore;
typedef struct nods{ arbore*rad; nods*ant;int frecv;} stiva;
/***********************************************/
/*Functia de punere a unui nou nod in stiva ordonata dupa frecventa*/
stiva *push(stiva*varf, arbore *a, int f )
{stiva *v,*p,*q;
v=new stiva; v->rad=a;v->frecv=f;v->ant=NULL;
if(!varf )
{varf=v; return varf;}
for(p=varf,q=NULL;(p!=NULL)* (p->frecv<f;q=p,p=p->ant

2.7. ALGORITMUL LUI HUFFMAN


v->ant=p;
if(q!=NULL) q->ant=v;
else {varf=v;}
return varf;}
/************************************************/
//Functia de stergere a varfului stivei
void pop(stiva *
{stiva*r;
r=varf;
varf=varf->ant;
delete r;
return;}
/***********************************************/
//Functia de fuzionare a doi arbori
arbore*fuziune(arbore *p, arbore*q)
{arbore*r;
r=new arbore;
r->symb=\00 ;
r->dr=p;r->st=q;
return r;}
/*********************************************/
//Functia de parcurgere in preordine a arborelui lui Huffman
//si de codificare a frunzelor
void rsd(arbore*rad, char*shot)
{char frunza[60];
for (int i=0;i<60;i++)
frunza[i]=\00 ;
if (rad==NULL)return;
if(rad->symb!=NULL)
cout<<rad->symb<<:<<shot<<endl;
if(shot!=NULL)
strcpy(frunza,shot);
strcat(frunza,0);
rsd(rad->st,frunza);
if(shot!=NULL)
strcpy(frunza,shot);
strcat(frunza,1);
rsd(rad->dr,frunza);}

47

48

CAPITOLUL 2. TIPURI DE STRUCTURI DE DATE


/********************************************/
//Functia de creare a unui arbore constand dintr-o singura
//frunza (radacina) care contine simbolul ce trebuie codificat
arbore *frunza(char *simb)
{arbore*r;r=new arbore;
r->symb=simb;r->st=NULL;r->dr=NULL; return r;}
/********************************************/
//Functia de parcurgere a stivei
void parcurge(stiva*s)
{stiva*rr;rr=s;
if(!rr) cout<<Stiva vida<<endl;
else {cout<<Urmeaza stiva<<endl;
while(rr){
cout<<(rr->rad)->symb<< ;
rr=rr->ant;}}
cout<<endl;return;}
/********************************************/
//Functia principala
void main()
{arbore *r1,*r2;int f1,f2; stiva *vf;
vf=new stiva; vf=NULL;
vf=push(vf,frunza(D),3);
vf=push(vf,frunza(A),1);
vf=push(vf,frunza(B),1);
vf=push(vf,frunza(C),1);
vf=push(vf,frunza(E),7);
parcurge(vf );
cout<<endl;
do
{r1=vf->rad;f1=vf->frecv;pop(vf );
r2=vf->rad;f2=vf->frecv;pop(vf );
vf=push(vf,fuziune(r1,r2),f1+f2);
} while(vf->ant);
cout<<Urmeaza codificarea<<endl;
rsd(vf->rad->st,0);
rsd(vf->rad->dr,1);}

2.7. ALGORITMUL LUI HUFFMAN

2.7.4

49

Optimalitatea algoritmului Huffman

De nitie. Un arbore de codificare T pentru alfabetul C este optim daca


pentru orice alt arbore de codificare T 0 al aceluiasi alfabet avem

COST (T ) =

X
cC

f (c) dT (c) COST (T 0 ) =

f (c) dT 0 (c) .

cC

Vom demonstra
n cele ce urmeaza ca algoritmul Huffman construieste
un arbore de codificare optim.
Lema 1. Fie T un arbore de codi care optim pentru alfabetul C. Daca
f (c) < f (c0 ) atunci dT (c) dT (c0 ) .
Demonstratie. Sa presupunem prin absurd ca avem f (c) < f (c0 ) si
dT (c) < dT (c0 ) . Schimb
and
ntre ele frunzele care contin pe c si c0 obtinem
un nou arbore de codificare cu costul
COST (T ) f (c) dT (c) f (c0 ) dT (c0 ) + f (c) dT (c0 ) + f (c0 ) dT (c) =
= COST (T ) (f (c) f (c0 )) (dT (c) dT (c0 )) < COST (T ) ,
ceea ce contrazice ipoteza de optimalitate.
Lema 2. Fie frecventele minime f1 si f2 corespunzatoare simbolurilor
c1 si c2 din alfabetul C. Atunci exista un arbore de codi care optim n care
frunzele c1 si c2 sunt frati.
Demonstrtie. Fie h
naltimea unui arbore de codificare optim T . Fie
un nod de ad
ancime h 1 si ci si cj fii sai, care evident sunt frunze. Sa
presupunem ca f (ci ) f (cj ) . Conform lemei precedente sau f (ci ) = f1
sau dT (ci ) dT (c1 ) de unde dT (ci ) = dT (c1 ) din alegerea lui .
In ambele
cazuri putem schimba
ntre ele frunzele ci si c1 fara a afecta costul arborelui.
La fel procedam cu c2 si cj si obtinem un arbore de codificare optim
n care
c1 si c2 sunt frati.
Teorema 5. Algoritmul Hu man construieste un arbore de codi care
optim.
Demonstratie (prin inductie dupa n = |C|). Teorema este evidenta pentru n 2. Sa presupunem ca n 3 si fie THuf f arborele construit cu
algoritmul Huffman pentru frecventele f1 f2 ... fn . Algoritmul aduna
frecventele f1 si f2 si construieste nodul corespunzator frecventei f1 + f2 . Fie

50

CAPITOLUL 2. TIPURI DE STRUCTURI DE DATE

0
THuf
imea de frecvente
f arborele construit cu algoritmul Huffman pentru mult
{f1 + f2 , f3 , ..., fn }. Avem

0
COST (THuf f ) = COST THuf
f + f1 + f2 ,
0
deoarece THuf f poate fi obtinut din THuf
nlocuind frunza corespunzatoare
f

frecventei f1 + f2 cu un nod intern av


and ca fii frunzele de frecvente f1 si f2 .
0
De asemenea, conform ipotezei de inductie THuf
f este un arbore de codificare
optim pentru un alfabet de n 1 simboluri cu frecventele f1 + f2 , f3 , ..., fn .
Fie Topt un arbore de codificare optim satisfac
and lema anterioara, adica
frunzele de frecvente f1 si f2 sunt frati
n Topt . Fie T 0 arborele obtinut din
Topt prin
nlocuirea frunzelor de frecvente f1 si f2 si a tatalui lor cu o singura
frunza de frecventa f1 + f2 . Atunci

COST (Topt ) = COST (T 0 ) + f1 + f2



0
COST THuf
f + f1 + f2 = COST (THuf f ) ,

0
deoarece COST (T 0 ) COST THuf
ie. Rezulta
f conform ipotezei de induct
de aici
COST (Topt ) = COST (THuf f ) .

Capitolul 3
Tehnici de sortare
3.1

Heapsort

De nitie. Un vector A [1..n] este un heap (ansamblu) daca satisface proprietatea de heap :
A [bk/2c] A [k] , 2 k n.
Folosim notatia bc pentru a desemna partea
ntreaga a unui numar real.
Un vector A care reprezinta un heap are doua atribute: lungime[A] este
numarul elementelor din vector si dimensiune-heap[A] reprezinta numarul elementelor heap-ului memorat
n vectorul A. Astfel, chiar daca A[1..lungime[A]]
contine
n fiecare element al sau date valide, este posibil ca elementele urmatoare
elementului A[dimensiune-heap[A]], unde dimensiune-heap[A] lungime[A],
sa nu apartina heap-ului.
Structurii de heap i se ataseaza
n mod natural un arbore binar aproape
complet (figura (3.1)).
Fiecare nod al arborelui corespunde unui element al vectorului care contine
valorile atasate nodurilor. Radacina arborelui este A[1]. Dat fiind un indice i, corespunzator unui nod, se poate determina usor indicii nodului tata,
T AT A(i) si ai fiilor ST AN G(i) si DREP T (i):
indicele T AT A(i)
returneaz
a bi/2c (partea
ntreaga a lui i/2),
indicele ST AN G(i)
returneaz
a 2i,
indicele DREP T (i)
51

52

CAPITOLUL 3. TEHNICI DE SORTARE

Figura 3.1: Exemplu de heap reprezentat sub forma unui arbore binar si sub
forma unui vector

returneaz
a 2i + 1.
Pentru orice nod i diferit de radacina este,
n virtutea definitiei heap-ului,
adevarata proprietatea de heap:
A[T AT A(i)] A[i].
Definim naltimea unui nod al arborelui ca fiind numarul muchiilor celui
mai lung drum ce nu viziteaza tatal nodului si leaga nodul respectiv cu o
frunza. Evident,
naltimea arborelui este
naltimea radacinii.

3.1.1

Reconstituirea propriet
atii de heap

Functia ReconstituieHeap este un subprogram important


n prelucrarea
heap-urilor. Datele de intrare sunt un vector A si un indice i. Atunci c
and se
apeleaza ReconstituieHeap se presupune ca subarborii av
and ca radacini
nodurile ST AN G(i) si DREP T (i) sunt heap-uri. Dar cum elementul A[i]
poate fi mai mic dec
at descendentii sai, este posibil ca acesta sa nu respecte
proprietatea de heap. Sarcina functiei ReconstituieHeap este sa scufunde
n heap valoarea A[i], astfel
nc
at subarborele care are
n radacina
valoarea elementului de indice i, sa devina un heap.

3.1. HEAPSORT

53

Figura 3.2: Efectul functiei ReconstituieHeap

Urmeaza functia scrisa


n pseudocod.
ReconstituieHeap(A,i)
stSTANG(i)
drDREPT(i)
dac
a st dimensiune-heap[A] si A[st]>A[i] atunci
maximst
altfel
maximi
dac
a dr dimensiune-heap[A] si A[dr]>A[i] atunci
maximdr
dac
a maxim6=i atunci
schimb
a A[i]A[maxim]
ReconstituieHeap(A,maxim)

In figura (3.2) este ilustrat efectul functiei ReconstituieHeap.

In figura (3.2, a) este desenata configuratia initiala a heap-ului unde


A[2] nu respecta proprietatea de heap deoarece nu este mai mare dec
at
descendentii sai. Proprietatea de heap este restabilita pentru nodul 2
n
figura (3.2, b) prin interschimbarea lui A[2] cu A[4], ceea ce anuleaza proprietatea de heap pentru nodul 4. Apel
and recursiv ReconstituieHeap(A, 4)
se pozitioneaza valoarea lui i pe 4. Dupa interschimbarea lui A[4] cu A[9]

54

CAPITOLUL 3. TEHNICI DE SORTARE

asa cum se vede


n figura (3.2, c), nodul 4 ajunge la locul sau si apelul recursiv ReconstituieHeap(A, 9) nu mai gaseste elemente care nu
ndeplinesc
proprietatea de heap.

3.1.2

Constructia unui heap

Functia ReconstituieHeap poate fi utilizata de jos n sus pentru transformarea vectorului A[1..n]
n heap.
Cum toate elementele subsirului A [bn/2c + 1..n] sunt frunze, ele pot fi
considerate heap-uri formate din c
ate un element. Functia ConstruiesteHeap pe care o prezentam
n continuare traverseaza restul elementelor si
executa functia ReconstituieHeap pentru fiecare nod
nt
alnit. Ordinea
de prelucrare a nodurilor satisface cerinta ca subarborii, av
and ca radacina
descendenti ai nodului i sa formeze heap-uri
nainte ca ReconstituieHeap
sa fie executat pentru aceste noduri.
Urmeaza,
n pseudocod functia ConstruiesteHeap:
dimensiune-heap[A]lungime[A]
pentru i blungime[A]/2c,1 executa
ReconstituieHeap(A,i).
Figura (3.2) ilustreaza modul de aplicare a functiei ConstruiesteHeap.

In figura se vizualizeaza structurile de date (heap-urile)


n starea lor anterioara apelarii functiei ReconstituieHeap. (a) Se considera vectorul A
cu 10 elemente si arborele binar corespunzator. Se observa ca variabila de
control i a ciclului
n momentul apelului functiei ReconstituieHeap(A,i)
indica nodul 5. (b) reprezinta rezultatul; variabila de control i a ciclului
indica acum nodul 4. (c)-(e) vizualizeaza iteratiile succesive ale ciclului pentru din ConstruiesteHeap. Se observa ca atunci c
and se apeleaza functia
ReconstituieHeap pentru un nod dat, subarborii acestui nod sunt deja
heap-uri. (f) prezinta heap-ul final.

3.1.3

Algoritmul heapsort

Algoritmul de sortare heapsort


ncepe cu apelul functiei ConstruiesteHeap
n scopul transformarii vectorului de intrare A[1..n]
n heap. Deoarece
cel mai mare element al vectorului este atasat nodului radacina A[1], acesta
va ocupa locul definitiv
n vectorul ordonat prin interschimbarea sa cu A[n].
Mai departe, excluz
and din heap elementul de pe locul n, si micsor
and cu 1
dimensiune-heap[A], restul de A[1..(n 1)] elemente se pot transforma usor

3.1. HEAPSORT

Figura 3.3: Model de executie a functiei ConstruiesteHeap

55

56

CAPITOLUL 3. TEHNICI DE SORTARE

n heap, deoarece subarborii nodului radacina au proprietatea de heap, cu


eventuala exceptie a elementului ajuns
n nodul radacina. Pentru aceasta se
apeleaza functia ReconstituieHeap(A,1). Procedeul se repeta micsor
and
dimensiunea heap-ului de la n 1 la 2.
Urmeaza, scris
n pseudocod, algoritmul descris de functia Heapsort(A):
ConstruiesteHeap(A),
pentru ilungime[A],2 executa
schimba A[1]A[i]
dimensiune-heap[A]dimensiune-heap[A]-1
ReconstituieHeap(A,i)
Vom scrie programul heapsort si
n C + +. Fata de descrierea de mai

nainte a algoritmului, vor interveni c


ateva mici modificari datorate faptului
ca
n C + + indexarea elementelor unui vector
ncepe de la 0 si nu de la 1.
/**************************************/
#include<iostream.h>
void ReconstituieHeap(int* A, int i, int dimensiune )
{int a,maxim, stang=2*i+1,drept=stang+1;
if (stang<dimensiune&&A[stang]>A[i]) maxim=stang;
else maxim=i;
if (drept<dimensiune&&A[drept]>A[maxim]) maxim=drept;
if(maxim!=i){a=A[i];A[i]=A[maxim];A[maxim]=a;
ReconstituieHeap(A,maxim,dimensiune); }}
/************************************/
void ConstruiesteHeap( int* A, int dimensiune )
{ int i;
for (i = (dimensiune - 2)/2; i >= 0; i)
ReconstituieHeap(A, i, dimensiune);}
/***********************************/
void heapsort( int* A, int dimensiune )
{ int i, temp;
ConstruiesteHeap( A, dimensiune );
for (i = dimensiune-1; i >=1; i) {
temp = A[i]; A[i] = A[0]; A[0]=temp;
dimensiune=dimensiune-1;
ReconstituieHeap( A,0,dimensiune );}}
/***********************************/
void main()
{int N=10;

3.2. COZI DE PRIORITATI

57

int A[10];
A[0]=10;A[1]=8;A[2]=6;A[3]=5;
A[4]=11;A[5]=5;A[6]=17;A[7]=9;A[8]=3;A[9]=21;
heapsort(A,N);
cout<< Sirul sortat (metoda heapsort)<<endl;
for(int i=0;i<N;i++) cout<<A[i]<< ; }
Timpul de executie
Functia ReconstituieHeap consta din comparatii si interschimbari ale de
elemente aflate pe nivele consecutive. Timpul de executie al functiei va fi
deci de ordinul
naltimii arborelui binar asociat care este de ordinul O (ln n)
unde n este numarul de elemente din heap.
Functia ConstruiesteHeap apeleaza functia ReconstituieHeap de n
ori. Deducem de aici ca timpul de executie al functiei ConstruiesteHeap
este de ordinul O(n ln n). Functia heapsort apeleaza o data functia ConstruiesteHeap si de n 1 ori functia ReconstituieHeap. Rezulta de aici
ca timpul de executie al functiei heapsort este O (n ln n) + (n 1)O (ln n) =
O (n ln n) .

3.2

Cozi de priorit
ati

Vom prezenta
n cele ce urmeaza
nca o aplicatie a notiunii de heap: utilizarea
lui sub forma de coada cu prioritati.
Coada cu prioritati este o structura de date care contine o multime S de
elemente, fiecare av
and asociata o valoare numita cheie. Asupra unei cozi cu
prioritati se pot efectua urmatoarele operatii:
Insereaza(S, x) insereaza elementul x
n multimea S.Aceasta operatie
este scrisa
n felul urmator S S {x} .
ExtrageMax(S) elimina si
ntoarce elementul din S av
and cheia cea mai
mare.
O aplicatie a cozilor de prioritati o constituie planificarea lucrarilor pe
calculatoare partajate. Lucrarile care trebuie efectuate si prioritatile relative se memoreaza
ntr-o coada de prioritati. C
and o lucrare este terminata
sau
ntrerupta, functia ExtrageMax va selecta lucrarea av
and prioritatea
cea mai mare dintre lucrarile
n asteptare. Cu ajutorul functiei Insereaza
oric
and se introduce
n coada o sarcina noua.

58

CAPITOLUL 3. TEHNICI DE SORTARE

In mod natural o coada cu prioritati poate fi implementata utiliz


and un
heap. Functia Insereaza(S, x) insereaza un element
n heap-ul S. La prima
expandare a heap-ului se adauga o frunza arborelui. Apoi se traverseaza un
drum pornind de la aceasta frunza catre radacina
n scopul gasirii locului
definitiv al noului element.
Urmeaza instructiunile functiei Insereaza(S,x)
n pseudocod.
dimensiune-heap[S]dimensiune-heap[S]+1
i dimensiune-heap[S]
cat timp i>1 si S[TATA(i)]<cheie executa
S[i] S[TATA(i)]
iTATA[i]
S[i]cheie.
ExtrageMax(S) este asemanatoare functiei Heapsort, instructiunile
sale
n pseudocod fiind:
daca dimensiune-heap[S]<1 atunci
eroare depasire inferioara heap;
max S[1]
S[1] S[dimensiune-heap[S]]
dimensiune-heap[S]dimensiune-heap[S]-1
ReconstituieHeap(S,1)
returneaza max.
Urmeaza scris in C + + un program care implementeaza o coada cu prioritati.
#include<iostream.h>
/*******************************************/
void ReconstituieHeap(int* A, int i, int dimensiune )
{ int a,maxim, stang=2*i+1,drept=stang+1;
if (stang<dimensiune&&A[stang]>A[i]) maxim=stang;
else maxim=i;
if (drept<dimensiune&&A[drept]>A[maxim])
maxim=drept;
if(maxim!=i){a=A[i];A[i]=A[maxim];A[maxim]=a;
ReconstituieHeap(A,maxim,dimensiune); }}
/******************************************/
int ExtrageMax( int* A, int dim)
{int max;
if(dim<0) cout<<Depasire inferioara heap<<endl;
max=A[0];

3.2. COZI DE PRIORITATI


A[0]=A[dim];
ReconstituieHeap( A,0,dim- - );
return max;}
/******************************************/
void Insereaza(int* A, int cheie, int dimensiune )
{int i,tata;
i=dimensiune+1;
A[i]=cheie;tata=(i-1)/2;
while(i>0&&A[tata]<cheie)
{A[i]=A[tata];i=tata;A[i]=cheie;tata=(i-1)/2;} }
/***********************************************/
void main() {
char x,y;int cheie,dimensiune,max, S[100];
cout<<Indicati primul element<<endl;
cin>>max;
S[0]=max;
dimensiune=0;
cout<<Intrerupem?[d/n]<<endl;
cin>>x;
while(x!=d){
cout<<Extragem sau inseram[e/i]<<endl;
cin>>y;
switch (y)
{case e:
max=ExtrageMax( S,dimensiune );
dimensiune=dimensiune-1;
if (dimensiune>=0) {cout<<heap-ul ramas este:<<endl;
for (int i=0;i<=dimensiune;i++) cout<<S[i]<< ;
cout<<endl;
cout<<Elementul maxim este = <<max<<endl;
cout<<dimensiunea<<dimensiune<<endl;}
break;
default:cout<<Introduceti cheia<<endl;
cin>>cheie; Insereaza(S,cheie,dimensiune);
dimensiune=dimensiune+1;
cout<<dimensiunea<<dimensiune<<endl;
cout<<heap-ul este:<<endl;
for (int i=0;i<=dimensiune;i++) cout<<S[i]<< ;

59

60

CAPITOLUL 3. TEHNICI DE SORTARE


cout<<endl;}
cout<<Intrerupem?[d/n]<<endl;
cin>>x;}}

3.3

Sortarea rapid
a

Sortarea rapida este un algoritm care sorteaza pe loc (


n spatiul alocat sirului
de intrare). Cu acest algoritm, un sir de n elemente este sortat
ntr-un
timp de ordinul O (n2 ),
n cazul cel mai defavorabil. Algoritmul de sortare
rapida este deseori cea mai buna solutie practica deoarece are o comportare
medie remarcabila: timpul sau mediu de executie este O (n ln n) si constanta
ascunsa
n formula O (n ln n) este destul de mica.

3.3.1

Descrierea algoritmului

Ca si algoritmul de sortare prin interclasare, algoritmul de sortare rapida


ordoneaza un sir A [p..r] folosind tehnica divide si stapaneste:
Divide : Sirul A [p..r] este rearanjat
n doua subsiruri nevide A [p..q] si
A [q + 1..r] astfel
nc
at fiecare element al subsirului A [p..q] sa fie mai mic sau
egal cu fiecare element al subsirului A [q + 1..r] . Indicele q este calculat de
procedura de partitionare.
St
ap

aneste: Cele doua subsiruri A [p..q] si A [q + 1..r] sunt sortate prin


apeluri recursive ale aalgoritmului de sortare rapida.
Combin
a: Deoarece cele doua subsiruri sunt sortate pe loc, sirul A [p..r] =
A [p..q] A [q + 1..r] este ordonat.
Algoritmul (intitulat QUICKSORT(A, p, r) )
n pseudocod este urmatorul:
QUICKSORT(A, p, r):
//urmeaza algoritmul
daca p < r atunci
q PARTITIE(A, p, r)
QUICKSORT(A, p, q)
QUICKSORT(A, q + 1, r).
Pentru ordonarea sirului A se apeleaza QUICKSORT(A, 1,lungime[A]).
O alta functie a algoritmului este functia PARTITIE(A, p, r) care rearanjeaza pe loc sirul A [p..r] , return
and si indicele q mentionat mai sus:
PARTITIE(A, p, r)
//urmeaza functia

3.3. SORTAREA RAPIDA


x A[p]
i p
j r
cat timp i<j executa
{repeta
jj-1
pana cand A[j]x
repeta
ii+1
pana cand A[i]x
daca i<j atunci
interschimba A[i] A[j]; jj-1}
returneaza j.
Urmeaza programul scris
n C + + :
#include <iostream.h>
/*************************/
int Partitie(int*A, int p, int r) {int x,y,i,j;
x=A[p];
i=p;
j=r;
while(i<j)
{ while(A[j]>x)j- - ;
while (A[i]<x) i+ +;
if(i<j){y=A[i];A[i]=A[j];A[j]=y;j- -;}}
return j;}
/*****************************/
void Quicksort(int *A, int p, int r)
{int q;
if (p<r)
{q= Partitie(A,p,r);
Quicksort(A,p,q);
Quicksort(A,q+1,r); }}
/******************************/
void main()
{int *A,n;
cout<<Introduceti numarul de elemente<<endl;
cin>>n;
A=new int[n];

61

62

CAPITOLUL 3. TEHNICI DE SORTARE


for(int i=0;i<n;i++)
{cout<<A[<<i<<]=;cin>>*(A+i);}
Quicksort(A,0,n-1);
for(i=0;i<n;i++)
cout<< A[<<i<<]=<<A[i];}

3.3.2

Performanta algoritmului de sortare rapid


a

Timpul de executie al algoritmului depinde de faptul ca partitionarea este


echilibrata sau nu.
Cazul cel mai defavorabil
Vom demonstra ca cea mai defavorabila comportare a algoritmului de sortare
rapida apare
n situatia
n care procedura de partitionare produce un vector
de n 1 elemente si altul de 1 element. Mai
nt
ai observam ca timpul
de executie al functiei Partitie este O (n) . Fie T (n) timpul de executie
al algoritmului de sortare rapida. Avem pentru partitionarea amintita mai

nainte, formula de recurenta


T (n) = T (n 1) + O (n) ,
de unde rezulta
T (n) =

n
X

O (k) = O

k=1

n
X
k=1


= O n2 ,

adica, pentru un c > 0 suficient de mare,


T (n) cn2 .

(3.1)

Vom arata prin inductie ca estimarea (3.1) este valabila pentru orice
partitionare.
Sa presupunem ca partitionare produce subvectori de dimensiuni q si nq.
Cu ipoteza de inductie avem

c max

1qn1

T (n) = T (q) + T (n q) + O (n)




q 2 + (n q)2 + O (n) = c n2 2n + 2 + O (n) cn2 .

3.3. SORTAREA RAPIDA

63

Cazul cel mai favorabil


Daca functia de partitionare produce doi vectori de n/2 elemente, algoritmul
de sortare rapida lucreaza mult mai repede. Formula de recurenta
n acest
caz
T (n) = 2T (n/2) + O (n) ,
conduce, dupa cum s-a aratat
n cazul algoritmului de insertie prin interclasare la un timp de executie de ordinul O (n ln n) .
Estimarea timpului de executie mediu
Timpul de executie mediu se definete prin inductie cu formula
n1

1X
(T (q) + T (n q)) + O (n) .
T (n) =
n q=1
Presupunem ca
T (n) an ln n + b,
pentru un a > 0 si b > T (1) .
Pentru n > 1 avem
n1
n1
2X
2X
T (k) + O (n)
(ak ln k + b) + O (n) =
T (n) =
n k=1
n k=1
n1

2a X
2b
=
k ln k + (n 1) + O (n) .
n k=1
n
Tin
and cont de inegalitatea
n1
X

1
1
k ln k n2 ln n n2 ,
2
8
k=1

obtinem


2a 1 2
1 2
2b
T (n)
n ln n n + (n 1) + O (n)
n 2
8
n

a
a 
an ln n n + 2b + O (n) = an ln n + b + O (n) + b n an ln n + b,
4
4
an
sa domine expresia
deoarece valoarea lui a poate fi aleasa astfel
nc
at
4
O (n) + b. Tragem deci concluzia ca timpul mediu de executie a algoritmului
de sortare rapida este O (n ln n) .

64

3.4

CAPITOLUL 3. TEHNICI DE SORTARE

Metoda bulelor (bubble method)

Principiul acestei metode de sortare este urmatorul: pornind de la ultimul


element al sirului catre primul, se schimba
ntre ele fiecare element cu cel
anterior, daca elementul anterior (de indice mai mic) este mai mare.
In
felul acesta primul element al sirului este cel mai mic element. Se repeta
procedura pentru sirul format din ultimele n 1 elemente si asa mai departe,
obtin
andu-se
n final sirul de n elemente ordonat. Numarul de comparatii si
deci timpul de executie este de ordinul O (n2 ) .
Urmeaza programul scris
n C + +.
#include <iostream.h>
//returneaza p,q in ordine crescatoare
/*****************************/
void Order(int *p,int *q) {
int temp;
if(*p>*q) {temp=*p; *p=*q; *q=temp; } }
//Bubble sorting
void Bubble(int *a,int n)
{int i,j;
for (i=0; i<n; i++)
for (j=n-1; i<j; j)
Order(&a[j-1],&a[j]);}
//functia principala
void main()
int i,n=10; static int a[] = {7,3,66,3,-5,22,-77,2,36,-12};
cout<<Sirul initial<<endl;
for(i=0; i<n; i++)
cout<<a[i]<< ;cout<<endl;
Bubble(a,n);
cout<<Sirul sortat<<endl;
for(i=0; i<n; i++)
cout<<a[i]<< ;cout<<endl;
//Rezultatele obtinute
* Sirul initial * 7 3 66 3 -5 22 -77 2 36 -12 *
* Sirul sortat * -77 -12 -5 2 3 3 7 22 36 66 *

Capitolul 4
Tehnici de c
autare
4.1

Algoritmi de c
autare

Vom presupune ca
n memoria calculatorului au fost stocate un numar de n

nregistrari si ca dorim sa localizam o anumita nregistrare. Ca si


n cazul
sortarii , presupunem ca fiecare
nregistrare contine un c
amp special, numit
cheie. Colectia tuturor
nregistrarilor se numeste tabel sau sier. Un grup
mai mare de fisiere poarta numele de baza de date.

In cazul unui algoritm de cautare se considera un anumit argument K, iar


problema este de a cauta acea
nregistrare a carei cheie este tocmai K. Sunt
posibile doua cazuri: cautarea cu succes (reusita), c
and
nregistrarea cu cheia
avuta
n vedere este depistata, sau cautarea fara succes (nereusita), c
and
cheia niciunei
nregistrari nu coincide cu cheia dupa care se face cautarea.
Dupa o cautare fara succes, uneori este de dorit sa inseram o noua
nregistrare,
contin
and cheia K
n tabel; metoda care realizeaza acest lucru se numeste
algoritmul de cautare si insertie.
Desi scopul cautarii este de a afla informatia din
nregistrarea asociata
cheii K, algoritmii pe care
i vom prezenta
n continuare, tin
n general cont
numai de cheie, ignor
and celelalte c
ampuri.

In multe programe cautarea este partea care consuma cel mai mult timp,
de aceea folosirea unui bun algoritm de cautare se reflecta
n sporirea vitezei
de rulare a programului.
Exista de asemenea o importanta interactiune
ntre sortare si cautare,
dupa cum vom vedea
n cele ce urmeaza.
65

66

4.1.1

CAPITOLUL 4. TEHNICI DE CAUTARE

Algoritmi de c
autare secvential
a (pas cu pas)

Algoritmul S (cautare secventiala). Fiind dat un tabel de


nregistrari
R1 , R2 , ..., Rn , n 1, av
and cheile corespunzatoare K1 , K2 , ..., Kn , este cautata

nregistrarea corespunzatoare cheii K. Vom mai introduce o


nregistrare fictiva Rn+1 cu proprietatea ca valoarea cheii Kn+1 este at
at de mare
nc
at K nu
va capata nici-o data aceasta valoare (punem formal Kn+1 = ). Urmeaza
algoritmul scris
n pseudocod
i0
Executa
ii+1
Cat timp in si K 6= Ki
Daca K = Ki cautarea a avut succes
Altfel, cautarea nu a avut succes.

In cazul
n care cheile sunt ordonate crescator, o varianta a algoritmului
de cautare secventiala este
Algoritmul T (cautare secventiala
ntr-un tabel ordonat):
i0
Executa
ii+1
Cat timp in si K Ki
Daca K = Ki cautarea a avut succes
Altfel, cautarea nu a avut succes.
Sa notam cu pi probabilitatea ca sa avem K = Ki , unde p1 +p2 +...+pn =
1. Daca probabilitatile sunt egale,
n medie, algoritmul S consuma acelasi
timp ca si algoritmul T pentru o cautare cu succes. Algoritmul T efectueaza

nsa
n medie de doua ori mai repede cautarile fara succes.
Sa presupunem mai departe ca probabilitatile pi nu sunt egale. Timpul
necesar unei cautari cu succes este proportional cu numarul de comparatii,
care are valoarea medie
C n = p1 + 2p2 + ... + 3pn .
Evident, C n ia cea mai mica valoare atunci c
and p1 p2 ... pn , adica
atunci c
and cele mai utilizate
nregistrari apar la
nceput. Daca p1 = p2 =
... = pn = 1/n, atunci
n+1
Cn =
.
2

4.1. ALGORITMI DE CAUTARE

67

O repartitie interesanta a probabilitatilor este legea lui Zipf care a observat ca

n limbajele naturale, cuv


antul aflat pe locul n
n ierarhia celor mai utilizate
cuvinte apare cu o frecventa invers proportionala cu n:
c
c
c
1
1
1
p1 = , p2 = , ..., pn = , c =
, Hn = 1 + + ... + .
1
2
n
Hn
2
n
Daca legea lui Zipf guverneaza frecventa cheilor
ntr-un tabel, atunci
Cn =

n
Hn

1
iar cautarea
ntr-o astfel de circumstanta, este de circa ln n ori mai rapida
2
dec
at cautarea
n cazul general.

4.1.2

C
autarea
n tabele sortate (ordonate)

In cele ce urmeaza vom discuta algoritmi de cautare pentru tabele ale caror
chei sunt ordonate. Dupa compararea cheii date K cu o cheie Ki a tabelului,
cautarea continua
n trei moduri diferite dupa cum K < Ki , K = Ki sau
K > Ki . Sortarea tabelelor (listelor) este recomandabila
n cazul cautarilor
repetate. De aceea
n aceasta subsectiune vom studia metode de cautare
n
tabele ale caror chei sunt ordonate K1 < K2 < ... < Kn . Dupa compararea
cheilor K si Ki
ntr-o tabela ordonata putem avea K < Ki (caz
n care
Ri , Ri+1 , ..., Rn nu vor mai fi luate
n consideratie), K = Ki (
n acest caz
cautarea se termina cu succes) sau K > Ki (caz
n care R1 , R2 , ..., Ri nu vor
mai fi luate
n consideratie).
Faptul ca o cautare, chiar fara succes duce la eliminarea unora din cheile
cu care trebuie comparata K, duce la o eficientizare a cautarii.
Vom prezenta mai departe un algoritm general de cautare
ntr-un tabel
sortat. Fie S = {K1 < K2 < ... < Kn } stocata
ntr-un vector K [1..n], adica
K [i] = Ki si fie o cheie a. Pentru a decide daca a S, comparam a cu un
element al tabelului si apoi continuam cu partea superioara sau cea inferioara
a tabelului. Algoritmul (numit
n cele ce urmeaza algoritmul B) scris
n
pseudocod este:
prim1
ultimn
urmatorun
ntreg
n intervalul [prim,ultim]
executa

68

CAPITOLUL 4. TEHNICI DE CAUTARE

{daca a<K[urmator] atunci ultimurmator-1


altfel primprim+1
urmatorun
ntreg
n intervalul [prim,ultim]}
cat timp a6=K[urmator] si ultim >prim
daca a=K[urmator] atunci avem cautare cu succes
altfel avem cautare fara succes.

In cazul
n care un
ntreg
n intervalul [prim,ultim]=prim spunem
ca avem o cautare liniara.
Daca un
ntreg
n intervalul [prim,ultim]=d(prim + ultim)/2e spunem
ca avem o cautare binara.
Amintim ca folosim notatia bc pentru partea
ntreaga a unui numar,
n
timp ce de are urmatoarea semnificatie

a dac
a a este
ntreg,
dae =
bac + 1 dac
a a nu este
ntreg.
Prezentam
n continuare un proiect pentru cautarea
ntr-o lista ordonata
(cu relatia de ordine lexicografica) de nume, pentru a afla pe ce pozitie se afla
un nume dat. Proiectul contine programele cautare.cpp, fcautare.cpp (
n
care sunt descrise functiile folosite de cautare.cpp) si fisierul de nume scrise

n ordine lexicografica search.dat.


/******************cautare.cpp**********/
#include <stdio.h>
#include <string.h>
#include <io.h>
typedef char Str20[21]; //Nume de lungime 20
extern Str20 a[], cheie; //vezi fcautare.cpp
char DaNu[2], alegere[2];
int marime, unde, cate;
void GetList() {
FILE *fp;
fp=fopen(search.dat,r);
marime=0;
while (!feof(fp)) {
fscanf(fp, s, a[marime]);
marime++;
}
fclose(fp);

4.1. ALGORITMI DE CAUTARE

69

printf(numarul de nume in lista = %d\n00 , marime}


//functii implementate in fcautare.cpp
void GasestePrimul(int, int *);
void GasesteToate(int, int *);
void Binar(int, int, int *);
void main() {
GetList(); // citeste numele din fisier
strcpy(DaNu,d);
while (DaNu[0]==d)
printf( Ce nume cautati? ); scanf(%s, cheie);
//Se cere tipul cautarii
printf( Secventiala pentru (p)rimul, (t)oate, ori (b)inara? );
scanf(%s,alegere);
switch(alegere[0]) {
case t:
GasesteToate(marime,
if (cate>0)
printf(%d aparitii gasite.\n00 , cate);
else
printf( %s nu a fost gasit.\n00 , cheie);
break;
case b:
Binar(0,marime-1,
if (unde>0)
printf( %s gasit la pozitia %d.\n00 , cheie, unde);
else
printf( %s nu a fost gasit.\n00 , cheie);
break;
case p:
GasestePrimul(marime,
if (unde>0)
printf( %s gasit la pozitia %d.\n00 , cheie, unde);
else
printf( %s nu a fost gasit.\n00 , cheie); }
printf( Inca o incercare (d/n)? ); scanf(%s, DaNu);
}
}
/******fcautare.cpp****************************/

70

CAPITOLUL 4. TEHNICI DE CAUTARE


/******************************************
!* Functii de cautare intr-o lista de nume *
!* ce urmeaza a fi folosite de programul Cautare.cpp. *
/*****************************************/
#include <string.h>
#include <stdio.h>
typedef char Str20[21]; //Nume de lungimea 20
Str20 a[100], cheie;
void GasestePrimul(int marime, int *unde) {
// Cautare secventiala intr-o lista de nume pentru
// a afla prima aparitie a cheii.
int iun;
iun=0;
while (strcmp(a[iun],cheie)!=0
if (strcmp(a[iun],cheie)!=0) iun=-1;
*unde = iun+1;
}
void GasesteToate(int marime, int *cate) {
// Cautare secventiala intr-o lista de nume pentru
// a afla toate aparitiile cheii.
int cat, i;
cat=0;
for (i=0; i<marime; i++)
if (strcmp(a[i],cheie)==0) {
printf( %s pe pozitia %d.\n00 , cheie, i + 1);
cat++;
}
*cate = cat;
}
void Binar(int prim, int ultim, int *unde) {
// Cautare binara intr-o lista ordonata pentru o aparitie
// a cheii specificate. Se presupune ca prim<ultim.
int urmator, pr, ul, iun;
pr=prim; ul=ultim; iun=-1;
while (pr <= ul
urmator=(pr+ul) / 2;
if (strcmp(a[urmator],cheie)==0)
iun=urmator;

4.1. ALGORITMI DE CAUTARE

71

else if (strcmp(a[urmator],cheie) > 0)


ul=urmator-1;
else
pr=urmator+1; }
*unde = iun+1;
}

4.1.3

Arbori de decizie asociati c


aut
arii binare

Pentru a
ntelege mai bine ce se
nt
ampla
n cazul algoritmului de cautare
binara, vom construi arborele de decizie asociat cautarii binare.
Arborele binar de decizie corespunzator unei cautari binare cu n
nregistrari
poate fi construit dupa cum urmeaza:
Daca n = 0, arborele este format din frunza [0]. Altfel nodul radacina este
dn/2e , subarborele st
ang este arborele binar construit asemanator cu dn/2e
1 noduri iar subarborele drept este arborele binar construit asemanator cu
bn/2c noduri si cu indicii nodurilor incrementati cu dn/2e . Am avut
n vedere
numai nodurile interne corespunzatoare unei cautari cu succes.
Prezentam mai jos un arbore de cautare binara pentru n = 16 (figura
4.1).

Figura 4.1: Arbore de cautare binara

72

CAPITOLUL 4. TEHNICI DE CAUTARE

Prima comparatie efectuata este K : K8 care este reprezentata de nodul


(8) din figura. Daca K < K8 , algoritmul urmeaza subarborele st
ang iar daca
K > K8 , este folosit subarborele drept. O cautare fara succes va conduce la
una din frunzele numerotate de la 0 la n; de exemplu ajungem la frunza [6]
daca si numai daca K6 < K < K7 .

4.1.4

Optimalitatea c
aut
arii binare

Vom face observatia ca orice arbore binar cu n noduri interne (etichetate


cu (1) , (2) , (3) , ... (n))si deci n + 1 frunze (etichetate cu [0] , [1] , [2] ,
... [n 1] , [n]), corespunde unei metode valide de cautare
ntr-un tabel
ordonat daca parcurs
n ordine simetrica obtinem [0] (1) [1] (2) [2] (3)
... [n 1] (n) [n] . Nodul intern care corespunde
n Algoritmul B lui
urmator[prim,ultim] va fi radacina subarborelui care are pe [ultim] drept
cea mai din dreapta frunza iar pe [prim] drept cea mai din st
anga frunza.
De exemplu
n figura 4.2, a) urmator[0,4]=2, urmator[3,4]=4, pe c
and

n figura 4.2, b) urmator[0,4]=1, urmator[3,4]=4.

Figura 4.2: Arbori de cautare


Vom demonstra
n cele ce urmeaza ca
ntre arborii de decizie asociati
algoritmului B de cautare, cei optimi sunt arborii corespunzatori cautarii
binara. Vom introduce mai
nt
ai doua numere ce caracterizeaza arborele T :

4.1. ALGORITMI DE CAUTARE

73

E (T ) , lungimea drumurilor externe ale arborelui reprezinta suma lungimilor


drumurilor (numarul de muchii) care unesc frunzele arborelui cu radacina iar
I (T ) , lungimea drumurilor interne ale arborelui reprezinta suma lungimilor
drumurilor care unesc nodurile interne cu radacina. De exemplu
n figura
4.2, a) E (T ) = 2 + 2 + 2 + 3 + 3 = 12, I (T ) = 1 + 1 + 2 = 4 iar
n figura
4.2, b) E (T ) = 1 + 2 + 3 + 4 + 4 = 14, I (T ) = 1 + 2 + 3 = 6.
In continuare
ne va fi necesara
Lema 3. Daca T este un arbore binar complet avand N frunze, atunci
E (T ) este minim daca si numai daca toate frunzele lui T se a a cel mult pe
doua nivele consecutive (cu 2q N frunze pe nivelul q 1 si 2N 2q frunze
pe nivelul q, unde q = dlog2 N e , nivelul radacinii ind 0).
Demonstratie. Sa presupunem ca arborele binar T are frunzele u si v (fie
x tatal lor), pe nivelul L iar frunzele y si z pe nivelul l astfel ca L l 2.
Vom construi un alt arbore binar T1 (vezi figura 4.3) transfer
and pe u si v

Figura 4.3: Optimizarea lungimii drumurilor externe


pe nivelul l + 1 ca fii ai lui y; x devine frunza iar y nod intern. Rezulta ca
E (T ) E (T1 ) = 2L (L 1) + l 2 (l + 1) = L l 1 1,
si deci T nu poate avea o lungime a drumurilor externe minima. Deci T are
toate frunzele pe un singur nivel daca N este o putere a lui 2 sau, altfel, pe
doua nivele consecutive q 1 si q, unde q = dlog2 N e .

74

CAPITOLUL 4. TEHNICI DE CAUTARE

Medoda de construire a arborilor binari de decizie asociat algoritmului B


conduce la
Lema 4. Daca 2k1 n < 2k , o cautare cu succes folosind algoritmul
B necesita cel mult k comparatii. Daca n = 2k 1, o cautare fara succes
necesita k comparatii iar daca 2k1 n < 2k 1, o cautare fara succes
necesita sau k 1 sau k comparatii. Aceasta nseamna ca pentru n = 2k 1
toate frunzele arborelui binar asociat algoritmului B sunt pe nivelul k iar
pentru 2k1 n < 2k 1 frunzele se gasesc pe nivelele k 1 si k.
Demonstratie. Lema este adevarata pentru k = 1. Fie k 2 si sa
presupunem (ipoteza de inductie) ca teorema este adevarata pentru orice
2k1 n < 2k . Daca 2k n < 2k+1 vom avea doua cazuri: (I) n este impar,
adica n = 2p + 1; (II) n este par adica n = 2p, unde p N, p 1.
Cazul (I): Radacina arborelui binar T , corespunzator algoritmului B are
eticheta p + 1 iar subarborele st
ang Tl si subarborele drept Tr contin c
ate p
noduri interne. Din relatia
2k 2p + 1 < 2k+1 ,
rezulta ca
2k1 p < 2k .
Aplic
and ipoteza de inductie ambilor subarbori Tl si Tr avem h (Tl ) = h (Tr ) =
k deci h (T ) = k + 1,
naltimea lui T fiind numarul maxim de comparatii
ale cheilor efectuate de algoritmul B
n cazul unei cautari cu succes. Daca
p = 2k 1 toate frunzele lui Tl si Tr se afla pe nivelul k, deci daca n =
2p + 1 = 2k+1 1 toate frunzele lui T se vor afla pe nivelul k + 1. Daca
2k1 p < 2k 1, ceea ce implica 2k n < 2k+1 1, cum (cu ipoteza de
inductie) at
at Tl c
at si Tr au frunzele pe nivelele k 1 sau k, deducem ca T
va avea frunzele pe nivelele k sau k + 1.
Cazul (II):
In acest caz radacina arborelui binar are eticheta p, Tl are
p 1 noduri interne iar Tr are p noduri interne. Cum 2k 2p < 2k+1 rezulta
ca 2k1 p < 2k . Avem de asemenea 2k1 p 1 < 2k
n afara cazului
c
and p = 2k1 si deci n = 2k .
In acest ultim caz arborele T este asemanator
cu arborele din figura 4.1: el are h (T ) = k + 1, N 1 frunze pe nivelul k si
doua frunze pe nivelul k + 1; teorema a fost demonstrata direct
n aceasta
circumstanta (n = 2k ). Ne mai ram
ane sa consideram cazul 2k < n < 2k+1 .
Cu ipoteza de inductie deducem ca h (Tl ) = h (Tr ) = k deci h (T ) = k + 1 iar
algoritmul B necesita cel mult k + 1 comparatii pentru o cautare cu succes.

4.1. ALGORITMI DE CAUTARE

75

Cum n este par, n 6= 2s 1, s 1, avem de aratat ca frunzele lui T se afla


pe nivelele k sau k + 1. Aceasta rezulta din aplicarea ipotezei de inductie la
subarborii Tl si Tr care au p 1 respectiv p noduri interne.
Intr-adevar, cum
p 1 6= 2k 1 rezulta ca frunzele lui Tl se afla pe nivelele k 1 sau k. Pentru
Tr toate frunzele se afla fie pe nivelul k (daca p = 2k 1) fie pe nivelele k 1
sau k. Rezulta ca frunzele lui T se afla pe nivelele k sau k + 1.
Deducem ca numarul de comparatii
n cazul unei cautari (cu sau fara
succes) este de cel mult blog2 N c + 1.
Vom demonstra
n continuare
Lem
a 5. Pentru orice arbore binar complet T este satisfacuta relatia
E (T ) = I (T ) + 2N,
unde N reprezinta numarul de noduri interne al lui T .
Demonstratie. Sa presupunem ca arborele binar T are j noduri interne
si j noduri externe (frunze) la nivelul j; j = 0, 1, ... (radacina este la nivelul
0). De exemplu
n figura 4.2, a) avem (0 , 1 , 2 , ...) = (1, 2, 1, 0, 0, ...) ,
(0 , 1 , 2 , ...) = (0, 0, 3, 2, 0, 0, ...) iar
n figura 4.2, b) avem (0 , 1 , 2 , ...) =
(1, 1, 1, 1, 0, 0, ...) , (0 , 1 , 2 , ...) = (0, 1, 1, 1, 2, 0, 0, ...) .
Consideram functiile generatoare asociate acestor siruri
A (x) =

j x , B (x) =

j=0

j xj ,

j=0

unde numai un numar finit de termeni sunt diferiti de 0. Este valabila relatia
2j1 = j + j , j = 0, 1, ...,
deoarece toate cele j1 noduri interne de la nivelul j 1 au fiecare
n parte
c
ate 2 fii pe nivelul k si numarul total al acestor fii este j + j . Rezulta de
aici ca
A (x) + B (x) =

(j + j ) x = 0 + 0 +

j=0

=1+2

X
j=1

j1 x = 1 + 2x

(j + j ) xj =

j=1

j1 x

j1

= 1 + 2x

j=1

j xj ,

j=0

adica
A (x) + B (x) = 1 + 2xA (x) .

(4.1)

76

CAPITOLUL 4. TEHNICI DE CAUTARE

P
Pentru x = 1 se obtine B (1) = 1 + A
(1),
dar
B
(1)
=
j=0 j este
P
arul de noduri
numarul de frunze ale lui T iar A (1) =
j=0 j este num
interne, deci numarul de noduri interne este cu 1 mai mic dec
at numarul de
nodduri externe. Deriv
and relatia (4.1) obtinem
A0 (x) + B 0 (x) = 2A (x) + 2xA0 (x) ,
B 0 (1) = 2A (1) + A0 (1) .
P
P
0
Cum A (1) = N, A0 (1) =
j=0 jj = E (T ) ,
j=0 jj = I (T ) , B (1) =
deducem relatia
E (T ) = I (T ) + 2N.
(4.2)
Reprezentarea sub forma de arbore binar a algoritmului binar de cautare
B, ne sugereaza cum sa calculam
ntr-un mod simplu numarul mediu de
comparatii. Fie CN numarul mediu de comparatii
n cazul unei cautari reusite
si fie CN0 numarul mediu de cautari
n cazul unei
ncercari nereusite. Avem
CN = 1 +

E (T )
I (T )
, CN0 =
.
N
N +1

(4.3)

Din (4.2) si (4.3) rezulta




1
CN = 1 +
CN0 1.
N

(4.4)

Rezulta ca CN este minim daca si numai daca CN0 este minim, ori dupa
cum am aratat mai
nainte acest lucru se
nt
ampla atunci si numai atunci
c
and frunzele lui T se afla pe cel mult doua nivele consecutive. Cum lemei 2
arborele asociat cautarii binare satisface aceasta ipoteza, am demonstrat:
Teorema 6. Cautarea binara este optima n sensul ca minimizeaza
numarul mediu de comparatii indiferent de reusita cautarii.

4.2

Arbori binari de c
autare

Am demonstrat
n sectiunea precedenta ca pentru o valoare data n, arborele
de decizie asociat cautarii binare realizeaza numarul minim de comparatii
necesare cautarii
ntr-un tabel prin compararea cheilor. Metodele prezentate

n sectiunea precedenta sunt potrivite numai pentru tabele de marime fixa


deoarece alocarea secventiala a
nregistrarilor face operatiile de insertie si

4.2. ARBORI BINARI DE CAUTARE

77

stergere foarte costisitoare.


In schimb, folosirea unei structuri de arbore
binar faciliteaza insertia si stergerea
nregistrarilor, fac
and cautarea
n tabel
eficienta.
De nitie: Un arbore binar de cautare pentru multimea
S = {x1 < x2 < ... < xn }
este un arbore binar cu n noduri {v1 , v2 , ..., vn } . Aceste noduri sunt etichetate
cu elemente ale lui S, adica exista o functie injectiva
CON T IN U T : {v1 , v2 , ..., vn } S.
Etichetarea pastreaza ordinea, adica n cazul n care vi este un nod al
subarborelui stang apartinand arborelui cu radacina vk ,atunci
CON T IN U T (vi ) < CON T IN U T (vk )
iar n cazul n care vj este un nod al subarborelui drept apartinand arborelui
cu radacina vk ,atunci
CON T IN U T (vk ) < CON T IN U T (vj ) .
O definitie echivalenta este urmatoarea : o traversare n ordine simetrica
a unui arbore binar de cautare pentru multimea S reproduce ordinea pe S.
Prezentam mai jos un program de insertie si stergere a nodurilor
ntr-un
arbore binar de cautare.
# include<iostream.h>
# include<stdlib.h>
int cheie;
struct nod{int inf; struct nod *st, *dr;} *rad;
/*********************************************/
void inserare(struct nod**rad)
{if(*rad==NULL)
{*rad=new nod;(*rad)->inf=cheie;
(*rad)->st=(*rad)->dr=NULL;return;}
if(cheie<(*rad)->inf ) inserare(&(*rad)->st);
else if(cheie>(*rad)->inf ) inserare(&(*rad)->dr);
else cout<<cheie<< exista deja in arbore.;}
/************************************************/
void listare(struct nod *rad,int indent)

78

CAPITOLUL 4. TEHNICI DE CAUTARE


{if(rad){listare(rad->st, indent+1);
cheie=indent;
while(cheie) cout<< ;
cout<<rad->inf;
listare(rad->dr,indent+1);}}
/***********************************************/
void stergere(struct nod**rad)
{struct nod*p,*q;
if(*rad==NULL)
{cout<<Arborele nu contine <<cheie<<endl;return;}
if(cheie<(*rad)->inf ) stergere(&(*rad)->st);
if(cheie>(*rad)->inf ) stergere(&(*rad)->dr);
if(cheie==(*rad)->inf )
{if((*rad)->dr==NULL)
{q=*rad;*rad=q->st; delete q;}
else
if((*rad)->st==NULL)
{q=*rad;*rad=q->dr; delete q;}
else
{for(q=(*rad),p=(*rad)->st;p->dr;q=p,p=p->dr);
(*rad)->inf=p->inf;
if((*rad)->st==p)(*rad)->st=p->st;
else q->dr=p->st; delete p;}}}
/*************************************************/
void main()
{rad=new nod;
cout<<Valoarea radacinii este:;cin>>rad->inf;
rad->st=rad->dr=NULL;
do{
cout<<Operatia:Listare(1)/Inserare(2)/Stergere(3)/Iesire(0);
cout<<endl;
cin>>cheie;if(!cheie) return;
switch(cheie)
{case 1:listare(rad,1);cout<<endl; break;
case 2: cout<<inf=;cin>>cheie;inserare(&rad);break;
case 3: cout<<inf=;cin>>cheie;stergere(&rad);break;}
}while(rad);
cout<<Ati sters radacina<<endl;}

4.2. ARBORI BINARI DE CAUTARE

79

Dupa cum se observa din program, ideea insertiei


n arborele binar de
cautare este urmatoarea: daca arborele este vid, se creaza un arbore av
and
drept unic nod si radacina nodul inserat; altfel se compara cheia nodului
inserat cu cheia radacinii. Daca avem cheia nodului inserat mai mica dec
at
cheia radacinii, se trece la subarborele st
ang si se apeleaza recursiv procedura de inserare, altfel se trece la subarborele drept si se apeleaza recursiv
procedura.
Procedura prezentata
n program de stergere a unui nod din arborele binar de cautare este de asemenea recursiva.
In cazul
n care cheia nodului
ce urmeaza a fi sters este mai mica dec
at cheia radacinii, se trece la subarborele st
ang si se apeleaza recursiv procedura de stergere; altfel se trece
la subarborele drept si se apeleaza recursiv procedura de stergere.
In cazul

n care nodul ce urmeaza a fi sters este chiar radacina vom avea mai multe
posibilitati: a) subarborele drept este vid: se sterge radacina iar fiul st
ang
al radacinii devine noua radacina; b) subarborele st
ang este vid: se sterge
radacina iar fiul drept al radacinii devine noua radacina; c) radacina are ambii
fii;
n acest se sterge cel mai din dreapta descendent al fiului st
ang al radacinii
iar informatia (cheia) acestuia
nlocuieste informatia (cheia) radacinii, fiul
st
ang al nodului eliminat devine totodata fiul drept al tatalui nodului eliminat. Daca fiul st
ang al radacinii nu are fiu drept, atunci el este cel eliminat,
informatia lui
nlocuieste informatia radacinii iar fiul lui st
ang devine fiul
st
ang al radacinii (figura 4.4).
Algoritmul de cautare, dupa o anumita cheie,
ntr-un arbore de cautare
este
n esenta urmatorul: comparam cheia
nregistrarii cautate cu cheia
radacinii. Daca cele doua chei coincid cautarea este reusita. Daca cheia

nregistrarii este mai mica dec


at cheia radacinii continuam cautarea
n subarborele st
ang iar daca este mai mare
n subarborele drept.
De foarte multe ori este util sa prezentam arborii binari de cautare ca
n
figura 4.5.
Aici arborele binar de cautare este prezentat ca un arbore complet
n
care informatiile (cheile) sunt stocate
n cele N noduri interne iar informatia
fiecarui nod extern (frunza) este intervalul deschis dintre doua chei consecutive, astfel
nc
at, daca xi , xi+1 sunt doua chei consecutive si vrem sa cautam
nodul ce contine cheia a cu a (xi , xi+1 ) sa fim condusi aplic
and algoritmul
de cautare (
ntr-un arbore binar de cautare) la frunza etichetata cu (xi , xi+1 ) .
Vom arata ca
naltimea medie a unui arbore binar de cautare este de
ordinul O (ln N ) (N este numarul nodurilor interne) si cautarea necesita
n
medie circa 2 ln N 1, 386 log2 N comparatii
n cazul
n care cheile sunt

80

CAPITOLUL 4. TEHNICI DE CAUTARE

Figura 4.4: Stergerea radacinii unui arbore binar de cautare

Figura 4.5: Arbore binar de cautare

4.3. ARBORI DE CAUTARE PONDERATI (OPTIMALI)

81

inserate
n arbore
n mod aleator. Sa presupunem ca cele N ! ordonari posibile ale celor N chei corespund la N ! modalitati de insertie. Numarul de
comparatii necesare pentru a gasi o cheie este exact cu 1 mai mare dec
at
numarul de comparatii efectuate atunci c
and cheia a fost inserata
n arbore.
Not
and cu CN numarul mediu de comparatii pentru o cautare reusita si cu
CN0 numarul mediu de comparatii pentru o cautare nereusita avem
CN = 1 +

C00 + C10 + ... + CN0 1


,
N

pentru ca
nainte de reusita cautarii vom avea cautari nereusite
n multimi
de 0, 1, ..., n 2 sau n 1 elemente. Tin
and cont si de relatia


1
CN = 1 +
CN0 1,
N
deducem
(N + 1) CN0 = 2N + C00 + C10 + ... + CN0 1 .
Scaz
and din aceasta ecuatie urmatoarea ecuatie
N CN0 1 = 2 (N 1) + C00 + C10 + ... + CN0 2
obtinem
(N + 1) CN0 N CN0 1 = 2 + CN0 1 CN0 = CN0 1 +

2
.
N +1

Cum C00 = 0 deducem ca


CN0 = 2HN +1 2,
de unde

4.3



1
2
CN = 2 1 +
HN +1 3
2 ln N.
N
N

Arbori de c
autare ponderati (optimali)

In cele ce urmeaza vom asocia fiecarui element din multimea ordonata S


c
ate o pondere (probabilitate). Ponderile mari indica faptul ca
nregistrarile
corespunzatoare sunt importante si frecvent accesate; este preferabil de aceea

82

CAPITOLUL 4. TEHNICI DE CAUTARE

ca aceste elemente sa fie c


at mai aproape de radacina arborelui de cautare
pentru ca accesul la ele sa fie c
at mai rapid.
Sa analizam
n continuare problema gasirii unui arbore optimal. De exemplu, Fie N = 3 si sa presupunem ca urmatoarele chei K1 < K2 < K3
au probabilitatie p, q respectiv r. Exista 5 posibili arbori binari de cautare
av
and aceste chei drept noduri interne (figura 4.6).

Figura 4.6: Arbori posibili de cautare si numarul mediu de comparatii pentru


o cautare reusita
Obtinem astfel 5 expresii algebrice pentru numarul mediu de comparatii

ntr-o cautare. C
and N este mare, este foarte costisitor sa construim toti
arborii de cautare pentru a vedea care din ei este cel optim. Vom pune de
aceea
n evidenta un algoritm de gasire al acestuia.
Fie S = {K1 < K2 < ... < KN }. Fie pi 0, i = 1, ..., N probabilitatea de
cautare a cheii a = Ki si qj 0, j = 0, 1, ...N probabilitatea de cautare a
cheii a (Kj , Kj+1 ) (punem K0 = si KN +1 = ). Avem deci
N
X
i=1

pi +

N
X

qj = 1.

j=0

(2N + 1) - tuplul (q0 , p1 , q1 , ..., pN , qN ) se numeste distributia probabilitatilor


(ponderilor) de cautare (acces). Fie T un arbore de cautare pentru S, fie iT

4.3. ARBORI DE CAUTARE PONDERATI (OPTIMALI)

83

ad
ancimea (nivelul) nodului intern i (al i - lea nod intern
n ordine simetrica) si fie jT ad
ancimea frunzei j (al (j + 1) lea nod extern sau frunza
(Kj , Kj+1 )).
Sa consideram o cautare a cheii a. Daca a = Ki , vom compara a cu
T
i + 1 elemente din arbore; daca Kj < a < Kj+1 , atunci vom compara a cu
jT elemente din arbore. Asadar
P ON D(T ) =

N
X
i=1

pi iT

N
 X
+1 +
qj jT ,
j=0

este numarul mediu de comparatii pentru o cautare. P ON D(T ) este lungimea


ponderata a drumurilor arborelui T (sau costul lui T relativ la o distributie
data a probabilitatilor de cautare).
Vom considera P ON D (T ) drept indicator de baza pentru eficienta operatiei
de cautare (acces) deoarece numarul asteptat de comparatii
ntr-o cautare
va fi proportional cu P ON D(T ). De exemplu,
n cazul arborelui din figura
4.6 b) (unde
n loc de p, q, r punem p1 , p2 , p3 ) avem
1 = 1, 2 = 23 = 0, 0 = 2, 1 = 3, 2 = 3, 3 = 1
si deci
P ON D(T ) = 2q0 + 2p1 + 3q1 + 3p2 + 3q2 + p3 + q3 .
(Vom omite indicele T c
and este clar din context la ce arbore ne referim.)
De nitie. Arborele de cauare T peste multimea ordonata S cu distributia
ponderilor de cautare (q0 , p1 , q1 , ..., pN , qN ), este optimal daca P ON D (T )
(costul arborelui sau lungimea ponderata a drumurilor arborelui) este minim
n raport cu costurile celorlalti arbori de cautare peste S.
Vom prezenta mai departe un algoritm de construire a unui arbore de
cautare optimal. Fie un arbore de cautare peste S av
and nodurile interne
etichetate cu 1, 2, ..., N (corespunzator cheilor K1 , ..., KN ) si frunzele etichetate
cu 0, 1, ..., N (corespunz
and lui (, K1 ) , (K1 , K2 ) , ..., (KN 1 , KN ) , (KN , )). Un
subarbore al acestuia ar putea avea nodurile interne i + 1, ..., j si frunzele
i, ..., j pentru 0 i, j n, i < j. Acest subarbore este la r
andul sau arbore de cautare pentru multimea cheilor {Ki+1 < ... < Kj }. Fie k eticheta
radacinii subarborelui.
Fie costul acestui subarboreP ON D (i, j)si greutatea sa:
GREU T (i, j) = pi+1 + ... + pj + qi + ... + qj ,

84

CAPITOLUL 4. TEHNICI DE CAUTARE

de unde rezulta imediat ca


GREU T (i, j) = GREU T (i, j 1) + pj + qj , GREU T (i, i) = qi .
Avem relatia
P ON D (i, j) = GREU T (i, j) + P ON D (i, k 1) + P ON D (k, j) .

Intr-adevar, subarborele st
ang al radacini k are frunzele i, i + 1, ..., k 1,
iar subarborele drept are frunzele k, k + 1, ..., j, si nivelul fiecarui nod din
subarborele drept sau st
ang este cu 1 mai mic dec
at nivelul aceluiasi nod
n
arborele de radacina k. Fie C (i, j) = min P ON D (i, j) costul unui subarbore
optimal cu ponderile {pi+1 , ..., pj ; qi , ..., qj } . Rezulta atunci pentru i < j :
C (i, j) = GREU T (i, j) + min (C (i, k 1) + C (k, j)) ,
i<kj

C (i, i) = 0.
Pentru i = j + 1 rezulta imediat ca
C (i, i + 1) = GREU T (i, i + 1) , k = i + 1.
Plec
and de la aceste relatii prezentam mai jos un program, scris
n limbajul C de construire a unui arbore optimal. C
ampul informatiei fiecarui nod
contine un caracter (litera) iar acestea se considera ordonate dupa ordinea
citirii.
/***********************************************/
#include<stdio.h>
#include<stdlib.h>
#include <io.h>
# define N 25
struct nod {char ch; struct nod *st,*dr;} *rd;
char chei[N];
//cheile de cautare se considera ordonate dupa ordinea citirii
int i,nr;
int p[N-1];/*ponderile cheilor*/
int q[N];
/*ponderile informatiilor aflate intre 2 chei consecutive*/
int c[N][N], greut[N][N], rad[N][N];
FILE*f;

4.3. ARBORI DE CAUTARE PONDERATI (OPTIMALI)

85

/****Functia de calcul a greutatii si costului**********/


void calcul()
{int x,min,i,j,k,h,m;
for(i=0;i<=nr;i++)
{greut[i][i]=q[i];
for(j=i+1;j<=nr;j++)
greut[i][j]=greut[i][j-1]+p[j]+q[j];}
for(i=0;i<=nr;i++) c[i][i]=q[i];
for(i=0;i<nr;i++)
{j=i+1;
c[i][j]=greut[i][j];
rad[i][j]=j;}
for(h=2;h<=nr;h++)
for(i=0;i<=nr-h;i++)
{j=i+h;m=rad[i][j-1];
min=c[i][m-1]+c[m][j];
for(k=m+1;k<=rad[i+1][j];k++)
{x=c[i][k-1]+c[k][j];
if(x<min)m=k;min=x;}}
c[i][j]=min+greut[i][j];
rad[i][j]=m;}}
/****Functia de generare a arborelui optimal****/
struct nod *arbore(int i, int j)
{struct nod *s;
if(i==j) s=NULL;
else{s=new nod;
s->st=arbore(i,rad[i][j]-1);
s->ch=chei[rad[i][j]];
s->dr=arbore(rad[i][j],j);}
return s;}
/****** Functia de listare indentata a nodurilor arborelui*****/
void listare(struct nod*r, int nivel)
{int i;
if(r){ listare(r->dr,nivel+1);i=nivel;
while(i) printf( );
printf(%c\n00 , r >ch);
listare(r->st, nivel+1);}}
\ F unctiaprincipala \

86

CAPITOLUL 4. TEHNICI DE CAUTARE

void main() {f=fopen(arboptim.dat,r);


fscanf(f,%d\n00 , &nr);
if(nr>0)
{fscanf(f,%d\n00 , &q[0]);
for(i=1;i<=nr;i++)
fscanf(f,%c %d\n%d\n00 , &chei[i], &p[i], &q[i]);
calcul();
printf(Lungimea medie a unei cautari: %f\n00 ,
(float)c[0][nr]/greut[0][nr]);
struct nod*radacina=arbore(0,nr);
listare(radacina,0);}}
/****************************************************/
Fisierul arboptim.dat contine pe prima linie numarul de noduri interne
ale arborelui, pe a doua linie valoarea ponderii q0 iar pe celelalte linii cheile
Ki cu ponderile pi si qi . Un exemplu de astfel de fisier este urmatorul:
/******************arboptim.dat***********************/
5
1
a02
b11
c10
f22
e30
d12
/**************************************************/

4.4

Arbori echilibrati

Insertia de noi noduri


ntr-un arbore binar de cautare poate conduce la arbori dezechilibrati
n care
ntre
naltimea subarborelui drept al unui nod si

naltimea subarborelui st
ang sa fie o mare diferenta.
Intr-un astfel de arbore
actiunea de cautare va consuma mai mult timp.
O solutie la problema mentinerii unui bun arbore de cautare a fost descoperita de G. M. Adelson-Velskii si E. M. Landis
n 1962 care au pus
n
evidenta asa numitii arbori echilibrati (sau arbori AVL).

4.4. ARBORI ECHILIBRATI

87

De nitie. Un arbore binar este echilibrat (AVL) daca naltimea subarborelui stang al oricarui nod nu difera mai mult decat 1 de naltimea
subarborelui sau drept.
De nitie. Diferenta dintre naltimea subarborelui drept si naltimea subarborelui stang poarta numele de factor de echilibru al unui nod.
Asadar, daca un arbore binar este echilibrat, atunci factorul de echilibru
al fiecarui nod este 1, 0 sau 1.

4.4.1

Arbori Fibonacci

O clasa importanta de arbori echilibrati este clasa de arbori Fibonacci de care


ne vom ocupa
n continuare.
Sa consideram mai
nt
ai sirul (Fn )n1 de numere Fibonacci, definite prin
urmatoarea formula de recurenta:
F1 = F2 = 1,
Fn+2 = Fn+1 + Fn , n 1.

(4.5)

Primii termeni din sirul lui Fibonacci sunt 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ... .
Pentru a gasi o formula explicita pentru numerele Fibonacci, vom cauta
o solutie a recurentei (4.5) de forma Fn = rn . Rezulta ca r satisface ecuatia
algebrica de gradul 2 :
r2 r 1 = 0,
cu solutia
r1,2

1 5
.
=
2

Solutia generala va fi
Fn = Ar1n + Br2n .
Constantele A si B vor fi determinate din conditiile F1 = F2 = 1 care conduc
la sistemul algebric

1+ 5
1 5
A
+B
= 1,
2
2

3+ 5
3 5
A
+B
= 1,
2
2
cu solutia

5
5
A=
, B=
.
5
5

88

CAPITOLUL 4. TEHNICI DE CAUTARE

Figura 4.7: Arbori Fibonacci

4.4. ARBORI ECHILIBRATI

89

Obtinem astfel formula lui Binet pentru numerele Fibonacci:

!n
!n
5 1+ 5
5 1 5
Fn =

.
5
2
5
2
.
Arborii binari Fibonacci, notati cu F Tk , k = 0, 1, 2, ... sunt definiti prin
recurenta dupa cum urmeaza:
- F T0 si F T1 sunt formati fiecare dintr-un singur nod extern etichetat cu
[0] ;
- pentru k 2, arborele Fibonacci de ordin k, F Tk are radacina etichetata
cu Fk ; subarborele st
ang al radacinii este F Tk1 iar subarborele drept al
radacinii este arborele F Tk2 cu etichetele tuturor nodurilor incrementate cu
Fk (eticheta radacinii lui F Tk ) (vezi figura 4.7).
Vom pune mai departe
n evidenta c
ateva proprietati ale arborilor Fibonacci.
Lem
a. Pentru orice k 1, arborele Fibonacci F Tk este un arbore echilibrat avand naltimea h (F Tk ) = k 1, Fk+1 frunze si Fk+1 1 noduri interne.
Demonstratie.Pentru k = 1 si k = 2 proprietatea este verificata. Sa
presupunem ca proprietatea este adevarata pentru toti arborii Fibonacci F Tk0
cu k 0 < k. Fie F Tk un arbore Fibonacci de ordin k > 3. Din definitia
recursiva obtinem ca h (F Tk ) = h (F Tk1 ) + 1 = k 1, numarul de frunze
al lui F Tk este egal cu Fk + Fk1 = Fk+1 iar numarul de noduri interne este
1 + (Fk 1) + (Fk1 1) = Fk+1 1. Din ipoteza de inductie, proprietatea
de echilibrare este satisfacuta pentru toate nodurile cu exceptia radacinii.
In
ceea ce priveste radacina, subarborele st
ang are
naltimea k2 iar subarborele
drept are
naltimea k 3, deci F Tk este echilibrat.

4.4.2

Propriet
ati ale arborilor echilibrati

Arborii echilibrati reprezinta o treapta intermediara


ntre clasa arborilor optimali (cu frunzele asezate pe doua nivele adiacente) si clasa arborilor binari
arbitrari. De aceea este firesc sa ne
ntrebam c
at de mare este diferenta
dintre un arbore optimal si un arbore echilibrat; prezentam
n acest context
urmatoarea teorema:
Teorem
a. Inaltimea unui arbore echilibrat T cu N noduri interne se a a
ntotdeauna ntre log2 (N + 1) si 1.4404 log2 (N + 2) 0.328.

90

CAPITOLUL 4. TEHNICI DE CAUTARE

Demonstratie. Un arbore binar de


naltime h are cel mult 2h 1 noduri
interne; deci
N 2h(T ) 1 h (T ) log2 (N + 1) .
Pentru a gasi limitarea superioara a lui h (T ), ne vom pune problema aflarii
numarului minim de noduri interne continute
ntr-un arbore echilibrat de

naltime h. Fie deci Th arborele echilibrat de


naltima h cu cel mai mic numar
de noduri posibil; unul din subarborii radacinii, de exemplu cel st
ang va avea

naltimea h 1 iar celalalt subarbore va avea


naltimea h 1 sau h 2. Cum
Th are numarul minim de noduri, va trebui sa consideram ca subarborele
st
ang al radacinii are
naltimea h 1 iar subarborele drept are
naltimea
h 2.Putem asadar considera ca subarborele st
ang al radacinii este Th1 iar
subarborele drept este Th2 .
In virtutea acestei consideratii, se demonstreaza
prin inductie ca arborele Fibonacci F Th+1 este arborele Th cautat,
n sensul
ca
ntre toti arborii echilibrati de
naltime impusa h, acesta are cel mai mic
numar de noduri. Conform lemei precedente avem

5
N Fh+2 1 =
5

!h+2
5
1+ 5

2
5

!h+2
1 5
1.
2

Cum

!h+2
1 5
> 1,
2

rezulta ca
log2 (N + 2) > (h + 2) log2

!
1+ 5
1
log2 5
2
2

h < 1.4404 log2 (N + 2) 0.328.


Din consideratiile facute pe parcursul demonstratiei teoremei, rezulta
urmatorul
Corolar. Intre arborii echilibrati cu un numar dat de noduri, arborii
Fibonacci au naltimea maxima, deci sunt cei mai putin performanti.

4.5. INSERTIA UNUI NOD INTR-UN ARBORE ECHILIBRAT

4.5

91

Insertia unui nod


ntr-un arbore echilibrat

Inserarea unui nou nod se efectueaza cu algoritmul cunoscut de inserare a


nodurilor
ntr-un arbore binar de cautare. Dupa inserare
nsa, va trebui sa
re-echilibram arborele daca vom ajunge
ntr-una din urmatoarele situatii:
- subarborelui st
ang al unui nod cu factorul de echilibru 1
i creste

naltimea;
- subarborelui drept al unui nod cu factorul de echilibru 1
i creste
naltimea.
Ambele cazuri sunt tratate apel
and la rotatii (pe care le vom descrie
n
cele ce urmeaza). Rotatiile implica doar modificari ale legaturilor
n cadrul
arborelui, nu si operatii de cautare, de aceea timpul lor de executie este de
ordinul O (1) .

4.5.1

Rotatii
n arbori echilibrati

Rotatiile sunt operatii de schimbare


ntre ele a unor noduri aflate
n relatia de
tata-fiu (si de refacere a unor legaturi) astfel
nc
at sa fie pastrata structura de
arbore de cautare. Astfel, printr-o rotatie simpla a unui arbore la stanga, fiul
drept al radacinii initiale devine noua radacina iar radacina initiala devine
fiul st
ang al noii radacini. Printr-o rotatie simpla a unui arbore la dreapta, fiul
st
ang al radacinii initiale devine noua radacina iar radacina initiala devine
fiul drept al noii radacini. O rotatie dubla la dreapta afecteaza doua nivele:
fiul drept al fiului st
ang al radacinii initiale devine noua radacina, radacina
initiala devine fiul drept al noii radacini iar fiul st
ang al radacinii initiale
devine fiul st
ang al noii radacini. O rotatie dubla la stanga afecteaza de
asemenea doua nivele: fiul st
ang al fiului drept al radacinii initiale devine
noua radacina, radacina initiala devine fiul st
ang al noii radacini iar fiul drept
al radacinii initiale devine fiul drept al noii radacini.
Vom studia cazurile de dezechilibru ce pot aparea si vom efectua reechilibrarea prin rotatii.
Cazul 1. Creste naltimea subarborelui stang al nodului a care are factorul
de echilibru initial 1.
a) Factorul de echilibru al fiului st
ang b al lui a este 1 (figura 4.8).
Aceasta
nseamna ca noul element a fost inserat
n subarborele A. Reechilibrarea necesita o rotatie simpla la dreapta a perechii tata-fiu (a > b).
b) Factorul de echilibru al fiului st
ang b al lui a este 1.

92

CAPITOLUL 4. TEHNICI DE CAUTARE

Figura 4.8: Rotatie simpla la dreapta pentru re-echilibrare

b.1) Factorul de echilibru al fiului drept c al lui b este 1 (figura 4.9).


Aceasta
nseamna ca noul element a fost inserat
n subarborele C. Pentru
re-echilibrare vom avea nevoie de o rotatie dubla la dreapta a nodurilor b <
c < a.
b.2) Factorul de echilibru al fiului drept c al lui b este -1. Se trateaza ca
si cazul b.1) (figura 4.10). .
b.3) Factorul de echilibru al fiului drept c al lui b este 0. Dezechilibrarea
este imposibila.
c) Factorul de echilibru al fiului st
ang b al lui a este 0. Dezechilibrarea
este imposibila.
Cazul I1. Creste naltimea subarborelui drept al nodului a care are factorul
de echilibru initial 1. Se trateaza asemanator cu cazul I).
a) Factorul de echilibru al fiului st
ang b al lui a este 1 (figura 4.11).
Aceasta
nseamna ca noul element a fost inserat
n subarborele C. Reechilibrarea necesita o rotatie simpla la st
anga a perechii tata-fiu (a < b).
b) Factorul de echilibru al fiului drept b al lui a este -1.
b.1) Factorul de echilibru al fiului st
ang c al lui b este -1(figura 4.12).
Aceasta
nseamna ca noul element a fost inserat
n subarborele B. Pentru reechilibrare vom avea nevoie de o rotatie dubla la st
anga a nodurilor b > c > a.

4.5. INSERTIA UNUI NOD INTR-UN ARBORE ECHILIBRAT

Figura 4.9: Rotatie dubla la dreapta pentru re-echilibrare

Figura 4.10: Rotatie dubla la dreapta pentru re-echilibrare

93

94

CAPITOLUL 4. TEHNICI DE CAUTARE

Figura 4.11: Rotatie simpla la st


anga pentru re-echilibrare

Figura 4.12: Rotatie dubla la st


anga pentru re-echilibrare

4.5. INSERTIA UNUI NOD INTR-UN ARBORE ECHILIBRAT

95

b.2) Factorul de echilibru al fiului drept c al lui b este 1. Se trateaza ca si


cazul b.1) (figura 4.13). .

Figura 4.13: Rotatie dubla la st


anga pentru re-echilibrare

b.3) Factorul de echilibru al fiului drept c al lui b este 0. Dezechilibrarea


este imposibila.
c) Factorul de echilibru al fiului st
ang b al lui a este 0. Dezechilibrarea
este imposibila.

4.5.2

Exemple

In arborele din figura 4.14 ne propunem sa inseram elementul 58. Suntem

n cazul I.a). Subarborii cu radacinile 70 si 80 devin dezechilibrati. Pentru


echilibrare se roteste perechea (60, 70) la dreapta, obtin
andu-se arborele din
figura 4.15.

In arborele din figura 4.16 ne propunem sa inseram elementul 68. Suntem

n cazul I.b.1). Pentru echilibrare se roteste la st


anga perechea (60, 65) si
apoi se roteste la dreapta perechea (70, 65). Se obtine
n final arborele din
figura 4.17.

96

CAPITOLUL 4. TEHNICI DE CAUTARE

Figura 4.14: Exemplu de insertie


ntr-un arbore echilibrat

Figura 4.15: Exemplu de insertie


ntr-un arbore echilibrat

4.5. INSERTIA UNUI NOD INTR-UN ARBORE ECHILIBRAT

Figura 4.16: Exemplu de insertie


ntr-un arbore echilibrat

Figura 4.17: Exemplu de insertie


ntr-un arbore echilibrat

97

98

4.5.3

CAPITOLUL 4. TEHNICI DE CAUTARE

Algoritmul de insertie
n arbori echilibrati

Pentru a insera un element x


ntr-un arbore binar de cautare echilibrat se
parcurg urmatoarele etape:
- se cauta pozitia
n care noul element trebuie inserat (ca
n orice arbore
binar de cautare);
- se insereaza elementul x (ca
n orice arbore binar de cautare);
- se reactualizeaza factorii de echilibru ai ascendentilor lui x p
ana la
radacina sau p
ana se gaseste cel mai apropiat ascendent p al lui x, daca
exista, astfel
nc
at subarborele T cu radacina p sa fie dezechilibrat;
- daca p exista, se re-echilibreaza subarborele T cu ajutorul unei rotatii
(simple sau duble).

4.6

S
tergerea unui nod al unui arbore echilibrat

Stergerea este similara insertiei: stergem nodul asa cum se procedeaza


n
cazul unui arbore binar de cautare si apoi re-echilibram arborele rezultat.
Deosebirea este ca numarul de rotatii necesar poate fi tot at
at de mare c
at
nivelul (ad
ancimea) nodului ce urmeaza a fi sters. De exemplu sa stergem
elementul x din figura 4.18.a).

In urma stergerii se ajunge la arborele ne-echilibrat din figura 4.18.b).


Subarborele cu radacina
n y este ne-echilibrat. Pentru a-l echilibra este
necesara o rotatie simpla la dreapta a perechii (y, i)obtin
andu-se arborele din
figura 4.19.d); si acest arbore este ne-echilibrat, radacina a av
and factorul
de echilibru 2. O rotatie dubla la dreapta a lui e, b si a, re-echilibreaza
arborele ajung
andu-se la arborele din figura 4.20.e).

4.6.1

Algoritmul de stergere a unui nod dintr-un arbore echilibrat

Fie data
nregistrarea av
and cheia x. Pentru a sterge dintr-un arbore de
cautare echilibrat nodul cu cheia x parcurgem urmatoarele etape
- se localizeaza nodul r av
and cheia x;
- daca r nu exista, algoritmul se termina;
- altfel, se sterge nodul r, utiliz
and algoritmul de stergere
ntr-un arbore
binar de cautare;.

4.6. STERGEREA UNUI NOD AL UNUI ARBORE ECHILIBRAT

99

Figura 4.18: Exemplu de stergere a unui nod dintr-un arbore echilibrat

Figura 4.19: Exemplu de stergere a unui nod dintr-un arbore echilibrat

100

CAPITOLUL 4. TEHNICI DE CAUTARE

Figura 4.20: Exemplu de stergere a unui nod dintr-un arbore echilibrat

- fie q nodul extern eliminat


n urma aplicarii algoritmului de stergere si p
tatal sau. Se re-echilibreaza (daca este cazul) arborele prin rotatii implic
and
nodul p si eventual ascendentii acestuia.
Vom arata ca numarul de rotatii necesar stergerii unui nod poate ajunge
p
ana la numarul ce indica nivelul nodului
n arbore. Observam mai
nt
ai ca
o rotatie reduce cu 1
naltimea arborelui caruia
i este aplicata. Fie a cel
mai apropiat predecesor al elementului sters x, astfel ca subarborele Ta cu
radacina
n a sa fie neechilibrat. Daca
nainte de rotatie h (Ta ) = k, atunci
dupa rotatie h (Ta ) = k 1.
Fie b tatal lui a (daca a nu este radacina arborelui). Avem urmatoarele
situatii favorabile:
- factorul de echilibru al lui b este 0: nu este necesara nici-o rotatie;
- factorul de echilibru al lui b este 1 si Ta este subarborele drept al lui b:
nu este necesara nici-o rotatie;
- factorul de echilibru al lui b este -1 si Ta este subarborele st
ang al lui b:
nu este necesara nici-o rotatie.
Dificultatile se ivesc atunci c
and:
- factorul de echilibru al lui b este -1 si Ta este subarborele drept al lui b;
- factorul de echilibru al lui b este 1 si Ta este subarborele st
ang al lui b

In ambele cazuri va trebui sa re-echilibram arborele T cu radacina


n b.

4.6. STERGEREA UNUI NOD AL UNUI ARBORE ECHILIBRAT

101

Sa observam ca
nainte de echilibrare h (T ) = k +1 iar dupa re-echilibrare
h (T ) = k. Astfel, subarborele av
and drept radacina pe tatal lui b poate
deveni ne-echilibrat si tot asa p
ana c
and ajungem la radacina arborelui initial
(
n cel mai rau caz).

In continuare prezentam un program, scris


n C de inserare si stergere de
noduri
ntr-un arbore binar de cautare echilibrat.

Figura 4.21: Exemplu de stergere a unui nod dintr-un arbore echilibrat

# include<stdio.h>
# include<malloc.h>
# define F 0
# define T 1
struct Nod{char Info;int FactEch;struct Nod *st;struct Nod *dr;};
struct Nod *Inserare (char , struct Nod *, int *);
void Listare(struct Nod *, int );
struct Nod *EchilibrareDr(struct Nod *, int *);
struct Nod *EchilibrareSt(struct Nod *, int *);
struct Nod *Sterge(struct Nod *, struct Nod *, int *);
struct Nod *StergeElement(struct Nod *, char , int *);
/*********************************************/
/* Functia de inserare in arborele de cautare */
struct Nod * Inserare (char Info, struct Nod *tata, int *H)

102

CAPITOLUL 4. TEHNICI DE CAUTARE

{struct Nod *Nod1;


struct Nod *Nod2;
if(!tata)
{tata = (struct Nod *) malloc(sizeof(struct Nod));
tata->Info = Info;
tata->st = NULL;
tata->dr = NULL;
tata->FactEch = 0;
*H = T;
return (tata);}
if(Info < tata->Info)
{tata->st = Inserare(Info, tata->st, H);
if(*H)
/* Creste inaltimea subarborelui stang */
{
switch(tata->FactEch)
{
case 1: /* Subarborele drept mai inalt*/
tata->FactEch = 0;
*H = F;
break;
case 0: /* Arbore echilibrat */
tata->FactEch = -1;
break;
case -1: /* Subarborele stang mai inalt */
Nod1 = tata->st;
if(Nod1->FactEch == -1)
{//Cazul din figura 4.8
printf(Rotatie simpla la dreapta \n00 );
tata->st= Nod1->dr;
Nod1->dr = tata;
tata->FactEch = 0;
tata = Nod1;
tata->FactEch = 0;}
else
/*cazul Nod1->FactEch == 0 nu este posibil pentru ca
am fi avut *H=F; ramane Nod1->FactEch == 1 ca in
figurile 4.9 si 4.10 */

4.6. STERGEREA UNUI NOD AL UNUI ARBORE ECHILIBRAT


{
printf(Rotatie dubla la dreapta \n00 );
Nod2 = Nod1->dr;
Nod1->dr = Nod2->st;
Nod2->st = Nod1;
tata->st = Nod2->dr;
Nod2->dr = tata;
if(Nod2->FactEch == -1)
tata->FactEch = 1;
else
tata->FactEch = 0;
if(Nod2->FactEch == 1)
Nod1->FactEch = -1;
else
Nod1->FactEch = 0;
tata = Nod2;}
tata->FactEch = 0;
*H = F;}}}
if(Info > tata->Info)
{
tata->dr = Inserare(Info, tata->dr, H);
if(*H)
/* Subarborele drept devine mai inalt */
{
switch(tata->FactEch)
{
case -1: /* Subarborele stang este mai inalt */
tata->FactEch = 0;
*H = F;
break;
case 0: /* Arbore echilibrat */
tata->FactEch = 1;
break;
case 1: /* Subarborele drept este mai inalt */
Nod1 = tata->dr;
if(Nod1->FactEch == 1)
{/*Cazul din figura 4.11 */
printf(Rotatie simpla la stanga \n00 );

103

104

CAPITOLUL 4. TEHNICI DE CAUTARE

tata->dr= Nod1->st;
Nod1->st = tata;
tata->FactEch = 0;
tata = Nod1;
tata->FactEch = 0;}
else
/*cazul Nod1->FactEch == 0 nu este posibil pentru ca
am fi avut *H=F; ramane Nod1->FactEch == -1 ca in
figurile 4.12 si 4.136 */
{printf(Rotatie dubla la stanga \n00 );
Nod2 = Nod1->st;
Nod1->st = Nod2->dr;
Nod2->dr = Nod1;
tata->dr = Nod2->st;
Nod2->st = tata;
if(Nod2->FactEch == 1)
tata->FactEch = -1;
else
tata->FactEch = 0;
if(Nod2->FactEch == -1)
Nod1->FactEch = 1;
else
Nod1->FactEch = 0;
tata = Nod2;
}
tata->FactEch = 0;
*H = F;}}}
return(tata);}
/*************************************************/
/* Functia de listare */
void Listare(struct Nod *Arbore,int Nivel)
{int i;
if (Arbore)
{
Listare(Arbore->dr, Nivel+1);
printf(\n00 );
for (i = 0; i < Nivel; i++)
printf( );

4.6. STERGEREA UNUI NOD AL UNUI ARBORE ECHILIBRAT


printf(%c, Arbore->Info);
Listare(Arbore->st, Nivel+1);
}
}
/* Echilibrare in cazul cand subarborele drept
devine mai inalt in comparatie cu cel stang*/
/**************************************/
struct Nod * EchilibrareDr(struct Nod *tata, int *H)
{
struct Nod *Nod1, *Nod2;
switch(tata->FactEch)
{
case -1:
tata->FactEch = 0;
break;
case 0:
tata->FactEch = 1;
*H= F;
break;
case 1: /* Re-echilibrare */
Nod1 = tata->dr;
if(Nod1->FactEch >= 0)
/* Cazul din figura 4.18 a) cu tata==y */
{
printf(Rotatie simpla la stanga \n00 );
tata->dr= Nod1->st;
Nod1->st = tata;
if(Nod1->FactEch == 0)
{
tata->FactEch = 1;
Nod1->FactEch = -1;
*H = F;
}
else
{
tata->FactEch = Nod1->FactEch = 0;
}
tata = Nod1;

105

106

CAPITOLUL 4. TEHNICI DE CAUTARE

}
else
{
printf(Rotatie dubla la stanga \n00 );
Nod2 = Nod1->st;
Nod1->st = Nod2->dr;
Nod2->dr = Nod1;
tata->dr = Nod2->st;
Nod2->st = tata;
if(Nod2->FactEch == 1)
tata->FactEch = -1;
else
tata->FactEch = 0;
if(Nod2->FactEch == -1)
Nod1->FactEch = 1;
else
Nod1->FactEch = 0;
tata = Nod2;
Nod2->FactEch = 0;
}
}
return(tata);
}
/* Echilibrare in cazul cand subarborele stang
devine mai inalt in comparatie cu cel drept*/
/*******************************************/
struct Nod * EchilibrareSt(struct Nod *tata, int *H)
{
struct Nod *Nod1, *Nod2;
switch(tata->FactEch)
{
case 1:
tata->FactEch = 0;
break;
case 0:
tata->FactEch = -1;
*H= F;
break;

4.6. STERGEREA UNUI NOD AL UNUI ARBORE ECHILIBRAT


case -1: /* Re-echilibrare */
Nod1 = tata->st;
if(Nod1->FactEch <= 0)
/*Cazul figurii 4.18 a) cu tata==e */
{
printf( Rotatie simpla la dreapta \n00 );
tata->st= Nod1->dr;
Nod1->dr = tata;
if(Nod1->FactEch == 0)
{
tata->FactEch = -1;
Nod1->FactEch = 1;
*H = F; }
else
{ tata->FactEch = Nod1->FactEch = 0; }
tata = Nod1; }
else
/*cazul din figura 4.21 cu tata==e */
{ printf(Rotatie dubla la dreapta \n00 );
Nod2 = Nod1->dr;
Nod1->dr = Nod2->st;
Nod2->st = Nod1;
tata->st = Nod2->dr;
Nod2->dr = tata;
if(Nod2->FactEch == -1)
tata->FactEch = 1;
else
tata->FactEch = 0;
if(Nod2->FactEch == 1)
Nod1->FactEch = -1;
else
Nod1->FactEch = 0;
tata = Nod2;
Nod2->FactEch = 0; } }
return(tata); }
/* Inlocuieste informatia nodulului Temp in care a fost gasita cheia
cu informatia celui mai din dreapta descendent al lui R (pe care
apoi il sterge)*/

107

108

CAPITOLUL 4. TEHNICI DE CAUTARE

/**********************************************/
struct Nod * Sterge(struct Nod *R, struct Nod *Temp, int *H)
{ struct Nod *DNod = R;
if( R->dr != NULL)
{ R->dr = Sterge(R->dr, Temp, H);
if(*H)
R = EchilibrareSt(R, H); }
else
{ DNod = R;
Temp->Info = R->Info;
R = R->st;
free(DNod);
*H = T; }
return(R); }
/* Sterge element cu cheia respectiva din arbore */
/**********************************************/
struct Nod * StergeElement(struct Nod *tata, char Info, int
*H)
{ struct Nod *Temp;
if(!tata) {
printf( Informatia nu exista \n00 );
return(tata); }
else { if (Info < tata->Info ) {
tata->st = StergeElement(tata->st, Info, H);
if(*H)
tata = EchilibrareDr(tata, H); }
else
if(Info > tata->Info) { tata->dr = StergeElement(tata->dr,
Info, H);
if(*H)
tata = EchilibrareSt(tata, H); }
else { Temp= tata;
if(Temp->dr == NULL) {
tata = Temp->st;
*H = T;
free(Temp); }
else
if(Temp->st == NULL) { tata = Temp->dr;

4.6. STERGEREA UNUI NOD AL UNUI ARBORE ECHILIBRAT

109

*H = T;
free(Temp); }
else
{ Temp->st = Sterge(Temp->st, Temp, H);
if(*H)
tata = EchilibrareDr(tata, H); } } }
return(tata); }
/* Functia principala*/
/*****************************************/
void main()
{ int H;
char Info ;
char choice;
struct Nod *Arbore = (struct Nod *)malloc(sizeof(struct Nod));
Arbore = NULL;
printf( Tastati b pentru terminare: \n00 );
choice = getchar();
while(choice != b)
{ fflush(stdin);
printf(Informatia nodului (tip caracter: a,b,1,2,etc.): \n00 );
scanf(%c,&Info);
Arbore = Inserare(Info, Arbore, &H);
printf(Arborele este: \n00 );
Listare(Arbore, 1); fflush(stdin);
printf(Tastati b pentru terminare: \n00 );
choice = getchar(); }fflush(stdin);
while(1) {
printf( Tastati b pentru terminare: \n00 );
printf( Introduceti cheia pe care vreti s-o stergeti: \n00 );
scanf(%c,&Info);
if (Info == b) break;
Arbore = StergeElement(Arbore, Info, &H);
printf( Arborele este: \n00 );
Listare(Arbore, 1); } }

110

CAPITOLUL 4. TEHNICI DE CAUTARE

Bibliografie
[1] T. H. CORMEN, C. E. LEISERSON, R. R. RIVEST, Introducere n
algoritmi, Edit. Computer Libris AGORA, Cluj-Napoca, 2000
[2] H. GEORGESCU, Tehnici de programare, Edit. Universitatii Bucuresti,
2005.
[3] D. E. KNUTH, The Art of Computer Programming, vol.1, Reading, Massachusets, 1969; vol. 3, Addison-Wesley, 1973.
[4] D. STOILESCU, Culegere de C/C++, Edit. Radial, Galati, 1998
[5] I. TOMESCU, Data Structures, Edit. Universitatii Bucuresti, 1998.

111

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