Sunteți pe pagina 1din 4

La grmad...

HEAP-uri

focus

Mihai Scoraru

n cadrul acestui articol vom prezenta una dintre cele mai interesante structuri de date care poate fi folosit pentru algoritmii de sortare i cutare. Aceasta se implementeaz relativ uor i permite crearea de programe care se execut ntr-un timp mai scurt dect cele care folosesc alte metode pentru organizarea datelor. Vom ncepe prin a defini structura de heap. Denumirea este preluat din limba englez i, n general, nu este folosit traducerea n limba romn. O traducere aproximativ ar putea fi grmad; se mai folosete i termenul de ansamblu. Un heap este un vector care poate fi considerat ca fiind un arbore binar dup cum se poate vedea i n figura urmtoare:

Definiia heap-ului

Numrul care apare n fiecare nod indic poziia nodului respectiv n vector. Aadar, rdcina corespunde primului element al vectorului, fiul su stng celui de-al doilea element, fiul drept celui de-al treilea i aa mai departe. Considerm vectorul V={15,13,14,10,13,9,6,2,6}. Arborele echivalent este urmtorul:

Pentru a obine vectorul corespunztor unui astfel de arbore, vom parcurge nivelurile arborelui ncepnd cu rdcina i fiecare nivel ncepnd cu nodul cel mai din stnga. Pentru ca un arbore binar s poat fi considerat un heap, toate nivelurile acestuia trebuie s fie complete, cu excepia ultimului care se completeaz ncepnd cu nodul cel mai din stnga.

nlimea unui heap este nlimea arborelui binar corespunztor. nlimea unui arbore este definit ca fiind egal cu adncimea maxim a unui nod din arbore. Adncimea unui nod este distana dintre acel nod i rdcina arborelui. Un arbore complet de nlime h are 1 + 2 + 4 + 8 + + 2h-1 = 2h - 1 noduri. Datorit faptului c ultimul nivel al unui heap poate s nu fie complet, numrul nodurilor unui heap este cuprins ntre 2h i 2h+1-1. Reciproc, obinem c un heap cu n noduri are nlimea [log2 n]. Studiind organizarea arborelui, observm c tatl nodului corespunztor unei poziii k > 1 n vector este nodul corespunztor poziiei [k/2]. Evident, tatl rdcinii nu exist. Reciproc, observm c fiii nodului corespunztor poziiei k sunt nodurile corespunztoare poziiilor 2k (fiul stng) i 2k+1 (fiul drept). n cazul n care valoarea 2k este egal cu numrul de noduri, atunci fiul drept nu mai exist, iar fiul stng este ultimul nod al heap-ului. Dac aceast valoare depete numrul de noduri, atunci nici unul dintre fii nu mai exist, nodul corespunztor poziiei k fiind o frunz. Cea mai important proprietate a heap-ului este aceea c valoarea oricrui nod nu poate fi mai mare dect valoarea nodului tat. Aceast proprietate face aceast structur foarte util pentru operaiile de cutare. Generaliznd, obinem c valoarea corespunztoare rdcinii unui subarbore este mai mare sau egal cu oricare dintre valorile corespunztoare oricrui nod din subarbore. De aici rezult c rdcina trebuie s conin valoarea maxim a elementelor din vector. Despre relaiile dintre valorile a dou noduri, care nu sunt unul descendentul celuilalt, nu se poate face nici o afirmaie. Cu alte cuvinte, faptul c un nod se afl pe un nivel mai apropiat de rdcin, nu nseamn neaprat c el are o valoare mai mare dect nodurile aflate pe niveluri mai deprtate.

23

Ginfo nr. 2 - februarie 2001

Aceast structur permite efectuarea foarte rapid a anumitor operaii: determinarea maximului valorilor dintr-un heap: O(1); crearea unui heap dintr-un vector oarecare: O(n); eliminarea unui element: O(log n); inserarea unui element: O(log n); sortarea unui vector: O(nlog n); cutarea unei valori: O(n).

focus

