Sunteți pe pagina 1din 55

INFORMATICĂ*I* IC.06.

Arbori

Capitolul IC.06. Arbori

Cuvinte-cheie
Arbori, reprezentări, inserare, căutare, parcurgere, ștergere,
Echilibrare AVL, arbori binari optimali, arbori heap, sortare heap

IC.06.1 Noţiuni generale

Definiţie 1: Un arbore este o structură dinamică T = (C, R ) în care C este o mulţime


de vârfuri (noduri) V şi R este o mulţime de muchii (arce) E . Deci T = (V , E ) şi E ⊆ V × V .
Structura T are proprietăţile:
1. ∃ nodul r ∈ V (nodul rădăcină), astfel încât ∀x ∈ V , ( x, r ) ∉ E ;
2.
∀x ∈ V \ {r} , ∃y ∈ V unic, astfel încât ( y, x ) ∈ E (toate nodurile diferite de rădăcină
au un predecesor şi numai unul);
3. ∀x ∈ V , ∃ o secvenţă {r = x0 , x1 , x2 ,K , xk = x} , cu xi ∈ V şi ( xi , xi +1 ) ∈ E
(proprietatea de conexiune);
4. ∀x ∈ V , ( x, x ) ∉ E (fără bucle).
Observaţie: Condiţia 4 este redundantă, dar a fost inclusă în definiţie din motive de claritate.
Proprietate a arborelui: Dacă T , T = (V , E ) , este un arbore şi r ∈ V este rădăcina arborelui,

{ }
atunci mulţimea T \ {r} = (V ʹ′, Eʹ′) , cu vʹ′ ∈ V \ {r} şi E ʹ′ = E \ ( r , x ) ( r , x ) ∈ E , poate fi
partiţionată astfel încât să avem mai mulţi arbori, a căror reuniune să fie T \ {r} şi oricare doi
arbori intersectaţi să dea mulţimea vidă:

T \ {r} = T1 U T2 UL U Tk şi Ti I T j ∀i ≠ j .

Definiţia de mai sus a unui arbore, împreună cu proprietatea respectivă, având în


vedere o structură dinamică şi recursivă, poate fi dată mai simplu astfel:
Definiţie 2: Fiind dată o mulţime V de elemente numite noduri (vârfuri), arborele este
un set finit de astfel de noduri, încât:
1. Există un nod, şi numai unul, care se numeşte rădăcina arborelui;
2. Celelalte noduri sunt repartizate în m ≥ 0 seturi disjuncte T1 , T2 ,K , Tm , fiecare set Ti
constituind la rândul său un arbore.
Nodul rădăcină r formează nivelul 0 al arborelui. Deoarece T1 , T2 ,K , Tm sunt la rândul lor
arbori, rădăcinile lor r1 , r2 ,K , rm formează nivelul 1 al arborelui. Nodul r are câte o legătură
(muchie) la nodurile r1 , r2 ,K , rm . Continuând acest procedeu se ajunge la nivelurile 2,3,… ale
arborelui.

r nivel 0

r1 r2 rm nivel 1

r11 r1k rm1 rmn nivel 2

  -1-
INFORMATICĂ*I* IC.06. Arbori

Un nod care nu are nici un nod subordonat se numeşte nod terminal sau nod frunză.
În legătură cu arborii, s-a adoptat un limbaj conform căruia, un nod care are descendenţi
direcţi se spune că este nod tată sau părinte. Nodurile descendente direct dintr-un nod tată
(părinte) se numesc noduri fiu ale lui. De exemplu, în modelul de mai sus nodul r1 este
părinte pentru nodurile fiu r11 ,K , r1k . Nodurile fiu ale aceluiaşi nod se numesc noduri frate.
Numărul de subarbori ai unui nod oarecare este gradul acelui arbore. Nodurile de grad
zero sunt nodurile terminale (frunze). Nodurile de grad mai mare sau egal cu 1 sunt noduri
interne sau neterminale. Gradul maxim al unuia din nodurile arborelui constituie și gradul
unui arbore.
Înălțimea unui arbore este dată de numărul de niveluri al acelui arbore.
Definiție: Un arbore cu ordinul (gradul) mai mic sau egal cu 2 se numește arbore
binar, iar unul cu gradul mai mare decât 2 se numește arbore multicăi.
În cazul unui arbore binar se face distincția între cei doi fii ai unui nod, fiii acestui nod
numindu-se fiul stâng și fiul drept.
Este eficient în aplicații care implementează arbori să se lucreze cu arbori echilibrați.
Echilibrarea unui arbore se poate face după următoarele criterii:
- înălțime;
- greutate;
- pondere.
De exemplu, echilibrarea după greutate se poate defini luând în calcul numărul de
noduri.
Definiție: Un arbore este echilibrat (după numărul de noduri) dacă pentru orice nod X
al său subarborii stâng Ts ( X ) și respectiv drept Td ( X ) ai lui X au un număr de noduri care

( )
diferă prin cel mult 1 adică card (Ts ( X ) ) − card (Td ( X ) ) ≤ 1 .
Definiție: Un arbore se numește complet dacă toate frunzele se află pe același nivel.
Un arbore poate fi văzut și ca un tip abstract de date. Operațiile elementare
caracteristice tipului arbore binar pot fi asupra nodurilor (creare, verificare, ștergere, etc.),
asupra muchiilor sau asupra subarborilor.

IC.06.2 Reprezentarea arborilor

Toate metodele de reprezentare ale unui arbore încearcă să pună în evidență fiii sau
părintele unui nod al arborelui.

IC.06.2.1 Reprezentarea „standard”

În reprezentarea standard în fiecare nod al arborelui, pe lângă informația utilă se


memorează informații de înlănțuire care indică descendenții.
Într-o primă variantă de reprezentare, fiecare nod este compus din informația utilă și
un vector de dimensiune fixă, în care se memorează legăturilor de tip pointer la descendenți.
Dimensiunea acestui vector este dată gradul maxim al nodurilor arborelui. Declarațiile de tip
folosite de această reprezentare sunt:

struct Nod{
Atom data;
Nod* vDesc[GRMAX];
};

  -2-
INFORMATICĂ*I* IC.06. Arbori

Un nod va ocupa o zonă de memorie de dimensiune fixă:

În figura de mai jos sunt reprezentate și înlănțuirile pentru arborele considerat:

În această reprezentare, daca p este un pointer la un nod, descendentul i al nodului va fi


identificat direct prin pointerul

p->vDesc[i]

Într-un arbore cu un număr N de noduri vor exista N-1 muchii, deci in total N-1 elemente din
vectorii de pointeri la descendenți sunt ocupate cu informație utilă. Se obține raportul:

N −1 1
=
N * GRMAX GRMAX

care indică gradul de utilizare eficientă a memoriei. În consecință aceasta reprezentare este
acceptabilă numai pentru arbori de grad mic.

IC.06.2.2 Reprezentarea „fiu-frate”

Pentru a obține o utilizare mai eficienta a memoriei, pot fi utilizate listele de


descendenți. Fiecare nod va conține pe lângă informația utila, doi pointeri: unul va indica lista
cu descendenții săi iar cel de-al doilea următorul nod din lista de descendenți din care face
parte.

  -3-
INFORMATICĂ*I* IC.06. Arbori

Struct Nod {
Atom data;
Nod* desc;
Nod* next;
};

În această variantă arborele de mai sus va avea următoarea reprezentare:

Având in vedere semnificația pointerilor conținuți într-un nod, această reprezentare se


mai numește reprezentarea fiu-frate. În aceasta reprezentare, dacă p este un pointer la un nod,
identificarea descendentul i al nodului va necesita parcurgerea listei înlănțuite a
descendenților, listă care începe cu:

p->desc

IC.06.2.3 Reprezentarea prin noduri de dimensiune variabilă

O a treia soluție de reprezentare combină economia de memorie cu avantajele


accesării descendenților pe bază de index. Aceasta soluție se bazează pe posibilitatea de a
aloca blocuri de memorie de lungime precizată.
Vom considera aceeași declarație pentru tipul Nod ca și în prima variantă de
reprezentare, adăugând în plus un câmp în care se memorează gradul nodului.

struct Nod{
Atom data;
int grad;
Nod* vDesc[GRMAX];
};

Economia de memorie se va realiza prin alocarea unor zone de memorie de lungime variabilă,
adaptată gradului fiecărui nod. Mai jos considerăm trei exemple de noduri de grad diferit:

- nod de grad 3:

  -4-
INFORMATICĂ*I* IC.06. Arbori

- nod de grad 1:

- nod terminal:

Pentru a realiza economia de memorie este necesar ca la alocarea spațiului pentru un


nod să se cunoască numărul de descendenți și în funcție de acest număr sa se aloce spațiul
necesar pentru vectorul de descendenți. Functia make_nod care alocă spațiu pentru un nod de
grad dat poate avea forma:

Nod* make_nod(int grad)


{
Nod *p=(Nod*)malloc(sizeof(Nod)-(GRMAX-grad)*sizeof(Nod*));
p->grad = grad;
return p;
}

Utilizând operatorul new, specific limbajului C++, alocarea va avea forma:

Nod* p = (Nod*) new char[sizeof(Nod)-(GRMAX-grad)*sizeof(Nod*)];

În această variantă, reprezentarea arborelui dat arată astfel:

IC.06.3 Aplicație propusă: Evaluarea expresiilor aritmetice

1. Funcția

Nod* creareArbore();

din Anexa citește de la intrare o expresie aritmetică, cu paranteze, care conține operanzi de o
cifră și operatorii + si *, si crează arborele de grad oarecare asociat expresiei. De exemplu
expresiei:

  -5-
INFORMATICĂ*I* IC.06. Arbori

1+2*3+4*(5+6)
i se asociază arborele:

Arborele este reprezentat după metoda 3, tipul Atom fiind echivalat cu tipul int (vezi Anexa).

Se cere:
- Să se determine și să se afișeze gradul arborelui.
- Să se afișeze valoarea tuturor operanzilor utilizați în expresie (fără operatori).
- Să se evalueze expresia și să se afișeze rezultatul.

2. Se dau două expresii care conțin numai operatorii + si *, operanzi specificați printr-o
singură literă și paranteze rotunde. Să se determine dacă cele doua expresii sunt identice.

Indicație: Se aduc cele două expresii la forma canonica (sumă de produse), se sortează
termenii și se verifică dacă expresiile rezultate sunt egale.
Expresia adusă la forma canonică va fi reprezentata sub forma unui arbore cu trei nivele, în
care:
- rădăcina este un nod Σ - semnificând un nod în care se face însumarea tuturor
descendenților;
- pe nivelul 2 vor fi noduri π - semnificând noduri in care se face produsul tuturor
descendenților;
- pe nivelul 3 vor fi operanzi.

Vom numi astfel de arbori "arbori Σ(π)".

De exemplu:

Pentru a crea arborele Σ(π) corespunzător formei canonice se prelucrează arborele


inițial executând următoarele operații:

[A] - în nodurile terminale: a --> Σ


|
π

  -6-
INFORMATICĂ*I* IC.06. Arbori

|
a

