Sunteți pe pagina 1din 45

Alocarea dinamica a memoriei

De cele mai multe ori , n aplicatii ,datele sunt grupate n colectii de date ,care trebuie organizate ntr-un anumit mod pentru a putea fi prelucrate. Pornind de la aceasta idee, a aparut notiunea de structura de date. O colectie de date nzestrata cu informatii structurale, care permit identificarea si selectia componentelor, se numeste structura de date.
Componentele unei structuri de date pot fi identificate si selectate fie prin numele lor, fie prin intermediul relatiilor structurale. Cea mai simpla relatie structurala este pozitia fiecarei componente n cadrul structurii. Asupra unei structuri de date se pot aplica mai multe tipuri de operatii:

1. vizualizarea elementelor structurii sub diverse forme; 2. actualizarea (adaugarea, modificarea sau stergerea anumitor componente); 3. mbogatirea structurala prin adaugarea unor informatii de legatura; 4. sortarea (aranjarea componentelor ntr-o anumita ordine stabilita de un anumit criteriu de ordonare) etc.
Memoria calculatorului este alcatuita din locatii de memorie succesive si etichetate. Aceasta eticheta poarta numele de adresa. Fiecare variabila, n momentul declararii ei are alocata o locatie de memorie, cunoscuta prin adresa sa. Un pointer este o variabila care ia ca valori adrese de memorie. Deci se poate memora adresa fiecarei variabile, chiar daca nu o cunoastem. Trebuie sa anuntam pointerul care va prelua aceasta valoare: deci sa-l declaram. Cum declaram si cum utilizam un pointer? Pointerul se declara ca o variabila, adica prin tip si prin nume: <tip> *<nume>; unde <tip> reprezinta tipul variabilelor a caror adresa va fi memorata n variabila pointer; * este un simbol pentru a indica faptul ca variabila este un pointer;<nume> este identificatorul C++ pentru numele variabilei. Observatii: simbolul " * " poate sa apara oriunde ntre <tip> si <nume>; tipul pionterului este dat de tipul variabilei a carei adresa de memorie dorim sa o retinem n pointer si deci nu exista tipul pointer ca atare;

Toti pointerii odata creati vor fi initializati cu ceva. Daca nu stim exact ce adresa vom memora n pointerul respectiv l vom initializa cu 0 adica valoarea NULL n limbajul C++, respectiv NIL n limbajul Pascal. Acest pointer se va numi pointer nul. Pointerul nul nu retine nici o adresa. Un pointer care nu se initializeaza se va numi pointer salbatic( wild pointer) si este foarte periculos. Iata un exemplu n care se lucreaza cu un poiter neinitilizat: int x, *p; .... Observatie! Atribuirea *p=x; face ca valoarea memorata n x sa fie "scrisa" ntr-o zona de memorie necunoscuta. Este foarte posibil ca acest lucru sa nu creeze probleme att timp ct programul are dimensiuni mici si zona alocata arbitrar pointerului nu intra n zona de program, de date sau a sistemului de operare. Dar marindu-se programul probabilitatea ca p sa pointeze o zona importanta de memorie creste, caz n care programul nu mai ruleaza complet sau chiar se opreste. Pentru a initializa un pointer cu adresa unei variabile se apeleaza la operatorul de adresare &. n exemplul urmator vom crea un pionter si n el vom memora adresa unei variabile de tip int: int x=10; int *px=0; // se construieste pointerul nul; px=&x;// se salveaza n px adresa lui x; Observatii: stim ca variabila px este un pointer pentru ca are simbolul "* " n fata; stim ca variabila px contine adresa variabilei x deoarece aceasta este precedata de operatorul de adresare &; *p=x; x=10;

Utiliznd un pointer, se poate afla valoarea variabilei a carei adresa este memorata n pointerul respectiv. Accesul la valoarea variabilei prin intermediul pointerului se realizeaza prin indirectare(deferentiere) Numele acestui proces provine de la faptul ca valoarea se acceseaza indirect prin pointer. Deci prin indirectare ntelegem accesarea valorii de la adresa memorata n pointer. n concluzie pointerul furnizeaza o metoda indirecta de a lucra cu valoarea pastrata la o anumita adresa si indirectarea se realizeaza utiliznd operatorul " * ".Cum schimbam valoarea unei variabile folosind indirectarea? int *px ; int x=1;

px=&x; //px contine adresa lui x; *px=5; /* se schimba continutul lui x prin intermediul pointerului care retine adresa lui x */ Operatorul * este utilizat n doua moduri distincte n lucrul cu pointerii: 1. Cnd un pointer este declarat, * indica faptul ca avem de-a face cu o variabila pointer si nu cu o variabila obisnuita; 2. Cnd un pointer este deferentiat, * indica faptul ca avem acces la valoarea de la adresa memorata de pointer. Este foarte important sa facem distinctie ntre pointer , adresa memorata ntr-un pointer si valoarea care se gaseste la adresa respectiva. Acest aspect constituie sursa de confuzii si erori n lucrul cu pointerii. Pointerii pot fi folositi la ascunderea variabilelor. Se poate folosi indirectarea simpla, dubla etc. Nivelul de indirectare se poate mari, dar acest lucru determina un acces mai dificil la continutul locatiilor de memorie.

Ce este memoria heap si cum o manipulam?


De ce sa utilizam pointerii, daca se poate accesa valoarea direct prin variabila? Singurul motiv pentru care lucram cu pointerii n acest mod este acela de a demonstra cum lucreaza. In mod real pointerii sunt utilizati pentru: Lucrul cu memoria heap; Transmiterea parametrilor prin referinta n cadrul functiilor; Manipularea claselor polimorfice;

Deoarece memoria interna a calculatorului cuprinde spatiul global, memoria heap registri, spatiu de cod si stiva vom arata de ce e important lucrul cu pointeri. Pe stiva sunt memorate variabilele locale si parametrii functiilor, n timp ce n spatiul global sunt memorate variabilele globale. n spatiul de cod este stocat codul, sursa (programul),iar registrii sunt utilizati pentru administrarea interna a functiilor. Restul memoriei este memoria heap. Sa ne referim la utilitatea memoriei heap. Variabilele locale nu sunt vazute dect n timpul executie functiei n care apar. Variabilele globale rezolva problema accesului neconditionat de oriunde din program, dar conduce la crearea unui cod dificil de urmarit. Memoria heap vine sa rezolve ambele tipuri de probleme ridicate de folosirea celor doua tipuri de variabile: locale si globale. Administrarea memoriei heap presupune lucrul cu adrese de memorie. Accesarea unei valori stocate n heap nu se poate face dect prin adresa locatiei rezervate anterior memorarii valorii respective.

