Sunteți pe pagina 1din 10

Structuri de Date – Lucrarea nr.

Complexitatea algoritmilor. Tablouri.

1. Complexitatea algoritmilor

1.1. Considerații teoretice


Analiza complexitații unui algoritm presupune evaluarea (estimarea) volumului de
resurse de calcul necesare pentru execuția algoritmului independent de performanțele
sistemului de calcul pe care rulează algoritmul. Resursele vizate sunt:
• timpul necesar pentru execuția tuturor prelucrărilor specificate în algoritm;
• spațiul de memorie necesar pentru stocarea datelor pe care le prelucrează algortmul.
Studierea complexităţii timp presupune analiza completă în cadrul algoritmului în unul
din următoarele cazuri:
• Cazul cel mai defavorabil (adeseori situații degenerate, de exemplu arbori binari
puternic dezechilibrați) – cel mai des studiat;
• Cazul mediu;
• Cazul cel mai favorabil – de exemplu valoarea (elementul) căutat este pe prima
poziție într-un tablou sau într-o listă;
• Cazul analizei amortizate.

Comportarea medie presupune probabilitatea de apariţie a diferitelor configuraţii de date la


intrare. Cazul cel mai defavorabil este cel mai studiat și este folosit, de obicei, pentru
compararea algoritmilor. Analiza amortizată a unui algoritm ia în calcul timpul mediu
necesar execuției, calculat pentru secvențe oarecare de operații ce acoperă cazurile cele mai
defavorabile. Această analiză este utilă dacă algoritmul nu are o comportare uniformă în
raport cu datele de intrare.

Complexitatea unui algoritm secvenţial se exprima de regulă utilizând notația O.

Definitie
Fie două funcţii f : N->N şi g : N->N. Spunem că f aparţine O(g) (are ordinul de complexitate
O(g)) şi se notează f = O(g) dacă şi numai dacă există o constantă reală c și un număr natural
n0 astfel încât pentru n> n0 => f(n) < c * g(n)

Observatie:
f : N->N este o funcţie f(n) cu n dimensiunea datelor de intrare.
f(n) reprezintă timpul de lucru al algoritmului exprimat in "pasi".

Dacă f este o funcţie polinomială de grad k atunci f = O(nk ). Funcția f și ordinul O exprimă
viteza de variaţie a funcţiei, în raport cu dimensiunea datelor de intrare.

Proprietăţi:

1) Fie f, g : N->N.
Daca f = O(g) k * f = O(g)
Structuri de Date – Lucrarea nr. 2

f = O(k * g) , k є R constant.

2) Fie f, g, h : N->N.
si: f = O(g)
g = O(h) atunci f = O(h)

3) Fie f1, f2, g1, g2 : N->N.


si: f1 = O(g1) ==> f1 + f2 = O( g1 + g2 )
f2 = O(g2) ==> f1 * f2 = O( g1 * g2 )

Aceasta proprietate permite ca, atunci când avem două bucle imbricate (de complexităţi
diferite), complexitatea totală să se obţină înmulţindu-se cele două complexităţi. Cele două
complexităţi se adună, dacă buclele sunt succesive.

Intre clasa funcţiilor logaritmice și cea a funcţiilor polinomiale exista relaţia:


O(nc ) ⸦ O(an ).

Între diferitele clase de complexitate, există următoare arelație:


O(1) ⸦ O(log n) ⸦ O(n) ⸦ O(nlog n) O(n2) ⸦ O(nk log n) ⸦ O(nk+1 ) ⸦ O(2n)

Pentru calculul complexităţii se va încerca încadrarea în clasa cea mai mică de complexitate
din acest șir:

O(1) clasa algoritmilor constanti;


O(log n) clasa algoritmilor logaritmici;
O(n) clasa algoritmilor liniari;
O(nlog n) clasa algoritmilor polilogaritmici;
O(n2 ) clasa algoritmilor patratici;
O(nklog n) clasa algoritmilor polilogaritmici;
O(nk+1) clasa algoritmilor polinomiali;
O(2n) clasa algoritmilor exponenţiali.

In tabelul de mai jos este prezentat timpul de execuție în raport cu dimensiunea problemei (n)
pentru câteva tipuri de algoritmi.

n O(log(n)) O(n) O(n*log(n)) O(n2) O(n3) O(2n)


10 1 10 10 100 1000 1024
20 1.30 20 26.02 400 8000 1048576
30 1.48 30 44.31 900 27000 1073741824
40 1.60 40 64.08 1600 64000 1.0995E+12
50 1.70 50 84.95 2500 125000 1.1259E+15
Exemple privind analiza algoritmilor

1. O buclă for
 n*O(1) = O(n)