[B] - in nodurile + : + --> Σ(π)


/ \
Σ1(π) Σ2(π)

Nodul Σ va avea ca descendenți atât descendenții lui Σ1 cât și ai lui Σ2 (listele de descendenți
vor fi concatenate).

[C] - nodurile * : * --> Σ(π)


/ \
Σ1(π) Σ2(π)

În acest caz arborele Σ(π) va conține un număr de descendenți egal cu produsul dintre
numărul de descendenți ai nodului Σ1 și numărul de descendenți ai nodului Σ2, obținuți prin
combinarea (înmulțirea) fiecare cu fiecare.

Anexa

Fisierul Arbore.h

#ifndef _ARBORE_H_
#define _ARBORE_H_

#define DIM_EXPR 100


#define GRMAX 20

#define Atom int

struct Nod
{
Atom data;
int grad;
Nod* vDesc[GRMAX];
};

Nod* creareArbore();

#endif

Fisierul Arbore.cpp

#include "arbore.h"
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#include <conio.h>
#include <stdio.h>
#include <iostream.h>

Nod* parse(char buffer[], int start, int end);

Nod* creareArbore()

  -7-
INFORMATICĂ*I* IC.06. Arbori

{
char buffer[DIM_EXPR];
cin >> buffer;
int length = strlen(buffer);

Nod* n = parse(buffer, 0, length-1);


return n;
}

Nod* parse(char buffer[], int start, int end)


{
int openP = 0;
int indici[GRMAX];
int k = 0;

openP = 0;
for (int i = start; i <= end; i++)
{
if ( buffer[i]=='(' )
openP ++;
if ( buffer[i]==')' )
openP --;
if ( buffer[i]=='+' )
{
if ( openP > 0)
continue;
indici[k++] = i;
}
}
if ( k > 0 )
{
Nod *p=(Nod*) new char[sizeof(Nod)-(GRMAX-k+1)*sizeof(Nod*)];
p->grad = k+1;
p->data='+';
p->vDesc[0] = parse(buffer, start, indici[0]-1);
for (int j = 1; j < p->grad - 1; j++)
{
p->vDesc[j] = parse(buffer, indici[j-1]+1, indici[j]-1);
}
p->vDesc[p->grad-1] = parse(buffer, indici[p->grad-2]+1, end);
return p;
}

openP = 0;
for (i = start; i <= end; i++)
{
if ( buffer[i]=='(' )
openP ++;
if ( buffer[i]==')' )
openP --;
if ( buffer[i]=='*' )
{
if ( openP > 0)
continue;
indici[k++] = i;
}
}
if ( k > 0 )
{
Nod *p=(Nod*) new char[sizeof(Nod)-(GRMAX-k+1)*sizeof(Nod*)];

  -8-
INFORMATICĂ*I* IC.06. Arbori

p->grad = k+1;
p->data='*';
p->vDesc[0] = parse(buffer, start, indici[0]-1);
for (int j = 1; j < p->grad - 1; j++)
{
p->vDesc[j] = parse(buffer, indici[j-1]+1, indici[j]-1);
}
p->vDesc[p->grad-1] = parse(buffer, indici[p->grad-2]+1, end);
return p;
}

if ( buffer[start] == '(' && buffer[end] == ')' )


return parse(buffer, start+1, end-1);

if ( start==end )
if ( isdigit(buffer[start]) )
{
Nod* p = (Nod*) new char[sizeof(Nod)-(GRMAX)*sizeof(Nod*)];
p->data = buffer[start];
p->grad = 0;
return p;
}

printf("\nExpresia de intrare este eronata. Apasati o tasta");


getch();
exit(1);
}

IC.06.4 Arbori binari

IC.06.4.1 Reprezentarea standard

În reprezentarea standard, un nod al arborelui este o structură cu un câmp conținând


eticheta nodului (data) și doua câmpuri pointeri la cei doi descendenți (lchild si rchild):

struct Nod
{
type data;
Nod* stg, *drt;
};

Astfel, arborele:

va avea următoarea reprezentare:

  -9-
INFORMATICĂ*I* IC.06. Arbori

Pentru a putea prelucra un arbore este suficient să cunoaștem un pointer la nodul rădăcină.
Valoarea nil pentru acest pointer va semnifica un arbore vid.

IC.06.4.2 Parcurgerea arborilor binari

Un arbore binar poate fi privit conform următoarei scheme recursive:

rad = rădăcină
SAS = SubArbore Stâng
SAD = SubArbore Drept

Pentru această schemă se definesc cele trei moduri de parcurgere a arborelui:

-
preordine: rad SAS SAD sau mai simplu RSD (Rădăcină Stânga Dreapta)
Se prelucrează mai întâi rădăcina apoi se parcurg în preordine subarborii stâng și
drept.
- inordine: SAS rad SAD sau SRD (Stânga Rădăcină Dreapta)
Se parcurge în inordine subarborele stâng, se prelucrează rădăcina și apoi se parcurge
în inordine subarborele drept.
- postordine: SAS SAD rad sau SDR (Stânga Dreapta Rădăcină)
Se parcurg mai întâi în postordine subarborii stâng și drept apoi se prelucrează
rădăcina.
De exemplu, pentru arborele:

cele trei parcurgeri prelucrează nodurile în ordinea:

preordine: ABDCEF
inordine: BDAECF
postordine: DBEFCA

  -10-
INFORMATICĂ*I* IC.06. Arbori

Se pot realiza aceste parcurgeri utilizând subrutine recursive. De exemplu, pentru parcurgerea
în preordine avem:

void PREORDINE(pNod p);


{
if (p!=NULL){
┌───────────────────┐
│ prelucreaza(*p); │
└───────────────────┘
PREORDINE(p->lchild);
PREORDINE(p->rchild);
}
}
sau
void PREORDINE(Nod* p);
{
┌──────────────────┐
│ prelucreaza(*p); │
└──────────────────┘
if(p->stg!=NULL) PREORDINE(p->stg);
if(p->drt!=NULL) PREORDINE(p->drt);
}

A doua varianta nu poate fi aplicata unui arbore vid, în timp ce prima tratează corect arborele
vid, în schimb execută un apel recursiv în plus pentru fiecare legătură care este NULL.
Alte modalităţi de parcurgere ale unui arbore sunt parcurgerea în lăţime sau în
adâncime.

IC.06.4.2 Aplicație

În cele ce urmează prezentăm o aplicație care efectueze următoarele:


1. afișează conținutul arborelui în inordine;
2. afișează conținutul arborelui în postordine;
3. determină adâncimea arborelui;
4. determină numărul de noduri din arbore;
5. determină numărul de frunze ale arborelui;
6. determină valoarea maximă și valoarea minimă din arbore;
7. afișează nodurile care au valoarea din rădăcina mai mare decât toate valorile din
subarborii descendenți;
8. afișează toate nodurile pentru care toate valorile conținute in subarborele stâng sunt
mai mici decât toate valorile conținute în subarborele drept;
9. pentru fiecare nod să se comute subarborele stâng cu cel drept.

În modulul ARBORE.CPP (vezi Anexa) sunt specificate declarațiile tipurilor:


struct Nod {
char data;
struct Nod *stg, *drt;
}
și funcția:

Nod* creareArbore();

  -11-
INFORMATICĂ*I* IC.06. Arbori

care citește un arbore specificat conform următoarei diagrame de sintaxă, și întoarce pointer la
rădăcina arborelui citit.

În diagrama: '-' - semnifică un arbore vid;


nume - este eticheta unui nod formata dintr-o literă.

Exemple: arbore vid: -

Anexa

Arbore.h

struct Nod{
char data;
Nod* stg, *drt;
};

Nod* creareArbore();

Arbore.cpp

#include <alloc.h>
#include <conio.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>

  -12-
INFORMATICĂ*I* IC.06. Arbori

#include "arbore.h"

void eroare();
char readchar();
char citesteNume();
Nod* citesteArbore();
Nod* creareArbore();

char car;

void eroare()
{
printf("Sirul de intrare este eronat!\n");
printf("Apasati tasta o tasta...");
getch();
exit(1);
}
char readchar()
{
char c;
do c=getchar(); while(c==' ');
return c;
}

char citesteNume()
{
char c;
if(!isalpha(car)) eroare();
c = car;
car = readchar();
return c;
}

Nod* citesteArbore()
{
Nod* rad;
if( car=='-' ) {
rad=0;
car = readchar();
}
else {
rad = (Nod*) malloc(sizeof(Nod));
rad->data = citesteNume();
if( car!='(' ) {
rad->stg = 0;
rad->drt = 0;
}
else {
car = readchar();
rad->stg = citesteArbore();
if( car!=',' ) rad->drt = 0;
else {
car = readchar();
rad->drt = citesteArbore();
}
if( car!=')' ) eroare();
car = readchar();
}
}
return rad;
}

  -13-
INFORMATICĂ*I* IC.06. Arbori

Nod* creareArbore()
{
printf("\nIntroduceti arborele:");
car = readchar();
return citesteArbore();
}

Funcțiile care realizează cerințele specificate sunt date mai jos, în modulul FUNCTII.CPP.
Afișarea este realizată în variantele recursivă și nerecursivă. Pentru varianta nerecursivă este
utilizată o stivă, din acest motiv am specificat și funcțiile pentru stiva respectivă în modulul
STACK.CPP

FUNCTII.CPP
#include <stdio.h>
#include <string.h>

#include “arbore.h”
#include “stack.cpp”

void AfisInordine(Nod* nod)


{
if(nod==0)
return;
else
{
AfisInordine(nod->stg);
printf(“%c “,nod->data);
AfisInordine(nod->drt);
}
}

void AfisPostordine(Nod* nod)


{
if(nod==0)
return;
else
{
AfisPostordine(nod->stg);
AfisPostordine(nod->drt);
printf(“%c “,nod->data);
}
}

int Adancime(Nod* nod)


{
if(nod==0) return 0;
int maxs=1, maxd=1;
maxs = 1 + Adancime(nod->stg);
maxd = 1 + Adancime(nod->drt);
if(maxs < maxd)
return maxd;
else
return maxs;
}

int Noduri(Nod* nod)

  -14-
INFORMATICĂ*I* IC.06. Arbori

{
if(nod==0)
return 0;
else
return 1 + Noduri(nod->stg) + Noduri(nod->drt);
}

int Frunze(Nod* nod)


{
if(nod==0) return 0;
if((nod->stg==0) && (nod->drt==0)) return 1;
else
return Frunze(nod->stg) + Frunze(nod->drt);
}

void Rad_sup(Nod* nod) //nod ce are valoarea mai mare decat val. Din
//subarbori
{
if(nod==0)
return;
else
{
if(nod->data==max(nod))
printf(“%c “,nod->data);
Rad_sup(nod->stg);
Rad_sup(nod->drt);
}
}

char max(Nod* nod) //val maxima dintr-un arbore


{
if(nod==0)
return 0;
char maxim;
maxim=nod->data;
if(maxim < max(nod->stg))
maxim=max(nod->stg);
if(maxim < max(nod->drt))
maxim=max(nod->drt);
return maxim;
}

