Atenţie! Aceasta este o versiune veche a paginii, scrisă la 2008-11-22 11:19:26.
Revizia anterioară   Revizia următoare  

Treap-uri

În acest articol voi prezenta o alternativă pentru arbori binari de căutare echilibraţi, precum AVL, Red-Black Trees, Splay Trees şi B-Trees.

Ce este un Treap?

Treap-ul este un arbore binar în care fiecare nod conţine două informaţii:

  • cheie;
  • prioritate.

Structura de date respectă doi invarianţi:

  • dacă parcurgem arborele în inordine, atunci vom obţine nodurile sortate; (invariantul arborilor de căutare)
  • prioritatea fiecărui nod este mai mare decât cea a fiilor săi. (invariantul heapurilor)

În consecinţă, treap-ul este un arbore binar de căutare pentru chei şi un max-heap pentru priorităţi.

În continuare, vom presupune că toate cheile şi priorităţile din Treap-ul T sunt distincte. În practică, presupunerea aceasta are un impact neglijabil.

Astfel, din moment ce T este un heap, nodul v cu prioritatea cea mai mare trebuie să fie rădăcina. Cum este şi un arbore binar de căutare, orice nod u cu cheie(u) < cheie(v) se găseşte în subarborele stâng al lui v, şi orice nod w cu cheie(w) > cheie(v) se găseşte în subarborele drept.

Este important să reţinem că un Treap este determinat în mod unic de chei şi de priorităţi. Prin inducţie se demonstrează că Treapul este arborele binar obţinut prin inserarea cheilor în ordinea descrescătoare a priorităţilor. Algoritmul de inserare este cel obişnuit pentru arborii de căutare. Astfel, cum fiecare set de priorităţi asociat nodurilor va aranja arborele într-un singur mod, probabilitatea ca arborele să fie echilibrat este rezonabil de mare, fapt datorat numărului mic al arborilor rău echilibraţi în comparaţie cu cei echilibraţi.

Avantaje

Heapurile şi Arborii Binari de Căutare sunt uşor de implementat şi de înţeles, iar treapurile sunt o combinaţie a acestor două concepte. Astfel, e suficient să fie înţeles invariantul, după care implementarea unui treap se poate face cu uşurinţă în 20 de minute, fără antrenament. De obicei, la structuri ca Arbori Roşu-Negrii trebuie folosite serii de rotaţii stânga şi dreapta complexe şi analizate o mulţime de cazuri, pe când la Treapuri facem doar câte o rotaţie stânga sau o rotaţie dreapta la fiecare pas al algoritmului. Ei nu sunt predaţi pentru că Arborii Roşu-Negrii sau AVL au demonstraţia că merg în O(log N) şi sunt exemple didactice, dar Treapurile, deşi cu o demonstraţie mai grea, sunt mult mai uşori de implementat şi poate şi puţin mai rapizi ca arborii AVL.

Operaţii

Costul operaţiilor de mai jos este proporţional cu adâncimea unui nod din Treap. Cu ajutorul probabilităţilor se poate deduce că adâncimea aşteptată a oricărui nod este O(log N), ceea ce implică costul unei operaţii să fie O(log N).

Căutare

Căutarea într-un Treap este identică cu cea într-un Arbore Binar de Căutare. Priorităţile din noduri sunt folosite pentru a menţine arborele echilibrat. Pentru a verifica dacă o valoare există putem proceda în felul următor:

int search(T* n, int key)
{
    if (n == nil)  return 0;
    if (key == n->key)  return 1;
    if (key < n->key)
        return search(n->left, key);
    else
        return search(n->right, key);
}

Se observă că algoritmul poate fi scris şi iterativ, lucru ce este recomandat.

Rotaţii

