Perché std :: map è implementato come un albero rosso-nero?


195

Perché è std::mapimplementato come un albero rosso-nero ?

Esistono diversi alberi di ricerca binaria bilanciata (BST). Quali sono stati i compromessi del design nella scelta di un albero rosso-nero?


26
Sebbene tutte le implementazioni che ho visto utilizzino un albero RB, si noti che questo è ancora dipendente dall'implementazione.
Thomas,

3
@Tommaso. Dipende dall'implementazione, quindi perché è così che tutte le implementazioni utilizzano alberi RB?
Denis Gorodetskiy,

1
Mi piacerebbe davvero sapere se qualche implementatore di STL ha pensato di utilizzare un Skip List.
Matthieu M.

2
La mappa e il set di C ++ sono in realtà una mappa e un set ordinati. Non sono implementati usando le funzioni hash. Ogni query richiederebbe O(logn)e non O(1), ma i valori verranno sempre ordinati. A partire da C ++ 11 (penso), ci sono unordered_mape unordered_set, che sono implementati usando le funzioni hash e mentre non sono ordinati, la maggior parte delle query e operazioni sono possibili in O(1)(mediamente)
SomethingSomething

@Thomas è vero, ma non è così interessante in pratica. Lo standard offre garanzie di complessità con un algoritmo specifico o una serie di algoritmi in mente.
Justin Meiners,

Risposte:


126

Probabilmente i due algoritmi dell'albero di auto-bilanciamento più comuni sono gli alberi Rosso-Nero e gli alberi AVL . Per bilanciare l'albero dopo un inserimento / aggiornamento entrambi gli algoritmi utilizzano la nozione di rotazioni in cui i nodi dell'albero vengono ruotati per eseguire il riequilibrio.

Mentre in entrambi gli algoritmi le operazioni di inserimento / eliminazione sono O (log n), nel caso della rotazione di ribilanciamento dell'albero Rosso-Nero è un'operazione O (1) mentre con AVL si tratta di un'operazione O (log n) , rendendo Albero rosso-nero più efficiente in questo aspetto della fase di riequilibrio e uno dei possibili motivi per cui è più comunemente usato.

Gli alberi rosso-nero sono utilizzati nella maggior parte delle librerie di raccolta, comprese le offerte di Java e Microsoft .NET Framework.


54
fai sembrare che gli alberi rosso-neri possano apportare modifiche agli alberi nel tempo O (1), il che non è vero. le modifiche dell'albero sono O (log n) sia per alberi rosso-neri che AVL. ciò rende discutibile se la parte di bilanciamento della modifica dell'albero sia O (1) o O (log n) perché l'operazione principale è già O (log n). anche dopo tutto il lavoro leggermente extra che fanno gli alberi AVL si ottiene un albero più bilanciato che porta a ricerche leggermente più veloci. quindi è un compromesso perfettamente valido e non rende gli alberi AVL inferiori agli alberi rosso-neri.
Negromante

35
Devi vedere oltre la complessità del runtime effettivo per vedere una differenza: gli alberi AVL hanno generalmente un runtime totale inferiore quando ci sono molte più ricerche rispetto agli inserimenti / eliminazioni. Gli alberi RB hanno un tempo di esecuzione totale inferiore quando vi sono molti più inserimenti / eliminazioni. La proporzione esatta in cui si verifica l'interruzione dipende ovviamente da molti dettagli di implementazione, hardware e utilizzo esatto, ma poiché gli autori delle biblioteche devono supportare una vasta gamma di modelli di utilizzo, devono prendere un'ipotesi istruita. AVL è anche leggermente più difficile da implementare, quindi potresti volerne beneficiare.
Steve Jessop,