char min(Nod* nod) //val minima dintr-un arbore


{
if(nod==0)
return 0;
char minim;
minim=nod->data;
if(minim > min(nod->stg))
minim=min(nod->stg);
if(minim > min(nod->drt))
minim=min(nod->drt);
return minim;
}

void Stg_inf(Nod* nod) //maxim subarbore stang<minim subarbore drept


{
if(nod==0)
return;
if((nod->stg==0) && (nod->drt==0))
return;

  -15-
INFORMATICĂ*I* IC.06. Arbori

else
{
if(min(nod->drt) > max(nod->stg))
printf(“%c “,nod->data);
Stg_inf(nod->stg);
Stg_inf(nod->drt);
}
}

void Comuta(Nod *&r)


{
Nod *aux;
if(r==0) return;
aux=r->drt; r->drt=r->stg; r->stg=aux;
Comuta(r->drt);
Comuta(r->stg);
}
void AfisInordine_nrec(Nod* p) //afisare inordine nerecursiva
{
Stack s;
initStack(s);
while(1)
{
while(p!=0)
{
push(s,p);
p=p->stg;
}
if(isEmpty(s)) break;
else
{
p=pop(s);
printf(“%c “, p->data);
p=p->drt;
}
}
}

void AfisPreordine_nrec(Nod* p) //afisare preordine nerecursiva


{
Stack s;
initStack(s);
while(1)
{
while(p!=0)
{
printf(“%c “, p->data);
push(s,p);
p=p->stg;
}
if(isEmpty(s)) break;
else
{
p=pop(s);
p=p->drt;
}
}
}

  -16-
INFORMATICĂ*I* IC.06. Arbori

STACK.CPP
#include <stdio.h>

typedef Nod* Atom;

struct Element{
Atom data;
Element* next;
};

typedef Element* Stack;

void initStack(Stack& S)
{
S=0;
}

int isEmpty(Stack& S)
{
return(S==0);
}

void push(Stack& S, Atom val)


{
Element *p;
p=new Element;
p->data=val;
p->next=S;
S=p;
}

Atom pop(Stack& S)
{
Atom aux;
Element *p;
p=S;
if(isEmpty(S)==1)
//printf("Eroare!Stiva vida!Nu putem extrage!");
return 0;
else
{
aux=p->data;
S=S->next;
delete p;
return aux;
}
}

Atom top(Stack& S)
{
if(isEmpty(S)==1)
//printf("Eroare!Stiva vida!Nu are varf!");
return 0;
else
return S->data;
}

  -17-
INFORMATICĂ*I* IC.06. Arbori

IC.06.5 Arbori binari de căutare (BST)

IC.06.5.1 Noțiuni generale

Definiție: Un arbore binar de căutare (BST) este un arbore binar cu următoarele


proprietăți:
10. fiecare nod conține un câmp cheie astfel încât pe mulțimea constantelor tipului cheii să
existe o relație de ordine;
11. oricare ar fi un nod x al arborelui, dacă y ∈ Ts ( x) (subarborele stâng) atunci
cheie(y)<cheie(x) și dacă y ∈ Td ( x) (subarborele drept) atunci cheie(y)>cheie(x).
De obicei cheile stocate în nodurile unui arbore sunt considerate distincte. O proprietate a
unui arbore BST constă în faptul că parcurgerea în inordine (SRD) sortează crescător
elementele mulţimii cheilor.

Arborii binari de căutare implementează eficient următoarele operaţii:


12. search(arbore, k) - determină dacă un element specificat prin cheia de sortare k, există
în arbore şi-l returnează dacă există;
13. insert(arbore, x) - inserează în arbore elementul x;
14. delete(arbore, k) - şterge un element specificat, specificat prin cheia k.

Proprietatea care defineşte structura unui arbore binar de căutare este următoarea:
valoarea cheii memorate în rădăcina este mai mare decât toate valorile cheilor conţinute în
subarborele stâng şi mai mică decât toate valorile cheilor conţinute în subarborele drept.
Această proprietate trebuie să fie îndeplinita pentru toţi subarborii, de pe orice nivel, în
arborele binar de căutare.

Exemplu (pentru fiecare nod s-au reprezentat numai cheile de căutare):

IC.06.5.2 Operaţii caracteristice

Inserarea şi căutarea unui nod într-un arbore binar de căutare

a) varianta recursivă:
Funcţia insert poate avea următoarea formă:

insert(r,a) // r - pointer la radacina (trasmis prin referinta)


// a - atomul de inserat
{
if r=0 then r = make_nod(a)
else if key(a) < key(data(r)) then
insert(lchild(r),a)
else if key(a) > key(data(r)) then

  -18-
INFORMATICĂ*I* IC.06. Arbori

insert(rchild(r),a)
}

unde funcţia care creează nodul este:


   
make_nod(a) // creeaza un nod nou in care memoreaza atomul a
{
p = get_sp() // aloca spatiu pentru un nod
data(p) = a
lchild(p) = rchild(p) = 0
return (p)
}

Pentru varianta de mai sus trebuie ca funcţia insert să modifice valoarea argumentului
r, pentru aceasta el va fi un parametru transmis prin referinţă. În implementarea C++ funcţia
insert poate avea prototipul:

void insert(Nod*& r, Atom a);

O variantă care nu necesita argument referinţă (deci poate fi implementata în C) este


dată mai jos.
 
insert(r,a)
{
if r=0 then return ( make_nod(a) )
else if key(a) < key(data(r)) then
lchild(r) = insert(lchild(r),a)
else if key(a) > key(data(r)) then
rchild(r) = insert(rchild(r),a)
return (r)
}
 
Apelul acestei variante va avea de fiecare data forma:
 
rad = insert(rad, a)  

 
Procedura search întoarce un pointer la nodul cu cheia de căutare dată sau pointerul
NULL dacă nu există nodul respectiv.

search(r,k)
{
if ( r=0 ) return NULL
else if k < key(data(r)) then
return ( search(lchild(r),k) )
else if k > key(data(r)) then
return ( search(rchild(r),k) )
else return (r)
}

b) varianta nerecursivă
Trebuie observat că atât operaţia search cât şi operaţia insert parcurg o ramură a arborelui (un
lanţ de la rădăcina spre o frunză). Această parcurgere poate fi efectuata iterativ. Este vorba de
a parcurge o înlănţuire, deci se impune o analogie cu parcurgerea listei înlănţuite.

  -19-
INFORMATICĂ*I* IC.06. Arbori

Parcurgerea listei înlănţuite Parcurgerea unei ramuri in arbore


p=cap; p=rad;
while(p!=0){ while(p!=0){
┌────────────────────┐ ┌───────────────┐
│ Prelucrare element │ │ Prelucrea nod │
└────────────────────┘ └───────────────┘
p = p->link; if(conditie) p=p->stg
else p=p->drt;
} }

Procedura de inserare într-un arbore binar de căutare, realizată nerecursiv, are


următoarea formă (presupunem că r este parametru transmis prin referinţă):

insert(r,a)
{
if ( r=0 ) r = make_nod(a)
else {p = r
while ( p<>0 )
{
p1 = p
if key(a)<key(data(p)) then p=lchild(p)
else if key(a)>key(data(p)) then p=rchild(p)
else return
}
if ( key(a)<key(data(p1)) )
lchild(p1) = make_nod(a)
else rchild(p1) = make_nod(a)
}
}

Ştergerea unui nod dintr-un arbore binar de cautare

În continuare se prezintă o funcţie C++ pentru ştergerea unei valori dintr-un arbore
binar de căutare care conţine numere întregi. Pentru ştergerea unei valori din arbore este
necesara mai întâi identificarea nodului care conţine această valoare. În acest scop folosim
tehnica prezentata la operaţia search. Pentru simplitate consideram nodurile etichetate cu
numere întregi care vor constitui chiar cheile de căutare (key(data(p) = data(p)).

struct Nod{
int data;
Nod* stg, *drt;
};

void delete(Nod*& rad, int a)


{
if(rad==NULL)
printf("Eroare: Valoarea %d nu este in arbore!", a);
else if( a<rad->data ) delete(rad->stg,a)
else if( a>rad->data ) delete(rad->drt,a)
else deleteRoot(rad);
}

S-a redus sarcina iniţială la a scrie funcţia deleteRoot care şterge rădăcina unui arbore
binar de căutare nevid. Trebuie tratate următoarele cazuri:

  -20-
INFORMATICĂ*I* IC.06. Arbori

1. rădăcina nu are nici un descendent şi ştergerea este o operaţie imediată.

2. rădăcina are un sigur descendent şi nodul şters va fi înlocuit cu subarborele descendent.

3. rădăcina are doi descendenţi şi ea va fi înlocuită cu nodul cu valoarea cea mai mare din
subarborele stâng, acest nod având întotdeauna cel mult un descendent. Nodul cel mai
mare dintr-un arbore (subarbore) binar de căutare se găseşte pornind din rădăcina şi
înaintând cât se poate spre dreapta.

De exemplu

Deci:

  -21-
INFORMATICĂ*I* IC.06. Arbori

Următoarea funcţie detaşează dintr-un arbore binar de căutare nevid nodul cu valoarea
cea mai mare şi întoarce un pointer la acest nod.

Nod* removeGreatest(Nod*& r)
{
Nod* p;
if( r->drt==0 ) {
p = r;
r = r->stg;
return p;
}
else return removeGreatest(r->rchild);
}

Varianta prezentată este recursiva. Se poate scrie uşor şi o variantă nerecursivă pentru
această procedură.

Ţinând cont de cazurile posibile prezentate, procedura deleteRoot va trata separat


cazurile:
- daca subarborele stâng este vid: promovează subarborele drept. Cazul in care si
subarborele drept este vid nu trebuie tratat separat, in acest caz se promovează arborele
vid (rad devine NULL);
- altfel daca, subarborele drept este vid: promovează subarborele stâng.
- altfel (ambii subarbori nu sunt vizi): înlocuieşte rădăcina cu cel mai mare nod din
subarborele sting.

void deleteRoot(Nod*& rad)


{
Nod* p = rad;
if( rad->stg==0) rad = rad->drt;
else if( rad->drt==0) rad = rad->stg;
else {
rad = removeGreatest (rad->stg);

  -22-
INFORMATICĂ*I* IC.06. Arbori

rad->stg = p->stg;
rad->drt = p->drt;
}
delete p;
}

Temă

1. Se citeşte de la intrare un şir de valori numerice întregi, pe o linie, separate de spaţii, şir
care se încheie cu o valoare 0.
a) Să se introducă valorile citite intr-un arbore binar de căutare (exclusiv valoarea 0 care
încheie şirul).
b) Să se afişeze în inordine conţinutul arborelui.
c) Se citeşte o valoare pentru care sa se verifice dacă este sau nu conţinută în arbore.
d) Se citeşte o valoare care să fie ştearsă din arbore şi apoi să se afişeze arborele în inordine.
e) Să se determine succesorul şi predecesorul unui nod.
f) Să se afişeze conţinutul arborelui parcurgându-l în lăţime.

Să se construiască un program C/C++ care, cu ajutorul unui meniu simplu, să permită


