Quale albero binario autobilanciante consiglieresti?


18

Sto imparando Haskell e come esercizio sto facendo alberi binari. Avendo creato un normale albero binario, voglio adattarlo per bilanciarmi da solo. Così:

  • Qual è il più efficiente?
  • Qual è la più facile da implementare?
  • Qual è il più usato?

Ma soprattutto, quale mi consigliate?

Presumo che questo appartenga qui perché è aperto al dibattito.


In termini di efficienza e facilità di implementazione, le efficienze generali sono ben definite, ma per la tua implementazione, ritengo che la cosa migliore sarebbe implementarne quante ne puoi trovare e quindi farci sapere quale funziona meglio ...
glenatron

Risposte:


15

Ti consiglierei di iniziare con un albero rosso-nero o un albero AVL .

L'albero rosso-nero è più veloce per l'inserimento, ma l'albero AVL ha un leggero margine per le ricerche. L'albero AVL è probabilmente un po 'più facile da implementare, ma non molto sulla base della mia esperienza.

L'albero AVL assicura che l'albero sia bilanciato dopo ogni inserimento o cancellazione (nessun sottoalbero ha un fattore di equilibrio maggiore di 1 / -1, mentre l'albero rosso-nero assicura che l'albero sia ragionevolmente bilanciato in qualsiasi momento.


1
Personalmente, trovo l'inserto rosso-nero più facile dell'inserto AVL. Il motivo è attraverso l'analogia (imperfetta) con gli alberi B. Gli inserti sono complicati, ma le eliminazioni sono malvagie (tanti casi da considerare). In effetti non ho più un'implementazione di eliminazione rosso-nero in C ++ di mia proprietà - l'ho eliminata quando mi sono reso conto (1) che non la usavo mai - ogni volta che volevo eliminare stavo cancellando più elementi, quindi ho convertito dall'albero in elenco, elimina dall'elenco, quindi converti nuovamente in un albero e (2) è stato comunque interrotto.
Steve314,

2
@ Steve314, gli alberi rosso-neri sono più facili, ma non sei stato in grado di realizzare un'implementazione che funzioni? Come sono allora gli alberi AVL?
dan_waterworth,

@dan_waterworth - Non ho ancora realizzato un'implementazione con un metodo di inserimento che funziona ancora - ho delle note, capisco il principio di base, ma non ho mai avuto la giusta combinazione di motivazione, tempo e fiducia. Se volevo solo versioni che funzionassero, è solo copia-pseudocodice-da-libro di testo-e-traduci (e non dimenticare che C ++ ha contenitori di librerie standard), ma dov'è il divertimento?
Steve314,

A proposito: credo (ma non posso fornire il riferimento) che un libro di testo abbastanza popolare includa un'implementazione errata di uno degli algoritmi di albero binario bilanciato - non sono sicuro, ma potrebbe essere l'eliminazione rosso-nero. Quindi non sono solo io ;-)
Steve314

1
@ Steve314, lo so, gli alberi possono essere diabolicamente complicati in un linguaggio imperativo, ma sorprendentemente, implementarli in Haskell è stato un gioco da ragazzi. Ho scritto un normale albero AVL e anche una variante spaziale 1D nel fine settimana e sono entrambi solo circa 60 righe.
dan_waterworth,

10

Vorrei prendere in considerazione un'alternativa se stai bene con strutture di dati randomizzate : Skip Lists .

Da un punto di vista di alto livello, è una struttura ad albero, tranne per il fatto che non è implementato come un albero ma come un elenco con più livelli di collegamenti.

Otterrai inserimenti / ricerche / eliminazioni O (log N) e non dovrai occuparti di tutti quei difficili casi di ribilanciamento.