Daca spatiul alocat pe stiva este eliberat n mod automat cnd o functie si termina executia, spatiul alocat n memoria heap, cu ajutorul unui pointer, se elibereaza n mod deliberat de utilizator cnd nu i mai foloseste. Avantajul accesarii memoriei n acest mod vis-a-vis de utilizarea variabilelor globale este acela ca numai functiile cu acces la pointeri au acces la date. Acest lucru realizeaza o interfata cu datele mai bine controlate. Evident pentru acest lucru este necesar sa se poata crea un pointer n memoria heap si sa se transmita acel pointer ntre functii. Alocarea spatiului n memoria heap se realizeaza prin utilizarea operatorului new numit operator de alocare. Sintaxa: new <tip> [<dimensiune>]: unde <tip> reprezinta tipul valorii care va fi memorata n heap, <dimensiune> reprezinta numarul de locatii de tipul <tip> alocate. New returneaza o adresa de memorie care va fi asignata unui pointer, daca mai exista spatiu disponibil, sau 0(NULL) n caz contrar. Zona heap, fiind limitata exista posibilitatea ca alocarea sa nu se poata face cnd aceasta este plina. Din acest motiv dupa alocarea zonei prin new trebuie sa se verifice daca aceasta a avut loc n mod real. Exemplu: int *x; p=new int; if (p!=NULL) // if(p = = 0) cout<<"Heap-ul este plin"; Odata ce spatiul de memorie alocat nu va mai este necesar, este cazul sa-l disponibilizati. Acest lucru se realizeaza apelnd la operatorul delete. Sintaxa: delete <pointer>; delete [ ] <pointer>; delete <nume vector> [ ];unde: <pointer> reprezinta numele variabilei pointer care a preluat adresa unei locatii alocate n heap prin new. A doua varianta se utilizeaza pentru disponibilizarea unei zone de memorie avnd mai multe locatii iar a treia variant se utilizeaz pentru eliberarea unei zone de memorie alocate printr-un vector. Trebuie retinut c pointerul nsusi, spre deosebire de memoria pointat de acesta, este o variabil care se declar ntr-o functie. Prin urmare cnd functia n care a fost declarat si ncheie executia, pointerul se pierde, este sters de pe stiv. Memoria alocat cu new si controlat de pointerul respectiv n schimb nu este eliberat automat. Respectiva zon de memorie devine indisponibil genernd o gaur de memorie(memory leak).Zona de memorie nu se mai "reface" nici cnd programul se termin. O alt cale prin care se pot crea guri de memorie este dat de reasignarea unui pointer nainte de disponibilitatea zonei pointate anterior de acesta. De exemplu: int *p=new int; p=10; p=new int; (2) (3) (1)

*p=100; delete p;

(4) (5)

Desi se disponibilizeaz n linia (5) zona alocat prin p, acest lucru se refer numai la a doua alocare efectuat n linia (3).Zona alocat n linia (1) este irecuperabil, adresa acesteia fiind pierdut. Evitarea acestui aspect se realizeaz introducnd ntre linia (2) si (3) delete p; Se pot declara si pointeri constanti folosind cuvntul cheie const. const int *p1; int *const p2; const int * const p3; Unde p1 este un pointer la un ntreg constant. Valoarea variabilei la care pointeaz nu poate fi schimbat. P2 este un pointer constant la un ntreg. Valoarea variabilei la care pointeaz poate fi schimbat dar p2 nu poate s retin alt adres dect cea primit initial.P3 este un pointer constant la un ntreg constant. Valoarea de la adresa retinut de pointer nu poate fi schimbat dar nici valoarea retinut n pointerul p3 nu poate fi alterat. Pentru a vedea cine rmne constant priviti n dreapta cuvntului cheie const. Cnd operm cu pointeri avem posibilitatea s-i comparm(compararea adreselor de memorie pe care le memoreaz ei),adunarea si scderea unui ntreg cu / dintr-un pointer; incrementarea, decrementarea etc. Este permis utilizarea operatorilor relationali si de egalitate. Adunarea unui ntreg la un pointer. Fie p un pointer si n un ntreg reprezentat prin alocarea unei variabile ntregi sau o constant de acelasi tip. Expresia p + n const n mrirea valorii adresei retinute n pointerul p cu n* dimensiunea n octeti necesar reprezentrii tipului pointerului p. Deoarece tipul float necesit patru octeti, adresele cresc din patru n patru. Scderea unui ntreg dintr-un pointer. Operatia este opus celei prezentate anterior. Deci p-n va conduce la micsorarea valorii pstrate n p cu n* dimensiunea n octeti necesar reprezentrii tipului pointerului p. Incrementarea si decrementarea pointerilor. Aceast operatie este identic cu adunarea, respectiv scderea, lui 1 la un pointer. Astfel ++p sau p++ este identic cu a scrie p=p+1 sau p+=1,dup cum - - p sau p- - este acelasi lucru cu a scrie p=p-1 sau p- =1, cu respectarea regulilor care se cunosc de la operatorii de incrementare / decrementare. Declararea unui tablou ofer posibilitatea de a lucra cu pointeri. Numele tabloului desemneaz un pointer constant si are ca valoare adres de memorie a primului element din tablou. Datorit acestui aspect, accesarea elementelor din tablou se poate face si cu ajutorul pointerului desemnat prin variabila ce reprezint tabloul. In mod clasic elementul i al unui

tablou este desemnat prin a[i]. Prin utilizarea pointerului constant a, *(a + i).De ce asa? Pentru c a reprezenta adresa locatiei de memorie a primului element: a[0],adresa elementului i, conform operatiilor cu pointeri, va fi a + i. Continutul locatiei respective va fi *(a + i). Elementele unei matrici sunt memorate pe linii, ntr-un spatiu continuu de memorie. Pentru a accesa un element trebuie s identificm locatia unde ncepe linia pe care se afla elementul, iar apoi s ne deplasm n cadrul acestei linii cu attea locatii ct este indicele de coloan. Astfel a[i][j] se identific la nivelul pointerilor cu *(*(a + i)+j).Deoarece numele tabloului reprezint un pointer constant, zona de memorie alocat de acesta nu se poate schimba. int a[10] , *p ;

a = p ; //eroare ,a este un pointer constant; n schimb este posibil s gestionm printr-un pointer o zon de memorie alocat prin declararea unui tablou.

REFERINE
O referinta este un alias al unui obiect. Atunci cnd crem o referint, aceast se initializeaz cu numele unui obiect, numit obiect tint. Din acest moment referinta actioneaz ca un nume alternativ al obiectului tint iar toate operatiile care se vor efectua asupra referintei se vor rsfrnge asupra obiectului tint. Declararea unei referinte se face respectnd urmtoarea sintaxa: <tip> & <nume_referinta>=<obiect_tint>; <tip> --- este acelasi cu tipul obiectului tint; <nume_referint>---este un identificator al limbajului de programare (C++); Pentru a putea declara o referint asupra unui obiect tint trebuie ca obiectul tint s fi fost deja declarat (anterior referintei).Operatorul de referentiere &, care apare n declararea referintei are acelasi simbol cu operatorul de adres, dar nu sunt unul si acelasi operator. Asa cum am vzut mai sus, o variabil poate fi gestionat n direct prin pointeri. Dar acest lucru poate fi fcut ntr-o manier mai simpl prin referinte , Cu o mic diferent, referintele nu pot fi reinitializate, fiind pentru totdeauna aliasurile obiectelor tint pentru care au fost construite initial.

n lucrul cu subprograme (functii si proceduri), se ntmpin probleme din dou motive: argumentele functiilor fiind transmise prin valoare, efectul operatiilor asupra lor nu era sesizat dect n interiorul functiei, si nu se poate returna din functie dect o valoare, instructiunea return acceptnd un singur parametru Eliminarea acestor neajunsuri se poate face prin transmiterea parametrilor prin referinte. n C++, acest lucru se poate face utiliznd pointerii sau referintele. Sintaxa difer n cele dou cazuri dar rezultatul este acelasi. De exemplu: #include<stdio.h> #include<conio.h> void cub(int x) void main()

