You are on page 1of 13

Traversarea

Prin traversarea unui arbore se înţelege execuţia unei anumite operaţii asupra tuturor nodurilor
arborelui. În timpul vizitării nodurile sunt vizitate într-o anumită ordine, astfel încât ele pot fi considerate
ca şi cum ar fi integrate într-o listă liniară. Există trei moduri de ordonare (traversare) a unei structuri de
arbore, numite preordine, inordine şi postordine.

Traversarea în preordine a arborelui A presupune traversarea rădăcinii R urmată de traversarea în


preordine a lui A1, apoi de traversarea în preordine a lui A2, şi aşa mai departe până la Ak inclusiv. 2.
Traversarea în inordine presupune parcurgerea în inordine a subarborelui A1, urmată de nodul rădăcină
R şi, în continuare, parcurgerea în inordine ale subarborilor A2, A3,..., Ak. 3. Traversarea în postordine a
arborelui A constă în traversarea în postordine a subarborilor A1, A2,..., Ak şi, în final, traversarea
nodului rădăcină R.

Implementarea arborilor generalizaţi prin indicator spre părinte


O manieră simplă de implementare o reprezintă utilizarea unui tablou (A), în care fiecare intrare A[I]
conţine un indice la părintele nodului I. Deci, dacă A[I].indice = J, atunci nodul J este părintele nodului I,
excepţie făcând cazul în care nodul I este chiar rădăcina arborelui. Această modalitate de implementare
face uz de proprietatea arborilor că orice nod are un singur părinte. Reprezentarea prin indicator spre
părinte are însă dezavantajul implementării dificile a operaţiilor legate de fii. Pentru a facilita acest lucru,
se impune stabilirea unei ordini artificiale a nodurilor în tablou, respectând următoarele reguli: -
numerotarea fiilor unui nod se face numai după ce nodul a fost numerotat; în consecinţă, fiii vor avea
întotdeauna un număr de ordine mai mare decît nodul părinte; - numerele fiilor cresc de la stânga la
dreapta.

Implementarea arborilor generalizaţi cu structuri de adiacenţă


Principial, fiecare nod trebuie să conţină, pe lângă cheie şi alte informaţii specifice, adresele tuturor fiilor
săi. Întrucât numărul de fii variază de la nod la nod, este bine să construim pentru fiecare nod o listă
înlănţuită cu adresele fiilor lui, iar în nod să memorăm doar adresa de început a acestei liste. În această
implementare se folosesc următoarele structuri de date:

struct _nodArb{ int cheie; ...info; struct _listaPFii *lf; }; struct _listaPFii{ struct _nodArb *fiu; struct
_listaPFii *urm; };
Arbori binary
Prin arbore binar se înţelege o mulţime de n noduri care dacă nu este vidă, conţine un anumit nod numit
rădăcină, iar restul nodurilor formează doi arbori binari disjuncţi, numiţi subarborele stâng şi
subarborele drept.

// Implementarea dinamică a arborilor binari Fiecare nod trebuie să conţină, pe lângă cheie şi alte
informaţii specifice, adresele celor cel mult doi fii ai săi. Structura de date folosită în această
implementare este următoarea: struct _nodAB{ int cheie; ...info; _nodAB *stang; _nodAB *drept; };

Arbori binari ordonaţi ABO


Prin arbore binar ordonat se înţelege un arbore binar care are următoarea proprietate: parcurgând
nodurile sale în inordine, secvenţa cheilor este monoton crescătoare.

Aşadar, având un arbore binar ordonat şi N un nod oarecare al său cu cheia C, toate nodurile din
subarborele stâng al lui N au cheile mai mici sau egale cu C şi toate nodurile din subarborele drept al lui
N au chei mai mari sau egale decît C. De aici rezultă un procedeu foarte simplu de căutare a unui nod cu
o cheie dată într-un arbore binar ordonat, şi anume: începând cu rădăcina, se trece la fiul stâng sau la
fiul drept după cum cheia căutată este mai mică sau mai mare decât cea a nodului curent. Numărul
comparaţiilor efectuate în acest caz este cel mult egal cu înălţimea arborelui.