Non ho mai considerato di implementarli in un linguaggio funzionale, e la pagina di Wikipedia non ne mostra nessuno, quindi potrebbe non essere facile (scritto sull'immutabilità)


Mi piace molto saltare gli elenchi e li ho implementati prima, anche se non in un linguaggio funzionale. Penso che ci proverò dopo questo, ma in questo momento sono su alberi auto-equilibranti.
dan_waterworth,

Inoltre, le persone usano spesso skiplist per strutture di dati simultanee. Potrebbe essere meglio, invece di forzare l'immutabilità, usare le primitive di concorrenza di haskell (come MVar o TVar). Tuttavia, questo non mi insegnerà molto sulla scrittura di codice funzionale.
dan_waterworth,

2
@ Fanatic23, una Skip List non è un ADT. L'ADT è un set o un array associativo.
dan_waterworth,

@dan_waterworth mio male, hai ragione.
Fanatic23,

5

Se vuoi iniziare con una struttura relativamente semplice (sia gli alberi AVL che gli alberi rosso-neri sono complicati), un'opzione è un passo - chiamato come una combinazione di "albero" e "mucchio".

Ogni nodo ottiene un valore "prioritario", spesso assegnato casualmente quando viene creato il nodo. I nodi sono posizionati nella struttura in modo da rispettare l'ordinamento delle chiavi e in modo da rispettare l'ordinamento simile a heap dei valori di priorità. Un ordinamento simile a un heap significa che entrambi i figli di un genitore hanno priorità inferiori rispetto al genitore.

EDIT cancellato "all'interno dei valori chiave" sopra: la priorità e l'ordinamento delle chiavi si applicano insieme, quindi la priorità è significativa anche per chiavi univoche.

È una combinazione interessante. Se le chiavi sono uniche e le priorità sono uniche, esiste una struttura ad albero unica per qualsiasi set di nodi. Anche così, inserti ed eliminazioni sono efficienti. A rigor di termini, l'albero può essere sbilanciato al punto in cui è effettivamente un elenco collegato, ma questo è estremamente improbabile (come con gli alberi binari standard), anche per casi normali come le chiavi inserite in ordine (a differenza degli alberi binari standard).


1
+1. Treaps è una mia scelta personale, ho persino scritto un post sul blog su come sono implementati.
P:

5

Qual è il più efficiente?

Vago e difficile rispondere. Le complessità computazionali sono tutte ben definite. Se questo è ciò che intendi per efficienza, non c'è un vero dibattito. In effetti, tutti i buoni algoritmi vengono forniti con prove e fattori di complessità.

Se intendi "tempo di esecuzione" o "utilizzo della memoria", dovrai confrontare le implementazioni effettive. Quindi lingua, tempo di esecuzione, sistema operativo e altri fattori entrano in gioco, rendendo difficile rispondere alla domanda.

Qual è la più facile da implementare?

Vago e difficile rispondere. Alcuni algoritmi possono sembrare complessi per te, ma per me banali.

Qual è il più usato?

Vago e difficile rispondere. Prima c'è il "da chi?" parte di questo? Solo Haskell? Che dire di C o C ++? In secondo luogo, c'è il problema del software proprietario in cui non abbiamo accesso alla fonte per fare un sondaggio.

Ma soprattutto, quale mi consigliate?

Presumo che questo appartenga qui perché è aperto al dibattito.

Corretta. Poiché i tuoi altri criteri non sono molto utili, questo è tutto ciò che otterrai.

È possibile ottenere la fonte per un gran numero di algoritmi ad albero. Se vuoi imparare qualcosa, potresti semplicemente implementare tutti quelli che riesci a trovare. Invece di chiedere una "raccomandazione", basta raccogliere tutti gli algoritmi che riesci a trovare.

Ecco la lista:

http://en.wikipedia.org/wiki/Self-balancing_binary_search_tree

Ne sono definiti sei popolari. Inizia con quelli.


3

Se sei interessato agli alberi di Splay, esiste una versione più semplice di quelli che credo siano stati descritti per la prima volta in un articolo di Allen e Munroe. Non ha le stesse garanzie prestazionali, ma evita le complicazioni nell'affrontare il riequilibrio "zig-zig" vs "zig-zag".

Fondamentalmente, durante la ricerca (comprese le ricerche di un punto di inserimento o di un nodo da eliminare), il nodo che si trova viene ruotato direttamente verso la radice, dal basso verso l'alto (ad es. Quando esce una funzione di ricerca ricorsiva). Ad ogni passo, selezioni una singola rotazione a sinistra o a destra a seconda che il bambino che vuoi fare un altro passo verso la radice sia il bambino giusto o il bambino a sinistra (se ricordo correttamente le mie direzioni di rotazione, questo è rispettivamente).

Come per gli alberi Splay, l'idea è che gli oggetti a cui si è avuto accesso di recente siano sempre vicini alla radice dell'albero, così rapidamente di nuovo accesso. Essendo più semplici, questi alberi Allen-Munroe ruotano su radice (ciò che li chiamo - non conosco il nome ufficiale) può essere più veloce, ma non hanno la stessa garanzia di prestazioni ammortizzata.

Una cosa: poiché questa struttura di dati, per definizione, muta anche per le operazioni di ricerca, probabilmente dovrebbe essere implementata monadicamente. IOW forse non è adatto alla programmazione funzionale.


Gli spettacoli sono un po 'fastidiosi dato che modificano l'albero anche quando lo trovano. Questo sarebbe piuttosto doloroso in ambienti multi-thread, che è una delle grandi motivazioni per usare un linguaggio funzionale come Haskell in primo luogo. Inoltre, non ho mai usato linguaggi funzionali prima d'ora, quindi forse questo non è un fattore.
Rapido Joe Smith

@Quick: dipende da come si intende utilizzare l'albero. Se lo stessi usando un vero codice di stile funzionale, lasceresti cadere la mutazione ad ogni ricerca (rendendo un albero di Splay un po 'sciocco), o finiresti per duplicare una parte sostanziale dell'albero binario su ogni ricerca, e tieni traccia dello stato dell'albero con cui lavori man mano che il tuo lavoro avanza (la ragione per cui probabilmente usi uno stile monadico). Quella copia potrebbe essere ottimizzata dal compilatore se non si fa più riferimento al vecchio stato dell'albero dopo la creazione di quello nuovo (ipotesi simili sono comuni nella programmazione funzionale), ma potrebbe non esserlo.
Steve314,

Nessuno dei due approcci sembra valga la pena. D'altra parte, nemmeno i linguaggi puramente funzionali per la maggior parte.
Rapido Joe Smith

1
@Quick: la duplicazione dell'albero è ciò che farai per qualsiasi struttura di dati dell'albero in un linguaggio funzionale puro per algoritmi mutanti come gli inserti. In termini di fonte, il codice non sarà così diverso dal codice imperativo che esegue aggiornamenti sul posto. Le differenze sono già state gestite, presumibilmente, per alberi binari non bilanciati. Finché non provi ad aggiungere collegamenti principali ai nodi, i duplicati condivideranno almeno i sottotitoli comuni e la profonda ottimizzazione in Haskell è piuttosto hardcore se non perfetta. Io stesso sono anti-Haskell in linea di principio, ma questo non è necessariamente un problema.
Steve314,

2

Un albero bilanciato molto semplice è un albero AA . È invariante è più semplice e quindi più facile da attuare. Per la sua semplicità, le sue prestazioni sono ancora buone.

Come esercizio avanzato, puoi provare a utilizzare i GADT per implementare una delle varianti di alberi bilanciati il ​​cui invariante è applicato dal tipo di sistema.

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.