Documente Academic
Documente Profesional
Documente Cultură
Arbori
Cuvinte-cheie
Arbori, reprezentări, inserare, căutare, parcurgere, ștergere,
Echilibrare AVL, arbori binari optimali, arbori heap, sortare heap
{ }
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 .
r nivel 0
r1 r2 rm nivel 1
-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.
Toate metodele de reprezentare ale unui arbore încearcă să pună în evidență fiii sau
părintele unui nod al arborelui.
struct Nod{
Atom data;
Nod* vDesc[GRMAX];
};
-2-
INFORMATICĂ*I* IC.06. Arbori
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.
-3-
INFORMATICĂ*I* IC.06. Arbori
Struct Nod {
Atom data;
Nod* desc;
Nod* next;
};
p->desc
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:
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.
De exemplu:
-6-
INFORMATICĂ*I* IC.06. Arbori
|
a
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).
Î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_
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* creareArbore()
-7-
INFORMATICĂ*I* IC.06. Arbori
{
char buffer[DIM_EXPR];
cin >> buffer;
int length = strlen(buffer);
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 ( 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;
}
struct Nod
{
type data;
Nod* stg, *drt;
};
Astfel, arborele:
-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.
rad = rădăcină
SAS = SubArbore Stâng
SAD = SubArbore Drept
-
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:
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:
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
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.
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”
-14-
INFORMATICĂ*I* IC.06. Arbori
{
if(nod==0)
return 0;
else
return 1 + Noduri(nod->stg) + Noduri(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);
}
}
-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);
}
}
-16-
INFORMATICĂ*I* IC.06. Arbori
STACK.CPP
#include <stdio.h>
struct Element{
Atom data;
Element* next;
};
void initStack(Stack& S)
{
S=0;
}
int isEmpty(Stack& S)
{
return(S==0);
}
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
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.
a) varianta recursivă:
Funcţia insert poate avea următoarea formă:
-18-
INFORMATICĂ*I* IC.06. Arbori
insert(rchild(r),a)
}
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:
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
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)
}
}
Î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;
};
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
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ă.
-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.
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
struct Nod
{
Tip_cheie cheie; //cheia este chiar valoarea
void *info; //pointer la informatia utila
Nod *fst, *fdr;
-23-
INFORMATICĂ*I* IC.06. Arbori
};
#endif;
FUNCTII.CPP
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <stdlib.h>
#include <time.h>
#include "arbore.h"
#include "coadagen.h"
//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;
}
-24-
INFORMATICĂ*I* IC.06. Arbori
{
while (r->fdr != 0) r=r->fdr;
return r;
}
//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);
}
}
-25-
INFORMATICĂ*I* IC.06. Arbori
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;
}
-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);
}
}
-27-
INFORMATICĂ*I* IC.06. Arbori
}
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
};
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;
}
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;
}
}
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:
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
- un nod al arborelui
- un subarbore de adâncime h
-30-
INFORMATICĂ*I* IC.06. Arbori
și
Rotații simple
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
Rotații duble
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.
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ă
-33-
INFORMATICĂ*I* IC.06. Arbori
Aplicație
#include <conio.h>
#include <stdio.h>
-34-
INFORMATICĂ*I* IC.06. Arbori
-35-
INFORMATICĂ*I* IC.06. Arbori
return a;
}
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.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
∑ 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
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
Generarea un OBST
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).
-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
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
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
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.
Indicaţii de implementare
Declaratii
-43-
INFORMATICĂ*I* IC.06. Arbori
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ă
╔═══════════╤════════════╤═════════════════╤═════════════════╗
║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
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
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) ║
╚═══════════╧════════════╧═════════════════╧═════════════════╝
char A[DIMMAX+1];
//Vectorul in care este plasata reprezentarea implicita
int n;
-46-
INFORMATICĂ*I* IC.06. Arbori
Parcurgerea în inordine:
void inordine(int i)
{
if (i<=n && (A[i]!='-'){
inordine(i*2);
prelucrare(A[i]);
inordine(i*2+1);
}
}
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:
-47-
INFORMATICĂ*I* IC.06. Arbori
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ă.
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:
-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.
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
BUILD_HEAP(A,N){
FOR i = 2 TO N DO
INSERT(A,i-1,A[i])
}
-50-
INFORMATICĂ*I* IC.06. Arbori
┌─
│ A[j] ≥ A[j*2] si
│ A[j] ≥ A[j*2+1] , pentru i+1 ≤ j ≤ [N/2]
└─
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
}
-51-
INFORMATICĂ*I* IC.06. Arbori
Temă
#include <stdio.h>
#include <conio.h>
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();
}
-52-
INFORMATICĂ*I* IC.06. Arbori
L13P2.CPP
#include <stdio.h>
#include <conio.h>
-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
/////////////////////////////////////////////////////////////
void Build_Heap(int V[100], int N)
{
int i;
for(i=2;i<N;i++)
Insert(V,i-1,V[i]);
}
-55-