Pentru adăugarea unui nou nod în arbore, acesta trebuie întâi creat, apoi inserat în arbore. Pe lângă
acestea, mai trebuie iniţializate câmpurile cu informaţii specifice nodului. Dacă arborele este vid, noul
nod va fi rădăcina lui. Dacă nu este vid, pentru inserarea noului nod, adresa acestuia trebuie memorată
în nodul care-i va fi părinte. Pentru a ajunge la acesta, trebuie întâi să-l căutăm.

Căutarea unui nod este mai simplă la arborii binari ordonaţi decât la cei neordonaţi, întrucât nu este
nevoie să parcurgem arborele până la găsire printr-una din metode (preordine, inordine, postordine).
Pornim de la rădăcină şi urmăm un drum fără reveniri în arbore, în funcţie de cheia căutată.

Căutarea se poate simplifica aplicând metoda fanionului şi modificând structurile arborelui astfel încât
orice referinţă către NULL se înlocuieşte cu o referinţă spre nodul fanion

Folosind fanion, înainte de începerea căutării, cheia nodului fanion se asignează cu valoarea căutată, x;
astfel, va exista în arbore cel puţin un nod cu acea cheie, şi, în cel mai rău caz, nodul va fi găsit pe
această poziţie.

Daca cautam sau vrem sa adaugam un nod in arbore trebuie sa o facem astfel incat arborele sa ramana
ordonat.

Dacă vrem să căutăm să adăugăm un nod copacului, atunci vom începe


prin compararea cheii pe care dorim să o găsim sau adăugăm cu rădăcina
dacă este mai mare decât rădăcina, apoi va merge la ramura dreaptă dacă
nu merge ramura spre stânga așa că merge până când găsim nodul sau nu
avem unde să mergem și apoi cumpărăm.

Tehnici de suprimare a nodurilor din ABO

Pentru a suprima nodul cu cheia x dintr-un arbore binar ordonat, mai întâi se caută dacă există un astfel
de nod în arbore. Dacă nu, suprimarea s-a încheiat. În caz că nodul există, atunci se suprimă nodul,
procedându-se de o asemenea manieră încât arborele să rămână ordonat şi în urma suprimării. În
procesul de suprimare se disting două cazuri, după cum nodul de suprimat are cel mult un fiu sau are doi
fii, şi anume: - Dacă nodul de suprimat are cel mult un fiu: în această situaţie referinţa care indica spre
nodul de suprimat (un câmp al tatălui nodului de suprimat sau, în cazul rădăcinii, referinţa spre
rădăcină), se modifică astfel încât să indice unicul fiu al nodului de suprimat, dacă acesta există, sau în
caz contrar, valoarea lui devine NULL.

Dacă nodul de suprimat are doi fii: - se caută predecesorul nodului în ordonarea în inordine. Se poate
demonstra că acest nod există şi că el nu are fiu drept; - se modifică nodul de suprimat asignând toate
câmpurile sale de date cu câmpurile corespunzătoare ale predecesorului. În acest moment predecesorul
se găseşte în dublu exemplar în structura de arbore: în locul său iniţial şi în locul nodului de suprimat; -
se suprimă predecesorul conform cazului anterior, deoarece acesta nu are fiu drept. Predecesorul Y al
unui nod X se poate detecta prin următoarea metodă: se construieşte o secvenţă de noduri care începe
cu fiul stâng al lui X, după care se alege drept succesor al fiecărui nod, fiul său drept. Primul nod al
secvenţei care nu are fiu drept este predecesorul.
Arbori echilibraţi AVL
un arbore este echilibrat dacă şi numai dacă înălţimile celor doi subarbori ai săi diferă cu cel mult 1.
Arborii care satisfac acest criteriu numesc arbori AVL (Andelson-Velskii şi Landis). În cele ce urmează,
aceşti arbori vor fi numiţi şi arbori echilibraţi.

Implementarea grafurilor cu matrici de adiacenţă


Structurile de date necesare pentru reprezentarea grafurilor prin matrici de adiacenţă sunt următoarele:

typedef struct _nodG{//definirea tipului unui nod

int cheie;

...info;

}nodG;

nodG nod[nrN];//tabloul de noduri

int arc[nrN][nrN];//matricea de adiacenta, nrN este numarul maxim de noduri

Mai jos este secvenţa de adăugare a unui nod cu cheia x:


int i;
nrN++;//incrementarea numarului de noduri

nod[nrN].cheie = x;
// + initializarea celorlalte informatii ale nodului
for(i=1;i<=nrN;i++)// initializarea cu 0 a conexiunilor cu celelalte noduri
arc[i][nrN] = arc[nrN][i] = 0;