Iesirea va fi: a=10 x=1000 si o linie mai jos a=10.Se observ c variabila "a" nu si-a modificat valoarea dup apelul functiei "cub". Aceasta deoarece transmiterea parametrilor n cazul functiei "cub" s-a fcut prin valoare. Se observ n schimb c parametrul x si-a schimbat valoarea n cadrul functiei. Pentru c aceast schimbare s se observe si n programul apelant, utilizm pointerii sau referintele. Astfel rescriem functia "cub" n cele dou variante: #include<stdio.h> #include<conio.h> void cub_pointeri(int *x) void cub_referinte(int &x) main() iesirea va fi: a din apelul cub_pointeri=8 a din apelul cub_referinte=512 Deoarece cub_pointeri lucreaz cu pointeri, vom identifica valoarea prin cotinutul de la adresa x. De aceea apare *x. Apelul functiei trebuie s transmit adresa lui x; prin urmare

apare ca parametru efectiv &a. Iar functia cub_referinte lucreaz cu aliasul variabilei a si att apelul ct si manevrarea variabilelor n cadrul functiei este mult mai natural. Din cele artate mai sus, ar fi de preferat utilizarea referintelor. Acestea sunt clare si usor de utilizat. Totusi referintele nu sunt reasignate. Astfel dac doriti s retineti adresa unui obiect ntr-o variabil si apoi aceeasi variabil s retin adresa altui obiect, trebuie s utilizati pointerii. Referintele nu pot fi nule. Prin urmare dac un obiect ntr-o conditie se va analiza vis-a-vis de valoarea NULL, nu vor fi utile referintele, ci pointerii. Dac operatorul new nu poate aloca memorie de heap, returneaz un pointer NULL. Pentru ca referintele nu pot avea valoarea NULL, nu trebuie s se initializeze o referint cu un pointer pn nu se verific dac pointerul nu are valoarea NULL. CONCLUZII: Utilizati transmiterea prin referint ori de cte ori este posibil; Nu lucrati cu pointeri dac nu se poate lucra cu referinte; Nu returnati referinta nici unui obiect local;

STRUCTURI

DE DATE

ALOCATE

DINAMIC

Ati sesizat c spatiul de memorie alocat unui tablou este acelasi de la nceputul si pn la sfrsitul executiei programului n care erau declarate. Pe de alt parte, de cele mai multe ori rmnea spatiu neutilizat. Alocati un vector de 10 elemente, dar utilizati doar 5 n program. Sau se poate ntmpla s mai avem nevoie de spatiu n plus fat de numrul maxim declarat. La fel se poate ntmpla si n cazul matricilor. Aceste neajunsuri nu se puteau remedia dect umblnd la declaratia tabloului---si mai exact, la dimensiunea acestuia. n nici un caz nu se puteau rezolva aceste aspecte prin program, n cursul executiei lui. Ca orice resurs memoria trebuie gestionat eficient si n acest scop se face apel la alocarea dinamic. Alocarea dinamic, spre deosebire de cea static, permite gestionarea memoriei n timpul executiei programului. Asadar se pot face alocri si eliberri ale spatiului de memorie "din mersul" programului, lucru imposibil n abordarea static a rezolvrii unei probleme. Dac n alocarea static se lucreaz cu tablouri n alocarea dinamic se lucreaz cu liste. Lista liniar nlantuit reprezint o structur dinamic de date (ea poate fi abordat si static, cu ajutorul vectorilor), n care elementele constituitive respect o anumit ordine, n sensul c fiecare element, cu exceptia primului, are un predecesor si fiecare element, cu exceptia ultimului are un succesor. Se numeste nod(celula, element) o unitate de informatie de sine stttoare (elementar)care poate s contin informatii utile si date de legtur. Cu alte cuvinte lista este o multime de noduri(celule).

Din categoria listelor vom prezenta n continuare: liste liniare simplu nlntuite, liste liniare dublu nlntuite, liste circulare, stive si cozi. 1. Liste simplu nlntuite :elementele au un singur pointer spre ele si sunt reprezentate de zona de informatie si zona de legtur (pointerul) ce retine adresa urmatorului nod n list. 2. Liste dublu nlntuite: elementele au doi pointeri spre ele si sunt reprezentate de asemenea de zona de informatie si zonele de legtur (pointerii) ce retin adresa nodului anterior si a celui urmtor. 3. Liste circulare :care pot fi dublu sau simplu nlntuite, au particularitatea c ultimul element este legat de primul, adic pointerul ultimului element vede adresa primului element. 4. Stiva: este o list liniar cu proprietatea c adugarea si eliminarea elementelor se face pe la un singur capt, numit vrful stivei .O stiv care nu contine nici un element se numeste stiv vid. n orice moment, putem scoate din stiv numai elementul care a fost introdus ultimul, motiv pentru care vom spune c stiva functioneaz dup principiul LIFO(last in first out). 5. Coada: este o list, n care adugarea elementelor se face pe la un capt numit "capt de introducere", iar eliminarea elementelor se face pe la cellalt capt numit "capt de extragere". In orice moment, putem scoate din coad numai elementul care n acel moment se afl la captul de extragere. Astfel spus elementele ies din coad n ordinea invers intrarii. Datorita acestei proprietti, se vor numi cozi FIFO(first in first out) .

ARBORI BINARI DE

CUTARE

Numim arbore binar de cutare un arbore binar n care orice nod, cu exceptia nodurilor terminale, se bucur de proprietatea c valoarea cheii este mai mare dect informatia din nodul fiu din stnga si mai mic dect informatia din nudul fiu din dreapta. Din definitie deducem c n arborele binar de cutare, informatia din noduri este unic. Numim cmp cheie acel cmp din structura nodului n functie de valoarea cruia se creeaz si se exploateaz arborele. n cele ce urmeaz cnd ne vom referi la informatia nodului vom considera valoarea cmpului cheie al nodului respectiv. O proprietate foarte important a arborelui binar de cutare este aceea c ofer posibilitatea ordonrii cresctoare a valorilor din cmpurile cheie, prin parcurgerea lui n inordine. Prin modul de constructie se observ c n arborele de cutare, gsirea unei informatii se face respectnd urmtorul algoritm: se caut n nodul curent; dac informatia s-a gsit, algoritmul se ncheie, altfel, dac informatia este mai mare dect informatia din nodul curent, se caut n subarborele drept al acestuia, altfel n subarborele stng.

Structurile arborescente reprezint structuri neliniare de date, cu multiple aplicatii n programarea si utilizarea calculatoarelor. ntr-un limbaj simplist structurile arborescente exprim relatii de "ramificare" ntre nodurile unui arbore, asemntoare configuratiei arborilor din natura, cu deosebirea c, n informatic, arborii "cresc" n jos. Se numeste arborescent un arbore care are un nod special numit radacina iar celelalte noduri pot fi repartizate n multimi disjuncte V1,V2,.Vm(m>=0),astfel nct n fiecare din aceste multimi disjuncte exist un nod adiacent cu rdcina iar subgrafurile generate de acestea sunt la rndul lor arborescente. n particular o arborescent cu un singur nod este format doar din nodul rdcin. Dac ordinea relativ a arborescentelor generate de multimile V1,V2,.Vm din definitie are important, arborescenta se numeste arbore ordonat. n reprezentarea grafic a unei arborescente, nodurile se deseneaz pe nivele, astfel: rdcina se afla pe primul nivel, varfurile adiacente cu rdcina pe nivelul urmtor, si asa mai departe.