efectuarea operaţiilor definite mai sus.

2. Să se construiască modul C/C++ care să conţină tipurile de date şi operaţiile care


implementează sub forma unui arbore binar de căutare o agenda de numere de telefon. Un nod
al arborelui va conţine:
- numele persoanei (cheie de căutare);
- numărul de telefon;

Să se definească procedurile care:


- inserează un element în agendă;
- şterge din agendă o persoana dată;
- caută în agenda numărul de telefon al unei persoane date;
- afişează agenda în întregime.

Să se construiască un program C/C++ care cu ajutorul unui meniu simplu să permită


selectarea operaţiilor definite mai sus.

Pentru problema 1 se pot folosi şi funcţiile date în modulele de mai jos (ARBORE.H,
FUNCTII.CPP, COADAGEN.H). Primul modul conţine structura unui nod şi prototipurile
funcţiilor. Deoarece pentru parcurgerea în lăţime s-a utilizat o coadă generică este dat şi
modulul respectiv. Suplimentar se prezintă şi o funcţie pentru afişarea indentată a unui arbore.

ARBORE.H
#ifndef ARBORE_H
#define ARBORE_H

typedef int Tip_cheie;

struct Nod
{
Tip_cheie cheie; //cheia este chiar valoarea
void *info; //pointer la informatia utila
Nod *fst, *fdr;

  -23-
INFORMATICĂ*I* IC.06. Arbori

};

Nod *SearchR(Nod *r, Tip_cheie k);


Nod* Search(Nod *r, Tip_cheie k);
Nod *SearchMin(Nod *r);
Nod *SearchMax(Nod *r);
Nod* MakeNod(Tip_cheie k);
void Insert(Nod *&r, Tip_cheie k);
void InsertN(Nod *&r, Tip_cheie k);
void AfisNod(Nod *r, int nivel);
void AfisArbore(Nod *r);
Nod *creareArbore();
void inorder(Nod *r);
void DelNod(Nod *&r, Tip_cheie k);
void ParcurgereLatime(Nod *r);
Nod* Parinte(Nod *rad, Nod *r);
Nod* SuccesorNod(Nod *rad, Tip_cheie k);
Nod* PredecesorNod(Nod *rad, Tip_cheie k);

#endif;

FUNCTII.CPP
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <stdlib.h>
#include <time.h>

#include "arbore.h"
#include "coadagen.h"

// functia Search intoarce pointerul la nodul de cheie k


// sau pointerul NULL daca nu exista nodul respectiv
// varianta recursiva
Nod *SearchR(Nod *r, Tip_cheie k)
{
if(r==0 || k==r->cheie) return r;
if (k<r->cheie) return SearchR(r->fst,k);
else
return SearchR(r->fdr,k);
}

//varianta nerecursiva
Nod* Search(Nod *r, Tip_cheie k)
{
while (r!=0 && k!=r->cheie)
if(k < r->cheie) r=r->fst;
else r=r->fdr;
return r;
}

// SerchMin - intoarce referinta (pointerul) la nodul cu cheia minima


Nod *SearchMin(Nod *r)
{
while (r->fst != 0) r=r->fst;
return r;
}

// SearchMax - intoarce referinta (pointerul) la nodul cu cheia maxima


Nod *SearchMax(Nod *r)

  -24-
INFORMATICĂ*I* IC.06. Arbori

{
while (r->fdr != 0) r=r->fdr;
return r;
}

//MakeNod creaza un nod si intoarce pointerul la nodul creat


Nod* MakeNod(Tip_cheie k)
{
Nod *p;
if((p=new Nod)==0) {printf("MakeNod -eroare"); exit(1);}
p->cheie=k;
p->fst=p->fdr=0;
return p;
}

//Insert - insereaza un nod de cheie data intr-un arbore BST


//varianta recursiva
void Insert(Nod *&r, Tip_cheie k)
{
if(r==0) r=MakeNod(k);
else
if (r->cheie > k) Insert(r->fst,k);
else
if(r->cheie <k) Insert(r->fdr,k);
else printf("nod existent\n");
//daca cheile coincid nu se face inserarea
}

//Varinata nerecursiva
void InsertN(Nod *&r, Tip_cheie k)
{
if(r==0) r=MakeNod(k);
else
{
Nod *p, *p1; p=r;
while(p!=0)
{
p1=p;
if(p->cheie > k) p=p->fst;
else
if(p->cheie < k) p=p->fdr;
else //nodul exista
{ printf("nodul exista\n"); return;}
} // se iese cu p=0 si p1 indica nodul dupa care se insereaza
if(k < p1->cheie) p1->fst=MakeNod(k);
else p1->fdr=MakeNod(k);
}
}

//AfisNod - afiseaza arborele indentat


void AfisNod(Nod *r, int nivel)
{
int i;
for(i=0; i<nivel; i++) printf("\t");
if(r==0) printf("-\n"); //se tipareste "-" pentru ceva vid
else
{
printf("%d\n", r->cheie);
AfisNod(r->fst, nivel+1);
AfisNod(r->fdr, nivel+1);
}

  -25-
INFORMATICĂ*I* IC.06. Arbori

void AfisArbore(Nod *r)


{
AfisNod(r,1);
}

Nod *creareArbore()
{
Nod *r; Tip_cheie k;
r=0;
printf("Introduceti arborele (cheia coincide cu data), 0 - exit\n");
do
{
printf("cheie: "); scanf("%d",&k);
if (k==0) break;
InsertN(r,k);
} while (1);
return r;
}

void inorder(Nod *r)


{
if(r!=0)
{
inorder(r->fst);
printf("%d ", r->cheie);
inorder(r->fdr);
}
}

//DelNod - sterge nodul cu cheia k dintr-un arbore BST


void DelNod(Nod *&r, Tip_cheie k)
{
if(r==0) return; //arbore vid sau nod inexistent
if(r->cheie > k) DelNod(r->fst,k);
else
if(r->cheie < k) DelNod(r->fdr,k);
else //s-a gasit nodul cu cheia k
{
Nod *p;
if(r->fst==0) //nodul are un singur fiu: fiul drept
{
p=r; r=r->fdr; delete p;
}
else
if(r->fdr==0) //nodul are un singur fiu: fiul stang;
{
p=r; r=r->fst; delete p;
}
else // nodul are doi fii
{
int i; i=2*rand()/RAND_MAX;
if (i!=0)
{
p=SearchMin(r->fdr); //cauta nodul de cheie minima
//din subarborele drept
r->cheie = p->cheie;
DelNod(r->fdr, p->cheie);//sterge nodul respectiv
}

  -26-
INFORMATICĂ*I* IC.06. Arbori

else
{
p=SearchMax(r->fst);//caut nodul de cheie maxima
//din subarborele stang
r->cheie=p->cheie;
DelNod(r->fst, p->cheie);//sterge nodul respectiv
}
}
}
}

//ParcurgereLatime
void ParcurgereLatime(Nod *r)
{
if(r==0) return;
Queue Q; InitQ(Q);
Put(Q,(void*)r);
Nod *p;
while(!IsEmptyQ(Q))
{
p = (Nod*)Get(Q);
printf(" %d ", p->cheie);
if(p->fst != 0) Put(Q,(void*)p->fst);
if(p->fdr != 0) Put(Q,(void*)p->fdr);
}
}

//Parinte(rad, p) determina parintele nodului p din arborele cu radacina


//rad
//intoarce pointerul la parinte
Nod* Parinte(Nod *rad, Nod *r)
{
if(r==rad) return 0; //radacina n-are parinte
Nod *p, *q;
p=q=rad;
while(q != r)
{
p=q;
if(q->cheie < r->cheie)
q=q->fdr;
else
q=q->fst;
}
return p;
}

//SuccesorNod() - determina pointerul la succesorul unui nod de cheie k

Nod* SuccesorNod(Nod *rad, Tip_cheie k)


{
Nod *r;
r=Search(rad,k);
if(r==0) return 0;
if(r->fdr!=0)//daca exista fiu drept, succesorul este minimul din SAD
return SearchMin(r->fdr);
Nod*p;
p=Parinte(rad, r);
while(p!=0 && r==p->fdr)//cat timp exista parinte si r este fiu drept
{ //urc in arbore pana cand r va fi intr-un
r=p; //subarbore stang al unui nod
p=Parinte(rad, p);

  -27-
INFORMATICĂ*I* IC.06. Arbori

}
return p;
}

//PredecesorNod() - determina pointerul la predecesorul unui nod de cheie k


Nod* PredecesorNod(Nod *rad, Tip_cheie k)
{
Nod *r;
r=Search(rad,k);
if(r==0) return 0;
if(r->fst!=0)//daca exista fiu stang, predec. este maximul din SAS
return SearchMax(r->fst);
Nod*p;
p=Parinte(rad, r);
while(p!=0 && r==p->fst)//cat timp exista parinte si r este fiu stang
{ //urc in arbore pana cand r va fi intr-un
r=p; //subarbore drept al unui nod
p=Parinte(rad, p);
}
return p;
}

COADAGEN.H
#include <malloc.h>
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>

typedef struct El
{
void *info;
struct El *succ;
} ELEMENT;

struct Queue
{
ELEMENT *head, *tail; //pointeri la primul si ultimul element
};

void InitQ(Queue &q)


{
q.head = q.tail = 0;
}

int IsEmptyQ(Queue q)
{
return (q.head == 0 && q.tail== 0);
}

void* Front(Queue q)
{
if(IsEmptyQ(q)) {printf("Front -- coada vida\n"); exit(1);}
return q.head->info;
}

void* Get(Queue &q)


{
void *temp;
if(IsEmptyQ(q)) {printf("Get -- coada vida\n"); exit(1);}

temp = q.head->info;

  -28-
INFORMATICĂ*I* IC.06. Arbori

if(q.head==q.tail)
{
q.head = q.tail = 0;
return temp;
}
else
{
ELEMENT *p;
p=q.head;
q.head=p->succ;
free(p);
return temp;
}
}

void Put(Queue &q, void *x)


{
ELEMENT *p;
if((p=(ELEMENT*)malloc(sizeof(ELEMENT)))==0)
{printf("Put -- eroare alocare"); exit(1);}
p->info=x;
p->succ=NULL;
if(IsEmptyQ(q))
{q.head=q.tail=p;}
else
{
q.tail->succ=p;
q.tail=p;
}
}

IC.06.6 Arbori binari de căutare AVL – echilibraţi

IC.06.6.1 Introducere

Un arbore binar de căutare este AVL echilibrat dacă pentru fiecare nod, diferența
dintre adâncimea subarborelui stâng şi cea a subarborelui drept este cel mult 1. Numele AVL
provine de la iniţialele numelor celor ce l-au descoperit: Adelsohn-Velskii şi Landis. Deci
arborii AVL sunt arbori echilibraţi după înălţime (reamintim că există şi echilibrări după
greutate sau ponderi).
S-a demonstrat că adâncimea (înălţimea) h a unui arbore AVL echilibrat cu n noduri
satisface relaţia:

log2 (n + 1) ≤ h ≤ 1.4404 ∗ log2 (n + 2) − 0.328

Optimul este atins pentru arborii total echilibraţi. Din relaţia anterioară rezultă că
parcurgerea unei ramuri într-un arbore binar de căutare echilibrat AVL se va face in O(log n)
paşi.
Factorul de echilibru bf (balance factor) este definit prin diferenţa dintre adâncimea
(înălţimea) subarborelui stâng şi adâncimea (înălţimea) subarborelui drept:

bf = h (Ts ) − h (Td )

  -29-
INFORMATICĂ*I* IC.06. Arbori

şi poate avea valorile: −1, 0, + 1 .


Operaţia de căutare într-un arbore binar de căutare care are şi proprietatea de a fi
echilibrat este o operaţie de complexitate O(log n). Se pune problema construirii operaţiilor
insert şi delete astfel încât ele să conserve proprietatea de arbore binar echilibrat dar, în
acelaşi timp, să fie la rândul lor operaţii O(log n). Pentru a realiza acest lucru se memorează în
structura unui nod şi factorul de echilibru (uneori se foloseşte şi terminologia „factor de
dezechilibru”).

IC.06.6.2 Metode de echilibrare

La inserare sau ștergere, arborele trebuie reorganizat modificându-i-se structura,


numai atunci când inserarea sau ștergerea dezechilibrează un nod ( bf > 1) (care deja este
dezechilibrat în acelaşi sens). Operaţiile prin care este realizată echilibrarea sunt denumite
rotaţii și sunt descrise mai jos.
În următoarele figuri s-a reprezentat:

- un nod al arborelui

- un subarbore de adâncime h

- un subarbore de adâncime h-1

Rotațiile se aplică arborilor (subarborilor) care, în urma inserării sau ștergerii, au


nodul rădăcină dezechilibrat, dar toți subarborii sunt echilibrați.
La inserarea unui nod X intr-un arbore, sunt patru cazuri posibile care necesită
efectuarea unei rotații:

  -30-
INFORMATICĂ*I* IC.06. Arbori

și

Rotații simple

a) Rotație simplă dreapta

