0% au considerat acest document util (0 voturi)
11 vizualizări

Curs_Java

Documentul este un curs despre algoritmică și programare în Java, publicat în Brașov în 2002. Acesta include informații despre instalarea mediului Java, concepte fundamentale de programare, referințe, gestionarea excepțiilor și programarea orientată pe obiecte. Structura documentului este organizată în capitole și subcapitole, fiecare abordând diferite aspecte ale programării în Java.

Încărcat de

ion marian
Drepturi de autor
© © All Rights Reserved
Respectăm cu strictețe drepturile privind conținutul. Dacă suspectați că acesta este conținutul dumneavoastră, reclamați-l aici.
Formate disponibile
Descărcați ca PDF, TXT sau citiți online pe Scribd
0% au considerat acest document util (0 voturi)
11 vizualizări

Curs_Java

Documentul este un curs despre algoritmică și programare în Java, publicat în Brașov în 2002. Acesta include informații despre instalarea mediului Java, concepte fundamentale de programare, referințe, gestionarea excepțiilor și programarea orientată pe obiecte. Structura documentului este organizată în capitole și subcapitole, fiecare abordând diferite aspecte ale programării în Java.

Încărcat de

ion marian
Drepturi de autor
© © All Rights Reserved
Respectăm cu strictețe drepturile privind conținutul. Dacă suspectați că acesta este conținutul dumneavoastră, reclamați-l aici.
Formate disponibile
Descărcați ca PDF, TXT sau citiți online pe Scribd
Sunteți pe pagina 1/ 182

Daniel DANCIU Silviu DUMITRESCU

ALGORITMICĂ ŞI PROGRAMARE


Curs şi probleme de seminar

JAVA

Braşov 2002
Cuprins

1 Instalarea mediului Java 6


1.1 Obţinerea mediului Java pentru platforma dumneavoastră . . . . . . . . . . 6
1.1.1 Medii de dezvoltare integrată . . . . . . . . . . . . . . . . . . . . . . 6
1.2 Instalarea mediului Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.2.1 Instrucţiuni de instalare pentru Windows . . . . . . . . . . . . . . . 7
1.2.2 Instrucţiuni de instalare pentru Linux/Unix . . . . . . . . . . . . . . 7

2 Noţiuni fundamentale de programare in Java 9


2.1 Mediul de lucru Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.2 Primul program Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.2.1 Comentarii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.2.2 Funcţia main . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.2.3 Scrierea pe ecran . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.3 Tipuri de date primitive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.3.1 Tipurile primitive . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.3.2 Constante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.3.3 Declararea şi iniţializarea tipurilor primitive ı̂n Java . . . . . . . . . 12
2.3.4 Citire/scriere de la terminal . . . . . . . . . . . . . . . . . . . . . . . 12
2.4 Operatori de bază . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.4.1 Operatori de atribuire . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.4.2 Operatori aritmetici binari . . . . . . . . . . . . . . . . . . . . . . . 14
2.4.3 Operatori aritmetici unari . . . . . . . . . . . . . . . . . . . . . . . . 14
2.4.4 Conversii de tip . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.5 Instrucţiuni condiţionale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.5.1 Operatori relaţionali . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.5.2 Operatori logici . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.5.3 Instrucţiunea if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.5.4 Instrucţiunea while . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.5.5 Instrucţiunea for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.5.6 Instrucţiunea do . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.5.7 Instrucţiunile break şi continue . . . . . . . . . . . . . . . . . . . . . 20
2.5.8 Instrucţiunea switch . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.5.9 Operatorul condiţional . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.6 Metode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.6.1 Supraı̂ncărcarea numelor la metode . . . . . . . . . . . . . . . . . . . 23

2
CUPRINS 3

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

3 Referinţe 25
3.1 Ce este o referinţă . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.2 Fundamente despre obiecte şi referinţe . . . . . . . . . . . . . . . . . . . . . 27
3.2.1 Operatorul punct (.) . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.2.2 Declararea obiectelor . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.2.3 Colectarea de gunoaie (garbage collection) . . . . . . . . . . . . . . . 28
3.2.4 Semnificaţia lui = . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
3.2.5 Transmiterea de parametri . . . . . . . . . . . . . . . . . . . . . . . 29
3.2.6 Semnificaţia lui == . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
3.2.7 Supraı̂ncărcarea operatorilor pentru obiecte . . . . . . . . . . . . . . 30
3.3 Şiruri de caractere (stringuri) . . . . . . . . . . . . . . . . . . . . . . . . . . 30
3.3.1 Fundamentele utilizării stringurilor . . . . . . . . . . . . . . . . . . . 30
3.3.2 Concatenarea stringurilor . . . . . . . . . . . . . . . . . . . . . . . . 31
3.3.3 Comparaţia stringurilor . . . . . . . . . . . . . . . . . . . . . . . . . 32
3.3.4 Alte metode pentru stringuri . . . . . . . . . . . . . . . . . . . . . . 32
3.3.5 Conversia de la string la tipurile primitive . . . . . . . . . . . . . . . 32
3.4 Şiruri . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.4.1 Declaraţie, Atribuire şi Metode . . . . . . . . . . . . . . . . . . . . . 33
3.4.2 Expansiunea dinamică a şirurilor . . . . . . . . . . . . . . . . . . . . 35
3.4.3 Şiruri cu mai multe dimensiuni . . . . . . . . . . . . . . . . . . . . . 38
3.4.4 Argumente ı̂n linie de comandă . . . . . . . . . . . . . . . . . . . . 38
3.5 Tratarea excepţiilor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
3.5.1 Procesarea excepţiilor . . . . . . . . . . . . . . . . . . . . . . . . . . 39
3.5.2 Excepţii uzuale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
3.6 Intrare şi ieşire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
3.6.1 Operaţii de bază pe fluxuri (stream-uri) . . . . . . . . . . . . . . . . 42
3.6.2 Obiectul StringTokenizer . . . . . . . . . . . . . . . . . . . . . . . . 43
3.6.3 Fişiere secvenţiale . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
3.7 Probleme propuse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46

4 Obiecte şi clase 47


4.1 Ce este programarea orientată pe obiecte? . . . . . . . . . . . . . . . . . . . 47
4.2 Un exemplu simplu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
4.3 Metode uzuale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
4.3.1 Constructori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
4.3.2 Modificatori şi Accesori . . . . . . . . . . . . . . . . . . . . . . . . . 52
4.3.3 Afişare şi toString . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
4.3.4 Metoda equals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
4.3.5 Variabile şi metode statice . . . . . . . . . . . . . . . . . . . . . . . . 53
4.3.6 Metoda main . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
4.4 Pachete . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
4.4.1 Directiva import . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
4.4.2 Instrucţiunea package . . . . . . . . . . . . . . . . . . . . . . . . . . 55
4.4.3 Variabila sistem CLASSPATH . . . . . . . . . . . . . . . . . . . . . 55
4 CUPRINS

4.4.4 Reguli de vizibilitate Package-Friendly . . . . . . . . . . . . . . . . . 56


4.4.5 Compilarea separată . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
4.5 Alte operaţii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
4.5.1 Referinţa this . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
4.5.2 Prescurtarea this pentru constructori . . . . . . . . . . . . . . . . . . 57
4.5.3 Operatorul instanceof . . . . . . . . . . . . . . . . . . . . . . . . . . 58
4.5.4 Atribute statice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
4.5.5 Iniţializatori statici . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
4.6 Probleme propuse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59

5 Moştenire 68
5.1 Ce este moştenirea? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
5.2 Sintaxa de bază Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
5.2.1 Reguli de vizibilitate . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
5.2.2 Constructor şi super . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
5.2.3 Metode şi clase final . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
5.2.4 Redefinirea unei metode . . . . . . . . . . . . . . . . . . . . . . . . . 73
5.2.5 Metode şi clase abstracte . . . . . . . . . . . . . . . . . . . . . . . . 74
5.3 Exemplu: Extinderea clasei Shape . . . . . . . . . . . . . . . . . . . . . . . 76
5.4 Moştenire multiplă . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
5.5 Interfeţe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
5.5.1 Definirea unei interfeţe . . . . . . . . . . . . . . . . . . . . . . . . . . 82
5.5.2 Implementarea unei interfeţe . . . . . . . . . . . . . . . . . . . . . . 82
5.5.3 Interfeţe multiple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
5.6 Implementarea de componente generice . . . . . . . . . . . . . . . . . . . . 84
5.7 Anexă - clasa Reader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
5.8 Probleme propuse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88

6 Analiza eficienţei algoritmilor 90


6.1 Ce este analiza algoritmilor? . . . . . . . . . . . . . . . . . . . . . . . . . . 90
6.2 Notaţia asimptotică . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
6.2.1 O notaţie pentru ordinul de mărime al timpului de execuţie al unui
algoritm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
6.3 Tehnici de analiza algoritmilor . . . . . . . . . . . . . . . . . . . . . . . . . 94
6.3.1 Sortarea prin selecţie . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
6.3.2 Sortarea prin inserţie . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
6.3.3 Turnurile din Hanoi . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
6.4 Analiza algoritmilor recursivi . . . . . . . . . . . . . . . . . . . . . . . . . . 97
6.4.1 Metoda iteraţiei . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
6.4.2 Inducţia constructivă . . . . . . . . . . . . . . . . . . . . . . . . . . 97
6.4.3 Recurenţe liniare omogene . . . . . . . . . . . . . . . . . . . . . . . . 98
6.4.4 Recurenţe liniare neomogene . . . . . . . . . . . . . . . . . . . . . . 100
6.4.5 Schimbarea variabilei . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
6.5 Probleme propuse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
CUPRINS 5

7 Structuri de date 107


7.1 De ce avem nevoie de structuri de date? . . . . . . . . . . . . . . . . . . . . 107
7.2 Stive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
7.3 Cozi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
7.4 Liste ı̂nlănţuite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
7.5 Arbori binari de căutare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
7.6 Tabele de repartizare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
7.7 Cozi de prioritate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
7.8 Aplicaţie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124

8 Metoda Backtracking 129


8.1 Prezentare generală . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
8.2 Prezentarea metodei . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
8.2.1 Atribuie şi avansează . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
8.2.2 Încercare eşuată . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
8.2.3 Revenire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
8.2.4 Revenire după construirea unei soluţii . . . . . . . . . . . . . . . . . 134
8.3 Implementarea metodei backtracking . . . . . . . . . . . . . . . . . . . . . . 135
8.4 Probleme clasice care admit rezolvare prin metoda backtracking . . . . . . . 137
8.4.1 Problema generării permutărilor . . . . . . . . . . . . . . . . . . . . 137
8.4.2 Generarea aranjamentelor şi a combinărilor . . . . . . . . . . . . . . 138
8.4.3 Problema damelor . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
8.4.4 Problema colorării hărţilor . . . . . . . . . . . . . . . . . . . . . . . 141
8.5 Probleme propuse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143

9 Divide et Impera 153


9.1 Noţiuni elementare referitoare la recursivitate . . . . . . . . . . . . . . . . . 153
9.1.1 Funcţii recursive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
9.1.2 Recursivitatea nu ı̂nseamnă recurenţă . . . . . . . . . . . . . . . . . 157
9.2 Prezentarea metodei Divide et Impera . . . . . . . . . . . . . . . . . . . . . 159
9.3 Căutare binară . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
9.4 Sortarea prin interclasare (MergeSort) . . . . . . . . . . . . . . . . . . . . . 161
9.5 Sortarea rapidă (QuickSort) . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
9.6 Expresii aritmetice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
9.7 Probleme propuse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167

10 Algoritmi Greedy 172


10.1 Problema spectacolelor (selectarea activităţilor) . . . . . . . . . . . . . . . . 172
10.1.1 Demonstrarea corectitudinii algoritmului . . . . . . . . . . . . . . . . 173
10.2 Elemente ale strategiei Greedy . . . . . . . . . . . . . . . . . . . . . . . . . 174
10.2.1 Proprietatea de alegere Greedy . . . . . . . . . . . . . . . . . . . . . 176
10.2.2 Substructură optimă . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
10.3 Minimizarea timpului mediu de aşteptare . . . . . . . . . . . . . . . . . . . 177
10.4 Interclasarea optimă a mai multor şiruri ordonate . . . . . . . . . . . . . . 178
10.5 Probleme propuse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
Capitolul 1

Instalarea mediului Java

În acest capitol, vom da câteva posibilităţi de descărcare a softului, necesar pentru rularea
unor aplicaţii Java, de pe Internet precum şi instalarea acestuia.

1.1 Obţinerea mediului Java pentru platforma dumneav-


oastră
Mediul de bază Java constă dintr-un browser web unde puteţi vizualiza applet-urile Java,
un compilator Java ce transformă codul sursă Java ı̂n cod binar şi un interpretor pentru
rularea programelor Java. Aveţi 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 să luaţi toate părţile de la aceeaşi sursă. De obicei, browser-ul vostru
de web va fi Internet Explorer sau Netscape. Celelalte le puteţi obţine de la Sun’s Java
Developer Kit (JDK). Sun publică versiuni pentru toate platformele (Windows, Solaris,
X86 Linux).
JDK nu include un browser web dar conţine un applet viewer pentru testarea applet-urilor.
JDK include de asemenea compilatorul javac, interpretorul java, profiler-ul javaprof, gene-
ratotul fişierelor C de tip header (pentru integrarea metodelor scrise ı̂n C ı̂ntr-o clasă Java)
javah precum şi depanatorul Java şi generatorul de documentaţie. Mai multă documentaţie
despre JDK puteţi găsi ı̂n pagina de web a firmei Sun.
Sun furnizează maşina virtuală Java pentru Solaris, X86 Linux şi Wndows 95, 98, NT.
Pentru această lucrare aveţi nevoie de Java 2 Software Development Kit, versiunea 1.2
(JDK 1.2) sau următoarele. Versiunea JDK 1.3, utilizată şi ea destul de des, nu este
disponibilă pentru toate platformele. Totuşi, diferenţele ı̂ntre JDK1.2 şi JDK1.3 nu sunt
foarte importante.

1.1.1 Medii de dezvoltare integrată


Posibilităţile de dezvoltare integrată ale aplicaţiilor Java sunt ı̂ncă primitive ı̂n comparaţie
cu ceea ce este disponibil pentru C++. Se pare că cel puţin deocamdată mediile de
dezvoltare integrată (IDE-Integrated Development Environments) nu sunt foarte perfor-
mante. Acestea includ Metrowerks Code Warrior, Borland JBuilder, WinGate Visual Cafe

6
1.2. INSTALAREA MEDIULUI JAVA 7

sau Microsoft Visual J++.

1.2 Instalarea mediului Java


Pentru ı̂nceput trebuie să vă descărcaţi JDK-ul pe care urmează apoi să ı̂l instalaţi.

1.2.1 Instrucţiuni de instalare pentru Windows


Ştergeţi mai ı̂ntâi toate variantele de JDK pe care le aveţi deja instalate, mai ales dacă
doriţi să puneţi noul JDK ı̂ntr-un alt director. De asemenea, trebuie să folosiţi regedit
pentru a şterge toate cheile anterior instalate.
Aveţi nevoie de aproximativ 60 MB de spaţiu de memorie liber pentru instalarea JDK-
ului. Executaţi un dublu clic pe icoana din File Manager sau selectând Run... din meniul
Program Manager File editaţi calea către fişier. Aceasta va poduce dezarhivarea incluzând
toate directoarele şi subdirectoarele necesare. Vom presupune că instalarea s-a făcut ı̂n
C:\jdk.
Este necesar să adăugaţi 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 fişierul
autoexec.bat.
Pentru a vă asigura că mediul vostru Java este corect configurat, deschideţi o fereastră
DOS şi editaţi ”javac nofile.java”. Astfel:

C:\>javac nofile.java

Dacă primiţi răspunsul:

error: Can’t read: nofile.java

atunci instalarea a fost făcută cu succes. Dacă primiţi răspunsul:

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 corectă. Trebuie rezolvate aceste probleme ı̂nainte de a continua.

1.2.2 Instrucţiuni de instalare pentru Linux/Unix


Aveţi nevoie de aproximativ 60 MB de spaţiu de memorie liber pentru instalarea JDK-
ului dar dublu ar fi de mare ajutor. Modul de dezarhivare dintr-un fişier gzipped tar este
următorul:

% gunzip jk1_2_2-linux-i386.tar.gz
% tar xvf jdk1_2_2-linux-i386.tar
8 CAPITOLUL 1. INSTALAREA MEDIULUI JAVA

Numele exact al fişierului poate fi un pic modificat dacă folosiţi o platformă diferită ca şi
Irix sau o versiune diferită.
Puteţi face dezarhivarea ı̂n directorul curent sau dacă aveţi drepturi de root ı̂ntr-un alt
loc ca de exemplu /usr/local/java unde toţi utilizatorii pot avea acces la fişiere. Oricum
drepturile de root nu sunt necesare pentru a instala Java.
Dezarhivarea crează toate directoarele şi subdirectoarele necesare. Calea exactă nu este
importantă, dar pentru simplitate vom presupune ı̂n continuare că instalarea s-a făcut ı̂n
/usr/local/java. Veţi găsi fişierele ı̂n /usr/local/java/jdk1.2.2. Dacă dezarhivaţi o versiune
diferită, atunci fişierele vor fi ı̂ntr-o cale uşor modificată ca de exemplu /usr/local/java/jdk1.3.
Este posibil ca mai multe versiuni de JDK să coexiste armonios ı̂n acelaşi sistem. Dacă
dezarhivaţi altundeva decât /usr/local/java trebuie să ı̂nlocuiţi /usr/local/java cu calea
completă până la directorul java. Dacă instalaţi ı̂n directorul curent puteţi folosi ˜/java
ı̂n loc de calea completă.
Acum trebuie să adăugaţi 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

Puteţi să adăugaţi liniile anterioare la sfârşitul fişierelor .profile sau .cshrc pentru a nu le
mai scrie la fiecare login-are.
Pentru a vă asigura că mediul vostru Java este corect configurat, editaţi ”javac nofile.java”
la prompt-ul vostru shell:

% javac nofile.java

Dacă primiţi răspunsul:

error: Can’t read: nofile.java

atunci instalarea a fost făcută cu succes. Dacă primiţi răspunsul:

javac: Command not found

sau ceva similar atunci mediul Java nu a fost bine instalat sau variabila PATH nu are o
valoare corectă. Trebuie rezolvate aceste probleme ı̂nainte de a continua.
Capitolul 2

Noţiuni fundamentale de
programare in Java

2.1 Mediul de lucru Java


Codul sursă Java este conţinut ı̂n fişiere text care au extensia .java. Compilatorul local,
care este de obicei javac sau jikes1 , compilează programul şi generează fişiere .class care
conţin byte-code. Byte-code este un limbaj intermediar portabil care este interpretat de
către interpretorul de Java, numit java.

2.2 Primul program Java


Să ı̂ncepem prin a examina programul simplu din Figura 2.1. Acest program tipareşte
un scurt mesaj pe ecran. Numerele din stânga fiecarei linii nu fac parte din program. Ele
sunt furnizate doar pentru o mai uşoară referire a secvenţelor de cod.
Transpuneţi programul ı̂ntr-un fişier cu numele FirstProgram.java2 după care compilaţi-l
şi rulaţi-l. Java este case-sensitive, ceea ce ı̂nseamnă că face deosebirile ı̂ntre literele mari
şi mici.

1. //Primul program
2. public class FirstProgram
3. {
4. public static void main(String[] args)
5. {
6. System.out.println("Primul meu program Java") ;
7. }
8. }

Figura 2.1 Un prim program simplu


1
javac este compilatorul de la Sun, jikes este compilatorul de la IBM şi este preferat de mulţi program-
atori deoarece este mult mai rapid.
2
Atenţie la literele mari şi mici!

9
10 CAPITOLUL 2. NOŢIUNI FUNDAMENTALE DE PROGRAMARE IN JAVA

2.2.1 Comentarii
În Java există trei tipuri de comentarii. Prima formă, care este moştenită de la C ı̂ncepe
cu /* şi se termină cu */. Iată 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 formă de comentarii este moştenită de la limbajul C++ şi ı̂ncepe cu //.
Nu există simbol pentru ı̂ncheiere, deoarece un astfel de comentariu se extinde automat
până la sfârşitul liniei curente. Acest comentariu este folosit ı̂n linia 1 din Figura 2.1.
Cea de-a treia formă este asemănătoare cu prima doar că ı̂ncepe cu /** ı̂n loc de /*.
Acestă formă de comentariu este utilizată pentru a furniza informaţii utilitarului javadoc.
Comentariile au fost introduse pentru a face codul mai lizibil pentru programatori. Un
program bine comentat reprezintă un semn al unui bun programator.

2.2.2 Funcţia main


Un program Java constă dintr-o colecţie de clase care interacţionează ı̂ntre ele prin inter-
mediul metodelor. Echivalentul Java al unei proceduri sau funcţii din Pascal sau C este
metoda statică, pe care o vom descrie mai pe larg ı̂n acest capitol. Atunci când se execută
un program Java, va fi invocată automat metoda statică main. Linia 4 din Figura 2.1
arată că metoda main poate fi eventual invocată cu anumiţi parametri ı̂n linia de comandă.
Tipul parametrilor funcţiei main cât şi tipul funcţiei, void, sunt obligatorii.

2.2.3 Scrierea pe ecran


Programul din Figura 2.1 constă dintr-o singură instrucţiune, aflată la linia 6. Funcţia
println reprezintă principalul mecanism de scriere ı̂n Java, fiind echivalent ı̂ntr-o anumită
măsură cu funcţia writeln din Pascal sau printf din C. În această situaţie se scrie un şir
de caractere la fluxul de ieşire standard System.out. Vom discuta despre citire/scriere mai
târziu. Deocamdata ne mulţumim doar să amintim că aceeaşi sintaxa este folosită pentru
a scrie orice fel de entitate, fie că este vorba despre un ı̂ntreg, real, şir de caractere sau alt
tip.

2.3 Tipuri de date primitive


Java defineşte opt tipuri primitive de date, oferind de asemenea, o foarte mare flexibili-
tate ı̂n a defini noi tipuri de date, numite clase. Totuşi ı̂n Java, există câteva diferenţe
estenţiale ı̂ntre tipurile de date primitive şi cele definite de utilizator. În această secţiune
vom examina tipurile primitive şi operaţiile 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 reţine Valori


byte ı̂ntreg pe 8 biţi -128 la 127
short ı̂ntreg pe 16 biţi -32768 la 32767
int ı̂ntreg pe 32 biţi -2.147.483.648 la 2.147.483.647
long ı̂ntreg pe 64 biţi −263 la 263 − 1
float virgulă mobilă pe 32 biţi 6 cifre semnificative, (10−46 la 1038 )
double virgulă mobilă pe 64 biţi 15 cifre semnificative, (10−324 la 10308 )
char caracter unicode
boolean variabila booleană false şi true

Figura 2.2 Cele opt tipuri de date primitive ı̂n Java

Cel mai des utilizat este tipul ı̂ntreg specificat prin cuvântul cheie int. Spre deosebire
de majoritatea altor limbaje de programare, marja de valori a tipurilor ı̂ntregi nu este
dependentă de maşină. Java acceptă şi tipurile ı̂ntregi byte, short şi long. Numerele reale
(virgulă mobilă) sunt reprezentate de tipurile float şi double. Tipul double are mai multe
cifre semnficative, de aceea utilizarea lui este recomandată ı̂n locul tipului float. Tipul char
este folosit pentru a reprezenta caractere. Un char ocupă 16 biţi pentru a putea reprezenta
standardul Unicode. Standardul Unicode conţine peste 30.000 de caractere distincte care
acoperă principalele limbi scrise (inclusiv Japoneza, Chineza etc.). Prima parte a tabelei
Unicode este identică cu tabela ASCII. Ultimul tip primitiv este boolean; o variabilă 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. Notaţia octală este indi-
cată printr-un 0 nesemnificativ la ı̂nceput; notaţia hexa este indicată printr-un 0x sau 0X
la ı̂nceput. Iata câteva moduri echivalente de a reprezenta ı̂ntregul 37: 37, 045, 0x25.
Notaţiile octale şi hexazecimale nu vor fi utilizate ı̂n acest curs. Totuşi trebuie să fim
constienţi de ele pentru a folosi 0-uri la ı̂nceput doar acolo unde chiar vrem aceasta.
O constantă caracter este cuprinsă ı̂ntre apostrofuri, cum ar fi ’a’. Intern, Java inter-
pretează această constantă ca pe un număr (codul Unicode). Ulterior, funcţiile de scriere
vor transforma acest număr ı̂n caracterul corespunzător. Constantele caracter mai pot fi
reprezentate şi ı̂n forma:

’\uxxxx’.

unde xxxx este un numar ı̂n baza 16 reprezentând codul Unicode al caracterului.
Constantele de tip şir de caractere sunt cuprinse ı̂ntre ghilimele, ca ı̂n ”Primul meu program
Java”. Există anumite secvenţe speciale, numite secvenţe escape, care sunt folosite pentru
anumite caractere speciale. Noi vom folosi mai ales

’\n’, ’\\’, ’\’’ si ’\"’,

care ı̂nseamnă respectiv linie nouă, backslash, apostrof şi ghilimele.


12 CAPITOLUL 2. NOŢIUNI FUNDAMENTALE DE PROGRAMARE IN JAVA

2.3.3 Declararea şi iniţializarea tipurilor primitive ı̂n Java


Orice variabilă Java, inclusiv cele primitive, sunt declarate prin descrierea numelui a ti-
pului şi, opţional, a valorii iniţiale. Numele variabilei trebuie sa fie un identificator. Un
identificator poate să conţină orice combinaţie de litere, cifre şi caracterul underscore
(liniuţa de subliniere). Identificatorii nu pot ı̂ncepe cu o cifră. Cuvintele rezervate, cum ar
fi int nu pot fi identificatori. Nu pot fi utilizaţi nici identificatorii care deja sunt declaraţi
şi sunt vizibili.
Java este case-sensitive, ceea ce ı̂nseamnă că sum şi Sum reprezintă identificatori diferiţi.
În acest text vom folosi următoarea convenţie pentru numele variabilelor:
• toate numele de variabilă ı̂ncep cu literă mică, iar cuvintele noi din cadrul numelui
ı̂ncep cu literă mare. De exemplu: sumaMaxima, nodVizitat etc.
• numele claselor ı̂ncepe cu literă mare. De exemplu: FirstProgram, ArithmeticExcep-
tion, BinaryTree etc.
Alte convenţii vor mai fi prezentate pe parcurs.
Iată câteva exemple de declaraţii de variabile:
int numarElemente ;
double mediaGenerala ;
int produs = 1, suma = 0 ;
int produs1 = produs ;
O variabilă este bine să fie declarată imediat ı̂nainte de a fi folosită. Asa cum vom vedea
mai târziu, locul unde este declarată determină domeniul de vizibilitate şi semnificaţia ei.

2.3.4 Citire/scriere de la terminal


Scrierea la terminal ı̂n Java se realizează cu funcţia println şi nu pune probleme ma-
jore. Lucrurile nu stau deloc la fel cu citirea de la tastatură, care se realizează mult mai
anevoios. Acest lucru se datorează ı̂n primul rând faptului că programele Java nu sunt
concepute pentru a citi de la tastatură. În imensa majoritate a cazurilor programele Java
ı̂şi preiau datele dintr-o interfaţă grafică (Applet-urile), din forme HTML (Java Servlets,
Java Server Pages) sau din fişiere.
Citirea şi scrierea de la consolă sunt realizate prin readLine, respectiv println. Fluxul de
intrare standard este System.in iar fluxul de ieşire standard este System.out.
Mecanismul de bază pentru citirea/scrierea formatată foloseste tipul String, care va fi de-
scris ı̂n capitolul următor. La afişare, operatorul + concatenează două String-uri. Pentru
tipurile primitive, dacă parametrul scris nu este de tip String se face o conversie temporară
la String. Aceste conversii pot fi definite şi pentru obiecte, asa cum vom arăta mai târziu.
Pentru citire se asociază un obiect de tipul BufferedReader cu System.in. Apoi se citeşte
un String care va fi ulterior prelucrat.

2.4 Operatori de bază


Această secţiune descrie operatorii de bază care sunt disponibili ı̂n Java. Aceşti operatori
sunt utilizaţi pentru a crea expresii. O constantă sau o variabilă reprezintă o expresie,
2.4. OPERATORI DE BAZĂ 13

la fel ca şi combinaţiile de constante şi variabile cu operatori. O expresie urmată de ”;”
reprezintă o instrucţiune simplă. În paragraful 2.5 vom prezenta alte tipuri de instrucţiuni,
care vor introduce noi tipuri de operatori.

2.4.1 Operatori de atribuire


Programul simplu din Figura 2.3 ilustrează câţiva 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 ı̂nlanţuiţi ca ı̂n:
z=y=x=0.
Un alt operator de atribuire este += al cărui mod de utilizare este ilustrat ı̂n linia 18.
Operatorul += adaugă valoarea aflată la dreapta (operatorului) la variabila din stânga.
Astfel, valoarea lui c este incrementată de la 6 la 14. Java oferă şi alţi operatori de atribuire
cum ar fi -=, *= şi /= care modifică variabila aflată ı̂n partea stângă prin scadere, ı̂nmulţire
şi respectiv ı̂mpărţire.
1. public class OperatorTest
2. {
3. //program care ilustreaza operatorii de baza
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 + " " + c) ;
16. a = c ;
17. System.out.println(a + " " + b + " " + c) ;
18. c += b ;
19. System.out.println(a + " " + b + " " + c) ;
20. a = b + c ;
21. System.out.println(a + " " + b + " " + c) ;
22. a++ ;
23. ++b ;
24. c = a++ + ++b ;
25. System.out.println(a + " " + b + " " + c) ;
26. }
27.}

Figura 2.3 Program care ilustrează anumiţi operatori simpli


14 CAPITOLUL 2. NOŢIUNI FUNDAMENTALE DE PROGRAMARE IN JAVA

2.4.2 Operatori aritmetici binari


Linia 20 din Figura 2.3 ilustrează unul dintre operatorii binari care sunt tipici pentru
limbajele de programare: operatorul de adunare (+). Operatorul + are ca efect adunarea
conţinutului variabilelor b şi c; valorile lui b şi c rămân neschimbate. Valoarea rezultată
este atribuită lui a. Alţi operatori aritmetici folosiţi ı̂n Java sunt: -, *, / şi % utilizaţi
respectiv pentru scădere, ı̂nmulţire, ı̂mpărţire şi rest.
Împărţirea a două valori ı̂ntregi are ca valoare doar partea ı̂ntregă a rezultatului. De e-
xemplu 3/2 are valoarea 1, dar 3.0/2 are valoarea 1.5.
Asa cum este şi normal, adunarea şi scăderea au aceeaşi prioritate. Această prioritate
este mai mică decât cea a grupului format din ı̂nmulţire, ı̂mpărţire şi rest; astfel 1 + 2*3
are valoarea 7. Toţi aceşti operatori sunt evaluaţi de la stânga la dreapta (astfel 3-2-2 are
valoarea -1). Toţi operatorii aritmetici au o anumită prioritate şi o anumită asociere.

2.4.3 Operatori aritmetici unari


În plus faţă de operatorii aritmetici binari care necesită doi operanzi, Java dispune şi de
operatori unari care necesită doar un singur operand. Cel mai cunoscut operator unar
este operatorul minus (-) care returnează operandul cu semn opus. Astfel, -x, este opusul
lui x.
Java oferă de asemenea operatorul de autoincrementare care adaugă 1 la valoarea unei
variabile, notat prin ++, şi operatorul de autodecrementare care scade 1 din valoarea
variabilei, notat cu - -. Un caz banal de utilizare a acestor operatori este exemplificat
ı̂n liniile 22 şi 23 din Figura 2.3. În ambele cazuri operatorul ++ adaugă 1 la valoarea
variabilei. În Java, ca şi ı̂n C, orice expresie are o valoare. Astfel, un operator aplicat
unei variabile generează o expresie cu o anumită valoare. Deşi faptul că variabila este
incrementată ı̂nainte de execuţia următoarei instrucţiuni este garantat, se pune ı̂ntrebarea:
”Care este valoarea expresiei de autoincrementare dacă ea este utilizată ı̂n cadrul unei alte
expresii?”
În acest caz, locul unde se plasează operatorul ++ este esenţial. Semnficaţia lui ++x
este că valoarea expresiei este egală cu noua valoare a lui x. Acest operator este numit
incrementare prefixată. În mod analog, x++ ı̂nseamnă că valoarea expresiei este egală cu
valoarea originală a lui x. Acesta este numit incrementare postfixată. Aceste trăsături
sunt exemplificate ı̂n linia 24 din Figura 2.3. Atât a, cât şi b sunt incrementate cu 1, iar
c este obţinut prin adunarea valorii iniţiale a lui a (care este 23) cu valoarea incrementată
a lui b (care este 10).

2.4.4 Conversii de tip


Operatorul conversie de tip, numit adeseori şi operatorul de cast, este utilizat pentru a
genera o variabilă temporară de un nou tip. Să considerăm, de exemplu, secvenţa de cod:

double rest ;
int x = 6 ;
int y = 10 ;
rest = x / y ; //mai mult ca sigur gresit!
2.5. INSTRUCŢIUNI CONDIŢIONALE 15

La efectuarea operaţiei de ı̂mpărţire, atât x cât şi y fiind numere ı̂ntregi, se va realiza o
ı̂mpărţire ı̂ntreagă şi se obţine 0. Întregul 0 este apoi convertit implicit la double astfel
ı̂ncât să poată fi atribuit lui rest. Probabil că intenţia noastră era aceea de a atribui lui
rest valoarea 0.6. Soluţia este de a converti temporar pe x sau pe y la double, pentru ca
ı̂mpărţirea să se realizeze ı̂n virgulă mobilă. Acest lucru se poate obţine astfel:

rest = ( double ) x / y ;

De remarcat că nici x şi nici y nu se schimbă. Se crează o variabilă temporară fără nume,
având valoarea 6.0, iar valoarea ei este utilizată pentru a efectua ı̂mpărţirea. Operatorul
de conversie de tip are o prioritate mai mare decât operatorul de ı̂mpărţire, de aceea
conversia de tip se efectuează ı̂nainte de a se efectua ı̂mpărţirea.

2.5 Instrucţiuni condiţionale


Această secţiune este dedicată instrucţiunilor care controlează fluxul de execuţie al pro-
gramului: instrucţiunile condiţionale şi iteraţia.

2.5.1 Operatori relaţionali


Testul fundamental care poate fi realizat asupra tipurilor primitive este comparaţia. Com-
paraţia se realizează utilizând operatorii de egalitate/inegalitate şi operatorii de comparaţie
(<, > etc.). În Java, operatorii de egalitate/inegalitate sunt == respectiv !=. De exemplu,

exprStanga == exprDreapta

are valoarea true dacă exprStanga şi exprDreapta sunt egale; altfel are valoarea false.
Analog, expresia:

exprStanga != exprDreapta

are valoarea true dacă exprStanga şi exprDreapta sunt diferite; altfel are valoarea false.
Operatorii de comparaţie sunt <, <=, >, >= iar semnficaţia lor este cea naturală pentru
tipurile fundamentale. Operatorii de comparaţie au prioritate mai mare decât operatorii
de egalitate. Totuşi, ambele categorii au prioritate mai mică decât operatorii aritmetici,
dar mai mare decât operatorii de atribuire. Astfel, veti constata adeseori că folosirea
parantezelor nu va fi necesară. Toţi aceşti operatori se asociază de la stânga la dreapta,
dar cunoaşterea acestui lucru nu ne foloseşte prea mult. De exemplu, ı̂n expresia a < b < 6,
prima comparaţie generează o valoare booleană, iar a doua expresie este greşită, deoarece
operatorul < nu este definit pentru valori booleene. Paragraful următor descrie cum se
poate realiza acest test ı̂n mod corect.

2.5.2 Operatori logici


Java dispune de operatori logici care sunt utilizaţi pentru a simula operatorii and, or
şi not din algebra Booleană. Aceşti operatori mai sunt referiţi uneori şi sub numele de
conjuncţie, disjuncţie şi, respectiv, negare, simbolurile corespunzătoare fiind &&, || şi !.
Implementarea corectă a testului din paragraful anterior este:
16 CAPITOLUL 2. NOŢIUNI FUNDAMENTALE DE PROGRAMARE IN JAVA

a<b && b<6


Prioritatea conjuncţiei şi a disjuncţiei este suficient de mică faţă de prioritatea celorlalţi
operatori din expresie pentru ca parantezele să nu fie necesare. && are prioritate mai
mare decât ||, iar ! are aceeaşi prioritate cu alţi operatori unari (++, - -, vezi Figura
2.4).
Categorie Exemple Asociere
Operatori pe referinţe . [] Stânga la dreapta
Unari + + −− ! − (tip) Dreapta la stânga
Multiplicativi ∗ / % Stânga la dreapta
Aditivi + − Stânga la dreapta
Shiftare (pe biti) << >> >>> Stânga la dreapta
Relationali < <= > >= instanceof Stânga la dreapta
Egalitate == ! = Stânga la dreapta
AND pe biţi & Stânga la dreapta
XOR pe biţi ˆ Stânga la dreapta
OR pe biţi | Stânga la dreapta
AND logic && Stânga la dreapta
OR logic || Stânga la dreapta
Condiţional ?: Dreapta la stânga
Atribuire = ∗= /= %= += −= Dreapta la stânga

Figura 2.4 Operatori Java listaţi ı̂n ordinea priorităţii3


O regulă importantă este că operatorii && şi || folosesc evaluarea booleană scurtcircuitată4 .
Aceasta ı̂nseamnă că dacă rezultatul poate fi determinat evaluând prima expresie, a doua
nu mai este evaluată. De exemplu, ı̂n expresia:
x !=0 && 1/x != 3
dacă x este 0, atunci prima jumatate este false. Aceasta ı̂nsemnă că rezultatul conjuncţiei
va fi fals, deci a doua expresie nu mai este evaluată. Acesta este un detaliu important,
deoarece ı̂mpărţirea la 0 ar genera un comportament eronat.

2.5.3 Instrucţiunea if
Instrucţiunea if este instrucţiunea fundamentală de decizie. Forma sa simplă este:
if( expresie )
instructiune
urmatoarea instructiune
Dacă expresie are valoarea true atunci se execută instructiune; ı̂n caz contrar instructiune
nu se execută. După ce instrucţiunea if se ı̂ncheie (fară o excepţie netratată), controlul
este preluat de următoarea instructiune.
Opţional, putem folosi instrucţiunea if-else după cum urmează:
3
Prin asociere ı̂nţelegem odinea de evaluare ı̂ntr-o expresie care conţine operatori de acelaşi tip şi nu
are paranteze.
4
Numită uneori şi evaluare booleană parţială
2.5. INSTRUCŢIUNI CONDIŢIONALE 17

if( expresie )
instructiune1
else
instructiune2
urmatoarea instructiune
În acest caz, dacă expresie are valoarea true, atunci se execută instructiune1; altfel se
execută instructiune2. În ambele cazuri controlul este apoi preluat de urmatoarea instruc-
tiune. Iată 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 reţinut că doar o singură instrucţiune poate exista pe ramura de if sau de else indiferent
de cum indentaţi codul. Iată două erori frecvente la ı̂ncepători:
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 greşală constă ı̂n a pune ; după if. Simbolul ; reprezintă ı̂n sine instrucţiunea vidă;
ca o consecinţă, acest fragment de cod nu va fi compilabil (else nu va fi asociat cu nici
un if). După ce am corectat această eroare, rămânem cu o eroare de logică: ultima linie
de cod nu face parte din if, deşi acest lucru este sugerat de indentare. Pentru a rezolva
această problemă vom utiliza un bloc ı̂n care grupăm o secvenţă de instrucţiuni printr-o
pereche de acolade:
if( x == 0 )
{
System.out.println( "x este 0" ) ;
}
else
{
System.out.print( "x este" ) ;
System.out.println( x ) ;
}
Observaţi că am folosit acolade şi pe ramura de if deşi acestea nu sunt absolut necesare.
Această practică ı̂mbunătăţeşte foarte mult lizibilitatea codului şi vă invităm şi pe dum-
neavoastră să o adoptaţi.
Instrucţiunea if poate să facă parte dintr-o altă instrucţiune if sau else, la fel ca şi celelalte
instrucţiuni de control prezentate ı̂n continuare ı̂n această secţiune.
18 CAPITOLUL 2. NOŢIUNI FUNDAMENTALE DE PROGRAMARE IN JAVA

2.5.4 Instrucţiunea while


Java, ca şi Pascal sau C, dispune de trei instrucţiuni de ciclare: instrucţiunea while,
instrucţiunea do şi instrucţiunea for. Sintaxa instrucţiunii while este:

while( expresie )
instructiune
urmatoarea instructiune

Observaţi că, la fel ca şi la instrucţiunea if, nu există ; ı̂n sintaxă. Dacă apare un ; , după
while, va fi considerat ca instrucţiune vidă.
Cât timp expresie este true se execută instructiune; apoi expresie este evaluată din nou.
Dacă expresie este false de la bun ı̂nceput, atunci instructiune nu va fi executată niciodată.
În general, instructiune face o acţiune care ar putea modifica valoarea lui expresie; ı̂n
caz contrar, ciclarea s-ar putea produce la infinit. Când instrucţiunea while se ı̂ncheie,
controlul este preluat de urmatoarea instructiune.

2.5.5 Instrucţiunea for


Instrucţiunea while ar fi suficientă pentru a exprima orice fel de ciclare. Totuşi, Java
mai oferă ı̂ncă două forme de a realiza ciclarea: instrucţiunea for şi instrucţiunea do.
Instrucţiunea for este utilizată ı̂n primul rând pentru a realiza iteraţia. Sintaxa ei este:

for( initializare; test; actualizare)


instructiune
urmatoarea instructiune

În această situaţie, initializare, test şi actualizare sunt toate expresii, şi toate trei sunt
opţionale. Dacă test lipseste, valoarea sa implicita este true. După paranteza de ı̂nchidere
nu se pune ;.
Instrucţiunea for se execută realizând mai ı̂ntâi initializare. Apoi, cât timp test este
true, au loc următoarele două instrucţiuni: se execută instructiune, iar apoi se execută
actualizare. Dacă initializare şi actualizare sunt omise, instrucţiunea for se va comporta
exact ca şi instrucţiunea while.
Avantajul instrucţiunii for constă ı̂n faptul că se poate vedea clar marja pe care iterează
variabilele contor.
Următoarea secvenţă de cod afişează primele 100 numere ı̂ntregi pozitive:

for( int i=1; i<100; ++i)


{
System.out.println( i );
}

Acest fragment ilustrează şi practica obişnuită pentru programatorii Java (şi C++) de a
declara un contor ı̂ntreg ı̂n secvenţa de iniţializare a ciclului. Durata de viaţă a acestui
contor se extinde doar ı̂n interiorul ciclului.
Atât initializare cât şi actualizare pot folosi operatorul virgulă pentru a permite expresii
multiple. Următorul fragment ilustrează această tehnică frecvent folosită:
2.5. INSTRUCŢIUNI CONDIŢ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 şi instrucţiunile if. De exemplu, putem găsi toate
perechile de numere mici a căror sumă este egală cu produsul lor (cum ar fi 2 şi 2, a căror
sumă şi produs este 4) folosind secvenţa 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 Instrucţiunea do
Instrucţiunea while realizează un test repetat. Dacă testul este true atunci se execută
instrucţiunea din cadrul ei. Totuşi, dacă testul iniţial este false, instrucţiunea din cadrul
ciclului nu este executată niciodată. În anumite situaţii avem nevoie ca instrucţiunile din
ciclu să se execute cel puţin o dată. Acest lucru se poate realiza utilizând instrucţiunea
do. Instrucţiunea do este asemănătoare cu instrucţiunea while, cu deosebirea că testul este
realizat după ce instrucţiunile din corpul ciclului se execută. Sintaxa sa este:

do
instructiune
while( expresie ) ;
urmatoarea instructiune ;

Remarcaţi faptul că instrucţiunea do se termină cu ;. Un exemplu tipic ı̂n care se utilizează
instrucţiunea do este dat de fragmentul de (pseudo-) cod de mai jos:

do
{
afiseaza mesaj ;
citeste data ;
}while( data nu este corecta ) ;

Instrucţiunea do este instrucţiunea de ciclare cel mai puţin utilizată. Totuşi, atunci când
vrem să executăm ceva cel puţin o dată ciclul, şi for nu poate fi utilizat, atunci do este
alegerea potrivită.
20 CAPITOLUL 2. NOŢIUNI FUNDAMENTALE DE PROGRAMARE IN JAVA

2.5.7 Instrucţiunile break şi continue


Instrucţiunile for şi while au condiţia de terminare ı̂naintea instrucţiunilor care se repetă.
Instrucţiunea do are condiţia de terminare după instrucţiunile care se repetă. Totuşi, ı̂n
anumite situaţii, s-ar putea să fie nevoie să ı̂ntrerupem ciclul ı̂n mijlocul instrucţiunilor
care se repetă. În acest scop, se poate folosi instrucţiunea break. De obicei, instrucţiunea
break apare ı̂n cadrul unei instrucţiuni if, ca ı̂n exemplul de mai jos:

while(...)
{
...
if( conditie )
{
break ;
}
...
}

În cazul ı̂n care sunt două cicluri imbricate, instrucţiunea break părăseşte doar ciclul cel
mai din interior. Dacă există mai mult de un ciclu care trebuie terminat, break nu va
funcţiona corect, şi mai mult ca sigur că aţi proiectat prost algoritmul. Totuşi, Java oferă
aşa numitul break etichetat. În acest caz, o anumită instrucţiune de ciclare este etichetată
şi instrucţiunea break poate fi aplicată acelei instrucţiuni de ciclare, indiferent de numarul
de cicluri imbricate. Iată un exemplu:

eticheta:
while(...)
{
while(...)
{
...
if( conditie )
{
break eticheta;
}
}
}
//controlul programului trece aici dupa executia lui break

În anumite situaţii dorim sa renunţăm la execuţia iteraţiei curente din ciclu şi să trecem la
următoarea iteraţie a ciclului. Acest lucru poate fi realizat cu instrucţiunea continue. Ca
şi break, instrucţiunea continue este urmată de ; şi se aplică doar ciclului cel mai interior
ı̂n cazul ciclurilor imbricate. Următorul fragment tipăreşte primele 100 de numere ı̂ntregi,
cu excepţia celor divizibile cu 10:

for( int i = 1 ; i <= 100; i++)


{
if( i % 10 == 0)
2.5. INSTRUCŢIUNI CONDIŢIONALE 21

{
continue ;
}
System.out.println( i ) ;
}

Desigur, că exemplul de mai sus poate fi implementat şi utilizând un if simplu. Totuşi
instrucţiunea continue este adeseori folosită pentru a evita imbricari complicate de tip
if-else ı̂n cadrul ciclurilor.

2.5.8 Instrucţiunea switch


Instrucţiunea switch este numită uneori şi instrucţiune de selecţie. Instrucţiunea switch
selectează dintre mai multe secvenţe de cod, una care va fi executată, funcţie 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 ı̂ntreagă. Instrucţiunea switch


compară valoarea lui expresie-selectare cu fiecare valoare-intreagă. Dacă are loc egalitatea
se execută instrucţiunea corespunzătoare (simplă sau compusă). Dacă nu are loc nici o
egalitate se execută instrucţiunea din default.
Observaţi că ı̂n exemplul de mai sus, fiecare case se ı̂ncheie cu un break care are ca efect
saltul la sfârşitul instrucţiunii switch. Acesta este modul obişnuit de a scrie o instrucţiune
de tip switch, dar prezenţa instrucţiunii break este opţională. Dacă instrucţiunea break
lipseste, atunci se va executa şi codul corespunzător instrucţiunilor case următoare până
când se ı̂ntâlneste un break. Deşi de obicei nu ne dorim un astfel de comportament, el
poate fi uneori util pentru programatorii experimentaţi.
Programul de mai jos crează litere aleator şi determină dacă acestea sunt vocale sau
consoane (ı̂n limba engleză):

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. NOŢ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 Instrucţiunea switch

Funcţia Math.random() generează o valoare ı̂n intervalul [0,1). Prin ı̂nmulţirea valorii
returnate de această funcţie cu numarul de litere din alfabet (26 litere) se obţine un numar
ı̂n intervalul [0,26). Adunarea cu prima literă (’a’, care are de fapt valoarea 97) are ca
efect transpunerea ı̂n intervalul [97,123). În final se foloseste operatorul de conversie de
tip pentru a trunchia numărul la o valoare din multimea 97,98,...,122, adică un cod ASCII
al unui caracter din alfabetul englez .

2.5.9 Operatorul condiţional


Operatorul codiţional este folosit ca o prescurtare pentru instrucţiuni simple de tipul if-else.
Forma sa generală este:

exprTest ? expresieDa : expresieNu ;

Mai ı̂ntâi se evaluează exprTest urmată fie de expresieDa fie de expresieNu, rezultând
astfel valoarea ı̂ntregii expresii. expresieDa este evaluată dacă exprTest are valoarea true;
ı̂n caz contrar se evaluează expresieNu. Prioritatea operatorului condiţional este chiar
deasupra operatorilor de atribuire. Acest lucru permite omiterea parantezelor atunci când
asignăm rezultatul operatorului condiţional unei variabile. Ca un exemplu, minimul a
două variabile poate fi calculat după cum urmează:

valMin = x < y ? x : y ;
2.6. METODE 23

2.6 Metode
Ceea ce ı̂n alte limbaje de programare numeam procedură sau funcţie, ı̂n Java este numit
metodă. O definiţie mai exactă şi completă a noţiunii de metodă o vom da mai târziu. În
acest paragraf prezentăm doar câteva noţiuni elementare pentru a putea scrie funcţii de
genul celor din C sau Pascal pe care să le folosim ı̂n câteva programe simple.
Antetul unei metode constă dintr-un nume, o listă (eventual vidă) de parametri şi un tip
pentru valoarea returnată. Codul efectiv al metodei numit adeseori corpul metodei este
un bloc (o secvenţa de instrucţiuni cuprinsă ı̂ntre acolade). Definirea unei metode constă
ı̂n antet şi corp. Un exemplu de metodă şi de o funcţie main care o utilizează este dat ı̂n
Figura 2.6.
Prin prefixarea metodelor cu ajutorul cuvintelor cheie public static putem mima ı̂ntr-
o oarecare masură funcţiile din Pascal şi C. Deşi această tehnică este utilă ı̂n anumite
situaţii, ea nu trebuie utilizată ı̂n mod abuziv. Numele metodei este un identificator.
Lista de parametri constă din 0 sau mai mulţi parametri formali, fiecare având un tip
precizat. Când o metodă este apelată, parametrii actuali sunt trecuţi ı̂n parametrii for-
mali utilizând atribuirea obişnuită. Aceasta ı̂nsemnă că tipurile primitive sunt transmise
utilizând exclusiv transmiterea prin valoare. Parametrii actuali nu vor putea fi modificaţi
de către funcţie. Definirile metodelor pot apărea ı̂n orice ordine.
Instrucţiunea return este utilizată pentru a ı̂ntoarce o valoare către codul apelant. Dacă
tipul funcţiei este void atunci nu se ı̂ntoarce nici o valoare şi se foloseşte
return ;
fără 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 şi apelul unei metode

2.6.1 Supraı̂ncărcarea numelor la metode


Să presupunem că dorim să scriem o metoda care calculează maximul a trei numere ı̂ntregi.
Un antet pentru această metodă ar fi:
int max(int a, int b, int c)
24 CAPITOLUL 2. NOŢIUNI FUNDAMENTALE DE PROGRAMARE IN JAVA

În unele limbaje de programare (Pascal, C), acest lucru nu ar fi permis dacă există deja
o functie max cu doi parametri. De exemplu, se poate să avem deja declarată o metodă
max cu antetul:

int max(int a, int b)

Java permite supraı̂ncărcarea (engl. overloading) numelui metodelor. Aceasta ı̂nsemnă că
mai multe metode pot fi declarate ı̂n cadrul aceleiaşi clase atâta timp cât semnăturile lor
(adică lista de parametri) diferă. Atunci când se face un apel al metodei max compilatorul
poate uşor să deducă despre care metodă este vorba examinând lista parametrilor de apel.
Se poate să existe metode supraı̂ncărcate cu acelaşi număr de parametri formali atâta
timp cât cel puţin unul din tipurile din lista de parametri este diferit.
De reţinut faptul că tipul funcţiei nu face parte din semnătura ei. Aceasta ı̂nseamnă că
nu putem avea două metode ı̂n cadrul aceleiaşi clase care să difere doar prin tipul valorii
returnate. Metode din clase diferite pot avea acelaşi nume, parametri şi chiar tip returnat;
despre aceasta vom discuta pe larg mai târziu.

2.7 Probleme propuse


1. Scrieţi o instucţiune while echivalentă cu ciclul for de mai jos. La ce ar putea fi
utilizat un astfel de ciclu?

for( ; ; )
instructiune

2. Scrieţi un program care generează tabelele pentru ı̂nmulţirea şi adunarea numerelor
cu o singură cifră.

3. Scrieti două metode statice. Prima să returneze maximul a trei numere ı̂ntregi, a
doua maximul a patru numere ı̂ntregi .

4. Scrieţi o metodă statică care primeşte ca parametru un an şi returnează true dacă
anul este bisect şi false ı̂n caz contrar.
Capitolul 3

Referinţe

În capitolul 1 am prezentat tipurile primitive din Java. Toate tipurile care nu sunt ı̂ntre
cele opt primitive, inclusiv tipurile importante cum ar fi stringurile, şirurile şi fişierele sunt
tipuri referinţă.
În acest capitol vom ı̂nvăţa:
• Ce este un tip referinţă şi ce este o variabilă referinţă
• Prin ce diferă un tip referinţă de un tip primitiv
• Exemple de tipuri referinţă incluzând stringuri, şiruri şi fluxuri
• Cum sunt utilizate excepţiile pentru a semnala comportamentul greşit al
unei secvenţe de cod

3.1 Ce este o referinţă


În capitolul 1 am examinat cele opt tipuri primitive, ı̂mpreună cu câteva operaţii care pot
fi realizate pe aceste tipuri. Toate celelalte tipuri de date din Java sunt referinţe. Ce este
deci o referinţă? O variabilă referinţă ı̂n Java (numită adeseori simplu referinţă) este o
variabilă care reţine adresa de memorie la care se află un anumit obiect.
point1 1000 (0,0)



point3 1024 (5,12)


✟ 3200 point2 = 1024
point2 3600 point1 = 1000


✟ 5124 point3 = 1000

Figura 3.1 Ilustrarea unei referinţe: Obiectul de tip Point stocat la adresa de memorie
1000 este referit atât de către point1 cât şi de către point3. Obiectul de tip Point stocat la
adresa 1024 este referit de către point2. Locaţiile de memorie unde sunt reţinute variabilele
au fost alese arbitrar.

Ca un exemplu, ı̂n Figura 3.1 există două obiecte de tipul Point1 . Presupunem că aceste
obiecte au fost stocate la adresele de memorie 1000 şi respectiv 1024. Pentru aceste două
1
Care conţin coordonatele unui punct din plan.

25
26 CAPITOLUL 3. REFERINŢE

obiecte am definit trei referinţe, point1, point2 şi point3. Atât point1 cât şi point3 referă
obiectul stocat la adresa 1000; point2 referă obiectul stocat la adresa 1024; aceasta ı̂nsemnă
că atât point1 cât şi point3 au valoarea 1000, iar point2 va avea valoarea 1024. Reţineţi
că locaţiile efective, cum ar fi 1000 şi 1024, sunt atribuite de compilator la discreţia sa
(unde găseşte memorie liberă). În consecinţă, aceste valori nu sunt utile efectiv ca valori
numerice. Totuşi, faptul că point1 şi point3 au aceeaşi valoare este folositor: ı̂nseamnă
că ele referă acelaşi obiect. O referinţă stochează ı̂ntotdeauna adresa la care un anumit
obiect se află, cu excepţia situaţiei când nu referă nici un obiect. În această situaţie va
stoca referinţa nulă, notată cu null. Java nu permite referinţe către tipurile primitive.
Există două mari tipuri de operaţii care se pot aplica variabilelor referinţă.
1. Prima categorie permite examinarea şi manipularea valorii referinţă. De exemplu,
dacă modificăm valoarea stocată ı̂n point1 (care este 1000), putem să facem ca point1
să refere un alt obiect. Putem de asemenea compara point1 şi point3 pentru a vedea
dacă referă acelaşi obiect.
2. A doua categorie de operaţii se aplică obiectului care este referit; am putea de
exemplu examina sau modifica starea unuia dintre obiectele de tipul Point (am
putea examina coordonatele x şi y ale unui obiect de tipul Point).
Înainte de a descrie ce se poate face cu ajutorul referinţelor, să descriem ce nu se poate
face. Să considerăm expresia point1*point2. Deoarece valorile reţinute de point1 şi point2
sunt respectiv 1000 şi 1024, produsul lor ar fi 1024000. Totuşi acest calcul este lipsit de
sens şi nu ar putea avea nici o valoarea practică. Variabilele referinţă reţin adrese, şi nu
poate fi asociată nici o semnificaţie logică ı̂nmulţirii adreselor.
Analog, point1++ nu are nici un sens ı̂n Java; ar sugera ca point1 - care are valoarea
1000 - să fie crescut la 1001, dar ı̂n acest caz nu ar mai referi un obiect valid. Multe alte
limbaje de programare definesc noţiunea de pointer care are un comportament similar cu
cel al unei variabile referinţă. Totuşi, pointerii ı̂n C sunt mult mai periculoşi, deoarece
este permisă aritmetica pe adresele stocate. În plus, deoarece C permite pointeri şi către
tipurile fundamentale trebuie avut grijă pentru a distinge ı̂ntre aritmetica pe adrese şi
aritmetica pe variabilele care sunt referite. Acest lucru se face prin dereferenţierea explicită
a pointerului. În practică, pointerii limbajului C tind să provoace numeroase erori greu
detectabile, care uneori ı̂i pot face şi pe programatorii experimentaţi să plângă de ciudă!
În Java, singurele operaţii care sunt permise asupra referinţelor (cu o singură excepţie
pentru Stringuri) sunt atribuirea via = şi comparaţia via == sau !=. De exemplu, prin
atribuirea lui point3 a valorii lui point2, vom face ca point3 să refere acelaşi obiect pe care ı̂l
referă point2. Acum expresia point2 == point3 este adevărată, deoarece ambele referinţe
stochează valoarea 1024 şi referă deci acelaşi obiect. point1 != point2 este de asemenea
adevărată, deoarece point1 şi point2 referă acum obiecte distincte.
Cealaltă categorie de operaţii se referă la obiectul care este referit. Există doar trei acţiuni
fundamentale care pot fi realizate:
1. Aplicarea unei conversii de tip
2. Accesul la un câmp al obiectului sau apelul unei metode prin operatorul punct (.)
3. Utilizarea operatorului instanceof pentru a verifica dacă obiectul reţinut are un an-
umit tip.
3.2. FUNDAMENTE DESPRE OBIECTE ŞI REFERINŢE 27

Secţiunea următoare ilustrează mai detaliat operaţiile pe referinţe.

3.2 Fundamente despre obiecte şi referinţe


În Java un obiect este orice variabilă de un tip ne-primitiv. Obiectele sunt tratate diferit
faţă de tipurile primitive. Variabilele de tipuri primitive sunt manipulate prin valoare,
ceea ce ı̂nsemnă că valorile lor sunt reţinute ı̂n acele variabile şi sunt copiate dintr-o vari-
abilă primitivă ı̂n altă variabilă primitivă ı̂n timpul instrucţiunii de atribuire. După cum
am arătat ı̂n secţiunea anterioară, variabilele referinţă stochează referinţe către obiecte.
Obiectul ı̂n sine este stocat undeva ı̂n memorie, iar variabila referinţă stochează adresa
de memorie a obiectului. Astfel, variabila referinţă nu este decât un nume pentru acea
zonă de memorie. Aceasta ı̂nsemnă că variabilele primitive şi cele referinţă vor avea un
comportament diferit. Această secţiune examinează mai ı̂n detaliu aceste diferenţe şi ilus-
trează operaţiile care sunt permise pe tipurile referinţă.

3.2.1 Operatorul punct (.)


Operatorul punct (.) este folosit pentru a selecta o metodă care se aplică unui obiect. De
exemplu, să presupunem că avem un obiect de tip Cerc care defineşte metoda arie. Dacă
variabila unCerc este o referinţă către un Cerc, atunci putem calcula aria (şi salva această
arie ı̂ntr-o variabilă de tip double) cercului referit astfel:

double arieCerc = unCerc.arie() ;

Este posibil ca variabila unCerc să reţină referinţa null. În acest caz, aplicarea operatorului
punct va genera o excepţie de tipul NullPointerException la execuţia programului. De
obicei această excepţie va determina terminarea anormală a programului.
Operatorul punct poate fi folosit şi pentru a accesa componentele individuale ale unui
obiect, dacă cel care a proiectat obiectul permite acest lucru. Capitolul următor descrie
cum se poate face acest lucru; tot acolo vom explica de ce ı̂n general este preferabil ca să
nu se permită accesul direct la componentele individuale ale unui obiect.

3.2.2 Declararea obiectelor


Am văzut deja care este sintaxa pentru declararea variabilelor primitive. Pentru obiecte
există o diferenţă importantă. Atunci când declarăm o referinţă, nu facem decât să
furnizăm un nume care poate fi utilizat pentru a referi un obiect stocat ı̂n prealabil ı̂n
memorie. Totuşi, declaraţia ı̂n sine nu furnizează şi acel obiect. Să presupunem, de exem-
plu, că avem un obiect de tip Cerc căruia dorim să ı̂i calculăm aria folosind metoda arie().
Să considerăm secvenţa de instrucţiuni de mai jos:

Cerc unCerc ; //unCerc poate referi un obiect de tip Cerc


double arieCerc = unCerc.arie() ; //calcul arie pentru cerc

Totul pare ı̂n regulă cu aceste instrucţiuni, până când ne aducem aminte că unCerc este
numele unui obiect oarecare de tip Cerc, dar nu am creat nici un cerc efectiv. În consecinţă,
după ce se declară variabila unCerc, aceasta va conţine valoarea null, ceea ce ı̂nseamnă că
28 CAPITOLUL 3. REFERINŢE

unCerc ı̂ncă nu referă un obiect Cerc valid. Aceasta ı̂nseamnă că a doua linie de program
este invalidă, deoarece ı̂ncercăm să calculăm aria unui cerc care ı̂ncă nu există. În exemplul
de faţă chiar compilatorul va detecta eroarea, afirmând că unCerc ”nu este iniţializat”.
În alte situaţii mai complexe compilatorul nu va putea detecta eroarea şi se va genera o
NullPointerException ı̂n timpul execuţiei programului.
Singura posibilitate (normală) de a aloca memorie unui obiect este folosirea cuvântului
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

Remarcaţi parantezele care se pun după numele obiectului.


Adeseori programatorii combină declararea şi iniţializarea 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 iniţiale. De exemplu, obiec-
tul de tip Cerc ar putea fi construit cu trei parametri, doi pentru coordonatele centrului
şi 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 aştepta ca atunci când nu mai
este nevoie de ele să trebuiască să le distrugem. Totuşi, ı̂n Java, când un obiect din
memorie nu mai este referit de nici o variabilă, memoria pe care o consumă va fi eliberată
automat. Această tehnică se numeşte colectare de gunoaie.

3.2.4 Semnificaţia lui =


Să presupunem că avem două variabile de tipuri primitive x şi y. Atunci, semnificaţia
instrucţiunii de atribuire

x = y ;

este simplă: valoarea stocată ı̂n y este stocată ı̂n variabila primitivă x. Modificările ulte-
rioare ale lui x sau y nu au efecte asupra celeilalte.
Pentru obiecte, semnificaţia lui = este aceeaşi: se copiază valorile stocate. Dacă x şi y
sunt referinţe (de tipuri compatibile), atunci, după operaţia de atribuire, x va referi acelaşi
obiect ca şi y. Ceea ce se copiază ı̂n acest caz sunt adrese. Obiectul pe care x ı̂l referea
ı̂nainte nu mai este referit de x. Dacă x a fost singura referinţă către acel obiect, atunci
obiectul nu mai este referit acum de nici o variabilă şi este disponibil pentru colectarea de
gunoaie. Reţineţi faptul că obiectele nu se copiază.
3.2. FUNDAMENTE DESPRE OBIECTE ŞI REFERINŢE 29

Iată câteva exemple. Să presupunem că dorim să creăm două obiecte de tip Cerc pentru
a calcula suma ariilor lor. Creăm mai ı̂ntâi obiectul cerc1, după care ı̂ncercăm să creăm
obiectul cerc2 prin modificarea lui cerc1 după cum urmează (vezi şi 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



cerc2 Cerc(0,0,10)


❥ Cerc(0,0,10) cerc1



cerc1 Cerc(0,0,20)


Figura 3.2 cerc1 şi cerc2 indică acelaşi obiect. Modificarea razei lui cerc2 implică şi
modificarea razei lui cerc1.
Acest cod nu va funcţiona corect, deoarece nu s-a construit decât un singur obiect de tip
Cerc. Astfel, cea de-a doua instrucţiune nu face decât să spună că cerc2 este un alt nume
pentru cerc1, construit anterior. Cercul construit ı̂n prima linie are acum două nume. A
treia instrucţiune modifică raza cercului la 20, dar de fapt se modifică raza unicului cerc
creat, deci ultima linie adună aria aceluiaşi cerc de rază 20.
Secvenţa de cod corectă 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 primă vedere, faptul că obiectele nu pot fi copiate, pare să fie o limitare severă. În
realitate nu este deloc aşa, deşi ne trebuie un pic de timp pentru a ne obişnui cu acest
lucru. Există totuşi anumite situaţii când chiar trebuie să copiem obiecte; ı̂n aceste situaţii
se va folosi metoda clone(). clone() foloseşte new pentru a crea un nou obiect duplicat.
Totuşi, ı̂n acestă lucrare metoda clone() nu este folosită.

3.2.5 Transmiterea de parametri


Din cauză că apelul se face prin valoare, parametri actuali (de apel) se transpun ı̂n
parametri formali folosind atribuirea obişnuită. Dacă parametrul trimis este un tip referinţă,
atunci ştim deja că prin atribuire atât parametrul formal, cât şi parametrul de apel vor
referi acelaşi obiect. Orice metodă aplicată parametrului formal este astfel implicit aplicată
şi parametrului de apel. În alte limbaje de programare acest tip de apel se numeşte apelare
prin referinţă. Utilizarea acestei noţiuni ı̂n Java ar fi oarecum nepotrivită, deoarece ne-ar
putea face să credem că transmiterea referinţelor s-ar face ı̂n mod diferit. În realitate,
transmiterea parametrilor nu s-a modificat; ceea ce s-a modificat sunt parametrii ı̂n sine,
care nu mai sunt tipuri primitive, ci tipuri referinţă.
30 CAPITOLUL 3. REFERINŢE

3.2.6 Semnificaţia lui ==


Pentru tipurile primitive operaţia == are valoarea true dacă au valori identice. Pentru
tipuri referinţă semnificaţia lui == este diferită, dar perfect consistentă cu discuţia din
paragraful anterior. Două variabile referinţă sunt egale via == dacă ele referă acelaşi
obiect (s-au ambele sunt null). Să considerăm următorul 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 ;

În acest caz avem două obiecte. Primul este cunoscut sub numele de cerc1, al doilea este
cunoscut sub două nume: cerc2 şi cerc3. Expresia cerc2 == cerc3 este adevărată. Totuşi,
deşi cerc1 şi cerc2 par să refere obiecte care au aceeaşi valoare, expresia cerc1 == cerc2
este falsă, deoarece ele referă obiecte diferite. Aceleaşi reguli se aplică şi pentru operatorul
!=.
Cum facem ı̂nsă pentru a vedea dacă obiectele referite sunt identice? De exemplu, cum
putem să verificăm faptul că cerc1 şi cerc2 referă obiecte Cerc care sunt egale? Obiectele
pot fi comparate folosind metoda equals. Vom vedea ı̂n curând un exemplu de folosire a
lui equals, ı̂n care vom discuta despre tipul String (paragraful 3.3). Fiecare obiect are o
metodă equals, care, ı̂n mod implicit, nu face altceva decât testul ==. Pentru ca equals să
funcţioneze corect, programatorul trebuie să redefinească această metodă pentru obiectele
pe care le crează.

3.2.7 Supraı̂ncărcarea operatorilor pentru obiecte


În afara unei singure excepţii pe care o vom discuta ı̂n paragraful următor, operatorii nu
pot fi definiţi pentru a lucra cu obiecte2 . Astfel, nu există operatorul < pentru nici un
fel de obiect. Pentru acest scop, va trebui definită o metodă, cum ar fi lessThan, care va
realiza comparaţia.

3.3 Şiruri de caractere (stringuri)


Şirurile de caractere ı̂n Java sunt definite folosind obiectul String. Limbajul Java face să
pară că String este un tip primitiv, deoarece pentru el sunt definiţi operatorii + şi +=
pentru concatenare. Totuşi, acesta este singurul tip referinţă pentru care Java a permis
supraı̂ncărcarea operatorilor. În rest, String se comportă ca orice alt obiect.

3.3.1 Fundamentele utilizării stringurilor


Există două reguli fundamentale referitoare la obiectele de tip String. Prima este aceea că,
exceptând operatorul de concatenare, obiectele de tip String se comportă ca toate celelalte
obiecte. A doua regulă este aceea că stringurile sunt ne-modificabile. Aceasta ı̂nseamnă
că, odată construit, un obiect de tip String nu mai poate fi modificat. Deoarece obiectele
2
Aceasta este o diferenţă notabilă ı̂ntre Java şi C++, care permite supraı̂ncărcarea operatorilor pentru
obiecte. Inginerii de la Sun au considerat că supraı̂ncărcarea operatorilor pentru obiecte aduce mai multe
probleme decât beneficii şi au decis ca Java să nu permită acest lucru
3.3. ŞIRURI DE CARACTERE (STRINGURI) 31

de tip String nu se pot modifica, putem folosi liniştiţi operatorul = pentru ele. Iată un
exemplu:

String vid = "" ;


String mesaj = "Salutare!" ;
String repetat = mesaj ;

După aceste declaraţii, există două obiecte de tip String. Primul este un şir vid, şi este
referit de variabila vid. Al doilea este şirul ”Salutare!”, care este referit de variabilele mesaj
şi repetat. Pentru majoritatea obiectelor, faptul că obiectul este referit de două variabile
ar putea genera probleme. Totuşi, deoarece stringurile nu pot fi modificate, partajarea lor
nu pune nici un fel de probleme. Singura posibilitate de a modifica valoarea către care
referă variabila repetat este aceea de a construi un nou obiect de tip String şi a-l atribui
lui repetat. Această operaţie nu va avea nici un efect asupra valorii pe care o referă mesaj.

3.3.2 Concatenarea stringurilor


Java nu permite supraı̂ncărcarea operatorilor pentru tipurile referinţă. Totuşi, pentru
comoditate, se acordă o excepţie specială pentru concatenarea de obiecte de tipul String.
Atunci când cel puţin unul dintre operanzi este de tip String, operatorul + realizează
concatenarea. Rezultatul este o referinţă către un obiect nou construit de tip String. Iată
câteva exemple:

"Sunt" + " curajos!" //rezulta "Sunt curajos!"


2 + " mere" //rezulta "2 mere"
"mere " + 2 //rezulta "mere 2"
"a" + "b" + "c" //rezulta "abc"

Şirurile de caractere formate dintr-un singur caracter NU trebuie ı̂nlocuite cu constante


de tip caracter (constantele caracter sunt de fapt numere).
Java dispune şi de operatorul += pentru şiruri de caractere. Efectul instrucţiunii str+=
expr este str = str + expr. Cu alte cuvinte str va referi un nou String generat de str +
expr.
Este important să observăm că ı̂ntre atribuirea:

i=i+5 //i este un intreg

şi atribuirea:

str=str+"hello" //str este un String

există o diferenţă esenţială. În primul caz, variabila i este incrementată cu 5; locaţia de
memorie a lui i nu se modifică. În al doilea caz, se crează un nou string având valoarea
str+”hello”. După atribuire, str va referi acest nou string. Fostul string referit va fi supus
lui garbage-collection dacă nu a existat o altă referinţa către el.
32 CAPITOLUL 3. REFERINŢE

3.3.3 Comparaţia stringurilor


Deoarece operatorul de atribuire funcţionează pe şiruri de caractere, am fi tentaţi să
credem că funcţionează şi operatorii relaţionali. Acest lucru nu este ı̂nsă adevărat.
Conform regulii privind supraı̂ncărcarea operatorilor, operatorii relaţionali ( <, <=, >,
>=) nu sunt definiţi pentru obiecte de tip String. Mai mult, operatorii == şi != au
semnificaţia clasică pentru obiecte de tip referinţă. De exemplu, pentru două obiecte de
tip String, x şi y, expresia x == y este adevărată doar dacă x şi y referă acelaşi obiect de
tip String. Astfel, dacă x şi y referă obiecte diferite cu conţinut identic, expresia x == y
este falsă. Acelaşi raţionament este valabil şi pentru !=.
Pentru a testa egalitatea a două obiecte de tip String, se foloseşte metoda equals. Expresia
x.equals(y) este adevărată dacă şirurile de caractere referite de x şi de y sunt identice.
Un test mai general poate fi realizat cu metoda compareTo. Expresia x.compareTo(y)
compară două obiecte de tip String x şi y. Valoarea returnată este un număr negativ, zero
sau un număr pozitiv dacă x este mai mic, egal, respectiv mai mare decât y.

3.3.4 Alte metode pentru stringuri


Lungimea unui obiect de tip String (un şir vid are lungimea 0) poate fi obţinută cu metoda
length(). Deoarece, length() este o metodă parantezele sunt necesare.
Există două metode pentru a accesa caracterele din interiorul unui String. Metoda charAt
returnează caracterul aflat la poziţia specificată (primul caracter este pe poziţia 0). Metoda
substring returnează o referinţă către un String nou construit. Metoda are ca parametri
poziţia de ı̂nceput şi poziţia primului caracter neinclus.
Iată un exemplu de folosire al acestor metode:
String mesaj = "Hello" ;
int lungimeMesaj = mesaj.length() ; //lungimea este 5
char ch = mesaj.charAt(1) ; //ch este ’e’
String subSir = mesaj.substring( 2 , 4 ) ; //sub este "ll"

3.3.5 Conversia de la string la tipurile primitive


Metoda toString() poate fi utilizată pentru a converti orice tip primitiv la String. De
exemplu, toString(45) returnează o referinţă către şirul nou construit ”45”. Majoritatea
obiectelor furnizează o implementare a metodei toString(). De fapt, atunci când 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, există o variantă
a metodei toString() care permite precizarea unei anumite baze. Astfel, instrucţiunea:
System.out.println( Integer.toString( 55 , 2 ) ) ;
are ca efect tipărirea reprezentării ı̂n baza 2 a numărului 55.
Pentru a converti un String la un int se poate folosi metoda Integer.parseInt(). Această
metodă generează o excepţie dacă String-ul convertit nu conţine o valoare ı̂ntreagă. Despre
excepţii vom vorbi pe scurt ı̂n paragraful 3.5. Pentru a obţine un double dintr-un String
se poate utiliza metoda parseDouble(). Iată două exemple:
int x = Integer.parseInt( "75" ) ;
double y = Double.parseDouble( "3.14" ) ;
3.4. ŞIRURI 33

3.4 Şiruri
Şirurile sunt structura fundamentală prin care se pot reţine mai multe elemente de acelaşi
tip. În Java, şirurile nu sunt tipuri primitive; ele se comportă foarte asemănător cu un
obiect. Din acest motiv, multe dintre regulile care sunt valabile pentru obiecte se aplică
şi la şiruri.
Fiecare element dintr-un şir poate fi accesat prin mecanismul de indiciere oferit de opera-
torul [ ]. Spre deosebire de limbajele C sau C++, Java verifică validitatea indicilor3 .
În Java, ca şi ı̂n C, şirurile sunt ı̂ntotdeauna indiciate de la 0. Astfel, un şir a cu 3
elemente este format din a[0], a[1], a[2]. Numărul de elemente care pot fi stocate ı̂n şirul a
este permanent reţinut ı̂n variabila a.length. Observaţi că aici (spre deosebire de String-uri)
nu se pun paranteze. O parcurgere tipică pentru un şir ar fi:

for( int i = 0 ; i < a.length ; i++ )

3.4.1 Declaraţie, Atribuire şi Metode


Un şir de elemente ı̂ntregi se declară astfel:

int[] sir1 ;

Deoarece un şir este un obiect, declaraţia de mai sus nu alocă memorie pentru şir. Variabila
sir1 este doar un nume (referinţă) pentru un şir de numere ı̂ntregi, şi ı̂n acest moment
valoarea ei este null. Pentru a aloca 100 de numere ı̂ntregi, vom folosi instrucţiunea:

sir1 = new int[100];

Acum sir1 este o referinţă către un şir de 100 de numere ı̂ntregi.


Există şi alte posibilităţi de a declara şiruri. De exemplu, putem unifica declaraţia şi
alocarea de memorie:

int[] sir1 = new int[100];

Se pot folosi şi liste de iniţializare, ca ı̂n C sau C++. În exemplul următor se alocă un şir
cu patru elemente, care va fi referit de către variabila sir2:

int[] sir2 = {3, 4, 6, 19} ;

Parantezele pătrate pot fi puse fie ı̂nainte fie după numele şirului. Plasarea parantezelor
ı̂nainte de nume face mai vizibil faptul că este vorba de un şir, de aceea vom folosi această
notaţie.
Declararea unui şir de obiecte (deci nu tipuri primitive) foloseşte aceeaşi sintaxă. Trebuie
să reţineţi ı̂nsă că după alocarea şirului, fiecare element din şir va avea valoarea null.
Pentru fiecare element din şir trebuie alocată memorie separat. De exemplu, un şir cu 5
cercuri se construieşte astfel:
3
Acesta este un lucru foarte important care vine ı̂n ajutorul programatorilor, mai ales a celor ı̂ncepători.
Indicii acăror valoare depăşesc numărul de elemente alocat sunt adeseori cauza multor erori obscure ı̂n C
şi C++. În Java, accesarea unui şir cu un indice ı̂n afara limitei este imediat semnalată prin excepţia
IndexOutOfBoundsException.
34 CAPITOLUL 3. REFERINŢ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 ilustrează modul de folosire al şirurilor ı̂n Java. În jocul de
loterie, şase numere de la 1 la 49 sunt selectate săptămânal. Programul alege aleator
numere pentru 1000 de jocuri. Programul afişează apoi de câte ori a apărut fiecare număr
ı̂n cele 1000 de jocuri. Linia 14 declară un şir de numere ı̂ntregi care reţine numărul de
apariţii ale fiecărui număr. Deoarece indicierea şirurilor ı̂ncepe de la 0, adunarea cu 1 este
esenţială. Fără această adunare am fi avut un şir cu elemente de la 0 la 48, şi orice acces
la elementul cu indicele 49 ar fi generat o excepţie IndexOutOfBoundsException. Ciclul
din liniile 15-18 iniţializează valorile şirului cu 0. Restul programului este relativ simplu.
Se foloseşte din nou metoda Math.random() care generează un număr ı̂n intervalul [0,1).
Rezultatele sunt afişate ı̂n liniile 28-31.
Dat fiind faptul că şirul este un tip referinţă, operatorul = nu copiază şiruri. De aceea
dacă x şi y sunt şiruri, efectul secvenţei de instrucţiuni:
int[] x = new int[100] ;
int[] y = new int[100] ;
...
x = y ;
este că x şi y referă acum al doilea şir.
Şirurile pot fi utilizate ca parametri pentru metode. Regulile de transmitere se deduc logic
din faptul că şirul este o referinţă. Să presupunem că avem o metodă f care acceptă un
şir de int ca parametru. Apelul şi definirea arată astfel:
f( sirActual ) ; //apelul metodei
void f( int[] sirFormal) ; //declaratia metodei
Conform convenţiilor de transmitere a parametrilor ı̂n Java pentru tipurile referinţă, vari-
abilele sirActual şi sirFormal referă acelaşi obiect. Astfel, accesul la sirFormal[i] este de
fapt un acces la sirActual[i]. Aceasta ı̂nseamnă că variabilele conţinute ı̂n şir pot fi mod-
ificate de către metodă. O observaţie importantă este aceea că linia de cod din cadrul lui
f():
sirFormal = new int [20] ;
nu are nici un efect asupra lui sirActual. Acest lucru se datorează faptului că ı̂n Java
transmiterea parametrilor se face prin valoare, deci sirFormal este o nouă referinţă către
şir. Instrucţiunea de mai sus nu face decât să schimbe şirul către care referă sirFormal
(vezi Figura 3.3).
3.4. ŞIRURI 35
sirActual sirActual sirFormal sirActual sirFormal


❍ ❍

❥ ✟

✙ ❍

❥ ❍❍

a. ı̂nainte de apel b. imediat după apel c. după atribuirea sirFormal=new int[20];


Figura 3.3 Transmiterea parametrilor ı̂n Java
Deoarece numele şirurilor sunt doar nişte referinţe, o funcţie poate să returneze un şir.
1. //clasa demonstrativa pentru siruri
2. public class Loterie
3. {
4. //genereaza numere de loterie intre 1 si 49
5. //afiseaza numarul de aparitii al fiecarui numar
6. //declaratii constante:
7. public static final int NUMERE = 49 ;
8. public static final int NUMERE_PE_JOC = 6 ;
9. public static final int JOCURI = 1000 ;
10. //main
11. public static final void main( String[] args )
12. {
13. //genereaza numerele
14. int [] numere = new int[NUMERE+1] ;
15. for( int i=0 ; i < numere.length; ++i )
16. {
17. numere[i] = 0 ;
18. }
19.
20. for(int i =0 ; i < JOCURI; ++i)
21. {
22. for( int j = 0 ; j < NUMERE_PE_JOC; ++j )
23. {
24. numere[ (int)(Math.random()*49)+1 ] ++ ;
25. }
26. }
27. //afisare rezultate
28. for( int k = 1 ; k <= NUMERE; ++k )
29. {
30. System.out.println( k + ": " + numere[k] ) ;
31. }
32. }
33. }
Figura 3.4 Program demonstrativ pentru şiruri

3.4.2 Expansiunea dinamică a şirurilor


Să presupunem că dorim să citim o secvenţă de numere şi să o reţinem ı̂ntr-un şir. Una
dintre proprietăţile fundamentale ale şirurilor este aceea că ı̂nainte de a fi utilizate, trebuie
36 CAPITOLUL 3. REFERINŢE

să alocăm memorie pentru un număr fix de elemente care vor fi stocate. Dacă nu ştim de
la bun ı̂nceput câte elemente vor fi stocate ı̂n şir, va fi dificil să alegem o valoare rezonabilă
pentru dimensiunea şirului. Această secţiune prezintă o metodă prin care putem extinde
dinamic şirul dacă dimensiunea iniţială se dovedeşte a fi prea mică. Această tehnică poartă
numele de expansiune dinamică a şirurilor şi permite alocarea de şiruri de dimensiune
arbitrară pe care le putem redimensiona pe măsură ce programul rulează.
Alocarea obişnuită de memorie pentru şiruri se realizează astfel:

int[] a = new int[10] ;

Să presupunem că după ce am făcut această declaraţie, hotărâm că de fapt avem nevoie
de 12 elemente şi nu de 10. În această situaţie putem folosi următoarea manevră:

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 gândire este suficient pentru a ne convinge că această operaţie este con-
sumatoare de resurse, deoarece trebuie copiat originalul ı̂napoi ı̂n noul şir a. De exemplu,
dacă extensia dinamică a numărului de elemente ar trebui făcută ca răspuns la citirea
de date, ar fi ineficient să expansionăm ori de câte ori citim câteva elemente. Din acest
motiv, de câte ori se realizează o extensie dinamică, numărul de elemente este crescut cu
un coeficient multiplicativ. Am putea de exemplu dubla numărul de elemente la fiecare
expansiune dinamică. Astfel, dintr-un şir cu N elemente, generăm un şir cu 2N elemente,
iar costul expansiunii este ı̂mpărţit ı̂ntre cele N elemente care pot fi inserate ı̂n şir fără a
realiza extensia.
Pentru a face ca lucrurile să fie mai concrete, Figura 3.5 prezintă un program care citeşte
un număr nelimitat de numere ı̂ntregi de la tastatură şi le reţine ı̂ntr-un şir a cărui dimen-
siune este extinsă dinamic. Funcţia resize realizează expansiunea (sau contracţia!) şirului
returnând o referinţă către un şir nou construit. Similar, metoda getInts returnează o
referinţă către şirul ı̂n care sunt citite elementele.
La ı̂nceputul lui getInts(), nrElemente este iniţializat cu 0 şi ı̂ncepem cu un şir cu 5 e-
lemente. În linia 36 citim ı̂n mod repetat câte un element. Dacă şirul este ”umplut”,
lucru indicat de intrarea ı̂n testul de la linia 36, atunci şirul este expansionat prin apelul
metodei resize. Liniile 51-61 realizează expansiunea şirului folosind strategia prezentată
anterior. La linia 40, elementul citit este stocat ı̂n tablou, iar numărul de elemente citite
este incrementat. În final, ı̂n lina 48 contractăm şirul la numărul de elemente citite efectiv.

1. import java.io.* ;
2. //clasa pentru citirea unui numar nelimitat de valori intregi
3. public class ReadInts
4. {
5. public static void main( String[] args )
6. {
7. int [] array = getInts( ) ;
3.4. Ş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. REFERINŢ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ăr nelimitat de numere ı̂ntregi urmată de
afişarea lor

3.4.3 Şiruri cu mai multe dimensiuni


În anumite situaţii trebuie să stocăm datele ı̂n şiruri cu mai multe dimensiuni. Cel mai
des folosite sunt matricele, adică şirurile cu două dimensiuni. Alocarea de memorie pentru
şiruri cu mai multe dimensiuni se realizează precizând numărul de elemente pentru fiecare
indice, iar indicierea se face plasând fiecare indice ı̂ntre paranteze pătrate. Ca un exemplu,
declaraţia:

int[][] x = new int[2][3] ;

defineşte 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ă


Parametrii transmişi ı̂n linie de comandă sunt disponibili prin examinarea parametrilor
funcţiei main. Şirul de stringuri numit args din funcţia main conţine parametri transmişi
ı̂n linie de comandă. De exemplu, dacă avem un program numit Echo.java pe care ı̂l
executăm cu comanda:

java Echo buna ziua

parametrul args[0] va fi o referinţă către stringul ”buna”, iar parametrul args[1] va fi o


referinţă către ”ziua”. Astfel, programul de mai jos implementează comanda echo exis-
tentă atât ı̂n DOS cât şi ı̂n Linux (de fapt, implementează o versiune simplificată a acestei
comenzi care afişează la consolă şirurile trimise ca argument):

1. public class Echo


2. {
3. //afiseaza parametrii primiti in linie de comanda
4. public static void main(String[] args)
5. {
6. if( args.length == 0 )
7. {
8. System.out.println( "Nu exista argumente" ) ;
3.5. TRATAREA EXCEPŢIILOR 39

9. return ;
10. }
11. for( int i=0 ; i < args.length; ++i)
12. {
13. System.out.print( args[i] ) ;
14. }
15. }
16. }

3.5 Tratarea excepţiilor


Excepţiile sunt obiecte care reţin informaţie şi care sunt transmise ı̂n afara secvenţelor
return. Excepţiile sunt propagate ı̂napoi prin secvenţa de funcţii apelate până când o anu-
mită metodă prinde excepţia. Excepţiile sunt folosite pentru a semnala situaţii excepţie,
cum ar fi erorile.
De fiecare dată când o excepţie este ı̂ntâlnită ı̂ntr-o metodă a unei clase, programul va
permite recuperarea pierderilor cauzate de excepţie şi se va ı̂ncheia fără a cauza căderea
sistemului. Prin pregătirea tratării condiţiilor de excepţie ı̂n care va ajunge execuţia
programului, se va crea un program mult mai prietenos pentru utilizator. Un program Java
poate detecta erori şi apoi indica sistemului de execuţie ce erori a ı̂ntâlnit prin generarea
unor condiţii de excepţie ce vor duce la oprirea execuţiei şi afişarea unui cod de eroare,
sau dacă doriţi să trataţi unele excepţii ı̂ntr-un mod propriu, puteti folosi o clauză catch
pentru a obţine controlul ı̂ntr-o situaţie de excepţie.

3.5.1 Procesarea excepţiilor


Secvenţa de cod din Figura 3.6 prezintă modul de folosire al excepţiilor. Secvenţa de
cod care ar putea genera o excepţie care să fie propagată este inclusă ı̂ntr-un bloc try.
Blocul try se extinde de la linia 12 la linia 16. Imediat după blocul try trebuie să apară
secvenţele de tratare a excepţiilor. Programul ”sare” la secvenţa de tratare a excepţiilor
doar ı̂n situaţia ı̂n care se generează o excepţie; ı̂n momentul generării excepţiei, blocul
try din care excepţia provine se consideră a fi ı̂ncheiat.
Fiecare dintre blocurile catch este ı̂ncercat pe rând până când se găseşte o secvenţă de
tratare adecvată. Deoarece Exception se potriveşte cu toate excepţiile generate, ea este
adecvată pentru orice fel de excepţie generată ı̂n blocul try. Excepţiile generate de secvenţa
try din programul nostru pot fi IOException generate de readLine dacă apare o eroare la
citire, şi NumberFormatException generată de parseInt dacă linia citită de la tastatură nu
poate fi convertită la int.
În cazul unei excepţii se execută codul din blocul catch -ı̂n situaţia noastră linia 19. După
aceasta blocul catch şi blocul conţinând cuplul try/catch se consideră ı̂ncheiate. Un mesaj
referitor la eroarea generată este tipărit folosind obiectul de tip Exception numit e. Putem
alege să facem şi alte acţiuni ı̂n caz de eroare, cum ar fi furnizarea de mesaje de eroare
mai detaliate etc.

1. import java.io.* ;
2.
40 CAPITOLUL 3. REFERINŢ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 excepţiilor

Se pot preciza diferite tipuri de instrucţiuni pentru fiecare tip de excepţie ı̂ntâlnită uti-
lizând instrucţiunea throw. De asemenea, puteţi avea propriile obiecte definite ca excepţii,
prin care să trataţi anumite evenimente speciale care se pot ı̂ntâmpla pe timpul execuţiei
programului. Pentru a vă crea o clasă proprie de excepţii, trebuie ca aceasta să fie o
subclasă4 a clasei Exception.

public class NewException extends Exception


{
...
}

Acum puteţi trata excepţia ı̂n modul dorit:

...
try
{
...
throw new NewException(e);
...
}
catch(NewException e)
4
Despre conceptul de moştenire vom discuta ı̂n capitolul 5.
3.5. TRATAREA EXCEPŢIILOR 41

{
...//tratarea noii exceptii
}
catch(Exception e)
{
...//tratarea unor erori generale
}
...

După cum am mai spus după tratarea unei excepţii este ignorată orice secvenţă de
instrucţiuni. Totuşi, dacă este neapărată nevoie să se execute anumite operaţii, veţi invoca
clauza finally. Blocul finally este executat şi ı̂n cazul ı̂n care nu are loc nici o excepţie ı̂n
blocul try.

try
{
...
}
finally
{
...
}

3.5.2 Excepţii uzuale


Există mai multe tipuri de excepţii standard ı̂n Java. O primă categorie de excepţii o con-
stituie excepţiile de execuţie standard (standard runtime exceptions) cum ar fi ı̂mpărţirea
unui ı̂ntreg la 0 sau accesarea unui element de tablou cu indice ilegal. Având ı̂n vedere
faptul că aceste excepţii pot apărea practic ı̂n orice secvenţă de program, ar fi o muncă
sisifică să definim secvenţe de tratare a unor astfel de excepţii. Dacă se furnizează un bloc
catch aceste excepţii se comportă exact la fel ca celelalte excepţii. Dacă apare o excepţie
standard pentru care nu există un bloc catch aceasta se propagă normal, trecând chiar şi de
funcţia main. În această situaţie, excepţia produce o terminare anormală a programului,
ı̂nsoţită de un mesaj de eroare. Figura 3.7 prezintă câteva dintre cele mai uzuale erori
standard de execuţie.

EXCEPŢIE STANDARD DE EXECUŢIE SEMNIFICAŢIE

ArithmeticException Depăşire sau ı̂mpărţirea unui ı̂ntreg la 0


NumberFormatException Conversie nepermisă a unui String la un tip numeric
IndexOutOfBoundsException Indice ilegal ı̂ntr-un şir sau String
NegativeArraySizeException Tentativă de a crea un şir cu nr. negativ de elemente
NullPointerException Tentativă de a folosi o referinţă care are valoarea null
SecurityException Încălcare de securitate ı̂n timpul execuţiei

Figura 3.7 Câteva excepţii de execuţie uzuale.


42 CAPITOLUL 3. REFERINŢE

Majoritatea excepţiilor sunt de tipul excepţii standard tratate (standard checked excep-
tions). Dacă se apelează o metodă care poate genera direct sau indirect o astfel de
excepţie, atunci programatorul fie trebuie să o trateze cu un bloc de tip catch sau să
indice explicit faptul că excepţia urmează să fie propagată prin folosirea clauzei throws
ı̂n antetul metodei. Reţineţi faptul că excepţia tot va trebui tratată la un moment dat,
deoarece metoda main (care este la ultimul nivel) nu poate avea o clauză throws.

EXCEPŢIE STANDARD TRATATĂ SEMNIFICAŢIE

java.io.EOFException Terminare de fişier ı̂nainte de ı̂ncheierea citirii


java.io.FileNotFoundException Fişierul nu a fost găsit pentru a fi deschis
java.io.IOException Cuprinde majoritatea erorilor de intrare/ieşire
InterruptedException Aruncată de metoda Thread.Sleep

Figura 3.8 Excepţii standard tratate uzual

Ultima categorie de excepţii sunt erorile care nu sunt prinse de Exception. De obicei aceste
excepţii nu pot fi tratate. Cea mai uzuală este OutOfMemoryError. Pentru a prinde orice
excepţie posibilă, prindeţi un obiect de tip Throwable şi utilizaţi clauza throws. Această
clauză apare ı̂n antetul unei metode pentru a specifica tipurile de excepţii ce pot fi lansate
ı̂n metoda respectivă. Numele acestor tipuri sunt clase derivate din clasa Exception care
la rândul ei este derivată din clasa Throwable. Clasele Exception şi Throwable se află ı̂n
java.lang.

3.6 Intrare şi ieşire


Intrarea şi ieşirea ı̂n Java se realizează cu ajutorul claselor din pachetul java.io. Din
acest motiv, orice program care foloseşte rutinele de intrare/ieşire trebuie să cuprindă
instrucţiunea:

import java.io.* ;

Biblioteca Java de intrare/ieşire este extrem de sofisticată şi are un număr foarte mare
de opţiuni. În această fază vom examina doar elementele de bază referitoare la intrare şi
ieşire, concentrându-ne ı̂n ı̂ntregime asupra operaţiilor de intrare-ieşire formatate.

3.6.1 Operaţii de bază pe fluxuri (stream-uri)


Există trei fluxuri predefinite pentru operaţii I/O de la terminal: System.in, intrarea
standard, System.out, ieşirea standard şi System.err, fluxul de erori.
Aşa cum am arătat deja, metodele print şi println sunt folosite pentru afişare formatată.
Orice fel de tip poate fi convertit la o formă tipăribilă, folosind metoda toString; ı̂n multe
cazuri, acest lucru este realizat automat. Spre deosebire de C şi C++ care dispun de un
număr enorm de opţiuni de formatare, afişarea ı̂n Java se face exclusiv prin concatenare
de String-uri, fără nici o formatare.
O modalitate simplă de a realiza citirea este aceea de a citi o singură linie ı̂ntr-un obiect
3.6. INTRARE ŞI IEŞIRE 43

de tip String folosind readLine. Metoda readLine preia caractere de la intrare până când
ı̂ntâlneşte un terminator de linie sau sfârşit de fişier. Metoda returnează caracterele citite
(din care extrage terminatorul de linie) ca un String nou construit. Pentru a putea folosi
readLine trebuie să construim un obiect BufferedReader dintr-un obiect InputStreamReader
care la rândul său este construit din System.in. Acest lucru a fost ilustrat ı̂n Figura 3.6,
liniile 8 şi 9.
Dacă primul caracter citit este EOF, atunci funcţia readLine returnează null. Dacă apare
o eroare la citire se generează o excepţie de tipul IOException. Dacă şirul citit nu poate fi
convertit la o valoare ı̂ntreagă se generează o NumberFormatException.

3.6.2 Obiectul StringTokenizer


Să ne reamintim că pentru a citi o valoare primitivă, cum ar fi int, foloseam readLine
pentru a prelua linia ca pe un String şi apoi aplicam metoda parseInt pentru a converti
stringul la tipul primitiv.
În multe situaţii avem mai multe elemente pe aceeaşi linie. Să presupunem, de exemplu,
că fiecare linie are două valori ı̂ntregi. Java furnizează clasa StringTokenizer pentru a
separa String-ul ı̂n elemente lexicale (engl. token ). Pentru a putea folosi obiecte ale clasei
StringTokenizer se foloseşte directiva:

import java.util.* ;

Folosirea obiectului StringTolenizer este prezentată ı̂n programul din Figura 3.9. Obiectul
StringTokenizer este construit ı̂n linia 19 prin furnizarea obiectului String reprezentând
linia citită. Metoda countTokens din linia 20 ne va da numărul de cuvinte citite (elemente
lexicale). Metoda nextToken ı̂ntoarce următorul cuvânt necitit ca pe un String. Această
ultimă metodă generează o NoSuchElementException dacă nu există nici un cuvânt rămas,
dar aceasta este o excepţie de execuţie standard (vezi 3.5.2) şi nu trebuie prinsă. La liniile
25 şi 26 folosim nextToken urmată de parseInt pentru a obţine un int. Toate erorile sunt
prinse ı̂n blocul catch.
Implicit, elementele lexicale sunt separate de spaţii. Obiectul StringTokenizer poate fi ı̂nsă
construit şi ı̂n aşa fel ı̂ncât să recunoască şi alte caractere drept delimitatori.

1. //program pentru exemplificarea clasei


2. //StringTokenizer. Programul citeste doua numere aflate pe
3. //aceeasi linie si calculeaza maximul lor
4.
5. import java.io.* ;
6. import java.util.* ;
7.
8. public class TokenizerTest
9. {
10. public static void main(String[] args)
11. {
12. BufferedReader in = new BufferedReader(
13. new InputStreamReader(System.in) ) ;
14.
44 CAPITOLUL 3. REFERINŢ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 Fişiere secvenţiale


Una dintre regulile fundamentale ı̂n Java este aceea că ceea ce se poate face pentru operaţii
I/O de la terminal se poate face şi pentru operaţii I/O din fişiere. Pentru a lucra cu fişiere
obiectul BufferedReader nu se construieşte pe baza unui InputStreamReader. Vom folosi
ı̂n schimb un obiect de tip FileReader care va fi construit pe baza unui nume de fişier.
Un exemplu pentru ilustrarea acestor idei este prezentat ı̂n Figura 3.10. Acest program
listează conţinutul fişierelor text care sunt precizate ca parametri ı̂n linie de comandă.
Funcţia main parcurge pur şi simplu argumentele din linia de comandă, apelând metoda
listFile pentru fiecare argument. În metoda listFile construim obiectul de tip FileReader
la linia 26 şi apoi ı̂l folosim pe acesta pentru construirea obiectului de tip BufferedReader
ı̂n linia următoare. Din acest moment, citirea este identică cu cea de la tastatură.
După ce am ı̂ncheiat citirea din fişier, trebuie să ı̂nchidem fişierul, altfel s-ar putea să nu
mai putem deschide alte stream-uri (să atingem limita maximă de stream-uri care pot
fi deschise). Nu putem face acest lucru ı̂n primul bloc try, deoarece o excepţie ar putea
genera părăsirea prematură a blocului şi fişierul nu ar fi ı̂nchis. Din acest motiv, fişierul
este ı̂nchis după secvenţa try/catch.
Scrierea formatată ı̂n fişiere este similară cu citirea formatată.

1. //program pentru afisarea de fisiere


2. //al caror nume este precizat in linia de comanda
3. import java.io.* ;
4.
5. public class Lister
3.6. INTRARE ŞI IEŞIRE 45

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 conţinutului unui fişier


46 CAPITOLUL 3. REFERINŢE

3.7 Probleme propuse


1. O sumă de verificare este un ı̂ntreg pe 32 de biti (int) care este suma codurilor
Unicode ale caracterelor dintr-un fişier (se permite depăşirea, deşi aceasta este puţin
probabilă dacă toate caracterele sunt ASCII). Două fişiere identice au aceeaşi sumă
de verificare. Scrieţi un program care calculează suma de verificare pentru un fişier
care este furnizat ca parametru ı̂n linie de comandă.

2. Modificaţi programul Echo.java din acest capitol astfel ı̂ncât dacă nu se transmit
parametri de la linia de comandă să folosească intrarea standard.

3. Scrieţi o metodă care returnează true dacă stringul str1 este prefix pentru stringul
str2. Nu folosiţi nici o altă metodă generală de căutare pe stringuri ı̂n afară de
charAt.
Capitolul 4

Obiecte şi clase

În acest capitol vom ı̂ncepe să discutăm despre programarea orientată pe obiecte (object
oriented programming - OOP). O componentă fundamentală a programării orientate pe
obiecte este specificarea, implementarea şi folosirea obiectelor. În capitolul anterior am
văzut deja căteva exemple de obiecte, cum ar fi string-urile şi fişierele (stream-urile) care
fac parte din bibliotecile limbajului Java. Am putut observa şi faptul că fiecare obiect este
caracterizat de o anumită stare care poate fi modificată prin aplicarea operatorului punct
(.). În limbajul Java, starea şi funcţionalitatea unui obiect se definesc prin intermediul
unei clase. Un obiect este de fapt o instanţă a unei clase.

4.1 Ce este programarea orientată pe obiecte?


Programarea orientată pe obiecte s-a impus ca modelul dominant al anilor ’90. În această
secţiune vom prezenta modul ı̂n care Java suportă programarea orientată pe obiecte şi
vom menţiona câteva dintre principiile ei fundamentale.
În centrul programării orientate pe obiecte se află noţiunea de obiect. Obiectul este o
variabilă care are o structură şi o stare. Fiecare obiect dispune de operaţii prin intermediul
cărora i se poate manipula starea. Aşa cum am văzut deja, ı̂n limbajul Java se face
distincţie ı̂ntre un obiect şi o variabilă de un tip primitiv, dar aceasta este o specificitate a
limbajului Java şi nu a programării orientate pe obiecte. Pe lângă operaţiile cu un caracter
general, asupra obiectelor mai putem să realizăm:

• Crearea de noi obiecte, ı̂nsoţită eventual de iniţializarea obiectelor

• Copierea şi testarea egalităţii

• Realizarea de operaţii de intrare/ieşire cu obiectele

Obiectul trebuie privit ca o unitate atomică pe care utilizatorul nu ar trebui să o disece.
În mod normal, nu ne punem problema de a jongla cu biţii din care este format un număr
reprezentat ı̂n virgulă mobilă şi ar fi de-a dreptul ridicol să ı̂ncercăm să incrementăm un
astfel de număr prin modificarea directă a reprezentării sale interne.
Principiul atomicităţii este cunoscut sub numele de ascunderea informaţiei. Utilizatorul
nu are acces direct la părţile unui obiect sau la implementarea sa. Acestea vor putea
fi accesate doar prin intermediul metodelor care au fost furnizate ı̂mpreună cu obiectul.

47
48 CAPITOLUL 4. OBIECTE ŞI CLASE

Putem privi fiecare obiect ca fiind ambalat cu mesajul ”Nu deschideţi! Nu conţine compo-
nente reparabile de către utilizator!”. În viaţa de zi cu zi, majoritatea celor care ı̂ncearcă să
repare componente cu această inscripţie sfârşesc prin a face mai mult rău decât bine. Din
acest punct de vedere, programarea imită lumea reală. Gruparea datelor şi a operaţiilor
asupra acestor date ı̂n acelaşi ı̂ntreg (agregat), având grijă să ascundem detaliile de im-
plementare ale agregatului este cunoscută sub numele de ı̂ncapsulare.
Unul dintre principalele scopuri ale programării orientate pe obiecte este refolosirea codu-
lui. La fel cum inginerii refolosesc din nou şi din nou aceleaşi componente ı̂n proiectare,
programatorii ar trebui să refolosească obiectele ı̂n loc să le reimplementeze. Atunci când
avem deja un obiect care implementează exact comportamentul pe care ı̂l dorim, refolosirea
nu pune nici un fel de probleme. Adevărata provocare apare atunci când dorim să folosim
un obiect care deja există, dar care, deşi are un comportament foarte similar cu ceea ce
vrem, nu corespunde exact cu necesităţile noastre.
Limbajele de programare orientate pe obiecte furnizează mai multe mecanisme ı̂n acest
scop. Unul dintre mecanisme este folosirea codului generic. Dacă implementarea este
identică, şi diferă doar tipul de bază al obiectului, nu este necesar să rescriem complet co-
dul ei: vom scrie ı̂n schimb un cod generic care funcţionează pentru orice tip. De exemplu,
algoritmul de sortare al unui şir de obiecte nu depinde de obiectele care sunt sortate, deci
se poate implementa un algoritm generic de sortare.
Moştenirea este un alt mecanism care permite extinderea funcţionalităţii unui obiect. Cu
alte cuvinte, putem crea noi tipuri de date care să extindă (sau să restricţioneze) pro-
prietăţile tipului de date original.
Un alt principiu important al programării orientate pe obiecte este polimorfismul. Un tip
referinţă polimorfic poate să refere obiecte de mai multe tipuri. Atunci când se apelează o
metodă a tipului polimorfic, se va selecta automat metoda care corespunde tipului referit
ı̂n acel moment.
Un obiect ı̂n Java este o instanţă a unei clase. O clasă este similară cu un tip record din
Pascal sau cu o structură din C, doar că există două ı̂mbunătăţiri majore. În primul rând,
membrii pot fi atât funcţii cât şi date, numite ı̂n acest context metode respectiv atribute. În
al doilea rând, domeniul de vizibilitate al acestor membri poate fi restricţionat. Deoarece
metodele care manipulează starea obiectului sunt membri ai clasei, ele sunt accesate prin
intermediul operatorului punct, la fel ca şi atributele. În terminologia programării orien-
tate pe obiecte, atunci când apelăm o metodă a obiectului spunem că ”trimitem un mesaj”
obiectului.

4.2 Un exemplu simplu


Să ne amintim că, atunci când proiectăm o clasă, este important să ascundem detaliile
interne faţă de utilizatorul clasei. Clasa poate să ı̂şi definească funcţionalitatea prin inter-
mediul metodelor. Unele dintre aceste metode vor descrie cum se creează şi iniţializează
o instanţă a clasei, cum se realizează testele de egalitate şi cum se scrie starea clasei.
Celelalte metode sunt specifice structurii particulare pe care o are clasa. Ideea este că
utilizatorul nu trebuie să aibă dreptul de a modifica direct starea obiectului, el va trebui
să folosească metodele clasei pentru a realiza acest lucru. Această idee poate fi impusă
prin ascunderea anumitor membri faţă de utilizator. Pentru a realiza aceasta vom preciza
4.2. UN EXEMPLU SIMPLU 49

ca aceşti membri să fie stocaţi ı̂n secţiunea private. Compilatorul va avea grijă ca membri
din secţiunea private să fie inaccesibili utilizatorului acelui obiect. În general, toate datele
membru ar trebui să fie declarate private.
Figura 4.1 prezintă modul de definire al unei clase care modelează un cerc. Definirea
clasei constă ı̂n două părţi: public şi private. Secţiunea public reprezintă porţiunea care
este vizibilă pentru utilizatorul obiectului. Deoarece datele sunt ascunse faţă de utiliza-
tor, secţiunea public va conţine ı̂n mod normal numai metode şi constante. În exemplul
nostru avem două metode pentru a scrie şi a citi raza obiectelor de tip Circle. Celelalte
două metode calculează aria respectiv lungimea obiectului de tip Circle. Secţiunea private
conţine datele: acestea sunt invizibile pentru utilizatorul obiectului. Atributul radius va
trebui accesat doar prin intermediul metodelor publice setRadius şi 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 prezintă modul de folosire al unui obiect de tip Circle. Deorece setRadius,
getRadius, area şi length sunt membri ai clasei, ei sunt accesaţi folosind operatorul punct.
50 CAPITOLUL 4. OBIECTE ŞI CLASE

Atributul radius ar fi putut şi el să fie accesat folosind operatorul punct, dacă nu ar fi
fost declarat de tip private. Accesarea lui radius din linia 15 ar fi fost ilegală dacă nu era
comentată.
Să rezumăm terminologia ı̂nvăţată. Clasa conţine membri care pot fi atribute (câmpuri,
date) sau metode (funcţii). Metodele pot acţiona asupra atributelor şi pot apela alte
metode. Modificatorul de vizibilitate public ı̂nseamnă că membrul respectiv este accesibil
oricui prin intermediul operatorului punct. Modificatorul de vizibilitate private ı̂nseamnă
că membrul respectiv este accesibil doar metodelor clasei. Dacă nu se pune nici un mo-
dificator de vizibilitate, atunci accesul la membru este de tip friendly, despre care vom
vorbi mai târziu. Mai există şi un al patrulea modificator, numit protected pe care ı̂l vom
prezenta ı̂n capitolul următor.
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ă simplă de testare a clasei Circle

4.3 Metode uzuale


Există metode care sunt comune pentru toate clasele. Alte metode definesc comportamente
specifice unei anumite clase. În această secţiune vom prezenta metodele care sunt comune
tuturor claselor: constructorii, modificatorii, accesorii, toString şi equals.

4.3.1 Constructori
Aşa cum am menţionat deja, una dintre proprietăţile fundamentale ale obiectelor este că
acestea pot fi definite şi, eventual, iniţializate. În limbajul Java, metoda care controlează
modul ı̂n care un obiect este creat şi iniţializat este constructorul. Deoarece Java permite
supraı̂ncărcarea metodelor, o clasă poate să definească mai mulţi constructori.
Dacă la definirea clasei nu se furnizează nici un constructor, cum este cazul clasei Circle
din Figura 4.1, compilatorul creează un constructor implicit care iniţializează fiecare
dată membru cu valorile implicite. Aceasta ı̂nseamnă că atributele de tipuri primitive
4.3. METODE UZUALE 51

sunt iniţializate cu 0, iar atributele de tip referinţă sunt iniţializate cu null. Astfel, ı̂n
cazul nostru, atributul radius va avea implicit valoarea 0.
Pentru a furniza un constructor, vom scrie o metodă care are acelaşi nume cu clasa şi
care nu returnează nimic. În Figura 4.3 avem doi constructori: unul ı̂ncepe la linia 8,
iar celălalt la linia 16. Folosind aceşti doi constructori vom putea construi obiecte de tip
Date ı̂n următoarele moduri:

Date d1 = new Date( ) ;


Date d2 = new Date( 15, 3, 2000) ;

De remarcat faptul că odată ce aţi definit un constructor pentru clasă, compilatorul nu
mai generează constructorul implicit fără parametri. Dacă aveţi nevoie de un astfel de
constructor, va trebui să ı̂l scrieţi. Astfel constructorul din linia 8 trebuie definit obligatoriu
pentru a putea construi un obiect de tipul celui referit de către d1.

1. //clasa Java simpla pentru stocarea unei


2. //date calendaristice
3. //nu se face validarea datelor
4.
5. public class Date
6. {
7. //constructor fara parametri
8. public Date( )
9. {
10. day = 1 ;
11. month = 1 ;
12. year = 2000 ;
13 }
14.
15. //constructor cu trei parametri
16. public Date( int theDay, int theMonth, int theYear )
17. {
18. day = theDay ;
19. month = theMonth ;
20. year = theYear ;
21. }
22.
23. //test de egalitate
24. //intoarce true daca Obiectul x
25. //este egal cu obiectul curent
26. public boolean equals( Object x )
27. {
28. if( ! (x instanceof Date) )
29. return false ;
30.
31. Date date = (Date) x ;
32. return date.day == day && date.month == month
52 CAPITOLUL 4. OBIECTE ŞI CLASE

33. && date.year == year ;


34. }
35.
36. //conversie la String
37. public String toString()
38. {
39. return day + "/" + month + "/" + year ;
40. }
41.
42. //atribute
43. private int day ;
44. private int month ;
45. private int year ;
46.}

Figura 4.3 O clasă Date minimală care ilustrează constructorii şi metodele equals şi
toString

4.3.2 Modificatori şi Accesori

Atributele sunt declarate de obicei ca fiind private. Aceasta ı̂nseamnă că ele nu vor putea
fi direct accesate de către rutinele care nu aparţin clasei. Există totuşi multe situaţii ı̂n
care dorim să examinăm sau chiar să modificăm valoarea unui atribut.
O posibilitate este aceea de a declara atributele ca fiind public. Aceasta este ı̂nsă o alegere
proastă, deoarece ı̂ncalcă principiul ascunderii informaţiei. Putem ı̂nsă scrie metode care să
examineze sau să modifice valoarea fiecărui câmp. O metodă care citeşte, dar nu modifică
starea unui obiect este numită accesor. O metodă care modifică starea unui obiect este
numită modificator (engl. mutator).
Cazuri particulare de accesori şi modificatori sunt cele care acţionează asupra unui singur
câmp. 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 regulă cu set, cum ar fi setRadius.
Avantajul folosirii unui modificator este că acesta poate verifica dacă starea obiectului este
corectă. Astfel un modificator care alterează câmpul day al unui obiect de tip Date poate
verifica corectitudinea datei care rezultă.

4.3.3 Afişare şi toString

În general, dorim să afişăm starea unui obiect folosind metoda print. Pentru a putea face
acest lucru trebuie definită o metodă cu numele de toString. Această metodă ı̂ntoarce
un String care poate fi afişat. Ca un exemplu, ı̂n Figura 4.3 am prezentat o imple-
mentare rudimentară a unei metode toString pentru clasa Date ı̂n liniile 37-40. Definirea
acestei metode ne permite să scriem un obiect d1 de tip Date cu instrucţiunea sys-
tem.out.print(d1).
4.4. PACHETE 53

4.3.4 Metoda equals


Metoda equals este folosită pentru a testa dacă două referinţe indică obiecte care au aceeaşi
valoare (stare). Antetul acestei metode este ı̂ntodeauna

public boolean equals( Object rhs )

Aţi remarcat probabil nedumeriţi faptul că parametrul trimis este de tip Object şi nu de tip
Date, cum ar fi fost de aşteptat. Raţiunea pentru acest lucru o să o prezentăm ı̂n capitolul
despre polimorfism. În general, metoda equals pentru o clasă X este implementată ı̂n aşa
fel ı̂ncât să returneze true doar dacă rhs este o instanţă a lui X şi, ı̂n plus, după conversia
la X toate tipurile primitive sunt egale (via ==) şi toate tipurile referinţă 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 şi metode statice


Există anumite cuvinte cheie ale limbajului Java care specifică proprietăţi speciale pen-
tru unele variabile sau metode. Un astfel de cuvânt cheie este cuvântul rezervat static.
Variabilele şi metodele declarate static ı̂ntr-o clasă, sunt aceleaşi pentru toate obiectele,
adică pentru toate variabilele de tipul acelei clase. Variabilele statice pot fi accesate fără
să fie nevoie de o instanţiere a clasei respective (adică de o variabilă de clasa respectivă).
Analog, nici metodele statice nu au nevoie de o instanţiere a clasei pentru a fi folosite.
Metodele statice pot utiliza variabile statice declarate ı̂n interiorul clasei.
Cel mai cunoscut exemplu de metodă statică este main. Alte exemple de metode sta-
tice pot fi găsite ı̂n clasele String, Integer şi Math. Exemple de astfel de metode sunt
String.valueOf, Integer.parseInt, Math.sin şi Math.max. Accesul la metodele statice re-
spectă aceleaşi reguli de vizibilitate ca şi metodele normale.

4.3.6 Metoda main


Atunci când este invocat, interpretorul java caută metoda main din clasa care i se dă ca
parametru. Astfel, putem scrie câte o metodă main pentru fiecare clasă. Acest lucru ne
permite să testăm funcţionalitatea de bază a claselor individuale. Trebuie să avem totuşi
ı̂n vedere faptul că plasarea funcţiei main ı̂n cadrul clasei ne conferă mai multă vizibilitate
decât ne-ar fi permis ı̂n general. Astfel, apeluri ale metodelor private pot fi făcute ı̂n test,
dar ele vor eşua ı̂ntr-un cadru mai general.

4.4 Pachete
Pachetele sunt folosite pentru a organiza clasele similare. Fiecare pachet constă dintr-o
mulţime de clase. Clasele care sunt ı̂n acelaşi pachet au restricţii de vizibilitate mai slabe
ı̂ntre ele decât dacă ar fi ı̂n pachete diferite.
Java furnizează 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 şi System. Clase mai cunoscute din java.util sunt Date, Random, StringTokenizer.
54 CAPITOLUL 4. OBIECTE Ş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 specificată ca P.C. De exemplu, putem declara un obiect de
tip Date care să conţină data şi ora curentă astfel:
java.util.Date today = new java.util.Date() ;
Observaţi că prin specificarea numelui pachetului din care face parte clasa evităm con-
flictele care pot fi generate de clase cu acelaşi nume din pachete diferite1 .

4.4.1 Directiva import


Utilizarea permanentă a numelui pachetului din care fac parte clasele poate fi uneori
deosebit de anevoioasă. Pentru a evita acest lucru se foloseşte directiva import:
import NumePachet.NumeClasa ;
sau
import NumePachet.* ;
Dacă recurgem la prima formă a directivei import, vom putea folosi NumeClasa ca o
prescurtare pentru numele clasei cu calificare completă. Dacă folosim cea de-a doua formă,
toate clasele din pachet vor putea fi abreviate cu numele lor.
De exemplu, realizând directivele import de mai jos:
import java.util.Date ;
import java.io.* ;
putem să folosim:
Date today = new Date() ;
FileReader file = new FileReader( name ) ;
Folosirea directivelor import economiseşte timpul de scriere. Având ı̂n vedere că cea de-a
doua formă este mai generală, ea este cel mai des folosită. Există două dezavantaje ale
folosirii directivei import. Primul dezavantaj este acela că la citirea codului va fi mai greu
de stabilit pachetul din care o anumită clasă face parte. Al doilea este acela că folosirea
celei de-a doua forme poate să introducă prescurtări neintenţionate pentru anumite clase,
ceea ce va genera conflicte de denumire care vor trebui rezolvate prin folosirea de nume
de clase calificate.
Să presupunem că avem următoarele directive:
import java.util.* ; //pachet predefinit
import myutil.* ; //pachet definit de catre noi
cu intenţia de a importa clasa java.util.Random şi o clasă pe care am definit-o chiar noi.
Atunci, dacă noi avem propria clasă Date ı̂n pachetul myutil, directiva import va genera
un conflict cu java.util.Date şi de aceea clasa va trebui să fie complet calificată. Am fi
putut evita aceste probleme dacă am fi folosit prima formă
1
De exemplu, cu clasa Date definită anterior
4.4. PACHETE 55

import java.util.Random ;

Directivele import trebuie să apară ı̂nainte de orice declarare a unei clase. Pachetul
java.lang este automat inclus ı̂n ı̂ntregime. Acesta este motivul pentru care putem folosi
prescurtări de genul Math.max, System.out, Integer.parseInt etc.

4.4.2 Instrucţiunea package


Pentru a indica faptul că o clasă face parte dintr-un anumit pachet trebuie să realizăm
două lucruri. În primul rând trebuie să scriem o instrucţiune package pe prima linie,
ı̂nainte de a declara clasa. Apoi, va trebui să plasăm clasa ı̂n directorul corespunzător.

4.4.3 Variabila sistem CLASSPATH


Java caută pachetele şi clasele ı̂n directoarele care sunt precizate ı̂n variabila sistem
CLASSPATH. Ce ı̂nseamnă aceasta? Iată două posibile setări pentru CLASSPATH, mai
ı̂ntâi 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

În ambele cazuri variabila CLASSPATH conţine directoarele (sau arhivele) care conţin
fişierele .class din pachete. De exemplu, dacă variabila CLASSPATH nu este setată corect,
nu veţi putea compila nici măcar cel mai banal program Java, deoarece pachetul java.lang
nu va fi găsit. O clasă care se află ı̂n pachetul P va trebui să fie pusă ı̂ntr-un director
cu numele P care să se afle ı̂ntr-un director din CLASSPATH. Directorul curent (.) este
ı̂ntotdeauna ı̂n variabila CLASSPATH, deci dacă lucraţi ı̂ntr-un singur director principal,
puteţi crea subdirectoarele chiar ı̂n el. Totuşi, ı̂n majoritatea situaţiilor, veţi dori să creaţi
un subdirector Java separat şi să creaţi directoarele pentru pachete chiar ı̂n el. În această
situaţie va trebui să adăugaţi la variabila CLASSPATH acest director. Acest lucru a fost
realizat ı̂n exemplul de mai sus prin adăugarea directorului $HOME/java la CLASSPATH.
În directorul Java veţi putea acum crea subdirectorul io. În subdirectorul io vom plasa
codul pentru clasa Reader2 . O aplicaţie va putea ı̂n această situaţie să folosească metoda
readInt fie prin

int x = io.Reader.readInt() ;

sau pur şi simplu prin

int x = Reader.readInt() ;

dacă se furnizează directiva import corespunzătoare.


2
Reader este o clasă ajutătoare care conţine metode pentru citirea de la tastatură a tipurilor uzuale.
Codul acestei clase este dat ı̂n paragraful 5.7.
56 CAPITOLUL 4. OBIECTE ŞI CLASE

4.4.4 Reguli de vizibilitate Package-Friendly


Pachetele au câteva reguli de vizibilitate importante. În primul rând, dacă pentru un
membru al unei clase nu se precizează nici un modificator de vizibilitate (public, protected
sau private), atunci membrul respectiv devine (package) friendly. Aceasta ı̂nseamnă că
acel câmp este vizibil doar pentru clasele din cadrul aceluiaşi pachet. Aceasta este o viz-
ibilitate mai puţin restrictivă decât private, dar mai restrictivă decât public (care este
vizibil şi pentru membrii din alte clase).
În al doilea rând, doar clasele public din cadrul unui pachet pot fi folosite din afara pa-
chetului. Acesta este motivul pentru care am pus ı̂ntotdeauna modificatorul public ı̂n faţa
unei clase. Clasele nu pot fi declarate private sau protected. Accesul de tip friendly se
extinde şi pentru clase. Dacă o clasă nu este declarată ca fiind de tip public, atunci ea va
putea fi accesată doar de clasele din cadrul aceluiaşi 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 făcând parte din acelaşi pachet
implicit. Ca o consecinţă, accesul de tip friendly se aplică pentru toate aceste clase. Acesta
este motivul pentru care vizibilitatea nu este afectată dacă omitem să punem modifica-
torul public din clasele care nu fac parte dintr-un pachet. Totuşi această modalitate de
folosire a accesului friendly nu este recomandată.

4.4.5 Compilarea separată


Atunci când un program constă din mai multe fişiere .java, fiecare fişier trebuie compilat
separat. În mod normal, fiecare clasă este plasată ı̂ntr-un fişier .java propriu. Ca urmare
a compilării vom obţine o colecţie de fişiere .class. Clasele sursă pot fi compilate ı̂n orice
ordine.

4.5 Alte operaţii


În această secţiune vom prezenta ı̂ncă trei cuvinte cheie importante: this, instanceof şi
static. this are mai multe utilizări ı̂n Java. Două dintre ele le vom prezenta ı̂n această
secţiune. instanceof are şi el mai multe utilizări; ı̂l vom folosi aici pentru a ne asigura că o
conversie de tip se poate realiza. Şi cuvântul cheie static are mai multe semnficaţii. Vom
vorbi despre metode statice, atribute statice şi iniţializatori statici.

4.5.1 Referinţa this


Cea mai cunoscută utilizare pentru this este ca o referinţă la obiectul curent. Imaginaţi-vă
că this vă indică ı̂n fiecare moment locul unde vă aflaţi. O utilizare tipică pentru this este
calificarea atributelor unei clase ı̂n cadrul unei metode a clasei care primeşte 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 OPERAŢII 57

În codul de mai sus, pentru a face distincţie ı̂ntre parametrul radius şi atributul cu acelaşi
nume (care este ”ascuns” de către parametru) se foloseşte sintaxa this.radius pentru a
referi atributul clasei.
Un alt exemplu de folosire al lui this este pentru a testa că parametrul pe care o metodă
ı̂l primeşte nu este chiar obiectul curent. Să presupunem, de exemplu, că avem o clasă
Account care are o metodă finalTransfer pentru a transfera toată suma de bani dintr-un
cont ı̂n altul. Metoda ar putea fi scrisă astfel:

public void finalTransfer(Account account)


{
dollars += account.dollars ;
account.dollars = 0 ;
}

Să considerăm secvenţa de cod de mai jos:

Account account1 ;
Account account2 ;
....
account2 = account1 ;
account1.finalTransfer(account2) ;

Deoarece transferăm bani ı̂n cadrul aceluiaşi cont, nu ar trebui să fie nici o modificare ı̂n
cadrul contului. Totuşi, ultima linie din finalTransfer are ca efect golirea contului debitor.
O modalitate de a evita o astfel de situaţie 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 mulţi constructori care au un comportament similar. Putem
folosi this ı̂n cadrul unui constructor pentru a apela ceilalţi constructori ai clasei. O altă
posibilitate de a scrie constructorul fără parametri pentru clasa Date este:

public Date()
{
this(1, 1, 2000) ;//apeleaza constructorul Date(int,int,int)
}

Se pot realiza şi exemple mai complicate, dar ı̂ntotdeauna apelul lui this trebuie să fie
prima instrucţiune din constructor, celelalte instrucţiuni fiind ı̂n continuarea acesteia.
58 CAPITOLUL 4. OBIECTE ŞI CLASE

4.5.3 Operatorul instanceof


Operatorul instanceof realizează o testare de tip ı̂n timpul execuţiei. Rezultatul expresiei:

exp instanceof NumeClasa

este true dacă exp este o instanţă a lui NumeClasa şi false ı̂n caz contrar. Dacă exp este
null rezultatul este ı̂ntotdeauna false. Operatorul instanceof este folosit de obicei ı̂nainte
de o conversie de tip, şi adeseori este folosit ı̂n legătură cu referinţele polimorfice pe care
le vom prezenta mai târziu.

4.5.4 Atribute statice


Atributele statice sunt folosite ı̂n situaţia ı̂n care avem variabile care trebuie partajate de
către toate instanţele unei clase. De obicei atributele statice sunt constante simbolice, dar
acest lucru nu este obligatoriu. Atunci când un atribut al unei clase este declarat de tip
static, doar o singură instanţă a acelei variabile va fi creată. Ea nu face parte din nici o
instanţă a clasei. Ea se comportă ca un fel de variabilă globală unică, vizibilă ı̂n cadrul
clasei. Cu alte cuvinte, dacă avem declaraţia

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 frecventă pentru câmpurile statice o reprezintă constantele. De exemplu, clasa
Integer defineşte atributul MAX VALUE astfel

public final static int MAX_VALUE = 2147483647 ;

Analog se defineşte şi constanta PI din clasa Math pe care am folosit-o ı̂n clasa Circle.
Modificatorul final indică faptul că identificatorul care urmează este o constantă. Dacă
această constantă nu ar fi fost un atribut static, atunci fiecare instanţă a clasei Integer ar
fi avut un atribut cu numele de MAX VALUE, irosind astfel spaţiu ı̂n memorie.
Astfel, vom avea o singură variabilă cu numele de MAX VALUE. Ea poate fi accesată de
oricare dintre metodele clasei Integer prin identificatorul MAX VALUE. Ea va putea fi
folosită şi de către un obiect de tip Integer numit x prin sintaxa x.MAX VALUE, ca orice
alt câmp. Acest lucru este permis doar pentru că MAX VALUE este public. În sfârşit,
MAX VALUE poate fi folosit şi prin intermediul numelui clasei ca Integer. MAX VALUE
(tot pentru că este public). Această ultimă folosire nu ar fi fost permisă pentru un câmp
care nu este static.
Chiar şi fără modificatorul final, atributele statice sunt foarte folositoare. Să presupunem
că vrem să reţinem numărul de obiecte de tip Circle care au fost construite. Pentru aceasta
avem nevoie de o variabilă statică. În clasa Circle vom face declaraţia:

private static int numarInstante = 0 ;


4.6. PROBLEME PROPUSE 59

Vom putea apoi incrementa numărul de instanţe ı̂n constructor. Dacă acest câmp 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.
Remarcaţi faptul că, deorece un atribut de tip static nu necesită un obiect care să ı̂l
controleze, fiind partajat de către toate instanţele clasei, el poate fi folosit de către o
metodă statică (dacă regulile de vizibilitate permit acest lucru). Atributele nestatice ale
unei clase vor putea fi folosite de către o metodă statică doar dacă se furnizează şi un
obiect care să le controleze.

4.5.5 Iniţializatori statici


Atributele statice sunt iniţializate atunci când clasa este ı̂ncărcată. Uneori este ı̂nsă nevoie
de o iniţializare mai complexă. Să presupunem de exemplu că avem nevoie de un şir static
care să conţină rădăcinile pătrate ale primelor 100 numere naturale. O posibilitate ar fi
să definim o metodă statică şi să cerem programatorului să apeleze acea metodă ı̂nainte
de a folosi şirul.
O alternativă la această soluţie este folosirea iniţializatorului static. Un exemplu este
prezentat ı̂n Figura 4.4. Aici iniţializatorul static se extinde de la linia 5 la linia 11.
Iniţializatorul static trebuie să urmeze imediat după 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 iniţializator static.

4.6 Probleme propuse


1. Scrieţi o clasă care suportă numere raţionale. Atributele clasei ar trebui să fie două
variabile de tip long una pentru numitor şi una pentru numărător. Stocaţi numărul
sub formă de fracţie ireductibilă, cu numitorul pozitiv. Formaţi un număr rezonabil
de constructori; metodele add, subtract, multiply, divide; de asemenea, toString,
equals şi compareTo (care se comportă ca cea din clasa String). Asiguraţi-vă că
toString merge ı̂n cazul ı̂n care numitorul este zero.
2. Implementaţi o clasă pentru numere complexe, Complex. Adăugaţi aceleaşi metode
ca la clasa Raţional, dacă au sens (de exemplu, compareTo nu are sens aici). Adăugaţi
accesori pentru partea reală şi cea imaginară.
60 CAPITOLUL 4. OBIECTE ŞI CLASE

3. Implementaţi o clasă completă IntType care să suporte un număr rezonabil de con-
structori; metodele add, subtract, multiply, divide; de asemenea, toString, equals şi
compareTo. Menţineţi IntType sub forma unui şir de cifre suficient de mare.

4. Implementaţi o clasă simplă numită Date. Clasa va trebui să reprezinte orice dată
ı̂ntre 1 Ianuarie 1800 şi 31 Decembrie 2500. Definiţi metode care să permită scăderera
a două date, incrementarea unei date cu un număr de zile; compararea a două date.
O dată va fi reprezentată intern ca numărul de zile care au trecut de la o anumită
dată,(de exemplu prima zi din 1800).

Soluţie: Am creat două şiruri care sunt atribute statice. Primul numit pana1Lun
va conţine numărul de zile până la prima zi din fiecare lună a anilor care nu sunt
bisecţi. Astfel, el va conţine 0,31,59,90 etc. Cel de-al doilea şir, pana1Ian va conţine
numărul de zile până la ı̂nceputul fiecărui an, ı̂ncepând cu 1800. Astfel, el va conţine
0, 365, 730, 1095, 1460, 1826 etc, deoarece 1800 nu este an bisect, dar 1804 este.
Programul va iniţializa acest şir o singură dată. Am utilizat acest şir pentru con-
versia din reprezentarea internă (număr de zile de la 01.01.1800) ı̂n reprezentarea
externă (zi/lună/an).
Clasa este prezentată ı̂n cele ce urmează:

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 Ş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 Ş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 ’mare’cu "+(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 ’mica’cu "+(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 Ş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 67

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);
}
}
Capitolul 5

Moştenire

Aşa cum am menţionat ı̂n capitolul anterior, unul dintre principalele scopuri ale pro-
gramării orientate pe obiecte este reutilizarea codului. La fel cum inginerii folosesc aceleaşi
componente din nou şi din nou ı̂n proiectarea circuitelor, programatorii au posibilitatea să
refolosească obiectele, ı̂n loc să le reimplementeze. În limbajele de programare orientate pe
obiecte, mecanismul fundamental pentru refolosirea codului este moştenirea. Moştenirea
ne permite să extindem funcţionalitatea unui obiect. Cu alte cuvinte, putem crea noi
obiecte cu proprietăţi extinse (sau restrânse) ale tipului original, formând astfel o ierarhie
de clase. De asemenea, moştenirea este mecanismul pe care Java ı̂l foloseşte pentru a
implementa metode şi clase generice. În acest capitol vom prezenta:

• principiile generale ale moştenirii, inclusiv polimorfismul

• cum este moştenirea implementată ı̂n Java

• cum poate fi derivată o colecţie de clase dintr-o singură clasă abstractă

• interfaţa, care este un caz particular de clasă abstractă

• cum se poate realiza programarea generică ı̂n Java folosind interfeţe

5.1 Ce este moştenirea?


Moştenirea este principiul fundamental al programării orientate pe obiecte care permite
refolosirea codului ı̂ntre clasele ı̂nrudite. Moştenirea modelează relaţii de tipul ESTE-
UN (sau ESTE-O). Într-o relaţie de tip ESTE-UN, spunem că clasa derivată ESTE-O
(variaţiune) a clasei de bază. De exemplu, Cerc ESTE-O Curbă, iar Maşină ESTE-UN
Vehicul. În schimb, o Elipsă NU-ESTE-UN Cerc. Relaţiile de moştenire formează ierarhii.
De exemplu, putem extinde clasa Maşină la MaşinăStrăină (pentru care se plăteşte vamă)
şi MaşinăAutohtonă (care nu plăteşte vamă) etc.
Un alt tip de relaţie ı̂ntre obiecte este relaţia ARE-UN sau ESTE-COMPUS-DIN. Această
relaţie nu are proprietăţile care ar fi normale ı̂ntr-o ierarhie de moştenire. Un exemplu
de astfel de relaţie este că Maşină ARE-UN Volan. Relaţiile de tip ARE-UN nu trebuie
modelate prin moştenire. Ele se vor modela prin agregare, ı̂n care componentele devin
simple câmpuri de tip private.

68
5.1. CE ESTE MOŞTENIREA? 69

Chiar şi limbajul Java foloseşte din plin moştenirea pentru a-şi implementa propriile bib-
lioteci de clase. Un exemplu relativ familiar ı̂l constituie excepţiile. Java defineşte clasa
Exception. Aşa cum am văzut deja, există mai multe tipuri de excepţii, cum ar fi NullPoint-
erException şi ArrayIndexOutOfBoundsException. Fiecare constituie o clasă separată, ı̂nsă
toate dispun de metoda toString pentru a furniza mesaje de eroare utile ı̂n depanare.
Moştenirea modelează aici o relaţie de tip ESTE-UN. NullPointerException ESTE-O Ex-
ception. Datorită relaţiei de tip ESTE-UN, proprietatea fundamentală a moştenirii garan-
tează că orice metodă care poate fi aplicată lui Exception poate fi aplicată şi lui Null-
PointerException. Mai mult decât atât, un obiect de tip NullPointerException poate
să fie referit de către o referinţă de tip Exception (reciproca nu este adevărată!). Ast-
fel, deoarece metoda toString este o metodă disponibilă ı̂n clasa Exception, vom putea
ı̂ntotdeauna scrie:
catch( Exception e )
{
System.out.println( e.toString() ) ;
}
Dacă e referă un obiect de tip NullPointerException, atunci e.toString() este un apel corect.
Funcţie de modul de implementare al ierarhiei de clase, metoda toString ar putea fi invari-
antă sau ar putea fi specializată pentru fiecare clasă distinctă. Atunci când o metodă este
invariantă ı̂n cadrul unei ierarhii, adică are aceeaşi funcţionalitate pentru toate clasele din
ierarhie, nu va trebui să rescriem implementarea acelei metode pentru fiecare clasă.
Apelul lui toString mai ilustrează un alt principiu important al programării orientate pe
obiecte, cunoscut sub numele de polimorfism. O variabilă referinţă care este polimorfică
poate să refere obiecte de tipuri diferite. Atunci când se aplică o metodă referinţei, se
selectează automat operaţia adecvată pentru tipul obiectului care este referit ı̂n acel mo-
ment. În Java, toate tipurile referinţă sunt polimorfice. În cazul unei referinţe de tip
Exception se ia o decizie ı̂n timpul execuţiei: se va apela metoda toString pentru obiectul
pe care e ı̂l referă ı̂n acel moment ı̂n timpul execuţiei. Acest proces este cunoscut sub
numele de legare târzie sau legare dinamică.
În cazul moştenirii avem o clasă de bază din care sunt derivate alte clase. Clasa de bază
este clasa pe care se bazează moştenirea. O clasă derivată moşteneşte toate proprietăţile
clasei de bază, ı̂nsemnând că toţi membri publici ai clasei de bază devin membri publici
ai clasei derivate. Clasa derivată poate să adauge noi atribute şi metode şi poate modifica
semnificaţia metodelor moştenite. Fiecare clasă derivată este o clasă complet nouă. Clasa
de bază nu este ı̂n nici un fel afectată de modificările aduse ı̂n clasele derivate. Astfel, la
crearea clasei derivate este imposibil să se strice ceva ı̂n clasa de bază.
O clasă derivată este compatibilă ca tip cu clasa de bază, ceea ce ı̂nseamnă că o variabilă
referinţă de tipul clasei de bază poate referi un obiect al clasei derivate, dar nu şi invers.
Clasele surori (cu alte cuvinte clasele derivate dintr-o clasă comună) nu sunt compatibile
ca tip.
Aşa cum am menţionat anterior, folosirea moştenirii generează de obicei o ierarhie de clase.
Figura 5.1 prezintă o mică parte din ierarhia de excepţii a limbajului Java. Remarcaţi
faptul că NullPointerException este indirect derivată din Exception. Acest lucru nu consti-
tuie nici o problemă, deoarece relaţiile de tipul ESTE-UN sunt tranzitive. Cu alte cuvinte,
dacă X ESTE-UN Y şi Y ESTE-UN Z, atunci X ESTE-UN Z. Ierarhia Exception ilustrează
70 CAPITOLUL 5. MOŞTENIRE

ı̂n acelaşi timp designul clasic ı̂n care se extrag caracteristicile comune ı̂n clasa de bază,
urmate de specializări ı̂n clasele derivate. ı̂ntr-o astfel de ierarhie spunem că clasa derivată
este o subclasă a clasei de bază, iar clasa de bază este o superclasă a clasei derivate. Aceste
relaţii sunt tranzitive; mai mult, operatorul instanceof funcţionează pentru subclase. Ast-
fel, dacă obj este de tipul X (şi nu e null), atunci expresia obj instanceof Z este adevărată.
În următoarele secţiuni vom examina următoarele probleme:
• Care este sintaxa folosită pentru a deriva o clasă nouă dintr-o clasă de
bază?
• Cum afectează acest lucru statutul membrilor private sau public?
• Cum precizăm faptul că o metodă este invariantă pentru o ierarhie de
clase?
• Cum specializăm o metodă?
• Cum factorizăm aspectele comune ı̂ntr-o clasă abstractă, pentru a crea
apoi o ierarhie?
• Putem deriva o clasă nouă din mai mult de o clasă (moştenire multiplă)?
• Cum se foloseşte moştenirea pentru a implementa codul generic?
Câteva dintre aceste subiecte sunt ilustrate prin implementarea unei clase Shape din
care vom deriva clasele Circle, Square şi Rectangle. Ne vom folosi de acest exemplu
pentru a vedea cum Java implementează polimorfismul cât şi pentru a vedea cum poate
fi moştenirea folosită pentru a implementa metode generice.
ArrayIndexOutOfBoundsException





IndexOutOfBoundsException

✣ ❏
✡ ❏
✡ ❏
✡ ❏

RuntimeException ❍ StringIndexOutOfBoundsException
❍❍



✡ NullPointerException

Exception ✡ EOFException
✻ ❏
❏ ✡


❏ ✡


IOException ✡





FileNotFoundException

Figura 5.1 O parte din ierarhia de excepţii a limbajului Java


5.2. SINTAXA DE BAZĂ JAVA 71

5.2 Sintaxa de bază Java


O clasă derivată moşteneşte toate proprietăţile clasei de bază. Clasa derivată poate apoi
să adauge noi atribute, să redefinească metode sau să adauge noi metode. Fiecare clasă
derivată este o clasă complet nouă. Aspectul general al unei clase derivate este prezentat
ı̂n Figura 5.2. Clauza extends declară faptul că o clasă este derivată dintr-o altă clasă.

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

Iată o descriere scurtă a unei clase derivate:

• În general, toate atributele sunt private, deci atributele suplimentare vor fi adăugate
clasei derivate prin precizarea lor ı̂n secţiunea private.

• Orice metode din clasa de bază care nu sunt precizate ı̂n clasa derivată sunt moştenite
nemodificat, cu excepţia constructorului. Cazul particular al constructorului ı̂l vom
prezenta ı̂n paragraful 5.2.2.

• Orice metodă din clasa de bază care este definită ı̂n secţiunea public a clasei derivate
este redefinită. Noua metodă va fi aplicată obiectelor din clasa derivată.

• Metodele public din clasa de bază nu pot fi redefinite ı̂n secţiunea private a clasei
derivate.

• Clasei derivate ı̂i putem adăuga metode suplimentare.

5.2.1 Reguli de vizibilitate


Ştim deja că orice membru care este declarat a fi private este accesibil doar ı̂n metodele
clasei. Rezultă deci că nici un membru private din clasa de bază nu va fi accesibil ı̂n clasa
derivată.
Există situaţii ı̂n care clasa derivată trebuie să aibă acces la membri clasei de bază. Există
două opţiuni pentru a realiza acest lucru. Prima este aceea de a utiliza accesul de tip
public sau friendly. Totuşi, accesul de tip public permite accesul şi altor clase, pe lângă
72 CAPITOLUL 5. MOŞTENIRE

clasele derivate. Accesul de tip friendly funcţionează doar dacă ambele clase sunt ı̂n acelaşi
pachet.
Dacă dorim să restrângem accesul unor membri, astfel ı̂ncât ei să fi vizibili doar pentru
clasele derivate, putem să declarăm membri ca fiind de tip protected. Un membru de tip
protected este private pentru toate clasele cu excepţia claselor derivate (şi a claselor din
acelaşi pachet). Declararea atributelor ca fiind public sau protected ı̂ncalcă principiile
ı̂ncapsulării şi ascunderii informaţiei şi se recurge la ea doar din motive de comoditate. De
obicei, este preferabil să se scrie modificatori şi accesori sau să se folosească accesul de tip
friendly. Totuşi, dacă o declaraţie protected vă scuteşte de scrierea de cod stufos, atunci
se poate recurge la ea.

5.2.2 Constructor şi super


Fiecare clasă derivată trebuie să ı̂şi definească proprii constructori. Dacă nu se scrie nici un
constructor, Java va genera un constructor implicit fără parametri. Acest constructor va
apela constructorul fără parametri al clasei de bază pentru membrii care au fost moşteniţi,
după care va aplica iniţializarea implicită pentru atributele adăugate (adică 0 pentru
tipurile primitive şi null pentru tipurile referinţă).
Construirea unei obiect al unei clase derivate are loc prin construirea prealabilă a porţiunii
moştenite. Acest lucru este natural, deoarece principiul ı̂ncapsulării afirmă că partea
moştenită este o entitate unică, iar constructorul clasei de bază ne spune cum să iniţializăm
această entitate.
Constructorii clasei de bază pot fi apelaţi explicit prin metoda super. Astfel, constructorul
implicit pentru o clasă derivată are de fapt forma:

public Derived()
{
super() ;
}

Metoda super poate fi apelată şi cu parametri care să se potrivească cu un constructor
din clasa de bază. De exemplu, dacă clasa de bază are un constructor care acceptă 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 să apară doar ı̂n prima linie dintr-un constructor. Dacă nu se face un
apel explicit, compilatorul va realiza automat un apel al metodei super fără parametri.

5.2.3 Metode şi clase final


Aşa cum am precizat deja mai devreme, clasa derivată poate să redefinească sau să accepte
nemodificate metodele din clasa de bază. În multe cazuri este clar faptul că o anumită
metodă trebuie să fie invariantă de-a lungul ierarhiei, ceea ce ı̂nseamnă că clasele derivate
5.2. SINTAXA DE BAZĂ JAVA 73

nu trebuie să o redefinească. În acest caz, putem declara metoda ca fiind de tip final iar
ea nu va putea fi redefinită.
Pe lângă faptul că declararea unei metode final este o practică bună de programare, ea
poate genera şi cod mai rapid. Declararea unei metode final (atunci când este cazul) con-
stituie o practică bună de programare deoarece intenţiile noastre devin astfel clare pentru
cititorul programului si al documentaţiei şi, pe de altă parte, putem preveni redefinirea
accidentală pentru o metodă care nu trebuie să fie redefinită.
Pentru a vedea de ce folosirea lui final poate conduce la cod mai eficient, să presupunem
că avem o clasă de bază numită Base care defineşte o metodă final f , iar Derived este o
clasă care extinde Base. Să considerăm funcţia:
void xxx(Base obj)
{
obj.f()
}
Deoarece f este o metodă final, nu are nici o importanţă dacă ı̂n momentul execuţiei obj
referă un obiect de tip Base sau un obiect de tip Derived; definirea lui f este invariantă, deci
ştim de la ı̂nceput ceea ce f va face. O consecinţă a acestui fapt este că decizia pentru codul
care va fi executat se ia ı̂ncă de la compilare, şi nu la execuţie. Acest proces este cunoscut
sub numele de legare statică. Deoarece legarea se face la compilare şi nu la execuţie,
programul ar trebui să ruleze mai repede. Dacă acest fapt este sau nu perceptibil efectiv
(ţinând cont de viteza de prelucrare a procesoarelor din generaţia actuală) depinde de
numărul de ori ı̂n care evităm deciziile din timpul execuţiei ı̂n timpul rulării programului.
Un corolar al acestei observaţii ı̂l constituie faptul că dacă f este o metodă banală, cum ar
fi un accesor pentru un atribut, compilatorul ar putea să ı̂nlocuiască apelul lui f, direct cu
corpul funcţiei. Astfel, apelul funcţiei va fi ı̂nlocuit cu o singură linie care accesează un
atribut, economisindu-se astfel timp. Dacă f nu ar fi fost declarată 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 diferită.
Metodele statice nu au un obiect care să le controleze, deci apelul lor este rezolvat ı̂ncă de
la compilare folosind legarea statică.
Similare metodelor final sunt clasele final. O clasă final nu mai poate fi extinsă. În
consecinţă, toate metodele unei astfel de clase sunt metode final. Ca un exemplu, clasa
Integer este o clasă final. De remarcat faptul că dacă o clasă are doar membri final, ea nu
este neapărat o clasă final.

5.2.4 Redefinirea unei metode


Metodele din clasa de bază pot fi redefinite ı̂n clase derivate prin furnizarea unei metode
din clasa derivată care să aibă aceeaşi semnătură. Metoda din clasa derivată trebuie să
aibă aceeaşi semnătură şi nu are dreptul să adauge excepţii la lista throws.
Adeseori, metoda din clasa derivată trebuie să apeleze metoda din clasa de bază. Acest
proces este numit redefinire parţială. Cu alte cuvinte vrem să facem ceea ce face şi metoda
din clasa de bază, plus ı̂ncă ceva. Apeluri ale clasei de bază pot fi realizate folosind super.
Iată un exemplu:
public class Student extends Human
74 CAPITOLUL 5. MOŞTENIRE

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

5.2.5 Metode şi clase abstracte


Până acum am văzut faptul că unele metode sunt invariante de-a lungul ierarhiei de clase
(metodele final), iar alte metode ı̂şi modifică semnificaţia de-a lungul ierarhiei. O a treia
posibilitate este ca o metodă din clasa de bază să aibă sens doar pentru clasele derivate,
şi vrem ca ea să fie obligatoriu definită ı̂n clasele derivate; totuşi, implementarea metodei
nu are nici un sens pentru clasa de bază. În această situaţie, putem declara metoda ca
fiind abstractă.
O metodă abstractă este o metodă care declară funcţionalităţi care vor trebui neapărat
implementate până la urmă ı̂n clasele derivate. Cu alte cuvinte o metodă abstractă spune
ceea ce obiectele derivate trebuie să facă. Totuşi, ea nu furnizează nici un fel de imple-
mentare; fiecare clasă derivată trebuie să vină cu propria implementare.
O clasă care are cel puţin o metodă abstractă este o clasă abstractă. Java pretinde ca toate
clasele abstracte să fie definite explicit ca fiind abstracte. Atunci când o clasă derivată nu
redefineşte o metodă abstractă, metoda rămâne abstractă şi ı̂n clasa de bază. În consecinţă
dacă o clasă care nu intenţionăm să fie abstractă nu redefineşte toate metodele abstracte,
compilatorul va detecta inconsistenţa şi va genera un mesaj de eroare.
Un exemplu simplu de clasă abstractă este clasa Shape (shape ı̂nseamnă formă sau curbă),
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 prezintă ierarhia de clase care rezultă.

Figura 5.3 Ierarhia de clase pentru forme

Clasa Shape poate să aibă membri care să fie comuni pentru toate clasele. Într-un exemplu
mai extins, aceasta ar putea include coordonatele extremităţilor obiectului. Ea declară şi
defineşte metode cum ar fi positionOf, care sunt independente de tipul formei; positionOf
ar fi o metodă final. Ea defineşte şi metode care se aplică fiecărui obiect ı̂n parte. Unele
dintre aceste metode nu au nici un sens pentru clasa abstractă Shape. De exemplu, este
dificil de calculat aria unui obiect oarecare; metoda area va fi declarată abstract.
Aşa cum am menţionat anterior, existenţa cel puţin a unei metode abstracte, face clasa
să devină şi ea abstractă, deci ea nu va putea fi instanţiată. Astfel, nu vom putea crea un
obiect de tip Shape; vom putea crea doar obiecte derivate. Totuşi, ca de obicei, o referinţă
de tip Shape poate să refere orice formă concretă derivată, cum ar fi Circle sau Rectangle.
Exemplu:
5.2. SINTAXA DE BAZĂ JAVA 75

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

Codul din Figura 5.4 prezintă clasa abstractă Shape. La linia 13, declarăm o variabilă
de tip String care reţine tipul formei, folosită doar ı̂n clasele derivate. Atributul este de
tip private, deci clasele derivate nu au acces direct la el. Restul clasei cuprinde o listă de
metode.
Constructorul nu va fi apelat niciodată direct, deoarece Shape este o clasă abstractă. Avem
totuşi nevoie de un constructor care să fie apelat din clasele derivate pentru a iniţializa
atributele private. Constructorul clasei Shape stabileşte valoarea atributului name.
Linia 15 declară metoda abstractă area. area este o metodă abstractă deoarece nu putem
furniza nici un calcul implicit al ariei pentru o clasă derivată care nu ı̂şi defineşte propria
metodă de calcul a ariei.
Metoda de comparaţie din liniile 22-25 nu este abstractă, deoarece ea poate fi aplicată
ı̂n acelaşi mod pentru toate clasele derivate. De fapt, definirea ei este invariantă de-a
lungul ierarhiei, de aceea am declarat-o final. Parametrul rhs (de la ”right-hand-side”)
reprezintă un alt obiect de tip Shape, a cărui arie se compară cu cea a obiectului curent.
Este interesant de remarcat faptul că variabila rhs poate să refere orice instanţă a unei
clase derivate din Shape (de exemplu o referinţă a clasei Rectangle). Astfel este posibil ca
folosind această metodă să comparăm aria obiectului curent (care poate fi, de exemplu, o
instanţă 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 afişează numele formei şi aria ei. Ca şi metoda de
comparaţie, ea este invariantă de-a lungul ierarhiei, de aceea a fost declarată final.
Înainte de a trece mai departe, să rezumăm cele 4 tipuri de metode ale unei clase:

1. Metode finale. Apelul lor este rezolvat ı̂ncă de la compilare. Folosim metode final
doar atunci când metoda este invariantă de-a lungul ierarhiei (adică atunci când
metoda nu este niciodată redefinită).

2. Metode abstracte. Apelul lor este rezolvat ı̂n timpul execuţiei. Clasa de bază nu
furnizează nici o implementare a lor şi este abstractă. Clasele derivate trebuie fie să
implementeze metoda, fie devin ele ı̂nsele abstracte.

3. Metode statice. Apelul este rezolvat la compilare, deoarece nu există obiect care să
le controleze.

4. Alte metode. Apelul este rezolvat la execuţie. Clasa de bază furnizează o imple-
mentare implicită care fie va fi redefinită ı̂n clasele derivate, fie acceptată nemodifi-
cată.

1. //Clasa de baza abstracta pentru forme


2. //
3. //CONSTRUIREA: nu este permisa; Shape este abstracta
4. //constructorul cu un param. este furnizat ptr. clasele derivate
5. //
6. //***************Metode publice***************************
76 CAPITOLUL 5. MOŞ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ă abstractă Shape

5.3 Exemplu: Extinderea clasei Shape


În această secţiune vom implementa clasele derivate din clasa Shape şi vom prezenta cum
sunt ele utilizate ı̂ntr-o manieră polimorfică. Iată enunţul problemei:

Sortare de forme. Se citesc N forme (cercuri, dreptunghiuri sau pătrate). Să se afişeze
ordonate după arie.

Implementarea claselor, prezentată ı̂n Figura 5.5 este simplă şi nu ilustrează aproape
nici un concept pe care să nu-l fi prezentat deja. Singura noutate este că clasa Square este
derivată din clasa Rectangle care este, la rândul ei, derivată din Shape. La implementarea
acestor clase trebuie să:

1. Definim un nou constructor

2. Să examinăm fiecare metodă care nu este final sau abstract pentru a vedea dacă dorim
să o acceptăm nemodificată. Pentru fiecare astfel de metodă care nu corespunde cu
necesităţile clasei trebuie să furnizăm o nouă definire.
5.3. EXEMPLU: EXTINDEREA CLASEI SHAPE 77

3. Definirea fiecărei metode abstracte.


4. Adăugarea de alte metode dacă 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.}
78 CAPITOLUL 5. MOŞ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 şi Square, care va fi salvat ı̂n
trei fişiere sursă separate.

Pentru fiecare clasă am scris un constructor simplu care permite iniţializarea cu dimensi-
unile de bază (rază pentru cercuri, lungimea laturilor pentru dreptunghiuri şi pătrate). În
constructor, vom iniţializa mai ı̂ntâi partea moştenită prin apelul lui super. Fiecare clasă
trebuie să definească o metodă area, deoarece Shape a declarat-o ca fiind abstractă. Dacă
uităm să scriem o metodă area pentru una dintre clase, eroarea va fi detectată ı̂ncă de la
compilare, deoarece - dacă metoda area lipseşte - atunci clasa derivată este şi ea abstractă.
Observaţi că clasa Square este dispusă să accepte metoda area moştenită de la Rectangle,
de aceea nu o mai redefineşte.
După ce am implementat clasele, suntem gata să rezolvăm problema originală. Vom folosi
un şir de clase Shape. Reţineţi faptul că prin aceasta nu alocăm memorie pentru nici un
obiect de tip Shape (ceea ce ar fi ilegal); se alocă memorie doar pentru un şir de referinţe
către Shape. Aceste referinţe vor putea referi obiecte de tip Circle, Rectangle sau Square1 .
În Figura 5.6 facem exact acest lucru. Mai ı̂ntâi citim obiectele. La linia 21, apelul
lui readShape constă ı̂n citirea unui caracter, urmată de dimensiunile figurii şi de crearea
unui nou obiect de tip Shape. Figura 5.7 prezintă o implementare primitivă a acestei
rutine. Observaţi că ı̂n cazul unei erori la citire se creează un cerc de rază 0 şi se ı̂ntoarce o
referinţă la el. O soluţie mai elegantă ı̂n această situaţie ar fi fost să definim şi să aruncăm
o excepţie.
După aceasta, fiecare obiect create de către readShape este referit de către un element
al şirului shapes. Se apelează insertionSort pentru a sorta formele. În final afişăm şirul
rezultat de forme, apelând 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ă pentru polimorfism (poli=mai multe, morphos=forme).
Într-adevă referinţa de tip Shape (shape=formă) poate referi clase de mai multe (poli) forme (morphos):
Circle, Rectangle şi Square.
5.3. EXEMPLU: EXTINDEREA CLASEI SHAPE 79

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 şi afişarea lor ı̂n ordine crescătoare.

1. //creaza un obiect adecvat de tip Shape functie de


2. //datele de intrare.
3. //utilizatorul introduce ’c’, ’s’ sau ’r’ pentru a indica
4. //forma, apoi introduce dimensiunile
5. //in caz de eroare se intoarce un cerc de raza 0
80 CAPITOLUL 5. MOŞ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ă simplă pentru citirea şi ı̂ntoarcerea unei noi forme.
49.//sortare porin insertie
5.4. MOŞTENIRE MULTIPLĂ 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 inserţie

5.4 Moştenire multiplă


Toate exemplele prezentate până acum, derivau o clasă dintr-o singură clasă de bază. În
cazul moştenirii multiple o clasă poate fi derivată din mai mult de o clasă de bază. De
exemplu, putem avea clasele Student şi Angajat. Din aceste clase ar putea fi derivată o
clasă AngajatStudent.
Deşi moştenirea multiplă pare destul de atrăgătoare, şi unele limbaje (cum ar fi C++)
chiar o implementează, ea este ı̂mbibată de subtilităţi care fac proiectarea claselor deosebit
de dificilă. De exemplu, cele două clase de bază ar putea conţine metode care au aceeaşi
semnătură, dar implementări diferite, sau ar putea avea atribute cu acelaşi nume. Care
dintre ele ar trebui folosit?
Din aceste motive, Java nu permite moştenirea multiplă. Java furnizează ı̂nsă o alterna-
tivă, numită interfaţă.

5.5 Interfeţe
Interfaţa ı̂n Java este cea mai abstractă clasă posibilă. Ea constă doar din metode ab-
stracte publice şi din atribute statice şi finale.
Spunem că o clasă implementează o anumită interfaţă dacă furnizează definiţii pentru
toate metodele abstracte din cadrul interfeţei. O clasă care implementează o interfaţă se
comportă ca şi când ar fi extins o clasă abstractă precizată de către acea interfaţă.
În principiu, diferenţa esenţială dintre o clasă abstractă şi o interfaţă este că deşi amândouă
furnizează o specificaţie a ceea ce clasele derivate trebuie să facă, interfaţa nu poate furniza
nici un fel de detaliu de implementare sub forma de atribute sau de metode implementate.
Consecinţa practică a acestui lucru este că interfeţele nu suferă de problemele potenţiale
pe care le are moştenirea multiplă, deoarece nu putem avea implementări diferite pen-
tru aceeaşi metodă. Astfel, deşi o clasă poate să extindă o singură clasă, ea poate să
implementeze mai mult de o singură interfaţă.
82 CAPITOLUL 5. MOŞTENIRE

5.5.1 Definirea unei interfeţe


Din punct de vedere sintactic, nimic nu este mai simplu decât precizarea unei interfeţe.
Interfaţa arată ca o declaraţie a unei clase, doar că foloseşte cuvântul cheie interface. Ea
constă dintr-o listă de metode care trebuie declarate. Un exemplu de interfaţă este Com-
parable, prezentată ı̂n Figura 5.9.
Interfaţa Comparable precizează două metode pe care orice clasă derivată din ea trebuie
să le implementeze: compareTo şi lessThan. Metoda compareTo se va comporta similar
cu metoda compareTo a clasei String. Observaţi că nu este necesar să precizăm faptul că
aceste metode sunt public sau abstract, deoarece acest lucru este implicit pentru metodele
unei interfeţe.

public Interface Comparable


{
int compareTo(Comparable rhs) ;
boolean lessThan(Comparable rhs) ;
}

Figura 5.9 Interfaţa Comparable

5.5.2 Implementarea unei interfeţe


O clasă implementează o interfaţă ı̂n doi paşi:

1. declară că implementează interfaţa şi

2. defineşte implementări pentru toate metodele din interfaţă.

Un exemplu este prezentat ı̂n Figura 5.10, ı̂n care se defineşte clasa MyInteger. Clasa
MyInteger are un comportament asemănător cu al clasei Integer, din pachetul java.lang.
În linia 1 se vede că atunci când implementăm o interfaţă folosim cuvântul cheie imple-
ments ı̂n loc de extends. În această clasă putem scrie orice metode dorim, dar trebuie
să definim cel puţin metodele din interfaţă. Interfaţa este implementată ı̂n liniile 27-36.
Remarcaţi faptul că trebuie să implementăm exact metoda precizată ı̂n cadrul interfeţei;
din acest motiv aceste metode au ca parametru un obiect de tip Comparable şi nu un
MyInteger.
O clasă care implementează o interfaţă poate fi extinsă cu condiţia să nu fie finală. Astfel,
dacă MyInteger nu ar fi fost finală, am fi putut-o extinde.
O clasă care implementează o interfaţă poate totuşi să extindă şi o altă clasă. De exemplu,
am fi putut, ı̂n principiu, scrie:

public class MyInteger extends Integer implements Comparable

Acest cod este incorect doar pentru că Integer este o clasă finală care nu poate fi extinsă.
5.5. INTERFEŢ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ă), care implementează interfaţa Com-
parable

5.5.3 Interfeţe multiple


Aşa cum am menţionat mai devreme, o clasă poate să implementeze mai mult de o singură
interfaţă. Sintaxa pentru a realiza acest lucru este simplă. O clasă poate implementa mai
84 CAPITOLUL 5. MOŞTENIRE

multe interfeţe prin:

1. precizarea interfeţelor pe care le implementează

2. implementarea tuturor metodelor din interfeţe.

Interfaţa este cea mai abstractă clasă posibilă şi reprezintă o soluţie elegantă la problema
moştenirii multiple.

5.6 Implementarea de componente generice


Să ne reamintim că unul dintre scopurile principale ale programării orientate pe obiecte este
suportul pentru reutilizarea codului. Unul dintre mecanismele importante folosite pentru
ı̂ndeplinirea acestui scop este programarea generică: Dacă implementarea unei metode este
identică pentru mai multe clase (cu excepţia tipului de bază al obiectului), se poate folosi
o implementare generică pentru a descrie funcţionalitatea de bază. De exemplu, putem
scrie o metodă care să sorteze un şir de elemente; algoritmul pentru această metodă 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
utilizează şabloane pentru a implementa programarea generică, Java nu oferă suport pentru
implementarea directă a programării generice, deoarece programarea generică poate fi
implementată folosind conceptele de bază ale moştenirii. În această secţiune vom prezenta
cum pot fi implementate metode şi clase generice ı̂n Java folosind principiile de bază ale
moştenirii.
Ideea de bază ı̂n Java este că putem implementa o clasă generică folosind o superclasă
adecvată, cum ar fi Object. În Java, dacă o clasă nu extinde o altă clasă, atunci ea extinde
implicit clasa Object (definită ı̂n pachetul java.lang). Ca o consecinţă, fiecare clasă este o
subclasă a lui Object. Să considerăm clasa MemoryCell din Figura 5.11. Această clasă
poate să reţină un obiect de tip Object. Deoarece Object este clasă de bază pentru orice
clasă din Java, rezultă că clasa noastră poate să 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ă MemoryCell

Există două detalii care trebuie luate ı̂n considerare atunci când folosim această strategie.
Ambele sunt ilustrate ı̂n Figura 5.12. Funcţia main scrie valoarea 5 ı̂ntr-un obiect Mem-
oryCell, după care citeşte din obiectul MemporyCell. În primul rând, tipurile primitive
nu sunt obiecte. Astfel, m.write(5) ar fi fost incorect. Totuşi, aceasta nu este o problemă,
deoarece Java dispune de clase wrapper (de ”ı̂mpachetare”) pentru cele opt tipuri primi-
tive. Astfel, obiectul de tip MemoryCell va reţine un obiect de tip Integer.
Al doilea detaliu este că rezultatul lui m.read() este un Object. Deoarece ı̂n clasa Object
este definită o metodă toString, nu este necesar să facem conversia de la Object la Integer.
Referinţa returnată de m.read() (de tip Object) este polimorfică şi ea referă de fapt un
obiect de tip Integer. În consecinţă, se va apela automat metoda toString a clasei Integer.
Dacă am fi vrut totuşi să extragem valoarea reţinută ı̂n obiectul de tip MemoryCell, ar fi
trebuit să scriem o linie de genul

int i = ( (Integer)m.read() ).intValue() ;

ı̂n care se converteşte mai ı̂ntâi valoarea returnată de read la Integer, după care se foloseşte
metoda intValue() pentru a obţine un int.
Deoarece clasele wrapper sunt clase finale, constructorul şi accesorul intValue pot fi ex-
pandate inline de către compilator, generând astfel un cod la fel de eficient ca utilizarea
directă 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 sortării. Am scris deja o metodă insertionSort care
lucrează cu un şir de clase Shape. Ar fi interesant să rescriem această metodă pentru un
şir generic. Figura 5.13 prezintă o metodă de sortare generică insertionSort care este
practic identică cu metoda de sortare din Figura 5.8 doar că foloseşte Comparable ı̂n loc
de Shape. Putem pune această metodă ı̂ntr-o clasă Sort. Observaţi că nu sortăm Object,
ci Comparable. Metoda insertionSort sortează un şir de elemente Comparable, deoarece
foloseşte lessThan. Aceasta ı̂nseamnă că doar clasele care implementează interfaţa Com-
parable pot fi sortate astfel. De remarcat faptul că insertionSort nu poate să sorteze un
86 CAPITOLUL 5. MOŞTENIRE

şir de obiecte Shape, deoarece clasa Shape din Figura 5.4 nu implementează interfaţa
Comparable. Unul dintre exerciţii propune modificarea clasei Shape ı̂n acest sens.
Pentru a vedea cum poate fi folosită metoda generică de sortare vom scrie un program,
prezentat ı̂n Figura 5.14, care citeşte un număr nelimitat de valori ı̂ntregi, le sortează şi
afişează rezultatul. Metoda readIntArray a clasei Reader (prezentată ı̂n anexă) este folosită
pentru a citi un şir de ı̂ntregi. Transformăm apoi şirul citit ı̂ntr-un şir de elemente care
implementează interfaţa Comparable. În linia 15 creăm şirul, iar ı̂n liniile 16-19 obiectele
care sunt stocate ı̂n şir. Sortarea este realizată ı̂n linia 22. În final, afişăm rezultatele ı̂n
liniile 26-29. De reţinut că metoda toString este implicit apelată 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.
5.7. ANEXĂ - CLASA READER 87

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 şir de ı̂ntregi, sortarea şi afişarea lor

5.7 Anexă - clasa Reader


1. package io ;
2. //clasa va trebui salvata intr-un director cu numele "io"
3. //directorul in care se afla "io" va trebui adaugat in CLASSPATH
4.
5. import java.io.* ;
6. import java.util.StringTokenizer ;
7. public class Reader
8. {
9. public static String readString()
10. {
11. BufferedReader in = new BufferedReader(
12. new InputStreamReader( System.in ) ) ;
13. try
14. {
15. return in.readLine() ;
16. }
17. catch(IOException e)
18. {
19. //ignore
20. }
21. return null ;
22. }
23.
24. public static int readInt()
25. {
26. return Integer.parseInt(readString()) ;
27. }
28.
29. public static double readDouble()
30. {
88 CAPITOLUL 5. MOŞTENIRE

31. return Double.parseDouble(readString()) ;


32. }
33.
34. public static char readChar()
35. {
36. BufferedReader in = new BufferedReader(
37. new InputStreamReader( System.in ) ) ;
38. try
39. {
40. return (char)in.read() ;
41. }
42. catch(IOException e)
43. {
44. //ignore
45. }
46. return ’\0’ ;
47. }
48.
49. public static int[] readIntArray()
50. {
51. String s = readString() ;
52. StringTokenizer st = new StringTokenizer( s ) ;
53. //aloca memorie pentru sir
54. int[] a = new int[ st.countTokens() ] ;
55.
56. for(int i=0; i<a.length; ++i)
57. {
58. a[i] = Integer.parseInt( st.nextToken() ) ;
59. }
60.
61. return a ;
62. }
63.
64.}

5.8 Probleme propuse


1. Scrieţi două metode generice min şi max, fiecare acceptând doi parametri de tip
Comparable. Folosiţi aceste metode ı̂ntr-o clasă numită MyInteger.

2. Scrieţi două metode generice min şi max, fiecare acceptând un şir de Comparable.
Folosiţi apoi aceste metode pentru tipul MyInteger.

3. Pentru exemplul cu clasa Shape modificaţi metodele readShape si main astfel ı̂ncât
ele să arunce şi să prindă excepţii (ı̂n loc să creeze un cerc de raza 0) atunci când se
observă o eroare la citire.
5.8. PROBLEME PROPUSE 89

4. Modificaţi clasa Shape astfel ı̂ncât ea să poată fi folosită de către un algoritm de
sortare generic.

5. Rescrieţi ierarhia de clase Shape pentru a reţine aria ca un membru privat, care
este calculat de către constructorul pentru Shape. Constructorii din clasele derivate
trebuie să calculeze aria si să trimită rezultatul către metoda super. Faceţi din area
o metodă finală care doar returnează valoarea acestui atribut.

6. Adăugaţi conceptul de poziţie la ierarhia Shape prin includerea coordonatelor ca


date membru. Adăugaţi apoi o metoda distance.

7. Scrieţi o clasă abstractă pentru Date din care să derivati apoi o clasă Gregorian Date
(dată ı̂n formatul nostru obişnuit).
Capitolul 6

Analiza eficienţei algoritmilor

În prima parte a acestei lucrări am examinat cum putem folosi programarea orientată pe
obiecte pentru proiectarea şi implementarea programelor ı̂ntr-un mod profesional. Totuşi,
aceasta este doar jumătate din problemă.
Calculatorul este folosit de obicei pentru a prelucra cantităţi mari de informaţie. Atunci
când executăm un program pe date de intrare de dimensiuni mari, trebuie să fim siguri
că algoritmul se va termina ı̂ntr-un timp rezonabil. Acest lucru este aproape ı̂ntotdeauna
independent de limbajul de programare folosit, ba chiar şi de metodologia aplicată (cum
ar fi programarea orientată pe obiecte, sau programarea procedurală).
Un algoritm este un set bine precizat de instrucţiuni pe care calculatorul le va executa
pentru a rezolva o problemă. Odată ce am găsit un algoritm pentru o anumită problemă,
şi am determinat că algoritmul este corect, pasul următor este de a determina cantitatea
de resurse, cum ar fi timpul şi cantitatea de memorie, pe care algoritmul le cere. Acest
pas este numit analiza algoritmului. Un algoritm care are nevoie de câţiva gigabaiţi de
memorie pentru a rula nu este bun de nimic pe maşinile de la ora actuală, chiar dacă el
este corect.
În acest capitol vom vedea:

• Cum putem estima timpul cerut de un algoritm

• Tehnici pentru reducerea drastică a timpului de execuţie al unui algoritm

• Un cadru matematic care descrie la un mod mai riguros timpul de execuţie


al algoritmilor

6.1 Ce este analiza algoritmilor?


Cantitatea de timp pe care orice algoritm o cere pentru execuţie depinde aproape ı̂ntotdeauna
de cantitatea de date de intrare pe care o procesează. Este de aşteptat că sortarea a 10.000
de elemente să necesite mai mult timp decât sortarea a 10 elemente. Timpul de execuţie
al unui algoritm este astfel o funcţie de dimensiunea datelor de intrare. Valoarea exactă a
acestei funcţii depinde de mai mulţi factori, cum ar fi viteza calculatorului pe care rulează
programul, calitatea compilatorului şi, ı̂n anumite situaţii, calitatea programului. Pentru
un program dat, care rulează pe un anumit calculator, putem reprezenta grafic timpul

90
6.1. CE ESTE ANALIZA ALGORITMILOR? 91

de execuţie al algoritmilor. În Figura 6.1 am realizat un astfel de grafic pentru patru
programe. Curbele reprezintă patru funcţii care sunt foarte des ı̂ntâlnite ı̂n analiza algo-
ritmilor: liniară, O(nlogn), pătratică şi cubică. Dimensiunea datelor de intrare variază de
la 1 la 100 de elemente, iar timpii de execuţie de la 0 la 5 milisecunde. O privire rapidă
asupra graficelor din Figura 6.1 şi Figura 6.2, ne lămureşte că ordinea preferinţelor
pentru timpii de execuţie este liniar, O(nlogn), pătratic şi cubic.
Să luăm ca exemplu descărcarea (download-area) unui fişier de pe Internet. Să presupunem
că la ı̂nceput apare o ı̂ntârziere de două secunde (pentru a stabili conexiunea), după care
descărcarea se va face la 1.6 KB/sec. În această situaţie, dacă fişierul de adus are N
kilobaiţi, timpul de descărcare a fişierului este descris de formula T(N)=N/1.6+2. Aceasta
este o funcţie liniară. Se poate calcula uşor că descărcarea unui fişier de 80K va dura aprox-
imativ 52 de secunde, ı̂n timp ce descărcarea unui fişier de două ori mai mare (160K) va
dura 102 secunde, deci cam de două ori mai mult. Această proprietate, ı̂n care timpul este
practic direct proporţional cu cantitatea de date de intrare, este specifică unui algoritm
liniar, şi constituie adeseori o situaţie ideală. Aşa cum se vede din grafice, unele curbe
neliniare pot conduce la timpi de execuţie foarte mari. În acest capitol vom prezenta
următoarele probleme:

• Cu cât este mai bună o curbă ı̂n comparaţie cu o altă curbă?

• Cum putem calcula pe care curbă se situează un anumit algoritm?

• Cum putem proiecta algoritmi care să nu se situeze pe curbele nefavorabile?

Figura 6.1 Timpi de execuţie pentru date de intrare mici

Figura 6.2 Timpi de execuţie pentru date de intrare moderate

O funcţie cubică este o funcţie al cărei termen dominant este N 3 , ı̂nmulţit cu o constantă.
De exemplu, 10N 3 + N 2 + 40N + 80 este o funcţie cubică. Similar, o funcţie pătratică
are termenul dominant N 2 ı̂nmulţit cu o constantă, iar o funcţie liniară are un termen
dominant care este N ı̂nmulţit cu o constantă.
Oricare dintre cele trei funcţii prezentate mai sus poate fi mai mică decât cealaltă ı̂ntr-un
punct dat. Acesta este motivul pentru care nu ne interesează valorile efective ale timpilor
de execuţie, ci rata lor de creştere. Acest lucru este justificabil prin trei argumente. În
primul rând, pentru funcţiile cubice, cum ar fi cea prezentată ı̂n Figura 6.2, atunci când
N are valoarea 1000, valoarea funcţiei cubice este aproape complet determinată de val-
oarea termenului cubic. Funcţia 10N 3 + N 2 + 40N + 80 are valoarea 10.001.040.080 pentru
N = 1000, din care 10.000.000.000 se datorează termenului 10N 3 . Dacă am fi folosit doar
termenul cubic pentru a estima valoarea funcţiei, ar fi rezultat o eroare de aproximativ
0.01%. Pentru un N suficient de mare, valoarea funcţiei este determinată aproape complet
de termenul ei dominant (semnificaţie termenului suficent de mare, depinde de funcţia ı̂n
cauză).
Un al doilea motiv pentru care măsurăm doar rata de creştere a funcţiilor este că valoarea
92 CAPITOLUL 6. ANALIZA EFICIENŢEI ALGORITMILOR

exactă a constantei multiplicative pentru termenul dominant diferă de la un calculator la


altul. De exemplu, calitatea compilatorului poate să influenţeze destul de mult valoarea
constantei. În al treilea rând, valorile mici pentru N sunt de obicei nesemnificative. Din
Figura 6.1 se observă că pentru N = 10, toţi algoritmii se ı̂ncheie ı̂n mai puţin de 3 ms.
Diferenţa dintre cel mai bun şi cel mai slab algoritm este mai mică decât un clipit de ochi.
Pentru a reprezenta rata de creştere a unui algoritm se foloseşte aşa-numita notaţie asimp-
totică (engl. ”Big-Oh notation”). De exemplu, rata de creştere pentru un algoritm pătratic
este notată cu O(N 2 ). Notaţia asimptotică ne permite să stabilim o ordine parţială ı̂ntre
funcţii prin compararea termenului lor dominant.
Vom dezvolta ı̂n acest capitol aparatul matematic necesar pentru analiza eficienţei algorit-
milor, urmărind ca această incursiune matematică să nu fie excesiv de formală. Vom arăta
apoi, pe bază de exemple, cum poate fi analizat un algoritm. O atenţie specială o vom
acorda tehnicilor de analiză a algoritmilor recursivi, prezentaţi ı̂n capitolul 6 al acestui
curs.

6.2 Notaţia asimptotică


Notaţia asimptotică are rolul de a estima timpul de calcul necesar unui algoritm pentru a
furniza rezultatul, funcţie de dimensiunea datelor de intrare.

6.2.1 O notaţie pentru ordinul de mărime al timpului de execuţie al


unui algoritm
Fie N mulţimea numerelor naturale, ℜ mulţimea numerelor reale. Fie f : N → [0, ∞) o
funcţie arbitrară. Definim mulţimea de funcţii:

O(f ) = {t : N → [0, ∞) | ∃c > 0, ∃n0 ∈ N, astf el incit ∀ n ≥ n0 avem t(n) ≤


c ∗ f (n)}

Cu alte cuvinte, O(f ) (se citeşte ”ordinul lui f”) este mulţimea tuturor funcţiilor t mărginite
superior de un multiplu real pozitiv al lui f, pentru valori suficient de mari ale argumentului
n. Vom conveni să spunem că t este ı̂n ordinul lui f (sau, echivalent, t este ı̂n O(f ) , sau
t ∈ O(f ) ) chiar şi atunci când t(n) este negativ sau nedefinit pentru anumite valori
n < n0 . În mod similar, vom vorbi despre ordinul lui f chiar şi atunci când valoarea f(n)
este negativă sau nedefinită pentru un număr finit de valori ale lui n; ı̂n acest caz, vom alege
n0 suficient de mare, astfel ı̂ncât pentru n ≥ n0 acest lucru să nu mai apară. De exemplu,
vom vorbi despre ordinul lui n/log n , chiar dacă pentru n=0 şi n=1 funcţia nu este
definită. În loc de t ∈ O(f ), uneori este mai convenabil să folosim notaţia t(n) ∈ O(f (n))
, subı̂nţelegând aici că t(n) şi f(n) sunt funcţii.
Fie un algoritm dat şi fie o funcţie t : N → [0, ∞) astfel ı̂ncât o anumită implementare a
algoritmului să necesite cel mult t(n) unităţi de timp pentru a rezolva un caz de mărime
n, n ∈ N . Principiul invarianţei1 ne asigură atunci că orice implementare a algoritmului
necesită un timp ı̂n ordinul lui t. Cu alte cuvinte, acest algoritm necesită un timp ı̂n
ordinul lui f pentru orice funcţie f : N → [0, ∞) pentru care t ∈ O(f ). În particular avem
1
Acest principiu afirmă că două implementări diferite ale unui algoritm nu diferă, ca eficienţă, decât
cel mult printr-o constantă multiplicativă
6.2. NOTAŢIA ASIMPTOTICĂ 93

relaţia: t ∈ O(t) . Vom căuta, ı̂n general, să găsim cea mai simplă funcţie f , astfel ı̂ncât
t ∈ O(f ).
Exemplul 6.1 Fie funcţia t(n) = 3n2 − 9n + 13 . Pentru n suficient de mare, vom avea
relaţia t(n) ≤ 4n2 . În consecinţă, luând c=4, 2
√ putem spune că t(n) ∈ O(n ). La fel de
2
bine puteam să spunem că t(n) ∈ O(13n − 2n + 12.5), dar pe noi ne interesează să
găsim o expresie cât mai simplă. Este adevărată şi relaţia t(n) ∈ O(n4 ), dar, aşa cum
vom vedea mai târziu, suntem interesaţi de a mărgini cı̂t mai strâns ordinul de mărime al
algoritmului, pentru a putea obiectiva cât mai bine durata sa de execuţie.
Proprietăţile de bază ale lui O(f ) sunt date ca exerciţii (1 - 5) şi ar fi recomandabil să le
studiaţi ı̂nainte de a trece mai departe.
Notaţia asimptotică defineşte o relaţie de ordine parţială ı̂ntre funcţii şi deci ı̂ntre eficienţa
relativă a diferiţilor algoritmi care rezolvă o anumită problemă. Vom da ı̂n continuare o
interpretare algebrică a notaţiei asimptotice. Pentru oricare două funcţii f, g : N → ℜ∗
definim următoarea relaţie binară: f ≤ g dacă O(f ) ⊆ O(g). Relaţia ”≤ ” este o relaţie de
ordine parţială (reflexivă, tranzitivă, antisimetrică) ı̂n mulţimea funcţiilor definite pe N şi
cu valori ı̂n [0, ∞) (exerciţiul 4). Definim şi o relaţie de echivalenţă: f ≡ g dacă O(f )=O(g).
Prin această relaţie obţinem clase de echivalenţă, o clasă de echivalenţă cuprinzând toate
funcţiile care diferă ı̂ntre ele printr-o constantă multiplicativă. De exemplu, lg n ≡ ln n
şi avem o clasă de echivalenţă a funcţiilor logaritmice, pe care o notăm generic cu O(log
n) . Notând cu O(1) clasa de echivalenţă a algoritmilor cu timpul mărginit superior de
o constantă (cum ar fi interschimbarea a două numere, sau maximul a trei elemente),
ierarhia celor mai cunoscute clase de echivalenţă este:
O(1) ⊂ O(log n) ⊂ O(n) ⊂ O(nlog n) ⊂ O(n2 ) ⊂ O(n3 ) ⊂ O(2n )
Această ierarhie corespunde unei clasificări a algoritmilor după un criteriu al performanţei.
Pentru o problemă dată, dorim mereu să obţinem un algoritm corespunzător unei clase cât
mai ”de jos” (cu timp de execuţie cât mai mic). Astfel, se consideră a fi o mare realizare
dacă ı̂n locul unui algoritm exponenţial găsim un algoritm polinomial. Exerciţiul 5 ne dă
o metodă de simplificare a calculelor ı̂n care apare notaţia asimptotică. De exemplu:
n3 + 4n2 + 2n + 7 ∈ O(n3 + (4n2 + 2n + 7)) = O(max(n3 , 4n2 + 2n + 7)) = O(n3 )
Ultima egalitate este adevărată chiar dacă max(n3 , 4n2 + 2n + 7) 6= n3 pentru 0 ≤ n ≤ 4
, deoarece notaţia asimptotică se aplică doar pentru n suficient de mare. De asemenea,
3 3 3 3 3
n3 −3n2 −n−8 ∈ O( n2 +( n2 −3n2 −n−8)) = O(max( n2 , n2 −3n2 −n−8)) = O( n2 ) = O(n3 )
chiar dacă pentru 0 ≤ n ≤ 6 polinomul este negativ. Exerciţiul 8 tratează cazul unui
polinom oarecare.
Notaţia O(f ) este folosită pentru a limita superior timpul necesar unui algoritm, măsurând
eficienţa (complexitatea computaţională) a algoritmului respectiv. Uneori este interesant
să estimăm şi o limită inferioară a acestui timp. În acest scop, definim mulţimea:
Ω(f ) = {t : N → [0, ∞) | ∃c > 0, ∃n0 ∈ N, astf el incit ∀ n ≥ n0 avem t(n) ≥
c ∗ f (n)}
Există o anumită dualitate ı̂ntre notaţiile O(f ) şi Ω(f ): pentru două funcţii oarecare
f, g : N → [0, ∞), avem:
94 CAPITOLUL 6. ANALIZA EFICIENŢEI ALGORITMILOR

f ∈ O(g) dacă şi numai dacă g ∈ Ω(f ) .

O estimare foarte precisă a timpului de execuţie se obţine atunci când timpul de execuţie
al unui algoritm este limitat atât inferior cât şi superior de câte un multiplu real pozitiv
al aceleaşi funcţii. În acest scop, introducem notaţia:

Θ(f ) = O(f ) ∩ Ω(f )

numită ordinul exact al lui f. Pentru a compara ordinele a două funcţii, notaţia Θ nu este
ı̂nsă mai puternică decât notaţia O , ı̂n sensul că O(f )=O(g) este echivalent cu Θ(f ) =
Θ(g).
Există situaţii ı̂n care timpul de execuţie al unui algoritm depinde simultan de mai mulţi
parametri. Aceste situaţii sunt tipice pentru anumiţi algoritmi care operează cu grafuri şi
la care timpul depinde atât de numărul de vârfuri cât şi de numărul de muchii. Notaţia
asimptotică se generalizează ı̂n mod natural şi pentru funcţii cu mai multe variabile. Astfel,
pentru o funcţie arbitrară f : N × N → [0, ∞) definim

O(f ) = {t : N × N → [0, ∞) | ∃c > 0, ∃n0 , m0 ∈ N, astf el incit ∀ m ≥ m0 , ∀ n ≥


n0 avem t(m, n) ≤ c ∗ f (m, n)}.

Similar se obţin şi celelalte generalizări.

6.3 Tehnici de analiza algoritmilor


Nu există o metodă standard pentru analiza eficienţei unui algoritm. Este mai curând o
chestiune de raţionament, intuiţie şi experienţă. Vom arăta pe bază de exemple cum se
poate efectua o astfel de analiză.

6.3.1 Sortarea prin selecţie


Considerăm algoritmul de sortare prin selecţia minimului, reprodus mai jos:

pentru i = 1, n − 1
//se calculează poziţia minimului lui a(i), a(i + 1), . . . , a(n)
P ozM in ← i //initializăm minimul cu indicele primului element
pentru j = i + 1, n
dacă a(i) < a(P ozM in) atunci
P ozM in = i
sfârşit dacă
//se aşează minimul pe poziţia i
aux ← a(i)
a(i) ← a(P ozM in)
a(P ozM in) ← aux
sfârşit pentru //după j
sfârşit pentru //după i

Timpul pentru o singură execuţie a ciclului pentru după variabila j poate fi mărginit
superior de o constantă a. În total, pentru un i fixat, ţinând cont de faptul că se realizează
6.3. TEHNICI DE ANALIZA ALGORITMILOR 95

n-i iteraţii, acest ciclu necesită un timp de cel mult b + a(n − i) unităţi, unde b este o
constantă reprezentând timpul necesar pentru iniţializarea buclei. O singură execuţie a
buclei exterioare are loc ı̂n cel mult c + b + a(n − i) unităţi de timp, unde c este o altă
constantă. Ţinând cont de faptul că bucla după j se realizează de n-1 ori, timpul total de
execuţie al algoritmului este cel mult:
Pn−1
d+ i=1 (c + b + a(n − i))
unităţi de timp, d fiind din nou o constantă. Simplificăm această expresie şi obţinem
a 2 a 2
2 n + (b + c − 2 )n + (d − c − b), de unde deducem că algoritmul necesită un timp ı̂n O(n ).
O analiză similară asupra limitei inferioare arată că timpul este de fapt ı̂n Θ(n ). Nu2

este necesar să considerăm cazul cel mai nefavorabil sau cazul mediu deoarece timpul de
execuţie al sortării prin selecţie este independent de ordonarea prealabilă a elementelor de
sortat.
În acest prim exemplu am analizat toate detaliile. De obicei ı̂nsă, detalii cum ar fi timpul
necesar iniţializarii ciclurilor nu se vor considera explicit, deoarece ele nu afectează ordinul
de complexitate al algoritmului. Pentru cele mai multe situaţii, este suficient să alegem
o anumită instrucţiune din algoritm ca barometru şi să numărăm de câte ori se execută
această instrucţiune. În cazul nostru, putem alege ca barometru testul
a[i] < a[P ozM in]
n(n−1)
din bucla interioară. Este uşor de observat că acest test se execută de 2 ori.

6.3.2 Sortarea prin inserţie


Timpul pentru algoritmul de sortare prin inserţie (secţiunea 5.3, Figura 5.8) este depen-
dent de ordonarea prealabilă a elementelor de sortat. Vom folosi comparaţia
tmp.lessThan( a[j-1] )
din ciclul for ca barometru.
Să presupunem că p este fixat şi fie n = a.length lungimea şirului. Cel mai nefavorabil
caz apare atunci când tmp < a[j − 1] pentru fiecare j ı̂ntre p şi 1, algoritmul făcând ı̂n
această situaţie p − 1 comparaţii. Acest lucru se ı̂ntâmplă (pentru fiecare valoare a lui p
de la 1 la n − 1) atunci când tabloul a este iniţial ordonat descrescător. Numărul total de
comparaţii pentru cazul cel mai nefavorabil este:
Pn n(n−1)
i=2 i −1= 2 ∈ Θ(n2 )
Vom estima acum timpul mediu necesar pentru un caz oarecare. Presupunem că elementele
tabloului a sunt distincte şi că orice permutare a lor are aceeaşi probabilitate de apariţie.
Atunci, dacă 1 ≤ k ≤ p , probabilitatea ca a[p] să fie cel de-al k-lea cel mai mare element
dintre elementele a[1], a[2], . . . , a[p] este p1 . Pentru un p fixat, condiţia a[p] < a[p − 1] este
falsă cu probabilitatea p1 , deci probabilitatea ca să se execute comparaţia ”tmp < a[j − 1]”
o singură dată ı̂nainte de ieşirea din bucla while este p1 . Comparaţia ”tmp < a[j − 1]” se
execută de exact două ori tot cu probabilitatea p1 , etc. Probabilitatea ca să se execute
comparaţia de exact p − 1 ori este p2 , deoarece aceasta se ı̂ntâmplă atât când tmp <
b[0] cât şi când b[0] ≤ tmp < b[1]! Numărul mediu de comparaţii, pentru un p fixat,
96 CAPITOLUL 6. ANALIZA EFICIENŢEI ALGORITMILOR

este ı̂n consecinţă, suma numărului de comparaţii pentru fiecare situaţie, ı̂nmulţită cu
probabilitatea de apariţie a acelei situaţii:

ci = 1 1i + 2 1i + . . . + (i − 2) 1i + (i − 1) 2i = i+1
2 − 1
i
P 2
Pentru a sorta n elemente, avem nevoie de ni=2 ci comparaţii, ceea ce este egal cu n +3n
4 −
2 Pn −1
Hn ∈ Θ(n ). Prin Hn = i=1 i ∈ Θ(log n) am notat al n-lea termen al seriei armonice.
Se observă că algoritmul de sortare prin inserare efectuează pentru cazul mediu de două ori
mai puţine comparaţii decât pentru cazul cel mai nefavorabil. Totuşi, ı̂n ambele situaţii,
numărul comparaţiilor este ı̂n Θ(n2 ).
Cu toate că algoritmul necesită un timp ı̂n Ω(n2 ) atât pentru cazul mediu cât şi pentru
cel mai nefavorabil caz, pentru cazul cel mai favorabil (când iniţial tabloul este ordonat
crescător) timpul este ı̂n O(n). De fapt, pentru cazul cel mai favorabil, timpul este şi ı̂n
Ω(n) (deci ı̂n Θ(n)).

6.3.3 Turnurile din Hanoi


Matematicianul francez Eduard Lucas a propus ı̂n 1883 o problemă care a devenit apoi
celebră mai ales datorită faptului că a prezentat-o sub forma unei legende. Se spune că
Brahma (Zeul Creaţiei la hinduşi) a fixat pe Pământ trei tije de diamant şi pe una din ele
a pus ı̂n ordine crescătoare 64 de discuri de aur de dimensiuni diferite, astfel ı̂ncât discul
cel mai mare era jos. Brahma a creat şi o mănăstire, iar sarcina călugărilor era să mute
toate discurile pe o altă tijă. Singura operaţiune permisă era mutarea câte unui singur disc
de pe o tijă pe alta, astfel ı̂ncât niciodată să nu se pună un disc mai mare peste un disc
mai mic. Legenda spune că sfârşitul lumii se va petrece atunci când călugării vor săvârşi
lucrarea. Aceasta se dovedeşte a fi o previziune extrem de optimistă asupra sfârşitului
lumii. Presupunând că ı̂n fiecare secundă se mută un disc şi lucrând fără ı̂ntrerupere, cele
64 de discuri nu pot fi mutate nici ı̂n 500 de miliarde de ani de la ı̂nceputul acţiunii!
Pentru a rezolva problema, vom numerota cele trei tije cu 1, 2 şi respectiv 3. Se observă
că pentru a muta cele n discuri de pe tija cu numărul i pe tija cu numărul j (i şi j iau
valori ı̂ntre 1 şi 3) este necesar să transferăm primele n − 1 discuri de pe tija i pe tija
6 − i − j (adică pe tija rămasă liberă), apoi să transferăm discul n de pe tija i pe tija j, iar
apoi retransferăm cele n − 1 discuri de pe tija 6 − i − j pe tija j. Cu alte cuvinte, reducem
problema mutării a n discuri la problema mutării a n − 1 discuri. Următoarea metodă
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 iniţiale, facem apelul


6.4. ANALIZA ALGORITMILOR RECURSIVI 97

hanoi(64,1,2);
Considerăm instrucţiunea println ca barometru. Timpul necesar algoritmului este expri-
mat prin următoare recurenţă:
(
1 dacă n = 1
t(n) =
2t(n − 1) + 1 dacă n > 1
Vom demonstra ı̂n secţiunea 6.4 că t(n) = 2n − 1. Rezultă t ∈ Θ(2n ).
Acest algoritm este optim ı̂n sensul că este imposibil să mutăm discuri de pe o tijă pe alta
cu mai puţin de 2n − 1 operaţii. Pentru a muta 64 de discuri vor fi ı̂n consecinţă necesare
un număr astronomic de 264 peraţii. Implementarea ı̂n oricare limbaj de programare care
admite exprimarea recursivă se poate face aproape ı̂n mod direct.

6.4 Analiza algoritmilor recursivi


Am văzut ı̂n exemplul precedent cât de puternică şi ı̂n acelaşi timp cât de elegantă este
recursivitatea ı̂n elaborarea unui algoritm. Cel mai important câştig al exprimării re-
cursive este faptul că ea este naturală şi compactă, fără să ascundă esenţa algoritmului
prin detaliile de implementare. Pe de altă parte, apelurile recursive trebuie folosite cu
discernământ, deoarece solicită şi ele resursele calculatorului (timp şi memorie). Analiza
unui algoritm recursiv implică aproape ı̂ntotdeauna rezolvarea unui sistem de recurenţe.
Vom vedea ı̂n continuare cum pot fi rezolvate astfel de recurenţe. Începem cu tehnica cea
mai simplă.

6.4.1 Metoda iteraţiei


Cu puţină experienţă şi intuiţie putem rezolva de multe ori astfel de recurenţe prin metoda
iteraţiei: se execută primii paşi, se intuieşte forma generală, iar apoi se demonstrează prin
inducţie matematică că forma este corectă. Să considerăm de exemplu recurenţa problemei
turnurilor din Hanoi. Se observă că pentru a muta n discuri este necesar să mutăm n − 1
discuri, apoi să mutăm un disc şi ı̂n final din nou n − 1 discuri. În consecinţă, pentru un
anumit n > 1 obţinem succesiv:
Pn−2
t(n) = 2t(n − 1) + 1 = 22 t(n − 2) + 2 + 1 = . . . = 2n−1 t(1) + i=0 2i
Rezultă t(n) = 2n − 1. Prin inducţie matematică se demonstrează acum cu uşurinţă că
această formă generală este corectă.

6.4.2 Inducţia constructivă


Inducţia matematică este folosită de obicei ca tehnică de demonstrare a unei aserţiuni
deja enunţate. Vom vedea ı̂n această secţiune că inducţia matematică poate fi utilizată cu
succes şi ı̂n descoperirea parţială a enunţului aserţiunii. Aplicând această tehnică, putem
simultan să demonstrăm o aserţiune doar parţial specificată şi să descoperim specificaţiile
care lipsesc şi datorită cărora aserţiunea este corectă. Vom vedea că această tehnică a
inducţiei constructive este utilă pentru rezolvarea anumitor recurenţe care apar ı̂n contex-
tul analizei algoritmilor. Începem cu un exemplu.
Fie funcţia F : N → N definită prin recurenţa:
98 CAPITOLUL 6. ANALIZA EFICIENŢEI ALGORITMILOR

(
0 dacă n = 1
f (n) =
f (n − 1) + n altfel
n(n+1)
Să presupunem pentru moment că nu ştim că f (n) = 2 . Avem:
Pn Pn
f (n) = i=0 i ≤ i=0 n = n2

şi deci f (n) ∈ O(n2 ). Aceasta ne sugerează să formulăm ipoteza inducţiei specificate parţial
IISP(n) conform căreia f este de forma f (n) = an2 + bn + c. Această ipoteză este parţială
ı̂n sensul că a, b şi c nu sunt ı̂ncă cunoscute. Tehnica inducţiei constructive constă ı̂n a
demonstra prin inducţie matematică această ipoteză incompletă şi a determina ı̂n acelaşi
timp valorile constantelor necunoscute a, b şi c.
Presupunem că IISP(n-1) este adevărată 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). Dacă dorim să arătăm
că IISP(n) este adevărată, trebuie să arătăm că f (n) = an2 + bn + c. Prin identificarea
coeficienţilor puterilor lui n, obţinem ecuaţiile 1 + b − 2a = b şi a − b + c = c, cu soluţia
a = b = 21 , c putând fi oarecare. Avem acum o ipoteză mai completă, pe care o numim
2
tot IISP(n), f (n) = n2 + n2 + c. Am arătat că dacă IISP(n-1) este adevărată pentru un
anumit n ≥ 1, atunci este adevărată şi IISP(n). Rămâne să arătăm că este adevărată şi
IISP(0). Trebuie să arătăm că f (0) = a02 + b0 + c. Ştim că f (0) = 0, deci IISP(0) este
2
adevărată pentru c = 0. În concluzie am demonstrat că f (n) = n2 + n2 pentru orice n.

6.4.3 Recurenţe liniare omogene


Există din fericire şi tehnici care pot fi folosite aproape automat pentru a rezolva anumite
clase de recurenţe. Vom ı̂ncepe prin a considera ecuaţii recurente liniare omogene, adică
ecuaţii de forma:

a0 tn + a1 tn−1 + . . . + ak tn−k = 0 (*)

unde ti sunt valorile pe care le căutăm, iar coeficienţii ai sunt constante.


Conform intuiţiei2 , vom căuta soluţii de forma:

tn = xn

unde x este o constantă (deocamdată necunoscută). Dacă ı̂nlocuim această soluţie ı̂n (*),
obţinem

a0 xn + a1 xn−1 + . . . + ak xn−k = 0

Soluţiile acestei ecuaţii sunt fie soluţia trivială x = 0, care nu ne interesează, fie soluţiile
ecuaţiei:

a0 xk + a1 xk−1 + . . . + ak = 0

care se numeşte ecuaţia caracteristică a recurenţei liniare şi omogene(*). Presupunând


deocamdată că cele k rădăcini r1 , r2 , . . . , rk ale acestei ecuaţii caracteristice sunt distincte,
se verifică uşor că orice combinaţie liniară
2
De fapt, adevărul este că aici nu este vorba de intuiţie, ci de experienţă
6.4. ANALIZA ALGORITMILOR RECURSIVI 99

Pk n
tn = i=1 ci ri

este o soluţie a recurenţei (*), unde constantele c1 , c2 , . . . , ck sunt determinate de condiţiile


iniţiale. Se poate demonstra faptul că (*) are soluţii numai de această formă.
Să exemplificăm prin recurenţa care defineşte şirul lui Fibonacci (secţiunea 7.1)

tn = tn−1 + tn−2 , n ≥ 2

iar t0 = 0, t1 = 1 . Putem să rescriem această recurenţă sub forma

tn − tn−1 − tn−2 = 0

care are ecuaţia caracteristică

x2 − x − 1 = 0

1∓ 5
cu rădăcinile r1,2 = 2 . Soluţia generală are forma:

tn = c1 r1n + c2 r2n

Impunând condiţiile iniţiale, t0 = 0, t1 = 1, obţinem

c1 + c2 = 0, n = 0

c1 r1n + c2 r2n = 1, n = 1

de unde determinăm

c1.2 = ∓ √15

Deci, tn = √1 (r n + r2n ). Observăm că r1 = φ, r2 = −φ−1 şi obţinem:


5 1

tn = √1 (φn − (−φ)−n )
5

care este cunoscuta relaţie a lui Moivre, descoperită la ı̂nceputul secolului XVIII. Nu
prezintă nici o dificultate să arătăm acum că timpul pentru calculul recursiv al şirului lui
Fibonacci este ı̂n Θ(φn ) .
Cum procedăm ı̂nsă atunci când rădăcinile ecuaţiei caracteristice nu sunt distincte? Se
poate arăta că dacă r este o rădăcină de multiplicitate m a ecuaţiei caracteristice, atunci
tn = rn , tn = nrn , tn = n2 rn , . . . , tn = nm−1 rn sunt soluţii pentru (*). Soluţia generală
pentru o astfel de recurenţă este atunci o combinaţie liniară a acestor termeni şi a ter-
menilor proveniţi de la celelalte rădăcini ale ecuaţiei caracteristice. Din nou, sunt de
determinat exact k constante din condiţiile iniţiale.
Vom da din nou un exemplu. Fie recurenţa

tn = 5tn−1 − 8tn−2 + 4tn−3

cu t0 = 0, t1 = 1, t2 = 2. Ecuaţia caracteristică are rădăcinile 1 (de multiplicitate 1) şi 2


(de multiplicitate 2). Soluţia generală este:

tn = c1 1n + c2 2n + c3 n2n

Din condiţiile iniţiale, obţinem c1 = −2, c2 = 2, c3 = − 21 .


100 CAPITOLUL 6. ANALIZA EFICIENŢEI ALGORITMILOR

6.4.4 Recurenţe liniare neomogene


Considerăm acum recurenţe de următoarea formă mai generală

a0 tn + a1 tn−1 + . . . ak tn−k = bn p(n) (**)

unde b este o constantă, iar p(n) este un polinom ı̂n n de grad d. Ideea generală este ca
prin manipulări convenabile să reducem un astfel de caz la o formă omogenă.
De exemplu, o astfel de recurenţă poate fi:

tn − 2tn−1 = 3n

În acest caz b = 3 şi p(n) = 1 un polinom de grad 0. O simplă manipulare ne permite să
reducem acest exemplu la forma (*). Înmulţind recurenţa cu 3, obţinem:

3tn − 6tn−1 = 3n+1

Înlocuind pe n cu n + 1 ı̂n recurenţa originală, avem:

tn+1 − 2tn = 3n+1

În final, scădem aceste două ecuaţii şi obţinem:

tn+1 − 5tn + 6tn−1 = 0

Am obţinut o recurenţă omogenă pe care o putem rezolva ca ı̂n secţiunea precedentă.


Ecuaţia caracteristică este:

x2 − 5x + 6 = 0

adică (x − 2)(x − 3) = 0.
Intuitiv, observăm că factorul (x − 2) corespunde părţii stângi a recurenţei originale, ı̂n
timp ce factorul (x − 3) a apărut ca rezultat al manipulărilor efectuate pentru a scăpa de
partea dreaptă.
Iată al doilea exemplu:

tn − 2tn−1 = (n + 5)3n

Manipulările necesare sunt puţin mai complicate. Trebuie să:

1. Înmulţim recurenţa cu 9

2. Înlocuim ı̂n recurenţă pe n cu n + 2

3. Înlocuim ı̂n recurenţă pe n cu n + 1 şi să ı̂nmulţim apoi cu -6.

Adunând cele trei ecuaţii obţinute anterior avem:

tn+2 − 8tn+1 + 21tn − 18tn−1 = 0

Am ajuns din nou la o ecuaţie omogenă. Ecuaţia caracteristică corespunzătoare este

x3 − 8x2 + 21x − 18 = 0
6.4. ANALIZA ALGORITMILOR RECURSIVI 101

adică (x − 2)(x − 3)2 . Încă o dată, observăm că factorul (x − 2) provine din partea stângă
a recurenţei originale, ı̂n timp ce factorul (x − 3)2 este rezultatul manipulării.
Generalizând acest procedeu, se poate arăta că pentru a rezolva (**) este suficient să luăm
următoarea ecuaţie caracteristică:

(a0 xk + a1 xk−1 + . . . + ak )(x − b)d+1 = 0

Odată ce s-a obţinut această ecuaţie, se procedează ca ı̂n cazul omogen.


Vom rezolva acum recurenţa corespunzătoare problemei turnurilor din Hanoi

tn = 2tn−1 + 1, n ≥ 1

iar t0 = 0. Rescriem recurenţa astfel

tn − 2tn−1 = 1

care este de forma (**) cu b = 1 şi p(n) = 1, un polinom cu grad 0. Ecuaţia caracteristică
este atunci (x − 1)(x − 2), cu soluţiile 1 şi 2. Soluţia generală a recurenţei este:

tn = c1 1n + c2 2n

Avem nevoie de două condiţii iniţiale. Ştim că t0 = 0; pentru a găsi cea de-a doua condiţie
calculăm

t1 = 2t0 + 1

Din condiţiile iniţiale, obţinem tn = 2n − 1.


Observaţie: dacă ne interesează doar ordinul lui tn , nu este necesar să calculăm efectiv
constantele ı̂n soluţia generală. Dacă ştim că tn = c1 1n + c2 2n , rezultă că tn ∈ O(2n ). Din
faptul că numărul de mutări a unor discuri nu poate fi negativ sau constant (deoarece avem
ı̂n mod evident tn ≥ n), deducem că c2 > 0. Avem atunci tn ∈ Ω(2n ) şi deci tn ∈ Θ(2n ).
Putem obţine chiar ceva mai mult.
Substituind soluţia generală ı̂napoi ı̂n recurenţa originară, găsim

1 = tn − 2tn−1 = c1 + c2 2n − 2(c1 + c2 2n−1 ) = −c1

Indiferent de condiţia iniţială, c1 este -1.

6.4.5 Schimbarea variabilei


Uneori putem rezolva recurenţe mai complicate printr-o schimbare de variabilă. În exem-
plele care urmează, vom nota cu T(n) termenul general al recurenţei şi cu tk termenul noii
recurenţe obţinute printr-o schimbare de variabilă. Presupunem pentru ı̂nceput că n este
o putere a lui 2.
Un prim exemplu este recurenţa

T (n) = 4T ( n2 ) + n, n > 1

ı̂n care ı̂nlocuim pe n cu 2k , notăm tk = T (2k ) = T (n) şi obţinem:

tk = 4tk−1 + 2k
102 CAPITOLUL 6. ANALIZA EFICIENŢEI ALGORITMILOR

Ecuaţia caracteristică a acestei recurenţe liniare este

(x − 4)(x − 2) = 0

şi deci tk = c1 4k + c2 2k . Înlocuim pe k cu lg n

T (n) = c1 n2 c2 n

Rezultă că T (n) ∈ O(n2 | n este o putere a lui 2).


Un al doilea exemplu ı̂l reprezintă ecuaţia

T (n) = 4T ( n2 ) + n2 , n > 1

Procedând la fel, ajungem la recurenţa

T (n) = 4T ( n2 ) + n2 , n > 1

cu ecuaţia caracteristică

(x − 4)2 = 0

şi soluţia generală tk = c1 4k + c2 k4k . Atunci,

T (n) = c1 n2 + c2 n2 lg n

şi obţinem că T (n) ∈ O(n2 log n | n este o putere a lui 2),
În fine, să considerăm şi exemplul

T (n) = 3T ( n2 ) + cn, n > 1

c fiind o constantă. Obţinem succesiv

T (2k ) = 3T (2k−1 ) + c2k

tk = 3tk−1 + c2k

cu ecuaţia caracteristică

(x − 3)(x − 2) = 0

tk = c1 3k + c2 2k

T (n) = c1 3lg n + c2 n

şi, deoarece

alg b = blg a

obţinem

T (n) = c1 nlg 3 + c2 n
6.5. PROBLEME PROPUSE 103

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


În toate aceste exemple am folosit notaţia asimptotică condiţionată. Pentru a arăta că
rezultatele obţinute sunt adevărate pentru orice n, este suficient să adăugăm condiţia ca
T(n) să fie eventual nedescrescătoare.
Putem enunţa acum o proprietate care este utilă ca reţetă pentru analiza algoritmilor cu
recursivităţi de forma celor din exemplele precedente. Proprietatea, a cărei demonstrare
o lăsăm ca exerciţiu, este foarte utilă la analiza algoritmilor Divide et Impera prezentaţi
ı̂n capitolul 7.
Propoziţie. Fie T : N → ℜ+ o funcţie eventual nedescrescătoare

T (n) = aT ( nb ) + cnk , n > n0


n
unde: n0 ≥ 1, b ≥ 2 şi k ≥ 0 sunt ı̂ntregi; a şi c sunt numere reale pozitive; n0 este o
putere a lui b. Atunci avem:


 Θ(nk ) dacă a < bk
T (n) ∈ Θ(nk log n) dacă a = bk

 Θ(nlogb a ) dacă a > bk

6.5 Probleme propuse


1. Care din următoarele afirmaţii sunt adevărate?

(a) n2 ∈ O(n3 )
(b) n3 ∈ O(n2 )
(c) 2n+1 ∈ O(2n )
(d) (n + 1)! ∈ O(n!)
(e) pentru orice funcţie f : N → R∗ , f ∈ O(n) ⇒ [f 2 ∈ O(n2 )]
(f) pentru orice funcţie f : N → R∗ , f ∈ O(n) ⇒ [2f ∈ O(2n )]

2. Demonstraţi că relaţia ”∈ O” este tranzitivă: dacă f ∈ O(g) şi g ∈ O(h), atunci
f ∈ O(h). Deduceţi de aici că dacă g ∈ O(h), atunci O(g) ⊆ O(h).

3. Găsiţi două funcţii f, g : N → R∗ , astfel ı̂ncât f ∈


/ O(g) şi g ∈
/ O(f ).

Indicaţie: f (n) = n, g(n) = n1+sin n .

4. Pentru oricare două funcţii f, g : N → R∗ definim următoarea relaţie binară: f ≤ g


dacă O(f ) ⊆ O(g). Demonstraţi că relaţia ”≤” este o relaţie de ordine parţială ı̂n
mulţimea funcţiilor definite pe N şi cu valori ı̂n R∗ .

Indicaţie: Trebuie arătat că relaţia este parţială, reflexivă, tranzitivă şi antisimetrică.
Ţineţi cont de exerciţiul 3.

5. Pentru oricare două funcţii f, g : N → R∗ demonstraţi că O(f + g) = O(max(f, g))


unde suma şi maximul se iau punctual.
104 CAPITOLUL 6. ANALIZA EFICIENŢEI ALGORITMILOR

6. Fie f (n) = am nm + . . . + a1 n + a0 un polinom de grad m, cu am > 0. Arătaţi că


f ∈ O(nm ).

7. O(n2 ) = O(n3 + (n2 − n3 )) = O(max(n3 , n2 − n3 )) = O(n3 ). Unde este eroarea?


Pn
8. i=1 i
= 1 + 2 + . . . + n ∈ O(1 + 2 + . . . + n) = O(max(1 + 2 + . . . + n)) = O(n).
Unde este eroarea?

9. Pentru oricare două funcţii f, g : N → R∗ demonstraţi că Θ(f ) + Θ(g) = Θ(f + g) =


Θ(max(f, g)) = max(Θ(f ), Θ(g)), unde suma şi maximul se iau punctual.

10. Analizaţi eficienţa următorilor algoritmi:

(a) pentru i=1,n


pentru j=1,5
{operaţie elementară}
(b) pentru i=1,n
pentru j=1,i+1
{operaţie elementară}
(c) pentru i=1,n
pentru j=1,6
pentru k=1,n
{operaţie elementară}
(d) pentru i=1,n
pentru j=1,i
pentru k=1,n
{operaţie elementară}

11. Construiţi un algoritm cu timpul ı̂n Θ(n log n).

12. Fie un algoritm:


pentru i=0,n
j←i
cât timp j<>0
j←j div 2
Găsiţi ordinul exact al timpului de execuţie.

13. Rezolvaţi următoarea recurenţă: tn − 3tn−1 − 4tn−2 = 0, n ≥ 2 cu t0 = 0, t1 = 1.

14. Care este timpul de execuţie pentru un algoritm recursiv cu recurenţa: tn = 2tn−1 +
n.

Indicaţie: Se ajunge la ecuaţia caracteristică (x − 2)(x − 1)2 = 0, iar soluţia generală


este tn = c1 2n + c2 1n + c3 n1n . Rezultă că tn ∈ O(2n ).
Substituind soluţia generală ı̂n recurenţă, obţinem că, indiferent de condiţia iniţială,
c2 = −2 şi c3 = −1. Atunci, toate soluţiile interesante ale recurenţei trebuie să aibă
c1 > 0 şi ele sunt toate ı̂n Ω(2n ), deci ı̂n Θ(2n ).
6.5. PROBLEME PROPUSE 105

15. Să se calculeze secvenţa de sumă maximă, formată din termeni consecutivi, a unui
şir de numere.

Soluţie: Problema are soluţii de diferite ordine de complexitate timp. Puteţi să
ı̂ncercaţi voi să le găsiţi. În acest moment vom da soluţia optimă din punct de vedere
al complexităţii. Este vorba de o singură parcurgere a şirului. Tehnica folosită este
cea a programării 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 EFICIENŢ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

Mulţi algoritmi necesită o reprezentare adecvată a datelor pentru a fi cu adevărat eficienţi.


Reprezentarea datelor, ı̂mpreună cu operaţiile care sunt permise asupra datelor formează o
structură de date. Fiecare structură de date permite inserarea de elemente. Structurile de
date diferă ı̂n privinţa modului ı̂n care permit accesul la membrii din grup. Unele permit
accesarea şi ştergerea arbitrară. Altele impun anumite restricţii, cum ar fi permiterea
accesului doar la ultimul sau la primul element inserat.
Acest capitol prezintă şapte dintre cele mai uzuale structuri de date: stive, cozi, liste
ı̂nlănţuite, arbori, arbori binari de căutare, tabele de repartizare (hash-tables) şi cozi de
prioritate. Vom defini fiecare structură de date şi vom furniza o estimare intuitivă pentru
complexitatea ı̂n timp a operaţiilor de inserare, ştergere şi accesare.
În acest capitol vom vedea:

• Descrierea structurilor de date uzuale, operaţiile permise pe ele şi timpii


lor de execuţie

• Pentru fiecare structură de date, vom defini o interfaţă Java conţinând


protocolul care trebuie implementat

• Unele aplicaţii ale structurilor de date

Vom urmări să evidenţiem faptul că specificarea operaţiilor suportate de o structură de
date, care ı̂i descrie funcţionalitatea, este independentă 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ă
pe obiecte: reutilizarea componentelor. Aşa cum vom vedea mai târziu ı̂n acest capitol,
structurile de date descrise sunt folosite ı̂n multe situaţii. Odată ce o structură de date a
fost implementată, ea poate fi folosită din nou şi din nou ı̂n aplicaţii de natură diversă1 .
Această abordare - separarea interfeţei de implementare - este o parte fundamentală a
orientării pe obiecte. Cel care foloseşte structura de date nu trebuie să vadă implementarea
ei, ci doar operaţiile admisibile. Aceasta ţine de partea de ı̂ncapsulare şi ascundere a
1
De altfel, bibliotecile Java cuprind majoritatea structurilor de date uzuale

107
108 CAPITOLUL 7. STRUCTURI DE DATE

informaţiei din programarea orientată pe obiecte. O altă parte importantă a programării


orientate pe obiecte este abstractizarea. Trebuie să proiectăm cu grijă structura de date,
deoarece vom scrie programe care folosesc aceste structuri de date fără să aibă acces la
implementarea lor. Aceasta va face ı̂n schimb ca interfaţa să fie mai curată, mai flexibilă,
şi, de obicei, mai uşor de implementat.
Toate structurile de date sunt uşor de implementat dacă nu ne punem problema eficienţei.
Acest lucru permite să adăugăm componente ”ieftine” ı̂n program doar pentru depanare.
Exerciţiile de la sfârşitul capitolului vă cer scrierea de implementări ineficiente, care sunt
adecvate pentru a procesa cantităţi mici de date. Putem apoi ı̂nlocui aceste implementări
”ieftine” cu implementări care au o performanţă (ı̂n timp şi/sau ı̂n spaţiu) mai bună
şi care sunt adecvate pentru procesarea unei cantităţi mai mari de informaţie. Deoarece
interfeţele sunt fixate, aceste modificări nu necesită practic nici o modificare ı̂n programele
care folosesc aceste structuri de date.
Vom descrie structurile de date prin intermediul interfeţelor. De exemplu, stiva este
precizată prin intermediul interfeţei Stack. Clasa care implementează această interfaţă
va implementa toate metodele specificate ı̂n Stack, la care se mai pot adăuga anumite
funcţionalităţi.

1. public interface MemCell


2. {
3. Object read() ;
4. void write( Object x ) ;
5. }

Figura 7.1 Interfaţă pentru clasa MemoryCell

Ca un exemplu, ı̂n Figura 7.1 este descrisă o interfaţă pentru clasa MemoryCell din capi-
tolul anterior. Interfaţa descrie funcţiile disponibile; clasa concretă trebuie să definească
aceste funcţii. Implementarea este prezentată ı̂n Figura 7.2 şi este identică cu cea din
capitolul anterior cu excepţia 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 să observaţi faptul că structurile de date definite ı̂n acest capitol reţin
referinţe către elementele inserate, şi nu copii ale elementelor. Din acest motiv este bine
ca ı̂n structura de date să fie plasate obiecte nemodificabile (cum ar fi String, Integer etc.)
pentru ca un utilizator extern să nu poată să schimbe starea unui obiect care este ı̂nglobat
ı̂ntr-o structură de date.
7.2. STIVE 109

7.2 Stive
O stivă este o structură de date ı̂n care orice tip de acces este permis doar pe ultimul
element inserat. Comportamentul unei stive este foarte asemănător cu cel al unui maldăr
de farfurii. Ultima farfurie adăugată va fi plasată ı̂n vârf, fiind ı̂n consecinţă uşor de
accesat, ı̂n timp ce farfuriile puse cu mai mult timp ı̂n urmă vor fi mai greu de accesat,
putând periclita stabilitatea ı̂ntregii grămezi. Astfel, stiva este adecvată ı̂n situaţiile ı̂n care
avem nevoie să accesăm doar elementul din vârf. Toate celelalte elemente sunt inaccesibile.
Cele trei operaţii naturale, inserare, ştergere şi căutare, sunt denumite ı̂n cazul unei stive
push, pop şi top. Cele trei operaţii sunt ilustrate ı̂n Figura 7.4. O interfaţă Java pentru
o stivă abstractă este prezentată ı̂n Figura 7.5. Interfaţa declară şi o metodă topAndPop
care combină două operaţii; tot aici apare şi un element nou: clauza throws prin care se
declară că o metodă poate să arunce către metoda apelantă o anumită excepţie. În cazul
nostru, metodele pop, top şi topAndPop pot arunca o UnderflowException ı̂n cazul ı̂n care
se ı̂ncearcă accesarea unui element când stiva este goală. Această excepţie va trebui să
fie prinsă până la urmă de o metodă apelantă. Clasa UnderflowException este definită ı̂n
Figura 7.3, şi ea este practic identică cu clasa Exception. Important pentru noi este că
diferă tipul, ceea ce ne permite să prindem doar această excepţie cu o secvenţă de tipul
catch(UnderflowException e):

1. package Exceptions ;
2. public class UnderflowException extends Exception
3. {
4. public UnderflowException(String thrower)
5. {
6. super( thrower ) ;
7. }
8. }

Figura 7.3 Codul pentru clasa UnderflowException. Această clasă este practic identică
cu Exception, diferă doar tipul.

Figura 7.6 prezintă un exemplu de utilizare al clasei Stack. Observaţi că stiva poate
fi folosită pentru a inversa ordinea elementelor; de remarcat un mic artificiu folosit aici:
ieşirea din ciclul for se realizează ı̂n momentul ı̂n care stiva se goleşte şi metoda topAnd-
Pop aruncă UnderflowException. Am recurs la acest artificiu pentru a ı̂nţelege mecanismul
excepţiilor. Folosirea acestui artificiu ı̂n mod curent nu este recomandată, deoarece reduce
lizibilitatea codului.
Fiecare operaţie pe stivă trebuie să ia o cantitate constantă de timp, indiferent de dimen-
siunea stivei, la fel cum accesarea farfuriei din vârful grămezii este rapidă, indiferent de
numărul de farfurii din teanc. Accesul la un element oarecare din stivă nu este eficient,
de aceea el nici nu este permis de către interfaţa noastră.
110 CAPITOLUL 7. STRUCTURI DE DATE

push pop,top
❏ ✡✡

❏❏
❫ ✡

Stiva

Figura 7.4 Modelul unei stive: Inserarea ı̂n stivă se face prin push, accesul prin top,
ştergerea prin pop.

Stiva este deosebit de utilă deoarece sunt multe aplicaţii pentru care trebuie să accesăm
doar ultimul element inserat. Un exemplu ilustrativ este salvarea parametrilor şi vari-
abilelor 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 Interfaţă pentru stivă

1. import DataStructures.* ;
2. import Exceptions.* ;
3.
4. public final class TestStack
5. {
6. public static void main( String args[])
7. {
8. Stack s = new StackAr() ;
9.
7.3. COZI 111

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 altă structură simplă de date este coada. În multe situaţii este important să avem acces
şi/sau să ştergem ultimul element inserat. Dar, ı̂ntr-un număr la fel de mare de situaţii,
acest lucru nu numai că nu mai este important, este chiar nedorit. De exemplu, ı̂ntr-o
reţea de calculatoare care au acces la o singură imprimantă este normal ca dacă ı̂n coada
de aşteptare se află mai multe documente spre a fi tipărite, prioritatea să ı̂i fie acordată
documentului cel mai vechi. Acest lucru nu numai că este corect, dar este şi necesar pentru
a garanta că procesul nu aşteaptă la infinit. Astfel, pe sistemele mari este normal să se
folosească cozi de tipărire. Operaţiile fundamentale suportate de cozi sunt:

• enqueue - inserarea unui element la capătul cozii

• dequeue - ştergerea primului element din coadă

• getFront - accesul la primul element din coadă

enqueue dequeue,getFront
✲ ✲
Queue

Figura 7.7 Modelul unei cozi: Intrarea se face prin enqueue, accesul prin getFront,
ştergerea prin dequeue.
112 CAPITOLUL 7. STRUCTURI DE DATE

Figura 7.7 ilustrează operaţiile pe o coadă. Tradiţional, metodele dequeue şi getFront
sunt combinate ı̂ntr-una singură. La fel am făcut şi noi aici. Metoda dequeue returnează
primul element, după care ı̂l scoate din coadă.
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 Interfaţă pentru coadă

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
7.4. LISTE ÎNLĂNŢUITE 113

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 afişa:


Continut: 0 1 2 3 4

Figura 7.8 ilustrează interfaţa pentru o coadă, iar Figura 7.9 prezintă modul de utilizare
al cozii. Deoarece operaţiile pe o coadă sunt restricţionate ı̂ntr-un mod asemănător cu
operaţiile pe o stivă, este de aşteptat ca şi aceste operaţii să fie implementate ı̂ntr-un timp
constant. Într-adevăr, toate operaţiile pe o coadă pot fi implementate ı̂n timp constant,
O(1).

7.4 Liste ı̂nlănţuite


Într-o listă ı̂nlănţuită elementele sunt reţinute discontinuu, spre deosebire de şiruri ı̂n care
elementele sunt reţinute ı̂n locaţii continue. Acest lucru este realizat prin stocarea fiecărui
obiect ı̂ntr-un nod care conţine obiectul şi o referinţă către următorul element ı̂n listă, ca
ı̂n Figura 7.10. În acest model se reţin referinţe atât către primul cât şi către ultimul
element din listă. Concret vorbind, un nod al unei liste arată la modul următor:

class ListNode
{
Object data ; //continutul listei
ListNode next ;
}

first last
❏ ✡✡
❏❏ ✡
✲ ✲ ✲ ✲
a1 a2 a3 a4

Figura 7.10 O listă simplu ı̂nlănţuită

În orice moment, putem adăuga ı̂n listă un nou element x prin următoarele operaţii:
114 CAPITOLUL 7. STRUCTURI DE DATE

last.next = new ListNode() ; //creaza un nou nod


last = last.next ; //actualizeaza last
last.data = x ; //plaseaza pe x in nod
last.next = null ; //ultimul nu are successor
În cazul unei liste ı̂nlănţuite un element oarecare nu mai poate fi găsit cu un singur acces.
Aceasta este oarecum similar cu diferenţa ı̂ntre accesarea unei melodii pe CD (un singur
acces) şi accesarea unei melodii pe casetă (acces secvenţial). Deşi din acest motiv listele
pot să pară mai puţin atractive decât şirurile, există totuşi câteva avantaje importante.
În primul rând, inserarea unui element ı̂n mijlocul listei nu implică deplasarea tuturor
elementelor de după punctul de inserare. Deplasarea datelor este foarte costisitoare (din
punct de vedere al timpului), iar listele ı̂nlănţuite permit inserarea cu un număr constant
de instrucţiuni de atribuire.
Merită observat că dacă permitem accesul doar la first, atunci obţinem o stivă, iar dacă
permitem inserări doar la last şi accesări doar la first, obţinem o coadă.
În general, atunci când folosim o listă, avem nevoie de mai multe operaţii, cum ar fi găsirea
sau ştergerea unui element oarecare din listă. Trebuie să permitem şi inserarea unui nou
element ı̂n orice punct. Aceasta este deja mult mai mult decât ne permite o stivă sau o
coadă.
Pentru a accesa un element ı̂n listă, trebuie să obţinem o referinţă către nodul care ı̂i core-
spunde. Evident că oferirea unei referinţe către un element ı̂ncalcă principiul ascunderii
informaţiei. Trebuie să ne asigurăm că orice acces la listă prin intermediul unei referinţe
nu periclitează structura listei. Pentru a realiza acest lucru, lista este definită ı̂n două
părţi: o clasă listă şi o clasă iterator. Figura 7.11 furnizează interfaţa de bază pentru o
listă ı̂nlănţuită, oferind şi metodele care descriu doar starea listei.
Figura 7.12 defineşte o clasă iterator care este folosită pentru toate operaţiile de acce-
sare a listei. Pentru a vedea cum funcţionează această clasă, să examinăm secvenţa de cod
clasică pentru afişarea tuturor elementelor din cadrul unei structuri liniare. Dacă lista ar
fi stocată ı̂ntr-un şir, secvenţa de cod ar arăta astfel:
//parcurge sirul a, afisand fiecare element
for(int index = 0; index < a.length; ++index)
System.out.println( a[index] ) ;
În Java elementar, codul pentru a itera o listă 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. package DataStructures;
2.
3. //Interfata pentru lista
4. //
5. //Accesul se realizeaza prin clasa ListItr
6. //
7. //********OPERATII PUBLICE*************
8. //boolean isEmpty()-->Intoarce true daca lista e goala
7.4. LISTE ÎNLĂNŢUITE 115

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


10.
11.public interface List
12.{
13. boolean isEmpty() ;
14. void makeEmpty() ;
15.}

Figura 7.11 Interfaţa pentru o listă abstractă

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 Interfaţă pentru un iterator de listă abstract

Mecanismul de iterare pe care l-ar folosi limbajul Java ar fi similar cu ceva de genul
(deoarece ListItr este o interfaţă, ListItr care urmează după new va fi ı̂nlocuit cu o clasă
care implementează 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

Iniţializarea dinaintea ciclului for creează un iterator al listei. Testul de terminare a ciclului
foloseşte metoda isInList definită pentru clasa ListItr. Metoda advance trece la următorul
nod din cadrul listei. Putem accesa elementul curent prin apelul metodei retrieve definită
ı̂n ListItr. Principiul general este că accesul fiind realizat prin intermediul clasei ListItr,
securitatea datelor este garantată. Putem avea mai mulţi iteratori care să traverseze
simultan o singură listă.
Pentru ca să funcţioneze corect, clasa ListItr trebuie să menţină două obiecte. În primul
rând, are nevoie de o referinţă către nodul curent. În al doilea rând are nevoie de o
referinţă către obiectul de tip List pe care ı̂l indică; această referinţă este iniţializată o
singură dată ı̂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 afişa: 4 3 2 1 0
Deşi discuţia s-a axat pe liste simplu ı̂nlănţuite, interfeţele din Figura 7.11 şi Figura
7.12 pot fi folosite pentru oricare tip de listă, indiferent de implementarea pe care o are
7.5. ARBORI BINARI DE CĂUTARE 117

la bază. Interfaţa nu precizează faptul că este nevoie de liste simplu ı̂nlănţuite.

7.5 Arbori binari de căutare


În capitolul anterior am văzut că cea mai eficientă căutare ı̂ntr-un şir este căutarea binară,
care se poate aplica atunci când elementele şirului sunt ordonate, timpul de execuţie fiind
logaritmic.
Să presupunem că avem nevoie de o structură de date ı̂n care, pe lângă căutare, dorim
să putem adăuga sau şterge eficient elemente. O astfel de structură este arborele binar de
căutare. Figura 7.14 ilustrează operaţiile de bază permise asupra unui arbore binar de
căutare. O posibilă interfaţă este prezentată ı̂n Figura 7.15.

insert find,remove
❏ ✡

❏ ✡

❫ ✡

Arbore binar de căutare

Figura 7.14 Modelul pentru arborele binar de căutare. Căutarea binară este extinsă pen-
tru a permite inserări şi ştergeri.

Setul de operaţii permise este acum extins pentru a permite găsirea unui element arbitrar,
alături de inserare şi ştergere. Metoda find ı̂ntoarce o referinţă către un obiect care este
egal (ı̂n sensul metodei compareTo din interfaţa Comparable) cu elementul căutat. Dacă nu
există nici un element egal cu cel căutat find aruncă o excepţie. Aceasta este o decizie de
proiectare. O altă posibilitate ar fi fost să ı̂ntoarcem null ı̂n cazul ı̂n care elementul căutat
nu este găsit. Diferenţa ı̂ntre cele două abordări constă ı̂n faptul că metoda noastră ı̂l
obligă pe programator să trateze explicit situaţia ı̂n care căutare nu are succes. În cealaltă
situaţie, dacă am fi ı̂ntors null, şi nu s-ar fi făcut verificările necesare, programul ar fi
generat un NullPointerException la prima tentativă de a folosi referinţa. Din punct de
vedere al eficienţei, versiunea cu excepţii ar putea fi ceva mai lentă, dar este puţin probabil
ca rezultatul să fie observabil, cu excepţia situaţiei ı̂n care codul ar fi foarte des executat
ı̂n cadrul unor situaţii ı̂n care viteza este critică.
În mod similar, inserarea unui element care deja este ı̂n arbore este semnalată printr-o
DuplicateItemException. Există şi alte posibile alternative. Una dintre constă ı̂n a permite
noii valori să suprascrie valoarea stocată.

1. package DataStructures ;
2.
3. import Exceptions.* ;
4.
5. //*********OPERATII PUBLICE*********
6. // void insert( x )-->insereaza x
7. // void remove( x )-->sterge x
8. // 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 Interfaţa pentru un arbore binar de căutare

Vom da ca exemplu un arbore binar care reţine stringuri. Deoarece ı̂n arbore putem reţine
doar obiecte de tip Comparable, nu putem folosi direct clasa String (deoarece aceasta nu
implementează clasa Comparable). Din acest motiv, vom scrie o clasă MyString, prezentată
ı̂n Figura 7.16 (această clasă implementează şi interfaţa Hashable, care va fi folosită ı̂n
secţiunea următoare). Figura 7.17 prezintă modul ı̂n care arborele binar de căutare
poate fi folosit pentru obiecte de tip MyString.
Interfaţa SearchTree mai are două metode suplimentare: una pentru a găsi cel mai mic
element şi una pentru a găsi cel mai mare element. Se poate arăta că transpirând un
pic mai mult se poate găsi foarte eficient şi cel mai mic al k-lea element, pentru oricare k
trimis ca parametru.
Iată o sinteză a timpilor de execuţie pentru operaţiile pe un arbore binar de căutare. Este
normal să sperăm că timpii de execuţie pentru find, insert şi remove să fie logaritmici,
deoarece aceasta este valoarea pe care am obţinut-o pentru căutarea binară. Din nefericire,
pentru cea mai simplă implementare a arborelui binar de căutare, acest lucru nu este
adevărat. Timpul mediu de execuţie este logaritmic, dar ı̂n cazul cel mai nefavorabil,
timpul de execuţie este O(n), caz care apare destul de frecvent. Totuşi, prin aplicarea
anumitor trucuri de algoritmică se pot obţine anumite structuri mai complexe (arbori
roşu-negru) care au ı̂ntr-adevăr un cost logaritmic pentru fiecare operaţie.
7.5. ARBORI BINARI DE CĂUTARE 119

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() ;
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ă arbori de căutare.


Programul va afişa: Georgica, Ionel nu a fost găsit.

Ce putem spune despre operaţiile findMin şi findMax? În mod cert, aceste operaţii necesită
un timp constant ı̂n cazul căutării binare, deoarece implică doar accesarea unui element
indiciat. În cazul unui arbore binar de căutare aceste operaţii iau acelaşi timp ca o căutare
obişnuită, adică O(log n) ı̂n cazul mediu şi O(n) ı̂n cazul cel mai nefavorabil. După cum
ı̂i sugerează şi numele, arborele binar de căutare este implementat ca un arbore binar,
necesitând astfel câte două referinţe pentru fiecare element.
7.6. TABELE DE REPARTIZARE 121

7.6 Tabele de repartizare


Există foarte multe aplicaţii care necesită o căutare dinamică bazată doar pe un nume.
O aplicaţie clasică este tabela de simboluri a unui compilator. Pe măsură ce compilează
programul, compilatorul trebuie să reţină numele (ı̂mpreună cu tipul, scopul, locaţia de
memorie) tuturor identificatorilor care au fost declaraţi. Atunci când vede un identificator
ı̂n afara unei instrucţiuni de declarare, compilatorul verifică să vadă dacă acesta a fost
declarat. Dacă a fost, compilatorul verifică informaţia adecvată din tabela de simboluri.
Având ı̂n vedere faptul că arborele binar de căutare permite acces logaritmic la obiecte
cu denumiri oarecare, de ce am avea nevoie de o altă structură de date? Răspunsul este
că arborele binar de căutare poate să dea un timp de execuţie liniar pentru accesul unui
element, iar pentru a ne asigura de cost logaritmic este nevoie de algoritmi mult mai
sofisticaţi.
Tabela de repartizare este o structură de date care evită timpul de execuţie liniar, ba
mai mult, suportă aceste operaţii ı̂n timp (mediu) constant. Astfel, timpul de acces la un
element din tabelă nu depinde de numărul de elemente care sunt ı̂n tabelă. În acelaşi timp,
tabela de repartizare nu foloseşte apeluri la rutinele de alocare a memoriei (ca arborele
binar). Aceasta face ca tabela de repartizare să fie rapidă ı̂n practică. Un alt avantaj faţă
de arborele binar de căutare este că elementele stocate ı̂n tabela de repartizare nu trebuie
să implementeze interfaţa Comparable.
Operaţiile permise sunt date ı̂n Figura 7.18 iar o interfaţă este prezentată ı̂n Figura
7.19. În acest caz, inserarea unui element care este duplicat ne generează o excepţie, ci
elementul va fi ı̂nlocuit cu noua valoare. Aceasta este o alternativă la metoda pe care
am aplicat-o ı̂n cazul arborilor binari de căutare. Tabela de repartizare funcţionează
doar pentru elemente care implementează interfaţa Hashable2 . Interfaţa Hashable cere o
funcţie de repartizare, care converteşte obiectul de tip Hashable ı̂ntr-un ı̂ntreg. Metoda
are următorul antet:

//intoarce un intreg intre 0 si tableSize-1


int hash(int tableSize) ;

Elementele dintr-o tabelă de repartizare trebuie să redefinească şi metoda equals. Figura
7.16 arată cum clasa MyString implementează interfaţa Hashable prin implementarea
metodelor hash şi equals. Un exemplu de utilizare a tabelelor de repartizare este dat ı̂n
Figura 7.20.
Una dintre utilizările obişnuite ale tabelelor de repartizare sunt dicţionarele. Un dicţionar
reţine obiecte care constau ı̂ntr-o cheie, care este căutată ı̂n dicţionar, şi definiţia cheii,
care este returnată. Putem utiliza tabele de repartizare pentru a implementa dicţionarul
astfel:

• obiectul stocat este o clasă care reţine atât cheia cât şi definiţia ei

• egalitatea, comparaţia şi funcţia de repartizare se bazează doar pe cheia din obiectul
stocat
2
În limba engleză, tabelele de repartizare se numesc ”Hashtable”, de aceea un element care poate fi
repartizat se numeşte ”Hashable”.
122 CAPITOLUL 7. STRUCTURI DE DATE

• căutarea se face prin construirea unui obiect e cu cheia dorită, urmată de apelarea
metodei find din tabela de repartizare

• definiţia este obţinută prin folosirea unei referinţe f căreia ı̂i este atribuită valoarea
ı̂ntoarsă de find.

insert find,remove
❏ ✡✡

❏❏
❫ ✡

Tabelă de repartizare

Figura 7.18 Modelul pentru tabela de repartizare: Oricare element etichetat poate fi
adăugat sau şters ı̂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 Interfaţa pentru tabele de repartizare.

1. import DataStructures.* ;
2. import Exceptions.* ;
3.
4. //program simplu pentru testarea arborilor de cautare
5.
6. public final class TestHashTable
7. {
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 foloseşte tabele de repartizare.

Programul va afisa: Gasit Georgica.

7.7 Cozi de prioritate


Deşi documentele trimise unei imprimante sunt aşezate, de obicei, ı̂ntr-o coadă, aceasta nu
este ı̂ntotdeauna cea mai bună variantă. De exemplu, un document poate să fie deosebit de
important, deci el ar trebui executat imediat ce imprimanta este disponibilă. De asemeni,
dacă imprimanta a terminat de tipărit un document, iar ı̂n coadă se află câteva documente
având 1-2 pagini şi un document având 100 de pagini, ar fi normal ca documentul lung să
fie tipărit ultimul, chiar dacă nu este ultimul document trimis.
Analog, ı̂n cazul unui sistem multiutilizator, sistemul de operare trebuie să decidă la un
moment dat care dintre mai multe procese trebuie să fie executat. În general, un proces
poate să se execute doar o perioadă de timp fixată. Şi aici este normal ca procesele care
au nevoie de un timp foarte mic să aibă prioritate.
Dacă vom atribui fiecărei sarcini câte un număr, atunci numărul mai mic (pagini tipărite,
resurse folosite) va indica o prioritate mai mare. Astfel, vom dori să accesăm cel mai mic
element dintr-o colecţie de elemente şi să ı̂l ştergem din cadrul colecţiei. Acestea sunt
operaţiile findMin şi deleteMin. Structura de date care oferă o implementare foarte efi-
cientă a acestor operaţii se numeşte coadă de priorităţi. Figura 7.21 ilustrează operaţiile
fundamentale pentru coada de priorităţi.
124 CAPITOLUL 7. STRUCTURI DE DATE
insert findMin,deleteMin
❏ ✡✡

❏❏
❫ ✡

Coadă de prioritate

Figura 7.21 Modelul pentru coada de priorităţi: Doar elementul minim este accesibil.

7.8 Aplicaţie
Vom da ı̂n continuare o variantă a clasei SearchTree, prezentată anterior, numită Node care
conţine metodele elementare, prezentate ı̂n comentariu, dar ı̂n plus va permite adăugarea
unui nod indiferent că acesta mai există ı̂n arbore precum şi faptul că rezultatul funţiei
de căutare este boolean deci nu necesită tratarea ca excepţie. Clasa BinaryTree este o
aplicaţie a clasei Node iar clasa TestBinaryTree conţine 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. APLICAŢ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. APLICAŢ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ă


În informatică apar frecvent situaţii ı̂n care rezolvarea unei probleme conduce la deter-
minarea unor vectori de forma:

x = (x1 , x2 , . . . , xn )

unde:

• fiecare componentă xi aparţine unei mulţimi finite Vi

• componentele vectorului x respectă anumite relaţii, numite condiţii interne, astfel


ı̂ncât x este o soluţie a problemei dacă şi numai dacă aceste condiţii sunt satisfăcute
de componentele x1 , x2 , . . . , xn ale vectorului.

Produsul cartezian V1 × V2 × . . . × Vn se numeşte spaţiul soluţiilor posibile. Elementele


acestui produs cartezian care respectă condiţiile interne se numesc soluţii ale problemei.

Exemplul 8.1 Fie două mulţimi de litere V1 = {A, B, C} şi V2 = {M, N }. Se cere să
se determine acele perechi (x1 , x2 ) cu proprietatea că dacă 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 şase soluţii posibile doar acestea ı̂ndeplinesc condiţiile puse ı̂n enunţul
problemei.

Exemplul 8.2 Se dă mulţimea cu elementele {A, B, C, D}. Se cere să se genereze toate
permutările elementelor acestei mulţimi.
Se cer deci mulţimile x = {x1 , x2 , x3 , x4 } care respectă condiţiile:

• xi 6= xj pentru i 6= j

• xi aparţine mulţimii V = V1 = V2 = V3 = V4 = {A, B, C.D} .

129
130 CAPITOLUL 8. METODA BACKTRACKING

Există mulţi vectori care respectă aceste condiţii: {A, B, C, D}, {B, A, C, D}, {B, C, D, A},
{B, D, A, C} etc. Mai exact, numărul de permutări ale elementelor unei mulţimi cu 4 el-
emente este 4! = 24.
O modalitate de rezolvare a problemei ar fi să se genereze toate cele 44 = 256 elemente
ale produsului cartezian V1 × V2 × V3 × V4 (reprezentând soluţiile posibile) şi să se aleagă
dintre ele cele 24 care respectă condiţiile interne. Să observăm ı̂nsă că dacă ı̂n loc de 4
elemente mulţimea noastră ar avea 7 elemente, vor exista 77 = 823.543 variante posibile,
dintre care doar 7! = 5040 vor respecta condiţiile interne.
Conform celor arătate mai sus, este indicat ca, pentru a rezolva o problemă, să elaborăm
algoritmi al căror timp de lucru să nu fie atât de mare, sau, dacă este posibil, să nu fie
exponenţial. Metoda backtracking este o metodă foarte importantă de elaborare a algorit-
milor pentru problemele de genul celor descrise mai sus. Deşi algoritmii de tip backtracking
au şi ei, ı̂n general, complexitate exponenţială, ei sunt totuşi net superiori unui algoritm
de genul celui descris mai sus, care generează toate soluţiile posibile.

8.2 Prezentarea metodei


Metoda backtracking urmăreşte să evite generarea tuturor soluţiilor posibile, scurtându-se
astfel drastic timpul de calcul.
Componentele vectorului x primesc valori ı̂n ordinea crescătoare a indicilor (noi vom nota
aceste valori cu v1 , v2 , . . . , vn cu scopul de a face diferenţa ı̂ntre o componentă care nu
are o valoare atribuită, xk , şi o componentă care are atribuită o valoare, vk ). Aceasta
ı̂nseamnă că lui xk nu i se atribuie o valoare decât după ce x1 , x2 , . . . , xk−1 au primit valori
care nu contrazic condiţiile interne. Mai mult decât atât, valoarea vk atribuită lui xk va
trebui astfel aleasă ı̂ncât v1 , v2 , . . . , vk să respecte şi ele anumite condiţii, numite condiţii
de continuare, care sunt deduse de către programator pe baza condiţiilor interne. Astfel,
dacă ı̂n exemplul 8.2, prima componentă, x1 , a primit valoarea v1 = A, este clar că lui
x2 nu i se va mai putea atribui această valoare (elementele unei permutări trebuie să fie
diferite).
Neı̂ndeplinirea condiţiilor de continuare exprimă faptul că oricum am alege valorile pentru
componentele xk+1 , . . . , xn , nu vom obţine nici o soluţie (deci condiţiile de continuare sunt
strict necesare pentru obţinerea unei soluţii). Prin urmare, se va trece la atribuirea unei
valori lui xk , doar dacă condiţiile de continuare pentru componentele x1 , x2 , . . . , xk (care
au valorile v1 , v2 , . . . , vk ) sunt ı̂ndeplinite. În cazul neı̂ndeplinirii condiţiilor de continuare,
se alege o nouă valoare pentru xk sau, ı̂n cazul ı̂n care mulţimea valorilor posibile, Vk , a
fost epuizată, se ı̂ncearcă să se facă o nouă alegere pentru componenta precedentă, xk−1 ,
a vectorului, micşorând pe k cu o unitate. Această revenire la componenta precedentă dă
numele metodei, exprimând faptul că dacă nu putem avansa, urmărim (engl. track = ”a
urmări”) ı̂napoi (engl. back = ”ı̂napoi”) secvenţa curentă din soluţie.
Trebuie observat faptul că respectarea condiţiilor de continuare de către v1 , v2 , . . . , vk nu
reprezintă o garanţie a faptului că vom obţine o soluţie continuând căutarea cu aceste val-
ori. Deci condiţiile de continuare sunt condiţii necesare pentru ca v1 , v2 , . . . , vk să conducă
la o soluţie, dar nu sunt condiţii suficiente.
Alegerea condiţiilor de continuare este foarte importantă, o alegere bună ducând la o re-
ducere substanţială a numărului de calcule. În cazul ideal, aceste condiţii ar trebui să
8.2. PREZENTAREA METODEI 131

fie nu numai necesare, ci chiar suficiente pentru obţinerea unei soluţii. În practică se
urmăreşte găsirea unor condiţii de continuare care să fie cât mai ”dure”, adică să elimine
din start cât mai multe soluţii neviabile. De obicei, condiţiile de continuare reprezintă
restricţia condiţiilor interne la primele k componente ale vectorului. Evident, condiţiile de
continuare ı̂n cazul k=n sunt chiar condiţiile interne.
De exemplu, o condiţie de continuare ı̂n cazul celei de-a doua probleme luată ca exemplu
ar fi:

vk 6= vi , ∀i = 1, k − 1
Prin metoda backtracking, orice vector este construit progresiv, ı̂ncepând cu prima com-
ponentă şi mergând către ultima, cu eventuale reveniri asupra valorilor atribuite anterior.
Reamintim că x1 , x2 , . . . , xn primesc valori ı̂n mulţimile V1 , V2 , . . . , Vn . Prin atribuiri sau
ı̂ncercări de atribuiri eşuate din cauza nerespectării condiţiilor de continuare, anumite
valori sunt ”consumate”. Pentru o componentă oarecare xk vom nota prin Ck mulţimea
valorilor consumate la momentul curent. Evident, Ck ⊂ Vk .
O descriere completă a stării ı̂n care se află algoritmul la un moment dat se poate face
prin precizarea următoarelor elemente:
1. numărul de componente ale vectorului x, cărora li s-au atribuit valori, având valoarea
k-1.
2. valorile curente v1 , v2 , . . . , vk−1 ale primelor k-1 componente ale vectorului x: x1 , x2 ,
. . ., xk−1 .
3. mulţimile de valori consumate C1 , C2 , . . . , Ck pentru fiecare din componentele x1 , x2 ,
. . ., xk .
Această descriere poate fi sintetizată ı̂ntr-un tabel numit configuraţie, având următoarea
formă:
!
v1 , . . . , vk−1 xk , xk+1 , . . . , xn
C1 , . . . , Ck−1 Ck , φ, . . . , φ
Semnificaţia unei astfel de configuraţii este următoarea:
1. ı̂n ı̂ncercarea de a construi un vector soluţie, componentelor x1 , x2 , . . . , xk−1 li s-au
atribuit valorile v1 , v2 , . . . , vk−1 .
2. aceste valori satisfac condiţiile de continuare
3. urmează să se atribuie o valoare componentei xk ; deoarece valorile consumate până
ı̂n prezent sunt cele din mulţimea Ck , componenta xk va primi o valoare vk din
Vk − Ck .
4. valorile consumate pentru componentele x1 , x2 , . . . , xk sunt cele din mulţimile C1 , C2 ,
. . ., Ck , cu precizarea că valorile curente v1 , v2 , . . . , vk−1 sunt consumate, deci apar
ı̂n mulţimile C1 , C2 , . . ., Ck−1 .
5. pentru componentele xk+1 , . . . , xn nu s-a ı̂ncercat nici o atribuire, deci nu s-a con-
sumat nici o valoare şi, prin urmare, Ck+1 , . . . , Cn sunt vide.
132 CAPITOLUL 8. METODA BACKTRACKING

6. până ı̂n acest moment au fost construite eventualele soluţii de forma:

• (c1 , . . .) cu c1 ∈ C1 − {v1 };
• (v1 , c2 , . . .) cu c2 ∈ C2 − {v2 };
.....
• (v1 , v2 , . . . , vk−2 , ck−1 , . . .) cu ck−1 ∈ Ck−1 − {vk−1 };
• (v1 , v2 , . . . , vk−1 , ck , . . .) cu ck ∈ Ck ;

Această ultimă afirmaţie este mai dificil de ı̂nţeles şi recomandăm reluarea ei după lec-
turarea exemplului de mai jos.
În construirea permutărilor mulţimii cu elementele {A, B, C, D} din Exemplul 8.2, pentru
k = 4, configuraţia
!
3 1 2 x4
{1, 2, 3} {1} {1, 2} {1, 2}

are, conform celor arătate mai ı̂nainte, următoarea semnificaţie:

1. componentele x1 , x2 , x3 au primit valorile 3,1,2.

2. tripletul 3,1,2 satisface condiţiile de continuare

3. urmează să se atribuie o valoare componentei x4 . Componenta x4 ia valori din


mulţimea V4 − C4 , adică 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 această situaţie

6. până ı̂n acest moment au fost deja construite soluţiile de forma (ı̂n ordinea ı̂n care
au fost descrise la punctul 6 de mai sus):

• (1, . . .) adică (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, . . .)


adică (2,1,3,4), (2,1,4,3), (2,3,1,4), (2,3,4,1), (2,4,1,3), (2,4,3,1)
• soluţii de această formă nu există, deoarece C2 − {v2 } = φ
• soluţii de forma (3, 1, 1, . . .); nu există soluţii de această formă ;
• soluţii de forma (3,1,2,1), (3,1,2,2); nu există soluţii de această formă ;

Metoda backtracking ı̂ncepe a fi aplicată ı̂n situaţia ı̂n care nu s-a făcut nici o atribuire
asupra componentelor lui x, deci nu s-a consumat nici o valoare, şi se ı̂ncearcă atribuirea
unei valori primei componente. Acest lucru este specificat prin configuraţia iniţială, a cărei
formă este:
!
x1 , . . . , xn
φ, . . . , φ

ı̂n care toate mulţimile Ck sunt vide.


Un alt caz special este cel al configuraţiilor soluţie, având forma:
8.2. PREZENTAREA METODEI 133

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

cu semnificaţia că vectorul (v1 , . . . , vn ) este soluţie a problemei. Astfel, pentru Exemplul
8.2, configuraţia:
!
A B C D
{A} {A, B} {A, B, C} {A, B, C, D}

are semnificaţia că vectorul (A,B,C,D) constituie o soluţie a problemei.


Metoda backtracking constă ı̂n a porni de la configuraţia iniţială şi a-i aplica acesteia
una dintre cele patru tipuri de transformări prezentate ı̂n continuare, până la epuizarea
tuturor variantelor. Fiecare transformare se aplică ı̂n anumite condiţii bine precizate. La
un moment dat doar o singură transformare poate fi aplicată. Presupunem că ne aflăm
ı̂n configuraţia descrisă anterior, ı̂n care s-au atribuit valori primelor k − 1 componente.
Transformările care pot fi aplicate unei configuraţii sunt:

8.2.1 Atribuie şi avansează


Acest tip de modificare are loc atunci când mai există valori neconsumate pentru xk (deci
Ck ⊂ Vk ), iar valoarea aleasă vk are proprietatea că (v1 , . . . , vk ) respectă condiţiile de
continuare. În acest caz valoarea vk se atribuie lui xk şi se adaugă mulţimii Ck , după care
se avansează la componenta următoare, xk+1 . Această modificare a configuraţiei poate fi
reprezentată ı̂n felul următor:
! vk !
. . . , vk−1 xk , xk+1 , . . . . . . , vk−1 , vk xk+1 , . . .

. . . , Ck−1 Ck , φ, . . . . . . , Ck−1 , Ck ∪ {Vk } φ, . . .
De exemplu, la generarea permutărilor, avem următoarea schimbare de configuraţie pornind
de la starea iniţială:
! A !
x1 x2 x3 x4 A x2 x3 x4

φ φφ φ {A} φ φ φ

8.2.2 Încercare eşuată


Acest tip de modificare are loc atunci când, ca şi ı̂n cazul anterior, mai există valori
neconsumate pentru xk , dar valoarea vk aleasă nu respectă condiţiile de continuare. În
acest caz, vk este adăugată mulţimii Ck (deci este consumată), dar nu se avansează la
componenta următoare. Modificarea este notată prin:
! vk !
. . . , vk−1 xk , xk+1 , . . . z }| { . . . , vk−1 xk , xk+1 . . .
===
. . . , Ck−1 Ck , φ, . . . . . . , Ck−1 Ck ∪ {Vk }, φ, . . .
În exemplul nostru cu generarea permutărilor, următoarea transformare este:
! A !
A x2 x3 x4 z }| { A x2 x3 x4
===
{A} φ φ φ {A} {A}φ φ
134 CAPITOLUL 8. METODA BACKTRACKING

8.2.3 Revenire
Acest tip de transformare apare atunci când toate valorile pentru componenta xk au fost
consumate (Ck = Vk ). În acest caz se revine la componenta precedentă, xk−1 , ı̂ncercându-
se atribuirea unei noi valori acestei componente. Este important de remarcat faptul că
revenirea la xk−1 implică faptul că pentru xk se vor ı̂ncerca din nou toate variantele
posibile, deci mulţimea Ck trebuie din nou să fie vidă. Transformarea este notată prin:
! !
. . . , vk−1 xk , xk+1 , . . . . . . , vk−2 xk−1 , xk , . . .

. . . , Ck−1 Ck , φ, . . . . . . , Ck−2 Ck−1 , φ, . . .
O situaţie de revenire, ı̂n exemplul cu generarea permutărilor este dată de configuraţia:
! !
3 1 2 x4 3 1 x3 x4

{1, 2, 3} {1} {1, 2} {1, 2, 3, 4} {1, 2, 3} {1} {1, 2} φ

8.2.4 Revenire după construirea unei soluţii


Acest tip de transformare se realizează atunci când toate componentele vectorului au
primit valori care satisfac condiţiile interne, adică a fost găsită o soluţie. În această
situaţie se revine din nou la cazul ı̂n care ultima componentă, xn urmează să primească o
valoare. Transformarea se notează astfel:
! !
. . . , vn . . . , vn−1 xn
✛sol
. . . , Cn . . . , Cn−1 Cn
În exemplul nostru cu generarea permutărilor, revenirea după găsirea primei soluţii este
dată de diagrama:
! !
1 2 3 4 1 2 3 x4
✛sol
{1} {1, 2} {1, 2, 3} {1, 2, 3, 4} {1} {1, 2} {1, 2, 3} {1, 2, 3, 4}
Revenirea după construirea unei soluţii poate fi considerată ca fiind un caz particular al
revenirii dacă adăugăm vectorului soluţie x o componentă suplimentară xn+1 , care nu
poate lua nici o valoare (Vn+1 = φ).
O problemă importantă este cea a ı̂ncheierii procesului de căutare a soluţiilor, sau, cu alte
cuvinte ne putem pune ı̂ntrebarea: transformările succesive aplicate configuraţiei iniţiale
se ı̂ncheie vreodată sau continuă la nesfârşit? Evident că pentru ca metoda backtracking
să constituie un algoritm trebuie să respecte şi ea proprietăţile unui algoritm enunţate
ı̂ncă din primul capitol, ı̂ntre care se află şi proprietatea de finitudine. Demonstrarea
finitudinii algoritmilor de tip backtracking se bazează pe următoarea observaţie simplă:
prin transformările succesive de configuraţie nu este posibil ca o configuraţie să se repete,
iar numărul de elemente al produsului cartezian V1 × V2 × . . . × Vn este finit; prin urmare,
la un moment dat se va ajunge la configuraţia:
!
x1 x2 . . . xn
V1 φ . . . φ
8.3. IMPLEMENTAREA METODEI BACKTRACKING 135

numită configuraţie finală. În configuraţia de mai sus ar trebui să aibă loc o revenire
(deoarece toate valorile pentru prima componentă au fost consumate), adică o deplasare a
barei verticale la stânga. Acest lucru este imposibil, şi algoritmul se ı̂ncheie deoarece nici
una din cele patru transformări nu poate fi aplicată. În practică, această ı̂ncercare de a
deplasa bara de pe prima poziţie (k = 1) pe o poziţie anterioară (k = 0) este utilizată pe
post de condiţie de terminare a algoritmului.
Înainte de a trece la implementarea efectivă a metodei backtracking ı̂n pseudocod, să
generăm diagramele de stare pentru Exemplul 8.1:
! A ! M ! ! N
x1 x2 A x2 A M sol A x2 z }| {
✲ ✲ ✛ ===
φ φ {A} φ {A} {M } {A} {M }

! ! B ! M !
A x2 x1 x2 B x2 B M
✛ ✲ ✲
{A} {M, N } {A} φ {A, B} φ {A, B} {M }

! N ! ! C
sol B x2 z }| { B x2 x1 x2
✛ === ✛ ✲
{A, B} {M } {A, B} {M, N } {A, B} φ

! M ! ! N
C x2 C M sol C x2
✲ ✛ ✲
{A, B, C} φ {A, B, C} {M } {A, B, C} {M }

! ! !
C N sol C x2 x1 x2
✛ ✛
{A, B, C} {M, N } {A, B, C} {M, N } {A, B, C} φ

8.3 Implementarea metodei backtracking


Procesul de obţinere a soluţiilor prin metoda backtracking este uşor de programat deoarece
la fiecare pas se modifică foarte puţine componente (indicele k, reprezentând poziţia barei,
componenta xk şi mulţimea Ck ).
Algoritmul corespunzător (ı̂n pseudocod) este următorul:

iniţializează (citeşte) mulţimile de valori, V1 , . . . , Vn


k ← 1 //se construieşte configuraţia iniţială
pentru i = 1, n
Ci ← φ
//acum ı̂ncepe efectiv aplicarea celor 4 transformări, funcţie de caz
cât timp k > 0 //k = 0ı̂nseamnă terminarea căutării
dacă k = n + 1 atunci //configuraţia este tip soluţie
reţine soluţia v1 , . . . , vn
k ← k − 1 //revenire după soluţie
altfel
dacă Ck 6= Vk atunci //mai există valori neconsumate
136 CAPITOLUL 8. METODA BACKTRACKING

alege o valoare vk din Ck − Vk


Ck = Ck ∪ vk //valoarea vk este consumată
dacă v1 , . . . , vk respectă condiţiile de continuare atunci
xk ← vk ; //atribuie şi
k ← k + 1; //avansează
altfel //ı̂ncercare eşuată, nu fac nimic
altfel //revenire
Ck ← φ, k ← k − 1
sfârşit cât timp

Algoritmul de mai sus funcţionează pentru cazul cel mai general, dar este destul de dificil
de programat din cauza lucrului cu mulţimile Ck şi Vk . Din fericire, adeseori ı̂n practică
mulţimile Vk au forma

Vk = {1, 2, . . . , sk }

deci fiecare mulţime Vk poate fi reprezentată foarte simplu, prin numărul său de elemente,
sk . Pentru a simplifica şi mai mult lucrurile, vom alege valorile pentru fiecare componentă
xk ı̂n ordine crescătoare, pornind de la 1, şi până la sk . În această situaţie, mulţimea de
valori consumate Ck va fi de forma {1, 2, . . . , vk } şi, drept consecinţă va putea fi reprezen-
tată doar prin valoarea vk .
Consideraţiile de mai sus permit ı̂nlocuirea algoritmului anterior, bazat pe mulţimi, cu un
algoritm simplificat, care lucrează numai cu numere.
La Exemplul 8.1, vom conveni să reprezentăm pe A,B,C prin valorile 1,2,3, iar pe M şi N
prin 1 şi 2. În această situaţie, configuraţiile succesive se vor reprezenta mai simplu astfel:

  1   1    
| x1 x2 ✲ 1 | x2 ✲ 1 2 | sol
✛ 1 | x2
etc

Algoritmul ı̂n pseudocod pentru cazul particular prezentat mai sus se concretizează ı̂n
următoarea metodă 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 observă că metoda backtracking apelează ı̂ncă două metode:

• metoda retSol, care, aşa cum sugerează şi numele ei, reţine soluţia, constând ı̂n
valorile vectorului x. Cel mai adesea această metodă realizează o simplă afişare a
soluţiei şi, eventual, o comparare cu soluţiile găsite anterior.

• metoda continuare(k) verifică dacă valorile primelor k componente ale vectorului x


satisfac condiţiile de continuare; ı̂n cazul afirmativ este ı̂ntoarsă valoarea true, iar ı̂n
caz contrar este ı̂ntoarsă valoarea false.

8.4 Probleme clasice care admit rezolvare prin metoda back-


tracking
8.4.1 Problema generării permutărilor
Se dă mulţimea A cu elementele {a1 , a2 , . . . , an }. Să se genereze toate permutările ele-
mentelor acestei mulţimi.
Se observă că această problemă este o simplă generalizare a Exemplului 8.2 din subcapi-
tolul precedent. Mai mult decât atât, problema poate fi redusă la a genera permutările
mulţimii de indici {1, 2, . . . , n}. În această situaţie vom avea V1 = V2 = . . . = Vn =
{1, 2, . . . , n}, deci putem aplica varianta simplificată a metodei bactracking.
Condiţiile interne pe care trebuie să le respecte un vector soluţie sunt:

xi 6= xj pentru ∀ i, j = 1, n, i 6= j.

Condiţiile de continuare pentru componenta numărul k, sunt o simplă restricţie a condiţiilor


interne:

xi 6= xk pentru ∀ i = 1, k − 1 .

Prin urmare codul pentru funcţia 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 şi ea extrem de simplă ı̂n această situaţie: se scriu elementele mulţimii
A, ordonate după 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 permutărilor se obţine din metoda backtracking
pentru cazul general ı̂nlocuind numărul de elemente al mulţimilor Vk , sk cu valoarea n.

8.4.2 Generarea aranjamentelor şi a combinărilor


Vom prezenta acum modalitatea prin care se poate adapta foarte uşor algoritmul de
generare a permutărilor unei mulţimi pentru a genera aranjamentele şi combinările acelei
mulţimi. Pentru a simplifica lucrurile, vom presupune că mulţimea A este formată din
primele n numere naturale, adică A = {1, 2, . . . , n}.
Reamintim faptul că prin aranjamente de n luate câte m (n ≥ m), notate Am n se ı̂nţeleg
toate mulţimile ordonate cu m elemente formate din elemente ale mulţimii A, cu alte
cuvinte toţi vectorii de forma:
x = (x1 , . . . , xn ), unde xi ∈ {1, 2, . . . , m}, xi 6= xj , ∀i, j = 1, n
Se observă că, din punct de vedere al reprezentării formale, singura diferenţă dintre aran-
jamente şi permutări este că aranjamentele au lungime m ı̂n loc de n. De altfel, pentru
m=n aranjamentele şi permutările coincid.

Exemplul 8.3 Aranjamentele de 3 luate câte 2 (A23 )sunt:

(1,2), (1,3), (2,1), (2,3), (3,1), (3,2).


Condiţiile interne şi, ı̂n consecinţă, condiţiile de continuare, sunt identice cu cele de la
generarea permutărilor. Prin urmare şi funcţia de continuare este identică cu cea de la
permutări. Unde este totuşi diferenţa? Având ı̂n vedere că lungimea vectorului este m şi
nu n, condiţia de găsire a unei soluţii trebuie ı̂nlocuită 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 ı̂nlocuită cu:

if(k==m)

Desigur, aceeaşi modificare este necesară şi ı̂n metoda retSol, ı̂n care secvenţa

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

se va ı̂nlocui cu

for(int i=0; i<m; ++i)

Să vedem acum modalitatea de generarea a combinărilor. Reamintim că prin combinări
de n luate câte m (notat Cnm ) se notează toate submulţimile cu m elemente ale mulţimii
A = {1, 2, . . . , n}.

Exemplul 8.4 Combinările de 3 luate câte 2 (C32 ) sunt:

(1,2), (1,3), (2,3).

Diferenţa ı̂ntre combinări şi aranjamente este dată de faptul că, ı̂n cazul combinărilor, or-
dinea ı̂n care apar componentele nu contează ( combinarea (1,2) este aceeaşi cu combinarea
(2,1) etc.; tocmai din acest motiv noi am optat ı̂n exemplul de mai sus să aranjăm com-
ponentele unei combinări ı̂n ordine crescătoare). Prin urmare, combinările unei mulţimi
cu n elemente luate câte m sunt definite de vectorii:

x = (x1 , . . . , xm ), unde x1 < x2 < . . . < xm .

Condiţia de continuare ı̂n cazul combinărilor va fi pur şi simplu:

xk > xk−1 pentru k > 1.

Metodele backtracking şi retSol sunt ı̂n cazul combinărilor identice cu cele de la aranja-
mente. Diferenţa apare la funcţia de continuare, care are următoarea formă (forma din
dreapta este mai criptică, dar mai elegantă):
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 variantă mai eficientă de generare
a combinărilor ı̂n care funcţia de continuare este complet eliminată.
140 CAPITOLUL 8. METODA BACKTRACKING

8.4.3 Problema damelor


Să se aşeze n dame pe o tablă de şah de dimensiune n×n astfel ı̂ncât damele să nu fie pe
aceeaşi linie, aceeaşi coloană sau aceeaşi diagonală (damele să nu se atace ı̂ntre ele).
Reamintim că ı̂n jocul de şah, o damă ”atacă” poziţiile aflate pe aceeaşi linie sau coloană
şi pe diagonală. O posibilă aşezare a damelor pe o tablă de şah de dimensiuni 4x4 este
dată ı̂n figura de mai jos:

×
×
×
×

Figura 8.1 O soluţie pentru problema damelor ı̂n cazul unei table de dimensiuni 4x4

Să vedem cum putem reformula problema damelor pentru a o aduce la o problemă de tip
backtracking. Se observă cu uşurinţă că pe o linie a tablei de şah se poate afla o singură
damă, prin urmare putem conveni că prima damă se va aşeza pe prima linie, a doua damă
pe a doua linie etc. Rezultă că pentru a cunoaşte poziţia damei numărul k este suficient să
ştim coloana pe care aceasta se găseşte. O soluţie a problemei se poate astfel reprezenta
printr-un vector

x = (x1 , x2 , . . . , xn ), xk ∈ {1, 2, . . . , n},

unde xk reprezintă coloana pe care se găseşte dama numărul k.


Cu această notaţie, vectorul soluţie corespunzător exemplului din figura de mai sus este:
(2,4,1,3).
Să vedem acum care este condiţia ca două dame distincte, k şi i, să se atace:

• ı̂n mod cert damele nu pot fi pe aceeaşi linie

• damele sunt pe aceeaşi coloană dacă xk = xi

• damele sunt pe aceeaşi diagonală dacă distanţa dintre abscise este egală cu distanţa
dintre ordonate, adică:

|xk − xi | = |k − i|

Condiţia de continuare este ca dama curentă, k, să nu atace nici una dintre damele care
deja sunt aşezate pe tablă, adică:

xk 6= xi şi |xk − xi | =
6 |k − i| pentru ∀ i = 1, k − 1.

Funcţia 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 ;
}

Modificările care trebuie aduse metodelor retSol şi backtracking sunt minime şi le lăsăm
ca exerciţiu.
Observaţie: Problema damelor este primul exemplu de problemă ı̂n care condiţiile de
continuare sunt necesare, dar nu sunt suficiente. De exemplu (pentru n=4), la ı̂nceput,
algoritmul va aşeza prima damă pe prima coloană, a doua damă pe a treia coloană, iar
cea de-a treia damă nu va putea fi aşezată pe nici o poziţie, fiind necesară o revenire.

8.4.4 Problema colorării hărţilor


Se dă o hartă ca cea din figura de mai jos, ı̂n care sunt reprezentate schematic 6 ţări,
dintre care unele au graniţe comune. Presupunând că dispunem doar de trei culori (roşu,
galben, verde), se cere să se determine toate variantele de colorare a hărţii astfel ı̂ncât
oricare două ţări vecine (care au frontieră comună) să fie colorate diferit.
 
0 1 1 0 0 1
 1 0 1 1 1 0 
T2 



 1 1 0 1 0 1 
T1 T3 T4 T5  
 0 1 1 0 1 1 
T6  
 0 1 0 1 0 1 
1 0 1 1 1 0

Figura 8.2 O hartă reprezentând şase ţări şi matricea de vecinătăţi desenată alăturat

Pentru a memora relaţia de vecinătate ı̂ntre două ţări vom utiliza o matrice de dimensiuni
6 × 6 numită vecin definită prin:
(
true dacă tările Ti şi Tj sunt vecine
vecin[i, j] =
f alse altfel

Figura 8.2 reprezintă matricea de vecinătăţi pentru harta cu 6 ţări ı̂n care s-a făcut
convenţia că 1 reprezintă true şi 0 reprezintă false.
Problema se poate generaliza uşor şi la o hartă cu n ţări care trebuie colorată cu m culori.
Vom utiliza pentru uşurarea expunerii harta cu 6 ţări de mai sus.
În această problemă, un vector soluţie x = (x1 , x2 , . . . , xn ) reprezintă o variantă de colorare
a hărţii, având semnificaţia că ţara numărul i va fi colorată cu culoarea xi . În exemplul
nostru, xi poate fi 1,2 sau 3, corespunzând respectiv culorilor roşu, galben, verde.
Condiţia de continuare este ca ţara căreia urmărim să ı̂i atribuim o culoare să aibă o
culoare distinctă de ţările cu care are graniţă. Cu alte cuvinte, trebuie să avem:

xi 6= xk dacă A[i, k] = 1, ∀i = 1, k − 1

Funcţia de continuare este ı̂n această situaţie:


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 identică cu cea standard şi o lăsăm ca exerciţiu.
Programul principal trebuie să realizeze citirea datelor (numărul de ţări n, numărul de
culori disponibile m şi matricea de vecinătăţi).
Pentru o mai bună ı̂nţelegere a mecanismului metodei backtracking aplicată la problema
colorării hărţilor, putem să ne imaginăm că dispunem de 6 cutii identice V1 , V2 , . . . , V6 ,
fiecare dintre cutii conţinând trei creioane colorate notate cu r - roşu, g - galben, v - verde.
Fiecare cutie Vk conţine creioanele care pot fi utilizate pentru colorarea ţării Tk .
O vizualizare a procesului de căutare a soluţiilor poate fi obţinut dacă aranjăm cele 6
cutii ı̂n ordine (fiecărei ţări ı̂i asociem o cutie) şi punem un semn ı̂naintea cutiei din care
urmează să se aleagă un creion (marcajul corespunde barei verticale de la configuraţii);
iniţial acest semn este ı̂n stânga primei cutii. Atunci când se alege un creion dintr-o cutie
corespunzătoare unei ţări el va fi aşezat fie pe ţara respectivă dacă nu există o ţară vecină
8.5. PROBLEME PROPUSE 143

cu aceeaşi culoare, fie lângă cutie ı̂n caz contrar. Astfel, mulţimile Ci de valori consumate
la un moment dat sunt alcătuite din creioanele de lângă cutia Vi şi de pe ţara Ti . Cu
aceste precizări, cele 4 modificări de configuraţie au următoarele semnificaţii concrete:

• atribuie şi avansează: se aşează creionul ales pe ţara corespunzătoare şi se trece la
cutia următoare

• ı̂ncercare eşuată: creionul ales este aşezat lângă cutia din care a fost scos

• revenire: creioanele corespunzătoare ţării curente sunt repuse ı̂n totalitate la loc şi
se trece la cutia precedentă

• revenire după găsirea unei soluţii: semnul este adus la stânga ultimei cutii.

Procesul se ı̂ncheie ı̂n momentul ı̂n care toate creioanele ajung din nou ı̂n cutiile ı̂n care
se aflau iniţial.

8.5 Probleme propuse


1. Să se afişeze toate modurile ı̂n care n persoane pot fi aşezate la o masă rotundă
precum şi numărul acestora.

Indicaţie: Există două posibilităţi de rezolvare:

(a) Se vor genera toate variantele posibile, prin metoda backtracking, şi se vor con-
toriza. Se va afişa apoi numărul lor. Va trebui ı̂nsă să ţineţi seama de faptul
că unele dispuneri sunt identice din cauza mesei circulare.
(b) Mult mai elegant, se va ţine cont de combinatorică. Astfel, cu n obiecte se pot
forma n! permutări. Cum, ı̂n cazul dispunerii lor circulare 1, 2, . . . , n, respec-
tiv 2, 3, . . . , n, 1, . . ., n, 1, 2, . . . , n − 1 sunt identice, rezultă că din n astfel de
permutări trebuie considerată doar una. Numărul de permutări va fi aşadar
n!
n = (n − 1)!.

2. Idem problema 1, cu precizarea că anumite persoane nu se agreează, deci nu pot fi


aşezate una lângă cealaltă. La intrare se mai furnizează o matrice simetrică A, cu
următoarea semnificaţie:
(
1 dacă nu se agează
A(i, j) =
0 altfel

3. Să se modifice algoritmul de generare a combinărilor prezentat ı̂n paragraful 7.4.2


astfel ı̂ncât funcţia de continuare să nu mai fie necesară.

Indicaţie: Pentru fiecare componentă x[k] se porneşte cu valoarea x[k − 1] + 1.

4. Găsiţi toate soluţiile de colorare cu trei culori a hărţii din Figura 8.2.
144 CAPITOLUL 8. METODA BACKTRACKING

5. Se dau n mulţimi A1 , A2 , . . . , An . Să se afişeze produsul lor cartezian.

Indicaţie: Generarea produsului cartezian ı̂nseamnă de fapt generarea ı̂ntregului


spaţiu de soluţii, adică un backtracking ı̂n care funcţia de continuare lipseşte iar
pe fiecare nivel al stivei sunt incărcate pe rând toate elementele unei mulţimi fixate.

6. Se dă o mulţime A = {1, 2, . . . , n}. Să se afişeze toate submulţimile acestei mulţimi.

Indicaţie: Se generează toţi vectorii caracteristici de lungime n. Prin vector car-


acteristic se ı̂nţelege un vector ce are doar valorile 1 sau 0 pentru fiecare element cu
semnificaţia:
(
1 dacă i aparţine submulţimii
x[i] =
0 altfel

Există şi o soluţie ce generează vectorii caracteristici de lungime n prin adunarea ı̂n
baza 2. Iniţial vectorul este nul, corespunzător mulţimi vide, iar apoi prin adunări
repetate se generează toate submulţimile. Atenţie, numărul total de submulţimi este
2n !

7. O firmă dispune de n angajaţi, dintre care p sunt femei. Firma trebuie să formeze o
delegaţie de m persoane dintre care k sunt femei. Să se afişeze toate delegaţiile care
se pot forma.

Indicaţie: Pentru a forma o delegaţie de k femei din p disponibile avem la dispoziţie


Cpk variante. Delagaţia de m persoane poate fi completată cu oricare din variantele
m−k
de Cn−p de alegere a bărbaţilor din delegaţie. Aşadar numărul total de variante este
k m−k
Cp ∗ Cn−p , ce urmează a fi calculat.
Generarea efectivă se bazează pe un vector caracteristic cu semnificaţia:
(
1 dacă persoana e femeie
x[i] =
0 altfel

Funcţia de continuare va număra femeile din delegaţie şi nu va lăsa ca numărul lor
să-l depăşească pe k.

8. Se consideră mulţimea A = {1, 2, . . . , n}. Să se furnizeze toate partiţiile aces-


tei mulţimi. (O partiţie a unei mulţimi este o scriere a mulţimii ca reuniune de
submulţimi disjuncte).

Indicaţie: Vom genera partiţia sub forma unui vector cu n componente ı̂n care
x[i] = k are semnificaţia că elementul i aparţine submulţimii k a partiţiei considerate.
Ca exemplu, pentru n = 4 putem avea, la un moment dat, vectorul x = (1, 2, 1, 2) ceea
ce corespunde partiţiei: A = {1, 3} ∪ {2, 4}. Ar fi de remarcat că vectorul caracter-
istic poate lua valori ce vor avea aceeaşi interpretare, ca de exemplu x = (2, 1, 2, 1)
ceea ce corespunde partiţiei: A = {2, 4} ∪ {1, 3}. Dar reuniunea e comutativă şi
partiţia astfel obţinută e identică cu anterioara. Pentru a evita acest lucru vom
8.5. PROBLEME PROPUSE 145

impune ca fiecare componentă a vectorului să poată avea cel mult valoarea k, unde
k este indicele elementului. Semnificaţia ar fi că elementul cu indicele 1 va putea
face parte doar din submulţimea 1, cel cu indicele 2 doar din submulţimile 1 şi 2
etc. O altă restricţie ar fi aceea că un element nu poate lua o valoarea mai mare ca
max + 1 unde max este valoare maximă a elementelor de rang inferior. Acest lucru
se justifică prin faptul că x = (1, 1, 3, 1) nu ar avea nici o semnificaţie.

9. Un comis-voiajor trebuie să viziteze un număr n de oraşe, pornind din oraşul numărul
1. El trebuie să viziteze fiecare oraş o singură dată, după care să se ı̂ntoarcă ı̂n oraşul
1. Cunoscând legăturile existente ı̂ntre oraşe, se cere să se găsească toate rutele posi-
bile pe care le poate efectua comis-voiajorul.

Indicaţie: Se va crea o matrice de adiacenţă (cunoscută din teoria grafurilor), care


este o matrice simetrică:
(
1 dacă există legătură ı̂ntre oraşul i şi j
A(i, j) =
0 altfel

Funcţia de continuare va testa dacă la elmentul actual se poate ajunge din anteriorul,
ı̂n vectorul x. Ca observaţie trebuie spus că pentru a obţine soluţiile distincte trebuie
făcut un artificiu asemănător cu cel de la problema anterioară.

10. Idem problema anterioară, cu precizarea că pentru fiecare drum ı̂ntre două oraşe
se cunoaşte distanţa care trebuie parcursă. Se cere să se găsească ruta de lungime
minimă.

Indicaţie: În momentul reţinerii soluţiei se va calcula lungimea drumului parcurs.


Se va compara această lungime cu lungimea anterioară considerată minimă şi se va
reţine valoarea actuală minimă ı̂mpreună cu drumul parcurs.
Această problemă este celebră prin faptul că este un exemplu pentru imposibilitatea
aflării soluţiei exacte altfel decât prin backtracking. Datorită complexităţii mari a
metodei s-au găsit metode mai puţin complexe (metodele euristice) dar care dau o
soluţie cu o marjă de aproximare.

11. Presupunem că avem de plătit o sumă s şi avem la dispoziţie un număr nelimitat
de bancnote şi monezi de valoare ν1 , ν2 , . . . , νn . Să se furnizeze toate variantele de
plată a sumei utilizând numai aceste monezi.

Soluţie: Rezolvarea acestei probleme va avea la bază o variantă mai modularizată a


clasei Bactracking prezentată anterior. Aceasta variantă o vom da ı̂n clasa Back-
tracking1, ce urmează, dar ea păstrează ı̂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ării, dată ı̂n clasa Plati ce implementează clasa abstractă Backtracking1,
este de a ı̂ncarca ı̂n stiva fiecare monedă sau bacnotă. Condiţia de continuare este
aceea ca suma pe care o am până ı̂n acel moment să fie mai mică sau egală decât
suma dorită. Dacă această condiţie este ı̂ndeplinită pot continua căutarea sau am
8.5. PROBLEME PROPUSE 147

dat peste soluţie, altfel voi fi nevoit să cobor un nivel ı̂n stivă. Ca un artificiu am
introdus ı̂n metoda succesor o condiţie suplimentară pentru a nu mai căuta o soluţie
deja găsită. De exemplu: soluţia 1+2 este aceeaşi cu 2+1. Am presupus de asemenea
că valorile monedelor şi bacnotelor sunt ordonate crescător. Cu un pic de efort se
poate evita acest lucru reţinând 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ăţi

12. Idem problema anterioară, cu precizarea că trebuie să plătim suma respectivă cu un
număr cât mai mic de monezi şi bancnote.

Indicţie: Fată de rezolvarea problemei anterioare se poate face, spre exemplu, o mod-
ificare care să compare, ı̂n momentul găsirii unei soluţii, indicele stivei cu cel găsit
la soluţiile anterioare.

13. Idem problema anterioară pentru cazul ı̂n care dispunem doar de un număr n1 , n2 , . . . , nn
de monezi de valoare ν1 , ν2 , . . . , νn .

Indicaţie: Deosebirile faţă de problemele anterioare constau:

• ı̂n stivă vom ı̂ncărca numărul de monezi sau bacnote folosite nu şi valoarea lor.
Astfel, fiecărui nivel ı̂i corespunde o anumită valoare;
• la funcţia succesor vom avea grijă să nu depăşim numărul alocat din fiecare
valoare iar pe de altă parte indicele stivei va trebui să nu depăşească numărul
de valori disponibil;
• sumele se vor calcula prin cumularea produselor dintre valoare şi numărul de
valori folosite.

14. Fiind dat un număr natural n, să se genereze toate partiţiile sale. O partiţie a unui
număr reprezintă scrierea sa ca sumă de numere naturale nenule.
8.5. PROBLEME PROPUSE 149

Indicaţie: O soluţie ar putea fi cea de la problema cu Plata unei Sume de Bani.

15. Fiind dat un număr natural n, să se genereze toate descompunerile sale ca sumă de
numere prime.

Indicaţie: Faţă de problema anterioară se poate verifica, la continuare, dacă numărul


ales este prim.

16. O fotografie alb-negru este reprezentată sub forma unei matrice cu elemente 0 sau
1. În fotografie sunt reprezentate unul sau mai multe obiecte. Porţiunile core-
spunzătoare obiectelor au valoarea 1 ı̂n matrice. Se cere să se determine dacă fo-
tografia reprezintă unul sau mai multe obiecte.
Exemplu: Matricea de mai jos reprezintă două obiecte:
 
0 1 1 0
 1 0 0 0 
 
 
 0 0 1 1 
1 1 1 0

17. Un teren dreptunghiular este ı̂mpărţit ı̂n m × n parcele, reprezentate sub forma unei
matrice A, cu m linii şi n coloane. Fiecare element al matricei este un număr real
care reprezintă ı̂nălţimea parcelei respective. Pe una dintre parcele se află plasată
o bilă. Se cere să se furnizeze toate posibilităţile prin care bila poate să părăsească
terenul, cunoscut fiind faptul că bila se poate rostogoli numai pe parcele ı̂nvecinate
a căror ı̂nălţime este strict inferioară ı̂nălţimii parcelei pe care bila se află.

Indicaţie: Aceasta şi problema anterioară sunt cazuri tipice de backtracking ı̂n plan.
Ideea rezolvării constă ı̂n ı̂ncercarea de a ajunge la o poziţie vecină respectând condiţi-
ile problemei. Modalitatea de mişcare este dată de cele 8 direcţii cardinale N, NV,
V, SV, S, SE, E, NE. La fiecare pas avem grijă să nu ieşim din spaţiul alocat decât
cu o soluţie nouă. Este posibil să dăm peste aceeaşi soluţie aşa ı̂ncât o vom reţine pe
anterioara. Practic, stiva va ı̂ncărca cordonatele curente şi direcţia ı̂n care urmează
să ne deplasăm.

18. Pe o tabla de sah de dimensiune 8 × 8 se ı̂ncearcă poziţionarea a n pioni după


următoarele reguli:

(a) Pe fiecare linie se află doi pioni.


(b) Pe fiecare coloană se află cel mult doi pioni.
(c) Pe fiecare paralelă la diagonala principală se află cel mult doi pioni.

Soluţie: Programul dat ı̂n continuare prezintă ı̂n Figura 8.5 clasa de bază Back-
tracking care este usor modificată faţă de cea dată ı̂n secţiunea 8.4, iar ı̂n Figura 8.6
clasa derivată Pions ce implementează metodele abstracte şi conţine funcţia 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

În acest capitol vom studia o altă metodă fundamentală de elaborare a algoritmilor, nu-
mită Divide et Impera. Ca şi Backtracking, Divide et Impera se bazează pe un principiu
extrem de simplu: descompunem problema ı̂n două (sau mai multe) subprobleme de di-
mensiuni mai mici; rezolvăm subproblemele, iar soluţia pentru problema iniţială se obţine
combinând soluţiile subproblemelor ı̂n care a fost descompusă. Rezolvarea subproblemelor
se face ı̂n acelaşi mod cu problema iniţială. Procedeul se reia până când subproblemele
devin atât de simple ı̂ncât admit o rezolvare imediată.
Încă din descrierea globală a acestei tehnici s-au strecurat elemente de recursivitate. Pentru
a putea ı̂nţelege mai bine această metodă de elaborare a algoritmilor care este eminamente
recursivă, vom prezenta ı̂n secţiunea 9.1 câteva elemente fundamentale referitoare la re-
cursivitate. Continuăm apoi ı̂n secţiunea 9.2 cu prezentarea generală a metodei, urmată
de rezolvarea anumitor probleme de Divide et Impera deosebit de importante: căutare
binară, sortarea prin interclasare, sortarea rapidă şi evaluarea expresiilor aritmetice.

9.1 Noţiuni elementare referitoare la recursivitate


În acest paragraf vom reaminti câteva elemente esenţial referitoare la recursivitate. Cei
care stăpânesc deja acest mecanism, pot să treacă direct la prezentarea metodei Divide et
Impera din paragraful 9.2.
Având ı̂n vedere faptul că recursivitatea este un mecanism de programare general, care nu
ţine doar de limbajul Java, prezentarea făcută va folosi şi limbajul pseudocod la descrierea
algoritmilor recursivi, pentru a nu ı̂ncărca prezentarea cu detalii de implementare.

9.1.1 Funcţii recursive


Recursivitatea este un concept care derivă ı̂n mod direct din noţiunea de recurenţă matem-
atică. Recursivitatea este un instrument elegant şi puternic pe care programatorii ı̂l au la
dispoziţie pentru a descrie algoritmii. Este interesant de reţinut faptul că programatorii
obişnuiau să utilizeze recursivitatea pentru a descrie algoritmii cu mult ı̂nainte ca limba-
jele de programare să suporte implementarea directă a acestui concept.
Din punct de vedere informatic, o subrutină (procedură sau funcţie) recursivă este o
subrutină care se autoapelează. Să luăm ca exemplu funcţia factorial, a cărei definiţie
matematică recurentă este:

153
154 CAPITOLUL 9. DIVIDE ET IMPERA

(
n ∗ F act(n − 1) pentru n ≥ 1
F act(n) =
1 pentru n = 0

Din exemplul de mai sus se observă că factorialul este definit funcţie de el ı̂nsuşi, dar
pentru o valoare a parametrului mai mică cu o unitate. Iată acum care este implementarea
recursivă a factorialului, folosind o funcţie algoritmică (stânga) şi implementarea Java
(dreapta):

funcţie fact(n) public static long fact(int n)


dacă n=0 atunci {
fact ← 1 if( n==0 )
altfel return 1 ;
fact ← n*fact(n-1) else
return return n*fact(n-1);
}
Se observă că funcţia de mai sus nu este decât o ”traducere” aproape directă a formulei
matematice anterioare. Trebuie să remarcăm că, aşa cum vom vedea ı̂n continuarea acestui
capitol, la baza funcţionării acestor funcţii stă un mecanism foarte precis, care nu este atât
de trivial cum ar părea la prima vedere.
Să luăm ca al doilea exemplu, calculul celebrului şir al lui Fibonacci, care este definit
recurent astfel:
(
F ib(n − 1) + F ib(n − 2) pentru n > 1
F ib(n) =
n pentru n = 0, 1

Implementarea ı̂n pseudocod, respectiv Java a şirului lui Fibonacci este:


funcţie fib(n) public static long fib(int n)
dacă 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 return fib(n-1)+fib(n-2);
}
Se observă că ı̂n ambele exemple am ı̂nceput cu aşa numita condiţie de terminare:

dacă n=0 sau n=1 atunci


Fib←n

care corespunde cazului ı̂n care nu se mai fac apeluri recursive. O funcţie recursivă care
nu are condiţie de terminare va genera apeluri recursive interminabile, care se soldează
inevitabil cu eroarea java.lang.StackOverflowError (depăşire de stivă, deoarece aşa cum
vom vedea, fiecare apel recursiv presupune salvarea anumitor date pe stivă, iar stiva are o
dimensiune finită). Condiţia de terminare ne asigură de faptul că atunci când parametrul
funcţiei devine suficient de mic, nu se mai realizează apeluri recursive şi funcţia este cal-
culată direct.
Ideea fundamentală care stă la baza ı̂nţelegerii profunde a mecanismului recursivităţii este
9.1. NOŢIUNI ELEMENTARE REFERITOARE LA RECURSIVITATE 155

aceea că ı̂n esenţă, un apel recursiv nu diferă cu nimic de un apel de funcţie obişnuit.
Pentru a veni ı̂n sprijinul acestei afirmaţii trebuie să studiem mai ı̂n amănunţime ce se
petrece ı̂n cazul unui apel de funcţie.
Se cunoaşte faptul că ı̂n situaţia ı̂n care compilatorul ı̂ntâlneşte un apel de funcţie, acesta
predă controlul execuţiei funcţiei respective, după care se revine la următoarea instrucţiune
de după apel. ı̂ntrebarea care apare ı̂n mod firesc este: de unde ştie compilatorul unde să
se ı̂ntoarcă la terminarea funcţiei? De unde ştie care au fost valorile variabilelor ı̂nainte
de a se preda controlul funcţiei? Răspunsul este simplu: ı̂nainte de a realiza un apel
de funcţie compilatorul salvează complet starea programului (linia de la care s-a realizat
apelul, valorile variabilelor locale, valorile parametrilor de apel) pe stivă, urmând ca la
revenirea din subrutină să reı̂ncarce starea care a fost ı̂nainte de apel de pe stivă.
Pentru exemplificare să considerăm următoarea procedură (nerecursivă) care afişează o
linie a unei matrice. Atât linia care trebuie afişată, cât şi matricea sunt transmise ca
parametru:

procedură AfisLin(a: tmatrice; n,lin: integer)


pentru i = 1,n
scrie a[lin,i]
return

Procedura AfisLin este apelată de procedura AfisMat descrisă mai jos, care afişează linie
cu linie o ı̂ntreagă matrice pe care o primeşte ca parametru:

procedură AfisMat(a: tmatrice; n: integer)


pentru i = 1,n
AfisLin(a,n,i)
return

Să presupunem că procedura AfisMat este apelată ı̂ntr-un program astfel:

...
AfisMat(a,5)
...

pentru a afişa o matrice de dimensiuni 5 × 5.


În momentul ı̂n care compilatorul ı̂ntâlneşte acest apel, el salvează pe stivă linia de la care
s-a făcut apelul (să spunem 2181), valoarea matricei a şi alte variabile locale declarate ı̂n
program:

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

Figura 9.1 Stiva programului după apelul procedurii AfisMat


156 CAPITOLUL 9. DIVIDE ET IMPERA

Controlul va fi apoi preluat de către procedura AfisMat, care intră ı̂n ciclul pentru cu
apelul: AfisLin(a,n,i) aflat să zicem la linia 2198.
În acest moment controlul va fi preluat de către procedura AfisLin, dar nu ı̂nainte de a
adăuga la vârful stivei linia de la care s-a făcut apelul, valorile parametrilor şi a variabilei
locale i:

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


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

Figura 9.2 Conţinutul stivei programului după apelul procedurii AfisLin

Procedura AfisLin va tipări prima linie a matricei, după care execuţia ei se ı̂ncheie. ı̂n
acest moment compilatorul consultă vârful stivei pentru a vedea unde trebuie să revină şi
care au fost valorile parametrilor şi variabilelor locale ı̂nainte de apel. Variabila i devine
2, şi din nou se apelează procedura AfisLin etc.
Remarcăm aici faptul că atât procedura AfisMat cât şi procedura AfisLin utilizează o vari-
abilă locală numită i. Nu poate exista nici o confuzie ı̂ntre cele două variabile, deoarece
ı̂n momentul execuţiei lui AfisLin, valoarea variabilei i din AfisMat este salvată pe stivă.
Să vedem acum evoluţia stivei program ı̂n cazul calculului recursiv al lui Fact(5). Pre-
supunem că linia 2145 are loc apelul recursiv: f act ← n ∗ f act(n − 1).
Pentru a realiza ı̂nmulţirea respectivă, trebuie ca ı̂ntâi să se calculeze Fact(n-1). Cum n
are valoarea 5, pe stivă se va depune F act(4). Abia după ce valoarea lui F act(4) va fi
calculată se poate calcula valoarea lui F act(5). Calculul lui F act(4) implică ı̂nsă calculul
lui F act(3), care implică la rândul lui calculul lui F act(2), F act(1), F act(0). Calculul lui
F act(0) se realizează prin atribuire directă, fără nici un apel recursiv:

dacă n=0 atunci


f act ← 1

ı̂n acest moment, stiva programului conţine toate apelurile recursive realizate până 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 Conţinutul stivei programului când n devine 0, ı̂n cazul calcului lui Fact(5).
Se presupune că linia cu apelul recursiv este situată la adresa 2145.
9.1. NOŢIUNI ELEMENTARE REFERITOARE LA RECURSIVITATE 157

F act(1) fiind calculat se poate reveni la calculul ı̂nmulţirii 2*f act(1) = 2, apoi, F act(2)
fiind calculat se revine la calculul ı̂nmulţirii 3*f act(2)=6 s.a.m.d. până se calculează
5*f act(4)=120 şi se revine ı̂n programul apelant.
Să vedem acum modul ı̂n care se realizează calculul recursiv al şirului lui Fibonacci. Vom
vedea că timpul de calcul al acestei recurenţe este incomparabil mai mare faţă de cal-
culul factorialului. Să presupunem că funcţia fib se apelează cu parametrul n = 3.
În această situaţie, se depune pe stivă apelul f ib(3) ı̂mpreună cu linia de unde s-a re-
alizat apelul (de exemplu, 2160). În 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). Această sumă nu poate fi calculată ı̂nainte de a-l calcula pe f ib(2). Cal-
culul lui f ib(2) presupune calcularea sumei f ib(1) + f ib(0). f ib(1) şi f ib(0) se calculează
direct la următorul apel recursiv, după care se calculează suma lor, f ib(2) = 2. Abia
acum se revine la suma f ib(2) + f ib(1) şi se calculează f ib(1), după care se revine şi se
calculează f ib(3).
Modul de calcul al lui f ib(n) recursiv se poate reprezenta foarte sugestiv arborescent.
Rădăcina arborelui este f ib(n), iar cei doi fii sunt apelurile recursive pe care f ib(n) le
generează, şi anume f ib(n − 1) şi f ib(n − 2). Apoi se reprezintă apelurile recursive gener-
ate 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 apelurilor recursive din şirul lui Fibonacci

Din figura de mai sus se observă că anumite valori ale şirului lui Fibonacci se calculează
de mai multe ori. fib(n) şi f ib(n − 1) se calculează o dată, f ib(n − 2) se calculează de două
ori, f ib(n − 3) de 3 ori s.a.m.d.

9.1.2 Recursivitatea nu ı̂nseamnă recurenţă


Implementarea recursivă a funcţiilor recurente este uşor de ı̂nţeles, datorită mecanismului
simplu de transpunere a recurenţei ı̂ntr-o funcţie recursivă. Totuşi, recursivitatea nu se
limitează doar la implementarea recurenţelor matematice. Putem defini la fel de bine
158 CAPITOLUL 9. DIVIDE ET IMPERA

şi operaţii recursive. O operaţie recursivă este definită funcţie de ea ı̂nsăşi, pentru o
problemă de dimensiune mică. De exemplu, operaţia de inversare a n caractere se poate
defini recursiv astfel: se extrage primul caracter din şir, apoi se inversează cele n − 1
caractere rămase după care se adaugă la final caracterul extras. Acest principiu ı̂l aplicăm
ı̂n exemplul care urmează.

Exemplul 9.1 Să se scrie o funcţie care citeşte o secvenţă de caractere până ce ı̂ntâlneşte
caracterul ”.”, după care afişează caracterele ı̂n ordine inversă.

Rezolvarea acestei probleme se poate formula recursiv astfel: inversarea caracterelor unui
şir de n elemente implică inversarea caracterelor rămase după citirea primului caracter, şi
scrierea ı̂n final a primului caracter:

Inv(n) = Citeste(a) + Inv(n − 1) + Scrie(a)

ı̂n consecinţă procedura de inversare va avea forma (parametrul n a fost eliminat, el fiind
dat ı̂n formulă doar pentru claritate):

funcţie inversare public static void inversare( )


citeşte a {
′ ′
dacă a <> . atunci inversare char a;
scrie inversare a=Reader.readChar();
return if(a!=’.’)
{
inversare();
}
System.out.print(a);
}

Este important de notat că pentru ca funcţia să funcţioneze corect, variabila a trebuie
declarată ca variabilă locală; astfel, toate valorile citite vor fi salvate pe stivă, de unde vor
fi extrase succesiv (ı̂n ordinea inversă citirii) după ı̂ntâlnirea caracterului ”.”.

Exemplul 9.2 Transformarea unui număr din baza 10 ı̂ntr-o bază b, mai mică decât 10.

Să ne reamintim algoritmul clasic de trecere din baza 10 ı̂n baza b. Numărul se ı̂mparte
la b şi se reţine restul. Câtul se ı̂mparte din nou la b şi se reţine restul ... până când câtul
devine mai mic decât b. Rezultatul se obţine prin scrierea ı̂n ordine inversă a resturilor
obţinute.
Formularea recursivă a acestei rezolvări pentru trecerea unui număr n ı̂n baza b este:
(
T ransf (n div b) + Scrie(n mod b) pentru n ≥ b
T ransf (n) =
− pentru n < b

Procedura care realizează acest cod este descrisă astfel:


9.2. PREZENTAREA METODEI DIVIDE ET IMPERA 159

funcţie transform(n:integer) public static void transform(int n)


rest=n mod b {
dacă 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 că ı̂n această funcţie variabila rest trebuie să fie declarată local, pentru a fi
salvată pe stivă, ı̂n timp ce variabila b este bine să fie declarată global, deoarece valoarea
ei nu se modifică, salvarea ei pe stivă ocupând spaţiu inutil.

9.2 Prezentarea metodei Divide et Impera


Divide et Impera este o metodă specială prin care se pot aborda anumite categorii de
probleme. Ca şi celelalte metode de elaborare a algoritmilor, Divide et Impera se bazează
pe un principiu extrem se simplu: se descompune problema iniţială ı̂n două (sau mai multe)
subprobleme de dimensiune mai mică, care se rezolvă, după care soluţia problemei iniţiale
se obţine combinând soluţiile subproblemelor ı̂n care a fost descompusă. Se presupune că
fiecare dintre problemele ı̂n care a fost descompusă problema iniţială se poate descompune
ı̂n alte subprobleme, la fel cum a fost descompusă şi problema iniţială. Procedeul de
descompunere se repetă până când, după descompuneri succesive, se ajunge la probleme
de dimensiune mică, pentru care există rezolvare directă.
Evident, nu orice gen de problemă se pretează la a fi abordată cu Divide et Impera. Din
descrierea de mai sus reiese că o problemă abordabilă cu această metodă trebuie să aibă
două proprietăţi:

1. Să se poată descompune ı̂n subprobleme

2. Soluţia problemei iniţiale să se poată construi simplu pe baza soluţiei subproblemelor

Modul ı̂n care metoda a fost descrisă, conduce ı̂n mod natural la o implementare recursivă,
având ı̂n vedere faptul că şi subproblemele se rezolvă ı̂n acelaşi mod cu problema iniţială.
Iată care este forma generală a unei funcţii Divide et Impera:

funcţie DivImp(P: Problemă)


dacă Simplu(P) atunci
RezolvăDirect(P) ;
altfel
Descompune(P, P1, P2) ;
DivImp(P1) ;
DivImp(P2) ;
Combină(P1, P2) ;
return
160 CAPITOLUL 9. DIVIDE ET IMPERA

În consecinţă, putem spune că abordarea Divide et Impera implică trei paşi la fiecare nivel
de recursivitate:
1. Divide problema ı̂n două subprobleme.

2. Stăpâneşte (Cucereşte) cele două subprobleme prin rezolvarea acestora ı̂n mod re-
cursiv.

3. Combină soluţiile celo două subprobleme ı̂n soluţia finală pentru problema iniţială.

9.3 Căutare binară


Căutarea binară este o metodă eficientă de regăsire a unor valori ı̂ntr-o secvenţă ordonată.
Deşi este trecută ı̂n acest capitol, căutarea binară nu este un exemplu tipic de problemă
Divide et Impera, deoarece ı̂n cazul ei se rezolvă doar una din cele două subprobleme, deci
lipseşte faza de recombinare a soluţiilor. Enunţul problemei de căutare binară este:

Se dă un vector cu n componente (ı̂ntregi), ordonate crescător şi un număr ı̂ntreg. Să se
decidă dacă se găseşte ı̂n vectorul dat, şi, ı̂n caz afirmativ, să se furnizeze indicele poziţiei
pe care se găseşte.

O rezolvare imediată a problemei presupune parcurgerea secvenţială a vectorului dat, până


când p este găsit, sau am ajuns la sfârşitul vectorului. Această rezolvare ı̂nsă nu foloseşte
faptul că vectorul este sortat.
Căutarea binară procedează ı̂n felul următor: se compară p, cu elementul din mijlocul
vectorului; dacă p este egal cu acel element, căutarea s-a ı̂ncheiat. Dacă este mai mare, se
caută doar ı̂n prima jumătate, iar dacă este mai mic, se caută doar ı̂n a doua jumătate.
Cititorul atent a observat cu siguranţă că ı̂n această situaţie problema nu se descompune
ı̂n două subprobleme care se rezolvă, după care se construieşte soluţia, ci se reduce la una
sau la alta din subprobleme. Cei trei paşi ai lui Divide et Impera sunt ı̂n această situaţie:
1. Divide: ı̂mparte şirul de n elemente ı̂n care se realizează căutarea ı̂n două şiruri cu
n/2 elemente.

2. Stăpâneşte: Caută ı̂n una dintre cele două jumătăţi, funcţie de valoarea elementului
din mijloc.

3. Combină: Nu există.
Iată care este funcţia care realizează căutarea elementului x ı̂n şirul a, ı̂ntre indicii low şi
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 ;
}

Poziţia pe care se găseşte elementul x ı̂n şirul a este dată de apelul:

poz = binarySearch(a,x,0,a.length-1)

9.4 Sortarea prin interclasare (MergeSort)


Sortarea prin interclasare este, alături de sortarea rapidă (QuickSort) şi sortarea cu ansam-
ble (HeapSort), una dintre metodele eficiente de ordonare a elementelor unui şir. Enunţul
problemei este următorul:

Să se ordoneze crescător un vector cu n componente ı̂ntregi.

Principiul de rezolvare este următorul: se ı̂mparte vectorul ı̂n două părţi egale şi se
sortează fiecare jumătate, apoi se interclasează cele două jumătăţi. Descompunerea ı̂n
două jumătăţi se realizează până când se ajunge la vectori cu un singur element, care
nu mai necesită sortare. Algoritmul de sortare prin interclasare urmează ı̂ndeaproape
paradigma Divide et Impera. În mare, modul ei de operare este:

1. Divide: ı̂mparte şirul de n elemente care urmează a fi sortat ı̂n două şiruri cu n/2
elemente

2. Stăpâneşte: Sortează recursiv cele două subşiruri utilizând sortarea prin interclasare

3. Combină: Interclasează subşirurile sortate pentru a obţine rezultatul final.

Procedura MergeSort de mai jos implementează algoritmul de sortare prin interclasare.


Apelul iniţial al funcţiei este:

mergeSort(a,0,a.length-1)

unde n este numărul 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);
}
}
Funcţia de interclasare ı̂n acest caz este analoagă cu funcţia de interclasare obişnuită
a două şiruri, diferenţa constând ı̂n faptul că acum se interclasează două jumătăţi ale
aceluiaşi şir, iar rezultatul se va depune ı̂n final tot ı̂n şirul interclasat. Lăsăm scrierea
acestei rutine ca exerciţiu.

9.5 Sortarea rapidă (QuickSort)


Sortarea rapidă este, aşa cum ı̂i spune şi numele, cea mai rapidă metodă de sortare cunos-
cută ı̂n prezent . Există foarte multe variante ale acestei metode, o parte dintre ele având
doar rolul de a micşora timpul de execuţie ı̂n cazul cel mai nefavorabil. Vom prezenta
aici varianta clasică, despre care veţi remarca cu surprindere că este neaşteptat de simplă.
Enunţul problemei este identic cu cel de la sortarea prin interclasare.

Să se ordoneze crescător un vector de numere ı̂ntregi.

Metoda de sortare rapidă prezentată ı̂n acest paragraf este dintr-un anumit punct de
vedere complementara metodei Mergesort. Diferenţa ı̂ntre cele două metode este dată de
faptul că ı̂n timp ce la Mergesort mai ı̂ntı̂i vectorul se ı̂mpărţea ı̂n două părţi după care se
sorta fiecare parte şi apoi se interclasau cele două jumătăţi, la Quicksort ı̂mpărţirea se face
ı̂n aşa fel ı̂ncât cele două şiruri să nu mai trebuiască a fi interclasate după sortare, adică
primul şir să conţină doar elemente mai mici decât al doilea şir. Cu alte cuvinte, ı̂n cazul
lui Quicksort etapa de recombinare este trivială, deoarece problema este astfel ı̂mpărţită ı̂n
subprobleme astfel ı̂ncât să nu mai fie necesară interclasarea şirurilor. Etapele lui Divide
et Impera pot fi descrise ı̂n această situaţie astfel:
1. Divide: Împarte şirul de n elemente care urmează a fi sortat ı̂n două şiruri, astfel
ı̂ncât elementele din primul şir să fie mai mici decât elementele din al doilea şir

2. Stăpâneşte: Sortează recursiv cele două subşiruri utilizând sortarea rapidă

3. Combină: Şirul sortat este obţinut din concatenarea celor două subşiruri sortate.
Funcţia care realizează ı̂mpărţirea ı̂n subprobleme (astfel ı̂ncât elementele primului şir să
fie mai mici decât elementele celui de-al doilea) se datorează lui C. A. Hoare, care a găsit
o metodă de a realiza această ı̂mpărţire (numită partiţionare) ı̂n timp liniar.
Procedura de partiţionare rearanjează elementele tabloului funcţie de primul element,
numit pivot, astfel ı̂ncât elementele mai mici decât primul element sunt trecute ı̂n stânga
9.5. SORTAREA RAPIDĂ (QUICKSORT) 163

lui, iar elementele mai mari decât primul element sunt trecute ı̂n dreapta lui. De exemplu,
dacă avem vectorul:
a = (7, 8, 5, 2, 3),
atunci procedura de partiţionare va muta elementele 5,2 şi 3 ı̂n stânga lui 7, iar 8 va fi
ı̂n dreapta lui. Cum se realizează acest lucru? Şirul este parcurs simultan de doi indici:
primul indice, low, pleacă de la primul element şi este incrementat succesiv, al doilea indice,
high, porneşte de la ultimul element şi este decrementat succesiv. În situaţia ı̂n care a[low]
este mai mare decât a[high], elementele se interschimbă. Partiţionarea este ı̂ncheiată ı̂n
momentul ı̂n care cei doi indici se ı̂ntâlnesc (devin egali) undeva ı̂n interiorul şirului. La
un pas al algoritmului, fie se incrementează low, fie se decrementează high; ı̂ntotdeauna
unul dintre cei doi indici, low sau high, este poziţionat pe pivot. Atunci când low indică
pivotul, se decrementează high, iar atunci când high indică pivotul se incrementează low.
Iată cum funcţionează partiţionarea pe exemplul de mai sus. La ı̂nceput, low indică primul
element, iar high indică ultimul element:
a = (7, 8, 5, 2, 3)
↑ ↑
low high
Deoarece a[low] > a[high] elementele 7 şi 3 se vor interschimba. După 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 şi 7 se vor interschimba. După 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 şi 2 se vor interschimba. După 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, fără a se
realiza interschimbări.
În acest moment low şi high s-au suprapus (au devenit egale), deci partiţionarea s-a
ı̂ncheiat. Pivotul este pe poziţia a 4-a, care este de fapt şi poziţia lui finală ı̂n şirul
sortat.
Funcţia de partiţionare de mai jos primeşte ca parametri limitele inferioară, respectiv su-
perioară ale şirului care se partiţionează şi returnează poziţia pe care se află pivotul ı̂n
finalul partiţionării. Poziţia pivotului este importantă deoarece ne dă locul ı̂n care şirul
va fi despărţit ı̂n două subşiruri.
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
}

Două observaţii importante merită făcute asupra acestei funcţii:

1. Variabila pozPivot poate lua valoarea false dacă pivotul este indicat de low sau true
dacă pivotul este indicat de high. Atribuirea pozP ivot =!pozP ivot are ca efect
schimbarea stării acestei variabile din false ı̂n true sau invers.

2. Funcţia se foloseşte ı̂n mod inteligent de transmiterea prin valoare a parametrilor,


deoarece modifică variabilele low şi high, fără ca această modificare să afecteze pro-
gramul principal.

Procedura de sortare propriu-zisă, respectă structura Divide et Impera obişnuită, doar că
funcţia de recombinare a soluţiilor nu mai este necesară, deoarece am realizat partiţionarea
ı̂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ă o expresie aritmetică ı̂n care operanzii sunt simbolizaţi prin litere mici (de la a la
z), iar operatorii sunt ’+’, ’-’, ’/’, şi ’*’ cu semnificaţia cunoscută. Se cere să se scrie un
program care transformă expresia ı̂n formă poloneză postfixată.

Reamintim faptul că forma poloneză postfixată (Lukasiewicz) este obţinută prin scrierea
operatorului după cei doi operanzi, şi nu ı̂ntre ei. Această formă are avantajul că nu
9.6. EXPRESII ARITMETICE 165

necesită paranteze pentru a schimba prioritatea operatorilor. Ea este utilizată adeseori ı̂n
informatică 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 formă poloneză constă ı̂n a
căuta care este operatorul din expresie cu prioritatea cea mai mică, şi de a aşeza acest
operator ı̂n finalul expresiei, urmând ca prima parte a formei poloneze să fie formată din
transformarea expresiei din stânga operatorului, iar a doua parte a formei poloneze să fie
formată din transformarea expresiei din dreapta operatorului.
Cele două subexpresii urmează a se trata ı̂n mod analog, până când se ajunge la o subex-
presie de lungime 1, care va fi obligatoriu un operand şi care nu mai necesită transformare.
Schematic, dacă avem expresia:

E=E1 op E2

unde E1 şi E2 sunt subexpresii, iar op este operatorul cu prioritatea cea mai mică (deci
operatorul unde expresia se poate ”rupe” ı̂n două), atunci forma poloneză a lui E, notată
Pol(E), se obţine astfel:

Pol(E)=Pol(E1) Pol(E2) op .

Expresia de mai sus exprimă faptul că forma poloneză postfixată a lui E se obţine prin
scrierea ı̂n formă poloneză postfixată a celor două subexpresii, urmate de operatorul care
le separă. Expresia de mai sus este o expresie recursivă specifică tehnicii Divide et Impera.
Etapele sunt ı̂n această situaţie (ilustrate ı̂n metoda din Figura9.6):

1. Divide: ı̂mparte expresia aritmetică ı̂n două subexpresii legate printr-un operator de
prioritate minimă.

2. Stăpâneşte: Transformă recursiv ı̂n formă poloneză cele două subexpresii.

3. Combină: Scrie cele două subexpresii ı̂n formă poloneză urmate de operatorul care
le leagă.

Funcţia de trecere ı̂n formă poloneză (Figura9.6) primeşte ca parametru indicele infe-
rior şi superior reprezentând limitele ı̂ntre care se ı̂ncadrează subexpresia ı̂n expresia fără
paranteze. Şirul original este conţinut ı̂n string-ul a. Funcţia eliminare paranteze trans-
formă şirul original ı̂ntr-un şir fără paranteze pe baza priorităţii operatorilor. Funcţia
polonez returnează un şir de caractere care conţine expresia ı̂n formă poloneză:

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ă expresia dată ı̂ntr-o expresie fără paranteze

În şirul p declarat ı̂n linia 3 vom trece valori ce corespund priorităţilor membrilor expresiei.
Calculul priorităţilor se face astfel:

• prioritatea iniţială a operatorilor ”+” şi ”-” este 1;

• prioritatea iniţială a operatorilor ”*” şi ”/” este 10;

• la prioritatea unui operator se adună 10 pentru fiecare pereche de paranteze ı̂ntre


care se găseşte;

• prioritatea unui operand este 1000;


9.7. PROBLEME PROPUSE 167

• prioritatea parantezelor este 0.

În liniile 29-38 se crează noua expresie fără paranteze ı̂n variabila de tip referinţă eps prin
testarea valorilor lui p. În paralel se va crea şi referinţa epf ce va conţine 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ă expresia fără paranteze ı̂ntr-o formă poloneză

Linia 12 a funcţiei testează dacă nu s-a ajuns la o expresie care are un singur caracter.
În caz afirmativ, valoarea funcţiei este chiar caracterul (operandul) respectiv. Altfel, aşa
cum spuneam la descrierea problemei, vom ı̂mpărţi expresia ı̂n două subexpresii legate prin
operatorul de prioritate minimă. Acesta va fi găsit ı̂n liniile 5-12 prin testarea valorilor lui
epf.

9.7 Probleme propuse


1. Reprezentaţi evoluţia stivei pentru procedurile şi funcţiile recursive din acest capitol.

2. Calculaţi de câte ori se recalculează valoarea Fk ı̂n cazul calcului recursiv al valorii
Fn a şirului lui Fibonacci.

3. Să se calculeze recursiv şi iterativ cel mai mare divizor comun a două numere după
formulele (Euclid):
(
cmmdc(b, a mod b) pentru a mod b 6= 0
cmmdc(a, b) =
b altfel
168 CAPITOLUL 9. DIVIDE ET IMPERA

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

4. Să se calculeze recursiv şi iterativ funcţia lui Ackermann, dată de formula:

 n+1
 pentru m = 0
Ack(m, n) = Ack(m − 1, 1) pentru n = 0

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

5. Să se calculeze combinările după formula de recurenţă din triunghiul lui Pascal:

k−1
Cnk = Cn−1
k + Cn−1 .

Calculaţi apoi combinările după formula clasică:

n!
Cnk = k!(n−k)! .

Ce constaţi? Cum explicaţi ceea ce aţi constat? (dacă aţi constat...)

6. Să se calculeze recursiv şi iterativ funcţia Manna-Pnueli, dată de formula:


(
x−1 pentru x ≥ 12
F (x) =
F (F (x + 2)) altfel

7. Să se scrie o funcţie care calculează recursiv suma cifrelor unui număr după formula:
(
n mod 10 + S(n div 10) pentru n > 0
S(n) =
0 pentru n = 0

8. Se consideră două şiruri definite recurent după formulele:

an−1 +bn−1 p
an = 2 şi bn = an−1 bn−1 , cu a0 = a şi b0 = b.

Să se scrie un program recursiv care calculează aceste şiruri.

9. (Partiţiile unui număr) Un număr natural n se poate descompune ca sumă de-


screscătoare de numere naturale. De exemplu, pentru numărul 4 avem descom-
punerile 2+1+1 sau 3+1. Prin P (n, k) se notează numărul de posibilităţi de a-l
descompune pe n ca sumă (descrescătoare) de k numere. De exemplu, P (4, 2) =
2 (4 = 3 + 1, 4 = 2 + 2). Numerele P (n, k) verifică relaţia de recurenţă:

P (n + k, k) = P (n, 1) + P (n, 2) + . . . + P (n, k) cu P (n, 1) = P (n, n) = 1.

Să se calculeze numărul total de descompuneri ale numărului n.


9.7. PROBLEME PROPUSE 169

10. Să se scrie o funcţie care calculează maximul elementelor unui vector utilizând
tehnica Divide et Impera.

Indicaţie: Se ı̂mparte vectorul ı̂n două jumătăţi egale, se calculează recursiv maximul
celor două jumătăţi şi se alege numărul mai mare.

11. (Turnurile din Hanoi) Se dau trei tije simbolizate prin literele A, B şi C. Pe tija
A se află n discuri de diametre diferite aşezate descrescător ı̂n ordinea diametrelor,
cu diametrul maxim la bază. Se cere să se mute discurile pe tija B respectând
următoarele reguli:

(a) la fiecare pas se mută un singur disc


(b) nu este permisă aşezarea unui disc cu diametru mai mare peste un disc cu
diametrul mai mic

Indicaţie: Formularea recursivă a soluţiei este: se mută primele n-1 discuri de pe


tija A pe tija C folosind ca tijă intermediară tija B; se mută discul rămas pe A pe
tija B; se mută discurile de pe tija C pe tija B folosind ca tijă intermediară tija A.
Parcurgerea celor trei etape permite definirea recursivă a şirului H(n,a,b,c) astfel:
(
ab dacă n = 1
H(n, a, b, c) =
H(n − 1, a, c, b), ab, H(n − 1, c, b, a) dacă 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. Scrieţi un program ı̂n care calculatorul să ghicească cı̂t se poate de repede un număr
natural la care v-aţi gândit. Numărul este cuprins ı̂ntre 1 şi 32.000. Atunci când
calculatorul propune un număr i se va răspunde prin 1, dacă numărul este prea mare,
2 dacă numărul este prea mic şi 0 dacă numărul a fost ghicit.

Indicaţie: Problema foloseşte metoda căutarii binare prezentată ı̂n acest capitol.

13. (Problema tăieturilor) Se dă o bucată dreptunghiulară de tablă de dimensiune l × h,


având pe suprafaţa ei n găuri de coordonate numere ı̂ntregi (colţul din stânga jos al
tablei este considerat centrul sistemului de coordonate). Să se determine care este
bucata de arie maximă fără găuri care poate fi decupată din suprafaţa originală.
Sunt permise doar tăieturi orizontale sau verticale.

Indicaţie: Se caută ı̂n bucata curentă prima gaură. Dacă o astfel de gaură există,
atunci problema se ı̂mparte ı̂n alte patru subprobleme de acelaşi tip. Dacă suprafaţa
nu are nici o gaură, atunci se compară suprafaţa ei cu suprafeţele fără gaură obţinute
până la acel moment. Dacă suprafaţa este mai mare, atunci se reţin coordonatele
ei.
Coordonatele găurilor sunt date ı̂n doi vectori xv şi yv. Coordonatele dreptunghiurilor
170 CAPITOLUL 9. DIVIDE ET IMPERA

ce apar pe parcursul problemei sunt reţinute prin colţul stânga jos (x,y), lungime şi
lăţime (l,h).
Pentru a se afla ı̂n interioul unui dreptunghi o gaură trebuie să indeplinească simul-
tan condiţiile:

(a) xv(i) > x;


(b) xv(i) < x + l;
(c) yv(i) > y;
(d) yv(i) < y + h.

Tăietura verticală prin această gaură determină două dreptunghiuri:

(a) x,y,xv(i)-x,h;
(b) xv(i),y,l+x-xv(i),h.

Tăietura orizontală prin această gaură determină alte două dreptunghiuri:

(a) x,y,l,yv(i)-y;
(b) x,yv(i),l,h+y-yv(i).

Clasa Taieturi este prezentată ı̂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 171

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;
}
}
}
}
Capitolul 10

Algoritmi Greedy

Algoritmii aplicaţi problemelor de optimizare sunt, ı̂n general, compuşi dintr-o secvenţă
de paşi, la fiecare pas existând mai multe alegeri posibile. Un algoritm Greedy va alege
la fiecare moment de timp soluţia care pare a fi cea mai bună. Deci este vorba despre o
alegere optimă, făcută local, cu speranţa că ea va conduce la un optim global. Acest capi-
tol tratează probleme de optimizare care pot fi rezolvate cu ajutorul algoritmilor Greedy.
Algoritmii Greedy conduc ı̂n multe cazuri la soluţii optime, dar nu ı̂ntotdeauna... În
secţiunea 10.1 vom prezenta mai ı̂ntâi o problemă simplă dar netrivială, problema se-
lectării activităţilor, a cărei soluţie poate fi calculată ı̂n mod eficient cu ajutorul unei
metode de tip Greedy. În secţiunea 10.2 se recapitulează câteva elemente de bază ale
metodei Greedy. Urmează apoi prezentarea câtorva probleme specifice.
Metoda Greedy este destul de puternică şi se aplică cu succes unui spectru larg de prob-
leme. Cursurile de teoria grafurilor conţin mai mulţi algoritmi care pot fi priviţi ca aplicaţii
ale metodei Greedy, cum ar fi algoritmii de determinare a arborelui parţial de cost minim
(Kruskal, Prim) sau algoritmul lui Dijkstra pentru determinarea celor mai scurte drumuri
pornind dintr-un vârf.

10.1 Problema spectacolelor (selectarea activităţilor)


Primul exemplu pe care ı̂l vom considera este o problemă de repartizare a unei resurse (o
sală de spectacol) mai multor activităţi care concurează pentru a obţine resursa respectivă
(diferite spectacole care vor să ruleze ı̂n sala respectivă). Vom vedea că un algoritm de tip
Greedy reprezintă o metodă simplă şi elegantă pentru programarea unui număr maxim de
spectacole care nu se suprapun (numite activităţi compatibile reciproc).

Să presupunem că dispunem de o mulţime S = 1,2,...,n de n activităţi (spectacole) care


doresc să folosească o aceeaşi resursă (sala de spectacole). Această resursă poate fi folosită
de o singură activitate la un anumit moment de timp. Fiecare activitate i are un timp de
start si şi un timp de terminare ti , unde si ≤ ti . Dacă este selectată activitatea i, ea se
desfăşoară pe durata intervalului [si , ti ) . Două activităţi sunt compatibile dacă duratele
lor de desfăşurare sunt disjuncte. Problema spectacolelor (selectării activităţilor) constă
din selectarea unei mulţimi maximale de activităţi compatibile ı̂ntre ele.

172
10.1. PROBLEMA SPECTACOLELOR (SELECTAREA ACTIVITĂŢILOR) 173

Un algoritm Greedy pentru această problemă este descris de următoarea funcţie, prezen-
tată ı̂n pseudocod. Vom presupune că spectacolele (adică datele de intrare) sunt ordonate
crescător după timpul de terminare:

t1 ≤ t2 ≤, . . . , ≤ tn .

În cazul ı̂n care activităţile nu sunt ordonate astfel, ordonarea poate fi făcută ı̂n timpul
O(nlgn) (folosind Mergesort sau Quicksort). Algoritmul de mai jos presupune că datele
de intrare s şi t sunt reprezentate ca vectori.

funcţie SELECT-SPECTACOLE-GREEDY(s, t)
A ← {1} //A este mulţimea spectacolelor care sunt selectate
j ← 1 //j este indicele ultimului spectacol selectat
pentru i = 2, n
dacă si ≥ tj atunci //spectacolul i ı̂ncepe după ce j s-a terminat
A ← A ∪ {i} //se adaugă i la spect. selectate
j ← i //ultimul spectacol este acum i
return A

În mulţimea A se introduc activităţile selectate. Variabila j identifică ultima activitate


introdusă ı̂n A. Deoarece activităţile sunt considerate ı̂n ordinea crescătoare a timpilor lor
de terminare, tj va reprezenta ı̂ntotdeauna timpul maxim de terminare a oricărei activităţi
din A. Aceasta ı̂nseamnă că:

tj = max{tk |k ∈ A}

În liniile 2-3 din algoritm se selectează activitatea 1 (activitatea 1 trebuie planificată prima,
deoarece se termină cel mai repede), se iniţializează A astfel ı̂ncât să nu conţină decât
această activitate, iar variabila j ia ca valoare această activitate. În continuare ı̂n ciclul
pentru se consideră pe rând fiecare activitate i se adaugă mulţimii A dacă este compatibilă
cu celelalte activităţi deja selectate. Pentru a vedea dacă activitatea i este compatibilă
cu toate celelalte activităţi existente la momentul curent ı̂n A, este suficient ca momentul
de start si să nu fie mai devreme decât momentul de terminare tj al activităţii cel mai
recent adăugate mulţimii A. Dacă activitatea i este compatibilă, atunci ea este adăugată
mulţimii A, iar variabila j este actualizată. Procedura SELECT-SPECTACOLE-GREEDY
este foarte eficientă. Ea poate planifica o mulţime S de n activităţi ı̂n O(n), presupunând
că activităţile au fost deja ordonate după timpul lor de terminare. Activitatea aleasă
de procedura SELECT-SPECTACOLE-GREEDY este ı̂ntotdeauna cea cu primul timp
de terminare care poate fi planificată legal. Activitatea astfel selectată este o alegere
”Greedy” (lacomă) ı̂n sensul că, intuitiv, ea lasă posibilitatea celorlalte activităţi rămase
pentru a fi planificate. Cu alte cuvinte, alegerea Greedy maximizează cantitatea de timp
neplanificată rămasă.

10.1.1 Demonstrarea corectitudinii algoritmului


Până la acest moment, noi nu ne-am pus problema de a demonstra corectitudinea algorit-
milor prezentaţi. Totuşi, trebuie să menţionăm că există o ı̂ntreagă ramură a algoritmicii
174 CAPITOLUL 10. ALGORITMI GREEDY

care se ocupă exclusiv de demonstrarea corectitudinii algoritmilor. În general, demon-


strarea corectitudinii unui algoritm nu este deloc simplă, de aceea am evitat să prezentăm
demonstraţiile ı̂n acest curs introductiv. Am ales să demonstrăm corectitudinea acestui
algoritm, deoarece pe de o parte ea este ilustrativă pentru o ı̂ntreagă clasă de probleme,
iar pe de altă parte, demonstraţia nu este dificilă.

Teorema 10.1 Algoritmul SELECT-SPECTACOLE-GREEDY furnizează soluţia optimă


(număr maxim de spectacole) pentru problema selectării activităţilor.

Demonstraţie: Fie S = {1, 2, . . . , n} mulţimea activităţilor care trebuie planificate, ordo-


nate crescător după timpul de terminare. În consecinţă, activitatea 1 se termină cel mai
devreme. Vom arăta că există o soluţie optimă care ı̂ncepe cu activitatea 1.
Să presupunem că avem o soluţie A ⊆ S optimă pentru o instanţă a problemei. Pentru
simplitate, presupunem că activităţile din A sunt ordonate după timpul de terminare.
Dacă primul spectacol din A este chiar 1, atunci demonstraţia este ı̂ncheiată. Dacă primul
spectacol din A nu este 1, atunci ı̂nlocuim primul spectacol cu spectacolul 1, obţinând
evident o soluţie corectă, deoarece spectacolul 1 se va termina mai devreme decât primul
spectacol din A. Am arătat astfel că există o soluţie optimă pentru S care ı̂ncepe cu ac-
tivitatea 1.
Mai mult, odată ce este făcută alegerea activităţii 1, problema se reduce la determinarea
soluţiei optime pentru activităţile din S care sunt compatibile cu activitatea 1. Fie
S ′ = {i ∈ S|si ≥ t1 } mulţimea activităţilor care ı̂ncep după ce 1 se termină. Rezultă
că dacă A este o soluţie optimă pentru S, atunci A′ = A − {1} este o soluţie optimă pentru
S’. Dacă nu ar fi aşa, atunci ar exista o soluţie optimă B’ pentru S’ care să aibă mai multe
activităţi decât A’. Adăugând activitatea 1 la B’, vom obţine o soluţie pentru S cu mai
multe activităţi decât soluţia A, ceea ce este absurd.
Astfel, prin inducţie după numărul de alegeri făcute se poate arăta că alegând primul
spectacol compatibil la fiecare pas, se obţine o soluţie optimă.

10.2 Elemente ale strategiei Greedy


Un algoritm Greedy determină o soluţie optimă a unei probleme ı̂n urma unei succesiuni
de alegeri. La fiecare moment de decizie din algoritm este aleasă opţiunea care pare a
fi cea mai potrivită. Această strategie euristică nu produce ı̂ntotdeauna soluţia optimă,
dar există şi cazuri când aceasta este obţinută, cum ar fi ı̂n cazul problemei selectării ac-
tivităţilor. În acest paragraf vom prezenta câteva proprietăţi generale ale metodei Greedy.
Cum se poate decide dacă un algoritm Greedy poate rezolva o problemă particulară de
optimizare? În general nu există o modalitate de a stabili acest lucru, dar există anumite
caracteristici pe care le au majoritatea problemelor care se rezolvă prin tehnici Greedy:
proprietatea de alegere Greedy şi substructura optimă.
În cazul general o problemă de tip Greedy, are următoarele componente:

• o mulţime de candidaţi (lucrări de planificat, vârfuri ale grafului, etc);

• o funcţie care verifică dacă o anumită mulţime de candidaţi constituie o soluţie


posibilă (nu neapărat optimă) a problemei;
10.2. ELEMENTE ALE STRATEGIEI GREEDY 175

• o funcţie care verifică dacă o mulţime de candidaţi este fezabilă, adică dacă este
posibil să completăm această mulţime astfel ı̂ncât să obţinem o soluţie posibilă (nu
neapărat optimă) a problemei (verifică dacă planificarea este formată din activităţi
care nu se suprapun etc.);

• o funcţie de selecţie care indică la orice moment care este cel mai promiţător dintre
candidaţii ı̂ncă nefolosiţi (se alege spectacolul compatibil care se termină cel mai
repede);

• o funcţie obiectiv care dă valoarea unei soluţii (numărul de lucrări planificate, tim-
pul necesar executării tuturor lucrărilor ı̂ntr-o anumită ordine, lungimea drumului
pe care l-am găsit, etc.); aceasta este funcţia pe care urmărim să o optimizăm (min-
imizăm/maximizăm).

Pentru a rezolva o problemă de optimizare cu Greedy, căutăm o soluţie posibilă care să
optimizeze valoarea funcţiei obiectiv. Un algoritm Greedy construieşte soluţia pas cu pas
. Iniţial, mulţimea candidaţilor selectaţi este vidă. La fiecare pas, ı̂ncercăm să adăugăm
acestei mulţimi cel mai promiţător candidat, conform funcţiei de selecţie. Dacă, după
o astfel de adăugare, mulţimea de candidaţi selectaţi nu mai este fezabilă, eliminăm ul-
timul candidat adăugat; acesta nu va mai fi niciodată considerat. Dacă, după adăugare,
mulţimea de candidaţi selectaţi este fezabilă, ultimul candidat adăugat va rămâne de acum
ı̂ncolo ı̂n ea. De fiecare dată când lărgim mulţimea candidaţilor selectaţi, verificăm dacă
această mulţime nu constituie o soluţie posibilă a problemei noastre. Dacă algoritmul
Greedy funcţionează corect, prima soluţie găsită va fi totodată o soluţie optimă a proble-
mei. (Soluţia optimă nu este ı̂n mod necesar unică: se poate ca funcţia obiectiv să aibă
aceeaşi valoare optimă pentru mai multe soluţii posibile.) Descrierea ı̂n pseudocod a unui
algoritm Greedy general este:

funcţie greedy( C ) // C este mulţimea candidaţilor


S ← φ // S este mulţimea ı̂n care construim soluţia
cât timp not soluţie(S) şi C 6= φ
x← un element din C care maximizează select(x)
C ← C − {x}
dacă fezabil(S ∪ {x}) atunci S ← S ∪ {x}
dacă soluţie(S) atunci
return S
altfel
return ”nu există soluţie”

Este de ı̂nţeles acum de ce un astfel de algoritm se numeşte ”lacom” (am putea să-l numim
şi ”nechibzuit”). La fiecare pas, procedura alege cel mai bun candidat la momentul re-
spectiv, fără să-i pese de viitor şi fără să se răzgândească. Dacă un candidat este inclus ı̂n
soluţie, el rămâne acolo; dacă un candidat este exclus din soluţie, el nu va mai fi niciodată
reconsiderat. Asemenea unui ı̂ntreprinzător care urmăreşte câştigul imediat ı̂n dauna celui
de perspectivă, un algoritm Greedy acţionează simplist. Totuşi, ca şi ı̂n afaceri, o astfel
de metodă poate da rezultate foarte bune tocmai datorită simplităţii ei.
Funcţia de selectare este de obicei derivată din funcţia obiectiv; uneori aceste două funcţii
176 CAPITOLUL 10. ALGORITMI GREEDY

sunt chiar identice.


Un exemplu simplu de algoritm Greedy este algoritmul folosit pentru rezolvarea următoarei
probleme.

Să presupunem că dorim să dăm restul unui client, folosind un număr cât mai mic de
monezi.

În acest caz, elementele problemei sunt:

• Candidaţii: mulţimea iniţială de monezi de 1, 5 şi 25 unităţi, ı̂n care presupunem că
din fiecare tip de monedă avem o cantitate nelimitată;

• O soluţie posibilă: valoarea totală a unei astfel de mulţimi de monezi selectate trebuie
să fie exact valoarea pe care trebuie să o dăm ca rest;

• O mulţime fezabilă: valoarea totală a unei astfel de mulţimi de monezi selectate nu


este mai mare decât valoarea pe care trebuie să o dăm ca rest;

• Funcţia de selecţie: se alege cea mai mare monedă din mulţimea de candidaţi rămasă;

• Funcţia obiectiv: numărul de monezi folosite ı̂n soluţie; se doreşte minimizarea aces-
tui număr.

Se poate demonstra că algoritmul Greedy va găsi ı̂n acest caz mereu soluţia optimă - restul
cu un număr minim de monezi. Pe de altă parte, presupunând că există şi monezi de 12
unităţi sau că unele din tipurile de monezi lipsesc din mulţimea iniţială de candidaţi, se
pot găsi contraexemple pentru care algoritmul nu găseşte soluţia optimă, sau nu găseşte
nici o soluţie cu toate că există soluţie.
Evident, soluţia optimă se poate găsi ı̂ncercând toate combinările posibile de monezi (abor-
dare backtracking). Acest mod de lucru necesită ı̂nsă foarte mult timp de calcul.
Un algoritm Greedy nu duce deci ı̂ntotdeauna la soluţia optimă sau la o soluţie. Este doar
un principiu general, urmând ca pentru fiecare caz ı̂n parte să determinăm dacă obţinem
sau nu soluţia optimă.

10.2.1 Proprietatea de alegere Greedy


Prima caracteristică a unei probleme de tip Greedy este aceea de a avea proprietatea
alegerii Greedy, adică se poate ajunge la o soluţie optimă global, realizând alegeri (Greedy)
optime local . Într-un algoritm Greedy se realizează orice alegere care pare a fi cea mai
bună la momentul respectiv, iar subproblema rezultată este rezolvată după ce alegerea
este făcută. Alegerea realizată de un algoritm Greedy poate depinde de alegerile făcute
până ı̂n momentul respectiv, dar nu poate depinde de alegerile ulterioare sau de soluţiile
subproblemelor.
Desigur, trebuie să demonstrăm că o alegere Greedy la fiecare pas conduce la o soluţie
optimă global, şi aceasta este o problemă mai delicată. De obicei, demonstraţia examinează
o soluţie optimă global. Apoi se arată că soluţia poate fi modificată astfel ı̂ncât la fiecare
pas este realizată o alegere Greedy, iar această alegere reduce problema la una similară dar
de dimensiuni mai reduse. Se aplică apoi principiul inducţiei matematice pentru a arăta
10.3. MINIMIZAREA TIMPULUI MEDIU DE AŞTEPTARE 177

că o alegere Greedy poate fi utilizată la fiecare pas. Faptul că o alegere Greedy conduce
la o problemă de dimensiuni mai mici reduce demonstraţia corectitudinii la demonstrarea
faptului că o soluţie optimă trebuie să evidenţieze o substructură optimă.

10.2.2 Substructură optimă


O problemă evidenţiază o substructură optimă dacă o soluţie optimă a problemei conţine
soluţii optime ale subproblemelor. Această proprietate este cheia pentru aplicarea pro-
gramării dinamice sau a unui algoritm Greedy. Ca exemplu al unei structuri optime, să
ne reamintim demonstraţia corectitudinii algoritmului pentru problema selectării specta-
colelor, unde se arată că dacă o soluţie optimă A a problemei selectării activităţilor ı̂ncepe
cu activitatea 1, atunci mulţimea activităţilor A′ = A{1} este o soluţie optimă pentru
problema selectării activităţilor S ′ = {i ∈ S|si ≥ t1 }.

10.3 Minimizarea timpului mediu de aşteptare


O singură staţie de servire (procesor, pompă de benzină, etc) trebuie să satisfacă cererile
a n clienţi. Timpul de servire necesar fiecărui client este cunoscut ı̂n prealabil: pentru
clientul i este necesar un timp ti , i = 1, n. Dorim să minimizăm timpul total de aşteptare:
Pn
T = i=1 (timpul de aşteptare pentru clientul i)

Ceea ce este acelaşi lucru cu a minimiza timpul mediu de aşteptare, care este Tn .
De exemplu, dacă avem trei clienţi cu t1 = 5, t2 = 10, t3 = 3, sunt posibile şase ordini de
servire:
Ordine T impul(T )
1 2 3 5 + (5 + 10) + (5 + 10 + 3)
1 3 2 5 + (5 + 3) + (5 + 3 + 10)
2 1 3 10 + (10 + 5) + (10 + 5 + 3)
2 3 1 10 + (10 + 3) + (10 + 3 + 5)
3 1 2 3 + (3 + 5) + (3 + 5 + 10) optim
3 2 1 3 + (3 + 10) + (3 + 10 + 5)

În primul caz, clientul 1 este servit primul, clientul 2 aşteaptă până este servit clientul
1 şi apoi este servit, clientul 3 aşteaptă până sunt serviţi clienţii 1, 2 şi apoi este servit.
Timpul total de aşteptare a celor trei clienţi este 38.
Algoritmul Greedy este foarte simplu - la fiecare pas se selectează clientul cu timpul minim
de servire din mulţimea de clienţi rămasă. Vom demonstra că acest algoritm este optim.
Fie I = (i1 i2 . . . in ) o permutare oarecare a ı̂ntregilor {1, 2, . . . , n}. Dacă servirea are loc
ı̂n ordinea I, avem:
Pn
T (I) = ti1 + (ti1 + ti2 ) + (ti1 + ti2 + ti3 ) + . . . = nti1 + (n − 1)ti2 + . . . = k=1 (n − k + 1)tik

Presupunem acum că I este astfel ı̂ncât putem găsi doi ı̂ntregi a < b cu

tia > tib


178 CAPITOLUL 10. ALGORITMI GREEDY

deci există un client care necesită un timp mai lung de deservire, şi care este servit ı̂nainte.
Interschimbăm pe ia cu ib ı̂n I; cu alte cuvinte, clientul care a fost servit al b-lea va fi servit
acum al a-lea şi invers. Obţinem o nouă ordine de servire I’, care este de preferat deoarece
Pn
T (I ′ ) = (n − a + 1)tib + (n − b + 1)tia + k=1,k6=a,b (n − k + 1)tik

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

Aplicând succesiv pasul de mai sus se obţine o permutare optimă J = (j1 , j2 , . . . , jn )


pentru care avem:

tj1 ≤ tj2 ≤ . . . ≤ tjn .

Prin metoda Greedy, selectând permanent clientul cu timpul cel mai mic de deservire,
obţinem deci ı̂ntotdeauna planificarea optimă a clienţilor.
Problema poate fi generalizată pentru un sistem cu mai multe staţii de servire.
Scrierea algoritmului se reduce la o banală ordonare a clienţilor crescător după timpul de
deservire şi o lăsăm ca exerciţiu.

10.4 Interclasarea optimă a mai multor şiruri ordonate


Să presupunem că avem două şiruri S1 şi S2 , de lungime m şi n, ordonate crescător
şi că dorim să obţinem prin interclasarea lor şirul ordonat crescător care conţine exact
elementele din cele două şiruri. Dacă interclasarea are loc prin deplasarea elementelor din
cele două şiruri ı̂n noul şir rezultat, atunci numărul deplasărilor este m + n.
Generalizând, să considerăm acum n şiruri S1 , S2 , . . . Sn , fiecare şir Si , i = 1, n, fiind format
din qi elemente ordonate crescător (vom numi qi lungimea lui Si ). Ne propunem să obţinem
şirul S ordonat crescător, conţinând exact elementele din cele n şiruri. Vom realiza acest
lucru prin interclasări succesive de câte două şiruri. Problema constă ı̂n determinarea
ordinii optime ı̂n care trebuie efectuate aceste interclasări, astfel ı̂ncât timpul total să fie
cât mai mic. Exemplul de mai jos ne arată că problema astfel formulată nu este banală,
adică nu este deloc indiferent ı̂n ce ordine se fac interclasările.

Exemplul 10.1 Fie şirurile S1 , S2 , S3 de lungimi q1 = 30, q2 = 20, q3 = 10. Dacă inter-
clasăm pe S1 cu S2 , iar rezultatul ı̂l interclasăm cu S3 , numărul total al deplasărilor este
(30+20)+(50+10)=110. Dacă interclasăm pe S3 cu S2 , iar rezultatul ı̂l interclasăm cu
S1 , numărul total al deplasărilor este (10+20)+(30+30)=90, deci cu 20 de operaţii mai
puţin.

Ataşăm fiecărei strategii de interclasare câte un arbore binar ı̂n care valoarea fiecărui vârf
este dată de lungimea şirului pe care ı̂l reprezintă. Dacă şirurile S1 , S2 , . . . , S6 au lungimile
q1 = 30, q2 = 10, q3 = 20, q4 = 30, q5 = 50, q6 = 10 două astfel de strategii de interclasare
sunt reprezentate prin arborii din Figura 10.1.
10.4. INTERCLASAREA OPTIMĂ A MAI MULTOR ŞIRURI ORDONATE 179

150 150
❅ ❅
❅ ❅
140 10 ❅
❅ 90 60
❅ ❅ ❅
130 10 ❅ ❅
❅ 40 50 30 30
❅ ❅
110 20 ❅
❅ 20 20
❅ ❅
80 30 ❅
❅ 10 10

30 50

Figura 10.1 Reprezentarea strategiilor de interclasare


Observăm că fiecare arbore are 6 vârfuri terminale, corespunzând celor 6 şiruri iniţiale şi
5 vârfuri neterminale, corespunzând celor 5 interclasări care definesc strategia respectivă.
Numerotăm vârfurile ı̂n felul următor: Vârful terminal i, i = 1, 6, va corespunde şirului Si ,
iar vârfurile neterminale se numerotează de la 7 la 11 ı̂n ordinea obţinerii interclasărilor
respective (Figura 10.2).
Strategia Greedy apare ı̂n Figura 10.1b şi constă ı̂n a interclasa mereu cele mai scurte
două şiruri disponibile la momentul respectiv.

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

1 5

Figura 10.2 Numerotarea varfurilor arborilor din Figura 10.1


Pentru a interclasa şirurile S1 , S2 , . . . , Sn , de lungimi q1 , q2 , . . . , qn , obţinem pentru fiecare
strategie câte un arbore binar cu n vârfuri terminale numerotate de la 1 la n şi n − 1
vârfuri neterminale numerotate de la n + 1 la 2n − 1. Definim pentru un arbore oarecare
A de acest tip lungimea externă ponderată:
180 CAPITOLUL 10. ALGORITMI GREEDY

Pn
L(A) = i=1 ai qi

unde ai este adâncimea vârfului i. Se observă că numărul total de deplasări de elemente
pentru strategia corespunzătoare lui A este chiar L(A). Soluţia optimă a problemei noastre
este atunci arborele (strategia) pentru care lungimea externă ponderată este minimă.

Teorema 10.2 Prin metoda Greedy, ı̂n care se interclasează la fiecare pas cele două şiruri
de lungime minimă, se obţine şirul s cu un număr minim de operaţii.

Demonstraţie: Demonstrăm prin inducţie. Pentru n = 1, proprietatea este verificată.


Presupunem că proprietatea este adevărată pentru n − 1 şiruri. Fie A arborele strategiei
Greedy de interclasare a n şiruri de lungime q1 ≤ q2 ≤ . . . ≤ qn . Fie B un arbore cu
lungimea externă ponderată minimă, corespunzător unei strategii optime de inerclasare a
celor n şiruri. În arborele A apare subarborele:

q1 + q2





q1 q2

reprezentând prima interclasare făcută conform strategiei Greedy. În arborele B, fie un
vârf neterminal de adâncime maximă. Cei doi fii ai acestui vârf sunt atunci două vârfuri
terminale qj şi qk . Fie B’ arborele obţinut din B schimbând ı̂ntre ele vârfurile q1 şi qj ,
respectiv q2 şi qk . Evident, L(B ′ ) ≤ L(B). Deoarece B are lungimea externă ponderată
minimă, rezultă că L(B) = L(B ′ ). Eliminând din B’ vârfurile q1 şi q2 , obţinem un ar-
bore B” cu n − 1 vârfuri terminale q1 + q2 , q3 , . . . , qn . Arborele B ′ are lungimea externă
ponderată minimă şi L(B ′ ) = L(B”) + (q1 + q2 ). Rezultă că şi B” are lungimea externă
ponderată minimă. Atunci, conform ipotezei inducţiei, avem L(B”) = L(A′ ) unde A’ este
arborele strategiei Greedy de interclasare a şirurilor de lungime q1 + q2 , q3 , . . . , qn . Cum A
se obţine din A’ atasând la vârful q1 + q2 fiii q1 şi q2 , iar B’ se obţine ı̂n acelaşi mod din
B”, rezultă că L(A) = L(B ′ ) = L(B) . Proprietatea este deci adevărată pentru orice n.
Implementarea eficientă a acestui algoritm presupune utilizarea unei structuri de date nu-
mită Heap.
Se poate implementa algoritmul utilizând următorul principiu:

• se parcurge şirul de lungimi şi se aleg două şiruri de lungime minimă

• se deplasează cele două lungimi la sfârşitul şirului

• se ı̂nlocuiesc ultimele două elemente din şir, cu un element reprezentând suma lor

• se descreşte lungimea şirului cu o unitate

Scrierea efectivă a algoritmului o propunem ca exerciţiu.


10.5. PROBLEME PROPUSE 181

10.5 Probleme propuse


1. (Problema rucsacului) Avem la dispoziţie un rucsac de capacitate M şi n obiecte
diferite (câte unul din fiecare) cu costurile ci şi greutatea gi . Scrieţi un algoritm
care aşează aceste obiecte ı̂n rucsac astfel ı̂ncât costul total să fie maxim. Suma
greutăţilor obiectelor din rucsac nu poate depăşi capacitatea rucsacului. Dacă un
obiect nu ı̂ncape ı̂n rucsac, se poate lua doar o parte (fracţiune) din el.

Indicaţie: Este uşor de intuit că pentru obţinerea unui profit (cost) total maxim
trebuie cărate obiecte de greutate mică şi cost mare. Demonstraţia riguroasă a
afirmaţiei anterioare v-o propunem spre rezolvare.
Aşadar, va trebui, iniţial, să ordonăm descrescător obiectele după raportul gcii . Apoi,
vom folosi următorul algoritm:

funcţie RUCSAC(c,g,M,x,n)
pentru i=1,n execută
x(i)←0
rest←M
i←1
cât timp i ≤ n şi rest > 0 execută
dacă g(i)>rest atunci
x(i)←rest/g(i)
altfel
x(i)←1
rest←rest-x(i)w(i)
return x

2. (Problema discretă a rucsacului) Acelaşi enunţ ca la problema precedentă, cu diferenţa


că dintr-un obiect nu se poate pune o fracţiune (un obiect fie se pune ı̂ntreg, fie nu
se pune). Arătaţi că algoritmul Greedy de la problema precedentă nu furnizează
ı̂ntotdeauna soluţie optimă ı̂n acest caz. Găsiţi un algoritm care furnizează ı̂ntotdeauna
soluţie optimă! Ce complexitate are acest algoritm?

Indicaţie: Această problemă are soluţia exactă determinată doar printr-un algoritm
de tip backtracking. Exemplu ı̂n sprijinul celor afirmate anterior: fie c=(5,3,3) şi
g=(3,2,2) iar M=4. Prin aplicarea algoritmului de la problema precedentă am selecta
doar obiectul 1 (obiectul 2 nu ar ı̂ncăpea ı̂ntreg ı̂n rucsac deci nu ar putea fi selectat)
şi am obţine costul 5 ı̂n timp ce dacă am selecta obiectele 2 şi 3 am obţine costul 6.

3. Scrieţi algoritmul pentru problema interclasării optimale din paragraful 10.4.

4. Găsiţi o soluţie Greedy pentru problema comis-voiajorului propusă la capitolul 8.


Este această soluţie optimă?
182 CAPITOLUL 10. ALGORITMI GREEDY

Indicaţie: Conform strategiei greedy, vom construi ciclul pas cu pas, adăugând la
fiecare iteraţie cea mai scurtă muchie disponibilă cu următoarele proprietăţi:

• nu formează un ciclu cu muchiile deja selectate (exceptând pentru ultima muchie


aleasă, care completează ciclul)
• nu există ı̂ncă două muchii deja selectate, astfel ı̂ncât cele trei muchii să fie
incidente ı̂n acelaşi vârf.

Această soluţie nu este optimă iar un exemplu vă propunem să-l găsiţi.

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