Nivel 0 rdcina

Nodurile adiacente cu si care se deseneaz pe nivelul

Nivel 1 descendenti

urmtor ,se numesc

ai rdcinii. n mod analog Pentru un nod dat, nodurile adia Nivel 2 nivelul urmtor n arbore se numesc Nivel 3 g descendentii nodului respectiv. d e f cente lui care se afla pe

Descendentii aceluiasi nod se mai numesc si frati. n figur nodurile b si c sunt noduri-frate si totodat reprezint descendentii rdcinii. Varfurile d, e, f sunt descendentii nodului c. Dac un nod x este descendentul nodului y, mai spunem c y este printele nodului x. Folosim deci termenii "descendent" si "printe" pentru a exprima relatii ntre noduri aflate pe nivele consecutive n reprezentarea arborilor. Terminologia utilizat n structurile arborescente include chiar cuvinte ca fiu, tat, frati, unchi, strbunic, veri, cu nteles similar celui din vorbirea curent. Un arbore binar este un arbore ordonat n care fiecare nod are cel mult doi descendenti. Asadar, un arbore binar contine o radacina si cel mult doi subarbori binari disjuncti ce descind din aceasta, pe care i numim subarborele stng, respectiv subarborele drept. Daca un subarbore lipseste spunem ca este vid.

Arborele binar din figura alaturata are ca subarborele

1 iar ca subarbore drept

stng arborele binar cu radacina 2

2 radacina 2

arborele binar cu radacina 3. Arborele binar cu

are att subarborele stng ct si cel drept vizi. Arborele

4 vid, iar 5 6

binar cu radacina 3 are subarborele drept

subarborele stng nevid (arborele binar cu radacina 4).

Sa observam ca un arbore binar nu reprezinta un caz particular de arbore. De exemplu, arborii binari de mai jos sunt distincti (primul are radacina a si subarborele drept vid, iar al doilea are radacina a cu subarborele stng vid), desi ca arbori sunt identici.

Un terminal

vrf

fara

descendenti a

se

numeste a

nod

sau frunza. Pentru arborele binar din figura de mai sus nodurile 2, 5 si 6 sunt noduri frunza. ntr-un arbore binar, nivelele nodurilor se numeroteaza cu zero ncepnd de la

radacina. Astfel, radacina se a afla pe nivelul 0, fii radacinii pe nivelul 1, fii acestora pe nivelul 2 si asa mai departe. Un arbore binar n care fiecare nod care nu este terminal are exact doi descendenti se numeste arbore binar complet. Arborele de mai jos este un arbore binar complet: b b

Un arbore binar complet cu n noduri terminale,

1 nivel, are n total 2n-1

toate situate pe acelasi

2 n=2r, unde r este

noduri .Vom arata mai nti ca

ultimul nivel al arborelui, cel pe care se afla

nodurile terminale. Demonstram prin indicatie 4 7 dupa numarul nivelului, ca n arborele binar din

ipoteza, pe nivelul k se gasesc 2k noduri (k este cel 5 6 mult egal cu numarul de ordine al ultimului nivel din arbore). ntr-adevar, pe nivelul 0 se afla doar radacina, deci 20=1 noduri. Presupunem ca pe nivelul k se gasesc 2 noduri si sa demonstram ca pe nivelul k+1 se gasesc 2k+1 noduri. Daca nivelul k+1 apartine arborelui, atunci nodurile de pe acest nivel sunt doua cte doua descendente din nodurile de pe nivelul k, nivel pe care, conform ipotezei, sunt 2k noduri. Prin urmare, numarul total de noduri de pe nivelul k+1 este 2 * 2k = 2k+1. Rezulta ca pe ultimul nivel r al arborelui binar vor fi 2r noduri.
k

Sa calculam acum numarul total din arbore: 20+21+.+2r = 2r+1-1 = 2 * 2r-1 = 2n-1. Un arbore binar complet are un numar impar de noduri.

REPREZENTAREA ARBORILOR BINARI Exist mai multe moduri de reprezentare a arborilor binari: a) reprezentarea standard---se specific rdcina RAD si, pentru fiecare varf, se precizeaz descendentul stang si descendentul drept, in caz c acestia exist. Modul concret n care se precizeaz descendentii poate s difere:putem utiliza vectori, asa cum vom detalia imediat sau putem folosi avantajele alocrii dinamice, care permite sa legm un nod de printele sau utilizand adrese reale de memorie.in prima variant se folosesc doi vectori S si D ,pentru fiecare varf i,S[i] fiind descendentul stang iar D[i] descendentul drept.In general informatia continut de un anumit nod difer de numrul su de ordine.De exemplu un arbore binar poate avea ca informatii n noduri nume de persoane.In acest caz, se mai utilizeaz un vector INF, astfel ncat pentru fiecare nod i, INF[i] s contin informatia asociat nodului respectiv.Pentru arborele din figura de mai sus,cu n=7 vrfuri,avem: RAD=1,S=(2,0,4,5,0,0,0) iar D=(3,0,7,4,0,0,0). Se observ c in general nu este esential s precizm rdcina,ea putnd fi dedus din

vectorii S si D, dat fiind faptul c rdcina este singurul varf care nu este descendentul vreunui alt varf.Cu ajutorul alocrii dinamice se defineste urmtoarea structur: typedef struct nod TNOD; si se declar: TNOD * rad; In definirea tipului de date TNOD am considerat un singur camp de tip int pentru a retine numrul de ordine al varfului.Este desigur permis ca structura respectiv s contin oricate campuri (de diverse tipuri) pentru memorarea informatiilor asociate nodurilor. Esentiale pentru prelucrarea arborelui sunt legturile stanga si dreapta(respectiv st si dr) pentru fiecare nod, si adresa nodului rdcin rad. b) se utilizeaz doi vectori DESC si TATA:in vectorul DESC ,avand valori -1,0,1, se precizeaz pentru fiecare nod ce fel de descendent este el pentru tatl sau.(stang nu are parinte sau drept) iar in vectorul TATA se indic pentru fiecare nod nodul printe.cum nodul rdcin,nu are nod printe,componenta corespunzatoare din vectorul TATA este 0.Pentru arborele binar din figura de mai sus,cu n=7 varfuri, avem TATA=(0,1,1,3,4,4,3) si DESC=(0,-1,1,-1,-1,1,1). PARCURGEREA ARBORILOR BINARI Prin parcurgerea unui arbore se intelege examinarea in mod sistematic a nodurilor sale, asfel ncat fiecare nod s fie accesat o dat si numai o dat.Aceast misiune este numit si "vizitare" a varfurilor arborilor si are ca scop prelucrarea informatiilor continute de acestea.Arborele este o structur neliniar de organizare a datelor,iar rolul traversrii sale este tocmai conceperea unei aranjri liniare a varfurilor fructificand avantajele acestei organizari. Exista trei modalitti principale de parcurgere a arborilor binari: preordine: se viziteaz rdcina, se traverseaz subarborele stang in se traverseaz subarborele drept in preordine; preordine,