Starea inițială Starea finală

Subarborii T1, T2 și T3 au inițial aceeași adâncime h, iar nodul A are factorul de


dezechilibru -1. Prin inserarea unui nod în subarborele T1 se mărește adâncimea acestuia și
nodul A devine dezechilibrat. Prin operația numita „rotație simplă dreapta” se obține un
arbore care respecta inegalitatea valabila pentru starea inițială:

T1 < B < T2 < A < T3

ceea ce înseamnă că toate nodurile din subarborele T1 sunt etichetate cu valori mai mici decât
eticheta (cheia) lui B, B este mai mic decât etichetele tuturor nodurilor din subarborele T2,
etc.
Arborele rezultat în urma rotației este echilibrat și are aceeași adâncime cu arborele inițial,
adică are adâncimea (înălțimea): h + 2 .

  -31-
INFORMATICĂ*I* IC.06. Arbori

b) Rotație simplă stânga

Se obține din situația simetrică celei prezentate mai sus:

Starea inițială Starea finală

Rotații duble

Situațiile rezolvate de rotațiile duble sunt:

Pentru a le analiza este nevoie să reprezentăm detaliat subarborele T2. Deoarece acesta are
adâncimea h rezultă că are subarbori de adâncime h − 1 .

  -32-
INFORMATICĂ*I* IC.06. Arbori

Rotațiile duble au ca efect echilibrarea arborelui indiferent de subarborele (t2s sau t2d) în
care este inserat nodul X.

a) Rotație dublă stânga-dreapta

Starea inițială Starea finală

Numele de rotație dublă rezultă din faptul că starea finală poate fi atinsă aplicând două rotații
simple:
A doua rotație
Starea inițială Prima rotație (simplă dreapta pt. A)
(simpla stânga pt. B) Starea finală

Inițial subarborii T1 și T3 au adâncimea h iar t2s si t2d au adâncimea h − 1 . Valoarea


inserată va mări adâncimea unuia din subarborii t2s sau t2d. În ambele cazuri se aplică mai
intâi o rotație simplă spre stânga nodului B și apoi o rotație simplă spre dreapta nodului A.
Rezultă un arbore echilibrat indiferent în care din subarborii t2s si t2d s-a făcut inserarea.
Procedeul este corect și atunci când nodul inserat este nodul C și fiecare din subarborii T1,
t2s, t2d, și T3 este arbore vid.
Din nou trebuie notat faptul ca adâncimea arborelui rezultat este egală cu cea a
arborelui inițial.

  -33-
INFORMATICĂ*I* IC.06. Arbori

b) Rotație dublă dreapta-stânga

Se obține prin simetrie fată de situația de mai sus.

Starea inițială Starea finală

Aplicație

În cele ce urmează se prezintă un program simplu C/C++ care implementează


operațiile prezentate.

#include <conio.h>
#include <stdio.h>

typedef int Atom;


struct NAVL{
Atom data;
char bf;
NAVL *fst, *fdr;
};

//adancimea unui arbore


int depth(NAVL *a)
{
if(a==0) return 0;
int ms=1, md=1;
ms = ms + depth(a->fst);
md = md + depth(a->fdr);
return (ms<md) ? md : ms;
}

//Rotatie simpla stanga


NAVL *RotSStanga(NAVL *a)
{
NAVL *b;
b = a->fdr;
a->fdr = b->fst;
b->fst = a;
a->bf = depth(a->fst) – depth(a->fdr); //actualizeaza bf

  -34-
INFORMATICĂ*I* IC.06. Arbori

b->bf = depth(b->fst) – depth(b->fdr);


return b; //b devine noua rad
}

//Rotatie simpla dreapta


NAVL *RotSDreapta(NAVL *a)
{
NAVL *b;
b = a->fst;
a->fst = b->fdr;
b->fdr = a;
a->bf = depth(a->fst) – depth(a->fdr);
b->bf = depth(b->fst) – depth(b->fdr);
return b; //b devine noua rad
}

//rotatie dubla dreapta (stanga-dreapta)


NAVL *RotDStDr(NAVL *a)
{
a->fst = RotSStanga(a->fst);
a = RotSDreapta(a);
return a;
}

//rotatie dubla stanga (dreapta-stanga)


NAVL *RotDDrSt(NAVL *a)
{
a->fdr = RotSDreapta(a->fdr);
a = RotSStanga(a);
return a;
}

NAVL *InsAVL(NAVL *a, Atom x)


{
if (a==0)
{
a=new NAVL;
a->data = x; a->fst=a->fdr=0; a->bf=0;
return a;
}
else
{
if(x < a->data) //insereaza in ramura stanga
{
a->fst = InsAVL(a->fst, x);
if((depth(a->fst) – depth(a->fdr)) == 2)
a = (x < a->fst->data) ? RotSDreapta(a) : RotDStDr(a);
else
a->bf = depth(a->fst) – depth(a->fdr); //actualizeaza bf
}
else
if(x > a->data) //insereaza in ramura dreapta
{
a->fdr = InsAVL(a->fdr, x);
if((depth(a->fst) – depth(a->fdr)) == -2)
a = (x > a->fdr->data) ? RotSStanga(a):RotDDrSt(a);
else
a->bf = depth(a->fst) – depth(a->fdr); //actualizez bf
}
}

  -35-
INFORMATICĂ*I* IC.06. Arbori

return a;
}

//AfisNod – afiseaza arborele indentat


void AfisNod(NAVL *r, int nivel)
{
int i;
for(i=0; i<nivel; i++) printf(„\t”);
if(r==0) printf(„-\n”); //se tipareste „-„ pentru ceva vid
else
{ printf(„%d\n”, r->data);
AfisNod(r->fst, nivel+1);
AfisNod(r->fdr, nivel+1);
}
}

void AfisArbore(NAVL *r)


{
AfisNod(r,1);
}

void creareArbore(NAVL *&r)


{
Atom k;
printf(„Introduceti arborele (cheia coincide cu data), 0 – exit\n”);
do
{ printf(„data: „); scanf(„%d”, &k);
if (k==0) break;
r=InsAVL(r,k);
} while (1);
}

void main ()
{
Atom val;
NAVL *rad; rad=0;
creareArbore(rad);
AfisArbore(rad);
printf(„\nIntroduceti valoarea de inserat: „);
scanf(„%d”, &val);
rad = InsAVL(rad, val);
AfisArbore(rad);
getch();

IC.06.7 Arbori binari de căutare optimali

IC.06.7.1 Introducere

În cazul arborilor binari de căutare (BST – Binary Search Tree), pentru un nod (sau
cheie) oarecare, timpul de căutare este proporţional cu adâncimea nodului (distanţa de la
acesta la rădăcină). Evident, unele chei sunt căutate mai des altele mai rar. Ar fi convenabil ca
acele chei care sunt căutate mai des să fie plasate mai aproape de rădăcina arborelui, iar acele
chei care sunt căutate mai rar să fie plasate mai departe de rădăcina arborelui.

  -36-
INFORMATICĂ*I* IC.06. Arbori

Dacă pentru fiecare dintre cheile unui arbore binar ordonat cunoaştem frecvenţa cu
care această cheie va apărea în cadrul operaţiilor ulterioare de căutare, se pot construi arbori
binari optimi pentru care cheile mai des căutate să fie plasate mai aproape de rădăcină.
Frecvenţa de căutare poate fi privită şi ca “probabilitatea” ca o anumită cheie să fie ţinta unei
operaţii viitoare de căutare în BST.
Pentru un arbore binar ordonat conţinând cheile k1 , k2 ,K , kn , se consideră că fiecare
cheie ki are asociată o “probabilitate de apariţie” pi , respectând proprietăţile:

0 ≤ pi ≤ 1, unde i = 1,K , n
p1 + p2 + K + pn = 1

Arborele binar ordonat poate fi numit “optim” (OBST) dacă:

∑ p gnivel(k ) = minim
i
i i (1)

Suma anterioară poartă numele de cost al arborelui sau drum ponderat, fiind suma drumurilor
de la rădăcina la fiecare nod, ponderate cu probabilităţile de acces la noduri.

Exemplu. Pentru un BST cu nodurile de chei 4, 5, 8, 10, 11, 12, 14 există mai multe topologii
care să respecte proprietatea BST, printre care şi reprezentările din figura de mai jos.

10   10  

 5   14    5   12  

 4    8   11    4    8   11   14  

12  
a) b)
Fig. 1. Topologii ale unui BST

Deşi reprezentarea din figura b) corespunde unui AVL, acest fapt nu garantează faptul
că arborele este optimal în sensul definiţiei de mai sus. Arborele din figura a) poate fi optimal
numai dacă frecvenţele de acces ale nodurilor sunt mai mari pentru nodurile de pe niveluri
mici şi sunt mici pentru nodurile de pe niveluri mari. Putem admite că este posibil ca un BST
degenerat să fie unul optimal.
Mai apare o problemă: pe lângă frecvenţele de accesare a nodurilor existente în arbore
(căutări încheiate cu succes), cum putem evidenţia căutările unor chei ce nu fac parte din
arbore (căutări eşuate) şi cum le vom lua în considerare în construirea unui OBST?

  -37-
