Arbori Arborii, ca şi listele, sunt structuri dinamice. Elementele structurale ale unui arbore sunt noduri şi arce orientate care unesc nodurile. Deci, în fond, un arbore este un graf orientat degenerat. Există mai multe tipuri de arbori, dar de o importanţă deosebită sunt arborii binari. Aceştia se caracterizează prin faptul că fiecare nod poate avea doar două noduri copil, rezultând o structură ca în fig. 1, în care prin cercuri sunt reprezentate nodurile, iar prin săgeţi arcele. Primul nod din arbore se numeşte nod rădăcină, iar nodurile finale pe fiecare ramură a arborelui se numesc noduri frunze. Din cele arătate mai sus rezultă că un nod este descris de structura de mai jos: struct ANOD int val; ANOD* st; ANOD* dr; ; unde val este valoarea conţinută de nodul respectiv, st este un pointer spre fiul stâng al nodului respectiv (NULL dacă nu există nici un nod fiu stâng) şi dr este un pointer spre fiul stâng al nodului respectiv (NULL dacă nu există nici un nod fiu drept). Pentru a pregăti crearea unui arbore, vom scrie următoarea secvenţă de program: #include <stdio.h> #include <conio.h> struct ANOD int val; ANOD* st; ANOD* dr; void main(void) clrscr(); ANOD* radacina=null; Figura 1 Prin aceasta, am declarat un pointer care va pointa spre rădăcina arborelui. Cum în acest stadiu arborele nu are nici un nod, acest pointer va fi NULL. Similar listelor, asupra arborilor se pot defini mai multe operaţii, cum ar fi: - inserarea unui nod în arbore; - ştergerea unui nod frunză într-un arbore; - parcurgerea arborelui; - căutarea unui nod din arbore. 1
Operaţiile de inserare şi căutare vor fi făcute pe baza unui criteriu, care precizează poziţia în arbore (stânga, dreapta) şi valoarea nodului la care se adaugă nodul fiu, respectiv direcţia de căutare în arbore a nodului dorit. În general acest criteriu nu este unic şi depinde de modul în care utilizatorul doreşte să implementeze arborele. În exemplul nostru, se propune o funcţie criteriu cu implementarea de mai jos: int criteriu(anod* p1, ANOD* p2) if (p1->val > p2->val) return -1; if (p1->val < p2->val) return 1; return 0; Funcţia primeşte ca argumente doi parametri de tip nod şi returnează următoarele valori: - -1 dacă valoarea primului parametru este mai mare decât valoarea celui de al doilea parametru; - +1 dacă valoarea primului parametru este mai mică decât valoarea celui de al doilea parametru; - 0 dacă cele două valori sunt egale; În funcţie de valoarea returnată, se vor utiliza pointerii spre stânga sau spre dreapta în operaţia de identificare a nodului copil care urmează să fie procesat. În cazul nostru, funcţia este utilizată astfel: - dacă valoarea nodului părinte este mai mare decât valoarea nodului copil, parcurgerea arborelui, respectiv inserarea nodului frunză, se va face spre stânga; - dacă valoarea nodului părinte este mai mică decât valoarea nodului copil, parcurgerea arborelui, respectiv inserarea nodului frunză, se va face spre dreapta; - dacă valorile celor două noduri sunt egale, nu se inserează nici un nod, pentru că un nod de valoarea dată există deja pe această ramură; Inserarea unui nod în arbore Pentru inserarea unui nod în arbore vom implementa următoare funcţie: ANOD* insnod(anod* st, int vl) ANOD *p, *q; p=new ANOD; p->val=vl; p->st=null; p->dr=null; if (st==null) // arbore gol! st=p; else q=st; for(;;) // se insereaza in subarborele sting if(criteriu(q,p)<0) 2
if (q->st==null) q->st=p; break; else q=q->st; // cauta pina ajunge la frunza continue; // se insereaza in subarborele drept if(criteriu(q,p)>0) if (q->dr==null) q->dr=p; break; else q=q->dr; continue; return st; Funcţia primeşte ca şi argumente adresa nodului rădăcină şi valoarea ataşată nodului ce urmează să fie inserat. Ea trebuie să returneze un pointer ANOD* pentru cazul în care se inserează chiar nodul rădăcină (arborele este vid înainte de inserare). Funcţia efectuează următoarele: - creează un nou nod, căruia îi atribuie valoarea primită ca şi argument, iar pointerii st şi dr sunt încărcaţi cu valoarea NULL; - testează dacă arborele vid, declară noul nod ca fiind rădăcină şi îi returnează adresa; - dacă arborele nu este vid, pornind de la rădăcină, se deplasează în arbore respectând criteriul, pentru a ajunge la nodul părinte al nodului nou inserat. Astfel, pentru fiecare nod se execută următoarele: o dacă criteriu()=-1, se face nod current nodul copil stâng al nodului curent; o dacă criteriu()=1, se face nod current nodul copil stâng al nodului curent; o dacă a ajuns la frunză (st sau dr = NULL), inserează noul nod ca şi copil stâng sau ca şi copil drept, în funcţie de valoarea criteriului; - este demn de remarcat modul de implementare a buclelor, prin intermediul instrucţiunilor continue şi break. În timp ce continue sparge doar blocurile if, rămânând în ciclul for, astfel încât de fiecare dată, în funcţie de criteriu, se trece 3
la un nod copil, break, care este apelat când se ajunge la nodul părinte al nodului nou inserat, sparge şi ciclul for, având ca efect terminarea funcţiei. Dacă vom insera în program liniile de mai jos, vom construi un arbore ca în figura 2. radacina=insnod(radacina,); radacina=insnod(radacina,); radacina=insnod(radacina,); radacina=insnod(radacina,4); radacina=insnod(radacina,7); radacina=insnod(radacina,40); Să vedem de ce s-a obţinut această structură: - apelul funcţiei pentru valoarea găseşte arborele 4 7 40 vid. Deci, se creează un nou nod, căruia i se atribuie valoarea, pentru care se fac pointerii spre nodurile Figura 2 copil NULL şi care este declarat nod rădăcină (fig. 3.1); - apelul funcţiei cu valoarea, creează un nou nod, după care pornind de la rădăcină, caută poziţia în care să-l insereze. Cum valoarea nodului este mai mare decât valoarea rădăcinii, criteriu()>0 şi arborele va fi parcurs spre dreapta. Datorită faptului că pointerul dr al nodului rădăcină este NULL, acesta este încărcat cu adresa noului nod, noul nod va fi inserat ca fiu drept al acestuia (fig 3.2); - apelul funcţiei cu valoarea, creează un nou nod, după care pornind de la rădăcină, caută poziţia în care să-l insereze. Cum valoarea nodului este mai mică decât valoarea rădăcinii, criteriu()<0 şi arborele va fi parcurs spre stânga. Datorită faptului că pointerul st al nodului rădăcină este NULL, acesta este încărcat cu adresa noului nod, noul nod va fi inserat ca fiu stâng al acestuia (fig 3.3); - apelul funcţiei cu valoarea 4, creează un nou nod, după care pornind de la rădăcină, caută poziţia în care să-l insereze. Cum valoarea nodului este mai mică decât valoarea rădăcinii, criteriu()<0 şi arborele va fi parcurs spre stânga, trecându-se la nodul cu valoare. În acest nod, din nou criteriu()<0 şi arborele va fi parcurs spre stânga. Datorită faptului că pointerul st al nodului cu valoare este NULL, acesta este încărcat cu adresa noului nod, noul nod va fi inserat ca fiu stâng al nodului de valoare (fig 3.4); - apelul funcţiei cu valoarea 7, creează un nou nod, după care pornind de la rădăcină, caută poziţia în care să-l insereze. Cum valoarea nodului este mai mică decât valoarea rădăcinii, criteriu()<0 şi arborele va fi parcurs spre stânga, trecându-se la nodul cu valoare. În acest nod, criteriu()>0 şi arborele va fi parcurs spre dreapta. Datorită faptului că pointerul dr al nodului cu valoare este NULL, acesta este încărcat cu adresa noului nod, noul nod va fi inserat ca fiu drept al nodului de valoare (fig 3.); - apelul funcţiei cu valoarea 40, creează un nou nod, după care pornind de la rădăcină, caută poziţia în care să-l insereze. Cum valoarea nodului este mai mare decât valoarea rădăcinii, criteriu()>0 şi arborele va fi parcurs spre dreapta, 4
trecându-se la nodul cu valoare. În acest nod, criteriu()>0 şi arborele va fi parcurs spre dreapta. Datorită faptului că pointerul dr al nodului cu valoare este NULL, acesta este încărcat cu adresa noului nod, noul nod va fi inserat ca fiu drept al nodului de valoare (fig 3.6); 1 2 3 4 6 4 4 7 4 7 40 Parcurgerea unui arbore Pentru listarea valorii unui nod se implementează funcţia: void afiseaza(anod* p) printf(" %d ",p->val); Pentru prelucrarea informaţiei conţinută de arbore, sau pentru listarea conţinutului acestuia, trebuire realizată parcurgerea arborelui. Parcurgerea unui arbore se poate face în mai multe moduri, dintre care, trei sunt demne de reţinut: - în preordine înseamnă accesul la rădăcină şi apoi parcurgerea celor doi subarbori ai ei, întâi subarborele stâng, apoi cel drept. Subarborii, care sunt la rândul lor arbori binari, se parcurg în acelaşi mod. Funcţia care implementează parcurgerea în preordine a arborelui şi afişarea conţinutului nodului este: void preord(anod* p) if (p!=null) afiseaza(p); preord(p->st); preord(p->dr); În programul principal se va adăuga linia printf("\n Preordine : "); preord(radacina); Dacă se compilează şi se execută programul, se va obţine rezultatul:
Preordine : 4 7 40 Să verificăm: o se afişează valoarea rădăcinii () şi se parcurge întâi subarborele drept; o se trec la nodul de valoare, care este noua rădăcină pentru subarborele de sub el. Se parcurge subarborele stâng; o noua rădăcină este nodul de valoare 4. Acesta nu mai are nici o frunză, deci, conform algoritmului, se parcurge subarborele drept al subarborelui cu rădăcina. Deci, va fi afişat nodul de valoare 7; o cum acest nod nu are nici o frunză, se parcurge subarborele drept pornind din precedenta rădăcină (nodul de valoare ), deci se trece la nodul de valoare ; o cum acest nod nu are subarbore stâng, se parcurge subarborele drept, trecându-se la nodul de valoare 40; o nu mai există noduri neparcurse, deci algoritmul se opreşte; - în inordine înseamnă întâi parcurgerea subarborelui stâng, apoi accesul la rădăcină şi apoi parcurgerea subarborelui drept. Cei doi subarbori se parcurg în acelaşi mod. Funcţia care implementează parcurgerea în inordine a arborelui şi afişarea conţinutului nodului este: void inord(anod* p) if (p!=null) inord(p->st); afiseaza(p); inord(p->dr); În programul principal se va adăuga linia printf("\n Inordine : "); inord(radacina); Dacă se compilează şi se execută programul, se va obţine rezultatul: Inordine : 4 7 40 Să verificăm: o se porneşte la parcurgerea arborelui stâng, de la cel mai din stânga nod. Acesta este nodul de valoare 4; o se trece la rădpcina subarborelui, deci în cazul nostru la nodul de valoare ; o conform algoritmului, se parcurge subarborele drept al subarborelui. În cazul nostru, se trece la nodul de valoare 7; o cum nu mai există noduri neparcurse în subarbore, se continuă parcurgerea subarborelui stîng. Nodul al cărui copil stâng este nodul de valoare este 6
nodul de valoare. Acesta este noua rădăcină şi fiind chiar rădăcina arborelui, se trece la parcurgerea subarborelui drept al acestuia. o ar urma reluarea parcurgerii subarborelui stâng al subarborelui drept, pornind de la frunza cea mai din stânga. Cum nu există copii stângi pentru nici un nod, se parcurg doar subarborii din dreapta, deci nodurile şi 40; - în postordine înseamnă parcurgerea subarborelui stâng, al subarborelui drept şi apoi accesul la rădăcină. Cei doi subarbori se parcurg în acelaşi mod. Funcţia care implementează parcurgerea în postordine a arborelui şi afişarea conţinutului nodului este: void postord(anod* p) if (p!=null) postord(p->st); postord(p->dr); afiseaza(p); În programul principal se va adăuga linia printf("\n Postordine : "); postord(radacina); Dacă se compilează şi se execută programul, se va obţine rezultatul: Postordine : 4 7 40 Să verificăm: o se parcurge subarborele stâng pornind de la nodul cel mai din stânga, adică 4; o se parcurge subarborele drept al subarborelui, adică nodul 7; o se accesează rădăcina subarborelui, adică nodul ; o se parcurge subarborele drept al părintelui nodului. Cum acesta nu conţine nici un arbore stâng, se parcurge pornind de la nodul cel mai din dreapta, adică 40, ; o se accesează rădăcina celor doi subarbori, adică nodul ; o cum aceasta este rădăcina arborelui, algoritmul se termină; Ştergerea arborelui Ştergerea unui arbore implică parcurgerea arborelui în postordine cu ştergerea fiecărui nod în parte. Vom implementa următoarele funcţii: void sterg(anod* p) if (p!=null) 7
sterg(p->st); sterg(p->dr); delete p; ANOD* stergarb(anod* p) sterg(p); p=null; return p; Dacă în programul principal se vor adăuga liniile printf("\n\n Afisare valoare radacina dupa stergere arborelui: "); afiseaza(radacina); se va obţine rezultatul Afisare valoare radacina dupa stergere arborelui: NULL 8