Datorit echivalenei dintre un heap i un vector, datele nu vor fi reprezentate n memorie n form arborescent, ci n cea vectorial. Pentru implementare vom folosi limbajul C++. Vom considera heap-ul ca fiind un vector; tipul corespunztor va putea fi definit astfel: typedef int Heap[MAX], unde MAX reprezint dimensiunea maxim a heap-ului. Deoarece dimensiunea heap-ului poate fi destul de mare, este recomandabil ca acesta s fie pstrat ntr-o variabil global i s nu fie transmis ca parametru pentru anumite funcii. Vom declara global un vector prin Heap h;. Operaiile cu heap-uri trebuie efectuate astfel nct structura de heap s fie meninut n permanen, adic arborele s aib toate nivelurile complete cu excepia ultimului, iar valoarea nici unui nod s nu depeasc valoarea nodului printe. Vom prezenta n continuare fiecare operaie amintit. Determinarea maximului Datorit faptului c rdcina corespunde primului element din vector i ea conine valoarea maxim, o funcie care determin valoarea maxim a elementelor dintr-un heap, va returna valoarea primului element al vectorului. Implementarea este urmtoarea:

Implementarea

arbore, fiind nlocuit cu unul dintre fiii si. Dac alegem nodul cu valoarea 8, atunci structura de heap nu va fi corect deoarece, dup interschimbare, acest nod va avea ca fii un nod cu valoarea 10 i unul cu valoarea 1, proprietatea de heap nefiind respectat. Dac alegem nodul cu valoarea 10, acesta va avea ca fii un nod cu valoarea 1 i unul cu valoarea 8, proprietatea de heap fiind respectat n acest caz. Ca urmare, vom interschimba nodul avnd valoarea 1 cu nodul avnd valoarea 10. Ajungem n urmtoarea situaie:

Totui, propritatea de heap nu este nc respectat, deoarece nodul cu valoarea 1 are un fiu cu valoarea 2, deci cu valoare mai mare. De aceast dat avem un singur fiu, deci singura opiune este de a interschimba cele dou noduri. Ajungem n urmtoarea situaie n care avem o structur de heap corect:

int Max(){ return h[1]. }

Din motive care vor fi explicate ulterior, poziia 0 nu este folosit.

Ginfo nr. 2 - februarie 2001

Crearea unui heap dintr-un vector oarecare Pentu a prezenta aceast operaie avem nevoie de o alta i anume reconstituirea structurii de heap atunci cnd avem un singur nod care nu respect proprietatea de heap deoarece are o valoare mai mic dect unul dintre fiii si. O asemenea situaie este urmtoarea:

24

Nodul cu valoarea 1 nu se afl pe poziia corect deoarece valoarea sa este mai mic dect cel puin una dintre valorile fiilor si. n acest caz nodul va trebui cobort n