6
L'albero RB non è una "implementazione predefinita". Ogni implementatore sceglie un'implementazione. Per quanto ne sappiamo, hanno tutti scelto alberi RB, quindi presumibilmente questo è per prestazioni o per facilità di implementazione / manutenzione. Come ho detto, il punto di interruzione per le prestazioni potrebbe non implicare che pensano che ci siano più inserimenti / eliminazioni rispetto alle ricerche, solo che il rapporto tra i due è al di sopra del livello in cui pensano che RB probabilmente superi AVL.
Steve Jessop,

9
@Denis: purtroppo l'unico modo per ottenere numeri è fare un elenco di std::mapimplementazioni, rintracciare gli sviluppatori e chiedere loro quali criteri hanno usato per prendere la decisione, quindi rimane una speculazione.
Steve Jessop,

4
Manca tutto questo è il costo, per nodo, per memorizzare le informazioni ausiliarie necessarie per prendere decisioni di bilancio. Gli alberi rosso-nero richiedono 1 bit per rappresentare il colore. Gli alberi AVL richiedono almeno 2 bit (per rappresentare -1, 0 o 1).
SJHowe,

47

Dipende davvero dall'uso. L'albero AVL di solito ha più rotazioni di ribilanciamento. Quindi, se la tua applicazione non ha troppe operazioni di inserimento ed eliminazione, ma pesa molto sulla ricerca, probabilmente l'albero AVL è una buona scelta.

std::map usa l'albero rosso-nero in quanto ottiene un ragionevole compromesso tra la velocità di inserimento / cancellazione dei nodi e la ricerca.


1
Sei sicuro di questo??? Personalmente penso che l'albero rosso-nero sia uno o più complesso, mai più semplice. L'unica cosa, è nell'albero Rd-Black, il riequilibrio si verifica meno spesso di AVL.
Eric Ouellet,

1
@Eric Teoricamente, sia l'albero R / B che l'albero AVL hanno complessità O (log n)) per l'inserimento e la cancellazione. Ma una grande parte del costo dell'operazione è la rotazione, che è diversa tra questi due alberi. Si prega di fare riferimento a discuss.fogcreek.com/joelonsoftware/… Citazione: "il bilanciamento di un albero AVL può richiedere rotazioni O (log n), mentre un albero nero rosso impiegherà al massimo due rotazioni per bilanciarlo (anche se potrebbe essere necessario esaminare i nodi O (log n) per decidere dove sono necessarie le rotazioni). " Modificato i miei commenti di conseguenza.
webbertiger,

27

