Documente Academic
Documente Profesional
Documente Cultură
SUPORT DE CURS
ANUL II
Semestrul 2
Cluj–Napoca
2010
2
CUPRINS
I. INFORMAŢII GENERALE
I.1. Date de identificare ale cursului 5
I.1.1. Date de identificare despre titularul de curs 5
I.1.2. Date de identificare curs şi contact tutori 5
I.2. Condiţionări şi cunoştinţe prerechizite 5
I.3. Descrierea cursului 5
I.4. Organizarea temelor din cadrul cursului 5
I.5. Materiale bibliografice obligatorii 6
I.6. Materiale şi instrumente necesare pentru curs 6
I.7. Calendar al cursului 6
I.8. Politica de evaluare şi notare 7
I.9. Elemente de deontologie academică 7
I.10. Studenţi cu dizabilităţi 7
I.11. Strategii de studiu recomandate 7
3
4.4. Tabela de dispersie 71
4.5. Lista liniară simplu înlănţuită 72
4.6. Lista liniară circulară simplu înlănţuită 74
4.7. Lista liniară dublu înlănţuită 74
4.8. Structura de tip stivă (LIFO Last In First Out) 75
4.9. Structura de tip coadă 76
4.10. Grafuri neorientate 77
4.11. Grafuri orientate 81
4.12. Arbori 85
4
INFORMAŢII GENERALE
5
problematizarea informaţiilor prezentate, implicarea în rezolvarea problemelor propuse;
găsirea de soluţii alternative la problemele propuse.
Studentul are libertatea de a-şi gestiona singur, fără constrângeri, modalitatea şi
timpul de parcurgere a cursului. Este însă recomandată parcurgerea succesivă a
modulelor prezentate în cadrul suportului de curs, în ordinea indicată şi rezolvarea
sarcinilor sugerate la finalul fiecărui modul.
Bibliografia suplimentară
[Aho83] Aho A, Hopcroft J, Ullman J– Data Structures and Algorithms, Addison
Wesley, 1983
[Cormen00] Cormen, Th.; Leiserson Ch; Rivest, R. -Introducere în Algoritmi, Ed Agora
2000 (traducere)
[Knuth99] Knuth - Arta programării calculatoarelor, vol, 1, 2, 3, ed. Teora, 1999
(traducere)
6
Calendarul activităţilor este unul orientativ, fiind susceptibil unor modificări
ulterioare, acestea urmând să fie comunicate studenţilor.
7
II. SUPORTUL DE CURS PROPRIU-ZIS
MODULUL NUMĂRUL 1
Algoritmii sunt cel mai statornic domeniu din lumea informaticii; mulţi
algoritmi au rămas neschimbaţi în ultimii 50-60 de ani (bazele unora datând de câteva
mii de ani), în timp ce limbajele de programare, tehnica de calcul, etc., au avut salturi
uriaşe. Cea mai bună carte de programare: "The Art of Computer Programming", scrisă
de D. E. Knuth, profesor în SUA a apărut pentru prima oară în anul 1968, la editura
Addison Wesley, fiind tradusă în limba română la Editura Tehnică în 1974 sub titlul de
"Tratat de programarea calculatoarelor". În anul 1998, cartea a fost reeditată tot la
editura Addison Wesley, fiind tradusă apoi în 2000 la editura Teora sub titlul "Arta
programării calculatoarelor". Modificările între cele două versiuni sunt minore, în timp
ce tehnica de calcul a evoluat exponenţial. Bill Gates a afirmat că “Dacă te crezi un bun
programator, citeşte Arta programării calculatoarelor a lui Knuth. Dacă poţi citi toată
cartea, trimite-mi neapărat un CV”.
8
(operaţii aritmetice fundamentale) în opera sa “Scurtă carte despre calcul algebric”.
Mai târziu, această descriere apare sub denumirea de algoritm în « Elementele lui
Euclid » . Algoritmul lui Euclid pentru calculul celui mai mare divizor comun a două
numere naturale este primul algoritm cunoscut în matematică. În timpul lui Adam Riese
(sec XVI) se foloseau algoritmi pentru dublarea, înjumătăţirea sau înmulţirea unor
numere. Alţi algoritmi apar în lucrările lui Stifer (1544) şi Cardano (1545). Chiar şi
Leibnitz vorbeşte de algoritmi « de înmulţire ». Kronecker (1886) şi Dedekind (1888)
dau naştere teoriei funcţiilor recursive care devine un concept important în teoria
algoritmilor. De abia în deceniul trei şi patru al secolului XX teoria recursivităţii şi a
algoritmilor prinde contur prin lucrările lui Skolem, Ackermann, Sudan, Godel, Church,
Kleene, Turing etc.
9
operaţii care se desfăşoară algoritmic dar nu orice problemă poate fi rezolvată
algoritmic.
-generalitatea – algoritmul trebuie să fie cât mai general astfel ca să rezolve o clasă cât
mai largă de probleme şi nu o problemă particulară sau punctuală. El trebuie să
poată fi aplicat oricărui set de date iniţiale ale problemei pentru care a fost întocmit.
De exemplu algoritmul de rezolvare a ecuaţiei de gradul II ax2+bx+c=0 trebuie să
rezolve toate cazurile pentru o mulţime infinită de date de intrare (a, b şi c
aparţinând numerelor reale).
-claritatea– acţiunile algoritmului trebuie să fie clare, simple şi riguros specificate. Un
algoritm trebuie să descrie cu precizie ordinea operaţiilor care se vor efectua.
-finitudinea –un algoritm trebuie să admită o descriere finită şi să conducă la soluţia
problemei după un număr finit de operaţii. Orice metodă (algoritm) aplicat trebuie
să conveargă spre soluţia problemei. în timp ce metoda matematică este corectă
chiar dacă ea converge către soluţie doar la infinit, un algoritm trebuie să întoarcă
rezultatul după un număr finit de paşi. Acolo unde matematica nu oferă o
10
demonstraţie, nici un algoritm nu va fi capabil să o ofere. De exemplu, pentru e
verifica corectitudinea Conjecturii lui Goldbach: “Orice număr par se scrie ca sumă
de două numere prime”, s-a putut uşor scrie un algoritm şi deşi programul elaborat a
fost lăsat să ruleze pînă la valori extrem de mari, fără să apară nici un contra-
exemplu, totuşi conjectura n-a putut fi astfel infirmată (dar nici demonstrată).
-corectitudinea – un algoritm trebuie să poată fi aplicat şi să producă un rezultat corect
pentru orice set de date de intrare valide. De asemenea, un algoritm trebuie să prevadă
modul de soluţionare a tuturor situaţiilor posibile care pot apare în rezolvarea unei
probleme date. Principiul conform căruia “Errare humanum est” nu se aplică în cazul
algoritmilor, aceştia nu vor greşi şi nu se vor plictisi niciodată şi vor duce programul la
sfârşit indiferent de câte ori li se va cere. Orice eroare a unui program are în spate fie o
greşeală în algoritm fie o transcriere eronată a algoritmului în programul elaborat pe
baza lui. Corectitudinea este de 2 feluri: corectitudine totală (faptul că pentru orice date
de intrare algoritmul determină valori corecte de ieşire) şi parţială (finititudinea
algoritmului pentru orice set de date de intrare). Verificarea corectitudinii unui algoritm
se poate face folosind:
- varianta experimentală prin testarea algoritmului pentru diverse instanţe ale
problemei. Avantajul acestei variante îl constituie simplitatea iar dezavantajul îl
constituie faptul că testarea nu poate acoperi întotdeauna toate variantele
posibile de date de intrare
- varianta analitică se bazează pe demonstrarea funcţionării corecte a algoritmului
pentru orice date de intrare, garantând astfel corectitudinea. Uneori este dificil
de găsit o demonstraţie. Abordarea analitică conduce la o mai bună înţelegere a
algoritmului şi la identificarea secventelor ce conţin eventuale erori.
Demonstrarea analitică a corectitudinii unui algoritm presupune parcurgerea a 3
paşi:
a)Identificarea precondiţiilor problemei (proprietăţile datelor de intrare)
b)Identificarea postcondiţiilor problemei (proprietăţile rezultatelor)
c)Demonstrarea faptului că pornind de la precondiţii şi aplicând prelucrările
specificate în algoritm ajungem la satisfacerea postcondiţiilor
-performanţa – algoritmul trebuie să fie eficient privind resursele utilizate şi anume să
utilizeze memorie minimă şi să se termine într-un timp minim.
-robusteţea – reprezintă abilitatea algoritmului de a recunoaşte situaţiile în care
problema ce se rezolvă nu are sens şi de a se comporta în consecinţă (de exemplu,
prin mesaje de eroare corespunzătoare). Un algoritm robust nu trebuie să fie afectat
de datele de intrare eronate.
Este posibil ca pentru rezolvarea unei probleme să existe mai mulţi algoritmi. În
aceste cazuri, la alegerea algoritmului se ţine seama de diverse criterii cum ar fi: viteza
de calcul, mărimea erorilor de calcul, spaţiul de memorie internă a calculatorului
necesar programului corespunzător algoritmului, etc. De regulă, nu putem construi un
algoritm care să satisfacă toate criteriile de performanţă. De aceea, se alege algoritmul
care răspunde cel mai bine cerinţelor care se impun. În cadrul activităţii de cercetare şi
elaborare de software din domeniul tehnologiei informaţiei este determinantă
elaborarea, testarea şi implementarea de algoritmi cât mai performanţi.
11
1.3. Reprezentarea algoritmilor
Pseudocod
Scheme logice
12
începutul şi sfârşitul unei proceduri (subprogram), specificându-se în acest caz în
interiorul său numele procedurii şi parametrii de intrare şi cei de ieşire. Sfârşitul
procedurii este marcat printr-un bloc delimitator ce conţine cuvântul RETURN.
Blocul de intrare/ ieşire are forma unui paralelogram sau a unui trapez. Acest
bloc se foloseşte la citirea datelor de intrare ale algoritmului şi afişarea rezultatelor.
Uneori în locul simbolului “=” pentru atribuire se folosesc simbolurile “:=” sau “←“.
Blocul de decizie are forma unui romb. El pune în evidenţă etapele de decizie
sau punctele de ramificaţie ale algoritmului. Se evaluează condiţia: dacă este adevarată
se continuă cu ramura având indicativul da, altfel cu ramura nu.
13
Fig. 1.4. Blocul de decizie
Structura secvenţială
Cea mai simplă structură întâlnită în schemele logice este structura secvenţială.
Această structură reprezintă un proces de calcul format dintr-o operaţie elementară, cum
este citirea unei valori sau o operaţie de atribuire, dar poate fi şi o combinaţie de alte
structuri. Structura secvenţială indică execuţia succesivă a operaţiilor de bază şi a
structurilor de control în ordinea în care apar în schema logică; în general, orice schemă
logică cuprinde secvenţele:
• citirea datelor
• iniţializarea variabilelor
• prelucrări
• tipărirea rezultatelor
14
Structura alternativă
În funcţie de valoarea de adevăr a condiţiei, se execută una din secvenţe, după
care se trece la prelucrarea următoare; cele două ramuri se exclud mutual; este posibil ca
una din ramuri sa fie vidă.
15
Fig. 1.8. Structura alternativă cu condiţionare posterioară
16
1.5. Rezolvarea numerică a ecuaţiilor algebrice sau transcendente
Separarea rădăcinilor
O ecuaţie de forma: f(x)=0, unde f:R→R
se numeşte algebrică dacă funcţia f(x) este un polinom sau poate fi adusă la formă
polinomială.
x+sin(x) = 0
ex – 1 = 0
sunt transcendente.
În fiecare interval:
(-∞,x1),(x1,x2),…(xi,xi+1), …(xM, ∞)
se află o rădăcină reală a ecuaţiei f(x)=0 dacă şi numai dacă funcţia f ia valori contrare
la capetele intervalului adică dacă f(xi)⋅f(xi+1)<0.
Această metodă deseori nu se poate aplica deoarece rezolvarea ecuaţiei f’(x)=0
este la fel de dificilă precum rezolvarea ecuaţiei f(x)=0.
În schimb următoarea teoremă este des utilizată pentru separarea rădăcinilor unei
ecuaţii.
17
Dacă o funcţie continuă f(x) admite valori de semn contrar la capetele unui
interval [a,b], adică f(a)⋅f(b)<0 atunci în intervalul [a,b] se găseşte cel puţin o rădăcină a
ecuaţiei f(x)=0 (mai precis un număr impar de rădăcini).
f(a)⋅f(b)<=0
f(b)
Y-Axis
f(c)
a
c=(a+b)/2
X-Axis b
f(a)
noul
c
Fig. 1.10. Metoda înjumătăţirii intervalului
18
Observaţii:
Dacă sunt îndeplinite condiţiile iniţiale ale problemei, această metodă sigur
converge spre soluţia unică (aproximativă) a ecuaţiei.
Metoda este simplă dar are cele mai slabe proprietăţi de convergenţă.
f(b)
Y-Axis
f(xi)
xi
X-Axis b
f(a)
Se duce o secantă care uneşte punctele (ai,f(ai)) şi (bi,f(bi)). Dacă xi este punctul
în care secanta intersectează axa ordonatelor, atunci putem scrie ecuaţia corzii:
x i − a i f ( x i ) − f (a i )
=
b i − a i f ( b i ) − f (a i )
a i f ( b i ) − b i f (a i )
xi =
f ( b i ) − f (a i )
19
Identic, algoritmul se opreşte în momentul în care |ai-bi|<=ε.
f(x) + x – x = 0
x = f(x) + x
x = F(x).
x1=F(x0)
20
Metoda lui Newton (a tangentei)
În acest caz, punctul faţă de care se stabileşte intervalul reprezintă intersecţia
tangentei dusă la curba y=f(x) cu axa ordonatelor.
Fie x0 punctul în care se duce tangenta la y=f(x). Alegerea punctului x0 se face
astfel încât:
f(x0)⋅f’’(x0)>0
f(x0)
Y-Axis
f(x1)
X-Axis x0
x1
y-f(x0)=f’(x0)(x1-x0)
f (x 0 )
x1 = x 0 −
f ' (x 0 )
|xi-xi+1|<=ε.
21
Metoda lui Newton este mai rapid convergentă decât metoda substituţiilor
succesive. Condiţia este ca derivata funcţiei f(x) să nu fie prea complicată.
22
Se dau 2 matrici A(m,n) şi B(m,n). Matricea C(m,n) se numeşte suma matricilor
A şi B dacă cij = aij + bij oricare ar fi i=1,m, j=1,n.
Pentru a înmulţi o matrice cu un scalar, se înmulţeşte fiecare element al matricei
cu acel scalar.
Două matrici A(m,n) şi B(l,p) se pot înmulţi dacă numărul de coloane al primei
matrici este egal cu numărul de linii al celei de a doua, deci dacă n=l.
Dacă n=l atunci A(m,n)⋅B(l,p)=C(m,p).
o elementul c11 se obţine însumând produsele elementelor dintre prima linie a
n
matricii A cu prima coloană a matricii B, c11 = ∑ a1k bk 1
k =1
o elementul c12 se obţine însumând produsele elementelor dintre prima linie a
n
matricii A cu a doua coloană a matricii B, c12 = ∑ a1k bk 2
k =1
n
o formula generală pentru cij este cij = ∑ aik bkj pentru i=1,m şi j=1,p
k =1
Pentru produsul a 2 matrici pătratice, algoritmul necesită n3 înmulţiri şi (n-1)n2
adunări scalare.
Algoritmul în pseudocod este următorul:
Citeşte m,n,p,A(m,n),B(n,p)
Pentru i=1 la m execută
Pentru j=1 la p execută
c(i,j)=0;
Pentru k=1 la n execută
c(i,j)=c(i,j)+a(i,k)*b(k,j)
Sf-pentru
Sf-pentru
Sf-pentru
Afişează C(m,p)
Inversa unei matrici pătratice A, notată A-1 este matricea care îndeplineşte
condiţia:
A⋅A-1=A-1⋅A=I (unde I este matrice unitate).
O matrice este inversabilă dacă şi numai dacă determinantul asociat ei este
nenul. O astfel de matrice se numeşte nesingulară.
Inversa unei matrici se poate calcula cu formula:
1
A -1 = A* unde det A este determinantul matricei A iar A* este matricea adjunctă a
det A
matricei A. Pentru a obţine matricea adjunctă se fac următoarele operaţii:
23
o Se obţine matricea transpusă AT a matricei A
o Se calculează elementele matricii adjuncte a*ij prin calcularea determinantului
matricei ce se obţine prin eliminarea liniei i şi coloanei j, valoare ce se
înmulţeşte cu (-1)i+j.
Acest procedeu este greoi. De aceea, pentru a calcula inversa unei matrici se
foloseşte următoarea teoremă:
Dacă printr-un şir finit de operaţii elementare o matrice se aduce la matricea
unitate, atunci aplicând aceleaşi operaţii asupra unei matrici unitate, se va obţine
matricea inversă.
Prin operaţii elementare se înţelege:
o înmulţirea unei linii cu o constantă
o schimbarea a 2 linii între ele
o înmulţirea unei linii cu o constantă şi adunarea ei la o altă linie.
Pentru a calcula inversa unei matrici, extindem matricea la dreapta cu o matrice unitate.
a11 a12 … a1n 1 0 ... 0
a21 a22 … a2n 0 1 ... 0
… ...
an1 an2 … ann 0 0 ... 1
Pasul 1:
• Dacă a11=0 atunci căutăm un ai1<>0 pentru i=2,n.
o Dacă nu se găseşte atunci se dă mesaj “Matrice neinversabilă” şi se
opreşte algoritmul.
o Dacă se găseşte, se schimbă linia 1 cu linia i.
a1 j
• Se împarte linia 1 cu a11 deci a1 j = pentru i=1,2n (atenţie: toate operaţiile se
a11
fac asupra matricei extinse).
• Se înmulţeşte linia 1 cu ak1 şi se scade din linia k. Deci akj=akj-akj⋅a1j pentru
j=2,2n şi k=2,n.
După pasul 1 matricea arată astfel:
1 a12 … a1n a1 n+1 a1 n+2 ... a1 2n
0 a22 … a2n a2 n+1 a2 n+2 ... a2 2n
… ...
0 an2 … ann an n+1 an n+2 ...an 2n
Pasul i:
• Dacă ai i=0 atunci căutăm un aki<>0 pentru k=i+1,n.
24
o Dacă nu se găseşte atunci se dă mesaj “Matrice neinversabilă” şi se
opreşte algoritmul.
o Dacă se găseşte, se schimbă linia i cu linia k.
a ij
• Se împarte linia i cu aii deci a ij = pentru j=i+1,2n.
aii
• Se înmulţeşte linia k cu aki şi se scade din linia k. Deci akj=akj-aki⋅aij pentru j=i,2n
şi k=1,n-{i}.
a)metode exacte (directe) -sunt algoritmi finiţi pentru calculul unor soluţii aşa zis
exacte a sistemelor de ecuaţii liniare. Aici se pot menţiona: metoda lui Gauss, metoda
lui Gauss-Jordan etc. O dată cu creşterea ordinului sistemului se pot acumula erorile de
rotunjire, denaturând soluţiile.
b)metode iterative –permit găsirea soluţiei sistemelor de ecuaţii liniare cu o anumită
precizie într-un număr finit de paşi, pe baza unui proces iterativ convergent infinit.
Există cazuri când metodele iterative nu converg.
X = lim x k
k →∞
Procesul este întrerupt după un număr finit de paşi în momentul atingerii unei
anumite precizii date. Astfel de metode sunt metoda lui Jacobi şi metoda lui Gauss-
Seidel.
25
Pornim de la următorul sistem de n ecuaţii cu n necunoscute:
a11x1 + a12x2 + … + a1jxj + … +a1nxn=b1
…
ai1x1 + ai2x2 + … + aijxj + … +ainxn=bi
…
an1x1 + an2x2 + … + anjxj + … +annxn=bn
Pasul 1:
• Dacă a11=0 căutăm un ai1<>0, pentru i=2,n.
o Dacă nu găsim, tipărim un mesaj “Metoda lui Gauss nu se poate utiliza”
şi oprim execuţia algoritmului
o Dacă am găsit un ai1<>0, atunci schimbăm linia 1 cu linia i între ele
(inclusiv termenii liberi corespunzători).
a1 j a i1
• Pentru i=2,n, calculăm a ij ← a ij - , pentru j=1,n. (deoarece prima linie este
a11
a i1
înmulţită cu - şi adunată la linia i). Astfel, se înmulţeşte prima linie cu
a 11
a a 31
- 21 şi se adună la linia 2. Se înmulţeşte prima linie cu - şi se adună la linia
a 11 a 11
3 ş.a.m.d. până la ultima linie.
• Având în vedere că operaţiile făcute asupra matricei sistemului trebuie efectuate
b1 ai1
şi asupra vectorului termenilor liberi vom avea: bi ← bi - , pentru i=2,n.
a11
Pasul i:
Sistemul a ajuns la următoarea formă:
26
aij a ki
• Pentru k=i+1,n, calculăm a kj ← a kj - , pentru j=i,n. (deoarece linia i este
a ii
a ki a i +1i
înmulţită cu - şi adunată la linia k). Astfel, se înmulţeşte linia i cu - şi se
a ii a ii
a i + 2i
adună la linia i+1. Se înmulţeşte linia i cu - şi se adună la linia i+2 ş.a.m.d.
a ii
până la ultima linie.
• Având în vedere că operaţiile făcute asupra matricei sistemului trebuie efectuate
bi a ki
şi asupra vectorului termenilor liberi vom avea: bk ← bk - , pentru k=i+1,n.
aii
bi - ∑a ij xj
j= i +1
• xi = , pentru i=n-1,1
aii
27
START Cauta_pivot(i,k) Aplica_transf(i)
P=a(k,i)/a(i,i)
k=1 DA a(k,i)<>0
Return(k) j=i
i=1 NU NU
a(k,j)=a(k,j)-
DA a(i,j)*p
k=k+1
DA a(i,i)=0 DA j=j+1
Cauta_pivot(i,k) k<=n
j<=n
k<>0 Return(0)
DA NU b(k)=b(k)-b(i)*p
DA Schimba_linii(i,k) NU k=k+1
Schimba_linii(i,k)
j=i+1 k<=n
DA k<>0
i=i+1 s=0
j<=n
j=i+1
i<=n-1&&k
b(i)<-> b(k)
Nu DA s=s+a(i,j)*x(j)
DA
DA a(n,n)=0 Return j=j+1
k=0 j<=n
NU Determ_solutii()
x(i)=(b(i)-s)/a(i,i)
DA k=0 NU
X(n)=b(n)/a(n,n)
“metoda lui i=i-1
Gauss nu se Determ_solutii()
poate folosi”
i=n-1
i>=1
x(i),i=1,n
1
Return
STOP
28
Observaţie: Erorile de rotunjire în calculul elementelor matricii sunt cu atât mai mici cu
cât elementul pivot este mai mare în valoare absolută (deoarece eroarea la împărţire este
cu atât mai mică cu cât împărţitorul este mai mare). De aceea, algoritmul se poate
modifica în sensul de a căuta ca element pivot elementul maxim de pe coloana curentă
în loc de a ne opri la primul element diferit de zero.
Metoda Gauss-Jordan
x1=t1+s12x2+ … +s1nxn
xi=si1x1+si2x2 +….+ti+…+sinxn
…
xn=sn1+sn2x2+…+tn
X=T+S⋅X
Xk=T+S⋅X(k-1), k=1,2, …
xi(0)=ti
n
( k −1)
xi(k)=ti+ ∑s ij xj k=1,2,…
j=1, j≠ i
29
Condiţia de convergenţă a metodei Jacobi este ca elementele de pe diagonala
principală să fie dominante în valoare absolută. De exemplu, pentru sistemul de 3
ecuaţii cu 3 necunoscute:
4x1+x2+2x3=16
x1+2x2+5x3=12
x1+3x2+x3=10
-pentru prima linie 4>1+2 “Adevărat”
-pentru a doua linie 2>1+5 “Fals” –se impune schimbarea liniei 2 cu linia 3. După
schimbare:
-pentru a doua linie 3>1+1 “Adevărat”
-pentru a treia linie 5>1+2 “Adevărat”
Să se scrie un program care determină dacă un an este bisect sau nu. Anii bisecţi
sunt divizibili cu 4, excepţie făcând cei care sunt divizibili cu 100, care trebuie să fie
divizibil şi cu 400.
30
Se citeşte un şir de caractere reprezentând un număr în baza 16. Să se afişeze
numărul convertit în baza 10.
31
Se citesc m şi n lungimile a 2 numere întregi foarte lungi. Se citesc apoi cele m
cifre reprezentând numărul a şi n cifre reprezentând numărul b. Fiecare număr se va
păstra în câte un şir ce va conţine cifrele sale. Să se calculeze într-un al 3-lea şir suma
celor două numere foarte mari.
32
Să se găsească soluţia aproximativă a ecuaţiei x3-4x+3=0 folosind metoda
înjumătăţirii intervalului şi un număr de 10 iteraţii, ştiind că ecuaţia admite o soluţie în
intervalul [0,2.5] Să se găsească eroarea absolută ε ştiind că soluţia exactă este α=1.
REZUMAT Acest modul este compus din trei părţi: prima parte începe cu prezentarea
unui scurt istoric al algoritmilor, caracteristicile noţiunii de algoritm,
modalităţi de reprezentare a algorimilor, structurile de bază din
programarea structurată.
Al doua parte are ca scop prezentarea principalelor metode de rezolvare
numerică a ecuaţiilor algebrice și transcendente.
Al treia parte urmărește problematica calculului matricial. Sunt prezentate
metode de rezolvare a unui sistem de n ecuaţii cu n necunoscute.
Intrebări Ce este un algoritm?
recapitulative Ce este un program?
Care este diferenţa dintre un algoritm şi un program?
Care sunt caracteristicile unui algoritm?
Care sunt modalităţile de reprezentare a unui algoritm?
Ce este limbajul pseudocod?
Ce este o schemă logică?
Ce tipuri de blocuri se utilizează într-o schemă logică?
Care sunt cele 3 structuri de bază dintr-un algoritm?
Definiţi următoarele noţiuni: structura secvenţială, structura alternativă,
structura repetitivă cu condiţionare anterioară, structura repetitivă cu
condiţionare posterioară, structura repetitivă cu număr cunoscut de paşi.
Daţi un exemplu de ecuaţie transcendentă.
Cum definiţi rădăcina unei ecuaţii.
Cum definiţi rădăcina aproximativă a unei ecuaţii.
Enunţaţi Teorema lui Rolle.
Explicaţi metoda înjumătăţirii intervalului.
Explicaţi metoda secantei.
Explicaţi metoda aproximaţiilor successive.
Explicaţi metoda lui Newton.
Definiţi următoarele noţiuni: matrice, transpusa unei matrici, matrice
pătratică, matrice linie, matrice coloană, matrice nulă, diagonala
33
principală a unei matrici, diagonala secundară a unei matrici, matrice
simetrică, matrice antisimetrică, matrice superior triunghiulară, matrice
inferior triunghiulară, matrice superior trapezoidală, matrice inferior
trapezoidală, matrice diagonală, matrice unitate, matrice ortogonală,
inverse unei matrici
Daţi algoritmul în pseudocod sau sub formă de schemă logică pentru
produsul a 2 matrici
Daţi algoritmul în pseudocod sau sub formă de schemă logică pentru
aflarea inversei unei matrici pătratice
Daţi algoritmul în pseudocod sau sub formă de schemă logică pentru
produsul a 2 matrici
Descrieţi algoritmul metodei lui Gauss
Explicaţi diferenţa dintre metoda lui Gauss şi metoda lui Gauss-Jordan
Descrieţi algoritmul metodei iterative Jacobi
Explicaţi diferenţa dintre metoda iterativă Jacobi şi Gauss-Seidel
Bibliografie [Negrescu01], [Bologa06] cap 1, 2, 3, 4, 5, 6
34
MODULUL NUMĂRUL 2
ALGORITMI DE SORTARE
35
dar ceea ce ne interesează priveşte algoritmul folosit. Problema timpului se pune pentru
valori mari ale lui n.
Se defineşte complexitatea unui algoritm ca fiind T(n)=O(f(n)) dacă în expresia
complexităţii termenul dominant are forma c*f(n), unde c este o constantă.
Dacă T(n) este timpul cerut de algoritm, determinarea expresiei lui T(n) este
deseori greu de realizat. Pentru unii algoritmi se poate găsi o valoare minimă şi una
maximă, valori care diferă în termenul dominant printr-o constantă multiplicativă.:
c1*f(n)<=T(n)<=c2*f(n), pentru orice n>n0
Spunem că T(n)=O(f(n)) dacă există două constante pozitive n0 şi c astfel încât
T(n)<=cf(n) pentru orice n>n0.
Pentru valori mari ale lui n se observă că O(c*f(n)) este similar cu a exprima o
complexitate de tip O(2*n) sau cu O(n/2). În schimb, pentru valori mici ale lui n, un
algoritm de complexitate O(n) având un timp de execuţie de forma T(n)=k*n necesită
un timp mai mare decât un algoritm de complexitate O(n2)
De asemenea O(f(n)+g(n)) este identic cu O(f(n) dacă f(n)>=g(n). De exemplu
O(n2+n) este identic cu a scrie O(n2), deoarece într-o funcţie polinomială termenul de
grad maxim este dominant.
Un algoritm la care timpul de execuţie nu depinde de dimensiunea problemei
este un algoritm de timp constant a cărui complexitate se notează cu O(1).
Timpul de execuţie al unui algoritm de complexitate O(n) creşte liniar (direct
proporţional) cu valoarea lui n. Un algoritm liniar are complexitatea O(n), unul pătratic
O(n2) iar unul cubic O(n3).
Un algoritm se numeşte polinomial dacă are o complexitate egală cu O(p(n))
unde p(n) este o funcţie polinomială.
O altă clasă de algoritmi sunt cei exponenţiali. Sunt asimilaţi algoritmilor
exponenţiali şi cei de forma n*ln(n) deşi matematic nu sunt nici polinomiali nici
exponenţiali.
Algoritmii polinomiali sunt preferaţi celor exponenţiali, aceştia din urmă
devenind inutilizabili pentru valori mari ale lui n. Trebuie avute în vedere următoarele
aspecte:
o un algoritm exponenţial poate fi mai eficient decât unul polinomial pentru valori
mici ale lui n. Astfel f(n)=2n ia valori mai mici decât f(n)=n5 pentru n<=20.
o există algoritmi exponenţiali care se comportă acceptabil pentru probleme
particulare.
Pentru exemplificare prezentăm un tabel care prezintă evoluţia timpului de
execuţie al unui program funcţie de tipul de complexitate şi de valoarea lui n.
36
În cazul în care dispunem de un calculator capabil să efectueze 109
instrucţiuni/secundă, timpul necesar pentru execuţia unui algoritm evoluează astfel:
37
Există o dependenţă între reprezentarea datelor ce trebuie sortate şi alegerea
algoritmului de sortare. Astfel, algoritmii de sortare se împart în:
o algoritmi interni –în cazul acestora, secvenţa de sortat este păstrată în memoria
internă
o algoritmi externi –secvenţa de sortat este păstrată pe un suport extern.
Sortare ordinară
Cel mai simplu algoritm de sortare (dar şi cel mai ineficient) se bazează pe
următorul principiu: un şir este sortat dacă prin parcurgerea lui de la început până la
sfârşit, fiecare element este mai mic decât succesorul. Dacă această condiţie nu este
îndeplinită, inversăm cele 2 elemente. Sortarea se încheie în momentul în care
parcurgerea şirului se face fără a fi necesară nici o inversare (fiecare element este mai
mic decât succesorul său).
38
Cazul mediu: În general, cazul mediu este mai greu de definit. Uneori se ia o medie
între cazul cel mai favorabil şi cazul cel mai nefavorabil. În cazul acestei metode, putem
considera: Nc=(n+1)(n-1)/2; Mc=3n(n-1)/4.
Complexitatea metodei este O(n2).
Metoda este stabilă datorită faptului că pentru elemente egale nu se face
inversare, deci ele îşi vor păstra ordinea şi după sortare.
O variantă îmbunătăţită a acestui algoritm, presupune compararea fiecărui
element alt şirului cu toate celelalte elemente, făcându-se interschimbarea atunci când
elementul mai mare are indexul mai mic.
Principiul acestei metode este următorul: şirul este împărţit în 2 părţi: o “Parte
stânga” sortată şi o “Parte dreapta” nesortată. Iniţial “Partea stângă” este vidă iar “Partea
dreapta” conţine tot şirul. Se caută minimul din “Partea dreapta” şi se adaugă la sfârşitul
“Părţii stânga”. Algoritmul se încheie în momentul în care “Partea dreaptă” mai conţine
un singur element. Acest ultim element este maximul din şir deci, în final, “Partea
stânga” va conţine toate elementele şirului sortate, iar “Partea dreapta” devine vidă.
Metoda este stabilă; în cazul unor elemente egale, datorită faptului că căutarea
de minim se face de la stânga la dreapta, deci un element cu cheie egală care se află mai
la stânga va rămâne şi după sortare mai la stânga decât altul cu aceeaşi cheie.
Complexitatea metodei:
Numărul de comparaţii de chei este independent de ordinea iniţială a cheilor .
Nc=(n2 –3n +2) / 2
Numărul atribuirilor este cel puţin 3 pentru fiecare valoare a lui i.
Mc=3(n-1)
Complexitatea metodei este O(n2).
39
Sortare prin inserţie (Insertion Sort)
Analog ca la sortarea prin selecţie, şirul este împărţit în 2 părţi: o “Parte stânga”
sortată şi o “Parte dreapta” nesortată. Iniţial “Partea stângă” este vidă iar “Partea
dreapta” conţine tot şirul. La sortarea prin inserţie se ia primul element din “Partea
dreaptă”, care se inserează la locul său din “Partea stângă”.
Corectitudine: pentru cazul i=1, secvenţa de un singur element este deja sortată.
Presupunem adevărată afirmaţia a[1]..a[i] sortat, urmând să demonstrăm că este valabilă
şi pentru i+1. După terminarea pasului i vom avea i=i+1. După parcurgerea
instrucţiunilor din ciclu, a[i+1] va fi inserat în poziţia corespunzătoare din a[1]...a[i],
deci a[1]...a[i+1] va fi sortat.
Complexitate:
Cazul cel mai favorabil: Ciclul while nu se execută niciodată (vectorul este direct sortat
crescător). În acest caz: Nc=n-1, Mc=3*(n-1)
Cazul cel mai nefavorabil: Ciclul while se execută până când la atingerea primului
element din partea sortată, adică se execută de i-1 ori. În acest caz: Nc=n(n-1)/2, Mc=(n2
+5n –6) / 2
Cazul mediu: Dacă cazul cel mai defavorabil înseamnă i-1 execuţii ale ciclului while
(pentru pasul i) iar cazul cel mai favorabil 0 execuţii, atunci în cazul mediu, este
necesară parcurgerea a jumătate din şirul a[1]...a[i-1] deci Nc=(n2 + n - 2) / 4,
Mc= (n2 +11n –12) / 4.
Complexitatea metodei este O(n2).
40
nesortată şi inserarea lui în “Partea dreaptă”), santinela se va găsi după ultimul element
al şirului şi va avea o valoare foarte mare (mai mare decât oricare din elementele
şirului).
Mmin=0
Mmax=3C=3(n2 –3n +2) / 2
Mmed=3(n2 –3n +2) / 4
Algoritmul a fost propus de D.L. Shell în anul 1959. Punctul de plecare a fost
faptul că sortarea prin inserţie directă este lentă, deoarece interschimbă doar elemente
adiacente. În cazul în care cel mai mic element se află pe ultima poziţie în vectorul de
sortat, atunci vor fi necesare n-1 interschimbări pentru a-l aduce în poziţia corectă.
41
Având în vedere acest fapt, algoritmul Shell Sort permite interschimbarea unor elemente
care se află la distanţe mai mari unele de altele, micşorând astfel timpul de execuţie.
Algoritmul Shell Sort defineşte noţiunea de vector h sortat. Un vector este h sortat dacă
luând tot al h-lea element pornind din orice poziţie obţinem o secvenţă sortată.
Algoritmul începe cu valori mai mari ale lui h, continuând apoi cu valori din ce în ce
mai mici ale lui h. În momentul în care h=1 (vectorul este 1-sortat) şirul este sortat în
întregime.
42
algoritm de sortare cunoscut, pentru un şir cu n>1000 elemente algoritmul fiind de cel
puţin 1,5-2 ori mai rapid ca alt algoritm de sortare.
Principiul acestei metode este următorul: Se alege un element pivot din tabloul
ce trebuie sortat. Tabloul este astfel partiţionat în 2 subtablouri, alcătuite de o parte şi de
alta a acestui pivot, astfel: elementele mai mari decât pivotul sunt mutate în dreapta
pivotului iar elementele mai mici în stânga pivotului. Cele 2 subtablouri sunt sortate în
mod independent prin apeluri recursive ale algoritmului.
Observaţii:
1) Nu există nici un test pentru ieşirea celor 2 indici i şi j din limitele l şi r. Indicele
i începe din l şi tinde spre r. Indicele i nu va putea depăşi valoarea r deoarece, în
cazul în care nu găseşte până la r un element mai mare sau egal cu pivotul, vom
ieşi cu i egal cu r deoarece elementul a[i] este egal cu el însuşi (a[r]). Deci
santinela pentru indicele i este chiar elementul pivot a[r]. Pentru j este nevoie de
o santinelă, şi aceasta este a[0] care este iniţializat cu valoarea minimă posibilă
(-MAXINT pentru numere întregi).
2) După ieşirea din ciclul for (când i>=j), determinarea indicelui elementului cu
care este interschimbat pivotul se face după următorul raţionament: a[i]>=a[r] şi
elementele din secvenţa a[l]..a[i-1] <= a[r]; similar pentru j se poate afirma că
a[j]<=a[r] şi elementele din secvenţa a[j+1]...a[r] >=a[r]. Dacă se alege i ca şi
poziţie de interschimbare, elementele din secvenţa a[i+1]...a[r] >=a[r] iar
a[i]>=a[r] atunci interschimbarea elementelor a[i] cu a[r] conduce spre o partiţie
dreapta corectă. Dacă, în mod greşit, s-ar fi ales ca indice de interschimbare
valoarea j, elementele din secvenţa a[j+1]...a[r} >=a[r] dar faptul că parcurgerea
dreapta s-a oprit la j nu poate conduce decât la concluzia că a[j}<=a[r].
43
3) Alegerea ca element pivot a extremităţii finale a secvenţei de sortat nu este
obligatorie. Analog, se poate alege ca element pivot extremitatea stângă a
secvenţei de sortat, caz în care nu mai este nevoie de santinela stânga a[0] ci de
o santinelă egală cu MAXINT după ultimul element al şirului. În acest caz
indicele de interschimbare va fi j.
4) Performanţele foarte bune ale acestui algoritm se datorează ciclului intern scurt,
nefăcându-se decât incrementare/decrementare de index şi comparaţie cu o
valoare fixă. De asemenea, renunţarea la santinela necesară (stânga în cazul
pivotului extremitate dreaptă respectiv santinelă dreapta în cazul pivotului
extremitate stânga) ar impune introducerea unui test suplimentar într-unul din
ciclurile while, fapt ce ar conduce la o scădere de performanţă a algoritmului.
5) Condiţia de ieşire din recursivitate este r>l. Dacă am fi folosit r>=l atunci fiecare
element din şir ar fi fost folosit o dată ca pivot în partiţii triviale (de un singur
element), ceea ce ar scădea de asemenea performanţa algoritmului.
6) Algoritmul se dovedeşte ineficient în cazul unor vectori deja sortaţi, prin
degenerarea partiţiilor şi generarea a n apeluri succesive, foarte costisitoare ca
timp de execuţie. În acest caz s-ar ajunge la o complexitate de ordinul O(n2) (în
mod normal ea trebuie să fie de O(nlog(n)). Pentru acest caz, algoritmul va fi
modificat în sensul de a alege ca element pivot nu una din extremităţile
secvenţei de sortat ci o poziţie aleatoare, înainte de a începe procesul de
partiţionare.
Corectitudine:
Algoritmul de sortare rapidă este un algoritm care utilizează metoda “Divide et
Impera”. La fiecare apel recursiv se parcurg cele 3 etape caracteristice acestei tehnici de
programare.
o Descompunerea şirului în 2 subşiruri de dimensiuni mai mici
o Rezolvarea recursivă a subproblemelor pentru a obţine satisfacerea proprietăţii
că elementele din stânga pivotului să fie mai mici decât pivotul iar elementele
din dreapta să fie mai mari decât pivotul.
o Recompunerea subproblemelor pentru a obţine şirul iniţial sortat
Quick Sort partiţionează şirul iniţial a[l,r] în 2 subşiruri a[l,i-1] şi a[i+1,r] şi
aduce în a[i] un element astfel încât să avem satisfăcută proprietatea: orice element din
a[l]...a[i-1]<=a[i] şi orice element din a[i+1]...a[r]>=a[i].
Complexitate:
Cazul cel mai favorabil este ca la fiecare partiţionare, subvectorul partiţionat să se
împartă în 2 părţi aproximativ egale. Timpul de execuţie pentru un şir de dimensiune n
va fi dat de relaţia de recurenţă: T(n)=2T(n/2)+n (relaţie tipică pentru algoritmi de tipul
Divide et Impera). În consecinţă, avem o complexitate de ordinul O(nlog2(n)).
În cazul cel mai nefavorabil (vector deja sortat), complexitatea este de ordinul O(n2).
În cazul mediu, complexitatea este de ordinul O(nlog2(n)).
Pentru n=10 numărul de operaţii poate fi n3/2 ≈ 32, n2=100, nlog2(n) ≈ 33 iar pentru
n=1000 n3/2 ≈ 31623, n2=1.000.000, nlog2(n) ≈ 9966 (de aproximativ 100 de ori mai
rapid decât un algoritm de sortare cu complexitate O(n2)).
44
Sortare prin interclasare (Merge Sort)
Algoritmul de sortare prin inserţie este eficient pentru valori mici ale lui n
(n<=16). De aceea, sortarea prin interclasare propune o sortare bazată pe principiul
Divide et Impera, care să utilizeze sortarea prin inserţie pentru valori mici ale lui n,
rezultate prin descompunerea şirului iniţial în subşiruri.
Algoritmul Merge Sort se bazează pe 3 paşi esenţiali:
o separarea tabloului în 2 părţi de mărimi cât mai apropiate
o sortarea acestor părţi prin apeluri recursive, până la atingerea unor valori mici
ale lui n pentru care se aplică sortarea prin inserţie sau până la atingerea unor
subşiruri de 1 element (cazul banal)
o interclasarea părţilor sortate obţinându-se direct un şir ordonat crescător
function merge_sort(a,l,r)
Dacă l<r
Atunci m=(l+r)/2
merge_sort(a,l,m);
merge_sort(a,m+1,r);
interclaseaza(a,l,m,r);
Sf-Dacă
45
Un heap poate fi reprezentat şi sub forma unui tablou. Astfel, dacă v este un
tablou care conţine reprezentarea unui heap avem următoarea proprietate:
Elementele v[i]..v[n] îndeplinesc condiţia de structură a heapului:
∨ j>i avem: v[j]>v[2*j], dacă 2*j≤n, respectiv
v[j]>v[2*j+1] dacă 2*j+1≤n.
Evident, pentru valori ale lui j mai mari decât n/2 nu se pune problema
îndeplinirii condiţiilor de mai sus.
Observaţie: Există şi o altă variantă de heap în care cheia fiecărui nod este mai
mare sau egală cu cea a tatălui său. Algoritmul diferă puţin în acest caz faţă de
algoritmul prezentat. Practic, nu mai este necesară mutarea primului element pe ultima
poziţie, fiind necesară doar câte o fază de retrogradare pentru fiecare nou minim inclus
în partea sortată.
46
adâncimea arborelui. Dacă n este numărul de noduri din arbore, 2k<=n<=2k+1 iar
adâncimea arborelui este k+1.
2k-1<n<=2k+1-1 deci k<=log2(n)<k+1 deci k=log2(n). Complexitatea este O(log2(n)).
Se poate aplica în cazul în care avem n chei, valori de tip întreg cuprinse în
intervalul 1..n neexistând astfel duplicate. Principiul metodei este următorul: locul
elementului a[i] este pe poziţia i. Astfel avem următoarea secvenţă de sortare:
comp_sort(A,B,n)
Pentru i=1 la n execută
b(a(i))←a(i)
Sf-pentru
comp_sort(A,n)
Pentru i=1 la n execută
Cât-timp a(i)!=i execută
interschimbă(a[i],a[a[i]])
Sf-cât-timp
Sf-pentru
47
Metoda se numeşte şi sortarea prin determinarea distribuţiilor cheilor.
Complexitatea:
Iniţializarea vectorului count are o complexitate O(nmax) unde nmax este numărul
maxim de elemente existent în intervalul [vmax, vmin].
Faza de numărare (al doilea ciclu al funcţiei de sortare) are o complexitate O(n), unde n
este numărul de elemente al şirului de sortat.
Al treilea ciclu (de generare a şirului sortat) va avea o complexitate O(nmax+n) în cazul
cel mai defavorabil (toate numerele sunt egale) şi O(nmax) în cazul cel mai favorabil
(toate numerele sunt distincte).
Principiul acestei metode este următorul: rezultatul comparaţiei a două chei este
determinat numai de valoarea biţilor din prima poziţie la care ele diferă (considerând
biţii de la stânga spre dreapta). Astfel, se sortează elementele tabloului astfel încât toate
elementele ale căror chei încep cu un bit zero să fie trecute în faţa celor care încep cu 1;
se formează astfel două partiţii ale tabloului iniţial. Cele două partiţii se sortează
independent, în fiecare din ele se aplică aceeaşi metodă, pentru bitul următor. Rezultă
astfel 4 partiţii iar fiecare din ele se vor sorta după al treilea bit ş.a.m.d. Astfel rezultă o
abordare recursivă a acestei metode de sortare, procesul desfăşurându-se analog ca la
metoda Quicksort: se parcurge tabloul de la stânga spre dreapta până se găseşte o cheie
care începe cu 1; se parcurge apoi de la dreapta spre stânga până se găseşte o cheie care
începe cu 0; se interschimbă cele două chei; se continuă în acest mod până când indicii
de parcurgere se întâlnesc; în acest moment tabloul are două partiţii. Se reia procedura,
considerând al doilea bit, pentru fiecare din partiţiile rezultate.
48
simplitatea reprezentării, dar algoritmul poate utiliza orice numere întregi pozitive
(bineînţeles ele trebuie să se încadreze în limitele tipului propus pentru tabloul ce
urmează a fi sortat).
Principiul acestei metode este următorul: Se sortează cheile examinând biţii lor
de la dreapta spre stânga. La pasul i, cheile sunt deja sortate după ultimii i-1 biţi ai lor.
Sortarea după bitul i constă în extragerea elementelor a căror bit i este 0 şi plasarea lor
înaintea elementelor a căror bit i este 1.
Sortarea după acest principiu nu este stabilă. De aceea, această metodă trebuie să
fie combinată cu sortarea prin numărare (având în vedere că trebuie sortat stabil un
tablou cu doar 2 valori: 0 şi 1).
49
2.6. Metode de sortare externă
Multe aplicaţii de sortare implică procesarea unor fişiere foarte mari, care nu
încap în întregime în memorie. Aceste metode sunt denumite metode de sortare externă
şi prezintă anumite particularităţi:
o dacă în cazul sortărilor interne aveam un acces direct la oricare din elementele
structurii de date sortate, în cazul acestor metode de sortare, accesul depinde de
tipul de structură folosită. Astfel, în cazul unor structuri cu acces secvenţial,
elementele structurii pot fi accesate doar utilizând funcţiile prim şi succesor
astfel:
o prim(S), succesor(prim(S),S), succesor(succesor(prim(S),S),S) ...
o Cu excepţia primului element, accesul la un element de rangul i se face doar prin
parcurgerea secvenţială a elementelor de rang 1...i-1, după care avem acces la
elementul de rang i. Astfel, costul accesării unui element într-o structură
secvenţială este foarte ridicat
o într-o structură cu acces secvenţial, interschimbarea unor elemente, nu se poate
face la fel de rapid ca în cazul unui vector.
o pe lângă criteriile de complexitate prezentate în cadrul sortărilor interne, la cele
externe se urmăresc şi numărul de faze în cadrul unui pas şi numărul de paşi de
sortare. Faza de sortare reprezintă grupul de operaţii care conduc la parcurgerea
integrală sau rescrierea integrală a informaţiei conţinute iniţial în structura de
sortat. Pasul de sortare reprezintă mulţimea minimă a fazelor de sortare care,
prin aplicare repetitivă, conduce la sortarea structurii iniţiale.
o sortările externe utilizează un anumit număr de structuri de manevră. Acest
număr constituie un nou indicator de apreciere a eficientei unui algoritm de
sortare externă.
În general, în cazul sortărilor externe, colecţiile de date ce urmează să fie sortate
sunt denumite şi fişiere sau benzi. Având în vedere că nu este posibil transferul întregii
colecţii de date în memorie, sortarea internă urmată de rescrierea colecţiei sortate pe
suportul extern, majoritatea sortărilor externe utilizează următoarea strategie: se face o
primă parcurgere a structurii care se sortează, structură care se împarte în blocuri care
încap în memorie. Dacă datele ce urmează a fi sortate ocupă un volum de n ori mai mare
decât spaţiul de memorie de care dispunem, colecţia se va împărţi în n subcolecţii care
vor fi transferate succesiv în memoria internă. Pentru fiecare din aceste subcolecţii se
face pe rând câte o sortare internă după care se salvează pe disc în structuri
intermediare, după care se interclasează două câte două subcolecţiile sortate, obţinându-
se în final sortarea dorită. Pentru a efectua un număr cât mai mic de operaţii de transfer,
se recomandă interclasarea subcolecţiilor de dimensiuni minime până la obţinerea
întregii colecţii sortate.
Din punct de vedere al tehnicii folosite, se observă că sortările externe utilizează
Divide et Impera, fişierul care urmează să fie sortat fiind împărţit în părţi care vor fi
sortate şi apoi interclasate.
Fie a secvenţa care urmează să fie sortată. Principiul acestei metode este
următorul:
50
o Secvenţa a se împarte în 2 jumătăţi, pe care le denumim b şi c
o Se interclasează b cu c obţinându-se câte un element din fiecare, rezultând
perechi ordonate care vor forma o nouă secvenţă a
o Pentru noua secvenţă a se repetă primii doi paşi, combinându-se perechile
ordonate în quadruple ordonate
o Se repetă primii doi paşi, dublând de fiecare dată lungimea subsecvenţelor de
interclasat, până la atingerea întregii secvenţe
Complexitate:
Numărul de comparaţii este egal cu numărul cheilor din benzile b şi c care sunt
interclasate şi trecute apoi în banda a. În cazul cel mai defavorabil, cheile sunt dispuse
în benzile b şi c astfel încât la terminarea ciclului principal while, unul din fişiere se
găseşte pe eof iar celălalt mai are o înregistrare care trebuie copiată în banda a. În cazul
cel mai favorabil, toate cheile din banda b sunt mai mici decât cheile din banda c sau
invers.
În cazul sortărilor externe, critice sunt citirile şi scriere din chei din memoria
operativă pe disc şi invers. Comparaţiile între chei, având în vedere că respectivele chei
se găsesc deja în memoria internă sunt mult mai rapide decât operaţiile de citire/scriere
pe bandă.
Datorită dublării lui p la fiecare pas, numărul de treceri ale algoritmului este
proporţional cu log2(n), unde n este numărul de elemente ale secvenţei. Algoritmul este
optim din punct de vedere al numărului de benzi folosite.
Sortarea prin interclasare este stabilă, deci două înregistrări având aceeaşi cheie
vor rămâne în aceeaşi ordine relativă şi după sortare.
Un algoritm asemănător, cel de interclasare folosind 4 benzi, utilizează un număr
mai ridicat de benzi (cu 1) dar evită faza de înjumătăţire.
51
faza de înjumătăţire, consumatoare de timp, îmbunătăţindu-se astfel performanţele
algoritmului. Metoda este denumită şi sortarea echilibrată utilizând o singură fază.
Se utilizează 4 benzi, una fiind secvenţa ce urmează să fie sortată şi 3 benzi de
manevră. La fiecare pas, 2 benzi sunt benzi sursă iar celelalte 2 sunt benzi destinaţie,
acestea inversându-şi rolurile după fiecare pas. Algoritmul parcurge următoarele etape:
o la prima trecere secvenţa a se distribuie alternativ câte p=1 elemente în fişierele
b şi c
o p-uplele de pe fişierele b şi c se interclasează şi se distribuie alternativ în
fişierele a şi d. Fişierele b şi c sunt în această fază fişiere sursă iar fişierele a şi d
sunt fişiere destinaţie. la fiecare trecere se interschimbă statutul fişierelor sursă
cu cel al fişierelor destinaţie
o se dublează p, se interclasează subsecvenţe de lungime p din fişierele sursă şi se
distribuie în fişierele destinaţie
Algoritmul se încheie în momentul în care p depăşeşte numărul de elemente ale
fişierului de sortat. Rezultatul (fişierul sortat) se va găsi în fişierul b.
52
numărul de elemente din fişierul de sortat). Algoritmul de sortare prin interclasare
naturală poate fi considerat o interclasare multiplă cu 2 căi).
Dacă se utilizează n fişiere, interclasarea se va face pe n/2 căi. Pentru a gestiona
aceste n fişiere se utilizează un vector de n elemente pentru a păstra situaţia fişierelor
(active sau vide). Pe măsură ce avansează procesul de sortare, se reduce numărul
monotoniilor precum şi cel al fişierelor active. Sortarea se încheie când ajunge, la o
singură monotonie şi deci un singur fişier activ, fişier care este sortat.
53
MODULUL NUMĂRUL 3
TEHNICI DE PROGRAMARE
3.1. Recursivitatea
54
o adresele parametrilor transmişi prin referinţă. În acest caz nu se crează copii
locale, ci operarea se face direct asupra spaţiului de memorie afectat
parametrilor efectivi, de apel.
o valorile tuturor variabilelor locale (declarate la nivelul procedurii sau funcţiei).
Pentru fiecare apel recursiv al unei proceduri (funcţii) se crează copii locale ale
tutoror parametrilor transmişi prin valoare si variabilelor locale, ceea ce duce la
risipă de memorie.
Din punct de vedere al modului în care se realizează autoapelul, există două
tipuri de recursivitate: directă şi indirectă.
În cazul recursivităţii directe procedura (sau funcţia) se autoapelează (în corpul
său). Exemplul clasic este definirea funcţiei factorial:
n!=(n-1)!⋅n, 0!=1, n∈N.
Recursivitatea indirectă are loc atunci când o procedură (funcţie) apelează o altă
procedură (funcţie), care la rândul ei o apelează pe ea. Un astfel de exemplu ar fi
următorul:
Se consideră două valori reale, pozitive a0,b0 şi n un număr natural. Definim şirul:
an=(an-1+bn-1)/2 bn=an-1bn-1
Orice algoritm recursiv are o condiţie de oprire pusă de programator, în caz contrar se
va umple stiva (datorită propagării la nesfârşit a autoapelului) şi aplicaţia va genera
eroare (Stack Overflow –Depăşire de stivă). De asemenea, în cazul unui număr mare de
autoapelări, există posibilitatea ca să se umple stiva, caz în care programul se va termina
cu aceeaşi eroare.
O funcţie recursivă trebuie astfel scrisă încât să respecte regulile:
o funcţia trebuie să poată fi executată cel puţin o dată fără a se autoapela
o funcţia recursivă se va autoapela într-un mod în care se tinde spre atingerea
situaţiei de execuţie fără autoapel.
55
Exemplul 1. Calculul valorii n !.
#include<stdio.h>
#include<conio.h>
void main(void)
{ int n ;
int fact(int) ;
clrscr() ;
scanf(“%d“,&n) ;
printf(“%d !=%d“,n,fact(n));
getch();
}
int fact(int n)
{ if ( !n) return 1 ;
else return (n*fact(n-1));
}
Metoda backtracking
Observaţii:
nu pentru toate problemele n este cunoscut de la început;
56
x1,x2,x3…xn pot fi la rândul lor vectori;
mulţimile A1,A2,A3…An pot coincide.
2 8 9
3 4 10 11
5 6 7
Fig. 3.2. Parcurgerea în adâncime (depth first)
57
Tehnica Backtracking are la bază un principiu extrem de simplu:
k<-1
Cât timp (k>0) //k creşte şi scade alternativ până la 0
Dacă (k>n) //adevărat în cazul în care avem o soluţie
//completă
Afiseaza_solutia();
k<-k-1; //pentru a permite căutarea unei noi
//soluţii
Altfel
Dacă mai există valori candidat pentru x[k]
Atribuie următoarea valoare candidat;
Dacă x[k] este acceptabil
k<-k+1; //se trece la următorul element
Sf-dacă
Altfel
x[k]=0; //x[k] nu este acceptabil
k<-k-1; //se încearcă găsirea unei noi valori
//pentru x[k-1]
Sf-dacă
Sf-dacă
58
Sf-cat timp
Observaţii:
59
adâncime, pentru e-nodul curent se generează toţi descendenţii şi se trec în lista de
noduri vii sau moarte (în funcţie dacă au sau nu descendenţi). Se trece la următorul e-
nod, care se alege din lista nodurilor vii. Strategia breadth first organizează lista
nodurilor vii ca o coadă (în timp ce în cazul metodei backtracking, care realizează
parcurgerea în adâncime, această listă este organizată ca o stivă).
Un exemplu pentru strategia de parcurgere breadth-first este prezentat în figura
următoare:
1
2 3 4
5 6 7 8
9 10 11
60
s1∈T(s0), s2∈T(s1), … sn∈T(sn-1).
Distanţa de la o stare si∈S la starea finală sf este în general greu de calculat. De
cele mai multe ori se poate determina doar după ce am rezolvat problema în întregime.
Din această cauză va trebui căutată o aproximantă a acesteia, care să poată fi calculată
pentru problema concretă dată.
Criteriul de alegere a unei stări din mulţimea stărilor active Sa constă în
determinarea stării sc care realizează minimul sumei distanţei d1 şi d2:
d1(sc)+d2(sc) =min{d1(s)+d2(s) | s∈Sa}
Strategia Branch and Bound este următoarea: iniţial mulţimea stărilor active Sa
se iniţializează cu {s0} iar pentru o mulţime de stări active obţinută la un moment dat se
alege o stare sc de continuare (stare curentă) iar în locul ei se depune mulţimea stărilor
ce se ramifică din ea Sa=Sa-{sc}∪T(sc). Dacă printr-o astfel de transformare (ramificare)
am ajuns la o stare finală sf∈T(sc) atunci problema e rezolvată.
Problema care se pune în cazul metodei Branch and Bound este de a ne asigura
la început că problema are soluţie, în caz contrar putând ajunge la un ciclu infinit.
Metoda Branck and Bound poate fi exprimată în pseudocod astfel:
Function Branch_and_Bound
Citeşte s0; //citim starea initială
Sa={s0}; //iniţializăm mulţimea stărilor active
Repetă
sc=Alege(Sa); //starea care realizează min (d1+d2)
Sa=Sa-{sc}; //se extrage starea aleasă din mulţimea stărilor active
St=T(sc); //se ramifică starea curentă
Pentru fiecare s∈St execută
Pred(s)=sc; //reţine calea de întoarcere pentru tipărirea soluţiei
Sf-pentru
Sa=Sa∪St; //depunem în Sa mulţimea stărilor ce se ramifică din
//starea curentă
Până când Sa=∅ sau St conţine o stare finală
Dacă Sa=∅ atunci tipareşte_mesaj(“Problemă fără solutie”);
Altfel sk=sf; //pornim de la starea finală
Repetă
Tipăreşte sk; //tipărim starea curentă
sk=Pred(sk); //continuăm cu predecesorul lui sk
Până când sk=s0; //până ajungem la starea iniţială s0
Tipăreşte(sk); //tipărim şi starea iniţială
Sf-dacă
61
până când dimensiunea acestora devine suficient de mică pentru a fi rezolvate în mod
direct (cazul de bază, probleme care admit o rezolvare imediată). După rezolvarea
subproblemelor se execută faza de combinare a rezultatelor în vederea rezolvării întregii
probleme .
Metoda Divide Et Impera se poate aplica în rezolvarea unei probleme care
îndeplineşte următoarele condiţii:
4se poate descompune în două sau mai multe subprobleme ;
4aceste suprobleme sunt independente una faţă de alta (o subproblemă nu se
rezolvă pe baza alteia şi nu foloseşte rezultatele celeilalte) ;
4aceste subprobleme sunt similare cu problema iniţială;
4la rândul lor subproblemele se pot descompune (dacă este necesar) în alte
subprobleme mai simple;
4aceste subprobleme simple se pot soluţiona imediat prin algoritmul simplificat.
Deoarece puţine probleme îndeplinesc condiţiile de mai sus, aplicarea metodei
este destul de rară. Complexitatea algoritmilor bazaţi pe metoda Divide et Impera este în
general bună (de multe ori pătratică, chiar n * log(n) sau logaritmică).
Etapele rezolvării unei probleme în cadrul acestei metode sunt :
• descompunerea problemei iniţiale în subprobleme independente, similare
problemei de bază, de dimensiuni mai mici;
• descompunerea treptată a subproblemelor în alte subprobleme din ce în ce mai
simple, până când se pot rezolva imediat, prin algoritmul simplificat;
• rezolvarea subproblemelor simple;
• combinarea soluţiilor găsite pentru construirea soluţiilor subproblemelor de
dimensiuni din ce în ce mai mari ;
• combinarea ultimelor soluţii determină obţinerea soluţiei problemei iniţiale.
Întrucât metoda este în esenţă recursivă, ea se implementează cel mai natural
folosind subprograme recursive. Dacă dorim totuşi să facem o implementare iterativă,
va trebui sa folosim în program o stivă în care să încărcăm subproblemele apărute şi
nerezolvate, pe care să o gestionăm explicit.
procedure rezolva(x:problema);
begin
if {x e divizibil in subprobleme} then
begin
{divide pe x in parti x1,...,xk}
rezolva(x1); {...} rezolva(xk);
{combina solutiile partiale intr-o}
{solutie pentru x}
end
else {rezolva pe x direct}
end;
62
Metoda Greedy
63
o funcţie obiectiv care dă valoarea unei soluţii (timpul necesar executarii tuturor
lucrarilor intr-o anumita ordine, lungimea drumului pe care l-am gasit etc); aceasta este
funcţia pe care urmărim să o optimizăm (minimizăm/maximizăm).
function greedy(C)
{C este mulţimea candidaţilor}
S←Ø {S este mulţimea în care construim soluţia}
while not solutie(S) and C ≠Ø do
x ← un element din C care maximizează/minimizează select(x)
C ← C \ {x}
if fezabil(S U {x}) then S ← S U {x}
if solutie(S) then return S
else return “nu există soluţie”
Metode euristice
64
fi îmbunătăţite. Pentru unele probleme practice aceste soluţii pot fi considerate soluţii
aproximative, care pot fi de multe ori mai mult decât satisfăcătoare.
Un algoritm euristic este deci un algoritm care furnizează o soluţie
aproximativă, nu neapărat optimă, care poate fi implementată uşor şi dă rezultate în
timp util, de ordin polinomial.
În practică, metodele euristice sunt de multe ori eficiente, fiind considerate
satisfăcătoare pentru beneficiar dacă diferenţele între soluţia obţinută şi cea optimă este
acceptabilă.
Un exemplu pentru acest tip de abordare îl constituie problema comis-
voiajorului. Fiind date n localităţi şi distanţele dintre ele, se cere să se determine traseul
de lungime minimă pe care comis-voiajorul trebuie să-l parcurgă astfel încât să viziteze
toate localităţile şi să se întoarcă în localitatea din care a plecat, trecând doar o singură
dată prin fiecare localitate.
O rezolvare euristică a acestei probleme foloseşte următoarea strategie: vom
alege întotdeauna ce mai apropiată localitate (faţă de cea în care ne aflăm la un moment
dat) dintre cele nevizitate. Un traseu fiind un circuit închis, putem considera ca punct de
pornire oricare din localităţile date. Algoritmul va alege, pe rând, diverse localităţi de
start, va determia traseul corespunzător după strategia de mai sus iar în final va alege
traseul de lungime minimă. Această metodă euristică nu ne garantează alegerea optimă,
dar din mai multe trasee acceptabile, îl vom alege pe cel mai bun (scurt).
Programare dinamică
I ->Dacă şirul D1, ..., Dn duce sistemul în mod optim din S0 în Sn, atunci pentru orice 1
<= k <= n, şirul Dk, ..., Dn duce sistemul în mod optim din Sk-1 în Sn.
II->Dacă şirul D1, ..., Dn duce sistemul în mod optim din S0 în Sn, atunci pentru orice 1
<= k <= n, şirul D1, ..., Dk duce sistemul în mod optim din S0 în Sk.
III->Dacă şirul D1, ..., Dn duce sistemul în mod optim din S0 în Sn, atunci pentru orice 1
<= k <= n, şirul D1, ..., Dk duce sistemul in mod optim din S0 în Sk, iar şirul Dk+1, ..., Dn
duce sistemul în mod optim din Sk în Sn (pentru k<n).
S0, ..., Sn sunt nişte stări oarecare din mulţimea stărilor posibile, iar cu Di sistemul trece
din Si-1 în Si. Oricare din principiile de optimalitate de mai sus exprimă faptul ca
optimul total implică optimul parţial. Evident, optimul parţial nu implică neapărat
optimul total. De exemplu, într-un graf fie Dxy drumul cel mai scurt între x şi y iar Dyz
drumul cel mai scurt între y şi z. Nu putem trage concluzia că drumul cel mai scurt între
x şi z este format din Dxy şi Dyz; acestea reprezintă fiecare câte un optim parţial dar acest
drum nu este un optim total.
65
Deci oricare din principiile de optimalitate afirmă doar ca optimul total poate fi
găsit printre optimele parţiale, nu indică însă şi care din ele este; putem căuta optimul
total doar printre optimele parţiale, ceea ce reduce considerabil căutarea.
Modul de cautare a optimului total printre optimele parţiale depinde forma în
care este îndeplinit principiul de optimalitate şi se face pe baza unor relaţii de recurenţă
deduse din structura problemei şi anume:
-dacă este îndeplinit în forma I, spunem că se aplica metoda înainte; în acest caz, pe
baza unor relaţii de recurenţă se calculează optimurile de la stările mai depărtate de final
în funcţie de optimurile de la stările mai apropiate de final şi se determină deciziile ce
leagă aceste optime între ele (se merge deci de la sfârşit către început); în final se
găseşte optimul total, apoi se determină şirul de decizii care îl realizează compunând
deciziile calculate anterior mergând de la început către sfârşit.
-dacă este îndeplinit în forma II, spunem că se aplică metoda înapoi; în acest caz
calculele recurente se fac de la început spre sfârşit, iar in final se află optimul total şi se
determină şirul de decizii care îl realizează compunând deciziile de la sfârşit către
început.
-dacă este îndeplinit în forma III, spunem că se aplica metoda mixtă; în acest caz pe
baza unor relaţii de recurenţă se calculează optimurile între stările mai îndepărtate între
ele în funcţie de cele între stări mai apropiate între ele şi se determină deciziile care
interconectează aceste optimuri; în final se află optimul total, apoi se determină şirul de
decizii care îl realizeaza mergând de la capete spre interior (se determină prima şi ultima
decizie, apoi o decizie intermediară, apoi câte o decizie intermediară între cele două
perechi succesive, etc.)
66
MODULUL NUMĂRUL 4
STRUCTURI DE DATE
67
O altă clasificare după tipul de acces la elementele structurii, le grupează pe
acestea în 2 categorii:
o cu acces direct –La o structură cu acces direct putem referi o anumită
componentă a structurii fără a ţine cont de restul componentelor. O astfel de
structură este tabloul, care permite referirea la un anumit element utilizând un
indice care precizează poziţia elementului în cadrul tabloului.
o cu acces secvenţial –În cadrul unei structuri cu acces secvenţial, accesul la o
anumită componentă se face prin traversarea unor componente din acea
structură. Un astfel de acces se întâlneşte în cadrul structurilor dinamice
înlănţuite, în cadrul cărora căutarea unui anumit element se face utilizând un
pointer care va indica pe rând diverse elemente din cadrul structurii până la
identificarea elementului căutat (în cazul unei proceduri de căutare).
În cazul structurilor ordonate există o anumite ordine a poziţiilor elementelor
colecţiei, prin care succesorul unui element din colecţie se găseşte pe poziţia imediat
următoare. Sistemul de relaţie între elementele unei astfel de structuri are deci la bază
arhitectura poziţiilor, fiind astfel exterior elementelor colecţiei referite de structură.
Astfel, relaţiile între elementele colecţiei sunt implicite iar aceste structuri se mai
numesc şi structuri implicite. Implementarea unei astfel de structuri presupune alocarea
în prealabil a unor locaţii succesive de memorie, suficient de mare încât să satisfacă
cerinţele potenţiale în ceea ce priveşte numărul de elemente ale structurii, în care se vor
putea încărca şi prelucra elemente. Faptul că zona de memorie alocată este de o
dimensiune nemodificabilă constituie un handicap şi limitează potenţialul unei astfel de
structuri.
Structurile înlănţuite (explicite) sunt caracterizate prin includerea informaţiilor
privind poziţia elementului succesor şi/sau predecesor în compoziţia fiecărui element al
structurii. Succesorul şi/sau predecesorul unui element este identificat prin intermediul
informaţiilor de înlănţuire conţinute de elementul respectiv. Astfel, sistemul de relaţii al
elementelor este înregistrat în structura acestora deci este explicit. Un tablou este o
structură implicită în timp de o listă liniară simplu înlănţuită este o structură explicită.
Dacă structurile sunt create pentru a fi utilizate în memoria internă ele se numesc
structuri interne. Acestea au o durată determinată de viaţă, ele dispar fie odată cu
oprirea programului, fie sunt create şi distruse în anumite momente ale execuţiei
programului. Structurile create pentru a fi reprezentate pe un suport extern de memorare
sunt denumite structuri externe (exemplu: fişierele de date). Structurile externe au
caracter permanent, de lungă durată.
Asupra unei structuri de date se pot executa mai multe operaţii:
crearea –reprezintă operaţia de memorare pe suportul de reprezentare (intern sau
extern) a unor elemente ce vor constitui starea iniţială a structurii de date
actualizarea –este o operaţie care permite: adăugarea unor noi elemente la o
structură deja creată; modificarea valorilor unor componente ale structurii;
ştergerea (suprimarea) unor elemente componente ale structurii
consultarea –permite accesul la valorile elementelor structurii pentru prelucrarea
valorii acestora
sortarea –permite aranjarea elementelor structurii după nişte criterii stabilite, pe
baza unui criteriu de ordonare, în funcţie de valoarea sau valorile unor chei,
componente ale elementelor structurii
ventilarea –reprezintă operaţia de descompunere (desfacere) a unei structuri de
date în două sau mai multe structuri (componente)
68
fuzionarea –este operaţia de combinare a elementelor a două sau mai multe
structuri într-una singură
Alte operaţii definite pe colecţia referită de o structură derivă din caracterul
static sau dinamic al acesteia. Astfel, structurile pot fi clasificate şi după posibilitatea ca
operaţiile să îi afecteze sau nu structura. Corespunzător caracterului static sau dinamic
al colecţiei de date avem două tipuri de structuri: statice şi dinamice. Pentru fiecare tip
de structură operaţiile sunt specifice.
Structurile statice sunt colecţii finite, nemodificabile în timp. Acestea au pe tot
parcursul existentei lor acelaşi număr de componente şi în aceeaşi ordine. Sistemul de
relaţii are un rol secundar în determinarea operaţiilor definite pe structurile statice.
Acestea sunt dominant dependente de compoziţia colecţiei referită de structură.
Structurile dinamice sunt colecţii potenţial infinite, modificabile în timp
(numărul de elemente depinzând de spaţiul de memorie disponibil în care se poate aloca
spaţiu pentru elementele structurii). Structurile dinamice îşi pot modifica structura, în
sensul modificării numărului şi poziţiei componentelor. Operaţiile caracteristice
structurilor dinamice sunt dominant depedendente de sistemul de relaţii definit peste
colecţie. Pentru structurile dinamice, specifice sunt următoarele operaţii:
crearea colecţiei
explorarea (traversarea) colecţiei în scopul determinării elementelor care
compun colecţia sau al determinării apartenenţei sau neapartenenţei unui
element oarecare la colecţie
inserarea, modificarea sau ştergerea unui anumit element din colecţie
identificarea succesorului sau al predecesorului unui element conform cu
sistemul de relaţii definit peste colecţie
4.2. Fişierul
Fişierul este o structură de date externă fiind o colecţie ordonată de date aflată
pe un suport extern de memorare. Din punct de vedere logic, componentele unui fişier
se numesc articole. Zona de memorie utilizată pentru înregistrarea unui articol se
numeşte înregistrare.
Un articol este alcătuit din mai multe câmpuri.
În funcţie de lungimea unui articol (mărime exprimată în octeţi, egală cu suma
lungimilor componentelor unui articol, adică a câmpurilor) avem:
o fişiere cu articole de lungime fixă
o fişiere cu articole de lungime variabilă
Evidenţierea articolului curent se face cu ajutorul unui pointer de fişier. Acest
pointer indică în permanenţă articolul curent dintr-un anumit fişier. La deschiderea unui
fişier acest pointer se poziţionează automat pe primul articol al fişierului. Prin anumite
comenzi de pozitionare, putem modifica valoarea acestui indicator de înregistrare, fie
trecând la următorul articol, fie prin poziţionarea directă pe un articol cu un anumit
indicativ. Sfârşitul fişierului este evidenţiat printr-o marcă specială, denumită EOF (End
of File), care indică faptul că am depăşit ultimul articol şi practic ne găsim la sfârşitul
fişierului.
Un fişier se caracterizează printr-o anumită metodă de organizare. Prin modul
de organizare se înţelege o serie de reguli după care se memorează informaţia pe
suportul extern. Alegerea unui anumit mod de organizare va influenţa şi modalitatea de
acces la articolele din cadrul fişierului.
69
Există două modalităţi de acces în cazul fişierelor:
• accesul secvenţial –reprezintă modalitatea de acces la articolele fişierului printr-
o parcurgere secvenţială, articol după articol până la regăsirea articolului dorit
• accesul direct presupune regăsirea articolului dorit direct, pe baza adresei
respectivului articol, neţinând astfel seama de restul articolelor din cadrul
fişierului
După modul de organizare al fişierelor distingem:
fişiere secvenţiale –înregistrarea articolelor din fişier de face în locaţii
consecutive. Organizarea secvenţială impune o singură metodă posibilă
de acces la înregistrări şi anume accesul secvenţial.
fişiere relative –organizarea relativă impune înregistrarea articolelor în
funcţie de valoarea unei chei, având la bază valori ale unui/unor câmpuri
din respectivul articol. Într-o astfel de organizare, informaţiile sunt
organizate în celule de aceeaşi lungime, numărul căsuţei indicând poziţia
relativă a căsuţei în cadrul fişierului. Pentru a înregistra un articol într-un
fişier cu organizare relativă, se va calcula valoarea cheii, care va decide
locaţia de memorie în care va fi salvat respectivul articol. Căutarea unui
anumit articol se poate face folosind fie un acces secvenţial (ineficient
pentru o organizare a fişierului de acest tip) sau un acces direct aflând pe
baza cheii adresa de memorie la care este memorat articolul având o
cheie dată.
fişiere indexate –în cazul acestui tip de organizare, articolele sunt
înregistrate secvenţial, în locaţii consecutive de memorie. Paralel cu
fişierul care conţine articolele propriu-zise, este gestionat şi unul sau mai
multe fişiere index. Aceste fişiere index se crează pe baza unor chei de
indexare. Accesul la un articol dint-un fişier indexat, se face prin
explorarea tabelei de indecşi până la întâlnirea indexului ce conţine o
cheie dată după care accesul va fi direct în fişier la articolul
corespunzător, pe baza adresei astfel determinate.
4.3. Tabloul
Fiind dat un număr natural k şi An={1,2,…ni} i=1,k unde Ani conţine primele ni
numere naturale, atunci tabloul este o funcţie f:An1xAn2x..Ank->T unde T este o
mulţime oarecare.
Un tablou este o colecţie omogenă de date în care fiecare element poate fi
identificat pe baza unui index, colecţia asigurând acces direct şi timp de acces constant
pentru fiecare element al ei.
Dacă k=1 atunci tabloul este unidimensional iar componentele sunt identificate
cu ajutorul unui singur indice. Dacă k=2 tabloul este o matrice şi vom avea nevoie de 2
indici pentru a identifica o componentă. Numărul maxim de dimensiuni pe care îl putem
folosi în cazul tablourilor n-dimensionale depinde de fiecare limbaj de programare în
parte.
Observaţie: în funcţie de specificul fiecărui limbaj de programare,
implementarea unui tablou va avea mulţimea de indecşi diferită astfel: limbajul Pascal
utilizează indici pentru tablouri pornind în general de la valoarea 1 (dar ei pot varia între
orice limite întregi) în timp ce în limbajul C indicii pornesc de la 0.
70
Caracteristicile acestui tip de structură sunt:
-este o structură ordonată, succesorul fiecărui element se găseşte în locaţia următoare de
memorie. Numerotarea locaţiilor de memorie permite accesarea directă a oricărui
element, folosind numerele asociate locaţiilor de memorie (indici sau indecşi).
-este o structură statică, datorită imposibilităţii modificării în timp (în cursul execuţiei
programului) a dimensiunii acesteia. Deci este necesară estimarea dimensiunii unui
tablou în timpul scrierii programului. Poziţia fiecărui element rămâne neschimbată, ceea
ce se pot modifica fiind valorile fiecărui element al tabloului.
-tabloul poate fi văzut şi ca o mulţime de perechi de forma (index, valoare) unde index
aparţine unei mulţimi a indecşilor iar valoare unei mulţimi a valorilor.
Structurarea elementelor din mulţimea T într-un tablou ne va permite să ştim de
exemplu care este primul sau ultimul element din tablou. Vom putea accesa elementul i
sau elementul de pe linia i şi coloana j a unei matrici. Acest mod de structurare este
ineficient în cazul în care numărul elementelor variază în limite largi, deoarece va trebui
să precizăm încă din faza de compilare a programului numărul maxim de elemente ale
tabloului.
Ordinea de memorare a componentelor este dată de ordinea lexicografică
definită peste indici. În cazul unui tablou bidimensional (matrice), în general
reprezentarea în memorie se face pe linii (elementele primei linii urmate apoi de
elementele celei de a doua linii ş.a.m.d.), după cum o matrice poate fi văzută ca un
vector de linii.
71
numesc şi coliziuni pentru că mai multe elemente îşi dispută aceeaşi poziţie în tabela de
dispersie.
Asupra funcţiei f se impun două condiţii:
-valoarea ei pentru un număr k∈K să rezulte cât mai simplu şi rapid
-să minimizeze coliziunile
a) Calcularea unei noi adrese în acelaşi vector pentru sinonimele care găsesc
ocupată poziţia rezultată din calcul. În acest caz fie se caută prima poziţie liberă,
fie se aplică o a doua metodă de dispersie.
b) Metode care plasează coliziunile în afara vectorului principal. Se pot folosi fie
un vector auxiliar, fie liste înlănţuite de sinonime care pleacă din poziţia
rezultată din calcul pentru fiecare grup de sinonime. Vor exista astfel mai multe
liste, fiecare conţinând înregistrări cu aceleaşi cod de dispersie. Aceste metode
asigură un timp mai bun de regăsire dar folosesc mai multă memorie. Există
posibilitatea de extindere nelimitată dar performanţele de căutare ale unui
anumit element se vor degrada semnificativ.
1 .
2 .
Lista de depasire pentru elementul 1
3 .
4 .
. Lista de depasire pentru elementul 3
.
.
n .
O listă liniară (numită şi listă înlănţuită -”Linked List”) este o colecţie de n>=0
elemente x[1], … x[n] toate de un tip oarecare, numite noduri între care există o relaţie
de ordine determinată de poziţia lor relativă. Ea este deci o mulţime eşalonată de
elemente de acelaşi tip având un număr arbitrar de elemente. Numărul n al nodurilor se
numeşte lungimea listei. Dacă n=0, lista este vidă. Dacă n>=1, x[1] este primul nod iar
x[n] este ultimul nod. Pentru 1<k<n, x[k] este precedat de x[k-1] şi urmat de x[k+1].
Acest tip de structură de date se aseamănă cu o structură standard: tipul tablou
cu o singură dimensiune (vector), ambele structuri conţinând elemente de acelaşi tip iar
72
între elemente se poate stabili o relaţie de ordine. Una dintre deosebiri constă în
numărul variabil de elemente care constituie lista liniară, dimensiunea acesteia nu
trebuie declarată şi deci cunoscută anticipat (în timpul compilării) ci se poate modifica
dinamic în timpul execuţiei programului, în funcţie de necesităţi. Astfel utilizatorul nu
trebuie să fie preocupat de posibilitatea depăşirii unei dimensiuni estimate iniţial,
singura limită fiind mărimea zonei heap din care se solicită memorie pentru noile
elemente ale listei liniare. Un vector ocupă în memorie un spaţiu continuu de memorie,
pe când elementele unei liste simplu înlănţuite se pot găsi la adrese nu neapărat
consecutive de memorie.
O altă deosebire avantajează vectorii, deoarece referirea unui element se face
prin specificarea numărului de ordine al respectivului element, pe când accesul la
elementele unei liste liniare se face secvenţial, pornind de la capul listei (adresa
primului nod al listei) până la ultimul element al ei, ceea ce măreşte uneori considerabil
timpul de acces la un anumit element. Pentru o listă liniară este obligatoriu să existe o
variabilă, declarată în timpul compilării, denumită cap de listă care să păstreze adresa
primului element al listei. Aceasta va fi fie o variabilă globală fie va fi transmisă ca
parametru fiecărei funcţii care doreşte să exploateze lista. Pierderea acestei valori va
duce la imposibilitatea accesării elementelor listei liniare.
Pentru implementarea dinamică a unei liste liniare (folosind pointeri), nodurile
listei vor fi structuri ce conţin două tipuri de informaţie:
o câmpurile ce conţin informaţia structurală a nodului, numite într-un cuvânt
INFO
o câmpurile ce conţin informaţia de legătură, numite LINK ce vor conţine pointeri
la nodurile listei. Înlănţuirea secvenţială a elementelor unei liste se face utilizând
variabile de tip pointer, care specifică adresa de memorie a elementelor
adiacente. Fiecare nod are un predecesor şi un succesor (mai puţin elementele
prim şi ultim dacă lista nu este circulară).
Listele înlănţuite cu un singur câmp de legătură se numesc liste simplu înlănţuite
(legătura indică următorul element din listă) iar cele cu 2 câmpuri de legătură liste dublu
înlănţuite (o legătură indică nodul precedent iar cealaltă nodul succesor).
În cazul listelor simplu înlănţuite, fiecare element al listei conţine adresa
următorului element al listei. Ultimul element poate conţine ca adresă de legătură fie
constanta NULL (sau constanta 0 care nu indică nici un alt element), fie adresa primului
element al listei, în cazul listelor liniare circulare.
Cap lista
. . . ... 0
73
typedef struct LISTA {
int cheie; //informaţia propriu-zisă
// alte informaţii
struct LISTA *urm; //informaţia de legătură, pointer spre următorul
//element al listei
} lista;
Lista circulară simplu înlănţuită este tot o listă liniară simplu înlănţuită cu
deosebirea că nu există ultimul element, adică pointerul de legătură al ultimului nod al
listei nu indică valoarea NULL ci indică spre primul element al listei.
Listele dublu înlănţuite sunt tot liste liniare, cu deosebirea că fiecare element al
listei are două referinţe: una spre elementul precedent şi una spre elementul succesor.
În C, definirea unei liste dublu înlănţuite se poate face astfel:
74
Cap lista
NU NU
. . . . . ... .
LL LL
O stivă (în engleză stack) este o structură de tip LIFO (Last In First Out =ultimul
intrat primul ieşit) şi este un caz particular al listei liniare în care toate inserările
(depunerile –în engleză push) şi ştergerile (sau extragerile -în engleză pop) (în general
orice acces) sunt făcute la unul din capetele listei, numit vârful stivei. Acest nod poate fi
citit, poate fi şters sau în faţa lui se poate insera un nou nod care devine cap de stivă.
push pop
75
În abordarea statică, stiva poate fi organizată pe un spaţiu de memorare de tip
tablou unidimensional.
În abordarea dinamică, implementarea unei stive se poate face folosind o
structură de tip listă liniară simplu înlănţuită în care inserarea se va face tot timpul în
capul listei iar ştergerea de asemenea se va face în capul listei.
O coadă (în engleză queue) este o structură de tip FIFO (First In First Out =
Primul venit primul servit), în care toate inserările se fac la un capăt al ei (numit capul
cozii) iar ştergerile (extragerile) (în general orice acces) se fac la celălalt capăt (numit
sfârşitul cozii). În cazul cozii, avem nevoie de doi pointeri, unul către primul element al
cozii (capul cozii), iar altul către ultimul său element (sfârşitul cozii). Există şi o
variantă de coadă circulară, în care elementele sunt legate în cerc, iar cei doi pointeri,
indicând capul şi sfârşitul cozii, sunt undeva pe acest cerc.
push pop
76
-cea de a doua, în care adăugarea se face la sfârşitul listei iar extragerea se face din
capul listei.
Definiţii:
7
4
3
2
6
5
1 8
7
4
3
77
Un graf parţial al unui graf se obţine păstrând aceeaşi mulţime de vârfuri şi
eliminând o parte din muchii.
Un subgraf al unui graf G=(X,U) este un graf H=(Y,V) astfel încât Y⊂X iar V
conţine toate muchiile din U care au ambele extremităţi în Y.
2
6
5
1 8
3
Fig.4.8. Subgraf al grafului G
Gradul unui vârf este numărul muchiilor incidente cu acel vârf (sau, altfel spus,
numărul de noduri adiacente cu acesta). Gradul unui vârf x se notează cu d(x).
Un vârf care are gradul 0 se numeşte vârf izolat. Un vârf care are gradul 1 se
numeşte vârf terminal.
În cazul grafului G, vârful 8 este vârf izolat iar vârfurile 6 şi 7 sunt izolate.
78
Un graf este conex dacă pentru orice pereche de noduri există un lanţ care le
uneşte.
O componentă conexă a unui graf este un subgraf al grafului de referinţă,
maximal în raport cu proprietatea de conexitate (între oricare 2 vârfuri există lanţ şi nu
există un alt subgraf al grafului de referinţă care să îndeplinească proprietatea de mai
sus şi să conţină acest subgraf).
Un graf este conex dacă admite o singură componentă conexă.
Astfel L[0] este un pointer spre structura l_a ce conţine lista vecinilor pentru primul
nod, L[1] pentru al doilea nod etc..
Vectorul muchiilor
struct muchie
{ int nodi, nodf;}
struct muchie MUCHII[20];
79
Parcurgerea grafurilor neorientate:
80
n-1 arce. În concluzie, dacă există vreun drum de la xi la xj atunci există şi un drum
elementar şi, deci, va exista o putere a lui A, între A1 şi An-1, în care poziţia (i,j) este
diferită de 0. Pentru deciderea existenţei unui drum între oricare două noduri este
suficientă, deci, calcularea doar a primelor n-1 puteri ale lui A.
Grafuri ponderate:
Într-un graf neorientat, fiecărei muchii i se poate asocia o pondere (un cost).
Putem considera un graf neorientat fără ponderi ca fiind o particularizare a unui graf cu
costuri, fiecare muchie având costul 1. Această pondere (sau cost) va exprima
intensitatea relaţiei dintre 2 noduri ale grafului.
Reprezentarea unui astfel de graf G=(X,U) se face utilizând o matrice a
ponderilor P(n,n) a cărei elemente se definesc astfel:
Utilizarea grafurilor ponderate este des întâlnită în practică. Cel mai sugestiv
exemplu este cel al unei reţele de oraşe, nodurile grafului reprezentând oraşele, muchiile
reprezentând legăturile (şoselele) dintre oraşe iar costul lor este dat de distanţa dintre
oraşe.
Grafuri hamiltoniene:
Un lanţ elementar care conţine toate nodurile unui graf se numeşte lanţ
hamiltonian.
Un ciclu elementar care conţine toate vârfurile grafului se numeşte ciclu
hamiltonian.
Graful care conţine un ciclu hamiltonian se numeşte graf hamiltonian.
Grafuri euleriene:
Un lanţ simplu care conţine toate muchiile unui graf este un lanţ eulerian.
Un ciclu eulerian este un ciclu care conţine toate muchiile grafului.
Un graf eulerian este un graf care conţine un ciclu eulerian.
Condiţie necesară şi suficientă: Un graf este eulerian dacă şi numai dacă oricare vârf al
său are gradul par.
81
2
6
5
1 8
7
4
3
Gradul exterior al unui vârf x, notat d*(x), reprezintă numărul arcelor care ies
din nodul x, adică numărul arcelor de forma (x,z) ε U.
Gradul interior al unui vârf x, notat d-(x), reprezintă numărul arcelor care intră
în nodul x, adică numărul arcelor de forma (y,x) ε U.
Gradul total al unui vârf x, dintr-un graf orientat, este un număr natural ce
reprezintă suma gradelor interior şi exterior şi este egal cu numărul arcelor incidente cu
vârful.
În cazul unui graf orientat, un nod x este izolat dacă d*(x)= d-(x)=0.
Se definesc mulţimile:
Γ ( x) = {y ∈X (x, y ) ∈U }reprezintă mulţimea nodurilor ce constituie extremităţi finale
+
ale arcelor care pleacă din nodul x (mulţimea succesorilor lui x).
82
Γ − ( x ) = {y ∈ X ( y, x ) ∈ U }reprezintă mulţimea nodurilor ce constituie extremităţi iniţiale
ale arcelor care intră în nodul x (mulţimea predecesorilor lui x).
Se numeşte drum în graful G, un şir de noduri D={x1, x2, x3, …, xk}, unde x1, x2,
x3, …, xk ∈ x, cu proprietatea că oricare două noduri consecutive sunt adiacente, adică
există arcele [x1, x2], [x2, x3], …, [xk-1,xk] care aparţin lui U.
Deosebirea unui drum faţă de un lanţ constă în faptul că de-a lungul unui arc ne
putem deplasa numai în sensul dat de orientarea arcului.
Un graf este tare conex dacă între oricare două noduri există cel puţin un drum.
Un graf este simplu conex dacă între oricare două noduri există cel puţin un lanţ.
Observaţie: Pentru grafuri neorientate noţiunile de tare conex şi simplu conex sunt
echivalente, graful numindu-se doar conex
O componentă tare conexă a unui graf G = (X,U) este un subgraf al lui G care
este tare conex şi nu este subgraful nici unui alt subgraf tare conex al lui G (altfel spus,
între oricare două noduri din componentă există cel putţn un drum şi nu mai există nici
un nod în afara componentei legat printr-un drum de un nod al componentei).
83
1 2 3 4 5 6 7 8
1 1 1 1 0 0 0 0 0
2 0 0 0 1 1 0 0 0
3 0 0 0 1 0 0 0 0
4 1 0 0 0 1 0 0 0
5 0 0 0 0 0 0 0 0
6 0 0 0 0 0 0 1 0
7 0 0 0 0 0 0 0 0
8 0 0 0 0 0 0 0 0
Grafurile orientate se pot reprezentra şi cu ajutorul matricei de incidenţă B
care se defineşte astfel:
b(i,j) = 1, dacă arcul [xi,xj] are extremitatea iniţială în xi
b(i,j) = -1, dacă arcul [xi,xj] are extremitatea finală în xi
b(i,j) = 0, în rest.
Exemplu:
struct arc
{ int nodi, nodf;}
struct arc ARCE[50];
84
typedef struct NODG {
int cheie;
… informaţii
struct NODL *inc_lista, *sfarsit_lista; //începutul şi sfârşitul listei de adiacenţă
struct NODG *urmg; //pointer spre următorul nod al grafului
} nodg;
Una din problemele care apar foarte des în practică este găsirea drumului optim
între două noduri ale unui graf.
Formalizarea problemei drumului optim este următoarea:
"Fiind dat un graf G = (X,U) şi o funcţie care asociază fiecărui arc o valoare reală, să se
găsească, pentru o pereche dată de noduri, drumul (drumurile) de valoare optimă
(minimă sau/şi maximă) între cele două noduri şi valoarea acestuia (acestora)".
Valoarea unui drum este dată de suma valorilor arcelor care îl compun. Pentru
problema drumului optim s-au elaborat mai multe categorii de algoritmi, după cum
urmează:
1. Algoritmi prin calcul matricial (Bellman-Kalaba, I. Tomescu, Bellman-Schimbell);
2. Algoritmi prin ajustări succesive (Ford);
3. Algoritmi prin inducţie (Dantzig);
4. Algoritmi prin ordonare prealabilă a vârfurilor grafului;
5. Algoritmi prin extindere selectivă (Dijkstra).
4.12. Arbori
85
noduri dată, graful cu numărul maxim de arce astfel încât să se păstreze
proprietatea că nu are cicluri.
Predecesorul (părintele) unui nod se defineşte astfel: Un nod x din care porneşte
un arc către nodul y se spune că este predecesorul nodului y.
Succesorul (descendentul sau fiul) unui nod se defineşte astfel: Un nod y către
care există o legătură directă din nodul x se spune că este succesorul nodului x.
Ordinea legăturilor spre fii este importantă şi de aceea vorbim de fiul stâng şi
fiul drept al unui nod.
Într-un arbore, fiecare nod poate avea mai mulţi succesori, dar un singur
predecesor, cu excepţia unui singur nod (nodul rădăcină) care nu are nici un predecesor
(în nodul rădăcină nu intră nici un arc). Cu excepţia rădăcinii, în fiecare nod al unui
arbore intră un singur arc. Orice arbore cu n noduri are n-1 arce.
Un arbore poate fi privit şi ca o extindere a unei liste liniare. Astfel, un arbore în
care fiecare nod are un singur succesor, pe aceeaşi parte, este de fapt o listă liniară.
Structura de arbore este o structură ierarhică, cu noduri aşezate pe diverse
niveluri, cu relaţii de tip părinte-fiu între noduri. Fiecare nod are o anumită înălţime faţă
de rădăcină; toţi succesorii unui nod se află pe acelaşi nivel şi au aceeaşi înălţime.
Rădăcina arborelui se află pe nivelul 0, descendenţii direcţi ai acesteia pe nivelul 1,
descendenţii direcţi ai unui nod de pe nivelul i se află pe nivelul i+1.
Nodurile fii ale aceluiaşi nod se numesc noduri gemene (engleză siblings).
Adâncimea unui nod reprezintă lungimea drumului dintre rădăcină şi acest nod.
Înălţimea unui nod este lungimea celui mai lung drum dintre acest nod şi un nod
terminal.
Înălţimea arborelui este înălţimea rădăcinii.
Nivelul unui nod este înălţimea arborelui, minus adâncimea acestui nod.
O frunză (numită şi nod terminal) este un nod care nu are nici un fiu.
Gradul unui nod reprezintă numărul de descendenţi din nod. Dacă gradul
rădăcinii este 0, arborele este format doar din nodul rădăcină.
Gradul unui arbore reprezintă maximul din mulţimea gradelor tuturor nodurilor.
Lungimea căii unui anumit nod reprezintă numărul de ramificări de la rădăcină
la acel nod. Lungimea căii nodului rădăcină este 0, lungimea căii descendenţilor direcţi
ai rădăcinii este 1, etc.
O mulţime de arbori disjuncţi formează o pădure.
86
Nr_max fii va conţine numărul maxim de fii pe care îi poate avea un nod.
Construirea unui arbore multicăi se poate face astfel:
-pentru fiecare nod se citeşte informaţia utilă şi numărul de fii (<=nr_max_fii)
-nodurile se citesc în postordine iar adresele lor pot fi păstrate într-o stivă până la
apariţia nodului al cărui fiu sunt. În acest moment, adresele fiilor se extrag din stivă
completându-se astfel adresele nodului tată (către nodurile fii). După aceasta adresa
nodului tată se depune în stivă iar algoritmul continuă până când în stivă singurul nod
rămas va fi nodul rădăcină al arborelui.
Traversarea arborilor
Arbori binari
Un arbore binar este format dintr-un nod rădăcină şi maxim 2 descendenţi care
la rândul lor sunt arbori binari.
a c
d g
e f
87
inf(i) st(i) dr(i) t(i)
1 a 0 0 2
2 b 1 3 0
3 c 4 7 2
4 d 5 6 3
5 e 0 0 4
6 f 0 0 4
7 g 0 0 3
Reprezentarea dinamică a aceluiaşi arbore binar este următoarea:
Adr
rad
b st dr
a st dr c st dr
d st dr g st dr
e st dr f st dr
88
Traversarea unui arbore binar:
Pentru arborii binari, se pot considera următoarele traversări:
o Traversare în preordine: se vizitează nodul rădăcină, apoi subarborele stâng şi
în final subarborele drept (R St Dr).
o Traversare în inordine: se vizitează subarborele stâng, apoi nodul rădăcină şi în
final subarborele drept (St R Dr)
o Traversare în postordine: se vizitează subarborele stâng, apoi subarborele drept
şi în final nodul rădăcină (St Dr R)
Pentru fiecare din cele 3 traversări, la vizitarea fiecărui subarbore se aplică din
nou aceeaşi regulă de vizitare
Un arbore binar este complet dacă fiecare nod care nu este frunză are exact doi
descendenţi (deci gradul oricărui nod este 0 sau 2).
6 12
4 7 9
2 5 10
89
Traversând în inordine arborele binar de căutare din figura de mai sus, obţinem
un şir ordonat crescător: 2 4 5 6 7 8 9 10 12.
Cheia maximă şi cheia minimă dintr-un arbore binar de căutare sunt uşor de
găsit: cheia minimă se găseşte în nodul cel mai din stânga (la care ajungem avansând
succesiv cu un pointer numai pe legături de tipul “succesor stâng”, pornind de la
rădăcină). Cheia maximă se află în nodul cel mai din dreapta.
Un arbore binar complet este un heap dacă, având definită o relaţie de ordine
asupra informaţiei conţinute în nodurile arborelui, avem (pentru fiecare nod al
arborelui): nod ≤ nod subarbore stâng şi nod ≤ nod subarbore drept (deci cheia fiecărui
nod din arbore este mai mare decât cheile descendenţilor sau altfel spus fiecare nod are
o cheie mai mică sau egală cu cea a tatălui său).
90