inordine:se traverseaz subarborele stang in inordine,se viziteaz rdcina,se traverseaz subarborele drept in inordine; postordine:se traverseaz subarborele stang in postordine,se traverseaz subarborele drept in postordine,se viziteaz rdcina;

Pentru arborele binar de mai sus(n=7 noduri),parcurgerea in preordine genereaz varfurile in ordinea:1,2,3,4,5,6,7;parcurgerea in inordine furnizeaz varfurile in ordinea:2,1,5,4,6,3,7;iar parcurgerea in postordine conduce la:2,5,6,4,7,3,1;

FORMA

PREFIXATA A

EXPRESIILOR

ARITMETICE

Notatia polonez a expresiilor aritmetice este una dintre cele mai importante aplicatii ale arborilor binari si a fost introdus de matematicianul polonez J.Lukasiewicz. Se stie c un program scris ntr-un limbaj de programare este supus unui proces complex de transformri, pan ce el ajunge n format direct executabil de ctre procesorul unui calculator. Vom vorbi aici despre modalitatea prin care expresiile aritmetice sunt transformate n faz de compilare ntr-o form intermediar care st la baza generrii codului obiect corespunztor(necesar n faza de executie pentru evaluarea acelei expresii) Vom defini in continuare notatia fr paranteze asociat expresiilor aritmetice. Vom lucra, pentru simplitate, cu expresii aritmetice alctuite din operanzi care sunt variabile al cror nume este format dintr-o singur litera si constante alctuite doar dint-o singur cifr. operatorii permisi in expunerea noastr sunt doar cei binari, si anume: adunarea(+), scderea(-),nmultirea(*),mprtirea(/) si ridicarea la putere(^). Mai ntai s vedem cum putem asocia unei expresii aritmetice E, admise in contextul n care lucrm, un arbore binar complet. A1.Dac E este format dintr-un singur operand, i asociem un arbore binar format doar din rdcin, n care punem operandul respectiv. A2.Dac E=E1 op E2 unde "op" este unul din operatorii permisi, iar E1 si E2 sunt expresii aritmetice, i asociem lui E arborele binar complet care are n rdcin operatorul "op", ca subarbore stang arborele binar asociat expresiei E1 iar ca subarbore drept arborele binar asociat expresiei E2. A3.Dac E=(E1) unde E1 este o expresie aritmetic, i asociem expresiei E arborele binar asociat expresiei E1. Pentru expresiile aritmetice simple a+b, a-b, a*b, a/b, a^b arborii binari asociati sunt desenati mai jos:

S observm c, deoarece operatiile de scdere, mprtire si ridicare la putere,nu sunt comutative, arborii obtinuti sunt ntr-adevr binari, deoarece se face distinctie ntre descendentul stang(care contine primul operand) si descendentul drept(care contine al doilea operand). Pentru expresia a*b+c/d-e arborele binar complet asociat este urmtorul:

S introducem n continuare notatia polonez prefixatasociat unei expresii aritmetice E: Definitie: fie E o expresie aritmetic.forma polonez prefixata "E" a lui E se obtine astfel: P1:Daca E=a,unde a este o Constanta sau o variabila, "E" =a . P2:Daca E=E1 op E2 atunci "E"=op "E1" "E2", pentru orice expresii aritmeticeE1 si E2. P3: Daca E=(E1) atunci "E"= "E1",pentru orice expresie aritmetica E1.

Pentru expresiile a+b, a-b, a*b, a/b, a^b notatiile poloneze sunt: +ab,-ab, ab, /ab, si respectiv ^ab. Pentru expresia E=ab+c/d-e forma poloneza prefixata este + *ab/cde.

Intr-adevar "a*b+c/d-e" = -"a*b+c/d" e = - + "a*b" "c/d" e = - + * ab / cd e. Sa observam ca daca parcurgem in preordine arborele binar din ultima figura obtinem forma poloneza prefixata asociata expresiei E.Acest lucru este adevarat pentru orice expresie arutmetica,deoarece modul in care se asociaza arborele binar complet prin regulile A1,A2,A3 este in stransa legatura cu modul in care se defineste forma poloneza prefixata prin regulile P1,P2,P3.Obtinem deci urmatorul rezultat:parcurgerea in preordine a unui arbore binar asociat unei expresii aritmetice E da forma poloneza prexita a expresie E. Observatii: forma poloneza asociata unei expresii aritmetice nu este unica; astfel : "a+b+c"= +"ab+c"= +a+bc dar si "a+b+c"= + "a+bc"= + +abc; in forma poloneza prefixata nu se mai folosesc paranteze,acestea nemaifiind necesare pentru a marca prioritatile de calcul; am definit forma poloneza prefixata pentru expresii aritmetice care folosesc numai operatori binari.Se pot accepta si operatori unari + si - ,dar mult mai simpla este asimilarea lor cu operatorii binari + si -.Astfel vom scrie 0+a in loc de +a si 0-a in loc de -a.

Modul in care se foloseste forma poloneza prefixata pentru evaluarea unei expresii aritmetice E este urmatorul: Se parcurge forma poloneza prefixata de la dreapta la stanga,utilizandu-se o stiva pentru memorarea operanzilor in rezerva.Daca simbolul curent nu este operand,el se introduce in stiva (evident, in varful ei).Daca simbolul citit este un operator,se aplica acest operator primilor doi operanzi din stiva,obtinandu-se un rezultat r;cei doi operanzi sunt eliminati din stiva si se adauga in stiva rezultatul r obtinut.Se trece apoi la prelucrarea urmatorului simbol din sirul care contine forma poloneza prefixata a expresiei aritmetice E. Acesta este un mod simplificat dar suficient de intuitiv de utilizare a formei poloneze prefixate pentru evaluarea expresiilor aritmetice.A reiesit ca in stiva se pun direct valorile operanzilor, ceec ce,pe de o parte este greu,pe de alta parte de multe ori este imposibil, deoarece, valorile unor operanzi nu se cunosc in faza de compilare,acestea urmand a fi citite in faza de executie. In realitate dupa generarea formei poloneze a unei expresii aritmetice,se genereaza o succesiune de instructiuni in cod masina care vor evalua expresia in faza de executie.pentru aceasta se foloseste de asemenea o stiva,dar in ea vor fi trecute

adresele operanzilor si nu valorile lor, pentru ca in succesiunea instructiunilor ce vor evalua expresia aritmetica se folosesc adresele operanzilor.pentru rezultatele intermediare se folosesc zone auxiliare de memorie. Ne propunem in continuare sa realizam un program ,care pornind de la o aritmetica oarecare,admisa,sa-i determine forma poloneza prefixata. Pentru aceasta vom asocia expresiei date arborele binar complet conform regulilor A1,A2,A3,pe care apoi il vom parcurge in preordine. expresie