Să urmărim (în desen) efectul rotaţiilor asupra structurii de heap a arborelui. Pentru început, să presupunem că arborele din figura din stânga are o structură de heap cu excepţia lui w care are o prioritate mai mare decât a lui z. Dacă îl rotim pe w spre dreapta (figură rotaţie stânga - dreapta) vedem, în figura din dreapta, că structura de heap este satisfăcută. Din moment ce &A& era un subarbore valid al lui w, va rămâne în continuare un subarbore valid. Cum B şi C se aflau iniţial sub z ei aveau o prioritate mai mică decât a lui z, şi, astfel, după rotaţie ei sunt subarbori valizi pentru z. Este clar că z este un fiu valid al lui w, prin presupunerea făcută iniţial.

Pe de altă parte, să presupun că arborele are o structură de heap, cu excepţia lui z care are o prioritate mai mică decât a ambilor fii şi să presupunem că w este fiul cu prioritatea mai mare.

Inserare

În cadrul operaţiei de inserare vom atribui unui nod nou o prioritate şi îl vom insera, conform algoritmului standard de inserare într-un arbore binar, la baza arborelui. Cheile formează un arbore de căutare, dar priorităţile pot să nu mai formeze un heap. Pentru a redobândi această proprietate, cât timp nodul z are prioritate mai mare decât părintele său, se execută o rotaţie în z, o operaţie locală care scade nivelul lui z cu 1 şi creşte nivelul părintelui lui z cu 1, timp în care invariantul arborilor de căutare este menţinut. Timpul de inserare a lui z este proporţional cu adâncimea lui înainte de rotaţii - trebuie să coborâm după care să urcăm înapoi efectuând rotaţiile necesare.

Ştergere

Operaţia de ştergere este inversul operaţiei de inserare. (Se va desena o figură) Să presupunem că vrem să ştergem nodul z. Cât timp z nu este o frunză, alegem fiul w cu prioritatea mai mare şi facem o rotaţie pentru a aduce pe w în locul lui z. Dacă alegem fiul stâng vom face o rotaţie spre dreapta şi, analog, dacă alegem fiul drept vom face o rotaţie spre stânga. (desen) Când z devine frunză îl ştergem din arbore. Dacă nu îl luăm în considerare pe z, în urma rotaţiilor cei doi invarianţi se menţin. (explicat la rotaţii)

De adăugat: insert, remove, split, join, lookup, rotleft, rotright, print, balance, meld O(mlog(n/m)) - unirea a două treapuri T1 şi T2 fără nicio relaţie de ordine între ele, difference O(mlog(n/m)) - din T1 şi T2 rezultă un T care conţine cheile din T1 care nu sunt în T2, interpretarea geometrică, explicarea rotaţiilor (cum şi de ce se menţin invarianţii).

Coding

Vă puteţi uita, ca şi comparaţie, la funcţiile erase sau balance din articolul următor despre arborii AVL.

struct T {
    int key;
    int priority;
    T *left, *right;
} *R, *nil;

void init(T* &R) 
{
    srand(unsigned(time(0)));

    R = nil = new T();
    nil->key = nil->priority = 0,
        nil->left = nil->right = NULL;
}

void rotleft(T* &n) 
{
    T *t = n->left;
    n->left = t->right, t->right = n;
    n = t;           
}

void rotright(T* &n) 
{
    T *t = n->right;
    n->right = t->left, t->left = n;
    n = t;           
}

void balance(T* &n) 
{
    if (n->left->priority > n->priority)
        rotleft(n);
    else if (n->right->priority > n->priority)
        rotright(n);
}

void insert(T* &n, int key) 
{
    if (n == nil) 
    {
        n = new T();
        n->key = key, n->priority = rand() + 1,
                    n->left = n->right = nil;
        return;
    }
    if (key < n->key)
        insert(n->left, key);
    else if (key > n->key)
        insert(n->right, key);

    balance(n);
}

void erase(T* &n, int key) 
{
    if (n == nil)   return ;

    if (key < n->key)
        erase(n->left, key);
    else if (key > n->key)
        erase(n->right, key);
    else {    
        (n->left->priority > n->right->priority) ? rotleft(n) : rotright(n);
        if (n != nil)  
            erase(n, key);
        else {
            delete n->left;
            n->left = NULL;
        }
    }
}