Documente Academic
Documente Profesional
Documente Cultură
12
B-arbori
1. Introducere
2. Operații pe structuri de tip B-arbore
3. Aplicații
1. Introducere
#define M 6
enum tipNod { frunza, interior };
struct bnod {
tipNod tip;
int n;
int val[M -1];
bnod *leg[M];
};
Căutarea este similară căutării într-un BST. Fie x cheia care trebuie căutată, se
pleacă de la rădăcină și se parcurge arborele în mod recursiv în jos. Pentru fiecare nod
intern se va verifica dacă acesta conține cheia. În caz afirmativ se va returna 𝑡𝑟𝑢𝑒, iar în
caz căutarea va continua de la cel mai apropiat descendent (de dinaintea nodului cu cheia
mai mare). Pentru nodurile de tip frunză care nu conțin cheia respectivă se va returna 𝑓𝑎𝑙𝑠𝑒.
RecursiveSearch(r,x)
i=0;
while i<n and val[i]<x
i=i+1;
endwhile;
if leg[i]==NULL
then return false;
else return RecursiveSearch(leg[i],x);
endif;
endRecursiveSearch;
p=leg[pos];
endwhile;
return false;
endIterativeSearch;
Cheile noi sunt inserate întotdeauna ca frunze. Fie 𝑘 cheia care trebuie inserată.
Similar arborilor BST, se pleacă de la rădăcină și se coboară spre nodurile frunză,
inserându-se valoarea în nodul frunză identificat. Spre deosebire de BST, există un număr
predefinit de chei care pot fi inserate într-un nod și de aceea trebuie verificat mereu dacă
respectiva cheie poate fi adăugată. Pentru a elibera spațiul necesar inserării într-un anumit
nod se folosește operația de spargere a unui nod descendent 𝑆𝑝𝑙𝑖𝑡 ilustrată grafic în figura
de mai jos:
1 50 80 100 200
1 50 100 200
55 70 85 90
55 70 80 85 90
T1 T2 T3 T4 T5 T6
T1 T2 T3 T4 T5 T6
Fie 𝑥 rădăcina arborelui și 𝑘 cheia ce trebuie inserată. Cât timp 𝑥 nu este frunză, se
identifică descendentul lui 𝑥 care urmează a fi vizitat și se notează cu 𝑦. Dacă 𝑦 nu este
complet, el devine noua rădăcină. Dacă 𝑦 este complet, acesta va fi spart și 𝑥 va indica
către unul din descendenții lui 𝑦 în funcție de 𝑘. Dacă 𝑘 este mai mic decât cheia mediană
din 𝑦, noua rădăcină va indica către prima jumătate din 𝑦, altfel, noua rădăcină va indica
către a doua parte din 𝑦. La spargerea nodului y, cheia mediană va migra către nodul părinte
𝑥. Dacă rădăcina nu mai are descendenți, algoritmul se oprește. 𝑥 trebuie să aibă spațiu
pentru inserarea unei chei după spargerea tuturor nodurilor, iar 𝑘 va fi inserat în 𝑥.
Avantajul de a sparge un nod înainte de inserare îl reprezintă traversarea acestuia o
singură dată. În cazul în care spargerea nu este efectuată înainte de a ajunge într-un nod
(inserare proactivă), ci fix în momentul inserării (inserare reactivă) poate necesita
traversarea tuturor nodurilor încă o data, plecând de la frunze spre rădăcină. Această
traversare este necesară atunci când toate nodurile situate pe calea de la rădăcină la frunză
sunt pline. De aceea, atunci când se ajunge la frunză, aceasta va fi spartă și una din chei va
urca un nivel. Spargerile se vor efectua în cascadă ținând cont că toate nodurile de pe cale
sunt pline. La rândul său, algoritmul de inserare proactiv are un dezavantaj în spargerile
inutile pe care le poate produce.
Fie secvența de întregi care vor fi inserați într-un B-arbore de grad minim 𝑡 = 3,
inițial vid: 10, 20, 30, 40, 50, 60, 70, 80 ș𝑖 90:
Inițial rădăcina arborelui este setată pe NULL.
Se inserează 10:
Structuri de Date – Lucrarea nr. 12
Se inserează în continuare 𝟐𝟎, 𝟑𝟎, 𝟒𝟎 ș𝒊 𝟓𝟎. Toate valorile vor fi inserate în nodul
rădăcină dat fiind faptul că aceasta poate conține maxim 2𝑡 − 1 = 5 chei:
Pentru inserarea lui 60, nu mai există poziții libere în nodul rădăcină. Acesta va fi spart,
iar valoarea 60 va fi inserată în nodul descendent adecvat:
Vor fi inserate în continuare, valorile 70 și 80. Acestea vor fi introduse în nodul frunză
adecvat fără a produce spargeri:
După inserarea lui 90, se va produce o spargere, iar cheia mediană va urca în nodul
părinte:
Structuri de Date – Lucrarea nr. 12
Dacă un nod 𝑣 este încărcat la maxim, pentru a insera o cheie nouă este necesară
spargerea acestuia. Prin spargere cheia mediană a nodului 𝑣 este mutată în părintele 𝑢 al
acestuia. Se va crea un nou nod 𝑤, toate cheile din 𝑣 situate la dreapta cheii mediane sunt
mutate în nodul 𝑤. Cheile din 𝑣 situate la stânga cheii mediane rămân în 𝑣. Nodul nou 𝑤
devine fiu imediat la dreapta cheii mediane care a fost mutată în părintele 𝑢, iar 𝑣 devine
fiu imediat la stânga. Spargerea transformă nodul cu 𝑀 − 1 chei în două noduri cu (𝑀 −
2)/2 chei fiecare.
InsertToNode(&v,k)
i=n;
if(type(v)==leaf) // directly insert value to node
then while i>=1 and k<val[i] execute
val[i+1]=val[i];
i=i-1;
endwhile;
val[i+1]=k;
n=n+1;
else
//search for the appropriate child node for inserting the value
while i>=1 and k<val[i] execute
i=i-1;
endwhile;
if k>val[i] then i=i+1;
endif;
if IsFull(leg[i]) //mai exact v->leg[i]->n==M-1
then SplitNode(v,i,leg[i]);
if k>val[i] then i=i+1; endif;
endif;
InsertToNode(leg[i],k);
endInsertToNode;
2.4. Ștergerea
Operația de ștergere este ceva mai complicată decât operația de inserarea, deoarece
cheia care va fi ștearsă poate proveni din orice nod și nu neapărat dintr-un nod frunză.
Problema apare la ștergerea unei chei dintr-un nod intern, operație care va necesita
rearanjarea nodurilor descendente. La fel ca operația de inserare, ștergerea trebuie să
păstreze proprietățile structurii de b-arbore. Similar modului în care se asigură aglomerarea
cheilor într-un nod la inserare, se vor evita diminuările excesive a numărului de chei dintr-
un nod în timpul ștergerii. Rădăcina poate fi o excepție în această situație, deoarece ea
poate conține mai puțin decât minimul de 𝑡 − 1 chei. Ștergerea poate fi abordată
asemănător încercării de a insera o valoare într-un nod plin. La fel cum algoritmul de
inserare trebuie să se întoarcă înapoi în arbore dacă nodul în care trebuie să se facă inserarea
este plin, ștergerea abordată asemănător trebuie să revină în sus în arbore, dacă nodul din
care ar trebui să se facă ștergerea, cu excepția rădăcinii, conține numărul minim de chei.
Procedura de ștergere elimină cheia 𝑘 din subarborele cu rădăcina în x. Această
procedură garantează că la fiecare apel recursiv pentru un nod 𝑥, numărul de chei din 𝑥
este minim 𝑡, unde 𝑡 este gradul arborelui. Această condiție necesită o cheie în plus față de
minimul cerut de condițiile structurii de tip b-arbore. Din acest motiv, unele chei trebuie
coborâte în nodurile copil înaintea apelului recursiv pentru copilul respectiv. Această
condiție suplimentară permite ștergerea unei chei din arbore printr-o singură traversare de
sus în jos, fără a necesita reveniri, cu o singură excepție. Specificațiile următoare se referă
la ștergerea dintr-un b-arbore în cazul în care nodul rădăcină 𝑥 devine nod intern fără nici
o cheie (cazurile 2.c) și 3.b)) atunci 𝑥 va fi șters, rădăcina fiind înlocuită de singurul ei
copil 𝑥. 𝑐1. Înălțimea arborelui va fi decrementată și se va conserva proprietatea structurii
că rădăcina conține cel puțin o cheie.
Structuri de Date – Lucrarea nr. 12
În cele ce urmează operația de ștergere este ilustrată pentru mai multe situații posibile:
1. Dacă cheia k care va fi ștearsă se află în nodul x de tip frunză, aceasta va fi ștearsă
simplu, fără implicații.
2. Dacă cheia k care va fi ștearsă se află într-un nod intern x:
a) Dacă nodul y, predecesorul lui k în nodul x, conține cel puțin t chei, atunci
trebuie identificat un 𝑘0 predecesorul lui k în subarborele cu rădăcina y. 𝑘0 va
fi șters recursiv și îl va înlocui pe 𝑘 în 𝑥.
b) Dacă 𝑦 are mai puțin de 𝑡 chei, va fi examinat descendentul 𝑧 al lui 𝑘 în nodul
𝑥. Dacă 𝑧 are cel puțin 𝑡 chei, atunci se identifică succesorul lui 𝑘, 𝑘0 din
subarborele cu rădăcina în 𝑧. 𝑘0 va fi șters recursiv și îl va înlocui pe 𝑘 în 𝑥.
c) Altfel, dacă atât 𝑦 cât și 𝑧 au doar 𝑡 − 1 chei, 𝑘 și 𝑧 vor fi grupate în nodul 𝑦
care va conține 2𝑡 − 1 chei. Astfel, 𝑧 va putea fi șters și 𝑘 va putea fi eliminat
recursiv din 𝑦.
3. Dacă cheia 𝑘 nu există în nodul intern, se va determina rădăcina celui mai apropiat
subarbore care conține cheia 𝑘 în 𝑥. 𝑐(𝑖), dacă există. Dacă aceasta conține doar
𝑡 − 1 chei, se vor efectua pașii 3.a) sau 3.b) pentru a garanta parcurgerea până la
un nod care sa conțină cel puțin 𝑡 chei. Algoritmul va continua recursiv pe cel mai
apropiat descendent al lui 𝑥.
a) Dacă rădăcina subarborelui lui 𝑥 indicată de 𝑥. 𝑐(𝑖) conține doar 𝑡 − 1 chei, dar
are la rândul ei un subarbore care conține în rădăcină cel puțin 𝑡 chei, nodul 𝑥
va fi spart și una dintre chei va ajunge în rădăcina subarborelui. Una din cheile
localizate în frații din stânga sau din dreapta lui 𝑥. 𝑐(𝑖) va fi propagată către 𝑥,
făcându-se legătura cheii cu 𝑥. 𝑐(𝑖).
b) Dacă rădăcina subarborelui indicat de 𝑥. 𝑐(𝑖) și frații săi din stânga și din
dreapta conțin 𝑡 − 1 chei, atunci subarborele va fi reunit cu unul din frați.
Această operație presupune mutarea unei chei din nodul 𝑥 în jos către noul nod
ca și cheie mediană.
Deoarece majoritatea cheilor dintr-un b-arbore sunt localizate în nodurile de tip
frunză, operațiile de ștergere sunt folosite cu precădere pentru a elimina chei din frunze.
Procedura recursivă de ștergere acționează printr-o traversare de sus în jos a arborelui, fără
a avea nevoie să revină într-un nod. Însă, atunci când trebuie ștearsă o cheie dintr-un nod
Structuri de Date – Lucrarea nr. 12
intern, procedura implică o traversare de sus în jos a arborelui și necesită revenirea la nodul
din care a fost făcută ștergerea pentru a înlocui cheia eliminată cu unul din predecesorii sau
din succesorii ei (cazurile 2.a) și 2.b)).
Dacă valoarea care trebuie ștearsă se regăsește în nodul curent, se va face ștergerea
în funcție de tipul nodului: nod intern sau nod frunză. Altfel, dacă valoarea ce trebuie
ștearsă nu este prezentă în nodul curent și nodul curent este un nod de tip frunză, se va
semnala imposibilitatea ștergerii. Dacă nodul în care ar trebui să existe valoarea are mai
puțin de (M-1)/2 valori, se va încerca împrumutul unei valori fie de la vecinul din stânga,
fie de la cel din dreapta. Dacă nodul din stânga conține mai mult de (M-1)/2 chei atunci se
va împrumuta o valoare de la acest nod. Dacă nodul din stânga conține mai puțin de (M-
1)/2 chei atunci se va încerca împrumutul unei valori de la nodul din dreapta. Dacă ambele
noduri stochează mai puțin de (M-1)/2 valori, atunci vom unifica nodurile prin regruparea
valorilor și a valoarii din nodul părinte într-un singur nod.
DeleteNode(&r, x)
pos=GetPosition(r,x);
if pos<n and val[pos]==x
then if IsLeaf(r)==true,
then DeleteFromLeafNode(r,pos);
else DeleteFromNonLeafNode(r,pos);
endif;
else
if IsLeaf(r)==false then return; endif;
if leg[pos] contains less or equal than (M-1)/2 keys,
then if pos!=1 and leg[pos] has more than (M-1)/2 keys
then BorrowFromPreviousNode(r,pos);
DeleteNode(leg[pos],x);
else if pos!=n+1 and leg[pos+1] has more than (M-1)/2 keys
then BorrowFromNextNode(r,pos);
DeleteNode(leg[pos],x);
else if pos!=n
then Merge(r,pos); //merge right
DeleteNode(leg[pos],x);
else Merge(r,pos-1); //merge left
DeleteNode(leg[pos-1],x);
endif;
endif;
endif;
else DeleteNode(leg[pos],x);
endif;
endif;
endDeleteNode;
if (p->leg[idx]->n >(M - 1) / 2)
{
//inlocuim valoarea din nod cu valoarea maxima din subarborele stang si cautam
nodul de valoare maxima = cel mai din dreapta nod
int pred;
bnod *crt = p->leg[idx];
while (crt->tip != frunza) //cat timp nu am ajuns la un nod frunza
crt = crt->leg[crt->n];
//ultima valoare din frunza cea mai din dreapta din SAS
pred = crt->val[crt->n - 1];
p->val[idx] = pred;
crt->val[crt->n - 1] = k;
deleteNode(p->leg[idx], k);
}
else if (p->leg[idx + 1]->n > (M - 1) / 2)
{
//inlocuim valoarea din nod cu valoarea minima din subarborele drept
//cautam nodul de valoare minima = cel mai din stanga nod
bnod *crt = p->leg[idx + 1];
while (crt->tip != frunza)
crt = crt->leg[0];
//prima valoare din frunza cea mai din stanga din SAD
int succ = crt->val[0];
p->val[idx] = succ;
crt->val[0] = k;
deleteNode(p->leg[idx + 1], k);
}
else
{
//daca nodurile din stanga si dreapta au mai putin de (M-1)/2 noduri,
//vom uni cele 2 noduri, dupa care vom sterge valoare din nodul nou creat
merge(p, idx);
deleteNode(p->leg[idx], k);
}
}
/* vom imprumuta o valoare de la nodul din stanga, imprumutul se va face prin intermediul
nodului parinte */
void borrowFromPrevious(bnod *&p, int idx)
{
if (drt->tip != frunza)
{
Structuri de Date – Lucrarea nr. 12
if (drt->tip != frunza)
{
drt->leg[0] = stg->leg[stg->n];
}
/* vom imprumuta o valoare din nodul din dreapta, imprumutul se va face prin intermediul
nodului parinte */
void borrowFromNext(bnod *&p, int idx)
{
stg->val[stg->n] = p->val[idx];
if (stg->tip != frunza)
{
stg->leg[stg->n + 1] = drt->leg[0];
}
p->val[idx] = drt->val[0];
if (drt->tip != frunza)
{
for (int i = 1; i <= drt->n; i++)
{
drt->leg[i - 1] = drt->leg[i];
}
}
int t = (M - 1) / 2;
if (childl->tip != frunza)
{ //copiem legaturile din nodul din dreapta
for (int i = 0; i <= childr->n; ++i)
childl->leg[i + t + 1] = childr->leg[i];
}
delete childr;
}
3. Aplicații
Se construiește o structură de tip B-arbore pe baza unor chei citite de la tastatura (șir care
se încheie cu o valoare 0) :
a) Să se scrie o funcție pentru inserarea unei valori în arbore, funcție care va fi apelată
pentru fiecare valoare din șir;
b) Să se afișeze în inordine conținutul arborelui ;
c) Se citește o valoare pentru care să 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 scrie funcții pentru afișarea nodurilor de cheie minimă respectiv maximă din
B arbore;