for (i=0;i<n;i++)
{
S;
} //secvenţă de ordin O(1)
Structuri de Date – Lucrarea nr. 2

2. Două bucle for

for (i=0;i<n;i++)  n*n*O(1) = O(n2)


for (j=0;j<n;j++)
{
S;
} //secvenţă de ordin O(1)

3. Două bucle for

for (j=0;j<n;j++) n(n + 1)


for (i=0;i<j;i++) i =  O(n2)
{ 2
S;
} //secvenţă de ordin O(1)

4. Buclă while
pas 1 2 3 4...... k
h=1;
while (h<=n) h 21 22 23 24..... 2k  n
{S; //secvenţă de ordin O(1)
h=2*h; 2k  n  k  log2 n  O(log n)
}

5. Buclă while ce conţine buclă for


h=n; f1 = O( g1 (n)) 
while (h>10-6)   f1  f 2 = O( g1 (n)  g 2 (n))
{ f 2 = O( g 2 (n))
for (i=0;i<n;i++)
S; //secvenţă de ordin O(1)
h=h/2; while – O(log n); for – O(n)  O(nlog n)
}

1.2. Algoritmi de sortare

Un vector ordonat reduce timpul anumitor operaţii ca de pildă căutarea unei valori
date, verificarea unicităţii elementelor, găsirea perechii celei mai apropiate, calculul
frecvenţei de apariţie a fiecărei valori distincte etc. Ordonarea vectorilor se face atunci când
este necesar, de exemplu pentru afişarea elementelor sortate după o anumită cheie.
În general, operaţia de sortare este eficientă dacă se aplică asupra vectorilor şi de obicei nu se
aplică altor structuri de date (liste, arbori neordonaţi sau tabele de dispersie).
Există mai mulţi algoritmi de sortare cu performanţe diferite. Cei mai ineficienţi
algoritmi de sortare au o complexitate de ordinul O(n2), iar cei mai buni necesită pentru cazul
mediu un timp de ordinul O(n*log(n)).
În anumite aplicaţii, ne interesează un algoritm de sortare “stabilă”, care păstrează
ordinea iniţială a valorilor egale din vectorul sortat. Există algoritmi care nu sunt stabili.
De multe ori prezintă interes algoritmii de sortare „pe loc”, care nu necesită memorie
suplimentară. Algoritmii de sortare “pe loc” a unui vector se bazează pe compararea de
elemente din vector, urmată eventual de schimbarea între ele a elementelor comparate pentru
a respecta condiţia de inegalitate valorică faţă de cele precedente si cele care-i urmează.
În algoritmii ce urmează, se consideră sortarea crescătoare a elementelor unui vector A.
Structuri de Date – Lucrarea nr. 2

Algoritmul Bubble Sort

Sortarea prin metoda bulelor (Bubble Sort) compară mereu elemente vecine. În prima
parcurgere, se compară toate perechile vecine (de la prima către ultima) şi dacă este cazul se
schimbă elementele între ele pentru a asigura ordinea dorită. După prima parcurgere, valoarea
maximă se va afla la sfârşitul vectorului. La următoarele parcurgeri se reduce treptat
dimensiunea vectorului, prin eliminarea valorilor finale (deja sortate). Dacă se compară
perechile de elemente vecine de la ultima către prima, atunci se aduce în prima poziţie
valoarea minimă şi apoi se modifică indexul de început.

PseudoCod:

bubble_sort(A,n)
{ swapped=1;
j=0;
while(swapped)
{
swapped=0;
j = j+1
for i = 1 to n-j do
if (A[i]>A[i+1]
{
tmp = A[i];
A[i] = A[i + 1];
A[i + 1] = tmp;
swapped = 1;
}
}
}

Timpul de sortare prin metoda bulelor este proporţional cu pătratul dimensiunii vectorului,
mai exact, complexitatea algoritmului este O(n2).

Algoritmul SelectionSort
Sortarea prin selecţie determină în mod repetat elementul minim dintre toate care urmează
unui element A[i] şi îl aduce în poziţia i, după care creşte indexul i.
void selSort( T a[ ], int n)
{ // sortare prin selectie
int i, j, m; // m = indice element minim dintre i,i+1,..n
for (i = 0; i < n-1; i++)
{ // in poz. i se aduce min (a[i+1],..[a[n])
m = i; // considera ca minim este a[i]
for (j = i+1; j < n; j++) // compara minim partial cu a[j]
//(j > i)
if ( a[j] < a[m] ) // a[m] este elementul minim
m = j;
swap(a,i,m); // se aduce minim din poz. m in pozitia i
}
}

Sortarea prin selecție are și ea complexitatea O(n2), dar în medie este mai rapidă decât
sortarea prin metoda bulelor (constanta care înmulțește pe n2 este mai mică).
Structuri de Date – Lucrarea nr. 2

Algoritmul Insertion Sort


Algoritmul INSERTION_SORT consideră că in pasul k, elementele A[1÷k-1] sunt
sortate, iar elementul k va fi inserat, astfel încât, după aceasta inserare, primele elemente
A[1÷k] sa fie sortate.
Pentru a realiza inserarea elementului k in secvenţa A[1÷k-1], aceasta presupune:
➢ memorarea elementului intr-o variabilă temporară;
➢ deplasarea tuturor elementelor din vectorul A[1÷k-1] care sunt mai mari decât A[k],
cu o poziţie la dreapta (aceasta presupune o parcurgere de la dreapta la stânga);
➢ plasarea lui A[k] in locul ultimului element deplasat.

Complexitate: O(n2)

PseudoCod:

insertion_sort(A,n)
{
for k = 2 to n do
temp = A[k]
i=k-1
while (i >= 1) and (A[i] > temp) do
A[i+1] = A[i]
i = i-1
A[i+1] = temp
}

Cazul cel mai defavorabil: situaţia in care deplasarea (la dreapta cu o poziţie in vederea
inserării) se face pana la începutul vectorului, adică şirul este ordonat descrescător.

Exprimarea timpului de lucru:


T(n) = 3 (n - 1) + 3 (1 + 2 + 3+ ... + n - 1) = 3(n-1) + 3n * (n - 1)/2
Rezulta complexitatea: T(n) = O(n2) funcţie polinomială de gradul II.

Observatie: Când avem mai multe bucle imbricate, termenii buclei celei mai interioare dau
gradul polinomului egal cu gradul algoritmului.

Bucla cea mai interioara ne dă complexitatea algoritmului.

1.3. Algoritmi de căutare

Căutarea Brute-Force

Fie A, de ordin n, un vector cu elemente oarecare. Se cere sa se determine daca o valoare b se


afla printre elementele vectorului.

Cautarea Brute-Force presupune parcurgerea elementelor vectorului si compararea cu


valoarea cautata.

PseudoCod:
Structuri de Date – Lucrarea nr. 2

BruteForce_search(A,n,b)
{
i = 1
while i <= n do
if A[i] = b
return(1)
i = i+1
return(0)
}
Calculul complexităţii algoritmului consta in determinarea numărului de ori pentru care se
executa bucla while. Cazul cel mai defavorabil este ca elementul căutat să nu se găsească în
vector. In acest caz, timpul de executie este T(n) = 3*n+2 => O(n)

Cautarea binara (Binary Search)


Fie A, de ordin n, un vector ordonat crescător. Se cere sa se determine daca o valoare b se
afla printre elementele vectorului. Limita inferioara se numeşte low, limita superioara se
numeşte high, iar mijlocul virtual al vectorului, mid (de la middle).

PseudoCod:

Binary_search(A,n,b)
{
low = 1
high = n
while low <= high do
mid = [(low + high)/2] // partea intreaga
if A[mid] = b then
return mid
else
if A[mid] > b then
high = mid-1 // restrang cautarea la stanga
else
low = mid+1 // restrang cautarea la dreapta
return(0)
}

Calculul complexităţii algoritmului consta in determinarea numărului de ori pentru care se


executa bucla while. Se observa că, la fiecare trecere, dimensiunea zonei căutate se
înjumătăţeşte.

Cazul cel mai defavorabil este ca elementul căutat să nu se găsească în vector.

Pentru simplitate, se consideră n = 2k , unde k este numărul de înjumătăţiri.

Rezultă k = log2 n și făcând o majorare,


T(n) <= log2 n + 1, a.î.
2 * 2k <= n < 2k+1

Rezultă complexitatea acestui algoritm: este O(log2 n). Dar, baza logaritmului se poate
ignora, deoarece: logax = logab * logbx si logab este o constantă, deci rămâne O(log n), adică
o funcţie logaritmică.
Structuri de Date – Lucrarea nr. 2

Căutare binară (Binary Search)


Se caută valoare b în vectorul A[n]
st – limita din stânga
dr - limita din dreapta
m – mijlocul (st+dr)/2

varianta iterativă Se consideră n=2k, k - numărul de


înjumătăţiri
BINARY_SEARCH(A,n,b)
st←1; k = log2 n  timpul de execuţie
dr←n;
while (st<=dr) do T (n)  log2 n + 1  O(log n)
m←(st+dr)/2;
if a[m]=b then
return m;
else
if a[m]>b then
dr←m-1;
else
st←m+1;
end_if
end_if
end_while
end
a- timpul pentru secvenţa *1
Varianta recursivă b- timpul pentru secvenţa *2
T(n) satisface relaţia de recurenţă
int binSearch(int a[],int b,int st,int dr)
{int m; T (n / 2) + a daca n  1
T (n) = 
assert(st<=dr);
b daca n = 1
if(st= =dr)
return(b= = a[dr]) ? dr: -1 //*1 se demonstrează prin inducţie că dacă
else { //*2 T(n) satisface relaţia de recurenţă de
m=(st+dr)/2; mai sus atunci:
if(b<=a[m])
return binSearch(a,b,st,m); T (n)  a  log( n) + b  O(log n)
else
return binSearch(a,b,m+1,dr);
}
}

TEMA

Analizați experimental complexitatea algoritmilor de căutare și sortare de mai sus. Populați


vectorul de intrare generand secvențe aleatoare de numere întregi, de diferite lungimi sau,
pentru cazul cel mai defavorabil pupulati vectorul cu valori descrescătoare pentr o sortare
crescătoare. Pentru a genera un vector cu valori aleatoare, puteți utiliza o secventa de cod de
forma următoare:
Structuri de Date – Lucrarea nr. 2

for (int i = 0; i < N;i++) //N este dimensiunea vectorului


vect[i]= rand() % 100 + 1; //valori între 1 și 100

Funcția rand() returnează un număr pseudo-random între 0 și RAND_MAX=32767

a) Analizati numarul de operatii executat de fiecare algoritm pentru diferite dimensiuni