Pentru construirea arborelui binar complet asociat,am folosit o serie de functii care reflecta definitia(recursiva) o unor expresii aritmetice pe baza unor componente mai simple: termeni,factori,subexpresii care contin operatorul de ridicare la putere.Vom prezenta mai jos un program care rezolva aceasta problema: # define NMAX 40 # include<stdio.h> # include<alloc.h> # include<fstream.h> typedef struct nod TNOD; typedef TNOD *ADR_TNOD; int i,n ; char c, sir[NMAX] ; void urmator(void) void factor (ADR_TNOD * r); void termen(ADR_TNOD * r); void putere(ADR_TNOD * r); void expr(ADR_TNOD * r) } void termen(ADR_TNOD *r) }

void factor (ADR_TNOD *r) } void putere (ADR_TNOD * r) } void afisare(ADR_TNOD r) } void main()

APLICATII
LISTE LINIARE SIMPLU INLANTUITE 1 Vom prezenta mai jos un program care exploateaza o lista liniara simplu inlantuita:

#include<stdio.h> #include<conio.h> #include<iostream.h> struct elem; void adaugare(elem * &prim,int inf) } elem * creare() } return prim; } elem * creare_prin_adaugare()

return prim; } void inserare_inainte(elem * &prim,int v,int inf) if(crt!=NULL) else } void inserare_dupa(elem * prim,int v,int inf) //parcurgem lista; if (crt!=NULL)//daca am gasit informatia

else } void stergere(elem * &prim,int v) if (crt!=NULL) //am gasit informatia

else } } void parcurgere(elem * prim) cout<<"\n"; getch(); }

void main() while(optiune<1&&optiune>8); switch(optiune)

case 2: case 3: case 4: case 5: case 6: case 7: } } while(optiune!=8); }


2 Scrieti un subprogram care primeste prin intermediul primului sau parametru a(de tip adresa) adresa primului element al unei liste liniare

simplu inlantuite,prin intermediul celui de-al doilea parametru n, primind o valoare nenula de cel mult 3 cifre.Subprogramul trebuie sa returneze prin cel de-al treilea parametru b(tot de tip adresa), adresa celui de-al n-lea element al listei sau adresa nula daca nu exista un asemenea element. # include<stdio.h> struct elem;

void lista(elem *a,int n,elem * &b) } void main () //listei lista(prim,3,nou); if (nou==NULL) printf("nu exista acest element"); else printf(" elementul este %d",nou->inf); } 3 Scrieti un subprogram care citeste de la tastatura 20 de numere si creeaza o lista simplu inlantuita prin inserarea succesiva a numerelor in lista astfel incat in final,lista sa contina numere aflate in ordine descrescatoare. Subprogramul va returna adresa de inceput a listei create. Se vor defini tipurile de date necesare si se va evita utilizarea altor structuri de date auxiliare. */ #include<stdio.h> struct elem ; elem *lista(void) return(s1); } void main (void) }

4 Scrieti un subprogram care elimina unul sau doua elemente(daca lista contine un numar par de noduri) din mijlocul unei liste.Subprogramul primeste ca parametru adresa de inceput a listei.Se vor defini si tipurile de date necesare subprogramului.*/ struct elem; #include<stdio.h> void list(elem *prim) } void stergere(elem *prim) if (i%2==0)

else

} void main (void) list(prim); printf("\n"); stergere(prim); list(prim); } 5 Scrieti declaratiile necesare pentru definirea unei liste dublu inlantuite,stiind ca un element al listei memoreaza ca informatie un numar natural de cel mult doua cifre.Stiind ca exista o lista dublu

inlantuita ale carei capete sunt marcate prin adresa nula,scrieti un subprogram care determina numarul de elemente ele listei.Subprogramul primeste printr-un parametru adresa unui element oarecare din lista si returneaza valoarea reprezentand numarul de elemente din lista. #include<stdio.h> struct elem; int nr(elem *crt)

aux=crt; while (aux->succ!=NULL)

return(nr1+nr2+1); } void main ()

ultim->succ=NULL; int k=nr(prim->succ->succ); printf("numarul de elemente este=%d",k); } 6 Scrieti declaratiile necesare pentrudefinirea unei liste simplu inlantuite,stiind ca un element al listei memoreza un caracter.Scrieti un subprogram care verifica daca exista doua pozitii

succesive intr-o lista circulara simplu inlantuita, pozitii care sa contina exact acelas caracter. Subprogramul primeste print-un parametru adresa unui element oarecare din lista si returneaza valoarea 1 daca exista doua caractere succesive identice in lista si 0 in caz contrar.*/ #include<stdio.h> struct elem; int exista (elem *prim)

while(pred !=prim); return 0; } void main (void)

crt->succ=prim; if(exista(prim)==1) printf("EXISTA DOUA LITERE SUCCESIVE IDENTICE"); else printf("NU EXISTA DOUA LITERE SUCCESIVE IDENTICE"); } 7 Sa se interclaseze doua liste alocate dinamic,care sunt ordonate crescator

si contin numere intregi. #include<iostream.h> #include<conio.h> typedef struct elem element; element* creare() else if(prim->val>info)

else

if(p1)

else

} } return prim; } void parcurgere (element * prim) cout<<"\n"; getch(); }

element* interclasare(element* element* plimbaret2) else

plimbaret1,

ultim=cap; while (plimbaret1 && plimbaret2)

if (plimbaret1->val> plimbaret2->val)

else

if(plimbaret1) ultim->next=plimbaret1; else ultim->next=plimbaret; return cap; } void main() 8 Sa se creeze un program care sa creeze si sa exploateze un arbore binar de cautare. #include<conio.h> #include<iostream.h> typedef struct nod NOD;

NOD *tns; void adaug(NOD * &r,int info) else if (r->info>info) adaug(r->s,info); else if (r->info<info) adaug(r->d,info); else cout<<"informatie duplicat"<<endl; } void creare(NOD * &r) } void inord(NOD *p) } void cauta(NOD *p,int x) NOD * detmin(NOD *p) return p; } NOD* cauta_sterge(NOD *pint x) else if (p->info>x)

else return p; } void sterge(NOD * &r,int x) else if (ns->d==ns->s)//nodul nu are descendenti;

else if(ns->d==NULL && ns->s)//nodul de sters are descendent stang

else
if(ns->s==NULL && ns->d)//nodul are descendent drept

else//nodul are doi descendenti;

else//minim are descendent drept

>info)

while(ns1->s && ns1->info<ns1->s-

} main() cout<<"\nDoriti sa cautati o informatie in arbore 1-da/0-nu?"; cin>>opt; while(opt)

getch(); } 9 Sa se inverseze legaturile intr-o lista liniara simplu inlantuita #include<iostream.h> #include<conio.h> typedef struct nodNOD; void creare(NOD* *p) } void parcurgere (NOD* prim) cout<<"\n"; getch(); } void inversare(NOD* &cap) cap=p; }

} void main(void) 10. Sa se realizeze reuniunea a doua multimi reprezentate ca liste liniare cu legaturi. Solutie: Dndu-se doua liste simplu nlantuite, pentru a determina reuniunea lor se considera toate elementele din a doua lista care nu se gasesc n prima lista si se insereaza n lista finala care este initializata cu elementele din prima lista. Listele se considera implementate fara header. Vom da n continuare functia care realizeaza lista de capat L ce reprezinta reuniunea elementelor listelor de capete L1 si L2. void Reuniune(celula *&L, celula *L1, celula *L2) // Prima lista este vida while(L2 != NULL) // Se parcurg elementele din a doua lista