Implementarea grafurilor cu structuri de adiacenţă


În această implementare se folosesc următoarele structuri de date:

typedef struct _nodG{//un nod al grafului = un nod al listei de noduri

int cheie;

...info;

struct _nodL *incL, *sfL;//inceputul si sfarsitul listei de adiacenta a nodului

struct _nodG *urmG;//campul de inlantuire in lista de noduri


}nodG;

typedef struct _nodL{//un nod al listei de adiacente

struct _nodG *nod;//nodul adiacent indicat

struct _nodL *urmL;//campul de inlantuire in lista de adiacente

}nodL;

nodG *incG, *sfG;//inceputul si sfarsitul listei de noduri

Pentru a urmări vizual mai uşor conexiunile, în locul pointerilor putem reprezenta direct
cheia nodul indicat:

1- 2 3 4

2- 1 3 5

3- 1 2

4- 1 6 7

5- 2 6

6- 4 5 7

7- 4 6

8- 9

9- 8
Traversarea în adâncime utilizând implementarea cu matrici de adiacență

Traversarea grafurilor în adâncime reprezintă o generalizare a traversării în preordine a


arborilor.

Principiul traversării (căutării) în adâncime a unui graf G este următorul: se marchează


iniţial toate nodurile ca fiind nevizitate. Traversarea debutează cu selecţia unui nod n al lui G pe
post de nod de pornire şi cu marcarea acestuia ca vizitat. În continuare, fiecare nod adiacent lui
n este traversat la rândul său şi marcat cu vizitat în cazul în care acesta nu a mai fost vizitat
anterior, apelând recursiv aceeaşi căutare în adâncime. În momentul în care toate nodurile la
care se poate ajunge pornind de la un nod iniţial n au fost vizitate, cercetarea lui n se termină.

Dacă în graf au rămas noduri nevizitate, se selectează unul dintre acestea pe post de nod de
pornire, şi procesul anterior se repetă, până când toate nodurile grafului au fost vizitate.
În procesul de traversare, arcele care au fost traversate în decursul trecerii de la un nod
la altul formează aşa numitul arbore de acoperire prin traversare în adâncime al grafului
respectiv (dacă graful nu este conex, se obţin mai mulţi arbori, câte unul pentru fiecare
componentă conexă). Pentru un graf dat arborele de acoperire obţinut prin căutare în
adâncime nu este unic, acesta depinzând de modul de reprezentare al grafului precum şi de
punctul de pornire, care impune ordinea în care arcele conectate la anumit nod sunt vizitate.
Din acest motiv, arborii de acoperire diferă ca formă.

Algoritmul de traversare în adâncime poate fi utilizat nu numai pentru determinarea arborelui


de acoperire pentru un graf, ci şi pentru rezolvarea altor probleme fundamentale ale prelucrării
grafurilor. Una dintre acestea o constituie determinarea numărului şi a componentelor conexe
ale unui graf. Acest lucru se poate face foarte simplu, deoarece strategia algoritmului
presupune vizitarea tuturor nodurilor dintr-o componentă conexă înainte de a trece la
următoarea componentă conexă. Posibilitatea împărţirii grafurilor în componente conexe este
deosebit de avantajoasă în cazul prelucrării ulterioare a acestora utilizând algoritmi complecşi
Componenta conexă 1: arborele de acoperire prin căutare în adâncime este format din arcele:
(1, 2), (2, 3), (2, 5), (5, 6), (6, 4), (4, 7).

Componenta conexă 2: arborele de acoperire prin căutare în adâncime este format din arcul: (8, 9).
. Traversarea prin cuprindere utilizând implementarea cu matrici de adiacență

Principiul traversării (căutării) prin cuprindere este următorul: pentru fiecare nod vizitat
x, se caută în imediata sa apropiere cuprinzând, în vederea vizitării, toate nodurile adiacente
lui. Se foloseşte o structură de coadă pentru a reţine nodurile care au fost vizitate, iniţial
aceasta fiind vidă.

În mod analog cu traversarea prin căutare în adâncime, se poate construi şi în acest caz
arborele de acoperire pentru un graf dat, numit arborele de acoperire prin traversare prin
cuprindere. De asemenea, pot fi evidenţiate în aceeaşi manieră componentele conexe ale
grafului, traversarea făcându-se prin epuizarea nodurilor unei componente conexe şi apoi
trecerea la următoarea componentă.

