Capitolul Cuvinte-cheie Tipuri de date, structuri, pointeri, complexitatea algoritmilor IC.0. Tipuri și structuri de date Un program dezvoltat pentru un calculator operează cu și asupra unor entități numite variabile. Acestea sunt caracterizate printr-un nume simbolic, un tip de dată și o valoare. În mod uzual, variabilele pot fi scalare (variabile care rețin un singur element) și agregate (sau structurate variabile prin intermediul cărora se rețin mai multe entități de același tip sau de tipuri de dată diferite) [U0][S03]. IC.0.. Tipuri de date fundamentale pentru limbajele C și C++ Nume Descriere Dimensiune Valori minime/maxime char Reprezentarea unui caracter octet cu semn: -8/7 fără semn: 0/55 int Valoare întreagă 4 octeți cu semn: - 47483648/47483647 fără semn: 0/49496795 short int Valoare întreagă trunchiată octeți cu semn: -3768/3767 fără semn: 0/65535 long int Valoarea întreagă extinsă 8 octeți cu semn: - 47483648/47483647 fără semn: 0/49496795 float cu semn: +/- 3.4e +/- 38 Valoarea reală în simplă 4 octeți (precizie de aproximativ precizie 7 zecimale) double long double Valoare reală în dublă precizie Valoare reală în dublă precizie 8 octeți 8/6 octeți Tabel : Tipuri fundamentale de date pentru limbajele C/C++ cu semn: +/-.7e +/- 308 (precizie de aproximativ 5 zecimale) cu semn: +/-.7e +/- 308 (precizie de aproximativ 5 zecimale) Observație: Valorile prezentate în coloanele Dimensiune și Valori minime/maxime sunt dependente de arhitectura calculatorului gazdă (procesor pe 3 sau pe 64 de biți) și de compilatorul C/C++ utilizat. IC.0.. Tipuri complexe de date structuri O structură reprezintă un tip de date agregat, definit utilizator. În esență, o structură este o colecție de una sau mai multe variabile grupate sub același nume simbolic. Variabilele incluse într-o structură pot fi de același tip sau de tipuri de date diferite și poartă numele de membrii structurii. Tipul de date utilizat în declararea unui membru al structurii poate fi tip de --
date fundamental sau tip complex de date (definit anterior declarării structurii curente). Membrii unei structuri pot fi variabile scalare, variabile agregate sau pointeri [U0][S03]. Declarea unui structuri: În exemplul următor se definește tipul complex de date student, care va conține două câmpuri: nume și nota [U0]. Student nume nota 0 > 'R' 'a' 'd' 'u' '\0' Figura : Exemplu de structură Limbajul C struct student char* nume; int nota; struct student char* nume; int nota; Limbajul C++ Declarea unei variabile de tipul structurii definite anterior: Limbajul C //x variabila de tip student struct student x; sau typedef struct student Student; Student x; Limbajul C++ //x variabila de tip student student x; IC.0. Organizarea structurilor de date Pentru fiecare variabilă utilizată în cadrul unui program se va aloca un spațiu de memorie corespunzător, în funcție de dimensiunea în octeți a tipului variabilei respective. Pentru tipurile primare de date (prezentate în cadrul subcapitolului..) dimensiunea zonei de memorie rezervată pentru un element este în mod uzual cea prezentată în tabelul. În cazul tipurilor complexe de date definite prin intermediul structurilor, zona de memorie ocupată de o eventuală variabilă nu poate fi determinată numai pe baza dimensiunii membrilor componenței. Indiferent de compilatorul utilizat, pentru o variabilă de tip structură se va rezerva spațiu de memorie pentru fiecare membru inclus în definirea tipului de date, în ordinea în care au fost declarați membrii respectivi. Exemplu (C++): struct doubleint int y; double x; --
La declararea unei variabile de tipul doubleint, în cadrul zonei de memorie rezervate pentru acea variabilă se vor regăsi, în ordine, un câmp alocat pentru membrul de tip int și un câmp alocat pentru membrul de tip double. Pentru a determina dimensiunea în octeți a unei variabile de tipul definit anterior se poate utiliza următorul exemplu de cod: Exemplu (C++): #include <cstdio> struct doubleint int y; double x; int main( void ) doubleint b; printf("dimensiune doubleint: %d\n", sizeof(b)); printf( "offset b.x: %d\n", (char*)(&b.x) - (char*)(&b) ); printf( "offset b.y: %d\n", (char*)(&b.y) - (char*)(&b) ); return 0; Rezultat: dimensiune doubleint: 6 offset b.y: 0 offset b.x: 8 Structura definită conține doi membri primul de tip int și al doilea de tip double. Considerând strict dimensiunile tipurilor primare de date utilizate (conform tabelului ), ar fi fost de așteptat ca dimensiunea variabilei b să fie de octeți. Totuși, după cum poate fi observat și din rulare programului, dimensiunea variabilei b este de 6 octeți. Zona de memorie alocată variabilei menționate anterior este organizată după cum urmează: i i i i r r r r d d d d d d d d Figura : Organizarea spațiului de memorie pentru Exemplul Legendă: - i octet rezervat pentru reprezentarea valorii de tip întreg; - d octet rezervat pentru reprezentarea valorii de tip real (în dublă precizie); - r octet rezervat pentru demarcarea unui bloc. Acest comportament este explicat prin faptul că, în funcție de procesorul sistemului pe care rulează o aplicație, rezervarea unei zone de memorie pentru un anumit tip de dată se realizează în blocuri de dimensiune egală cu dimensiunea în octeți a bancurilor de memorie disponibile. Exemplu 3 (C++): #include <cstdio> struct doubleint int y; char c; double x; int main( void ) doubleint b; printf("dimensiune doubleint: %d\n", sizeof(b)); printf("offset b.y: %d\n", (char*)(&b.y)-(char*)(&b)); printf("offset b.c: %d\n", (char*)(&b.c)-(char*)(&b)); printf("offset b.x: %d\n", (char*)(&b.x)-(char*)(&b)); return 0; -3-
Rezultat: dimensiune doubleint: 6 offset b.y: 0 offset b.c: 4 offset b.x: 8 i i i i c r r r d d d d d d d d Figura 3: Organizarea spațiului de memorie pentru Exemplul 3 Legendă: - i octet rezervat pentru reprezentarea valorii de tip întreg; - c octet rezervat pentru reprezentarea valorii de tip caracter; - d octet rezervat pentru reprezentarea valorii de tip real (în dublă precizie); - r octet rezervat pentru demarcarea unui bloc. IC.0.. Pointeri Obsevație: Conținutul acestui subcapitol este preluat din [U0].. Definire Pentru un tip de date T, o variabilă pointer la T se definește astfel: T* ptrt; // ptrt este un pointer la T O variabilă pointer la T poate reține adresa unui obiect de tip T. Exemple: int* pi; char* tab; Nod *nou, *cap; // pi este un pointer la int // tab este un pointer la char // nou și cap sunt pointeri la tipul Nod. Iniţializare Prima operaţie care se face cu un pointer este iniţializarea. Un pointer la T poate fi iniţializat cu: a) adresa unui T (care exista) pi = &i; pi i > *pi b) valoarea 0 (sau NULL) care semnifică adresă invalidă cap = 0; -4-
cap 0 c) valoarea altui pointer la T. De exemplu: dacă p și q sunt de tip T* și p conţine adresa unei variabile de tip T (a fost iniţializat în prealabil), atribuirea q = p va face ca ambii pointeri sa indice aceeaşi variabilă. p > > q d) adresa unui spaţiu de memorie alocat în zona de alocare dinamică. Spaţiul alocat poate să conţină un singur obiect de tip T, acesta se exprimă: in C: p = (T*) malloc( sizeof(t) ); in C++: p = new T; p *p > sau poate să conţină mai multe ( obiecte de tip T: in C: p = (T*) malloc( n*sizeof(t) ); in C++: p = new T[n]; p *p p[0] p[] p[] p[3] p[4] > Exprimările din C++ sunt în mod evident mult mai simple și le vom folosi pe acestea în continuare. Iată un exemplu: typedef Student* PStudent; PStudent* ptps; ptps = new PStudent[nr]; // pointer la tablou de pointeri la studenţi 3. Dereferenţierea -5-
Este operaţia prin care având un pointer la T (care conţine adresa unui T) obţinem o valoare stânga care se refera la obiectul indicat (obiectul pointat). Pentru a obţine obiectul pointat folosim operatorul * astfel: *pi = 5; // obiectul pointat ia valoarea 5!!! Atenție!!! Aceasta operaţie poate fi aplicată numai pointerilor care conţin într-adevăr adresa unui obiect. De exemplu, nu se poate face dereferențierea unui pointer nul (cu valoare 0) sau a unui pointer neiniţializat. Este valabil și pentru operatorul -> care conţine și el o dereferențiere care se observă în scrierea echivalentă: ps->nota este echivalent cu (*ps).nota. 4. Pointeri si tablouri Numele unui tablou de T este convertit automat la tipul pointer la T", deci poate fi folosit pentru a iniţializa un pointer la T". Valoarea acestui pointer este adresa primului element al tabloului: T tab[0]; T* ptrt = tab; // ptrt conţine adresa primului element Un pointer la T este deseori folosit pentru a se referi pe rând la elementele unui tablou. Următoarele operaţii semnifica: ptrt++ ptrt-- // pointează la următorul element din tablou // creează o valoare stânga! // pointează la elementul precedent din tablou // creează o valoare stânga! 5. Eliberarea spaţiului alocat dinamic Dacă un pointer a fost iniţializat cu adresa unui spaţiu din zona de alocare dinamică, atunci când nu mai avem nevoie de spaţiul respectiv (adică nu mai avem nevoie de obiectul din spaţiul respectiv) vom elibera spaţiul. El va putea fi astfel utilizat pentru alocări ulterioare. Dacă p este un pointer care a fost iniţializat printr-o alocare de memorie, eliberarea memoriei alocate se exprimă: în C: free(p); în C++: delete p;!!!atentie!!! Nici free(), nici delete nu modifică valoarea pointerului p, dar obiectul a cărui adresa este conţinută de p nu trebuie sa fie referit după eliberare. -6-
IC.0.3 Noțiuni fundamentale de analiza și complexitatea algoritmilor Evaluarea complexității algoritmilor secvențiali are drept scop evaluarea consumului de timp și de spațiu de memorie [C0][U0]. Studierea consumului de memorie se referă în acest caz la estimarea consumului de memorie suplimentară (sau auxiliară/temporară) utilizată în rezolvarea unei probleme. Există 3 cazuri implicate în analiza complexității unui algoritm [C0]: a. analiza configurației de date celei mai nefavorabile (sau analiza cazurilor degenerate); b. analiza configurației de date celei mai favorabile; c. analiza comportamentului pe cazuri de medie presupune probabilitatea de apariție a diferitelor configurații a datelor de intrare. Exemplu 4: Se dorește sortarea crescătoare a valorilor numerice stocate într-un vector. Configurațiile amintite mai sus sunt următoarele: a. cazul cel mai nefavorabil vectorul este sortat descrescător (relația de ordine a elementelor din vector este inversă față de ceea ce se dorește); b. cazul cel mai favorabil vectorul este deja sortat crescător; c. cazuri medii elementele din vector nu respectă nici un criteriu de sortare. În mod uzual, complexitatea timp și spațiu a algoritmilor se analizează pentru cazurile cele mai nefavorabile [C0][U0]. Rezultatele obținute în urma acestui tip de analiză sunt utilizate pentru a compara performanțelor algoritmilor și pentru a estima comportamentul acestora în funcție de variația dimensiunii datelor de intrare. Complexitatea unui algoritm secvenţial se exprima de regulă in limbajul ordinului O. Definiție: [U0] Fie două funcţii f : N N şi g : N N. Spunem ca 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 si un număr natural n 0 astfel încât pentru n > n f ( < c g( ) 0 n Observatie: f : N N este o funcție f(, cu n dimensiunea datelor de intrare. f( reprezintă timpul de lucru al algoritmului exprimat în pași. Lema: [C0] Dacă f este o funcție polinomială de grad k, de forma: k k f ( = ak n + ak n +... + an + a Atunci f = O( n k ). 0, Demonstrație: Efectuând majorări în membru drept al funcției f se obține: k f < n ( a + a +... + a + a ), n Notând cu ( k k 0 > ( ak + ak +... + a + a0 ) = c, c R, obținem: f ( n ) < c n k, n > n = 0 Concluzie f = O( n k ), și ordinul O exprimă viteza de variație a funcției, funcție de argument. -7-
Proprietăți ale ordinului de complexitate [C0][U0] ) Fie f, g : N N. Dacă f = O(g) atunci k f = O(g), f = O( k g), k R constant. ) Fie f, g, h : N N și: f = O(g) g = O(h). Atunci f = O(h) 3) Fie f, f, g, g : N N și: f = O ), f = O ). Atunci: f + f = O g + ) și f f = O g ) ( g ( g ( g ( g Aceasta ultimă proprietate permite ca, atunci când avem două bucle imbricate (de complexităţi diferite), complexitatea totală să se obţină înmulţindu-se cele doua complexităţi. Cele două complexităţi se adună, dacă buclele sunt succesive [U0]. IC.0.4 Evaluarea complexității și clase de complexitate După cum a fost menționat în subcapitolul.3, evaluarea complexității unui algoritm implică determinarea unei relații de variație a timpului și/sau a spațiului de memorie consumat relativ la dimensiunea datelor de intrare ale problemei. În ceea ce privește timpu consumat, intuitiv, se poate afirma că ordinul de complexitate timp indică numărul de pași efectuat de algoritm pentru a identifica soluția dorită. Rezultatul evaluării complexității unui algoritm înseamnă practic încadrarea algoritmului studiat într-una din clasele de complexitate de mai jos [C0][U0]: O() clasa algoritmilor constanti; O(log clasa algoritmilor logaritmici; O( clasa algoritmilor liniari; O(nlog clasa algoritmilor polilogaritmici; O(n ) clasa algoritmilor patratici; O(n k log clasa algoritmilor polilogaritmici; O(n k+ ) clasa algoritmilor polinomiali; O( n ) clasa algoritmilor exponenţiali. Între clasele de complexitate anterioare au loc următoarele incluziuni: k k+ n O() O(log O( O( nlog O( n ) O( n log O( n ) O( ) IC.0.5 Exemple de analiză. O buclă for for (i=0;i<n;i++) S; //secvenţă de ordin O(). Două bucle for for (i=0;i<n;i++) for (j=0;j<n;j++) S; //secvență de ordin O() n O( ) = O( n n O() = O( n ) -8-
3. Două bucle for for (j=0;j<n;j++) for (i=0;i<j;i++) S; //secvență de ordin O() 4. Buclă while h=; while (h<= s; //secvenţă de ordin O() h=*h; 5. Buclă while ce conţine buclă for h=n; while (h>0-6 ) for (i=0;i<n;i++) S; //secvenţă de ordin O() h=h/; n(n + ) i = O ( n ) pas h k 3 4... 3 4... k k n n k log n O(log f f = O( g = O( g ( ) f ( ) f = O( g while O(log O( n log for O( ( g ( ) Bibliografie [C0] Craus M., Bârsan C., Structuri de date și algoritmi, Editura Gh. Asachi, Iași, 00 [U0] Ungureanu F., Structuri de date și algoritmi Note de curs, Universitatea Tehnică Gheorghe Asachi din Iași, Facultatea de Automatică și Calculatoare [S03] Șerban E., Programarea calculatoarelor Note de curs, Universitatea Tehnică Gheorghe Asachi din Iași, Facultatea de Automatică și Calculatoare -9-