Cuprins
1. Tipuri dinamice de date. Pointeri ......................................................................
3
2. Subprograme ..........................................................................................................
13
3. Subprograme recursive .......................................................................................
30
4. Tipul de dată ARTICOL ......................................................................................
44
5. Fişiere de date ......................................................................................................
52
6. Algoritmi de prelucrare a fişierelor binare ...................................................
72
7. Structuri dinamice de date. Liste ....................................................................
110
8. Grafuri ....................................................................................................................
122
9. Structuri arborescente ......................................................................................
160
10. Elemente de programare orientată obiect ...................................................
186
Bibliografie .................................................................................................................
212
1 Tipuri dinamice de date. Pointeri
Pointerul este un tip de dată predefinit, care are ca valoare adresa unei zone de memorie (figura 1.1). Memoria internă Segment:offset Pointer
Zona de memorie indicată de pointer
Figura 1.1 Un pointer este adresa unei alte zone de memorie Folosirea pointerilor prezintă următoarele avantaje: • înlocuirea expresiilor cu indici – înmulţirile din formula de calcul al rangului se transformă în adunări şi deplasări; • posibilitatea alocării dinamice a memoriei; • folosirea tipurilor procedurale de date; • calculul adreselor. În operaţiile cu pointeri se folosesc următorii operatori specifici: Operatori Simbol Utilizare Operator de referenţiere * tip* Operator de referenţiere & &nume Operator de dereferenţiere * *nume * ⇒ defineşte un nou tip de dată (pointer la tip); & ⇒ extrage adresa unei variabile (creează o referinţă); * ⇒ accesează conţinutul zonei de memorie indicate de pointer. Cei doi operatori au efect invers: *&nume Ù nume. Exemplu: *&nume reprezintă valoarea de la adresa variabilei nume (valoarea variabilei nume).
3
Tipuri dinamice de date. Pointeri
1.1 Declararea şi iniţializarea pointerilor Fie TIP un tip de dată oarecare în limbajul C (inclusiv void). Declararea TIP* nume; este o declaraţie de pointer. TIP* este un nou tip de dată denumit pointer spre TIP, iar nume este o variabilă de tipul pointer spre TIP. Exemple: ⇒ n este o variabilă de tip pointer spre întreg; • int* n; • struct complex {a,b:real;}* x; ⇒ x este o variabilă de tip pointer spre o structură de tipul complex; • void* p; ⇒ p este o variabilă de tip pointer spre void; p poate primi ca valoare adresa unei zone de memorie de orice tip. Dacă TIP este un tip oarecare (mai puţin void) atunci tipul TIP* este adresa unei zone de memorie de un tip cunoscut. Operaţiile care se pot efectua asupra zonei respective de memorie sunt definite de tipul acesteia. Dacă TIP este void, atunci TIP* este adresa unei zone de memorie de tip necunoscut. Deoarece nu se cunoaşte tipul zonei de memorie, nu sunt definite operaţiile care se pot efectua asupra ei. Pentru pointerii din exemplele anterioare se rezervă în memoria principală (în segmentul de date) câte o zonă de 4B în care se va memora o adresă (sub forma segment:offset). Când variabila nume nu este iniţializată prin declarare, ea primeşte implicit valoarea NULL. La execuţie, poate primi ca valoare adresa unei variabile numai de tipul TIP. Dacă TIP este void, atunci nume poate primi adresa oricărei variabile, de orice tip. Exemple: int* nume; int a; float b; nume = &a; => este o atribuire corectă; nume are ca valoare adresa variabilei a. nume = &b; => este o atribuire incorectă; nume poate primi ca valoare doar
adresa unei variabile întregi. void* nume; int a; float b; nume = &a; => nume = &b;
ambele atribuiri sunt corecte; nume poate primi ca valoare adresa oricărei variabile, de orice tip. Iniţializarea pointerilor se poate realiza ca în exemplul precedent sau, ca şi pentru celelalte variabile, la declarare, astfel: int a; int* nume=&a;
Se observă folosirea operatorului de referenţiere & pentru a crea o referinţă către variabila a. La alocarea dinamică a memoriei se foloseşte o altă metodă pentru iniţializarea unui pointer. Operatorul de dereferenţiere se utilizează atât
4
Programarea calculatoarelor
pentru definirea tipului pointer, cât şi pentru referirea datelor de la adresa indicată de pointer. Exemplu: int a,b,c; int* nume; void* nume2; b=5; nume=&a; *nume=b; c=*nume+b; nume2=&b; *(int*)nume2=10; c=*(int*)nume2;
Se observă folosirea conversiei de tip (typecasting), atunci când se lucrează cu pointeri spre tipul void (fără tip). Chiar dacă un pointer spre tipul void poate primi ca valoare adresa unei variabile de orice tip, pentru a putea lucra cu ea este necesară gestionarea corectă a tipului operanzilor.
1.2 Utilizarea pointerilor 1.2.1 Operaţii cu pointeri Asupra pointerilor se pot efectua operaţii aritmetice. Fie secvenţa: int *nume,*nume2, c, a, b; nume=&a; nume2=&a;
Incrementare/decrementare Dacă nume este pointer spre un tip TIP, prin incrementare/decrementare, valoarea lui nume se incrementează/decrementează cu numărul de octeţi necesari pentru a memora o dată de tip TIP, adică cu sizeof(TIP). nume++ Ù nume are ca valoare o adresă care este incrementată şi primeşte valoarea nume+sizeof(int) (care este adresa lui b); nume2-Ù nume are ca valoare o adresă care este decrementată şi primeşte valoarea nume-sizeof(int) (care este adresa lui c);
Situaţia iniţială este următoarea:
nume 4B
nume2 4B
c
a 2B 2B
b 2B
5
Tipuri dinamice de date. Pointeri
După cele două operaţii: nume 4B
nume2 4B
c
a 2B 2B
b 2B
Analog se execută operaţiile ++nume şi --nume. Exemplu: float v[20]; float* p; int i; p=&v[i];
=> i poate avea valori între 0 şi 19
În urma atribuirii ++p sau p++, p va avea ca valoare adresa lui v[i] plus 4 octeţi, adică adresa lui v[i+1]. Adunarea/scăderea unui întreg În general, dacă p este un pointer spre un tip TIP, atunci când se adună un întreg n la pointerul p, rezultatul va fi tot un pointer spre TIP, care are ca valoare adresa memorată în p, la care se adună de n ori numărul de octeţi necesari pentru a memora o dată de tip TIP, adică n*sizeof(TIP). Asemănător se execută scăderea unui întreg dintr-un pointer. nume+n Ù nume primeşte valoarea nume+n*sizeof(int) nume-n Ù nume primeşte valoarea nume-n*sizeof(int) Exemplu: Fie p şi q pointeri spre tipul float (float* p, *q). Presupunând că p a fost iniţializat cu valoarea 0x0fff:0x3450, în urma operaţiei q=p+3, q primeşte valoarea 0xfff:0x345c (se adună 3*4 octeţi). În urma operaţiei q=p-2, q primeşte valoarea 0xffff:0x344a (se scad 2*4 octeţi). Operaţiile descrise anterior se folosesc frecvent în lucrul cu masive. Compararea a doi pointeri Limbajul C permite compararea a doi pointeri într-o expresie, folosind oricare din operatorii relaţionali (==, !=, <, >, <=, >=). Rezultatul expresiei nume op nume2 (unde op este unul din operatorii precizaţi anterior) este adevărat (nenul) sau fals (zero) după cum nume este egal, mai mare sau mai mic decât nume2. Doi pointeri sunt egali dacă adresele care constituie valorile lor sunt egale. Privind memoria internă liniar, începând de la 0x0000:0x0000, un pointer p este mai mare decât altul q, dacă adresa pe care o conţine p este mai îndepărtată de începutul memoriei decât adresa conţinută de q. Este permisă şi compararea unui pointer cu o valoare constantă. Uzual se foloseşte comparaţia cu valoarea NULL pentru a verifica dacă pointerul a fost iniţializat (un pointer neiniţializat are valoarea NULL), folosind unul din operatorii == sau !=. Valoarea NULL este definită în stdio.h astfel: #define NULL 0
6
Programarea calculatoarelor
De multe ori se preferă comparaţia directă cu zero (nume==0 sau nume!=0). În loc de nume=0 se poate folosi expresia nume. Aceasta se interpretează astfel: dacă nume nu a fost iniţializat, atunci are valoarea NULL (adică 0), deci expresia este falsă. În caz contrar valoarea expresiei este nenulă, deci adevărată. Asemănător se foloseşte expresia !nume. Exemplu: float* p,q,r,t; float a,b; p=&a; q=&b; r=&a; a=5; b=7; if(t) printf("Pointer initializat!\n"); else printf("Pointer neinitializat!\n"); if(p==r) printf("Pointeri egali\n"); else printf("Pointeri diferiti\n"); if(p>q) printf("%d\n",a); else printf("%d\n",b);
Pe ecran se va afişa: Pointer neinitializat! Pointeri egali 7
deoarece t are valoarea NULL, variabilele p şi r au ca valoare adresa lui a, iar q conţine adresa lui b, care este mai mare decât a lui a (datorită faptului că a a fost alocat primul). Diferenţa dintre doi pointeri Fie secvenţa: int m[50],* a, * b; a=&m[i]; b=&m[j];
unde i şi j sunt întregi în intervalul [0..49]. Expresia a-b are valoarea i-j, interpretată ca distanţă între adresele a şi b, exprimată în zone de memorie de lungime sizeof(int). Valoarea unei expresii diferenţă se calculează astfel: se face diferenţa între cele două adrese (în octeţi), apoi se împarte la dimensiunea tipului de dată referită de cei doi pointeri (tipul int în exemplul de mai sus – vezi figura 1.2). Cei doi pointeri trebuie să refere acelaşi tip de dată, altfel rezultatul nu are semnificaţie. Operaţia este utilă în lucrul cu masive. m
i
a
j
b
Figura 1.2 Reprezentarea semnificaţiei variabilelor din exemplul anterior
7
Tipuri dinamice de date. Pointeri
1.2.2 Legătura între pointeri şi masive În limbajul C numele unui masiv este un pointer către tipul de dată al elementele masivului. Pentru masivele unidimensionale: int m[50]; Ù m are tipul int* int* p; Ù p are tipul int* Diferenţa constă în faptul că zona de memorie către care punctează m este rezervată la compilare (ceea ce nu se întâmplă în cazul pointerilor declaraţi ca atare). De aceea m nici nu poate primi valori în timpul execuţiei programului (nu se poate schimba adresa memorată în m). El memorează adresa primului element din masiv. Referirea unui element m[i] este echivalentă cu *(m+i) – conţinutul de la adresa m+i. Limbajul C nu face niciun fel de verificări în privinţa depăşirii limitelor indicilor masivului, de aceea expresiile m[500] sau m[-7] vor fi considerate corecte de compilator, existând riscul unor erori logice. Este sarcina programatorului să se asigure că indicii nu vor depăşi limitele. Pentru masivele bidimensionale: int m[50][50]; Ù m are semnificaţia următoare: m[i][j] Ù *(*(m+i)+j), reprezintă conţinutul de la adresa j plus conţinutul de la adresa memorată în i plus m. Aceasta poate fi interpretată astfel: m este un pointer spre un vector de pointeri, fiecare element al vectorului fiind la rândul lui un pointer spre o linie a matricei (un vector de elemente de tip float). În acest fel se alocă matricele în mod dinamic (figura 1.3). Analog pot fi interpretate masivele cu mai multe dimensiuni. m
m[0,0]
m[0,1]
…
m[0,49]
m[0,0]
m[0,1]
…
m[0,49]
m[2,0]
m[2,1]
…
m[2,49]
m[3,0]
m[3,1]
…
m[3,49]
m[4]
m[4,0]
m[4,1]
…
m[4,49]
… m[49]
…
…
…
…
…
m[49,49
m[0] m[1] m[2] m[3]
m[49,0] m[49,1]
Figura 1.3 Reprezentarea modului de alocare dinamică a spaţiului necesar pentru memorarea unei matrice 50x50
8
Programarea calculatoarelor
Exemple: • un masiv cu trei dimensiuni float m[10][10][10] poate fi interpretat ca un pointer spre un vector de pointeri spre matrice; • un masiv cu n dimensiuni este tratat ca un pointer spre un vector de pointeri către masive cu n-1 dimensiuni. Pentru a lucra cu elementele unei matrice se poate folosi adresarea indexată (m[i] pentru vectori sau m[i][j] pentru matrice) sau adresarea elementelor prin pointeri (*(m+i) pentru vectori sau *(*(m+i)+j) pentru matrice etc.). De asemenea se poate declara un pointer iniţializat cu adresa de început a masivului, iar elementele masivului să fie referite prin intermediul acestui pointer. Exemple:
float* v[10]; p=v;
float* p;
După atribuire, pointerul p conţine adresa de început a masivului şi poate fi folosit pentru referirea elementelor masivului. De exemplu, v[3] şi p[3] referă aceeaşi zonă de memorie. Să se scrie secvenţa de program care citeşte de la tastatură elementele unei matrice, folosind un pointer pentru adresarea elementelor matricei. int m,n; float a[10][10]; printf("Nr. linii:\n"; scanf("%d", &m); printf("Nr. coloane:\n"); scanf("%d", &n); for(i=0;i<m;i++) for(j=0;j
Observaţie: *(m+i)+j este un pointer, care conţine adresa elementului a[i][j]; în funcţia scanf trebuie transmise ca parametri adresele unde se depun valorile citite; în exemplul anterior se putea scrie &*(*(m+i)+j), şi, reducînd, rezultă *(m+i)+j.
1.2.3 Alocarea dinamică a memoriei Pentru a memora o valoare de un anumit tip în heap este necesar să se declare un pointer către acel tip de dată, apoi să se rezerve memoria necesară. Pentru a rezerva spaţiu în heap se foloseşte funcţia standard: void* malloc(unsigned n);
Funcţia rezervă o zonă de n octeţi în heap şi returnează adresa acesteia. Deoarece funcţia returnează pointer spre void este necesară conversia spre tipul dorit, astfel: int* nume; nume=(int *) malloc(sizeof(int));
⇔ rezervă în heap spaţiu
pentru o valoare de tip întreg.
9
Tipuri dinamice de date. Pointeri
Eliberarea unei zone de memorie rezervate anterior se face prin funcţia standard: void free(void* p);
Funcţia primeşte ca parametru un pointer (indiferent de tip) spre zona de memorie pe care trebuie să o elibereze. Limbajul C oferă posibilitatea de a aloca contiguu zone de memorie pentru mai multe date de acelaşi tip, prin funcţia standard: void* calloc(unsigned nr_elem, unsigned dim_elem);
Funcţia calloc rezervă o zonă contiguă de memorie pentru mai multe elemente de acelaşi tip, întorcând un pointer spre zona respectivă. int* masiv; masiv=(int*)calloc(50,sizeof(int));
⇔ rezervă spaţiu de
memorie pentru un vector cu 50 de elemente întregi. Există şi o variantă a lui malloc care returnează în mod explicit un pointer „îndepărtat” (far): void* farmalloc(unsigned long n);
Pentru eliberarea unei zone de memorie rezervate prin farmalloc se foloseşte funcţia standard: void farfree(void* p);
Exemple: 1. Alocarea de spaţiu în heap pentru o matrice. int** m; int n,p; /* se alocă spaţiu pentru vectorul cu adresele celor n linii ale matricei */ m=(int**)malloc(m*sizeof(int*)); for(int i=0;i<m;i++) /*se alocă spaţiu pentru fiecare linie a matricei, cîte p elemente*/ m[i]=(int*)malloc(n*sizeof(int));
2. Să se scrie un subprogram pentru citirea de la tastatură a dimensiunii şi elementelor unui vector memorat în heap. void cit_vect(int *n, float **v) { int i; printf("Nr. elemente: "); scanf("%d ", n); *v=(float*)malloc(*n*sizeof(float));
10
Programarea calculatoarelor for(i=0;i<*n;i++) { printf("v(%d)= ",i); scanf("%f",&(*v)[i]); } }
3. Să se scrie o funcţie care să citească cel mult n numere întregi şi le păstreze în zona de memorie a cărei adresă de început este dată printr-un pointer. Funcţia returnează numărul valorilor citite. int cit_nr(int n, int* p) { int nr, i; int* q=p+n; // q este adresa unde se termina zona //rezervata pentru cele n numere i=0; while(p
1.2.4 Modificatorul const În limbajul C constantele simbolice se declară prin directiva de preprocesare #define. O altă posibilitate de lucru cu constante este iniţializarea unei variabile cu o valoare şi interzicerea modificării valorii acesteia. În acest scop se foloseşte modificatorul const. Sunt permise următoarele forme de utilizare: a)
tip const nume = valoare; const tip nume = valoare;
sau
Declaraţia este echivalentă cu tip nume=valoare dar, în plus, nu permite modificarea valorii lui nume printr-o expresie de atribuire nume = valoare_noua; Faţă de o constantă simbolică, în acest caz se rezervă spaţiu de memorie în care se înscrie valoarea constantei (constantă obiect). b)
tip const* nume = valoare; const tip* nume = valoare;
sau
Prin această declarare se defineşte un pointer spre o zonă cu valoare constantă. Nu este permisă atribuirea de genul *nume=valoare_noua, dar se poate ca variabilei nume să i se atribuie o adresă (de exemplu, nume = p, unde p este un pointer spre tip). Pentru a modifica valoarea înscrisă în memorie la adresa memorată de
11
Tipuri dinamice de date. Pointeri
pointerul nume se poate folosi totuşi un alt pointer: tip *t; t=nume; *t=valoare_noua;
c)
const tip* nume;
Construcţia se foloseşte la declararea parametrilor formali, pentru a împiedica modificarea lor în corpul subprogramelor, în cazul în care apelatorul are nevoie de valorile iniţiale.
1.2.5 Tratarea parametrilor din linia de comandă În linia de comandă a unui program pot să apară parametri (sau argumente). Aceştia sunt şiruri de caractere despărţite prin spaţii. Programul poate accesa argumentele prin intermediul parametrilor predefiniţi ai funcţiei main: void main(int argc, char* argv[])
unde argc conţine numărul de parametri ai programului, incrementat cu 1. Exemplu: Dacă programul nu are niciun parametru, argc are valoarea 1, dacă programul are doi parametri, argc are valoarea 3 etc. Variabila argv este un vector de pointeri care conţine adresele de memorie unde s-au stocat şirurile de caractere care constituie parametrii programului. Primul şir (argv[0]) conţine identificatorul fişierului (inclusiv calea completă) care memorează programul executabil. Următoarele şiruri conţin parametrii în ordinea în care au apărut în linia de comandă (parametrii în linia de comandă sunt şiruri de caractere separate prin spaţii). Interpretarea acestor parametri cade în sarcina programului. Exemplu: Să se scrie un program care afişează parametrii din linia de comandă. #include<stdio.h> main(int argc, char *argv[]); { int i; printf("Fisierul executabil: %s\n", argv[0]); for(i=1;i<argc;i++) printf("Parametrul nr. %d: %s\n",i, argv[i]); }
12
2 Subprograme
Conform teoriei programării, subprogramele sunt clasificate în funcţii, care returnează un singur rezultat prin „numele” funcţiei şi oricâte prin parametri de ieşire şi proceduri, care returnează oricâte rezultate, prin intermediul parametrilor de ieşire. Un program C este un ansamblu de funcţii care realizează activităţi bine definite. Există o funcţie, numită main(), care este apelată la lansarea în execuţie a programului. Subprogramele C sunt, în mod nativ, funcţii. Pot fi construite subprograme care nu returnează niciun rezultat prin numele lor, comportându-se ca o procedură (conform definiţiei din teorie). Sistemele C au colecţii de biblioteci care conţin funcţii standard. Textul sursă al unui program C poate fi partiţionat în mai multe fişiere. Fiecare fişier constă dintr-un set de funcţii şi declaraţii globale. Fişierele care constituie partiţia pot fi compilate şi, eventual, testate separat, dar numai unul va conţine funcţia main().
2.1 Construirea şi apelul subprogramelor Funcţiile C sunt formate din antet şi un corp. Antetul are forma: tip nume([lista-parametri-formali])
unde: • tip poate fi un tip simplu de dată. Dacă lipseşte, este considerat tipul implicit (int pentru unele compilatoare, void pentru altele); • nume este un identificator care reprezintă numele funcţiei; • lista-parametrilor-formali conţine parametrii formali sub forma: [tip1 identificator1[,tip2 identificator[,tip3 identificator …]]]
13
Subprograme
Parametrii sunt separaţi prin virgulă. La limită, lista poate fi vidă. Pentru fiecare parametru trebuie specificat tipul, chiar dacă mai mulţi parametri sunt de acelaşi tip (nu este posibilă definirea de liste de parametri cu acelaşi tip). Pentru funcţiile care nu întorc o valoare prin numele lor, tipul funcţiei va fi void sau va fi omis. Corpul este o instrucţiune compusă: conţine declaraţiile locale şi instrucţiunile executabile care implementează algoritmul. Corpul funcţiei se execută până la ultima instrucţiune sau până la executarea instrucţiunii return. Forma ei generală este: return(expresie); return expresie; return;
sau sau
Prima şi a doua formă sunt folosite în cazul funcţiilor care returnează o valoarea prin numele lor. Prin executarea acestei instrucţiuni se evaluează expresia, valoarea sa este atribuită funcţiei şi se încheie execuţia funcţiei. A treia formă este folosită în cazul funcţiilor care nu returnează nicio valoare prin numele lor (poate chiar să lipsească). Dacă este prezentă, efectul ei este încheierea execuţiei funcţiei. Tipul expresiei din instrucţiunea return trebuie să coincidă cu tipul funcţiei. În limbajul C nu este admisă imbricarea, adică definirea unui subprogram în cadrul altui subprogram şi nu sunt permise salturi cu instrucţiunea goto (instrucţiune de salt necondiţionat) în afara subprogramului. Declararea unui subprogram apare, în cadrul fişierului sursă, înaintea primului apel. Există cazuri particulare în care, fie funcţiile se apelează unele pe altele (de exemplu, cazul recursivităţii mutuale), fie definiţia nu se află în fişierul sursă. Pentru a oferi compilatorului posibilitatea să efectueze verificarea validităţii apelurilor, sunt prevăzute declaraţii ale subprogramelor fără definire. Aceste declaraţii se numesc prototipuri şi apar în afara oricărui corp de funcţie. Sintaxa generală este: tip nume ([lista-parametri-formali]);
Prototipul este de fapt un antet de funcţie după care se scrie caracterul; (punct şi virgulă). Numele parametrilor pot lipsi, fiind suficientă specificarea tipurilor lor. Prototipul trebuie inserat în program înaintea primului apel al funcţiei. Domeniul de valabilitate a declaraţiei unui subprogram este limitat la partea care urmează declaraţiei din fişierul sursă. Prototipurile funcţiilor standard se află în fişiere header (cu extensia .h). Utilizarea unei funcţii din bibliotecă impune includerea fişierului asociat, cu directiva #include.
14
Programarea calculatoarelor
Fiind funcţii, subprogramele C se apelează ca operanzi în expresii, prin numele funcţiei urmate de lista parametrilor reali. Expresia care conţine apelul poate la limită să conţină un singur operand şi chiar să fie o instrucţiune de tip expresie. În aceste cazuri valoarea returnată de funcţie se pierde, nefiind folosită în niciun fel. Exemple: Să se scrie o funcţie care calculează cel mai mare divizor comun dintre două numere întregi nenule, utilizând algoritmul lui Euclid şi un apelant pentru testare. #include <stdio.h> /*definirea functiei cmmdc*/ int cmmdc(int a, int b) { int r,d=a,i=b; do {r=d%i; d=i; i=r;} while(r<>0); return i;} void main() { int n1,n2; printf("Numerele pentru care se va calcula cmmdc:"); scanf("%d%d",&n1,&n2); if(n1&&n2) printf("\ncmmdc=%d",cmmdc(n1,n2)); else printf("Numerele nu sunt nenule!"); }
Acelaşi exemplu folosind un prototip pentru funcţia cmmdc: #include <stdio.h> /* prototipul functiei cmmdc*/ int cmmdc(int, int); void main() { int n1,n2; printf("Numerele pentru care se va calcula cmmdc:"); scanf("%d%d",&n1,&n2); if(n1&&n2) printf("\ncmmdc=%d",cmmdc(n1,n2)); else printf("Numerele nu sunt nenule! "); } /*definirea functiei cmmdc*/ int cmmdc(int a, int b) { int r,d=a,i=b; do {r=d%i; d=i; i=r;} while(r<>0); return i; }
15
Subprograme
2.2 Transferul datelor între apelant şi apelat În practica programării, s-au conturat două posibilităţi de transfer al datelor între apelant şi apelat: prin parametri şi prin variabile globale. Prin utilizarea variabilelor globale nu se face un transfer propriu-zis, ci se folosesc în comun anumite zone de memorie.
2.2.1 Transferul prin parametri Principial, transferul se poate face prin valoare sau prin adresă. În limbajul C este implementat numai transferul prin valoare (valoarea parametrului real este copiată în stivă, iar subprogramul lucrează numai cu această copie). Operaţiile efectuate asupra unui parametru formal scalar (care nu este masiv) nu modifică, la ieşirea din subprogram, parametrul real corespunzător. Transferul valorii este însoţit de eventuale conversii de tip realizate pe baza informaţiilor de care dispune compilatorul despre subprogram. Dacă prototipul precede apelul subprogramului şi nu există o sublistă variabilă de parametri, conversiile se fac similar atribuirilor. Exemplu: tip_returnat nume(tip_parametru p);
Ù p este transferat prin valoare
Folosind transferul prin valoare se pot transmite numai parametri de intrare în subprogram. Pentru a putea folosi parametri de ieşire trebuie simulat transferul prin adresă. În acest scop, se vor efectua explicit operaţiile care se fac automat la transferul prin adresă din alte limbaje: se transmite ca parametru adresa parametrului real, iar în subprogram se lucrează cu indirectare. Exemplu: tip_returnat nume(tip_parametru *p);
Ù p este transferat prin valoare,
fiind adresa parametrului real. Pentru parametrii de tip masiv, simularea transferului prin adresă se face în mod implicit, datorită modului de construire a masivelor în C: numele masivului este un pointer. La apel, în stivă se va transfera adresa masivului iar referirea elementelor se face automat prin calcul de adrese (vezi capitolul Tipuri dinamice de date. Pointeri). Următoarele prototipuri sunt echivalente: tip_returnat nume1(float v[], int n); tip_returnat nume2(float *v, int n);
Exemple: 1. Să se calculeze produsul scalar dintre doi vectori. a) rezultatul se întoarce prin numele funcţiei:
16
Programarea calculatoarelor float ps(float x[], float y[], int n) { int i,prod=0; for(i=0;i
Apelul se realizează astfel: float a[30],b[30]; int dimensiune; ……………… printf("Produsul scalar al vectorilor a si b este:%f", ps(a,b,dimensiune));
b)
rezultatul se întoarce prin parametru, simulând transferul prin adresă: void ps(float x[], float y[], int n, float *prod) { int i; *prod=0; for(i=0;i
Apelul se realizează astfel: float a[30],b[30],produs_scalar; int dimensiune; ps(a,b,dimensiune,&produs_scalar); printf("Produsul scalar al vectorilor a si b este:%f", produs_scalar);
2. Să se calculeze elementul maxim dintr-un vector şi poziţiile tuturor apariţiilor acestuia (v, n sunt parametri de intrare; max, nr_ap, poz sunt parametri de ieşire). void maxim(float v[],int n,float *max,int poz[]) { int i; for(*max=v[0],i=1;i
*nr_ap,int
Apelul se realizează astfel: float a[30],el_max; int dimensiune,nr_aparitii,pozitii[30]; maxim(a,dimensiune,&max,&nr_aparitii,pozitii);
Antetul subprogramului este echivalent cu construcţia void maxim(float *v, int n, float *max, int *nr_ap, int *poz)
pentru care corpul subprogramului este acelaşi.
17
Subprograme
3.
Să se calculeze produsul a două matrice statice. void produs(float a[][10],float b[][20], float c[][20],int m, int n,int p) { int i,j,k; for(i=0;i<m;i++) for(j=0;j
Observaţie: Deşi un tablou nu poate fi returnat ca tip masiv prin numele unei funcţii, se pot scrie funcţii care returnează prin nume un tablou ca pointer – deoarece numele tabloului este echivalent în C cu adresa sa (pointer la începutul masivului). Unui astfel de masiv i se alocă memorie în funcţia care îl calculează. Numele său este returnat ca pointer la primul element al tabloului. Exemple: 1. Să se calculeze produsul dintre o matrice şi un vector. #include<malloc.h> …………………… float * prod(float a[][30], float v[],int m, int n) { float *p;int i,j; p=(float *)malloc(sizeof(float)*m); for(i=0;i<m;i++) for(p[i]=0,j=0;j
Apelul se realizează astfel: a) float a[20][30], b[30], *c; int m,n; ………………………… c=prod(a,b,m,n);
Cu vectorul c se lucrează în modul obişnuit: elementele se referă prin indexare (c[i], i=0..m ). b) float a[20][30], b[30]; int m,n; …………………………
Se lucrează cu „vectorul” prod(a,b,m,n) – elementele sale se referă ca prod(a,b,m,n)[i], i=0..m. Observaţie: La fiecare referire de element se apelează şi se execută funcţia, ceea ce duce la consum mare şi inutil de resurse. Este preferabilă prima variantă. 2. Să se realizeze un program C pentru ridicarea unei matrice la o putere. Pentru aceasta se folosesc două funcţii care returnează, prin pointeri, produsul a două matrice (înmulţire), respectiv ridicarea unei matrice la o putere (putere).
18
Programarea calculatoarelor #include<stdio.h> #include
#include float** inmultire(float **a,float **b,int n) { int i,j,k; float **c; c=(float **)malloc(n*sizeof(float *)); for(i=0;i
19
Subprograme
Simularea transmiterii parametrilor prin adresă Limbajul C permite transmiterea parametrilor numai prin valoare (la apelul subprogramelor se copiază în stivă valoarea parametrului real şi subprogramul lucrează cu această copie). Subprogramul nu poate modifica valoarea parametrului din apelator. Dacă parametrul formal este un masiv, el este de fapt un pointer (adresa de început a masivului). Folosind această proprietate, se pot modifica valorile elementelor masivului, iar modificările se vor propaga în blocul apelator, deoarece valoarea care se copiază în stivă este adresa de început a masivului. Masivul rămâne în memoria principală şi poate fi modificat prin intermediul adresei sale de început. Astfel se poate simula transmiterea parametrilor prin adresă folosind pointerii. Subprogramul poate modifica valori care să se propage în apelator. În acest scop se transmite ca parametru un pointer spre variabila cu care trebuie să lucreze subprogramul apelat, care va lucra în mod explicit cu pointerul. Un exemplu în acest sens este funcţia de citire a datelor de la tastatură. Parametrii acestei funcţii sunt adresele variabilelor ale căror valori trebuie citite. Exemplu: 1. Fie un subprogram care calculează suma elementelor unui vector v de lungime n. void suma(float s, float v[], int n) { int i; for(s=0,i=0;i
Subprogramul suma calculează suma elementelor vectorului, dar nu poate fi folosită de apelator deoarece valoarea sumei este cunoscută numai în interiorul funcţiei (parametrul a fost transmis prin valoare). În apelator valoarea variabilei corespunzătoare parametrului formal s nu va fi modificată. Pentru ca subprogramul să fie utilizabil, trebuie ca parametrul s să fie un pointer spre variabila în care se va memora suma elementelor vectorului: void suma(float* s, float v[], int n) { int i; for(s=0,i=0;i
La apelul funcţiei, primul parametru actual este adresa variabilei în care se memorează suma: void main() { float x, m[20]; //… suma(&x, m, n); //… }
int n;
20
Programarea calculatoarelor
2. Să se realizeze un subprogram care citeşte de la tastatură o valoare întreagă care aparţine unui interval dat. void citire(int a, int b, int* x) { do printf("Introduceti numarul: "); scanf("%d", x); while((*x<=a)||(*x>=b)); }
2.2.2 Transferul prin variabile globale Variabilele globale se declară în afara funcţiilor. Ele pot fi referite din orice alte funcţii. De aceea, schimbul de valori între apelant şi apelat se poate realiza prin intermediul lor. Variabilele declarate într-o funcţie se numesc locale (din clasa automatic) şi pot fi referite numai din funcţia respectivă. Domeniul de valabilitate a unei variabile locale este blocul (funcţia sau instrucţiunea compusă) în care a fost definită. Exemplu: #include <stdio.h> int a; float z(char b) { int b; …………… } main() { int c; …………… { /* instructiunea compusa r */ int d; …………… } }
Domeniile de valabilitate a referirilor variabilelor declarate sunt: b poate fi referit doar în funcţia z; c poate fi referit doar în funcţia main; d poate fi referit doar în instrucţiunea compusă r; a este globală şi poate fi referită de oriunde.
2.3 Pointeri spre funcţii În limbajul C, numele unei funcţii este un pointer care indică adresa de memorie unde începe codul executabil al funcţiei. Aceasta permite transmiterea funcţiilor ca parametri în subprograme precum şi lucrul cu tabele de funcţii. În acest scop trebuie parcurse următoarele etape: a. Declararea unei variabile de tip procedural (pointer spre funcţie): tip_rezultat (*nume_var)(lista_parametri_formali);
21
Subprograme
unde nume_var este o variabilă de tip procedural şi are tipul pointer spre funcţie cu parametrii lista_parametri_formali şi care returnează o valoare de tipul tip_rezultat. Lui nume_var i se poate atribui ca valoare doar numele unei funcţii de prototip: tip_rezultat nume_f(lista_parametrilor_formali);
b. Descrierea funcţiei care utilizează parametrii procedurali: void f(…,tip_rezultat (*nume)(lista_parametrilor_formali),…) { tip_rezultat x; … x=(*nume)(lista_parametrilor_actuali); …}
unde nume este parametrul formal de tip procedural. c. Apelul funcţiei cu parametri procedurali: tip_rezultat nume_functie(lista_parametrilor_formali) { … } void main() { … f(…, nume_functie, …); }
Exemplu: Fie o funcţie care efectuează o prelucrare asupra unui vector. Nu se cunoaşte apriori tipul prelucrării, aceasta fiind descrisă de o altă funcţie, primită ca parametru. Pot exista mai multe funcţii care descriu prelucrări diferite asupra unui vector şi oricare din ele poate fi transmisă ca parametru. float suma(float *v, int n) { for(int i=0, float s=0; i
Apelul se realizează prin transmiterea ca parametru real a funcţiei potrivite prelucrării dorite.
22
Programarea calculatoarelor void main() { float tab[10]; int m,i; printf("Numarul de elemente(<10): "); scanf("%d ", &m); for(i=0,i<m;i++) {printf("a(%d)=",i); scanf("%f",&tab[i]); } printf("Se calculeaza suma elementelor…\n"); functie(tab, m, suma); printf("Se calculeaza media elementelor…\n"); functie(tab, m, media); return; }
Limbajul C permite lucrul cu variabile de tip pointer, care conţin adresa de început a unei funcţii (a codului său executabil). Aceste variabile permit transferul adresei funcţiei asociate ca parametru, precum şi apelul funcţiei prin intermediul pointerului său. Următoarea declaraţie defineşte pointer_f ca pointer spre funcţia cu rezultatul tip_returnat şi parametrii parametri. tip_returnat (*pointer_f)([parametri])
Observaţie: Nu trebuie să se confunde un pointer la o funcţie cu o funcţie care are ca rezultat un pointer, cu sintaxa de forma tip_returnat *pointer_f([parametri]). Adresa unei funcţii se obţine prin simpla specificare a identificatorului acesteia (fără specificarea parametrilor sau parantezelor) şi poate fi atribuită unui pointer spre funcţie cu rezultat şi parametri compatibili. Pointerul poate fi folosit ulterior pentru apelul funcţiei sau transmis ca parametru real în apelul unui subprogram care conţine, în lista parametrilor formali, un pointer la un prototip de funcţie compatibilă. Exemple: 1. Să se aproximeze soluţia unei ecuaţii de forma f(x)=0 prin metoda bisecţiei. #include<stdio.h> #include #include<math.h> /*prototipul functiei bisectie*/ void bisectie(float,float,float(*f)(float),float,long,int *,float *); /*prototipul functiei pentru care se aplica metoda bisectiei*/ float fct(float); /* functia principala*/ void main() { float a,b,eps,x;
23
Subprograme int cod; long n; float (*functie)(float); clrscr(); printf("Introduceti capetele intervalului:"); scanf("%f%f",&a,&b); printf("\nEroarea admisa:"); scanf("%f",&eps); printf("\nNumarul maxim de iteratii:"); scanf("%li",&n); functie=fct; bisectie(a,b,functie,eps,n,&cod,&x); if(!cod) printf("\nNu se poate calcula solutia aproximativa"); else printf("\n Solutia aproximativa este: %f",x); } /*descrierea functiei pentru care se aplica metoda bisectiei*/ float fct(float x) { return x*x*x-3*x+14; } /*functia ce implementeaza metoda bisectiei*/ void bisectie(float a,float b,float (*f)(float),float eps,long n, int *cod,float *x) { int gata=0; long c; for(c=0;(c
2. Să se sorteze un şir cu elemente de un tip neprecizat, dar pe care se poate defini o relaţie de ordine (de exemplu numeric, şir de caractere, caracter). Metoda aleasă spre exemplificare este sortarea prin selecţie directă. Un subprogram de sortare care să nu depindă de tipul elementelor şi de criteriul de sortare considerat trebuie să aibă ca parametri formali: - vectorul de sortat, ca pointer la tipul void, asigurându-se astfel posibilitatea realizării operaţiei de schimbare a tipului („cast”) în funcţie de necesităţile ulterioare (la momentul apelului se poate realiza modificarea tipului void * în tip_element *, unde tip_element reprezintă tipul elementelor vectorului de sortat); - dimensiunea vectorului de sortat şi numărul de octeţi din reprezentarea tipului elementelor vectorului; - pointerul la o funcţie de comparare, cu argumente de tip void *, care să permită la apel atât schimbarea de tip, cît şi descrierea efectivă a relaţiei de ordine.
24
Programarea calculatoarelor
Cum tipul elementelor vectorului nu este cunoscut la momentul descrierii procedurii de sortare, operaţia de atribuire nu poate fi folosită, ea fiind înlocuită de o funcţie de copiere a unui număr prestabilit de octeţi, de la o adresă sursă la una destinaţie. O astfel de funcţie există în biblioteca mem.h, sintaxa ei fiind: void *memmove(void *destinaţie, const void *sursă, unsigned n)
Pentru accesarea adresei elementului de rang i din vector se foloseşte formula (char *)v+i*nr_octeti. Fişierul sursă care conţine funcţia de sortare descrisă anterior este: //fisier exp_tip.cpp #include <mem.h> include void sort(void *v, int n, int dim, int (*compara)(const void * ,const void * )) { int i,j; void *aux; aux=malloc(dim); for(i=0;i
Exemplu de apel pentru un vector de numere reale: #include <stdio.h> #include #include "exp_tip.cpp" int compara(const void *a, const void *b) { if(*(float *)a>*(float *)b)return 1; else return 0; } void main() { float vect[20]; int n,i; clrscr(); printf("Dimensiunea vectorului:"); scanf("%d",&n); printf("\nElementele:"); for(i=0;i
25
Subprograme
Exemplu de apel pentru un vector de cuvinte (şiruri de caractere): #include <stdio.h> #include <string.h> #include #include "exp_tip.cpp" int compara(const void *a, const void *b) { if(strcmp((char *)a, (char *)b)>0)return 1; else return 0; } void main() { typedef char cuvant[10]; cuvant vect[20]; int n; clrscr(); printf("Dimensiunea vectorului de cuvinte:"); scanf("%d",&n); printf("\nCuvintele:"); for(int i=0;i
2.4 Funcţii cu număr variabil de parametri Bibliotecile limbajului C conţin subprograme standard cu număr variabil de parametri. Limbajul C permite definirea funcţiilor utilizator cu număr variabil de parametri, prin utilizarea unui set de macrodefiniţii, declarate în biblioteca stdarg.h, care permit accesul la lista de parametri. Fişierul stdarg.h declară tipul va_list şi funcţiile va_start, va_arg şi va_end, în care: - va_list este un pointer către lista de parametri. În funcţia utilizator corespunzătoare trebuie declarată o variabilă (numită în continuare ptlist) de acest tip, care va permite adresarea parametrilor; - va_start iniţializează variabila ptlist cu adresa primului parametru din sublista variabilă. Prototipul acestei funcţii este: void va_start(va_list ptlist, ultim)
unde ultim reprezintă numele ultimului parametru din sublista variabilă. În unele situaţii (vezi exemplele) se transferă în acest parametru numărul de variabile trimise.
26
Programarea calculatoarelor
- va_arg întoarce valoarea parametrului următor din sublista variabilă. Prototipul acestei funcţii este: tip_element va_arg(va_list ptlist, tip_element)
unde tip_element este tipul elementului transferat din listă. După fiecare apel al funcţiei va_arg, variabila ptlist este modificată astfel încât să indice următorul parametru. - va_end încheie operaţia de extragere a valorilor parametrilor şi trebuie apelată înainte de revenirea din funcţie. Prototipul funcţiei este: void va_end(va_list ptlist)
Problema numărului de parametri şi tipurilor lor este tratată de programator. Exemple: 1. Să se calculeze cel mai mare divizor comun al unui număr oarecare de numere întregi. #include<stdio.h> #include #include<stdarg.h> int cmmdc_var(int,...); int cmmdc(int, int); void main() { int x,y,z,w; clrscr(); scanf("%d%d%d%d",&x,&y,&z,&w); printf("\nCmmdc al primelor 3 numere:%d\n",cmmdc_var(3,x,y,z)); printf("\nCmmdc al tuturor numerelor:%d\n",cmmdc_var(4,x,y,z,w)); } //cel mai mare divizor comun a doua numere int cmmdc(int x,int y) { int d=x,i=y,r; do{ r=d%i; d=i;i=r; } while(r); return d; } //cel mai mare divizor comun a nr numere int cmmdc_var(int nr,...) { va_list ptlist; /*initializarea lui ptlist cu adresa de parametri*/ va_start(ptlist,nr); //extragerea primului parametru, de tip int x=va_arg(ptlist,int); for(int i=1;i
inceput a listei de
27
Subprograme {
//extragerea urmatorului element din lista de parametri y=va_arg(ptlist,int); z=cmmdc(x,y);x=z;
} va_end(ptlist); return x; }
2. Să se interclaseze un număr oarecare de vectori. Spre deosebire de exemplul anterior, în care în lista de parametri a funcţiei cu număr oarecare de parametri figurau elemente de acelaşi tip (int), acest exemplu ilustrează modul de transfer şi acces la elemente de tipuri diferite. Funcţiei intre_var i se transmit la apel vectorul rezultat, iar pentru fiecare vector de interclasat, adresa de început (pointer la tipul double) şi numărul de elemente (int). Numărul parametrilor din lista variabilă este, în acest, caz 2*numărul de vectori de interclasat. #include<stdarg.h> #include<stdio.h> #include void inter(double *,int,double *,int,double *); void inter_var(double *,int nr,...); void main() { int n1,n2,n3,n4; double x1[10],x2[10],x3[10],x4[10],z[50]; clrscr(); scanf("%d%d%d%d",&n1,&n2,&n3,&n4); for(int i=0;i
28
Programarea calculatoarelor void inter(double *x, int n1, double *y, int n2, double *z) { int i,j,k; for(i=0,j=0,k=0;(i
inceput
a
listei
de
29
3 Subprograme recursive
Recursivitatea este tehnica de programare în care un subprogram se autoapelează. Limbajul C face parte din clasa limbajelor de programare care admit scrierea de funcţii recursive. În continuare sunt prezentate câteva exemple simple de subprograme C prin intermediul cărora sunt calculate recursiv valorile n! , C nk , f ο g ο f , unde n , k ∈ N şi f, g funcţii, f , g : R → R. De asemenea, este ilustrată maniera în care sunt efectuate apelurile recursive şi tratarea condiţiilor terminale.
3.1 Calcul recursiv Calculul valorii n! pentru n dat poate fi efectuat pe baza formulei recursive
⎧1, n = 0 n! = ⎨ . ⎩n(n − 1)! , n > 0
Fie fact(n) funcţia C care calculează n!. Dacă n ≥ 1, evaluarea lui fact(n) rezultă prin multiplicarea cu n a valorii calculate de apelul fact(n-1), cu fact(0)=1. Cu alte cuvinte, apelul funcţiei fact(n) realizează calculul „imediat” dacă n=0, altfel presupune un nou apel al aceleiaşi funcţii pentru valoarea argumentului decrementată. Cazurile în care este posibilă evaluarea „imediată” se numesc condiţii terminale. În limbajul C, funcţia fact este long fact(unsigned n) { if (!n) return 1; return n*fact(n-1); }
Utilizarea formulei
C nk =
n! k ! (n − k )!
pentru calculul combinărilor
( n , k ∈ N date) este ineficientă şi uneori imposibilă deoarece n!, pentru n ≥ 13 nu
30
Subprograme recursive
poate fi reprezentat în calculator ca dată de un tip întreg, chiar dacă numărul C nk este relativ mic şi poate fi reprezentat prin intermediul unui tip întreg. Pe baza relaţiei de recurenţă C nk = C nk−1 + C nk−−11 , valoarea C nk poate fi calculată astfel. Fie comb(n,k) funcţia care calculează C nk . Conform relaţiei de recurenţă, dacă n ≥ k ≥ 1, atunci evaluarea corespunzătoare apelului comb(n,k) revine la însumarea rezultatelor obţinute prin apelurile comb(n-1,k) şi comb(n-1, k-1), unde comb(n,0)=1, n ≥ 0. Dacă evaluările comb(n-1,k) şi comb(n-1, k-1) sunt realizate în acelaşi mod, rezultă că apelul comb(n,k) va determina o secvenţă de apeluri ale aceleiaşi funcţii pentru valori ale argumentelor din ce în ce mai mici, până când este îndeplinită una din condiţiile terminale comb(n,0)=1, comb(k,k)=1. Soluţia recursivă a evaluării C nk este: long comb(unsigned n, unsigned k) { if (k>n) return 0; if ((k==0)||(k=n)) return1; return comb(n-1,k)+comb(n-1,k-1); }
Funcţiile C recursive pentru calculul n! , C nk , unde n , k ∈ N realizează apeluri recursive directe. Schema unui apel recursiv poate fi descrisă astfel: se verifică dacă este îndeplinită cel puţin una din condiţiile terminale; dacă este îndeplinită o condiţie terminală, atunci calculul este încheiat şi controlul este returnat unităţii apelante, în caz contrar este iniţiat calculul pentru noile valori ale parametrilor, calcul care presupune unul sau mai multe apeluri recursive. Mecanismul prin care este efectuat apelul unui subprogram se bazează pe utilizarea stivei memoriei calculatorului. Fiecare apel determină introducerea în stivă a valorilor parametrilor formali, a adresei de revenire şi a variabilelor locale. La momentul execuţiei, aceste informaţii sunt extrase cu eliminare din stivă, eliberându-se spaţiul ocupat. În cazul subprogramelor recursive, mecanismul funcţionează astfel: este generat un număr de apeluri succesive cu ocuparea spaţiului din stivă necesar efectuării apelurilor până la îndeplinirea unei condiţii terminale; apelurile sunt executate în ordinea inversă celei în care au fost generate, iar operaţia de inserare în stivă poate produce depăşirea spaţiul de memorie rezervat. De exemplu, în cazul apelului fact(3), secvenţa de apeluri recursive este: fact(2), fact(1), fact(0). În continuare execuţia determină fact(0)=1, fact(1)=1*fact(0)=1, fact(2)=2*fact(1)=2, fact(3)=3*fact(2)=6. Evoluţia determinată de apelul fact(3) în stivă este ilustrată în figurile 3.1.a şi 3.1.b, unde (○) reprezintă adresa de revenire în punctul de unde a fost efectuat apelul fact(3). Apelurile recursive ale unui subprogram S1 pot fi şi indirecte, în sensul că este efectuat un apel al unui alt subprogram S2 şi S2 iniţiază un apel al lui S1. De
31
Programarea calculatoarelor
exemplu, calculul valorilor funcţiei h=f◦g◦f , unde f,g:R→R sunt funcţii date poate fi descris astfel. Pentru funcţiile f, g definite prin:
3
2
Fact=3*Fact(2) Fact=2*Fact(1) 3 (o)
Fact=3*Fact(2) (o)
1
0
Fact=1*Fact(0)
Fact=1
2
1
Fact=2*Fact(1)
Fact=1*Fact(0) 2
3
Fact=2*Fact(1)
Fact=3*Fact(2)
3 (o)
Fact=3*Fact(2) (o)
Figura 3.1.a Evoluţia în stivă până la verificarea condiţiei terminale n=0
32
Subprograme recursive
1
2
Fact=1
Fact=2
2
3
Fact=2*Fact(1)
Fact=3*Fact(2)
3
3
Fact=3*Fact(2)
Fact=6 (o) (o)
Figura 3.1.b Eliberarea stivei după execuţia determinată de condiţia terminală
⎧2 x 3 + 1, x < 5 ⎧⎪5 x 2 − 3 x + 2 , x ≤ 1 ⎪ f (x ) = ⎨ x 4 + 2 , 5 ≤ x < 8 , g (x ) = ⎨ 3 ⎪⎩ x − x + 5 , x > 1 ⎪3 , x > 8 ⎩ funcţiile C pentru calculul h=f◦g◦f pot fi descrise astfel, float f(float x) { if (x<5) return 2*pow(x,3)+1; if (x<8) return pow(x,4)+2; return 3; } float g(float x) { if (x<=1) return 5*x*x-3*x+2; return pow(x,3)-x+5; } float h(float x) { return f(g(f(x))); }
33
Programarea calculatoarelor
3.2 Aplicaţii cu subprograme recursive 1. Să se scrie o funcţie C care citeşte o secvenţă oarecare de cuvinte a1, a2, ….., an terminată cu simbolul # şi afişează anan-1…a1. Pentru rezolvarea problemei se utilizează funcţia recursivă Scrie. void Scrie() { char cuvant[100]; scanf(“%s“,&cuvant); if (strcmp(cuvant,“#“)) { Scrie; printf( “%s\n“,cuvant); } }
2. Calculul valorii funcţiei Ackermann. Funcţia Ackermann este definită pentru argumentele m,n numere naturale prin
⎧n + 1, m = 0 ⎪ a (m , n ) = ⎨a (m − 1,1), n = 0 ⎪a (m − 1, a (m , n − 1)), altfel ⎩ Funcţia C Ackermann calculează valoarea funcţiei a pentru m, n parametri naturali daţi. long Ackermann(unsigned m, unsigned n) { if (!m) return n+1; if (!n) return Ackermann(m-1,1); return Ackermann(m-1,Ackermann(m,n-1)); }
3. Problema calculului celui mai mare divizor comun dintre două numere naturale a şi b poate fi rezolvată recursiv, conform definiţiei următoare,
⎧a , a = b (a ,b ) = ⎪⎨( a − b ,b ), a > b ⎪( a ,b − a ), b > a ⎩ Funcţia C cmmdc(a,b) este, long cmmdc(long a, long b) { if (a==b) return a; if (a>b) return cmmdc(a-b,b); return cmmdc(a,b-a); }
34
Subprograme recursive
4. Problema turnurilor din Hanoi ilustrează foarte bine avantajele recursivităţii. Problema poate fi enunţată astfel: se presupune că există trei tije a, b, c, pe tija a fiind plasate n discuri de diametre diferite în ordinea descrescătoare a acestora. Se cere ca cele n discuri de pe tija a să fie deplasate pe tija c astfel încât să fie îndeplinite condiţiile: • la fiecare mutare este deplasat unul dintre discurile aflate pe poziţia superioară pe una din tije; • oricare din discuri poate fi aşezat numai pe un disc de diametru mai mare; • tija b poate fi folosită pentru deplasări intermediare. Notând cu P(n,a,c) problema transferului celor n discuri de pe tija a pe tija c, pentru rezolvarea ei putem raţiona în modul următor. Dacă s-a rezolvat problema P(n-1,a,b), atunci discul de diametru maxim care se află încă pe tija a este deplasat pe tija c şi în continuare se rezolvă problema P(n-1,b,c). Soluţia recursivă este prezentată în funcţia Hanoi. Exemplu Presupunând că discurile sunt numerotate în ordinea crescătoare a diametrelor cu etichetele 1, 2, 3, o soluţie a problemei pentru n=3 poate fi descrisă astfel. Tija a 1 2 3 2 3 3 3
1 1
Tija b
2 1 2 1 2 2
Tija c
Mutarea efectuată a⇒c
1
a⇒b
1
c⇒b a⇒c
3
b⇒a
3 2 3 1 2 3
b⇒c a⇒c
#include <stdio.h> #include void Hanoi(unsigned n,unsigned a, unsigned b,unsigned c) {
35
Programarea calculatoarelor if(n>0) { Hanoi(n-1,a,c,b); printf("Transfer disc de pe tija %u pe tija %u\n",a,b); Hanoi(n-1,c,b,a); } } void main() { unsigned n,a,b,c; clrscr(); printf("n=");scanf("%u",&n); Hanoi(n,1,2,3);getch(); }
5. Căutarea în vectori sortaţi (căutarea binară) Fie v este un vector de numere reale sortat crescător şi k este un număr real dat. Problema este de a identifica (dacă există) o valoare poz, astfel încât v[poz]=k. Rezolvarea ei porneşte cu luarea în considerare a întregului vector v şi
⎡n⎤
determinarea poziţiei mijlocului m = ⎢ ⎥ . Dacă v[m]=k, atunci poz:=m. Dacă ⎣2⎦ v[m]>k, atunci se procedează în acelaşi mod cu vectorul format din primele m componente din v, altfel cu cel format din componentele v[m+1],…,v[n-1]. Se generează astfel subvectori din ce în ce mai mici până când valoarea k este găsită sau până când nu mai poate fi generat un nou subvector. #include <stdio.h> #include int cauta_binar(float *,int,int,float); void main() { clrscr(); printf("Dimensiunea vectorului:"); int n; scanf("%i",&n); printf("Elementele vectorului\n"); float v[100]; for(unsigned i=0;ils) return -1; int mij=(li+ls)/2; if(v[mij]==k)
36
Subprograme recursive return mij; if(v[mij]>k) return cauta_binar(v,li,mij-1,k); return cauta_binar(v,mij+1,ls,k); }
6. Sortarea crescătoare prin inserare Pentru sortarea crescătoare a unei secvenţe de numere reale se poate raţiona astfel: dacă P(n) este problema sortării crescătoare a secvenţei a1, a2,…,an şi P(n-1) este problema sortării primelor n-1 componente, atunci soluţia problemei P(n) rezultă din soluţia problemei P(n-1) prin inserarea lui an în soluţia problemei P(n-1). Fiecare problemă intermediară P(k), k = 2 ,..., n este rezolvată aplicând aceeaşi metodă P(1) fiind o problemă „gata rezolvată” (condiţie terminală). Funcţia insera realizează inserarea valorii x în vectorul v în poziţia „corectă”. Funcţia recursivă inssort realizează sortarea vectorului cu n componente prin inserţie. void insera(float *v,int *n,float x) { for(int i=0;(i<*n)&&(x>v[i]);i++); for(int j=*n;j>=i+1;j--)v[j]=v[j-1]; v[i]=x;(*n)++; } void inssort(float *v,int n) { if(n) { inssort(v,n-1);int m=n-1; insera(v,&m,v[n-1]); } }
7. Pot fi realizate desene prin compunerea într-o manieră recursivă a unor figuri geometrice primitive. Compunerea constă în repetarea primitivelor considerate şi a rezultatelor obţinute prin rotirea lor într-un sens sau celălalt. Astfel, dacă mulţimea de primitive H0 constă dintr-un punct şi pentru compunere este considerat un segment de lungime h, atunci: H1 rezultă din patru exemple (cópii, realizări, instanţe, clone) de primitive din H0 unite prin segmente de lungime h; H2 rezultă din 16 exemple din H0 unite prin 15 segmente de lungime h/2 ş.a.m.d. De asemenea, H2 se poate obţine prin interconectarea a patru cópii ale lui H1 rotite cu unghiuri drepte şi prin interconectarea punctelor izolate prin segmente de aceeaşi lungime. Generalizând, o curbă Hn rezultă din patru cópii ale unei curbe Hn-1, punctele izolate fiind unite prin segmente de lungime hn=h/2n. Curbele rezultate se numesc curbele Hilbert Hi, i ≥ 0.
37
Programarea calculatoarelor
H1
H2
H3
Dacă cele patru părţi ale unei curbe Hilbert Hk sunt notate A, B, C, D şi se reprezintă prin săgeţi rutinele care desenează segmentele care le interconectează, atunci rezultă următoarele scheme recursive.
A: D ← A↓ A→ B B: C↑B→B↓ A A:
D ← A↓ A→ B
C: B→C ↑C ← D D: A↓ D← D↑C Prin executarea următoarei surse C sunt obţinute curbele Hilbert H4. #include #include #include #include
<stdio.h> <stdlib.h>
const n=5; const h0=480; int int int int void void void void
i=0; h; x,y,x0,y0,gm; gd=DETECT; A(int); B(int); D(int); C(int);
void main() { clrscr(); initgraph(&gd,&gm,"D:\BC\BGI"); setbkcolor(0); setcolor(4); h=h0;y0=x0=h/2; do{ i++;h/=2; x0+=h/2;y0+=h/2; x=x0;y=y0;moveto(x,y); A(i); }
38
Subprograme recursive while(i0) { D(i-1);x-=h;lineto(x,y); A(i-1);y-=h;lineto(x,y); A(i-1);x+=h;lineto(x,y); B(i-1); } } void B(int i) { if (i>0) { C(i-1);y+=h;lineto(x,y); B(i-1);x+=h;lineto(x,y); B(i-1);y-=h;lineto(x,y); A(i-1); } } void C(int i) { if (i>0) { B(i-1);x+=h;lineto(x,y); C(i-1);y+=h;lineto(x,y); C(i-1);x-=h;lineto(x,y); D(i-1); } } void D(int i) { if (i>0) { A(i-1);y-=h;lineto(x,y); D(i-1);x-=h;lineto(x,y); D(i-1);y+=h;lineto(x,y); C(i-1); } }
39
Programarea calculatoarelor
Curba Hilbert obţinută este
8. În cazul curbelor Hilbert, toate unghiurile determinate de segmentele care unesc punctele sunt de măsură 900. Dacă se consideră ca valori pentru măsurile unghiurilor determinate de aceste segmente 450, 900, 1350, rezultă curbele Sierpinski Sn, n ≥ 1. Curba Sierpinski S2 este,
Recursia pentru obţinerea curbelor Sierpinski poate fi descrisă astfel. S: A A: A
B
C
D
B⇒D
A
40
Subprograme recursive
B: B
C⇓ A
B
C: C
D⇐B
C
D: D
A⇑ C
D
unde săgeţile duble indică segmente de lungime 2h. Următorul program desenează curbele Sierpinski S4. #include #include #include #include
<stdio.h> <stdlib.h>
const n=4; const h0=412; int i=0; int h; int x,y,x0,y0,gm; int gd=DETECT; void A(int); void B(int); void D(int); void C(int); void main() { clrscr(); initgraph(&gd,&gm,"d:\bc\bgi"); setbkcolor(15); setcolor(8); h=h0/4; x0=2*h; y0=3*h; do{ i++; x0-=h;h/=2;y0+=h; x=x0;y=y0; moveto(x,y); A(i);x+=h;y-=h;lineto(x,y); B(i);x-=h;y-=h;lineto(x,y); C(i);x-=h;y+=h;lineto(x,y); D(i);x+=h;y+=h;lineto(x,y);} while(i!=n); getch(); closegraph(); } void A(int i) { if (i>0) { A(i-1);x+=h;y-=h; lineto(x,y); B(i-1);x+=2*h; lineto(x,y);
41
Programarea calculatoarelor D(i-1);x+=h;y+=h; lineto(x,y); A(i-1); } } void B(int i) { if (i>0) { B(i-1);x-=h;y-=h; lineto(x,y);C(i-1); y-=2*h; lineto(x,y); A(i-1);x+=h;y-=h; lineto(x,y); B(i-1); } } void C(int i) { if (i>0) { C(i-1);x-=h;y+=h; lineto(x,y); D(i-1);x-=2*h; lineto(x,y); B(i-1);x-=h;y-=h; lineto(x,y); C(i-1); } } void D(int i) { if (i>0) { D(i-1);x+=h;y+=h; lineto(x,y); A(i-1);y+=2*h; lineto(x,y); C(i-1);x-=h;y+=h; lineto(x,y); D(i-1); } }
Rezultatul execuţiei programului este prezentat în următoarea figură.
42
Subprograme recursive
43
4 Tipul de dată articol
Articolul este o structură de date eterogenă, cu acces direct la elementele sale, între care există o relaţie de ordine ierarhică.
4.1 Caracteristici generale şi mod de declarare Articolul poate fi reprezentat sub formă de arbore, ale cărui noduri sunt asociate componentelor structurii. Componentele de pe ultimul nivel sunt scalare şi se numesc date elementare sau câmpuri. Datele de pe celelalte niveluri, denumite date de grup, se constituie prin agregarea datelor de pe nivelurile inferioare. Data de grup de cel mai înalt nivel (rădăcina arborelui) corespunde articolului în ansamblu. Conceptual, datele de grup de pe diverse niveluri au aceleaşi proprietăţi ca şi articolul, ceea ce permite ca această structură să fie construită recursiv, prin descompunerea în structuri cu aceleaşi proprietăţi (figura 4.1). Declararea împreună a tipului articol şi a variabilelor de acest tip se realizează conform sintaxei: struct tip_articol{lista_campuri} var1,var2,…,varn;
unde tip_articol este identificatorul asociat tipului articol, iar var1, var2,…, varn sunt identificatorii asociaţi variabilelor de tipul articol declarat. Parametrii declaraţiei pot lipsi (dar nu toţi deodată). Dacă lipsesc parametrii var1, var2,…, varn, atunci tip_articol trebuie să fie prezent, fiind numai o declarare explicită de tip nou, utilizabil ulterior la alte declarări. Dacă lipseşte tip_articol, atunci trebuie să fie prezentă lista de variabile (nevidă), caz în care este vorba de o declarare de variabile de tip articol, fără însă a declara şi un tip utilizator nou. În continuare, tip_articol este un tip nou de date, iar var1, var2,…, varn sunt variabile de tipul tip_articol. Variabilele pot fi declarate şi ca masive, ale căror
44
Tipul de dată articol
elemente sunt de tip articol: var1[dim1][dim2]…[dimn]. DATA ZI
LUNA
dată de grup (articol) AN
date elementare
a) dată de grup (articol)
PERSOANA
dată de grup (articol) NUME
ADRESA
DATA NAŞTERII
date elementare
ZI
LUNA
AN
b)
Figura 4.1 Exemple de structuri de articole O variabilă de tip articol poate fi declarată şi ulterior definirii tipului: struct tip_articol var1;
Descrierea constituie o definire implicită de un nou tip de dată. Este posibilă definirea explicită a unui nou tip de dată, adăugând cuvântul rezervat typedef în faţa declarării (în acest caz nu mai pot fi declarate simultan şi variabile). Lista_campuri este o înşiruire de declaraţii de câmpuri separate prin punct şi virgulă, asemănătoare declaraţiilor de variabile, de forma tip_camp nume_camp. Câmpurile unei structuri pot fi variabile simple, masive sau alte articole. Lista câmpurilor nu poate fi vidă. Exemplu: definirea tipului de dată număr complex, a unei variabile simple şi a unui masiv unidimensional cu elemente de acest tip se poate face în oricare din următoarele variante (pentru un număr complex se vor reţine partea reală şi partea imaginară): a) struct COMPLEX{float r,i;}a,b[100]; b) struct COMPLEX{float r,i;}; struct COMPLEX a,b[100]; c) struct COMPLEX{float r,i;}; COMPLEX a,b[100]; d) struct {float r,i;}COMPLEX; COMPLEX a,b[100]; e) typedef struct {float r,i;} COMPLEX; COMPLEX a,b[100]; f) typedef struct COMPLEX{float r,i;}; struct COMPLEX a,b[100]; g) typedef struct COMPLEX{float r,i;}; COMPLEX a,b[100];
45
Programarea calculatoarelor
Din punct de vedere practic, utilizarea tipului articol este strâns legată de prelucrarea fişierelor. În lucrul cu variabilele de tip articol se recomandă declararea identificatorului de tip. În acest mod, identificatorul de tip articol poate fi folosit în definirea mai multor variabile. În procesul de descriere a unui articol, arborele se parcurge în preordine (de la rădăcină spre extremităţi şi de la stânga la dreapta). Exemplu: pentru exemplele din figura 4.1, declararea poate fi realizată prin definire recursivă, astfel: struct tip_data { unsigned zi; char luna[3]; int an; }; struct persoana { char nume[30]; char adresa[50]; struct tip_data data_nasterii; } angajat;
Dacă nu ar fi existat declaraţia tipului articol tip_data, atunci tipul persoana putea fi scris astfel: struct persoana { char nume[30]; char adresa[50]; struct { unsigned zi; char luna[3]; int an; } data_nasterii; } angajat;
Variabilele de tip articol se reprezintă intern ca succesiuni de câmpuri elementare, cu reprezentarea internă şi lungimea fizică specifice tipurilor lor. Lungimea zonei de memorie rezervată pentru variabila de tip articol rezultă din însumarea lungimilor câmpurilor. Aceasta nu poate depăşi 65520 octeţi (ca orice variabilă de tip structurat). Pentru structura unui articol îşi dovedeşte utilitatea operatorul sizeof, care asigură determinarea lungimii zonei de memorie asociate unei variabile sau unui tip de date. Exemplu: Considerând declaraţiile anterioare, expresia sizeof(data_nasterii) are valoarea 8, iar sizeof(angajat) are valoarea 90. Din punct de vedere fizic, identificatorii câmpurilor din descrierea articolului reprezintă deplasări faţă de începutul acestuia. Adresa fizică a unui câmp rezultă din însumarea adresei articolului cu deplasarea sa. Structura arborescentă a articolelor poate fi exprimată sugestiv şi prin machete, care evidenţiază componentele, natura, lungimea declarată şi lungimea fizică ale acestora (figurile 4.2 şi 4.3).
46
Tipul de dată articol
4.2 Referirea articolului şi a elementelor componente Datele de tip articol pot fi referite în două moduri: global sau pe componente. Referirea globală este permisă numai în operaţia de atribuire, cu condiţia ca ambele variabile (sursă şi destinaţie) să fie articole de acelaşi tip. Referirea pe componente (prin numele lor) este o reflectare a faptului că articolul este o structură cu acces direct. Referirea unor componente de tip articol din structura altui articol este posibilă numai în operaţia de atribuire, în condiţiile precizate anterior la referirea globală. În cele ce urmează se are în vedere numai referirea componentelor de tip dată elementară, situate pe ultimul nivel al structurii. Referirea câmpurilor unei structuri se face prin calificare, folosind operatorul. (punct). În referirea prin calificare, asigurarea identificării unice a câmpurilor se realizează prin asocierea numelui acestora cu numele articolului care le conţine. Construcţia rămâne la această formă în cazul în care structura are numai două niveluri: articolul şi câmpurile elementare ale acestuia. Exemple: Folosind tipul COMPLEX definit anterior, avem:
a.r , a.i - se referă partea reală, respectiv imaginară a variabilei a b[10].r - se referă partea reală a celui de-al 11-lea element al vectorului b #include <string.h> main() { struct articol {char nume[40]; char adresa[30]; int an, luna, zi;} struct articol pers; …………… strcpy(pers.nume, "Popescu Ion"); strcpy(pers.adresa, "Bucuresti, Pta. Romana 6"); pers.an=1979; pers.luna=3; pers.zi=15; }
În articolele cu structură recursivă se realizează calificarea progresivă cu articolele de pe nivelurile superioare, primul calificator fiind numele articolului rădăcină. În lanţul de calificări, numele articolului rădăcină este nume de variabilă, celelalte fiind nume de câmpuri ale articolului. Dacă anumite componente sunt structuri de date de alte tipuri (de exemplu masive sau şiruri de caractere), în referirea elementelor lor se aplică, pe lângă calificare, regulile specifice acestor structuri. Exemple: 1. Referirea prin calificare a câmpurilor articolului angajat de tipul persoana (vezi exemplele anterioare) se realizează astfel: angajat.nume; angajat.adresa; angajat.data_nasterii.zi; angajat.data_nasterii.luna; angajat.data_nasterii.an
47
Programarea calculatoarelor
În aceste referiri, angajat este identificatorul variabilei articol, celelalte elemente sunt identificatori de câmpuri. Construcţiile angajat.nume şi angajat.adresa corespund referirii globale a câmpurilor respective, care sunt şiruri de caractere. Pentru a referi, de exemplu, primul caracter din şir, se scrie: angajat.nume[0]. 2. Se presupune un articol cu structura din figura 4.2. Cod Magazin
Vînzări lunare Luna 2 … Real … 4 …
Luna 1 real 4
Întreg 2
Luna 12 real 4
Figura 4.2 Structura de articol pentru exemplul 2 Articolul se declară astfel:
struct magazin { int cod_magazin; float vanzari_lunare[12]; } articol;
Articolul are 50 de octeţi, iar referirea câmpurilor se realizează astfel: articol.cod_magazin; articol.vanzari_lunare[i], cu i=0,1,…,11.
3. Se presupune un articol cu structura din figura 4.3. ...
Materia primă 30
Cod
Norma de consum
...
Cod
Norma de consum
întreg
întreg
real
...
întreg
real
1
2
4
...
2
4
Cod produs
Număr materii prime
întreg 2
Materia primă 1
Figura 4.3 Structura de articol pentru exemplul 3 Cu toate că numărul de materii prime utilizate poate fi variabil de la un produs la altul, în descrierea articolului se alege valoarea maximă a acestuia: struct a { int cod_mat; float norma; }; struct produs { int cod_produs; unsigned char nr_mat; struct a materii_prime[30]; } articol ;
Articolul are 183 de octeţi, iar referirea câmpurilor se realizează astfel:
articol.cod_produs; articol.nr_mat; articol.materii_prime[i].cod_mat; articol.materii_prime[i].norma;
Constantele de tip articol sunt cu tip şi păstrează caracteristicile acestora, descrise în § 4.2. În momentul compilării se rezervă zone de memorie pentru acestea, iar câmpurile articolelor sunt iniţializate cu valorile precizate de utilizator. Declararea constantelor presupune definirea anterioară a tipului articol. Valoarea iniţială trebuie să fie de acelaşi tip cu câmpul căruia îi corespunde. Când articolul conţine la rândul său alt articol, identificarea câmpului care se iniţializează se face pe niveluri, folosind perechi corespunzătoare de acolade.
48
Tipul de dată articol
Exemple:
#include <stdio.h> void main() { //exemplul 1 struct persoana { char nume[40]; char adresa[30]; struct { int zi, luna, an;} datan; }; //exemplul 2 struct magazin { int cod_magazin; float vanzari_lunare[12]; }; //exemplul 3 struct a { int cod_mat; float norma;}; struct produs { int cod_produs; unsigned char nr_mat; struct a materii_prime[30]; }; //Initializarea articolului din exemplul 1: struct persoana p={"Popescu Ion", "Bucuresti, Magheru 14", 2, 4, 1960}; //sau cu evidentierea structurii data nasterii: struct persoana p1={"Popescu Ion", "Bucuresti, Magheru 14", {2, 4, 1960}}; printf("\n%i",p1.datan.an); //Initializarea articolului din exemplul 2: struct magazin gigel_srl={200, 1,2,3,4,5,6,7,8,9,10,11,12}; //sau cu evidentierea structurii de masiv: struct magazin gigel_srl1={200, {1,2,3,4,5,6,7,8,9,10,11,12}}; printf("\n%6.2f",gigel_srl1.vanzari_lunare[10]); //Initializarea articolului din exemplul 3 (doar primele 4 materii //prime, restul de 26 vor fi initializate automat cu valori nule: struct produs z={243,5,{{2420,25.4},{3251,70.21},{1421,8.4},{51,7.2}}}; printf("\n%6.2f",z.materii_prime[2].norma); }
4.3 Articole cu structuri complexe În activitatea de programare pot fi întâlnite aplicaţii care reclamă utilizarea articolelor cu structură variabilă. La iniţializarea câmpurilor unui astfel de articol, constanta de tip articol se asociază unei singure structuri, deoarece zona de memorie rezervată pentru articol este unică. Pentru acest tip de articol, limbajul pune la dispoziţia utilizatorilor tipul predefinit reuniune (union), care se comportă ca şi tipul struct cu o singură diferenţă: la un moment dat al execuţiei programului, în zona de memorie rezervată articolului nu este memorat decât unul dintre câmpurile acestuia.
49
Programarea calculatoarelor
Declararea tipului reuniune se realizează astfel: union nume_tip { tip_cimp1 cimp1; tip_cimp2 cimp2; ................ tip_cimpn cimpn;};
Lungimea zonei de memorie rezervate pentru o variabilă de tip reuniune va fi egală cu maximul dintre lungimile câmpurilor componente. Gestiunea conţinutului respectivei zone de memorie va trebui realizată de către programator. Exemplu: Se presupune un articol cu structura din figura 4.4. Nume
char[40]
Data naşterii lun zi an a
Forma de învăţământ
An de studiu int
zi
id
bursa
valoare
loc de muncă
char
float
char[30]
data angajării zi
lună
an
Figura 4.4 Articol cu structură variabilă Declararea şi iniţializarea câmpurilor unui student la zi pentru structura articolului din figura 6.4 se realizează astfel: #include <stdio.h> void main() {//Declararea articolului cu structura variabila: struct articol { char nume[40]; struct { int zi, luna, an;} datan; int an_st; char forma_inv; union { struct {char bursa; float valoare;}zi; struct {char loc_m[30]; struct {int zi, luna, an;}data_ang; }id; } parte_vb; }; //Initializarea campurilor unui student la zi: struct articol a={"Popescu Felix",{4,1,1974} ,1,'Z',{'D',250.5}}; printf("\nData nasterii: %i.%i.%i, Forma de inv.: %c, Val. bursa: %6.2f", a.datan.zi, a.datan.luna, a.datan.an, a.forma_inv, a.parte_vb.zi.valoare); }
Din punct de vedere fizic, existenţa părţii variabile într-un articol generează, la compilare, deplasări egale faţă de începutul articolului pentru toate variantele de descriere. Astfel, pentru descrierea din exemplul de mai sus se generează deplasarea 49 faţă de începutul articolului, atât pentru câmpul bursa, cât şi pentru loc_m.
50
Tipul de dată articol
4.4 Constante de tip articol Constantele de tip articol pot fi constante cu tip (variabile iniţializate la compilare) şi constante „obiect”, pentru care în momentul compilării se rezervă zone de memorie, iar câmpurile articolelor sunt iniţializate cu valorile precizate de utilizator. Valoarea iniţială trebuie să fie de acelaşi tip cu câmpul căruia îi corespunde. Când articolul conţine la rândul său alt articol, identificarea câmpului care se iniţializează se face pe niveluri, folosind perechi corespunzătoare de acolade. Constantele cu tip joacă rol de variabile care se iniţializează cu o valoare în faza de compilare, ele putând să-şi modifice valoarea pe parcursul execuţiei programului. tip nume_const = {lista_valori};
Constantele obiect sunt variabile iniţializate la declarare, pentru care se rezervă memorie, dar conţinutul lor nu poate fi modificat pe parcursul programului. const tip nume_const = {lista_valori};
Exemplul 1: #include<stdio.h> void main() { struct persoana { char nume[40]; char adresa[30]; struct { int zi, luna, an;} datan; }; persoana pers={"Popescu Ion", "Bucuresti; Magheru 14", {2, 4, 1960}}; //constanta cu tip pers.datan.zi=4; }
Exemplul 2: #include<stdio.h> void main() { struct persoana { char nume[40]; char adresa[30]; struct {int zi, luna, an;} datan; }; const persoana pers={"Popescu Ion", "Bucuresti; Magheru 14", {2, 4, 1960}}; //constanta obiect // pers.datan.zi=4; genereaza eroare la compilare }
51
5 Fişiere de date
Prelucrarea automată a datelor presupune un sistem de organizare a acestora după metode şi procedee specifice. Organizarea datelor este un proces complex care include identificarea, clasificarea şi descrierea proprietăţilor acestora, gruparea lor în colecţii, reprezentarea pe purtători tehnici, definirea şi realizarea procedurilor de prelucrare etc. Deoarece datele se memorează, de obicei, pe purtători tehnici de informaţii, dar se prelucrează numai când sunt prezente în memoria internă, acestea trebuie organizate atât extern cât şi intern. În organizarea externă a datelor se identifică două niveluri de abordare, după cum se are în vedere acest proces din perspectiva utilizatorului sau a purtătorilor fizici externi pe care se înregistrează datele. Cele două niveluri de abordare, numite logic, respectiv fizic, precum şi realizarea trecerii de la unul la celălalt, în condiţiile specifice diverselor sisteme de calcul, se bazează pe o serie de concepte, cum ar fi: fişierul şi articolul, purtătorul tehnic de date, metoda de organizare şi modul de acces, operaţiile de prelucrare etc.
5.1 Fişierul şi articolul Fişierul reprezintă termenul generic care desemnează structurile de date externe. El este o mulţime (colecţie) de date omogene din punct de vedere al semnificaţiei şi al cerinţelor de prelucrare. În purtătorul extern, fişierul are, pe lângă partea de date, şi alte informaţii de identificare (etichete). Privit din punctul de vedere al prelucrării, un fişier este o colecţie ordonată de date, numite articole. Articolul este constituit dintr-o mulţime ordonată de valori ale unor caracteristici ce aparţin, uzual, unei singure entităţi (obiect, fenomen, proces etc.) din domeniul de activitate abordat. De exemplu, într-un fişier care conţine datele personale ale salariaţilor dintr-o unitate economică, un articol grupează valorile caracteristicilor unei singure persoane.
52
Fişiere de date
Componentele articolului destinate diverselor caracteristici sunt denumite câmpuri de date. Depinzând de natura, ordinul de mărime şi forma de reprezentare externă a valorilor asociate, fiecare câmp de date are o lungime, exprimată uzual în octeţi. Lungimea unui articol este dată de suma lungimilor câmpurilor care îl compun. După cum toate articolele dintr-un fişier au sau nu aceeaşi lungime, se face distincţie între fişierele cu articole de lungime fixă sau variabilă. Modul de implementare fizică a celor două tipuri de fişiere diferă de la un sistem la altul şi chiar de la un limbaj la altul. Pe purtătorul fizic extern, partea de date a fişierului se prezintă ca o succesiune de octeţi cu un conţinut binar fără semnificaţie informaţională. În momentul prelucrării, prin descrieri şi operaţii adecvate, din succesiunea memorată extern se „decupează" entităţi (articole, blocuri, linii sau câmpuri) cu structuri corespunzătoare prelucrării. Tipul entităţii care se „decupează" depinde de tipul fişierului.
5.2 Metode de organizare şi tipuri de acces Principiile şi regulile după care se memorează articolele unui fişier pe purtătorul extern, cu asigurarea protecţiei şi regăsirii acestora, constituie metoda de organizare. În evoluţia organizării datelor externe s-au cristalizat mai multe metode, dintre care, cele mai uzuale sunt secvenţială, relativă şi indexată. Principala diferenţiere între metodele de organizare o reprezintă tipurile de acces admise. Tipul de acces reprezintă modalitatea de regăsire (localizare) a articolelor din fişier. Noţiunea de acces trebuie aplicată atât pentru operaţia de scriere, cît şi pentru cea de citire a datelor. Poziţia din/în care se face citirea/scrierea în cadrul fişierului este indicată de un pointer. Accesul la datele înregistrate pe un purtător tehnic poate fi secvenţial sau direct, în funcţie de modul în care se stabileşte pointerul. Accesul secvenţial este posibil la toţi purtătorii tehnici de date şi presupune înscrierea înregistrărilor în ordinea furnizării lor sau regăsirea în ordinea în care au fost înscrise în suport (figura 5.1). P(Ak)=f (P(Ak-1)) Traversare
A1
A2
...
Ak-1
Ak
...
An
EOF
Figura 5.1 Principiul de realizare a accesului secvenţial la articole Pointerul de fişier avansează, în scriere şi citire, de la o entitate (articol, bloc, linie sau câmp) la alta. Dacă pointerul se exprimă prin deplasare faţă
53
Programarea calculatoarelor
de începutul fişierului, atunci, matematic, acest lucru se poate exprima astfel: P(A1) = 0; P(Ak) = f(P(Ak-1)) = P(Ak-1)+lartk-1; pentru k=2,n; unde Ak este articolul k şi lartk este lungimea articolului k. O problemă importantă care se pune la consultarea (citirea) în acces secvenţial este controlul ajungerii la sfârşitul fişierului. După citirea ultimei entităţi (articol, bloc, linie sau câmp), pointerul indică marcatorul de sfârşit de fişier EOF (figura 5.2). Poziţia pointerului după citirea ultimului articol
A1
A2
...
Ak-1
Ak
...
An
EOF
Figura 5.2 Pointerul după citirea ultimului articol din fişier În limbajele de programare se regăsesc două modalităţi de sesizare a sfârşitului de fişier: a) Sesizarea sfârşitului de fişier în cadrul operaţiei de citire (limbajele FORTRAN, COBOL, C). Sfârşitul este sesizat la citirea marcatorului de sfârşit de fişier. Situaţia din figura 5.2 nu este considerată sfârşit de fişier. Abia la următoarea citire se întâmplă acest lucru (pointerul de fişier avansează după marcatorul de sfârşit de fişier). b) Sesizarea sfârşitului de fişier independent de operaţia de citire (limbajele BASIC, PASCAL). În acest caz, dacă pointerul este pe marcatorul de sfârşit de fişier (după ultimul articol, bloc, linie, câmp, ca în figura 5.2) se consideră sfârşit de fişier. Următoarea citire produce eroare de intrare/ieşire (I/E). Proiectarea algoritmilor de prelucrare a fişierelor este determinată de modalitatea în care se sesizează sfârşitul de fişier. Accesul direct este posibil numai la fişierele care au o anumită organizare, au ca entitate de transfer articolul sau blocul şi sunt memorate pe discuri magnetice. Accesul direct se bazează pe existenţa unui algoritm implementat în sistem care asigură regăsirea (localizarea) articolelor în funcţie de o informaţie de regăsire. Valoarea pointerului este determinată direct, fără să depindă de valoarea sa anterioară: P(Ak)=f(irk), unde Ak este articolul k, iar irk este o informaţie de regăsire a articolului k. În funcţie de algoritmul şi informaţia de regăsire, există două tipuri de acces direct: după cheie şi după numărul relativ al articolului. În cazul accesului direct după cheie, articolul este regăsit prin aplicarea unui algoritm asupra unei informaţii de identificare de tip cheie: P(Ak)=f(cheiek). În cazul accesului direct după numărul relativ - care se mai numeşte, simplu, acces relativ - (figura 5.3), articolul este localizat în fişier prin numărul său, stabilit, în cadrul fişierului, de la valoarea zero: P*(Ak)=(k-1); P(Ak)=P*(Ak)×lart. P*(Ak) reprezintă poziţia exprimată în număr relativ, iar P(Ak) reprezintă poziţia exprimată
54
Fişiere de date
prin deplasare, în octeţi, faţă de începutul fişierului (la unele sisteme numărul relativ este stabilit de la unu: P*(Ak)=k). La scriere, articolul Ak (numărul relativ k-1) se memorează pe poziţia sa, celelalte k-1 articole anterioare putând să nu existe (pe suport există însă rezervat loc pentru ele). La citire, articolul Ak (cu numărul relativ k-1, kn) este localizat direct şi conţinutul lui se transferă în memoria internă. Acces direct prin numărul relativ k-1 P(Ak)=k-1
A1 0
A2
...
Ak-1
Ak
...
An
1
...
k
k-1
...
n-1
EOF
Număr relativ
Figura 5.3 Principiul de realizare a accesului direct prin număr relativ Fişierele organizate secvenţial, cu articole de lungime variabilă, admit numai accesul secvenţial. Fişierele organizate secvenţial, cu articole sau blocuri de lungime fixă, admit atât accesul secvenţial, cât şi pe cel relativ. Acest lucru derivă din faptul că accesul relativ este realizat de sistem printr-o deplasare secvenţială faţă de începutul acestuia, deplasare care este egală cu valoarea expresiei: număr_relativ × lungime_articol.
5.3 Structura sistemului de fişiere sub MS-DOS/Windows Sistemul de operare MS-DOS utilizează o formă logică arborescentă de grupare a fişierelor de pe discuri în directoare şi subdirectoare. Un director (subdirector) poate conţine fişiere şi/sau alte subdirectoare (figura 5.4). În limbajul curent folosit de practicieni se utilizează noţiunea de director şi în cazul subdirectoarelor. Un disc DOS conţine un singur director rădăcină, care la rândul lui are zero sau mai multe subdirectoare şi/sau fişiere. Subdirectoarele pot avea oricâte niveluri de imbricare. Frunzele arborelui sunt, cel mai adesea, fişiere, dar pot fi şi subdirectoare vide. Unitatea de disc, directorul şi subdirectorul în care se lucrează la un moment dat se numesc curente. Pentru ca un fişier să fie localizat în cadrul structurii arborescente se foloseşte un identificator extern (specificator) care are următoarea formă sintactică: [n:][cale][\]nume_fişier[.extensie] n este numele unităţii de disc (A:, B:, C: etc.). Prin lipsă, se consideră unitatea curentă; cale (path) este calea de acces de la directorul rădăcină până la subdirectorul dorit. Fiecare nume de director (subdirector) din interiorul căii este
55
Programarea calculatoarelor
precedat de caracterul backslash (\). Prin lipsă, se consideră calea subdirectorului curent. Calea selectată la un moment dat poate începe de la rădăcină sau de la subdirectorul curent. Când calea începe cu caracterul backslash (\) căutarea începe de la rădăcină; în caz contrar, căutarea începe de la directorul curent. Rădăcina (pe discul C:\)
F4
F1
D1
D2
F2
D4
D5
F8
D3
F5
F2
F6
F3
F7
F9
Figura 5.4 Exemplu de structură arborescentă de directori Fiecare subdirector conţine două intrări speciale marcate prin caracterul ".", respectiv caracterele ".." în locul numelui de fişier. Prima intrare realizează „autopunctarea”, indicând faptul că entitatea este subdirector (nu fişier de date), a doua intrare „punctează” subdirectorul părinte. În construirea căii de acces se poate folosi succesiunea de două puncte pentru a indica subdirectorul părinte. nume_fişier este numele extern al fişierului, format din maxim 8 caractere alfanumerice, mai puţin unele caractere speciale, ca: ." \ / : ' > < + = ; , ). Există o serie de nume prestabilite, asociate unor dispozitive standard de intrare/ieşire, care nu pot fi utilizate de programator pentru propriile fişiere: CON, AUX, COM1, COM2, LPT1, LPT2, LPT3, NULL, PRN, CLOCK$ (dispozitiv pentru ceasul de timp real). extensia este formată din maxim trei caractere alfanumerice prin care utilizatorul are posibilitatea să-şi identifice fişiere cu conţinuturi diferite. Prin lipsă nu se asumă nicio valoare. Exemple (structura din figura 5.4): 1. C:\F1 → Fişierul F1 din rădăcină; 2. C:\D2\F2 → Fişierul F2 din subarborele C:-D2; 3. C:\F2 → Fişierul F2 din directorul rădăcină; 4. C:\D2\D4\F9 → Fişierul F9 din subarborele C:-D2-D4; 5. Pentru a indica fişierul F9 se poate folosi una din scrierile: C:\D2\D4\F9 → de oriunde; \D2\D4\F9 → de oriunde din unitatea C:; ..\D4\F9 → din subdirectorul D5; F9 → din subdirectorul D4.
56
Fişiere de date
Producătorii de software au impus unele denumiri de extensii, care, deşi opţionale, oferă posibilitatea simplificării referirii fişierelor în unele comenzi sau aplicaţii. Exemple: 6. Standard MS-DOS: .COM .EXE .SYS .OBJ .BAT 7. Standarde de firmă: .ARC .ZIP .DBF 8. Formate ASCII: .ASM .BAS .PAS .CBL .C .TXT 9. Formate grafice: .PCX .MSP .WPG
→ program executabil; → program executabil; → driver de sistem; → program obiect; → fişiere de comenzi DOS (prelucrări BATCH). → arhivă compactată cu PKPAK sau ARC; → arhivă compactată cu PKZIP sau WINZIP; → bază de date DBASE. → program sursă ASSEMBLER; → program sursă BASIC; → program sursă PASCAL; → program sursă COBOL; → program sursă C; → fişier text; → Paint Brush; → Microsoft Windows; → WordPerfect.
5.4 Operaţii de prelucrare a fişierelor Asupra unui fişier se pot executa diverse operaţii de prelucrare, numite şi de gestiune, care se împart în operaţii la nivel de fişier şi la nivel de articol. Operaţiile la nivel de fişier se referă la aspecte ca: înscrierea fişierului în [sub]directoare, validarea şi interzicerea accesului la fişier (deschidere/închidere), ştergerea fişierului din [sub]directoare (ştergere) etc. Aceste operaţii se regăsesc, în totalitate, la prelucrarea fişierelor pe discuri magnetice. În cazul purtătorilor nereutilizabili, singurele operaţii care au sens sunt cele de deschidere/închidere a fişierelor. Operaţiile la nivel de articol se referă la accesul la entităţile de date ale fişierului (articole, blocuri, linii sau câmpuri) în vederea prelucrării lor. Privite sub aspectul semnificaţiei pentru utilizator, aceste operaţii se referă la: înscrierea iniţială a entităţilor pe purtătorul tehnic (populare), actualizarea fişierului prin includerea de noi entităţi (adăugare), modificarea valorilor unor câmpuri din
57
Programarea calculatoarelor
anumite entităţi (modificare), eliminarea entităţilor care nu mai sunt necesare (ştergere), regăsirea entităţilor în vederea satisfacerii unor cerinţe de informare (consultare). În programele C, operaţiile de I/E sunt realizate cu ajutorul unei mulţimi de funcţii specializate pentru căutare, scriere, citire etc. În concluzie, dacă din punctul de vedere al utilizatorului operaţiile de prelucrare se descriu relativ simplu, prin apeluri de funcţii, realizarea efectivă a lor de către sistemul de calcul este complexă. În sistemul de operare MS-DOS sunt incluse funcţii de întrerupere care, prin intermediul BIOS (Basic Input Output System), lansează anumite operaţii cu un echipament. Din punct de vedere al reprezentării datelor în suportul extern, se disting fişiere text, în care toate datele sunt sub formă ASCII (un caracter/octet) şi fişiere binare, în care toate datele sunt memorate în forma identică cu cea din memoria principală (MP). Strâns legat de lucrul cu cele două tipuri de fişiere este modul în care se face transferul datelor între memoria principală şi suportul extern: transfer posibil cu conversie (în cazul fişierelor text) şi transfer fără conversie (în cazul fişierelor binare). Trebuie făcută remarca, deosebit de importantă, că din punct de vedere fizic fişierul se reprezintă în suportul extern ca o succesiune de octeţi. Această succesiune poate fi tratată logic ca un fişier de un tip sau altul. Este sarcina programatorului să asigure „suprapunerea” corectă a fişierului logic peste cel fizic. Din acest punct de vedere se poate spune că prin fişier logic se înţelege, prioritar, un mod de prelucrare şi mai puţin un mod de memorare. Indiferent de limbajul de programare folosit, operaţiile necesare pentru prelucrarea fişierelor sunt: • descrierea fişierului (crearea tabelei care memorează caracteristicile fişierului); • asignarea fişierului intern (numele logic) la unul extern (fizic); • deschiderea fişierului; • operaţii de acces la date („articole”); • închiderea fişierului. Pentru lucrul cu fişiere trebuie identificate tipurile acestora, metodele de organizare, modurile de acces şi tipurile de articole acceptate. Din punct de vedere al tipurilor de date, în C există un singur tip de fişiere: flux de octeţi (înşiruire de octeţi, fără niciun fel de organizare sau semnificaţie). Organizarea acestui flux de octeţi este secvenţială. Accesul la fişiere se poate face secvenţial sau direct (cu excepţia fişierelor standard, la care accesul este numai secvenţial). În bibliotecile limbajului există funcţii predefinite pentru prelucrarea fişierelor. Funcţiile de prelucrare la nivel superior a fişierelor tratează fluxul de octeţi acordându-i o semnificaţie oarecare. Putem spune că din punct de vedere al prelucrării, la acest nivel, ne putem referi la fişiere text şi fişiere binare. Există fişiere standard, care sunt gestionate automat de sistem, dar asupra cărora se poate interveni şi în mod explicit. Acestea sunt: • fişierul standard de intrare (stdin);
58
Fişiere de date
• fişierul standard de ieşire (stdout); • fierul standard pentru scrierea mesajelor de eroare (stderr); • fişierul standard asociat portului serial (stdaux); • fişierul standard asociat imprimantei cuplate la portul paralel (stdprn). Fişierele standard pot fi redirectate conform convenţiilor sistemului de operare, cu excepţia lui stderr care va fi asociat întotdeauna monitorului. În lucrul cu fişiere (sau la orice apel de sistem), în caz de eroare în timpul unei operaţii se setează variabila errno, definită în errno.h, stddef.h şi stdlib.h. Valorile posibile sunt definite în stdlib.h. În limbajul C există două niveluri de abordare a lucrului cu fişiere: nivelul inferior de prelucrare (fără gestiunea automată a zonelor tampon de intrare/ieşire) şi nivelul superior de prelucrare (se folosesc funcţii specializate de gestiune a fişierelor). În continuare, prin specificator de fişier se va înţelege un nume extern de fişier, conform convenţiilor sistemului de operare. Specificatorul de fişier poate să conţină strict numele fişierului sau poate conţine şi calea completă până la el.
5.4.1 Nivelul inferior de prelucrare a fişierelor Nivelul inferior de prelucrare este folosit rar, numai în programele de sistem. La acest nivel, descrierea fişierelor se realizează în corpul programelor, caracteristicile acestora obţinându-se din context. Maniera de prelucrare este asemănătoare celei de la nivelul sistemului de operare. Nu există un tip anume de dată, fişierul fiind referit printr-un index care indică intrarea într-o tabelă de gestiune a resurselor sistemului de operare. Acest index este de tip int şi se numeşte manipulator de fişier (handle). Manipulatorul este creat şi gestionat de către sistemul de operare. Utilizatorul îl foloseşte pentru a indica sistemului fişierul asupra cărui doreşte să facă prelucrări. Pentru utilizarea acestui nivel, în programul C trebuie incluse bibliotecile standard io.h, stat.h şi fcntl.h. Crearea şi asignarea unui fişier nou se realizează prin apelul funcţiei creat, care are următorul prototip: int creat(const char* numef, int protecţie);
Funcţia returnează manipulatorul fişierului nou creat; numef este un pointer spre un şir de caractere care defineşte specificatorul de fişier, iar protecţie defineşte modul de protecţie a fişierului creat (protecţia este dependentă de sistemul de operare). În biblioteca stat.h sunt definite următoarele valori pentru parametrul protecţie: S_IREAD (citire), S_IWRITE (scriere), S_IEXEC (execuţie). Aceste valori pot fi combinate folosind operatorul | (sau logic pe biţi). Funcţia creat poate fi apelată şi pentru un fişier existent. Efectul produs este ştergerea fişierului existent şi crearea unuia gol, cu acelaşi nume; conţinutul fişierului existent se pierde. În caz de eroare se returnează valoarea –1 şi se setează variabila globală
59
Programarea calculatoarelor
errno, care defineşte tipul erorii. Valorile obişnuite pentru errno sunt EBADF (manipulator eronat, nu a fost găsit fişierul) sau EACCES (fişierul nu poate fi accesat). Deschiderea unui fişier existent se realizează prin apelul funcţiei open, care are următorul prototip: int open(const char *path,int access[,unsigned mod]);
Funcţia returnează manipulatorul fişierului; numef este pointer spre un şir de caractere care defineşte specificatorul de fişier; acces este modul de acces la fişier; constantele care descriu modurile de acces la fişier sunt descrise în fcntl.h. Cele mai importante sunt: O_RDONLY – fişierul va fi accesat numai pentru citire; O_WRONLY – fişierul va fi accesat numai pentru scriere; O_RDWR – fişierul va fi accesat atât pentru citire, cât şi pentru scriere; O_CREAT: fişierul va fi creat ca nou. Aceste moduri pot fi combinate folosind operatorul |. Mod este folosit numai dacă parametrul acces conţine şi valoarea O_CREAT, caz în care indică modul de protecţie a acestuia: S_IWRITE – se permite scrierea în fişier; S_IREAD – se permite citirea din fişier; S_IREAD|S_IWRITE – se permite atât scrierea, cât şi citirea din fişier. Citirea dintr-un fişier se realizează prin apelul funcţiei read, care are următorul antet: int read(int nf, void* zonat, unsigned n);
Funcţia returnează numărul de octeţi citiţi din fişier; nf este manipulatorul de fişier (alocat la crearea sau deschiderea fişierului), zonat este un pointer spre zona tampon în care se face citirea (aceasta este definită de programator), iar n este dimensiunea zonei receptoare (numărul maxim de octeţi care se citesc). Numărul maxim de octeţi care pot fi citiţi este 65534 (deoarece 65535 – 0xFFF – se reprezintă intern la fel ca -1, indicatorul de eroare). În cazul citirii sfârşitului de fişier se va returna valoarea 0 (0 octeţi citiţi), iar la eroare se returnează -1 (tipul erorii depinde de sistemul de operare). Fişierul standard de intrare (stdin) are descriptorul de fişier 0. Scrierea într-un fişier se realizează prin apelul funcţiei write, care are următorul prototip: int write(int nf, void* zonat, unsigned n);
Funcţia returnează numărul de octeţi scrişi în fişier; nf este manipulatorul de fişier (alocat la crearea sau deschiderea fişierului), zonat este un pointer spre zona tampon din care se face scrierea (aceasta este definită de programator); n este numărul de octeţi care se scriu. Numărul maxim de octeţi care pot fi citiţi este 65534 (deoarece 65535 – 0xFFF – se reprezintă intern la fel ca -1, indicatorul
60
Fişiere de date
de eroare). În general, trebuie ca la revenirea din funcţia write, valoarea returnată să fie egală cu n; dacă este mai mică, s-a produs o eroare (probabil discul este plin). La scrierea în fişiere text, dacă în fluxul octeţilor care se scriu apare caracterul LF, write va scrie în fişier perechea CR/LF. În caz de eroare, valoarea returnată este -1 şi se setează variabila errno. Fişierul standard de ieşire (stdout) are manipulatorul 1, iar cel de eroare (stderr) are manipulatorul 2. Închiderea unui fişier se realizează prin apelul funcţiei close, care are următorul prototip: int close(int nf);
Funcţia returnează valoarea 0 (închidere cu succes) sau -1 (eroare); nf este manipulatorul de fişier. De asemenea, închiderea unui fişier se realizează automat, dacă programul se termină prin apelul funcţiei exit. Poziţionarea într-un fişier se realizează prin apelul funcţiei lseek, care are următorul prototip: long lseek(int nf, long offset, int start);
Funcţia returnează poziţia faţă de începutul fişierului, în număr de octeţi; nf este manipulatorul de fişier; offset este un parametru de tip long (numărul de octeţi peste care se va deplasa pointerul în fişier), iar start este poziţia faţă de care se face deplasarea: 0 (începutul fişierului), 1 (poziţia curentă în fişier) sau 2 (sfârşitul fişierului). La eroare returnează valoarea -1L. Exemple: 1. Apelul vb=lseek(nf, 0l, 2); Ù realizează poziţionarea la sfârşitul fişierului (în continuare se poate scrie în fişier folosind write); 2. Apelul vb=lseek(nf, 0l, 0); Ù realizează poziţionarea la începutul fişierului. Ştergerea unui fişier existent se realizează prin apelul funcţiei unlink, care are următorul prototip: int unlink(const char* numef);
Funcţia returnează 0 (ştergere cu succes) sau -1 (eroare); numef este un pointer spre un şir de caractere care defineşte specificatorul de fişier. În caz de eroare se setează variabila errno cu valoarea ENOENT (fişierul nu a fost găsit) sau EACCES (accesul interzis pentru această operaţie, de exemplu pentru fişiere read only). Pentru a putea şterge un fişier read only trebuie mai întâi schimbate
61
Programarea calculatoarelor
drepturile de acces la fişier, folosind funcţia chmod: int chmod(const char *cale, int mod);
unde cale este specificatorul de fişier, iar mod noile permisiuni. Permisiunile sunt aceleaşi ca la funcţia open. Rezultatul întors de chmod are aceeaşi semnificaţie ca şi unlink. Verificarea atingerii sfârşitului de fişier se face folosind funcţia eof: int eof(int nf);
unde nf este manipulatorul fişierului. Funcţia returnează valoarea 1 dacă pointerul este poziţionat pe sfârşitul fişierului, 0 în caz contrat şi -1 în caz de eroare (nu este găsit fişierul – errno primeşte valoarea EBADF). Exemplu: #include #include #include #include #include
<sys\stat.h> <string.h> <stdio.h>
int main(void) { int handle; char msg[] = "This is a test"; char ch; /* create a file */ handle = open("TEST.$$$", O_CREAT | O_RDWR, S_IREAD | S_IWRITE); /* write some data to the file */ write(handle, msg, strlen(msg)); /* seek to the begining of the file */ lseek(handle, 0L, SEEK_SET); /* reads chars from the file until we hit EOF */ do {read(handle, &ch, 1); printf("%c", ch);} while (!eof(handle)); close(handle); return 0;}
Bibliotecile limbajului conţin şi alte funcţii pentru prelucrarea fişierelor la nivel inferior, inclusiv variante ale funcţiilor anterioare, apărute odată cu dezvoltarea sistemelor de operare.
62
Fişiere de date
5.4.2 Nivelul superior de prelucrare a fişierelor La acest nivel, un fişier se descrie ca pointer către o structură predefinită (FILE – tabela de descriere a fişierului (FIB)): FILE* f;
Tipul FILE (descris în stdio.h) depinde de sistemul de operare. Fişierul este considerat ca flux de octeţi, din care funcţiile de prelucrare preiau secvenţe pe care le tratează într-un anumit fel (sau în care inserează secvenţe de octeţi). Funcţiile folosite la acest nivel pot fi împărţite în trei categorii: funcţii de prelucrare generale, funcţii de citire/scriere cu conversie şi funcţii de citire/scriere fără conversie. Funcţiile de prelucrare generală se aplică tuturor fişierelor, indiferent de tipul informaţiei conţinute; prelucrarea efectuată de acestea nu are niciun efect asupra conţinutului fişierului. Funcţiile care lucrează cu conversie se aplică fişierelor care conţin informaţie de tip text (linii de text, separate prin perechea CR/LF, iar la sfârşit se găseşte caracterul CTRL-Z). Funcţiile care lucrează fără conversie se aplică fişierelor care conţin informaţie binară. Funcţiile de citire/scriere deplasează pointerul de citire/scriere al fişierului, spre sfârşitul acestuia, cu un număr de octeţi egal cu numărul de octeţi transferaţi (fără a trece de sfârşitul de fişier).
Funcţii de prelucrare generală Deschiderea şi asignarea se realizează prin apelul funcţiei fopen. Funcţia returnează un pointer spre o structură de tip FILE (în care sunt înscrise date referitoare la fişierul deschis) sau NULL dacă fişierul nu se poate deschide: FILE* fopen(const char* nume_extern,const char* mod);
Parametrul nume_extern constituie specificatorul de fişier iar mod este un şir de caractere care specifică modul de deschidere a fişierului. Asignarea se realizează prin expresie de atribuire de tipul: nume_intern=fopen(sir_nume_extern,sir_mod);
Exemplu: FILE* f; f = fopen("PROD.DAT","r");
63
Programarea calculatoarelor
Modurile în care poate fi deschis un fişier sunt prezentate în tabelul 5.1. Modurile de deschidere a unui fişier Tabelul 5.1
Mod
Scop Deschide un fişier existent pentru adăugare la sfârşit (extindere) sau îl creează a dacă nu există. Este permisă numai scrierea. Numai pentru fişiere text. r Deschide un fişier existent numai pentru citire. Suprascrie un fişier existent sau creează unul nou, permiţându-se numai w operaţia de scriere. Deschide un fişier existent pentru adăugare la sfârşit (extindere) sau îl creează a+ dacă nu există. Sunt permise citiri şi scrieri. Numai pentru fişiere text. r+ Deschide un fişier existent pentru citire şi scriere Suprascrie un fişier existent sau creează unul nou, permiţându-se atât citiri, w+ cât şi scrieri.
La opţiunile de mai sus se poate adăuga b pentru fişiere binare sau t pentru fişiere text. Dacă nu este prezentă nici litera b, nici litera t, modul considerat depinde de valoarea variabilei _fmode: dacă valoarea este O_BINARY, se consideră fişier binar; dacă valoarea este O_TEXT, se consideră fişier text. De obicei implicită este valoarea O_TEXT. Modurile uzuale pentru deschiderea fişierelor sunt prezentate în tabelul 5.2. Moduri uzuale pentru deschiderea fişierelor Tabelul 5.2
Operaţia de gestiune Creare Consultare Actualizare Creare şi actualizare Extindere
Fişiere text w r w+ a
Fişiere binare wb rb r+b rwb, w+b
Închiderea fişierelor se realizează prin apelul funcţiei fclose, care are următorul prototip: int fclose(FILE* f);
Funcţia închide fişierul primit ca parametru şi returnează valoarea 0 în caz de succes sau -1, în caz de eroare. Înainte de închiderea fişierului, sunt golite toate zonele tampon asociate lui. Zonele tampon alocate automat de sistem sunt eliberate.
64
Fişiere de date
Revenirea la începutul fişierului se realizează prin funcţia rewind, cu prototipul: void rewind(FILE *f);
Executarea funcţiei are ca efect poziţionarea la începutul fişierului f (care era deschis anterior), resetarea indicatorului de sfârşit de fişier şi a indicatorilor de eroare (se înscrie valoarea 0). După apelul lui rewind poate urma o operaţie de scriere sau citire din fişier. Testarea sfîrşitului de fişier, se realizează prin apelul macrodefiniţiei feof: int feof(FILE* f);
Macro-ul furnizează valoarea indicatorului de sfârşit de fişier asociat lui f. Valoarea acestui indicator este setată la fiecare operaţie de citire din fişierul respectiv. Valoarea întoarsă este 0 (fals) dacă indicatorul are valoarea sfârşit de fişier şi diferit de zero (adevărat) în caz contrar. Apelul lui feof trebuie să fie precedat de apelul unei funcţii de citire din fişier. După atingerea sfârşitului de fişier, toate încercările de citire vor eşua, până la apelul funcţiei rewind sau închiderea şi redeschiderea fişierului. Golirea explicită a zonei tampon a unui fişier se realizează prin apelul funcţiei fflush, care are următorul prototip: int fflush(FILE* f);
Dacă fişierul f are asociată o zonă tampon de ieşire, funcţia scrie în fişier toate informaţiile din acesta, la poziţia curentă. Dacă fişierul are asociată o zonă tampon de intrare, funcţia îl goleşte. În caz de succes returnează valoarea zero, iar în caz de eroare valoarea EOF (definită în stdio.h). Exemplu: Înainte de a citi un şir de caractere de la tastatură, zona tampon trebuie golită pentru a preveni citirea unui şir vid (datorită unei perechi CR/LF rămase în zona tampon de la o citire anterioară a unei valori numerice). Ştergerea se realizează prin apelul: fflush(stdin);
Aflarea poziţiei curente în fişier se realizează prin apelul uneia din funcţiile fgetpos sau ftell: int fgetpos(FILE* f,fpos_t* poziţie);
65
Programarea calculatoarelor
După apel, la adresa poziţie se află poziţia pointerului de citire/scriere din fişierul f, ca număr relativ al octetului curent. Primul octet are numărul 0. Valoarea returnată poate fi folosită pentru poziţionare cu funcţia fsetpos. În caz de succes funcţia întoarce valoarea 0, iar în caz de eroare o valoare nenulă şi setează variabila errno la valoarea EBADF sau EINVAL. long ftell(FILE* f);
returnează poziţia în fişierul f a pointerului de citire/scriere în caz de succes sau -1L în caz contrar. Dacă fişierul este binar, poziţia este dată în număr de octeţi faţă de începutul fişierului. Valoarea poate fi folosită pentru poziţionare cu funcţia fseek. Modificarea poziţiei pointerului de citire/scriere se poate face prin poziţionare relativă: int fseek(FILE* f,long deplasare,int origine);
unde deplasare reprezintă numărul de octeţi cu care se deplasează pointerul în fişierul f, iar origine reprezintă poziţia faţă de care se deplasează pointerul. Parametrul origine poate fi: SEEK_SET (0) – poziţionare faţă de începutul fişierului; SEEK_CUR (1) – poziţionare faţă de poziţia curentă; SEEK_END (2) – poziţionare faţă de sfârşitul fişierului. Funcţia returnează valoarea 0 în caz de succes (şi uneori şi în caz de eşec). Se semnalează eroare prin returnarea unei valori nenule numai în cazul în care f nu este deschis. Poziţionarea absolută se face cu funcţia: int fsetpos(FILE* f,const fpos_t poziţie);
Pointerul de citire/scriere se mută în fişierul f la octetul cu numărul indicat de parametrul poziţie (care poate fi o valoare obţinută prin apelul lui fgetpos). Ambele funcţii resetează indicatorul de sfârşit de fişier şi anulează efectele unor eventuale apeluri anterioare ale lui ungetc asupra acelui fişier. Redenumirea sau mutarea unui fişier existent se poate realiza prin apelul funcţiei rename, care are următorul prototip: int rename(const char* n_vechi,const char* n_nou);
unde n_vechi reprezintă vechiul nume al fişierului, iar n_nou reprezintă numele nou. Dacă numele vechi conţine numele discului (de exemplu C:), numele nou trebuie să conţină acelaşi nume de disc. Dacă numele vechi conţine o cale, numele nou nu este obligat să conţină aceeaşi cale. Folosind o altă cale se obţine mutarea fişierului pe disc. Folosind aceeaşi cale (sau nefolosind calea) se obţine redenumirea fişierului. Nu sunt permise wildcard-uri (?, *) în cele două nume.
66
Fişiere de date
În caz de succes se întoarce valoarea 0. În caz de eroare se întoarce -1 şi errno primeşte una din valorile: ENOENT – nu există fişierul, EACCES – nu există permisiunea pentru operaţie sau ENOTSAM – dispozitiv diferit (mutarea se poate face doar pe acelaşi dispozitiv). Ştergerea unui fişier existent se poate realiza prin apelul funcţiei unlink, prezentată anterior, sau remove, care are următorul prototip: int remove(const char* cale);
unde cale reprezintă specificatorul fişierului (trebuie să fie închis).
Funcţii de citire/scriere fără conversie Funcţiile efectuează transferuri de secvenţe de octeţi între memoria internă şi un fişier de pe disc, fără a interveni asupra conţinutului sau ordinii octeţilor respectivi. Citirea dintr-un fişier binar se realizează prin apelul funcţiei fread, care are următorul prototip: size_t fread(void* ptr,size_t dim,size_t n,FILE* f);
Funcţia citeşte din fişierul f, de la poziţia curentă, un număr de n entităţi, fiecare de dimensiune dim, şi le depune, în ordinea citirii, la adresa ptr. fread returnează numărul de entităţi citite. În total se citesc, în caz de succes, n*dim octeţi. În caz de eroare sau când se întâlneşte sfârşitul de fişier, funcţia returnează o valoare negativă sau 0; size_t este definit în mai multe header-e (între care stdio.h) şi este un tip de dată folosit pentru a exprima dimensiunea obiectelor din memorie. Este compatibil cu tipul unsigned. Exemplu: struct complex {int x,y} articol; FILE * f_complex; if(f_complex=fopen("NR_COMPL.DAT", "rb") fread(&articol,sizeof(articol),1,f_complex); else printf("Fisierul nu poate fi deschis");
În exemplul anterior se deschide un fişier binar din care se citeşte un articol de tip struct complex, care se depune în variabila articol. Scrierea într-un fişier binar se poate realiza prin apelul funcţiei fwrite, care are următorul prototip: size_t fwrite(const void* ptr,size_t dim,size_t n,FILE* f);
67
Programarea calculatoarelor
Funcţia scrie în fişierul f, începând cu poziţia curentă, un număr de n entităţi contigue, fiecare de dimensiune dim, aflate în memorie la adresa ptr; fwrite returnează numărul entităţilor scrise cu succes. În caz de eroare se returnează o valoare negativă. Exemplu: struct complex {int x,y} articol; FILE *pf; pf=fopen("NR_COMPL.DAT","wb"); fwrite(& articol,sizeof (articol),1,pf);
Exemplul anterior creează un fişier binar nou în care scrie o secvenţă de octeţi conţinând reprezentarea binară a unei date de tip struct complex. Exemplu: Să se scrie funcţia care calculează numărul de articole dintr-un fişier binar, cunoscând lungimea în octeţi a unui articol. Funcţia are ca parametri fişierul şi lungimea în octeţi a unui articol. Prin numele funcţiei se întoarce numărul de articole din fişier. int nrart(FILE *f, int l) { long p; int n; p=ftell(f); fseek(f,0,2); n=ftell(f)/l; fseek(f,0,p); return n;}
Funcţii de citire/scriere cu conversie Funcţiile efectuează transferuri de secvenţe de octeţi între memoria internă şi un fişier de pe disc, convertind secvenţa de la reprezentarea internă (binară) la reprezentarea externă (ASCII) şi invers. Transferul de caractere se efectuează prin următoarele funcţii: int int int int
fgetc(FILE* f); fputc(int c, FILE *f); getc(FILE* f); putc(int c, FILE *stream);
Funcţia fgetc şi macrodefiniţia getc returnează următorul caracter din fişierul f (după ce îl converteşte la reprezentarea de tip întreg fără semn). Dacă s-a ajuns la sfîrşitul fişierului, funcţia va întoarce EOF (valoarea -1). Tot EOF va întoarce şi dacă sunt probleme la citirea din fişier. Funcţia fputc şi macrodefiniţia putc scriu caracterul c în fişierul f. În caz de eroare se returnează valoarea c, altfel se returnează EOF.
68
Fişiere de date
Funcţia ungetc pune caracterul c în zona tampon de citire asociată fişierului f. La următoarea citire cu fread sau getc acesta va fi primul octet/caracter citit. Un al doilea apel al funcţiei ungetc, fără să fie citit primul caracter pus în flux, îl va înlocui pe acesta. Apelarea funcţiilor fflush, fseek, fsetpos, sau rewind şterge aceste caractere din flux. În caz de succes, ungetc returnează caracterul c, iar în caz de eroare returnează EOF. Transferul de şiruri de caractere se efectuează prin funcţiile: char* fgets(char* s,int n,FILE* f); int fputs(const char* s,FILE* f);
Funcţia fgets citeşte un şir de caractere din fişierul f şi îl depune la adresa s. Transferul se încheie atunci când s-au citit n-1 caractere sau s-a întâlnit caracterul newline. La terminarea transferului, se adaugă la sfârşitul şirului din memorie caracterul nul ‘\0’. Dacă citirea s-a terminat prin întâlnirea caracterului newline, acesta va fi transferat în memorie, caracterul nul fiind adăugat după el (spre deosebire de gets, care nu îl reţine). La întâlnirea sfârşitului de fişier (fără a fi transferat vreun caracter) sau în caz de eroare fgets returnează NULL. În caz de succes returnează adresa şirului citit (aceeaşi cu cea primită în parametrul s). Funcţia fputs scrie în fişierul f caracterele şirului aflat la adresa s. Terminatorul de şir (‘\0’) nu este scris şi nici nu se adaugă caracterul newline (spre deosebire de puts). În caz de succes fputs returnează ultimul caracter scris. În caz de eroare returnează EOF. Transferul de date cu format controlat este realizat prin funcţiile: int fprintf(FILE* f,const char* format[,…]); int fscanf(FILR* f,const char* format[,…]);
Cele două funcţii lucrează identic cu printf şi scanf. Singura diferenţă constă în fişierul în/din care se transferă datele. Dacă printf şi scanf lucrează cu fişierele standard stdin şi stdoud, pentru fprintf şi fscanf este necesară precizarea explicită a fişierului cu care se lucrează, prin parametrul f. Deşi nu lucrează cu fişiere în mod direct, se pot folosi şi funcţiile int sprintf(char *s,const char *format[,...]); int sscanf(const char *s,const char *format[,...]);
Aceste funcţii lucrează identic cu printf şi scanf, diferenţa constând în entitatea din/în care se transferă datele. În locul fişierelor standard, acest funcţii folosesc o zonă de memorie de tip şir de caractere, a cărei adresă este furnizată în parametrul s. Şirul de la adresa s poate fi obţinut prin transfer fără format dintr-un fişier text (pentru sscanf) sau poate urma să fie scris într-un fişier text prin funcţia fputs.
69
Programarea calculatoarelor
Pentru tratarea erorilor se folosesc următoarele funcţii: void clearerr (FILE* f);
Funcţia resetează indicatorii de eroare şi indicatorul de sfârşit de fişier pentru fişierul f (se înscrie valoarea 0). Odată ce indicatorii de eroare au fost setaţi la o valoare diferită de 0, operaţiile de intrare/ieşire vor semnala eroare până la apelul lui clearerr sau rewind. int ferror (FILE* nume_intern);
Ferror este o macrodefiniţie care returnează codul de eroare al ultimei operaţii de intrare/ieşire asupra fişierului nume_intern (0 dacă nu s-a produs eroare). Exemplu: #include <stdio.h> int main(void) { FILE *f; /* deschide fisierul pentru scriere*/ f=fopen("test.ttt","w"); /* se produce eroare la incercarea de citire */ getc(f); if(ferror(f)) /* s-a produs eroare de I/E? */ {/* afiseaza mesaj de eroare */ printf("Eroare al citirea din test.ttt\n"); //reseteaza indicatorii de eroare si sfarsit fisier clearerr(f);} fclose(f); return 0;}
de
Exemplu: Să se scrie un program care calculează şi afişează valoarea unei funcţii introduse de la tastatură într-un punct dat. Funcţia se introduce ca şir de caractere şi poate conţine apeluri de funcţii standard C (vezi şi [Smeu95]). Programul creează un fişier sursă C (în care este scrisă forma funcţiei, ca subprogram C), apoi compilează şi execută un alt program, care va include subprogramul creat. Descrierea funcţiei introduse de la tastatură trebuie să conţină maxim 200 caractere. a) Fişierul 51_iii_a.cpp conţine programul care realizează citirea formei funcţiei, compilarea şi execuţia programului care calculează valoarea funcţiei. #include<stdlib.h> #include<stdio.h> #include #include<string.h> #include<process.h> void main()
70
Fişiere de date { char s1[213]="return("; char s2[]="double f(double x)\r\n\{\r\n"; FILE *f; int n,i,j; f=fopen("functie.cpp","w"); fputs(s2,f); printf("functia f(x)="); gets(&s1[7]); strncat(s1,");\r\n}",6); fputs(s1,f); fclose(f); system("bcc –Id:\borlandc\include -Ld:\borlandc\lib 51_iii_b.cpp>> tmp.txt"); execl("51_iii_b ",NULL); }
b) Fişierul 51_iii_b conţine programul care face citeşte punctul x, calculează valoarea funcţiei în acest punct şi o afişează. #include<stdio.h> #include #include<math.h> #include"functie.cpp" void main() { double x; printf("x=");scanf("%lf",&x); printf("f(%7.2lf)=%7.2lf",x,f(x)); getch(); }
71
6 Algoritmi de prelucrare a fişierelor binare
Din punct de vedere al operaţiilor de gestiune solicitate de diverse aplicaţii, fişierele binare se pot grupa în: fişiere care nu sunt actualizate (ţinute la zi) şi fişiere care sunt actualizate. De obicei, fişierele din prima grupă se regăsesc în aplicaţii matematice sau ca fişiere temporare şi de tranzacţii în aplicaţii de gestiune economică. Fişierele din cea de-a doua grupă sunt, de obicei, fişiere permanente (principale) în aplicaţii de gestiune economică şi au particularităţi de proiectare, referitoare, în special, la asigurarea ştergerii şi adăugării de articole.
6.1 Caracteristici generale ale algoritmilor de prelucrare a fişierelor Organizarea datelor în fişiere memorate pe medii magnetice externe presupune proiectarea unor algoritmi specifici operaţiilor de gestiune a acestora, denumiţi generic algoritmi de prelucrare a fişierelor de date. Datorită complexităţii aplicaţiilor care prelucrează fişiere este recomandată aplicarea metodei modularizării algoritmilor şi programelor. Modularizarea presupune ca, pe baza analizei problemei, să se descompună rezolvarea ei în părţi distincte, numite module, astfel încât fiecare dintre acestea să îndeplinească anumite funcţii. Descompunerea se poate realiza în mai multe faze (pe mai multe niveluri), prin metoda top-down. Criteriile de descompunere în module depind, în mare măsură, de experienţa programatorilor. Ele se referă, în principal, la: omogenizarea funcţiilor; utilizarea diverselor structuri de date; separarea funcţiilor de intrare/ieşire de funcţiile de prelucrare; utilizarea unor module deja existente; utilizarea eficientă a resurselor calculatorului (timp UC, memorie internă, periferie) etc. Modulele se implementează în program prin subprograme interne sau externe. De cele mai multe ori, o aplicaţie necesită existenţa mai multor fişiere
72
Algoritmi de prelucrare a fişierelor binare
active simultan, cu rol diferit (de intrare, de ieşire, de intrare/ieşire). Indiferent de numărul fişierelor utilizate, în marea majoritate a algoritmilor, logica prelucrării este coordonată, la un moment dat, de un singur fişier, obligatoriu de intrare, parcurs secvenţial, numit fişier conducător (sau director). Fişierul conducător are proprietatea că articolele lui pot fi citite logic independent de prelucrarea altor fişiere. Altfel spus, un fişier nu este conducător dacă prelucrarea articolelor sale este dependentă de existenţa (de citirea) articolului altui fişier. Accesul la datele memorate în fişierul conducător se realizează la nivel de articol. De aceea, algoritmii de prelucrare, indiferent de operaţia de gestiune, necesită utilizarea unei structuri repetitive pentru parcurgerea (parţială sau integrală) a fişierului respectiv. Algoritmii de prelucrare cu fişier conducător pot fi reprezentaţi prin schema logică generalizată, concepută modularizat, redată în figura 6.1.
Figura 6.1 Schema logică generală a unui algoritm de prelucrare cu fişier conducător Modulul ÎNCEPUT se realizează o singură dată, înaintea prelucrării primului articol al fişierului conducător şi cuprinde următoarele grupe de operaţii: Operaţii iniţiale standard, obligatorii oricărui algoritm şi care includ: punerea în corespondenţă a fişierelor logice cu fişiere fizice, deschiderea fişierelor, şi, pentru anumite variante, iniţializarea unei variabile logice pentru sfârşit de fişier (SF) şi citirea primului articol. Operaţii iniţiale specifice, facultative, existenţa lor depinzând de particularităţile problemei abordate şi care includ, în principal: iniţializări de variabile de total, afişări ale antetului, titlului şi/sau a capului de tabel pentru situaţii de ieşire etc. Modulul PRELUCRARE se execută repetitiv şi cuprinde, pe de o parte,
73
Programarea calculatoarelor
totalitatea operaţiilor de prelucrare a articolului curent al fişierului conducător operaţii specifice fiecărei probleme - şi, pe de altă parte, citirea unui articol din fişierul conducător. Ordinea celor două operaţii (citire şi prelucrare) depinde de varianta de algoritm aleasă. Modulul SFÎRŞIT se execută o singură dată, după prelucrarea ultimului articol al fişierului conducător şi include următoarele grupe de operaţii: operaţii finale standard, corespunzând închiderii fişierelor implicate în prelucrare; operaţii finale specifice, care depind de natura problemei şi includ, de regulă: afişarea variabilelor de total, a statisticilor privind operaţiile de gestiune executate, închiderea situaţiilor de ieşire etc. Modalitatea de detectare/tratare a sfârşitului de fişier conduce la existenţa mai multor variante ale schemei generale de prelucrare cu fişier conducător, prin forme particulare ale condiţiei sfîrşit_de_prelucrare. În funcţie de variantele alese, se pot construi scheme logice valabile pentru toate tipurile de fişiere sau numai pentru fişierele binare. Scheme valabile pentru toate tipurile de fişiere Detectarea sfârşitului de fişier, cu macrodefiniţia feof, caz în care testarea sfârşitului de fişier trebuie să urmeze după o operaţie de citire a unui articol. Algoritmul trebuie să conţină o citire iniţială în modulul ÎNCEPUT şi o citire curentă la sfârşitul modulului PRELUCRARE - (figura 6.2). Acest algoritm se poate aplica fişierelor vide sau nevide.
Operatii initiale
Operatii finale
Figura 6.2 Scheme logice valabile numai pentru fişiere binare
74
Algoritmi de prelucrare a fişierelor binare
Detectarea sfîrşitului de fişier prin operaţia de citire, verificând rezultatul întors de funcţia de citire (fread). Dacă rezultatul este mai mic decât numărul de blocuri de date care trebuie citite, înseamnă că s-a ajuns la sfârşitul fişierului. Întrucât, uzual, la o operaţie de citire se citeşte un articol întreg, în cazul atingeri sfârşitului de fişier, rezultatul întors de funcţia fread va fi 0. Rezultatul poate fi preluat într-o variabilă pentru a fi folosit în condiţia de terminare a prelucrării (figura 6.3) sau poate fi verificat direct, folosind apelul funcţiei fread în expresia (condiţia) care controlează sfârşitul prelucrării (figura 6.4). În ambele variante fişierul conducător este binar, vid sau nevid. START
Operaţiiinitiale iniţiale Operatii
SF = fread (…)
SF != 0
Da Prelucrare articol
Nu
SF = fread (…)
Operaţiifinale finale Operatii
STOP
Figura 6.3
Operatii initiale
Operatii finale
Figura 6.4
75
Programarea calculatoarelor
Prelucrarea unui număr cunoscut de articole, prin determinarea în modulul ÎNCEPUT a numărului de articole din fişierul conducător se regăseşte în figura 6.5. Limbajul C nu oferă o funcţie standard pentru calcularea numărului de articole dintr-un fişier binar, deoarece, din punctul de vedere al limbajului, fişierele nu conţin articole. Din punctul de vedere al utilizatorului, cunoscând dimensiunea unui articol, se poate calcula numărul de articol de fişier, împărţind lungimea acestuia la lungimea unui articol (ambele măsurate în număr de octeţi). Lungimea fişierului este egală cu poziţia curentă, atunci când pointerul de citire se află la sfârşitul fişierului. Pentru aflarea numărului de articole, se foloseşte secvenţa următoare: p=ftell(f); fseek(f,0,SEEK_END); l=ftell(f); nr=l/sizeof(tip_articol); fseek(f,p,SEEK_SET);
unde: variabila p, de tip long reţine poziţia curentă în fişier; f este fişierul a cărui lungime trebuie calculată; variabila l reţine poziţia curentă (în număr de octeţi faţă de începutul fişierului, deci lungimea fişierului măsurată în octeţi); variabila nr va primi ca valoare numărul de articole din fişier; tip_articol este tipul articolelor din fişier (din punctul de vedere al utilizatorului). Împărţirea se face exact, deoarece fişierul conţine un număr întreg de articole – utilizarea acestei secvenţe asupra unui fişier care conţine articole de alt tip (sau are conţinut de altă natură) va duce la rezultate incorecte.
Operatii initiale
Operatii finale
Figura 6.5
76
Algoritmi de prelucrare a fişierelor binare
Caracteristica generală a algoritmilor de prelucrare cu fişier conducător este parcurgerea secvenţială a fişierului conducător şi efectuarea unor prelucrări în funcţie de fiecare articol citit din acesta. Problema care se pune este detectarea sfârşitului de fişier. În C, macrodefiniţia feof nu face decât să furnizeze valoarea indicatorului de sfârşit de fişier, care este setat de operaţia de citire; în program, citirea trebuie să apară înaintea verificării sfârşitului de fişier. Forma generală a algoritmului este: while(!feof(f)) { <prelucrare articol citit> }
Exemplu: Crearea şi consultarea unui fişier text care memorează elemente întregi, folosind funcţia feof pentru gestionarea sfârşitului de fişier. La crearea fişierului, fişier conducător este fişierul standard de intrare. La afişare, conducător este fişierul f. #include<stdio.h> #include void main() { FILE *f; int x; long dim; clrscr(); f=fopen("numere.dat","w+"); scanf("%d",&x); while(!feof(stdin)) {fprintf(f,"%d\n",x); scanf("%d",&x);} fseek(f,0,SEEK_SET); fscanf(f,"%d",&x); while(!feof(f)) {printf("%d\t",x); fscanf(f,"%d",&x);} fclose(f); getch();}
Acelaşi exemplu, folosind fişier binar: #include<stdio.h> #include void main() { FILE *f; int x,g; long dim; clrscr(); f=fopen("numere.dat","wb+"); scanf("%d",&x); while(!feof(stdin)) {fwrite(&x,sizeof(x),1,f); scanf("%d",&x);} fseek(f,0,SEEK_SET); fread(&x,sizeof(x),1,f); while(!feof(f)) {printf("%d\t",x); fread(&x,sizeof(x),1,f);} fclose(f); c=getch();}
77
Programarea calculatoarelor
Fişierele utilizate într-o aplicaţie informatică au rol diferit în procesul prelucrării, în funcţie de scopul lor: de intrare, de ieşire, de intrare/ieşire, temporare, de tip listă etc. Aceste caracteristici conduc la algoritmi specifici fiecărei operaţii de gestiune în parte (creare, populare, consultare şi actualizare), fiind însă variante derivate din schema generală a unui algoritm de prelucrare cu fişier conducător. Deoarece aplicaţiile informatice din domeniul economic, social, administrativ etc. utilizează, cu predilecţie, fişiere cu articole de aceeaşi structură (sau un număr mic de structuri diferite), alegând limbajul C, se poate aprecia că cele mai performante sunt fişierele binare, ale căror articole sunt date declarate ca structuri (folosind tipul de date struct). Această alegere este motivată din următoarele puncte de vedere: descrierea articolelor este apropiată atât descrierii naturale a structurii unei entităţi din lumea reală (formată din câmpuri cu nume, lungime, reprezentare internă proprie, semnificaţie şi factor de repetabilitate diferite), cât şi descrierii din alte limbaje; există posibilitatea de a descrie explicit mai multe structuri pentru articolele aceluiaşi fişier (articole cu structură variabilă); operaţiile de acces la înregistrări se realizează cu viteză mare, datorită lipsei conversiilor la transferul între memoria principală şi memoria externă. Fişierele cu conţinut de tip text sunt recomandate a fi utilizate ca fişiere de ieşire, pentru realizarea de liste, situaţii finale, rapoarte etc., fiind rezidente pe disc, în general, până la listarea lor la imprimantă. Fişierele cu conţinut de tip text pot constitui şi sursa de creare a fişierelor binare, dacă acestea au fost populate cu date, fie prin editoare de texte, fie prin alte limbaje (Cobol, Fortran, Basic, Pascal), sisteme de gestiune a bazelor de date (DBase, FoxPro, Oracle etc.), constituind unicul mijloc de compatibilitate directă.
6.2 Algoritmi de prelucrare a fişierelor binare care nu necesită actualizare Asupra fişierelor binare care nu necesită actualizare se realizează, de obicei, operaţiile de creare (populare) şi consultare. Dintre operaţiile de actualizare pot fi realizate, fără mari complicaţii, modificarea şi adăugarea densă de articole. Popularea fişierelor se realizează prin preluarea datelor fie din alte fişiere primare (cu conţinut binar sau de tip text), fie de la tastatură (popularea interactivă). În ultimul caz, cel mai des întâlnit în practică, fişierul conducător corespunde mulţimii datelor introduse de la tastatură. Articolele sunt preluate câmp cu câmp, neexistând posibilitatea citirii unei variabile de tip articol şi, în plus, introducerea unei date este adesea însoţită de proceduri de validare specifice, cu reintroducerea ei în cazul unei erori. Sfârşitul introducerii datelor de la tastatură (şi implicit al procesului de populare a fişierului) poate fi: De tip chestionar, prin consultarea utilizatorului, privind continuarea sau nu a introducerii articolelor. Pentru un volum mare de date, varianta prezintă dezavantajul
78
Algoritmi de prelucrare a fişierelor binare
măririi timpului de prelucrare. Convenţional, prin introducerea pentru primul câmp din articol a unei valori prestabilite, cu semnificaţie de sfârşit de prelucrare. Standard, prin introducerea caracterului CTRL-Z, cu rol de sfârşit de fişier text. Schema logică a algoritmului de prelucrare este similară celei din figura 6.2., cu următoarele particularităţi: • modulul ÎNCEPUT are ca ultime operaţii, afişarea numelui primului câmp din articol şi citirea valorii sale; • modulul PRELUCRARE începe cu citirea următorului câmp, urmată de citirea celorlalte câmpuri (eventual cu validările stabilite) şi se termină cu afişarea numelui primului câmp şi cu citirea valorii acestuia (pentru articolul următor) (similar operaţiei din modulul ÎNCEPUT). O altă problemă a populării fişierelor binare o reprezintă aşezarea articolelor pe suportul extern. Din acest punct de vedere se întâlnesc două modalităţi: Populare densă, prin care articolele se scriu unul după altul, în ordinea în care au fost furnizate, fără a se lăsa locuri libere (acces secvenţial). Pentru fişierele care nu necesită actualizare acesta este tipul recomandat. Populare aleatoare, prin care articolele sunt scrise în casetele (virtuale) ale căror numere relative sunt furnizate explicit de utilizator (acces direct). Scrierea unui articol se realizează după poziţionarea pe numărul relativ dorit. La populare, nr_relativ nu este limitat decât de spaţiul existent pe suportul extern. Metoda are dezavantajul că necesită evidenţa "articolelor vide". În cazul fişierelor care nu necesită actualizare, popularea aleatoare se recomandă numai dacă, după creare, fişierul este dens. Pentru poziţionarea pe articolul cu numărul relativ n se foloseşte funcţia fseek astfel: fseek(f, n*sizeof(tip_articol), SEEK_SET);
unde n este numărul relativ al articolului iar tip_articol este tipul de dată care îi corespunde. Exemplu: 1. Să se creeze cu populare densă un fişier PRODUSE.DAT cu informaţii despre producţia cantitativă într-un an, la o societate comercială. Articolele au următoarea structură logică: Cod produs
Denumire Produs
Preţ Mediu
Cantităţi lunare 1
2
...
12
Articolele sunt introduse de la terminal, câmp cu câmp. Terminarea introducerii datelor este marcată standard, prin introducerea caracterului CTRL-Z.
79
Programarea calculatoarelor
#include <stdio.h> typedef struct { int cod; char denumire[20]; float pret_mediu; int cant[12]; } PRODUS; void main() { FILE* f; PRODUS x; char nume_fisier[20]; int i; //---INCEPUT--printf("\n\nNumele fisierului: "); gets(nume_fisier); if(!(f=fopen(nume_fisier,"wb"))) printf("\n\nNu fisierul cu numele %s",nume_fisier); else { printf("\nCod produs: "); scanf("%d",&x.cod); //---Aici se termina operatiile initiale---
poate
fi
creat
while(!feof(stdin)) { //---PRELUCRARE ARTICOL--printf("Denumire produs: "); fflush(stdin); gets(x.denumire); printf("Pret mediu: "); scanf("%f",&x.pret_mediu); printf("Cantitate lunara:\n"); for(i=0;i<12;i++) { printf(" - luna %d: ",i+1); scanf("%d",&x.cant[i]); } fwrite(&x,sizeof(PRODUS),1,f); //---Aici se incheie prelucrarea articolului--printf("\nCod produs: "); scanf("%d",&x.cod); } //---SFIRSIT--fclose(f); } }
Observaţii: Dacă se doreşte crearea fişierului de date cu populare în acces direct, programul este similar, cu următoarele diferenţe: câmpul COD indică numărul relativ al articolului în fişier şi nu va fi memorat (nu va face parte din declaraţia tipului PRODUS), fiind redundant; scrierea articolului va fi precedată de apelul funcţiei fseek(f,codt*sizeof(PRODUS),SEEK_SET);
unde codt este o variabilă independentă în care se citeşte codul de la terminal. Consultarea fişierelor are numeroase variante, în funcţie de scopul prelucrării. După modul de regăsire a articolelor în cadrul fişierului, ea poate fi secvenţială, directă sau mixtă.
80
Algoritmi de prelucrare a fişierelor binare
Consultarea secvenţială presupune regăsirea articolelor în ordinea în care au fost scrise pe suportul tehnic de informaţii. După numărul articolelor prelucrate, consultarea secvenţială poate fi: integrală, când se prelucrează toate articolele fişierului, începând cu primul şi terminând cu ultimul; cu selecţie, când se prelucrează numai acele articole care au una sau mai multe caracteristici comune (valori identice pentru acelaşi câmp). După numărul de caracteristici, selecţia poate fi simplă, dublă, multiplă. Pentru consultarea secvenţială se poate utiliza oricare din tipurile de algoritmi prezentaţi anterior. Exemplu: 2. Să se afişeze pe ecran conţinutul fişierului creat la exemplul 1. #include <stdio.h> typedef struct { int cod; char denumire[20]; float pret_mediu; int cant[12]; } PRODUS; void main() { FILE* f; PRODUS x; char nume_fisier[20]; int i; //---INCEPUT--printf("\n\nNumele fisierului: "); gets(nume_fisier); if(!(f=fopen(nume_fisier,"rb"))) printf("\n\nNu poate fisierul cu numele %s",nume_fisier); else { fread(&x,sizeof(PRODUS),1,f); //---Aici se termina operatiile initiale--while(!feof(f)) { //---PRELUCRARE ARTICOL--printf("\n\nCod produs:\t\t%d",x.cod); printf("\nDenumire produs:\t%s",x.denumire); printf("\nPret mediu:\t\t %7.2f",x.pret_mediu); printf("\nCantitati lunare:\t"); for(i=0;i<12;i++) printf("%3d ",x.cant[i]); //---Aici se incheie prelucrarea articolului--fread(&x,sizeof(PRODUS),1,f); } //---SFIRSIT--fclose(f); } }
fi
deschis
3. Obţinerea unei situaţii cu mai multe grade de total. Pentru aceasta se stabilesc câmpuri asociate gradelor de total, numite caracteristici de grupare sau caracteristici de control. O caracteristică de control este un câmp al articolului din fişierul de date, care are aceeaşi valoare pentru mai multe înregistrări. Astfel, articolele care au valoare comună pentru o caracteristică de grupare se pot ordona pe submulţimi, formând o grupă de control. Fişierul poate constitui, în ansamblul său, caracteristica
81
Programarea calculatoarelor
de grupare de cel mai înalt nivel, pentru care se poate calcula totalul general. Numărul maxim de grade de total este superior cu unu numărului de caracteristici de control stabilite. Între caracteristicile de grupare se stabileşte o relaţie de ordine ierarhică. Pentru prelucrarea fişierului, cu utilizare minimă de memorie, articolele trebuie sortate după caracteristicile de control. Acest tip de prelucrare intră în categoria consultărilor secvenţiale integrale şi urmează algoritmul de principiu din figurile 6.2 sau 6.3. Prelucrarea unui fişier sortat după criteriile enunţate, presupune existenţa unor operaţii standard, executate la schimbarea valorii fiecărei caracteristici de control stabilite: operaţii iniţiale ale unei grupe de control prin care se iniţializează variabila de total specifică grupei; se salvează valoarea caracteristicii primului articol din grupă; alte operaţii iniţiale specifice grupei; operaţii finale ale unei grupe de control prin care se afişează totalul calculat pentru caracteristica ce se schimbă; se cumulează totalul grupei curente la totalul grupei ierarhic superioare; alte operaţii finale specifice grupei; condiţia de prelucrare a unei grupe de control conţine, pe lângă condiţia specifică, toate celelalte condiţii din amonte. Raportul final este listat la imprimantă, fie direct, ca fişier de ieşire, fie creat pe suport magnetic, ca fişier text, în vederea imprimării ulterioare. Structura unei pagini a raportului şi controlul trecerii la o nouă pagină trebuie asigurate de programator. În continuare (figura 6.6) se prezintă structura de principiu a unui program de obţinere a unui raport final, cu control după două caracteristici şi trei grade de total, unde cîmp_1 şi cîmp_2 sunt caracteristicile de control (câmpuri din articol), v1 şi v2 sunt variabile de lucru pentru salvarea caracteristicilor, val_art e valoarea care interesează din fiecare articol (câmp al articolului sau valoare calculată pe baza unor câmpuri ale articolului), iar TOTG, TOT1 şi TOT2 sunt variabile pentru calculul gradelor de total. Analog, se poate extinde pentru oricâte caracteristici şi grade de total.
82
Algoritmi de prelucrare a fişierelor binare START Operaţii iniţiale generale TOTG=0
Citeşte articol
!feof(f)
Da Operaţii iniţiale Grupa 1 TOT1=0 v1=cîmp_1
! feof(f) şi v1==cîmp_1
Da Operaţii iniţiale Grupa 2
Nu Operaţii finale Geupa 1
TOT2=0 v2=cîmp_2
TOTG+=TOT1 ! feof(f) şi v1==cîmp_1 şi v2==cîmp_2
Da Prelucrare articol
Operaţii finale generale
Nu
Operaţii finale Grupa 2 TOT1+=TOT2
STOP Nu
TOT2+=val_art Citeşte articol
Figura 6.6 Schema logică – problema cu grade de total Consultarea în acces direct presupune regăsirea articolului după numărul relativ. Întrucât fişierele sunt considerate ca fluxuri de octeţi, trebuie calculată poziţia articolului dorit în fişier ca produs între numărul său relativ şi dimensiunea unui articol în octeţi (nr*sizeof(tip_articol)). Secvenţa care realizează acest lucru este: fseek(f,nr*sizeof(tip_articol), SEEK_SET); fread(&art,sizeof(tip_articol), 1, f);
83
Programarea calculatoarelor
Numărul relativ este furnizat de utilizator şi trebuie să aparţină domeniului 0..dimensiune fişier-1 (dimensiunea calculată ca număr de articole). Pentru evitarea situaţiilor în care numărul relativ se află în afara acestui domeniu, se va include în program validarea apartenenţei numărului relativ la intervalul acceptat. Algoritmul de consultare în acces direct a unui fişier are un alt fişier conducător (de exemplu tastatura). Exemplu: 4. { // citire nume fisier extern f=fopen(nume_fisier, "rb"); // calculare numar de articole din fisier printf("\nNr. relativ: "); scanf("%d",&r); //citirea numarului relativ al articolului while(!feof(stdin)) { if(r>=nr_art) printf("\n Articol inexistent !"); else { fseek(f,r*sizeof(tip_articol),SEEK_SET); fread(&art,sizeof(tip_articol),1,f); // -----------------------//PRELUCRARE ARTICOL //-----------------------} printf("\nNr. Relativ (sau CTRL-Z): "); scanf("%d",&r); } fclose(f); }
Consultarea în acces mixt utilizează o combinaţie între accesul direct şi cel secvenţial, în vederea prelucrării unui grup de articole, memorate contiguu în fişier şi selectabile printr-o condiţie. Pentru fişierele binare, metoda poate fi aplicată dacă se doreşte selectarea articolelor dintre două limite ale numerelor relative (limita inferioară - li şi limita superioară - ls). Algoritmul trebuie să verifice relaţia 0≤li≤ls≤dimensiune fişier, după care parcurgerea fişierului poate fi realizată prin orice tip de structură repetitivă. Exemplu: 5. { // citire nume fisier extern f=fopen(nume_fisier, "rb"); // calculare numar articole din fisier printf("\nLimita inferioara: "); scanf("%d",&li); // citirea nr. relativ al primului articol // din secventa printf("\nLimita superioara: "); scanf("%d",&ls); // citirea nr. relativ al ultimului articol // din secventa if((0
84
Algoritmi de prelucrare a fişierelor binare { fread(&art,sizeof(tip_articol),1,f); // ----------------------// Prelucrare articol // ----------------------} } else printf(" Nu este indeplinita conditia de limite"); fclose(f) }
Adăugarea de articole se realizează, în general, cu tranzacţii de la terminal, similar operaţiei de populare. Pentru o corectă exploatare ulterioară, adăugarea trebuie să fie densă. Acest lucru poate fi realizat astfel: Adăugare la sfârşit (extindere), după ultimul articol scris. Operaţia se realizează similar populării în acces secvenţial, după poziţionarea pe marcatorul de sfârşit de fişier, apelând funcţia fseek: fseek(f,0,SEEK_END); Exemplu: 6. { // citire nume fisier extern f=fopen(nume_fisier, "rb"); fseek(f,0,SEEK_END); // pozitionare dupa ultimul // articol scris printf("Cimp 1: "); scanf("%d ",&art.cimp_1); while(!feof(stdin)) { // ----------------------------------------------// Preluare de la tastatura a celorlalte // campuri din articol // ----------------------------------------------printf("Cimp 1: "); scanf("%d ",&art.cimp_1); } fclose(f) }
Inserarea unor articole. Se aplică în cazul în care articolele sunt scrise în fişier în ordinea crescătoare (descrescătoare) a valorilor unui anumit câmp. În acest caz, noul articol va fi inserat între două articole, astfel: se caută (cu un algoritm secvenţial, binar etc.) poziţia k în care trebuie inserat noul articol; se copiază, glisând cu o poziţie spre dreapta, toate articolele de la sfârşitul fişierului până la articolul cu numărul relativ k; se scrie în acces direct noul articol, în poziţia k.
85
Programarea calculatoarelor
Exemplu: 7. { // articolele fisierului sunt in ordinea // crescatoare a valorii campului 1 // citire nume fisier extern *) f=fopen(nume_fisier, "rb+ "); // calculare numar de articole din fisier printf("\nCimp 1: "); // introducerea campului dupa care // sunt sortate articolele scanf("%d ",&art_nou.cimp_1); while(!feof(stdin)) // adaugarea mai multor articole { // ----------------------------------// Preluare de la tastatura a celorlalte // campuri din articolul de adaugat // ----------------------------------// secventa de cautare a pozitiei //in care se va insera articolul } fseek(f,0,SEEK_SET); // pozitionare pe inceput de fisier fread(&art_existent,sizeof(tip_articol),1,f); while((!feof(f))&&(art_existent.cimp_1<art_nou.cimp_1) fread(&art_existent,sizeof(tip_articol),1,f); if(!feof(f)) { k=ftell(f)-sizeof(tip_articol); //articolul se va // insera in pozitia k for(i=nr_art-1;i>=k;i--) { fseek(f,i*sizeof(tip_articol),SEK_SET); fread(&art_existent,sizeof(tip_articol),1,f); fwrite(&art_existent,sizeof(tip_articol),1,f); } } else k=nr_art; // articolul se adauga la sfirsitul // fisierului fseek(f,k*sizeof(tip_articol),SEK_SET); fwrite(&art_nou,sizeof(tip_articol),1,f); printf("\nCimp 1: "); // introducerea campului dupa care // sunt sortate articolele scanf("%d ",&art_nou.cimp_1);rite('Camp 1: ') } fclose(f); }
Modificarea valorii unor câmpuri din articol se realizează în mai multe etape: se citeşte articolul care se modifică (fseek şi fread); se modifică (în zona articol din memoria principală) câmpurile cu valorile dorite, introduse, în general, de la tastatură; se repoziţionează pe articolul respectiv cu fseek(f,ftell(f)-sizeof(tip_articol),SEEK_SET); se scrie articolul modificat, cu funcţia fwrite. O problemă importantă rămâne selectarea câmpurilor care se modifică, pentru fiecare articol în parte. O variantă simplă este afişarea vechii valori, urmată de introducerea noii valori, în variabile independente de tip şir de caractere. În cazul în care şirul introdus este vid (s-a apăsat numai ENTER), respectivul câmp, prin convenţie, nu se modifică.
86
Algoritmi de prelucrare a fişierelor binare
Altfel, câmpului respectiv i se va atribui valoarea citită în variabila independentă, eventual prin conversie, pentru câmpurile numerice. Exemplu: 8. // -----------------------------// cautare articol de modificat // ------------------------------ *) fread(&art,sizeof(tip_articol),1,f); printf("Codul: %d - ",art.cod); //afisare vechea valoare fflush(stdin); gets(cods); // citire noua valoare; cods este de tip sir if(strlen(cods)) { art.cod=atoi(cods); // conversie din ASCII in binar printf("Denumire: %s - ",art.den); // afisare vechea // valoare gets(dens); // citire noua valoare if(strlen(dens) strcpy(dens,art.den); // copiere noua valoare // ---------------------------------// Introducerea celorlalte campuri // din articol // ---------------------------------// repozitionare pe articol feek(f,ftell(f)-sizeof(tip_articol),SEEK_SET); // rescriere articol modificat fwrite(&art,sizeof(tip_articol),1,f); }
O altă variantă se poate realiza prin folosirea unei machete de ecran în care se afişează valorile actuale ale fiecărui câmp de modificat, se poziţionează succesiv cursorul la începutul fiecărui câmp, cu două răspunsuri posibile ale utilizatorului: <ENTER>, caz în care se menţine actuala valoare, respectiv o tastă diferită de <ENTER>, reprezentând primul caracter al noii valori.
6.3 Algoritmi de prelucrare a fişierelor binare care necesită actualizare Prelucrarea fişierelor binare care necesită actualizare trebuie să asigure posibilitatea ştergerii articolelor şi să elimine riscul de suprascriere a articolelor adăugate. Pentru aceasta, trebuie proiectate structuri particulare de articole şi concepute operaţii de gestiune specifice. Fără a epuiza multitudinea soluţiilor de rezolvare a problemelor de gestiune a fişierelor care necesită actualizare, în continuare, se prezintă câteva soluţii posibile. În orice situaţie, limitările de regăsire prin acces secvenţial sau relativ a articolelor în fişier reduc aria folosirii limbajului în probleme de gestiune. Marele inconvenient îl constituie lipsa accesului după cheie, cel care corespunde cel mai bine gestiunii în sistemele reale.
87
Programarea calculatoarelor
În cele ce urmează se analizează trei tipuri de probleme: probleme în care se utilizează asocierea externă a numărului relativ la articolul corespunzător (codificare prin număr relativ); probleme în care se utilizează asocierea internă a numărului relativ la articolul corespunzător, iar acesta poate emana extern (se generează nomenclatoare după fiecare actualizare de fişier); probleme în care se utilizează extern coduri (chei) şi intern numere relative. 6.3.1 Probleme care utilizează codificarea externă prin numere relative Nomenclatorul de articole conţine numărul relativ al fiecăruia dintre ele. Nomenclatorul este elaborat extern (automat sau neautomat). Orice operaţie de regăsire în acces relativ presupune introducerea din exterior a numărului relativ. La crearea iniţială, fiecare articol este înscris la numărul său relativ predefinit. Asigurarea ştergerii şi adăugării controlate poate fi făcută în diverse moduri: Extinderea articolelor logice cu un indicator de stare (un octet), ajungându-se la forma din figura 6.8. IS
Articol propriu-zis
Figura 6.8 Structura articolului care include indicatorul de stare Indicatorul de stare (notat IS) poate lua una din cele două valori posibile (de exemplu 0 pentru articol inactiv – inexistent sau şters, 1 pentru articol prezent). Cu această convenţie, operaţiile de acces la articole se realizează în următoarele condiţii: scrierea în fişier este permisă numai pentru articolele cu IS=0; citirea din fişier este permisă numai pentru articolele cu IS=1. Preformarea presupune deschiderea fişierului ca nou (crearea unui fişier nou) şi scrierea unui număr de articole (la limită, zero) cu IS=0. Includerea operaţiei de preformare conduce la dispariţia distincţiei dintre populare şi adăugare. Datorită faptului că fişierul se deschide ca existent, orice operaţie de scriere a unui nou articol se tratează ca adăugare. Într-un sistem de programe, deschiderea cu modul wb a unui fişier se realizează o singură dată, în procedura de preformare. Scrierea în acces direct presupune furnizarea numărului relativ (nr) al articolului. În funcţie de valoarea lui nr se disting următoarele situaţii: - dacă nr=FileSize(f), are loc extinderea fişierului cu preformarea articolelor cu numerele relative cuprinse în domeniul dimensiune fişier..nr-1. Noul articol se scrie pe poziţia nr. Se remarcă faptul că scrierea în acces direct permite preformarea iniţială cu zero articole. Scrierea în acces secvenţial se face fără verificare de existenţă. Scrierea are
88
Algoritmi de prelucrare a fişierelor binare
loc în poziţia dată de pointerul curent. Procedura face IS=1. Utilizarea ei se recomandă numai la popularea densă. Citirea în acces direct presupune furnizarea numărului relativ (nr). Ea verifică dacă IS=1. Citirea în acces secvenţial analizează articolele începând cu cel de la pointerul curent. Articolele cu IS=0 sunt ignorate, până la întâlnirea primului articol cu IS=1 sau până se ajunge la sfârşit de fişier. Ştergerea se realizează în acces direct. Ea presupune citirea articolului şi, dacă ştergerea este permisă (IS=1), se modifică indicatorul de stare (IS=0) şi se scrie articolul pe vechiul loc. Rescrierea realizează scrierea unui articol în poziţia ftell(f)sizeof(tip_articol), dacă vechiul articol din această poziţie are IS=1. Folosirea articolului zero ca tabelă de ocupare în fişier. Fiecărui articol din fişier îi corespunde câte un octet în primul articol: articolului cu numărul relativ i îi corespunde octetul a[i]. Primul articol are structura char a[max], unde max este o constantă care indică numărul maxim de articole pe care le poate avea fişierul pe durata existenţei sale. Dacă articolul i este prezent, a[i]=1; dacă articolul i este inactiv (inexistent sau şters), a[i]=0. Cu această structură, operaţiile de acces la articole se realizează în următoarele condiţii: scrierea în fişier a articolului cu numărul relativ i este permisă numai dacă a[i]=0; citirea din fişier a articolului cu numărul relativ i este permisă numai dacă a[i]=1. Ştergerea articolului i presupune verificarea existenţei sale (a[i]=1) şi realizarea operaţiei a[i]=0. Adăugarea unui articol i presupune verificarea inexistenţei lui (a[i]=0), înscrierea articolului şi realizarea operaţiei a[i]=1. Utilizarea acestei modalităţi necesită încărcarea iniţială în memoria principală a articolului cu numărul relativ zero. În programele care realizează ştergeri sau/şi adăugări, înainte de închiderea fişierului trebuie rescris articolul zero în fişier. Datorită restricţiei impuse pentru numărul de articole din fişier, acest model de gestiune a articolelor este ineficient pentru multe probleme. Se pot concepe algoritmi prin care în tabela de ocupare în fişier fiecărui articol îi corespunde un bit, în loc de un octet. În acest fel numărul maxim de articole ce pot fi adăugate în fişier se măreşte de 8 ori. 6.3.2 Probleme care utilizează codificarea internă prin numere relative Înscrierea articolelor în fişiere, chiar cea iniţială, se face în primele articole inactive, asociindu-se astfel intern un număr relativ fiecărui articol. Deosebirea esenţială între această soluţie şi cea prezentată anterior, constă în modul de realizare a adăugării secvenţiale. După fiecare sesiune de actualizare va trebui listat nomenclatorul de coduri interne (numere relative). Articolelor din fişier li se asociază structura din figura 6.8. Preformarea, consultarea, modificarea şi ştergerea sunt similare celor prezentate în §6.3.1. Adăugarea unui articol se realizează în
89
Programarea calculatoarelor
condiţii diferite, după cum această operaţie are loc printre articolele existente, respectiv după ultimul articol (extindere). În ambele situaţii, condiţiile de realizare sunt determinate de modul de acces folosit: secvenţial sau direct. Adăugarea în acces secvenţial se bazează pe presupunerea că utilizatorul nu impune o corespondenţă prestabilită între conţinut şi numărul articolului, adică în alţi termeni, că se acceptă o codificare automată. Adăugarea în acces secvenţial poate fi utilizată în două variante: Cu verificarea existenţei de articole libere, caz în care se adaugă noul articol în prima poziţie găsită disponibilă (IS=0), eventual la sfârşit (extindere), dacă nu mai există articole libere în interior. Această variantă presupune existenţa unei soluţii de gestiune a articolelor libere (în urma preformării sau a ştergerii logice). Dintre soluţiile posibile pot fi menţionate: Folosirea articolului zero pentru colectarea numărului articolelor libere, într-o structură de forma celei din figura 6.9. nal WORD
Articolul 0
al[1] WORD
al[2] WORD
... ...
al[nal] WORD
Figura 6.9 Structura articolului zero, pentru gestiunea articolelor libere În această soluţie, nal este numărul articolelor libere şi al[i], cu i=1..nal, reprezintă poziţiile relative ale articolelor libere. Soluţia prezintă avantajul timpului redus de căutare şi de atribuire a unei poziţii pentru noul articol. Numărul de articole libere ce pot fi gestionate în acest mod este limitat de descrierea articolelor principale ale fişierului. De exemplu, dacă articolul principal are 128 de octeţi, această soluţie permite gestionarea a 63 de articole libere (dacă se impune pentru articolul zero aceeaşi lungime ca şi pentru celelalte articole; pentru primul articol se poate accepta şi o dimensiune diferită – mai mare – dar nu cu mult mai mare şi oricum este o dimensiune stabilită de la început, care nu mai poate fi mărită la nevoie). La ştergerea logică a unui articol se realizează incrementarea valorii lui nal, iar al[nal] primeşte ca valoare numărul relativ al articolului şters. Folosirea articolului zero ca început al unei liste simple (sau dublu) înlănţuite a articolelor libere, într-o structură de principiu de forma celei din figura 6.10. pal
0
au
ual
0
...
au
0
au
...
Figura 6.10 Gestionarea articolelor libere prin liste
90
Algoritmi de prelucrare a fişierelor binare
În această soluţie, articolul zero punctează pe primul (pal) şi pe ultimul (ual) articol liber, iar fiecare articol punctează pe următorul (au). Numărul articolelor libere care pot fi gestionate în acest mod este oarecare. La adăugarea unui nou articol, se verifică dacă există articole libere (pal<>0) şi dacă există, se atribuie primul articol din listă articolului de adăugat, actualizându-se componenta articolului zero. La ştergerea unui articol, trebuie asigurată includerea sa în lista articolelor libere, operaţia fiind posibilă la oricare din capetele listei. Căutarea secvenţială a primului articol liber, fără organizarea unei gestiuni a acestora. Deşi mai costisitoare ca timp de căutare, aceasta este soluţia cea mai simplă sub aspectul programării, eliminând necesitatea unei structuri distincte a articolului zero şi operaţiile legate de întreţinerea colecţiei sau listei articolelor libere. În concluzie, este de preferat ultima variantă atunci când timpul de căutare nu este prohibitiv. Fără verificarea existenţei de articole libere, caz în care articolul este adăugat direct la sfârşit (extindere). Această variantă este avantajoasă când are loc introducerea de la început a majorităţii articolelor. Ea poate fi asociată cu preformarea cu zero articole, fiecare sesiune de adăugare de noi articole fiind realizată prin extinderea fişierului. Adăugarea în acces direct presupune o codificare anterioară (preluarea numărului relativ din nomenclatorul editat după fiecare creare/adăugare secvenţială) şi se realizează identic cu operaţia de scriere directă prezentată în §6.3.1. 6.3.3 Probleme care utilizează corespondenţa internă dintre chei şi numere relative Majoritatea aplicaţiilor de gestiune economică utilizează fişiere de date în care articolele trebuie regăsite după valorile unui câmp de identificare, numit cheie. Problema corespondenţei între chei şi numerele relative ale articolelor din fişierul de date se poate rezolva prin intermediul unui fişier binar suplimentar, cu rol de tabelă de indexuri. O astfel de organizare se numeşte indexată. Articolele fişierului tabelă de indexuri au structura din figura 6.11. IS
Cheie
Număr relativ (nr)
Figura 6.11 Structura articolului din tabela de indexuri Indicatorul de stare (IS) are rol identic cu cel prezentat în §6.3.1. Cheie este un câmp în care se memorează valoarea cheii articolului existent logic în fişierul de date, al cărui număr relativ corespunzător este memorat în câmpul nr. Articolele tabelei de indexuri sunt, în orice moment, sortate crescător după valorile câmpului cheie. Articolele din fişierul de date sunt memorate aleator. O parte dintre acestea nuşi regăsesc corespondent în tabela de indexuri, fiind considerate şterse. Orice operaţie de acces la articolele fişierului de date se realizează numai prin intermediul tabelei,
91
Programarea calculatoarelor
gestionată automat de funcţiile unei biblioteci specializate şi care este netransparentă utilizatorului (bibliotecă utilizator, nu face parte din limbaj). Operaţiile de acces la nivel de fişier sunt deschiderea şi închiderea fişierului de date. La rândul ei, deschiderea poate fi pentru creare (ca fişier nou) sau pentru consultare şi întreţinere (ca fişier vechi). Procedurile realizează, pe lângă operaţiile asupra fişierului de date şi gestionarea automată a tabelei de indexuri: formarea numelui său extern, asignarea numelui fizic la numele logic, deschiderea ca fişier nou sau vechi, închiderea. Operaţiile de gestiune ce se pot realiza cu fişierele de date astfel organizate sunt: Crearea în acces secvenţial presupune furnizarea articolelor sortate strict crescător după valorile câmpului ales drept cheie. Articolele sunt scrise cu ajutorul funcţiei de scriere în acces secvenţial. Eroarea de cheie invalidă poate apărea la tentativa de scriere a unui articol a cărui cheie este mai mică sau egală decât ultima înscrisă în fişierul de date. Crearea în acces direct se realizează cu funcţia de scriere în acces direct, articolele fiind furnizate în orice ordine. Eroarea de cheie invalidă apare la tentativa de scriere a unui articol a cărui cheie este egală cu una din cele prezente în fişier. Consultarea în acces secvenţial presupune regăsirea articolelor în ordinea strict crescătoare a valorilor cheii. Ea se realizează cu ajutorul funcţiei de citire în acces secvenţial, care detectează şi sfârşitul fişierului de date. Consultarea în acces mixt permite selectarea unui grup de articole, memorate logic contiguu în fişier, selecţie realizată pe baza valorilor cheii primului şi ultimului articol din grupul dorit. Accesul mixt presupune poziţionarea pe primul articol prin citirea în acces direct (sau poziţionare şi citire în acces secvenţial), urmată de exploatarea în acces secvenţial, până la găsirea cheii ultimului articol dorit, sau până la sfârşitul fişierului. Adăugarea de articole se realizează utilizând funcţia de scriere în acces direct. Articolele pot fi furnizate în orice ordine a valorilor cheii. Eroarea de cheie invalidă apare la tentativa de scriere a unui articol a cărui cheie este egală cu una deja existentă. Modificarea unor câmpuri din articol se realizează în următoarele etape: citirea articolului de modificat (în acces secvenţial sau direct); modificarea, în zona articol corespunzătoare, a câmpurilor dorite, cu excepţia câmpului cheie; rescrierea articolului, cu procedura de rescriere. Eroarea de cheie invalidă apare în cazul tentativei de rescriere a unui articol a cărui cheie este diferită de cea a articolului citit anterior. Ştergerea în acces secvenţial elimină articolul curent din fişierul de date. În general, articolul trebuie mai întâi identificat printr-o citire (în acces secvenţial sau direct) sau prin poziţionare. Procedura returnează eroare în cazul tentativei de ştergere după ultimul articol existent. Ştergerea în acces direct elimină articolul a cărui cheie este precizată. Operaţia nu trebuie precedată de citirea articolului şi returnează eroare în cazul
92
Algoritmi de prelucrare a fişierelor binare
furnizării unei chei inexistente în fişier. În aplicaţii, este preferabilă ştergerea în acces secvenţial, deoarece permite (datorită citirii care o precede), vizualizarea articolului şi luarea unei decizii în condiţii de siguranţă. Exemplu: 9. Exemplul următor descrie funcţii, tipuri de date şi variabile publice pentru prelucrarea unui fişier organizat indexat. Pentru aceste exemplu, fişierul de date este format din articole cu următoarea structură: cheie
denumire
preţ
cantitate
Figura 6.12 Structura articolului din tabela de indexuri Exemplul poate fi adaptat pentru orice altă structură, modificând corespunzător structura articolului. În acest exemplu, fişierul index va fi o variabilă globală, accesibilă tuturor subprogramelor. Tipurile definite sunt următoarele: typedef struct{ char cheie[7]; char den[35]; float pu; float cant; } ARTICOL; //tipul articol din fisierul de date typedef struct{ char is; char cheie[7]; long nr_rel; } ART_INDEX; //tipul articol din tabela de indexuri FILE* ind; char nume_index[20];
//fisierul index //numele extern al fisierului index
Pentru implementarea operaţiilor de gestiune specifice unui fişier organizat indexat sunt necesare următoarele subprograme: ¾ Funcţia de deschidere a tabelei index ca fişier nou, cu prototipul void new_index(char *nume);
Funcţia primeşte ca parametru numele extern al fişierului de date (nume) şi creează un fişier nou, tabela de indexuri, cu extensia .idx. ¾ Funcţia de deschidere a tabelei de indexuri, pentru consultare şi întreţinere, cu prototipul void open_index(char *nume);
Funcţia primeşte ca parametru numele extern al fişierului de date (nume), şi deschide ca existentă tabela de indexuri, cu extensia .idx. ¾ Funcţia de închidere a tabelei de indexuri, cu prototipul void closeindex();
93
Programarea calculatoarelor
Funcţia realizează închiderea tabelei de indexuri asociate fişierului de date. ¾ Funcţia pentru citirea în acces secvenţial a unui articol din fişierul de date, cu prototipul int ReadSec(fisier f,articol *a);
Funcţia are ca parametri numele intern al fişierului de date şi adresa unde se depune articolul citit, dacă acest lucru este posibil şi returnează - 1, dacă citirea a fost posibilă; - 0, în caz contrar. Citirea unui articol din fişierul de date este realizată prin intermediul tabelei de indexuri, astfel: este citit un articol din tabelă, de la poziţia curentă a pointerului de fişier şi apoi este citit articolul cu numărul relativ dat de câmpul nr_rel al articolului citit din fişierul de indexuri. Dacă, în tabela de indexuri, pointerul de fişier indică sfârşitul de fişier, atunci citirea nu este posibilă şi funcţia returnează valoarea 0. Prin apelul repetat al funcţiei ReadSec, dacă tabela de indexuri are poinetrul plasat înaintea primului articol, sunt obţinute articolele din fişierul de date în ordinea strict crescătoare a valorii cheii. ¾ Funcţia pentru citirea în acces direct a unui articol din fişierul de date, cu prototipul int ReadKey(fisier f,articol *a,char *Key);
Funcţia are ca parametri numele intern al fişierului de date, adresa unde se depune articolul citit, dacă acest lucru este posibil, precum şi cheia articolului care va fi citit şi returnează - 1, dacă citirea a fost posibilă; - 0, în caz contrar. Funcţia apelează modulul de căutare binară în tabela de indexuri a cheii Key, SeekKey. Atunci când cheia este găsită, citeşte articolul cu numărul relativ corespunzător articolului din tabela de indexuri şi returnează valoarea 1, altfel returnează valoarea 0. ¾ Funcţia pentru scrierea în acces secvenţial a unui articol în fişierul de date, cu prototipul int WriteSec(fisier f,articol a);
Funcţia are ca parametri numele intern al fişierului de date şi articolul ce va fi scris, dacă acest lucru este posibil, şi returnează - 1, dacă scrierea a fost posibilă; - 0, în caz contrar. Funcţia adaugă un articol în fişierul de date, concomitent cu extinderea tabelei de indexuri cu o nouă înregistrare, a cărei cheie este mai mare decât cele existente. În cazul în care cheia este mai mică sau egală cu a ultimului articol din tabelă, este returnată valoarea 0, corespunzătoare situaţiei în care scrierea nu este posibilă.
94
Algoritmi de prelucrare a fişierelor binare
¾ Funcţia pentru scrierea în acces direct a unui articol în fişierul de date, cu prototipul int WriteKey(fisier f,articol a);
Funcţia are ca parametri numele intern al fişierului, articolul ce va fi scris, dacă acest lucru este posibil, şi returnează - 1, dacă scrierea a fost posibilă; - 0, în caz contrar. Funcţia adaugă un articol la sfârşitul fişierului de date. Cheia acestuia, a.cheie, poate avea orice valoare (care nu există deja în tabela de indexuri). Iniţial, tabela se extinde cu un nou articol şi apoi este reordonată (prin apelul funcţiei Sort). În cazul în care cheia articolului de scris este deja prezentă în tabela de indexuri, articolul nu este scris în fişier şi funcţia returnează valoarea 0. Căutarea cheii în tabela de indexuri pentru stabilirea posibilităţii scrierii este realizată prin apelul funcţiei SeekKey. ¾ Funcţia pentru ştergerea în acces secvenţial a unui articol, cu prototipul int DeleteSec();
Funcţia returnează - 1, dacă ştergerea a fost posibilă; - 0, în caz contrar. Funcţia şterge logic articolul curent din fişierul de date. Ştergerea se realizează fizic în tabela de indexuri. Iniţial, indicatorul de stare este setat pe 0 şi apoi se elimină articolul din tabelă, prin apelul funcţiei Sort. Funcţia returnează valoarea 0, corespunzătoare situaţiei de eroare, dacă pointerul curent al tabelei de indexuri indică marcatorul de sfârşit de fişier. ¾ Funcţia pentru ştergerea în acces direct a unui articol, cu prototipul int DeleteKey(char *Key);
Funcţia primeşte ca parametru de intrare cheia articolului care va fi şters şi returnează - 1, dacă ştergerea a fost posibilă; - 0, în caz contrar. Funcţia şterge logic din fişierul de date articolul a cărui cheie este primită ca parametru. Ştergerea este realizată fizic din tabela de indexuri, analog ştergerii în acces secvenţial. Funcţia returnează valoarea 0, corespunzătoare situaţiei de eroare, dacă Key nu este regăsită în tabela de indexuri. Căutarea este realizată prin apelul funcţiei SeekKey. Pentru implementarea funcţiilor descrise mai sus, sunt utilizate următoarele funcţii auxiliare: ¾ Funcţia pentru sortarea tabelei de indexuri, cu eliminarea articolelor cu stare 0, cu prototipul void Sort();
Funcţia realizează sortarea articolelor tabelei de indexuri, crescător după câmpul cheie, precum şi ştergerea fizică a tuturor articolelor cu indicator de stare 0.
95
Programarea calculatoarelor
¾ Funcţia pentru căutarea articolului cu cheie dată, cu prototipul int SeekKey(char *Key)
Funcţia primeşte ca parametru de intrare cheia articolului căutat şi returnează - 1, dacă articolul a fost găsit; - 0, în caz contrar. Funcţia realizează căutarea binară în tabela de indexuri, după câmpul cheie. Dacă articolul cu cheia Key este găsit, funcţia lasă pointerul de fişier pe acel articol (o citire secvenţială ulterioară determinând obţinerea articolului corespunzător din fişierul de date). Textul sursă care implementează toate aceste funcţii C este prezentat în continuare (în exemplele care urmează, aceste text va fi considerat salvat în fişierul index1.cpp). #include <stdio.h> #include <string.h> #define fisier FILE* typedef struct{ char cheie[7]; char den[35]; float pu; float cant; } ARTICOL; //tipul articol din fisierul de date typedef struct{ char is; char cheie[7]; long nr_rel; } ART_INDEX; //tipul articol din tabela index fisier ind; char nume_index[20];
//fisierul index //numele extern al fisierului index
void Sort() { ART_INDEX a,b; fisier ind1; long i,j; ind1=fopen("temp.idx","wb+"); rewind(ind); fread(&a,sizeof(a),1,ind); while(!feof(ind)) { if(a.is)fwrite(&a,sizeof(a),1,ind1); fread(&a,sizeof(a),1,ind); } fclose(ind); fseek(ind1,0,SEEK_END); long n=ftell(ind1)/sizeof(a); for(i=0;i0) { fseek(ind1,i*sizeof(a),SEEK_SET); fwrite(&b,sizeof(a),1,ind1); fseek(ind1,j*sizeof(a),SEEK_SET);
96
Algoritmi de prelucrare a fişierelor binare fwrite(&a,sizeof(a),1,ind1); } } } rewind(ind1); ind=fopen(nume_index,"wb+"); fread(&a,sizeof(a),1,ind1); while(!feof(ind1)) { if(a.is)fwrite(&a,sizeof(a),1,ind); fread(&a,sizeof(a),1,ind1); } fclose(ind1); remove("temp.idx"); } /* cautarea articolului cu cheia Key si plasarea pointerului de fisier in tabela de indexuri pe articolul respectiv*/ int SeekKey(char *Key) { long ls=0, ld, m, n; ART_INDEX a; int gasit=0; fseek(ind,0,SEEK_END); n=ftell(ind)/sizeof(ART_INDEX); ld=n-1; while((ls<=ld)&&(!gasit)) { m=(ls+ld)/2; fseek(ind,m*sizeof(a),SEEK_SET); fread(&a,sizeof(a),1,ind); if(strcmp(a.cheie,Key)==0) gasit=1; else if(strcmp(a.cheie,Key)>0) ld=m-1; else ls=m+1; } if(gasit) fseek(ind,m*sizeof(a),SEEK_SET); return gasit; } void new_index(char *nume) { strcpy(nume_index,nume); strcat(nume_index,".idx"); ind=fopen(nume_index,"wb+"); } void open_index(char *nume) { strcpy(nume_index,nume); strcat(nume_index,".idx"); ind=fopen(nume_index,"rb+"); } void close_index() { fclose(ind); } int ReadSec(fisier f,ARTICOL *a) { ART_INDEX a1; int r; fread(&a1,sizeof(a1),1,ind); if(feof(ind))r=0; else { fseek(f,a1.nr_rel*sizeof(*a),SEEK_SET); fread(a,sizeof(*a),1,f); r=1; } return r; }
97
Programarea calculatoarelor int ReadKey(fisier f,ARTICOL *a,char *Key) { ART_INDEX a1; int r; if(SeekKey(Key)) { fread(&a1,sizeof(a1),1,ind); fseek(f,a1.nr_rel*sizeof(*a),SEEK_SET); fread(a,sizeof(*a),1,f); r=1; } else r=0; return r; } int WriteSec(fisier f,ARTICOL a) { ART_INDEX a1, ai; long n, nl; int r; fseek(ind,0,SEEK_END); n=ftell(ind)/sizeof(a1); if(n>0) { fseek(ind,(n-1)*sizeof(a1),SEEK_SET); fread(&a1,sizeof(a1),1,ind); if(strcmp(a1.cheie,a.cheie)>0) r=0; else { ai.is=1; strcpy(ai.cheie,a.cheie); fseek(f,0,SEEK_END); n1=ftell(f)/sizeof(a); ai.nr_rel=n1; fseek(ind,0,SEEK_END); fwrite(&ai,sizeof(ai),1,ind); fwrite(&a,sizeof(a),1,f); r=1; } } else r=0; return r; } int WriteKey(fisier f,ARTICOL a) { char Key[7]; ART_INDEX a1; long n; strcpy(Key,a.cheie); if(SeekKey(Key)) r=0; else { a1.is=1; strcpy(a1.cheie,a.cheie); fseek(f,0,SEEK_END); n=ftell(f)/sizeof(a); a1.nr_rel=n; fwrite(&a,sizeof(a),1,f); fseek(ind,0,SEEK_END); fwrite(&a1,sizeof(a1),1,ind); Sort(); r=1; } return r; }
98
Algoritmi de prelucrare a fişierelor binare int DeleteSec() { ART_INDEX a1; long pos=ftell(ind); fread(&a1,sizeof(a1),1,ind); if(feof(ind)) r=0; else { fseek(ind,pos,SEEK_SET); a1.is=0; fwrite(&a1,sizeof(a1),1,ind); Sort(); r=1; } return r; } int DeleteKey(char *Key) { int r; if(SeekKey(Key)) r=DeleteSec(); else r=0; return r; }
Exemple 10. Scrieţi programul C pentru crearea în acces direct unui fişier binar cu articole având structura: cheie denumire preţ cantitate Datele sunt preluate de la tastatură până la apăsarea combinaţiei CTRL/Z pentru câmpul cheie. Fişierul creat este organizat indexat. #include "index1.cpp" #include void main() { ARTICOL a; char nume[20],nume1[20]; char x[7]; fisier f; clrscr(); printf(" numele fisierului de date in care adaugati:"); fflush(stdin); gets(nume); strcpy(nume1,nume); strcat(nume1,".dat"); f=fopen(nume1,"rb+"); if(f==NULL) { printf("Fisierul va fi creat"); f=fopen(nume1,"wb+"); new_index(nume); } else open_index(nume); printf("\nAdaugarea in acces direct dupa cheie\n"); printf("Introduceti cheia:"); fflush(stdin); gets(a.cheie); while(!feof(stdin)) { printf("Denumire produs:"); fflush(stdin); gets(a.den); printf("Pret produs:"); scanf("%f",&a.pu);
99
Programarea calculatoarelor printf("Cantitate:"); scanf("%f",&a.cant); if(WriteKey(f,a)) printf("Articol adaugat"); else printf("Exista articol"); getch(); clrscr(); printf("Introduceti cheia:"); fflush(stdin); gets(a.cheie); } fclose(f); close_index(); getch(); }
11. Se presupune creat şi populat fişierul de date din exemplul anterior. Scrieţi programul C pentru ştergerea acelor articole ale căror chei sunt introduse de la tastatură. Încheierea introducerii datelor este marcată standard. #include "index1.cpp" #include void main() { ARTICOL a; char nume[20],nume1[20]; char Key[7]; fisier f; char r; int i; clrscr(); printf(" numele fisierului de date din care stergeti:"); fflush(stdin); gets(nume); strcpy(nume1,nume); strcat(nume1,".dat"); f=fopen(nume1,"rb+"); if(f==NULL) printf("Fisierul nu exista!!"); else { open_index(nume); printf("\nStergerea in acces direct dupa cheie\n"); printf("Introduceti cheia:"); fflush(stdin); gets(Key); while(!feof(stdin)) { if(ReadKey(f,&a,Key)) { printf("Articolul:\n"); printf("Denumire:%20s\n",a.den); printf("Pret:%7.2f\n",a.pu); printf("Cantitate:%8.2f\n\n",a.cant); printf("Doriti stergerea?(D/Altceva)"); r=getch(); if(r=='D') { i=DeleteKey(Key); printf("Stergere efectuata"); } else printf("Stergerea nu a fost efectuata"); } else printf("Nu exista articol"); getch();
100
Algoritmi de prelucrare a fişierelor binare clrscr(); printf("Introduceti cheia:"); fflush(stdin); gets(a.cheie); } fclose(f); close_index(); getch(); } }
12. Scrieţi programul C pentru afişarea informaţiilor memorate în fişierul creat în exemplul 10. Articolele sunt afişate în ordine crescătoare a câmpului cheie. #include "index1.cpp" #include void main() { ARTICOL a; char nume[20],nume1[20]; char x[7]; fisier f; clrscr(); printf(" numele fisierului de date care este consultat:"); fflush(stdin); gets(nume); strcpy(nume1,nume); strcat(nume1,".dat"); f=fopen(nume1,"rb+"); if(f==NULL) printf("Fisierul nu exista!!"); else { open_index(nume); while(ReadSec(f,&a)) { printf("Cheie:"); puts(a.cheie); printf("\nDenumire produs:"); puts(a.den); printf("Pret produs:"); printf("7.2%f\n",a.pu); printf("Cantitate:"); printf("%8.2f\n\n",a.cant); getch(); } fclose(f); close_index(); getch(); } }
6.4 Sortarea fişierelor binare memorate dens Operaţia de sortare a unui fişier binar presupune aranjarea articolelor în ordinea crescătoare (descrescătoare) a valorilor unei zone, numită cheie de sortare. În cazul în care cheia de sortare este formată dintr-un singur câmp din cadrul articolului, operaţia se numeşte sortare simplă. Sortarea multiplă presupune aranjarea articolelor după valorile a două sau mai multe câmpuri, alcătuind, prin juxtapunere, cheia de sortare. Juxtapunerea câmpurilor (nu neapărat adiacente în cadrul articolului) se realizează pe lungimea efectivă a lor, alcătuind forma canonică a cheii de sortare. De
101
Programarea calculatoarelor
exemplu, dacă NUME şi PRENUME sunt două câmpuri distincte, declarate de tip şir de caractere, forma canonică a cheii de sortare după nume şi prenume este dată de lungimea efectivă a fiecărei date de tip şir. Dacă pentru sortarea simplă cheia de sortare poate fi însuşi câmpul din articol, pentru cea multiplă este necesară o zonă auxiliară de memorie, în care se construieşte cheia de sortare, în forma canonică. Sortarea unui fişier se poate realiza cu aducerea lui integrală în memorie (sortare în memorie) sau cu aducerea în memorie a câte unui articol (sortare "direct pe disc"). Indiferent de modul utilizat, sortarea poate fi realizată printr-unul din algoritmii cunoscuţi pentru masivele de date: sortare prin interschimbare, prin selecţie, prin inserţie etc. Sortarea în memorie este o metodă rapidă şi presupune: citirea întregului fişier în memoria principală, într-o structură internă de date (vector, arbore bina de sortare); sortarea efectivă după cheia de sortare (în cazul folosirii unui vector, operaţia nu este necesară dacă se foloseşte arbore de sortare); recrearea fişierului pe disc. Metoda se poate aplica numai fişierelor reduse ca dimensiuni sau cu lungime mică de articol, dată fiind capacitatea limitată a memoriei interne asociată unui program. Ea poate avea mai multe variante: Sortarea cu vehicularea întregului articol, presupune memorarea întregului fişier într-un vector de articole. Compararea pentru sortare se va realiza pe câmpul cheie de sortare, însă interschimbarea se realizează la nivelul întregului articol. Exemplu: 13. typedef struct { char grupa; char nume_student[30]; float medie; }STUDENT; void main() { FILE* f; STUDENT x[250], aux; int i,j,n; f=fopen("STUDENT.DAT","rb+"); fseek(f,0,SEEK_END); n:=ftell(f)/sizeof(STUDENT); rewind(f); // citirea fisierului initial in memorie for(i=0;ix[j].medie) { aux:=x[i]; //interschimbarea articolelor x[i]:=x[j]; //nu se poate folosi atribuirea mereu x[y]:=aux; } rewind(f);
102
Algoritmi de prelucrare a fişierelor binare for(i=0;i
Sortarea cu vehicularea cheii şi indexului, presupune memorarea într-un vector numai a valorii cheii de sortare, împreună cu numărul relativ al articolului din fişierul iniţial (indexul). Interschimbarea se va realiza la nivelul cheii de sortare, rezultând în final ordinea în care articolele vor fi scrise în fişier. Deoarece articolele, în întregime, sunt rezidente pe disc, fişierul sortat va fi creat cu un alt nume fizic, în acces secvenţial, preluînd articolele din fişierul iniţial, în acces direct. Cheile de sortare şi indexurile pot fi memorate în vectori distincţi sau într-unul singur, cu elemente de tip articol. Exemplu: 14. typedef struct { char grupa; char nume_student[30]; float medie; }STUDENT; typedef struct { float medie; int index; }CHEIE; void main() { FILE *f,*g; STUDENT y; CHEIE aux,x[250]; int i,j,n; f=fopen("STUDENT.DAT","rb"); fseek(f,0,SEEK_END); n:=ftell(f)/sizeof(STUDENT); rewind(f); g=fopen("STUDENTS.DAT","wb"); // ---------------------------------for(i=0;ix[j].medie) { aux=x[i]; x[i]:=x[j]; x[j]:=aux } //-------------------------------------for(i=0;i
103
Programarea calculatoarelor
Sortarea numai cu vehicularea indexului este o metodă mai bună decât precedenta, deoarece micşorează timpul de execuţie, prin eliminarea interschimbării valorilor cheii de sortare (mai ales când aceasta este multiplă). Valorile cheii de sortare şi numărului relativ corespunzător indexului se memorează în vectori distincţi. Comparaţiile se realizează pentru valorile cheii, dar interschimbarea se efectuează numai pentru indexuri. Se va crea un alt fişier fizic. Exemplu: 15. typedef struct { char grupa; char nume_student[30]; float medie; }STUDENT; void main() { FILE *f,*g; float x[250]; int index[250]; int i,j,n; STUDENT y; f=fopen("STUDENT.DAT","rb"); fseek(f,0,SEEK_END); n:=ftell(f)/sizeof(STUDENT); rewind(f); g=fopen("STUDENTS.DAT","wb"); //-----------------------------------------for(i=0;ix[j]) { aux=index[i]; index[i]=index[j]; index[j]=aux } //------------------------------------------for(i=0;i
Sortarea pe disc se aplică fişierelor mari, la care este imposibilă aducerea în memoria principală chiar şi a minimului de informaţii necesare sortării. În acest caz, operaţia se va realiza "direct" pe mediul magnetic, cu aducerea în memorie doar a două articole (pentru comparaţii) şi scrierea în acces direct în acelaşi fişier, prin utilizarea numărului relativ al articolelor prelucrate. Timpul de prelucrare va fi substanţial mai mare decât la metodele de sortare în memorie, deoarece operaţiile de intrare/ieşire sunt costisitoare din punct de vedere al resursei timp calculator.
104
Algoritmi de prelucrare a fişierelor binare
Se poate aplica oricare din algoritmii de sortare cunoscuţi, cu menţiunea că indicii i şi j vor fi utilizaţi pentru controlul numărului relativ al articolelor în fişier. Exemple: 16. Sortarea prin interschimbare do { vb=0; for(i=0;iy.medie) { fseek(f,sizeof(STUDENT)*i,SEEK_SET); fwrite(&y,sizeof(STUDENT),1,f); fwrite(&x,sizeof(STUDENT),1,f); vb=1; } while(vb);
17. Sortare prin selecţie for(i=0;iy.medie) { fseek(f,sizeof(STUDENT)*i,SEEK_SET); fwrite(&y,sizeof(STUDENT),1,f); fseek(f,sizeof(STUDENT)*j,SEEK_SET); fwrite(&x,sizeof(STUDENT),1,f); } } }
6.5 Interclasarea fişierelor binare memorate dens Interclasarea este operaţia prin care, din două sau mai multe mulţimi ordonate, se obţine o nouă mulţime, ordonată după acelaşi criteriu. Interclasarea fişierelor apare ca necesitate în aplicaţiile economice, mai ales în faza de postpopulare a fişierelor mari de date, create simultan pe submulţimi de mai mulţi utilizatori şi necesitând, în final, reunirea acestora într-unul singur. Condiţia apriori interclasării este ca toate fişierele parţiale să fie sortate după valorile aceluiaşi câmp, pe baza căruia se va realiza, prin comparări succesive, operaţia de interclasare. Câmpul poartă denumirea de cheie de interclasare. Interclasarea a n fişiere se poate realiza simplu prin aplicarea de n-1 ori a operaţiei de interclasare a două fişiere (figura 6.12).
105
Programarea calculatoarelor
1
2
3
4
...
n
Interclasare 1 ………..
Fişier 1
Interclasare 2 ………………………...
Fişier 2 Interclasare 3 …………………………………………...
Fişier 3 ... Interclasare 3 …………………………………………………………………………………………………………. Fişier final
Fişier n-1
Figura 6.13 Interclasarea a n fişiere Se obţin astfel n-1 fişiere intermediare (fişier i), din care numai ultimul se păstrează, celelalte (împreună cu fişierele iniţiale) se şterg, fie în finalul procesului, fie la sfârşitul fiecărei etape intermediare (recomandat). Interclasarea a două fişiere este similară operaţiei aplicate pentru doi vectori. Dimensiunea fişierului rezultat este suma dimensiunilor fişierelor iniţiale. Exemplu: 18. Se prezintă structura principială a unui program pentru interclasarea a două fişiere binare. Cheile de interclasare se află în câmpul c aparţinând articolelor art_1 şi art_2, corespunzătoare fişierelor de intrare f şi g, considerate populate dens. { //--------------------------------//citire nume externe ale fisierelor //--------------------------------f=fopen(nume_fisier_intrare_1, "rb"); g=fopen(nume_fisier_intrare_2, "rb"); h=fopen(nume_fisier_iesire, "wb"); fread(&art_1,sizeof(tip_articol),1,f); fread(&art_2,sizeof(tip_articol),1,g); while((!feof(f)&&(!feof(g))) if(art_1.c>art_2.c) { fwrite(&art_1,sizeof(tip_articol),1,h); fread(&art_1,sizeof(tip_articol),1,f); }
106
Algoritmi de prelucrare a fişierelor binare else { fwrite(&art_2,sizeof(tip_articol),1,h); fread(&art_2,sizeof(tip_articol),1,g); } while(!feof(f)) { fwrite(&art_1,sizeof(tip_articol),1,h); fread(&art_1,sizeof(tip_articol),1,f); } while(!feof(g)) { fwrite(&art_2,sizeof(tip_articol),1,h); fread(&art_2,sizeof(tip_articol),1,g); } fclose(f); fclose(g); fclose(h) }
6.6 Prelucrarea masivelor memorate în fişiere binare Una dintre aplicaţiile des întâlnite în lucrul cu fişiere este memorarea masivelor de date de dimensiuni foarte mari, care fac imposibilă aducerea lor integrală în memoria internă. Problema principală a prelucrării masivelor (vectori, matrice etc.) memorate în fişiere binare, o constituie determinarea poziţiei unui anumit element de masiv în cadrul fişierului. Indiferent de numărul de dimensiuni ale masivului şi de modalităţile de memorare a elementelor sale în cadrul fişierului, legătura între elementul de masiv care se referă şi numărul relativ al articolului care îl conţine se realizează pe baza funcţiei rang. În cazul masivelor memorate în fişiere, prelucrarea acestora depinde de unele caracteristici particulare: numărul de dimensiuni ale masivului; ordinea de memorare în fişier (în ordine lexicografică sau invers lexicografică); modul de memorare (dens sau nedens); ordinea de parcurgere a masivului. 6.6.1 Prelucrarea vectorilor De regulă, vectorii se memorează dens. Numărul relativ al articolului depinde de rangul elementului în cadrul vectorului, astfel: nr_relativ = rang(xi)+1 = i+1, pentru i=0..n-1, dacă articolul cu numărul relativ 0, fie nu este utilizat (caz în care dimensiunea vectorului este n = dimensiune fişier-1), fie memorează numărul efectiv de componente ale vectorului; nr_relativ = rang(xi) = i, pentru i=0..n, dacă vectorul se memorează începând cu primul articol (caz în care dimensiunea vectorului este n =dimensiunea fişierului). Exemplu: 19. Să se determine media aritmetică a elementelor unui vector foarte mare, memorat într-un fişier binar.
107
Programarea calculatoarelor
#include<stdio.h> void main() { FILE* vector; float element, medie; long i,n; vector=fopen("VECTOR.DAT","rb"); fseek(vector,0,SEEK_END); n=ftell(f)/sizeof(float); rewind(f); medie=0; for(i=0;i
6.6.2 Prelucrarea matricelor O matrice poate fi memorată într-un fişier binar nedens (similar memorării în MP) sau dens, în ordine lexicografică sau invers lexicografică. Numărul relativ al elementului aij se determină pe baza funcţiei rang, astfel: rang(aij) = i * nr_coloane + j, în cazul memorării lexicografice, unde nr_coloane este fie numărul coloanelor efective (populare densă), fie numărul coloanelor rezervate (populare nedensă); rang(aij) = j * nr_linii + i, în cazul memorării invers lexicografice, unde nr_linii este fie numărul liniilor efective (populare densă), fie numărul liniilor rezervate (populare nedensă). Fie m şi n numărul liniilor, respectiv coloanelor efective şi mr şi nr numărul liniilor, respectiv coloanelor rezervate (mr şi nr corespund dimensiunilor maxime din declaraţia unui masiv aflat în memoria principală). Pentru ca fişierul să conţină informaţii complete despre matrice, trebuie să memoreze, pe lângă elementele ei, şi: m (sau n), în cazul memorării dense. Când se memorează m, n se determină împărţind dimensiunea fişierului (mai puţin primul articol, unde se află m) la m; când se memorează n, m se determină împărţind dimensiunea fişierului (mai puţin primul articol, unde se află n) la n. Funcţia rang depinde de m sau n, după cum matricea este memorată invers lexicografic sau lexicografic; n şi nr, în cazul memorării nedense în ordine lexicografică. m se determină împărţind dimensiunea fişierului (mai puţin primele două articole, unde se află n şi nr) la nr, iar mr nu are relevanţă. Funcţia rang depinde de nr; m şi mr, în cazul memorării nedense în ordine invers lexicografică. N se determină împărţind dimensiunea fişierului (mai puţin primele două articole, unde se află m şi mr) la mr, iar nr nu are relevanţă. Funcţia rang depinde de mr. Funcţia rang se calculează şi se utilizează numai dacă problema de rezolvat implică parcurgerea matricei în altă ordine decât cea în care este memorată în fişier, deci consultarea acestuia se realizează în acces direct.
108
Algoritmi de prelucrare a fişierelor binare
Exemple: 20. Să se afişeze elementul maxim de pe fiecare coloană a unei matrice de dimensiuni mxn, memorate dens, într-un fişier binar, în ordine lexicografică. Primul articol conţine numărul de coloane. • Observaţie: primul articol are dimensiune diferită de celelalte: numărul de coloane este de tip întreg iar elementele matricei sunt reale. Din dimensiunea totală a fişierului, primii sizeof(int) octeţi sunt ocupaţi de numărul de coloane, restul constituie matricea propriu-zisă. La calcularea poziţiei unui element în matrice trebuie ţinut cont de faptul că matricea nu începe la începutul fişierului, ci după sizeof(int) octeţi. #include<stdio.h> void main() { FILE *f; float max, element; long i,j,r,m,n; f=fopen("MATRICE.DAT", "rb"); fread(&n,sizeof(int),1,f); //citire numar de coloane fseek(f,0,SEEK_END); m=(ftell(f)-sizeof(int))/(sizeof(float)*n); for(j=0;jmax) max=element; } printf("\Maximul pe coloana %2d este %7.3f",j,max); } fclose(f); }
21. Să se determine elementul maxim de pe fiecare coloană a unei matrice de dimensiuni m x n, memorată nedens într-un fişier binar, în ordine lexicografică. Primele două articole conţin numărul de coloane efective şi, respectiv, numărul rezervat de coloane. Rezolvarea este similară cu exemplul anterior, cu următoarele modificări: • din dimensiunea totală a fişierului, primii 2*sizeof(int) octeţi sunt ocupaţi de dimensiunile matricei (număr de coloane efectiv respectiv rezervat), iar restul constituie matricea propriu zisă. La calcularea poziţiei unui element în matrice trebuie ţinut cont de faptul că matricea nu începe la începutul fişierului, ci după 2*sizeof(int) octeţi. • La calcularea rangului unui element (şi implicit a poziţiei sale în fişier) se foloseşte numărul de coloane rezervare, nu numărul de coloane efective.
109
7 Structuri dinamice de date. Liste
Organizarea de tip listă corespunde unei structurări lineare a datelor, în sensul că la nivelul fiecărei componente există suficientă informaţie pentru identificarea următoarei componente a colecţiei. Datele unei mulţimi structurate prin intermediul listelor sunt referite de obicei prin termenii de noduri, celule, componente etc.
7.1 Reprezentarea listelor Reprezentarea unei liste poate fi realizată static prin intermediul structurii de date vector. În acest caz ordinea componentelor este dată de ordinea pe domeniul de valori corespunzător indexării şi, în consecinţă, următoarea componentă este implicit specificată. Memorarea unei mulţimi de date {d1, d2,…, dn} prin intermediul unei structuri statice poate fi realizată în limbajul C utilizând un masiv unidimensional. Principalele dezavantaje ale utilizării reprezentării statice rezidă din volumul de calcule necesare efectuării operaţiilor de inserare/eliminare de noduri şi din necesitatea păstrării unei zone de memorie alocată, indiferent de lungimea efectivă a listei. Aceste dezavantaje pot fi eliminate prin opţiunea de utilizare a structurilor dinamice. Componentele unei liste dinamice sunt omogene, de tip articol. Fiecare nod, considerat separat, este o structură eterogenă, conţinând o parte de informaţie şi câmpuri de legătură care permit identificarea celulelor vecine. Câmpurile de legătură sunt reprezentate de date de tip referinţă (adresă). În cazul listelor cu un singur câmp de legătură (simplu înlănţuite), valoarea câmpului indică adresa nodului următor, în timp ce în cazul listelor cu dublă legătură (dublu înlănţuite), valorile memorate în câmpurile de legătură sunt adresele componentelor care preced şi, respectiv, urmează celulei. În ambele situaţii, câmpul de legătură pentru indicarea celulei următoare corespunzător ultimei componente a listei are valoarea NULL în cazul listelor „deschise” (lineare)
110
Structuri dinamice de date. Liste
şi respectiv indică adresa primei componente din listă în cazul listelor „închise” (circulare). Declararea tipurilor de date C pentru definirea structurilor de liste dinamice simplu şi respectiv dublu înlănţuite este: a)
Listă simplu înlănţuită
typedef struct nod{ tip_informatie inf; struct nod *leg; } list, *lista;
b)
Listă dublu înlănţuită
typedef struct nod{ tip_informatie inf; struct nod *ls, *ld; } list, *lista;
unde tip_informatie este numele tipului de date C utilizat pentru memorarea fiecărei date din mulţimea{d1, d2,…, dn}. În cele ce urmează vom considera că tip_informatie este tipul C int.
7.2 Operaţii primitive asupra listelor Accesul la informaţia stocată într-o variabilă de tip listă revine la efectuarea următoarelor operaţii primitive: regăsirea nodului (dacă există) care corespunde unei chei date (condiţie impusă asupra valorii câmpului de informaţie), inserarea unei noi componente în listă, eliminarea componentei (componentelor) cu proprietatea că valorile câmpurilor de informaţie satisfac o anumită cerinţă şi înlocuirea câmpului de informaţie corespunzător unei componente printr-o informaţie dată (modificată). Accesarea componentelor unei liste reprezentată printr-o structură statică poate fi realizată atât secvenţial, cât şi direct, utilizând valorile indicelui considerat pentru indexare, în timp ce accesarea componentelor unei liste dinamice se realizează de regulă numai secvenţial, începând cu prima componentă şi continuând cu următoarele, pe baza valorilor câmpurilor de legătură. Convenţional, numim cap al listei dinamice pointerul a cărui valoare este adresa primei componente a listei. În continuare ne vom referi exclusiv la liste dinamice, studiul listelor reprezentate prin intermediul vectorilor fiind propus cititorului. 1. Parcurgerea datelor memorate într-o listă Funcţia C parc implementează parcurgerea unei liste dinamice în varianta simplu înlănţuită şi în cazul listelor dublu înlănţuite. Se presupune că declaraţiile de tip pentru definirea structurilor de liste menţionate anterior sunt globale, relativ la procedurile descrise în continuare. a) Lista reprezentată prin structură dinamică simplu înlănţuită void parc(lista cap) { if(cap) { printf("%i ",cap->inf); parc(cap->leg); } }
111
Programarea calculatoarelor
b) Lista reprezentată prin structură dinamică dublu înlănţuită void parc(lista cap) { if(cap) { printf("%i ",cap->inf); parc(cap->ls); } }
2. Regăsirea unei date într-o colecţie memorată într-o listă Funcţia C cauta calculează adresa nodului în care este găsit elementul căutat. Dacă valoarea căutată nu se regăseşte printre elementele listei, funcţia returnează valoarea NULL. a) Lista reprezentată prin structură dinamică simplu înlănţuită lista cauta(lista cap,int info) { if(cap==NULL)return NULL; else if(cap->inf==info) return cap; else return cauta(cap->leg,info); }
b) Lista reprezentată prin structură dinamică dublu înlănţuită lista cauta(lista cap,int info) { if(cap==NULL)return NULL; else if(cap->inf==info) return cap; else return cauta(cap->ls,info); }
3. Inserarea unei date într-o listă Includerea unei noi componente într-o listă poate fi realizată, în funcţie de cerinţele problemei particulare, la începutul listei, după ultima componentă din listă, înaintea/după o componentă cu proprietatea că valoarea câmpului de informaţie îndeplineşte o anumită condiţie. Deoarece prin inserarea unei componente se poate ajunge la depăşirea spaţiului disponibil de memorie, este necesară verificarea în prealabil dacă este posibilă inserarea sau nu (dacă se poate aloca spaţiu de memorie pentru componenta de inserat). În continuare ne vom referi la liste dinamice simplu înlănţuite. Operaţiile de inserare în cazul listelor dinamice dublu înlănţuite pot fi realizate similar cazului listelor simplu înlănţuite, cu specificarea ambelor câmpuri de adresă ale nodurilor. Pentru exemplificarea operaţiei de inserare sunt prezentate funcţiile de inserare la începutul listei, inserare după ultimul element al listei şi inserarea unei celule după un nod cu informaţie dată. Inserarea la începutul listei Funcţia C inserare_la_inceput returnează valoarea 1 dacă adăugarea unui nou element este posibilă (spaţiul de memorie este suficient pentru o nouă alocare), altfel returnează 0. În cazul în care inserarea este posibilă, prin apelul funcţii este realizată adăugarea unui nou nod la începutul listei.
112
Structuri dinamice de date. Liste int inserare_la_inceput(lista *cap,int info) { lista nou; if(nou=(lista)malloc(sizeof(list))) { nou->inf=info; nou->leg=*cap; *cap=nou; return 1; } return 0; }
Inserarea după ultima componentă a unei liste Funcţia C inserare_la_sfarsit returnează 1 dacă şi numai dacă este posibilă o inserare, altfel calculează 0. Pentru inserarea unui nou nod în listă după ultima celulă este necesar calculul ultimului nod al listei, notat p. int inserare_la_sfarsit(lista *cap,int info) { lista nou; if(nou=(lista)malloc(sizeof(list))) { nou->leg=NULL;nou->inf=info; if(cap==NULL)*cap=nou; else { for(lista p=*cap;p->leg;p=p->leg); p->leg=nou; } return 1; } return 0; }
Inserarea unei informaţii după o celulă cu informaţie cunoscută Inserarea unui nou nod într-o listă identificată prin variabila cap după o celulă p cu informaţie cunoscută, infod, poate fi realizată astfel. Este apelată funcţia de căutare cauta, care calculează nodul p cu proprietatea că informaţia memorată în p este infodat. Dacă p este adresa vidă sau dacă spaţiul de memorie disponibil nu este suficient, inserarea nu poate fi realizată. În caz contrar, este inserat un nou nod între celulele p şi p->leg. int inserare_dupa_informatie(lista cap,int info,int infod) { lista nou,p; if(nou=(lista)malloc(sizeof(list))) if(p=cauta(cap,infod)){ nou->inf=info; nou->leg=p->leg; p->leg=nou; return 1; } return 0; }
113
Programarea calculatoarelor
4. Eliminarea unei date dintr-o listă Modificarea conţinutului unei liste prin eliminarea uneia sau mai multor componente poate fi descrisă secvenţial, astfel încât este suficient să dispunem de o procedură care realizează eliminarea unei singure componente. Criteriile de eliminare pot fi formulate diferit, cele mai uzuale fiind: prima componentă, ultima componentă, prima componentă care îndeplineşte o anumită condiţie, respectiv componenta care precede/urmează primei componente care îndeplineşte o condiţie dată. În aceste cazuri este necesară verificarea existenţei în lista considerată a componentei ce trebuie eliminată. Verificarea asigură şi testarea faptului că lista prelucrată este vidă sau nu. Informaţia i ataşată nodului eliminat din listă reprezintă dată de ieşire pentru orice modul de eliminare, în cazul în care i nu este cunoscută înaintea eliminării (de exemplu, atunci când este solicitată eliminarea unei celule care conţine o informaţie dată) . Eliminarea unui nod p poate fi realizată logic sau fizic. Eliminarea logică a celulei p este efectuată excluzând p din lista dinamică prin setarea legăturii nodului care precede p pe adresa succesorului lui p, dacă p nu este adresa primului element al listei, cap, respectiv prin atribuirea adresei primului element al listei cu cap->leg, în caz contrar. Eliminarea cu ştergere fizică unui nod p presupune redenumirea acelui nod în scopul eliberării memoriei ocupate de p şi efectuarea operaţiilor descrise în cadrul procesului de eliminare logică. În continuare sunt prezentate următoarele tipuri de eliminări, cu ştergere fizică. Eliminarea primei componente a unei liste Funcţia C elimina_de_la_inceput returnează 1 dacă lista nu este vidă, deci eliminarea primului nod este posibilă, altfel returnează 0. Dacă lista conţine măcar un nod, este eliminată prima celulă. Int elimina_de_la_inceput(lista *cap,int *info) { if(*cap) { lista aux=*cap; *info=aux->inf; *cap=(*cap)->leg; free(aux); return 1; } return 0; }
Eliminarea ultimei componente a unei liste Similar operaţiei de inserare a unui nod după ultima celulă a unei liste, eliminarea ultimului nod presupune determinarea acelei celule p cu proprietatea că p->leg este NULL. Funcţia C elimina_ultim returnează 1 dacă lista nu este vidă,
114
Structuri dinamice de date. Liste
caz în care este eliminat cu ştergere ultimul nod al listei. Dacă lista este vidă, funcţia calculează valoarea 0. Int elimina_ultim(lista *cap,int *info) { if (*cap) { if((*cap)->leg) { for(lista p=*cap;p->leg->leg;p=p->leg); *info=p->leg->inf; free(p->leg); p->leg=NULL; } else { *info=(*cap)->inf; free(*cap); *cap=NULL; } return 1; } return 0; }
Eliminarea primei celule a unei liste care are informaţia egală cu o informaţie dată Pentru realizarea acestei operaţii se poate proceda astfel. Sunt calculate aux şi p, unde aux este nodul care precede celulei cu informaţie dată în lista din care este efectuată eliminarea (funcţia C cautaprecedent) şi p=aux->leg.. Dacă p este NULL, atunci eliminarea este imposibilă. Dacă aux este NULL, atunci eliminarea revine la extragerea cu ştergere a primului nod din listă, altfel este eliminată celula p, succesoare a lui aux în listă. Funcţia C elimina_informatie implementează operaţia de eliminare a unui nod cu informaţie dată, info, din lista identificată prin parametrul cap. lista cautaprecedent(lista cap,int info, lista *aux) { lista p; if(cap==NULL)return NULL; else { for(p=NULL,*aux=cap;(*aux)&& ((*aux)->inf-info);p=*aux,*aux=(*aux)->leg); if((*aux)==NULL)return NULL; return p; } } int elimina_informatie(lista *cap,int info) { lista aux,p; p=cautaprecedent(*cap,info,&aux); if(aux==*cap) { *cap=(*cap)->leg; free(aux); return 1; } else if(p) { p->leg=aux->leg;
115
Programarea calculatoarelor free(aux); return 1; } return 0; }
Eliminarea nodului care succede primei componente al cărei câmp de informaţie este cunoscut Funcţia C elimina_dupa_informatie returnează valoarea 1 dacă eliminarea este posibilă, altfel calculează valoarea 0. În situaţia în care informaţia infodat a fost găsită în cîmpul corespunzător nodului nodul p (p nu este NULL) şi p are succesor în listă, este realizată eliminarea nodului p->leg. int elimină_dupa_informatie(lista cap,int *info, int infodat) { lista aux,p; p=cauta(cap,infodat); if((p)&&(p->leg)) { aux=p->leg; p->leg=aux->leg; *info=aux->inf; free(aux); return 1; } return 0; }
7.3 Liste circulare În anumite situaţii este preferabilă renunţarea la structura de tip linear a listelor şi utilizarea unei legături de la ultima componentă către capul listei, rezultând structura de listă circulară. Principalul avantaj al utilizării acestui tip de structură rezidă din posibilitatea de accesare oricărui alt element al listei pornind din orice element. Dacă nodul căutat este situat după nodul curent, este iniţiat un proces de căutare similar listelor lineare. În caz contrar, nodul poate fi accesat prin parcurgerea listei de la primul său element, care, în procesul de căutare, este atins după parcurgerea în întregime a listei, începând de la nodul curent. În continuare sunt prezentate funcţiile C pentru realizarea unor operaţii de bază în lucrul cu liste circulare. #include<stdio.h> #include #include typedef struct nod { int inf; struct nod *leg; } list, *lista;
116
Structuri dinamice de date. Liste int inserare_la_inceput(lista int stergere_la_inceput(lista int inserare_la_sfarsit(lista int stergere_la_sfarsit(lista void parc(lista); lista cauta(lista,int);
*,int); *,int *); *,int); *,int *);
void main() { clrscr(); int n,info; lista cap=NULL; printf("Numarul de noduri:"); scanf("%i",&n); printf("Introduceti informatiile\n"); for(int i=0;iinf); for(p=p->leg;p-cap;p=p->leg)
117
Programarea calculatoarelor printf("%i ",p->inf); } else printf("\nLista vida"); } lista cauta(lista cap,int info) { if(cap==NULL)return NULL; if(cap->inf==info) return cap; for(lista p=cap->leg;p!=cap;p=p->leg) if(p->inf==info) return p; return NULL;} int inserare_la_inceput(lista *cap,int info) { lista nou,ultim; if(nou=(lista)malloc(sizeof(list))){ nou->inf=info; nou->leg=*cap; if(*cap){ for(ultim=*cap;ultim->leg!=(*cap);ultim=ultim->leg); ultim->leg=nou; } else nou->leg=nou; *cap=nou; return 1; } return 0; } int stergere_la_inceput(lista *cap,int *info) { if(*cap){ lista aux=*cap; *info=aux->inf; for(lista ultim=*cap; ultim->leg!=(*cap);ultim=ultim->leg); if(ultim==(*cap)) *cap=NULL; else{ *cap=(*cap)->leg; ultim->leg=*cap; } free(aux); return 1; } return 0; } int inserare_la_sfarsit(lista *cap,int info) { lista nou,ultim; if(nou=(lista)malloc(sizeof(list))){ nou->leg=*cap;nou->inf=info; if(*cap==NULL){ *cap=nou; (*cap)->leg=*cap; }
118
Structuri dinamice de date. Liste else{ for(ultim=*cap;ultim->leg!=(*cap); ultim=ultim->leg); ultim->leg=nou; } return 1; } return 0; } int stergere_la_sfarsit(lista *cap,int *info) { if (*cap){ if((*cap)->leg!=(*cap)){ for(lista pultim=*cap; pultim->leg->leg!=(*cap);pultim=pultim->leg); *info=pultim->leg->inf; free(pultim->leg); pultim->leg=(*cap); } else{ *info=(*cap)->inf; free(*cap); *cap=NULL; } return 1; } return 0; }
7.4 Stive şi cozi Aşa cum a rezultat din subcapitolele precedente, operaţiile de inserare şi eliminare sunt permise la oricare dintre componentele unei liste. O serie de aplicaţii pot fi modelate utilizând liste lineare în care introducerea şi respectiv eliminarea informaţiilor este permisă numai la capete. În acest scop au fost introduse tipurile de listă stivă şi coadă prin impunerea unui tip de organizare a aplicării operaţiilor de inserare şi eliminare. 7.4.1 Stiva Se numeşte stivă o listă organizată astfel încât operaţiile de inserare şi eliminare sunt permise numai la prima componentă. Acest mod de organizare corespunde unei gestiuni LIFO (Last In First Out) a informaţiei stocate. Operaţiile de bază efectuate asupra unei stive pot fi realizate similar cazului listelor dinamice lineare, ţinând cont că inserarea/extragerea unui element sunt posibile numai în prima poziţie (vezi modulul de inserare la începutul unei liste şi respectiv funcţia de extragere a primului nod dintr-o listă, prezentate în §7.2).
119
Programarea calculatoarelor
7.4.2 Coada Se numeşte coadă o listă organizată astfel încât operaţia de inserare este permisă la ultima componentă, iar operaţia de eliminare este permisă numai la prima componentă. Acest mod de organizare corespunde unei gestiuni FIFO (First In First Out) a informaţiei stocate. Implementarea unei liste de tip coadă poate fi efectuată atât printr-o structură statică (masiv unidimensional), cât şi printr-o structură dinamică de tip listă. Pentru optimizarea operaţiilor de inserare/extragere, în cazul implementării cozilor prin structuri dinamice lineare, este necesară utilizarea a două informaţii: adresa primei componente şi adresa ultimei componente. Aceste informaţii pot fi menţinute explicit prin utilizarea a doi pointeri sau prin utilizarea unui pointer şi a unei structuri de listă circulară. O variantă alternativă de implementare a unei liste de tip coadă dinamică este obţinută prin considerarea unei liste circulare, cu memorarea adresei ultimului element. În continuare sunt prezentate operaţiile de inserare şi extragere a unei informaţii dintr-o listă liniară de tip coadă. #include<stdio.h> #include #include typedef struct nod{ int inf; struct nod *leg; } list, *lista; int inserare(lista *,lista *,int); int extragere(lista *,lista *,int *); void parc(lista); void main() { clrscr(); int n,info; lista cap=NULL,ultim=NULL; printf("Numarul de noduri:"); scanf("%i",&n); printf("Introduceti informatiile\n"); for(int i=0;i
120
Structuri dinamice de date. Liste printf("\nCoada rezultata\n"); parc(cap); printf("\n\nCoada dupa o extragere:\n"); if(extragere(&cap,&ultim,&info)) parc(cap); else printf("\nEroare: Coada vida"); getch(); } void parc(lista cap) { if(cap){ printf("%i ",cap->inf); parc(cap->leg); } } int extragere(lista *cap,lista *ultim,int *info) { if(*cap){ lista aux=*cap; *info=aux->inf; if((*ultim)==(*cap)) *cap=*ultim=NULL; else *cap=(*cap)->leg; free(aux); return 1; } return 0; } int inserare(lista *cap,lista *ultim,int info) { lista nou; if(nou=(lista)malloc(sizeof(list))){ nou->inf=info;nou->leg=NULL; if(*cap==NULL) *cap=*ultim=nou; else{ (*ultim)->leg=nou; (*ultim)=nou; } return 1; }
return 0;}
121
8 Grafuri
Grafurile sunt structuri de date cu aplicaţii în multe domenii ale informaticii, algoritmii pentru reprezentarea şi prelucrarea grafurilor fiind consideraţi fundamentali în acest domeniu. În subcapitolul 8.1 sunt prezentate principalele noţiuni ale domeniului, precum şi modalităţile uzuale de reprezentare a structurii de graf. În continuare sunt descrise tehnicile de parcurgere a grafurilor în lăţime şi în adîncime. Traversarea în adîncime a grafurilor determină obţinerea unei clasificări a muchiilor, în funcţie de care pot fi derivate diferite proprietăţi ale grafurilor. Verificarea conexităţii şi calculul drumurilor în grafuri sunt tratate în subcapitolul 8.3. În finalul capitolului este studiată problema determinării circuitelor şi ciclurilor în grafuri şi digrafuri.
8.1 Definiţii şi reprezentări ale grafurilor Definiţia 8.1.1 Se numeşte graf sau graf neorientat o structură G=(V,E), unde V este o mulţime nevidă, iar E este o submulţime posibil vidă a mulţimii perechilor neordonate cu componente distincte din V. Elementele mulţimii V se numesc vârfuri, iar obiectele mulţimii E se numesc muchii. Dacă e ∈ E, e = (u,v) not.uv , vârfurile u şi v se numesc extremităţi ale lui e, muchia e fiind determinată de vârfurile u şi v. Dacă e=uv ∈ E se spune că vârfurile u, v sunt incidente cu muchia e. Definiţia 8.1.2 Fie G=(V,E) graf. Vârfurile u, v sunt adiacente în G dacă uv ∈ E. Definiţia 8.1.3 Graful G=(V,E) este graf finit, dacă V este o mulţime finită. În cadrul acestui capitol vor fi considerate în exclusivitate grafurile finite, chiar dacă acest lucru nu va fi precizat în mod explicit. Definiţia 8.1.4 Fie Gi =(V i,Ei), i=1,2 grafuri. G2 este un subgraf al grafului G1 dacă V2 ⊆ V1 şi E 2 ⊆ E1 . G2 este un graf parţial al lui G1 dacă V2=V1 şi G2 este subgraf al lui G1.
122
Grafuri
Definiţia 8.1.5 Un digraf este o structură D=(V,E), unde V este o mulţime nevidă de vârfuri, iar E este o mulţime posibil vidă de perechi ordonate cu componente elemente distincte din V. Elementele mulţimii E sunt numite arce sau muchii ordonate. Un graf direcţionat este o structură D=(V,E), unde V este o mulţime nevidă de vârfuri, iar E este o mulţime posibil vidă de perechi ordonate cu componente elemente din V, nu neapărat distincte. Evident, orice digraf este un graf direcţionat. Terminologia utilizată relativ la digrafuri este similară celei corespunzătoare grafurilor. În continuare vom referi prin muchie şi elementele mulţimii E ale unui graf direcţionat, în situaţia în care este tratat cazul unui graf oarecare (neorientat sau direcţionat). Definiţia 8.1.6 Se numeşte graf ponderat o structură (V,E,W), unde G=(V,E) este graf şi W este o funcţie definită prin W : E → (0 , ∞ ) . Funcţia W este numită pondere şi ea asociază fiecărei muchii a grafului un cost/câştig al parcurgerii ei. Definiţia 8.1.7 Fie G=(V,E) un graf, u,v∈V. Secvenţa de vârfuri Γ:u0,u1,..,un este un u-v drum dacă u0=u, un=v, uiui+1∈E pentru toţi i, 0 ≤ i ≤ n . Definiţia 8.1.8 Fie G=(V,E) un graf. Elementul v∈V se numeşte vârf izolat dacă, pentru orice e ∈ E, u nu este incident cu e. 8.1.1 Moduri de reprezentare a grafurilor Cea mai simplă reprezentare a unui graf este cea intuitivă, grafică; fiecare vârf este figurat printr-un punct, respectiv muchiile sunt reprezentate prin segmentele de dreaptă, orientate (în cazul digrafurilor) sau nu şi etichetate (în cazul grafurilor ponderate) sau nu, avînd ca extremităţi punctele corespunzătoare vârfurilor care o determină Exemple 8.1.1 Fie G=(V,E) graf, cu V={1,2,3,4,5,6}, E={(1,2),(1,3),(2,5),(3,5),(5,6)}. O posibilă reprezentare grafică este,
1 2 4 3
6
5
123
Programarea calculatoarelor
8.1.2 Fie D=(V,E) digraf, V={1,…,5}, E={(1,2), (1,3), (1,5), (2,5), (3,5), (4,1), (5,4)}. Digraful poate fi reprezentat grafic astfel,
1
4
2 3
5 8.1.3 Fie D=(V,E) graf direcţionat, V={1,2,3,4,5}, E={(1,2), (1,3), (1,5) (2,5), (3,5), (4,4)}. Reprezentarea grafică este,
1 4
2 3
5
8.1.4 Fie G=(V,E,W) graf ponderat, V={1,2,3,4}, E={(1,2), (1,3), (1,4), (2,3), (2,4)}, W((1,2))=5, W((1,3))=1, W((1,4))=7, W((2,3))=4, W((2,4))=2. O posibilă reprezentare grafică este:
1
5
1
2 2
4
3
7
4 În scopul reprezentării grafurilor în memoria calculatorului sunt utilizate în general următoarele structuri de date.
124
Grafuri
8.1.2
Reprezentarea matriceală
Grafurile, digrafurile şi grafurile direcţionate pot fi reprezentate prin matricea de adiacenţă. Dacă G=(V,E ) este graf, digraf sau graf direcţionat cu V = n , atunci matricea de adiacenţă A ∈ Mnxn({0,1}) are componentele,
⎧1, dacă (vi , v j ) ∈ E , aij = ⎨ ⎩0 , altfel
unde vi, vj reprezintă cel de-al i-lea, respectiv cel de-al j-lea nod din V. În cazul unui graf neorientat, matricea de adiacenţă este simetrică. Exemplu 8.1.5 Graful din exemplul 8.1.1, digraful din exemplul 8.1.2 şi graful direcţionat din exemplul 8.1.3 sunt reprezentate prin matricele de adiacenţă, ⎛0 ⎜ ⎜1 ⎜1 A=⎜ ⎜0 ⎜ ⎜0 ⎜0 ⎝
1 1 0 0 0⎞ ⎟ ⎛0 ⎜ 0 0 0 1 0⎟ ⎜0 0 0 0 1 0⎟ ⎟ (8.1.1), A = ⎜ 0 ⎜ 0 0 0 0 0⎟ ⎜1 ⎟ 1 1 0 0 1⎟ ⎜ ⎝0 0 0 0 1 0 ⎟⎠
1 0 0 0 0
1 0 0 0 0
0 0 0 0 1
1⎞ ⎛0 ⎜ ⎟ 1⎟ ⎜0 1 ⎟ (8.1.2), A = ⎜ 0 ⎜ ⎟ ⎜0 0⎟ ⎜ ⎟ 1⎠ ⎝0
1 0 0 0 0
1 0 0 0 0
0 0 0 1 0
1⎞ ⎟ 1⎟ 1 ⎟ (8.1.3) ⎟ 0⎟ ⎟ 0⎠
În cazul grafurilor ponderate, reprezentarea poate fi realizată prin matricea ponderilor. Dacă G=(V,E,W) este graf ponderat, V = n , W ∈ Mnxn((0, ∞ )) are componentele,
⎧W (( vi , v j )), dacă (vi , v j ) ∈ E wi , j = ⎨ ⎩α , altfel
unde vi, vj reprezintă cel de-al i-lea, respectiv cel de-al j-lea nod din V,
α = 0 , dacă ponderea are semnificaţia de câştig, respectiv α = ∞ în cazul în care se doreşte reprezentarea costurilor ca ponderi ale grafului. Exemplu 8.1.6 Presupunând că ponderile reprezintă costuri, matricea de reprezentare
⎛∞ ⎜ ⎜5 a grafului din exemplul 8.1.4. este W = ⎜ 1 ⎜ ⎜7 ⎝
5 1 7⎞ ⎟ ∞ 4 2⎟ . 4 ∞ ∞⎟ ⎟ 2 ∞ ∞ ⎟⎠
125
Programarea calculatoarelor
8.1.3 Reprezentarea tabelară Reţinînd muchiile prin intermediul extremităţilor şi eventual valoarea ponderii ei, se obţine reprezentarea tabelară, mai economică din punctul de vedere al spaţiului de memorie necesar. Dacă graful conţine vârfuri izolate atunci este necesară păstrarea acestora într-un vector suplimentar VS. Mulţimea muchiilor este reţinută într-o matrice A cu E linii şi c coloane, unde c=2 dacă graful nu este ponderat, altfel c=3. În primele două coloane se scriu perechile de vârfuri ce determină muchiile, în cazul grafurilor ponderate cea de-a treia coloană conţine valoarea ponderii muchiei respective. Exemple 8.1.7 Graful din exemplul 8.1.1 poate fi reprezentat astfel, VS=(4),
⎛1 ⎜ ⎜1 A = ⎜2 ⎜ ⎜3 ⎜5 ⎝
2⎞ ⎟ 3⎟ 5⎟ ⎟ 5⎟ 6 ⎟⎠
⎛1 ⎜ ⎜1 ⎜1 ⎜ 8.1.8 Digraful din exemplul 8.1.2 este reprezentat prin A = ⎜ 2 ⎜ ⎜3 ⎜4 ⎜⎜ ⎝5
2⎞ ⎟ 3⎟ 5⎟ ⎟ 5⎟ . ⎟ 5⎟ 1⎟ ⎟ 4 ⎟⎠
⎛1 ⎜ ⎜1 ⎜1 8.1.9 Graful direcţionat din 8.1.3. este reprezentat prin A = ⎜ ⎜2 ⎜ ⎜3 ⎜4 ⎝
2⎞ ⎟ 3⎟ 5⎟ ⎟. 5⎟ ⎟ 5⎟ 4 ⎟⎠
8.1.10 Graful ponderat din exemplul 8.1.4. nu are vârfuri izolate, deci este ⎛1 ⎜ ⎜1 reprezentat prin intermediul matricei A = ⎜ 2 ⎜ ⎜1 ⎜ ⎝2
2 3 3 4 4
5⎞ ⎟ 1⎟ 4⎟ . ⎟ 7⎟ ⎟ 2⎠
126
Grafuri
8.1.4 Reprezentarea prin intermediul listelor Această reprezentare permite utilizarea economică a spaţiului de memorare şi, în anumite cazuri, implementări mai eficiente pentru anumite clase de algoritmi. Vârfurile grafului sunt memorate într-o listă, fiecare nod al listei N conţinând o referinţă spre lista vecinilor vârfului memorat ca informaţie în N. Dacă graful nu este ponderat, el poate fi reprezentat prin structura listă de liste, şi anume: nodurile grafului se trec într-o listă L_nod, fiecare celulă având structura, informaţie legătură vecini legătură nod următor unde, • câmpul informaţie conţine identificatorul nodului; • legătură vecini reprezintă referinţa spre începutul listei vecinilor; • legătură nod următor conţine adresa următoarei celule din lista L_nod. Un graf ponderat poate fi reprezentat în mod similar, cu diferenţa că, fiecare celulă din lista vecinilor conţine şi ponderea muchiei respective (muchia care are ca extremităţi vârful referit prin identificatorul de nod din lista vecinilor şi respectiv vârful indicat de informaţia acelei celule din L_nod ce conţine adresa primului element al listei vecinilor).
8.2 Modalităţi de parcurgere a grafurilor Modalitatea de vizitare a tuturor vârfurilor grafului în care fiecare vârf al grafului este vizitat o singură dată se numeşte parcurgere sau traversare. În acest paragraf sunt prezentate metodele de parcurgere BF (în lăţime), DF (în adâncime) şi metoda DF generalizată, notată DFG. Primele două metode de parcurgere sunt aplicate grafurilor neorientate respectiv grafurilor direcţionate şi presupun selectarea unui vârf iniţial v0 şi identificarea acelor vârfuri ale grafului v cu proprietatea că există cel puţin un drum de la vârful iniţial către v. Grafurile cu proprietatea că oricare două vârfuri sunt conectate printr-un drum se numesc grafuri conexe şi sunt prezentate în § 8.3. Dacă graful este conex, atunci prin aplicarea metodelor de parcurgere vor fi identificate toate vârfurile grafului. Cele două modalităţi de parcurgere sunt prezentate în continuare în cazul grafurilor neorientate, extinderea la digrafuri şi grafuri direcţionate fiind imediată. Studiul proprietăţii metodei BF de a calcula distanţele minim între orice vârf al grafului conectat de vârful iniţial şi vârful iniţial este prezentat în cazul grafurilor oarecare. Parcurgerea DFG presupune vizitarea tuturor vârfurilor unui graf sau graf direcţionat prin aplicarea metodei DF tuturor vârfurilor care, după ultima traversare DF, nu au fost încă vizitate.
127
Programarea calculatoarelor
8.2.1 Metoda de parcurgere BF (Breadth First) Traversarea BF presupune parcurgerea în lăţime a grafului, în sensul că, vârfurile grafului sunt prelucrate în ordinea crescătoare a distanţelor la vârful iniţial (teorema 8.2.1). Distanţa de la u la v, notată δ (u , v ) , este numărul de muchii ale unui cel mai scurt u-v drum. La momentul iniţial vârf curent este v0. Deoarece vârful curent la fiecare moment trebuie să fie unul dintre vârfurile aflate la distanţă minimă de v0 se poate proceda în modul următor: iniţial lui v0 i se asociază valoarea 0, d [v0 ] = 0 şi
fiecărui vârf v ≠ v0 i se asociază valoarea ∞ , d [v ] = ∞ . Dacă valoarea asociată vârfului curent este m, atunci fiecăruia dintre vecinii acestuia de valoare ∞ li se asociază valoarea m+1. Se observă că, dacă după ce toate vârfurile de valoare m au fost considerate şi nici unui vârf nu i-a fost recalculată valoarea, atunci toate vârfurile conectate cu v0 au fost vizitate, deci calculul se încheie. Exemple 8.2.1 Fie graful,
1 2
3 6 4 7
5
şi v0=1.Valorile calculate prin aplicarea metodei prezentate sunt, vârf
1
2
3
4
5
6
7
0 0 0 0
∞
∞
∞
1 1 1
1 1 1
∞ ∞
∞ ∞
∞
1 1 1
2 2
2 2
d 0 1 2
1 1 1
128
Grafuri
8.2.2 Fie graful,
8
1 3
2
6
4 5 iniţial.
9
10 11
7
şi v0=1. Se observă că vârfurile 8, 9, 10 şi 11 nu sunt conectate cu vârful Valorile rezultate prin aplicarea metodei sunt: vârf
1
2
3
4
5
6
7
8
9
10
11
0 1 2
0 0 0 0
∞
∞
∞ ∞
∞
1 1 1
∞ ∞
∞
1 1 1
∞ ∞ ∞ ∞
∞ ∞ ∞ ∞
∞ ∞ ∞ ∞
∞ ∞ ∞ ∞
d
2 2
1 1 1
2 2
1 1 1
Se observă că valorile lui d calculate în final reprezintă numărul de muchii corespunzător celui mai scurt drum care conectează vârful iniţial cu vârful respectiv, pentru vârfurile neconectate cu v0 valoarea d[v0] rezultată la terminarea calculului este ∞ . Fie G=(V,E) un graf, V = n . O alternativă de implementare a metodei BF este construită prin utilizarea următoarelor structuri de date, • A matricea de adiacenţă a grafului; • o structură de tip coadă, C, în care sunt introduse vârfurile ce urmează a fi vizitate şi procesate (în sensul cercetării vecinilor lor); • un vector c cu n componente, unde, ⎧1, dacă i a fost adăugat în coadă ci = ⎨ ⎩0, altfel Componentele vectorului c sunt iniţializate cu valoarea 0. Parcurgerea BF poate fi descrisă astfel, • coada C este iniţializată cu vârful v0; • cât timp C ≠ Ø, este extras şi vizitat un vârf i din coadă, apoi sunt introduşi în coadă vecinii lui i care nu au fost deja introduşi (acele vârfuri k cu proprietatea că c[k]=0 şi a[i][k]=1). Vârfurile i ce au fost introduse în coadă sunt marcate prin c[i]=1.
129
Programarea calculatoarelor
Exemplu 8.2.3. Pentru graful din exemplul 8.2.1., aplicarea metodei de traversare BF determină următoarea evoluţie, c
t t=1 t=2 t=3 t=4 t=5 t=6 t=7 t=8
1
2
3
4
5
6
7
1 1 1 1 1 1 1 1
0 1 1 1 1 1 1 1 C
0 1 1 1 1 1 1 1
0 1 1 1 1 1 1 1
0 0 1 1 1 1 1 1
0 0 0 1 1 1 1 1
0 1 1 1 1 1 1 1
t t=1 t=2 t=3 t=4 t=5 t=6 t=7 t=8
1 2 3 4 7 5 6
3 4 7 5 6
4 7 5 6
7 5 6
Observaţie Deoarece graful din exemplul 8.2.1. este conex, traversarea BF realizează vizitarea tuturor vârfurilor grafului. Aplicarea metodei BF grafului din exemplul 8.2.2. nu determină vizitarea vârfurilor 8,9, 10 şi 11, deoarece acestea sunt vârfuri neconectate cu vârful iniţial. Cu alte cuvinte, metoda BF aplicată unui graf determină vizitarea tuturor vârfurilor care sunt conectate cu vârful iniţial selectat. Sursa C pentru implementarea metodei BF este, #include <stdio.h> #include #include typedef struct nn { int inf; struct nn *leg; } nod,* pnod; int insereaza_coada(pnod *head,pnod *tail,int info) { pnod nou; if(nou=(pnod)malloc(sizeof(nod))){ nou->inf=info; nou->leg=NULL; if(*head==NULL) *head=nou; else (*tail)->leg=nou; *tail=nou; return 1; } else return 0; }
130
Grafuri int extrage_coada(pnod *head,pnod *tail, int *info) { if(*head){ pnod aux=*head; *info=(*head)->inf; (*head)=(*head)->leg; free(aux); if(*head==NULL)*head=*tail=NULL; return 1; } else return 0;} void breadth_first(int v0,int a[10][10],int n) { pnod head=NULL; pnod tail=NULL; int c[10]; for(int i=0;i
În continuare sunt prezentate o serie de rezultate prin care este demonstrată proprietatea parcurgerii BF de a calcula distanţa minimă de la orice vârf v conectat de vârful iniţial v0 la v0. Lema 8.2.1 Fie G=(V,E) un graf oarecare şi v0 ∈ V arbitrar. Atunci,
pentru orice muchie (u , v ) ∈ E , δ (v0 , v ) ≤ δ (v0 ,u ) + 1 .
131
Programarea calculatoarelor
Demonstraţie Dacă u este conectat de v0 în G, atunci, evident, şi v este conectat de v0 în G. În acest caz, cel mai scurt drum de la v0 la v nu poate fi mai lung decît cel mai scurt drum de la v0 la u prelungit cu muchia (u,v), deci afirmaţia este demonstrată. În situaţia în care u nu este conectat de v0 în G, atunci, evident, rezultă inegalitatea δ (v0 , v ) ≤ δ (v0 ,u ) + 1 . Lema 8.2.2 Fie G=(V,E) un graf neorientat sau graf direcţionat şi v0 ∈ V vârf iniţial al procedurii de traversare BF. Atunci orice v ∈ V vizitat, are loc inegalitatea d [v ] ≥ δ (v0 , v ) . Demonstraţie Afirmaţia este demonstrată prin inducţie după ordinea vizitării BF a elementelor v ∈ V conectate cu v0 în G.
d [v ] = 0 v = v0 , rezultă şi, u ∈ V \ {v}, d [u ] = ∞ ≥ δ (u , v ) , deci afirmaţia este adevărată. Dacă
pentru
orice
Fie v vârful vizitat ca rezultat al procesării vârfului u. Prin aplicarea ipotezei inductive, d [u ] ≥ δ (v0 ,u ) , a rezultatului lemei 8.2.1 şi a procedurii de parcurgere BF obţinem, d [v ] = d [u ] + 1 ≥ δ (v0 ,u ) + 1 ≥ δ (v0 , v ) . Deoarece vârful v nu a fost anterior găsit în lista vecinilor nici unui nod studiat înaintea vârfului u, v este inserat în C. Lema 8.2.3 Fie G=(V,E) un graf neorientat sau graf direcţionat şi
C = {v1 , v 2 ,...,v p } coada calculată la un moment al aplicării procedurii de parcurgere BF. Atunci următoarele inegalităţile sunt verificate, [Cor,Lei şa]
[ ]
d v p ≤ d [v1 ] + 1
d [vi ] ≤ d [vi +1 ], ∀i = 1,..., p − 1 .
Teorema 8.2.1 Corectitudinea procedurii BF Fie G=(V,E) graf neorientat sau graf direcţionat şi v0 ∈ V vârf iniţial al procedurii de traversare BF. Atunci metoda BF calculează toate vârfurile v conectate cu v0 în G şi, pentru orice v ≠ v0 , v ∈ V vizitat, cel mai scurt v0-v drum este format dintr-un v0-u drum şi muchia (u,v), unde u este acel vârf prin procesarea căruia este determinată vizitarea lui v. Demonstraţie Fie Vk = {v ∈ V / δ (v0 , v ) = k } mulţimea vârfurilor situate la distanţă k de v0. Rezultatul teoremei este demonstrat prin inducţie după k, cu ipoteza inductivă, Ik: ∀v ∈ Vk , există un singur moment al execuţiei procedurii BF în care este determinată următoarea evoluţie, d [v ] = k şi v ∈ C ;
132
Grafuri
dacă v ≠ v0 , vârful u care determină inserarea lui v în C este element al mulţimii Vk −1 .
Pentru k = 0 , V0 = {v0 } . La momentul iniţial, C ← v0 şi d [v0 ] = 0 , deci I este verificată. Verificarea ipotezei Ik în condiţiile în care I0 ,…,Ik-1 sunt adevărate este bazată pe următoarea observaţie. Pe tot parcursul execuţiei procedurii BF, C ≠ Ø şi, dacă u ∈ C , atunci d [u ] şi vârful care a determinat procesarea lui u rămîn constante. Din lema 8.2.3 rezultă că, dacă C = v1 , v 2 ,..., v p , atunci
{
d [vi ] ≤ d [vi +1 ], ∀i = 1,..., p − 1 .
}
Fie v ∈ Vk , k ≥ 1 . Din proprietatea de monotonie şi ipoteza Ik-1, rezultă că v a fost inserat în C după ce toate vârfurile u ∈ Vk −1 au fost deja inserate în coadă. Deoarece δ (v0 , v ) = k , obţinem că există un v0-v drum de lungime k şi u ∈ Vk −1 astfel încât (u , v ) ∈ E . Fără a pierde din
generalitate, vom presupune că u este primul vârf din Vk −1 inserat în C. La momentul în care vârful u devine prim element al cozii C, toate vârfurile vecine cu u în G sunt inserate în C, deci şi vârful v. Rezultă că d [v ] = d [u ] + 1 = k , unde u este acel vârf care precede v pe un cel mai scurt v0-v drum. Observaţii 1. Demonstrarea teoremei de corectitudine a parcurgerii BF stabileşte şi o modalitate de calcul al unui cel mai scurt v0-v drum astfel. Pentru orice v ∈ V conectat cu v0 în G, fie p[v ] ∈ V vârful a cărui procesare a determinat inserarea lui v în C. Un v0-v drum de lungime minimă este v0- p[v ] drumul cel mai scurt
“prelungit” cu muchia ( p[v ], v ) . 2. Aplicarea metodei BF unui graf oarecare G determină obţinerea unui arbore (vezi capitolul 9) Gp, numit subgraful predecesorilor definit de BF pe G, unde G p = V p , E p şi
(
)
V p = {v ∈ V / p[v ] ∈ V } Υ {v0 } , E p = {( p[v ], v ) ∈ E / v ∈ V \ {v0 }} .
Exemplu 8.2.4 Prin aplicarea procedurii BF grafului din 8.2.1, obţinem,
1
2 5
3
4
7
6
133
Programarea calculatoarelor
8.2.2 Metoda de parcurgere DF (Depth First) Ideea metodei DF revine la parcurgerea în adâncime a grafurilor. Considerând v0 vârf iniţial şi M mulţimea vârfurilor vizitate de procedură, pentru vizitarea vecinilor este considerat unul din vârfurile din M cu proprietatea că lungimea drumului calculat de metodă până la vârful iniţial v0 este maximă. Implementarea acestei metode poate fi realizată în mai multe moduri, pentru menţinerea mulţimii vârfurilor grafului disponibilizate până la momentul curent fiind utilizată o structură de de date de tip stivă S. La momentul iniţial se introduce în stivă v0. La fiecare pas, se preia cu ştergere ca vârf curent vârful stivei S şi se introduc în stivă vecinii încă nevizitaţi ai vârfului curent. Un vârf se marchează ca vizitat în momentul introducerii lui în S. Calculul continuă până cînd este efectuat un acces de preluare din stivă şi se constată că S este vidă. Pentru gestiunea vârfurilor vizitate, se utilizează un vector c cu n componente, unde n reprezintă numărul vârfurilor grafului şi, la fiecare moment, componentele sunt:
⎧1, dacă i a fost vizitat ci = ⎨ ⎩0, altfel
Componentele vectorului c vor fi iniţializate cu valoarea 0. Exemple 8.2.5 Pentru graful,
1 3
2
6 4 7
5
şi v0=1, prin aplicarea metodei descrise, rezultă următoarea evoluţie. c
t t=1 t=2 t=3 t=4 t=5 t=6 t=7 t=8
1
2
3
4
5
6
7
1 1 1 1 1 1 1 1
0 1 1 1 1 1 1 1 S
0 1 1 1 1 1 1 1
0 1 1 1 1 1 1 1
0 0 0 0 1 1 1 1
0 0 1 1 1 1 1 1
0 1 1 1 1 1 1 1
t
t=1 t=2 t=3 t=4 t=5 t=6 t=7 t=8
1 7 6 4 5 3 2
4 4 3 3 2
3 3 2 2
2 2
134
Grafuri
Ordinea în care sunt vizitate vârfurilor corespunzător acestei variante de parcurgere DF este: 1, 2, 3, 4, 7, 6, 5. 8.2.6 Pentru graful din exemplul 8.2.2 vârfurile 8,9,10 care nu sunt conectate cu vârful iniţial nu vor fi vizitate nici prin aplicarea metodei DF. Ordinea în care sunt vizitate vârfurile corespunzătore acestei variante este: 1, 2, 3, 4, 6, 7, 5. O variantă de implementare a metodei DF rezultă prin gestionarea stivei S în modul următor. Iniţial vârful v0 este unicul component al lui S. La fiecare etapă se preia, fără ştergere, ca vârf curent vârful stivei. Se introduce în stivă unul dintre vecinii vârfului curent încă nevizitat. Vizitarea unui vârf revine la introducerea lui în S. Dacă vârful curent nu are vecini încă nevizitaţi, atunci el este eliminat din stivă şi este efectuat un nou acces de preluare a noului vârf al stivei ca vârf curent. Calculul se încheie în momentul în care este efectuat un acces de preluare a vârfului stivei ca vârf curent şi se constată că S este vidă. Evident, nici în cazul acestei variante nu vor fi vizitate vârfurile care nu sunt conectate cu vârful iniţial ales. Exemplu 8.2.7 Pentru graful,
1
2
3 6 4 7
5
şi v0=1, prin aplicarea metodei descrise, rezultă următoarea evoluţie. c t t=1 t=2 t=3 t=4 t=5 t=6 t=7 t=8 t=9 t=10 t=11 t=12 t=13 t=14
1
2
3
4
5
6
7
1 1 1 1 1 1 1 1 1 1 1 1 1 1
0 1 1 1 1 1 1 1 1 1 1 1 1 1
0 0 0 1 1 1 1 1 1 1 1 1 1 1
0 0 1 1 1 1 1 1 1 1 1 1 1 1
0 0 0 0 0 0 0 0 0 1 1 1 1 1
0 0 0 0 1 1 1 1 1 1 1 1 1 1
0 0 0 0 0 1 1 1 1 1 1 1 1 1
135
Programarea calculatoarelor S t t=1 t=2 t=3 t=4 t=5 t=6 t=7 t=8 t=9 t=10 t=11 t=12 t=13 t=14
1 2 4 3 6 7 6 3 4 5 4 2 1
1 2 4 3 6 3 4 2 4 2 1
1 2 4 3 4 2 1 2 1
1 2 4 2 1
1 2 1
1
1
Ordinea în care sunt vizitate vârfurile corespunzător acestei variante este: 1, 2, 4, 3, 6, 7, 5. Următoarea sursă C implementează varianta precedentă de parcurgere DF. #include <stdio.h> #include #include typedef struct nn{ int inf; struct nn *leg; }nod,* pnod; int insereaza_stiva(pnod *head,int info) { pnod nou; if(nou=(pnod)malloc(sizeof(nod))){ nou->inf=info; nou->leg=*head; *head=nou; return 1; } else return 0; } int extrage_stiva(pnod *head,int *info) { if(head){ pnod aux=*head; *info=(*head)->inf; (*head)=(*head)->leg; free(aux); return 1; } else return 0;} void depth_first(int v0,int a[10][10],int n) {
136
Grafuri pnod head=NULL; int c[10]; for(int i=0;i
8.2.3 Parcurgerea în adâncime în varianta generalizată – DFG Următoarea variantă de implementare a parcurgerii în adâncime, DFG, determină vizitarea tuturor vârfurilor grafului analizat (considerat neorientat sau direcţionat), indiferent dacă acesta este conex sau neconex. Fie G=(V,E) un graf oarecare. Vom presupune în continuare că niciun vârf din V nu este etichetat cu informaţia 0. Implementare traversării DFG utilizează următoarele structuri, • A, matricea de adiacenţă a grafului; • p, vectorul predecesorilor (vezi § 8.2.1); • f, vectorul care marchează încheierea analizării listei vârfurilor vecinilor nodului curent; • mark, definit pentru orice v ∈ V prin, 0, dacă v nu a fost încă analizat mark[v]= 1, dacă v este procesat la momentul curent 2, dacă consultarea lui v este încheiată • d, definit pentru orice v ∈ V prin d[v]=t, unde t este momentul de timp la care este iniţiată analiza vârfului v.
137
Programarea calculatoarelor
Considerând t variabilă publică desemnând momentul prelucrării, procedura DFG poate fi descrisă prin intermediul următoarelor funcţii. void DFG(graf G) { for( ∀u ∈ V ){ mark[u]=0; p[u]=0; } t=0; for( ∀u ∈ V ) if(!mark[u])DF_Visit(u); } void DF_Visit(varf u) { mark[u]=1; d[u]=t++; for( v ∈ V :A[u][v]==1) if(!mark[v]){ p[v]=u; DF_Visit(v); } mark[u]=2; f[u]=t++; }
Prin aplicarea parcurgerii în adâncime în varianta generalizată sunt obţinute informaţii suplimentare ale grafului de intrare. Metoda DFG determină obţinerea subgraful predecesorilor, G p , de tip graf pădure (componentele conexe – vezi § 8.3- sunt arbori - vezi capitolul 9). Fiecare componentă conexă a lui G p este construită prin executarea modulului DF_Visit. De asemenea, vârfurile u , v ∈ V au proprietatea că u = p[v ] dacă şi numai dacă funcţia DF_Visit(v) a fost apelată la momentul căutării în lista vecinilor vârfului u. O altă proprietate importantă a metodei de parcurgere DFG este aceea că, după încheierea calculului, este determinată o structură de tip paranteză astfel. Dacă momentul selectării vârfului u pentru procesare este marcat prin “(u” şi momentul încheierii prelucrării lui u este notat “u)”, atunci istoricul traversării DFG pentru calculul fiecărui arbore din G p poate fi reprezentat prin intermediul unei expresii corecte din punct de vedere al parantezării. Teorema 8.2.2 Corectitudinea parantezării determinată de aplicarea metodei DFG Fie G=(V,E) un graf sau un graf direcţionat. Prin aplicarea traversării DFG este obţinut următorul rezultat. Pentru orice u , v ∈ V , una şi numai una din următoarele afirmaţii este adevărată, [Cor,Lei şa]
138
Grafuri
1) 2)
[d [u ], f [u ]] şi [d [v], f [v]] sunt disjuncte; [d [u ], f [u ]] ⊂ [d [v ], f [v]] şi u este un descendent al lui v în arborele corespunzător din G p ;
3)
[d [u ], f [u ]] ⊃ [d [v ], f [v]]
şi u este un ancestor al lui v în arborele
corespunzător din G p . Observaţie Fie G=(V,E) un graf sau un graf direcţionat. Pe baza procedurii DFG poate fi realizată următoarea clasificare a elementelor e = (u , v ) ∈ E , 1) muchii de tip arbore în DF- graful pădure G p , etichetate cu T: (u , v )
are eticheta T dacă procesarea vârfului v a fost decisă ca rezultat al testării existenţei muchiei e; 2) muchii de tip înapoi, cu etichetă B: (u , v ) este muchie B dacă v este ancestorul lui u într-o componentă conexă a DF- grafului pădure G p ;
3) muchii de tip înainte, notate cu F: acele muchii (u , v ) , neetichetate cu T şi în care v este descendent al lui u într-o componentă conexă a DFgrafului pădure G p ; 4) muchii de tip trecere, etichetate cu C: toate muchiile (u , v ) rămase neetichetate după încheierea etichetării cu T, B şi F.
Teorema 8.2.3 Fie G=(V,E) un graf neorientat. Orice element e ∈ E este fie de tip T, fie de tip B. [Cor,Lei şa] Teorema 8.2.4 Criteriu de aciclicitate pentru grafuri direcţionate Un graf direcţionat este aciclic (vezi §8.4) dacă şi numai dacă niciuna dintre muchii nu este de tip B. [Cor,Lei şa] Exemple 8.2.8. Pentru graful 1
8 3
2
6
4 5
9
10
7
obţinem, 1) ordinea de parcurgere DFG a vârfurilor: 1,2,3,4,6,7,5,8,9,10
139
Programarea calculatoarelor
2) graful pădure G p ,
8 1
2
9
3 10
4 6
5
7 3) structurile paranteză: (1 (2 (3 3) (4 (6 (7 7) 6) (5 5) 4) 2) 1) şi (8 (9 (10 10) 9) 8) 4) clasificarea muchiilor,
2
T
1
4 T
6
9
3
T B
8
T 10
B
T
T
T 5
B
T 7 8.2.9 Fie graful direcţionat,
6 5
7 8
1
2
4
3
Prin parcurgerea DFG obţinem următoarea ordonare: 1,7,6,8,5,2,4,3. Subgraful predecesorilor este format din următoarele componente,
140
Grafuri
1
2 4
7 6
3
8
5 Structurile paranteză: (1 (7 (6 (5 5) 6) (8 8) 7) 1) şi (2 (4 4) (3 3) 2). Clasificarea muchiilor grafului direcţionat este,
1 T
2 T
T B
6 T
4
F
7
C
T
T C
B 3
8 C
5
8.3 Drumuri în grafuri. Conexitate 8.3.1 Drumuri; definiţii Una dintre cele mai importante proprietăţi ale grafurilor o constituie posibilitatea de accesare, prin intermediul unei secvenţe de muchii (arce), dintr-un vârf dat a oricărui alt vârf al grafului, proprietate cunoscută sub numele de conexitate sau conexiune. Aşa după cum a rezultat în §8.2., dacă G=(V,E) este un graf conex, atunci pentru orice vârf iniţial v0 considerat metodele BF şi DF permit vizitarea tuturor vârfurilor din V. Definiţia 8.3.1. Fie G=(V,E) un graf, u,v∈V. Secvenţa de vârfuri Γ: u0, u1,..,un este un u-v drum dacă u0=u, un=v, uiui+1∈E pentru toţi i, 0 ≤ i ≤ n . Lungimea drumului, notată l(Γ) este egală cu n. Convenţional, se numeşte drum trivial, un drum Γ cu l(Γ)=0. Definiţia 8.3.2. Fie Γ: u0, u1,..,un un drum în graful G=(V,E). Γ este un drum închis dacă u0=un; în caz contrar, Γ se numeşte drum deschis. Drumul Γ este
141
Programarea calculatoarelor
elementar dacă oricare două vârfuri din Γ sunt distincte, cu excepţia, eventual, a extremităţilor. Drumul Γ este proces dacă, pentru orice 0 ≤ i ≠ j ≤ n − 1 uiui+1 ≠ ujuj+1. Evident, orice drum elementar este un proces. Exemplu 8.3.1 Pentru graful,
v2
v4 v5
v1
v3
Γ1: v1, v2, v3, v2, v5, v3, v4 este un v1- v4 drum care nu este proces; Γ2: v1, v2, v5, v1, v3, v4 este un v1- v4 proces care nu este drum elementar; Γ3: v1, v3, v4 este un v1- v4 drum elementar. Definiţia 8.3.3. Fie Γ: u0, u1,..,un un drum în graful G=(V,E). Γ’: v0, v1,..,vm este un subdrum al lui Γ dacă Γ’ este un drum şi pentru orice j, 0 ≤ j ≤ m , există i, 0 ≤ i ≤ n astfel încât ui=vj. Observaţie Orice drum cu lungime cel puţin 1 conţine cel puţin un drum elementar cu aceleaşi extremităţi. Într-adevăr, dacă Γ: u0, u1,..,un nu este elementar, atunci există 0 ≤ i < j ≤ n şi i ≠ 0 sau j ≠ n astfel încât ui=uj. Atunci drumul
⎧u j u j +1 ...u n , dacă i = 0 ⎪ Γ ' : ⎨u0 u1 ...u i , dacă j = 0 ⎪u u ...u u ...u , dacă i ≠ 0 , j ≠ n ⎩ 0 1 i j +1 n este de asemenea un u0-un drum. Aplicînd în continuare eliminarea duplicatelor vârfurilor în modul descris, rezultă în final un u0-um drum elementar.
142
Grafuri
Exemplu 8.3.2 În graful,
v2
v6
v1
v7
v5
v4 v3
v8
v10
v9
dacă Γ: v1, v2, v4, v5, v3, v1, v2, v5, v6, v7, v8, v9, v5, v9, v8, v10, atunci Γ1: v1, v2, v5, v9, v8, v10, Γ2: v1, v2, v4, v5, v9, v8, v10 sunt v1-v10 subdrumuri elementare.
8.3.2 Matricea existenţei drumurilor; algoritmul Roy-Warshall Lema 2.3.1 Fie G=(V,E) un graf, V = n . Dacă A este matricea de adiacenţă asociată grafului, atunci, pentru orice p≥1, a ij( p ) este numărul vi-vj
(
)
drumurilor distincte de lungime p din graful G, unde A p = a ij( p ) . Demonstraţie Demonstrarea acestei afirmaţii este realizată prin inducţie după p Pentru p=1, deoarece pentru orice 1 ≤ i , j ≤ n există cel mult un vi-vj drum de lungime 1 şi dacă există, fie acesta Г: vi, vj. Rezultă că numărul vi-vj drumurilor de lungime 1 este egal cu aij(1) .
(a
(
)
Presupunem că Ap-1 = a ij( p −1) are proprietatea că pentru toţi 1 ≤ i , j ≤ n ,
( p −1 ) ij
) este egal cu numărul v -v drumurilor de lungime p-1 în G. = ∑a Cum A =A A = (a ( ) ) , rezultă că, 1 ≤ i , j ≤ n , a i
p
j
( p) ij
p
p-1
ij
p
k =1
( p −1 ) ik
a kj .
Orice vi-vj drum de lungime p în G conţine un vi-vk drum de lungime p-1 pentru un anume vk adiacent cu vj şi reciproc, pentru orice vk adiacent cu vj oricărui vi-vk drum de lungime p-1 îi corespunde un vi-vj drum de lungime p. Din relaţia care caracterizează elementele aij( p ) , utilizînd ipoteza
( )
inductivă, rezultă afirmaţia enunţată mai sus. Definiţia 8.3.4 Fie Mn({0,1)} mulţimea matricelor de dimensiuni nxn, componentele fiind elemente din mulţimea {0,1}. Pe Mn({0,1)}se definesc operaţiile binare, notate ⊕ şi ⊗ , astfel: pentru orice A=(aij), B=(bij) din Mn({0,1)}, A ⊕ B=(cij), A ⊗ B=(dij), unde 1 ≤ i , j ≤ n , cij=max{aij, bij} dij=max{min{aik, bkj}, 1 ≤ k ≤ n }.
143
Programarea calculatoarelor
{ ( ); k ≥ 1} secvenţa de k
(k)
Dacă A=(aij) ∈ Mn({0,1)}, se notează A = a ij matrice definită prin: (1 )
k
(k )
a ij
= A, A = A ⊗ A
( k −1 )
, ∀k ≥ 2 . Dacă A este matricea de adiacenţă a unui graf G=(V,E), atunci pentru fiecare k, 1 ≤ k ≤ n − 1, A
⎧1, dacă există drum de la i la j de lungime k =⎨ ⎩0 , altfel (1)
( n −1 )
(2)
se numeşte matricea existenţei Matricea M = A ⊕ A ⊕ Κ ⊕ A drumurilor în graful G. Semnificaţia componentelor matricei M este:
⎧0 , dacă nu există vi − v j drum în G ∀1 ≤ i , j ≤ n , mij = ⎨ ⎩1, altfel Exemplu 8.3.3 Pentru graful, 2 1 3
4
⎛0 ⎜ ⎜1 A=⎜ 1 ⎜ ⎜1 ⎝
1 1 1⎞ ⎟ 0 0 0⎟ , 0 0 1⎟ ⎟ 0 1 0 ⎟⎠
⎛1 ⎜ 2 ⎜0 A =⎜ 1 ⎜ ⎜1 ⎝
0 1 1⎞ ⎟ 1 1 1⎟ , 1 1 1⎟ ⎟ 1 1 1⎟⎠
⎛1 ⎜ 3 ⎜1 A =⎜ 1 ⎜ ⎜1 ⎝
1 1 1⎞ ⎟ 0 1 1⎟ , 1 1 1⎟ ⎟ 1 1 1⎟⎠
⎛1 ⎜ ⎜1 M =⎜ 1 ⎜ ⎜1 ⎝
1 1 1⎞ ⎟ 1 1 1⎟ 1 1 1⎟ ⎟ 1 1 1⎟⎠
Observaţie Calculul matricei existenţei drumurilor permite verificarea dacă un graf dat este conex. Graful este conex dacă şi numai dacă toate componentele matricei M sunt egale cu 1. Algoritmul Roy-Warshall calculează matricea existenţei drumurilor într-un graf G cu n vârfuri. void Roy_Warshall (unsigned char a[10][10],unsigned n,unsigned char m[10][10]) {int i,j,k; for (i=0;i
144
Grafuri
Datele de intrare sunt: n, numărul de noduri şi A, matricea de adiacenţă corespunzătoare grafului. Matricea M calculată de algoritm constituie ieşirea şi este matricea existenţei drumurilor în graful G.
8.3.3 Componente conexe ale unui graf Definiţia 8.3.5 Fie G=(V,E) graf netrivial. Vârfurile u,v ∈ V sunt conectate dacă există un u-v drum în G. Definiţia 8.3.6 Dacă G este un graf, atunci o componentă conexă a lui G este un subgraf conex al lui G, maximal în raport cu proprietatea de conexitate. Exemplu 8.3.4 Componentele conexe ale grafului
1
5
2 6 4 3 sunt: C1={1,2,3}, C2={4,5}, C3={6}. Observaţii 1) Un graf este conex dacă şi numai dacă numărul componentelor sale conexe este 1. 2) Mulţimile de vârfuri corespunzătoare oricăror două componente conexe distincte sunt disjuncte. Rezultă că mulţimile de vârfuri corespunzătoare componentelor conexe ale unui graf formează o partiţie a mulţimii vârfurilor grafului. Problema determinării componentelor conexe corespunzătoare unui graf poate fi rezolvată în modul următor. Iniţial, este selectat drept vârf curent un vârf al grafului pentru care este calculată componenta conexă care îl conţine. Dacă există vârfuri care nu aparţin componentei conexe determinate, este ales drept vârf curent unul dintre aceste vârfuri. În continuare este aplicată aceeaşi metodă, până când au fost găsite toate componentele conexe ale grafului. Determinarea componentei conexe care conţine un vârf v0 dat poate fi realizată pe baza următorului algoritm. Pentru G=(V,E), V = n , n ≥ 1 şi v0 ∈ V, paşii algoritmului sunt: Pas1: V0={v0}; E0= Φ ; i=0;
145
Programarea calculatoarelor
Pas 2: repetă Pas 3 până cînd Vi=Vi-1 şi Ei=Ei-1 Pas 3: i=i+1;
Vi = Vi −1 ∪ {v / v ∈ V, ∃u ∈ Vi −1 , uv ∈ E};
E i = E i −1 ∪ {e / e ∈ E, ∃u ∈ Vi −1 , u incident cu e}; Ieşirea este Gi=(Vi,Ei), componenta conexă din care face parte v0. Exemplu 8.3.5 Pentru graful,
1
7
2
4
5
8
3
9
6
Aplicarea algoritmului descris pentru v0=1, determină următoarea evoluţie: I i=0 i=1 i=2
Vi {1} {1,2,4} {1,2,4,7,8,5}
Ei Ø {(1,2),(1,4)} {(1,2),(1,4),(2,7),(2,8),(7,8),(4,5),(4,7),(5,8)}
8.3.4 Drumuri de cost minim Definiţia 8.3.7. Fie G=(V,E,w) un graf ponderat. Costul drumului Γ: u1,u2,..,un, notat L(Γ), este definit prin: n −1
L(Γ ) = ∑ w(u i ,u i +1 ) . i =1
Pentru orice u şi v vârfuri conectate în G, u ≠ v, w-distanţa între u şi v, notată D(u,v), este definită prin, D(u , v ) = min{L(Γ ), Γ ∈ Duv } , unde Duv desemnează mulţimea tuturor u-v drumurilor elementare din G. Dacă Γ ∈ Duv este astfel încât D(u,v)=L(Γ), drumul Γ se numeşte drum de cost minim. Observaţie Cu toate că este utilizat termenul de w-distanţă, în general D nu este o distanţă în sensul matematic al cuvîntului.În particular, dacă funcţia pondere asociază valoarea 1 fiecărei muchii a grafului, atunci pentru fiecare pereche de vârfuri distincte ale grafului, costul D(u,v) este lungimea unui cel mai scurt drum între cele două vârfuri. În acest caz D este o distanţă pe mulţimea vârfurilor.
146
Grafuri
Algoritmul Dijkstra Următorul algoritm a fost propus de către E. W. Dijkstra pentru determinarea w-distanţelor D(u0,v) şi a câte unui u0-v drum de cost minim pentru fiecare vârf v≠u0 într-un graf ponderat, unde u0 este prestabilit. Fie G=(V,E,w) un graf conex ponderat, u0∈V, S⊂V, u0∈S. Se notează S = V \ S şi D u 0 , S = min D(u 0 , x ); x ∈ S . Fie v∈ S astfel încât D(u0,v)=D(u0,
(
)
{
}
S ), Γ : u0, u1,…,upv un u0-v drum de cost minim. Evident, ∀0≤i≤p ui∈S şi Γ ’: u0, u1,…,up un u0- up drum de cost minim. De asemenea,
(
)
{
}
D u0 , S = min D(u0 ,u ) + w( uv ); u ∈ S , v ∈ S ,uv ∈ E .
(
)
Dacă x∈S, y∈ S astfel încât D u 0 , S = D(u 0 , x ) + w( xy ) , rezultă
D(u 0 , y ) = D(u 0 , x ) + w( xy ) .
Pentru determinarea a câte unui cel mai ieftin u0-v drum, algoritmul consideră o etichetare dinamică a vârfurilor grafului.Eticheta vârfului v este (L(v),u), unde L(v) este lungimea unui cel mai ieftin u0-v drum determinat până la momentul respectiv şi u este predecesorul lui v pe un astfel de drum. Pentru (V,E,w) graf conex ponderat, V = n şi u0∈V, calculul implicat de algoritmul Dijkstra poate fi descris astfel: Pas 1: i=0; S0={u0}; L(u0)=0, L(v)= ∞ pentru toţi v ∈ V, v≠u0. Dacă n=1 atunci stop Pas 2: Pentru toţi v∈ Si , dacă L(v)>L(ui)+w(uiv), atunci L(v)=L(ui)+w(uiv) şi etichetează v cu (L(v),ui). Pas 3: Se determină d=min{L(v), v∈ Si } şi se alege ui+1∈ Si astfel încât L(ui+1)=d. Pas 4: Si+1=Si ∪ {ui+1} Pas 5: i=i+1. Dacă i=n-1, atunci stop. Altfel, reia Pas 2. Observaţie Dacă (V,E,w) graf ponderat neconex, atunci, pentru u0∈V, algoritmul lui Dijkstra permite determinarea w-distanţelor D(u0,v) şi a câte unui u0-v drum de cost minim pentru toate vârfurile v din componenta conexă căreia îi aparţine u0. Exemplu 8.3.6 Fie graful ponderat, 1 5
1 9
2
3 16
2 5 4
5
147
Programarea calculatoarelor
Considerînd u0=1, etapele în aplicarea algoritmului Dijkstra sunt: P1: i=0; S0={1}; L(1)=0, L(i)= ∞ pentru toţi i = 2,5 . P2: S 0 ={2,3,4,5}, u0=1 L(2)= ∞ >L(1)+5=5 ⇒ L(2)=5, etichetează 2 cu 1 L(3)= ∞ >L(1)+1=1 ⇒ L(3)=1, etichetează 3 cu 1 L(4)= ∞ >L(1)+9=9 ⇒ L(4)=9, etichetează 4 cu 1 L(5)= ∞ , w(1,5)= ∞ , deci L(5) nu se modifică P3: selectează u1=3, L(3)=1, cea mai mică dintre w-distanţele calculate la P2 P4: S1={1,3} P5: i=i+1=1 ≠ 4, reia P2 P2: S1 ={2,4,5}, u1=3 Nu se modifică nicio etichetă şi nicio w-distanţă (w(3,i)= ∞ , pentru toţi i din S1 ) P3: selectează u2=2, L(2)=5, cea mai mică dintre w-distanţele calculate la P2 P4: S2={1,3,2} P5: i=i+1=2 ≠ 4, reia P2 P2: S 2 ={4,5}, u2=2 L(4)= 9>L(2)+2=7 ⇒ L(4)=7, etichetează 4 cu 2 L(5)= ∞ >L(2)+16=21, etichetează 5 cu 2 P3: selectează u3=4, L(4)=7, cea mai mică dintre w-distanţele calculate la P2 P4: S3={1,3,2,4} P5: i=i+1=3 ≠ 4, reia P2 P2: S3 ={5}, u3=4 L(5)= 21>L(4)+5=12, etichetează 5 cu 4 P3: selectează u4=5, L(5)=12, cea mai mică dintre w-distanţele calculate la P2 P4: S3={1,3,2,4,5} P5: i=i+1=4, stop. Algoritmul calculează următoarele rezultate: Vârful v până la care este 1 calculată w-distanţa D(1,v), eticheta lui v 0, 1
2
3
4
5
5, 1
1, 1
7, 2
12, 4
Drumurile de cost minim de la vârful 1 la fiecare dintre vârfurile grafului se stabilesc pe baza sistemului de etichete astfel: drumul de la 1 la un vârf v este dat de: v1, eticheta lui v, v2 eticheta lui v1 şamd, până se ajunge la eticheta 1. Astfel, v0 -drumurile de cost minim sunt: până la 2: 2,1; până la 3: 3,1; până la 4: 4,2,1; până la 5: 5,4,2,1.
148
Grafuri
Următoarea sursă C implementează algoritmul Dijkstra. #include<stdio.h> #include #include typedef struct{ int predv; float L; } eticheta; void creaza(int *s,int *sb,int nv,int u0) { s[0]=u0; for(int j=0,i=0;ir[s[k]].L+w[sb[j]][s[k]]){ r[sb[j]].L=r[s[k]].L+w[sb[j]][s[k]]; r[sb[j]].predv=s[k]; } int ui; for(j=0;j
149
Programarea calculatoarelor return r; } void main() { int n,i,j; clrscr(); printf("Numarul de varfuri"); scanf("%i",&n); printf("Matricea ponderilor:\n"); float w[50][50]; for(i=0;i
În anumite aplicaţii este necesară exclusiv determinarea w-distanţelor D(v0,v), pentru toţi v∈V. În acest caz algoritmul Roy-Floyd permite o rezolvare a acestei probleme mai simplu de implementat decît algoritmul Dijkstra. Algoritmul Roy-Floyd Pentru (V,E,w) graf ponderat, V = n şi W matricea ponderilor, sistemul de w-distanţe D(v0,v), v∈V, poate fi calculat pe baza următoarei funcţii (similară algoritmului Roy-Warshall), void Roy_Floyd (float w[10][10],unsigned n,float d[10][10],float MAX) {int i,j,k; for (i=0;i
150
Grafuri for (k=0;kd[i][j]+d[j][k]) d[i][k]=d[i][j]+d[j][k]; }
Matricea D calculată de algoritm este matricea w-distanţelor D(u,v) în graful ponderat conex (V,E,w); pentru orice 1 ≤ i , j ≤ n
⎧ D( vi , v j ), vi , v j sunt conectate d ij = ⎨ ⎩∞ , altfel Într-adevăr, procedura realizează calculul dinamic al w-distanţei între oricare două vârfuri i şi k, astfel: dacă există un drum i-k drum ce trece prin j ( 1 ≤ j ≤ n ), cu costul corespunzător (dij+djk) inferior costului curent (dik), atunci noul drum de la i la k via j este de cost mai mic decît costul drumului vechi, deci w-distanţa între i şi k trebuie reactualizată la dij+djk. Algoritmul Yen Algoritmul propus de Yen pentru calculul tuturor w-distanţelor într-un graf ponderat este mai eficient din punctul de vedere al volumului de operaţii decît algoritmul Roy-Floyd. Fie (V,E,w) un graf ponderat şi W matricea ponderilor. Pentru determinarea w-distanţelor de la vârful vk fixat la celelalte vârfuri ale grafului, algoritmul Yen iniţiază următoarele operaţii, Pas 1: D=W Pas 2: i=1; λ(k)=0, b(k)=0; λ(j)=0, pentru toţi 1 ≤ j ≤ n , j ≠ k Pas 3: Calculează min{dkj; 1 ≤ j ≤ n , λ(j)=1}; Determină j0 astfel încât λ(j0)=1 şi d kj0 = min{dkj; 1 ≤ j ≤ n , λ(j)=1} B(j0)= d kj0 , λ(j0)=0 d[k,j] =min{d[k,j],d[k,j0]+d[j0,j]}, pentru toţi j, 1 ≤ j ≤ n i=i+1 Pas 4: Dacă i
151
Programarea calculatoarelor
Exemplu 8.3.7 Fie graful
1
4 5
3 7
2 1
4
4
2 5 3
Se consideră vk=1.
⎛∞ ⎜ ⎜3 Pas 1: D = ⎜ 2 ⎜ ⎜7 ⎜4 ⎝
3 2 7 4 ⎞ ⎟ ∞ 5 ∞ ∞⎟ 5 ∞ 4 1⎟ ⎟ ∞ 4 ∞ ∞⎟ ∞ 1 ∞ ∞ ⎟⎠
Pas 2: i=1, λ=(0,1,1,1,1); B(1)=0
⎛∞ ⎜ ⎜3 Pas 3: j0=3, B(3)=2, λ=(0,1,0,1,1); D = ⎜ 2 ⎜ ⎜7 ⎜4 ⎝
3 2 6 ∞ 5 ∞ 5 ∞ 4 ∞ 4 ∞ ∞ 1 ∞
3 ⎞ ⎟ ∞⎟ 1 ⎟ ; i=2 ⎟ ∞⎟ ∞ ⎟⎠
Pas 4: i<5, reia Pas 3 Pas 3: j0=2, B(2)=3, λ=(0,0,0,1,1); nicio modificare în matricea D; i=3 Pas 4: i<5, reia Pas 3 Pas 3: j0=5, B(5)=3, λ=(0,0,0,1,0); nicio modificare în matricea D; i=4 Pas 4: i<5, reia Pas 3 Pas 3: j0=4, B(4)=6, λ=(0,0,0,0,0); nicio modificare în matricea D; i=5 Pas 4: i=5, stop.
152
Grafuri
8.4 Circuite şi cicluri în grafuri şi în digrafuri Definiţia 8.4.1 Fie G=(V,E) un graf netrivial, u, v∈V şi Γ un u-v drum în G. Γ se numeşte proces dacă toate muchiile drumului Γ sunt distincte. Drumul Γ este trivial dacă Γ : u,u. Definiţia 8.4.2 Drumul Γ este un circuit dacă Γ este un proces netrivial închis. Definiţia 8.4.3 Circuitul Γ : v1, v2,…., vn, v1 cu n≥3 este un ciclu al grafului, dacă, pentru orice i, j, cu 1 ≤ i , j ≤ n , i ≠ j , rezultă vi≠vj. Observaţie Orice ciclu este un drum elementar închis. Definiţia 8.4.4 Graful G este aciclic dacă nu există cicluri în G. Observaţie Într-un digraf D noţiunile de proces, circuit, ciclu sunt definite ca şi în cazul grafurilor. Exemple 8.4.1 În graful, v1 v4 v2 v3 v5
v6
Γ 1: v1, v2, v3, v6, v5 este un proces; Γ 2: v1, v2, v3, v6, v5, v3, v4, v1 este un circuit şi nu este ciclu; Γ 3: v1, v3, v5, v4, v1 este un ciclu. 8.4.2 Drumul Γ : v1,v2,v4,v3,v1 este un ciclu, deci graful conţine cicluri.
v1
v2
v3
v4
153
Programarea calculatoarelor
8.4.3 Digraful,
V1
V2
V3
V4
nu conţine cicluri. Definiţia 8.4.5 Fie D=(V,E) un digraf. Funcţiile grad exterior, odD, respectiv grad interior, idD, sunt definite prin, od D : V → N ; id D : V → N ,
∀u ∈ V , od D (u ) = {v / v ∈ V , uv ∈ E} ,
∀u ∈ V , id D (u ) = {v / v ∈ V , vu ∈ E}
Funcţia grad, notată degD, este definită astfel,
deg D : V → N, ∀u ∈ V, deg D (u ) = id D (u ) + od D (u ) .
Algoritmul Marimont Procedura Marimont verifică dacă un digraf D=(V,E), V = n , este sau nu aciclic. La terminarea calculului este afişat mesajul “DA”, dacă digraful D este aciclic, respectiv “NU”, în caz contrar. Descrierea pe paşi a algoritmului Marimont este, Pas 1: V0=V, E0=E, D0=(V0,E0) Pas 2: Dacă od D 0 (v ) ≥ 1 pentru toţi v∈V0, scrie “NU”, stop (dacă toate vârfurile sunt extremităţi iniţiale ale măcar unui arc, atunci există cicluri în D0); altfel, continuă. Pas 3: Selectează v∈V0 cu od D 0 (v ) = 0 ;V0=V0\{v}; E0=E0-{e/ e∈E0, e incidentă cu v în D0}; D0=(V0,E0) Pas 4: Dacă V0≠Ø, atunci reia pasul 2; altfel scrie “DA”, stop. Exemple 8.4.4 Pentru digraful,
1 e1 4
e4
e2
2
e3 3
e5 5
evoluţia algoritmului Marimont este,
154
Grafuri
Pas 1: V0={1,2,3,4,5}, E0={e1,e2,e3,e4,e5} Pas 2: od D 0 (5) = 0 , continuă
1 e1 4
e2
2
e3
e4
3
Pas 3: Selectează vârful 5, elimină 5 din V0, elimină arcul e5 E0 Pas 4: reia de la pasul 2 Pas 2: od D 0 (i ) = 1 pentru toţi i din V0 ={1,2,3,4}, scrie “NU”, stop. 8.4.5 Pentru digraful D:
6
e7
e9 7
5
4 e6
e8
1 e4
e5
e3 3
e1 2 e2
algoritmul Marimont determină următoarea secvenţă de operaţii: Pas 1: V0={1,2,3,4,5,6,7}, E0={e1,e2,e3,e4,e5,e6,e7,e8,e9} Pas 2: od D 0 (7 ) = 0 , continuă Pas 3: Selectează vârful 7, elimină 7 din V0, elimină arcele e8 şi e9 din E0
D0:
6
e7
5
4 e6
1 e4 e3 3
e5
e1 2 e2
Pas 4: reia de la pasul 2 Pas 2: od D 0 (6 ) = 0 , continuă
155
Programarea calculatoarelor
Pas 3: Selectează vârful 6, elimină 6 din V0, elimină arcul e7 din E0 D0:
5
1 e4
4 e6
e1
e5
2
e3 3
e2
Pas 4: reia de la pasul 2 Pas 2: od D 0 (5) = 0 , continuă Pas 3: Selectează vârful 5, elimină 5 din V0, elimină arcul e6 din E0
D0:
1 e4
4
e5
e3 3
e1 2 e2
Pas 4: reia de la pasul 2 Pas 2: od D 0 (4 ) = 0 , continuă Pas 3: Selectează vârful 4, elimină 4 din V0, elimină arcele e4, e5 şi e3 din E0
D0:
1 e1 2 3
e2
Pas 4: reia de la pasul 2 Pas 2: od D 0 (3) = 0 , continuă Pas 3: Selectează vârful 3, elimină 3 din V0, elimină arcul e2 din E0
D 0:
1 e1 2
156
Grafuri
Pas 4: reia de la pasul 2 Pas 2: od D 0 (2 ) = 0 , continuă Pas 3: Selectează vârful 2, elimină 2 din V0, elimină arcul e1 din E0
D0: • 1
Pas 4: reia de la pasul 2 Pas 2: od D 0 (1) = 0 , continuă Pas 3: Selectează vârful 1, elimină 1 din V0 V0=Ø Pas 4: scrie “DA”, stop. Algoritmul Marimont poate fi descris în C astfel, #include<stdio.h> #include typedef struct{ int vi,vf;} arc; int grad_exterior(arc *arce,int na,int v) { int od=0; for(int i=0;i
157
Programarea calculatoarelor int ciclic(int *varf,arc *arce,int nv, int na) { for(int i=0;i
158
Grafuri
if(Marimont(vf,arce,nv,na)) printf("\n\nDigraful este aciclic"); else printf("\n\nDigraful este ciclic"); getch(); }
159
9 Structuri arborescente
Una dintre cele mai studiate clase de grafuri sunt cele de tip arbore. În acest capitol sunt prezentate principalele caracteristici ale arborilor, algoritmi pentru calculul arborelui parţial de cost minim, arbori direcţionaţi, arbori cu rădăcină şi arbori binari. Pe lângă operaţiile primitive asupra arborilor – căutarea unei informaţii, inserarea unui nod, extragerea unui nod şi metode de parcurgere, sunt prezentate două clase importante de arbori binari: arbori de sortare şi arbori de structură.
9.1 Grafuri de tip arbore 9.1.1 Definiţii şi caracterizări ale grafurilor arbori Structurile cele mai simple şi care apar cel mai frecvent în aplicaţii sunt cele arborescente (arbori). Grafurile arbori constituie o subclasă a grafurilor conexe. Definiţia 9.1.1 Graful G este arbore dacă G este aciclic şi conex. Definiţia 9.1.2. Fie G=(V,E) graf arbore. Subgraful H=(V1,E1) al lui G este subarbore al lui G dacă H este graf arbore. Exemple 9.1.1. Graful
1 2
5
4
3
6 7
160
Structuri arborescente
este arbore, deoarece, orice (i,j) ∈ E , i≠j, există un i-j drum şi graful nu conţine cicluri. 9.1.2. Graful
1
4
3
2
7 5 7
nu este arbore, deoarece drumul Γ :1,4,6,2,1 este un ciclu. 9.1.3. Graful
1
4
3
7
2 5
9
6 8
nu este arbore, deoarece conţine trei componente conexe: {1,2,3,4,6}, {3} şi {7,8}. Verificarea proprietăţii unui graf de a fi arbore poate fi realizată prin intermediul unor algoritmi care să verifice calităţile de conexitate şi respectiv aciclicitate. De asemenea, verificarea proprietăţii unui graf de a fi arbore poate fi realizată astfel. Proprietatea 1. Un graf G=(V,E), cu V = n , E = m este graf arbore dacă şi numai dacă G este aciclic şi n=m+1. Exemple 9.1.4. Graful din 9.1.1 este arbore, pentru că este aciclic şi n=7, m=6. 9.1.5. Graful din 9.1.2. nu este arbore pentru că este ciclic. 9.1.6. Graful din exemplul 9.1.3. nu este arbore deoarece este aciclic, dar n=9, m=6. Proprietatea 2 Un graf G=(V,E), cu V = n , E = m este graf arbore dacă şi numai dacă G este conex şi n=m+1. Exemple 9.1.7. Graful din 9.1.1. este arbore deoarece este conex şi n=m+1.
161
Programarea calculatoarelor
9.1.8. Graful conex din exemplul 9.1.2. nu este arbore pentru că n=6 şi m=8. 9.1.9. Graful din 9.1.3. nu este conex, deci nu este graf arbore. Observaţie Fie G=(V,E) un graf. Următoarele afirmaţii sunt echivalente, 1. G este graf arbore; 2. G este graf conex minimal: oricare ar fi e∈E, prin eliminarea muchiei e din E, graful rezultat nu este conex; 3. G este graf aciclic maximal: prin adăugarea unei noi muchii în graf rezultă cel puţin un ciclu. Definiţia 9.1.3. Se numeşte graf asimetric un digraf D=(V,E) cu proprietatea că pentru orice u ,v ∈ E dacă uv∈E, atunci vu ∉ E. Digraful D este simetric dacă ∀u , v ∈ E , uv∈E, dacă şi numai dacă vu∈E. Definiţia 9.1.4. Fie D=(V,E) digraf netrivial. Graful G=(V,E’), unde E’={uv/ uv∈E sau vu∈E} se numeşte graf suport al digrafului D. Definiţia 9.1.5. Un arbore direcţionat este un graf orientat asimetric şi astfel încât graful suport corespunzător lui este graf arbore. Definiţia 9.1.6. Arborele direcţionat T=(V,E) este arbore cu rădăcină dacă există r∈V astfel încât, pentru orice u∈V, u ≠ r, există r-u drum în T. Vârful r se numeşte rădăcina arborelui direcţionat T. Definiţia 9.1.7. Fie T=(V,E) arbore direcţionat. Arborele T1=(V1,E1) este subarbore al lui T dacă V1⊆V, E1⊆E şi T1 este arbore direcţionat. Observaţie Graful suport al unui arbore direcţionat este aciclic, deci, pentru orice u∈V, u ≠ r, r-u drumul din T este unic. De asemenea, un arbore direcţionat are cel mult o rădăcină. Rezultă că, pentru orice u∈V, u ≠ r, distanţa de la rădăcină la vârful u este egală cu numărul de muchii ale r-u drumului în T. Exemple 9.1.10. Arborele direcţionat
1
3 6
5
2
7
4
8
9
10
este arbore cu rădăcină 1.
162
Structuri arborescente
9.1.11. Arborele direcţionat
1
2 7
3 4 5
6
nu are rădăcină. 9.1.12. Arborele
1
2
4 6
5
8
10
este un subarbore cu rădăcină 1 al arborelui din 9.1.10.
9.1.2 Reprezentări şi parcurgeri ale arborilor orientaţi Definiţia 9.1.8. Un arbore orientat este un arbore direcţionat cu rădăcină. Definiţia 9.1.9. Fie T=(V,E), un arbore orientat cu rădăcină r. Un vârf v ∈ V este situat pe nivelul i al arborelui T, dacă distanţa de la vârf la rădăcină este egală cu i. Rădăcina arborelui este considerată de nivel 0. Deoarece orice arbore orientat este în particular digraf, reprezentarea arborilor orientaţi poate fi realizată prin utilizarea oricăreia dintre modalităţile prezentate în §8.1. Datorită caracteristicilor arborilor orientaţi pot fi însă obţinute reprezentări mai eficiente din punct de vedere al spaţiului de memorie solicitat. Una dintre modalităţi este reprezentarea de tip FIU-FRATE, care constă în numerotarea convenţională a vârfurilor grafului şi memorarea, pentru fiecare vârf i al arborelui, a următoarelor informaţii, - FIU(i): numărul ataşat primului descendent al vârfului i; - FRATE(i): numărul ataşat vârfului descendent al tatălui vârfului i şi care urmează imediat lui i; - INF(i): informaţia ataşată vârfului i (de obicei valoarea i). Pentru reprezentarea arborelui sunt reţinute rădăcina şi numărul nodurilor. Absenţa „fiului”, respectiv a :fratelui” unui vârf este marcată printr-o valoare
163
Programarea calculatoarelor
din afara mulţimii de numere ataşate vârfurilor (de obicei valoarea 0). Exemplu 9.1.13. Arborele orientat
1
2
5
3
6
9 10
11
4
7
12 13
8
14
15
16
este reprezentat astfel, N=16, R=1 (rădăcina), FIU=(2,5,0,8,0,9,0,14,0,0,0,0,0,0,0,0) FRATE=(0,3,4,0,6,7,0,0,10,11,12,13,0,15,16,0) O alternativă a reprezentării FIU-FRATE poate fi obţinută prin utilizarea structurilor de date dinamice. Presupunând că fiecare vârf al arborelui are cel mult n descendenţi, fiecărui vârf îi este ataşată structura, identificator vârf
vector de legături către descendenţii vârfului adresă fiu 1 … adresă fiu n
Următoarea sursă C implementează problema construcţiei unui arbore orientat, reprezentat prin intermediul unei structuri dinamice arborescente. Numărul maxim de descendenţi ai unui nod este 4. În cazul unui număr mai mare de descendenţi este preferată în general reprezentarea FIU-FRATE, datorită dimensiunii spaţiului de memorie ocupat. Afişarea informaţiilor arborelui creat este realizată prin traversarea în A-preordine (a se vedea paragraful următor). #include<stdio.h> #include #include typedef struct nod{ int inf; struct nod *fiu[4]; } arb, *arbore; void inserare_tata(arbore *ptata,int k,int info) { arbore nou=(arbore)malloc(sizeof(arb)); nou->inf=info;
164
Structuri arborescente for(int i=0;i<4;i++)nou->fiu[i]=NULL; (*ptata)->fiu[k]=nou; } void inserare(arbore *ppred) { int j,info; arbore *pred; for(int nr=0;(*ppred)->fiu[nr];nr++){ (*pred)=(*ppred)->fiu[nr]; printf("Numarul de fii ai nodului %i:",(*pred)->inf); scanf("%i",&j); for(int k=0;k<j;k++){ scanf("%i",&info); inserare_tata(pred,k,info); } } for(nr=0;(*ppred)->fiu[nr];nr++) inserare(&((*ppred)->fiu[nr])); } void A_preordine(arbore r) { if(r){ printf("%i ",r->inf); for(int i=0;i<4;i++) A_preordine(r->fiu[i]); } } void main(){ clrscr(); int n,j,info; arbore radacina=NULL; printf("Introduceti informatiile pe niveluri\n"); printf("Introduceti radacina\n"); scanf("%i",&info); radacina=(arbore)malloc(sizeof(arb)); radacina->inf=info; for(int i=0;i<4;i++)radacina->fiu[i]=NULL; printf("Numarul de fii ai nodului %i",radacina->inf); scanf("%i",&j); for(int k=0;k<j;k++){ scanf("%i",&info); inserare_tata(&radacina,k,info); } arbore ppred=radacina; inserare(&ppred); printf("Parcurgerea A-preordine a arborelui : \n"); A_preordine(radacina); getch();}
Parcurgerea unui arbore orientat revine la aplicarea sistematică a unei reguli de vizitare a vârfurilor arborelui. Cele mai utilizate reguli de parcurgere a arborilor orientaţi sunt A-preordine, A-postordine şi parcurgerea pe niveluri.
165
Programarea calculatoarelor
Parcurgerea în A-preordine Modalitatea de vizitare a vârfurilor în parcurgerea în A-preordine poate fi descrisă astfel. Iniţial, rădăcina arborelui este selectată drept vârf curent. Este vizitat vârful curent şi sunt identificaţi descendenţii lui. Se aplică aceeaşi regulă de vizitare pentru arborii avînd ca rădăcini descendenţii vârfului curent, arborii fiind vizitaţi în ordinea precizată prin numerele ataşate vârfurilor rădăcină corespunzătoare. Exemplu 9.1.14. Pentru arborele orientat din exemplul 9.1.13., prin aplicarea parcurgerii în A-preordine, rezultă: 1,2,5,6,9,10,11,12,13,7,3,4,8,14,15,16. În reprezentarea FIU-FRATE, implementarea parcurgerii în A-preordine este realizată prin următoarea funcţie recursivă, cu parametru de intrare rădăcina arborelui curent. void A_preordine (nod R) { if (R){ vizit (R); A_preordine(FIU[R]); A_preordine(FRATE[R]); } }
În sursa prezentată în paragraful precedent, funcţia A-preordine implementează acest tip de traversare în cazul arborilor orientaţi reprezentaţi prin intermediul structurilor arborescente. Parcurgerea A-postordine Regula de parcurgerea în A-postordine este asemănătoare traversării Apreordine, singura diferenţă fiind aceea că, în acest tip de traversare, rădăcina fiecărui arbore este vizitată după ce au fost vizitate toate celelalte vârfuri ale arborelui. Exemplu 9.1.15. Pentru arborele orientat din exemplul 9.1.13. ordinea de vizitare a vârfurilor este: 5,9,10,11,12,13,6,7,2,3,14,15,16,8,4,1. Pentru arbori reprezentaţi prin structuri dinamice de date, implementarea parcurgerii în A-postordine poate fi obţinută pe baza următoarei funcţii recursive. Parametrul de intrare al funcţiei A_postordine reprezintă rădăcina arborelui curent în momentul apelului. void A_postordine (nod R) { if (R) { for(i=0;ileg[i]); vizit (R); } }
166
Structuri arborescente
Observaţie Parcurgerile în A-preordine şi A-postordine sunt variante de parcurgeri în adâncime (variante ale metodei DF). Ambele metode consideră prioritare vârfurile aflate la distanţă maximă faţă de rădăcina arborelui iniţial. Parcurgerea pe niveluri Parcurgerea unui arbore orientat pe niveluri constă în vizitarea vârfurilor sale în ordinea crescătoare a distanţelor faţă de rădăcină. Exemplu 9.1.16. Pentru arborele definit în exemplul 9.1.13., prin aplicarea parcurgerii pe niveluri, rezultă următoarea ordine de vizitare a nodurilor, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16. Ca şi în cazul metodei BF, implementarea parcurgerii pe niveluri este bazată pe utilizarea unei structuri de coadă C. La momentul iniţial rădăcina arborelui este inserată în C. Atîta timp cît timp coada este nevidă, este preluat cu ştergere un vârf din C, este vizitat şi sunt introduşi în coadă descendenţii săi. Calculul este încheiat cînd C=Ø. În cazul reprezentării FIU-FRATE a arborelui de traversat, parcurgerea pe niveluri poate fi implementată prin următoarea funcţie. void parcurgere_pe_niveluri(nod R,int FIU[],int FRATE[],int n) { ptcoada C=NULL;push(C,R); while (C) { pop(C,v); VIZIT(v); v=FIU[v]; while (v){ push(C,v); v=FRATE[v]; } } }
Observaţie Funcţiile push şi pop implementează inserarea unuei celule în coadă, respectiv extragerea unui element al cozii. Exemplu 9.1.17. Pentru arborele de la exemplul 9.1.13., evoluţia algoritmului este, C t t=1 t=2 t=3 t=4 t=5
1 2 3 4 5
8 3 4 5 6
4 5 6 7
6 7 8
7
167
Programarea calculatoarelor C t t=6 t=7 t=8 t=9 t=10 t=11 t=12 t=13 t=14 t=15 t=16 t=17
6 7 8 9 10 11 12 13 14 15 15
7 8 9 10 11 12 13 14 15 16
8 9 10 11 12 13 14 15 16
10 11 12 13 14 15 16
11 12 13 14 15 16
12 13 14 15 16
13 15 16
16
deci vârfurile sunt vizitate în ordinea: 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16. Observaţie Metoda BF pentru parcurgerea grafurilor este o generalizare a tehnicii de parcurgere pe niveluri a arborilor orientaţi. O alternativă de implementare a parcurgerii pe niveluri poate fi descrisă prin intermediul funcţiilor recursive frati şi parc. Coada C este o variabilă globală şi este iniţializată cu rădăcina arborelui. Parcurgerea este realizată prin apelul parc(C). void frati(v) {if (v){push(C,v); fraţi(FRATE[v]); } } void parc() {if (C){pop(C,v);VIZIT(v); frati(FIU[v]); parc(); } }
9.1.3 Arbori parţiali. Algoritmul Kruskal Definiţia 9.1.10. Fie G un graf. Subgraful parţial H este un arbore parţial al lui G dacă H este graf arbore. Definiţia 9.1.11. Fie G=(V,E,w) un graf ponderat conex.Dacă T=(V,E0) este un arbore parţial al grafului G’=(V,E), ponderea arborelui T, notată W(T), este definită prin W(T)= w( e ) .
∑
e∈E0
168
Structuri arborescente
Exemplu 9.1.18. Pentru graful ponderat
1 4 2
8
6
2
9
1
4
3
2
5
12
3
T este un arbore parţial de pondere 32.
1 4
2
3
2
6 8
4
5
9 3
Definiţia 9.1.12. Arborele parţial T0∈T(G) este arbore parţial minim pentru G dacă W(T0)=min{W(T); T∈T(G)}, unde T(G) este mulţimea arborilor parţiali corespunzători grafului G. Observaţie Dacă G este graf finit, atunci T(G) este o mulţime finită, deci orice graf finit ponderat şi conex are cel puţin un arbore parţial minim. În continuare este prezentat algoritmul Kruskal pentru determinarea unui arbore parţial minim al unui graf ponderat conex G=(V,E,w). Pas 1: i=1; E0=∅ Pas 2: Determină R={e/e∈E \ Ei-1 astfel încât graful (V,Ei-1 ∪ {e}) este aciclic} Dacă R=∅, atunci stop; altfel, selectează ei∈R cu w(ei)=min{w(e), e∈R}; Ei=Ei-1 ∪ {ei} Pas 3: i=i+1 şi reia pasul 2.
169
Programarea calculatoarelor
Arborele parţial de cost minim al grafului G este (V,Ei-1). Pentru implementarea algoritmului Kruskal, graful conex ponderat este reprezentat sub formă tabelară, muchiile fiind ordonate crescător după ponderi. Muchiile selectate de algoritm pot fi menţinute, de asemenea, într-o structură tabelară, sau doar marcate ca fiind incluse în mulţimea de muchii din arborele parţial minim a cărui construcţie este dorită. În varianta prezentată în continuare muchiile selectate sunt afişate. Verificarea condiţiei ca muchia selectată să nu formeze niciun ciclu cu muchiile selectate la etapele precedente este realizată prin utilizarea un vector TATA, definit astfel. Pentru fiecare vârf i (vârfurile grafului fiind numerotate de la 1 la n, unde n este numărul de noduri ale grafului), componenta TATA [i] este predecesorul său în arborele care conţine vârful i construit până la momentul curent dacă i nu este rădăcina acelui arbore, respectiv TATA[i] este egal cu –numărul de vârfuri ale arborelui de rădăcină i, în caz contrar. Componentele vectorului TATA sunt iniţializate cu valoarea -1. Calculul care realizează adăugarea unei noi muchii poate fi descris astfel. Este determinată o muchie de cost minim e=v1v2 care nu a fost selectată anterior. Dacă vârfurile v1 şi v2 nu aparţin aceluiaşi arbore, atunci proprietatea de aciclicitate este îndeplinită şi muchia e este adăugată la structura curentă. Adăugarea muchiei e selectate este realizată prin reunirea arborilor din care fac parte v1 şi v2 de rădăcini r1, respectiv r2, astfel: dacă TATA[r1] #include int radacina(int v,int *tata) { int u=v; while(tata[u]>=0) u=tata[u]; return u; } int kruskal(int a[][3],int nm, int nv) { int tata[50],i,j; int c=0; for(i=0;i
170
Structuri arborescente c+=a[j][2];printf("%i -> %i cost %i\n",v1+1,v2+1,a[j][2]); i++; } } return c; } void main() { clrscr(); int nv,nm, a[100][3]; printf("Numarul de varfuri:");scanf("%i",&nv); printf("Numarul de muchii");scanf("%i",&nm); printf("Matricea de reprezentare\n"); for(int i=0;i
Exemplu 9.1.19. Evoluţia determinată de program pentru graful 1 4
2
2
2 4
8
6 8
1 4
3
9
3
⎛2 ⎜ ⎜2 ⎜1 ⎜ ⎜1 5 A = ⎜3 ⎜ ⎜1 ⎜ ⎜4 ⎜5 ⎜⎜ ⎝3
3 1⎞ ⎟ 4 2⎟ 6 2⎟ ⎟ 5 3⎟ ⎟ 4 4⎟ 2 4⎟ ⎟ 6 8⎟ 6 8⎟ ⎟ 6 9 ⎟⎠
este: i, j după cea de-a t-a iteraţie muchia selectată TATA t=0 (-1,-1,-1,-1,-1,-1) t=1,i=0,j=0 (2,3) (-1,-2,2,-1,-1,-1) t=2,i=1,j=1 (2,4) (-1,-3,2,2,-1,-1) t=3,i=2,j=2 (1,6) (-2,-3,2,2,-1,1) t=4,i=3,j=3 (1,5) (-3,-3,2,2,1,1) t=5,i=4,j=4 (-3,-3,2,2,1,1) t=6,i=4,j=5 (1,2) (-5,1,1,2,1,1) MUCHIILE ARBORELUI MINIM: {(2,3),(2,4),(1,6),(1,5),(1,2)}
Costul 1 2 2 3 4 COSTUL: 12
171
Programarea calculatoarelor
9.2 Arbori binari 9.2.1 Reprezentarea arborilor binari. Modalităţi de parcurgere Definiţia 9.2.1 Un arbore binar este un arbore orientat cu proprietatea că pentru orice vârf v, od(v)≤2. Dacă od(v)=2, cei doi descendenţi sunt desemnaţi ca descendent stâng (fiu stânga) respectiv descendent drept (fiu dreapta). Pentru vârfurile cu od(v)=1, unicul descendent este specificat fie ca fiu stânga, fie ca fiu dreapta. Definiţia 9.2.2 Se numeşte nod terminal orice vârf v al arborelui cu od(v)=0. În caz contrar nodul v este neterminal. Reprezentarea unui arbore binar este realizată printr-o structură arborescentă. Pentru fiecare nod N al arborelui binar sunt memorate informaţia asociată lui N şi legăturile către descendenţii lui. Absenţa unui descendent este reprezentată prin NULL. identificator legătură fiu legătură fiu nod stâng drept Definiţia 9.2.3 Fie T=(V,E) un arbore binar cu rădăcina R. Subarborele stâng al lui T este ST=(V\{R},E\{RS}), unde S este fiul stânga al rădăcinii. Subarborele drept al lui T este DT=(V\{R},E\{RD}), unde D este fiul dreapta al rădăcinii. Exemplu 9.2.1 Pentru arborele binar, 1
2
3
4
5
6
8 subarborii rădăcinii
9
10
2
4
sunt:
3 6
5 8
Subarbore stâng
7
9
7
10 Subarbore drept
172
Structuri arborescente
În plus faţă de metodele A-preordine, A-postordine şi pe niveluri, parcurgerile în preordine (RSD), inordine (SRD) şi respectiv postordine (SDR) sunt special considerate pentru arbori binari şi au multiple aplicaţii. Regula de vizitare pentru aceste tipuri de parcurgere revine la parcurgerea subarborelui stâng şi parcurgerea subarborelui drept corespunzători vârfului curent. La momentul iniţial vârful curent este rădăcina arborelui. Diferenţa dintre cele trei tipuri de parcurgere este dată de momentul în care devine vizitat fiecare vârf al arborelui. În parcurgerea RSD (rădăcină-subarbore stâng-subarbore drept), fiecare vârf al arborelui este vizitat în momentul în care este vârf curent; în parcurgerea SRD, vizitarea vârfului curent R este efectuată după ce a fost parcurs subarborele stâng al lui R, respectiv în parcurgerea SDR vizitarea fiecărui vârf este efectuată după ce au fost parcurşi subarborii aferenţi lui. Exemplu 9.2.2 Pentru arborele de la exemplul 9.2.1., secvenţele de vârfuri rezultate prin aplicarea parcurgerilor RSD, SRD, SDR sunt: - preordine: 1,2,4,8,5,3,6,9,10,7 - inordine: 4,8,2,5,1,9,6,10,3,7 - postordine: 8,4,5,2,9,10,6,7,3,1.
9.2.2 Arbori de sortare Definiţia 9.2.4 Un arbore de sortare este un arbore binar cu următoarele proprietăţi, - fiecărui nod i al arborelui îi este ataşată o informaţie INF(i) dintr-o mulţime ordonată de valori; - pentru fiecare nod i, INF(i) este mai mare decât INF(j), pentru toate nodurile j din subarborele stâng al arborelui cu rădăcină i; - pentru fiecare nod i, INF(i) este mai mică decât INF(j), pentru toate nodurile j din subarborele drept al arborelui cu rădăcină i; - pentru orice vârfuri i şi j daca i≠j atunci INF(i)≠INF(j). Exemplu 9.2.3 Arborele binar 50
30 10
70 40
20
90 80
este arbore de sortare.
173
Programarea calculatoarelor
Operaţiile primitive asupra arborilor de sortare sunt inserarea unui nod, ştergerea unui nod şi parcurgerea arborelui (în preordine, inordine sau postordine). Inserarea şi ştergerea de noduri aplicate unui arbore de sortare trebuie realizate astfel încât arborele rezultat să fie de asemenea arbore de sortare. Observaţie Parcurgerea în inordine a unui arbore de sortare determină obţinerea secvenţei informaţiilor asociate vârfurilor arborelui în ordine crescătoare. Inserarea unui nod într-un arbore de sortare Algoritmul de inserare a unei informaţii nr în arborele de sortare de rădăcină rad este recursiv şi constă în efectuarea următoarelor operaţii: vârful curent v la momentul iniţial este rădăcina arborelui; dacă arborele de rădăcină v este vid, este generat arborele cu un singur nod, cu informaţia ataşată nr; altfel: - dacă informaţia ataşată nodului v este mai mare decât nr, atunci vârf curent devine fiul stânga al lui v; - dacă informaţia ataşată nodului v este egală cu nr, atunci stop; - dacă informaţia ataşată nodului v este mai mică decât nr, atunci vârf curent devine fiul dreapta al lui v. Exemplu 9.2.4 Aplicarea algoritmul descris pentru inserarea informaţiei 55 în arborele de sortare din exemplul 9.2.3 determină următoarele operaţii, INF(v)=50; 50<55, inserează în subarborele cu rădăcina avînd informaţia ataşată 70. INF(v)=70; 70>55, inserează în subarborele stâng cu rădăcina NULL. Este creat nodul cu informaţie 55, fiu stâng al nodului de informaţie 70. Arborele rezultat este
50
30 10
70 40
20
90
55 80
Ştergerea unei informaţii dintr-un arbore de sortare Algoritmul pentru ştergerea unei informaţii nr din arborele de sortare de rădăcină rad este recursiv şi poate fi descris astfel. Vârful curent v la momentul iniţial este rădăcina arborelui.
174
Structuri arborescente
1. dacă arborele este vid atunci stop; 2. altfel a) dacă informaţia ataşată nodului v este mai mare decât nr, atunci vârful curent devine fiul stânga al lui v; b) dacă informaţia ataşată nodului v este mai mică decât nr, vârful curent devine fiul dreapta al lui v; c) dacă INF(v)=nr atunci: c1) dacă subarborele stâng este vid, atunci adresa vârfului v este memorată într-o celulă suplimentară aux, v devine fiul dreapta al lui v, iar celula aux este eliberată din memorie; c2) dacă subarborele stâng este nevid atunci se determină cel mai mare element din subarborele stâng; c2.1) dacă fiul stânga al lui v nu are subarbore drept, atunci informaţia ataşată fiului stânga se transferă în vârful curent, iar fiul stânga este înlocuit cu fiul său stânga şi este eliberată memoria corespunzătoare celulei v->fius; c2.2) altfel, se transferă în rădăcină informaţia ataşată ultimului nod p determinat la c2), nodul p este înlocuit cu fiul său stâng şi celula corespunzătoare lui p este eliberată din memorie. Exemplu 9.2.5 Ştergerea informaţiei 70 din arborele de sortare din exemplul 9.2.4. este realizată astfel: 70>50, decide ştergerea din subarborele drept 70=70, decide ştergerea din arborele curent: rădăcina etichetată cu 70; există subarbore stâng iar acesta nu are subarbore drept- nodul cu informaţie 70 este etichetat cu 55, iar p este înlocuit cu subarborele său stâng (vid). Arborele rezultat
50
30 10
55 40
20
90 80
este arbore de sortare.
175
Programarea calculatoarelor
Observaţie Punctul c) de la pasul 2 al algoritmului de eliminare a unei informaţii dintr-un arbore de sortare poate fi înlocuit cu: c) dacă INF(v)=nr atunci: c1) dacă subarborele drept este vid, atunci adresa vârfului v este memorată într-o celulă suplimentară aux, v devine fiul stânga al lui v, iar celula aux este eliberată din memorie; c2) dacă subarborele drept este nevid atunci se determină cel mai mic element din subarborele drept, altfel: c2.1.) dacă fiul dreapta al lui v nu are subarbore stâng, atunci informaţia ataşată fiului dreapta este transferată în vârful curent, iar fiul dreapta este înlocuit cu fiul său dreapta şi este eliberată memoria corespunzătoare celulei v->fiud. c2.2) altfel, se transferă în rădăcină informaţia ataşată ultimului nod p determinat la c2), nodul p este înlocuit cu fiul său dreapta şi celula corespunzătoare lui p este eliberată din memorie. În următoarea sursă C sunt implementaţi algoritmii de adăugare şi ştergere în arbori de sortare. #include<stdio.h> #include #include typedef struct nod{ int inf; struct nod *l,*r; } arb, *arbore; void inserare(arbore *radacina,int info) { if(*radacina==NULL){ arbore nou; nou=(arbore)malloc(sizeof(arb)); nou->inf=info; nou->l=nou->r=NULL; *radacina=nou; } else if((*radacina)->inf>info) inserare(&((*radacina)->l),info); else if((*radacina)->infr),info); } int extragere(arbore *radacina,int info) { if(*radacina==NULL) return 0; else if((*radacina)->inf>info) return extragere(&((*radacina)->l),info); else if((*radacina)->infr),info);
176
Structuri arborescente else{ if((*radacina)->l==NULL){ arbore aux=*radacina; *radacina=(*radacina)->r; free(aux); } else{ arbore p,p1; for(p=(*radacina)->l;p->r;p1=p,p=p->r); if(((*radacina)->l)->r==NULL){ (*radacina)->inf=p->inf; (*radacina)->l=p->l; free(p); } else{ (*radacina)->inf=p->inf; arbore aux=p; p1->r=p->l; free(aux); } } return 1; } } void srd(arbore radacina) { if(radacina){ srd(radacina->l); printf("%i ",radacina->inf); srd(radacina->r); } } void main() { clrscr(); int n,info; arbore radacina=NULL; printf("Numarul de noduri:"); scanf("%i",&n); printf("Introduceti informatiile\n"); for(int i=0;i
177
Programarea calculatoarelor
9.2.3 Arbori de structură Expresiile aritmetice în care intervin numai operatori binari pot fi reprezentate prin intermediul arborilor binari în care fiecare nod neterminal are doi fii. Definiţia 9.2.5 Un arbore de structură are vârfurile etichetate astfel: - fiecare nod neterminal este etichetat cu un simbol corespunzător unuia dintre operatori; - fiecare nod terminal este etichetat cu un operand. Construcţia arborelui de structură corespunzător unei expresii aritmetice date se realizează pe baza parantezării existente în expresie şi a priorităţilor convenţional asociate operatorilor (ordinea operaţiilor) astfel încât rădăcina fiecărui subarbore este etichetată cu operatorul care se execută ultimul în evaluarea subexpresiei corespunzătoare acelui subarbore. Exemplu 9.2.6 Pentru expresia matematică (a+b)*(c-d)+e/g, arborele de structură corespunzător este +
/
* + a
b
c
e
g
d
Construcţia arborelui de structură pentru o expresie s este realizată în două etape: 1. ataşarea de priorităţi operatorilor şi operanzilor; priorităţile ataşate permit eliminarea parantezelor fără ca semnificaţia expresiei să se modifice; 2. construcţia propriu-zisă. Prima etapă este realizată astfel: - prioritatea iniţială a operatorilor ‘+’,’-‘ este 1 (dacă expresia nu conţine paranteze atunci în construcţie aceşti operatori vor fi primii luaţi în considerare în ordinea de la dreapta la stânga); - prioritatea iniţială a operatorilor ‘/’,’*‘ este 10 (dacă expresia nu conţine paranteze, aceştia sunt consideraţi după operatorii de prioritate 1 în ordinea de la dreapta la stânga); - prioritatea fiecărui operator este incrementată cu valoarea 10 pentru fiecare pereche de paranteze în interiorul cărora se află;
178
Structuri arborescente
- prioritatea ataşată fiecărui operand este MAXINT. După stabilirea sistemului de priorităţi sunt eliminate parantezele din expresie, ordinea de efectuare a operaţiilor în cadrul expresiei fiind indicată de vectorul de priorităţi ataşat. Construcţia arborelui de structură pe baza expresiei s din care au fost eliminate parantezele şi a vectorului de priorităţi, poate fi realizată recursiv în modul următor (la momentul iniţial expresia curentă este expresia dată): - pentru expresia curentă se determină operatorul/operandul de prioritate minimă care se ataşează ca etichetă a rădăcinii r a subarborelui de structură corespunzător ei; fie i poziţia acestuia în cadrul expresiei; - dacă expresia are un singur simbol, atunci r->fius=r->fiud=NULL; - altfel, se consideră subexpresiile s1 şi s2, constînd din simbolurile de pe poziţiile 0 pînă la i-1 şi respectiv i+1 pînă la lungimea şirului s.; arborii de structură corespunzători subexpresiilor s1 şi s2 se ataşează ca subarbore stâng, respectiv subarbore drept vârfului r. Exemplu 9.2.7 Etapele calculului sistemului de priorităţi şi al arborelui de structură pentru expresia de la exemplul 9.2.6 pot fi descrise astfel, Dim 1 2 3 3 4 4 5 6 7 7 8 9 10 11
vectorul prioritate (MAXINT) (MAXINT,11) (MAXINT,11,MAXINT) (MAXINT,11,MAXINT) (MAXINT,11,MAXINT,10) (MAXINT,11,MAXINT,10) (MAXINT,11,MAXINT,10,MAXINT) (MAXINT,11,MAXINT,10,MAXINT,11) (MAXINT,11,MAXINT,10,MAXINT,11,MAXINT) (MAXINT,11,MAXINT,10,MAXINT,11,MAXINT) (MAXINT,11,MAXINT,10,MAXINT,11,MAXINT,1) (MAXINT,11,MAXINT,10,MAXINT,11,MAXINT,1,MAXINT) (MAXINT,11,MAXINT,10,MAXINT,11,MAXINT,1,MAXINT,10) (MAXINT,11,MAXINT,10,MAXINT,11,MAXINT,1,MAXINT,10,MAXINT)
După eliminarea parantezelor, expresia rezultată este s=a+b*c-d+e/g. Arborele de structură este construit astfel: +
+ *
în construcţie
în construcţie
în construcţie
în construcţie în construcţie
179
Programarea calculatoarelor
+
+
în construcţie
*
în construcţie
a
+
în construcţie
*
în construcţie
+
în construcţie
a
în construcţie +
în construcţie
*
+ a
b
în construcţie
+
în construcţie
+
în construcţie
*
b
în construcţie
+
+ a
b
+
în construcţie
*
c
în construcţie
* +
în construcţie a
în construcţie
b
c
d
180
Structuri arborescente
+
/
* + a
în construcţie
b
în construcţie
d
c
+
/
* + a
b
în construcţie
e
d
c
+
/
* + a
b
c
e
g
d
Observaţie Construcţia arborelui de structură poate fi realizată în ipoteza în care expresia este corectă. Definiţia 9.2.6 Se numeşte forma poloneză directă a unei expresii, expresia rezultată în urma parcurgerii RSD a arborelui de structură. Se numeşte forma poloneză inversă a unei expresii, expresia rezultată în urma parcurgerii SDR a arborelui de structură.
181
Programarea calculatoarelor
Exemplu 9.2.8 Pentru expresia considerată la exemplul 9.2.7, forma poloneză directă este +*+ab-cd/eg. Forma poloneză inversă a expresiei date este ab+cd-*eg/+. Observaţie Parcurgerea arborelui în inordine determină secvenţa de simboluri rezultată prin eliminarea parantezelor din expresia dată. Restaurarea unei forme parantezate poate fi realizată printr-o parcurgere SRD şi anume în modul următor. La momentul iniţial vârful curent este rădăcina arborelui de structură. Dacă vârful curent v nu este vârf terminal, atunci se generează (s1) eticheta(v)(s2), unde eticheta(v) este operatorul etichetă a vârfului, s1 este secvenţa rezultată prin traversarea SRD a subarborelui stâng, s2 este secvenţa rezultată prin traversarea SRD a subarborelui drept. Dacă v este vârf terminal atunci este generată secvenţa eticheta(v). Evaluarea expresiilor aritmetice pe baza arborilor de structură Traversarea SRD a arborelui de structură ataşat unei expresii aritmetice permite evaluarea expresiei pentru valorile curente corespunzătoare variabilelor. Evaluarea poate fi efectuată în mod recursiv astfel. La momentul iniţial vârful curent este rădăcina arborelui. Dacă v este vârf curent atunci noua informaţie asociată lui v este: - val(eticheta(v)), dacă v este vârf terminal; - val(s1)eticheta(v)val(s2), dacă v este neterminal, unde val(s1), val(s2) sunt valorile rezultate prin evaluările subarborilor stâng şi respectiv drept ai lui v, val(eticheta(v)) este valoarea curentă a variabilei, dacă eticheta lui v este variabilă, respectiv valoarea constantei, dacă eticheta lui v este o constantă. Exemplu 9.2.9 Prin aplicarea metodei de evaluare descrise pentru a=3, b=2, c=5, d=2, e=6 şi g=2, obţinem:
18
3
15 5 3
3 2
5
6
2
2
182
Structuri arborescente
Construcţia arborelui de structură asociat unei expresii şi evaluarea expresiei pentru valori date ale operanzilor pot fi implementate prin intermediul următoarei surse C. #include<stdio.h> #include #include #include #include<string.h> #include<math.h> typedef struct nod{ char inf; float v; struct nod *l,*r; } arb, *arbore; void prioritati(char *s, int *prioritate) { int i,j,dim; //stabilirea prioritatilor for(i=j=dim=0;i<strlen(s);i++) switch(s[i]){ case ')':j-=10;break; case '(':j+=10;break; case '+':{prioritate[dim]=j+1;dim++;break;} case '-':{prioritate[dim]=j+1;dim++;break;} case '*':{prioritate[dim]=j+10;dim++;break;} case '/':{prioritate[dim]=j+10;dim++;break;} default:{prioritate[dim]=MAXINT;dim++;break;} } //eliminarea parantezelor for(i=0;i<strlen(s);) if((s[i]==')')||(s[i]=='(')){ for(j=i+1;j<strlen(s);j++)s[j-1]=s[j]; s[strlen(s)-1]='\0';} else i++; } void cr_arb_str(arbore *rad, unsigned p, unsigned u, char *s,int *pri) { int min=pri[p]; int poz=p; for(int i=p+1;i<=u;i++) if(min>pri[i]){min=pri[i];poz=i;} (*rad)=(arbore)malloc(sizeof(arb)); (*rad)->inf=s[poz]; if(p==u) (*rad)->l=(*rad)->r=NULL; else{ cr_arb_str(&((*rad)->l),p,poz-1,s,pri); cr_arb_str(&((*rad)->r),poz+1,u,s,pri); } }
183
Programarea calculatoarelor void forma_poloneza(arbore rad) { if(rad){ printf("%c",rad->inf); forma_poloneza(rad->l); forma_poloneza(rad->r); } } float eval(arbore rad) { char s[1]; if(rad){ if((rad->r==rad->l)&&(rad->l==NULL))return rad->v; else{ switch (rad->inf){ case '+':rad->v=eval(rad->l)+eval(rad->r);break; case '-':rad->v=eval(rad->l)-eval(rad->r);break; case '*':rad->v=eval(rad->l)*eval(rad->r);break; case '/':rad->v=eval(rad->l)/eval(rad->r);break; } return rad->v; } } } void atribuie_arbore(arbore rad) { if(rad){ if((rad->r==rad->l)&&(rad->l==NULL)){ printf("%c =",rad->inf); float t; scanf("%f",&t); rad->v=t; } else {atribuie_arbore(rad->l); atribuie_arbore(rad->r); } } } void main() { clrscr(); char s[100]; int p[100]; arbore radacina=NULL; printf("Expresia:"); scanf("%s",&s); prioritati(s,p);
184
Structuri arborescente int n=strlen(s); cr_arb_str(&radacina,0,n-1,s,p); printf("\nForma poloneza inversa "); forma_poloneza(radacina); printf("\n Valori pentru varabile\n"); atribuie_arbore(radacina); printf("\nEvaluarea: %7.3f",eval(radacina)); getch(); }
185
10 Elemente de programare orientată obiect
Programarea orientată obiect (Object Oriented Programming - OOP) reprezintă o tehnică ce s-a impus în anii ’90, dovedindu-se benefică pentru realizarea sistemelor software de mare complexitate. Noţiunea de obiect datează din anii ’60, odată cu apariţia limbajului Simula. Există limbaje – ca Smalltalk şi Eiffel – care corespund natural cerinţelor programării orientate obiect, fiind concepute în acest spirit. Recent au fost dezvoltate şi alte limbaje orientate obiect, fie pentru programare generală, fie pentru realizarea de scripturi – Java, Delphi, C++, Visual Basic .NET, C#, Python, Ruby. Unele dintre ele oferă în continuare şi posibilitatea programări procedurale (Delphi, C++). Toate limbajele folosite în prezent oferă şi facilităţi de programare orientată obiect – ADA, Fortran, Cobol, PHP etc. În prezent, există în funcţiune sisteme software de mare anvergură realizate în tehnica programării orientată obiect, principiile ei fiind suficient de bine clarificate, astfel încât să se treacă din domeniul cercetării în cel al producţiei curente de programe. Acest capitol prezintă o introducere în lucrul orientat obiect în limbajul C++, fără a acoperi toată problematica specifică.
10.1 Modelul de date orientat obiect OOP reprezintă o abordare cu totul diferită faţă de programarea procedurală, devenită deja „clasică”. Dacă în programarea clasică programatorul era preocupat să răspundă la întrebarea „ce trebuie făcut cu datele?”, adică să definească proceduri care să transforme datele în rezultate, în OOP accentul cade asupra datelor şi legăturilor între acestea, ca elemente prin care se modelează obiectele lumii reale. Se poate afirma, într-o primă analiză, că OOP organizează un
186
Elemente de programare orientată obiect
program ca o colecţie de obiecte, modelate prin date şi legături specifice, care interacţionează dinamic, adică manifestă un anumit „comportament”, producând rezultatul scontat. În general, pentru modelul de date orientat pe obiect, se consideră definitorii următoarele concepte: abstractizare, obiect, atribut, metodă, clasă, spaţiu propriu, spaţiu extins, încapsulare, moştenire şi polimorfism. Abstractizarea constituie procesul de simplificare a realităţii prin reţinerea caracteristicilor şi comportamentelor esenţiale şi constituirea lor într-un model adecvat rezolvării problemelor. Obiectul este un model informaţional al unei entităţi reale, care posedă, la un anumit nivel, o mulţime de proprietăţi şi care are, în timp, un anumit comportament, adică manifestă reacţii specifice în relaţiile cu alte entităţi din mediul său de existenţă. Ca model, un obiect este o unitate individualizabilă prin nume, care conţine o mulţime de date şi funcţii. Datele descriu proprietăţile şi nivelul acestora, iar funcţiile definesc comportamentul. Având în vedere proprietăţile comune şi comportamentul similar al entităţilor pe care le modelează, obiectele pot fi clasificate în mulţimi. O mulţime de obiecte de acelaşi fel constituie o clasă de obiecte, descrisă prin modelul comun al obiectelor sale. De exemplu, în figura 10.1, numerele complexe, ca perechi de numere reale de forma (parte reală, parte imaginară) pot fi descrise printr-un model comun, denumit ClasaComplex. Modelul arată că orice obiect de acest fel se caracterizează printr-o pereche de numere întregi şi că pe această mulţime sunt definite operaţii unare şi binare care arată cum „interacţionează” obiectele în interiorul mulţimii: un număr complex poate da naştere modulului şi opusului său, două numere complexe pot produce un alt număr complex ca sumă, produs etc. Generalizând, se poate afirma că o clasă de obiecte se manifestă ca un tip obiect, iar modelul comun al obiectelor este modelul de definire a tipului obiect. Astfel, obiectele individuale apar ca manifestări, realizări sau instanţieri ale clasei, adică exemplare particulare generate după modelul dat de tipul obiect. Altfel spus, o clasă poate fi considerată ca un tip special de dată, iar obiectele sale ca date de acest tip.
Figura 10.1 Clasă şi obiecte – mulţimea numerelor complexe
187
Programarea calculatoarelor
Acceptarea acestei semnificaţii pentru clase de obiecte este de natură să simplifice descrierea obiectelor şi să asigure un tratament al acestora similar tipurilor structurate de date din limbajele de programare: este suficientă o descriere a tipului obiect şi apoi se pot declara constante şi variabile de acest tip. Datele care reprezintă proprietăţile obiectelor se numesc atribute şi sunt de un anumit tip (de exemplu întregi, reale, caractere etc.). Setul de valori ale atributelor unui obiect la un moment dat formează starea curentă a obiectului respectiv. Funcţiile care definesc comportamentul obiectelor sunt cunoscute ca metode ale clasei. Împreună, atributele şi metodele sunt membrii clasei, identificabili prin nume. Pentru a pune în evidenţă faptul că un membru aparţine unui obiect se utilizează calificarea, astfel: nume_obiect.nume_membru. În figura 10.1, a.P_reală referă valoarea 1.0, iar a.Modul referă metoda Modul a obiectului a pentru a produce obiectul rezultat. Aşa cum sugerează figura 10.1, fiecare obiect trebuie să conţină valorile atributelor sale, deoarece ele definesc starea obiectului respectiv. Spaţiul de memorie ocupat de atributele unui obiect se numeşte spaţiu propriu al obiectului. În multe cazuri, între atribute se află pointeri care indică anumite zone de memorie alocate dinamic pentru obiect (de exemplu, clasa listă are ca membru atributul cap care conţine adresa primului nod al unei liste dinamice simplu înlănţuite). Acest spaţiu alocat dinamic aparţine tot obiectului, dar el se numeşte spaţiu extins al obiectului. Gestiunea acestui spaţiu extins trebuie asigurată de metodele clasei. Metodele, care descriu acţiuni identice pentru toate obiectele clasei, sunt memorate o singură dată, într-o zonă comună tuturor obiectelor clasei. Întrucât metodele descriu comportamentele obiectelor, ele nu pot fi apelate independent, ci numai în legătură cu un anumit obiect. Despre o metodă apelată pentru un anumit obiect se spune că se execută în contextul obiectului respectiv, iar acesta este numit obiect curent. Apelarea metodei este considerată ca trimitere de mesaj către obiectul curent, iar execuţia metodei reprezintă răspunsul (reacţia) obiectului curent la mesajul primit. Faptul că o metodă se execută în contextul obiectului curent înseamnă că are, în mod implicit, acces la toate atributele şi metodele obiectului. Acestea nu trebuie să apară ca parametri ai metodei. Pentru a utiliza alte obiecte, din aceeaşi clasă sau din clase diferite, metoda trebuie să aibă parametri corespunzători. De asemenea, pentru a simplifica scrierea, în interiorul unei metode referirea la membrii obiectului curent se face fără calificare. Pe baza acestor convenţii, în funcţiile Conjugat, Suma şi Modul, scrise în pseudocod, s-a specificat cu un parametru mai puţin decât numărul de operanzi pe care îi presupune operaţia respectivă, deoarece un operand este obiectul curent. Referirea la atributele obiectului curent se distinge de celelalte prin lipsa calificării. Descrierea în pseudocod a metodelor Conjugat, Suma şi Modul din clasa CComplex (figura 10.1) poate fi făcută astfel: void Conjugat(b); begin b.p_reala:=p_reala; b.p_imaginara:=-p_imaginara; end;
188
Elemente de programare orientată obiect void Suma(b,c); begin c.p_reala:=p_reala+b.p_reala; c.p_imaginara:=-p_imaginara+b.p_imaginara; end; float Modul(); begin Modul=sqrt(p_reala*p_reala+p_imaginara*p_imaginara); end;
Deoarece o clasă este un tip de dată, în definirea unei clase B se pot declara atribute de tip A, unde A este la rândul ei o clasă. Mai mult, o clasă A poate defini atribute de tip A. De exemplu clasa Carte, din figura 10.2 are atributul Autor de tipul Persoana care este, de asemenea, o clasă. Mai mult, Persoana are atributul Sef care este de acelaşi tip (Persoana).
Figura 10.2 Atribute de tip clasă Definirea atributelor unei clase ca tipuri ale altei clase pune în evidenţă o relaţie între clase şi deci între obiectele acestora. Din punct de vedere funcţional, metodele unei clase au destinaţii diverse. În multe cazuri şi depinzând de limbaj, unei clase i se poate defini o metodă (sau mai multe) constructor şi o metodă destructor. Un constructor este o metodă care creează un obiect, în sensul că îi alocă spaţiu şi/sau iniţializează atributele acestuia. Destructorul este o metodă care încheie ciclul de viaţă al unui obiect, eliberând spaţiul pe care acesta l-a ocupat. Încapsularea exprimă proprietatea de opacitate a obiectelor cu privire la structura lor internă şi la modul de implementare a metodelor. Ea este legată de securitatea programării, furnizând un mecanism care asigură accesul controlat la
189
Programarea calculatoarelor
starea şi funcţionalitatea obiectelor. Se evită astfel modificări ale atributelor obiectelor şi transformări ale acestora care pot să le „deterioreze”. Potrivit acestui mecanism, o clasă trebuie să aibă membrii împărţiţi în două secţiuni: partea publică şi partea privată. Partea publică este constituită din membri (atribute şi metode) pe care obiectele le oferă spre utilizare altor obiecte. Ea este interfaţa obiectelor clasei respective cu „lumea exterioară” şi depinde de proiectantul clasei. Modalitatea extremă de constituire a interfeţei este aceea a unei interfeţe compusă numai din metode. Dacă se doreşte ca utilizatorii obiectelor clasei să poată prelua şi/sau stabili valorile unor atribute ale acestora, interfaţa trebuie să prevadă metode speciale, numite accesorii, care au ca unic rol accesul la atribute. Partea privată cuprinde membri (atribute şi/sau metode) care servesc exclusiv obiectelor clasei respective. De regulă, în această parte se includ atribute şi metode care facilitează implementarea interfeţei şi a funcţionalităţii interne a obiectului. De exemplu, o stivă, ca tip de dată poate fi descrisă de o clasă Stiva în care interfaţa este constituită din metodele Push, Pop, Top, Empty, în timp ce pointerul la capul stivei, Cap şi numărătorul de noduri, Contor, ca atribute, sunt „ascunse” în partea privată. Ea se serveşte de obiectele altei clase, denumită Nod, ale cărei obiecte le înlănţuieşte în stivă (figura 10.3) Stiva
Cap: Nod Contor: Integer
Partea privată
Push ( ) Pop ( ) Top ( ) Empty ( )
Partea publică (Interfaţa)
Figura 10.3 Interfaţa obiectelor Trebuie remarcat că încapsularea înseamnă şi faptul că utilizatorul metodelor nu trebuie să cunoască codul metodelor şi nici nu trebuie să fie dependent de eventuala schimbare a acestuia, interfaţa fiind aceea care îi oferă funcţionalitatea obiectelor în condiţii neschimbate de apelare. Moştenirea reprezintă o relaţie între clase şi este, probabil, elementul definitoriu al OOP. Relaţia permite constituirea unei noi clase, numită derivată (sau fiu) pornind de la clase existente, denumite de bază (sau părinte). Dacă în procesul de construire participă o singură clasă de bază, moştenirea este simplă, altfel este multiplă.
190
Elemente de programare orientată obiect
Se spune că o clasă D moşteneşte o clasă A, dacă obiectele din clasa D conţin toate atributele clasei A şi au acces la toate metodele acestei clase. Din această definiţie, dacă D moşteneşte A, atunci obiectele din D vor avea toate atributele şi acces la toate metodele lui A, dar în plus: ¾ D poate defini noi atribute şi metode; ¾ D poate redefini metode ale clasei de bază; ¾ metodele noi şi cele redefinite au acces la toate atributele dobândite sau nou definite. În figura 10.4, clasa Cerc moşteneşte clasa Punct, deci un obiect de tipul Cerc va avea ca membri coordonatele x,y moştenite şi ca atribut propriu Raza. Funcţia Distanţa, definită pentru calculul distanţei dintre punctul curent şi punctul p, dat ca parametru, este accesibilă şi pentru obiectele Cerc şi va calcula distanţa dintre centrul cercului şi un alt punct, primit ca parametru. Funcţia Arie calculează aria din interiorul cercului, fiind nou definită. Funcţia Desenează este redeclarată de clasa Cerc, lucru impus de codul diferit pe care trebuie să-l aibă pentru desenarea obiectelor din această clasă (cerc sau altă figură). Punct X: int Y:int Desenează() Distanţa(p: punct): float
X= 100 Y= 100
Cerc Raza: int Arie():float Desenează() Distanţa(p: punct): float
X= 200 Y= 200 Raza= 50
Figura 10.4 Moştenirea simplă. Dacă se au în vedere mulţimi de clase, atunci se observă că relaţia de moştenire simplă induce un arbore ierarhic de moştenire pe această mulţime. Există o singură clasă iniţială, rădăcina arborelui, fiecare clasă are un singur ascendent (părinte) şi orice clasă care nu este frunză poate avea unul sau mai mulţi descendenţi (fii). În fine, cu privire la moştenirea simplă se pot face următoarele observaţii: • dacă se aduc modificări în clasa de bază, prin adăugarea de atribute şi/sau metode, nu este necesar să se modifice şi clasa derivată; • moştenirea permite specializarea şi îmbogăţirea claselor, ceea ce înseamnă că, prin redefinire şi adăugare de noi membri, clasa derivată are, în parte, funcţionalitatea clasei de bază, la care se adaugă elemente funcţionale noi; • moştenirea este mecanismul prin care se asigură reutilizarea codului sporind productivitatea muncii de programare.
191
Programarea calculatoarelor
Coborând (de obicei, în reprezentările grafice ale arborilor de clase, rădăcina se află în partea superioară) în arborele ierarhic al claselor de la rădăcină către frunze, se poate spune că întâlnim clase din ce în ce mai specializate. Prin moştenire se realizează o specializare a claselor. În sens invers, de la frunză către rădăcină, clasele sunt din ce în ce mai generale, avem o relaţie de generalizare. Clasa aflată la baza ierarhiei este cea mai generală. Limbajele de programare orientate obiect au implementate ierarhii standard extinse de clase, care corespund necesităţilor generale ale programării. Utilizatorii pot deriva clase noi din cele standard. Polimorfismul este un concept mai vechi al programării, cu diferite implementări în limbajele de programare care se bazează pe tipuri de date (limbaje cu tip). El şi-a găsit extensia naturală şi în modelul orientat pe date, implementat prin limbaje cu tip, în care clasa reprezintă tipul de date obiect. • Polimorfismul în limbajele de programare cu tip. Noţiunea de polimorfism exprimă capacitatea unui limbaj de programare cu tip de a exprima comportamentul unei proceduri independent de natura (tipul) parametrilor săi. De exemplu, o funcţie care determină cea mai mare valoare dintr-un şir de valori este polimorfică dacă poate fi scrisă independent de tipul acestor valori. În funcţie de modul de implementare, se disting mai multe tipuri de polimorfism. Polimorfismul ad-hoc se materializează sub forma unor funcţii care au toate acelaşi nume, dar se disting prin numărul şi/sau tipul parametrilor. Acest polimorfism este denumit şi supraîncărcare, având în vedere semantica specifică fiecărei funcţii în parte. Polimorfismul de incluziune se bazează pe o relaţie de ordine parţială între tipurile de date, denumită relaţie de incluziune sau inferioritate. Dacă un tip A este inclus (inferior) într-un tip B, atunci se poate trimite un parametru de tip A unei funcţii care aşteaptă un parametru de tip B. Astfel, un singur subprogram defineşte funcţional o familie de funcţii pentru toate tipurile inferioare celor declarate ca parametri. Un exemplu clasic este cazul tipului int, inferior tipului float în toate operaţiile de calcul. Polimorfism parametric constă în definirea unui model de procedură pentru care chiar tipurile sunt parametri. Acest polimorfism, denumit şi genericitate, presupune că procedura se generează pentru fiecare tip transmis la apel ca parametru. Cele trei tipuri de polimorfism există (toate sau numai o parte din ele) în limbajele clasice de programare, dar unele pot să nu fie accesibile programatorului. • Polimorfismul în limbajele orientate obiect. Limbajele orientate obiect sau extensiile obiect ale unor limbaje cu tip oferă, în mod natural, polimorfismul ad-hoc şi de incluziune. Polimorfismul ad-hoc intrinsec reprezintă posibilitatea de a defini în două clase independente metode cu acelaşi nume, cu parametri identici
192
Elemente de programare orientată obiect
sau diferiţi. Acest polimorfism nu necesită mecanisme speciale şi decurge simplu, din faptul că fiecare obiect este responsabil de tratarea mesajelor pe care le primeşte. Polimorfismul este de aceeaşi natură şi în cazul în care între clase există o relaţie de moştenire, cu precizarea că, în cazul în care o metodă din clasa derivată are parametrii identici cu ai metodei cu acelaşi nume din clasa de bază, nu mai este supraîncărcare, ci redefinire, după cum s-a precizat mai sus. Polimorfimsul de incluziune este legat de relaţia de moştenire şi de aceea se numeşte polimorfism de moştenire. Într-adevăr, relaţia de moştenire este o relaţie de ordine parţială, astfel încât dacă clasa D moşteneşte direct sau indirect clasa A, atunci D este inferior lui A. În aceste condiţii, orice metodă a lui A este aplicabilă la obiectele de clasă D şi orice metodă, indiferent de context, care are definit un parametru de tip A (părinte) poate primi ca argument corespunzător (parametru actual) un obiect de clasă D (fiu). Observaţie: un obiect de clasă A nu poate lua locul unui obiect de clasă D, deoarece A “acoperă” numai parţial pe D, care este o extensie şi o specializare a lui A. • Legare statică şi dinamică a metodelor. Legarea statică a metodelor se regăseşte atât în limbajele orientate obiect, cât şi în cele clasice. Compilatorul poate determina care metodă şi din care clasă este efectiv apelată într-un anumit context şi poate genera codul de apel corespunzător. Fie o clasă A şi o clasă D, unde D este derivată din A. Fie o metodă din clasa A, numită calculează, care este redefinită în clasa derivată, D. Atunci cînd metoda este apelată în contextul unui obiect static, compilatorul poate determina tipul acelui obiect (ca fiind parte a clasei A sau D). Astfel, el va şti ce metodă să apeleze (a clasei de bază sau cea redefinită, a clasei derivate). În acest caz are loc o legare statică a metodelor (decizia este luată în momentul compilării). Fie un pointer p, definit ca pointer spre clasa A. Datorită polimorfismului, în limbajele orientate obiect unui obiect din clasa părinte, desemnat indirect prin referinţă (pointer) şi nu prin nume, i se poate atribui un obiect fiu. În acest context, p poate primi ca valoare, în timpul execuţiei programului, adresa unui obiect din clasa A sau din clasa D. Nu se poate şti la momentul compilării ce se va întâmpla în timpul execuţiei programului, ca urmare nu se poate determina dacă, în contextul dat, trebuie apelată metoda clasei de bază sau metoda clasei derivate. De aceea, în locul din program în care este apelată metoda, compilatorul adaugă o secvenţă de cod care, la momentul execuţiei, va verifica tipul efectiv al obiectului şi, după caz, va realiza apelarea metodei adecvate. În acest caz are loc legarea dinamică a metodelor (sau la momentul execuţiei). Legarea dinamică este evident mai costisitoare decât cea statică, dar reprezintă o necesitate pentru a asigura elasticitatea necesară în realizarea programelor OOP, obiectele putând avea caracter de variabile dinamice.
193
Programarea calculatoarelor
10.2 Definirea claselor Definiţia unei clase este asemănătoare cu definiţia unui articol, însă în locul cuvântului rezervat struct se foloseşte cuvântul class: class nume { descriere membri; };
Membrii unei clase pot fi atribute sau metode. Atributele sunt descrise asemănător declaraţiilor de variabile independente (şi asemănător câmpurilor unui articol – struct), specificând tipul şi numele atributului respectiv. Membrii unei clase pot fi de orice tip, mai puţin de acelaşi tip cu clasa descrisă (dar pot fi pointeri către clasa descrisă). Metodele sunt descrise asemănător funcţiilor independente. Ele pot fi descrise integral în interiorul clasei (descriind antetul şi corpul lor) sau specificând în interiorul clasei doar prototipul funcţiei, corpul urmând să fie descris ulterior, în afara clasei. Este preferată a doua variantă, deoarece descrierea clasei este mai compactă decât în primul caz. Atunci când se descrie ulterior corpul unei metode, pentru a specifica apartenenţa sa la clasa respectivă, numele metodei este prefixat cu numele clasei din care face parte, folosind operatorul de rezoluţie (::), astfel: tip_rezultat nume_clasă::nume_metodă(lista parametrilor) corp metodă
Mai mult, funcţiile care sunt integral descrise în interiorul clasei sunt considerate funcţii inline1, de aceea ele trebuie să fie simple. Pentru funcţiile mai complexe, întotdeauna se recomandă să fie descrise folosind a doua variantă. Întrucât o clasă este un tip de dată, declararea unui obiect se face asemănător oricărei declaraţii de dată: nume_clasă nume_obiect;
Atunci când se doreşte lucrul cu obiecte dinamice, se poate declara o variabilă de tip pointer către clasă: nume_clasă* p_obiect; Declararea unui obiect mai este numită şi instanţierea clasei, în sensul că se creează o instanţă a acelei clase, o entitate concretă din mulţimea descrisă de clasa respectivă.
1
Apelul funcţiilor inline nu produce un salt în segmentul de cod către codul executabil al funcţiei, aşa cum se întâmplă în cazul funcţiilor obişnuite. Pentru aceste funcţii, compilatorul inserează în program, în locul apelului, secvenţa de cod corespunzătoare corpului funcţiei, înlocuind parametrii formali cu valorile actuale. Funcţiile inline au un comportament asemănător macrodefiniţiilor.
194
Elemente de programare orientată obiect
Exemplu Definirea clasei Complex, care implementează entitatea matematică număr complex. Clasa are atributele p_reala şi p_imaginara şi o metodă pentru afişarea valorii obiectului – afiseaza. class Complex { float p_reala,p_imaginara; void Afiseaza(); }; void Complex::Afiseaza() { printf("\n%5.2f%ci*%5.2f\n",p_reala,p_imaginara>=0?'+':'-', p_imaginara>=0?p_imaginara:-p_imaginara); } Complex tc;
Metoda afişează ţine cont de semnul părţii imaginare. Dacă aceasta este negativă, semnul minus este afişat înaintea simbolului i al părţii imaginare. Se declară obiectul tc de tipul Complex. Accesul la membrii obiectului se face folosind operatorul de calificare: nume_obiect.nume_membru
unde numele obiectului specifică din ce obiect este accesat atributul respectiv sau în contextul cărui obiect se execută metoda respectivă. Acest mod de accesare este folosit atunci când se lucrează cu obiecte statice. În cazul în care nu avem un obiect ci un pointer către un obiect, este necesară şi dereferenţierea pointerului, înainte de accesul la membri. Acest lucru este realizat folosind operatorul -> în locul operatorului de calificare: p_obiect -> nume_membru
Pentru implementarea conceptului de încapsulare, în interiorul unei definiţii de clasă pot fi folosiţi modificatori de acces. Aceştia sunt private, protected şi public (urmaţi de caracterul: „două puncte”). Domeniul de acţiune al unul modificator de acces începe în locul unde apare el şi se încheie la apariţia altui modificator sau la terminarea descrierii clasei. Implicit, toţi membri sunt consideraţi sub influenţa modificatorului private, deci orice membru aflat în afara domeniului de acţiune al unui modificator este considerat privat. Modificatorul public face ca toţi membri aflaţi în domeniul său de acţiune să poată fi accesaţi atât de către metodele clasei, cât şi de către orice entitate din afara clasei (membri publici). Modificatorul private face ca membrii aflaţi în domeniul său de acţiune să poată fi accesaţi numai de către metodele clasei respective (membri privaţi). Modificatorul protected este similar cu modificatorul private, dar membrii respectivi pot fi accesaţi şi de către metodele claselor derivate, dar numai în obiecte aparţinând claselor derivate.
195
Programarea calculatoarelor
De obicei atributele unei clase sunt declarate ca fiind private, iar metodele sunt împărţite, unele fiind publice (interfaţa clasei) şi unele private (detalii şi mecanisme interne de implementare a clasei. Deşi este tehnic posibil ca toţi membrii unei clase să fie privaţi, un obiect de acest tip nu poate fi folosit, neavând o interfaţă cu mediul exterior lui. De asemenea, toţi membrii unei clase pot fi publici, dar nu este recomandată această tehnică din motive de protecţie şi securitate. Exemplu În acest context, clasa Complex definită în exemplul anterior nu poate fi folosită, toţi membrii ei fiind privaţi. Pentru a putea folosi obiecte de tipul Complex, metoda afişează trebuie să fie publică. Descrierea clasei devine: class Complex { float p_reala,p_imaginara; public: void Afiseaza(); }; void Complex::Afiseaza() { printf("\n%5.2f%ci*%5.2f\n",p_reala,p_imaginara>=0?'+':'-', p_imaginara>=0?p_imaginara:-p_imaginara); }
Prin adăugarea modificatorului de acces public, metoda afişează este pusă la dispoziţia mediului extern, ca interfaţă a obiectului. Pentru accesul controlat la atributele private, clasele pot pune la dispoziţia mediului extern metode publice de acces, numite uzual metode accesorii. Acestea au rolul de a prezenta mediului extern valorile unora dintre atribute (acelea care pot prezenta interes) sau de a modifica valorile unora dintre atribute, în mod controlat (numai acele atribute pentru care modificarea la iniţiativa mediului extern are sens). Controlul depinde în fiecare caz de scopul clasei respective. Exemplu Adăugând metode accesorii clasei Complex, descrierea acesteia devine: class Complex { float p_reala,p_imaginara; public: void Afiseaza(); float GetR(); float GetI(); void SetR(float r); void SetI(float i); }; void Complex::Afiseaza() { printf("\n%5.2f%ci*%5.2f\n",p_reala,p_imaginara>=0?'+':'-', p_imaginara>=0?p_imaginara:-p_imaginara); }
196
Elemente de programare orientată obiect float Complex::GetR() { return p_reala; } float Complex::GetI() { return p_imaginara; } void Complex::SetR(float r) { p_reala=r; } void Complex::SetI(float i) { p_imaginara=i; }
Metodele accesorii definite mai sus (GetR, GetI, SetR, SetI) au rolul de a prezenta valorile atributelor şi respectiv de a stabili noi valori pentru ele. În acest exemplu nu se face nici un fel de control asupra modului în care sunt stabilite noile valori. Folosind descrierile de mai sus, următoarea secvenţă de program: void main() { Complex tc; Complex *pc; tc.SetR(5); tc.SetI(-4); tc.Afiseaza(); pc=&tc; pc->Afiseaza(); pc->SetR(-2); pc->SetI(3); pc->Afiseaza(); }
produce pe ecran următorul rezultat: 5.00-i* 4.00 5.00-i* 4.00 -2.00+i* 3.00
Pentru a evita eventualele confuzii, în interiorul corpului metodelor poate fi folosit pointerul implicit this atunci când se referă membrii obiectului curent. Acesta este gestionat automat şi are ca valoare adresa obiectului curent. Ca urmare, metoda SetR ar putea fi rescrisă astfel: void Complex::SetR(float r) { this -> p_reala = r; }
Pointerul this poate fi folosit şi pentru accesarea metodelor obiectului curent, în acelaşi mod. Ţinând cont de domeniul de valabilitate al declaraţiilor de tipuri de date, descrierea unei clase se face de obicei la începutul fişierului sursă în care urmează a fi folosită. Pentru o mai mare generalitate şi reutilizare mai uşoară, se preferă ca fiecare clasă nouă să fie descrisă într-un fişier sursă separat, care să fie inclus folosind directiva #include în programe.
197
Programarea calculatoarelor
10.3 Constructori Declararea obiectelor are ca efect alocarea de spaţiu în memorie, la fel ca în cazul declarării oricărei variabile. Acest spaţiu nu este iniţializat însă. Mai mult, în cazul în care obiectele clasei au şi spaţiu extins de memorie, acesta nu este alocat automat, obiectul declarat fiind astfel incomplet. Atributele unui obiect nu pot fi iniţializate la declarare într-o manieră asemănătoare datelor de tip articol (struct), deoarece de obicei atributele sunt private, deci inaccesibile din exteriorul obiectului. Pentru rezolvarea problemei iniţializării obiectelor există posibilitatea utilizării unor metode speciale, numite constructori. La terminarea ciclului de viaţă al obiectelor, este necesară dezalocarea lor. În general, aceasta se realizează automat, dar în cazul lucrului cu spaţiu extins, ea trebuie gestionată în mod explicit. Problema încheierii ciclului de viaţă al obiectelor este rezolvată prin utilizarea unor metode speciale numite destructori. Constructorii şi destructorii nu întorc niciun rezultat prin numele lor şi antetele lor nu precizează nici un tip pentru rezultat (nici măcar void). Constructorii sunt metode care au acelaşi nume cu clasa căreia îi aparţin. O clasă poate avea mai mulţi constructori, cu liste diferite de parametri (ca tip şi/sau număr) – metode supraîncărcate. Dacă nu este definit niciun constructor pentru o clasă, compilatorul va genera un constructor implicit, care nu face decât alocarea spaţiului propriu al obiectului, în momentul în care acesta a fost declarat. Ca urmare, în acest caz vom avea obiecte neiniţializate, urmând ca iniţializarea atributelor să se facă ulterior, prin intermediul metodelor accesorii. În exemplul anterior, pentru clasa Complex s-a generat un constructor implicit care alocă spaţiu pentru atributele p_reala şi p_imaginara. Iniţializarea s-a făcut prin intermediul metodelor SetR şi SetI. În cazul în care clasa prezintă cel puţin un constructor explicit, compilatorul nu mai generează constructorul implicit. Ca urmare nu se vor putea declara obiecte neiniţializate dacă parametrii constructorului nu au valori implicite. Constructorii nu pot fi apelaţi explicit, precum metodele obişnuite. Apelul lor se realizează numai la declararea obiectelor. De asemenea, nu se poate determina adresa constructorilor, aşa cum se poate face în cazul funcţiilor obişnuite. Am văzut mai sus că declaraţia unui obiect care nu are constructor explicit este identică cu declaraţia unei variabile simple. În cazul în care clasa prezintă constructori expliciţi valorile pentru iniţializare sunt transmise acestuia la declararea obiectului, asemănător listei de parametri reali la apelul unei funcţii: nume_clasă nume_obiect(lista_valori);
198
Elemente de programare orientată obiect
Exemplu Pentru clasa Complex se poate defini un constructor care să iniţializeze cei doi membri astfel: class Complex { float p_reala,p_imaginara; public: Complex(float a,float b); }; Complex::Complex(float a,float b) { p_reala=a; p_imaginara=b; }
Având acest constructor în cadrul clasei, nu putem declara obiecte neiniţializate (ca în exemplele anterioare), ci doar obiecte iniţializate: Complex a(3,4); Complex b(-1,3.2); Complex c;
//a reprezintă numarul 3+i*4 //b reprezintă numarul -1+i*3.2 //incorect
Atunci când există un singur constructor explicit, se aplică regulile transferului parametrilor similar ca la funcţiile obişnuite (se realizează conversia parametrilor reali către tipurile formale). Dacă sunt mai mulţi constructori, cu liste diferite de parametri, se alege acela a cărui listă de parametri corespunde ca tip şi număr cu lista valorilor (expresiilor) precizate la declararea obiectului. Pentru constructori, ca şi pentru orice altă funcţie, pot fi precizate valori implicite ale parametrilor. Acolo unde la apel lipsesc parametrii actuali, se folosesc valorile implicite. Ca urmare, la declararea obiectelor pot să nu fie precizate valori pentru toate atributele. Exemplu Se defineşte clasa Complex astfel: class Complex { float p_reala,p_imaginara; public: Complex(float a=0,float b=0); }; Complex::Complex(float a,float b) { p_reala=a; p_imaginara=b; }
Putem declara următoarele obiecte: Complex a; Complex b(-1); Complex c(2,3);
//a reprezintă numarul 0+i*0 //b reprezintă numarul -1+i*0 //c reprezintă numarul 2+i*3
Dacă prototipul constructorului ar fi fost Complex(float a,float b=0); atunci la declaraţia obiectului trebuie precizată obligatoriu valoarea pentru primul parametru (cel care va deveni valoarea atributului p_reala); ca urmare, dintre declaraţiile anterioare ar fi fost corecte numai cele ale obiectelor b şi c.
199
Programarea calculatoarelor
Acolo unde se poate folosi un singur parametru la declararea obiectului, se poate face iniţializarea asemănător iniţializării variabilelor simple. Folosind oricare dintre cei doi constructori din exemplul anterior, putem declara un obiect şi în modul următor: Complex a = 1;
//numarul 1+i*0
Valoarea declarată este atribuită primului parametru al constructorului. Pentru situaţiile în care avem nevoie atât de obiecte iniţializate, cât şi de obiecte neiniţializate, se adaugă în descrierea clasei atât un constructor care realizează iniţializarea obiectului, cât şi un constructor vid, care simulează constructorul implicit, adăugat de compilator atunci când nu se definesc constructori impliciţi. Constructorul vid are următoarea formă: Complex::Complex() { }
În acest context, declaraţia Complex a; are ca efect crearea obiectului a neiniţializat (se utilizează constructorul vid, nu constructorul cu valori implicite pentru parametri). Parametrii unui constructor pot fi de orice tip, mai puţin de tipul clasei respective (în exemplele anterioare, constructorul clasei Complex nu poate avea un parametru de tipul Complex. Este posibil însă să avem un parametru de tip pointer către clasa respectivă sau referinţă2 către clasa respectivă. Un constructor care primeşte ca parametru o referinţă către un obiect din acea clasă se numeşte constructor de copiere. Prin utilizarea unui constructor de copiere se poate iniţializa un obiect nou cu atributele unui obiect existent (se realizează o copie a acelui obiect). Constructorii de copiere pot avea şi alţi parametri, dar aceştia trebuie să aibă valori implicite. Compilatorul generează automat un constructor de copiere pentru toate clasele care nu au un constructor de copiere definit explicit. Exemplu Clasa Complex conţine un constructor de copiere: class Complex { float p_reala,p_imaginara; public: Complex(float a=0,float b=0); Complex(Complex &x); };
2
În C++ este implementat transferul parametrilor prin adresă. Pentru a transmite un parametru prin adresă, în lista de parametri se pune înaintea numelui său operatorul de referenţiere &
200
Elemente de programare orientată obiect Complex::Complex(float a,float b) { p_reala=a; p_imaginara=b; } Complex::Complex(Complex &x) { p_reala=x.p_reala; p_imaginara=x.p_imaginara; }
Putem declara următoarele obiecte: Complex a(3,4); Complex b = a; Complex c(b);
//numarul 3+i*4 //numarul 3+i*4 //numarul 3+i*4
Obiectul b este o copie a obiectului a şi va avea aceeaşi stare, imediat după declarare (aceleaşi valori ale atributelor). De asemenea, obiectul c este o copie a obiectului b. Pentru ambele obiecte s-a apelat constructorul de copiere. Constructorul implicit de copiere realizează o copie binară a spaţiului propriu de memorie al obiectului sursă. Ca urmare, dacă obiectele au spaţiu extins de memorie, în urma copierii ambele obiecte referă acelaşi spaţiu extins de memorie. Pentru a evita această situaţie anormală, atunci când clasele descriu obiecte care lucrează şi cu spaţiu extins este necesară folosirea unui constructor de copiere explicit, care să realizeze, pe lângă copierea atributelor, alocarea de spaţiu extins propriu pentru obiectul nou şi copierea spaţiului extins al obiectului sursă în acest spaţiu nou. Constructorii nu pot fi apelaţi în mod explicit în program. Singura situaţie în care poate să apară un astfel de apel este la declararea unui obiect, în modul următor: Complex a = Complex(2,5);
În cazul în care o clasă are ca membri obiecte, la declararea unui obiect al clasei se apelează întâi constructorii pentru obiectele membru şi apoi constructorul noului obiect.
10.4 Destructori Destructorii sunt metode speciale, asemănătoare constructorilor, care au rol invers: încheierea ciclului de viaţă al obiectelor. Aşa cum pentru fiecare clasă se generează un constructor implicit (dacă nu a fost prevăzut unul explicit), compilatorul generează şi un destructor implicit, dacă nu a fost prevăzut unul explicit. Spre deosebire de constructori, o clasă poate avea numai un destructor explicit. Ca şi constructorii, destructorii nu întorc niciun rezultat. Numele destructorului este numele clasei precedat de caracterul ~ (tilda). Destructorii nu au parametri.
201
Programarea calculatoarelor
Exemplu
class Complex { float p_reala, p_imaginara; public ~Complex(); } Complex::~Complex() { //descrierea corpului destructorului }
Pentru clasa Complex destructorul nu are nimic de făcut şi nu e necesară descrierea unui destructor explicit. Acest exemplu urmăreşte doar să arate cum se declară un destructor explicit. Spre deosebire de constructori, destructorii pot fi apelaţi explicit, atunci când este necesară ştergerea unui obiect. Apelul se face la fel ca pentru orice altă metodă, în contextul obiectului care trebuie şters: a.~Complex();
Utilizarea destructorilor este obligatorie atunci când se lucrează cu date dinamice, deoarece destructorii impliciţi nu pot elibera spaţiul alocat dinamic. În cazul în care la crearea unui obiect au fost apelaţi mai mulţi constructori, la ştergerea lui se apelează destructorii corespunzători, în ordine inversă.
10.5 Funcţii prieten În unele situaţii este nevoie ca funcţii care nu sunt membri ai unei clase să poată accesa atributele protejate ale clasei. În acest scop a fost introdus conceptul de funcţie prieten în C++. O funcţie prieten nu face parte din clasă, dar poate accesa atributele protejate. Pentru a specifica o funcţie prieten, în interiorul descrierii clasei se scrie prototipul funcţiei prieten, prefixat cu cuvântul rezervat friend. Întrucât funcţia prieten nu este membru al clasei, în interiorul său nu este definit pointerul this, ceea ce face ca funcţia prieten să nu poată accesa direct atributele obiectului. De aceea este necesar ca obiectul să fie parametru al funcţiei prieten. Ca urmare, o funcţie prieten are un parametru în plus faţă de o metodă. Modificatorii de acces nu au nicio influenţă asupra funcţiilor prieten, de aceea ele pot fi specificate oriunde în cadrul descrierii clasei. Exemplu În acest exemplu funcţia Afişează va fi scoasă în afara clasei Complex şi va fi declarată ca funcţie prieten. class Complex { float p_reala,p_imaginara; public: friend void Afiseaza(Complex x); Complex(float a=0,float b=0); Complex(Complex &x); };
202
Elemente de programare orientată obiect void Afiseaza(Complex x) { printf("\n%5.2f%ci*%5.2f\n",x.p_reala,x.p_imaginara>=0?'+':'-', x.p_imaginara>=0?x.p_imaginara:-x.p_imaginara); } void main() { Complex a(1,2); Afiseaza(a); }
Exemplu Să se implementeze clasa Stivă dinamică. O listă dinamică este formată din noduri, deci putem defini întâi clasa Nod, urmând a folosi tipul Nod pentru a descrie clasa Stivă. Pentru acest exemplu, datele memorate în nodurile stivei sunt de tip float. #include <stdio.h> typedef float TIP_INFO; class Nod { TIP_INFO info; Nod* next; public: float GetInfo(); Nod* GetNext(); Nod(float a, Nod* n); }; float Nod::GetInfo() { return info; } Nod* Nod::GetNext() { return next; } Nod::Nod(float a, Nod* n) { info=a; next=n; } class Stiva { Nod* Cap; public: Stiva(); Stiva(float a); ~Stiva(); void Push(float a); float Pop(); int Empty(); void Afiseaza(); }; void Stiva::Afiseaza() { Nod* x; x=Cap; while(x) { printf("%5.2f ",x->GetInfo()); x=x->GetNext(); } } Stiva::~Stiva() { Nod* x; while(Cap) { x=Cap; Cap=Cap->GetNext(); delete x; } }
203
Programarea calculatoarelor float Stiva::Pop() { float x; Nod* y; y=Cap; x=Cap->GetInfo(); Cap=Cap->GetNext(); delete y; return x;} void Stiva::Push(float a) { Cap=new Nod(a,Cap); } int Stiva::Empty() { return Cap?0:1; } Stiva::Stiva(float a) { Cap= new Nod(a,NULL); } Stiva::Stiva() { Cap=NULL; } void main() { Stiva s; int i; float x; if(s.Empty()) printf("\nStiva este goala"); else printf("\nStiva contine date"); for(i=0;i<10;i++) s.Push((float)i); s.Afiseaza(); x=s.Pop(); s.Afiseaza();
}
if(s.Empty()) printf("\nStiva este goala"); else printf("\nStiva contine date");
Clasa Nod conţine atributele info (informaţia utilă din nod) şi next, iar ca metode un constructor care iniţializează atributele obiectului şi metode accesorii pentru accesarea valorilor atributelor. Clasa Stiva conţine un singur atribut, Cap care are ca valoare adresa primului nod al stivei (vârful stivei). Constructorii clasei asigură crearea unei stive vide sau a unei stive cu un element. Metodele asigură adăugarea unei informaţii în stivă, respectiv extragerea unei informaţii. Metoda Afiseaza asigură afişarea pe ecran a informaţiilor din stivă.
10.6 Derivarea claselor Derivarea claselor este legată de implementarea conceptului de moştenire. În limbajul C++ este permisă moştenirea multiplă. Pentru a defini o clasă fiu ca fiind derivată dintr-o clasă părinte (sau mai multe clase părinte), se procedează astfel: class nume_clasa_fiu : lista_clase_părinte { descriere membri noi ai clasei fiu};
204
Elemente de programare orientată obiect
În lista claselor părinte se specifică numele claselor părinte, separate prin virgulă şi, eventual, precedate de modificatori de acces – se pot folosi modificatorii public sau private. Aceşti modificatori de acces definesc nivelul de protecţie a membrilor clasei părinte în clasa fiu, conform tabelului următor: Nivel acces în clasa părinte private protected public
Modificator de acces în lista claselor părinte public private public private public private
Nivel acces în clasa fiu inaccesibil inaccesibil protected private public private
Pentru fiecare clasă părinte se poate specifica un modificator de acces. Dacă nu se specifică niciun modificator de acces atunci, implicit, se consideră modificatorul public. Se observă că o clasă derivată are acces la membrii clasei părinte care au fost definiţi ca fiind publici sau protejaţi şi nu are acces la membrii privaţi. Prin derivare se construiesc ierarhii de clase, deci din clasa fiu se pot deriva alte clase noi. Dacă la derivare s-a folosit modificatorul de acces private, atunci toţi membrii clasei părinte vor deveni privaţi în clasa fiu şi o derivare în continuare nu mai este posibilă, ei fiind inaccesibili pentru orice altă clasă derivată din clasa fiu. Întrucât ierarhiile de clase nu sunt definitive, ci oferă posibilitatea extinderii prin adăugarea de noi clase derivate, se preferă ca la derivare să se folosească modificatorul de acces public. De aceea acesta este modificatorul explicit. Exemplu Fie clasa Punct care implementează entitatea punct geometric. Aceasta are atributele x şi y, care reprezintă coordonatele punctului în plan. Este inclusă o singură metodă, care desenează punctul pe ecran (această metodă nu va implementată în acest exemplu). class punct { int x,y; public: void deseneaza(); }; void punct::deseneaza() { //corpul nu este descris in acest exemplu }
Fie clasa Cerc care implementează entitatea geometrică cerc. Aceasta este descrisă prin coordonatele centrului cercului şi raza sa. Ca urmare clasa Cerc poate fi derivată din clasa Punct, adăugând un nou atribut (raza) şi o nouă metodă, pentru desenarea cercului.
205
Programarea calculatoarelor class cerc: punct { float raza; public: void deseneaza(); };
void cerc::deseneaza() { //corpul nu este descris in acest exemplu }
Clasa cerc o să aibă ca membri atributele x, y şi raza şi metodele desenează (moştenită de la clasa Punct, care va fi folosită pentru desenarea centrului cercului) şi desenează (nou definită, care va fi folosită pentru desenarea cercului). În lucrul cu ierarhii de clase se pune problema compatibilităţii tipurilor de date (clase) în cadrul atribuirilor şi a conversiilor tipurilor de date. Ca principiu, un obiect al unei clase părinte poate primi ca valoare un obiect al unei clase derivate. Acelaşi principiu este valabil şi în cazul pointerilor către obiecte. Utilizând exemplul de mai sus şi declaraţiile punct a, *pp; cerc b, *pc;
sunt corecte atribuirile pp=&a; pc=&b; a=b; pp=pc; pp=&b;
Nu sunt corecte următoarele atribuiri: pc=&a; b=a; pc=pp; Pot fi realizate atribuiri folosind conversia explicită a tipurilor de date, astfel: pc=(cerc *)pp; pc=(cerc *)&a;
10.6.1 Redefinirea atributelor Este posibil ca o clasă fiu să redefinească atribute moştenite de la clasa părinte (atribute publice sau protejate, întrucât cele private sunt oricum inaccesibile în clasa fiu). În acest caz, clasa fiu va avea două atribute cu acelaşi nume. Implicit, utilizarea numelui atributului respectiv referă atributul redefinit. Pentru a accesa atributul moştenit, trebuie folosit operatorul de rezoluţie, prefixând numele atributului cu numele clasei părinte.
206
Elemente de programare orientată obiect
Exemplu Fie o clasă Clasa_parinte care are un atribut a de tip float, atribut protejat, şi o clasă Clasa_fiu care redefineşte atributul a, de tip double. x este un obiect de tipul Clasa_fiu. class Clasa_parinte { protected: float a; //descrierea restului clasei }; Class Clasa_fiu: public Clasa_parinte { protected: double a; //descrierea restului clasei } Clasa_fiu x;
Expresia
x.a
referă atributul a al clasei derivate, de tip double. Pentru a accesa atributul a moştenit de la clasa părinte, în cadrul unei metode a obiectului x trebuie folosită expresia Clasa_parinte::a
10.6.2 Redefinirea metodelor La fel ca în cazul atributelor, o clasă fiu poate să redefinească metodele moştenite de la clasa părinte, în cazul în care metoda moştenită nu corespunde necesităţilor. În exemplul anterior, clasa Cerc redefineşte metoda desenează. Dacă a este un obiect de tipul Cerc, atunci apelul a.desenează(); sau desenează(); – efectuat din interiorul clasei Cerc – va lansa în execuţie metoda redefinită, cea descrisă de clasa Cerc, care va desena conturul cercului. Atunci când e nevoie să se apeleze metoda moştenită numele acesteia se prefixează cu numele clasei părinte, folosind operatorul de rezoluţie: punct::deseneaza();
Un astfel de apel poate să apară în interiorul metodei desenează a clasei Cerc pentru a desena centrul cercului. 10.6.3 Constructori şi destructori în relaţia de moştenire Constructorii şi destructorii nu se moştenesc precum alte metode. La crearea unui obiect al unei clase fiu se apelează întâi constructorul clasei părinte şi apoi constructorul clasei fiu. Dacă sunt mai multe clase părinte (moştenire multiplă) se apelează constructorii claselor părinte, în ordinea în care acestea apar în lista claselor părinte. La ştergerea unui obiect al unei clase fiu se apelează destructorii în ordine inversă faţă de constructori: întâi destructorul clasei fiu
207
Programarea calculatoarelor
şi apoi destructorii claselor părinte, în ordine inversă celei în care acestea apar în lista claselor părinte. Pentru a preciza parametrii reali utilizaţi pentru fiecare din constructorii claselor părinte, antetul constructorului clasei derivate are o formă specială: class Clasa_fiu: clasa_p1, clasa_p2, clasa_p3 { //attribute public: Clasa_fiu(); //constructorul clasei fiu } Clasa_fiu::clasa_fiu(…):clasa_p1(…),clasa_p2(…),clasa_p3(…) { //descrierea corpului constructorului }
Se observă că în descrierea clasei, constructorul se descrie în mod obişnuit, dar ulterior, în antetul constructorului apar apeluri ale constructorilor claselor părinte. Ordinea în care apar aceste apeluri nu are nici o importanţă, deoarece ordinea de apelare este dată de ordinea în care sunt specificate clasele părinte. Dacă una din clasele părinte nu are constructor, atunci nu o să apară un apel corespunzător în antetul constructorului clasei fiu, pentru ea apelându-se automat constructorul implicit. Dacă niciuna dintre clase nu are constructor explicit, atunci se folosesc constructorii impliciţi pentru toate clasele. O situaţie deosebită este aceea în care clasa fiu nu are constructor explicit, dar cel puţin una din clasele părinte are un constructor explicit. Deoarece în această situaţie nu se pot descrie explicit parametrii pentru apelarea constructorilor claselor părinte, aceşti constructori trebuie să aibă valori implicite pentru toţi parametrii. 10.6.4 Clase virtuale În cazul moştenirii multiple pot să apară situaţii în care o clasă derivate moşteneşte un atribut (sau mai multe) de mai multe ori, prin intermediul mai multor linii de moştenire. Aceste situaţii produc ambiguităţi legate de referirea atributului respectiv. Limbajul C++ oferă un mecanism simplu prin care să se revină astfel de situaţii, prin utilizarea claselor virtuale. Fie următoare ierarhie, în care clasa cf este derivată din clasele cp1, cp2 şi cp3, toate acestea fiind la rândul lor derivate din clasa cb. Clasa de la baza ierarhiei, cb are un atribut x. Acest atribut va fi moştenit în clasele cp1, cp2, cp3. Clasa cf va moşteni 3 exemplare ale atributului x.
208
Elemente de programare orientată obiect
Figura 10.5 Exemplu de moştenire multiplă Pentru a evita această situaţie, clasa cb poate fi declarată ca virtuală la descrierea claselor cp1, cp2 şi cp3, astfel: class { }; class { }; class { }; class { };
cp1: virtual public cb cp2: virtual public cb cp3: virtual public cb cf: public cp1, public cp2, public cp3,
Considerând aceste declaraţii, clasa cf moşteneşte o singură dată atributul x, prin intermediul clasei cp1. Ca principiu, atributul este moştenit prin intermediul clasei care apare prima în lista claselor părinte. În cazul în care în lista claselor părinte apar şi clase virtuale, se apelează întâi constructorii claselor virtuale, în ordinea în care au fost specificate, apoi constructorii claselor nevirtuale, în ordinea în care sunt acestea specificate. 10.6.5 Funcţii virtuale Există situaţii în care nu se poate decide în momentul compilării care este contextul curent în care se apelează o metodă, care a fost redefinită într-o clasă fiu. Astfel de situaţii apar atunci când se lucrează cu pointeri. Fie o clasă cp care conţine metoda executa, şi o clasă derivată din ea, numită cf, care redefineşte
209
Programarea calculatoarelor
metoda executa, cu aceeaşi listă de parametri. Fie următoarele declaraţii: cp a; cf b; cp* po;
//a este obiect de tipul cp //b este obiect de tipul cf // po este pointer catre clasa cp
Pointerul po poate lua ca valoare atât adresa unui obiect de tipul cp, cât şi adresa unui obiect de tipul cf. Fie apelul po->executa();
În momentul compilării nu se poate stabili ce metodă să se apeleze, a clasei părinte sau a clasei fiu. În astfel de situaţii compilatorul generează un apel către metoda clasei părinte. Limbajul C++ oferă posibilitatea de a întârzia decizia până la momentul execuţiei. În acest scop metoda clasei părinte se declară ca fiind virtuală prin scrierea cuvântului rezervat virtual înaintea antetului său. Este suficient ca metoda clasei de bază să fie declarată ca fiind virtuală, în mod automat şi metodele claselor derivate vor fi virtuale. Constructorii, destructorii şi funcţiile inline nu pot fi virtuale. 10.6.6 Clase abstracte În limbajul C++ este definit conceptul de funcţie virtuală pură. Acest concept este necesar în cazul ierarhiilor de clase. Există situaţii în care toate clasele ierarhiei trebuie să conţină o anumită metodă, implementarea ei fiind diferită în fiecare clasă derivată. Clasa de la baza ierarhiei este prea generală pentru a putea implementa metodele. În această situaţie în clasa de bază se includ metode virtuale pure. Prezenţa unei astfel de metode obligă toate clasele derivate să o conţină, fie că o redefinesc fie că nu. O metodă virtuală pură se declară asemănător cu o metodă virtuală, adăugând la sfârşitul antetului =0: virtual tip_rezultat nume_metoda(lista parametri) =0;
Metodele virtuale pure nu au un corp, nefiind implementate. O clasă care conţine o metodă virtuală pură se numeşte clasă abstractă. O clasă abstractă nu poate fi instanţiată, ea conţinând metode care nu sunt implementate. Clasele derivate din clase abstracte pot să redefinească metodele virtuale pure sau nu. Dacă metoda virtuală pură nu este implementată, atunci şi clasa respectivă este clasă abstractă şi nu poate fi instanţiată.
210
Elemente de programare orientată obiect
De exemplu, într-o ierarhie de clase care descriu figuri geometrice, fiecare clasă are nevoie de metode pentru desenarea figurii respective, calculul ariei sau perimetrului. La baza ierarhiei se află o clasă abstractă care include metode virtuale pure pentru aceste operaţii. Clasele derivate, vor implementa metodele conform specificului fiecărei figuri geometrice.
211
Bibliografie 1. [Aho, Hop şa] Aho A., Hopcroft J., Ullman J., Data Structures and Algorithms, Addison-Wesley, 1983 2. [Bras, Brat] Brassard G., Bratley P., Algoritmics: Theory and Practice, Prentice-Hall, 1988 3. [Cor, Lei şa] Cormen T., Leiserson C., Rivest R., Introduction to Algorithms, MIT Press, sixteenth printing, 1996 4. [Ghilic, 2003] Ghilic-Micu Bogdan, Roşca Ion Gh., Apostol Constantin, Stoica Marian, Lucia Cocianu Cătălina, Algoritmi în programare, Bucureşti, Editura ASE, 2003 5. [Gon] Gonnet G.H., Handbook of Algorithms and Date Structures, Addison-Wesley, 1984 6. [Hor] Horowitz E., Sahni S., Fundamentals of Computer Algorithms, Computer Science Press, 1978 7. [Knu] Knuth D., Fundamental Algorithms, vol 1 of The Art of Computer Programming, Addison-Wesley, 1973 8. [Knu] Knuth D., Sorting and Searching, vol 3 of The Art of Computer Programming, Addison-Wesley, 1973 9. [Negrescu, 1994] Negrescu Liviu, Limbajele C şi C++ pentru începători, Cluj-Napoca, Editura Microinfomatica, 1994 10. [Man] Manmber U., Introduction to Algorithms: A Creative Approach, Addison-Wesley, 1989 11. [Pop, Geo şa] Popovici Ct., Georgescu H., State L., Bazele informaticii, vol 1, Tip. Universităţii din Bucureşti, 1990 12. [Smeureanu, 1995] Ion Smeureanu, Ion Ivan, Marian Dârdală, Limbajul C/C++ prin exemple, Bucureşti,Editura Cison, 1995 13. [Tom] Tomescu I.., Probleme de combinatorică şi teoria grafurilor, Bucureşti, Editura Didactică şi Pedagogică, 1981 14. [Tud] Tudor S., Tehnici de programare, Bucureşti, Editura Teora, 1994 15. [Wil] Wilf H., Algorithms and Complexity, Prentice-Hall, 1986
212