else delete(k); // Element comun celor doua liste } } 11. Sa se realizeze intersectia a doua multimi reprezentate prin liste liniare simplu nlantuite. Solutie: Intersectia a doua multimi reprezentate ca liste simplu nlantuite se poate obtine parcurgnd a doua multime si, n cazul n care un element apartine primei liste se insereaza n lista L care reprezinta intersectia celor doua multimi si care se considera initial ca lista vida. n final se elibereaza elementele primei liste. void Intersectie(celula *&L, celula *L1, celula *L2) else delete(k); // Element comun celor doua liste }

while(L1 != NULL) // Eliminare elemente din prima lista

while(L2 != NULL) // Eliminare elemente din a doua lista

} 12. Fiind data o lista simplu nlantuita de numere ntregi, sa se creeze cu elementele ei doua liste ce contin elementele pare si respectiv impare ale ei. Solutie: Se parcurge lista initiala si n cazul n care elementul este par se insereaza n prima lista, iar daca este impar se insereaza n a doua lista. Functia care realizeaza aceasta este urmatoarea: void ValParImpar(celula *L, celula *&L1, celula *&L2) else } } 13. Sa se mparta elementele unei liste cu legaturi n doua liste, prima continnd elementele de ordin impar si cea de-a doua pe cele de ordin par. Solutie: Rezolvarea problemei presupune schimbarea legaturilor astfel: primul element din lista initiala va fi legat de al treilea, al doilea de al patralea etc. void PozParImpar(celula *L, celula *&L1, celula *&L2) p = L2 = L->leg; while(p != NULL)

p1->leg = NULL;

} 14. Sa se inverseze legaturile unei liste simplu nlantuite n asa fel nct primul element sa devina ultimul si reciproc. Solutie: Presupunem cazul listei reprezentate fara header. Rezolvarea problemei presupune folosirea a trei pointeri: p1, L, p3 care fac referire la trei elemente consecutive din lista; p1 si L se folosesc pentru inversarea legaturii, iar p3 va retine adresa capatului partii din lista initiala, nemodificata nca. void Inversare(celula *&L) L->leg = p1; } 15. Sa se elimine duplicatele dintr-o lista simplu nlantuita. Solutie: Pentru rezolvarea problemei se considera fiecare element al listei si se cauta n lista dupa acesta daca exista un alt element egal cu el caz n care se elimina din lista acest element. Se observa ca nu se schimba capatul listei. void Dubluri(celula *L) else p1 = k; p = p->leg; } }
16. Sa se creeze un arbore binar complet (plin) si sa se parcurga arborele n preordine, inordine si postordine, folosind pentru parcurgeri functii recursive. Solutie: Deoarece ntr-un arbore binar complet (plin) fiecare nod are exact doi descendenti, arborele are 2n-1 noduri, unde n reprezinta numarul de nivele al acestuia (nodul radacina al arborelui considerndu-se pe nivelul 1). De exemplu, un arbore binar complet cu 4 nivele poate arata astfel: 1

4 8 9 10 11 12 13 14 15

n acest caz, parcurgerea n preordine (radacina, descendent stng, descendent drept) nseamna vizitarea nodurilor 1, 2, 4, 8, 9, 5, 10, 11, 3, 6, 12, 13, 7, 14, 15 parcurgerea n inordine (descendent stng, radacina, descendent drept) nseamna 8, 4, 9, 2, 10, 5, 11, 1, 12, 6, 13, 3, 14, 7, 15 si parcurgerea n postordine (descendent stng, descendent drept, radacina) nseamna 8, 9, 4, 10, 11, 5, 2, 12, 13, 6, 14, 15, 7, 3, 1. Un program C++ ce rezolva problema poate fi: #include <iostream.h> #include <conio.h> #include <stdlib.h> #include <math.h> struct nod *rad; //arborele este dat de adresa celulei radacina int n; void CreareArb(nod *&rad); //nerecursiv, pe nivele void Preordine(nod *p); void Inordine(nod *p); void Postordine(nod *p); void StergeArb(nod *rad) void main()

void CreareArb(nod *&rad) //crearea se face pe nivele else

nv++; } } for (int j=pow(2,n-1);j<pow(2,n);j++) //ultimul nivel

} void Preordine(nod *p) } void Inordine(nod *p) } void Postordine(nod *p) } void StergeArb(nod *rad)//recursiv, in preordine

} 17. Sa se implementeze variantele nerecursive ale parcurgerii arborelui n preordine, inordine si postordine. Solutie: Variantele nerecursive folosesc o stiva si functiile pentru prelucrarea acesteia: int ns; //numarul de elemente efective din stiva nod *stiva[100]; void InitStiva() int StivaVida()

void InserareStiva(nod* elem) nod* VarfStiva() n acest caz, functiile nerecursive pentru parcurgerea n preordine si inordine pot fi: void PreordineNerecursiv(nod *p) if (StivaVida()) break; else

} } void InordineNerecursiv(nod *p) if (StivaVida()) break; else } } La parcurgerea n postordine, pentru ca trebuie parcursi att subarborele drept ct si cel drept nainte de a vizita nodul, trebuie retinut fiecare nod de doua ori n stiva: o data pentru a putea avea acces la legatura din stnga si a doua oara pentru legatura din dreapta. De aceea fiecarui nod introdus n stiva i se poate asocia o componenta a unui vector cu valoarea 0 daca a fost introdus n stiva o data sau 1 cnd este introdus n stiva a doua oara: int nr_retineri[100]; n acest caz, functia nerecursiva pentru parcurgerea arborelui n postordine poate fi: void PostordineNerecursiv(nod *p) while (!StivaVida())

else cout<<p->info<<" "; }

if (StivaVida()) break; } } 18. Sa se implementeze crearea recursiva a unui arbore binar. Solutie: Crearea recursiva a arborelui se poate face n preordine (nti se creeaza nodul, apoi recursiv descendentii), informatia nodurilor dndu-se n preordine. Pentru nodurile finale ale arborelui se poate da ca informatie un element din afara domeniului informatiei din nod (de exemplu, cnd nodurile arborelui sunt de tip ntreg se poate da un caracter, constanta NULL, etc.). n acest caz, functia C++ poate arata astfel: void CreareArb_Preordine(nod* &rad) else } 19. Sa se implementeze operatia de adaugare a unui nod si de cautare a unui element ntr-un arbore binar de cautare. Solutie: Cum arborele binar de cautare are proprietatea ca pentru fiecare nod neterminal al arborelui valoarea sa este mai mare dect valorile nodurilor din subarborele stng si mai mica sau egala dect valorile nodurilor din subarborele drept, atunci cautarea unui element se poate face recursiv astfel: int Exista(nod *rad, int elem) //verifica existenta elementului in arbore

else return 0; } Un nou nod se insereaza n arbore cautnd pozitia corespunzatoare si se insereaza ntotdeauna ca nod frunza. Astfel parcurgerea arborelui n inordine va duce la afisarea valorilor nodurilor n ordine crescatoare. int Adauga(nod *&rad, int elem)

else

