Sunteți pe pagina 1din 182

Daniel DANCIU

Silviu DUMITRESCU

S
ALGORITMICA
I PROGRAMARE
Curs si probleme de seminar
JAVA

Brasov 2002

Cuprins
1 Instalarea mediului Java
1.1 Obtinerea mediului Java pentru platforma dumneavoastra
1.1.1 Medii de dezvoltare integrata . . . . . . . . . . . .
1.2 Instalarea mediului Java . . . . . . . . . . . . . . . . . . .
1.2.1 Instructiuni de instalare pentru Windows . . . . .
1.2.2 Instructiuni de instalare pentru Linux/Unix . . . .

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

6
6
6
7
7
7

2 Notiuni fundamentale de programare in Java


2.1 Mediul de lucru Java . . . . . . . . . . . . . . . . . . . . . .
2.2 Primul program Java . . . . . . . . . . . . . . . . . . . . . .
2.2.1 Comentarii . . . . . . . . . . . . . . . . . . . . . . .
2.2.2 Functia main . . . . . . . . . . . . . . . . . . . . . .
2.2.3 Scrierea pe ecran . . . . . . . . . . . . . . . . . . . .
2.3 Tipuri de date primitive . . . . . . . . . . . . . . . . . . . .
2.3.1 Tipurile primitive . . . . . . . . . . . . . . . . . . .
2.3.2 Constante . . . . . . . . . . . . . . . . . . . . . . . .
2.3.3 Declararea si initializarea tipurilor primitive n Java
2.3.4 Citire/scriere de la terminal . . . . . . . . . . . . . .
2.4 Operatori de baza . . . . . . . . . . . . . . . . . . . . . . .
2.4.1 Operatori de atribuire . . . . . . . . . . . . . . . . .
2.4.2 Operatori aritmetici binari . . . . . . . . . . . . . .
2.4.3 Operatori aritmetici unari . . . . . . . . . . . . . . .
2.4.4 Conversii de tip . . . . . . . . . . . . . . . . . . . . .
2.5 Instructiuni conditionale . . . . . . . . . . . . . . . . . . . .
2.5.1 Operatori relationali . . . . . . . . . . . . . . . . . .
2.5.2 Operatori logici . . . . . . . . . . . . . . . . . . . . .
2.5.3 Instructiunea if . . . . . . . . . . . . . . . . . . . .
2.5.4 Instructiunea while . . . . . . . . . . . . . . . . . . .
2.5.5 Instructiunea for . . . . . . . . . . . . . . . . . . . .
2.5.6 Instructiunea do . . . . . . . . . . . . . . . . . . . .
2.5.7 Instructiunile break si continue . . . . . . . . . . . .
2.5.8 Instructiunea switch . . . . . . . . . . . . . . . . . .
2.5.9 Operatorul conditional . . . . . . . . . . . . . . . . .
2.6 Metode . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.6.1 Suprancarcarea numelor la metode . . . . . . . . . .

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

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

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

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

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

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

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

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

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

9
9
9
10
10
10
10
10
11
12
12
12
13
14
14
14
15
15
15
16
18
18
19
20
21
22
23
23

CUPRINS

2.7

Probleme propuse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24

3 Referinte
3.1 Ce este o referinta . . . . . . . . . . . . . . . . . .
3.2 Fundamente despre obiecte si referinte . . . . . . .
3.2.1 Operatorul punct (.) . . . . . . . . . . . . .
3.2.2 Declararea obiectelor . . . . . . . . . . . . .
3.2.3 Colectarea de gunoaie (garbage collection) .
3.2.4 Semnificatia lui = . . . . . . . . . . . . . .
3.2.5 Transmiterea de parametri . . . . . . . . .
3.2.6 Semnificatia lui == . . . . . . . . . . . . .
3.2.7 Suprancarcarea operatorilor pentru obiecte
3.3 Siruri de caractere (stringuri) . . . . . . . . . . . .
3.3.1 Fundamentele utilizarii stringurilor . . . . .
3.3.2 Concatenarea stringurilor . . . . . . . . . .
3.3.3 Comparatia stringurilor . . . . . . . . . . .
3.3.4 Alte metode pentru stringuri . . . . . . . .
3.3.5 Conversia de la string la tipurile primitive .
3.4 Siruri . . . . . . . . . . . . . . . . . . . . . . . . .
3.4.1 Declaratie, Atribuire si Metode . . . . . . .
3.4.2 Expansiunea dinamica a sirurilor . . . . . .
3.4.3 Siruri cu mai multe dimensiuni . . . . . . .
3.4.4 Argumente n linie de comanda . . . . . .
3.5 Tratarea exceptiilor . . . . . . . . . . . . . . . . . .
3.5.1 Procesarea exceptiilor . . . . . . . . . . . .
3.5.2 Exceptii uzuale . . . . . . . . . . . . . . . .
3.6 Intrare si iesire . . . . . . . . . . . . . . . . . . . .
3.6.1 Operatii de baza pe fluxuri (stream-uri) . .
3.6.2 Obiectul StringTokenizer . . . . . . . . . .
3.6.3 Fisiere secventiale . . . . . . . . . . . . . .
3.7 Probleme propuse . . . . . . . . . . . . . . . . . . .

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

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

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

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

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

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

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

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

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

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

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

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

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

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

25
25
27
27
27
28
28
29
30
30
30
30
31
32
32
32
33
33
35
38
38
39
39
41
42
42
43
44
46

4 Obiecte si clase
4.1 Ce este programarea orientata pe obiecte?
4.2 Un exemplu simplu . . . . . . . . . . . . .
4.3 Metode uzuale . . . . . . . . . . . . . . .
4.3.1 Constructori . . . . . . . . . . . .
4.3.2 Modificatori si Accesori . . . . . .
4.3.3 Afisare si toString . . . . . . . . .
4.3.4 Metoda equals . . . . . . . . . . .
4.3.5 Variabile si metode statice . . . . .
4.3.6 Metoda main . . . . . . . . . . . .
4.4 Pachete . . . . . . . . . . . . . . . . . . .
4.4.1 Directiva import . . . . . . . . . .
4.4.2 Instructiunea package . . . . . . .
4.4.3 Variabila sistem CLASSPATH . .

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

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

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

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

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

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

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

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

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

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

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

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

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

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

47
47
48
50
50
52
52
53
53
53
53
54
55
55

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

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

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

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

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

CUPRINS

4.5

4.6

4.4.4 Reguli de vizibilitate Package-Friendly


4.4.5 Compilarea separata . . . . . . . . . .
Alte operatii . . . . . . . . . . . . . . . . . .
4.5.1 Referinta this . . . . . . . . . . . . . .
4.5.2 Prescurtarea this pentru constructori .
4.5.3 Operatorul instanceof . . . . . . . . .
4.5.4 Atribute statice . . . . . . . . . . . . .
4.5.5 Initializatori statici . . . . . . . . . . .
Probleme propuse . . . . . . . . . . . . . . . .

5 Mostenire
5.1 Ce este mostenirea? . . . . . . . . . . .
5.2 Sintaxa de baza Java . . . . . . . . . . .
5.2.1 Reguli de vizibilitate . . . . . . .
5.2.2 Constructor si super . . . . . . .
5.2.3 Metode si clase final . . . . . . .
5.2.4 Redefinirea unei metode . . . . .
5.2.5 Metode si clase abstracte . . . .
5.3 Exemplu: Extinderea clasei Shape . . .
5.4 Mostenire multipla . . . . . . . . . . . .
5.5 Interfete . . . . . . . . . . . . . . . . . .
5.5.1 Definirea unei interfete . . . . . .
5.5.2 Implementarea unei interfete . .
5.5.3 Interfete multiple . . . . . . . . .
5.6 Implementarea de componente generice
5.7 Anexa - clasa Reader . . . . . . . . . . .
5.8 Probleme propuse . . . . . . . . . . . . .

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

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

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

6 Analiza eficientei algoritmilor


6.1 Ce este analiza algoritmilor? . . . . . . . . .
6.2 Notatia asimptotica . . . . . . . . . . . . . .
6.2.1 O notatie pentru ordinul de marime al
algoritm . . . . . . . . . . . . . . . . .
6.3 Tehnici de analiza algoritmilor . . . . . . . .
6.3.1 Sortarea prin selectie . . . . . . . . . .
6.3.2 Sortarea prin insertie . . . . . . . . . .
6.3.3 Turnurile din Hanoi . . . . . . . . . .
6.4 Analiza algoritmilor recursivi . . . . . . . . .
6.4.1 Metoda iteratiei . . . . . . . . . . . .
6.4.2 Inductia constructiva . . . . . . . . .
6.4.3 Recurente liniare omogene . . . . . . .
6.4.4 Recurente liniare neomogene . . . . .
6.4.5 Schimbarea variabilei . . . . . . . . . .
6.5 Probleme propuse . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.
.

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

.
.
.
.
.
.
.
.
.

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

.
.
.
.
.
.
.
.
.

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

.
.
.
.
.
.
.
.
.

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

.
.
.
.
.
.
.
.
.

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

. . . . .
. . . . .
timpului
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .

.
.
.
.
.
.
.
.
.

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

.
.
.
.
.
.
.
.
.

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

.
.
.
.
.
.
.
.
.

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

.
.
.
.
.
.
.
.
.

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

.
.
.
.
.
.
.
.
.

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

.
.
.
.
.
.
.
.
.

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

.
.
.
.
.
.
.
.
.

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

.
.
.
.
.
.
.
.
.

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

.
.
.
.
.
.
.
.
.

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

.
.
.
.
.
.
.
.
.

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

.
.
.
.
.
.
.
.
.

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

.
.
.
.
.
.
.
.
.

56
56
56
56
57
58
58
59
59

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

68
68
71
71
72
72
73
74
76
81
81
82
82
83
84
87
88

90
. . . . . . . . . . . . 90
. . . . . . . . . . . . 92
de executie al unui
. . . . . . . . . . . . 92
. . . . . . . . . . . . 94
. . . . . . . . . . . . 94
. . . . . . . . . . . . 95
. . . . . . . . . . . . 96
. . . . . . . . . . . . 97
. . . . . . . . . . . . 97
. . . . . . . . . . . . 97
. . . . . . . . . . . . 98
. . . . . . . . . . . . 100
. . . . . . . . . . . . 101
. . . . . . . . . . . . 103

CUPRINS

7 Structuri de date
7.1 De ce avem nevoie de structuri de date?
7.2 Stive . . . . . . . . . . . . . . . . . . . .
7.3 Cozi . . . . . . . . . . . . . . . . . . . .
7.4 Liste nlantuite . . . . . . . . . . . . . .
7.5 Arbori binari de cautare . . . . . . . . .
7.6 Tabele de repartizare . . . . . . . . . . .
7.7 Cozi de prioritate . . . . . . . . . . . . .
7.8 Aplicatie . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

8 Metoda Backtracking
8.1 Prezentare generala . . . . . . . . . . . . . . . . . . . . . . . . .
8.2 Prezentarea metodei . . . . . . . . . . . . . . . . . . . . . . . .
8.2.1 Atribuie si avanseaza . . . . . . . . . . . . . . . . . . . .
8.2.2 Incercare esuata . . . . . . . . . . . . . . . . . . . . . .
8.2.3 Revenire . . . . . . . . . . . . . . . . . . . . . . . . . . .
8.2.4 Revenire dupa construirea unei solutii . . . . . . . . . .
8.3 Implementarea metodei backtracking . . . . . . . . . . . . . . .
8.4 Probleme clasice care admit rezolvare prin metoda backtracking
8.4.1 Problema generarii permutarilor . . . . . . . . . . . . .
8.4.2 Generarea aranjamentelor si a combinarilor . . . . . . .
8.4.3 Problema damelor . . . . . . . . . . . . . . . . . . . . .
8.4.4 Problema colorarii hartilor . . . . . . . . . . . . . . . .
8.5 Probleme propuse . . . . . . . . . . . . . . . . . . . . . . . . . .
9 Divide et Impera
9.1 Notiuni elementare referitoare la recursivitate
9.1.1 Functii recursive . . . . . . . . . . . .
9.1.2 Recursivitatea nu nseamna recurenta
9.2 Prezentarea metodei Divide et Impera . . . .
9.3 Cautare binara . . . . . . . . . . . . . . . . .
9.4 Sortarea prin interclasare (MergeSort) . . . .
9.5 Sortarea rapida (QuickSort) . . . . . . . . . .
9.6 Expresii aritmetice . . . . . . . . . . . . . . .
9.7 Probleme propuse . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

10 Algoritmi Greedy
10.1 Problema spectacolelor (selectarea activitatilor) .
10.1.1 Demonstrarea corectitudinii algoritmului .
10.2 Elemente ale strategiei Greedy . . . . . . . . . .
10.2.1 Proprietatea de alegere Greedy . . . . . .
10.2.2 Substructura optima . . . . . . . . . . . .
10.3 Minimizarea timpului mediu de asteptare . . . .
10.4 Interclasarea optima a mai multor siruri ordonate
10.5 Probleme propuse . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.

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

.
.
.
.
.
.
.
.

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

.
.
.
.
.
.
.
.

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

.
.
.
.
.
.
.
.

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

.
.
.
.
.
.
.
.

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

107
107
109
111
113
117
121
123
124

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

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

129
. 129
. 130
. 133
. 133
. 134
. 134
. 135
. 137
. 137
. 138
. 140
. 141
. 143

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

153
. 153
. 153
. 157
. 159
. 160
. 161
. 162
. 164
. 167

.
.
.
.
.
.
.
. .

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.

172
172
173
174
176
177
177
178
181

Capitolul 1

Instalarea mediului Java


In acest capitol, vom da cateva posibilitati de descarcare a softului, necesar pentru rularea
unor aplicatii Java, de pe Internet precum si instalarea acestuia.

1.1

Obtinerea mediului Java pentru platforma dumneavoastr


a

Mediul de baza Java consta dintr-un browser web unde puteti vizualiza applet-urile Java,
un compilator Java ce transforma codul sursa Java n cod binar si un interpretor pentru
rularea programelor Java. Aveti nevoie de asemenea de un editor de texte ca emacs,
TextPad sau BBEdit. Alte unelte ca debugger, un mediu vizual de dezvoltare etc. nu sunt
absolut necesare.
Nu este necesar sa luati toate partile de la aceeasi sursa. De obicei, browser-ul vostru
de web va fi Internet Explorer sau Netscape. Celelalte le puteti obtine de la Suns Java
Developer Kit (JDK). Sun publica versiuni pentru toate platformele (Windows, Solaris,
X86 Linux).
JDK nu include un browser web dar contine un applet viewer pentru testarea applet-urilor.
JDK include de asemenea compilatorul javac, interpretorul java, profiler-ul javaprof, generatotul fisierelor C de tip header (pentru integrarea metodelor scrise n C ntr-o clasa Java)
javah precum si depanatorul Java si generatorul de documentatie. Mai multa documentatie
despre JDK puteti gasi n pagina de web a firmei Sun.
Sun furnizeaza masina virtuala Java pentru Solaris, X86 Linux si Wndows 95, 98, NT.
Pentru aceasta lucrare aveti nevoie de Java 2 Software Development Kit, versiunea 1.2
(JDK 1.2) sau urmatoarele. Versiunea JDK 1.3, utilizata si ea destul de des, nu este
disponibila pentru toate platformele. Totusi, diferentele ntre JDK1.2 si JDK1.3 nu sunt
foarte importante.

1.1.1

Medii de dezvoltare integrat


a

Posibilitatile de dezvoltare integrata ale aplicatiilor Java sunt nca primitive n comparatie
cu ceea ce este disponibil pentru C++. Se pare ca cel putin deocamdata mediile de
dezvoltare integrata (IDE-Integrated Development Environments) nu sunt foarte performante. Acestea includ Metrowerks Code Warrior, Borland JBuilder, WinGate Visual Cafe
6

1.2. INSTALAREA MEDIULUI JAVA

sau Microsoft Visual J++.

1.2

Instalarea mediului Java

Pentru nceput trebuie sa va descarcati JDK-ul pe care urmeaza apoi sa l instalati.

1.2.1

Instructiuni de instalare pentru Windows

Stergeti mai ntai toate variantele de JDK pe care le aveti deja instalate, mai ales dac
a
doriti sa puneti noul JDK ntr-un alt director. De asemenea, trebuie sa folositi regedit
pentru a sterge toate cheile anterior instalate.
Aveti nevoie de aproximativ 60 MB de spatiu de memorie liber pentru instalarea JDKului. Executati un dublu clic pe icoana din File Manager sau selectand Run... din meniul
Program Manager File editati calea catre fisier. Aceasta va poduce dezarhivarea incluzand
toate directoarele si subdirectoarele necesare. Vom presupune ca instalarea s-a facut n
C:\jdk.
Este necesar sa adaugati directotul C:\jdk\bin variabilei de mediu PATH. De exemplu:
C:\>set PATH="c:\jdk\bin;$PATH"
Acest lucru poate fi realizat permanent prin introducerea comenzii anterioare n fisierul
autoexec.bat.
Pentru a va asigura ca mediul vostru Java este corect configurat, deschideti o fereastr
a
DOS si editati javac nofile.java. Astfel:
C:\>javac nofile.java
Daca primiti raspunsul:
error: Cant read: nofile.java
atunci instalarea a fost facuta cu succes. Daca primiti raspunsul:
The name specified is not recognized as an
internal or external command, operable program or batch file.
sau ceva similar atunci mediul Java nu a fost bine instalat sau variabila PATH nu are o
valoare corecta. Trebuie rezolvate aceste probleme nainte de a continua.

1.2.2

Instructiuni de instalare pentru Linux/Unix

Aveti nevoie de aproximativ 60 MB de spatiu de memorie liber pentru instalarea JDKului dar dublu ar fi de mare ajutor. Modul de dezarhivare dintr-un fisier gzipped tar este
urmatorul:
% gunzip jk1_2_2-linux-i386.tar.gz
% tar xvf jdk1_2_2-linux-i386.tar

CAPITOLUL 1. INSTALAREA MEDIULUI JAVA

Numele exact al fisierului poate fi un pic modificat daca folositi o platforma diferita ca si
Irix sau o versiune diferita.
Puteti face dezarhivarea n directorul curent sau daca aveti drepturi de root ntr-un alt
loc ca de exemplu /usr/local/java unde toti utilizatorii pot avea acces la fisiere. Oricum
drepturile de root nu sunt necesare pentru a instala Java.
Dezarhivarea creaza toate directoarele si subdirectoarele necesare. Calea exacta nu este
importanta, dar pentru simplitate vom presupune n continuare ca instalarea s-a facut n
/usr/local/java. Veti gasi fisierele n /usr/local/java/jdk1.2.2. Daca dezarhivati o versiune
diferita, atunci fisierele vor fi ntr-o cale usor modificata ca de exemplu /usr/local/java/jdk1.3.
Este posibil ca mai multe versiuni de JDK sa coexiste armonios n acelasi sistem. Daca
dezarhivati altundeva decat /usr/local/java trebuie sa nlocuiti /usr/local/java cu calea
completa pana la directorul java. Daca instalati n directorul curent puteti folosi /java
n loc de calea completa.
Acum trebuie sa adaugati directorul /usr/local/java/jdk1.2.2/bin variabilei de mediu PATH.
Acest lucru se poate face astfel dependent de shell-ul vostru:
csh,tcsh:
% set PATH=($PATH/usr/local/java/jdk1.2.2/bin)
sh:
% PATH=($PATH/usr/local/java/bin); export $PATH
Puteti sa ad
augati liniile anterioare la sfarsitul fisierelor .profile sau .cshrc pentru a nu le
mai scrie la fiecare login-are.
Pentru a va asigura ca mediul vostru Java este corect configurat, editati javac nofile.java
la prompt-ul vostru shell:
% javac nofile.java
Daca primiti raspunsul:
error: Cant read: nofile.java
atunci instalarea a fost facuta cu succes. Daca primiti raspunsul:
javac: Command not found
sau ceva similar atunci mediul Java nu a fost bine instalat sau variabila PATH nu are o
valoare corecta. Trebuie rezolvate aceste probleme nainte de a continua.

Capitolul 2

Notiuni fundamentale de
programare in Java
2.1

Mediul de lucru Java

Codul sursa Java este continut n fisiere text care au extensia .java. Compilatorul local,
care este de obicei javac sau jikes1 , compileaza programul si genereaza fisiere .class care
contin byte-code. Byte-code este un limbaj intermediar portabil care este interpretat de
catre interpretorul de Java, numit java.

2.2

Primul program Java

Sa ncepem prin a examina programul simplu din Figura 2.1. Acest program tipareste
un scurt mesaj pe ecran. Numerele din stanga fiecarei linii nu fac parte din program. Ele
sunt furnizate doar pentru o mai usoara referire a secventelor de cod.
Transpuneti programul ntr-un fisier cu numele FirstProgram.java2 dupa care compilati-l
si rulati-l. Java este case-sensitive, ceea ce nseamna ca face deosebirile ntre literele mari
si mici.
1.
2.
3.
4.
5.
6.
7.
8.

//Primul program
public class FirstProgram
{
public static void main(String[] args)
{
System.out.println("Primul meu program Java") ;
}
}

Figura 2.1 Un prim program simplu


1
javac este compilatorul de la Sun, jikes este compilatorul de la IBM si este preferat de multi programatori deoarece este mult mai rapid.
2
Atentie la literele mari si mici!

10

2.2.1

CAPITOLUL 2. NOT
IUNI FUNDAMENTALE DE PROGRAMARE IN JAVA

Comentarii

In Java exista trei tipuri de comentarii. Prima forma, care este mostenita de la C ncepe
cu /* si se termina cu */. Iata un exemplu:
1. /* Acesta este un comentariu
2. pe doua linii */
Comentariile nu pot fi imbricate, deci nu putem avea un comentariu n interiorul altui
comentariu.
Cea de-a doua forma de comentarii este mostenita de la limbajul C++ si ncepe cu //.
Nu exista simbol pentru ncheiere, deoarece un astfel de comentariu se extinde automat
pana la sfarsitul liniei curente. Acest comentariu este folosit n linia 1 din Figura 2.1.
Cea de-a treia forma este asemanatoare cu prima doar ca ncepe cu /** n loc de /*.
Acesta forma de comentariu este utilizata pentru a furniza informatii utilitarului javadoc.
Comentariile au fost introduse pentru a face codul mai lizibil pentru programatori. Un
program bine comentat reprezinta un semn al unui bun programator.

2.2.2

Functia main

Un program Java consta dintr-o colectie de clase care interactioneaza ntre ele prin intermediul metodelor. Echivalentul Java al unei proceduri sau functii din Pascal sau C este
metoda static
a, pe care o vom descrie mai pe larg n acest capitol. Atunci cand se executa
un program Java, va fi invocata automat metoda statica main. Linia 4 din Figura 2.1
arata ca metoda main poate fi eventual invocata cu anumiti parametri n linia de comanda.
Tipul parametrilor functiei main cat si tipul functiei, void, sunt obligatorii.

2.2.3

Scrierea pe ecran

Programul din Figura 2.1 consta dintr-o singura instructiune, aflata la linia 6. Functia
println reprezinta principalul mecanism de scriere n Java, fiind echivalent ntr-o anumita
masura cu functia writeln din Pascal sau printf din C. In aceasta situatie se scrie un sir
de caractere la fluxul de iesire standard System.out. Vom discuta despre citire/scriere mai
tarziu. Deocamdata ne multumim doar sa amintim ca aceeasi sintaxa este folosita pentru
a scrie orice fel de entitate, fie ca este vorba despre un ntreg, real, sir de caractere sau alt
tip.

2.3

Tipuri de date primitive

Java defineste opt tipuri primitive de date, oferind de asemenea, o foarte mare flexibilitate n a defini noi tipuri de date, numite clase. Totusi n Java, exista cateva diferente
estentiale ntre tipurile de date primitive si cele definite de utilizator. In aceasta sectiune
vom examina tipurile primitive si operatiile fundamentale care pot fi realizate asupra lor.

2.3.1

Tipurile primitive

Java are opt tipuri de date primitive prezentate n Figura 2.2.

2.3. TIPURI DE DATE PRIMITIVE

11

Tip de data
Ce retine
Valori
byte
ntreg pe 8 biti
-128 la 127
short
ntreg pe 16 biti
-32768 la 32767
int
ntreg pe 32 biti
-2.147.483.648 la 2.147.483.647
long
ntreg pe 64 biti
263 la 263 1
float
virgula mobila pe 32 biti
6 cifre semnificative, (1046 la 1038 )
double
virgula mobila pe 64 biti 15 cifre semnificative, (10324 la 10308 )
char
caracter unicode
boolean
variabila booleana
false si true
Figura 2.2 Cele opt tipuri de date primitive n Java
Cel mai des utilizat este tipul ntreg specificat prin cuvantul cheie int. Spre deosebire
de majoritatea altor limbaje de programare, marja de valori a tipurilor ntregi nu este
dependenta de masina. Java accepta si tipurile ntregi byte, short si long. Numerele reale
(virgul
a mobila) sunt reprezentate de tipurile float si double. Tipul double are mai multe
cifre semnficative, de aceea utilizarea lui este recomandata n locul tipului float. Tipul char
este folosit pentru a reprezenta caractere. Un char ocupa 16 biti pentru a putea reprezenta
standardul Unicode. Standardul Unicode contine peste 30.000 de caractere distincte care
acoper
a principalele limbi scrise (inclusiv Japoneza, Chineza etc.). Prima parte a tabelei
Unicode este identica cu tabela ASCII. Ultimul tip primitiv este boolean; o variabila de tip
boolean poate lua una din valorile true sau false.

2.3.2

Constante

Constantele ntregi pot fi reprezentate n bazele 10, 8 sau 16. Notatia octal
a este indicata printr-un 0 nesemnificativ la nceput; notatia hexa este indicata printr-un 0x sau 0X
la nceput. Iata cateva moduri echivalente de a reprezenta ntregul 37: 37, 045, 0x25.
Notatiile octale si hexazecimale nu vor fi utilizate n acest curs. Totusi trebuie sa fim
constienti de ele pentru a folosi 0-uri la nceput doar acolo unde chiar vrem aceasta.
O constant
a caracter este cuprinsa ntre apostrofuri, cum ar fi a. Intern, Java interpreteaza aceasta constanta ca pe un numar (codul Unicode). Ulterior, functiile de scriere
vor transforma acest numar n caracterul corespunzator. Constantele caracter mai pot fi
reprezentate si n forma:
\uxxxx.
unde xxxx este un numar n baza 16 reprezentand codul Unicode al caracterului.
Constantele de tip sir de caractere sunt cuprinse ntre ghilimele, ca n Primul meu program
Java. Exista anumite secvente speciale, numite secvente escape, care sunt folosite pentru
anumite caractere speciale. Noi vom folosi mai ales
\n, \\, \ si \",
care nseamna respectiv linie nou
a, backslash, apostrof si ghilimele.

12

CAPITOLUL 2. NOT
IUNI FUNDAMENTALE DE PROGRAMARE IN JAVA

2.3.3

Declararea si initializarea tipurilor primitive n Java

Orice variabila Java, inclusiv cele primitive, sunt declarate prin descrierea numelui a tipului si, optional, a valorii initiale. Numele variabilei trebuie sa fie un identificator. Un
identificator poate sa contina orice combinatie de litere, cifre si caracterul underscore
(liniuta de subliniere). Identificatorii nu pot ncepe cu o cifra. Cuvintele rezervate, cum ar
fi int nu pot fi identificatori. Nu pot fi utilizati nici identificatorii care deja sunt declarati
si sunt vizibili.
Java este case-sensitive, ceea ce nseamna ca sum si Sum reprezinta identificatori diferiti.
In acest text vom folosi urmatoarea conventie pentru numele variabilelor:
toate numele de variabila ncep cu litera mica, iar cuvintele noi din cadrul numelui
ncep cu litera mare. De exemplu: sumaMaxima, nodVizitat etc.
numele claselor ncepe cu litera mare. De exemplu: FirstProgram, ArithmeticException, BinaryTree etc.
Alte conventii vor mai fi prezentate pe parcurs.
Iata cateva exemple de declaratii de variabile:
int numarElemente ;
double mediaGenerala ;
int produs = 1, suma = 0 ;
int produs1 = produs ;
O variabila este bine sa fie declarata imediat nainte de a fi folosita. Asa cum vom vedea
mai tarziu, locul unde este declarata determina domeniul de vizibilitate si semnificatia ei.

2.3.4

Citire/scriere de la terminal

Scrierea la terminal n Java se realizeaza cu functia println si nu pune probleme majore. Lucrurile nu stau deloc la fel cu citirea de la tastatura, care se realizeaza mult mai
anevoios. Acest lucru se datoreaza n primul rand faptului ca programele Java nu sunt
concepute pentru a citi de la tastatura. In imensa majoritate a cazurilor programele Java
si preiau datele dintr-o interfat
a grafic
a (Applet-urile), din forme HTML (Java Servlets,
Java Server Pages) sau din fisiere.
Citirea si scrierea de la consola sunt realizate prin readLine, respectiv println. Fluxul de
intrare standard este System.in iar fluxul de iesire standard este System.out.
Mecanismul de baza pentru citirea/scrierea formatata foloseste tipul String, care va fi descris n capitolul urmator. La afisare, operatorul + concateneaza doua String-uri. Pentru
tipurile primitive, daca parametrul scris nu este de tip String se face o conversie temporara
la String. Aceste conversii pot fi definite si pentru obiecte, asa cum vom arata mai tarziu.
Pentru citire se asociaza un obiect de tipul BufferedReader cu System.in. Apoi se citeste
un String care va fi ulterior prelucrat.

2.4

Operatori de baz
a

Aceasta sectiune descrie operatorii de baza care sunt disponibili n Java. Acesti operatori
sunt utilizati pentru a crea expresii. O constanta sau o variabila reprezinta o expresie,

13

2.4. OPERATORI DE BAZA

la fel ca si combinatiile de constante si variabile cu operatori. O expresie urmata de ;


reprezinta o instructiune simpl
a. In paragraful 2.5 vom prezenta alte tipuri de instructiuni,
care vor introduce noi tipuri de operatori.

2.4.1

Operatori de atribuire

Programul simplu din Figura 2.3 ilustreaza cativa operatori Java. Operatorul de atribuire
este semnul egal (=). De exemplu, n linia 16, variabilei a i se atribuie valoarea variabilei c
(care n acel moment are valoarea 6). Modificarile ulterioare ale variabilei c nu vor afecta
variabila a. Operatorii de atribuire pot fi nlantuiti ca n:
z=y=x=0.
Un alt operator de atribuire este += al carui mod de utilizare este ilustrat n linia 18.
Operatorul += adauga valoarea aflata la dreapta (operatorului) la variabila din stanga.
Astfel, valoarea lui c este incrementata de la 6 la 14. Java ofera si alti operatori de atribuire
cum ar fi -=, *= si /= care modifica variabila aflata n partea stanga prin scadere, nmultire
si respectiv mpartire.
1. public class OperatorTest
2. {
3. //program care ilustreaza operatorii de
4. //programul va afisa:
5. //12 8 6
6. //6 8 6
7. //6 8 14
8. //22 8 14
9. //24 10 33
10.
11. public static void main(String[] args)
12. {
13. int a = 12, b = 8, c = 6 ;
14.
15. System.out.println(a + " " + b + " " +
16. a = c ;
17. System.out.println(a + " " + b + " " +
18. c += b ;
19. System.out.println(a + " " + b + " " +
20. a = b + c ;
21. System.out.println(a + " " + b + " " +
22. a++ ;
23. ++b ;
24. c = a++ + ++b ;
25. System.out.println(a + " " + b + " " +
26. }
27.}

baza

c) ;
c) ;
c) ;
c) ;

c) ;

Figura 2.3 Program care ilustreaz


a anumiti operatori simpli

14

2.4.2

CAPITOLUL 2. NOT
IUNI FUNDAMENTALE DE PROGRAMARE IN JAVA

Operatori aritmetici binari

Linia 20 din Figura 2.3 ilustreaza unul dintre operatorii binari care sunt tipici pentru
limbajele de programare: operatorul de adunare (+). Operatorul + are ca efect adunarea
continutului variabilelor b si c; valorile lui b si c raman neschimbate. Valoarea rezultata
este atribuit
a lui a. Alti operatori aritmetici folositi n Java sunt: -, *, / si % utilizati
respectiv pentru sc
adere, nmultire, mp
artire si rest.
Impartirea a doua valori ntregi are ca valoare doar partea ntrega a rezultatului. De exemplu 3/2 are valoarea 1, dar 3.0/2 are valoarea 1.5.
Asa cum este si normal, adunarea si scaderea au aceeasi prioritate. Aceasta prioritate
este mai mica decat cea a grupului format din nmultire, mpartire si rest; astfel 1 + 2*3
are valoarea 7. Toti acesti operatori sunt evaluati de la stanga la dreapta (astfel 3-2-2 are
valoarea -1). Toti operatorii aritmetici au o anumita prioritate si o anumita asociere.

2.4.3

Operatori aritmetici unari

In plus fata de operatorii aritmetici binari care necesita doi operanzi, Java dispune si de
operatori unari care necesita doar un singur operand. Cel mai cunoscut operator unar
este operatorul minus (-) care returneaza operandul cu semn opus. Astfel, -x, este opusul
lui x.
Java ofera de asemenea operatorul de autoincrementare care adauga 1 la valoarea unei
variabile, notat prin ++, si operatorul de autodecrementare care scade 1 din valoarea
variabilei, notat cu - -. Un caz banal de utilizare a acestor operatori este exemplificat
n liniile 22 si 23 din Figura 2.3. In ambele cazuri operatorul ++ adauga 1 la valoarea
variabilei. In Java, ca si n C, orice expresie are o valoare. Astfel, un operator aplicat
unei variabile genereaza o expresie cu o anumita valoare. Desi faptul ca variabila este
incrementata nainte de executia urmatoarei instructiuni este garantat, se pune ntrebarea:
Care este valoarea expresiei de autoincrementare daca ea este utilizata n cadrul unei alte
expresii?
In acest caz, locul unde se plaseaza operatorul ++ este esential. Semnficatia lui ++x
este ca valoarea expresiei este egala cu noua valoare a lui x. Acest operator este numit
incrementare prefixat
a. In mod analog, x++ nseamna ca valoarea expresiei este egala cu
valoarea originala a lui x. Acesta este numit incrementare postfixat
a. Aceste trasaturi
sunt exemplificate n linia 24 din Figura 2.3. Atat a, cat si b sunt incrementate cu 1, iar
c este obtinut prin adunarea valorii initiale a lui a (care este 23) cu valoarea incrementata
a lui b (care este 10).

2.4.4

Conversii de tip

Operatorul conversie de tip, numit adeseori si operatorul de cast, este utilizat pentru a
genera o variabila temporara de un nou tip. Sa consideram, de exemplu, secventa de cod:
double rest ;
int x = 6 ;
int y = 10 ;
rest = x / y ; //mai mult ca sigur gresit!

2.5. INSTRUCT
IUNI CONDIT
IONALE

15

La efectuarea operatiei de mpartire, atat x cat si y fiind numere ntregi, se va realiza o


mpartire ntreaga si se obtine 0. Intregul 0 este apoi convertit implicit la double astfel
ncat s
a poata fi atribuit lui rest. Probabil ca intentia noastra era aceea de a atribui lui
rest valoarea 0.6. Solutia este de a converti temporar pe x sau pe y la double, pentru ca
mpartirea sa se realizeze n virgula mobila. Acest lucru se poate obtine astfel:
rest = ( double ) x / y ;
De remarcat ca nici x si nici y nu se schimba. Se creaza o variabila temporara fara nume,
avand valoarea 6.0, iar valoarea ei este utilizata pentru a efectua mpartirea. Operatorul
de conversie de tip are o prioritate mai mare decat operatorul de mpartire, de aceea
conversia de tip se efectueaza nainte de a se efectua mpartirea.

2.5

Instructiuni conditionale

Aceasta sectiune este dedicata instructiunilor care controleaza fluxul de executie al programului: instructiunile conditionale si iteratia.

2.5.1

Operatori relationali

Testul fundamental care poate fi realizat asupra tipurilor primitive este comparatia. Comparatia se realizeaza utilizand operatorii de egalitate/inegalitate si operatorii de comparatie
(<, > etc.). In Java, operatorii de egalitate/inegalitate sunt == respectiv !=. De exemplu,
exprStanga == exprDreapta
are valoarea true daca exprStanga si exprDreapta sunt egale; altfel are valoarea false.
Analog, expresia:
exprStanga != exprDreapta
are valoarea true daca exprStanga si exprDreapta sunt diferite; altfel are valoarea false.
Operatorii de comparatie sunt <, <=, >, >= iar semnficatia lor este cea naturala pentru
tipurile fundamentale. Operatorii de comparatie au prioritate mai mare decat operatorii
de egalitate. Totusi, ambele categorii au prioritate mai mica decat operatorii aritmetici,
dar mai mare decat operatorii de atribuire. Astfel, veti constata adeseori ca folosirea
parantezelor nu va fi necesara. Toti acesti operatori se asociaza de la stanga la dreapta,
dar cunoasterea acestui lucru nu ne foloseste prea mult. De exemplu, n expresia a < b < 6,
prima comparatie genereaza o valoare booleana, iar a doua expresie este gresita, deoarece
operatorul < nu este definit pentru valori booleene. Paragraful urmator descrie cum se
poate realiza acest test n mod corect.

2.5.2

Operatori logici

Java dispune de operatori logici care sunt utilizati pentru a simula operatorii and, or
si not din algebra Boolean
a. Acesti operatori mai sunt referiti uneori si sub numele de
conjunctie, disjunctie si, respectiv, negare, simbolurile corespunzatoare fiind &&, || si !.
Implementarea corecta a testului din paragraful anterior este:

16

CAPITOLUL 2. NOT
IUNI FUNDAMENTALE DE PROGRAMARE IN JAVA

a<b && b<6


Prioritatea conjunctiei si a disjunctiei este suficient de mica fata de prioritatea celorlalti
operatori din expresie pentru ca parantezele sa nu fie necesare. && are prioritate mai
mare decat ||, iar ! are aceeasi prioritate cu alti operatori unari (++, - -, vezi Figura
2.4).
Categorie
Exemple
Operatori pe referinte
. []
Unari
+ + ! (tip)
Multiplicativi
/ %
Aditivi
+
Shiftare (pe biti)
<< >> >>>
Relationali
< <= > >= instanceof
Egalitate
== ! =
AND pe biti
&
XOR pe biti

OR pe biti
|
AND logic
&&
OR logic
||
Conditional
?:
Atribuire
= = /= %= += =

Asociere
Stanga la dreapta
Dreapta la stanga
Stanga la dreapta
Stanga la dreapta
Stanga la dreapta
Stanga la dreapta
Stanga la dreapta
Stanga la dreapta
Stanga la dreapta
Stanga la dreapta
Stanga la dreapta
Stanga la dreapta
Dreapta la stanga
Dreapta la stanga

Figura 2.4 Operatori Java listati n ordinea priorit


atii3
O regula importanta este ca operatorii && si || folosesc evaluarea booleana scurtcircuitata4 .
Aceasta nseamna ca daca rezultatul poate fi determinat evaluand prima expresie, a doua
nu mai este evaluata. De exemplu, n expresia:
x !=0 && 1/x != 3
daca x este 0, atunci prima jumatate este false. Aceasta nsemna ca rezultatul conjunctiei
va fi fals, deci a doua expresie nu mai este evaluata. Acesta este un detaliu important,
deoarece mp
artirea la 0 ar genera un comportament eronat.

2.5.3

Instructiunea if

Instructiunea if este instructiunea fundamentala de decizie. Forma sa simpla este:


if( expresie )
instructiune
urmatoarea instructiune
Daca expresie are valoarea true atunci se executa instructiune; n caz contrar instructiune
nu se executa. Dupa ce instructiunea if se ncheie (fara o exceptie netratata), controlul
este preluat de urm
atoarea instructiune.
Optional, putem folosi instructiunea if-else dupa cum urmeaza:
3
Prin asociere ntelegem odinea de evaluare ntr-o expresie care contine operatori de acelasi tip si nu
are paranteze.
4
Numit
a uneori si evaluare boolean
a partial
a

2.5. INSTRUCT
IUNI CONDIT
IONALE

17

if( expresie )
instructiune1
else
instructiune2
urmatoarea instructiune
In acest caz, daca expresie are valoarea true, atunci se executa instructiune1; altfel se
executa instructiune2. In ambele cazuri controlul este apoi preluat de urmatoarea instructiune. Iata un exemplu:
System.out.println("1/x este") ;
if( x != 0 )
System.out.print( 1/x ) ;
else
System.out.print( "Nedefinit" ) ;
System.out.println() ;
De retinut ca doar o singura instructiune poate exista pe ramura de if sau de else indiferent
de cum indentati codul. Iata doua erori frecvente la ncepatori:
if( x == 0 ) ; //instructiune vida!!!
System.out.println( "x este 0" ) ;
else
System.out.print( "x este" ) ;
System.out.println(x) ; // a doua instructiune nu
// face parte din else
Prima gresala consta n a pune ; dupa if. Simbolul ; reprezinta n sine instructiunea vid
a;
ca o consecinta, acest fragment de cod nu va fi compilabil (else nu va fi asociat cu nici
un if). Dupa ce am corectat aceasta eroare, ramanem cu o eroare de logica: ultima linie
de cod nu face parte din if, desi acest lucru este sugerat de indentare. Pentru a rezolva
aceasta problema vom utiliza un bloc n care grupam o secventa de instructiuni printr-o
pereche de acolade:
if( x == 0 )
{
System.out.println( "x este 0" ) ;
}
else
{
System.out.print( "x este" ) ;
System.out.println( x ) ;
}
Observati ca am folosit acolade si pe ramura de if desi acestea nu sunt absolut necesare.
Aceasta practica mbunatateste foarte mult lizibilitatea codului si va invitam si pe dumneavoastra sa o adoptati.
Instructiunea if poate sa faca parte dintr-o alta instructiune if sau else, la fel ca si celelalte
instructiuni de control prezentate n continuare n aceasta sectiune.

18

2.5.4

CAPITOLUL 2. NOT
IUNI FUNDAMENTALE DE PROGRAMARE IN JAVA

Instructiunea while

Java, ca si Pascal sau C, dispune de trei instructiuni de ciclare: instructiunea while,


instructiunea do si instructiunea for. Sintaxa instructiunii while este:
while( expresie )
instructiune
urmatoarea instructiune
Observati ca, la fel ca si la instructiunea if, nu exista ; n sintaxa. Daca apare un ; , dupa
while, va fi considerat ca instructiune vida.
Cat timp expresie este true se executa instructiune; apoi expresie este evaluata din nou.
Daca expresie este false de la bun nceput, atunci instructiune nu va fi executata niciodata.
In general, instructiune face o actiune care ar putea modifica valoarea lui expresie; n
caz contrar, ciclarea s-ar putea produce la infinit. Cand instructiunea while se ncheie,
controlul este preluat de urmatoarea instructiune.

2.5.5

Instructiunea for

Instructiunea while ar fi suficienta pentru a exprima orice fel de ciclare. Totusi, Java
mai ofera nca doua forme de a realiza ciclarea: instructiunea for si instructiunea do.
Instructiunea for este utilizata n primul rand pentru a realiza iteratia. Sintaxa ei este:
for( initializare; test; actualizare)
instructiune
urmatoarea instructiune
In aceasta situatie, initializare, test si actualizare sunt toate expresii, si toate trei sunt
optionale. Daca test lipseste, valoarea sa implicita este true. Dupa paranteza de nchidere
nu se pune ;.
Instructiunea for se executa realizand mai ntai initializare. Apoi, cat timp test este
true, au loc urmatoarele doua instructiuni: se executa instructiune, iar apoi se executa
actualizare. Daca initializare si actualizare sunt omise, instructiunea for se va comporta
exact ca si instructiunea while.
Avantajul instructiunii for consta n faptul ca se poate vedea clar marja pe care itereaza
variabilele contor.
Urmatoarea secventa de cod afiseaza primele 100 numere ntregi pozitive:
for( int i=1; i<100; ++i)
{
System.out.println( i );
}
Acest fragment ilustreaza si practica obisnuita pentru programatorii Java (si C++) de a
declara un contor ntreg n secventa de initializare a ciclului. Durata de viata a acestui
contor se extinde doar n interiorul ciclului.
Atat initializare cat si actualizare pot folosi operatorul virgul
a pentru a permite expresii
multiple. Urmatorul fragment ilustreaza aceasta tehnica frecvent folosita:

2.5. INSTRUCT
IUNI CONDIT
IONALE

19

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


{
System.out.println( i + "\t" + sum) ;
}
Ciclurile pot fi imbricate la fel ca si instructiunile if. De exemplu, putem gasi toate
perechile de numere mici a caror suma este egala cu produsul lor (cum ar fi 2 si 2, a caror
suma si produs este 4) folosind secventa de cod de mai jos:
for( int i = 1; i <= 10; i++)
{
for(int j = 1; j <= 10; j++)
{
if( i+j == i*j )
{
System.out.println( i + ", " + j) ;
}
}
}

2.5.6

Instructiunea do

Instructiunea while realizeaza un test repetat. Daca testul este true atunci se execut
a
instructiunea din cadrul ei. Totusi, daca testul initial este false, instructiunea din cadrul
ciclului nu este executata niciodata. In anumite situatii avem nevoie ca instructiunile din
ciclu sa se execute cel putin o data. Acest lucru se poate realiza utilizand instructiunea
do. Instructiunea do este asemanatoare cu instructiunea while, cu deosebirea ca testul este
realizat dupa ce instructiunile din corpul ciclului se executa. Sintaxa sa este:
do
instructiune
while( expresie ) ;
urmatoarea instructiune ;
Remarcati faptul ca instructiunea do se termina cu ;. Un exemplu tipic n care se utilizeaz
a
instructiunea do este dat de fragmentul de (pseudo-) cod de mai jos:
do
{
afiseaza mesaj ;
citeste data ;
}while( data nu este corecta ) ;
Instructiunea do este instructiunea de ciclare cel mai putin utilizata. Totusi, atunci cand
vrem sa executam ceva cel putin o data ciclul, si for nu poate fi utilizat, atunci do este
alegerea potrivita.

20

2.5.7

CAPITOLUL 2. NOT
IUNI FUNDAMENTALE DE PROGRAMARE IN JAVA

Instructiunile break si continue

Instructiunile for si while au conditia de terminare naintea instructiunilor care se repeta.


Instructiunea do are conditia de terminare dupa instructiunile care se repeta. Totusi, n
anumite situatii, s-ar putea sa fie nevoie sa ntrerupem ciclul n mijlocul instructiunilor
care se repeta. In acest scop, se poate folosi instructiunea break. De obicei, instructiunea
break apare n cadrul unei instructiuni if, ca n exemplul de mai jos:
while(...)
{
...
if( conditie )
{
break ;
}
...
}
In cazul n care sunt doua cicluri imbricate, instructiunea break paraseste doar ciclul cel
mai din interior. Daca exista mai mult de un ciclu care trebuie terminat, break nu va
functiona corect, si mai mult ca sigur ca ati proiectat prost algoritmul. Totusi, Java ofera
asa numitul break etichetat. In acest caz, o anumita instructiune de ciclare este etichetata
si instructiunea break poate fi aplicata acelei instructiuni de ciclare, indiferent de numarul
de cicluri imbricate. Iata un exemplu:
eticheta:
while(...)
{
while(...)
{
...
if( conditie )
{
break eticheta;
}
}
}
//controlul programului trece aici dupa executia lui break
In anumite situatii dorim sa renuntam la executia iteratiei curente din ciclu si sa trecem la
urmatoarea iteratie a ciclului. Acest lucru poate fi realizat cu instructiunea continue. Ca
si break, instructiunea continue este urmata de ; si se aplica doar ciclului cel mai interior
n cazul ciclurilor imbricate. Urmatorul fragment tipareste primele 100 de numere ntregi,
cu exceptia celor divizibile cu 10:
for( int i = 1 ; i <= 100; i++)
{
if( i % 10 == 0)

2.5. INSTRUCT
IUNI CONDIT
IONALE

21

{
continue ;
}
System.out.println( i ) ;
}
Desigur, ca exemplul de mai sus poate fi implementat si utilizand un if simplu. Totusi
instructiunea continue este adeseori folosita pentru a evita imbricari complicate de tip
if-else n cadrul ciclurilor.

2.5.8

Instructiunea switch

Instructiunea switch este numita uneori si instructiune de selectie. Instructiunea switch


selecteaza dintre mai multe secvente de cod, una care va fi executata, functie de valoarea
unei expresii ntregi. Forma sa este:
switch( expresie-selectare )
{
case valoare-intreaga1: instructiune ; break ;
case valoare-intreaga2: instructiune ; break ;
//...
case valoare-intreagan: instructiune ; break ;
default: instructiune ;
}
expresie-selectare este o expresie care produce o valoare ntreaga. Instructiunea switch
compara valoarea lui expresie-selectare cu fiecare valoare-intreag
a. Daca are loc egalitatea
se executa instructiunea corespunzatoare (simpla sau compusa). Daca nu are loc nici o
egalitate se executa instructiunea din default.
Observati ca n exemplul de mai sus, fiecare case se ncheie cu un break care are ca efect
saltul la sfarsitul instructiunii switch. Acesta este modul obisnuit de a scrie o instructiune
de tip switch, dar prezenta instructiunii break este optionala. Daca instructiunea break
lipseste, atunci se va executa si codul corespunzator instructiunilor case urmatoare pan
a
cand se ntalneste un break. Desi de obicei nu ne dorim un astfel de comportament, el
poate fi uneori util pentru programatorii experimentati.
Programul de mai jos creaza litere aleator si determina daca acestea sunt vocale sau
consoane (n limba engleza):
1. //VowelsAndConsonants.java
2. // Program demonstrativ pentru instructiunea switch
3. public class VowelsAndConsonants
4. {
5. public static void main(String[] args)
6. {
7.
for(int i = 0; i < 100; i++)
8.
{
9.
char c = (char)(Math.random() * 26 + a);
10.
System.out.print(c + ": ");

22

CAPITOLUL 2. NOT
IUNI FUNDAMENTALE DE PROGRAMARE IN JAVA

11.
switch(c)
12.
{
13.
case a:
14.
case e:
15.
case i:
16.
case o:
17.
case u:
18.
System.out.println("vocala");
19.
break;
20.
case y:
21.
case w:
22.
System.out.println(
23.
"Uneori vocale "); //doar in limba engleza!!!
24.
break;
25.
default:
26.
System.out.println("consoana");
27.
} //switch
28. } //for
29. } //main
30.} //class
Figura 2.5 Instructiunea switch
Functia Math.random() genereaza o valoare n intervalul [0,1). Prin nmultirea valorii
returnate de aceasta functie cu numarul de litere din alfabet (26 litere) se obtine un numar
n intervalul [0,26). Adunarea cu prima litera (a, care are de fapt valoarea 97) are ca
efect transpunerea n intervalul [97,123). In final se foloseste operatorul de conversie de
tip pentru a trunchia numarul la o valoare din multimea 97,98,...,122, adica un cod ASCII
al unui caracter din alfabetul englez .

2.5.9

Operatorul conditional

Operatorul coditional este folosit ca o prescurtare pentru instructiuni simple de tipul if-else.
Forma sa generala este:
exprTest ? expresieDa : expresieNu ;
Mai ntai se evalueaza exprTest urmata fie de expresieDa fie de expresieNu, rezultand
astfel valoarea ntregii expresii. expresieDa este evaluata daca exprTest are valoarea true;
n caz contrar se evalueaza expresieNu. Prioritatea operatorului conditional este chiar
deasupra operatorilor de atribuire. Acest lucru permite omiterea parantezelor atunci cand
asignam rezultatul operatorului conditional unei variabile. Ca un exemplu, minimul a
doua variabile poate fi calculat dupa cum urmeaza:
valMin =

x < y ? x : y ;

2.6. METODE

2.6

23

Metode

Ceea ce n alte limbaje de programare numeam procedura sau functie, n Java este numit
metod
a. O definitie mai exacta si completa a notiunii de metoda o vom da mai tarziu. In
acest paragraf prezentam doar cateva notiuni elementare pentru a putea scrie functii de
genul celor din C sau Pascal pe care sa le folosim n cateva programe simple.
Antetul unei metode consta dintr-un nume, o lista (eventual vida) de parametri si un tip
pentru valoarea returnata. Codul efectiv al metodei numit adeseori corpul metodei este
un bloc (o secventa de instructiuni cuprinsa ntre acolade). Definirea unei metode const
a
n antet si corp. Un exemplu de metoda si de o functie main care o utilizeaza este dat n
Figura 2.6.
Prin prefixarea metodelor cu ajutorul cuvintelor cheie public static putem mima ntro oarecare masura functiile din Pascal si C. Desi aceasta tehnica este utila n anumite
situatii, ea nu trebuie utilizata n mod abuziv. Numele metodei este un identificator.
Lista de parametri consta din 0 sau mai multi parametri formali, fiecare avand un tip
precizat. Cand o metoda este apelata, parametrii actuali sunt trecuti n parametrii formali utilizand atribuirea obisnuita. Aceasta nsemna ca tipurile primitive sunt transmise
utilizand exclusiv transmiterea prin valoare. Parametrii actuali nu vor putea fi modificati
de catre functie. Definirile metodelor pot aparea n orice ordine.
Instructiunea return este utilizata pentru a ntoarce o valoare catre codul apelant. Dac
a
tipul functiei este void atunci nu se ntoarce nici o valoare si se foloseste
return ;
fara nici un parametru.
1. public class Minim
2. {
3. public static void main( String[] args )
4. {
5.
int a = 3 ;
6.
int b = 7 ;
7.
System.out.println( min(a,b) ) ;
8. }
9. //declaratia metodei min
10. public static int min( int x, int y )
11. {
12. return x < y ? x : y ;
13. }
14.}
Figura 2.6 Declararea si apelul unei metode

2.6.1

Supranc
arcarea numelor la metode

Sa presupunem ca dorim sa scriem o metoda care calculeaza maximul a trei numere ntregi.
Un antet pentru aceasta metoda ar fi:
int max(int a, int b, int c)

24

CAPITOLUL 2. NOT
IUNI FUNDAMENTALE DE PROGRAMARE IN JAVA

In unele limbaje de programare (Pascal, C), acest lucru nu ar fi permis daca exista deja
o functie max cu doi parametri. De exemplu, se poate sa avem deja declarata o metoda
max cu antetul:
int max(int a, int b)
Java permite supranc
arcarea (engl. overloading) numelui metodelor. Aceasta nsemna ca
mai multe metode pot fi declarate n cadrul aceleiasi clase atata timp cat semn
aturile lor
(adica lista de parametri) difera. Atunci cand se face un apel al metodei max compilatorul
poate usor sa deduca despre care metoda este vorba examinand lista parametrilor de apel.
Se poate sa existe metode suprancarcate cu acelasi numar de parametri formali atata
timp cat cel putin unul din tipurile din lista de parametri este diferit.
De retinut faptul ca tipul functiei nu face parte din semnatura ei. Aceasta nseamna ca
nu putem avea doua metode n cadrul aceleiasi clase care sa difere doar prin tipul valorii
returnate. Metode din clase diferite pot avea acelasi nume, parametri si chiar tip returnat;
despre aceasta vom discuta pe larg mai tarziu.

2.7

Probleme propuse

1. Scrieti o instuctiune while echivalenta cu ciclul for de mai jos. La ce ar putea fi


utilizat un astfel de ciclu?
for( ; ; )
instructiune
2. Scrieti un program care genereaza tabelele pentru nmultirea si adunarea numerelor
cu o singura cifra.
3. Scrieti doua metode statice. Prima sa returneze maximul a trei numere ntregi, a
doua maximul a patru numere ntregi .
4. Scrieti o metoda statica care primeste ca parametru un an si returneaza true daca
anul este bisect si false n caz contrar.

Capitolul 3

Referinte
In capitolul 1 am prezentat tipurile primitive din Java. Toate tipurile care nu sunt ntre
cele opt primitive, inclusiv tipurile importante cum ar fi stringurile, sirurile si fisierele sunt
tipuri referint
a.
In acest capitol vom nvata:
Ce este un tip referint
a si ce este o variabil
a referint
a
Prin ce difer
a un tip referint
a de un tip primitiv
Exemple de tipuri referint
a incluz
and stringuri, siruri si fluxuri
Cum sunt utilizate exceptiile pentru a semnala comportamentul gresit al
unei secvente de cod

3.1

Ce este o referint
a

In capitolul 1 am examinat cele opt tipuri primitive, mpreuna cu cateva operatii care pot
fi realizate pe aceste tipuri. Toate celelalte tipuri de date din Java sunt referinte. Ce este
deci o referinta? O variabil
a referint
a n Java (numita adeseori simplu referint
a) este o
variabila care retine adresa de memorie la care se afla un anumit obiect.
point1
H
j
H

point3
*



point2
*



1000
1024
3200
3600
5124

(0,0)
(5,12)
point2 = 1024
point1 = 1000
point3 = 1000

Figura 3.1 Ilustrarea unei referinte: Obiectul de tip Point stocat la adresa de memorie
1000 este referit at
at de c
atre point1 c
at si de c
atre point3. Obiectul de tip Point stocat la
adresa 1024 este referit de c
atre point2. Locatiile de memorie unde sunt retinute variabilele
au fost alese arbitrar.
Ca un exemplu, n Figura 3.1 exista doua obiecte de tipul Point1 . Presupunem ca aceste
obiecte au fost stocate la adresele de memorie 1000 si respectiv 1024. Pentru aceste dou
a
1

Care contin coordonatele unui punct din plan.

25

26

CAPITOLUL 3. REFERINT
E

obiecte am definit trei referinte, point1, point2 si point3. Atat point1 cat si point3 refera
obiectul stocat la adresa 1000; point2 refera obiectul stocat la adresa 1024; aceasta nsemna
ca atat point1 cat si point3 au valoarea 1000, iar point2 va avea valoarea 1024. Retineti
ca locatiile efective, cum ar fi 1000 si 1024, sunt atribuite de compilator la discretia sa
(unde gaseste memorie libera). In consecinta, aceste valori nu sunt utile efectiv ca valori
numerice. Totusi, faptul ca point1 si point3 au aceeasi valoare este folositor: nseamna
ca ele refera acelasi obiect. O referinta stocheaza ntotdeauna adresa la care un anumit
obiect se afla, cu exceptia situatiei cand nu refera nici un obiect. In aceasta situatie va
stoca referinta nul
a, notata cu null. Java nu permite referinte catre tipurile primitive.
Exista doua mari tipuri de operatii care se pot aplica variabilelor referinta.
1. Prima categorie permite examinarea si manipularea valorii referinta. De exemplu,
daca modificam valoarea stocata n point1 (care este 1000), putem sa facem ca point1
sa refere un alt obiect. Putem de asemenea compara point1 si point3 pentru a vedea
daca refera acelasi obiect.
2. A doua categorie de operatii se aplica obiectului care este referit; am putea de
exemplu examina sau modifica starea unuia dintre obiectele de tipul Point (am
putea examina coordonatele x si y ale unui obiect de tipul Point).
Inainte de a descrie ce se poate face cu ajutorul referintelor, sa descriem ce nu se poate
face. Sa consideram expresia point1*point2. Deoarece valorile retinute de point1 si point2
sunt respectiv 1000 si 1024, produsul lor ar fi 1024000. Totusi acest calcul este lipsit de
sens si nu ar putea avea nici o valoarea practica. Variabilele referinta retin adrese, si nu
poate fi asociata nici o semnificatie logica nmultirii adreselor.
Analog, point1++ nu are nici un sens n Java; ar sugera ca point1 - care are valoarea
1000 - sa fie crescut la 1001, dar n acest caz nu ar mai referi un obiect valid. Multe alte
limbaje de programare definesc notiunea de pointer care are un comportament similar cu
cel al unei variabile referinta. Totusi, pointerii n C sunt mult mai periculosi, deoarece
este permisa aritmetica pe adresele stocate. In plus, deoarece C permite pointeri si catre
tipurile fundamentale trebuie avut grija pentru a distinge ntre aritmetica pe adrese si
aritmetica pe variabilele care sunt referite. Acest lucru se face prin dereferentierea explicita
a pointerului. In practica, pointerii limbajului C tind sa provoace numeroase erori greu
detectabile, care uneori i pot face si pe programatorii experimentati sa planga de ciuda!
In Java, singurele operatii care sunt permise asupra referintelor (cu o singura exceptie
pentru Stringuri) sunt atribuirea via = si comparatia via == sau !=. De exemplu, prin
atribuirea lui point3 a valorii lui point2, vom face ca point3 sa refere acelasi obiect pe care l
refera point2. Acum expresia point2 == point3 este adevarata, deoarece ambele referinte
stocheaza valoarea 1024 si refera deci acelasi obiect. point1 != point2 este de asemenea
adevarata, deoarece point1 si point2 refera acum obiecte distincte.
Cealalta categorie de operatii se refera la obiectul care este referit. Exista doar trei actiuni
fundamentale care pot fi realizate:
1. Aplicarea unei conversii de tip
2. Accesul la un camp al obiectului sau apelul unei metode prin operatorul punct (.)
3. Utilizarea operatorului instanceof pentru a verifica daca obiectul retinut are un anumit tip.

3.2. FUNDAMENTE DESPRE OBIECTE S


I REFERINT
E

27

Sectiunea urmatoare ilustreaza mai detaliat operatiile pe referinte.

3.2

Fundamente despre obiecte si referinte

In Java un obiect este orice variabila de un tip ne-primitiv. Obiectele sunt tratate diferit
fata de tipurile primitive. Variabilele de tipuri primitive sunt manipulate prin valoare,
ceea ce nsemna ca valorile lor sunt retinute n acele variabile si sunt copiate dintr-o variabila primitiva n alta variabila primitiva n timpul instructiunii de atribuire. Dupa cum
am aratat n sectiunea anterioara, variabilele referinta stocheaza referinte catre obiecte.
Obiectul n sine este stocat undeva n memorie, iar variabila referinta stocheaza adresa
de memorie a obiectului. Astfel, variabila referinta nu este decat un nume pentru acea
zona de memorie. Aceasta nsemna ca variabilele primitive si cele referinta vor avea un
comportament diferit. Aceasta sectiune examineaza mai n detaliu aceste diferente si ilustreaza operatiile care sunt permise pe tipurile referinta.

3.2.1

Operatorul punct (.)

Operatorul punct (.) este folosit pentru a selecta o metoda care se aplica unui obiect. De
exemplu, sa presupunem ca avem un obiect de tip Cerc care defineste metoda arie. Dac
a
variabila unCerc este o referinta catre un Cerc, atunci putem calcula aria (si salva aceast
a
arie ntr-o variabila de tip double) cercului referit astfel:
double arieCerc = unCerc.arie() ;
Este posibil ca variabila unCerc sa retina referinta null. In acest caz, aplicarea operatorului
punct va genera o exceptie de tipul NullPointerException la executia programului. De
obicei aceast
a exceptie va determina terminarea anormala a programului.
Operatorul punct poate fi folosit si pentru a accesa componentele individuale ale unui
obiect, daca cel care a proiectat obiectul permite acest lucru. Capitolul urmator descrie
cum se poate face acest lucru; tot acolo vom explica de ce n general este preferabil ca s
a
nu se permit
a accesul direct la componentele individuale ale unui obiect.

3.2.2

Declararea obiectelor

Am vazut deja care este sintaxa pentru declararea variabilelor primitive. Pentru obiecte
exista o diferenta importanta. Atunci cand declaram o referinta, nu facem decat s
a
furnizam un nume care poate fi utilizat pentru a referi un obiect stocat n prealabil n
memorie. Totusi, declaratia n sine nu furnizeaza si acel obiect. Sa presupunem, de exemplu, ca avem un obiect de tip Cerc caruia dorim sa i calculam aria folosind metoda arie().
Sa consideram secventa de instructiuni de mai jos:
Cerc unCerc ;
//unCerc poate referi un obiect de tip Cerc
double arieCerc = unCerc.arie() ; //calcul arie pentru cerc
Totul pare n regula cu aceste instructiuni, pana cand ne aducem aminte ca unCerc este
numele unui obiect oarecare de tip Cerc, dar nu am creat nici un cerc efectiv. In consecinta,
dupa ce se declara variabila unCerc, aceasta va contine valoarea null, ceea ce nseamna c
a

28

CAPITOLUL 3. REFERINT
E

unCerc nca nu refera un obiect Cerc valid. Aceasta nseamna ca a doua linie de program
este invalida, deoarece ncercam sa calculam aria unui cerc care nca nu exista. In exemplul
de fata chiar compilatorul va detecta eroarea, afirmand ca unCerc nu este initializat.
In alte situatii mai complexe compilatorul nu va putea detecta eroarea si se va genera o
NullPointerException n timpul executiei programului.
Singura posibilitate (normala) de a aloca memorie unui obiect este folosirea cuvantului
cheie new. new este folosit pentru a construi un nou obiect. O posibilitate de a face acest
lucru este:
Cerc unCerc ;
//unCerc poate referi un obiect de tip Cerc
unCerc = new Cerc() ; //acum unCerc refera un obiect alocat
double arieCerc = unCerc.arie() ; //calcul arie pentru cerc
Remarcati parantezele care se pun dupa numele obiectului.
Adeseori programatorii combina declararea si initializarea obiectului ca n exemplul de
mai jos:
Cerc unCerc = new Cerc() ; //acum unCerc refera un obiect alocat
double arieCerc = unCerc.arie() ; //calcul arie pentru cerc
Multe obiecte pot fi de asemenea construite cu anumite valori initiale. De exemplu, obiectul de tip Cerc ar putea fi construit cu trei parametri, doi pentru coordonatele centrului
si unul pentru lungimea razei.
Cerc unCerc = new Cerc(0,0,10) ; //cerc cu centru in origine si de raza 10
double arieCerc = unCerc.arie() ; //calcul arie pentru cerc

3.2.3

Colectarea de gunoaie (garbage collection)

Deoarece toate obiectele trebuie construite, ne-am putea astepta ca atunci cand nu mai
este nevoie de ele sa trebuiasca sa le distrugem. Totusi, n Java, cand un obiect din
memorie nu mai este referit de nici o variabila, memoria pe care o consuma va fi eliberata
automat. Aceasta tehnica se numeste colectare de gunoaie.

3.2.4

Semnificatia lui =

Sa presupunem ca avem doua variabile de tipuri primitive x si y. Atunci, semnificatia


instructiunii de atribuire
x = y ;
este simpla: valoarea stocata n y este stocata n variabila primitiva x. Modificarile ulterioare ale lui x sau y nu au efecte asupra celeilalte.
Pentru obiecte, semnificatia lui = este aceeasi: se copiaza valorile stocate. Daca x si y
sunt referinte (de tipuri compatibile), atunci, dupa operatia de atribuire, x va referi acelasi
obiect ca si y. Ceea ce se copiaza n acest caz sunt adrese. Obiectul pe care x l referea
nainte nu mai este referit de x. Daca x a fost singura referinta catre acel obiect, atunci
obiectul nu mai este referit acum de nici o variabila si este disponibil pentru colectarea de
gunoaie. Retineti faptul ca obiectele nu se copiaza.

29

3.2. FUNDAMENTE DESPRE OBIECTE S


I REFERINT
E

Iata cateva exemple. Sa presupunem ca dorim sa cream doua obiecte de tip Cerc pentru
a calcula suma ariilor lor. Cream mai ntai obiectul cerc1, dupa care ncercam sa cream
obiectul cerc2 prin modificarea lui cerc1 dupa cum urmeaza (vezi si Figura 3.2):
Cerc cerc1 = new Cerc(0,0,10) ; //un cerc de raza 10
Cerc cerc2 = cerc1 ;
cerc2.setRaza(20) ; // modificam raza la 20 ;
double arieCercuri = cerc1.arie() + cerc2.arie() ; //calcul arie

cerc1
cerc2

H
j
H

cerc2 Cerc(0,0,10)
H
j
H

cerc1 Cerc(0,0,20)





Cerc(0,0,10)

cerc1
H
Y
H

*



Figura 3.2 cerc1 si cerc2 indic


a acelasi obiect. Modificarea razei lui cerc2 implic
a si
modificarea razei lui cerc1.
Acest cod nu va functiona corect, deoarece nu s-a construit decat un singur obiect de tip
Cerc. Astfel, cea de-a doua instructiune nu face decat sa spuna ca cerc2 este un alt nume
pentru cerc1, construit anterior. Cercul construit n prima linie are acum doua nume. A
treia instructiune modifica raza cercului la 20, dar de fapt se modifica raza unicului cerc
creat, deci ultima linie aduna aria aceluiasi cerc de raza 20.
Secventa de cod corecta ar fi:
Cerc cerc1 = new Cerc(0,0,10) ; //un cerc de raza 10
Cerc cerc2 = new Cerc() ;
cerc2.setRaza(20) ; // modificam raza la 20 ;
double arieCercuri = cerc1.arie() + cerc2.arie() ; //calcul arie
La o prima vedere, faptul ca obiectele nu pot fi copiate, pare sa fie o limitare severa. In
realitate nu este deloc asa, desi ne trebuie un pic de timp pentru a ne obisnui cu acest
lucru. Exista totusi anumite situatii cand chiar trebuie sa copiem obiecte; n aceste situatii
se va folosi metoda clone(). clone() foloseste new pentru a crea un nou obiect duplicat.
Totusi, n acesta lucrare metoda clone() nu este folosita.

3.2.5

Transmiterea de parametri

Din cauza ca apelul se face prin valoare, parametri actuali (de apel) se transpun n
parametri formali folosind atribuirea obisnuita. Daca parametrul trimis este un tip referint
a,
atunci stim deja ca prin atribuire atat parametrul formal, cat si parametrul de apel vor
referi acelasi obiect. Orice metoda aplicata parametrului formal este astfel implicit aplicat
a

si parametrului de apel. In alte limbaje de programare acest tip de apel se numeste apelare
prin referint
a. Utilizarea acestei notiuni n Java ar fi oarecum nepotrivita, deoarece ne-ar
putea face sa credem ca transmiterea referintelor s-ar face n mod diferit. In realitate,
transmiterea parametrilor nu s-a modificat; ceea ce s-a modificat sunt parametrii n sine,
care nu mai sunt tipuri primitive, ci tipuri referinta.

30

3.2.6

CAPITOLUL 3. REFERINT
E

Semnificatia lui ==

Pentru tipurile primitive operatia == are valoarea true daca au valori identice. Pentru
tipuri referinta semnificatia lui == este diferita, dar perfect consistenta cu discutia din
paragraful anterior. Doua variabile referinta sunt egale via == daca ele refera acelasi
obiect (s-au ambele sunt null). Sa consideram urmatorul exemplu:
Cerc cerc1 = new Cerc(0,0,10) ; //un cerc de raza 10
Cerc cerc2 = new Cerc(0,0,10) ; //un alt cerc tot de raza 10
Cerc cerc3 = cerc2 ;
In acest caz avem doua obiecte. Primul este cunoscut sub numele de cerc1, al doilea este
cunoscut sub doua nume: cerc2 si cerc3. Expresia cerc2 == cerc3 este adevarata. Totusi,
desi cerc1 si cerc2 par sa refere obiecte care au aceeasi valoare, expresia cerc1 == cerc2
este falsa, deoarece ele refera obiecte diferite. Aceleasi reguli se aplica si pentru operatorul
!=.
Cum facem nsa pentru a vedea daca obiectele referite sunt identice? De exemplu, cum
putem sa verificam faptul ca cerc1 si cerc2 refera obiecte Cerc care sunt egale? Obiectele
pot fi comparate folosind metoda equals. Vom vedea n curand un exemplu de folosire a
lui equals, n care vom discuta despre tipul String (paragraful 3.3). Fiecare obiect are o
metoda equals, care, n mod implicit, nu face altceva decat testul ==. Pentru ca equals sa
functioneze corect, programatorul trebuie sa redefineasca aceasta metoda pentru obiectele
pe care le creaza.

3.2.7

Supranc
arcarea operatorilor pentru obiecte

In afara unei singure exceptii pe care o vom discuta n paragraful urmator, operatorii nu
pot fi definiti pentru a lucra cu obiecte2 . Astfel, nu exista operatorul < pentru nici un
fel de obiect. Pentru acest scop, va trebui definita o metoda, cum ar fi lessThan, care va
realiza comparatia.

3.3

S
iruri de caractere (stringuri)

Sirurile de caractere n Java sunt definite folosind obiectul String. Limbajul Java face sa
para ca String este un tip primitiv, deoarece pentru el sunt definiti operatorii + si +=
pentru concatenare. Totusi, acesta este singurul tip referinta pentru care Java a permis
suprancarcarea operatorilor. In rest, String se comporta ca orice alt obiect.

3.3.1

Fundamentele utiliz
arii stringurilor

Exista doua reguli fundamentale referitoare la obiectele de tip String. Prima este aceea ca,
exceptand operatorul de concatenare, obiectele de tip String se comporta ca toate celelalte
obiecte. A doua regula este aceea ca stringurile sunt ne-modificabile. Aceasta nseamna
ca, odata construit, un obiect de tip String nu mai poate fi modificat. Deoarece obiectele
2
Aceasta este o diferent
a notabil
a ntre Java si C++, care permite supranc
arcarea operatorilor pentru
obiecte. Inginerii de la Sun au considerat c
a supranc
arcarea operatorilor pentru obiecte aduce mai multe
probleme dec
at beneficii si au decis ca Java s
a nu permit
a acest lucru

3.3. S
IRURI DE CARACTERE (STRINGURI)

31

de tip String nu se pot modifica, putem folosi linistiti operatorul = pentru ele. Iata un
exemplu:
String vid = "" ;
String mesaj = "Salutare!" ;
String repetat = mesaj ;
Dupa aceste declaratii, exista doua obiecte de tip String. Primul este un sir vid, si este
referit de variabila vid. Al doilea este sirul Salutare!, care este referit de variabilele mesaj
si repetat. Pentru majoritatea obiectelor, faptul ca obiectul este referit de doua variabile
ar putea genera probleme. Totusi, deoarece stringurile nu pot fi modificate, partajarea lor
nu pune nici un fel de probleme. Singura posibilitate de a modifica valoarea catre care
refera variabila repetat este aceea de a construi un nou obiect de tip String si a-l atribui
lui repetat. Aceasta operatie nu va avea nici un efect asupra valorii pe care o refera mesaj.

3.3.2

Concatenarea stringurilor

Java nu permite suprancarcarea operatorilor pentru tipurile referinta. Totusi, pentru


comoditate, se acorda o exceptie speciala pentru concatenarea de obiecte de tipul String.
Atunci cand cel putin unul dintre operanzi este de tip String, operatorul + realizeaz
a
concatenarea. Rezultatul este o referinta catre un obiect nou construit de tip String. Iat
a
cateva exemple:
"Sunt" + " curajos!"
2 + " mere"
"mere " + 2
"a" + "b" + "c"

//rezulta "Sunt curajos!"


//rezulta "2 mere"
//rezulta "mere 2"
//rezulta "abc"

Sirurile de caractere formate dintr-un singur caracter NU trebuie nlocuite cu constante


de tip caracter (constantele caracter sunt de fapt numere).
Java dispune si de operatorul += pentru siruri de caractere. Efectul instructiunii str+=
expr este str = str + expr. Cu alte cuvinte str va referi un nou String generat de str +
expr.
Este important sa observam ca ntre atribuirea:
i=i+5

//i este un intreg

si atribuirea:
str=str+"hello"

//str este un String

exista o diferenta esentiala. In primul caz, variabila i este incrementata cu 5; locatia de


memorie a lui i nu se modifica. In al doilea caz, se creaza un nou string avand valoarea
str+hello. Dupa atribuire, str va referi acest nou string. Fostul string referit va fi supus
lui garbage-collection daca nu a existat o alta referinta catre el.

32

3.3.3

CAPITOLUL 3. REFERINT
E

Comparatia stringurilor

Deoarece operatorul de atribuire functioneaza pe siruri de caractere, am fi tentati sa


credem ca functioneaza si operatorii relationali. Acest lucru nu este nsa adevarat.
Conform regulii privind suprancarcarea operatorilor, operatorii relationali ( <, <=, >,
>=) nu sunt definiti pentru obiecte de tip String. Mai mult, operatorii == si != au
semnificatia clasica pentru obiecte de tip referinta. De exemplu, pentru doua obiecte de
tip String, x si y, expresia x == y este adevarata doar daca x si y refera acelasi obiect de
tip String. Astfel, daca x si y refera obiecte diferite cu continut identic, expresia x == y
este falsa. Acelasi rationament este valabil si pentru !=.
Pentru a testa egalitatea a doua obiecte de tip String, se foloseste metoda equals. Expresia
x.equals(y) este adevarata daca sirurile de caractere referite de x si de y sunt identice.
Un test mai general poate fi realizat cu metoda compareTo. Expresia x.compareTo(y)
compara doua obiecte de tip String x si y. Valoarea returnata este un numar negativ, zero
sau un numar pozitiv daca x este mai mic, egal, respectiv mai mare decat y.

3.3.4

Alte metode pentru stringuri

Lungimea unui obiect de tip String (un sir vid are lungimea 0) poate fi obtinuta cu metoda
length(). Deoarece, length() este o metoda parantezele sunt necesare.
Exista doua metode pentru a accesa caracterele din interiorul unui String. Metoda charAt
returneaza caracterul aflat la pozitia specificata (primul caracter este pe pozitia 0). Metoda
substring returneaza o referinta catre un String nou construit. Metoda are ca parametri
pozitia de nceput si pozitia primului caracter neinclus.
Iata un exemplu de folosire al acestor metode:
String mesaj = "Hello" ;
int lungimeMesaj = mesaj.length() ;
char ch = mesaj.charAt(1) ;
String subSir = mesaj.substring( 2 , 4 ) ;

3.3.5

//lungimea este 5
//ch este e
//sub este "ll"

Conversia de la string la tipurile primitive

Metoda toString() poate fi utilizata pentru a converti orice tip primitiv la String. De
exemplu, toString(45) returneaza o referinta catre sirul nou construit 45. Majoritatea
obiectelor furnizeaza o implementare a metodei toString(). De fapt, atunci cand operatorul
+ are un operand de tip String, operandul care nu este de tip String este automat convertit
la String folosind metoda toString(). Pentru tipurile de date numerice, exista o varianta
a metodei toString() care permite precizarea unei anumite baze. Astfel, instructiunea:
System.out.println( Integer.toString( 55 , 2 ) ) ;
are ca efect tiparirea reprezentarii n baza 2 a numarului 55.
Pentru a converti un String la un int se poate folosi metoda Integer.parseInt(). Aceasta
metoda genereaza o exceptie daca String-ul convertit nu contine o valoare ntreaga. Despre
exceptii vom vorbi pe scurt n paragraful 3.5. Pentru a obtine un double dintr-un String
se poate utiliza metoda parseDouble(). Iata doua exemple:
int x = Integer.parseInt( "75" ) ;
double y = Double.parseDouble( "3.14" ) ;

3.4. S
IRURI

3.4

33

S
iruri

Sirurile sunt structura fundamentala prin care se pot retine mai multe elemente de acelasi
tip. In Java, sirurile nu sunt tipuri primitive; ele se comporta foarte asemanator cu un
obiect. Din acest motiv, multe dintre regulile care sunt valabile pentru obiecte se aplic
a
si la siruri.
Fiecare element dintr-un sir poate fi accesat prin mecanismul de indiciere oferit de operatorul [ ]. Spre deosebire de limbajele C sau C++, Java verifica validitatea indicilor3 .
In Java, ca si n C, sirurile sunt ntotdeauna indiciate de la 0. Astfel, un sir a cu 3
elemente este format din a[0], a[1], a[2]. Numarul de elemente care pot fi stocate n sirul a
este permanent retinut n variabila a.length. Observati ca aici (spre deosebire de String-uri)
nu se pun paranteze. O parcurgere tipica pentru un sir ar fi:
for( int i = 0 ; i < a.length ; i++ )

3.4.1

Declaratie, Atribuire si Metode

Un sir de elemente ntregi se declara astfel:


int[] sir1 ;
Deoarece un sir este un obiect, declaratia de mai sus nu aloca memorie pentru sir. Variabila
sir1 este doar un nume (referinta) pentru un sir de numere ntregi, si n acest moment
valoarea ei este null. Pentru a aloca 100 de numere ntregi, vom folosi instructiunea:
sir1 = new int[100];
Acum sir1 este o referinta catre un sir de 100 de numere ntregi.
Exista si alte posibilitati de a declara siruri. De exemplu, putem unifica declaratia si
alocarea de memorie:
int[] sir1 = new int[100];
Se pot folosi si liste de initializare, ca n C sau C++. In exemplul urmator se aloca un sir
cu patru elemente, care va fi referit de catre variabila sir2:
int[] sir2 = {3, 4, 6, 19} ;
Parantezele patrate pot fi puse fie nainte fie dupa numele sirului. Plasarea parantezelor
nainte de nume face mai vizibil faptul ca este vorba de un sir, de aceea vom folosi aceast
a
notatie.
Declararea unui sir de obiecte (deci nu tipuri primitive) foloseste aceeasi sintaxa. Trebuie
sa retineti nsa ca dupa alocarea sirului, fiecare element din sir va avea valoarea null.
Pentru fiecare element din sir trebuie alocata memorie separat. De exemplu, un sir cu 5
cercuri se construieste astfel:
3

Acesta este un lucru foarte important care vine n ajutorul programatorilor, mai ales a celor ncep
atori.
Indicii ac
aror valoare dep
asesc num
arul de elemente alocat sunt adeseori cauza multor erori obscure n C
si C++. In Java, accesarea unui sir cu un indice n afara limitei este imediat semnalat
a prin exceptia
IndexOutOfBoundsException.

34

CAPITOLUL 3. REFERINT
E

Cerc[] sirDeCercuri ;//declaram un sir de cercuri


sirDeCercuri = new Cerc[ 5 ] ;
//alocam memorie pentru 5 referinte la Cerc
for( int i = 0 ; i < sirDeCercuri.length ; ++i)
{
sirDeCercuri[ i ] = new Cerc() ;
//se aloca un obiect Cerc referintei nr. i
}
Programul din Figura 3.4 ilustreaza modul de folosire al sirurilor n Java. In jocul de
loterie, sase numere de la 1 la 49 sunt selectate saptamanal. Programul alege aleator
numere pentru 1000 de jocuri. Programul afiseaza apoi de cate ori a aparut fiecare numar
n cele 1000 de jocuri. Linia 14 declara un sir de numere ntregi care retine numarul de
aparitii ale fiecarui numar. Deoarece indicierea sirurilor ncepe de la 0, adunarea cu 1 este
esentiala. Fara aceasta adunare am fi avut un sir cu elemente de la 0 la 48, si orice acces
la elementul cu indicele 49 ar fi generat o exceptie IndexOutOfBoundsException. Ciclul
din liniile 15-18 initializeaza valorile sirului cu 0. Restul programului este relativ simplu.
Se foloseste din nou metoda Math.random() care genereaza un numar n intervalul [0,1).
Rezultatele sunt afisate n liniile 28-31.
Dat fiind faptul ca sirul este un tip referinta, operatorul = nu copiaza siruri. De aceea
daca x si y sunt siruri, efectul secventei de instructiuni:
int[] x = new int[100] ;
int[] y = new int[100] ;
...
x = y ;
este ca x si y refera acum al doilea sir.
Sirurile pot fi utilizate ca parametri pentru metode. Regulile de transmitere se deduc logic
din faptul ca sirul este o referinta. Sa presupunem ca avem o metoda f care accepta un
sir de int ca parametru. Apelul si definirea arata astfel:
f( sirActual ) ;
void f( int[] sirFormal) ;

//apelul metodei
//declaratia metodei

Conform conventiilor de transmitere a parametrilor n Java pentru tipurile referinta, variabilele sirActual si sirFormal refera acelasi obiect. Astfel, accesul la sirFormal[i] este de
fapt un acces la sirActual[i]. Aceasta nseamna ca variabilele continute n sir pot fi modificate de catre metoda. O observatie importanta este aceea ca linia de cod din cadrul lui
f():
sirFormal = new int [20] ;
nu are nici un efect asupra lui sirActual. Acest lucru se datoreaza faptului ca n Java
transmiterea parametrilor se face prin valoare, deci sirFormal este o noua referinta catre
sir. Instructiunea de mai sus nu face decat sa schimbe sirul catre care refera sirFormal
(vezi Figura 3.3).

35

3.4. S
IRURI

sirActual
H
j
H

sirActual sirFormal
H
j
H

sirActual

sirFormal

H
j
H

HH
j





a. nainte de apel b. imediat dup


a apel c. dup
a atribuirea sirFormal=new int[20];
Figura 3.3 Transmiterea parametrilor n Java
Deoarece numele sirurilor sunt doar niste referinte, o functie poate sa returneze un sir.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.

//clasa demonstrativa pentru siruri


public class Loterie
{
//genereaza numere de loterie intre 1 si 49
//afiseaza numarul de aparitii al fiecarui numar
//declaratii constante:
public static final int NUMERE = 49 ;
public static final int NUMERE_PE_JOC = 6 ;
public static final int JOCURI = 1000 ;
//main
public static final void main( String[] args )
{
//genereaza numerele
int [] numere = new int[NUMERE+1] ;
for( int i=0 ; i < numere.length; ++i )
{
numere[i] = 0 ;
}
for(int i =0 ; i < JOCURI; ++i)
{
for( int j = 0 ; j < NUMERE_PE_JOC; ++j )
{
numere[ (int)(Math.random()*49)+1 ] ++ ;
}
}
//afisare rezultate
for( int k = 1 ; k <= NUMERE; ++k )
{
System.out.println( k + ": " + numere[k] ) ;
}
}
}

Figura 3.4 Program demonstrativ pentru siruri

3.4.2

Expansiunea dinamic
a a sirurilor

Sa presupunem ca dorim sa citim o secventa de numere si sa o retinem ntr-un sir. Una


dintre proprietatile fundamentale ale sirurilor este aceea ca nainte de a fi utilizate, trebuie

36

CAPITOLUL 3. REFERINT
E

sa alocam memorie pentru un numar fix de elemente care vor fi stocate. Daca nu stim de
la bun nceput cate elemente vor fi stocate n sir, va fi dificil sa alegem o valoare rezonabila
pentru dimensiunea sirului. Aceasta sectiune prezinta o metoda prin care putem extinde
dinamic sirul daca dimensiunea initiala se dovedeste a fi prea mica. Aceasta tehnica poarta
numele de expansiune dinamic
a a sirurilor si permite alocarea de siruri de dimensiune
arbitrara pe care le putem redimensiona pe masura ce programul ruleaza.
Alocarea obisnuita de memorie pentru siruri se realizeaza astfel:
int[] a = new int[10] ;
Sa presupunem ca dupa ce am facut aceasta declaratie, hotaram ca de fapt avem nevoie
de 12 elemente si nu de 10. In aceasta situatie putem folosi urmatoarea manevra:
int original = a ;
//salvam referinta lui a
a = new int[12] ;
//alocam din nou memorie
for(int i=0; i < 10; i++) //copiem elementele in a
{
a[i] = original[i] ;
}
Un moment de gandire este suficient pentru a ne convinge ca aceasta operatie este consumatoare de resurse, deoarece trebuie copiat originalul napoi n noul sir a. De exemplu,
daca extensia dinamica a numarului de elemente ar trebui facuta ca raspuns la citirea
de date, ar fi ineficient sa expansionam ori de cate ori citim cateva elemente. Din acest
motiv, de cate ori se realizeaza o extensie dinamica, numarul de elemente este crescut cu
un coeficient multiplicativ. Am putea de exemplu dubla numarul de elemente la fiecare
expansiune dinamica. Astfel, dintr-un sir cu N elemente, generam un sir cu 2N elemente,
iar costul expansiunii este mpartit ntre cele N elemente care pot fi inserate n sir fara a
realiza extensia.
Pentru a face ca lucrurile sa fie mai concrete, Figura 3.5 prezinta un program care citeste
un numar nelimitat de numere ntregi de la tastatura si le retine ntr-un sir a carui dimensiune este extinsa dinamic. Functia resize realizeaza expansiunea (sau contractia!) sirului
returnand o referinta catre un sir nou construit. Similar, metoda getInts returneaza o
referinta catre sirul n care sunt citite elementele.
La nceputul lui getInts(), nrElemente este initializat cu 0 si ncepem cu un sir cu 5 elemente. In linia 36 citim n mod repetat cate un element. Daca sirul este umplut,
lucru indicat de intrarea n testul de la linia 36, atunci sirul este expansionat prin apelul
metodei resize. Liniile 51-61 realizeaza expansiunea sirului folosind strategia prezentata
anterior. La linia 40, elementul citit este stocat n tablou, iar numarul de elemente citite
este incrementat. In final, n lina 48 contractam sirul la numarul de elemente citite efectiv.
1.
2.
3.
4.
5.
6.
7.

import java.io.* ;
//clasa pentru citirea unui numar nelimitat de valori intregi
public class ReadInts
{
public static void main( String[] args )
{
int [] array = getInts( ) ;

3.4. S
IRURI

37

8.
9.
System.out.println("Elementele citite sunt: " ) ;
10. for( int i = 0; i < array.length; i++ )
11. {
12.
System.out.println( array[i] ) ;
13. }
14. }
15.
16. /* citeste un numar nelimitat de valori intregi
17. * fara a trata erorile */
18. public static int[] getInts()
19. {
20. //BufferedReader este prezentata in sectiunile urmatoare
21. BufferedReader in = new BufferedReader(
22.
new InputStreamReader( System.in ) ) ;
23.
24. int[] elemente = new int[ 5] ; //se aloca 5 elemente
25. int nrElemente = 0 ; //numarul de elemente citite
26. String s ; //sir in care se citeste cate o linie
27.
28. System.out.println("Introduceti numere intregi cate unul pe linie:");
29.
30. try
31. {
32.
//cat timp linia e nevida
33.
while( (s = in.readLine()) != null )
34.
{
35.
int nr = Integer.parseInt( s ) ;
36.
if( nrElemente == elemente.length ) //sirul a fost "umplut"
37.
{
38.
elemente=resize(elemente,elemente.length*2);
//dubleaza dimensiunea sirului
39.
}
40.
elemente[ nrElemente++ ] = nr ;
41.
}
42. }
43. catch( Exception e )
44. {
45.
//nu se trateaza exceptia
46. }
47. System.out.println( "Citire incheiata." ) ;
48. return resize( elemente, nrElemente ) ;
//trunchiaza sirul la numarul de elemente citite
49. }
50.
51. public static int[] resize( int[] sir, int dimensiuneNoua )

38

CAPITOLUL 3. REFERINT
E

52. {
53. int[] original = sir ;
54. int elementeDeCopiat=Math.min(original.length,dimensiuneNoua);
55. sir = new int[ dimensiuneNoua ] ;
56. for( int i=0; i<elementeDeCopiat; ++i)
57. {
58.
sir[i] = original[i] ;
59. }
60. return sir ;
61. }
62.}
Figura 3.5 Program pentru citirea unui num
ar nelimitat de numere ntregi urmat
a de
afisarea lor

3.4.3

S
iruri cu mai multe dimensiuni

In anumite situatii trebuie sa stocam datele n siruri cu mai multe dimensiuni. Cel mai
des folosite sunt matricele, adica sirurile cu doua dimensiuni. Alocarea de memorie pentru
siruri cu mai multe dimensiuni se realizeaza precizand numarul de elemente pentru fiecare
indice, iar indicierea se face plasand fiecare indice ntre paranteze patrate. Ca un exemplu,
declaratia:
int[][] x = new int[2][3] ;
defineste matricea x, n care primul indice poate fi 0 sau 1, iar al doilea este de la 0 la 2.

3.4.4

Argumente n linie de comand


a

Parametrii transmisi n linie de comanda sunt disponibili prin examinarea parametrilor


functiei main. Sirul de stringuri numit args din functia main contine parametri transmisi
n linie de comanda. De exemplu, daca avem un program numit Echo.java pe care l
executam cu comanda:
java Echo buna ziua
parametrul args[0] va fi o referinta catre stringul buna, iar parametrul args[1] va fi o
referinta catre ziua. Astfel, programul de mai jos implementeaza comanda echo existenta atat n DOS cat si n Linux (de fapt, implementeaza o versiune simplificata a acestei
comenzi care afiseaza la consola sirurile trimise ca argument):
1.
2.
3.
4.
5.
6.
7.
8.

public class Echo


{
//afiseaza parametrii primiti in linie de comanda
public static void main(String[] args)
{
if( args.length == 0 )
{
System.out.println( "Nu exista argumente" ) ;

3.5. TRATAREA EXCEPT


IILOR

9.
10.
11.
12.
13.
14.
15.
16.

3.5

39

return ;
}
for( int i=0 ; i < args.length; ++i)
{
System.out.print( args[i] ) ;
}
}
}

Tratarea exceptiilor

Exceptiile sunt obiecte care retin informatie si care sunt transmise n afara secventelor
return. Exceptiile sunt propagate napoi prin secventa de functii apelate pana cand o anumita metoda prinde exceptia. Exceptiile sunt folosite pentru a semnala situatii exceptie,
cum ar fi erorile.
De fiecare data cand o exceptie este ntalnita ntr-o metoda a unei clase, programul va
permite recuperarea pierderilor cauzate de exceptie si se va ncheia fara a cauza caderea
sistemului. Prin pregatirea tratarii conditiilor de exceptie n care va ajunge executia
programului, se va crea un program mult mai prietenos pentru utilizator. Un program Java
poate detecta erori si apoi indica sistemului de executie ce erori a ntalnit prin generarea
unor conditii de exceptie ce vor duce la oprirea executiei si afisarea unui cod de eroare,
sau daca doriti sa tratati unele exceptii ntr-un mod propriu, puteti folosi o clauza catch
pentru a obtine controlul ntr-o situatie de exceptie.

3.5.1

Procesarea exceptiilor

Secventa de cod din Figura 3.6 prezinta modul de folosire al exceptiilor. Secventa de
cod care ar putea genera o exceptie care sa fie propagata este inclusa ntr-un bloc try.
Blocul try se extinde de la linia 12 la linia 16. Imediat dupa blocul try trebuie sa apar
a
secventele de tratare a exceptiilor. Programul sare la secventa de tratare a exceptiilor
doar n situatia n care se genereaza o exceptie; n momentul generarii exceptiei, blocul
try din care exceptia provine se considera a fi ncheiat.
Fiecare dintre blocurile catch este ncercat pe rand pana cand se gaseste o secventa de
tratare adecvata. Deoarece Exception se potriveste cu toate exceptiile generate, ea este
adecvata pentru orice fel de exceptie generata n blocul try. Exceptiile generate de secventa
try din programul nostru pot fi IOException generate de readLine daca apare o eroare la
citire, si NumberFormatException generata de parseInt daca linia citita de la tastatura nu
poate fi convertita la int.
In cazul unei exceptii se executa codul din blocul catch -n situatia noastra linia 19. Dup
a
aceasta blocul catch si blocul continand cuplul try/catch se considera ncheiate. Un mesaj
referitor la eroarea generata este tiparit folosind obiectul de tip Exception numit e. Putem
alege sa facem si alte actiuni n caz de eroare, cum ar fi furnizarea de mesaje de eroare
mai detaliate etc.
1. import java.io.* ;
2.

40

CAPITOLUL 3. REFERINT
E

3. public class DivideByTwo


4. {
5. public static void main( String[] args )
6. {
7.
//BufferedReader este prezentata in sectiunile urmatoare
8.
BufferedReader in = new BufferedReader(
9.
new InputStreamReader( System.in ) ) ;
10.
11. System.out.println("Introduceti o valoare intreaga: ") ;
12. try
13. {
//aceasta linie poate genera o exceptie
14.
int x = Integer.parseInt( in.readLine() ) ;
15.
System.out.println( "Jumatatea lui "+x+"este"+(x/2)) ;
16. }
17. catch( Exception e )
18. {
//aceasta instructiune se executa daca linia 14 a generat o exceptie
19.
System.out.println( e ) ;
20. }
21. }
22.}
Figura 3.6 Program simplu pentru ilustrarea exceptiilor
Se pot preciza diferite tipuri de instructiuni pentru fiecare tip de exceptie ntalnita utilizand instructiunea throw. De asemenea, puteti avea propriile obiecte definite ca exceptii,
prin care sa tratati anumite evenimente speciale care se pot ntampla pe timpul executiei
programului. Pentru a va crea o clasa proprie de exceptii, trebuie ca aceasta sa fie o
subclasa4 a clasei Exception.
public class NewException extends Exception
{
...
}
Acum puteti trata exceptia n modul dorit:
...
try
{
...
throw new NewException(e);
...
}
catch(NewException e)
4

Despre conceptul de mostenire vom discuta n capitolul 5.

41

3.5. TRATAREA EXCEPT


IILOR

{
...//tratarea noii exceptii
}
catch(Exception e)
{
...//tratarea unor erori generale
}
...
Dupa cum am mai spus dupa tratarea unei exceptii este ignorata orice secventa de
instructiuni. Totusi, daca este neaparata nevoie sa se execute anumite operatii, veti invoca
clauza finally. Blocul finally este executat si n cazul n care nu are loc nici o exceptie n
blocul try.
try
{
...
}
finally
{
...
}

3.5.2

Exceptii uzuale

Exista mai multe tipuri de exceptii standard n Java. O prima categorie de exceptii o constituie exceptiile de executie standard (standard runtime exceptions) cum ar fi mpartirea
unui ntreg la 0 sau accesarea unui element de tablou cu indice ilegal. Avand n vedere
faptul ca aceste exceptii pot aparea practic n orice secventa de program, ar fi o munc
a
sisifica sa definim secvente de tratare a unor astfel de exceptii. Daca se furnizeaza un bloc
catch aceste exceptii se comporta exact la fel ca celelalte exceptii. Daca apare o exceptie
standard pentru care nu exista un bloc catch aceasta se propaga normal, trecand chiar si de
functia main. In aceasta situatie, exceptia produce o terminare anormala a programului,
nsotita de un mesaj de eroare. Figura 3.7 prezinta cateva dintre cele mai uzuale erori
standard de executie.

EXCEPT
IE STANDARD DE EXECUT
IE

SEMNIFICAT
IE

ArithmeticException

Dep
asire sau mp
artirea unui ntreg la 0

NumberFormatException

Conversie nepermis
a a unui String la un tip numeric

IndexOutOfBoundsException

Indice ilegal ntr-un sir sau String

NegativeArraySizeException

Tentativ
a de a crea un sir cu nr. negativ de elemente

NullPointerException

Tentativ
a de a folosi o referint
a care are valoarea null
Inc
alcare de securitate n timpul executiei

SecurityException

Figura 3.7 C
ateva exceptii de executie uzuale.

42

CAPITOLUL 3. REFERINT
E

Majoritatea exceptiilor sunt de tipul exceptii standard tratate (standard checked exceptions). Daca se apeleaza o metoda care poate genera direct sau indirect o astfel de
exceptie, atunci programatorul fie trebuie sa o trateze cu un bloc de tip catch sau sa
indice explicit faptul ca exceptia urmeaza sa fie propagata prin folosirea clauzei throws
n antetul metodei. Retineti faptul ca exceptia tot va trebui tratata la un moment dat,
deoarece metoda main (care este la ultimul nivel) nu poate avea o clauza throws.

EXCEPT
IE STANDARD TRATATA

SEMNIFICAT
IE

java.io.EOFException

Terminare de fisier nainte de ncheierea citirii

java.io.FileNotFoundException

Fisierul nu a fost g
asit pentru a fi deschis

java.io.IOException

Cuprinde majoritatea erorilor de intrare/iesire

InterruptedException

Aruncat
a de metoda Thread.Sleep

Figura 3.8 Exceptii standard tratate uzual


Ultima categorie de exceptii sunt erorile care nu sunt prinse de Exception. De obicei aceste
exceptii nu pot fi tratate. Cea mai uzuala este OutOfMemoryError. Pentru a prinde orice
exceptie posibila, prindeti un obiect de tip Throwable si utilizati clauza throws. Aceasta
clauza apare n antetul unei metode pentru a specifica tipurile de exceptii ce pot fi lansate
n metoda respectiva. Numele acestor tipuri sunt clase derivate din clasa Exception care
la randul ei este derivata din clasa Throwable. Clasele Exception si Throwable se afla n
java.lang.

3.6

Intrare si iesire

Intrarea si iesirea n Java se realizeaza cu ajutorul claselor din pachetul java.io. Din
acest motiv, orice program care foloseste rutinele de intrare/iesire trebuie sa cuprinda
instructiunea:
import java.io.* ;
Biblioteca Java de intrare/iesire este extrem de sofisticata si are un numar foarte mare
de optiuni. In aceasta faza vom examina doar elementele de baza referitoare la intrare si
iesire, concentrandu-ne n ntregime asupra operatiilor de intrare-iesire formatate.

3.6.1

Operatii de baz
a pe fluxuri (stream-uri)

Exista trei fluxuri predefinite pentru operatii I/O de la terminal: System.in, intrarea
standard, System.out, iesirea standard si System.err, fluxul de erori.
Asa cum am aratat deja, metodele print si println sunt folosite pentru afisare formatata.
Orice fel de tip poate fi convertit la o forma tiparibila, folosind metoda toString; n multe
cazuri, acest lucru este realizat automat. Spre deosebire de C si C++ care dispun de un
numar enorm de optiuni de formatare, afisarea n Java se face exclusiv prin concatenare
de String-uri, fara nici o formatare.
O modalitate simpla de a realiza citirea este aceea de a citi o singura linie ntr-un obiect

3.6. INTRARE S
I IES
IRE

43

de tip String folosind readLine. Metoda readLine preia caractere de la intrare pana cand
ntalneste un terminator de linie sau sfarsit de fisier. Metoda returneaza caracterele citite
(din care extrage terminatorul de linie) ca un String nou construit. Pentru a putea folosi
readLine trebuie sa construim un obiect BufferedReader dintr-un obiect InputStreamReader
care la randul sau este construit din System.in. Acest lucru a fost ilustrat n Figura 3.6,
liniile 8 si 9.
Daca primul caracter citit este EOF, atunci functia readLine returneaza null. Daca apare
o eroare la citire se genereaza o exceptie de tipul IOException. Daca sirul citit nu poate fi
convertit la o valoare ntreaga se genereaza o NumberFormatException.

3.6.2

Obiectul StringTokenizer

Sa ne reamintim ca pentru a citi o valoare primitiva, cum ar fi int, foloseam readLine


pentru a prelua linia ca pe un String si apoi aplicam metoda parseInt pentru a converti
stringul la tipul primitiv.
In multe situatii avem mai multe elemente pe aceeasi linie. Sa presupunem, de exemplu,
ca fiecare linie are doua valori ntregi. Java furnizeaza clasa StringTokenizer pentru a
separa String-ul n elemente lexicale (engl. token ). Pentru a putea folosi obiecte ale clasei
StringTokenizer se foloseste directiva:
import java.util.* ;
Folosirea obiectului StringTolenizer este prezentata n programul din Figura 3.9. Obiectul
StringTokenizer este construit n linia 19 prin furnizarea obiectului String reprezentand
linia citita. Metoda countTokens din linia 20 ne va da numarul de cuvinte citite (elemente
lexicale). Metoda nextToken ntoarce urmatorul cuvant necitit ca pe un String. Aceast
a
ultima metoda genereaza o NoSuchElementException daca nu exista nici un cuvant ramas,
dar aceasta este o exceptie de executie standard (vezi 3.5.2) si nu trebuie prinsa. La liniile
25 si 26 folosim nextToken urmata de parseInt pentru a obtine un int. Toate erorile sunt
prinse n blocul catch.
Implicit, elementele lexicale sunt separate de spatii. Obiectul StringTokenizer poate fi ns
a
construit si n asa fel ncat sa recunoasca si alte caractere drept delimitatori.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.

//program pentru exemplificarea clasei


//StringTokenizer. Programul citeste doua numere aflate pe
//aceeasi linie si calculeaza maximul lor
import java.io.* ;
import java.util.* ;
public class TokenizerTest
{
public static void main(String[] args)
{
BufferedReader in = new BufferedReader(
new InputStreamReader(System.in) ) ;

44

CAPITOLUL 3. REFERINT
E

15. System.out.println("Introduceti doua nr. pe aceeasi linie:");


16. try
17. {
18.
String s = in.readLine() ;
19.
StringTokenizer st = new StringTokenizer( s ) ;
20.
if( st.countTokens() != 2 )
21.
{
22.
System.out.println("Numar invalid de argumente!") ;
23.
return ;
24.
}
25.
int x = Integer.parseInt( st.nextToken() ) ;
26.
int y = Integer.parseInt( st.nextToken() ) ;
27.
System.out.println("Maximul este: "+Math.max(x,y)) ;
28. }
29. catch( Exception e)
30. {
31.
System.out.println( e ) ;
32. }
33. }
34.}
Figura 3.9 Program demonstrativ pentru clasa StringTokenizer

3.6.3

Fisiere secventiale

Una dintre regulile fundamentale n Java este aceea ca ceea ce se poate face pentru operatii
I/O de la terminal se poate face si pentru operatii I/O din fisiere. Pentru a lucra cu fisiere
obiectul BufferedReader nu se construieste pe baza unui InputStreamReader. Vom folosi
n schimb un obiect de tip FileReader care va fi construit pe baza unui nume de fisier.
Un exemplu pentru ilustrarea acestor idei este prezentat n Figura 3.10. Acest program
listeaza continutul fisierelor text care sunt precizate ca parametri n linie de comanda.
Functia main parcurge pur si simplu argumentele din linia de comanda, apeland metoda
listFile pentru fiecare argument. In metoda listFile construim obiectul de tip FileReader
la linia 26 si apoi l folosim pe acesta pentru construirea obiectului de tip BufferedReader
n linia urmatoare. Din acest moment, citirea este identica cu cea de la tastatura.
Dupa ce am ncheiat citirea din fisier, trebuie sa nchidem fisierul, altfel s-ar putea sa nu
mai putem deschide alte stream-uri (sa atingem limita maxima de stream-uri care pot
fi deschise). Nu putem face acest lucru n primul bloc try, deoarece o exceptie ar putea
genera parasirea prematura a blocului si fisierul nu ar fi nchis. Din acest motiv, fisierul
este nchis dupa secventa try/catch.
Scrierea formatata n fisiere este similara cu citirea formatata.
1.
2.
3.
4.
5.

//program pentru afisarea de fisiere


//al caror nume este precizat in linia de comanda
import java.io.* ;
public class Lister

3.6. INTRARE S
I IES
IRE

6. {
7.
public static void main( String[] args )
8.
{
9.
if( args.length == 0 )
10. {
11.
System.out.println( "Nu ati precizat nici un fisier!") ;
12. }
13.
14. for(int i = 0 ; i < args.length; ++i )
15. {
16.
listFile( args[i] ) ;
17. }
18. }
19.
20. public static void listFile( String fileName )
21. {
22. System.out.println("NUME FISIER: " + fileName) ;
23.
24. try
25. {
26.
FileReader file = new FileReader( fileName ) ;
27.
BufferedReader in = new BufferedReader( file ) ;
28.
String s ;
29.
while( ( s = in.readLine() ) != null )
30.
{
31.
System.out.println( s ) ;
32.
}
33. }
34. catch( Exception e )
35. {
36.
System.out.println( e ) ;
37. }
38. try
39. {
40.
if( in != null )
41.
{
42.
in.close() ;
43.
}
44. }
45.
catch( IOException e )
46.
{ //ignora exceptia
47.
}
48. }
49.}
Figura 3.10 Program de listare a continutului unui fisier

45

46

CAPITOLUL 3. REFERINT
E

3.7

Probleme propuse

1. O sum
a de verificare este un ntreg pe 32 de biti (int) care este suma codurilor
Unicode ale caracterelor dintr-un fisier (se permite depasirea, desi aceasta este putin
probabila daca toate caracterele sunt ASCII). Doua fisiere identice au aceeasi suma
de verificare. Scrieti un program care calculeaza suma de verificare pentru un fisier
care este furnizat ca parametru n linie de comanda.
2. Modificati programul Echo.java din acest capitol astfel ncat daca nu se transmit
parametri de la linia de comanda sa foloseasca intrarea standard.
3. Scrieti o metoda care returneaza true daca stringul str1 este prefix pentru stringul
str2. Nu folositi nici o alta metoda generala de cautare pe stringuri n afara de
charAt.

Capitolul 4

Obiecte si clase
In acest capitol vom ncepe sa discutam despre programarea orientat
a pe obiecte (object
oriented programming - OOP). O componenta fundamentala a programarii orientate pe
obiecte este specificarea, implementarea si folosirea obiectelor. In capitolul anterior am
vazut deja cateva exemple de obiecte, cum ar fi string-urile si fisierele (stream-urile) care
fac parte din bibliotecile limbajului Java. Am putut observa si faptul ca fiecare obiect este
caracterizat de o anumita stare care poate fi modificata prin aplicarea operatorului punct
(.). In limbajul Java, starea si functionalitatea unui obiect se definesc prin intermediul
unei clase. Un obiect este de fapt o instant
a a unei clase.

4.1

Ce este programarea orientat


a pe obiecte?

Programarea orientata pe obiecte s-a impus ca modelul dominant al anilor 90. In aceast
a
sectiune vom prezenta modul n care Java suporta programarea orientata pe obiecte si
vom mentiona cateva dintre principiile ei fundamentale.
In centrul programarii orientate pe obiecte se afla notiunea de obiect. Obiectul este o
variabila care are o structur
a si o stare. Fiecare obiect dispune de operatii prin intermediul
carora i se poate manipula starea. Asa cum am vazut deja, n limbajul Java se face
distinctie ntre un obiect si o variabila de un tip primitiv, dar aceasta este o specificitate a
limbajului Java si nu a programarii orientate pe obiecte. Pe langa operatiile cu un caracter
general, asupra obiectelor mai putem sa realizam:
Crearea de noi obiecte, nsotit
a eventual de initializarea obiectelor
Copierea si testarea egalit
atii
Realizarea de operatii de intrare/iesire cu obiectele
Obiectul trebuie privit ca o unitate atomic
a pe care utilizatorul nu ar trebui sa o disece.
In mod normal, nu ne punem problema de a jongla cu bitii din care este format un numar
reprezentat n virgula mobila si ar fi de-a dreptul ridicol sa ncercam sa incrementam un
astfel de numar prin modificarea directa a reprezentarii sale interne.
Principiul atomicitatii este cunoscut sub numele de ascunderea informatiei. Utilizatorul
nu are acces direct la partile unui obiect sau la implementarea sa. Acestea vor putea
fi accesate doar prin intermediul metodelor care au fost furnizate mpreuna cu obiectul.
47

48

CAPITOLUL 4. OBIECTE S
I CLASE

Putem privi fiecare obiect ca fiind ambalat cu mesajul Nu deschideti! Nu contine componente reparabile de catre utilizator!. In viata de zi cu zi, majoritatea celor care ncearca sa
repare componente cu aceasta inscriptie sfarsesc prin a face mai mult rau decat bine. Din
acest punct de vedere, programarea imita lumea reala. Gruparea datelor si a operatiilor
asupra acestor date n acelasi ntreg (agregat), avand grija sa ascundem detaliile de implementare ale agregatului este cunoscuta sub numele de ncapsulare.
Unul dintre principalele scopuri ale programarii orientate pe obiecte este refolosirea codului. La fel cum inginerii refolosesc din nou si din nou aceleasi componente n proiectare,
programatorii ar trebui sa refoloseasca obiectele n loc sa le reimplementeze. Atunci cand
avem deja un obiect care implementeaza exact comportamentul pe care l dorim, refolosirea
nu pune nici un fel de probleme. Adevarata provocare apare atunci cand dorim sa folosim
un obiect care deja exista, dar care, desi are un comportament foarte similar cu ceea ce
vrem, nu corespunde exact cu necesitatile noastre.
Limbajele de programare orientate pe obiecte furnizeaza mai multe mecanisme n acest
scop. Unul dintre mecanisme este folosirea codului generic. Daca implementarea este
identica, si difera doar tipul de baza al obiectului, nu este necesar sa rescriem complet codul ei: vom scrie n schimb un cod generic care functioneaza pentru orice tip. De exemplu,
algoritmul de sortare al unui sir de obiecte nu depinde de obiectele care sunt sortate, deci
se poate implementa un algoritm generic de sortare.
Mostenirea este un alt mecanism care permite extinderea functionalitatii unui obiect. Cu
alte cuvinte, putem crea noi tipuri de date care sa extinda (sau sa restrictioneze) proprietatile tipului de date original.
Un alt principiu important al programarii orientate pe obiecte este polimorfismul. Un tip
referinta polimorfic poate sa refere obiecte de mai multe tipuri. Atunci cand se apeleaza o
metoda a tipului polimorfic, se va selecta automat metoda care corespunde tipului referit
n acel moment.
Un obiect n Java este o instanta a unei clase. O clasa este similara cu un tip record din
Pascal sau cu o structura din C, doar ca exista doua mbunatatiri majore. In primul rand,
membrii pot fi atat functii cat si date, numite n acest context metode respectiv atribute. In
al doilea rand, domeniul de vizibilitate al acestor membri poate fi restrictionat. Deoarece
metodele care manipuleaza starea obiectului sunt membri ai clasei, ele sunt accesate prin
intermediul operatorului punct, la fel ca si atributele. In terminologia programarii orientate pe obiecte, atunci cand apelam o metoda a obiectului spunem ca trimitem un mesaj
obiectului.

4.2

Un exemplu simplu

Sa ne amintim ca, atunci cand proiectam o clasa, este important sa ascundem detaliile
interne fata de utilizatorul clasei. Clasa poate sa si defineasca functionalitatea prin intermediul metodelor. Unele dintre aceste metode vor descrie cum se creeaza si initializeaza
o instanta a clasei, cum se realizeaza testele de egalitate si cum se scrie starea clasei.
Celelalte metode sunt specifice structurii particulare pe care o are clasa. Ideea este ca
utilizatorul nu trebuie sa aiba dreptul de a modifica direct starea obiectului, el va trebui
sa foloseasca metodele clasei pentru a realiza acest lucru. Aceasta idee poate fi impusa
prin ascunderea anumitor membri fata de utilizator. Pentru a realiza aceasta vom preciza

4.2. UN EXEMPLU SIMPLU

49

ca acesti membri sa fie stocati n sectiunea private. Compilatorul va avea grija ca membri
din sectiunea private sa fie inaccesibili utilizatorului acelui obiect. In general, toate datele
membru ar trebui sa fie declarate private.
Figura 4.1 prezinta modul de definire al unei clase care modeleaza un cerc. Definirea
clasei consta n doua parti: public si private. Sectiunea public reprezinta portiunea care
este vizibila pentru utilizatorul obiectului. Deoarece datele sunt ascunse fata de utilizator, sectiunea public va contine n mod normal numai metode si constante. In exemplul
nostru avem doua metode pentru a scrie si a citi raza obiectelor de tip Circle. Celelalte
doua metode calculeaza aria respectiv lungimea obiectului de tip Circle. Sectiunea private
contine datele: acestea sunt invizibile pentru utilizatorul obiectului. Atributul radius va
trebui accesat doar prin intermediul metodelor publice setRadius si getRadius.
1. //clasa simpla Java care modeleaza un Cerc
2. import java.util.* ;
3. public class Circle
4. {
5.
6.
//raza cercului
7.
//valoarea razei nu poate fi modificata
8.
//direct de catre utilizator
9.
private double radius ;
10.
11. public void setRadius(double r) //modifica raza cercului
12. {
13. radius = r ;
14. }
15.
16. public double getRadius() //metoda ptr a obt raza cercului
17. {
18. return radius ;
19. }
20.
21. public double area() //metoda ptr calculul ariei cercului
22. {
23. return Math.PI*radius*radius ;
24. }
25.
26. public double length() //metoda ptr calculul lungimii
27. {
28. return 2*Math.PI*radius ;
29. }
30.}
Figura 4.1 Definirea clasei Circle
Figura 4.2 prezinta modul de folosire al unui obiect de tip Circle. Deorece setRadius,
getRadius, area si length sunt membri ai clasei, ei sunt accesati folosind operatorul punct.

50

CAPITOLUL 4. OBIECTE S
I CLASE

Atributul radius ar fi putut si el sa fie accesat folosind operatorul punct, daca nu ar fi


fost declarat de tip private. Accesarea lui radius din linia 15 ar fi fost ilegala daca nu era
comentata.
Sa rezumam terminologia nvatata. Clasa contine membri care pot fi atribute (campuri,
date) sau metode (functii). Metodele pot actiona asupra atributelor si pot apela alte
metode. Modificatorul de vizibilitate public nseamna ca membrul respectiv este accesibil
oricui prin intermediul operatorului punct. Modificatorul de vizibilitate private nseamna
ca membrul respectiv este accesibil doar metodelor clasei. Daca nu se pune nici un modificator de vizibilitate, atunci accesul la membru este de tip friendly, despre care vom
vorbi mai tarziu. Mai exista si un al patrulea modificator, numit protected pe care l vom
prezenta n capitolul urmator.
1. //clasa simpla de testare a clasei Circle
2. public class TestCircle
3. {
4. public static void main( String[] args )
5. {
6.
Circle circle = new Circle() ;
7.
8.
circle.setRadius(10) ;
9.
System.out.println("Raza este:" + circle.getRadius());
10. System.out.println("Aria cercului este:"+circle.area());
11. System.out.println("Lungimea este:" + circle.length());
12.
13. //urmatoarea linie ar genera o
14. //eroare de compilare
15. //circle.radius = 20 ;
16. }
17.}
Figura 4.2 O clas
a simpl
a de testare a clasei Circle

4.3

Metode uzuale

Exista metode care sunt comune pentru toate clasele. Alte metode definesc comportamente
specifice unei anumite clase. In aceasta sectiune vom prezenta metodele care sunt comune
tuturor claselor: constructorii, modificatorii, accesorii, toString si equals.

4.3.1

Constructori

Asa cum am mentionat deja, una dintre proprietatile fundamentale ale obiectelor este ca
acestea pot fi definite si, eventual, initializate. In limbajul Java, metoda care controleaza
modul n care un obiect este creat si initializat este constructorul. Deoarece Java permite
suprancarcarea metodelor, o clasa poate sa defineasca mai multi constructori.
Daca la definirea clasei nu se furnizeaza nici un constructor, cum este cazul clasei Circle
din Figura 4.1, compilatorul creeaza un constructor implicit care initializeaza fiecare
data membru cu valorile implicite. Aceasta nseamna ca atributele de tipuri primitive

4.3. METODE UZUALE

51

sunt initializate cu 0, iar atributele de tip referinta sunt initializate cu null. Astfel, n
cazul nostru, atributul radius va avea implicit valoarea 0.
Pentru a furniza un constructor, vom scrie o metoda care are acelasi nume cu clasa si
care nu returneaza nimic. In Figura 4.3 avem doi constructori: unul ncepe la linia 8,
iar celalalt la linia 16. Folosind acesti doi constructori vom putea construi obiecte de tip
Date n urmatoarele moduri:
Date d1 = new Date( ) ;
Date d2 = new Date( 15, 3, 2000) ;
De remarcat faptul ca odata ce ati definit un constructor pentru clasa, compilatorul nu
mai genereaza constructorul implicit fara parametri. Daca aveti nevoie de un astfel de
constructor, va trebui sa l scrieti. Astfel constructorul din linia 8 trebuie definit obligatoriu
pentru a putea construi un obiect de tipul celui referit de catre d1.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.

//clasa Java simpla pentru stocarea unei


//date calendaristice
//nu se face validarea datelor
public class Date
{
//constructor fara parametri
public Date( )
{
day = 1 ;
month = 1 ;
year = 2000 ;
}
//constructor cu trei parametri
public Date( int theDay, int theMonth, int theYear )
{
day = theDay ;
month = theMonth ;
year = theYear ;
}
//test de egalitate
//intoarce true daca Obiectul x
//este egal cu obiectul curent
public boolean equals( Object x )
{
if( ! (x instanceof Date) )
return false ;
Date date = (Date) x ;
return date.day == day && date.month == month

52

33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.}

CAPITOLUL 4. OBIECTE S
I CLASE

&& date.year == year ;


}
//conversie la String
public String toString()
{
return day + "/" + month + "/" + year ;
}
//atribute
private int day ;
private int month ;
private int year ;

Figura 4.3 O clas


a Date minimal
a care ilustreaz
a constructorii si metodele equals si
toString

4.3.2

Modificatori si Accesori

Atributele sunt declarate de obicei ca fiind private. Aceasta nseamna ca ele nu vor putea
fi direct accesate de catre rutinele care nu apartin clasei. Exista totusi multe situatii n
care dorim sa examinam sau chiar sa modificam valoarea unui atribut.
O posibilitate este aceea de a declara atributele ca fiind public. Aceasta este nsa o alegere
proasta, deoarece ncalca principiul ascunderii informatiei. Putem nsa scrie metode care sa
examineze sau sa modifice valoarea fiecarui camp. O metoda care citeste, dar nu modifica
starea unui obiect este numita accesor. O metoda care modifica starea unui obiect este
numita modificator (engl. mutator).
Cazuri particulare de accesori si modificatori sunt cele care actioneaza asupra unui singur
camp. Accesorii de acest tip au un nume care ncepe de obicei cu get, cum ar fi getRadius,
iar modificatorii au un nume care ncepe de regula cu set, cum ar fi setRadius.
Avantajul folosirii unui modificator este ca acesta poate verifica daca starea obiectului este
corecta. Astfel un modificator care altereaza campul day al unui obiect de tip Date poate
verifica corectitudinea datei care rezulta.

4.3.3

Afisare si toString

In general, dorim sa afisam starea unui obiect folosind metoda print. Pentru a putea face
acest lucru trebuie definita o metoda cu numele de toString. Aceasta metoda ntoarce
un String care poate fi afisat. Ca un exemplu, n Figura 4.3 am prezentat o implementare rudimentara a unei metode toString pentru clasa Date n liniile 37-40. Definirea
acestei metode ne permite sa scriem un obiect d1 de tip Date cu instructiunea system.out.print(d1).

4.4. PACHETE

4.3.4

53

Metoda equals

Metoda equals este folosita pentru a testa daca doua referinte indica obiecte care au aceeasi
valoare (stare). Antetul acestei metode este ntodeauna
public boolean equals( Object rhs )
Ati remarcat probabil nedumeriti faptul ca parametrul trimis este de tip Object si nu de tip
Date, cum ar fi fost de asteptat. Ratiunea pentru acest lucru o sa o prezentam n capitolul
despre polimorfism. In general, metoda equals pentru o clasa X este implementata n asa
fel ncat sa returneze true doar daca rhs este o instanta a lui X si, n plus, dupa conversia
la X toate tipurile primitive sunt egale (via ==) si toate tipurile referinta sunt egale (prin
aplicarea lui equals pentru fiecare membru).
Un exemplu de implementare a lui equals este dat n Figura 4.3 pentru clasa Date n
liniile 26-34. Operatorul instanceof va fi discutat n paragraful 4.5.3.

4.3.5

Variabile si metode statice

Exista anumite cuvinte cheie ale limbajului Java care specifica proprietati speciale pentru unele variabile sau metode. Un astfel de cuvant cheie este cuvantul rezervat static.
Variabilele si metodele declarate static ntr-o clasa, sunt aceleasi pentru toate obiectele,
adica pentru toate variabilele de tipul acelei clase. Variabilele statice pot fi accesate far
a
sa fie nevoie de o instantiere a clasei respective (adica de o variabila de clasa respectiva).
Analog, nici metodele statice nu au nevoie de o instantiere a clasei pentru a fi folosite.
Metodele statice pot utiliza variabile statice declarate n interiorul clasei.
Cel mai cunoscut exemplu de metoda statica este main. Alte exemple de metode statice pot fi g
asite n clasele String, Integer si Math. Exemple de astfel de metode sunt
String.valueOf, Integer.parseInt, Math.sin si Math.max. Accesul la metodele statice respecta aceleasi reguli de vizibilitate ca si metodele normale.

4.3.6

Metoda main

Atunci cand este invocat, interpretorul java cauta metoda main din clasa care i se da ca
parametru. Astfel, putem scrie cate o metoda main pentru fiecare clasa. Acest lucru ne
permite sa testam functionalitatea de baza a claselor individuale. Trebuie sa avem totusi
n vedere faptul ca plasarea functiei main n cadrul clasei ne confera mai multa vizibilitate
decat ne-ar fi permis n general. Astfel, apeluri ale metodelor private pot fi facute n test,
dar ele vor esua ntr-un cadru mai general.

4.4

Pachete

Pachetele sunt folosite pentru a organiza clasele similare. Fiecare pachet consta dintr-o
multime de clase. Clasele care sunt n acelasi pachet au restrictii de vizibilitate mai slabe
ntre ele decat daca ar fi n pachete diferite.
Java furnizeaza o serie de pachete predefinite, printre care java.io, java.lang, java.util,
java.applet, java.awt etc. Pachetul java.lang include, printre altele, clasele Integer, Math,
String si System. Clase mai cunoscute din java.util sunt Date, Random, StringTokenizer.

54

CAPITOLUL 4. OBIECTE S
I CLASE

Pachetul java.io cuprinde diferitele clase pentru stream-uri pe care le-am prezentat n
capitolul anterior.
Clasa C din pachetul P este specificata ca P.C. De exemplu, putem declara un obiect de
tip Date care sa contina data si ora curenta astfel:
java.util.Date today = new java.util.Date() ;
Observati ca prin specificarea numelui pachetului din care face parte clasa evitam conflictele care pot fi generate de clase cu acelasi nume din pachete diferite1 .

4.4.1

Directiva import

Utilizarea permanenta a numelui pachetului din care fac parte clasele poate fi uneori
deosebit de anevoioasa. Pentru a evita acest lucru se foloseste directiva import:
import NumePachet.NumeClasa ;
sau
import NumePachet.* ;
Daca recurgem la prima forma a directivei import, vom putea folosi NumeClasa ca o
prescurtare pentru numele clasei cu calificare completa. Daca folosim cea de-a doua forma,
toate clasele din pachet vor putea fi abreviate cu numele lor.
De exemplu, realizand directivele import de mai jos:
import java.util.Date ;
import java.io.* ;
putem sa folosim:
Date today = new Date() ;
FileReader file = new FileReader( name ) ;
Folosirea directivelor import economiseste timpul de scriere. Avand n vedere ca cea de-a
doua forma este mai generala, ea este cel mai des folosita. Exista doua dezavantaje ale
folosirii directivei import. Primul dezavantaj este acela ca la citirea codului va fi mai greu
de stabilit pachetul din care o anumita clasa face parte. Al doilea este acela ca folosirea
celei de-a doua forme poate sa introduca prescurtari neintentionate pentru anumite clase,
ceea ce va genera conflicte de denumire care vor trebui rezolvate prin folosirea de nume
de clase calificate.
Sa presupunem ca avem urmatoarele directive:
import java.util.* ;
import myutil.* ;

//pachet predefinit
//pachet definit de catre noi

cu intentia de a importa clasa java.util.Random si o clasa pe care am definit-o chiar noi.


Atunci, daca noi avem propria clasa Date n pachetul myutil, directiva import va genera
un conflict cu java.util.Date si de aceea clasa va trebui sa fie complet calificata. Am fi
putut evita aceste probleme daca am fi folosit prima forma
1

De exemplu, cu clasa Date definit


a anterior

4.4. PACHETE

55

import java.util.Random ;
Directivele import trebuie sa apara nainte de orice declarare a unei clase. Pachetul
java.lang este automat inclus n ntregime. Acesta este motivul pentru care putem folosi
prescurtari de genul Math.max, System.out, Integer.parseInt etc.

4.4.2

Instructiunea package

Pentru a indica faptul ca o clasa face parte dintr-un anumit pachet trebuie sa realizam
doua lucruri. In primul rand trebuie sa scriem o instructiune package pe prima linie,
nainte de a declara clasa. Apoi, va trebui sa plasam clasa n directorul corespunzator.

4.4.3

Variabila sistem CLASSPATH

Java cauta pachetele si clasele n directoarele care sunt precizate n variabila sistem
CLASSPATH. Ce nseamna aceasta? Iata doua posibile setari pentru CLASSPATH, mai
ntai pentru un sistem Windows 95/98/2000, iar apoi pentru un sistem Unix/Linux:
SET CLASSPATH = .;C:\JDK1.3\LIB\
export CLASSPATH=.:/usr/local/jdk1.3/lib:$HOME/java
In ambele cazuri variabila CLASSPATH contine directoarele (sau arhivele) care contin
fisierele .class din pachete. De exemplu, daca variabila CLASSPATH nu este setata corect,
nu veti putea compila nici macar cel mai banal program Java, deoarece pachetul java.lang
nu va fi gasit. O clasa care se afla n pachetul P va trebui sa fie pusa ntr-un director
cu numele P care sa se afle ntr-un director din CLASSPATH. Directorul curent (.) este
ntotdeauna n variabila CLASSPATH, deci daca lucrati ntr-un singur director principal,
puteti crea subdirectoarele chiar n el. Totusi, n majoritatea situatiilor, veti dori sa creati
un subdirector Java separat si sa creati directoarele pentru pachete chiar n el. In aceast
a
situatie va trebui sa adaugati la variabila CLASSPATH acest director. Acest lucru a fost
realizat n exemplul de mai sus prin adaugarea directorului $HOME/java la CLASSPATH.
In directorul Java veti putea acum crea subdirectorul io. In subdirectorul io vom plasa
codul pentru clasa Reader2 . O aplicatie va putea n aceasta situatie sa foloseasca metoda
readInt fie prin
int x = io.Reader.readInt() ;
sau pur si simplu prin
int x = Reader.readInt() ;
daca se furnizeaza directiva import corespunzatoare.
2
Reader este o clas
a ajut
atoare care contine metode pentru citirea de la tastatur
a a tipurilor uzuale.
Codul acestei clase este dat n paragraful 5.7.

56

4.4.4

CAPITOLUL 4. OBIECTE S
I CLASE

Reguli de vizibilitate Package-Friendly

Pachetele au cateva reguli de vizibilitate importante. In primul rand, daca pentru un


membru al unei clase nu se precizeaza nici un modificator de vizibilitate (public, protected
sau private), atunci membrul respectiv devine (package) friendly. Aceasta nseamna ca
acel camp este vizibil doar pentru clasele din cadrul aceluiasi pachet. Aceasta este o vizibilitate mai putin restrictiva decat private, dar mai restrictiva decat public (care este
vizibil si pentru membrii din alte clase).
In al doilea rand, doar clasele public din cadrul unui pachet pot fi folosite din afara pachetului. Acesta este motivul pentru care am pus ntotdeauna modificatorul public n fata
unei clase. Clasele nu pot fi declarate private sau protected. Accesul de tip friendly se
extinde si pentru clase. Daca o clasa nu este declarata ca fiind de tip public, atunci ea va
putea fi accesata doar de clasele din cadrul aceluiasi pachet.
Toate clasele care nu fac parte din nici un pachet, dar sunt accesibile fiind puse ntr-un
director din CLASSPATH, sunt considerate automat ca facand parte din acelasi pachet
implicit. Ca o consecinta, accesul de tip friendly se aplica pentru toate aceste clase. Acesta
este motivul pentru care vizibilitatea nu este afectata daca omitem sa punem modificatorul public din clasele care nu fac parte dintr-un pachet. Totusi aceasta modalitate de
folosire a accesului friendly nu este recomandata.

4.4.5

Compilarea separat
a

Atunci cand un program consta din mai multe fisiere .java, fiecare fisier trebuie compilat
separat. In mod normal, fiecare clasa este plasata ntr-un fisier .java propriu. Ca urmare
a compilarii vom obtine o colectie de fisiere .class. Clasele sursa pot fi compilate n orice
ordine.

4.5

Alte operatii

In aceasta sectiune vom prezenta nca trei cuvinte cheie importante: this, instanceof si
static. this are mai multe utilizari n Java. Doua dintre ele le vom prezenta n aceasta
sectiune. instanceof are si el mai multe utilizari; l vom folosi aici pentru a ne asigura ca o
conversie de tip se poate realiza. Si cuvantul cheie static are mai multe semnficatii. Vom
vorbi despre metode statice, atribute statice si initializatori statici.

4.5.1

Referinta this

Cea mai cunoscuta utilizare pentru this este ca o referinta la obiectul curent. Imaginati-va
ca this va indica n fiecare moment locul unde va aflati. O utilizare tipica pentru this este
calificarea atributelor unei clase n cadrul unei metode a clasei care primeste parametri cu
nume identic cu numele clasei. De exemplu, n clasa Circle, din Figura 4.1, putem defini
metoda setRadius astfel:
public void setRadius(double radius) //modifica raza cercului
{ //radius este parametrul iar this.radius este atributul clasei
this.radius = radius ;
}

4.5. ALTE OPERAT


II

57

In codul de mai sus, pentru a face distinctie ntre parametrul radius si atributul cu acelasi
nume (care este ascuns de catre parametru) se foloseste sintaxa this.radius pentru a
referi atributul clasei.
Un alt exemplu de folosire al lui this este pentru a testa ca parametrul pe care o metod
a
l primeste nu este chiar obiectul curent. Sa presupunem, de exemplu, ca avem o clas
a
Account care are o metoda finalTransfer pentru a transfera toata suma de bani dintr-un
cont n altul. Metoda ar putea fi scrisa astfel:
public void finalTransfer(Account account)
{
dollars += account.dollars ;
account.dollars = 0 ;
}
Sa consideram secventa de cod de mai jos:
Account account1 ;
Account account2 ;
....
account2 = account1 ;
account1.finalTransfer(account2) ;
Deoarece transferam bani n cadrul aceluiasi cont, nu ar trebui sa fie nici o modificare n
cadrul contului. Totusi, ultima linie din finalTransfer are ca efect golirea contului debitor.
O modalitate de a evita o astfel de situatie este folosirea unui test pentru pseudonime:
public void finalTransfer(Account account)
{
if( this = account)//se incearca un transfer in acelasi cont
{
return ;
}
dollars += account.dollars ;
account.dollars = 0 ;
}

4.5.2

Prescurtarea this pentru constructori

Multe clase dispun de mai multi constructori care au un comportament similar. Putem
folosi this n cadrul unui constructor pentru a apela ceilalti constructori ai clasei. O alt
a
posibilitate de a scrie constructorul fara parametri pentru clasa Date este:
public Date()
{
this(1, 1, 2000) ;//apeleaza constructorul Date(int,int,int)
}
Se pot realiza si exemple mai complicate, dar ntotdeauna apelul lui this trebuie sa fie
prima instructiune din constructor, celelalte instructiuni fiind n continuarea acesteia.

58

4.5.3

CAPITOLUL 4. OBIECTE S
I CLASE

Operatorul instanceof

Operatorul instanceof realizeaza o testare de tip n timpul executiei. Rezultatul expresiei:


exp instanceof NumeClasa
este true daca exp este o instanta a lui NumeClasa si false n caz contrar. Daca exp este
null rezultatul este ntotdeauna false. Operatorul instanceof este folosit de obicei nainte
de o conversie de tip, si adeseori este folosit n legatura cu referintele polimorfice pe care
le vom prezenta mai tarziu.

4.5.4

Atribute statice

Atributele statice sunt folosite n situatia n care avem variabile care trebuie partajate de
catre toate instantele unei clase. De obicei atributele statice sunt constante simbolice, dar
acest lucru nu este obligatoriu. Atunci cand un atribut al unei clase este declarat de tip
static, doar o singura instanta a acelei variabile va fi creata. Ea nu face parte din nici o
instanta a clasei. Ea se comporta ca un fel de variabila globala unica, vizibila n cadrul
clasei. Cu alte cuvinte, daca avem declaratia
public class Exemplu
{
private int x ;
private static int y ;
}
fiecare obiect de tip Exemplu va avea propriul atribut x, dar va fi doar un singur y partajat.
O folosire frecventa pentru campurile statice o reprezinta constantele. De exemplu, clasa
Integer defineste atributul MAX VALUE astfel
public final static int MAX_VALUE = 2147483647 ;
Analog se defineste si constanta PI din clasa Math pe care am folosit-o n clasa Circle.
Modificatorul final indica faptul ca identificatorul care urmeaza este o constanta. Daca
aceasta constanta nu ar fi fost un atribut static, atunci fiecare instanta a clasei Integer ar
fi avut un atribut cu numele de MAX VALUE, irosind astfel spatiu n memorie.
Astfel, vom avea o singura variabila cu numele de MAX VALUE. Ea poate fi accesata de
oricare dintre metodele clasei Integer prin identificatorul MAX VALUE. Ea va putea fi
folosita si de catre un obiect de tip Integer numit x prin sintaxa x.MAX VALUE, ca orice
alt camp. Acest lucru este permis doar pentru ca MAX VALUE este public. In sfarsit,
MAX VALUE poate fi folosit si prin intermediul numelui clasei ca Integer. MAX VALUE
(tot pentru ca este public). Aceasta ultima folosire nu ar fi fost permisa pentru un camp
care nu este static.
Chiar si fara modificatorul final, atributele statice sunt foarte folositoare. Sa presupunem
ca vrem sa retinem numarul de obiecte de tip Circle care au fost construite. Pentru aceasta
avem nevoie de o variabila statica. In clasa Circle vom face declaratia:
private static int numarInstante = 0 ;

4.6. PROBLEME PROPUSE

59

Vom putea apoi incrementa numarul de instante n constructor. Daca acest camp nu ar fi
fost de tip static am avea un comportament incorect, deoarece fiecare obiect de tip Circle
ar fi avut propriul atribut numarInstante care ar fi fost incrementat de la 0 la 1.
Remarcati faptul ca, deorece un atribut de tip static nu necesita un obiect care sa l
controleze, fiind partajat de catre toate instantele clasei, el poate fi folosit de catre o
metod
a statica (daca regulile de vizibilitate permit acest lucru). Atributele nestatice ale
unei clase vor putea fi folosite de catre o metoda statica doar daca se furnizeaza si un
obiect care sa le controleze.

4.5.5

Initializatori statici

Atributele statice sunt initializate atunci cand clasa este ncarcata. Uneori este nsa nevoie
de o initializare mai complexa. Sa presupunem de exemplu ca avem nevoie de un sir static
care sa contina radacinile patrate ale primelor 100 numere naturale. O posibilitate ar fi
sa definim o metoda statica si sa cerem programatorului sa apeleze acea metoda nainte
de a folosi sirul.
O alternativa la aceasta solutie este folosirea initializatorului static. Un exemplu este
prezentat n Figura 4.4. Aici initializatorul static se extinde de la linia 5 la linia 11.
Initializatorul static trebuie sa urmeze imediat dupa membrul static.
1. public class Squares
2. {
3.
private static double squareRoots[] = new double[100] ;
4.
5.
static
6.
{
7.
for( int i =0; i < squareRoots.length; ++i)
8.
{
9.
squareRoots[i] = Math.sqrt( (double) i ) ;
10. }
11. }
12. //restul clasei
13.}
Figura 4.4 Exemplu de initializator static.

4.6

Probleme propuse

1. Scrieti o clasa care suporta numere rationale. Atributele clasei ar trebui sa fie dou
a
variabile de tip long una pentru numitor si una pentru numarator. Stocati numarul
sub forma de fractie ireductibila, cu numitorul pozitiv. Formati un numar rezonabil
de constructori; metodele add, subtract, multiply, divide; de asemenea, toString,
equals si compareTo (care se comporta ca cea din clasa String). Asigurati-va c
a
toString merge n cazul n care numitorul este zero.
2. Implementati o clasa pentru numere complexe, Complex. Adaugati aceleasi metode
ca la clasa Rational, daca au sens (de exemplu, compareTo nu are sens aici). Adaugati
accesori pentru partea reala si cea imaginara.

60

CAPITOLUL 4. OBIECTE S
I CLASE

3. Implementati o clasa completa IntType care sa suporte un numar rezonabil de constructori; metodele add, subtract, multiply, divide; de asemenea, toString, equals si
compareTo. Mentineti IntType sub forma unui sir de cifre suficient de mare.
4. Implementati o clasa simpla numita Date. Clasa va trebui sa reprezinte orice data
ntre 1 Ianuarie 1800 si 31 Decembrie 2500. Definiti metode care sa permita scaderera
a doua date, incrementarea unei date cu un numar de zile; compararea a doua date.
O data va fi reprezentata intern ca numarul de zile care au trecut de la o anumita
data,(de exemplu prima zi din 1800).
Solutie: Am creat dou
a siruri care sunt atribute statice. Primul numit pana1Lun
va contine num
arul de zile p
an
a la prima zi din fiecare lun
a a anilor care nu sunt
bisecti. Astfel, el va contine 0,31,59,90 etc. Cel de-al doilea sir, pana1Ian va contine
num
arul de zile p
an
a la nceputul fiec
arui an, ncep
and cu 1800. Astfel, el va contine
0, 365, 730, 1095, 1460, 1826 etc, deoarece 1800 nu este an bisect, dar 1804 este.
Programul va initializa acest sir o singur
a dat
a. Am utilizat acest sir pentru conversia din reprezentarea intern
a (num
ar de zile de la 01.01.1800) n reprezentarea
extern
a (zi/lun
a/an).
Clasa este prezentat
a n cele ce urmeaz
a:
import java.io.*;
import java.util.*;
import java.math.*;
public class Data
{
static int p,ziua,luna,an;
static int pana1Ian[];
static int pana1Lun[]={0,31,59,90,120,151,181,212,243,273,304,334};
public Data()
// prin acest constructor care se apeleaze la inceputul executari
// programului calculam numarul de zile trecute pana la 1 ianuarie
// al anului i+1800 de la 1 ian 1800
{
pana1Ian=new int[701];//aloc memorie pentru vector
pana1Ian[0]=0;
for(int i=1;i<701;i++)
// acest vector va contine numarul zilelor pana in 1 ianuarie a
// anului i+1800
{
pana1Ian[i]=pana1Ian[i-1]+365;
if ((((i+1800-1)%4==0) &&((i+1800-1)%100!=0))||((i+1800-1)%400==0))
pana1Ian[i]+=1;//verificam daca nu este an bisect
}
}

4.6. PROBLEME PROPUSE

61

public static int readInt()


{
BufferedReader nr=new BufferedReader(new InputStreamReader(System.in));
try
{
p= Integer.parseInt(nr.readLine());
}//try
catch(IOException e)
{
System.out.println(e.toString());
}//catch
return p;
}//readInt
public static void readData()
{
do
{
System.out.print("Anul");
an=readInt();
}// citirea anului se repeta pana se introduce un numar
// intre 1800 si 2500
while((an>=2500)||(an<1800));
do
{
System.out.print("Luna");
luna=readInt();
}//citirea lunii se repeta pana se introduce un numar intre 1 si 12
while((luna>12)||(luna<1));
switch (luna)
{
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:
do
{
System.out.print("Ziua");
ziua=readInt();
}//se citeste o zi din lunile cu 31 de zile
while((ziua>31)||(ziua<1));
break;

62

CAPITOLUL 4. OBIECTE S
I CLASE

case 11:
case 4:
case 6:
case 9:
do
{
System.out.print("Ziua");
ziua=readInt();
}//se citeste o zi din lunile cu 30 de zile
while((ziua>30)||(ziua<1));
break;
default :
if (bisect(an)==1)
do
{
System.out.print("Ziua");
ziua=readInt();
}//se citeste o zi din luna februarie a unui an bisect
while((ziua>29)||(ziua<1));
else
do
{
System.out.print("Ziua");
ziua=readInt();
}//se citeste o zi din luna februarie a unui an nebisect
while((ziua>28)||(ziua<1));
break;
}
}//readData
public static int conversie(int ziua,int luna,int an)
{
int s=0;
s+=pana1Ian[an-1800];
if ((bisect(an)==1)&&(luna>2))
s+=1;
s+=pana1Lun[luna-1];
s+=ziua;
return s;
}//conversia unei date in numar de zile trecute de la 1 ianuarie 1800
public static int bisect(int an)
//functie pentru verificarea unui an bisect
{
if ((((an)%4==0) &&((an)%100!=0))||((an)%400==0))
return 1;

4.6. PROBLEME PROPUSE

63

else
return 0;
}
public static String toString(int a)
//conversia numarului de zile trecute de la 1 ian 1800 in data
{
int an=0,lu=0,zi=0;
int i=0,j=0;
while ((a>pana1Ian[j])&&(j<701))
{
j++;//cautam primul an care are trecute, pina la 1 ianuarie,
//un numar mai mare de zile decat numarul pe care dorim sa-l convertim
}
j--;//scadem unu din acel an si avem anul datei
an=j+1800;
if (an>1800)
a=a-pana1Ian[j];//scoatem din numarul initial numarul zilelor
//trecute pana la 1 ianuarie a anului gasit
if (bisect(an)==0)
//verificam daca anul e bisect si apoi cautam luna ce corespunde datei
{
i=0;
while ((pana1Lun[i]<a)&&(i<11))
i++;
if ((pana1Lun[i]<a)&&(i==11))
i++;
i--;
lu=i+1;
if (i >=0)
a=a-pana1Lun[i];
zi=a;
}
else //daca-i bisect problema are trei variante si anume
//daca-i mai mic de 60 se calculeaza ca mai sus, daca nu poate fi
//chiar 60 si atunci suntem pe 29 februarie
//altfel scadem 1 din numarul zilelor si obtinem o data dintr-un
//an obisnuit
{
if (a<60)
{
i=0;
while ((pana1Lun[i]<a)&&(i<12))
i++;
i--;
lu=i+1;

64

CAPITOLUL 4. OBIECTE S
I CLASE

if (i>=0)
a=a-pana1Lun[i];
zi=a;
}
if (a==60)
{
lu=2;
zi=29;
}
if (a>60)
{
a=a-1;
i=0;
while ((pana1Lun[i]<a)&&(i<12))
i++;
if ((pana1Lun[i]<a)&&(i==11))
i++;
i--;
lu=i+1;
if (i>=0)
a=a-pana1Lun[i];
zi=a;
}
}
return zi + "/" + lu + "/" + an ;
}
public static int incrementare(int a)
//incrementam numarul de zile cu un numar
{
int in;
do
{
System.out.print("Cu cate zile:");
in=readInt();
}
while ((in<0)||(in>a)||(in>256000));
return (a+in);
}
public static int decrementare(int a)
//decrementam numarul de zile cu un numar
{
int de;
do
{

4.6. PROBLEME PROPUSE

65

System.out.print("Cu cate zile:");


de=readInt();
}
while ((de<0)||(de>a)||(a-de<=0));
return (a-de);
}
public static void compareto(int a,int b)//compararea a doua date
{
if (a>b)
System.out.println("Prima data este mai marecu "+(a-b)+" zile");
if (a==b)
System.out.println("Date identice!! De ce le mai compari???");
if (a<b)
System.out.println("Prima data este mai micacu "+(b-a)+" zile");
}
public static boolean equals(int a,int b)
{
if (a==b)
return true;
else
return false;
}
public static void main(String[]args)
{
int v=0,a,b;
String s;
Data d=new Data();
do
{
System.out.println();
System.out.println(" Alegeti metoda");
System.out.println("
1 Conversia");
System.out.println("
2 Decrementare");
System.out.println("
3 Incrementare");
System.out.println("
4 Compareto");
System.out.println("
5 Equals");
System.out.println("
6 Diferenta");
System.out.println("
7 Exit");
System.out.print ("Alegeti numarul metodei:");
do
v=readInt();
while ((v<0)&&(v>7));
switch (v)

66

CAPITOLUL 4. OBIECTE S
I CLASE

{
case 1:
readData();
System.out.println(ziua+"/"+luna+"/"+an);
a=0;
a=conversie(ziua,luna,an);
System.out.println("conversia datei este numarul:"+a);
break;
case 2:
readData();
System.out.println(ziua+"/"+luna+"/"+an);
a=0;
a=conversie(ziua,luna,an);
a=decrementare(a);
s="";
s=toString(a);
System.out.println("decrementarea datei este data:"+s);
break;
case 3:
readData();
System.out.println(ziua+"/"+luna+"/"+an);
a=0;
a=conversie(ziua,luna,an);
a=incrementare(a);
s="";
s=toString(a);
System.out.println("incrementarea datei este data:"+s);
break;
case 4:
readData();
a=0;
a=conversie(ziua,luna,an);
readData();
b=0;;
b=conversie(ziua,luna,an);
compareto(a,b);
break;
case 5:
readData();
a=0;
a=conversie(ziua,luna,an);
readData();
b=0;
b=conversie(ziua,luna,an);
boolean bun;
bun=equals(a,b);

4.6. PROBLEME PROPUSE

if (bun)
System.out.println("Aceeasi data");
else
System.out.println("Data diferita");
break;
case 7 :
System.out.println("La revedere");
break;
case 6 :
readData();
a=0;
a=conversie(ziua,luna,an);
System.out.println("Scadem data.");
readData();
b=0;
b=conversie(ziua,luna,an);
System.out.println("Diferenta este de"+(a-b)+"zile");
break;
default :
System.out.println("Mai incercati");
}
}
while (v!=7);
}
}

67

Capitolul 5

Mostenire
Asa cum am mentionat n capitolul anterior, unul dintre principalele scopuri ale programarii orientate pe obiecte este reutilizarea codului. La fel cum inginerii folosesc aceleasi
componente din nou si din nou n proiectarea circuitelor, programatorii au posibilitatea sa
refoloseasca obiectele, n loc sa le reimplementeze. In limbajele de programare orientate pe
obiecte, mecanismul fundamental pentru refolosirea codului este mostenirea. Mostenirea
ne permite s
a extindem functionalitatea unui obiect. Cu alte cuvinte, putem crea noi
obiecte cu proprietati extinse (sau restr
anse) ale tipului original, formand astfel o ierarhie
de clase. De asemenea, mostenirea este mecanismul pe care Java l foloseste pentru a
implementa metode si clase generice. In acest capitol vom prezenta:
principiile generale ale mostenirii, inclusiv polimorfismul
cum este mostenirea implementat
a n Java
cum poate fi derivat
a o colectie de clase dintr-o singur
a clas
a abstract
a
interfata, care este un caz particular de clas
a abstract
a
cum se poate realiza programarea generic
a n Java folosind interfete

5.1

Ce este mostenirea?

Mostenirea este principiul fundamental al programarii orientate pe obiecte care permite


refolosirea codului ntre clasele nrudite. Mostenirea modeleaza relatii de tipul ESTEUN (sau ESTE-O). Intr-o relatie de tip ESTE-UN, spunem ca clasa derivata ESTE-O
(variatiune) a clasei de baza. De exemplu, Cerc ESTE-O Curba, iar Masina ESTE-UN
Vehicul. In schimb, o Elipsa NU-ESTE-UN Cerc. Relatiile de mostenire formeaza ierarhii.
De exemplu, putem extinde clasa Masina la MasinaStraina (pentru care se plateste vama)
si MasinaAutohtona (care nu plateste vama) etc.
Un alt tip de relatie ntre obiecte este relatia ARE-UN sau ESTE-COMPUS-DIN. Aceasta
relatie nu are proprietatile care ar fi normale ntr-o ierarhie de mostenire. Un exemplu
de astfel de relatie este ca Masina ARE-UN Volan. Relatiile de tip ARE-UN nu trebuie
modelate prin mostenire. Ele se vor modela prin agregare, n care componentele devin
simple campuri de tip private.
68

5.1. CE ESTE MOS


TENIREA?

69

Chiar si limbajul Java foloseste din plin mostenirea pentru a-si implementa propriile biblioteci de clase. Un exemplu relativ familiar l constituie exceptiile. Java defineste clasa
Exception. Asa cum am vazut deja, exista mai multe tipuri de exceptii, cum ar fi NullPointerException si ArrayIndexOutOfBoundsException. Fiecare constituie o clasa separata, ns
a
toate dispun de metoda toString pentru a furniza mesaje de eroare utile n depanare.
Mostenirea modeleaza aici o relatie de tip ESTE-UN. NullPointerException ESTE-O Exception. Datorita relatiei de tip ESTE-UN, proprietatea fundamentala a mostenirii garanteaza ca orice metoda care poate fi aplicata lui Exception poate fi aplicata si lui NullPointerException. Mai mult decat atat, un obiect de tip NullPointerException poate
sa fie referit de catre o referinta de tip Exception (reciproca nu este adevarata!). Astfel, deoarece metoda toString este o metoda disponibila n clasa Exception, vom putea
ntotdeauna scrie:
catch( Exception e )
{
System.out.println( e.toString() ) ;
}
Daca e refera un obiect de tip NullPointerException, atunci e.toString() este un apel corect.
Functie de modul de implementare al ierarhiei de clase, metoda toString ar putea fi invarianta sau ar putea fi specializat
a pentru fiecare clasa distincta. Atunci cand o metoda este
invarianta n cadrul unei ierarhii, adica are aceeasi functionalitate pentru toate clasele din
ierarhie, nu va trebui sa rescriem implementarea acelei metode pentru fiecare clasa.
Apelul lui toString mai ilustreaza un alt principiu important al programarii orientate pe
obiecte, cunoscut sub numele de polimorfism. O variabila referinta care este polimorfic
a
poate sa refere obiecte de tipuri diferite. Atunci cand se aplica o metoda referintei, se
selecteaza automat operatia adecvata pentru tipul obiectului care este referit n acel moment. In Java, toate tipurile referinta sunt polimorfice. In cazul unei referinte de tip
Exception se ia o decizie n timpul executiei: se va apela metoda toString pentru obiectul
pe care e l refera n acel moment n timpul executiei. Acest proces este cunoscut sub
numele de legare t
arzie sau legare dinamic
a.
In cazul mostenirii avem o clasa de baza din care sunt derivate alte clase. Clasa de baz
a
este clasa pe care se bazeaza mostenirea. O clas
a derivat
a mosteneste toate proprietatile
clasei de baza, nsemnand ca toti membri publici ai clasei de baza devin membri publici
ai clasei derivate. Clasa derivata poate sa adauge noi atribute si metode si poate modifica
semnificatia metodelor mostenite. Fiecare clasa derivata este o clasa complet noua. Clasa
de baza nu este n nici un fel afectata de modificarile aduse n clasele derivate. Astfel, la
crearea clasei derivate este imposibil sa se strice ceva n clasa de baza.
O clas
a derivata este compatibila ca tip cu clasa de baza, ceea ce nseamna ca o variabil
a
referinta de tipul clasei de baza poate referi un obiect al clasei derivate, dar nu si invers.
Clasele surori (cu alte cuvinte clasele derivate dintr-o clasa comuna) nu sunt compatibile
ca tip.
Asa cum am mentionat anterior, folosirea mostenirii genereaza de obicei o ierarhie de clase.
Figura 5.1 prezinta o mica parte din ierarhia de exceptii a limbajului Java. Remarcati
faptul ca NullPointerException este indirect derivata din Exception. Acest lucru nu constituie nici o problema, deoarece relatiile de tipul ESTE-UN sunt tranzitive. Cu alte cuvinte,
daca X ESTE-UN Y si Y ESTE-UN Z, atunci X ESTE-UN Z. Ierarhia Exception ilustreaz
a

70

CAPITOLUL 5. MOS
TENIRE

n acelasi timp designul clasic n care se extrag caracteristicile comune n clasa de baza,
urmate de specializari n clasele derivate. ntr-o astfel de ierarhie spunem ca clasa derivata
este o subclas
a a clasei de baza, iar clasa de baza este o superclas
a a clasei derivate. Aceste
relatii sunt tranzitive; mai mult, operatorul instanceof functioneaza pentru subclase. Astfel, daca obj este de tipul X (si nu e null), atunci expresia obj instanceof Z este adevarata.
In urmatoarele sectiuni vom examina urmatoarele probleme:
Care este sintaxa folosit
a pentru a deriva o clas
a nou
a dintr-o clas
a de
baz
a?
Cum afecteaz
a acest lucru statutul membrilor private sau public?
Cum preciz
am faptul c
a o metod
a este invariant
a pentru o ierarhie de
clase?
Cum specializ
am o metod
a?
Cum factoriz
am aspectele comune ntr-o clas
a abstract
a, pentru a crea
apoi o ierarhie?
Putem deriva o clas
a nou
a din mai mult de o clas
a (mostenire multipl
a)?
Cum se foloseste mostenirea pentru a implementa codul generic?
Cateva dintre aceste subiecte sunt ilustrate prin implementarea unei clase Shape din
care vom deriva clasele Circle, Square si Rectangle. Ne vom folosi de acest exemplu
pentru a vedea cum Java implementeaz
a polimorfismul cat si pentru a vedea cum poate
fi mostenirea folosita pentru a implementa metode generice.
ArrayIndexOutOfBoundsException


IndexOutOfBoundsException
J


J
^
J

RuntimeException H
StringIndexOutOfBoundsException
HH
j


NullPointerException

Exception

EOFException
J
6


^
J

IOException
J
J
J
^
J

FileNotFoundException

Figura 5.1 O parte din ierarhia de exceptii a limbajului Java

JAVA
5.2. SINTAXA DE BAZA

5.2

71

Sintaxa de baz
a Java

O clasa derivata mosteneste toate proprietatile clasei de baza. Clasa derivata poate apoi
sa adauge noi atribute, sa redefineasca metode sau sa adauge noi metode. Fiecare clas
a
derivat
a este o clasa complet noua. Aspectul general al unei clase derivate este prezentat
n Figura 5.2. Clauza extends declara faptul ca o clasa este derivata dintr-o alta clasa.
1. public class ClasaDerivata extends ClasaDeBaza
2. {
3. //orice membri (public sau protected) care nu sunt listati
4. //vor fi mosteniti nemodificati, cu exceptia constructorului
5.
6. //membri public
7. //constructor(i), daca cel implicit nu este suficient
8. //met din ClasaDeBaza a caror implementare este modificata
9. //metode publice aditionale
10.
11. //membri private
12. //atribute suplimentare (in general private)
13. //alte metode private
14.}
Figura 5.2 Aspectul general al unei clase derivate
Iata o descriere scurta a unei clase derivate:
In general, toate atributele sunt private, deci atributele suplimentare vor fi adaugate
clasei derivate prin precizarea lor n sectiunea private.
Orice metode din clasa de baza care nu sunt precizate n clasa derivata sunt mostenite
nemodificat, cu exceptia constructorului. Cazul particular al constructorului l vom
prezenta n paragraful 5.2.2.
Orice metoda din clasa de baza care este definita n sectiunea public a clasei derivate
este redefinita. Noua metoda va fi aplicata obiectelor din clasa derivata.
Metodele public din clasa de baza nu pot fi redefinite n sectiunea private a clasei
derivate.
Clasei derivate i putem adauga metode suplimentare.

5.2.1

Reguli de vizibilitate

S
tim deja ca orice membru care este declarat a fi private este accesibil doar n metodele
clasei. Rezulta deci ca nici un membru private din clasa de baza nu va fi accesibil n clasa
derivat
a.
Exista situatii n care clasa derivata trebuie sa aiba acces la membri clasei de baza. Exist
a
doua optiuni pentru a realiza acest lucru. Prima este aceea de a utiliza accesul de tip
public sau friendly. Totusi, accesul de tip public permite accesul si altor clase, pe lang
a

72

CAPITOLUL 5. MOS
TENIRE

clasele derivate. Accesul de tip friendly functioneaza doar daca ambele clase sunt n acelasi
pachet.
Daca dorim sa restrangem accesul unor membri, astfel ncat ei sa fi vizibili doar pentru
clasele derivate, putem sa declaram membri ca fiind de tip protected. Un membru de tip
protected este private pentru toate clasele cu exceptia claselor derivate (si a claselor din
acelasi pachet). Declararea atributelor ca fiind public sau protected ncalca principiile
ncapsularii si ascunderii informatiei si se recurge la ea doar din motive de comoditate. De
obicei, este preferabil sa se scrie modificatori si accesori sau sa se foloseasca accesul de tip
friendly. Totusi, daca o declaratie protected va scuteste de scrierea de cod stufos, atunci
se poate recurge la ea.

5.2.2

Constructor si super

Fiecare clasa derivata trebuie sa si defineasca proprii constructori. Daca nu se scrie nici un
constructor, Java va genera un constructor implicit fara parametri. Acest constructor va
apela constructorul fara parametri al clasei de baza pentru membrii care au fost mosteniti,
dupa care va aplica initializarea implicita pentru atributele adaugate (adica 0 pentru
tipurile primitive si null pentru tipurile referinta).
Construirea unei obiect al unei clase derivate are loc prin construirea prealabila a portiunii
mostenite. Acest lucru este natural, deoarece principiul ncapsularii afirma ca partea
mostenita este o entitate unica, iar constructorul clasei de baza ne spune cum sa initializam
aceasta entitate.
Constructorii clasei de baza pot fi apelati explicit prin metoda super. Astfel, constructorul
implicit pentru o clasa derivata are de fapt forma:
public Derived()
{
super() ;
}
Metoda super poate fi apelata si cu parametri care sa se potriveasca cu un constructor
din clasa de baza. De exemplu, daca clasa de baza are un constructor care accepta doi
parametri de tip int, atunci constructorul clasei derivate ar putea fi:
public Derived(int x, int y)
{
super(x, y) ;
// alte instructiuni
}
Metoda super poate sa apara doar n prima linie dintr-un constructor. Daca nu se face un
apel explicit, compilatorul va realiza automat un apel al metodei super fara parametri.

5.2.3

Metode si clase final

Asa cum am precizat deja mai devreme, clasa derivata poate sa redefineasca sau sa accepte
nemodificate metodele din clasa de baza. In multe cazuri este clar faptul ca o anumita
metoda trebuie sa fie invarianta de-a lungul ierarhiei, ceea ce nseamna ca clasele derivate

JAVA
5.2. SINTAXA DE BAZA

73

nu trebuie sa o redefineasca. In acest caz, putem declara metoda ca fiind de tip final iar
ea nu va putea fi redefinita.
Pe langa faptul ca declararea unei metode final este o practica buna de programare, ea
poate genera si cod mai rapid. Declararea unei metode final (atunci cand este cazul) constituie o practica buna de programare deoarece intentiile noastre devin astfel clare pentru
cititorul programului si al documentatiei si, pe de alta parte, putem preveni redefinirea
accidentala pentru o metoda care nu trebuie sa fie redefinita.
Pentru a vedea de ce folosirea lui final poate conduce la cod mai eficient, sa presupunem
ca avem o clasa de baza numita Base care defineste o metoda final f , iar Derived este o
clasa care extinde Base. Sa consideram functia:
void xxx(Base obj)
{
obj.f()
}
Deoarece f este o metoda final, nu are nici o importanta daca n momentul executiei obj
refera un obiect de tip Base sau un obiect de tip Derived; definirea lui f este invarianta, deci
stim de la nceput ceea ce f va face. O consecinta a acestui fapt este ca decizia pentru codul
care va fi executat se ia nca de la compilare, si nu la executie. Acest proces este cunoscut
sub numele de legare static
a. Deoarece legarea se face la compilare si nu la executie,
programul ar trebui sa ruleze mai repede. Daca acest fapt este sau nu perceptibil efectiv
(tinand cont de viteza de prelucrare a procesoarelor din generatia actuala) depinde de
numarul de ori n care evitam deciziile din timpul executiei n timpul rularii programului.
Un corolar al acestei observatii l constituie faptul ca daca f este o metoda banala, cum ar
fi un accesor pentru un atribut, compilatorul ar putea sa nlocuiasca apelul lui f, direct cu
corpul functiei. Astfel, apelul functiei va fi nlocuit cu o singura linie care acceseaza un
atribut, economisindu-se astfel timp. Daca f nu ar fi fost declarata final, acest lucru ar fi
fost imposibil, deoarece obj ar fi putut referi un obiect al unei clase derivate, pentru care
definirea lui f ar fi putut fi diferita.
Metodele statice nu au un obiect care sa le controleze, deci apelul lor este rezolvat nca de
la compilare folosind legarea statica.
Similare metodelor final sunt clasele final. O clasa final nu mai poate fi extinsa. In
consecinta, toate metodele unei astfel de clase sunt metode final. Ca un exemplu, clasa
Integer este o clasa final. De remarcat faptul ca daca o clasa are doar membri final, ea nu
este neaparat o clasa final.

5.2.4

Redefinirea unei metode

Metodele din clasa de baza pot fi redefinite n clase derivate prin furnizarea unei metode
din clasa derivata care sa aiba aceeasi semnatura. Metoda din clasa derivata trebuie s
a
aiba aceeasi semnatura si nu are dreptul sa adauge exceptii la lista throws.
Adeseori, metoda din clasa derivata trebuie sa apeleze metoda din clasa de baza. Acest
proces este numit redefinire partial
a. Cu alte cuvinte vrem sa facem ceea ce face si metoda
din clasa de baza, plus nca ceva. Apeluri ale clasei de baza pot fi realizate folosind super.
Iata un exemplu:
public class Student extends Human

74

CAPITOLUL 5. MOS
TENIRE

{
public doWork()
{
takeBreak();
//pauzele lungi si dese
super.doWork();//cheia marilor
takeBreak();
//succese
}
}

5.2.5

Metode si clase abstracte

Pana acum am vazut faptul ca unele metode sunt invariante de-a lungul ierarhiei de clase
(metodele final), iar alte metode si modifica semnificatia de-a lungul ierarhiei. O a treia
posibilitate este ca o metoda din clasa de baza sa aiba sens doar pentru clasele derivate,
si vrem ca ea sa fie obligatoriu definita n clasele derivate; totusi, implementarea metodei
nu are nici un sens pentru clasa de baza. In aceasta situatie, putem declara metoda ca
fiind abstracta.
O metod
a abstract
a este o metoda care declara functionalitati care vor trebui neaparat
implementate pana la urma n clasele derivate. Cu alte cuvinte o metoda abstracta spune
ceea ce obiectele derivate trebuie sa faca. Totusi, ea nu furnizeaza nici un fel de implementare; fiecare clasa derivata trebuie sa vina cu propria implementare.
O clasa care are cel putin o metoda abstracta este o clas
a abstract
a. Java pretinde ca toate
clasele abstracte sa fie definite explicit ca fiind abstracte. Atunci cand o clasa derivata nu
redefineste o metoda abstracta, metoda ramane abstracta si n clasa de baza. In consecinta
daca o clasa care nu intentionam sa fie abstracta nu redefineste toate metodele abstracte,
compilatorul va detecta inconsistenta si va genera un mesaj de eroare.
Un exemplu simplu de clasa abstracta este clasa Shape (shape nseamna forma sau curba),
pe care o vom folosi ntr-un exemplu n cadrul acestui capitol. Din Shape vom deriva forme
specifice cum ar fi Circle sau Rectangle. Putem deriva apoi Square ca un caz particular de
Rectangle. Figura 5.3 prezinta ierarhia de clase care rezulta.
.
Figura 5.3 Ierarhia de clase pentru forme
Clasa Shape poate sa aiba membri care sa fie comuni pentru toate clasele. Intr-un exemplu
mai extins, aceasta ar putea include coordonatele extremitatilor obiectului. Ea declara si
defineste metode cum ar fi positionOf, care sunt independente de tipul formei; positionOf
ar fi o metoda final. Ea defineste si metode care se aplica fiecarui obiect n parte. Unele
dintre aceste metode nu au nici un sens pentru clasa abstracta Shape. De exemplu, este
dificil de calculat aria unui obiect oarecare; metoda area va fi declarata abstract.
Asa cum am mentionat anterior, existenta cel putin a unei metode abstracte, face clasa
sa devina si ea abstracta, deci ea nu va putea fi instantiata. Astfel, nu vom putea crea un
obiect de tip Shape; vom putea crea doar obiecte derivate. Totusi, ca de obicei, o referinta
de tip Shape poate sa refere orice forma concreta derivata, cum ar fi Circle sau Rectangle.
Exemplu:

75

JAVA
5.2. SINTAXA DE BAZA

Shape a,b ;
a = new Circle( 3.0 ) ;
b = new Shape( "circle" ) ;

//corect
//incorect

Codul din Figura 5.4 prezinta clasa abstracta Shape. La linia 13, declaram o variabil
a
de tip String care retine tipul formei, folosita doar n clasele derivate. Atributul este de
tip private, deci clasele derivate nu au acces direct la el. Restul clasei cuprinde o lista de
metode.
Constructorul nu va fi apelat niciodata direct, deoarece Shape este o clasa abstracta. Avem
totusi nevoie de un constructor care sa fie apelat din clasele derivate pentru a initializa
atributele private. Constructorul clasei Shape stabileste valoarea atributului name.
Linia 15 declara metoda abstracta area. area este o metoda abstracta deoarece nu putem
furniza nici un calcul implicit al ariei pentru o clasa derivata care nu si defineste propria
metod
a de calcul a ariei.
Metoda de comparatie din liniile 22-25 nu este abstracta, deoarece ea poate fi aplicat
a
n acelasi mod pentru toate clasele derivate. De fapt, definirea ei este invarianta de-a
lungul ierarhiei, de aceea am declarat-o final. Parametrul rhs (de la right-hand-side)
reprezinta un alt obiect de tip Shape, a carui arie se compara cu cea a obiectului curent.
Este interesant de remarcat faptul ca variabila rhs poate sa refere orice instanta a unei
clase derivate din Shape (de exemplu o referinta a clasei Rectangle). Astfel este posibil ca
folosind aceasta metoda sa comparam aria obiectului curent (care poate fi, de exemplu, o
instant
a a clasei Circle) cu aria unui obiect de alt tip, derivat din Shape. Acesta este un
exemplu excelent de folosire a polimorfismului.
Metoda toString din liniile 27-30 afiseaza numele formei si aria ei. Ca si metoda de
comparatie, ea este invarianta de-a lungul ierarhiei, de aceea a fost declarata final.
Inainte de a trece mai departe, sa rezumam cele 4 tipuri de metode ale unei clase:
1. Metode finale. Apelul lor este rezolvat nca de la compilare. Folosim metode final
doar atunci cand metoda este invarianta de-a lungul ierarhiei (adica atunci cand
metoda nu este niciodata redefinita).
2. Metode abstracte. Apelul lor este rezolvat n timpul executiei. Clasa de baza nu
furnizeaza nici o implementare a lor si este abstracta. Clasele derivate trebuie fie s
a
implementeze metoda, fie devin ele nsele abstracte.
3. Metode statice. Apelul este rezolvat la compilare, deoarece nu exista obiect care s
a
le controleze.
4. Alte metode. Apelul este rezolvat la executie. Clasa de baza furnizeaza o implementare implicita care fie va fi redefinita n clasele derivate, fie acceptata nemodificata.
1.
2.
3.
4.
5.
6.

//Clasa de baza abstracta pentru forme


//
//CONSTRUIREA: nu este permisa; Shape este abstracta
//constructorul cu un param. este furnizat ptr. clasele derivate
//
//***************Metode publice***************************

76

CAPITOLUL 5. MOS
TENIRE

7. //double area()
--> Intoarce aria (abstracta)
8. //boolean lessThan --> Compara doua forme dupa arie
9. //String toString
--> Metoda uzuala pentru scriere
10.
11.abstract class Shape
12.{
13. private String name ;
14.
15. abstract public double area() ;
16.
17. public Shape( String shapeName )
18. {
19. name = shapeName ;
20. }
21.
22. final public boolean lessThan( Shape rhs )
23. {
24. return area() < rhs.area() ;
25. }
26.
27. final public String toString()
28. {
29. return name + "avand aria " + area() ;
30. }
31.}
Figura 5.4 Clasa de baz
a abstract
a Shape

5.3

Exemplu: Extinderea clasei Shape

In aceasta sectiune vom implementa clasele derivate din clasa Shape si vom prezenta cum
sunt ele utilizate ntr-o maniera polimorfica. Iata enuntul problemei:
Sortare de forme. Se citesc N forme (cercuri, dreptunghiuri sau p
atrate). S
a se afiseze
ordonate dup
a arie.
Implementarea claselor, prezentata n Figura 5.5 este simpla si nu ilustreaza aproape
nici un concept pe care sa nu-l fi prezentat deja. Singura noutate este ca clasa Square este
derivata din clasa Rectangle care este, la randul ei, derivata din Shape. La implementarea
acestor clase trebuie sa:
1. Definim un nou constructor
2. Sa examinam fiecare metoda care nu este final sau abstract pentru a vedea daca dorim
sa o acceptam nemodificata. Pentru fiecare astfel de metoda care nu corespunde cu
necesitatile clasei trebuie sa furnizam o noua definire.

5.3. EXEMPLU: EXTINDEREA CLASEI SHAPE

3. Definirea fiecarei metode abstracte.


4. Adaugarea de alte metode daca este necesar.
1. //clasele Circle, Square si Rectangle;
2. //toate sunt derivate din Shape
3. //
4. //CONSTRUCTORI: (a) cu raza (pentru cerc), (b) cu latura
5. //(pentru patrat), (c) cu lungime si latime (pentru dreptunghi)
6. //*****************METODE PUBLICE****************************
7. //double area()-->Implementeaza metoda abstracta din Shape
8.
9. //Fisierul trebuie separat in 3 pentru compilare!!!
10.
11.public class Circle extends Shape
12.{
13. static final private PI = 3.141592653589793 ;
14. private double radius ;
15.
16. public Circle( double rad )
17. {
18. super( "Circle" ) ;
19. radius = rad ;
20. }
21.
22. public double area()
23. {
24. return PI*radius*radius ;
25. }
26.}
27.
28.public class Rectangle extends Shape
29.{
30. private double length ;
31. private double width ;
32. public Rectangle( double len, double wid )
33. {
34. super( "Rectangle" ) ;
35. length = len ;
36. width = wid ;
37. }
38.
39. public double area
40. {
41. return length*width ;
42. }
43.}

77

78

CAPITOLUL 5. MOS
TENIRE

44.
45.public class Square extends Shape
46.{
47. public Square( double side)
48. {
49. super(side, side) ;
50. }
51.}
Figura 5.5 Codul complet pentru clasele Circle, Rectangle si Square, care va fi salvat n
trei fisiere surs
a separate.
Pentru fiecare clasa am scris un constructor simplu care permite initializarea cu dimensiunile de baza (raza pentru cercuri, lungimea laturilor pentru dreptunghiuri si patrate). In
constructor, vom initializa mai ntai partea mostenita prin apelul lui super. Fiecare clasa
trebuie sa defineasca o metoda area, deoarece Shape a declarat-o ca fiind abstracta. Daca
uitam sa scriem o metoda area pentru una dintre clase, eroarea va fi detectata nca de la
compilare, deoarece - daca metoda area lipseste - atunci clasa derivata este si ea abstracta.
Observati ca clasa Square este dispusa sa accepte metoda area mostenita de la Rectangle,
de aceea nu o mai redefineste.
Dupa ce am implementat clasele, suntem gata sa rezolvam problema originala. Vom folosi
un sir de clase Shape. Retineti faptul ca prin aceasta nu alocam memorie pentru nici un
obiect de tip Shape (ceea ce ar fi ilegal); se aloca memorie doar pentru un sir de referinte
catre Shape. Aceste referinte vor putea referi obiecte de tip Circle, Rectangle sau Square1 .
In Figura 5.6 facem exact acest lucru. Mai ntai citim obiectele. La linia 21, apelul
lui readShape consta n citirea unui caracter, urmata de dimensiunile figurii si de crearea
unui nou obiect de tip Shape. Figura 5.7 prezinta o implementare primitiva a acestei
rutine. Observati ca n cazul unei erori la citire se creeaza un cerc de raza 0 si se ntoarce o
referinta la el. O solutie mai eleganta n aceasta situatie ar fi fost sa definim si sa aruncam
o exceptie.
Dupa aceasta, fiecare obiect create de catre readShape este referit de catre un element
al sirului shapes. Se apeleaza insertionSort pentru a sorta formele. In final afisam sirul
rezultat de forme, apeland astfel implicit metoda toString.
1. import java.io.* ;
2.
3. class TestShape
4. {
5. private static BufferedReader in ;
6.
7. public static void main( String[] args )
8. {
9. try
10. {
1
Nici nu se poate inventa o ilustrare mai bun
a pentru polimorfism (poli=mai multe, morphos=forme).
Intr-adev
a referinta de tip Shape (shape=form
a) poate referi clase de mai multe (poli) forme (morphos):
Circle, Rectangle si Square.

5.3. EXEMPLU: EXTINDEREA CLASEI SHAPE

11. //Citeste numarul de figuri


12. in = new BufferedReader( new
13.
InputStreamReader( System.in ) ) ;
14. System.out.println( "Numarul de figuri: ") ;
15. int numShapes = Integer.parseInt(in.readLine()) ;
16.
17. //citeste formele
18. Shape[] shapes = new Shape[ numShapes ] ;
19. for( int i=0; i< numShapes; ++i)
20. {
21.
shapes[i] = readShape() ;
22. }
23.
24. //sortare si afisare
25. insertionSort( shapes ) ;
26. System.out.println("Figurile sortate dupa arie sunt: " );
27. for( int i=0; i< numShapes; ++i)
28. {
29.
System.out.println( shapes[i] ) ;
30. }
31. }
32. catch( Exception e)
33. {
34. System.out.println(e) ;
35. }
36.}
37.
38.private static Shape readShape()
39.{
40. /*Implementarea in Figura 5.7*/
41.}
42.
43.//sortare porin insertie
44.private static void insertionSort( Shape[] a )
45.{
46. /*Implementarea in Figura 5.8*/
47.}
48.}
Figura 5.6 Rutina main pentru citirea de figuri si afisarea lor n ordine cresc
atoare.
1.
2.
3.
4.
5.

//creaza un obiect adecvat de tip Shape functie de


//datele de intrare.
//utilizatorul introduce c, s sau r pentru a indica
//forma, apoi introduce dimensiunile
//in caz de eroare se intoarce un cerc de raza 0

79

80

CAPITOLUL 5. MOS
TENIRE

6.
7. private static Shape readShape()
8. {
9.
double rad ;
10. double len ;
11. double wid ;
12. String s ;
13.
14. try
15. {
16.
System.out.println( "Introduceti tipul formei: ") ;
17.
do
18.
{
19.
s = in.readLine() ;
20.
}while( s.length() == 0 ) ;
21.
22.
switch( s.charAt(0) )
23.
{
24.
case c:
25.
System.out.println("Raza cercului: " ) ;
26.
rad = Integer.parseInt( in.readLine() ) ;
27.
return new Circle( rad ) ;
28.
case s:
29.
System.out.println("Latura patratului: " ) ;
30.
len = Integer.parseInt( in.readLine() ) ;
31.
return new Square( len ) ;
32.
case r :
33.
System.out.println("Lung. si latimea dreptunghiului "
34.
+ "pe linii separate: ") ;
35.
len = Integer.parseInt( in.readLine() ) ;
36.
wid = Integer.parseInt( in.readLine() ) ;
37.
return new Rectangle( len, wid ) ;
38.
default:
39.
System.err.println( "Introduceti c, r sau s") ;
40.
return new Circle( 0 ) ;
41.
}
42. }
43. catch( IOException e )
44. {
45.
System.err.println( e ) ;
46.
return new Circle( 0 ) ;
47. }
48.}
Figura 5.7 Rutin
a simpl
a pentru citirea si ntoarcerea unei noi forme.
49.//sortare porin insertie


5.4. MOS
TENIRE MULTIPLA

81

50.private static void insertionSort( Shape[] a )


51.{
52. for( int p=1; p < a.length; ++p )
53. {
54. Shape tmp = a[p] ;
55. int j = p ;
56. for( ; j>0 && tmp.lessThan( a[j-1] ) ; --j)
57. {
58.
a[j] = a[j-1] ;
59. }
60. a[j] = tmp ;
61. }
62.}
Figura 5.8 Sortare prin insertie

5.4

Mostenire multipl
a

Toate exemplele prezentate pana acum, derivau o clasa dintr-o singura clasa de baza. In
cazul mostenirii multiple o clasa poate fi derivata din mai mult de o clasa de baza. De
exemplu, putem avea clasele Student si Angajat. Din aceste clase ar putea fi derivata o
clasa AngajatStudent.
Desi mostenirea multipla pare destul de atragatoare, si unele limbaje (cum ar fi C++)
chiar o implementeaza, ea este mbibata de subtilitati care fac proiectarea claselor deosebit
de dificila. De exemplu, cele doua clase de baza ar putea contine metode care au aceeasi
semnatura, dar implementari diferite, sau ar putea avea atribute cu acelasi nume. Care
dintre ele ar trebui folosit?
Din aceste motive, Java nu permite mostenirea multipla. Java furnizeaza nsa o alternativa, numita interfat
a.

5.5

Interfete

Interfata n Java este cea mai abstracta clasa posibila. Ea consta doar din metode abstracte publice si din atribute statice si finale.
Spunem ca o clasa implementeaz
a o anumita interfata daca furnizeaza definitii pentru
toate metodele abstracte din cadrul interfetei. O clasa care implementeaza o interfata se
comporta ca si cand ar fi extins o clasa abstracta precizata de catre acea interfata.
In principiu, diferenta esentiala dintre o clasa abstracta si o interfata este ca desi amandou
a
furnizeaza o specificatie a ceea ce clasele derivate trebuie sa faca, interfata nu poate furniza
nici un fel de detaliu de implementare sub forma de atribute sau de metode implementate.
Consecinta practica a acestui lucru este ca interfetele nu sufera de problemele potentiale
pe care le are mostenirea multipla, deoarece nu putem avea implementari diferite pentru aceeasi metoda. Astfel, desi o clasa poate sa extinda o singura clasa, ea poate s
a
implementeze mai mult de o singura interfata.

82

CAPITOLUL 5. MOS
TENIRE

5.5.1

Definirea unei interfete

Din punct de vedere sintactic, nimic nu este mai simplu decat precizarea unei interfete.
Interfata arata ca o declaratie a unei clase, doar ca foloseste cuvantul cheie interface. Ea
consta dintr-o lista de metode care trebuie declarate. Un exemplu de interfata este Comparable, prezentata n Figura 5.9.
Interfata Comparable precizeaza doua metode pe care orice clasa derivata din ea trebuie
sa le implementeze: compareTo si lessThan. Metoda compareTo se va comporta similar
cu metoda compareTo a clasei String. Observati ca nu este necesar sa precizam faptul ca
aceste metode sunt public sau abstract, deoarece acest lucru este implicit pentru metodele
unei interfete.

public Interface Comparable


{
int compareTo(Comparable rhs) ;
boolean lessThan(Comparable rhs) ;
}
Figura 5.9 Interfata Comparable

5.5.2

Implementarea unei interfete

O clasa implementeaza o interfata n doi pasi:


1. declara ca implementeaza interfata si
2. defineste implementari pentru toate metodele din interfata.
Un exemplu este prezentat n Figura 5.10, n care se defineste clasa MyInteger. Clasa
MyInteger are un comportament asemanator cu al clasei Integer, din pachetul java.lang.
In linia 1 se vede ca atunci cand implementam o interfata folosim cuvantul cheie implements n loc de extends. In aceasta clasa putem scrie orice metode dorim, dar trebuie
sa definim cel putin metodele din interfata. Interfata este implementata n liniile 27-36.
Remarcati faptul ca trebuie sa implementam exact metoda precizata n cadrul interfetei;
din acest motiv aceste metode au ca parametru un obiect de tip Comparable si nu un
MyInteger.
O clasa care implementeaza o interfata poate fi extinsa cu conditia sa nu fie finala. Astfel,
daca MyInteger nu ar fi fost finala, am fi putut-o extinde.
O clasa care implementeaza o interfata poate totusi sa extinda si o alta clasa. De exemplu,
am fi putut, n principiu, scrie:

public class MyInteger extends Integer implements Comparable


Acest cod este incorect doar pentru ca Integer este o clasa finala care nu poate fi extinsa.

5.5. INTERFET
E

83

1. final public class MyInteger implements Comparable


2. {
3. //constructor
4. public MyInteger( int value )
5. {
6.
this.value = value ;
7. }
8.
9. //cateva metode
10. public String toString()
11. {
12. return Integer.toString( value ) ;
13. }
14.
15. public int intValue()
16. {
17. return value ;
18. }
19.
20. public boolean equals( Object rhs )
21. {
22. return rhs instanceof MyInteger &&
23.
value == ( (MyInteger)rhs ).value ;
24. }
25.
26. //implementarea interfetei
27. public boolean lessThan( Comparable rhs )
28. {
29. return value < ( (MyInteger)rhs).value ;
30. }
31.
32. public int compareTo( Comparable rhs )
33. {
34. return value < ( (MyInteger)rhs).value ? -1 :
35.
value == ( (MyInteger)rhs).value ? 0 : 1 ;
36. }
37.
38. private int value ;
39.}
Figura 5.10 Clasa MyInteger (versiune preliminar
a), care implementeaz
a interfata Comparable

5.5.3

Interfete multiple

Asa cum am mentionat mai devreme, o clasa poate sa implementeze mai mult de o singur
a
interfata. Sintaxa pentru a realiza acest lucru este simpla. O clasa poate implementa mai

84

CAPITOLUL 5. MOS
TENIRE

multe interfete prin:


1. precizarea interfetelor pe care le implementeaza
2. implementarea tuturor metodelor din interfete.
Interfata este cea mai abstracta clasa posibila si reprezinta o solutie eleganta la problema
mostenirii multiple.

5.6

Implementarea de componente generice

Sa ne reamintim ca unul dintre scopurile principale ale programarii orientate pe obiecte este
suportul pentru reutilizarea codului. Unul dintre mecanismele importante folosite pentru
ndeplinirea acestui scop este programarea generic
a: Daca implementarea unei metode este
identica pentru mai multe clase (cu exceptia tipului de baza al obiectului), se poate folosi
o implementare generic
a pentru a descrie functionalitatea de baza. De exemplu, putem
scrie o metoda care sa sorteze un sir de elemente; algoritmul pentru aceasta metoda este
independent de tipul de obiecte care sunt sortate, deci putem folosi un algoritm generic.
Spre deosebire de multe dintre limbajele de programare mai noi (cum ar fi C++) care
utilizeaza sabloane pentru a implementa programarea generica, Java nu ofera suport pentru
implementarea directa a programarii generice, deoarece programarea generica poate fi
implementat
a folosind conceptele de baza ale mostenirii. In aceasta sectiune vom prezenta
cum pot fi implementate metode si clase generice n Java folosind principiile de baza ale
mostenirii.
Ideea de baza n Java este ca putem implementa o clasa generica folosind o superclasa
adecvata, cum ar fi Object. In Java, daca o clasa nu extinde o alta clasa, atunci ea extinde
implicit clasa Object (definita n pachetul java.lang). Ca o consecinta, fiecare clasa este o
subclasa a lui Object. Sa consideram clasa MemoryCell din Figura 5.11. Aceasta clasa
poate sa retina un obiect de tip Object. Deoarece Object este clasa de baza pentru orice
clasa din Java, rezulta ca clasa noastra poate sa stocheze orice fel de obiecte.
1. //clasa MemoryCell
2. // Object read() --> Intoarce valoarea stocata
3. // void write( Object x )
--> x este stocat
4.
5. public class MemoryCell
6. {
7. //metode publice
8. public Object read()
9. {
10. return storedValue() ;
11. }
12.
13. public void write( Object x )
14. {
15. storedValue = x ;
16. }

5.6. IMPLEMENTAREA DE COMPONENTE GENERICE

85

17.
18. private object storedValue ;
19.}
Figura 5.11 Clasa generic
a MemoryCell
Exista doua detalii care trebuie luate n considerare atunci cand folosim aceasta strategie.
Ambele sunt ilustrate n Figura 5.12. Functia main scrie valoarea 5 ntr-un obiect MemoryCell, dupa care citeste din obiectul MemporyCell. In primul rand, tipurile primitive
nu sunt obiecte. Astfel, m.write(5) ar fi fost incorect. Totusi, aceasta nu este o problem
a,
deoarece Java dispune de clase wrapper (de mpachetare) pentru cele opt tipuri primitive. Astfel, obiectul de tip MemoryCell va retine un obiect de tip Integer.
Al doilea detaliu este ca rezultatul lui m.read() este un Object. Deoarece n clasa Object
este definita o metoda toString, nu este necesar sa facem conversia de la Object la Integer.
Referinta returnata de m.read() (de tip Object) este polimorfica si ea refera de fapt un
obiect de tip Integer. In consecinta, se va apela automat metoda toString a clasei Integer.
Daca am fi vrut totusi sa extragem valoarea retinuta n obiectul de tip MemoryCell, ar fi
trebuit sa scriem o linie de genul
int i = ( (Integer)m.read() ).intValue() ;
n care se converteste mai ntai valoarea returnata de read la Integer, dupa care se foloseste
metoda intValue() pentru a obtine un int.
Deoarece clasele wrapper sunt clase finale, constructorul si accesorul intValue pot fi expandate inline de catre compilator, generand astfel un cod la fel de eficient ca utilizarea
directa a unui int.
1. public class TestMemoryCell
2. {
3.
public static void main( String[] args )
4.
{
5.
MemoryCell m = new MemoryCell() ;
6.
7.
m.write( new MyInteger( 5 ) ) ;
8.
System.out.println( "Continutul este: " + m.read() ) ;
9.
}
10. }
Figura 5.12 Folosirea clasei generice MemoryCell
Un al doilea exemplu este problema sortarii. Am scris deja o metoda insertionSort care
lucreaz
a cu un sir de clase Shape. Ar fi interesant sa rescriem aceasta metoda pentru un
sir generic. Figura 5.13 prezinta o metoda de sortare generica insertionSort care este
practic identica cu metoda de sortare din Figura 5.8 doar ca foloseste Comparable n loc
de Shape. Putem pune aceasta metoda ntr-o clasa Sort. Observati ca nu sortam Object,
ci Comparable. Metoda insertionSort sorteaza un sir de elemente Comparable, deoarece
foloseste lessThan. Aceasta nseamna ca doar clasele care implementeaza interfata Comparable pot fi sortate astfel. De remarcat faptul ca insertionSort nu poate sa sorteze un

86

CAPITOLUL 5. MOS
TENIRE

sir de obiecte Shape, deoarece clasa Shape din Figura 5.4 nu implementeaza interfata
Comparable. Unul dintre exercitii propune modificarea clasei Shape n acest sens.
Pentru a vedea cum poate fi folosita metoda generica de sortare vom scrie un program,
prezentat n Figura 5.14, care citeste un numar nelimitat de valori ntregi, le sorteaza si
afiseaza rezultatul. Metoda readIntArray a clasei Reader (prezentata n anexa) este folosita
pentru a citi un sir de ntregi. Transformam apoi sirul citit ntr-un sir de elemente care
implementeaza interfata Comparable. In linia 15 cream sirul, iar n liniile 16-19 obiectele
care sunt stocate n sir. Sortarea este realizata n linia 22. In final, afisam rezultatele n
liniile 26-29. De retinut ca metoda toString este implicit apelata pentru clasa MyInteger.
1. //sortare prin insertie
2. public static void insertionSort( Comparable[] a )
3. {
4. for( int p=1; p < a.length; ++p )
5. {
6.
Comparable tmp = a[p] ;
7.
int j = p ;
8.
for( ; j>0 && tmp.lessThan( a[j-1] ) ; --j)
9.
{
10.
a[j] = a[j-1] ;
11. }
12. a[j] = tmp ;
13. }
14.}
Figura 5.13 Algoritm de sortare generic
1. import io.*;//pachet facut de noi pentru a citi de la tastatura
2. public class SortIns
3. {
4. //program de test care citeste valori intregi de la terminal
5. //(cate unul pe linie), le sorteaza si apoi le afiseaza
6.
7. public static void main( String[] args )
8. {
9.
10. //citeste un sir de intregi
11. System.out.print("Introduceti elementele sirului: ") ;
12. int [] sir = Reader.readIntArray() ;
13.
14. //conversie la un sir de MyInteger
15. MyInteger[] sirNou = new MyInteger[ sir.length ] ;
16. for( int i=0; i<sir.length; ++i)
17. {
18.
sirNou[i] = new MyInteger( sir[i] ) ;
19. }
20.

- CLASA READER
5.7. ANEXA

21. //aplica metoda de sortare


22. Sort.insertionSort( sirNou ) ;
23.
24. //afiseaza rezultatul sortarii
25. System.out.println("Rezultatul sortarii: ") ;
26. for( int i=0; i<sirNou.length; ++i)
27. {
28.
System.out.println( sirNou[i] ) ;
29. }
30. }
31.}
Figura 5.14 Citire un sir de ntregi, sortarea si afisarea lor

5.7
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.

Anex
a - clasa Reader
package io ;
//clasa va trebui salvata intr-un director cu numele "io"
//directorul in care se afla "io" va trebui adaugat in CLASSPATH
import java.io.* ;
import java.util.StringTokenizer ;
public class Reader
{
public static String readString()
{
BufferedReader in = new BufferedReader(
new InputStreamReader( System.in ) ) ;
try
{
return in.readLine() ;
}
catch(IOException e)
{
//ignore
}
return null ;
}
public static int readInt()
{
return Integer.parseInt(readString()) ;
}
public static double readDouble()
{

87

88

CAPITOLUL 5. MOS
TENIRE

31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.}

5.8

return Double.parseDouble(readString()) ;
}
public static char readChar()
{
BufferedReader in = new BufferedReader(
new InputStreamReader( System.in ) ) ;
try
{
return (char)in.read() ;
}
catch(IOException e)
{
//ignore
}
return \0 ;
}
public static int[] readIntArray()
{
String s = readString() ;
StringTokenizer st = new StringTokenizer( s ) ;
//aloca memorie pentru sir
int[] a = new int[ st.countTokens() ] ;
for(int i=0; i<a.length; ++i)
{
a[i] = Integer.parseInt( st.nextToken() ) ;
}
return a ;
}

Probleme propuse

1. Scrieti doua metode generice min si max, fiecare acceptand doi parametri de tip
Comparable. Folositi aceste metode ntr-o clasa numita MyInteger.
2. Scrieti doua metode generice min si max, fiecare acceptand un sir de Comparable.
Folositi apoi aceste metode pentru tipul MyInteger.
3. Pentru exemplul cu clasa Shape modificati metodele readShape si main astfel ncat
ele sa arunce si sa prinda exceptii (n loc sa creeze un cerc de raza 0) atunci cand se
observa o eroare la citire.

5.8. PROBLEME PROPUSE

89

4. Modificati clasa Shape astfel ncat ea sa poata fi folosita de catre un algoritm de


sortare generic.
5. Rescrieti ierarhia de clase Shape pentru a retine aria ca un membru privat, care
este calculat de catre constructorul pentru Shape. Constructorii din clasele derivate
trebuie sa calculeze aria si sa trimita rezultatul catre metoda super. Faceti din area
o metoda finala care doar returneaza valoarea acestui atribut.
6. Adaugati conceptul de pozitie la ierarhia Shape prin includerea coordonatelor ca
date membru. Adaugati apoi o metoda distance.
7. Scrieti o clasa abstracta pentru Date din care sa derivati apoi o clasa Gregorian Date
(data n formatul nostru obisnuit).

Capitolul 6

Analiza eficientei algoritmilor


In prima parte a acestei lucrari am examinat cum putem folosi programarea orientata pe
obiecte pentru proiectarea si implementarea programelor ntr-un mod profesional. Totusi,
aceasta este doar jumatate din problem
a.
Calculatorul este folosit de obicei pentru a prelucra cantitati mari de informatie. Atunci
cand executam un program pe date de intrare de dimensiuni mari, trebuie sa fim siguri
ca algoritmul se va termina ntr-un timp rezonabil. Acest lucru este aproape ntotdeauna
independent de limbajul de programare folosit, ba chiar si de metodologia aplicata (cum
ar fi programarea orientata pe obiecte, sau programarea procedurala).
Un algoritm este un set bine precizat de instructiuni pe care calculatorul le va executa
pentru a rezolva o problema. Odata ce am gasit un algoritm pentru o anumita problema,
si am determinat ca algoritmul este corect, pasul urmator este de a determina cantitatea
de resurse, cum ar fi timpul si cantitatea de memorie, pe care algoritmul le cere. Acest
pas este numit analiza algoritmului. Un algoritm care are nevoie de cativa gigabaiti de
memorie pentru a rula nu este bun de nimic pe masinile de la ora actuala, chiar daca el
este corect.
In acest capitol vom vedea:
Cum putem estima timpul cerut de un algoritm
Tehnici pentru reducerea drastic
a a timpului de executie al unui algoritm
Un cadru matematic care descrie la un mod mai riguros timpul de executie
al algoritmilor

6.1

Ce este analiza algoritmilor?

Cantitatea de timp pe care orice algoritm o cere pentru executie depinde aproape ntotdeauna
de cantitatea de date de intrare pe care o proceseaza. Este de asteptat ca sortarea a 10.000
de elemente sa necesite mai mult timp decat sortarea a 10 elemente. Timpul de executie
al unui algoritm este astfel o functie de dimensiunea datelor de intrare. Valoarea exacta a
acestei functii depinde de mai multi factori, cum ar fi viteza calculatorului pe care ruleaza
programul, calitatea compilatorului si, n anumite situatii, calitatea programului. Pentru
un program dat, care ruleaza pe un anumit calculator, putem reprezenta grafic timpul
90

6.1.

91

CE ESTE ANALIZA ALGORITMILOR?

de executie al algoritmilor. In Figura 6.1 am realizat un astfel de grafic pentru patru


programe. Curbele reprezinta patru functii care sunt foarte des ntalnite n analiza algoritmilor: liniar
a, O(nlogn), p
atratic
a si cubic
a. Dimensiunea datelor de intrare variaza de
la 1 la 100 de elemente, iar timpii de executie de la 0 la 5 milisecunde. O privire rapid
a
asupra graficelor din Figura 6.1 si Figura 6.2, ne lamureste ca ordinea preferintelor
pentru timpii de executie este liniar, O(nlogn), patratic si cubic.
Sa luam ca exemplu descarcarea (download-area) unui fisier de pe Internet. Sa presupunem
ca la nceput apare o ntarziere de doua secunde (pentru a stabili conexiunea), dupa care
descarcarea se va face la 1.6 KB/sec. In aceasta situatie, daca fisierul de adus are N
kilobaiti, timpul de descarcare a fisierului este descris de formula T(N)=N/1.6+2. Aceasta
este o functie liniar
a. Se poate calcula usor ca descarcarea unui fisier de 80K va dura aproximativ 52 de secunde, n timp ce descarcarea unui fisier de doua ori mai mare (160K) va
dura 102 secunde, deci cam de doua ori mai mult. Aceasta proprietate, n care timpul este
practic direct proportional cu cantitatea de date de intrare, este specifica unui algoritm
liniar, si constituie adeseori o situatie ideala. Asa cum se vede din grafice, unele curbe
neliniare pot conduce la timpi de executie foarte mari. In acest capitol vom prezenta
urmatoarele probleme:
Cu cat este mai buna o curba n comparatie cu o alta curba?
Cum putem calcula pe care curba se situeaza un anumit algoritm?
Cum putem proiecta algoritmi care sa nu se situeze pe curbele nefavorabile?
.
Figura 6.1 Timpi de executie pentru date de intrare mici
.
Figura 6.2 Timpi de executie pentru date de intrare moderate
O functie cubic
a este o functie al carei termen dominant este N 3 , nmultit cu o constant
a.
3
2
De exemplu, 10N + N + 40N + 80 este o functie cubica. Similar, o functie patratic
a
are termenul dominant N 2 nmultit cu o constanta, iar o functie liniara are un termen
dominant care este N nmultit cu o constanta.
Oricare dintre cele trei functii prezentate mai sus poate fi mai mica decat cealalta ntr-un
punct dat. Acesta este motivul pentru care nu ne intereseaza valorile efective ale timpilor
de executie, ci rata lor de crestere. Acest lucru este justificabil prin trei argumente. In
primul rand, pentru functiile cubice, cum ar fi cea prezentata n Figura 6.2, atunci cand
N are valoarea 1000, valoarea functiei cubice este aproape complet determinata de valoarea termenului cubic. Functia 10N 3 + N 2 + 40N + 80 are valoarea 10.001.040.080 pentru
N = 1000, din care 10.000.000.000 se datoreaza termenului 10N 3 . Daca am fi folosit doar
termenul cubic pentru a estima valoarea functiei, ar fi rezultat o eroare de aproximativ
0.01%. Pentru un N suficient de mare, valoarea functiei este determinata aproape complet
de termenul ei dominant (semnificatie termenului suficent de mare, depinde de functia n
cauza).
Un al doilea motiv pentru care masuram doar rata de crestere a functiilor este ca valoarea

92

CAPITOLUL 6. ANALIZA EFICIENT


EI ALGORITMILOR

exacta a constantei multiplicative pentru termenul dominant difera de la un calculator la


altul. De exemplu, calitatea compilatorului poate sa influenteze destul de mult valoarea
constantei. In al treilea rand, valorile mici pentru N sunt de obicei nesemnificative. Din
Figura 6.1 se observa ca pentru N = 10, toti algoritmii se ncheie n mai putin de 3 ms.
Diferenta dintre cel mai bun si cel mai slab algoritm este mai mica decat un clipit de ochi.
Pentru a reprezenta rata de crestere a unui algoritm se foloseste asa-numita notatie asimptotica (engl. Big-Oh notation). De exemplu, rata de crestere pentru un algoritm patratic
este notata cu O(N 2 ). Notatia asimptotica ne permite sa stabilim o ordine partiala ntre
functii prin compararea termenului lor dominant.
Vom dezvolta n acest capitol aparatul matematic necesar pentru analiza eficientei algoritmilor, urmarind ca aceasta incursiune matematica sa nu fie excesiv de formala. Vom arata
apoi, pe baza de exemple, cum poate fi analizat un algoritm. O atentie speciala o vom
acorda tehnicilor de analiza a algoritmilor recursivi, prezentati n capitolul 6 al acestui
curs.

6.2

Notatia asimptotic
a

Notatia asimptotica are rolul de a estima timpul de calcul necesar unui algoritm pentru a
furniza rezultatul, functie de dimensiunea datelor de intrare.

6.2.1

O notatie pentru ordinul de m


arime al timpului de executie al
unui algoritm

Fie N multimea numerelor naturale, < multimea numerelor reale. Fie f : N [0, ) o
functie arbitrara. Definim multimea de functii:
O(f ) = {t : N [0, ) | c > 0, n0 N, astf el
c f (n)}

incit

n n0

avem

t(n)

Cu alte cuvinte, O(f ) (se citeste ordinul lui f) este multimea tuturor functiilor t marginite
superior de un multiplu real pozitiv al lui f, pentru valori suficient de mari ale argumentului
n. Vom conveni sa spunem ca t este n ordinul lui f (sau, echivalent, t este n O(f ) , sau
t O(f ) ) chiar si atunci cand t(n) este negativ sau nedefinit pentru anumite valori
n < n0 . In mod similar, vom vorbi despre ordinul lui f chiar si atunci cand valoarea f(n)
este negativa sau nedefinita pentru un numar finit de valori ale lui n; n acest caz, vom alege
n0 suficient de mare, astfel ncat pentru n n0 acest lucru sa nu mai apara. De exemplu,
vom vorbi despre ordinul lui n/log n , chiar daca pentru n=0 si n=1 functia nu este
definita. In loc de t O(f ), uneori este mai convenabil sa folosim notatia t(n) O(f (n))
, subntelegand aici ca t(n) si f(n) sunt functii.
Fie un algoritm dat si fie o functie t : N [0, ) astfel ncat o anumita implementare a
algoritmului sa necesite cel mult t(n) unitati de timp pentru a rezolva un caz de marime
n, n N . Principiul invariantei1 ne asigura atunci ca orice implementare a algoritmului
necesita un timp n ordinul lui t. Cu alte cuvinte, acest algoritm necesita un timp n
ordinul lui f pentru orice functie f : N [0, ) pentru care t O(f ). In particular avem
1
Acest principiu afirm
a c
a dou
a implement
ari diferite ale unui algoritm nu difer
a, ca eficient
a, dec
at
cel mult printr-o constant
a multiplicativ
a

93

6.2. NOTAT
IA ASIMPTOTICA

relatia: t O(t) . Vom cauta, n general, sa gasim cea mai simpla functie f , astfel nc
at
t O(f ).
Exemplul 6.1 Fie functia t(n) = 3n2 9n + 13 . Pentru n suficient de mare, vom avea
consecint
relatia t(n) 4n2 . In
a, lu
and c=4,
a t(n) O(n2 ). La fel de
putem spune c
2
bine puteam s
a spunem c
a t(n) O(13n 2n + 12.5), dar pe noi ne intereseaz
a s
a
4
g
asim o expresie c
at mai simpl
a. Este adev
arat
a si relatia t(n) O(n ), dar, asa cum
vom vedea mai t
arziu, suntem interesati de a m
argini ct mai str
ans ordinul de m
arime al
algoritmului, pentru a putea obiectiva c
at mai bine durata sa de executie.
Proprietatile de baza ale lui O(f ) sunt date ca exercitii (1 - 5) si ar fi recomandabil sa le
studiati nainte de a trece mai departe.
Notatia asimptotica defineste o relatie de ordine partiala ntre functii si deci ntre eficienta
relativa a diferitilor algoritmi care rezolva o anumita problema. Vom da n continuare o
interpretare algebrica a notatiei asimptotice. Pentru oricare doua functii f, g : N <
definim urmatoarea relatie binara: f g daca O(f ) O(g). Relatia este o relatie de
ordine partial
a (reflexiv
a, tranzitiv
a, antisimetric
a) n multimea functiilor definite pe N si
cu valori n [0, ) (exercitiul 4). Definim si o relatie de echivalent
a: f g daca O(f )=O(g).
Prin aceasta relatie obtinem clase de echivalenta, o clas
a de echivalent
a cuprinzand toate
functiile care difera ntre ele printr-o constanta multiplicativa. De exemplu, lg n ln n
si avem o clasa de echivalenta a functiilor logaritmice, pe care o notam generic cu O(log
n) . Notand cu O(1) clasa de echivalenta a algoritmilor cu timpul marginit superior de
o constanta (cum ar fi interschimbarea a doua numere, sau maximul a trei elemente),
ierarhia celor mai cunoscute clase de echivalenta este:
O(1) O(log n) O(n) O(nlog n) O(n2 ) O(n3 ) O(2n )
Aceasta ierarhie corespunde unei clasificari a algoritmilor dupa un criteriu al performantei.
Pentru o problema data, dorim mereu sa obtinem un algoritm corespunzator unei clase cat
mai de jos (cu timp de executie cat mai mic). Astfel, se considera a fi o mare realizare
daca n locul unui algoritm exponential gasim un algoritm polinomial. Exercitiul 5 ne d
a
o metoda de simplificare a calculelor n care apare notatia asimptotica. De exemplu:
n3 + 4n2 + 2n + 7 O(n3 + (4n2 + 2n + 7)) = O(max(n3 , 4n2 + 2n + 7)) = O(n3 )
Ultima egalitate este adevarata chiar daca max(n3 , 4n2 + 2n + 7) 6= n3 pentru 0 n 4
, deoarece notatia asimptotic
a se aplica doar pentru n suficient de mare. De asemenea,
3

n3 3n2 n8 O( n2 +( n2 3n2 n8)) = O(max( n2 , n2 3n2 n8)) = O( n2 ) = O(n3 )


chiar daca pentru 0 n 6 polinomul este negativ. Exercitiul 8 trateaza cazul unui
polinom oarecare.
Notatia O(f ) este folosita pentru a limita superior timpul necesar unui algoritm, masurand
eficienta (complexitatea computationala) a algoritmului respectiv. Uneori este interesant
sa estimam si o limita inferioara a acestui timp. In acest scop, definim multimea:
(f ) = {t : N [0, ) | c > 0, n0 N, astf el
c f (n)}

incit

n n0

avem

t(n)

Exista o anumita dualitate ntre notatiile O(f ) si (f ): pentru doua functii oarecare
f, g : N [0, ), avem:

94

CAPITOLUL 6. ANALIZA EFICIENT


EI ALGORITMILOR

f O(g) dac
a si numai daca g (f ) .
O estimare foarte precisa a timpului de executie se obtine atunci cand timpul de executie
al unui algoritm este limitat atat inferior cat si superior de cate un multiplu real pozitiv
al aceleasi functii. In acest scop, introducem notatia:
(f ) = O(f ) (f )
numita ordinul exact al lui f. Pentru a compara ordinele a doua functii, notatia nu este
nsa mai puternica decat notatia O , n sensul ca O(f )=O(g) este echivalent cu (f ) =
(g).
Exista situatii n care timpul de executie al unui algoritm depinde simultan de mai multi
parametri. Aceste situatii sunt tipice pentru anumiti algoritmi care opereaza cu grafuri si
la care timpul depinde atat de numarul de varfuri cat si de numarul de muchii. Notatia
asimptotica se generalizeaza n mod natural si pentru functii cu mai multe variabile. Astfel,
pentru o functie arbitrara f : N N [0, ) definim
O(f ) = {t : N N [0, ) | c > 0, n0 , m0 N, astf el incit
n0 avem t(m, n) c f (m, n)}.

m m0 , n

Similar se obtin si celelalte generalizari.

6.3

Tehnici de analiza algoritmilor

Nu exista o metoda standard pentru analiza eficientei unui algoritm. Este mai curand o
chestiune de rationament, intuitie si experienta. Vom arata pe baza de exemple cum se
poate efectua o astfel de analiza.

6.3.1

Sortarea prin selectie

Consideram algoritmul de sortare prin selectia minimului, reprodus mai jos:


pentru i = 1, n 1
//se calculeaz
a pozitia minimului lui a(i), a(i + 1), . . . , a(n)
P ozM in i //initializ
am minimul cu indicele primului element
pentru j = i + 1, n
dac
a a(i) < a(P ozM in) atunci
P ozM in = i
sf
arsit dac
a
//se aseaz
a minimul pe pozitia i
aux a(i)
a(i) a(P ozM in)
a(P ozM in) aux
sf
arsit pentru //dup
aj
sf
arsit pentru //dup
ai
Timpul pentru o singura executie a ciclului pentru dupa variabila j poate fi marginit
superior de o constanta a. In total, pentru un i fixat, tinand cont de faptul ca se realizeaza

95

6.3. TEHNICI DE ANALIZA ALGORITMILOR

n-i iteratii, acest ciclu necesita un timp de cel mult b + a(n i) unitati, unde b este o
constanta reprezentand timpul necesar pentru initializarea buclei. O singura executie a
buclei exterioare are loc n cel mult c + b + a(n i) unitati de timp, unde c este o alt
a
constanta. T
inand cont de faptul ca bucla dupa j se realizeaza de n-1 ori, timpul total de
executie al algoritmului este cel mult:
d+

Pn1
i=1

(c + b + a(n i))

unitati de timp, d fiind din nou o constanta. Simplificam aceasta expresie si obtinem
a
a 2
a algoritmul necesita un timp n O(n2 ).
2 n + (b + c 2 )n + (d c b), de unde deducem c
O analiza similara asupra limitei inferioare arata ca timpul este de fapt n (n2 ). Nu
este necesar sa consideram cazul cel mai nefavorabil sau cazul mediu deoarece timpul de
executie al sortarii prin selectie este independent de ordonarea prealabila a elementelor de
sortat.
In acest prim exemplu am analizat toate detaliile. De obicei nsa, detalii cum ar fi timpul
necesar initializarii ciclurilor nu se vor considera explicit, deoarece ele nu afecteaza ordinul
de complexitate al algoritmului. Pentru cele mai multe situatii, este suficient sa alegem
o anumita instructiune din algoritm ca barometru si sa numaram de cate ori se execut
a
aceast
a instructiune. In cazul nostru, putem alege ca barometru testul
a[i] < a[P ozM in]
din bucla interioara. Este usor de observat ca acest test se executa de

6.3.2

n(n1)
2

ori.

Sortarea prin insertie

Timpul pentru algoritmul de sortare prin insertie (sectiunea 5.3, Figura 5.8) este dependent de ordonarea prealabila a elementelor de sortat. Vom folosi comparatia
tmp.lessThan( a[j-1] )
din ciclul for ca barometru.
Sa presupunem ca p este fixat si fie n = a.length lungimea sirului. Cel mai nefavorabil
caz apare atunci cand tmp < a[j 1] pentru fiecare j ntre p si 1, algoritmul facand n
aceasta situatie p 1 comparatii. Acest lucru se ntampla (pentru fiecare valoare a lui p
de la 1 la n 1) atunci cand tabloul a este initial ordonat descrescator. Numarul total de
comparatii pentru cazul cel mai nefavorabil este:
Pn

i=2 i

1=

n(n1)
2

(n2 )

Vom estima acum timpul mediu necesar pentru un caz oarecare. Presupunem ca elementele
tabloului a sunt distincte si ca orice permutare a lor are aceeasi probabilitate de aparitie.
Atunci, daca 1 k p , probabilitatea ca a[p] sa fie cel de-al k-lea cel mai mare element
dintre elementele a[1], a[2], . . . , a[p] este p1 . Pentru un p fixat, conditia a[p] < a[p 1] este
falsa cu probabilitatea p1 , deci probabilitatea ca sa se execute comparatia tmp < a[j 1]
o singura data nainte de iesirea din bucla while este p1 . Comparatia tmp < a[j 1] se
executa de exact doua ori tot cu probabilitatea p1 , etc. Probabilitatea ca sa se execute
comparatia de exact p 1 ori este p2 , deoarece aceasta se ntampla atat cand tmp <
b[0] cat si cand b[0] tmp < b[1]! Numarul mediu de comparatii, pentru un p fixat,

96

CAPITOLUL 6. ANALIZA EFICIENT


EI ALGORITMILOR

este n consecinta, suma numarului de comparatii pentru fiecare situatie, nmultita cu


probabilitatea de aparitie a acelei situatii:
ci = 1 1i + 2 1i + . . . + (i 2) 1i + (i 1) 2i =

i+1
2

1
i
2

Pentru a sorta n elemente, avem nevoie de ni=2 ci comparatii, ceea ce este egal cu n +3n

4
Pn 1
2
Hn (n ). Prin Hn = i=1 i (log n) am notat al n-lea termen al seriei armonice.
Se observa ca algoritmul de sortare prin inserare efectueaza pentru cazul mediu de doua ori
mai putine comparatii decat pentru cazul cel mai nefavorabil. Totusi, n ambele situatii,
numarul comparatiilor este n (n2 ).
Cu toate ca algoritmul necesita un timp n (n2 ) atat pentru cazul mediu cat si pentru
cel mai nefavorabil caz, pentru cazul cel mai favorabil (cand initial tabloul este ordonat
crescator) timpul este n O(n). De fapt, pentru cazul cel mai favorabil, timpul este si n
(n) (deci n (n)).
P

6.3.3

Turnurile din Hanoi

Matematicianul francez Eduard Lucas a propus n 1883 o problema care a devenit apoi
celebra mai ales datorita faptului ca a prezentat-o sub forma unei legende. Se spune ca
Brahma (Zeul Creatiei la hindusi) a fixat pe Pamant trei tije de diamant si pe una din ele
a pus n ordine crescatoare 64 de discuri de aur de dimensiuni diferite, astfel ncat discul
cel mai mare era jos. Brahma a creat si o manastire, iar sarcina calugarilor era sa mute
toate discurile pe o alta tija. Singura operatiune permisa era mutarea cate unui singur disc
de pe o tija pe alta, astfel ncat niciodata sa nu se puna un disc mai mare peste un disc
mai mic. Legenda spune ca sfarsitul lumii se va petrece atunci cand calugarii vor savarsi
lucrarea. Aceasta se dovedeste a fi o previziune extrem de optimista asupra sfarsitului
lumii. Presupunand ca n fiecare secund
a se muta un disc si lucrand fara ntrerupere, cele
64 de discuri nu pot fi mutate nici n 500 de miliarde de ani de la nceputul actiunii!
Pentru a rezolva problema, vom numerota cele trei tije cu 1, 2 si respectiv 3. Se observa
ca pentru a muta cele n discuri de pe tija cu numarul i pe tija cu numarul j (i si j iau
valori ntre 1 si 3) este necesar sa transferam primele n 1 discuri de pe tija i pe tija
6 i j (adica pe tija ramasa libera), apoi sa transferam discul n de pe tija i pe tija j, iar
apoi retransferam cele n 1 discuri de pe tija 6 i j pe tija j. Cu alte cuvinte, reducem
problema mutarii a n discuri la problema mutarii a n 1 discuri. Urmatoarea metoda
Java descrie acest algoritm recursiv.
public static void hanoi(int n,int i,int j)
{
if( n > 0 )
{
hanoi(n-1, i, 6-i-j) ;
System.out.println(i + "-->" + j) ;
hanoi(n-1, 6-i-j, i) ;
}
}
Pentru rezolvarea problemei initiale, facem apelul

97

6.4. ANALIZA ALGORITMILOR RECURSIVI

hanoi(64,1,2);
Consideram instructiunea println ca barometru. Timpul necesar algoritmului este exprimat prin urmatoare recurenta:
(

t(n) =

1 daca n = 1
2t(n 1) + 1 daca n > 1

Vom demonstra n sectiunea 6.4 ca t(n) = 2n 1. Rezulta t (2n ).


Acest algoritm este optim n sensul ca este imposibil sa mutam discuri de pe o tija pe alta
cu mai putin de 2n 1 operatii. Pentru a muta 64 de discuri vor fi n consecinta necesare
un numar astronomic de 264 peratii. Implementarea n oricare limbaj de programare care
admite exprimarea recursiva se poate face aproape n mod direct.

6.4

Analiza algoritmilor recursivi

Am vazut n exemplul precedent cat de puternica si n acelasi timp cat de eleganta este
recursivitatea n elaborarea unui algoritm. Cel mai important castig al exprimarii recursive este faptul ca ea este naturala si compacta, fara sa ascunda esenta algoritmului
prin detaliile de implementare. Pe de alta parte, apelurile recursive trebuie folosite cu
discernamant, deoarece solicita si ele resursele calculatorului (timp si memorie). Analiza
unui algoritm recursiv implica aproape ntotdeauna rezolvarea unui sistem de recurente.
Vom vedea n continuare cum pot fi rezolvate astfel de recurente. Incepem cu tehnica cea
mai simpla.

6.4.1

Metoda iteratiei

Cu putina experienta si intuitie putem rezolva de multe ori astfel de recurente prin metoda
iteratiei: se executa primii pasi, se intuieste forma generala, iar apoi se demonstreaza prin
inductie matematica ca forma este corecta. Sa consideram de exemplu recurenta problemei
turnurilor din Hanoi. Se observa ca pentru a muta n discuri este necesar sa mutam n 1
discuri, apoi sa mutam un disc si n final din nou n 1 discuri. In consecinta, pentru un
anumit n > 1 obtinem succesiv:
t(n) = 2t(n 1) + 1 = 22 t(n 2) + 2 + 1 = . . . = 2n1 t(1) +

Pn2
i=0

2i

Rezulta t(n) = 2n 1. Prin inductie matematica se demonstreaza acum cu usurinta c


a
aceasta forma generala este corecta.

6.4.2

Inductia constructiv
a

Inductia matematica este folosita de obicei ca tehnica de demonstrare a unei asertiuni


deja enuntate. Vom vedea n aceasta sectiune ca inductia matematica poate fi utilizata cu
succes si n descoperirea partiala a enuntului asertiunii. Aplicand aceasta tehnica, putem
simultan sa demonstram o asertiune doar partial specificata si sa descoperim specificatiile
care lipsesc si datorita carora asertiunea este corecta. Vom vedea ca aceasta tehnica a
inductiei constructive este utila pentru rezolvarea anumitor recurente care apar n contextul analizei algoritmilor. Incepem cu un exemplu.
Fie functia F : N N definita prin recurenta:

98

CAPITOLUL 6. ANALIZA EFICIENT


EI ALGORITMILOR

f (n) =

0 daca n = 1
f (n 1) + n altfel

Sa presupunem pentru moment ca nu stim ca f (n) =


f (n) =

Pn

i=0 i

Pn

i=0 n

n(n+1)
.
2

Avem:

= n2

si deci f (n) O(n2 ). Aceasta ne sugereaza sa formulam ipoteza inductiei specificate partial
IISP(n) conform careia f este de forma f (n) = an2 + bn + c. Aceasta ipoteza este partiala
n sensul ca a, b si c nu sunt nca cunoscute. Tehnica inductiei constructive consta n a
demonstra prin inductie matematica aceasta ipoteza incompleta si a determina n acelasi
timp valorile constantelor necunoscute a, b si c.
Presupunem ca IISP(n-1) este adevarata pentru un anumit n 1. Atunci, f (n 1) =
a(n 1)2 + b(n 1) + c = an2 + (1 + b 2a)n + (a b + c). Daca dorim sa aratam
ca IISP(n) este adevarata, trebuie sa aratam ca f (n) = an2 + bn + c. Prin identificarea
coeficientilor puterilor lui n, obtinem ecuatiile 1 + b 2a = b si a b + c = c, cu solutia
a = b = 21 , c putand fi oarecare. Avem acum o ipoteza mai completa, pe care o numim
2
tot IISP(n), f (n) = n2 + n2 + c. Am ar
atat ca daca IISP(n-1) este adevarata pentru un
anumit n 1, atunci este adevarata si IISP(n). Ramane sa aratam ca este adevarata si
IISP(0). Trebuie sa aratam ca f (0) = a02 + b0 + c. S
tim ca f (0) = 0, deci IISP(0) este
2
adevarata pentru c = 0. In concluzie am demonstrat ca f (n) = n2 + n2 pentru orice n.

6.4.3

Recurente liniare omogene

Exista din fericire si tehnici care pot fi folosite aproape automat pentru a rezolva anumite
clase de recurente. Vom ncepe prin a considera ecuatii recurente liniare omogene, adica
ecuatii de forma:
a0 tn + a1 tn1 + . . . + ak tnk = 0 (*)
unde ti sunt valorile pe care le cautam, iar coeficientii ai sunt constante.
Conform intuitiei2 , vom cauta solutii de forma:
tn = xn
unde x este o constanta (deocamdata necunoscuta). Daca nlocuim aceasta solutie n (*),
obtinem
a0 xn + a1 xn1 + . . . + ak xnk = 0
Solutiile acestei ecuatii sunt fie solutia triviala x = 0, care nu ne intereseaza, fie solutiile
ecuatiei:
a0 xk + a1 xk1 + . . . + ak = 0
care se numeste ecuatia caracteristic
a a recurentei liniare si omogene(*). Presupunand
deocamdata ca cele k radacini r1 , r2 , . . . , rk ale acestei ecuatii caracteristice sunt distincte,
se verifica usor ca orice combinatie liniara
2

De fapt, adev
arul este c
a aici nu este vorba de intuitie, ci de experient
a

99

6.4. ANALIZA ALGORITMILOR RECURSIVI

tn =

Pk

n
i=1 ci ri

este o solutie a recurentei (*), unde constantele c1 , c2 , . . . , ck sunt determinate de conditiile


initiale. Se poate demonstra faptul ca (*) are solutii numai de aceasta forma.
Sa exemplificam prin recurenta care defineste sirul lui Fibonacci (sectiunea 7.1)
tn = tn1 + tn2 , n 2
iar t0 = 0, t1 = 1 . Putem sa rescriem aceasta recurenta sub forma
tn tn1 tn2 = 0
care are ecuatia caracteristica
x2 x 1 = 0

cu radacinile r1,2 =

1 5
2 .

Solutia generala are forma:


tn = c1 r1n + c2 r2n

Impun
and conditiile initiale, t0 = 0, t1 = 1, obtinem
c1 + c2 = 0, n = 0
c1 r1n + c2 r2n = 1, n = 1
de unde determinam
c1.2 = 15
Deci, tn =

1 (r n
5 1

+ r2n ). Observam ca r1 = , r2 = 1 si obtinem:


tn =

1 (n
5

()n )

care este cunoscuta relatie a lui Moivre, descoperita la nceputul secolului XVIII. Nu
prezinta nici o dificultate sa aratam acum ca timpul pentru calculul recursiv al sirului lui
Fibonacci este n (n ) .
Cum procedam nsa atunci cand radacinile ecuatiei caracteristice nu sunt distincte? Se
poate arata ca daca r este o radacina de multiplicitate m a ecuatiei caracteristice, atunci
tn = rn , tn = nrn , tn = n2 rn , . . . , tn = nm1 rn sunt solutii pentru (*). Solutia general
a
pentru o astfel de recurenta este atunci o combinatie liniara a acestor termeni si a termenilor proveniti de la celelalte radacini ale ecuatiei caracteristice. Din nou, sunt de
determinat exact k constante din conditiile initiale.
Vom da din nou un exemplu. Fie recurenta
tn = 5tn1 8tn2 + 4tn3
cu t0 = 0, t1 = 1, t2 = 2. Ecuatia caracteristica are radacinile 1 (de multiplicitate 1) si 2
(de multiplicitate 2). Solutia generala este:
tn = c1 1n + c2 2n + c3 n2n
Din conditiile initiale, obtinem c1 = 2, c2 = 2, c3 = 21 .

100

6.4.4

CAPITOLUL 6. ANALIZA EFICIENT


EI ALGORITMILOR

Recurente liniare neomogene

Consideram acum recurente de urmatoarea forma mai generala


a0 tn + a1 tn1 + . . . ak tnk = bn p(n) (**)
unde b este o constanta, iar p(n) este un polinom n n de grad d. Ideea generala este ca
prin manipulari convenabile sa reducem un astfel de caz la o forma omogena.
De exemplu, o astfel de recurenta poate fi:
tn 2tn1 = 3n
In acest caz b = 3 si p(n) = 1 un polinom de grad 0. O simpla manipulare ne permite sa
reducem acest exemplu la forma (*). Inmultind recurenta cu 3, obtinem:
3tn 6tn1 = 3n+1
Inlocuind pe n cu n + 1 n recurenta originala, avem:
tn+1 2tn = 3n+1
In final, scadem aceste doua ecuatii si obtinem:
tn+1 5tn + 6tn1 = 0
Am obtinut o recurenta omogena pe care o putem rezolva ca n sectiunea precedenta.
Ecuatia caracteristica este:
x2 5x + 6 = 0
adica (x 2)(x 3) = 0.
Intuitiv, observam ca factorul (x 2) corespunde partii stangi a recurentei originale, n
timp ce factorul (x 3) a aparut ca rezultat al manipularilor efectuate pentru a scapa de
partea dreapta.
Iata al doilea exemplu:
tn 2tn1 = (n + 5)3n
Manipularile necesare sunt putin mai complicate. Trebuie sa:
1. Inmultim recurenta cu 9
2. Inlocuim n recurenta pe n cu n + 2
3. Inlocuim n recurenta pe n cu n + 1 si sa nmultim apoi cu -6.
Adunand cele trei ecuatii obtinute anterior avem:
tn+2 8tn+1 + 21tn 18tn1 = 0
Am ajuns din nou la o ecuatie omogena. Ecuatia caracteristica corespunzatoare este
x3 8x2 + 21x 18 = 0

101

6.4. ANALIZA ALGORITMILOR RECURSIVI

adica (x 2)(x 3)2 . Inca o data, observam ca factorul (x 2) provine din partea stang
a
a recurentei originale, n timp ce factorul (x 3)2 este rezultatul manipularii.
Generalizand acest procedeu, se poate arata ca pentru a rezolva (**) este suficient sa luam
urmatoarea ecuatie caracteristica:
(a0 xk + a1 xk1 + . . . + ak )(x b)d+1 = 0
Odata ce s-a obtinut aceasta ecuatie, se procedeaza ca n cazul omogen.
Vom rezolva acum recurenta corespunzatoare problemei turnurilor din Hanoi
tn = 2tn1 + 1, n 1
iar t0 = 0. Rescriem recurenta astfel
tn 2tn1 = 1
care este de forma (**) cu b = 1 si p(n) = 1, un polinom cu grad 0. Ecuatia caracteristic
a
este atunci (x 1)(x 2), cu solutiile 1 si 2. Solutia generala a recurentei este:
tn = c1 1n + c2 2n
Avem nevoie de doua conditii initiale. Stim ca t0 = 0; pentru a gasi cea de-a doua conditie
calculam
t1 = 2t0 + 1
Din conditiile initiale, obtinem tn = 2n 1.
Observatie: daca ne intereseaza doar ordinul lui tn , nu este necesar sa calculam efectiv
constantele n solutia generala. Daca stim ca tn = c1 1n + c2 2n , rezulta ca tn O(2n ). Din
faptul ca numarul de mutari a unor discuri nu poate fi negativ sau constant (deoarece avem
n mod evident tn n), deducem ca c2 > 0. Avem atunci tn (2n ) si deci tn (2n ).
Putem obtine chiar ceva mai mult.
Substituind solutia generala napoi n recurenta originara, gasim
1 = tn 2tn1 = c1 + c2 2n 2(c1 + c2 2n1 ) = c1
Indiferent de conditia initiala, c1 este -1.

6.4.5

Schimbarea variabilei

Uneori putem rezolva recurente mai complicate printr-o schimbare de variabila. In exemplele care urmeaza, vom nota cu T(n) termenul general al recurentei si cu tk termenul noii
recurente obtinute printr-o schimbare de variabila. Presupunem pentru nceput ca n este
o putere a lui 2.
Un prim exemplu este recurenta
T (n) = 4T ( n2 ) + n, n > 1
n care nlocuim pe n cu 2k , notam tk = T (2k ) = T (n) si obtinem:
tk = 4tk1 + 2k

102

CAPITOLUL 6. ANALIZA EFICIENT


EI ALGORITMILOR

Ecuatia caracteristica a acestei recurente liniare este


(x 4)(x 2) = 0
si deci tk = c1 4k + c2 2k . Inlocuim pe k cu lg n
T (n) = c1 n2 c2 n
Rezulta ca T (n) O(n2 | n este o putere a lui 2).
Un al doilea exemplu l reprezinta ecuatia
T (n) = 4T ( n2 ) + n2 , n > 1
Procedand la fel, ajungem la recurenta
T (n) = 4T ( n2 ) + n2 , n > 1
cu ecuatia caracteristica
(x 4)2 = 0
si solutia generala tk = c1 4k + c2 k4k . Atunci,
T (n) = c1 n2 + c2 n2 lg n
si obtinem ca T (n) O(n2 log n | n este o putere a lui 2),
In fine, sa consideram si exemplul
T (n) = 3T ( n2 ) + cn, n > 1
c fiind o constanta. Obtinem succesiv
T (2k ) = 3T (2k1 ) + c2k
tk = 3tk1 + c2k
cu ecuatia caracteristica
(x 3)(x 2) = 0
tk = c1 3k + c2 2k
T (n) = c1 3lg n + c2 n
si, deoarece
alg b = blg a
obtinem
T (n) = c1 nlg 3 + c2 n

103

6.5. PROBLEME PROPUSE

deci, T (n) O(nlg 3 | n este o putere a lui 2).


In toate aceste exemple am folosit notatia asimptotica conditionata. Pentru a arata c
a
rezultatele obtinute sunt adevarate pentru orice n, este suficient sa adaugam conditia ca
T(n) sa fie eventual nedescrescatoare.
Putem enunta acum o proprietate care este utila ca reteta pentru analiza algoritmilor cu
recursivitati de forma celor din exemplele precedente. Proprietatea, a carei demonstrare
o lasam ca exercitiu, este foarte utila la analiza algoritmilor Divide et Impera prezentati
n capitolul 7.
Propozitie. Fie T : N <+ o functie eventual nedescrescatoare
T (n) = aT ( nb ) + cnk , n > n0
unde: n0 1, b 2 si k 0 sunt ntregi; a si c sunt numere reale pozitive;
putere a lui b. Atunci avem:
T (n)

6.5

n
n0

este o

(nk ) daca a < bk


daca a = bk
(nlogb a ) daca a > bk

(nk log n)

Probleme propuse

1. Care din urmatoarele afirmatii sunt adevarate?


(a) n2 O(n3 )
(b) n3 O(n2 )
(c) 2n+1 O(2n )
(d) (n + 1)! O(n!)
(e) pentru orice functie f : N R , f O(n) [f 2 O(n2 )]
(f) pentru orice functie f : N R , f O(n) [2f O(2n )]
2. Demonstrati ca relatia O este tranzitiv
a: daca f O(g) si g O(h), atunci
f O(h). Deduceti de aici ca daca g O(h), atunci O(g) O(h).
3. Gasiti doua functii f, g : N R , astfel ncat f
/ O(g) si g
/ O(f ).
Indicatie: f (n) = n, g(n) = n1+sin n .
4. Pentru oricare doua functii f, g : N R definim urmatoarea relatie binara: f g
daca O(f ) O(g). Demonstrati ca relatia este o relatie de ordine partiala n
multimea functiilor definite pe N si cu valori n R .
Indicatie: Trebuie ar
atat c
a relatia este partial
a, reflexiv
a, tranzitiv
a si antisimetric
a.
T
ineti cont de exercitiul 3.
5. Pentru oricare doua functii f, g : N R demonstrati ca O(f + g) = O(max(f, g))
unde suma si maximul se iau punctual.

104

CAPITOLUL 6. ANALIZA EFICIENT


EI ALGORITMILOR

6. Fie f (n) = am nm + . . . + a1 n + a0 un polinom de grad m, cu am > 0. Aratati ca


f O(nm ).
7. O(n2 ) = O(n3 + (n2 n3 )) = O(max(n3 , n2 n3 )) = O(n3 ). Unde este eroarea?
8.

Pn

= 1 + 2 + . . . + n O(1 + 2 + . . . + n) = O(max(1 + 2 + . . . + n)) = O(n).


Unde este eroarea?
i=1 i

9. Pentru oricare doua functii f, g : N R demonstrati ca (f ) + (g) = (f + g) =


(max(f, g)) = max((f ), (g)), unde suma si maximul se iau punctual.
10. Analizati eficienta urmatorilor algoritmi:
(a) pentru i=1,n
pentru j=1,5
{operatie elementara}
(b) pentru i=1,n
pentru j=1,i+1
{operatie elementara}
(c) pentru i=1,n
pentru j=1,6
pentru k=1,n
{operatie elementara}
(d) pentru i=1,n
pentru j=1,i
pentru k=1,n
{operatie elementara}
11. Construiti un algoritm cu timpul n (n log n).
12. Fie un algoritm:
pentru i=0,n
ji
cat timp j<>0
jj div 2
Gasiti ordinul exact al timpului de executie.
13. Rezolvati urmatoarea recurenta: tn 3tn1 4tn2 = 0, n 2 cu t0 = 0, t1 = 1.
14. Care este timpul de executie pentru un algoritm recursiv cu recurenta: tn = 2tn1 +
n.
Indicatie: Se ajunge la ecuatia caracteristic
a (x 2)(x 1)2 = 0, iar solutia general
a
n
n
n
este tn = c1 2 + c2 1 + c3 n1 . Rezult
a c
a tn O(2n ).
Substituind solutia general
a n recurent
a, obtinem c
a, indiferent de conditia initial
a,
c2 = 2 si c3 = 1. Atunci, toate solutiile interesante ale recurentei trebuie s
a aib
a
c1 > 0 si ele sunt toate n (2n ), deci n (2n ).

6.5. PROBLEME PROPUSE

105

15. S
a se calculeze secventa de suma maxima, formata din termeni consecutivi, a unui
sir de numere.
Solutie: Problema are solutii de diferite ordine de complexitate timp. Puteti s
a

ncercati voi sa le gasiti. In acest moment vom da solutia optima din punct de vedere
al complexitatii. Este vorba de o singura parcurgere a sirului. Tehnica folosita este
cea a programarii dinamice.
public void subSirvalMax()
{
int max=0;//valoarea initiala a sumei subsirului de valoare maxima
int ci=0;//indicele de inceput al subsirului de valoare maxima
int i=0;//valoarea curenta a indicelui de inceput al subsirului actual
int cj=n;//indicele de sfarsit al subsirului de valoare maxima
int j=0;//valoarea curenta a indicelui de sfarsit al subsirului actual
int m=0;//valoarea sumei subsirului actual
for(int k=0;k<n;k++)//parcurgerea sirului dat
{
m+=a[k];
if (m>=max)
//daca suma actuala este mai mare de cea gasita pana acum atunci
//subsirul actual va fi cel de suma marxima
{
max=m;
j=k;
ci=i;
cj=j;
}//setam noile valori
if (m<0)
//daca subsirul actual are suma mai mica de 0 atunci daca
//am adauga un numar oricat de mare tot nu va fi un subsir
//cu suma maxima pentru ca suma astfel obtinuta este
//mai mica ca numarul pe care l-am adaugat
//si subsirul actual nu ne mai intereseaza
{
m=0;
i=k+1;
j=k+1;
}
//deci vom seta indicele de inceput si de sfarsit al subsirului actual
//cu indicele elementului urmator iar suma actuala o vom seta 0
} //am terminat verificarea
if ((max==0)&&(ci==0)&&(cj==n))
//daca nu s-au schimbat valorile initiale atunci inseamna
//ca nu avem nici un subsir cu valoare maxima
System.out.println("Suma maxima 0,secventa vida");

106

CAPITOLUL 6. ANALIZA EFICIENT


EI ALGORITMILOR

else
{//altfel afisam datele obtinute
System.out.println("Suma maxima este"+max);
System.out.println("Secventa este"+(ci+1)+" "+(cj+1));
}
}

Capitolul 7

Structuri de date
Multi algoritmi necesita o reprezentare adecvata a datelor pentru a fi cu adevarat eficienti.
Reprezentarea datelor, mpreuna cu operatiile care sunt permise asupra datelor formeaza o
structur
a de date. Fiecare structura de date permite inserarea de elemente. Structurile de
date difera n privinta modului n care permit accesul la membrii din grup. Unele permit
accesarea si stergerea arbitrara. Altele impun anumite restrictii, cum ar fi permiterea
accesului doar la ultimul sau la primul element inserat.
Acest capitol prezinta sapte dintre cele mai uzuale structuri de date: stive, cozi, liste
nl
antuite, arbori, arbori binari de c
autare, tabele de repartizare (hash-tables) si cozi de
prioritate. Vom defini fiecare structura de date si vom furniza o estimare intuitiva pentru
complexitatea n timp a operatiilor de inserare, stergere si accesare.
In acest capitol vom vedea:
Descrierea structurilor de date uzuale, operatiile permise pe ele si timpii
lor de executie
Pentru fiecare structur
a de date, vom defini o interfat
a Java contin
and
protocolul care trebuie implementat
Unele aplicatii ale structurilor de date
Vom urmari sa evidentiem faptul ca specificarea operatiilor suportate de o structura de
date, care i descrie functionalitatea, este independenta de modul de implementare.

7.1

De ce avem nevoie de structuri de date?

Structurile de date ne permit atingerea unui scop important n programarea orientat


a
pe obiecte: reutilizarea componentelor. Asa cum vom vedea mai tarziu n acest capitol,
structurile de date descrise sunt folosite n multe situatii. Odata ce o structura de date a
fost implementata, ea poate fi folosita din nou si din nou n aplicatii de natura diversa1 .
Aceasta abordare - separarea interfetei de implementare - este o parte fundamentala a
orientarii pe obiecte. Cel care foloseste structura de date nu trebuie sa vada implementarea
ei, ci doar operatiile admisibile. Aceasta tine de partea de ncapsulare si ascundere a
1

De altfel, bibliotecile Java cuprind majoritatea structurilor de date uzuale

107

108

CAPITOLUL 7. STRUCTURI DE DATE

informatiei din programarea orientata pe obiecte. O alta parte importanta a programarii


orientate pe obiecte este abstractizarea. Trebuie sa proiectam cu grija structura de date,
deoarece vom scrie programe care folosesc aceste structuri de date fara sa aiba acces la
implementarea lor. Aceasta va face n schimb ca interfata sa fie mai curata, mai flexibila,
si, de obicei, mai usor de implementat.
Toate structurile de date sunt usor de implementat daca nu ne punem problema eficientei.
Acest lucru permite sa adaugam componente ieftine n program doar pentru depanare.
Exercitiile de la sfarsitul capitolului va cer scrierea de implementari ineficiente, care sunt
adecvate pentru a procesa cantitati mici de date. Putem apoi nlocui aceste implementari
ieftine cu implementari care au o performanta (n timp si/sau n spatiu) mai buna
si care sunt adecvate pentru procesarea unei cantitati mai mari de informatie. Deoarece
interfetele sunt fixate, aceste modificari nu necesita practic nici o modificare n programele
care folosesc aceste structuri de date.
Vom descrie structurile de date prin intermediul interfetelor. De exemplu, stiva este
precizata prin intermediul interfetei Stack. Clasa care implementeaza aceasta interfata
va implementa toate metodele specificate n Stack, la care se mai pot adauga anumite
functionalitati.
1.
2.
3.
4.
5.

public interface MemCell


{
Object read() ;
void write( Object x ) ;
}

Figura 7.1 Interfat


a pentru clasa MemoryCell
Ca un exemplu, n Figura 7.1 este descrisa o interfata pentru clasa MemoryCell din capitolul anterior. Interfata descrie functiile disponibile; clasa concreta trebuie sa defineasca
aceste functii. Implementarea este prezentata n Figura 7.2 si este identica cu cea din
capitolul anterior cu exceptia clauzei implements.
public class MemoryCell implements MemCell
{
public Object read() { return storedValue ; }
public void write(Object x) { storedValue = x ; }
private Object storedValue ;
}
Figura 7.2 Implementarea clasei MemoryCell
Este important sa observati faptul ca structurile de date definite n acest capitol retin
referinte catre elementele inserate, si nu copii ale elementelor. Din acest motiv este bine
ca n structura de date sa fie plasate obiecte nemodificabile (cum ar fi String, Integer etc.)
pentru ca un utilizator extern sa nu poata sa schimbe starea unui obiect care este nglobat
ntr-o structura de date.

7.2. STIVE

7.2

109

Stive

O stiva este o structura de date n care orice tip de acces este permis doar pe ultimul
element inserat. Comportamentul unei stive este foarte asemanator cu cel al unui mald
ar
de farfurii. Ultima farfurie adaugata va fi plasata n varf, fiind n consecinta usor de
accesat, n timp ce farfuriile puse cu mai mult timp n urma vor fi mai greu de accesat,
putand periclita stabilitatea ntregii gramezi. Astfel, stiva este adecvata n situatiile n care
avem nevoie sa accesam doar elementul din varf. Toate celelalte elemente sunt inaccesibile.
Cele trei operatii naturale, inserare, stergere si cautare, sunt denumite n cazul unei stive
push, pop si top. Cele trei operatii sunt ilustrate n Figura 7.4. O interfata Java pentru
o stiva abstracta este prezentata n Figura 7.5. Interfata declara si o metoda topAndPop
care combina doua operatii; tot aici apare si un element nou: clauza throws prin care se
declara ca o metoda poate sa arunce catre metoda apelanta o anumita exceptie. In cazul
nostru, metodele pop, top si topAndPop pot arunca o UnderflowException n cazul n care
se ncearca accesarea unui element cand stiva este goala. Aceasta exceptie va trebui s
a
fie prinsa pana la urma de o metoda apelanta. Clasa UnderflowException este definita n
Figura 7.3, si ea este practic identica cu clasa Exception. Important pentru noi este c
a
difera tipul, ceea ce ne permite sa prindem doar aceasta exceptie cu o secventa de tipul
catch(UnderflowException e):
1.
2.
3.
4.
5.
6.
7.
8.

package Exceptions ;
public class UnderflowException extends Exception
{
public UnderflowException(String thrower)
{
super( thrower ) ;
}
}

Figura 7.3 Codul pentru clasa UnderflowException. Aceast


a clas
a este practic identic
a
cu Exception, difer
a doar tipul.
Figura 7.6 prezinta un exemplu de utilizare al clasei Stack. Observati ca stiva poate
fi folosita pentru a inversa ordinea elementelor; de remarcat un mic artificiu folosit aici:
iesirea din ciclul for se realizeaza n momentul n care stiva se goleste si metoda topAndPop arunca UnderflowException. Am recurs la acest artificiu pentru a ntelege mecanismul
exceptiilor. Folosirea acestui artificiu n mod curent nu este recomandata, deoarece reduce
lizibilitatea codului.
Fiecare operatie pe stiva trebuie sa ia o cantitate constanta de timp, indiferent de dimensiunea stivei, la fel cum accesarea farfuriei din varful gramezii este rapida, indiferent de
numarul de farfurii din teanc. Accesul la un element oarecare din stiva nu este eficient,
de aceea el nici nu este permis de catre interfata noastra.

110

CAPITOLUL 7. STRUCTURI DE DATE

push

pop,top

J
J
^
J

Stiva
Figura 7.4 Modelul unei stive: Inserarea n stiv
a se face prin push, accesul prin top,
stergerea prin pop.
Stiva este deosebit de utila deoarece sunt multe aplicatii pentru care trebuie sa accesam
doar ultimul element inserat. Un exemplu ilustrativ este salvarea parametrilor si variabilelor locale n cazul apelului unei alte subrutine.
1. package DataStructures ;
2. import Exceptions.* ;//pachet care contine UnderflowException
3.
4. //Interfata pentru stiva
5. //
6. //********************OPERATII PUBLICE*********************
7. // void push( x )
--> insereaza x
8. // void pop()
--> Sterge ultimul element inserat
9. //Object top()
--> Intoarce ultimul element inserat
10.//Object topAndPop()-->Intoarce si sterge ultimul element
11.// boolean isEmpty( )
--> Intoarce true daca stiva e vida
12.// void makeEmpty( )
--> Elimina toate elementele din stiva
13.
14.public interface Stack
15.{
16. void push( Object x ) ;
17. void pop( )
throws UnderflowException ;
18. Object top( )
throws UnderflowException ;
19. Object topAndPop( )
throws UnderflowException ;
20. boolean isEmpty( ) ;
21. void makeEmpty( ) ;
22.}
Figura 7.5 Interfat
a pentru stiv
a
1.
2.
3.
4.
5.
6.
7.
8.
9.

import DataStructures.* ;
import Exceptions.* ;
public final class TestStack
{
public static void main( String args[])
{
Stack s = new StackAr() ;

111

7.3. COZI

10. for(int i=0; i<5; ++i)


11. {
12.
s.push( new Integer(i) ) ;
13. }
14.
15. System.out.print("Continutul stivei este: " ) ;
16. try
17. {
18.
for( ; ; )
19.
{
20.
System.out.print( " " + s.topAndPop( ) );
21.
}
22. }
23. catch(UnderflowException e)
24. {
25. }
26. System.out.println() ;
27. }
28.}
Figura 7.6 Exemplu de utilizare a stivei; programul va afisa: Continutul stivei este 4 3
2 1 0.

7.3

Cozi

O alta structura simpla de date este coada. In multe situatii este important sa avem acces
si/sau sa stergem ultimul element inserat. Dar, ntr-un numar la fel de mare de situatii,
acest lucru nu numai ca nu mai este important, este chiar nedorit. De exemplu, ntr-o
retea de calculatoare care au acces la o singura imprimanta este normal ca daca n coada
de asteptare se afla mai multe documente spre a fi tiparite, prioritatea sa i fie acordat
a
documentului cel mai vechi. Acest lucru nu numai ca este corect, dar este si necesar pentru
a garanta ca procesul nu asteapta la infinit. Astfel, pe sistemele mari este normal sa se
foloseasca cozi de tiparire. Operatiile fundamentale suportate de cozi sunt:
enqueue - inserarea unui element la capatul cozii
dequeue - stergerea primului element din coada
getFront - accesul la primul element din coada
enqueue

dequeue,getFront

Queue

Figura 7.7 Modelul unei cozi: Intrarea se face prin enqueue, accesul prin getFront,
stergerea prin dequeue.

112

CAPITOLUL 7. STRUCTURI DE DATE

Figura 7.7 ilustreaza operatiile pe o coada. Traditional, metodele dequeue si getFront


sunt combinate ntr-una singura. La fel am facut si noi aici. Metoda dequeue returneaza
primul element, dupa care l scoate din coada.
1. package Datastructures ;
2.
3. import Exceptions.* ;
4.
5. //Interfata Queue
6. //
7. //**********OPERATII PUBLICE******************
8. //void enqueue( x )--> insereaza x
9. //Object getFront( )-->intoarce cel mai vechi element
10.//Object dequeue( )-->intoarce&sterge cel mai vechi elem
11.//boolean isEmpty()-->intoarce true daca e goala
12.//void makeEmpty()-->sterge elementele din coada
13.//********************************************
14.
15.public interface Queue
16.{
17. void enqueue(Object x) ;
18. Object getFront() throws UnderflowException;
19. Object dequeue() throws UnderflowException ;
20. boolean isEmpty() ;
21. void makeEmpty() ;
22.}
Figura 7.8 Interfat
a pentru coad
a
1. import Datastructures.* ;
2. import Exceptions.* ;
3.
4. //Program simplu pentru testarea cozilor
5.
6. public final class TestQueue
7. {
8. public static void main(String[] args)
9. {
10. Queue q = new QueueAr() ;
11.
12. for(int i=0; i<5; ++i)
13. {
14.
q.enqueue( new Integer( i ) ) ;
15. }
16.
17. System.out.print("Continut: " ) ;
18. try

113

UITE
7.4. LISTE INLANT

19. {
20.
for( ; ; )
21.
{
22.
System.out.print( " " + q.dequeue() ) ;
23.
}
24. }
25. catch( UnderflowException e )
26. {
27. //aici se ajunge cand stiva se goleste
28. }
29.
30. System.out.println( ) ;
31. }
32.}
Figura 7.9 Exemplu de utilizare a cozii; programul va afisa:
Continut: 0 1 2 3 4
Figura 7.8 ilustreaza interfata pentru o coada, iar Figura 7.9 prezinta modul de utilizare
al cozii. Deoarece operatiile pe o coada sunt restrictionate ntr-un mod asemanator cu
operatiile pe o stiva, este de asteptat ca si aceste operatii sa fie implementate ntr-un timp
constant. Intr-adevar, toate operatiile pe o coada pot fi implementate n timp constant,
O(1).

7.4

Liste nl
antuite

Intr-o lista nlantuita elementele sunt retinute discontinuu, spre deosebire de siruri n care
elementele sunt retinute n locatii continue. Acest lucru este realizat prin stocarea fiecarui
obiect ntr-un nod care contine obiectul si o referinta catre urmatorul element n lista, ca
n Figura 7.10. In acest model se retin referinte atat catre primul cat si catre ultimul
element din lista. Concret vorbind, un nod al unei liste arata la modul urmator:
class ListNode
{
Object data ; //continutul listei
ListNode next ;
}

a1

first

last

J
J
J

a2

a3

a4

Figura 7.10 O list


a simplu nl
antuit
a
In orice moment, putem adauga n lista un nou element x prin urmatoarele operatii:

114

last.next = new ListNode() ;


last = last.next ;
last.data = x ;
last.next = null ;

CAPITOLUL 7. STRUCTURI DE DATE

//creaza un nou nod


//actualizeaza last
//plaseaza pe x in nod
//ultimul nu are successor

In cazul unei liste nlantuite un element oarecare nu mai poate fi gasit cu un singur acces.
Aceasta este oarecum similar cu diferenta ntre accesarea unei melodii pe CD (un singur
acces) si accesarea unei melodii pe caseta (acces secvential). Desi din acest motiv listele
pot sa para mai putin atractive decat sirurile, exista totusi cateva avantaje importante.
In primul rand, inserarea unui element n mijlocul listei nu implica deplasarea tuturor
elementelor de dupa punctul de inserare. Deplasarea datelor este foarte costisitoare (din
punct de vedere al timpului), iar listele nlantuite permit inserarea cu un numar constant
de instructiuni de atribuire.
Merita observat ca daca permitem accesul doar la first, atunci obtinem o stiva, iar daca
permitem inserari doar la last si accesari doar la first, obtinem o coada.
In general, atunci cand folosim o lista, avem nevoie de mai multe operatii, cum ar fi gasirea
sau stergerea unui element oarecare din lista. Trebuie sa permitem si inserarea unui nou
element n orice punct. Aceasta este deja mult mai mult decat ne permite o stiva sau o
coada.
Pentru a accesa un element n lista, trebuie sa obtinem o referinta catre nodul care i corespunde. Evident ca oferirea unei referinte catre un element ncalca principiul ascunderii
informatiei. Trebuie sa ne asiguram ca orice acces la lista prin intermediul unei referinte
nu pericliteaza structura listei. Pentru a realiza acest lucru, lista este definita n doua
parti: o clasa lista si o clasa iterator. Figura 7.11 furnizeaza interfata de baza pentru o
lista nlantuita, oferind si metodele care descriu doar starea listei.
Figura 7.12 defineste o clasa iterator care este folosita pentru toate operatiile de accesare a listei. Pentru a vedea cum functioneaza aceasta clasa, sa examinam secventa de cod
clasica pentru afisarea tuturor elementelor din cadrul unei structuri liniare. Daca lista ar
fi stocata ntr-un sir, secventa de cod ar arata astfel:
//parcurge sirul a, afisand fiecare element
for(int index = 0; index < a.length; ++index)
System.out.println( a[index] ) ;
In Java elementar, codul pentru a itera o lista este:
//parcurge lista theList de tip List, afisand fiecare element
for(ListNode p = theList.first; p != null; p = p.next)
System.out.println( p.data ) ;
1.
2.
3.
4.
5.
6.
7.
8.

package DataStructures;
//Interfata pentru lista
//
//Accesul se realizeaza prin clasa ListItr
//
//********OPERATII PUBLICE*************
//boolean isEmpty()-->Intoarce true daca lista e goala

UITE
7.4. LISTE INLANT

115

9. //void makeEmpty()-->Sterge toate elementele


10.
11.public interface List
12.{
13. boolean isEmpty() ;
14. void makeEmpty() ;
15.}
Figura 7.11 Interfata pentru o list
a abstract
a
1. package DataStructures;
2. import Exceptions.* ;
3.
4. //Interfata ListItr
5. //
6. //***********OPERATII PUBLICE
7. //void insert( x ) --> Insereaza pe x dupa pozitia curenta
8. //void remove( x )-->Sterge pe x
9. //boolean find( x )-->Seteaza pozitia curenta pe elem. x
10.//void zeroth()-->Seteaza pozitia inainte de primul elem.
11.//void first()-->Seteaza pozitia curenta pe primul element
12.//void advance()-->Avanseaza la urmatorul element
13.//boolean isInList()-->True daca ne aflam in interiorul listei
14.//Object retrieve()-->Intoarce elementul de la poz. curenta
15.
16.public interface ListItr
17.{
18. void insert( Object x ) throws ItemNotFoundException ;
19. boolean find( Object x ) ;
20. void remove( Object x ) throws ItemNotFoundException ;
21. boolean isInList() ;
22. Object retrieve() ;
23. void zeroth() ;
24. void first( ) ;
25. void advance( ) ;
26.}
Figura 7.12 Interfat
a pentru un iterator de list
a abstract
Mecanismul de iterare pe care l-ar folosi limbajul Java ar fi similar cu ceva de genul
(deoarece ListItr este o interfata, ListItr care urmeaza dupa new va fi nlocuit cu o clas
a
care implementeaza ListItr):
//parcurge List, folosind abstractizarea si un iterator
ListItr itr = new ListItr( theList ) ;
for(itr.first(); itr.isInList() ; itr.advance() )
System.out.println( itr.retrieve() ) ;

116

CAPITOLUL 7. STRUCTURI DE DATE

Initializarea dinaintea ciclului for creeaza un iterator al listei. Testul de terminare a ciclului
foloseste metoda isInList definita pentru clasa ListItr. Metoda advance trece la urmatorul
nod din cadrul listei. Putem accesa elementul curent prin apelul metodei retrieve definita
n ListItr. Principiul general este ca accesul fiind realizat prin intermediul clasei ListItr,
securitatea datelor este garantata. Putem avea mai multi iteratori care sa traverseze
simultan o singura lista.
Pentru ca sa functioneze corect, clasa ListItr trebuie sa mentina doua obiecte. In primul
rand, are nevoie de o referinta catre nodul curent. In al doilea rand are nevoie de o
referinta catre obiectul de tip List pe care l indica; aceasta referinta este initializata o
singura data n cadrul constructorului.
1. import DataStructures.* ;
2. import Exceptions.* ;
3.
4. //program simplu de testare a listelor
5.
6. public final class TestList
7. {
8. public static void main(String args[])
9. {
10. List theList = new LinkedList() ;
11. ListItr itr = new LinkedListItr( theList ) ;
12. //se insereaza noi elemente pe prima pozitie
13. for(int i=0; i<5; ++i)
14. {
15.
try
16.
{
17.
itr.insert( new Integer(i) ) ;
18.
}
19.
catch( ItemNotFoundException e )
20.
{
21.
}
22.
itr.zeroth() ;
23. }
24.
25. System.out.println("Continutul listei: ") ;
26. for( itr.first() ; itr.isInList() ; itr.advance() )
27. {
28.
System.out.println( " " + itr.retrieve() ) ;
29. }
30. }
31.}
Figura 7.13 Exemplu de utilizare al listei.
Programul va afisa: 4 3 2 1 0
Desi discutia s-a axat pe liste simplu nlantuite, interfetele din Figura 7.11 si Figura
7.12 pot fi folosite pentru oricare tip de lista, indiferent de implementarea pe care o are

117

7.5. ARBORI BINARI DE CAUTARE

la baza. Interfata nu precizeaza faptul ca este nevoie de liste simplu nlantuite.

7.5

Arbori binari de c
autare

In capitolul anterior am vazut ca cea mai eficienta cautare ntr-un sir este cautarea binar
a,
care se poate aplica atunci cand elementele sirului sunt ordonate, timpul de executie fiind
logaritmic.
Sa presupunem ca avem nevoie de o structura de date n care, pe langa cautare, dorim
sa putem adauga sau sterge eficient elemente. O astfel de structura este arborele binar de
c
autare. Figura 7.14 ilustreaza operatiile de baza permise asupra unui arbore binar de
cautare. O posibila interfata este prezentata n Figura 7.15.
insert

find,remove

J
J
^
J

Arbore binar de cautare


Figura 7.14 Modelul pentru arborele binar de c
autare. C
autarea binar
a este extins
a pentru a permite inser
ari si stergeri.
Setul de operatii permise este acum extins pentru a permite gasirea unui element arbitrar,
alaturi de inserare si stergere. Metoda find ntoarce o referinta catre un obiect care este
egal (n sensul metodei compareTo din interfata Comparable) cu elementul cautat. Daca nu
exista nici un element egal cu cel cautat find arunca o exceptie. Aceasta este o decizie de
proiectare. O alta posibilitate ar fi fost sa ntoarcem null n cazul n care elementul cautat
nu este gasit. Diferenta ntre cele doua abordari consta n faptul ca metoda noastra l
obliga pe programator sa trateze explicit situatia n care cautare nu are succes. In cealalt
a
situatie, daca am fi ntors null, si nu s-ar fi facut verificarile necesare, programul ar fi
generat un NullPointerException la prima tentativa de a folosi referinta. Din punct de
vedere al eficientei, versiunea cu exceptii ar putea fi ceva mai lenta, dar este putin probabil
ca rezultatul sa fie observabil, cu exceptia situatiei n care codul ar fi foarte des executat
n cadrul unor situatii n care viteza este critica.
In mod similar, inserarea unui element care deja este n arbore este semnalata printr-o
DuplicateItemException. Exista si alte posibile alternative. Una dintre consta n a permite
noii valori sa suprascrie valoarea stocata.
1.
2.
3.
4.
5.
6.
7.
8.

package DataStructures ;
import Exceptions.* ;
//*********OPERATII PUBLICE*********
// void insert( x )-->insereaza x
// void remove( x )-->sterge x
// void removeMin()-->sterge cel mai mic element

118

CAPITOLUL 7. STRUCTURI DE DATE

9. // Comparable find()-->intoarce elementul egal cu x


10.// Comparable findMin()-->intoarce cel mai mic element
11.// Comparable findMax()-->intoarce cel mai mare element
12.// boolean isEmpty()-->intoarece true daca arborele e gol
13.// void makeEmpty()-->sterge toate elementele
14.// void printTree()-->afiseaza nodurile ordonate
15.
16.//************ERORI****************
17.//majoritatea rutinelor arunca ItemNotFoundException
18.//in diverse cazuri degenerate
19.//insert poate sa arunce DuplicateItemException daca elem
20.//este deja in arbore
21.
22.public interface SearchTree
23.{
24. void insert(Comparable x) throws DuplicateItemException ;
25. void remove(Comparable x) throws ItemNotFoundException ;
26. void removeMin() throws ItemNotFoundException ;
27. Comparable find(Comparable x) throws ItemNotFoundException;
28. Comparable findMin( ) throws ItemNotFoundException ;
29. Comparable findMax( ) throws ItemNotFoundException ;
30. boolean isEmpty() ;
31. void makeEmpty() ;
32. void printTree() ;
33.}
Figura 7.15 Interfata pentru un arbore binar de c
autare
Vom da ca exemplu un arbore binar care retine stringuri. Deoarece n arbore putem retine
doar obiecte de tip Comparable, nu putem folosi direct clasa String (deoarece aceasta nu
implementeaza clasa Comparable). Din acest motiv, vom scrie o clasa MyString, prezentata
n Figura 7.16 (aceasta clasa implementeaza si interfata Hashable, care va fi folosita n
sectiunea urmatoare). Figura 7.17 prezinta modul n care arborele binar de cautare
poate fi folosit pentru obiecte de tip MyString.
Interfata SearchTree mai are doua metode suplimentare: una pentru a gasi cel mai mic
element si una pentru a gasi cel mai mare element. Se poate arata ca transpirand un
pic mai mult se poate gasi foarte eficient si cel mai mic al k-lea element, pentru oricare k
trimis ca parametru.
Iata o sinteza a timpilor de executie pentru operatiile pe un arbore binar de cautare. Este
normal sa speram ca timpii de executie pentru find, insert si remove sa fie logaritmici,
deoarece aceasta este valoarea pe care am obtinut-o pentru cautarea binara. Din nefericire,
pentru cea mai simpla implementare a arborelui binar de cautare, acest lucru nu este
adevarat. Timpul mediu de executie este logaritmic, dar n cazul cel mai nefavorabil,
timpul de executie este O(n), caz care apare destul de frecvent. Totusi, prin aplicarea
anumitor trucuri de algoritmica se pot obtine anumite structuri mai complexe (arbori
rosu-negru) care au ntr-adevar un cost logaritmic pentru fiecare operatie.


7.5. ARBORI BINARI DE CAUTARE

1. import Datastructures.* ;
2.
3.public final class MyString implements Comparable, Hashable
4. {
5. private String value ;
6.
7. public MyString(String x)
8. {
9.
value = x ;
10. }
11.
12. public String toString()
13. {
14. return value ;
15. }
16.
17. public int compareTo(Comparable rhs)
18. {
19. return value.compareTo( ((MyString)rhs).value ) ;
20. }
21.
22. public boolean lessThan(Comparable rhs)
23. {
24. return compareTo(rhs) < 0 ;
25. }
26.
27. public boolean equals( Object rhs )
28. {
29. return value.equals( ( (MyString)rhs.value ) );
30. }
31.
32. public int hash(int tableSize)
33. {
34. return QuadraticProbingTable.hash(value, tableSize) ;
35. }
36.}
Figura 7.16 Clasa MyString
1. //program simplu pentru testarea arborilor de cautare
2.
3. public final class TestSearchTree
4. {
5. public static void main(String[] args)
6. {
7.
SearchTree t = new BinarySearchTree() ;

119

120

CAPITOLUL 7. STRUCTURI DE DATE

8.
MyString result = null ;
9.
10. try
11. {
12.
t.insert( new MyString( "Georgica" ) ) ;
13. }
14. catch(DupicateItemException e)
15. {
16. }
17.
18. try
19. {
20.
result = (MyString) t.find(new MyString("Georgica"));
21.
System.out.print("Gasit " + result + " " ) ;
22. }
23. catch(ItemNotFoundException e)
24. {
25.
System.out.print("Georgica nu a fost gasit") ;
26. }
27.
28. try
29. {
30.
result = (MyString) t.find( new MyString( "Ionel" ) ) ;
31.
System.out.print("Gasit " + result + " " ) ;
32. }
33. catch(ItemNotFoundException e)
34. {
35.
System.out.print("Ionel nu a fost gasit") ;
36. }
37.
38. System.out.println() ;
39. }
40.}

Figura 7.17 Model de program care utilizeaz


a arbori de c
autare.
Programul va afisa: Georgica, Ionel nu a fost g
asit.

Ce putem spune despre operatiile findMin si findMax? In mod cert, aceste operatii necesita
un timp constant n cazul cautarii binare, deoarece implica doar accesarea unui element
indiciat. In cazul unui arbore binar de cautare aceste operatii iau acelasi timp ca o cautare
obisnuita, adica O(log n) n cazul mediu si O(n) n cazul cel mai nefavorabil. Dupa cum
i sugereaza si numele, arborele binar de cautare este implementat ca un arbore binar,
necesitand astfel cate doua referinte pentru fiecare element.

7.6. TABELE DE REPARTIZARE

7.6

121

Tabele de repartizare

Exista foarte multe aplicatii care necesita o cautare dinamica bazata doar pe un nume.
O aplicatie clasica este tabela de simboluri a unui compilator. Pe masura ce compileaz
a
programul, compilatorul trebuie sa retina numele (mpreuna cu tipul, scopul, locatia de
memorie) tuturor identificatorilor care au fost declarati. Atunci cand vede un identificator
n afara unei instructiuni de declarare, compilatorul verifica sa vada daca acesta a fost
declarat. Daca a fost, compilatorul verifica informatia adecvata din tabela de simboluri.
Avand n vedere faptul ca arborele binar de cautare permite acces logaritmic la obiecte
cu denumiri oarecare, de ce am avea nevoie de o alta structura de date? Raspunsul este
ca arborele binar de cautare poate sa dea un timp de executie liniar pentru accesul unui
element, iar pentru a ne asigura de cost logaritmic este nevoie de algoritmi mult mai
sofisticati.
Tabela de repartizare este o structura de date care evita timpul de executie liniar, ba
mai mult, suporta aceste operatii n timp (mediu) constant. Astfel, timpul de acces la un
element din tabela nu depinde de numarul de elemente care sunt n tabela. In acelasi timp,
tabela de repartizare nu foloseste apeluri la rutinele de alocare a memoriei (ca arborele
binar). Aceasta face ca tabela de repartizare sa fie rapida n practica. Un alt avantaj fat
a
de arborele binar de cautare este ca elementele stocate n tabela de repartizare nu trebuie
sa implementeze interfata Comparable.
Operatiile permise sunt date n Figura 7.18 iar o interfata este prezentata n Figura
7.19. In acest caz, inserarea unui element care este duplicat ne genereaza o exceptie, ci
elementul va fi nlocuit cu noua valoare. Aceasta este o alternativa la metoda pe care
am aplicat-o n cazul arborilor binari de cautare. Tabela de repartizare functioneaz
a
doar pentru elemente care implementeaza interfata Hashable2 . Interfata Hashable cere o
functie de repartizare, care converteste obiectul de tip Hashable ntr-un ntreg. Metoda
are urmatorul antet:
//intoarce un intreg intre 0 si tableSize-1
int hash(int tableSize) ;
Elementele dintr-o tabela de repartizare trebuie sa redefineasca si metoda equals. Figura
7.16 arata cum clasa MyString implementeaza interfata Hashable prin implementarea
metodelor hash si equals. Un exemplu de utilizare a tabelelor de repartizare este dat n
Figura 7.20.
Una dintre utilizarile obisnuite ale tabelelor de repartizare sunt dictionarele. Un dictionar
retine obiecte care constau ntr-o cheie, care este cautata n dictionar, si definitia cheii,
care este returnata. Putem utiliza tabele de repartizare pentru a implementa dictionarul
astfel:
obiectul stocat este o clasa care retine atat cheia cat si definitia ei
egalitatea, comparatia si functia de repartizare se bazeaza doar pe cheia din obiectul
stocat
2
In limba englez
a, tabelele de repartizare se numesc Hashtable, de aceea un element care poate fi
repartizat se numeste Hashable.

122

CAPITOLUL 7. STRUCTURI DE DATE

cautarea se face prin construirea unui obiect e cu cheia dorita, urmata de apelarea
metodei find din tabela de repartizare
definitia este obtinuta prin folosirea unei referinte f careia i este atribuita valoarea
ntoarsa de find.

insert

find,remove

J
J
^
J

Tabela de repartizare
Figura 7.18 Modelul pentru tabela de repartizare: Oricare element etichetat poate fi
ad
augat sau sters n timp practic constant.
1. package DataStructures ;
2.
3. import Exceptions.* ;
4.
5. //Interfata Hashtable
6. //
7. //void insert( x )-->adauga x
8. //void remove(x)-->remove x
9. //Hashable find(x)-->intoarce elementul care se potriveste cu x
10.//void makeEmpty()-->sterge toate elementele
11.
12.//**************OPERATII PUBLIC******
13.
14.public interface HashTable
15.{
16. void insert( Hashable x ) ;
17. void remove( Hashable x) throws ItemNotFoundException ;
18. Hashable find( Hashable x) throws ItemNotFoundException ;
19. void makeEmpty() ;
20.}
Figura 7.19 Interfata pentru tabele de repartizare.
1.
2.
3.
4.
5.
6.
7.

import DataStructures.* ;
import Exceptions.* ;
//program simplu pentru testarea arborilor de cautare
public final class TestHashTable
{

7.7. COZI DE PRIORITATE

123

8. public static void main(String[] args)


9. {
10. HashTable h = new QuadraticProbingTable() ;
11. MyString result = null ;
12. try{
13.
h.insert( new MyString( "Georgica" ) ) ;
14. }
15. catch(DupicateItemException e)
16. {
17. }
18.
19. try
20. {
21.
result = (MyString) h.find(new MyString("Georgica"));
22.
System.out.println("Gasit " + result + " " ) ;
23. }
24. catch(ItemNotFoundException e)
25. {
26.
System.out.println("Georgica nu a fost gasit") ;
27. }
28. }
29.}
Figura 7.20 Exemplu de program care foloseste tabele de repartizare.
Programul va afisa: Gasit Georgica.

7.7

Cozi de prioritate

Desi documentele trimise unei imprimante sunt asezate, de obicei, ntr-o coada, aceasta nu
este ntotdeauna cea mai buna varianta. De exemplu, un document poate sa fie deosebit de
important, deci el ar trebui executat imediat ce imprimanta este disponibila. De asemeni,
daca imprimanta a terminat de tiparit un document, iar n coada se afla cateva documente
avand 1-2 pagini si un document avand 100 de pagini, ar fi normal ca documentul lung s
a
fie tiparit ultimul, chiar daca nu este ultimul document trimis.
Analog, n cazul unui sistem multiutilizator, sistemul de operare trebuie sa decida la un
moment dat care dintre mai multe procese trebuie sa fie executat. In general, un proces
poate sa se execute doar o perioada de timp fixata. Si aici este normal ca procesele care
au nevoie de un timp foarte mic sa aiba prioritate.
Daca vom atribui fiecarei sarcini cate un numar, atunci numarul mai mic (pagini tiparite,
resurse folosite) va indica o prioritate mai mare. Astfel, vom dori sa accesam cel mai mic
element dintr-o colectie de elemente si sa l stergem din cadrul colectiei. Acestea sunt
operatiile findMin si deleteMin. Structura de date care ofera o implementare foarte eficienta a acestor operatii se numeste coad
a de priorit
ati. Figura 7.21 ilustreaza operatiile
fundamentale pentru coada de prioritati.

124

CAPITOLUL 7. STRUCTURI DE DATE

insert

findMin,deleteMin

J
J
^
J

Coad
a de prioritate
Figura 7.21 Modelul pentru coada de priorit
ati: Doar elementul minim este accesibil.

7.8

Aplicatie

Vom da n continuare o varianta a clasei SearchTree, prezentata anterior, numita Node care
contine metodele elementare, prezentate n comentariu, dar n plus va permite adaugarea
unui nod indiferent ca acesta mai exist
a n arbore precum si faptul ca rezultatul funtiei
de cautare este boolean deci nu necesita tratarea ca exceptie. Clasa BinaryTree este o
aplicatie a clasei Node iar clasa TestBinaryTree contine metoda main.
class Node
{
private Node left,right;//legaturile arborelui binar
private double data;//informatia utila din nod
Node()//constructorul fara parametrii
{
left=null;
right=null;
data=0;
}
Node(double data)
//constructorul cu parametru pentru initializare informatiei utile
{
left=null;
right=null;
this.data=data;
}
public static Node linkNode(Node root,double data)//inserarea unui nou nod
{
if (root==null)
{
root=new Node();//alocam informatie pentru noul nod
root.setData(data);//adaugam informatia utila
root.setLeftNode(null);//setam descendentii ca fiind nuli
root.setRightNode(null);
}
else//altfel cautam locul lui
{

7.8. APLICAT
IE

125

if (data>root.getData())//daca avem o valoare mai mare atunci


//facem inserarea in subarborele drept
root.setRightNode(linkNode(root.getRightNode(),data));
else//altfel in subarborele drept
root.setLeftNode(linkNode(root.getLeftNode(),data));
}
return root;
}
public void displayInordine(Node root)
//parcurgerea in inordine a unui arbore binar (SRD)
{
if (root!=null)
{
displayInordine(root.getLeftNode());//se parcuge subarborele stang
System.out.print(" "+root.getData()+" ");//se viziteaza nodul
displayInordine(root.getRightNode());//se parcurge subarborele drept
}
}
public void displayPreordine(Node root)
//parcurgerea in preordine a unui arbore binar (RSD)
{
if (root!=null)
{
System.out.print(" "+root.getData()+" ");//se viziteaza nodul
displayPreordine(root.getLeftNode());//se parcurge subarborele stang
displayPreordine(root.getRightNode());//se parcurge subarborele drept
}
}
public void displayPostordine(Node root)
//parcurgerea in postordine a unui arbore binar
{
if (root!=null)
{
displayPostordine(root.getLeftNode());
displayPostordine(root.getRightNode());
System.out.print(" "+root.getData()+" ");
}
}
public static boolean search(Node root,double data)
//cautarea intr-un arbore de cautare
{
if (root!=null)

126

CAPITOLUL 7. STRUCTURI DE DATE

{
if (root.getData()==data)//daca informatia utila din nodul curent este cea
//cautata atunci atunci returnam adevarat altfel continuam cautarea
return true;
else
{
if(root.getdata()<data)//daca informatia utila decat cea cautata
search(root.getLeftNode(),data);//se merge in stanga
if(root.getdata()>data)//altfel
search(root.getRightNode(),data);//se merge in dreapta
}
}
return false;
}
public double getData()//returneaza informatia utila din nodul curent
{
return this.data;
}

public void setData(double data)//modifica informatia utila din nodul curent


{
this.data = data;
}
public Node getLeftNode()//trece la subarborele stang al nodului curent
{
return this.left;
}
public void setLeftNode(Node left)//adauga subarborele stang nodului curent
{
this.left = left;
}
public Node getRightNode()//trece la subarborele drept al nodului curent
{
return this.right;
}
public void setRightNode(Node right)//adauga subarborele drept nodului curent
{
this.right = right;
}
}

7.8. APLICAT
IE

127

Figura 7.22 Clasa Node.


class BinaryTree
{
Node rootNode=null;
public void addNode(double data)//adaugarea unui nod in arbore
{
rootNode=Node.linkNode(rootNode,data);
}
public void displayInordine()//se afiseaza arborele parcurs in inordine
{
System.out.print("Inordine:");
rootNode.displayInordine(rootNode);
System.out.println("");
}
public void displayPreordine()//se afiseaza arborele parcurs in preordine
{
System.out.print("Preordine:");
rootNode.displayPreordine(rootNode);
System.out.println("");
}
public void displayPostordine()//se afiseaza arborele parcurs in postordine
{
System.out.print("Postordine:");
rootNode.displayPostordine(rootNode);
System.out.println("");
}
public boolean search(double data)//cautarea unui nod in arbore
{
return Node.search(rootNode,data);
}
}
Figura 7.23 Clasa BinaryTree.
class TestBinaryTree// se testeaza clasa BinaryTree
{
public static void main(String args[])
{
BinaryTree btree=new BinaryTree();
btree.addNode(6);
btree.addNode(3);

128

CAPITOLUL 7. STRUCTURI DE DATE

btree.addNode(7); //se adauga arborelui aceste noduri


btree.addNode(2);
btree.addNode(4);
btree.addNode(8);
btree.displayInordine();//parcurge arborele binar in inordine
btree.displayPreordine();//parcurge arborele binar in preordine
btree.displayPostordine();//parcurge arborele binar in postordine
System.out.println(btree.search(7));
}
}
Figura 7.24 Clasa TestBinaryTree.

Capitolul 8

Metoda Backtracking
8.1

Prezentare general
a

In informatica apar frecvent situatii n care rezolvarea unei probleme conduce la determinarea unor vectori de forma:
x = (x1 , x2 , . . . , xn )
unde:
fiecare componenta xi apartine unei multimi finite Vi
componentele vectorului x respecta anumite relatii, numite conditii interne, astfel
ncat x este o solutie a problemei daca si numai daca aceste conditii sunt satisfacute
de componentele x1 , x2 , . . . , xn ale vectorului.
Produsul cartezian V1 V2 . . . Vn se numeste spatiul solutiilor posibile. Elementele
acestui produs cartezian care respecta conditiile interne se numesc solutii ale problemei.
Exemplul 8.1 Fie dou
a multimi de litere V1 = {A, B, C} si V2 = {M, N }. Se cere s
a
se determine acele perechi (x1 , x2 ) cu proprietatea c
a dac
a x1 este A sau B, atunci x2 nu
poate fi N.
Rezolvarea problemei de mai sus conduce la perechile:
(A, M ), (B, M ), (C, M ), (C, N )
deoarece din cele sase solutii posibile doar acestea ndeplinesc conditiile puse n enuntul
problemei.

Exemplul 8.2 Se d
a multimea cu elementele {A, B, C, D}. Se cere s
a se genereze toate
permut
arile elementelor acestei multimi.
Se cer deci multimile x = {x1 , x2 , x3 , x4 } care respect
a conditiile:
xi 6= xj pentru i 6= j
xi apartine multimii V = V1 = V2 = V3 = V4 = {A, B, C.D} .
129

130

CAPITOLUL 8. METODA BACKTRACKING

Exist
a multi vectori care respect
a aceste conditii: {A, B, C, D}, {B, A, C, D}, {B, C, D, A},
{B, D, A, C} etc. Mai exact, num
arul de permut
ari ale elementelor unei multimi cu 4 elemente este 4! = 24.
O modalitate de rezolvare a problemei ar fi s
a se genereze toate cele 44 = 256 elemente
ale produsului cartezian V1 V2 V3 V4 (reprezent
and solutiile posibile) si s
a se aleag
a
dintre ele cele 24 care respect
a conditiile interne. S
a observ
am ns
a c
a dac
a n loc de 4
elemente multimea noastr
a ar avea 7 elemente, vor exista 77 = 823.543 variante posibile,
dintre care doar 7! = 5040 vor respecta conditiile interne.
Conform celor ar
atate mai sus, este indicat ca, pentru a rezolva o problem
a, s
a elabor
am
algoritmi al c
aror timp de lucru s
a nu fie at
at de mare, sau, dac
a este posibil, s
a nu fie
exponential. Metoda backtracking este o metod
a foarte important
a de elaborare a algoritmilor pentru problemele de genul celor descrise mai sus. Desi algoritmii de tip backtracking
au si ei, n general, complexitate exponential
a, ei sunt totusi net superiori unui algoritm
de genul celui descris mai sus, care genereaz
a toate solutiile posibile.

8.2

Prezentarea metodei

Metoda backtracking urmareste sa evite generarea tuturor solutiilor posibile, scurtandu-se


astfel drastic timpul de calcul.
Componentele vectorului x primesc valori n ordinea crescatoare a indicilor (noi vom nota
aceste valori cu v1 , v2 , . . . , vn cu scopul de a face diferenta ntre o componenta care nu
are o valoare atribuita, xk , si o componenta care are atribuita o valoare, vk ). Aceasta
nseamna ca lui xk nu i se atribuie o valoare decat dupa ce x1 , x2 , . . . , xk1 au primit valori
care nu contrazic conditiile interne. Mai mult decat atat, valoarea vk atribuita lui xk va
trebui astfel aleasa ncat v1 , v2 , . . . , vk sa respecte si ele anumite conditii, numite conditii
de continuare, care sunt deduse de catre programator pe baza conditiilor interne. Astfel,
daca n exemplul 8.2, prima componenta, x1 , a primit valoarea v1 = A, este clar ca lui
x2 nu i se va mai putea atribui aceasta valoare (elementele unei permutari trebuie sa fie
diferite).
Nendeplinirea conditiilor de continuare exprima faptul ca oricum am alege valorile pentru
componentele xk+1 , . . . , xn , nu vom obtine nici o solutie (deci conditiile de continuare sunt
strict necesare pentru obtinerea unei solutii). Prin urmare, se va trece la atribuirea unei
valori lui xk , doar daca conditiile de continuare pentru componentele x1 , x2 , . . . , xk (care
au valorile v1 , v2 , . . . , vk ) sunt ndeplinite. In cazul nendeplinirii conditiilor de continuare,
se alege o noua valoare pentru xk sau, n cazul n care multimea valorilor posibile, Vk , a
fost epuizata, se ncearca sa se faca o noua alegere pentru componenta precedenta, xk1 ,
a vectorului, micsorand pe k cu o unitate. Aceasta revenire la componenta precedenta da
numele metodei, exprimand faptul ca daca nu putem avansa, urmarim (engl. track = a
urmari) napoi (engl. back = napoi) secventa curenta din solutie.
Trebuie observat faptul ca respectarea conditiilor de continuare de catre v1 , v2 , . . . , vk nu
reprezinta o garantie a faptului ca vom obtine o solutie continuand cautarea cu aceste valori. Deci conditiile de continuare sunt conditii necesare pentru ca v1 , v2 , . . . , vk sa conduca
la o solutie, dar nu sunt conditii suficiente.
Alegerea conditiilor de continuare este foarte importanta, o alegere buna ducand la o reducere substantiala a numarului de calcule. In cazul ideal, aceste conditii ar trebui sa

131

8.2. PREZENTAREA METODEI

fie nu numai necesare, ci chiar suficiente pentru obtinerea unei solutii. In practica se
urmareste gasirea unor conditii de continuare care sa fie cat mai dure, adica sa elimine
din start cat mai multe solutii neviabile. De obicei, conditiile de continuare reprezint
a
restrictia conditiilor interne la primele k componente ale vectorului. Evident, conditiile de
continuare n cazul k=n sunt chiar conditiile interne.
De exemplu, o conditie de continuare n cazul celei de-a doua probleme luata ca exemplu
ar fi:
vk 6= vi , i = 1, k 1
Prin metoda backtracking, orice vector este construit progresiv, ncepand cu prima component
a si mergand catre ultima, cu eventuale reveniri asupra valorilor atribuite anterior.
Reamintim ca x1 , x2 , . . . , xn primesc valori n multimile V1 , V2 , . . . , Vn . Prin atribuiri sau
ncercari de atribuiri esuate din cauza nerespectarii conditiilor de continuare, anumite
valori sunt consumate. Pentru o componenta oarecare xk vom nota prin Ck multimea
valorilor consumate la momentul curent. Evident, Ck Vk .
O descriere completa a starii n care se afla algoritmul la un moment dat se poate face
prin precizarea urmatoarelor elemente:
1. numarul de componente ale vectorului x, carora li s-au atribuit valori, avand valoarea
k-1.
2. valorile curente v1 , v2 , . . . , vk1 ale primelor k-1 componente ale vectorului x: x1 , x2 ,
. . ., xk1 .
3. multimile de valori consumate C1 , C2 , . . . , Ck pentru fiecare din componentele x1 , x2 ,
. . ., xk .
Aceast
a descriere poate fi sintetizata ntr-un tabel numit configuratie, avand urmatoarea
forma:
v1 , . . . , vk1 xk , xk+1 , . . . , xn
Ck , , . . . ,
C1 , . . . , Ck1

Semnificatia unei astfel de configuratii este urmatoarea:


1. n ncercarea de a construi un vector solutie, componentelor x1 , x2 , . . . , xk1 li s-au
atribuit valorile v1 , v2 , . . . , vk1 .
2. aceste valori satisfac conditiile de continuare
3. urmeaza sa se atribuie o valoare componentei xk ; deoarece valorile consumate pan
a
n prezent sunt cele din multimea Ck , componenta xk va primi o valoare vk din
Vk Ck .
4. valorile consumate pentru componentele x1 , x2 , . . . , xk sunt cele din multimile C1 , C2 ,
. . ., Ck , cu precizarea ca valorile curente v1 , v2 , . . . , vk1 sunt consumate, deci apar
n multimile C1 , C2 , . . ., Ck1 .
5. pentru componentele xk+1 , . . . , xn nu s-a ncercat nici o atribuire, deci nu s-a consumat nici o valoare si, prin urmare, Ck+1 , . . . , Cn sunt vide.

132

CAPITOLUL 8. METODA BACKTRACKING

6. pana n acest moment au fost construite eventualele solutii de forma:


(c1 , . . .) cu c1 C1 {v1 };
(v1 , c2 , . . .) cu c2 C2 {v2 };
.....
(v1 , v2 , . . . , vk2 , ck1 , . . .) cu ck1 Ck1 {vk1 };
(v1 , v2 , . . . , vk1 , ck , . . .) cu ck Ck ;
Aceasta ultima afirmatie este mai dificil de nteles si recomandam reluarea ei dupa lecturarea exemplului de mai jos.
In construirea permutarilor multimii cu elementele {A, B, C, D} din Exemplul 8.2, pentru
k = 4, configuratia
3
1
2
x4
{1, 2, 3} {1} {1, 2} {1, 2}

are, conform celor aratate mai nainte, urmatoarea semnificatie:


1. componentele x1 , x2 , x3 au primit valorile 3,1,2.
2. tripletul 3,1,2 satisface conditiile de continuare
3. urmeaza sa se atribuie o valoare componentei x4 . Componenta x4 ia valori din
multimea V4 C4 , adica una din valorile {3, 4}.
4. C1 = {1, 2, 3}, C2 = {1}, C3 = {1, 2}, C4 = {1, 2}
5. k + 1 = 5 > n, deci acest subpunct nu are obiect n aceasta situatie
6. pana n acest moment au fost deja construite solutiile de forma (n ordinea n care
au fost descrise la punctul 6 de mai sus):
(1, . . .) adica (1,2,3,4), (1,2,4,3), (1,3,2,4), (1,3,4,2), (1,4,2,3), (1,4,3,2) (2, . . .)
adica (2,1,3,4), (2,1,4,3), (2,3,1,4), (2,3,4,1), (2,4,1,3), (2,4,3,1)
solutii de aceasta forma nu exista, deoarece C2 {v2 } =
solutii de forma (3, 1, 1, . . .); nu exista solutii de aceasta forma ;
solutii de forma (3,1,2,1), (3,1,2,2); nu exista solutii de aceasta forma ;
Metoda backtracking ncepe a fi aplicat
a n situatia n care nu s-a facut nici o atribuire
asupra componentelor lui x, deci nu s-a consumat nici o valoare, si se ncearca atribuirea
unei valori primei componente. Acest lucru este specificat prin configuratia initial
a, a carei
forma este:
x1 , . . . , xn
, . . . ,

n care toate multimile Ck sunt vide.


Un alt caz special este cel al configuratiilor solutie, avand forma:

133

8.2. PREZENTAREA METODEI

v1 , . . . , vn
C1 , . . . , Cn

cu semnificatia ca vectorul (v1 , . . . , vn ) este solutie a problemei. Astfel, pentru Exemplul


8.2, configuratia:
!

A
B
C
D
{A} {A, B} {A, B, C} {A, B, C, D}

are semnificatia ca vectorul (A,B,C,D) constituie o solutie a problemei.


Metoda backtracking consta n a porni de la configuratia initiala si a-i aplica acesteia
una dintre cele patru tipuri de transformari prezentate n continuare, pana la epuizarea
tuturor variantelor. Fiecare transformare se aplica n anumite conditii bine precizate. La
un moment dat doar o singura transformare poate fi aplicata. Presupunem ca ne aflam
n configuratia descrisa anterior, n care s-au atribuit valori primelor k 1 componente.
Transformarile care pot fi aplicate unei configuratii sunt:

8.2.1

Atribuie si avanseaz
a

Acest tip de modificare are loc atunci cand mai exista valori neconsumate pentru xk (deci
Ck Vk ), iar valoarea aleasa vk are proprietatea ca (v1 , . . . , vk ) respecta conditiile de
continuare. In acest caz valoarea vk se atribuie lui xk si se adauga multimii Ck , dupa care
se avanseaza la componenta urmatoare, xk+1 . Aceasta modificare a configuratiei poate fi
reprezentata n felul urmator:
. . . , vk1 xk , xk+1 , . . .
. . . , Ck1 Ck , , . . .

vk

. . . , vk1 ,
vk xk+1 , . . .
, . . .
. . . , Ck1 , Ck {Vk }

De exemplu, la generarea permutarilor, avem urmatoarea schimbare de configuratie pornind


de la starea initiala:
x1 x2 x3 x4

8.2.2

A
-

x2 x3 x4
A
{A}

Incercare esuat
a

Acest tip de modificare are loc atunci cand, ca si n cazul anterior, mai exista valori
neconsumate pentru xk , dar valoarea vk aleasa nu respecta conditiile de continuare. In
acest caz, vk este adaugata multimii Ck (deci este consumata), dar nu se avanseaz
a la
componenta urmatoare. Modificarea este notata prin:
. . . , vk1 xk , xk+1 , . . .
. . . , Ck1 Ck , , . . .

vk
===
z }| {

. . . , vk1 xk ,
xk+1 . . .
. . . , Ck1 Ck {Vk }, , . . .

In exemplul nostru cu generarea permutarilor, urmatoarea transformare este:


x2 x3 x4
A
{A}

A
===

z }| {

A
x2 x3 x4
{A} {A}

134

8.2.3

CAPITOLUL 8. METODA BACKTRACKING

Revenire

Acest tip de transformare apare atunci cand toate valorile pentru componenta xk au fost
consumate (Ck = Vk ). In acest caz se revine la componenta precedenta, xk1 , ncercanduse atribuirea unei noi valori acestei componente. Este important de remarcat faptul ca
revenirea la xk1 implica faptul ca pentru xk se vor ncerca din nou toate variantele
posibile, deci multimea Ck trebuie din nou sa fie vida. Transformarea este notata prin:
. . . , vk1 xk , xk+1 , . . .
. . . , Ck1 Ck , , . . .

. . . , vk2 xk1 , xk , . . .
. . . , Ck2 Ck1 , , . . .

O situatie de revenire, n exemplul cu generarea permutarilor este data de configuratia:


3
1
2
{1, 2, 3} {1} {1, 2}

8.2.4

x4
{1, 2, 3, 4}

3
1
x3 x4
{1, 2, 3} {1} {1, 2}

Revenire dup
a construirea unei solutii

Acest tip de transformare se realizeaza atunci cand toate componentele vectorului au


primit valori care satisfac conditiile interne, adica a fost gasita o solutie. In aceasta
situatie se revine din nou la cazul n care ultima componenta, xn urmeaza sa primeasca o
valoare. Transformarea se noteaza astfel:
. . . , vn
. . . , Cn

. . . , vn1 xn
. . . , Cn1 Cn

sol

In exemplul nostru cu generarea permutarilor, revenirea dupa gasirea primei solutii este
data de diagrama:
1
2
3
4
{1} {1, 2} {1, 2, 3} {1, 2, 3, 4}

!
sol

x4
1
2
3
{1} {1, 2} {1, 2, 3} {1, 2, 3, 4}

Revenirea dupa construirea unei solutii poate fi considerata ca fiind un caz particular al
revenirii daca adaugam vectorului solutie x o componenta suplimentara xn+1 , care nu
poate lua nici o valoare (Vn+1 = ).
O problema importanta este cea a ncheierii procesului de cautare a solutiilor, sau, cu alte
cuvinte ne putem pune ntrebarea: transform
arile succesive aplicate configuratiei initiale
se ncheie vreodat
a sau continu
a la nesf
arsit? Evident ca pentru ca metoda backtracking
sa constituie un algoritm trebuie sa respecte si ea proprietatile unui algoritm enuntate
nca din primul capitol, ntre care se afla si proprietatea de finitudine. Demonstrarea
finitudinii algoritmilor de tip backtracking se bazeaza pe urmatoarea observatie simpla:
prin transformarile succesive de configuratie nu este posibil ca o configuratie sa se repete,
iar numarul de elemente al produsului cartezian V1 V2 . . . Vn este finit; prin urmare,
la un moment dat se va ajunge la configuratia:
x1 x2 . . . xn
V1 . . .

135

8.3. IMPLEMENTAREA METODEI BACKTRACKING

numita configuratie final


a. In configuratia de mai sus ar trebui sa aiba loc o revenire
(deoarece toate valorile pentru prima componenta au fost consumate), adica o deplasare a
barei verticale la stanga. Acest lucru este imposibil, si algoritmul se ncheie deoarece nici
una din cele patru transformari nu poate fi aplicata. In practica, aceasta ncercare de a
deplasa bara de pe prima pozitie (k = 1) pe o pozitie anterioara (k = 0) este utilizata pe
post de conditie de terminare a algoritmului.
Inainte de a trece la implementarea efectiva a metodei backtracking n pseudocod, s
a
generam diagramele de stare pentru Exemplul 8.1:
x1 x2

! A
-

A
x2
{A} {M, N }

sol


A x2
{A}
!

C
x2
{A, B, C}

N
===
z }| {

C
N
{A, B, C} {M, N }

8.3

x1 x2
{A}

B
x2
{A, B} {M }

! M
-

A M
{A} {M }

! B
-

sol


sol


B
x2
{A, B}

B
x2
{A, B} {M, N }
C
M
{A, B, C} {M }

A
x2
{A} {M }
! M
-

sol


C
x2
{A, B, C} {M, N }

C
x2
{A, B, C} {M }
!


N
===

z }| {

B M
{A, B} {M }

x1 x2
{A, B}

C
-

! N
-

x1
x2
{A, B, C}

Implementarea metodei backtracking

Procesul de obtinere a solutiilor prin metoda backtracking este usor de programat deoarece
la fiecare pas se modifica foarte putine componente (indicele k, reprezentand pozitia barei,
componenta xk si multimea Ck ).
Algoritmul corespunzator (n pseudocod) este urmatorul:
initializeaz
a (citeste) multimile de valori, V1 , . . . , Vn
k 1 //se construieste configuratia initial
a
pentru i = 1, n
Ci
//acum ncepe efectiv aplicarea celor 4 transform
ari, functie de caz
a terminarea c
aut
arii
c
at timp k > 0 //k = 0nseamn
dac
a k = n + 1 atunci //configuratia este tip solutie
retine solutia v1 , . . . , vn
k k 1 //revenire dup
a solutie
altfel
dac
a Ck 6= Vk atunci //mai exist
a valori neconsumate

136

CAPITOLUL 8. METODA BACKTRACKING

alege o valoare vk din Ck Vk


Ck = Ck vk //valoarea vk este consumat
a
dac
a v1 , . . . , vk respect
a conditiile de continuare atunci
xk vk ; //atribuie si
k k + 1; //avanseaz
a
altfel //ncercare esuat
a, nu fac nimic
altfel //revenire
Ck , k k 1
sf
arsit c
at timp

Algoritmul de mai sus functioneaza pentru cazul cel mai general, dar este destul de dificil
de programat din cauza lucrului cu multimile Ck si Vk . Din fericire, adeseori n practica
multimile Vk au forma
Vk = {1, 2, . . . , sk }
deci fiecare multime Vk poate fi reprezentata foarte simplu, prin numarul sau de elemente,
sk . Pentru a simplifica si mai mult lucrurile, vom alege valorile pentru fiecare componenta
xk n ordine crescatoare, pornind de la 1, si pana la sk . In aceasta situatie, multimea de
valori consumate Ck va fi de forma {1, 2, . . . , vk } si, drept consecinta va putea fi reprezentata doar prin valoarea vk .
Consideratiile de mai sus permit nlocuirea algoritmului anterior, bazat pe multimi, cu un
algoritm simplificat, care lucreaza numai cu numere.
La Exemplul 8.1, vom conveni sa reprezentam pe A,B,C prin valorile 1,2,3, iar pe M si N
prin 1 si 2. In aceasta situatie, configuratiile succesive se vor reprezenta mai simplu astfel:


| x1 x2

1 | x2

2 |

sol


1 | x2

etc
Algoritmul n pseudocod pentru cazul particular prezentat mai sus se concretizeaza n
urmatoarea metoda Java:
public void backtracking()
{
int k =0 ;
while(k>=0)
{
if(k==n) //am gasit o solutie
{
retSol() ; //afisam solutia
k-- ;
//revenire dupa gasirea unei solutii
}
else
{
if(x[k]<s[k])
//mai sunt valori neconsumate
{

8.4. PROBLEME CLASICE CARE ADMIT REZOLVARE PRIN METODA BACKTRACKING

137

x[k]++ ;
//se ia urmatoarea valoare
if( continuare(k) ) //respecta cond. de cont?
{
k++ ;
//avanseaza
}
}
else
{
x[k--] = 0 ;
}
}
}
}

//revenire

Se observa ca metoda backtracking apeleaza nca doua metode:


metoda retSol, care, asa cum sugereaza si numele ei, retine solutia, constand n
valorile vectorului x. Cel mai adesea aceasta metoda realizeaza o simpla afisare a
solutiei si, eventual, o comparare cu solutiile gasite anterior.
metoda continuare(k) verifica daca valorile primelor k componente ale vectorului x
satisfac conditiile de continuare; n cazul afirmativ este ntoarsa valoarea true, iar n
caz contrar este ntoarsa valoarea false.

8.4
8.4.1

Probleme clasice care admit rezolvare prin metoda backtracking


Problema gener
arii permut
arilor

Se d
a multimea A cu elementele {a1 , a2 , . . . , an }. S
a se genereze toate permut
arile elementelor acestei multimi.
Se observa ca aceasta problema este o simpla generalizare a Exemplului 8.2 din subcapitolul precedent. Mai mult decat atat, problema poate fi redusa la a genera permutarile
multimii de indici {1, 2, . . . , n}. In aceasta situatie vom avea V1 = V2 = . . . = Vn =
{1, 2, . . . , n}, deci putem aplica varianta simplificata a metodei bactracking.
Conditiile interne pe care trebuie sa le respecte un vector solutie sunt:
xi 6= xj pentru i, j = 1, n,

i 6= j.

Conditiile de continuare pentru componenta numarul k, sunt o simpla restrictie a conditiilor


interne:
xi 6= xk pentru i = 1, k 1 .
Prin urmare codul pentru functia de continuare este foarte simplu:
public boolean cont(int k)
{
for(int i=0; i<k; ++i)

138

CAPITOLUL 8. METODA BACKTRACKING

{
if(x[k]==x[i])
{
return false ;
}
}
return true ;
}
Metoda retSol este si ea extrem de simpla n aceasta situatie: se scriu elementele multimii
A, ordonate dupa permutarea x.
public void retSol()
{
for(int i=0; i<n; ++i)
{
System.out.print(a[x[i]-1] + " ") ;
}
System.out.println() ;
}
Metoda backtracking pentru generarea permutarilor se obtine din metoda backtracking
pentru cazul general nlocuind numarul de elemente al multimilor Vk , sk cu valoarea n.

8.4.2

Generarea aranjamentelor si a combin


arilor

Vom prezenta acum modalitatea prin care se poate adapta foarte usor algoritmul de
generare a permutarilor unei multimi pentru a genera aranjamentele si combinarile acelei
multimi. Pentru a simplifica lucrurile, vom presupune ca multimea A este formata din
primele n numere naturale, adica A = {1, 2, . . . , n}.
Reamintim faptul ca prin aranjamente de n luate cate m (n m), notate Am
nteleg
n se
toate multimile ordonate cu m elemente formate din elemente ale multimii A, cu alte
cuvinte toti vectorii de forma:
x = (x1 , . . . , xn ), unde xi {1, 2, . . . , m},

xi 6= xj ,

i, j = 1, n

Se observa ca, din punct de vedere al reprezentarii formale, singura diferenta dintre aranjamente si permutari este ca aranjamentele au lungime m n loc de n. De altfel, pentru
m=n aranjamentele si permutarile coincid.
Exemplul 8.3 Aranjamentele de 3 luate c
ate 2 (A23 )sunt:
(1,2), (1,3), (2,1), (2,3), (3,1), (3,2).
Conditiile interne si, n consecinta, conditiile de continuare, sunt identice cu cele de la
generarea permutarilor. Prin urmare si functia de continuare este identica cu cea de la
permutari. Unde este totusi diferenta? Avand n vedere ca lungimea vectorului este m si
nu n, conditia de gasire a unei solutii trebuie nlocuita cu k=m. Prin urmare, n metoda
backtracking linia:

8.4. PROBLEME CLASICE CARE ADMIT REZOLVARE PRIN METODA BACKTRACKING

139

if(k==n)
va fi nlocuita cu:
if(k==m)
Desigur, aceeasi modificare este necesara si n metoda retSol, n care secventa
for(int i=0; i<n; ++i)
se va nlocui cu
for(int i=0; i<m; ++i)
Sa vedem acum modalitatea de generarea a combinarilor. Reamintim ca prin combinari
de n luate cate m (notat Cnm ) se noteaza toate submultimile cu m elemente ale multimii
A = {1, 2, . . . , n}.
Exemplul 8.4 Combin
arile de 3 luate c
ate 2 (C32 ) sunt:
(1,2), (1,3), (2,3).
Diferenta ntre combinari si aranjamente este data de faptul ca, n cazul combinarilor, ordinea n care apar componentele nu conteaza ( combinarea (1,2) este aceeasi cu combinarea
(2,1) etc.; tocmai din acest motiv noi am optat n exemplul de mai sus sa aranjam componentele unei combinari n ordine crescatoare). Prin urmare, combinarile unei multimi
cu n elemente luate cate m sunt definite de vectorii:
x = (x1 , . . . , xm ), unde x1 < x2 < . . . < xm .
Conditia de continuare n cazul combinarilor va fi pur si simplu:
xk > xk1 pentru k > 1.
Metodele backtracking si retSol sunt n cazul combinarilor identice cu cele de la aranjamente. Diferenta apare la functia de continuare, care are urmatoarea forma (forma din
dreapta este mai criptica, dar mai eleganta):
public boolean cont(int k)
public boolean cont(int k)
{
{
if (k > 0 && x[k] <= x[k 1])
return k == 0 || x[k] > x[k 1];
{
}
return false;
}
else
{
return true;
}
}
Una din problemele de la finalul capitolului propune o varianta mai eficienta de generare
a combinarilor n care functia de continuare este complet eliminata.

140

8.4.3

CAPITOLUL 8. METODA BACKTRACKING

Problema damelor

S
a se aseze n dame pe o tabl
a de sah de dimensiune nn astfel nc
at damele s
a nu fie pe
aceeasi linie, aceeasi coloan
a sau aceeasi diagonal
a (damele s
a nu se atace ntre ele).
Reamintim ca n jocul de sah, o dama ataca pozitiile aflate pe aceeasi linie sau coloana
si pe diagonala. O posibila asezare a damelor pe o tabla de sah de dimensiuni 4x4 este
data n figura de mai jos:

Figura 8.1 O solutie pentru problema damelor n cazul unei table de dimensiuni 4x4
Sa vedem cum putem reformula problema damelor pentru a o aduce la o problema de tip
backtracking. Se observa cu usurinta ca pe o linie a tablei de sah se poate afla o singura
dama, prin urmare putem conveni ca prima dama se va aseza pe prima linie, a doua dama
pe a doua linie etc. Rezulta ca pentru a cunoaste pozitia damei numarul k este suficient sa
stim coloana pe care aceasta se gaseste. O solutie a problemei se poate astfel reprezenta
printr-un vector
x = (x1 , x2 , . . . , xn ),

xk {1, 2, . . . , n},

unde xk reprezinta coloana pe care se g


aseste dama numarul k.
Cu aceasta notatie, vectorul solutie corespunzator exemplului din figura de mai sus este:
(2,4,1,3).
Sa vedem acum care este conditia ca doua dame distincte, k si i, sa se atace:
n mod cert damele nu pot fi pe aceeasi linie
damele sunt pe aceeasi coloana daca xk = xi
damele sunt pe aceeasi diagonala daca distanta dintre abscise este egala cu distanta
dintre ordonate, adica:
|xk xi | = |k i|
Conditia de continuare este ca dama curenta, k, sa nu atace nici una dintre damele care
deja sunt asezate pe tabla, adica:
xk 6= xi si |xk xi | =
6 |k i| pentru i = 1, k 1.
Functia de continuare este:
public boolean continuare(int k)
{
for(int i=0; i<k; ++i)
{
if(x[i]==x[k] || k-i == Math.abs(x[k]-x[i]) )

8.4. PROBLEME CLASICE CARE ADMIT REZOLVARE PRIN METODA BACKTRACKING

141

{
return false ;
}
}
return true ;
}
Modificarile care trebuie aduse metodelor retSol si backtracking sunt minime si le lasam
ca exercitiu.
Observatie: Problema damelor este primul exemplu de problema n care conditiile de
continuare sunt necesare, dar nu sunt suficiente. De exemplu (pentru n=4), la nceput,
algoritmul va aseza prima dama pe prima coloana, a doua dama pe a treia coloana, iar
cea de-a treia dama nu va putea fi asezata pe nici o pozitie, fiind necesara o revenire.

8.4.4

Problema color
arii h
artilor

Se d
a o hart
a ca cea din figura de mai jos, n care sunt reprezentate schematic 6 t
ari,
dintre care unele au granite comune. Presupun
and c
a dispunem doar de trei culori (rosu,
galben, verde), se cere s
a se determine toate variantele de colorare a h
artii astfel nc
at
oricare dou
a t
ari vecine (care au frontier
a comun
a) s
a fie colorate diferit.

T1

T2
T3 T4 T5
T6

0
1
1
0
0
1

1
0
1
1
1
0

1
1
0
1
0
1

0
1
1
0
1
1

0
1
0
1
0
1

1
0
1
1
1
0

Figura 8.2 O hart


a reprezent
and sase t
ari si matricea de vecin
at
ati desenat
a al
aturat
Pentru a memora relatia de vecinatate ntre doua tari vom utiliza o matrice de dimensiuni
6 6 numita vecin definita prin:
(

vecin[i, j] =

true daca tarile Ti si Tj sunt vecine


f alse altfel

Figura 8.2 reprezinta matricea de vecinatati pentru harta cu 6 tari n care s-a facut
conventia ca 1 reprezinta true si 0 reprezinta false.
Problema se poate generaliza usor si la o harta cu n tari care trebuie colorata cu m culori.
Vom utiliza pentru usurarea expunerii harta cu 6 tari de mai sus.
In aceasta problema, un vector solutie x = (x1 , x2 , . . . , xn ) reprezinta o varianta de colorare
a hartii, avand semnificatia ca tara numarul i va fi colorata cu culoarea xi . In exemplul
nostru, xi poate fi 1,2 sau 3, corespunzand respectiv culorilor rosu, galben, verde.
Conditia de continuare este ca tara careia urmarim sa i atribuim o culoare sa aiba o
culoare distincta de tarile cu care are granita. Cu alte cuvinte, trebuie sa avem:
xi 6= xk daca A[i, k] = 1,
Functia de continuare este n aceasta situatie:

i = 1, k 1

142

CAPITOLUL 8. METODA BACKTRACKING

public boolean continuare(int k)


{
for(int i=0; i<k; ++i)
{
if(x[i]==x[k] && vecin[k][i]==1 )
{
return false ;
}
}
return true ;
}
Metoda retSol este:
public void retSol()
{
for(int i=0; i<n; ++i)
{
if(x[i]==1)
{
System.out.print("rosu") ;
}
else
if(x[i]==2)
{
System.out.print("galben") ;
}
else if(x[i]==3)
{
System.out.print("verde") ;
}
}
System.out.println() ;
}
Metoda backtracking este aproape identica cu cea standard si o lasam ca exercitiu.
Programul principal trebuie sa realizeze citirea datelor (numarul de tari n, numarul de
culori disponibile m si matricea de vecinatati).
Pentru o mai buna ntelegere a mecanismului metodei backtracking aplicata la problema
colorarii hartilor, putem sa ne imaginam ca dispunem de 6 cutii identice V1 , V2 , . . . , V6 ,
fiecare dintre cutii continand trei creioane colorate notate cu r - rosu, g - galben, v - verde.
Fiecare cutie Vk contine creioanele care pot fi utilizate pentru colorarea tarii Tk .
O vizualizare a procesului de cautare a solutiilor poate fi obtinut daca aranjam cele 6
cutii n ordine (fiecarei tari i asociem o cutie) si punem un semn naintea cutiei din care
urmeaza sa se aleaga un creion (marcajul corespunde barei verticale de la configuratii);
initial acest semn este n stanga primei cutii. Atunci cand se alege un creion dintr-o cutie
corespunzatoare unei tari el va fi asezat fie pe tara respectiva daca nu exista o tara vecina

143

8.5. PROBLEME PROPUSE

cu aceeasi culoare, fie langa cutie n caz contrar. Astfel, multimile Ci de valori consumate
la un moment dat sunt alcatuite din creioanele de langa cutia Vi si de pe tara Ti . Cu
aceste precizari, cele 4 modificari de configuratie au urmatoarele semnificatii concrete:
atribuie si avanseaz
a: se aseaza creionul ales pe tara corespunzatoare si se trece la
cutia urmatoare
ncercare esuat
a: creionul ales este asezat langa cutia din care a fost scos
revenire: creioanele corespunzatoare tarii curente sunt repuse n totalitate la loc si
se trece la cutia precedenta
revenire dup
a g
asirea unei solutii: semnul este adus la stanga ultimei cutii.
Procesul se ncheie n momentul n care toate creioanele ajung din nou n cutiile n care
se aflau initial.

8.5

Probleme propuse

1. Sa se afiseze toate modurile n care n persoane pot fi asezate la o masa rotund


a
precum si numarul acestora.
Indicatie: Exist
a dou
a posibilit
ati de rezolvare:
(a) Se vor genera toate variantele posibile, prin metoda backtracking, si se vor contoriza. Se va afisa apoi num
arul lor. Va trebui ns
a s
a tineti seama de faptul
c
a unele dispuneri sunt identice din cauza mesei circulare.
(b) Mult mai elegant, se va tine cont de combinatoric
a. Astfel, cu n obiecte se pot
forma n! permut
ari. Cum, n cazul dispunerii lor circulare 1, 2, . . . , n, respectiv 2, 3, . . . , n, 1, . . ., n, 1, 2, . . . , n 1 sunt identice, rezult
a c
a din n astfel de
permut
ari trebuie considerat
a doar una. Num
arul de permut
ari va fi asadar
n!
=
(n

1)!.
n
2. Idem problema 1, cu precizarea ca anumite persoane nu se agreeaza, deci nu pot fi
asezate una langa cealalta. La intrare se mai furnizeaza o matrice simetrica A, cu
urmatoarea semnificatie:
(

A(i, j) =

1 daca nu se ageaza
0
altfel

3. Sa se modifice algoritmul de generare a combinarilor prezentat n paragraful 7.4.2


astfel ncat functia de continuare sa nu mai fie necesara.
Indicatie: Pentru fiecare component
a x[k] se porneste cu valoarea x[k 1] + 1.
4. Gasiti toate solutiile de colorare cu trei culori a hartii din Figura 8.2.

144

CAPITOLUL 8. METODA BACKTRACKING

5. Se dau n multimi A1 , A2 , . . . , An . Sa se afiseze produsul lor cartezian.


Indicatie: Generarea produsului cartezian nseamn
a de fapt generarea ntregului
spatiu de solutii, adic
a un backtracking n care functia de continuare lipseste iar
pe fiecare nivel al stivei sunt inc
arcate pe r
and toate elementele unei multimi fixate.
6. Se da o multime A = {1, 2, . . . , n}. Sa se afiseze toate submultimile acestei multimi.
Indicatie: Se genereaz
a toti vectorii caracteristici de lungime n. Prin vector caracteristic se ntelege un vector ce are doar valorile 1 sau 0 pentru fiecare element cu
semnificatia:
(

x[i] =

1 dac
a i apartine submultimii
0
altfel

Exist
a si o solutie ce genereaz
a vectorii caracteristici de lungime n prin adunarea n
baza 2. Initial vectorul este nul, corespunz
ator multimi vide, iar apoi prin adun
ari
repetate se genereaz
a toate submultimile. Atentie, num
arul total de submultimi este
2n !
7. O firm
a dispune de n angajati, dintre care p sunt femei. Firma trebuie sa formeze o
delegatie de m persoane dintre care k sunt femei. Sa se afiseze toate delegatiile care
se pot forma.
Indicatie: Pentru a forma o delegatie de k femei din p disponibile avem la dispozitie
Cpk variante. Delagatia de m persoane poate fi completat
a cu oricare din variantele
mk
arbatilor din delegatie. Asadar num
arul total de variante este
de Cnp de alegere a b
mk
k
Cp Cnp , ce urmeaz
a a fi calculat.
Generarea efectiv
a se bazeaz
a pe un vector caracteristic cu semnificatia:
(

x[i] =

1 dac
a persoana e femeie
0
altfel

Functia de continuare va num


ara femeile din delegatie si nu va l
asa ca num
arul lor
s
a-l dep
aseasc
a pe k.
8. Se considera multimea A = {1, 2, . . . , n}. Sa se furnizeze toate partitiile acestei multimi. (O partitie a unei multimi este o scriere a multimii ca reuniune de
submultimi disjuncte).
Indicatie: Vom genera partitia sub forma unui vector cu n componente n care
x[i] = k are semnificatia c
a elementul i apartine submultimii k a partitiei considerate.
Ca exemplu, pentru n = 4 putem avea, la un moment dat, vectorul x = (1, 2, 1, 2) ceea
ce corespunde partitiei: A = {1, 3} {2, 4}. Ar fi de remarcat c
a vectorul caracteristic poate lua valori ce vor avea aceeasi interpretare, ca de exemplu x = (2, 1, 2, 1)
ceea ce corespunde partitiei: A = {2, 4} {1, 3}. Dar reuniunea e comutativ
a si
partitia astfel obtinut
a e identic
a cu anterioara. Pentru a evita acest lucru vom

145

8.5. PROBLEME PROPUSE

impune ca fiecare component


a a vectorului s
a poat
a avea cel mult valoarea k, unde
k este indicele elementului. Semnificatia ar fi c
a elementul cu indicele 1 va putea
face parte doar din submultimea 1, cel cu indicele 2 doar din submultimile 1 si 2
etc. O alt
a restrictie ar fi aceea c
a un element nu poate lua o valoarea mai mare ca
max + 1 unde max este valoare maxim
a a elementelor de rang inferior. Acest lucru
se justific
a prin faptul c
a x = (1, 1, 3, 1) nu ar avea nici o semnificatie.
9. Un comis-voiajor trebuie sa viziteze un numar n de orase, pornind din orasul numarul
1. El trebuie sa viziteze fiecare oras o singura data, dupa care sa se ntoarca n orasul
1. Cunoscand legaturile existente ntre orase, se cere sa se gaseasca toate rutele posibile pe care le poate efectua comis-voiajorul.
Indicatie: Se va crea o matrice de adiacent
a (cunoscut
a din teoria grafurilor), care
este o matrice simetric
a:
(

A(i, j) =

1 dac
a exist
a leg
atur
a ntre orasul i si j
0
altfel

Functia de continuare va testa dac


a la elmentul actual se poate ajunge din anteriorul,
n vectorul x. Ca observatie trebuie spus c
a pentru a obtine solutiile distincte trebuie
f
acut un artificiu asem
an
ator cu cel de la problema anterioar
a.
10. Idem problema anterioara, cu precizarea ca pentru fiecare drum ntre doua orase
se cunoaste distanta care trebuie parcursa. Se cere sa se gaseasca ruta de lungime
minima.
momentul retinerii solutiei se va calcula lungimea drumului parcurs.
Indicatie: In
Se va compara aceast
a lungime cu lungimea anterioar
a considerat
a minim
a si se va
retine valoarea actual
a minim
a mpreun
a cu drumul parcurs.
Aceast
a problem
a este celebr
a prin faptul c
a este un exemplu pentru imposibilitatea
afl
arii solutiei exacte altfel dec
at prin backtracking. Datorit
a complexit
atii mari a
metodei s-au g
asit metode mai putin complexe (metodele euristice) dar care dau o
solutie cu o marj
a de aproximare.
11. Presupunem ca avem de pl
atit o suma s si avem la dispozitie un numar nelimitat
de bancnote si monezi de valoare 1 , 2 , . . . , n . Sa se furnizeze toate variantele de
plata a sumei utilizand numai aceste monezi.
Solutie: Rezolvarea acestei probleme va avea la baz
a o variant
a mai modularizat
aa
clasei Bactracking prezentat
a anterior. Aceasta variant
a o vom da n clasa Backtracking1, ce urmeaz
a, dar ea p
astreaz
a ntru totul ideile teoretice din paragrafele
anterioare.
public abstract class BackTracking1
{
int n ; //dimensiunea problemei
boolean as,ev;

146

CAPITOLUL 8. METODA BACKTRACKING

int[] x ; //vectorul solutie


public BackTracking1(int n)
{
this.n = n ;
x = new int[n] ;
for(int i=0; i< n ; ++i)
x[i] = -1 ;
//initializarea elementelor sirului cu -1
}
public void back()
{
int k =0 ;
init(k,x);
while(k>=0)
{
do
{
as=succesor(k,x);//verifica daca am succesor pe nivelul respectiv
if(as) ev=valid(k,x);//daca am succesor e valid?
}while(!((!as)||(as &&ev)));
if(as)
{
if(solutie(k)) tipar(k);
else
{
++k;
init(k,x);
}
}
else k--;
}
}
public abstract boolean succesor( int k, int[] x) ;
public abstract boolean valid( int k, int[] x) ;
public abstract void tipar(int k) ;
public abstract void init( int k, int[] x);
public abstract boolean solutie( int k);
}
Figura 8.3 Clasa Backtracking1
Ideea rezolv
arii, dat
a n clasa Plati ce implementeaz
a clasa abstract
a Backtracking1,
este de a ncarca n stiva fiecare moned
a sau bacnot
a. Conditia de continuare este
aceea ca suma pe care o am p
an
a n acel moment s
a fie mai mic
a sau egal
a dec
at
suma dorit
a. Dac
a aceast
a conditie este ndeplinit
a pot continua c
autarea sau am

8.5. PROBLEME PROPUSE

147

dat peste solutie, altfel voi fi nevoit s


a cobor un nivel n stiv
a. Ca un artificiu am
introdus n metoda succesor o conditie suplimentar
a pentru a nu mai c
auta o solutie
deja g
asit
a. De exemplu: solutia 1+2 este aceeasi cu 2+1. Am presupus de asemenea
c
a valorile monedelor si bacnotelor sunt ordonate cresc
ator. Cu un pic de efort se
poate evita acest lucru retin
and valorile ntr-un vector separat iar n rezolvare se vor
referi doar indicii vectorului.
import io.*;
public class Plati extends BackTracking1
{
int m[]=Reader.readIntArray();
int s=9;
public boolean succesor( int k, int[] x)
{
if((x[k]<m.length-1)&&((k==0)||((k>0)&&(x[k]<x[k-1]))))
{
x[k]++;
return true;
}
else return false;
}
public boolean valid( int k, int[] x)
{
int i,s1=0;
for(i=0;i<=k;i++)
s1=s1+m[x[i]];
if(s<s1) return false;
else return true;
}
public void tipar(int k)
{
System.out.println("solutie");
for(int i=0;i<=k;i++)
System.out.print(m[x[i]]);
System.out.println();
}
public void init( int k, int[] x)
{
x[k]=-1;
}

148

CAPITOLUL 8. METODA BACKTRACKING

public boolean solutie( int k)


{
int i,s1;
for(i=0,s1=0;i<=k;i++)
s1=s1+m[x[i]];
if(s==s1) return true;
else return false;
}
public Plati(int n)
{
super(n) ;//lansarea constructorului din clasa backtracking1;
}
public static void main(String[] args)
{
Plati plati= new Plati(200) ;
plati.back() ;
}
}
Figura 8.4 Clasa Pl
ati
12. Idem problema anterioara, cu precizarea ca trebuie sa platim suma respectiva cu un
numar cat mai mic de monezi si bancnote.
Indictie: Fat
a de rezolvarea problemei anterioare se poate face, spre exemplu, o modificare care s
a compare, n momentul g
asirii unei solutii, indicele stivei cu cel g
asit
la solutiile anterioare.
13. Idem problema anterioara pentru cazul n care dispunem doar de un numar n1 , n2 , . . . , nn
de monezi de valoare 1 , 2 , . . . , n .
Indicatie: Deosebirile fat
a de problemele anterioare constau:
n stiv
a vom nc
arca num
arul de monezi sau bacnote folosite nu si valoarea lor.
Astfel, fiec
arui nivel i corespunde o anumit
a valoare;
la functia succesor vom avea grij
a s
a nu dep
asim num
arul alocat din fiecare
valoare iar pe de alt
a parte indicele stivei va trebui s
a nu dep
aseasc
a num
arul
de valori disponibil;
sumele se vor calcula prin cumularea produselor dintre valoare si num
arul de
valori folosite.
14. Fiind dat un numar natural n, sa se genereze toate partitiile sale. O partitie a unui
numar reprezinta scrierea sa ca suma de numere naturale nenule.

149

8.5. PROBLEME PROPUSE

Indicatie: O solutie ar putea fi cea de la problema cu Plata unei Sume de Bani.


15. Fiind dat un numar natural n, sa se genereze toate descompunerile sale ca suma de
numere prime.
Indicatie: Fat
a de problema anterioar
a se poate verifica, la continuare, dac
a num
arul
ales este prim.
16. O fotografie alb-negru este reprezentata sub forma unei matrice cu elemente 0 sau
1. In fotografie sunt reprezentate unul sau mai multe obiecte. Portiunile corespunzatoare obiectelor au valoarea 1 n matrice. Se cere sa se determine daca fotografia reprezinta unul sau mai multe obiecte.
Exemplu: Matricea de mai jos reprezinta doua obiecte:

0
1
0
1

1
0
0
1

1
0
1
1

0
0
1
0

17. Un teren dreptunghiular este mpartit n m n parcele, reprezentate sub forma unei
matrice A, cu m linii si n coloane. Fiecare element al matricei este un numar real
care reprezinta naltimea parcelei respective. Pe una dintre parcele se afla plasat
a
o bila. Se cere sa se furnizeze toate posibilitatile prin care bila poate sa paraseasc
a
terenul, cunoscut fiind faptul ca bila se poate rostogoli numai pe parcele nvecinate
a caror naltime este strict inferioara naltimii parcelei pe care bila se afla.
Indicatie: Aceasta si problema anterioar
a sunt cazuri tipice de backtracking n plan.
Ideea rezolv
arii const
a n ncercarea de a ajunge la o pozitie vecin
a respect
and conditiile problemei. Modalitatea de miscare este dat
a de cele 8 directii cardinale N, NV,
V, SV, S, SE, E, NE. La fiecare pas avem grij
a s
a nu iesim din spatiul alocat dec
at
cu o solutie nou
a. Este posibil s
a d
am peste aceeasi solutie asa nc
at o vom retine pe
anterioara. Practic, stiva va nc
arca cordonatele curente si directia n care urmeaz
a
s
a ne deplas
am.
18. Pe o tabla de sah de dimensiune 8 8 se ncearca pozitionarea a n pioni dup
a
urmatoarele reguli:
(a) Pe fiecare linie se afla doi pioni.
(b) Pe fiecare coloana se afla cel mult doi pioni.
(c) Pe fiecare paralela la diagonala principala se afla cel mult doi pioni.
Solutie: Programul dat n continuare prezint
a n Figura 8.5 clasa de baz
a Backtracking care este usor modificat
a fat
a de cea dat
a n sectiunea 8.4, iar n Figura 8.6
clasa derivat
a Pions ce implementeaz
a metodele abstracte si contine functia main.
public abstract class BackTracking
{

150

CAPITOLUL 8. METODA BACKTRACKING

int n;
//dimensiunea problemei
int[] x; //vectorul solutie
public BackTracking(int n, int m)//constructorul
{
this.n = n ;
x = new int[n] ;
for(int i=0; i< n ; ++i)
{
x[i] = -1 ;//initializarea cu -1 a elem vect solutie
}
}
public void back()
{
int k =0 ;
while(k>=0)
{
if(k==n) //am gasit o solutie si am afisat-o
k-- ; //pasul "back" dupa gasirea solutiei
else
{
if(x[k]<7)//in vectorul solutie x[k] este coloana
{
x[k]++ ;
if( cont(k) ) //verifica conditia ce continuare
{
k++;//cautam locul urmatorului pion
if (k!=n)//daca nu am ajuns la solutie
//verificam daca suntem la al doilea pion pe aceeasi linie.
//Pentru usurinta verificarilor de asezare
//(pentru primul pion deja verificasem) am presupus ca cei doi pioni
//de pe aceeasi linie se afla pe nivelele consecutive in stiva
//k si k+1 unde k este par, si initial pe aceeasi coloana.
{
if (((k-1)%2==0))
x[k]=x[k-1];
}
else
retSol();//afisarea rezultatului
}
}
else
x[k--] = -1 ;
//intoarcere cu o pozitie pentru ca nu se poate continua
}

8.5. PROBLEME PROPUSE

151

}
}
public abstract boolean cont( int k ) ;
public abstract void retSol() ;
}
Figura 8.5 Clasa Backtracking
import java.io.* ;
public class Pions extends BackTracking
{
public boolean cont(int k)
{
int l=0,m=0;
for(int i=0; i<k; ++i)
{
if (x[i]==x[k]) //aflam citi pioni se mai afla pe aceeasi coloana
l++;
//cu ultimul pion introdus
}
if ((k%2==1)&&(x[k]==x[k-1]))
return false;
int e=k/2-1,f=x[k]-1;
while ((e>-1)&&(f>-1))
//aflam daca pe orice paralela la diagonala principala
//se afla mai mult de 2 pioni dupa introd pionului de pe nivelul k
{
if ((x[e*2]==f)||(x[2*e+1]==f))
//x[e*2]este primul element de pe linia e
//iar x[e*2+1]este al doilea element de pe linia e
m++;
e--;
f--;
}
if ((l<2) &&(m<2))
return true;//se poate continua daca pe aceeasi coloana sau
//paralela cu diagonala pricipala nu se afla mai mult de doi pioni
else
return false;
}
public void retSol()//afisarea rezultatului
{
System.out.println();
for(int l=0;l<8;l++)

152

CAPITOLUL 8. METODA BACKTRACKING

{
for(int j=0; j<=7; ++j)
{
if((j==x[2*l])||(j==x[2*l+1]))
{
System.out.print("X ");//daca pe linia l unul dintre pioni este
//pe coloana j atunci il afisam (marcat cu X)
}
else
{
System.out.print("O ") ;
}
}
System.out.println() ;
}
}
public Pions(int n)
{
super(n,n) ;
//apelarea constructorului din clasa de baza backtracking;
}
public static void main(String[] args)
{
Pions pion = new Pions(16);
//constructorul este apelat pentru 16 pioni
pion.back();
//ce vor fi amplasati pe o tabla de sah obisnuita de dimensiune 8x8
}
}
Figura 8.6 Clasa Pions

Capitolul 9

Divide et Impera
In acest capitol vom studia o alta metoda fundamentala de elaborare a algoritmilor, numita Divide et Impera. Ca si Backtracking, Divide et Impera se bazeaza pe un principiu
extrem de simplu: descompunem problema n dou
a (sau mai multe) subprobleme de dimensiuni mai mici; rezolv
am subproblemele, iar solutia pentru problema initial
a se obtine
combin
and solutiile subproblemelor n care a fost descompus
a. Rezolvarea subproblemelor
se face n acelasi mod cu problema initiala. Procedeul se reia pana cand subproblemele
devin atat de simple ncat admit o rezolvare imediata.
Inca din descrierea globala a acestei tehnici s-au strecurat elemente de recursivitate. Pentru
a putea ntelege mai bine aceasta metoda de elaborare a algoritmilor care este eminamente
recursiva, vom prezenta n sectiunea 9.1 cateva elemente fundamentale referitoare la recursivitate. Continuam apoi n sectiunea 9.2 cu prezentarea generala a metodei, urmat
a
de rezolvarea anumitor probleme de Divide et Impera deosebit de importante: c
autare
binar
a, sortarea prin interclasare, sortarea rapid
a si evaluarea expresiilor aritmetice.

9.1

Notiuni elementare referitoare la recursivitate

In acest paragraf vom reaminti cateva elemente esential referitoare la recursivitate. Cei
care st
apanesc deja acest mecanism, pot sa treaca direct la prezentarea metodei Divide et
Impera din paragraful 9.2.
Avand n vedere faptul ca recursivitatea este un mecanism de programare general, care nu
tine doar de limbajul Java, prezentarea facuta va folosi si limbajul pseudocod la descrierea
algoritmilor recursivi, pentru a nu ncarca prezentarea cu detalii de implementare.

9.1.1

Functii recursive

Recursivitatea este un concept care deriva n mod direct din notiunea de recurent
a matematic
a. Recursivitatea este un instrument elegant si puternic pe care programatorii l au la
dispozitie pentru a descrie algoritmii. Este interesant de retinut faptul ca programatorii
obisnuiau sa utilizeze recursivitatea pentru a descrie algoritmii cu mult nainte ca limbajele de programare sa suporte implementarea directa a acestui concept.
Din punct de vedere informatic, o subrutina (procedura sau functie) recursiva este o
subrutin
a care se autoapeleaz
a. Sa luam ca exemplu functia factorial, a carei definitie
matematica recurenta este:
153

154

CAPITOLUL 9. DIVIDE ET IMPERA

F act(n) =

n F act(n 1) pentru n 1
1
pentru n = 0

Din exemplul de mai sus se observa c


a factorialul este definit functie de el nsusi, dar
pentru o valoare a parametrului mai mica cu o unitate. Iata acum care este implementarea
recursiva a factorialului, folosind o functie algoritmica (stanga) si implementarea Java
(dreapta):
functie fact(n)
public static long fact(int n)
daca n=0 atunci
{
fact 1
if( n==0 )
altfel
return 1 ;
fact n*fact(n-1)
else
return
return n*fact(n-1);
}
Se observa ca functia de mai sus nu este decat o traducere aproape directa a formulei
matematice anterioare. Trebuie sa remarcam ca, asa cum vom vedea n continuarea acestui
capitol, la baza functionarii acestor functii sta un mecanism foarte precis, care nu este atat
de trivial cum ar parea la prima vedere.
Sa luam ca al doilea exemplu, calculul celebrului sir al lui Fibonacci, care este definit
recurent astfel:
(

F ib(n) =

F ib(n 1) + F ib(n 2) pentru n > 1


n
pentru n = 0, 1

Implementarea n pseudocod, respectiv Java a sirului lui Fibonacci este:


functie fib(n)
public static long fib(int n)
daca n=0 sau n=1 atunci
{
fib n
if( n==0 || n==1 )
altfel
return n ;
fib fib(n-1)+fib(n-2)
else
return fib(n-1)+fib(n-2);
return
}
Se observa ca n ambele exemple am nceput cu asa numita conditie de terminare:
dac
a n=0 sau n=1 atunci
Fibn
care corespunde cazului n care nu se mai fac apeluri recursive. O functie recursiva care
nu are conditie de terminare va genera apeluri recursive interminabile, care se soldeaza
inevitabil cu eroarea java.lang.StackOverflowError (dep
asire de stiv
a, deoarece asa cum
vom vedea, fiecare apel recursiv presupune salvarea anumitor date pe stiva, iar stiva are o
dimensiune finita). Conditia de terminare ne asigura de faptul ca atunci cand parametrul
functiei devine suficient de mic, nu se mai realizeaza apeluri recursive si functia este calculata direct.
Ideea fundamentala care sta la baza ntelegerii profunde a mecanismului recursivitatii este

9.1. NOT
IUNI ELEMENTARE REFERITOARE LA RECURSIVITATE

155

aceea ca n esent
a, un apel recursiv nu difer
a cu nimic de un apel de functie obisnuit.
Pentru a veni n sprijinul acestei afirmatii trebuie sa studiem mai n amanuntime ce se
petrece n cazul unui apel de functie.
Se cunoaste faptul ca n situatia n care compilatorul ntalneste un apel de functie, acesta
preda controlul executiei functiei respective, dupa care se revine la urmatoarea instructiune
de dupa apel. ntrebarea care apare n mod firesc este: de unde stie compilatorul unde s
a
se ntoarc
a la terminarea functiei? De unde stie care au fost valorile variabilelor nainte
de a se preda controlul functiei? Raspunsul este simplu: nainte de a realiza un apel
de functie compilatorul salveaza complet starea programului (linia de la care s-a realizat
apelul, valorile variabilelor locale, valorile parametrilor de apel) pe stiva, urmand ca la
revenirea din subrutina sa rencarce starea care a fost nainte de apel de pe stiva.
Pentru exemplificare sa consideram urmatoarea procedura (nerecursiva) care afiseaza o
linie a unei matrice. Atat linia care trebuie afisata, cat si matricea sunt transmise ca
parametru:
procedur
a AfisLin(a: tmatrice; n,lin: integer)
pentru i = 1,n
scrie a[lin,i]
return
Procedura AfisLin este apelata de procedura AfisMat descrisa mai jos, care afiseaza linie
cu linie o ntreaga matrice pe care o primeste ca parametru:
procedur
a AfisMat(a: tmatrice; n: integer)
pentru i = 1,n
AfisLin(a,n,i)
return
Sa presupunem ca procedura AfisMat este apelata ntr-un program astfel:
...
AfisMat(a,5)
...
pentru a afisa o matrice de dimensiuni 5 5.
In momentul n care compilatorul ntalneste acest apel, el salveaza pe stiva linia de la care
s-a facut apelul (sa spunem 2181), valoarea matricei a si alte variabile locale declarate n
program:

2181; Af isM at(n, a); . . .


Figura 9.1 Stiva programului dup
a apelul procedurii AfisMat

156

CAPITOLUL 9. DIVIDE ET IMPERA

Controlul va fi apoi preluat de catre procedura AfisMat, care intra n ciclul pentru cu
apelul: AfisLin(a,n,i) aflat sa zicem la linia 2198.
In acest moment controlul va fi preluat de catre procedura AfisLin, dar nu nainte de a
adauga la varful stivei linia de la care s-a facut apelul, valorile parametrilor si a variabilei
locale i:

2198; Af isLin(n, a, i); i = 1; . . .


2181; Af isM at(n, a); . . .
Figura 9.2 Continutul stivei programului dup
a apelul procedurii AfisLin
Procedura AfisLin va tipari prima linie a matricei, dupa care executia ei se ncheie. n
acest moment compilatorul consulta varful stivei pentru a vedea unde trebuie sa revina si
care au fost valorile parametrilor si variabilelor locale nainte de apel. Variabila i devine
2, si din nou se apeleaza procedura AfisLin etc.
Remarcam aici faptul ca atat procedura AfisMat cat si procedura AfisLin utilizeaza o variabila locala numita i. Nu poate exista nici o confuzie ntre cele doua variabile, deoarece
n momentul executiei lui AfisLin, valoarea variabilei i din AfisMat este salvata pe stiva.
Sa vedem acum evolutia stivei program n cazul calculului recursiv al lui Fact(5). Presupunem ca linia 2145 are loc apelul recursiv: f act n f act(n 1).
Pentru a realiza nmultirea respectiva, trebuie ca ntai sa se calculeze Fact(n-1). Cum n
are valoarea 5, pe stiva se va depune F act(4). Abia dupa ce valoarea lui F act(4) va fi
calculata se poate calcula valoarea lui F act(5). Calculul lui F act(4) implica nsa calculul
lui F act(3), care implica la randul lui calculul lui F act(2), F act(1), F act(0). Calculul lui
F act(0) se realizeaza prin atribuire directa, fara nici un apel recursiv:
dac
a n=0 atunci
f act 1
n acest moment, stiva programului contine toate apelurile recursive realizate pana acum:

2145; F act(0);
2145; F act(1);
2145; F act(2);
2145; F act(3);
2145; F act(4);
xxxx; F act(5);
Figura 9.3 Continutul stivei programului c
and n devine 0, n cazul calcului lui Fact(5).
Se presupune c
a linia cu apelul recursiv este situat
a la adresa 2145.

157

9.1. NOT
IUNI ELEMENTARE REFERITOARE LA RECURSIVITATE

F act(1) fiind calculat se poate reveni la calculul nmultirii 2*f act(1) = 2, apoi, F act(2)
fiind calculat se revine la calculul nmultirii 3*f act(2)=6 s.a.m.d. pana se calculeaz
a
5*f act(4)=120 si se revine n programul apelant.
Sa vedem acum modul n care se realizeaza calculul recursiv al sirului lui Fibonacci. Vom
vedea ca timpul de calcul al acestei recurente este incomparabil mai mare fata de calculul factorialului. Sa presupunem ca functia fib se apeleaza cu parametrul n = 3.
In aceasta situatie, se depune pe stiva apelul f ib(3) mpreuna cu linia de unde s-a realizat apelul (de exemplu, 2160). In linia 2160 a procedurii are loc apelul recursiv:
f ib f ib(n 1) + f ib(n 2) care n cazul nostru, n fiind 3, presupune calcularea sumei
f ib(2) + f ib(1). Aceasta suma nu poate fi calculata nainte de a-l calcula pe f ib(2). Calculul lui f ib(2) presupune calcularea sumei f ib(1) + f ib(0). f ib(1) si f ib(0) se calculeaz
a
direct la urmatorul apel recursiv, dupa care se calculeaza suma lor, f ib(2) = 2. Abia
acum se revine la suma f ib(2) + f ib(1) si se calculeaza f ib(1), dupa care se revine si se
calculeaza f ib(3).
Modul de calcul al lui f ib(n) recursiv se poate reprezenta foarte sugestiv arborescent.
Radacina arborelui este f ib(n), iar cei doi fii sunt apelurile recursive pe care f ib(n) le
genereaza, si anume f ib(n 1) si f ib(n 2). Apoi se reprezinta apelurile recursive generate de f ib(n 2) s.a.m.d:
#

Fib(n)
"!
@
@ '$
'$
@

Fib(n-1)

Fib(n-2)

&%
'$
'$

@
&%
@
'$
'$
@

Fib(n-2)

Fib(n-3)

Fib(n-3)

&%
&%

Fib(n-4)

&%
&%

Figura 9.4 Reprezentarea arborescent


a a apelurilor recursive din sirul lui Fibonacci
Din figura de mai sus se observa ca anumite valori ale sirului lui Fibonacci se calculeaz
a
de mai multe ori. fib(n) si f ib(n 1) se calculeaza o data, f ib(n 2) se calculeaza de dou
a
ori, f ib(n 3) de 3 ori s.a.m.d.

9.1.2

Recursivitatea nu nseamn
a recurent
a

Implementarea recursiva a functiilor recurente este usor de nteles, datorita mecanismului


simplu de transpunere a recurentei ntr-o functie recursiva. Totusi, recursivitatea nu se
limiteaza doar la implementarea recurentelor matematice. Putem defini la fel de bine

158

CAPITOLUL 9. DIVIDE ET IMPERA

si operatii recursive. O operatie recursiva este definita functie de ea nsasi, pentru o


problema de dimensiune mica. De exemplu, operatia de inversare a n caractere se poate
defini recursiv astfel: se extrage primul caracter din sir, apoi se inverseaza cele n 1
caractere ramase dupa care se adauga la final caracterul extras. Acest principiu l aplicam
n exemplul care urmeaza.
Exemplul 9.1 S
a se scrie o functie care citeste o secvent
a de caractere p
an
a ce nt
alneste
caracterul ., dup
a care afiseaz
a caracterele n ordine invers
a.
Rezolvarea acestei probleme se poate formula recursiv astfel: inversarea caracterelor unui
sir de n elemente implica inversarea caracterelor ramase dupa citirea primului caracter, si
scrierea n final a primului caracter:
Inv(n) = Citeste(a) + Inv(n 1) + Scrie(a)
n consecinta procedura de inversare va avea forma (parametrul n a fost eliminat, el fiind
dat n formula doar pentru claritate):

functie inversare
public static void inversare( )
citeste a
{
0
0
daca a <> . atunci inversare
char a;
scrie inversare
a=Reader.readChar();
return
if(a!=.)
{
inversare();
}
System.out.print(a);
}
Este important de notat ca pentru ca functia sa functioneze corect, variabila a trebuie
declarata ca variabila locala; astfel, toate valorile citite vor fi salvate pe stiva, de unde vor
fi extrase succesiv (n ordinea inversa citirii) dupa ntalnirea caracterului ..
Exemplul 9.2 Transformarea unui num
ar din baza 10 ntr-o baz
a b, mai mic
a dec
at 10.
Sa ne reamintim algoritmul clasic de trecere din baza 10 n baza b. Numarul se mparte
la b si se retine restul. Catul se mparte din nou la b si se retine restul ... pana cand catul
devine mai mic decat b. Rezultatul se obtine prin scrierea n ordine inversa a resturilor
obtinute.
Formularea recursiva a acestei rezolvari pentru trecerea unui numar n n baza b este:
(

T ransf (n) =

T ransf (n div b) + Scrie(n mod b) pentru n b

pentru n < b

Procedura care realizeaza acest cod este descrisa astfel:

9.2. PREZENTAREA METODEI DIVIDE ET IMPERA

159

functie transform(n:integer)
public static void transform(int n)
rest=n mod b
{
daca n < b atunci transform(n div b)
int rest=n%b;
scrie rest
if(n < b)
return
{
transform(n/b);
}
System.out.print(rest);
}
De remarcat ca n aceasta functie variabila rest trebuie sa fie declarata local, pentru a fi
salvata pe stiva, n timp ce variabila b este bine sa fie declarata global, deoarece valoarea
ei nu se modifica, salvarea ei pe stiva ocupand spatiu inutil.

9.2

Prezentarea metodei Divide et Impera

Divide et Impera este o metoda speciala prin care se pot aborda anumite categorii de
probleme. Ca si celelalte metode de elaborare a algoritmilor, Divide et Impera se bazeaz
a
pe un principiu extrem se simplu: se descompune problema initial
a n dou
a (sau mai multe)
subprobleme de dimensiune mai mic
a, care se rezolv
a, dup
a care solutia problemei initiale
se obtine combin
and solutiile subproblemelor n care a fost descompus
a. Se presupune c
a
fiecare dintre problemele n care a fost descompusa problema initiala se poate descompune
n alte subprobleme, la fel cum a fost descompusa si problema initiala. Procedeul de
descompunere se repeta pana cand, dupa descompuneri succesive, se ajunge la probleme
de dimensiune mica, pentru care exista rezolvare directa.
Evident, nu orice gen de problem
a se preteaza la a fi abordata cu Divide et Impera. Din
descrierea de mai sus reiese ca o problema abordabila cu aceasta metoda trebuie sa aib
a
doua proprietati:
1. Sa se poata descompune n subprobleme
2. Solutia problemei initiale sa se poata construi simplu pe baza solutiei subproblemelor
Modul n care metoda a fost descrisa, conduce n mod natural la o implementare recursiv
a,
avand n vedere faptul ca si subproblemele se rezolva n acelasi mod cu problema initiala.
Iata care este forma generala a unei functii Divide et Impera:
a)
functie DivImp(P: Problem
dac
a Simplu(P) atunci
Rezolv
aDirect(P) ;
altfel
Descompune(P, P1, P2) ;
DivImp(P1) ;
DivImp(P2) ;
Combin
a(P1, P2) ;
return

160

CAPITOLUL 9. DIVIDE ET IMPERA

In consecinta, putem spune ca abordarea Divide et Impera implica trei pasi la fiecare nivel
de recursivitate:
1. Divide problema n doua subprobleme.
2. St
ap
aneste (Cucereste) cele doua subprobleme prin rezolvarea acestora n mod recursiv.
3. Combin
a solutiile celo doua subprobleme n solutia finala pentru problema initiala.

9.3

C
autare binar
a

C
autarea binar
a este o metoda eficienta de regasire a unor valori ntr-o secventa ordonata.
Desi este trecuta n acest capitol, cautarea binara nu este un exemplu tipic de problema
Divide et Impera, deoarece n cazul ei se rezolva doar una din cele doua subprobleme, deci
lipseste faza de recombinare a solutiilor. Enuntul problemei de cautare binara este:
Se d
a un vector cu n componente (ntregi), ordonate cresc
ator si un num
ar ntreg. S
a se
decid
a dac
a se g
aseste n vectorul dat, si, n caz afirmativ, s
a se furnizeze indicele pozitiei
pe care se g
aseste.
O rezolvare imediata a problemei presupune parcurgerea secventiala a vectorului dat, pana
cand p este gasit, sau am ajuns la sfarsitul vectorului. Aceasta rezolvare nsa nu foloseste
faptul ca vectorul este sortat.
Cautarea binara procedeaza n felul urmator: se compara p, cu elementul din mijlocul
vectorului; daca p este egal cu acel element, cautarea s-a ncheiat. Daca este mai mare, se
cauta doar n prima jumatate, iar daca este mai mic, se cauta doar n a doua jumatate.
Cititorul atent a observat cu siguranta ca n aceasta situatie problema nu se descompune
n doua subprobleme care se rezolva, dupa care se construieste solutia, ci se reduce la una
sau la alta din subprobleme. Cei trei pasi ai lui Divide et Impera sunt n aceasta situatie:
1. Divide: mparte sirul de n elemente n care se realizeaza cautarea n doua siruri cu
n/2 elemente.
2. St
ap
aneste: Cauta n una dintre cele doua jumatati, functie de valoarea elementului
din mijloc.
3. Combin
a: Nu exista.
Iata care este functia care realizeaza cautarea elementului x n sirul a, ntre indicii low si
high .
public static int binarySearch(int[] a, int x, int low, int high)
{
if(low <= high)
{
int middle = (low + high)/2 ;
if( x == a[middle] )
{

9.4. SORTAREA PRIN INTERCLASARE (MERGESORT)

161

return middle ;
}
else
{
if( x < a[middle] )
{
return binarySearch(a, x, low, middle -1 ) ;
}
else
{
return binarySearch(a, x, middle+1, high) ;
}
}
}
return -1 ;
}
Pozitia pe care se gaseste elementul x n sirul a este data de apelul:
poz = binarySearch(a,x,0,a.length-1)

9.4

Sortarea prin interclasare (MergeSort)

Sortarea prin interclasare este, alaturi de sortarea rapid


a (QuickSort) si sortarea cu ansamble (HeapSort), una dintre metodele eficiente de ordonare a elementelor unui sir. Enuntul
problemei este urmatorul:
S
a se ordoneze cresc
ator un vector cu n componente ntregi.
Principiul de rezolvare este urmatorul: se mparte vectorul n doua parti egale si se
sorteaza fiecare jumatate, apoi se interclaseaza cele doua jumatati. Descompunerea n
doua jumatati se realizeaza pana cand se ajunge la vectori cu un singur element, care
nu mai necesita sortare. Algoritmul de sortare prin interclasare urmeaza ndeaproape
paradigma Divide et Impera. In mare, modul ei de operare este:
1. Divide: mparte sirul de n elemente care urmeaza a fi sortat n doua siruri cu n/2
elemente
2. St
ap
aneste: Sorteaza recursiv cele doua subsiruri utilizand sortarea prin interclasare
3. Combin
a: Interclaseaza subsirurile sortate pentru a obtine rezultatul final.
Procedura MergeSort de mai jos implementeaza algoritmul de sortare prin interclasare.
Apelul initial al functiei este:
mergeSort(a,0,a.length-1)
unde n este numarul de elemente al tabloului a.

162

CAPITOLUL 9. DIVIDE ET IMPERA

public static void mergeSort(int[] a, int low, int high)


{
if(low<high)
{
int mid=(low+high)/2;
mergeSort(a,low,mid);
mergeSort(a,mid+1,high);
intercls(a,low,mid,high);
}
}
Functia de interclasare n acest caz este analoaga cu functia de interclasare obisnuita
a doua siruri, diferenta constand n faptul ca acum se interclaseaza doua jumatati ale
aceluiasi sir, iar rezultatul se va depune n final tot n sirul interclasat. Lasam scrierea
acestei rutine ca exercitiu.

9.5

Sortarea rapid
a (QuickSort)

Sortarea rapid
a este, asa cum i spune si numele, cea mai rapida metoda de sortare cunoscuta n prezent . Exista foarte multe variante ale acestei metode, o parte dintre ele avand
doar rolul de a micsora timpul de executie n cazul cel mai nefavorabil. Vom prezenta
aici varianta clasica, despre care veti remarca cu surprindere ca este neasteptat de simpla.
Enuntul problemei este identic cu cel de la sortarea prin interclasare.
S
a se ordoneze cresc
ator un vector de numere ntregi.
Metoda de sortare rapida prezentata n acest paragraf este dintr-un anumit punct de
vedere complementara metodei Mergesort. Diferenta ntre cele doua metode este data de
faptul ca n timp ce la Mergesort mai nti vectorul se mpartea n doua parti dupa care se
sorta fiecare parte si apoi se interclasau cele doua jumatati, la Quicksort mpartirea se face
n asa fel ncat cele doua siruri sa nu mai trebuiasca a fi interclasate dupa sortare, adica
primul sir sa contina doar elemente mai mici decat al doilea sir. Cu alte cuvinte, n cazul
lui Quicksort etapa de recombinare este triviala, deoarece problema este astfel mpartita n
subprobleme astfel ncat sa nu mai fie necesara interclasarea sirurilor. Etapele lui Divide
et Impera pot fi descrise n aceasta situatie astfel:
1. Divide: Imparte sirul de n elemente care urmeaza a fi sortat n doua siruri, astfel
nc
at elementele din primul sir s
a fie mai mici dec
at elementele din al doilea sir
2. St
ap
aneste: Sorteaza recursiv cele doua subsiruri utilizand sortarea rapida
3. Combin
a: Sirul sortat este obtinut din concatenarea celor doua subsiruri sortate.
Functia care realizeaza mpartirea n subprobleme (astfel ncat elementele primului sir sa
fie mai mici decat elementele celui de-al doilea) se datoreaza lui C. A. Hoare, care a gasit
o metoda de a realiza aceasta mpartire (numita partitionare) n timp liniar.
Procedura de partitionare rearanjeaza elementele tabloului functie de primul element,
numit pivot, astfel ncat elementele mai mici decat primul element sunt trecute n stanga

163

(QUICKSORT)
9.5. SORTAREA RAPIDA

lui, iar elementele mai mari decat primul element sunt trecute n dreapta lui. De exemplu,
daca avem vectorul:
a = (7, 8, 5, 2, 3),
atunci procedura de partitionare va muta elementele 5,2 si 3 n stanga lui 7, iar 8 va fi
n dreapta lui. Cum se realizeaza acest lucru? Sirul este parcurs simultan de doi indici:
primul indice, low, pleaca de la primul element si este incrementat succesiv, al doilea indice,
high, porneste de la ultimul element si este decrementat succesiv. In situatia n care a[low]
este mai mare decat a[high], elementele se interschimba. Partitionarea este ncheiata n
momentul n care cei doi indici se ntalnesc (devin egali) undeva n interiorul sirului. La
un pas al algoritmului, fie se incrementeaza low, fie se decrementeaza high; ntotdeauna
unul dintre cei doi indici, low sau high, este pozitionat pe pivot. Atunci cand low indic
a
pivotul, se decrementeaza high, iar atunci cand high indica pivotul se incrementeaza low.
Iata cum functioneaza partitionarea pe exemplul de mai sus. La nceput, low indica primul
element, iar high indica ultimul element:
a = (7, 8, 5, 2, 3)

low
high
Deoarece a[low] > a[high] elementele 7 si 3 se vor interschimba. Dupa interschimbare,
pivotul va fi indicat de high, deci low va fi incrementat:
a = (3, 8, 5, 2, 7)

low high
Din nou avem a[low] > a[high], elementele 8 si 7 se vor interschimba. Dupa interschimbare,
pivotul va fi indicat de low, deci high va fi decrementat:
a = (3, 7, 5, 2, 8)

low high
Din nou avem a[low] > a[high], elementele 7 si 2 se vor interschimba. Dupa interschimbare,
pivotul va fi indicat de high, deci low va fi incrementat:
a = (3, 2, 5, 7, 8)

low high
De data aceasta avem a[low] <= a[high], deci low va fi incrementat din nou, fara a se
realiza interschimbari.
In acest moment low si high s-au suprapus (au devenit egale), deci partitionarea s-a
ncheiat. Pivotul este pe pozitia a 4-a, care este de fapt si pozitia lui final
a n sirul
sortat.
Functia de partitionare de mai jos primeste ca parametri limitele inferioara, respectiv superioar
a ale sirului care se partitioneaza si returneaza pozitia pe care se afla pivotul n
finalul partitionarii. Pozitia pivotului este importanta deoarece ne da locul n care sirul
va fi despartit n doua subsiruri.

164

CAPITOLUL 9. DIVIDE ET IMPERA

public static int partitionare(int low, int high)


{
boolean pozPivot=false;//variabila care ne spune daca low indica pivotul
while(low<high)//indicii de parcurgere nu s-au suprapus
{
if(a[low]>a[high])
{
interschimba(a,low,high);
pozPivot= !pozPivot;//alt indice indica acum pivotul
}
pozPivot?low++:high--;
}
return(low);//se returneaza pozitia pivotului
}
Doua observatii importante merita facute asupra acestei functii:
1. Variabila pozPivot poate lua valoarea false daca pivotul este indicat de low sau true
daca pivotul este indicat de high. Atribuirea pozP ivot =!pozP ivot are ca efect
schimbarea starii acestei variabile din false n true sau invers.
2. Functia se foloseste n mod inteligent de transmiterea prin valoare a parametrilor,
deoarece modifica variabilele low si high, fara ca aceasta modificare sa afecteze programul principal.
Procedura de sortare propriu-zisa, respecta structura Divide et Impera obisnuita, doar ca
functia de recombinare a solutiilor nu mai este necesara, deoarece am realizat partitionarea
nainte de apel:
public static void quickSort(int low,int high)
{
if(low<high)//daca subsirul mai are cel putin doua elemente
{
int mid=partitionare(low, high);//partitioneaza
quickSort(low, mid-1);//sorteaza prima jumatate
quickSort(mid+1, high);//sorteaza a doua jumatate
}
}

9.6

Expresii aritmetice

Se d
a o expresie aritmetic
a n care operanzii sunt simbolizati prin litere mici (de la a la
z), iar operatorii sunt +, -, /, si * cu semnificatia cunoscut
a. Se cere s
a se scrie un
program care transform
a expresia n form
a polonez
a postfixat
a.
Reamintim faptul ca forma polonez
a postfixat
a (Lukasiewicz) este obtinuta prin scrierea
operatorului dupa cei doi operanzi, si nu ntre ei. Aceasta forma are avantajul ca nu

165

9.6. EXPRESII ARITMETICE

necesita paranteze pentru a schimba prioritatea operatorilor. Ea este utilizata adeseori n


informatica pentru a evalua expresii.
Exemplul 9.3

1. a+b se scrie ab+

2. a*(b+c) se scrie abc+*


3. (a+b)*(c+d) se scrie ab+cd+*
Unul dintre cei mai simpli algoritmi de a trece o expresie n forma poloneza consta n a
cauta care este operatorul din expresie cu prioritatea cea mai mica, si de a aseza acest
operator n finalul expresiei, urmand ca prima parte a formei poloneze sa fie formata din
transformarea expresiei din stanga operatorului, iar a doua parte a formei poloneze sa fie
formata din transformarea expresiei din dreapta operatorului.
Cele doua subexpresii urmeaza a se trata n mod analog, pana cand se ajunge la o subexpresie de lungime 1, care va fi obligatoriu un operand si care nu mai necesita transformare.
Schematic, daca avem expresia:
E=E1 op E2
unde E1 si E2 sunt subexpresii, iar op este operatorul cu prioritatea cea mai mica (deci
operatorul unde expresia se poate rupe n doua), atunci forma poloneza a lui E, notat
a
Pol(E), se obtine astfel:
Pol(E)=Pol(E1) Pol(E2) op .
Expresia de mai sus exprima faptul ca forma poloneza postfixata a lui E se obtine prin
scrierea n forma poloneza postfixata a celor doua subexpresii, urmate de operatorul care
le separa. Expresia de mai sus este o expresie recursiva specifica tehnicii Divide et Impera.
Etapele sunt n aceasta situatie (ilustrate n metoda din Figura9.6):
1. Divide: mparte expresia aritmetica n doua subexpresii legate printr-un operator de
prioritate minima.
2. St
ap
aneste: Transforma recursiv n forma poloneza cele doua subexpresii.
3. Combin
a: Scrie cele doua subexpresii n forma poloneza urmate de operatorul care
le leaga.
Functia de trecere n forma poloneza (Figura9.6) primeste ca parametru indicele inferior si superior reprezentand limitele ntre care se ncadreaza subexpresia n expresia far
a
paranteze. Sirul original este continut n string-ul a. Functia eliminare paranteze transforma sirul original ntr-un sir fara paranteze pe baza prioritatii operatorilor. Functia
polonez returneaza un sir de caractere care contine expresia n forma poloneza:
1. public void eliminare_paranteze()
2. {
3. int[] p=new int[a.length()];
4. int i,j=0;
5. for(i=0;i<a.length();i++)

166

CAPITOLUL 9. DIVIDE ET IMPERA

6. {
7.
switch(a.charAt(i))
8.
{
9.
case (:
10.
j=j+10;
11.
p[i]=0;
12.
break;
13.
case ):
14.
j=j-10;
15.
p[i]=0;
16.
break;
17.
case +:
18.
case -:
19.
p[i]=1+j;
20.
break;
21.
case *:
22.
case /:
23.
p[i]=10+j;
24.
break;
25.
default:
26.
p[i]=1000;
27. }
28. }
29. j=0;
30. for(i=0;i<a.length();i++)
31. {
32. if(p[i]!=0)
33. {
34.
eps=eps+a.charAt(i);
35.
epf[j]=p[i];
36.
j++;
37. }
38. }
39.}
Figura 9.5 Metoda ce transform
a expresia dat
a ntr-o expresie f
ar
a paranteze
In sirul p declarat n linia 3 vom trece valori ce corespund prioritatilor membrilor expresiei.
Calculul prioritatilor se face astfel:
prioritatea initiala a operatorilor + si - este 1;
prioritatea initiala a operatorilor * si / este 10;
la prioritatea unui operator se aduna 10 pentru fiecare pereche de paranteze ntre
care se gaseste;
prioritatea unui operand este 1000;

167

9.7. PROBLEME PROPUSE

prioritatea parantezelor este 0.


In liniile 29-38 se creaza noua expresie fara paranteze n variabila de tip referinta eps prin
testarea valorilor lui p. In paralel se va crea si referinta epf ce va contine toate valorile din
p diferite de 0.
1. public String polonez(int li,int ls)
2. {
3. String s="";
4. int i=li,j,min;
5. for(min=epf[li], j=li; j<=ls;j++)
6. {
7.
if(min>epf[j])
8.
{
9.
min=epf[j];
10.
i=j;
11. }
12. }
13. if(li==ls)
14. {
15. s=s+eps.charAt(i);
16. return s;
17. }
18. else
19. return polonez(li,i-1)+polonez(i+1,ls)+eps.charAt(i);
20.}
Figura 9.6 Metoda ce transform
a expresia f
ar
a paranteze ntr-o form
a polonez
a
Linia 12 a functiei testeaza daca nu s-a ajuns la o expresie care are un singur caracter.
In caz afirmativ, valoarea functiei este chiar caracterul (operandul) respectiv. Altfel, asa
cum spuneam la descrierea problemei, vom mparti expresia n doua subexpresii legate prin
operatorul de prioritate minima. Acesta va fi gasit n liniile 5-12 prin testarea valorilor lui
epf.

9.7

Probleme propuse

1. Reprezentati evolutia stivei pentru procedurile si functiile recursive din acest capitol.
2. Calculati de cate ori se recalculeaza valoarea Fk n cazul calcului recursiv al valorii
Fn a sirului lui Fibonacci.
3. Sa se calculeze recursiv si iterativ cel mai mare divizor comun a doua numere dup
a
formulele (Euclid):
(

cmmdc(a, b) =

cmmdc(b, a mod b) pentru a mod b 6= 0


b
altfel

168

CAPITOLUL 9. DIVIDE ET IMPERA

cmmdc(a, b) =

cmmdc(b, |a b|) pentru a 6= b


b
altfel

4. Sa se calculeze recursiv si iterativ functia lui Ackermann, data de formula:

Ack(m, n) =

n+1

pentru m = 0
Ack(m 1, 1)
pentru n = 0

Ack(m 1, Ack(m, n 1)) altfel

5. Sa se calculeze combinarile dupa formula de recurenta din triunghiul lui Pascal:


k1
k
Cnk = Cn1
+ Cn1
.

Calculati apoi combinarile dupa formula clasica:


Cnk =

n!
k!(nk)! .

Ce constati? Cum explicati ceea ce ati constat? (daca ati constat...)


6. Sa se calculeze recursiv si iterativ functia Manna-Pnueli, data de formula:
(

F (x) =

x1
pentru x 12
F (F (x + 2)) altfel

7. Sa se scrie o functie care calculeaza recursiv suma cifrelor unui numar dupa formula:
(

S(n) =

n mod 10 + S(n div 10) pentru n > 0


0
pentru n = 0

8. Se considera doua siruri definite recurent dupa formulele:


an =

an1 +bn1
2

si bn =

an1 bn1 , cu a0 = a si b0 = b.

Sa se scrie un program recursiv care calculeaza aceste siruri.


9. (Partitiile unui num
ar) Un numar natural n se poate descompune ca suma descrescatoare de numere naturale. De exemplu, pentru numarul 4 avem descompunerile 2+1+1 sau 3+1. Prin P (n, k) se noteaza numarul de posibilitati de a-l
descompune pe n ca suma (descrescatoare) de k numere. De exemplu, P (4, 2) =
2 (4 = 3 + 1, 4 = 2 + 2). Numerele P (n, k) verifica relatia de recurenta:
P (n + k, k) = P (n, 1) + P (n, 2) + . . . + P (n, k) cu P (n, 1) = P (n, n) = 1.
Sa se calculeze numarul total de descompuneri ale numarului n.

169

9.7. PROBLEME PROPUSE

10. S
a se scrie o functie care calculeaza maximul elementelor unui vector utilizand
tehnica Divide et Impera.
Indicatie: Se mparte vectorul n dou
a jum
at
ati egale, se calculeaz
a recursiv maximul
celor dou
a jum
at
ati si se alege num
arul mai mare.
11. (Turnurile din Hanoi) Se dau trei tije simbolizate prin literele A, B si C. Pe tija
A se afla n discuri de diametre diferite asezate descrescator n ordinea diametrelor,
cu diametrul maxim la baza. Se cere sa se mute discurile pe tija B respectand
urmatoarele reguli:
(a) la fiecare pas se muta un singur disc
(b) nu este permisa asezarea unui disc cu diametru mai mare peste un disc cu
diametrul mai mic
Indicatie: Formularea recursiv
a a solutiei este: se mut
a primele n-1 discuri de pe
tija A pe tija C folosind ca tij
a intermediar
a tija B; se mut
a discul r
amas pe A pe
tija B; se mut
a discurile de pe tija C pe tija B folosind ca tij
a intermediar
a tija A.
Parcurgerea celor trei etape permite definirea recursiv
a a sirului H(n,a,b,c) astfel:
(

H(n, a, b, c) =

ab
dac
a n=1
H(n 1, a, c, b), ab, H(n 1, c, b, a) dac
a n>1

Exemplu: Pentru n = 2 avem:


H(2, a, b, c) = H(1, a, c, b), ab, H(1, c, b, a) = ac, ab, cb
12. Scrieti un program n care calculatorul sa ghiceasca ct se poate de repede un num
ar
natural la care v-ati gandit. Numarul este cuprins ntre 1 si 32.000. Atunci cand
calculatorul propune un numar i se va raspunde prin 1, daca numarul este prea mare,
2 daca numarul este prea mic si 0 daca numarul a fost ghicit.
Indicatie: Problema foloseste metoda c
autarii binare prezentat
a n acest capitol.
13. (Problema t
aieturilor) Se da o bucata dreptunghiulara de tabla de dimensiune l h,
avand pe suprafata ei n gauri de coordonate numere ntregi (coltul din stanga jos al
tablei este considerat centrul sistemului de coordonate). Sa se determine care este
bucata de arie maxima fara gauri care poate fi decupata din suprafata original
a.
Sunt permise doar taieturi orizontale sau verticale.
Indicatie: Se caut
a n bucata curent
a prima gaur
a. Dac
a o astfel de gaur
a exist
a,
atunci problema se mparte n alte patru subprobleme de acelasi tip. Dac
a suprafata
nu are nici o gaur
a, atunci se compar
a suprafata ei cu suprafetele f
ar
a gaur
a obtinute
p
an
a la acel moment. Dac
a suprafata este mai mare, atunci se retin coordonatele
ei.
Coordonatele g
aurilor sunt date n doi vectori xv si yv. Coordonatele dreptunghiurilor

170

CAPITOLUL 9. DIVIDE ET IMPERA

ce apar pe parcursul problemei sunt retinute prin coltul st


anga jos (x,y), lungime si
l
atime (l,h).
Pentru a se afla n interioul unui dreptunghi o gaur
a trebuie s
a indeplineasc
a simultan conditiile:
(a) xv(i) > x;
(b) xv(i) < x + l;
(c) yv(i) > y;
(d) yv(i) < y + h.
T
aietura vertical
a prin aceast
a gaur
a determin
a dou
a dreptunghiuri:
(a) x,y,xv(i)-x,h;
(b) xv(i),y,l+x-xv(i),h.
T
aietura orizontal
a prin aceast
a gaur
a determin
a alte dou
a dreptunghiuri:
(a) x,y,l,yv(i)-y;
(b) x,yv(i),l,h+y-yv(i).
Clasa Taieturi este prezentat
a n continuare:
public class Taieturi
{
int[] xv,yv;
public T(int[] a, int[] b)
{
xv=new int[a.length];
xv=a;
yv=new int[a.length];
yv=b;
}
public void taietura_max(int x, int y, int l, int h, int[] max)
{
int i=0;
boolean gasit=false;
while ((i<=xv.length-1)&&(!gasit))
{
if((xv[i]>x)&&(xv[i]<x+l)&&(yv[i]>y)&&(yv[i]<y+h))
gasit=true;
else
i++;
}
if (gasit)
{
taietura_max(x,y,xv[i]-x,h,max);

9.7. PROBLEME PROPUSE

taietura_max(xv[i],y,l+x-xv[i],h,max);
taietura_max(x,y,l,yv[i]-y,max);
taietura_max(x,yv[i],l,h+y-yv[i],max);
}
else
{
if((l*h)>(max[0]*max[1]))
{
max[2]=x;
max[3]=y;
max[0]=l;
max[1]=h;
}
}
}
}

171

Capitolul 10

Algoritmi Greedy
Algoritmii aplicati problemelor de optimizare sunt, n general, compusi dintr-o secventa
de pasi, la fiecare pas existand mai multe alegeri posibile. Un algoritm Greedy va alege
la fiecare moment de timp solutia care pare a fi cea mai buna. Deci este vorba despre o
alegere optim
a, f
acut
a local, cu speranta ca ea va conduce la un optim global. Acest capitol trateaza probleme de optimizare care pot fi rezolvate cu ajutorul algoritmilor Greedy.
Algoritmii Greedy conduc n multe cazuri la solutii optime, dar nu ntotdeauna... In
sectiunea 10.1 vom prezenta mai ntai o problema simpla dar netriviala, problema selectarii activitatilor, a carei solutie poate fi calculata n mod eficient cu ajutorul unei
metode de tip Greedy. In sectiunea 10.2 se recapituleaza cateva elemente de baza ale
metodei Greedy. Urmeaza apoi prezentarea catorva probleme specifice.
Metoda Greedy este destul de puternic
a si se aplica cu succes unui spectru larg de probleme. Cursurile de teoria grafurilor contin mai multi algoritmi care pot fi priviti ca aplicatii
ale metodei Greedy, cum ar fi algoritmii de determinare a arborelui partial de cost minim
(Kruskal, Prim) sau algoritmul lui Dijkstra pentru determinarea celor mai scurte drumuri
pornind dintr-un varf.

10.1

Problema spectacolelor (selectarea activit


atilor)

Primul exemplu pe care l vom considera este o problema de repartizare a unei resurse (o
sala de spectacol) mai multor activitati care concureaza pentru a obtine resursa respectiva
(diferite spectacole care vor sa ruleze n sala respectiva). Vom vedea ca un algoritm de tip
Greedy reprezinta o metoda simpla si eleganta pentru programarea unui numar maxim de
spectacole care nu se suprapun (numite activitati compatibile reciproc).
S
a presupunem c
a dispunem de o multime S = 1,2,...,n de n activit
ati (spectacole) care
doresc s
a foloseasc
a o aceeasi resurs
a (sala de spectacole). Aceast
a resurs
a poate fi folosit
a
de o singur
a activitate la un anumit moment de timp. Fiecare activitate i are un timp de
start si si un timp de terminare ti , unde si ti . Dac
a este selectat
a activitatea i, ea se
desf
asoar
a pe durata intervalului [si , ti ) . Dou
a activit
ati sunt compatibile dac
a duratele
lor de desf
asurare sunt disjuncte. Problema spectacolelor (select
arii activit
atilor) const
a
din selectarea unei multimi maximale de activit
ati compatibile ntre ele.

172

ILOR)
10.1. PROBLEMA SPECTACOLELOR (SELECTAREA ACTIVITAT

173

Un algoritm Greedy pentru aceasta problema este descris de urmatoarea functie, prezentata n pseudocod. Vom presupune ca spectacolele (adica datele de intrare) sunt ordonate
crescator dupa timpul de terminare:
t1 t2 , . . . , tn .
In cazul n care activitatile nu sunt ordonate astfel, ordonarea poate fi facuta n timpul
O(nlgn) (folosind Mergesort sau Quicksort). Algoritmul de mai jos presupune ca datele
de intrare s si t sunt reprezentate ca vectori.
functie SELECT-SPECTACOLE-GREEDY(s, t)
A {1} //A este multimea spectacolelor care sunt selectate
j 1 //j este indicele ultimului spectacol selectat
pentru i = 2, n
dac
a si tj atunci //spectacolul i ncepe dup
a ce j s-a terminat
A A {i} //se adaug
a i la spect. selectate
j i //ultimul spectacol este acum i
return A
In multimea A se introduc activitatile selectate. Variabila j identifica ultima activitate
introdusa n A. Deoarece activitatile sunt considerate n ordinea crescatoare a timpilor lor
de terminare, tj va reprezenta ntotdeauna timpul maxim de terminare a oricarei activitati
din A. Aceasta nseamna ca:
tj = max{tk |k A}
In liniile 2-3 din algoritm se selecteaza activitatea 1 (activitatea 1 trebuie planificata prima,
deoarece se termina cel mai repede), se initializeaza A astfel ncat sa nu contina dec
at

aceast
a activitate, iar variabila j ia ca valoare aceasta activitate. In continuare n ciclul
pentru se considera pe rand fiecare activitate i se adauga multimii A daca este compatibil
a
cu celelalte activitati deja selectate. Pentru a vedea daca activitatea i este compatibil
a
cu toate celelalte activitati existente la momentul curent n A, este suficient ca momentul
de start si sa nu fie mai devreme decat momentul de terminare tj al activitatii cel mai
recent adaugate multimii A. Daca activitatea i este compatibila, atunci ea este adaugat
a
multimii A, iar variabila j este actualizata. Procedura SELECT-SPECTACOLE-GREEDY
este foarte eficienta. Ea poate planifica o multime S de n activitati n O(n), presupunand
ca activitatile au fost deja ordonate dupa timpul lor de terminare. Activitatea aleas
a
de procedura SELECT-SPECTACOLE-GREEDY este ntotdeauna cea cu primul timp
de terminare care poate fi planificata legal. Activitatea astfel selectata este o alegere
Greedy (lacom
a) n sensul ca, intuitiv, ea lasa posibilitatea celorlalte activitati ramase
pentru a fi planificate. Cu alte cuvinte, alegerea Greedy maximizeaza cantitatea de timp
neplanificata ramasa.

10.1.1

Demonstrarea corectitudinii algoritmului

Pana la acest moment, noi nu ne-am pus problema de a demonstra corectitudinea algoritmilor prezentati. Totusi, trebuie sa mentionam ca exista o ntreaga ramura a algoritmicii

174

CAPITOLUL 10. ALGORITMI GREEDY

care se ocupa exclusiv de demonstrarea corectitudinii algoritmilor. In general, demonstrarea corectitudinii unui algoritm nu este deloc simpla, de aceea am evitat sa prezentam
demonstratiile n acest curs introductiv. Am ales sa demonstram corectitudinea acestui
algoritm, deoarece pe de o parte ea este ilustrativa pentru o ntreaga clasa de probleme,
iar pe de alta parte, demonstratia nu este dificila.
Teorema 10.1 Algoritmul SELECT-SPECTACOLE-GREEDY furnizeaz
a solutia optim
a
(num
ar maxim de spectacole) pentru problema select
arii activit
atilor.
Demonstratie: Fie S = {1, 2, . . . , n} multimea activitatilor care trebuie planificate, ordonate crescator dupa timpul de terminare. In consecinta, activitatea 1 se termina cel mai
devreme. Vom arata ca exista o solutie optima care ncepe cu activitatea 1.
Sa presupunem ca avem o solutie A S optima pentru o instanta a problemei. Pentru
simplitate, presupunem ca activitatile din A sunt ordonate dupa timpul de terminare.
Daca primul spectacol din A este chiar 1, atunci demonstratia este ncheiata. Daca primul
spectacol din A nu este 1, atunci nlocuim primul spectacol cu spectacolul 1, obtinand
evident o solutie corecta, deoarece spectacolul 1 se va termina mai devreme decat primul
spectacol din A. Am aratat astfel ca exista o solutie optima pentru S care ncepe cu activitatea 1.
Mai mult, odata ce este facuta alegerea activitatii 1, problema se reduce la determinarea
solutiei optime pentru activitatile din S care sunt compatibile cu activitatea 1. Fie
S 0 = {i S|si t1 } multimea activitatilor care ncep dupa ce 1 se termina. Rezulta
ca daca A este o solutie optima pentru S, atunci A0 = A {1} este o solutie optima pentru
S. Daca nu ar fi asa, atunci ar exista o solutie optima B pentru S care sa aiba mai multe
activitati decat A. Adaugand activitatea 1 la B, vom obtine o solutie pentru S cu mai
multe activitati decat solutia A, ceea ce este absurd.
Astfel, prin inductie dupa numarul de alegeri facute se poate arata ca alegand primul
spectacol compatibil la fiecare pas, se obtine o solutie optima.

10.2

Elemente ale strategiei Greedy

Un algoritm Greedy determina o solutie optima a unei probleme n urma unei succesiuni
de alegeri. La fiecare moment de decizie din algoritm este aleasa optiunea care pare a
fi cea mai potrivita. Aceasta strategie euristic
a nu produce ntotdeauna solutia optima,
dar exista si cazuri cand aceasta este obtinuta, cum ar fi n cazul problemei selectarii activitatilor. In acest paragraf vom prezenta cateva proprietati generale ale metodei Greedy.
Cum se poate decide daca un algoritm Greedy poate rezolva o problema particulara de
optimizare? In general nu exista o modalitate de a stabili acest lucru, dar exista anumite
caracteristici pe care le au majoritatea problemelor care se rezolva prin tehnici Greedy:
proprietatea de alegere Greedy si substructura optim
a.
In cazul general o problema de tip Greedy, are urmatoarele componente:
o multime de candidati (lucrari de planificat, varfuri ale grafului, etc);
o functie care verifica daca o anumita multime de candidati constituie o solutie
posibil
a (nu neap
arat optim
a) a problemei;

10.2. ELEMENTE ALE STRATEGIEI GREEDY

175

o functie care verifica daca o multime de candidati este fezabil


a, adica daca este
posibil sa completam aceasta multime astfel ncat sa obtinem o solutie posibila (nu
neaparat optima) a problemei (verifica daca planificarea este formata din activitati
care nu se suprapun etc.);
o functie de selectie care indica la orice moment care este cel mai promitator dintre
candidatii nca nefolositi (se alege spectacolul compatibil care se termina cel mai
repede);
o functie obiectiv care da valoarea unei solutii (numarul de lucrari planificate, timpul necesar executarii tuturor lucrarilor ntr-o anumita ordine, lungimea drumului
pe care l-am gasit, etc.); aceasta este functia pe care urmarim sa o optimizam (minimizam/maximizam).
Pentru a rezolva o problema de optimizare cu Greedy, cautam o solutie posibila care s
a
optimizeze valoarea functiei obiectiv. Un algoritm Greedy construieste solutia pas cu pas
. Initial, multimea candidatilor selectati este vida. La fiecare pas, ncercam sa adaugam
acestei multimi cel mai promitator candidat, conform functiei de selectie. Daca, dup
a
o astfel de adaugare, multimea de candidati selectati nu mai este fezabila, eliminam ultimul candidat adaugat; acesta nu va mai fi niciodata considerat. Daca, dupa adaugare,
multimea de candidati selectati este fezabila, ultimul candidat adaugat va ramane de acum
ncolo n ea. De fiecare data cand largim multimea candidatilor selectati, verificam dac
a
aceast
a multime nu constituie o solutie posibila a problemei noastre. Daca algoritmul
Greedy functioneaza corect, prima solutie gasita va fi totodata o solutie optima a problemei. (Solutia optima nu este n mod necesar unica: se poate ca functia obiectiv sa aib
a
aceeasi valoare optima pentru mai multe solutii posibile.) Descrierea n pseudocod a unui
algoritm Greedy general este:
functie greedy( C ) // C este multimea candidatilor
S // S este multimea n care construim solutia
c
at timp not solutie(S) si C 6=
x un element din C care maximizeaz
a select(x)
C C {x}
dac
a fezabil(S {x}) atunci S S {x}
dac
a solutie(S) atunci
return S
altfel
return nu exist
a solutie
Este de nteles acum de ce un astfel de algoritm se numeste lacom (am putea sa-l numim
si nechibzuit). La fiecare pas, procedura alege cel mai bun candidat la momentul respectiv, fara sa-i pese de viitor si fara sa se razgandeasca. Daca un candidat este inclus n
solutie, el ramane acolo; daca un candidat este exclus din solutie, el nu va mai fi niciodat
a
reconsiderat. Asemenea unui ntreprinzator care urmareste castigul imediat n dauna celui
de perspectiva, un algoritm Greedy actioneaza simplist. Totusi, ca si n afaceri, o astfel
de metoda poate da rezultate foarte bune tocmai datorita simplitatii ei.
Functia de selectare este de obicei derivata din functia obiectiv; uneori aceste doua functii

176

CAPITOLUL 10. ALGORITMI GREEDY

sunt chiar identice.


Un exemplu simplu de algoritm Greedy este algoritmul folosit pentru rezolvarea urmatoarei
probleme.
S
a presupunem c
a dorim s
a d
am restul unui client, folosind un num
ar c
at mai mic de
monezi.
In acest caz, elementele problemei sunt:
Candidatii: multimea initiala de monezi de 1, 5 si 25 unitati, n care presupunem ca
din fiecare tip de moneda avem o cantitate nelimitata;
O solutie posibil
a: valoarea totala a unei astfel de multimi de monezi selectate trebuie
sa fie exact valoarea pe care trebuie sa o dam ca rest;
O multime fezabil
a: valoarea totala a unei astfel de multimi de monezi selectate nu
este mai mare decat valoarea pe care trebuie sa o dam ca rest;
Functia de selectie: se alege cea mai mare moneda din multimea de candidati ramasa;
Functia obiectiv: numarul de monezi folosite n solutie; se doreste minimizarea acestui numar.
Se poate demonstra ca algoritmul Greedy va gasi n acest caz mereu solutia optima - restul
cu un numar minim de monezi. Pe de alta parte, presupunand ca exista si monezi de 12
unitati sau c
a unele din tipurile de monezi lipsesc din multimea initiala de candidati, se
pot gasi contraexemple pentru care algoritmul nu gaseste solutia optima, sau nu gaseste
nici o solutie cu toate ca exista solutie.
Evident, solutia optima se poate gasi ncercand toate combinarile posibile de monezi (abordare backtracking). Acest mod de lucru necesita nsa foarte mult timp de calcul.
Un algoritm Greedy nu duce deci ntotdeauna la solutia optima sau la o solutie. Este doar
un principiu general, urmand ca pentru fiecare caz n parte sa determinam daca obtinem
sau nu solutia optima.

10.2.1

Proprietatea de alegere Greedy

Prima caracteristica a unei probleme de tip Greedy este aceea de a avea proprietatea
alegerii Greedy, adica se poate ajunge la o solutie optima global, realizand alegeri (Greedy)
optime local . Intr-un algoritm Greedy se realizeaza orice alegere care pare a fi cea mai
buna la momentul respectiv, iar subproblema rezultata este rezolvata dupa ce alegerea
este facuta. Alegerea realizata de un algoritm Greedy poate depinde de alegerile facute
pana n momentul respectiv, dar nu poate depinde de alegerile ulterioare sau de solutiile
subproblemelor.
Desigur, trebuie sa demonstram ca o alegere Greedy la fiecare pas conduce la o solutie
optima global, si aceasta este o problema mai delicata. De obicei, demonstratia examineaza
o solutie optima global. Apoi se arata ca solutia poate fi modificata astfel ncat la fiecare
pas este realizata o alegere Greedy, iar aceasta alegere reduce problema la una similara dar
de dimensiuni mai reduse. Se aplica apoi principiul inductiei matematice pentru a arata

177

10.3. MINIMIZAREA TIMPULUI MEDIU DE AS


TEPTARE

ca o alegere Greedy poate fi utilizata la fiecare pas. Faptul ca o alegere Greedy conduce
la o problema de dimensiuni mai mici reduce demonstratia corectitudinii la demonstrarea
faptului ca o solutie optima trebuie sa evidentieze o substructura optima.

10.2.2

Substructur
a optim
a

O problema evidentiaza o substructur


a optim
a daca o solutie optima a problemei contine
solutii optime ale subproblemelor. Aceasta proprietate este cheia pentru aplicarea programarii dinamice sau a unui algoritm Greedy. Ca exemplu al unei structuri optime, s
a
ne reamintim demonstratia corectitudinii algoritmului pentru problema selectarii spectacolelor, unde se arata ca daca o solutie optima A a problemei selectarii activitatilor ncepe
cu activitatea 1, atunci multimea activitatilor A0 = A{1} este o solutie optima pentru
problema selectarii activitatilor S 0 = {i S|si t1 }.

10.3

Minimizarea timpului mediu de asteptare

O singur
a statie de servire (procesor, pomp
a de benzin
a, etc) trebuie s
a satisfac
a cererile
a n clienti. Timpul de servire necesar fiec
arui client este cunoscut n prealabil: pentru
clientul i este necesar un timp ti , i = 1, n. Dorim s
a minimiz
am timpul total de asteptare:
T =

Pn

i=1 (timpul

de asteptare pentru clientul i)

Ceea ce este acelasi lucru cu a minimiza timpul mediu de asteptare, care este Tn .
De exemplu, daca avem trei clienti cu t1 = 5, t2 = 10, t3 = 3, sunt posibile sase ordini de
servire:
Ordine
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

T impul(T )
5 + (5 + 10) + (5 + 10 + 3)
5 + (5 + 3) + (5 + 3 + 10)
10 + (10 + 5) + (10 + 5 + 3)
10 + (10 + 3) + (10 + 3 + 5)
3 + (3 + 5) + (3 + 5 + 10) optim
3 + (3 + 10) + (3 + 10 + 5)

In primul caz, clientul 1 este servit primul, clientul 2 asteapta pana este servit clientul
1 si apoi este servit, clientul 3 asteapta pana sunt serviti clientii 1, 2 si apoi este servit.
Timpul total de asteptare a celor trei clienti este 38.
Algoritmul Greedy este foarte simplu - la fiecare pas se selecteaza clientul cu timpul minim
de servire din multimea de clienti ramasa. Vom demonstra ca acest algoritm este optim.
Fie I = (i1 i2 . . . in ) o permutare oarecare a ntregilor {1, 2, . . . , n}. Daca servirea are loc
n ordinea I, avem:
T (I) = ti1 + (ti1 + ti2 ) + (ti1 + ti2 + ti3 ) + . . . = nti1 + (n 1)ti2 + . . . =

Pn

Presupunem acum ca I este astfel ncat putem gasi doi ntregi a < b cu
tia > tib

k=1 (n

k + 1)tik

178

CAPITOLUL 10. ALGORITMI GREEDY

deci exista un client care necesita un timp mai lung de deservire, si care este servit nainte.
Interschimbam pe ia cu ib n I; cu alte cuvinte, clientul care a fost servit al b-lea va fi servit
acum al a-lea si invers. Obtinem o noua ordine de servire I, care este de preferat deoarece
T (I 0 ) = (n a + 1)tib + (n b + 1)tia +

Pn

k=1,k6=a,b (n

k + 1)tik

T (I) T (I 0 ) = (n a + 1)(tia tib ) + (n b + 1)(tib tia ) = (b a)(tia tib ) > 0


Aplicand succesiv pasul de mai sus se obtine o permutare optima J = (j1 , j2 , . . . , jn )
pentru care avem:
tj1 tj2 . . . tjn .
Prin metoda Greedy, selectand permanent clientul cu timpul cel mai mic de deservire,
obtinem deci ntotdeauna planificarea optima a clientilor.
Problema poate fi generalizata pentru un sistem cu mai multe statii de servire.
Scrierea algoritmului se reduce la o banala ordonare a clientilor crescator dupa timpul de
deservire si o lasam ca exercitiu.

10.4

Interclasarea optim
a a mai multor siruri ordonate

Sa presupunem ca avem doua siruri S1 si S2 , de lungime m si n, ordonate crescator


si ca dorim sa obtinem prin interclasarea lor sirul ordonat crescator care contine exact
elementele din cele doua siruri. Daca interclasarea are loc prin deplasarea elementelor din
cele doua siruri n noul sir rezultat, atunci numarul deplasarilor este m + n.
Generalizand, sa consideram acum n siruri S1 , S2 , . . . Sn , fiecare sir Si , i = 1, n, fiind format
din qi elemente ordonate crescator (vom numi qi lungimea lui Si ). Ne propunem sa obtinem
sirul S ordonat crescator, continand exact elementele din cele n siruri. Vom realiza acest
lucru prin interclasari succesive de cate doua siruri. Problema consta n determinarea
ordinii optime n care trebuie efectuate aceste interclasari, astfel ncat timpul total sa fie
cat mai mic. Exemplul de mai jos ne arata ca problema astfel formulata nu este banala,
adica nu este deloc indiferent n ce ordine se fac interclasarile.
Exemplul 10.1 Fie sirurile S1 , S2 , S3 de lungimi q1 = 30, q2 = 20, q3 = 10. Dac
a interclas
am pe S1 cu S2 , iar rezultatul l interclas
am cu S3 , num
arul total al deplas
arilor este
(30+20)+(50+10)=110. Dac
a interclas
am pe S3 cu S2 , iar rezultatul l interclas
am cu
S1 , num
arul total al deplas
arilor este (10+20)+(30+30)=90, deci cu 20 de operatii mai
putin.
Atasam fiecarei strategii de interclasare cate un arbore binar n care valoarea fiecarui varf
este data de lungimea sirului pe care l reprezinta. Daca sirurile S1 , S2 , . . . , S6 au lungimile
q1 = 30, q2 = 10, q3 = 20, q4 = 30, q5 = 50, q6 = 10 doua astfel de strategii de interclasare
sunt reprezentate prin arborii din Figura 10.1.

179

A MAI MULTOR S
10.4. INTERCLASAREA OPTIMA
IRURI ORDONATE

150

150
@
@

140

@
@

10

130

40
@

20
@

80
30

30
10

@
@

50

30

@
@

30

@
@

20

60

@
@

10

110

90

20

@
@

10

50

Figura 10.1 Reprezentarea strategiilor de interclasare


Observ
am ca fiecare arbore are 6 varfuri terminale, corespunzand celor 6 siruri initiale si
5 varfuri neterminale, corespunzand celor 5 interclasari care definesc strategia respectiva.
Numerotam varfurile n felul urmator: Varful terminal i, i = 1, 6, va corespunde sirului Si ,
iar varfurile neterminale se numeroteaza de la 7 la 11 n ordinea obtinerii interclasarilor
respective (Figura 10.2).
Strategia Greedy apare n Figura 10.1b si consta n a interclasa mereu cele mai scurte
doua siruri disponibile la momentul respectiv.
11

11
10

6
10

2
8

4
2

@
@

@
@

@
@

@
@

@
@
@

@
@

Figura 10.2 Numerotarea varfurilor arborilor din Figura 10.1


Pentru a interclasa sirurile S1 , S2 , . . . , Sn , de lungimi q1 , q2 , . . . , qn , obtinem pentru fiecare
strategie cate un arbore binar cu n varfuri terminale numerotate de la 1 la n si n 1
varfuri neterminale numerotate de la n + 1 la 2n 1. Definim pentru un arbore oarecare
A de acest tip lungimea externa ponderata:

180

CAPITOLUL 10. ALGORITMI GREEDY

L(A) =

Pn

i=1 ai qi

unde ai este adancimea varfului i. Se observa ca numarul total de deplasari de elemente


pentru strategia corespunzatoare lui A este chiar L(A). Solutia optima a problemei noastre
este atunci arborele (strategia) pentru care lungimea externa ponderata este minima.
Teorema 10.2 Prin metoda Greedy, n care se interclaseaz
a la fiecare pas cele dou
a siruri
de lungime minim
a, se obtine sirul s cu un num
ar minim de operatii.
Demonstratie: Demonstram prin inductie. Pentru n = 1, proprietatea este verificata.
Presupunem ca proprietatea este adevarata pentru n 1 siruri. Fie A arborele strategiei
Greedy de interclasare a n siruri de lungime q1 q2 . . . qn . Fie B un arbore cu
lungimea externa ponderata minima, corespunzator unei strategii optime de inerclasare a
celor n siruri. In arborele A apare subarborele:

q1 + q2
@
@
@

q1

@
@

q2

reprezentand prima interclasare facuta conform strategiei Greedy. In arborele B, fie un


varf neterminal de adancime maxima. Cei doi fii ai acestui varf sunt atunci doua varfuri
terminale qj si qk . Fie B arborele obtinut din B schimband ntre ele varfurile q1 si qj ,
respectiv q2 si qk . Evident, L(B 0 ) L(B). Deoarece B are lungimea externa ponderata
minima, rezulta ca L(B) = L(B 0 ). Eliminand din B varfurile q1 si q2 , obtinem un arbore B cu n 1 varfuri terminale q1 + q2 , q3 , . . . , qn . Arborele B 0 are lungimea externa
ponderata minima si L(B 0 ) = L(B) + (q1 + q2 ). Rezulta ca si B are lungimea externa
ponderata minima. Atunci, conform ipotezei inductiei, avem L(B) = L(A0 ) unde A este
arborele strategiei Greedy de interclasare a sirurilor de lungime q1 + q2 , q3 , . . . , qn . Cum A
se obtine din A atasand la varful q1 + q2 fiii q1 si q2 , iar B se obtine n acelasi mod din
B, rezulta c
a L(A) = L(B 0 ) = L(B) . Proprietatea este deci adevarata pentru orice n.
Implementarea eficienta a acestui algoritm presupune utilizarea unei structuri de date numita Heap.
Se poate implementa algoritmul utilizand urmatorul principiu:

se parcurge sirul de lungimi si se aleg doua siruri de lungime minima


se deplaseaza cele doua lungimi la sfarsitul sirului
se nlocuiesc ultimele doua elemente din sir, cu un element reprezentand suma lor
se descreste lungimea sirului cu o unitate
Scrierea efectiva a algoritmului o propunem ca exercitiu.

10.5. PROBLEME PROPUSE

10.5

181

Probleme propuse

1. (Problema rucsacului) Avem la dispozitie un rucsac de capacitate M si n obiecte


diferite (cate unul din fiecare) cu costurile ci si greutatea gi . Scrieti un algoritm
care aseaza aceste obiecte n rucsac astfel ncat costul total sa fie maxim. Suma
greutatilor obiectelor din rucsac nu poate depasi capacitatea rucsacului. Daca un
obiect nu ncape n rucsac, se poate lua doar o parte (fractiune) din el.
Indicatie: Este usor de intuit c
a pentru obtinerea unui profit (cost) total maxim
trebuie c
arate obiecte de greutate mic
a si cost mare. Demonstratia riguroas
a a
afirmatiei anterioare v-o propunem spre rezolvare.
Asadar, va trebui, initial, s
a ordon
am descresc
ator obiectele dup
a raportul gcii . Apoi,
vom folosi urm
atorul algoritm:
functie RUCSAC(c,g,M,x,n)
pentru i=1,n execut
a
x(i)0
restM
i1
c
at timp i n si rest > 0 execut
a
dac
a g(i)>rest atunci
x(i)rest/g(i)
altfel
x(i)1
restrest-x(i)w(i)
return x

2. (Problema discret
a a rucsacului) Acelasi enunt ca la problema precedenta, cu diferenta
ca dintr-un obiect nu se poate pune o fractiune (un obiect fie se pune ntreg, fie nu
se pune). Aratati ca algoritmul Greedy de la problema precedenta nu furnizeaz
a
ntotdeauna solutie optima n acest caz. Gasiti un algoritm care furnizeaza ntotdeauna
solutie optima! Ce complexitate are acest algoritm?
Indicatie: Aceast
a problem
a are solutia exact
a determinat
a doar printr-un algoritm
de tip backtracking. Exemplu n sprijinul celor afirmate anterior: fie c=(5,3,3) si
g=(3,2,2) iar M=4. Prin aplicarea algoritmului de la problema precedent
a am selecta
doar obiectul 1 (obiectul 2 nu ar nc
apea ntreg n rucsac deci nu ar putea fi selectat)
si am obtine costul 5 n timp ce dac
a am selecta obiectele 2 si 3 am obtine costul 6.
3. Scrieti algoritmul pentru problema interclasarii optimale din paragraful 10.4.
4. Gasiti o solutie Greedy pentru problema comis-voiajorului propusa la capitolul 8.
Este aceasta solutie optima?

182

CAPITOLUL 10. ALGORITMI GREEDY

Indicatie: Conform strategiei greedy, vom construi ciclul pas cu pas, ad


aug
and la
fiecare iteratie cea mai scurt
a muchie disponibil
a cu urm
atoarele propriet
ati:
nu formeaz
a un ciclu cu muchiile deja selectate (except
and pentru ultima muchie
aleas
a, care completeaz
a ciclul)
nu exist
a nc
a dou
a muchii deja selectate, astfel nc
at cele trei muchii s
a fie
incidente n acelasi v
arf.
Aceast
a solutie nu este optim
a iar un exemplu v
a propunem s
a-l g
asiti.

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