n concluzie, algoritmul de reconstituire a proprietii de heap va cobor n arbore nodul care nu se afl pe poziia corect pn n momentul n care nu mai avem fii sau proprietatea de heap este respectat. Pentru a cobor nodul n arbore l vom interschimba cu fiul care are valoarea maxim. Aceast operaie poart denumirea de scufundare. Mai trebuie precizat faptul c, datorit relaiilor dintre poziiile unui nod i cele ale fiilor si, acestea pot fi determinate folosind doar operaii pe bii (care sunt foarte rapide): Poziia fiului stng al nodului de pe poziia k se obine prin deplasarea la stnga cu o poziie a valorii k: k<<1. Poziia fiului drept al nodului de pe poziia k se obine prin deplasarea la stnga cu o poziie a valorii k i setarea pe 1 a bitului celui mai nesemnificativ: (k<<1)|1. Poziia tatlui nodului de pe poziia k se obine prin deplasarea la dreapta cu o poziie a valorii k: k>>1. Se observ acum motivul pentru care am ales s nu folosim poziia 0 a vectorului. Poziia fiului stng al nodului de pe poziia 0 ar fi fost 0<<1=0. Exist artificii care permit i folosirea acestei poziii, dar nu insistm asupra lor. Vom implementa o funcie care realizeaz operaia de scufundare. Aceasta va avea ca parametri dimensiunea heap-ului i poziia nodului care nu respect proprietatea de heap.
void Scufunda(int n,int k){ int aux,fiu=0;

// se alege fiul cu valoarea mai mare if ((k<<1)<=n){ fiu=k<<1; if ((k<<1)|1<n && h[(k<<1)|1]>H[k<<1]) fiu|=1; // interschimbare numai daca este nevoie if (h[fiu]<=h[k]) fiu=0; } else fiu=0; while (fiu){ // interschimbare aux=h[k]; h[k]=h[fiu]; h[fiu=k]=aux; // se alege urmatorul fiu if (k<<1<=n){ fiu=k<<1; if ((k<<1)|1<n && h[(k<<1)|1]>H[k<<1]) fiu|=1; // interschimbare numai daca este nevoie if (h[fiu]<=h[k]) fiu=0; } else fiu=0; } }

cont de modul n care se fac scufundrile. Frunzele reprezint aproximativ jumtate din noduri i nu sunt scufundate deloc. Aproximativ un sfert dintre noduri se afl imediat deasupra frunzelor i acestea sunt scufundate cel mult un nivel. O optime dintre noduri se afl cu dou niveluri deasupra frunzelor, deci sunt scufundate cel mult dou nide veluri. Continund pn la rdcin, obinem un numr cel mult
[log 2 n ]

k =0

n k k +1 2

scufundri. Dac nlimea

focus

arborelui este h, atunci n este, aproximativ, 2h. nlocuind n formul obinem

k 2
k =0

2h ; dup efectuarea calculelor k +1

se obine ordinul de complexitate O(2h) pentru algoritmul de construire a unui heap. Deoarece avem h = [log2 n] vom obine ordinul de complexitate O(2log n ) = O(n) .
2

Am preferat o variant nerecursiv datorit vitezei mai mari de execuie. Acest algoritm funcioneaz numai dac cei doi subarbori, care au ca rdcin fiii nodului de pe poziia k, au o structur de heap corect. Vom prezenta acum modalitatea prin care aceast funcie de scufundare a unui element se folosete pentru a transforma un vector oarecare ntr-un heap. Este evident faptul c frunzele arborelui au o structur de heap corect. Ca urmare, vom putea apela funcia Scufunda() pentru nodurile imediat superioare frunzelor. Dup aceste apeluri, vom avea structuri de heap corecte pentru nodurile imediat superioare frunzelor. Vom trece apoi, pe rnd, pe niveluri superioare pn cnd vom ajunge la rdcin. n concluzie, va trebui s apelm funcia Scufunda() ncepnd cu nodul de pe poziia [n/2] i continund cu nodurile de pe poziiile anterioare pn cnd vom ajunge la prima poziie. Vom scrie o funcie care are ca parametru dimensiunea h a heap-ului.
void ConstruiesteHeap(int n){ for (int i=(n>>1);i;Scufunda(n,i--)); }

Eliminarea unui element Pentru a efectua aceast operaie avem nevoie, de asemenea, de o alta i anume reconstituirea structurii de heap atunci cnd avem un singur nod care nu respect proprietatea de heap, deoarece are valoarea mai mare dect tatl su. Este exact fenomenul invers celui prezentat n cazul operaiei de scufundare. n acest caz nodul va trebui urcat n arbore. Vom presupune c restul heap-ului este corect. Vom interschimba acest nod cu tatl su. Dup interschimbare, nodul ajunge n poziia tatlui i subarborele avnd rdcina n acest nod este un heap corect deoarece valoarea tatlui era mai mare dect cea a celuilalt fiu. Totui, s-ar putea ca, dup interschimbare, nodul s aib un tat cu o valoare mai mic. n acest caz nodul va trebui din nou urcat n arbore. Procedeul va continua pn cnd nodul curent devine rdcina arborelui sau nu mai are un tat cu o valoare mai mic. Aceast operaie poart numele de ridicare. Vom implementa o funcie care realizeaz operaia de ridicare. Aceasta va avea ca parametri dimensiunea heap-ului h i poziia nodului care nu respect proprietatea de heap.
void Ridica(int n,int k){ int val=h[k]; while (k>1 && val>h[k>>1]) h[k]=h[k>>=1]; h[k]=val; }

Ginfo nr. 2 - februarie 2001

n cele ce urmeaz vom determina complexitatea acestui algoritm de construire a unui heap dintr-un vector oarecare. La prima vedere, avem n noduri, care pot fi scufundate cel mult [log2 n] niveluri deci, s-ar prea c avem un ordin de complexitate O(nlog n). n realitate aceast determinare a complexitii este eronat, deoarece nu ine

Vom prezenta acum modul n care se folosete aceast funcie pentru a elimina un element dintr-un heap. Dup eliminarea unui element, n locul su apare un gol n care trebuie s amplasm un alt element. Deoarece toate nivelurile (cu excepia ultimului) trebuie s fie complete, iar ultimul trebuie s fie ocupat n partea stng, trebuie s eliminm ultimul element din heap. Vom amplasa valoarea

25

focus

corespunztoare acestui element n locul gol i vom verifica dac structura de heap este pstrat. tim c singurul loc n care structura de heap s-ar putea s nu fie respectat este poziia elementului ters care a fost nlocuit cu ultimul element. Dac valoarea este mai mare dect cea a tatlui, acest nod va trebui ridicat n arbore. Dac nodul are o valoare mai mic dect cea a unui fiu, atunci nodul trebuie scufundat n arbore. n continuare vom prezenta o funcie care realizeaz eliminarea unui element dintr-un heap.
void Elimina(int& n,int k){ h[k]=h[n--]; if (k>1 && h[k]>h[k>>1]) Ridica(n,k); else Scufunda(n,k); }

ultima poziie va rmne liber. Deoarece n vectorul sortat maximul trebuie amplasat exact pe aceast poziie, la fiecare pas nu trebuie dect s interschimbm prima i ultima poziie a heap-ului i apoi s scufundm elementul care a devenit rdcina arborelui. Algoritmului heapsort poate fi implementat prin funcia HeapSort() prezentat n continuare:
void HeapSort(int n){ ConstruiesteHeap(n); for (int i=n;i>=2;){ int aux=h[1]; h[1]=h[i]; h[i]=aux; Scufunda(--i,1); } }

Deoarece o scufundare sau o ridicare se poate face cu cel mult h niveluri (h este nlimea arborelui), iar h este [log2 n], ordinul de complexitate al algoritmului de eliminare este O(log n). Dup eliminare, dimensiunea heap-ului se reduce cu 1, deci variabila n trebuie decrementat. Inserarea unui element Dac dorim s inserm un nou element ntr-un heap, operaia este mult mai simpl. Trebuie doar s l amplasm pe poziia n+1 a vectorului i s l ridicm pn cnd ajunge n poziia corect. O funcie care realizeaz operaia de inserare este prezentat n continuare.
void Insereaza(int& n,int val){ h[++n]=val; Ridica(n,n); }

Pentru aceast operaie folosirea heap-urilor nu este cea mai indicat soluie, deoarece complexitatea nu poate fi redus sub O(n). Aceasta se datoreaz faptului c, dei putem fi siguri c un nod cu valoarea mai mic este descendent al unui nod cu valoarea mai mare, nu tim n care dintre cei doi subarbori s l cutm. Totui, fa de cutarea secvenial se poate aduce o mbuntire; dac rdcina unui subarbore are o valoare mai mic dect valoarea cutat, atunci vom ti c descendenii rdcinii vor avea valori mai mici, deci nu mai are rost s cutm n acel subarbore. Astfel, s-ar putea ca poriuni mari din heap s nu mai trebuiasc explorate, dar n cazul cel mai defavorabil, este necesar parcurgerea ntregului heap. Algoritmul poate fi implementat recursiv, destul de simplu, dup cum se poate vedea n continuare.
int Cauta(int nod,int val){ if (nod>n || val>h[nod]) return 0; if (val==h[nod]) return 1; return Cauta(nod>>1,val) || Cauta((nod>>1)|1,val); }

Cutarea unei valori

Elementul poate fi ridicat cel mult h niveluri, deci ordinul de complexitate al algoritmului este O(log n). Dup eliminare dimensiunea heap-ului crete cu 1, deci variabila n trebuie incrementat. Sortarea unui vector Algoritmul de sortare care folosete structura de heap poart denumirea de heapsort. Vom ncepe prin a construi un heap pentru vectorul pe care dorim s l sortm. Dup aceea eliminm maximul i apoi refacem heap-ul. Dup refacere, al doilea element se afl pe prima poziie a vectorului; l eliminm i refacem din nou heap-ul. Continum pn cnd nu mai avem elemente n heap. Algoritmul de extragere a maximului are complexitatea O(1), iar refacerea heap-ului necesit, aa cum vom vedea, o singur scufundare, deci are complexitatea O(log n). Avnd n vedere faptul c efectum n extrageri, complexitatea algoritmului heapsort este O(nlog n). Algoritmul poate fi implementat astfel nct s nu necesite spaiu suplimentar de memorie. Dup o eliminare a maximului, dimensiunea heap-ului se va reduce cu 1, deci
Ginfo nr. 2 - februarie 2001

Aceast funcie trebuie apelat cu valoarea 1 pentru parametrul nod. 1. Thomas H. Cormen, Charles E. Leisserson, Ronald R. Rivest, Introducere n algoritmi, Editura Computer Libris Agora, Cluj - 2000 2. Ctlin Frncu, Psihologia concursurilor de informatic, Editura L&S, Bucureti - 1997
Mihai Scoraru este redactor-ef adjunct al GInfo. Poate fi contactat prin e-mail la adresa skortzy@xnet.ro.

Bibliografie

26

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