if (elem<rad->info) return Adauga(rad->ls,elem); else return Adauga(rad->ld,elem); } nainte de apelul functiei trebuie facuta initializarea arborelui, care consta n executarea instructiunii rad=NULL. De exemplu: Initial rad=NULL Adauga(rad , 5) va conduce la arborele: 5 Adauga(rad , 9) va conduce la arborele: 5 9 Adauga(rad , 7) va conduce la arborele: 5 9 7 Adauga(rad , 3) va conduce la arborele: 3 7 Adauga(rad , 8) va conduce la arborele: 5 9 5

3 7 8

Adauga(rad , 12) va conduce la arborele: 3 7 8 9 12

etc.

20. (Problema lui Josephas) Se da o lista cu elementele 1, 2, ., n. ncepnd de la primul element, cu un pas dat sa se afiseze si sa se elimine toate elementele listei. Solutie: Se foloseste o implementare cu lista circulara simplu nlantuita fara header, primul element fiind la adresa data de pointerul L si ultimul element al listei fiind legat la adresa data de pointerul u. Functia care rezolva problema este data n continuare:

L x1 . xn-1 xn

void Josephas(celula *L, int pas) while(p->leg != L) p = p->leg; cout << "Elementele sunt urmatoarele: "; while(L->leg != L)

k = L; p->leg = L = k->leg; cout << k->info; delete(k); } cout << L->info; delete(L); } 21. Sa se implementeze operatiile cu liste dublu nlantuite.

Solutie: Se foloseste o reprezentare a listei fara header si spre deosebire de listele simplu nlantuite aici exista doua legaturi ale unei celule: una spre celula din stnga (anterioara) si una spre celula din dreapta (urmatoare) L x1 NULL . . xn-1 xn NULL

n acest caz, structura unei celule este: struct celula *L; Codul C++ pentru initializarea listei poate fi: void Initializare() Inserarea unui nou element a la nceputul listei se poate face astfel: void InserareInc(int a) Inserarea unui nou element a dupa al m-lea elemnt din lista sau la sfrsitul listei, daca m este mai mare dect numarul de elemente din lista se poate face astfel: void InserareInt(celula *&L, int a, int m) k->legs = s; k->legd = d; if(s != NULL) s->legd = k; else L = k; if(d != NULL) d->legs = k; } Inserarea unui nou element a la sfarsitul listei se poate face astfel: void InserareSf(celula *&L, int a) while(s->legd != NULL) s = s->legd; s->legd = k; k->legs = s; } Eliminarea primului element din lista se poate face astfel: void StergereInc(celula *&L, int &a) a = k->info; L = L->legd;

if(L != NULL) L->legd = NULL; delete(k); } Eliminarea elementului de ordin m din lista se poate face astfel: void StergereInt(celula *&L, int &a, int m) if(m) cout << "Nu exista element de ordinul dat"; else

} Eliminarea ultimului element din lista se poate face astfel: void StergereSf(celula *&L, int &a) a = k->info; if(s != NULL) s->legd = NULL; else L = NULL; delete(k); } 22. Sa se implementeze operatiile elementare (initializare, inserare element si eliminare element) pentru o stiva reprezentata secvential. Solutie: Stiva este data de vectorul elementelor si de pozitia vrfului stivei care indica primului loc liber din vector, aici facndu-se inserarea unui element si fiind, de fapt, egal cu numarul de elemente din lista. #define M 50 //dimensiunea maxima a vectorului int V; //pozitia varfului stivei in vector int x[M]; //vectorul elementelor 0 1 . V-1 V . M-1 x0 x1 . xV-1 .

Operatia de initializare presupune doar initializarea pozitiei vrfului cu valoarea 0 (prima pozitie libera din vector, care n C++ este 0). void InitializareStiva() V=0 . M-1

Introducerea unui element a n stiva se face astfel: pe pozitia V se pune noul element si apoi se trece vrfului pe pozita urmatoare. void InserareElem(int a) Eliminarea unui element a din stiva consta n pozitionarea vrfului stivei pe elementul anterior si preluarea valorii lui. void StergereElem(int &a) 23. Sa se implementeze operatiile elementare (initializare, introducere element, eliminare element, afisarea elementelor stivei si stergerea tuturor celulelor) pentru o stiva cu reprezentare cu legaturi simple. Solutie: O reprezentare grafica a implementarii dinamice a stivei este:

V x1

x2

xn

NULL

Structura celulelor stivei este cea a listelor liniare simplu nlantuite: struct celula *V; unde V este vrful stivei. Operatia de initializare se face cu functia: Void Initializare() Operatia de introducere a unui element a n stiva presupune crearea unei noi celule n care se pune noul element, legarea acestuia de fostul vrf al stivei si mutarea vrfului stivei pe noul elenment:

V NULL k

x1

x2

xn

a void Inserare(int a) Eliminarea unui element din stiva, adica stergerea vrfului stivei, presupune pastrarea adresei celulei din vrf ntr-un pointer k, mutarea vrfului stivei pe urmatorul element din stiva si eliberarea celulei de la adresa data de k, dupa ce i s-a retinut valoarea. void Stergere(int &a) else cout << "Stiva vida"; } Afisarea elementelor stivei se face prin parcurgerea listei de la vrful spre baza: void Listare() } } Eliminarea celulelor din stiva se face parcurgnd stiva de la vrf la baza: void StergeStiva() } 24. Sa se calculeze nerecursiv valoarea functiei Manna-Pnueli pentru o valoare ntreaga data a argumentului x. Solutie: Functia Manna-Pnueli este definita n felul urmator:

De exemplu, pentru x = 8: f(8) = f(f(10)) = f(f(f(12))) = f(f(11)) = f(f(f(13))) = = f(f(12)) = f(11) = f(f(13)) = f (12) = 11. Pentru calculul valorii functiei nerecursiv, se poate folosi o stiva, n care initial se pune valoarea lui x, n exemplul de mai sus 8. Daca vrful stivei este mai mic dect 12, se adauga n stiva elementul cu 2 unitati mai mare dect acesta; altfel, daca stiva are mai mult de un element se scot doua elemente din stiva; se pune n stiva elementul mai mic cu o unitate dect vechiul vrf al stivei. Procesul se termina cnd stiva devine vida, rezultatul calculat fiind ultimul element extras mai putin cu o unitate.

12 10 8 8 10 8 11 8 11 8 12 8 11 13 11 12 => f (8) = 12 - 1 =11.

13

Un program C++ ce rezolva problema poate fi urmatorul: # include <iostream.h> # include <conio.h> struct celula *V; void Initializare(); void Inserare(int a); void Stergere(int *a); void Listare(); void main() else

else

} } while(1); } void Initializare() void Inserare(int x) void Stergere(int *a) // Se presupune stiva nevida

void Listare() } cout << endl; getch(); } 25. Sa se implementeze operatiile elementare pe o coada reprezentata nlantuit. Solutie: Implemetarea nlantuita a cozilor se poate face folosind o lista cu header si doi pointeri: pointerul V catre header si pointerul B catre ultima celula efectiva din lista. Insearea unui nou element se face dupa celula la care se refera B (la sfrsitul cozii) si eliminarea se face la prima celula efectiva de dupa header. n acest caz structura listei este: struct celula *V,*B; iar functiile de initializarea cozii, de inserare si stergere a unui element n/din coada, respectiv de stergere a cozii se pot face astfel: void Initializare() void Inserare(int a) void Stergere(int &a) } void Listare() } } void StergeCoada() delete(V); }

S-ar putea să vă placă și