ale vectorului de intrare. Se vor utiliza contoare pentru numărarea operațiilor
specifice algoritmilor de căutare și sortare.
b) Masurati timpul de executie pentru diferitele lungimi ale vectorului de intrare.
Obs.: Excludeti instructiunile de contorizare adaugate la pct a).
Pentru masurarea timpului de executie puteti utiliza functionalitatea oferita de libraria
<chrono>:

#include <chrono>
……
std::chrono::time_point<std::chrono::system_clock> start, end;
start = std::chrono::system_clock::now();

//secventa de masurat
//...

end = std::chrono::system_clock::now();

std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(end -


start).count() << " ms" << std::endl;
std::cout << std::chrono::duration_cast<std::chrono::microseconds>(end -
start).count() << " us" << std::endl;

Obs.: Realizand un grafic al valorilor obtinute din măsurători pentru diferite dimensiuni ale
vectorului de intrare se va observa tipul dependentei timpului de execuție/numărului de
instrucțiuni de dimensiunea problemei (adică ordinul de complexitate). Verificați!
Structuri de Date – Lucrarea nr. 2

2. Tablouri
Tabloul este o colecţie omogenă de date în care fiecare element poate fi identificat pe baza
unui index, colecţia asigurând timp de acces constant pentru fiecare element. Prin
reprezentarea tabloului se înţelege plasarea elementelor în locaţii succesive de memorie.
Locaţiile de memorie pot fi numerotate, numărul locaţiei reprezentând indexul elementului.

Probleme

1. Să se scrie un program care să afişeze elemntele unei matrice pătratice de diemnsiune


impară, în ordinea parcurgerii acesteia în spirală, începând cu colţul stânga-sus.

Exemplu: Pentru matricea

a11 a12 a13


A = a21 a22 a23
a31 a32 a33
ordinea parcurgerii este a11 a12 a13 a23 a33 a32 a31 a21 a22.

2. Să se scrie un program care roteşte cu 90o, în sens trigonometric, o matrice pătratică de


dimensiune impară. Drept centru de rotaţie se va considera elementul din mijloc. NU se
utilizează matrici suplimentare.

Exemplu: dacă
a11 a12 a13
A = a21 a22 a23
a31 a32 a33
atunci A rotită cu 90 grade este

a13 a23 a33


A = a12 a22 a32
a11 a21 a31
3. O secţiune a unui tablou A[1..n] este formată din elementele A[i], A[i+1],..., A[j], i<=j.
Suma unei secţiuni este suma elementelor sale. Să se scrie un program care calculează
secţiunea de sumă maximă.

4. Se consideră un tablou bidimensional A cu elemente de tip întreg şi cu proprietatea că


elementele fiecărei linii şi fiecărei coloane sunt ordonate crescător. Se ştie că elementul x
apare printre elementele tabloului. Să se scrie un program C care să determine i0,j0 astfel încât
A[i0,j0] = x . Dacă există mai multe asfel de poziţii pe care apare x, atunci programul va
Structuri de Date – Lucrarea nr. 2

determina una dintre ele (nu are importanţă care); se impune însă ca numărul de operaţii să
fie minim.

TEME

Să se implementeze soluţiile tuturor problemelor prezentate în laborator.

Aplicațiile neterminate în timpul orelor de laborator ramân ca teme pentru studiu


individual!