Paşii urmaţi pentru traversare sunt:

- iniţializarea cozii cu nodul 1 şi marcarea lui ca vizitat; coada: 1


- prelucrarea cozii (pentru nodul x: adăugarea în coadă a nodurilor adiacente cu x şi
nevizitate şi marcarea lor ca vizitate; marcarea arcului corespunzător pentru
arborele de acoperire; scoaterea lui x din coadă) => prima componentă conexă:
arbore de acoperire minim

Un arbore de acoperire minim al unui graf ponderat este arborele de acoperire al cărui
cost este cel puţin la fel de mic ca şi costul oricărui arbore de acoperire al grafului. Pentru un
graf pot exista mai mulţi arbori de acoperire minimi.

Gradul unui nod x , notat cu d(x),reprezinta numarul muchiilor incidente care trec prin
nodul x.

Un graf se numeşte conex dacă de la fiecare nod al său există un drum spre oricare nod al
grafului, respectiv dacă oricare pereche de noduri aparţinând nodului este conectată. Un graf
care nu este conex este format din componente conexe.

Fie G=(V, E) un graf orientat, unde V are n elemente (n varfuri) si E are m elemente (m arce).
Definitie: G1=(V1, E1)este o componenta tare conexa daca:
- pentru orice pereche x,y de varfuri din V1 exista un
drum de la x la y si drum de la y la x
- nu exista alt subgraf al lui G, G2=(V2, E2) care sa
indeplineasca prima conditie si care sa-l contina pe G1
Caracteristicile arborilor B

fiecare pagină conţine cel mult 2n noduri (chei)

fiecare pagină, cu excepţia uneia singure numită
pagină rădăcină, conţine cel puţin n noduri

fiecare pagină este fie o pagină terminală (caz în
care nu are descendenţi), fie are m+1
descendenţi, unde m reprezintă numărul de chei
în pagină

toate paginile terminale apar la acelaşi nivel

Algoritmul lui Prim

Principiul algoritmului este următorul: fie G graful ponderat pentru care se doreşte
determinarea arborelui de acoperire minim şi fie N mulţimea nodurilor acestuia. Algoritmul
începe prin selecţia unui nod de pornire şi introducerea lui în mulţimea U. În continuare, într-o
manieră ciclică, se selectează la fiecare pas arcul cu cost minim (u, v) care conectează un nod
din mulţimea U cu un alt nod din mulţimea V = N – U şi se adaugă acest arc arborelui de
acoperire minim, iar nodul v lui U. Ciclul se repetă până când toate nodurile au trecut în
submulţimea U, adică U = N.
Pasul 5: nod = 1 2 3 4 5 6 7

apropiat = 1 1 1 3 6 4 6

costMin = I 5 I I 3 I I -> vom alege arcul (6 5)

Pasul 6: nod = 1 2 3 4 5 6 7

apropiat = 1 1 1 3 6 4 6

costMin = I 5 I I I I I -> vom alege arcul (1 2)

Astfel, pentru graful din figura 9.1, aplicarea algoritmului lui Prim conduce la arborele de
acoperire minim format din arcele: (1, 3), (3, 4), (4, 6), (6, 7), (6, 5) şi (1,2), reprezentat în figura
9.2.
Tehnica căutării bazată pe prioritate folosită pentru determinarea arborelui de
acoperire minim este ca principiu identică cu algoritmul lui Prim, diferenţele constând doar în
modul de implementare. Principiul este: alegem nodul rădăcină, apoi, până la adăugarea în
arbore a tuturor nodurilor, la fiecare pas alegem nodul legat prin cea mai „ieftină” muchie de
unul dintre nodurile alese deja.
Nodurile nealese care sunt legate direct de cel puţin un nod dintre cele alese formează
aşa-numita „vecinătate”. Nodul ales la fiecare pas, aparţine, aşadar, vecinătăţii. El va fi cel cu
prioritatea maximă. În acest caz, „prioritatea maximă” o are nodul care conduce la arcul cu
ponderea minimă.
Metoda utilizează o coadă bazată pe prioritate în cadrul căreia sunt introduse, pe rând,
toate nodurile din clasa „vecinătate”, în funcţie de prioritatea acestora (cost mic, prioritate
mare). Rând pe rând, aceste noduri sunt extrase din coada cu prioritate (având în vedere că
nodul cu prioritate maximă, deci cost minim, va fi întotdeauna primul). La extragerea unui nod,
x, acesta va fi marcat (inclus în arborele de acoperire minim), apoi, pentru fiecare nod adiacent
lui x şi neales, y, se încearcă introducerea acestuia în coada cu prioritate cu prioritatea p egală
cu ponderea arcului (x, y). În coada cu prioritate, nodurile sunt întotdeauna ordonate în funcţie
de prioritatea lor. La încercarea de a introduce pe y cu prioritatea p în coadă, avem cazurile:

