Perché gli alberi rosso-neri sono così popolari?


46

Sembra che ovunque io guardi, le strutture di dati vengono implementate usando alberi rosso-neri ( std::setin C ++, SortedDictionaryin C #, ecc.)

Avendo appena coperto (a, b), alberi rosso-nero e AVL nella mia classe di algoritmi, ecco cosa sono uscito (anche chiedendo in giro professori, sfogliando alcuni libri e google un po '):

  • Gli alberi AVL hanno una profondità media inferiore rispetto agli alberi rosso-neri, quindi la ricerca di un valore nell'albero AVL è sempre più veloce.
  • Gli alberi rosso-neri apportano meno modifiche strutturali per bilanciarsi rispetto agli alberi AVL, il che potrebbe renderli potenzialmente più veloci per l'inserimento / l'eliminazione. Sto dicendo potenzialmente, perché ciò dipenderebbe dal costo della modifica strutturale all'albero, poiché ciò dipenderà molto dal tempo di esecuzione e dall'attuazione (potrebbe anche essere completamente diverso in un linguaggio funzionale quando l'albero è immutabile?)

Esistono molti parametri di riferimento online che mettono a confronto gli alberi AVL e rosso-nero, ma ciò che mi ha colpito è che il mio professore sostanzialmente ha detto che di solito avresti fatto una delle due cose:

  • O non ti interessa molto delle prestazioni, nel qual caso la differenza del 10-20% di AVL rispetto al rosso-nero nella maggior parte dei casi non importa affatto.
  • Oppure ti preoccupi davvero delle prestazioni, nel qual caso dovresti abbandonare sia gli alberi AVL che gli alberi rosso-nero e andare con gli alberi B, che possono essere modificati per funzionare molto meglio (o (a, b) -trees, I ' Metterò tutti quelli nello stesso paniere.)

Il motivo è che un albero B archivia i dati in modo più compatto nella memoria (un nodo contiene molti valori) e ci saranno molti meno errori nella cache. È inoltre possibile modificare l'implementazione in base al caso d'uso e far dipendere l'ordine dell'albero B dalla dimensione della cache della CPU, ecc.

Il problema è che non riesco a trovare quasi nessuna fonte in grado di analizzare l'utilizzo nella vita reale di diverse implementazioni di alberi di ricerca su hardware moderno reale. Ho esaminato molti libri sugli algoritmi e non ho trovato nulla che potesse confrontare insieme diverse varianti di alberi, oltre a mostrare che uno ha una profondità media inferiore rispetto all'altro (il che non dice molto su come si comporterà l'albero in programmi reali.)

Detto questo, c'è un motivo particolare per cui gli alberi rosso-neri vengono usati ovunque, quando in base a quanto detto sopra, gli alberi B dovrebbero essere più performanti? (poiché l'unico punto di riferimento che ho trovato mostra anche http://lh3lh3.users.sourceforge.net/udb.shtml , ma potrebbe trattarsi solo di un'implementazione specifica). O è il motivo per cui tutti usano alberi rosso-neri perché sono piuttosto facili da implementare o, in altre parole, difficili da implementare male?

Inoltre, come cambia questo quando ci si sposta nel regno dei linguaggi funzionali? Sembra che sia Clojure che Scala utilizzino i tentativi mappati di array Hash , dove Clojure utilizza un fattore di ramificazione di 32.


8
Per aggiungere al tuo dolore, la maggior parte degli articoli che confrontano diversi tipi di alberi di ricerca eseguono ... meno di esperimenti ideali.
Raffaello

1
Non l'ho mai capito da solo, secondo me gli alberi AVL sono più facili da implementare rispetto agli alberi rosso-neri (meno casi durante il ribilanciamento) e non ho mai notato una differenza significativa nelle prestazioni.
Jordi Vermeulen,

3
Una discussione pertinente dei nostri amici su StackOverflow Perché std :: map è implementato come un albero rosso-nero? .
Hendrik Jan

Risposte:


10

Per citare la risposta alla domanda " Traversals from the root in AVL trees and Red Black Trees "

Per alcuni tipi di alberi di ricerca binari, inclusi alberi rosso-neri ma non alberi AVL, le "correzioni" sull'albero possono essere facilmente previste sulla discesa ed eseguite durante un singolo passaggio dall'alto verso il basso, rendendo superfluo il secondo passaggio. Tali algoritmi di inserzione sono in genere implementati con un ciclo piuttosto che con la ricorsione, e spesso in pratica sono leggermente più veloci rispetto alle loro controparti a due passaggi.

Quindi un inserto dell'albero RedBlack può essere implementato senza ricorsione, su alcune CPU la ricorsione è molto costosa se si supera la cache delle chiamate di funzione (ad es. SPARC a causa dell'uso della finestra Register )

(Ho visto il software girare 10 volte più velocemente su Sparc rimuovendo una chiamata di funzione, il che ha portato a un percorso del codice spesso chiamato troppo profondo per la finestra del registro. Dato che non sai quanto sarà profonda la finestra del registro il sistema del tuo cliente e non sai fino a che punto dello stack di chiamate ti trovi nel "percorso del codice attivo", non utilizzare la ricorsione rende più prevedibile.)

Inoltre, non rischiare di rimanere senza stack è un vantaggio.


Ma un albero bilanciato con 2 ^ 32 nodi richiederebbe non più di circa 32 livelli di ricorsione. Anche se il frame dello stack è di 64 byte, non si tratta di più di 2 kb di spazio dello stack. Può davvero fare la differenza? Ne dubiterei.
Björn Lindqvist,

@ BjörnLindqvist, Sul processore SPARC negli anni '90 ho spesso ottenuto una velocità superiore a 10 volte modificando un percorso di codice comune da una profondità dello stack da 7 a 6! Leggi come ha registrato i file ....
Ian Ringrose,

9

Ho fatto ricerche anche su questo argomento di recente, quindi ecco i miei risultati, ma tieni presente che non sono un esperto di strutture di dati!

Ci sono alcuni casi in cui non puoi usare affatto B-alberi.

Un caso di spicco è std::mapdi C ++ STL. Lo standard richiede che insertnon invalidi gli iteratori esistenti

Nessun iteratore o riferimento sono invalidati.

http://en.cppreference.com/w/cpp/container/map/insert

Questo esclude B-tree come implementazione perché l'inserimento si sposterebbe intorno agli elementi esistenti.

Un altro caso d'uso simile sono le strutture dati intrusive. Cioè, invece di archiviare i tuoi dati all'interno del nodo dell'albero, memorizzi i puntatori ai figli / genitori all'interno della tua struttura:

// non intrusive
struct Node<T> {
    T value;
    Node<T> *left;
    Node<T> *right;
};
using WalrusList = Node<Walrus>;

// intrusive
struct Walrus {
    // Tree part
    Walrus *left;
    Walrus *right;

    // Object part
    int age;
    Food[4] stomach;
};

Non è possibile rendere invadente un albero B, perché non è una struttura di dati solo puntatore.

In jemalloc vengono utilizzati, ad esempio, alberi intrusivi rosso-neri per gestire blocchi di memoria liberi. Questa è anche una struttura di dati popolare nel kernel Linux.

Credo anche che l'implementazione "single pass tail recursive" non sia la ragione della popolarità dell'albero rosso nero come struttura di dati mutabili .

logn

O(1)

O(1)

La variante descritta in opendatastructures utilizza puntatori parent, un down pass ricorsivo per l'inserimento e un pass ripetuto iterativo per i fixup. Le chiamate ricorsive sono in posizioni di coda e i compilatori ottimizzano questo in un ciclo (ho controllato questo in Rust).

O(1)


3

Bene, questa non è una risposta autorevole, ma ogni volta che devo codificare un albero di ricerca binaria bilanciato, è un albero rosso-nero. Ci sono alcuni motivi per questo:

1) Il costo medio di inserimento è costante per gli alberi rosso-neri (se non è necessario cercare), mentre è logaritmico per gli alberi AVL. Inoltre, comporta al massimo una complicata ristrutturazione. È ancora O (log N) nel peggiore dei casi, ma si tratta solo di semplici ricolorazioni.

2) Richiedono solo 1 bit di informazioni extra per nodo e spesso puoi trovare un modo per ottenerlo gratuitamente.

3) Non devo farlo molto spesso, quindi ogni volta che lo faccio devo capire come farlo di nuovo. Le regole semplici e la corrispondenza con 2-4 alberi fa sembrare facile ogni volta , anche se il codice risulta essere complicato ogni volta . Spero ancora che un giorno il codice risulti semplice.

4) Il modo in cui l'albero rosso-nero divide il nodo dell'albero 2-4 corrispondente e inserisce la chiave centrale nel nodo padre 2-4 semplicemente ricolorandolo è super elegante. Adoro farlo.


0

Gli alberi rosso-nero o AVL hanno un vantaggio rispetto agli alberi B e simili quando la chiave è lunga o per qualche altro motivo spostare una chiave è costoso.

Ho creato la mia alternativa std::setall'interno di un grande progetto, per una serie di motivi di performance. Ho scelto AVL sul rosso-nero per motivi di prestazioni (ma quel piccolo miglioramento delle prestazioni non era la giustificazione per il mio lancio invece di std :: set). La "chiave", complicata e difficile da spostare, è stata un fattore significativo. Gli alberi (a, b) hanno ancora senso se hai bisogno di un altro livello di riferimento indiretto davanti alle chiavi? Gli alberi AVL e rosso-nero possono essere ristrutturati senza spostare le chiavi, quindi hanno questo vantaggio quando le chiavi sono costose da spostare.


Ironia della sorte, gli alberi rosso-neri sono "solo" un caso speciale di (a, b) -trees, quindi la questione sembra dipendere da una modifica dei parametri? (cc @Gilles)
Raffaello
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.