INFORMATICĂ*I* IC.06. Arbori

Pentru o astfel de abordare, vom porni de la Fig. 1 a) şi vom construi arborele extins
corespunzător (fig. 2).

10

5 14

4 8 11 E7

E0 E1 E2 E3 E4 12

E5 E6

Fig. 2. Arborele OBST extins

Un BST extins se obţine dintr-un BST prin adăugarea nodurilor succesoare Ei (noduri
extinse sau fictive - marcate cu pătrate in fig. 2) la toate frunzele din arborele de la care am
plecat. Un nod Ei corespunde tuturor căutărilor eşuate pentru valori cuprinse în intervalul
cheilor ( ki , ki +1 ) . Astfel, timpul de căutare pentru un element oarecare x este dat fie de
adâncimea nodului de cheie x fie de adâncimea pseudonodului corespunzător intervalului
dintre noduri care ar putea să-l conţină pe x. Notând cu qi probabilitatea de a căuta un nod de
cheie x ce satisface relaţia ki < x < ki +1 , atunci costul unui BST se defineşte:

n n
C= ∑ p gnivel (k ) + ∑ q g(nivel ( E ) − 1)
i =1
i i
i =0
i i (2)

Expresia (nivel(Ei)-1) în a doua sumă se justifică prin faptul că încadrarea unui nod x într-un
interval se decide la nivelul părintelui unui pseudonod nu la nivelul pseudonodului. Pentru un
BST optimal, valoarea lui C este minimă.
Orice subarbore BST conţine cheile ordonate în domeniul ki L k j pentru 1 ≤ i ≤ j ≤ n
şi va avea propriile noduri fictive Ei-1 … Ej. Problema construirii unui OBST revine la a
construi subarbori optimali. Dacă kr este rădăcina unui arbore optimal cu n chei, una din
cheile k1 ,K , kr −1 va fi rădăcina subarborelui stâng şi una din cheile kr +1 ,K , kn va fi rădăcina

  -38-
INFORMATICĂ*I* IC.06. Arbori

subarborelui drept (a se vedea figura de mai jos). Dacă rădăcinile celor doi subarbori se aleg
astfel încât aceştia să fie optimali avem garanţia că arborele de rădăcină kr este optimal.

kr
p1...pr-1 pr+1...pn
q0...qr-1 qr...qn

k1...kr-1 kr+1...kn

C(1,r-1) C(r+1,n)
Fig. 3. Subarbore optimal

Ce se întâmplă când avem subarbori nuli? Dacă într-un subarbore cu cheile ki ... kj
găsim cheia ki ca fiind rădăcina subarborelui optimal vom aprecia că subarborele stâng
conţine doar nodul fictiv Ei-1 şi deci costul unei căutări pentru o cheie de valoare mai mică
decât ki va depinde doar de qi-1.
Dacă se notează cu C(i,j) costul unui subarbore optimal cu cheile ki, . . . kj, relaţia de
calcul este:
⎧⎪ k −1
⎡ ⎤
C(i,j) = min ⎨ pk + ⎢ qi-1 + ( pm + qm ) + C ( i,k − 1)⎥

i≤k ≤ j
⎩⎪ ⎣ m =i ⎦
j
⎡ ⎤ ⎫
+ ⎢ qk + ∑ ( pm + qm ) + C ( k + 1,j)⎥⎬⎪
⎣⎢ m = k +1 ⎦⎥ ⎪⎭ (3)
j
⎪⎧ ⎪⎫
= min ⎨C ( i,k − 1) + C ( k + 1,j) + qi-1 + ( pm + qm )⎬ ∑
i≤k ≤ j
⎩⎪ m =i ⎭⎪

În relaţia (3), i≥1, j≤n şi j≥i-1. Cazul j=i-1 corespunde situaţiei subarborelui vid când se ia în
considerare doar Ei-1. Ultimul cost care va fi calculat este C(1,n):

⎧⎪ k −1 n
⎡ ⎤ ⎡ ⎤ ⎫⎪
C(1,n) = min ⎨ pk + ⎢ q0 + ( pi + qi ) + C (1,k − 1)⎥ + ⎢ qk +
∑ ∑( pi + qi ) + C ( k + 1,n )⎥ ⎬ (4)
1≤ k ≤ n
⎩⎪ ⎣ i =1 ⎦ ⎣ i = k +1 ⎦ ⎭⎪

Această relaţie de recurenţă evidenţiază faptul că atunci când un arbore de cost minim devine
subarbore al unui alt nod, nivelul (adâncimea) fiecărui nod din subarbore creşte cu 1 iar costul
calculat anterior creşte cu suma tuturor probabilităţilor din subarbore.
Dacă kr este rădăcina unui subarbore optimal de chei ki, . . . kj, relaţia (3) poate fi rescrisă:

  -39-
INFORMATICĂ*I* IC.06. Arbori

C(i,j) = pr + ( C (i,k − 1) + w(i, r − 1) ) + ( C ( k + 1,j) + w(r + 1, j ) )


cu w(i, j ) = w(i, r − 1) + pr + w(r + 1, j ) (5)
rezulta C (i, j ) = C (i, r − 1) + C (r − 1, j ) + w(i, j )

Relaţia de recurenţă de mai sus presupune că se ştie nodul de cheie kr ce urmează a fi


rădăcină. Alegerea nodului rădăcină care va garanta cel mai mic cost conduce la următoarea
relaţie pentru calculul recursive:

⎪⎧min{C (i, r − 1) + C (r + 1, j ) + w(i, j )}, daca i ≤ j; i ≤ r ≤ j⎪⎫


c(i, j ) = ⎨ ⎬ (6)
⎩⎪ q j −1 , daca j=i-1 ⎭⎪

Pentru a putea construi structura OBST (evidenţa legăturilor între noduri şi


descendenţi) se defineşte o matrice R(i,j) (1≤i≤j≤n) care va memora indexul r pentru care kr
este rădăcina unui OBST de chei ki ... kj.

Generarea un OBST

O metodă “primitivă” de generare a unui OBST pentru un set de chei ar fi să se


genereze toţi arborii de căutare posibili, să se calculeze costul fiecăruia şi să se reţină arborele
de cost minim. Evident o astfel de abordare nu este fezabilă deoarece presupune un ordin de
complexitate exponenţial.
Din cele prezentate anterior este evident că un algoritm recursiv este o soluţie realistă.
Pentru implementare se construiesc următoarele matrice:

Wi,j – matricea ponderilor care se obţine cu relaţia:


j j
Wi , j = ∑ pk + ∑q k .
k =i +1 k =i

Matricea W conţine pe diagonala principală elementele qj.


Ci,j – matricea costurilor pentru OBST, ale cărei elemente se calculează conform relaţie (6):

Ci ,i = Wi ,i
;
Ci , j = Wi , j + min i <k ≤ j (Ci ,k −1 + Ck , j )

Ri,j – matricea ce conţine indexul cheii pentru care se obţine un minimum corespunzător în
matricea Ci,j.

OBST(i,j) - reprezintă arborele optimal ce conţine cheile ki,....kj. Fiecare subarbore OBST(i,j)
va avea rădăcina kRi,j şi subarborii OBST(i,k-1) şi OBST(k+1,j).

Exemplu de generare a unui OBST

Până acum, teoria prezentată a făcut referire la probabilităţile pi de căutare cu succes a


unei chei care se află într-un arbore ordonat şi qi probabilităţile “eşuate” de a nu găsi în arbore
cheia căutată. Aceste probabilităţi se calculează uşor dacă se cunosc frecvenţele de accesare

  -40-
INFORMATICĂ*I* IC.06. Arbori

ale nodurilor (cu sau fără succes) şi numărul total de noduri. In exemplu următor, vor fi
utilizate frecvenţele de căutare, lăsând în seama studentului calcularea probabilităţilor (fapt ce
nu este obligatoriu).
Se va genera un arbore de căutare optimal, cu 6 noduri şi cu următoarele frecvenţe de
căutare a cheilor şi “între chei”:

Index 0 1 2 3 4 5 6
K
3 7 10 15 20 25
(Valoare cheie)
p - 10 3 9 2 0 10
q 5 6 4 4 3 8 0

Pas1. Se calculează matricea ponderilor W cu relaţiile de mai sus. Astfel se obţine:

W (0, 0) = q0 = 5 W (0, 1) = q0 + q1 + p1 = 5 + 6 + 10 = 21
W (1, 1) = q1 = 6 W (0, 2) = W (0, 1) + q2 + p2 = 21 + 4 + 3 = 28
W (2, 2) = q2 = 4 W (0, 3) = W (0, 2) + q3 + p3 = 28 + 4 + 9 = 41
W (3, 3) = q3 = 4 W (0, 4) = W (0, 3) + q4 + p4 = 41 + 3 + 2 = 46
W (4, 4) = q4 = 3 W (0, 5) = W (0, 4) + q5 + p5 = 46 + 8 + 0 = 54
W (5, 5) = q5 = 8 W (0, 6) = W (0, 5) + q6 + p6 = 54 + 0 + 10 = 64
W (6, 6) = q6 = 0 W (1, 2) = W (1, 1) + q2 + p2 = 6 + 4 + 3 = 13 .... şi
rezultă:

W 0 1 2 3 4 5 6
0 5 21 28 41 46 54 64
1 6 13 26 31 39 49
2 4 17 22 30 40
3 4 9 17 27
4 3 11 21
5 8 18
6 0

Deoarece suma elementelor vectorului p este 34 şi suma elementelor vectorului q este 30,
suma tuturor accesărilor este 64, iar matricea W rescrisă în termeni de probabilitate este W':

W' 0 1 2 3 4 5 6
0 0.08 0.33 0.44 0.64 0.72 0.84 1.00
1 0.09 0.20 0.41 0.48 0.61 0.77
2 0.06 0.27 0.34 0.47 0.63
3 0.06 0.14 0.27 0.42
4 0.05 0.17 0.33
5 0.13 0.28
6 0.00

Pas. 2. Elementele de pe diagonala principală a matricei de cost sunt egale cu cele ale
diagonalei principale din matricea ponderilor. Elementele de deasupra diagonalei principale se
calculează cu relaţiile:
C (0, 1) = W (0, 1) + (C (0, 0) + C (1, 1)) = 21 + 5 + 6 = 32
C (1, 2) = W (1, 2) + (C (1, 1) + C (2, 2)) = 13 + 6 + 4 = 23
C (2, 3) = W (2, 3) + (C (2, 2) + C (3, 3)) = 17 + 4 + 4 = 25
C (3, 4) = W (3, 4) + (C (3, 3) + C (4, 4)) = 9 + 4 + 3 = 16

  -41-
INFORMATICĂ*I* IC.06. Arbori

C (4, 5) = W (4, 5) + (C (4, 4) + C (5, 5)) = 11 + 3 + 8 = 22


C (5, 6) = W (5, 6) + (C (5, 5) + C (6, 6)) = 18 + 8 + 0 = 26
Tot în acest pas, elementele de deasupra diagonalei principale din R se completează cu
valorile indexului i din C(i,j).

