APLICAŢII ELEMENTARE CU ARBORI I. CONSIDERAŢII TEORETICE Din punct de vedere etimologic termenul de arbore a fost introdus de către matematicianul Arthur Cayley în 1857, plecând de la o analogie botanică. Structurile arborescente reprezintă structuri neliniare de date cu aplicaţii în programare, care exprimă relaţii de ramificare între noduri, asemănătoare configuraţiei arborilor din natură, cu deosebirea că în informatică arborii cresc în jos (au rădăcină în vârf). Se numeşte arbore un graf conex şi fără cicluri. REPREZENTAREA ARBORILOR CU RĂDĂCINĂ Prin structuri de date înlănţuite. Reprezentarea fiecărui nod al arborilor printr-un obiect. Pointerii către celelalte noduri pot varia în funcţie de tipul arborelui. Arbori binari Definiţie: Un arbore binar este o mulţime finită de noduri care este fie vidă, fie reprezintă un arbore ordonat în care fiecare nod are cel mult doi descendenţi (stâng şi drept). Un nod fără descendenţi se numeşte nod terminal sau frunză. Un arbore binar în care fiecare nod care nu este terminal are exact doi descendenţi se numeşte arbore binar complet. Un arbore binar complet care are n noduri terminale, toate situate pe acelaşi nivel, are în total 2n-1 noduri. Reprezentarea standard presupune memorarea pointerilor pentru părinte, pentru descendentul stâng şi descendentul drept al fiecărui nod din arbore. Dacă rădăcină(t) = NIL (sau NULL) arborele este vid. Rădăcină se află pe nivelul 0 al arborelui. Reprezentarea prin vector de TAŢI şi eventual prin vector de DESCENDENŢI. Vectorul TATĂ precizează pentru fiecare vârf i nodul TATĂ[i], care reprezintă părintele său în arbore (rădăcina subarborelui de care aparţine). Vectorul DESCENDENŢI indică prin valoarea -1 sau +1 dacă vârful i este descendentul stâng sau drept al părintelui său TATĂ[i]. Pentru rădăcina arborelui TATĂ[rădăcină]=0 şi DESCENDENŢI[rădăcină]=0. Arbori cu rădăcină cu număr nelimitat de ramuri Schema reprezentării arborilor binari poate fi extinsă la orice clasă de arbori în care numărul de descendenţi ai fiecărui nod nu depăşeşte o constantă k (din motive de alocare şi reprezentare). Dacă numărul descendenţilor este mărginit de o constantă mare şi majoritatea nodurilor au un număr mic de descendenţi se va irosi multă memorie. Reprezentarea descendent-stâng, frate-drept - schemă "inteligentă" pentru folosirea arborilor binari la reprezentarea arborilor cu număr arbitrar de descendenţi. Utilizează un spaţiu de O(n) pentru orice arbore cu rădăcină şi cu n noduri. Fiecare nod conţine un pointer spre părinte p. Nu există decât doi pointeri spre descendenţi: fiu-stâng[x] referă cel mai din stânga descendent al nodului x şi frate-drepţ[x] referă fratele lui x, cel mai apropiat spre dreapta. Dacă nodul x nu are descendenţi atunci fiu-stâng[x] = NIL, iar dacă nodul x este cel mai din dreapta descendent al părintelui său atunci frate-drept[x] = NIL.
Alte reprezentări ale arborilor Prin ansambluri (heap - arbore binar complet într-un singur tablou plus un indice. Prin păstrarea doar a pointerilor spre părinţi; nu există pointeri spre descendenţi. Arborii sunt traversaţi numai spre rădăcină. ARBORI BINARI DE CĂUTARE Sunt structuri de date înlănţuite (în care fiecare nod este un obiect) organizate sub formă de arbori binari ce posedă multe operaţii specifice mulţimilor dinamice (CAUTĂ, MINIM, MAXIM, PREDECESOR, SUCCESOR, lnserează şi ŞTERGE). Operaţiile de bază pe arborii binari consumă un timp proporţional cu înălţimea arborelui. Pentru un arbore binar complet cu n noduri, aceste operaţii se execută în cazul cel mai defavorabil întrun timp O(lgn). Înălţimea unui arbore binar de căutare construit aleator este O(lgn). Ce este un arbore binar de căutare? Fiecare obiect (nod al arborelui binar de căutare) conţine câmpurile: cheie şi date adiţionale. Subarbore stâng, subarbore drept şi părinte care referă spre nodurile corespunzătoare fiului stâng, fiului drept şi respectiv părintelui nodului. Dacă un fiu sau un părinte lipseşte, câmpul corespunzător acestuia va conţine valoarea NIL. Nodul rădăcină este singurul nod din arbore care are valoarea NIL pentru câmpul părinte p. Cheile sunt întotdeauna astfel memorate încât ele satisfac proprietatea arborelui binar de căutare: cheia oricărui nod din subarborele stâng este mai mică sau egală decât cheia din rădăcină care este la rândul ei mai mică sau egală decât cheia oricărui nod din subarborele drept. Proprietatea arborelui binar de căutare permite tipărirea tuturor cheilor în ordine crescătoare folosind un algoritm recursiv simplu (traversarea arborelui în in-ordine - cheia rădăcinii unui arbore se tipăreşte între valorile din subarborele său stâng şi cele din subarborele său drept). Există alte două tipuri de parcurgeri ale arborilor binari de căutare: pre-ordine şi post-ordine. ARBORE-TRAVERSARE-INORDINE(x) 1: dacă x NIL atunci 2: ARBORE-TRAVERSARE-INORDINE(stânga[x]) 3: afişează cheie[x] 4: ARBORE-TRAVERSARE-INORDINE(dreapta[x]) Căutare recursivă - în arbore binar de căutare Folosită la căutarea unui nod având cheia cunoscută, intr-un arbore binar de căutare. Se cunosşte pointerul x la rădăcina arborelui şi valoarea k a cheii căutate. ARBORE-CAUTĂ returneazâ un pointer la nodul având cheia k dacă există un astfel de nod în arbore sau NIL în caz contrar. ARBORE-CAUTĂ(x, k) 1: dacă x = NIL sau k = cheie[x] atunci 2: returnează x 3: dacă k < cheie[x] atunci 4: returnează ARBORE-CAUTĂ(stânga[x], k) 5: altfel
6: returnează ARBORE-CAUTĂ(dreapta[x], k) Căutare iterativă - în arbore binar de căutare ARBORE-CAUTĂ-ITERATIV(x, k) 1: cât timp x NIL şi k cheie[x] execută 2: dacă k < cheie[x] atunci 3: x stânga[x] 4: altfel 5: x dreapta[x] 6: returnează x Minimul şi maximul Determinarea elementului având cheia minimă dintr-un arbore binar de căutare se realizează întotdeauna urmând pointerii fiu stânga începând cu rădăcina şi terminând când se întâlneşte NIL. Pentru determinarea elementului de cheie maximă se procedează simetric. ARBORE-MINIM(x) 1: cât timp stânga[x] NIL execută 2: x stânga[x] 3: returnează x ARBORE-MAXIM(x) 1: cât timp dreapta[x] NIL execută 2: x dreapta[x] 3: returnează x Succesorul şi predecesorul unui nod Cunoscând un nod dintr-un arbore binar de căutare, este importantă uneori determinarea succesorului său în ordinea de sortare determinată de traversarea în inordine a arborelui (în cazul ştergerii din arbore a unui nod având ambii fii, pentru realizarea legăturilor). Dacă toate cheile sunt distincte, succesorul nodului x este nodul având cea mai mică cheie mai mare decât cheie[x]. Structura de arbore binar de căutare permite determinarea succesorului unui nod chiar şi fără compararea cheilor. ARBORE-SUCCESOR tratează două alternative: dacă subarborele drept al nodului x nu este vid, atunci succesorul lui x este chiar cel mai din stânga nod din acest subarbore drept. În celălalt caz, când subarborele drept al nodului x este vid şi x are un succesor y, atunci y este cel mai de jos strămoş al lui x al cărui fiu din stânga este de asemenea strămoş al lui x. ARBORE-SUCCESOR(x) 1: dacă dreapta[x] NIL atunci 2: returnează ARBORE-MINIM(dreapta[x]) 3: y p[x] 4: cât timp y NIL şi x = dreapta[y] execută 5: x y 6: y p[y] 7: returnează y
Teorema: Pe un arbore binar de căutare de înălţime h, operaţiile pe mulţimi dinamice CAUTĂ, MINIM, MAXIM, SUCCESOR şi PREDECESOR se pot executa într-un timp O(h). Inserarea şi ştergerea Provoacă modificarea mulţimii dinamice reprezentată de arborele binar de căutare. Structura de date trebuie modificată în sensul că ea trebuie pe de o parte să reflecte inserarea sau ştergerea, iar pe de altă parte să conserve proprietatea arborelui binar de căutare. Inserarea unui element nou - relativ simplă, pe când gestiunea ştergerii unui element este mai complicată. Inserarea ARBORE-INSEREAZĂ primeşte ca parametrii arborele binar de căutare T şi nodul z pentru care cheie[z] = v (valoarea de inserat), stânga[z] = NIL şi dreapta[z] = NIL. ARBORE-INSEREAZĂ(T, z) 1: y NIL 2: x rădăcină[t] 3: cât timp x NIL execută 4: y x 5: dacă cheie[z] < cheie[x] atunci 6: x stânga[x] 7: altfel 8: x dreapta[x] 9: p[z] y 10: dacă y = NIL atunci 11: rădăcină[t] z t2: altfel dacă cheie[z] < cheie[y] atunci 13: stânga[y] z 14: altfel 15: dreapta[y] z Ştergerea ARBORE-ŞTERGE primeşte ca argument un pointer la z (nodul care va fi şters). Distingem trei situaţii: Dacă z nu are fii, se va modifica părintele său p[z] pentru a-i înlocui fiul z cu NIL. Dacă nodul are un singur fiu, z va fi eliminat din arbore prin inserarea unei legături de la părintele lui z la fiul lui z. Dacă nodul are doi fii, se va elimina din arbore succesorul y al lui z, care nu are fiu stâng şi apoi se vor înlocui cheia şi datele adiţionale ale lui z cu cheia şi datele adiţionale ale lui y. ARBORE-ŞTERGE(T, z) 1: dacă stânga[z] = NIL sau dreapta[z] = NIL atunci 2: y z 3: altfel 4: y ARBORE-SUCCESOR(z) 5: dacă stânga[y] NIL atunci
6: x stânga[y] 7: altfel 8: x dreapta[y] 9: dacă x NIL atunci 10: p[x] p[y] 11: dacă p[y] = NIL atunci 12: rădăcină[t] x 13: altfel dacă y =stânga[p[y]] atunci 14: stânga[p[y]] x 15: altfel 16: dreapta[p[y]] x 17: dacă y z atunci 18: cheie[z] cheie[y] se copiază şi datele adiţionale ale lui y 19: returnează y II. APLICAŢII II.1. Scrieţi un program care foloseşte o abordare recursivă şi alocare dinamică pentru descrierea operaţiilor aferente arborilor binari. a) Creare arbore. b) Calcularea numărului de noduri (chei din arbore). c) Ştergerea întregului arbore. d) Căutarea unui element în arbore. e) Parcurgerea arborelui în preordine (SDR), inordine (SRD), postordine (RSD) f) Determinarea elementului minim şi maxim din arbore. g) Suma elementelor pare din arbore. h) Afişarea tuturor numerelor prime din arbore. i) Listarea nodurilor aflate pe un anumit nivel în arbore. j) Determinarea adâncimii maxime a unui arbore binar (adică al numărului de nivel maxim asociat nodurilor terminale) şi a elementelor de pe calea respectivă. k) Verificarea dacă un arbore primit ca parametru reprezintă un arbore binar de căutare (S<R<D). II.2. Scrieţi un program care implementează operaţiile aferente arborilor binari de căutare (CAUTĂ, MINIM, MAXIM, PREDECESOR, SUCCESOR, lnserează şi ŞTERGE) folosind funcţiile descrise în prezentul document. II.3. Scrieţi un program care pornind de la un graf neorientat determină dacă este arbore. Abordare cu STL (şablonul queue <int>q;). Se citeşte dintr-un fişier pe câte o linie separat numărul de noduri şi perechile de noduri între care exista o muchie. Practic se verifică întâi dacă numărul de noduri este cu unul mai mult decât numărul de muchii (condiţie necesară dar nu şi suficientă). Dacă numărul de muchii este mai mare sau egal decât numărul de muchii înseamnă că sigur există un ciclu în graf, motiv pentru care graful nu este arbore. Condiţia de suficienţă este realizată dacă graful este conex. Acest lucru se verifică prin parcurgerea grafului în lăţime: se introduce într-o coadă primul nod şi de la acesta se parcurg muchiile incidente cu alte noduri. Nodurile adiacente (vizitate) se introduc în coadă pentru a fi parcurse muchiile incidente acestora altele decât cele anterior parcurse. După analiza unui nod (verificarea adiacenţei cu toate celelalte noduri) acesta se
scoate din coadă. Dacă se goleşte coada se verifică dacă au fost vizitate toate nodurile caz în care graful este conex, şi ajungând în acest punct (nu conţine cicluri) el este arbore. Se consideră structura de date şi prototipurile următoarelor funcţii: struct nod{ int inf; struct nod *st,*dr; }*cap,*p; struct nod *creare(void); crearea recursivă a arborelui: întâi informația curentă, apoi crearea subarborelui stâng şi în final a celui drept. struct nod *creare(void){ struct nod *p; printf("\nintroduceti informatia: "); p=(struct nod*)malloc(sizeof(struct nod)); scanf("%d",&p >inf); p >st=p >dr=null; printf("\n\ndoriti introducerea subarborelui stang(d/n)? "); if(getche()!='n')p >st=creare(); puts("\n\ndoriti introducerea subarborelui drept(d/n)? "); if(getche()!='n')p >dr=creare(); return(p); } void parcurgere_preord(struct nod *p); parcurgerea în preordine a arborelui: întâi se afişează / analizează rădăcina, apoi subarborele stâng apoi cel drept. void parcurgere_inordine(struct nod *p); parcurgerea în ordine a arborelui: întâi se afişează / analizează subarborele stâng, apoi rădăcina şi în final subarborele drept. void parcurgere_postordine(struct nod *p); parcurgerea în postordine a arborelui: întâi se afişează / analizează subarborele stâng, apoi cel drept şi în final rădăcina. int numar_elemente(struct nod *p); numărul de elemente al unui arbore este egal cu 1 + numărul de elemente al subarborelui stâng + numărul de elemente al subarborelui drept. void citestema(int[100][100],int &); citirea din fişier a listei de muchii şi transformarea în matrice de adiacență şi obținerea numărului de noduri al grafului.
De asemenea, este necesară o variabilă ajutătoare declarată global: int vizitat[100]; care reţine informaţii despre vizitarea sau nu a unui nod din graf, precum şi coada în care se introduc nodurile queue<int>q;. BIBLIOGRAFIE [Iva98] Ivaşc C., Prună M. Bazele informaticii (Grafuri şi elemente de combinatorică), Manual pentru clasa a X-a, Editura Petrion, 1998. [Cor90] Cormen, T. H., Leiserson, C. E., Rivest, R. L. Introduction to Algorithms. McGraw- Hill, New York, 1990.