- dacă y se află deja în coadă

- cu o prioritate mai mare sau egală cu p (cost mai mic pentru ajunge la el de la
nodurile deja alese), nu-l mai introducem

- cu o prioritate mai mică decât p, atunci îl vom muta în coadă pe poziţia


corespunzătoare noii ponderi p

- dacă y nu se află în coadă, îl vom introduce cu ponderea p.


Algoritmul lui Floyd

1 1

2 8 2
1 1
2 4 3 2 4 2 3 2 4
6 2 2
2 8 7 3
7 7
1 1 5 1 1
5 6 6

Figura 11.1. Graf orientat şi ponderat Figura 11.2. Drumurile minime din 1
O problemă derivată din problema drumurilor minime cu origine unică, pe care
algoritmul lui Dijkstra prezentat mai sus o rezolvă, o reprezintă problema determinării
drumurilor minime corespunzătoare tuturor perechilor de noduri. Evident, această problemă
poate fi rezolvată şi cu ajutorul algoritmului lui Dijkstra, considerând pe rând, fiecare nod al
grafului drept origine. O altă posibilitate o reprezintă algoritmul lui Floyd.
Acesta utilizează o matrice a de dimensiuni nrNoduri x nrNoduri, unde memorează
lungimile drumurilor minime. Iniţial, a[i, j] = cost[i, j], pentru i != j; dacă nu există arc de la i la
j, cost[i, j] = mare. Elementele diagonalei principale în matricea a se iniţializează la zero.
Algoritmul execută nrNoduri iteraţii asupra matricii a, rezultând pe rând matricile a1, a2,
..., anrNoduri. După cea de-a k-a iteraţie, a[i, j] va conţine costul drumului minim de la i la j, care
nu trece prin nici un nod cu număr mai mare decât k, notat cu ak[i, j]
(figura 11.5). Astfel, pentru calcul lui ak[i, j], se compară ak-1[i, j], adică costul drumului de la i la
j fără a trece prin nodul k, şi nici printr-un alt nod cu număr mai mare decât k, cu ak-1[i, k] + ak-
1[k, j], adică costul drumului de la i la k însumat cu costul drumului de la k la j, fără a trece prin
nici un nod cu număr mai mare sau egal cu k. Dacă acesta din urmă se dovedeşte a fi mai scurt,
atunci costul acestuia se atribuie lui ak[i, j]; altfel, acesta rămâne neschimbat.
matricea drumurilor: matricea costurilor:
1 2 3 4 5 6 7 1 2 3 4 5 6 7
1 0 0 0 3 7 7 4 1 0 2 1 3 7 6 5
2 4 0 0 3 0 7 4 2 14 0 4 6 6 9 8
3 4 7 0 0 7 7 4 3 10 8 0 2 6 5 4
4 0 7 1 0 7 7 0 4 8 6 9 0 4 3 2
5 4 0 2 3 0 7 4 5 16 2 6 8 0 11 10
6 4 5 5 0 0 0 4 6 11 3 7 3 1 0 5
7 6 6 6 6 6 0 0 7 12 4 8 4 2 1 0

Din matricea putem reconstitui drumul minim (drum[i, j] = 0 înseamnă că între i şi j


există legătură directă), aşa ca în funcţia AfisDrum. De exemplu,
drum[5, 6](=7) = drum[5, 7](=4), 7, drum[7, 6](=0)
= drum[5, 4](=3), 4, drum[4, 7](=0), 7
= drum[5, 3](=2), 3, drum[3, 4](=0), 4, 7
= drum[5, 2](=0), 2, drum[2, 3](=0), 3, 4, 7
= 2, 3, 4, 7
Acest drum are costul a[5, 6] = 11.