Pas 3. În continuare se calculează celelalte elemente ale matricei C conform relaţiilor de mai
sus şi se completează a doua diagonală din matricea R cu indexul k aferent valorii de cost
minim – acele valori sunt subliniate in relaţiile de mai jos şi corespund valorilor minime
scrise cu roşu.
C (0, 2) = W (0, 2) + min (C (0, 0) + C (1, 2), C (0, 1) + C (2, 2)) = 28 + min (28, 36) = 56
C (1, 3) = W (1, 3) + min (C (1, 1) + C (2, 3), C (1, 2) + C (3, 3)) = 26 + min (31, 27) = 53
C (2, 4) = W (2, 4) + min (C (2, 2) + C (3, 4), C (2, 3) + C (4, 4)) = 22 + min (20, 28) = 42
C (3, 5) = W (3, 5) + min (C (3, 3) + C (4, 5), C (3, 4) + C (5, 5)) = 17 + min (26, 24) = 41
C (4, 6) = W (4, 6) + min (C (4, 4) + C (5, 6), C (4, 5) + C (6, 6)) = 21 + min (29, 22) = 43

C 0 1 2 3 4 5 6 C 0 1 2 3 4 5 6
0 5 32 0 5 32 56
1 6 23 1 6 23 53
2 4 25 2 4 25 42
3 4 16 3 4 16 41
4 3 22 4 3 22 43
5 8 26 5 8 26
6 0 6 0
Matricea costurilor după iteraţia a doua şi a treia

Calculele continuă în aceeaşi manieră şi în final se obţin matricele C şi R de mai jos:

Lungimea medie a drumului de căutare, numită şi costul arborelui de căutare sau lungimea de
cale ponderată este egală cu max(C(i,j))/max(W(i,j))=C(0,6)/W(0,6).

Pas 4. Construirea arborelui. Arborele de căutare optimal obţinut este prezentat în figura 4 de
mai jos şi are un drum ponderat de 2,93=188/64. Calcul poziţiilor nodurilor în arbore se face
astfel:
• rădăcina arborelui optimal are indexul cheii dat de R (0, 6)=3 → K3 este cheia
rădăcinii de valoare 10 şi frecvenţă de accesare 9;
• In general, dacă indexul rădăcinii unui arbore optimal este pe poziţia (i,j), atunci
indexul cheii rădăcinii subarborelui stâng este pe poziţia (i,R(i,j)-1). In cazul
considerat, indexul subarborelui stâng este dat de R(0, 2) =1 →K1 este cheia nodului
de valoare 3 şi frecvenţă de accesare 10;

  -42-
INFORMATICĂ*I* IC.06. Arbori

dacă indexul rădăcinii unui arbore optimal este pe poziţia (i,j), atunci indexul cheii

rădăcinii subarborelui drept este pe poziţia (R(i,j),j). In exemplul nostru, indexul
subarborelui drept al rădăcinii este dat de R(3, 6) =6 →K6 este rădăcina subarborelui
drept de valoare 25 şi frecvenţă de accesare 10.
Mai departe arborele se construieşte după aceleaşi reguli.

Fig. 4. OBST obţinut pentru exemplul numeric considerat

Indicaţii de implementare

În continuare sunt prezentate declaraţiile şi funcţiile propuse pentru implementarea unei


aplicaţii care creează şi afişează un OBST.

Declaratii

#define nmax 20 //nr max de noduri (chei)

//structura arborelui optimal


struct OBST
{ char KEY;
OBST *left, *right;
};

int C[nmax][nmax]; //matrice cost


int W[nmax][nmax]; //matrice ponderi
int R[nmax][nmax]; //radacinile subarborilor optimali. Va contine
//indecsii cheilor din vectorul KEYS,
//corespunzator radacinilor subarborilor
int q[nmax]; //frecventa de cautare intre chei=esec in ce
//priveste cautarea
int p[nmax]; //frecventa de cautare a cheilor
int nr_keys; //numarul de noduri (chei)
char KEYS[nmax]; //

//Functia pentru calculul matricelor W, C si R