Gli alberi AVL hanno un'altezza massima di 1,44logn, mentre gli alberi RB hanno un massimo di 2logn. L'inserimento di un elemento in un AVL può comportare un ribilanciamento in un punto dell'albero. Il riequilibrio termina l'inserimento. Dopo l'inserimento di una nuova foglia, l'aggiornamento degli antenati di quella foglia deve essere effettuato fino alla radice o fino a un punto in cui i due sottotitoli abbiano la stessa profondità. La probabilità di dover aggiornare i nodi k è 1/3 ^ k. Riequilibrare è O (1). La rimozione di un elemento può comportare più di un riequilibrio (fino a metà della profondità dell'albero).

Gli alberi RB sono alberi B dell'ordine 4 rappresentati come alberi di ricerca binari. Un 4-nodo nell'albero B produce due livelli nel BST equivalente. Nel peggiore dei casi, tutti i nodi dell'albero sono 2 nodi, con una sola catena di 3 nodi fino a una foglia. Quella foglia si troverà ad una distanza di 2 lgn dalla radice.

Scendendo dalla radice al punto di inserimento, è necessario cambiare 4 nodi in 2 nodi, per assicurarsi che qualsiasi inserimento non saturi una foglia. Tornando dall'inserimento, tutti questi nodi devono essere analizzati per assicurarsi che rappresentino correttamente i 4 nodi. Questo può essere fatto anche scendendo nell'albero. Il costo globale sarà lo stesso. Non c'è pranzo gratis! La rimozione di un elemento dall'albero è dello stesso ordine.

Tutti questi alberi richiedono che i nodi trasportino informazioni su altezza, peso, colore, ecc. Solo gli alberi Splay sono privi di tali informazioni aggiuntive. Ma la maggior parte delle persone ha paura degli alberi Splay, a causa della fragilità della loro struttura!

Infine, gli alberi possono anche trasportare informazioni sul peso nei nodi, consentendo il bilanciamento del peso. Possono essere applicati vari schemi. Si dovrebbe riequilibrare quando una sottostruttura contiene più di 3 volte il numero di elementi dell'altra sottostruttura. Il riequilibrio viene nuovamente eseguito con una rotazione singola o doppia. Questo significa un caso peggiore di 2.4logn. Si può cavarsela con 2 volte invece di 3, un rapporto molto migliore, ma può significare lasciare un po 'meno dell'1% dei sottotitoli sbilanciati qua e là. Difficile!

Quale tipo di albero è il migliore? AVL di sicuro. Sono i più semplici da codificare e hanno la loro altezza peggiore più vicina al logn. Per un albero di 1000000 elementi, un AVL avrà un'altezza massima di 29, un RB 40 e un peso bilanciato di 36 o 50 a seconda del rapporto.

Esistono molte altre variabili: casualità, rapporto di aggiunte, eliminazioni, ricerche, ecc.


2
Buona risposta. Ma se gli AVL sono i migliori, perché la libreria standard implementa std :: map come albero RB?
Denis Gorodetskiy,

14
Non sono d'accordo sul fatto che gli alberi AVL siano senza dubbio i migliori. Sebbene abbiano un'altezza ridotta, richiedono (in totale) più lavoro per eseguire il riequilibrio rispetto agli alberi rosso / nero (O (log n) riequilibrante rispetto a O (1) ammortizzato). Splay alberi potrebbe essere molto, molto meglio e la tua affermazione che le persone hanno paura di loro è infondata. Non esiste uno schema di bilanciamento degli alberi "migliore" universale là fuori.
templatetypedef

Risposta quasi perfetta. Perché hai detto che AVL è il migliore. Questo è semplicemente sbagliato ed è per questo che la maggior parte delle implementazioni generali usa l'albero rosso-nero. È necessario disporre di un rapporto piuttosto elevato di manipolazione read over per scegliere AVL. Inoltre, AVL ha un ingombro di memoria leggermente inferiore rispetto a RB.
Eric Ouellet,

Concordo sul fatto che l'AVL tende a essere migliore nella maggior parte dei casi, perché di solito gli alberi vengono cercati più spesso di quanto vengano inseriti. Perché l'albero RB è così ampiamente considerato migliore quando è quello con un leggero vantaggio nel caso in cui si scrive per lo più in scrittura e, cosa ancora più importante, un leggero svantaggio nel caso in cui si legge principalmente? Si ritiene davvero che inserirai più di quello che troverai?
doug65536,

25

Le risposte precedenti riguardano solo le alternative dell'albero e il rosso nero probabilmente rimane solo per ragioni storiche.

Perché non una tabella hash?

Un tipo richiede solo l' <operatore (confronto) da utilizzare come chiave in un albero. Tuttavia, le tabelle hash richiedono che ogni tipo di chiave abbia unhash funzione definita. Mantenere i requisiti di tipo al minimo è molto importante per la programmazione generica, quindi è possibile utilizzarlo con un'ampia varietà di tipi e algoritmi.

La progettazione di una buona tabella hash richiede una conoscenza intima del contesto in cui verrà utilizzata. Dovrebbe utilizzare l'indirizzamento aperto o il concatenamento collegato? Quali livelli di carico dovrebbe accettare prima del ridimensionamento? Dovrebbe usare un hash costoso che evita le collisioni o uno che è approssimativo e veloce?

Poiché l'STL non può prevedere quale sia la scelta migliore per la propria applicazione, l'impostazione predefinita deve essere più flessibile. Gli alberi "funzionano" e si adattano bene.

(C ++ 11 ha aggiunto tabelle hash unordered_map. Dalla documentazione è possibile vedere che richiede l'impostazione delle politiche per configurare molte di queste opzioni.)

E gli altri alberi?

Gli alberi Red Black offrono una ricerca rapida e si bilanciano da soli, a differenza dei BST. Un altro utente ha sottolineato i suoi vantaggi rispetto all'albero AVL auto-bilanciante.

Alexander Stepanov (Il creatore di STL) ha detto che avrebbe usato un albero B * invece di un albero rosso-nero se avesse scritto di std::mapnuovo, perché è più amichevole per le moderne cache di memoria.

Uno dei maggiori cambiamenti da allora è stata la crescita delle cache. I cache miss sono molto costosi, quindi la località di riferimento è molto più importante ora. Le strutture dati basate su nodo, che hanno una bassa località di riferimento, hanno molto meno senso. Se progettassi STL oggi, avrei un diverso set di contenitori. Ad esempio, un albero B * in memoria è una scelta molto migliore di un albero rosso-nero per l'implementazione di un contenitore associativo. - Alexander Stepanov

Le mappe dovrebbero sempre usare alberi?

Un'altra possibile implementazione delle mappe sarebbe un vettore ordinato (ordinamento per inserzione) e una ricerca binaria. Funzionerebbe bene per i contenitori che non vengono modificati spesso ma che vengono interrogati frequentemente. Lo faccio spesso in C come qsorte bsearchsono integrati.

Devo anche usare la mappa?

Considerazioni sulla cache significano che raramente ha senso usare std::listo std::dequeoltrestd:vector anche per quelle situazioni che ci hanno insegnato a scuola (come rimuovere un elemento dal centro dell'elenco). Applicando lo stesso ragionamento, utilizzare un ciclo for per la ricerca lineare di un elenco è spesso più efficiente e più pulito rispetto alla creazione di una mappa per alcune ricerche.

Naturalmente la scelta di un contenitore leggibile è in genere più importante delle prestazioni.


3

Aggiornamento 14/06/2017: webbertiger modifica la sua risposta dopo aver commentato. Devo sottolineare che la sua risposta ora è molto meglio per i miei occhi. Ma ho mantenuto la mia risposta come ulteriori informazioni ...

A causa del fatto che penso che la prima risposta sia sbagliata (correzione: non entrambe le cose) e la terza ha un'affermazione sbagliata. Sento di dover chiarire le cose ...

I 2 alberi più popolari sono AVL e Red Black (RB). La differenza principale risiede nell'utilizzo:

  • AVL: migliore se il rapporto di consultazione (leggi) è maggiore della manipolazione (modifica). La stampa del footprint di memoria è leggermente inferiore a RB (a causa del bit richiesto per la colorazione).
  • RB: Meglio in casi generali in cui esiste un equilibrio tra consultazione (leggi) e manipolazione (modifica) o più modifiche rispetto alla consultazione. Un footprint di memoria leggermente più grande a causa della memorizzazione della bandiera rosso-nera.

La differenza principale viene dalla colorazione. Nell'albero RB hai meno azioni di riequilibrio rispetto a AVL perché la colorazione ti consente talvolta di saltare o ridurre le azioni di riequilibrio che hanno un costo relativo elevato. A causa della colorazione, l'albero RB ha anche un livello più elevato di nodi perché potrebbe accettare nodi rossi tra quelli neri (con possibilità di ~ 2x più livelli) rendendo la ricerca (leggi) un po 'meno efficiente ... ma perché è un costante (2x), rimane in O (log n).

Se si considera l'hit performance per una modifica di un albero (significativo) VS l'hit performance della consultazione di un albero (quasi insignificante), diventa naturale preferire RB rispetto a AVL per un caso generale.


2

È solo la scelta della tua implementazione: potrebbero essere implementate come qualsiasi albero bilanciato. Le varie scelte sono tutte comparabili con differenze minori. Pertanto, qualsiasi è buono come un altro.

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.