Curs_Java
Curs_Java
JAVA
Braşov 2002
Cuprins
2
CUPRINS 3
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
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
Î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.
6
1.2. INSTALAREA MEDIULUI JAVA 7
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
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.
% 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:
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
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
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. }
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:
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.
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
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.
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.
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.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
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.
Î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:
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
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:
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
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:
{
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.
switch( expresie-selectare )
{
case valoare-intreaga1: instructiune ; break ;
case valoare-intreaga2: instructiune ; break ;
//...
case valoare-intreagan: instructiune ; break ;
default: instructiune ;
}
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
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 .
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.}
Î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:
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.
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
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
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.
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:
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.
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ă.
Î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ă.
de tip String nu se pot modifica, putem folosi liniştiţi operatorul = pentru ele. Iată un
exemplu:
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.
şi atribuirea:
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.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:
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:
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:
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
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:
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ă:
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
defineşte matricea x, ı̂n care primul indice poate fi 0 sau 1, iar al doilea este de la 0 la 2.
9. return ;
10. }
11. for( int i=0 ; i < args.length; ++i)
12. {
13. System.out.print( args[i] ) ;
14. }
15. }
16. }
1. import java.io.* ;
2.
40 CAPITOLUL 3. REFERINŢE
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.
...
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
{
...
}
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.
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.
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.
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.
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.
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.}
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
Î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.
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.
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.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.}
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:
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.
Figura 4.3 O clasă Date minimală care ilustrează constructorii şi metodele equals şi
toString
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ă.
Î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
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.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 .
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.
Î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() ;
int x = Reader.readInt() ;
Î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:
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 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
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.
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
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:
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.
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 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
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
else
return 0;
}
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 ;
}
{
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:
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
• Î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.
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.
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:
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.
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.
{
public doWork()
{
takeBreak(); //pauzele lungi si dese
super.doWork();//cheia marilor
takeBreak(); //succese
}
}
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ă.
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ă:
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
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
Figura 5.6 Rutina main pentru citirea de figuri şi afişarea lor ı̂n ordine crescătoare.
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
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
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:
Acest cod este incorect doar pentru că Integer este o clasă finală care nu poate fi extinsă.
5.5. INTERFEŢE 83
Interfaţa este cea mai abstractă clasă posibilă şi reprezintă o soluţie elegantă la problema
moştenirii multiple.
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.}
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
ı̂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.
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.}
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.
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
Î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:
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:
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
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
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:
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
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.
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)).
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.
(
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.
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
Pk n
tn = i=1 ci ri
tn = tn−1 + tn−2 , n ≥ 2
tn − tn−1 − tn−2 = 0
x2 − x − 1 = 0
√
1∓ 5
cu rădăcinile r1,2 = 2 . Soluţia generală are forma:
tn = c1 r1n + c2 r2n
c1 + c2 = 0, n = 0
c1 r1n + c2 r2n = 1, n = 1
de unde determinăm
c1.2 = ∓ √15
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 = c1 1n + c2 2n + c3 n2n
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:
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
1. Înmulţim recurenţa cu 9
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ă:
tn = 2tn−1 + 1, n ≥ 1
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
T (n) = 4T ( n2 ) + n, n > 1
tk = 4tk−1 + 2k
102 CAPITOLUL 6. ANALIZA EFICIENŢEI ALGORITMILOR
(x − 4)(x − 2) = 0
T (n) = c1 n2 c2 n
T (n) = 4T ( n2 ) + n2 , n > 1
T (n) = 4T ( n2 ) + n2 , n > 1
cu ecuaţia caracteristică
(x − 4)2 = 0
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
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
(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).
Indicaţie: Trebuie arătat că relaţia este parţială, reflexivă, tranzitivă şi antisimetrică.
Ţineţi cont de exerciţiul 3.
14. Care este timpul de execuţie pentru un algoritm recursiv cu recurenţa: tn = 2tn−1 +
n.
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.
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
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.
107
108 CAPITOLUL 7. STRUCTURI DE DATE
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.
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.}
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
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 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.}
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.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).
class ListNode
{
Object data ; //continutul listei
ListNode next ;
}
first last
❏ ✡✡
❏❏ ✡
✲ ✲ ✲ ✲
a1 a2 a3 a4
În orice moment, putem adăuga ı̂n listă un nou element x prin următoarele operaţii:
114 CAPITOLUL 7. STRUCTURI DE DATE
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
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.}
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):
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.
insert find,remove
❏ ✡
✣
❏ ✡
❏
❫ ✡
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
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.}
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
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.}
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
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(double data)
//constructorul cu parametru pentru initializare informatiei utile
{
left=null;
right=null;
this.data=data;
}
{
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;
}
class BinaryTree
{
Node rootNode=null;
Metoda Backtracking
x = (x1 , x2 , . . . , xn )
unde:
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:
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
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.
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
• (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}
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):
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
φ, . . . , φ
!
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}
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} φ
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} φ
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:
• 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.
xi 6= xj pentru ∀ i, j = 1, n, i 6= j.
xi 6= xk pentru ∀ i = 1, k − 1 .
{
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.
if(k==n)
va fi ı̂nlocuită cu:
if(k==m)
Desigur, aceeaşi modificare este necesară şi ı̂n metoda retSol, ı̂n care secvenţa
se va ı̂nlocui cu
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}.
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:
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
×
×
×
×
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
• 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.
{
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.
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
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.
(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)!.
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
6. Se dă o mulţime A = {1, 2, . . . , n}. Să se afişeze toate submulţimile acestei mulţimi.
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.
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.
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.
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ă.
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.
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 Plati(int n)
{
super(n) ;//lansarea constructorului din clasa backtracking1;
}
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 .
• ı̂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
15. Fiind dat un număr natural n, să se genereze toate descompunerile sale ca sumă de
numere prime.
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.
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.
}
}
import java.io.* ;
{
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;
}
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.
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):
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:
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:
Să presupunem că procedura AfisMat este apelată ı̂ntr-un program astfel:
...
AfisMat(a,5)
...
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:
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:
ı̂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.
ş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:
ı̂n consecinţă procedura de inversare va avea forma (parametrul n a fost eliminat, el fiind
dat ı̂n formulă doar pentru claritate):
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
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.
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:
Î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ă.
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.
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 ;
}
poz = binarySearch(a,x,0,a.length-1)
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
mergeSort(a,0,a.length-1)
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
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
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.
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:
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.
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ă.
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ă:
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:
Î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.
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.
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 .
n!
Cnk = k!(n−k)! .
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
an−1 +bn−1 p
an = 2 şi bn = an−1 bn−1 , cu a0 = a şi b0 = b.
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:
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.
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) x,y,xv(i)-x,h;
(b) xv(i),y,l+x-xv(i),h.
(a) x,y,l,yv(i)-y;
(b) x,yv(i),l,h+y-yv(i).
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.
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
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ă.
• 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:
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
Să presupunem că dorim să dăm restul unui client, folosind un număr cât mai mic de
monezi.
• 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;
• 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ă.
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ă.
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
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
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.
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
11 11
❅ ❅
❅ ❅
10 6 ❅
❅ 10 9
❅ ❅ ❅
9 2 ❅ ❅
❅ 8 5 1 4
❅ ❅
8 3 ❅
❅ 7 3
❅ ❅
7 4 ❅
❅ 2 6
❅
1 5
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.
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 ı̂nlocuiesc ultimele două elemente din şir, cu un element reprezentând suma lor
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
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.
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:
Această soluţie nu este optimă iar un exemplu vă propunem să-l găsiţi.