void Matrix()
{
int x, min; int i, j, k, h,m;
//construiesc matricea de ponderi W
for(i = 0; i <= nr_keys; i++)
{ ...................
}
//Construesc matricea cost C si matricea R

  -43-
INFORMATICĂ*I* IC.06. Arbori

for(i = 0; i <= nr_keys; i++)


//relatia pentru diagonala principala din C;
.......................
for(i = 0; i <= nr_keys -1; i++)
{
j = i + 1;
//relatia pentru a doua diagonală din C (deasupra celei cunoscute)
.......................................
//relatia pentru diagonala matricei R
.......................................
}
//completez matricele C si R
for(h = 2; h <= nr_keys; h++)
for(i = 0; i <= nr_keys -h; i++)
{
j = i + h; m = R[i][j-1]; min = C[i][m-1] + C[m][j];
for(k = m+1; k <= R[i+1][j]; k++)
{ //caut minim si index pentru minim (m)
x = C[i][k-1] + C[k][j];
if(x < min)
{ m = k;
min = x;
}
}
C[i][j] = W[i][j] + min;
R[i][j] = m; //indexul lui k pentru care s-a obtinut
//minimum se pune in R
}
//afisez W
cout<<"\nMatricea W\n";
...........................................
//Afisez C
cout<<"\n Matricea cost"<<endl;
............................................
//Afisez R
cout<<"\n Matricea R:\n";
........................................

//Construiesc BST optimal


OBST *Build_OBST(int i, int j)
{ OBST *p;
if(i == j) p = NULL;
else
{ p = new OBST; //se aloca memorie
p->KEY = KEYS[R[i][j]]; //pun valoarea cheii
p->left = Build_OBST(i, R[i][j] -1); //subarbore sting
p->right = Build_OBST(R[i][j], j); //subarbore drept
}
return p;
}

In main se citesc numarul de chei (nr_keys), vectorii KEYS, p (de la 1 la nr_keys) şi q (de la 0
la nr_keys) apoi se apelează funcţia Buld_OBST(0,nr_keys) şi cea de afişare indentată a
OBST.

  -44-
INFORMATICĂ*I* IC.06. Arbori

Temă

1. Să se creeze şi să afişeze un OBST cu chei de tip char, pe baza frecvenţelor de căutare.


2. Modificaţi aplicaţia astfel încât arborele optimal să fie creat pe baza probabilităţilor de
accesare. Să se afişeze drumul mediu.
3. Care este ordinul de complexitate pentru generarea unui OBST? Justificaţi.

IC.06.8 Arbori parţial ordonaţi (arbore heap)

IC.06.8.1 Reprezentarea implicită a arborilor binari

În reprezentarea implicită, structura arborelui nu este specificată prin informații de


înlănțuire (pointeri), ci prin poziția în care nodurile apar intr-un vector. Astfel, rădăcina
arborelui va fi memorata în elementul V[1], descendenții acesteia în V[2] si V[3],
descendenții nodului memorat în V[2], vor fi memorați în V[4] și V[5] iar cei ai nodului
memorat în V[3] în V[6] și V[7], si așa mai departe.
Pentru un nod care este memorat în poziția i fiul sting va fi memorat in poziția 2*i iar
cel drept în poziția 2*i+1:

╔═══════════╤════════════╤═════════════════╤═════════════════╗
║Pozitie nod│Pozitie tata│Pozitie fiu sting│Pozitie fiu drept║
╠═══════════╪════════════╪═════════════════╪═════════════════╣
║ i │ [i/2] │ 2*i │ 2*i+1 ║
╚═══════════╧════════════╧═════════════════╧═════════════════╝

De exemplu, arborele:

va fi reprezentat implicit:

│ A │ B │ C │ D │ E │ F │ G │
└───┴───┴───┴───┴───┴───┴───┘
1 2 3 4 5 6 7

Atunci când arborele nu este complet nodurile lipsă vor fi înlocuite cu o valoare speciala care
indică lipsa nodului.
De exemplu, arborele:

va fi reprezentat implicit:

  -45-
INFORMATICĂ*I* IC.06. Arbori

│ A │ B │ C │ - │ - │ F │ G │
└───┴───┴───┴───┴───┴───┴───┘
1 2 3 4 5 6 7

Pentru arbori dezechilibrați reprezentarea implicită face risipă de spațiu de memorare:

Arborele:

va fi reprezentat implicit:

│ A │ B │ C │ - │ - │ D │ E │ - │ - │ - │ - │ - │ F │ G │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
1 2 3 4 5 6 7 9 10 11 12 13 14 15

Dimensiunea vectorului necesar pentru a memora un anumit arbore este mai mică sau egală
cu:
2ad − 1

unde ad este adâncimea arborelui.

Observație: În C/C++ indicele primului element din vector este 0. Modul în care s-a definit
reprezentarea implicită a arborilor binari nu permite reprezentarea rădăcinii arborelui in V[0],
deoarece 0*2 = 0. Se poate alege una din următoarele soluții:
- elementul de vector V[0] să rămână neîntrebuințat;
- să adaptăm relațiile:

╔═══════════╤════════════╤═════════════════╤═════════════════╗
║Pozitie nod│Pozitie tata│Pozitie fiu sting│Pozitie fiu drept║
╠═══════════╪════════════╪═════════════════╪═════════════════╣
║ i │[(i+1)/2]-1 │ 2*(i+1)-1 │ 2*(i+1) ║
╚═══════════╧════════════╧═════════════════╧═════════════════╝

Pentru simplitate, în exemplele următoare, alegem prima soluție.

Pentru exemplificarea modului de utilizare a reprezentării implicite a arborilor binari,


în continuare este prezentată parcurgerea unui astfel de arbore in inordine. Presupunem
următoarele declarații de date globale:

char A[DIMMAX+1];
//Vectorul in care este plasata reprezentarea implicita
int n;

  -46-
INFORMATICĂ*I* IC.06. Arbori

//Indicele ultimului nod care apare memorat in vector.


//Daca structura arborelui nu prezinta "goluri" n este
//numarul de noduri din arbore

Parcurgerea în inordine:

void inordine(int i)
{
if (i<=n && (A[i]!='-'){
inordine(i*2);
prelucrare(A[i]);
inordine(i*2+1);
}
}

Această procedură va fi apelată din programul principal cu linia:

inordine(1); //1 este pozitia in vector a radacinii

IC.06.8.2 Arbori heap, operații caracteristice

Proprietatea care definește structura unui arbore heap este următoarea: Valoarea cheii
memorate în rădăcină este mai mare decât toate valorile cheilor conținute în subarborii
descendenții. Această proprietate trebuie să fie îndeplinită pentru toți subarborii, de pe orice
nivel în arborele heap.

Exemplu:

Aplicațiile principale ale arborilor heap sunt:


- implementarea tipului de date abstract "coada cu prioritate".
- algoritmul de sortare HeapSort.

O coadă cu prioritate este o structură de date de tip "container" pentru care se


definesc următoarele funcții de acces:
INSERT(C, X) - pune în coada C atomul X;
REMOVE(C) - scoate din coada C și returnează atomul cu valoarea cea mai mare a
cheii.
Arborii heap pot fi reprezentați explicit (reprezentarea standard a arborilor binari) sau
implicit. Reprezentarea implicita a arborilor heap este cea mai folosita, si ea va face obiectul
celor prezentate in continuare.
Analizăm în continuare operațiile INSERT și REMOVE pentru arbori heap în
reprezentare implicită. Arborii heap la care ne referim sunt formați din k nivele complete iar
pe nivelul k+1 nodurile sunt grupate în partea stângă:

  -47-
INFORMATICĂ*I* IC.06. Arbori

Rezultă, în reprezentarea implicită, un vector fără "goluri" de dimensiune N:

Operația INSERT
Se adaugă X la sfârșitul vectorului și apoi se promovează spre rădăcina până ajunge în
poziția în care structura de heap a arborelui în reprezentarea implicită este respectată.

De exemplu, se inserează valoarea 35 în heapul de mai sus:

Pentru a păstra structura arborelui valoarea 35 trebuie promovata până pe nivelul 2. Valorile
de pe porțiunea din ramura parcursă vor fi retrogradate cu un nivel. Rezultă arborele:

Modificările sunt aplicate de fapt vectorului cu reprezentarea implicită:

  -48-
INFORMATICĂ*I* IC.06. Arbori

Vectorul

În algoritmul următor promovarea valorii inserate se face "din aproape în aproape", aceasta
urcând în arbore din nivel în nivel.

INSERT(A,N,X)// N trebuie sa fie pasat prin referinta


{
A[N+1] = X //adauga valoarea de inserat la sfirsitul vectorului
N = N+1
fiu = N
parinte = N/2 // impartire intreaga
WHILE parinte>=1 DO // parcurge ramura spre radacina
│IF A[parinte]<A[fiu] THEN
│ │SCHIMBA(A[parinte], A[fiu]); //retrogradeaza A[parinte]
│ │fiu := parinte; │
│ │parinte := parinte div 2; │ // urca spre radacina
│ELSE parinte := 0; // pentru parasirea buclei
}

Operația REMOVE
Operația REMOVE șterge din heap valoarea cea mai mare, valoare care se află
întotdeauna în rădăcina arborelui heap, deci în prima poziție a heap-ului. Pentru a reface
structura de heap se aduce în prima poziție ultimul element din vector și apoi se retrogradează
până se reface structura de heap. La retrogradarea cu un nivel valoarea din rădăcina este
schimbată cu descendentul care are valoarea maximă.

  -49-
INFORMATICĂ*I* IC.06. Arbori

Algoritmul este următorul:

REMOVE(A,N) // N trebuie sa fie pasat prin referinta


{
IF N=0 THEN eroare
ELSE │ret_val = A[1]; //Valoarea de returnat
│A[1] = A[N]; //Aduce ultimul element in radacina
│N = N-1; //Micsoreaza heapul
│parinte = 1;
│fiu = 2;
│WHILE fiu<=N DO //parcurge o ramura in sens descendent
│ │IF (fiu+1<=N) and (A[fiu]<A[fiu+1]) THEN
│ │ fiu = fiu+1; // este ales fiu cel mai mare
│ │IF A[fiu]>A[parinte] THEN // daca parinte<max(fii)
│ │ │SCHIMBA(A[parinte],A[fiu]); // retrogradeaza
│ │ │parinte := fiu; │
│ │ │fiu := fiu*2; │ // coboara
│ │ELSE fiu=N+1; // pentru parasirea buclei
}

IC.06.8.3 Algoritmul Heapsort

Heapsort este un algoritm de sortare care nu necesită memorie suplimentară (sortează


vectorul pe loc), care funcționează în doua etape:
[A] transformarea vectorului de sortat într-un arbore heap;
[B] reducerea treptată a dimensiunii heap-ului prin scoaterea valorii din rădăcina.
Valorile rezultate în ordine descrescătoare sunt plasate în vector pe spațiul eliberat, de la
dreapta la stânga.

[A] Transformarea vectorului de sortat într-un arbore heap

Construirea heap-ului se poate face urmând două strategii:


[A1] Construirea heap-ului de sus în jos
Vectorul va fi parcurs de la stânga la dreapta, corespunzător unei deplasări în arbore de sus în
jos. Pentru adăugarea elementului din pozitia i în vector se presupune că elementele A[1..i-1]
formează un heap.

Adăugarea elementului A[i] este realizată prin intermediul operației INSERT:

BUILD_HEAP(A,N){
FOR i = 2 TO N DO
INSERT(A,i-1,A[i])
}

  -50-
INFORMATICĂ*I* IC.06. Arbori

Construirea heap-ului prin aceasta metodă este un algoritm de complexitate O ( n log n )


[A2] Construirea heap-ului de jos în sus:
Vectorul va fi parcurs de la dreapta la stânga, corespunzător unei deplasări în arbore de jos în
sus. Pentru adăugarea elementului din poziția i în vector se presupune că elementele
A[i+1...N] respectă condiția care definește un heap, constituind "baza" (elementele de pe
nivelele inferioare ale) unui heap, deci:

┌─
│ A[j] ≥ A[j*2] si
│ A[j] ≥ A[j*2+1] , pentru i+1 ≤ j ≤ [N/2]
└─

Elementul adăugat, A[i], va fi retrogradat la fel cu modul de retrogradare folosit în procedura


REMOVE.

BUILD_HEAP(A,N)
{
FOR i = [N/2] TO 1 STEP -1 DO
RETRO(A,N,i)
}

RETRO(A,N,i)
{
parinte := i;
fiu := 2*i;
WHILE fiu<=N DO // parcurge o ramura in sens descendent
│IF (fiu+1<=N) and (A[fiu]<A[fiu+1]) THEN
│ fiu := fiu+1; // este ales fiu cel mai mare
│IF A[fiu]>A[parinte] THEN // daca parinte<max(fii)
│ │SCHIMBA(A[parinte],A[fiu]); // retrogradeaza
│ │parinte := fiu; │
│ │fiu := fiu*2; │ // coboara
│ELSE fiu:=N+1; // pentru parasirea buclei
}

Construirea heap-ului prin aceasta metoda este un algoritm de complexitate O ( n log n ) .

  -51-
INFORMATICĂ*I* IC.06. Arbori

Temă

1. Implementați operațiile de inserare și ștergere dintr-un heap. Implementați următorul


algoritm de sortare: valorile de sortat sunt inserate într-un heap, apoi sunt șterse până la
golirea heap-ului, rezultând ordinea descrescătoare.

2. Implementați algoritmul de sortare Heapsort.

Soluția adoptată poate fi cea din programul de mai jos.

#include <stdio.h>
#include <conio.h>

void Insert(int V[100], int &N, int a);


int Remove(int V[100], int &N);
void afisare(int V[100], int N);

void main()
{
int V[100], N=0, i, a=1;
clrscr();
fflush(stdin);
printf("\nIntroduceti arborele (0-iesire):");
while(a!=0)
{
scanf("%d", &a);
if(a!=0)
Insert(V, N, a);
else
break;
}
for(i=1; i<=N; i++) printf(" %d ",V[i]);
afisare(V,N);
printf("\nNr. in ordine descrescatoare: ");
while(N>=1)
{ printf("%d ", Remove(V, N)); }
getch();
}

void Insert(int V[100], int &N, int a)


{
int fiu, parinte, aux;
N=N+1; V[N]=a; fiu=N; parinte=N/2;
while(parinte>=1)
{
if(V[parinte] < V[fiu])
{
aux=V[parinte];
V[parinte]=V[fiu];
V[fiu]=aux;
fiu=parinte;
parinte=parinte/2;
}
else
parinte=0;
}
}

  -52-
INFORMATICĂ*I* IC.06. Arbori

int Remove(int V[100], int &N)


{
int fiu, parinte, aux, a;
if (N==0) printf("\nEroare!");
else
{
a=V[1]; V[1]=V[N];
N=N-1;
parinte=1;
fiu=2;
while(fiu<=N)
{
if((fiu+1 <= N) && (V[fiu] < V[fiu+1]))
fiu=fiu+1;
if(V[fiu] > V[parinte])
{
aux=V[fiu];
V[fiu]=V[parinte];
V[parinte]=aux;
parinte=fiu;
fiu=2*parinte;
}
else fiu=N+1; //break
}
return a;
}
}

void afisare(int V[100], int N)


{
int nivel=1, div=1, n=1, i, nr_nod_nivel;
while (n<=N)
{
nr_nod_nivel=1;
for(i=1; i<=nivel-1; i++)
nr_nod_nivel = 2*nr_nod_nivel; //nr. noduri pe nivel
//div - nr de intervale in care se imparte o linie = 2**i, i-
nivelul
div=nr_nod_nivel*2;
for(i=1; i<=nr_nod_nivel; i++)
{
gotoxy((2*i-1)*80/div, nivel+15);
printf("%d", V[n]);
if (n<N) n++;
else { n=N+1; break; }
}
nivel=nivel+1;
}
}

L13P2.CPP

#include <stdio.h>
#include <conio.h>

void Insert(int V[100], int &N,int a);


int Remove(int V[100], int &N);
void Build_Heap(int V[100], int N);

  -53-
INFORMATICĂ*I* IC.06. Arbori

void main()
{
int nr_elem;
int V[100], N=0;
int i=0, a=1;
clrscr();
fflush(stdin);
printf("\nDati vectorul:");
while(a!=0)
{
scanf("%d",&a);
if(a!=0)
{
N++;
V[N]=a;
}
else
break;
}
Build_Heap(V, N);
nr_elem=N;
while(N>=1)
{
i=Remove(V,N);
V[N+1]=i;
}
printf("\nNr. in ordine crescatoare:");
for(i=1;i<=nr_elem;i++)
printf("%d ",V[i]);
getch();
}

///////////////////////////////////////
void Insert(int V[100], int &N,int a)
{
int fiu, parinte, aux;
N=N+1; V[N]=a; fiu=N;
parinte=N/2;
while(parinte>=1)
{
if(V[parinte] < V[fiu])
{
aux=V[parinte];
V[parinte]=V[fiu];
V[fiu]=aux;
fiu=parinte;
parinte=parinte/2;
}
else
parinte=0;
}
}

/////////////////////////////////////////////////////////////////
int Remove(int V[100], int &N)
{
int fiu,parinte,aux,a;
if (N==0) printf("\nEroare!");
else
{

  -54-
INFORMATICĂ*I* IC.06. Arbori

a=V[1]; V[1]=V[N]; N=N-1;


parinte=1;
fiu=2;
while(fiu<=N)
{
if((fiu+1 <= N) && (V[fiu] < V[fiu+1]))
fiu=fiu+1;
if(V[fiu] > V[parinte])
{
aux=V[fiu];
V[fiu]=V[parinte];
V[parinte]=aux;
parinte=fiu;
fiu=2*parinte;
}
else fiu=N+1;
}
return a;
}
}

/////////////////////////////////////////////////////////////
void Build_Heap(int V[100], int N)
{
int i;
for(i=2;i<N;i++)
Insert(V,i-1,V[i]);
}

  -55-

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