Risposte:
Litigare sulle prestazioni degli alberi binari non ha senso: non sono una struttura di dati, ma una famiglia di strutture di dati, tutte con caratteristiche di prestazione diverse. Mentre è vero che gli alberi binari sbilanciati si comportano molto peggio degli alberi binari autobilanciati per la ricerca, ci sono molti alberi binari (come i tentativi binari) per i quali "bilanciamento" non ha alcun significato.
map
e set
nelle librerie di molte lingue.Il motivo per cui gli alberi binari vengono utilizzati più spesso degli alberi n-ary per la ricerca è che gli alberi n-ary sono più complessi, ma di solito non offrono un reale vantaggio di velocità.
In un albero binario (bilanciato) con m
nodi, passare da un livello all'altro richiede un confronto e ci sono log_2(m)
livelli, per un totale di log_2(m)
confronti.
Al contrario, un albero n-ary richiederà log_2(n)
confronti (usando una ricerca binaria) per passare al livello successivo. Poiché ci sono log_n(m)
livelli totali, la ricerca richiederà log_2(n)*log_n(m)
= log_2(m)
confronti totali. Quindi, sebbene gli alberi n-ary siano più complessi, non offrono alcun vantaggio in termini di confronti totali necessari.
(Tuttavia, gli alberi n-ary sono ancora utili in situazioni di nicchia. Gli esempi che vengono immediatamente in mente sono quad-alberi e altri alberi di partizione spaziale, in cui la divisione dello spazio usando solo due nodi per livello renderebbe inutilmente complessa la logica; e B-alberi utilizzati in molti database, in cui il fattore limitante non è il numero di confronti effettuati a ciascun livello, ma quanti nodi possono essere caricati dal disco rigido contemporaneamente)
Quando la maggior parte delle persone parla di alberi binari, molto spesso non pensa agli alberi di ricerca binari , quindi lo tratterò per primo.
Un albero di ricerca binaria non bilanciato è in realtà utile per poco più che educare gli studenti sulle strutture di dati. Questo perché, a meno che i dati non arrivino in un ordine relativamente casuale, l'albero può facilmente degenerare nella sua forma peggiore, che è un elenco collegato, poiché i semplici alberi binari non sono bilanciati.
Un buon esempio: una volta ho dovuto riparare alcuni software che caricavano i suoi dati in un albero binario per la manipolazione e la ricerca. Ha scritto i dati in forma ordinata:
Alice
Bob
Chloe
David
Edwina
Frank
in modo che, quando lo rileggevo, finiva con il seguente albero:
Alice
/ \
= Bob
/ \
= Chloe
/ \
= David
/ \
= Edwina
/ \
= Frank
/ \
= =
che è la forma degenerata. Se vai a cercare Frank in quell'albero, dovrai cercare tutti e sei i nodi prima di trovarlo.
Gli alberi binari diventano veramente utili per la ricerca quando li equilibrate. Ciò comporta la rotazione di alberi secondari attraverso il loro nodo radice in modo che la differenza di altezza tra due alberi secondari sia minore o uguale a 1. L'aggiunta di quei nomi sopra uno alla volta in un albero bilanciato ti darebbe la seguente sequenza:
1. Alice
/ \
= =
2. Alice
/ \
= Bob
/ \
= =
3. Bob
_/ \_
Alice Chloe
/ \ / \
= = = =
4. Bob
_/ \_
Alice Chloe
/ \ / \
= = = David
/ \
= =
5. Bob
____/ \____
Alice David
/ \ / \
= = Chloe Edwina
/ \ / \
= = = =
6. Chloe
___/ \___
Bob Edwina
/ \ / \
Alice = David Frank
/ \ / \ / \
= = = = = =
Puoi effettivamente vedere interi sotto-alberi che ruotano a sinistra (nei passaggi 3 e 6) quando vengono aggiunte le voci e questo ti dà un albero binario bilanciato in cui la ricerca nel caso peggiore è O(log N)
piuttosto che la O(N
) che dà la forma degenerata. In nessun caso il NULL ( =
) più alto differisce dal più basso di più di un livello. E, nella struttura finale di cui sopra, è possibile trovare Frank da solo guardando tre nodi ( Chloe
, Edwina
e, infine, Frank
).
Certo, possono diventare ancora più utili quando si creano alberi bilanciati a più vie invece di alberi binari. Ciò significa che ogni nodo contiene più di un oggetto (tecnicamente, contengono N oggetti e N + 1 puntatori, un albero binario è un caso speciale di un albero a più vie a 1 via con 1 oggetto e 2 puntatori).
Con un albero a tre vie, si finisce con:
Alice Bob Chloe
/ | | \
= = = David Edwina Frank
/ | | \
= = = =
Questo è in genere utilizzato nel mantenimento delle chiavi per un indice di elementi. Ho scritto un software di database ottimizzato per l'hardware in cui un nodo ha esattamente le dimensioni di un blocco del disco (diciamo 512 byte) e metti quante più chiavi possibile in un singolo nodo. I puntatori in questo caso erano in realtà numeri di record in un file di accesso diretto a lunghezza fissa separato dal file di indice (quindi è X
possibile trovare il numero di record semplicemente cercando di X * record_length
).
Ad esempio, se i puntatori sono 4 byte e la dimensione della chiave è 10, il numero di chiavi in un nodo da 512 byte è 36. Sono 36 chiavi (360 byte) e 37 puntatori (148 byte) per un totale di 508 byte con 4 byte sprecati per nodo.
L'uso di chiavi a più vie introduce la complessità di una ricerca a due fasi (ricerca a più vie per trovare il nodo corretto combinato con una piccola ricerca sequenziale (o binaria lineare) per trovare la chiave corretta nel nodo) ma il vantaggio in fare meno I / O su disco più che compensare questo.
Non vedo alcun motivo per farlo per una struttura in memoria, sarebbe meglio attenersi a un albero binario bilanciato e mantenere semplice il codice.
Inoltre, tieni presente che i vantaggi di O(log N)
over O(N)
non appaiono davvero quando i tuoi set di dati sono piccoli. Se stai usando un albero a più vie per memorizzare le quindici persone nella tua rubrica, è probabilmente eccessivo. I vantaggi derivano dalla memorizzazione di qualcosa come ogni ordine dei tuoi centomila clienti negli ultimi dieci anni.
Il punto centrale della notazione big-O è indicare cosa succede mentre l' N
infinito si avvicina. Alcune persone potrebbero non essere d'accordo, ma è anche accettabile utilizzare l'ordinamento a bolle se sei sicuro che i set di dati rimarranno al di sotto di una determinata dimensione, a condizione che nient'altro sia prontamente disponibile :-)
Per quanto riguarda altri usi per alberi binari, ce ne sono molti, come:
Vista la quantità di spiegazioni che ho generato per gli alberi di ricerca, sono reticente ad approfondire gli altri, ma dovrebbe bastare a ricercarli, se lo desideri.
L'organizzazione del codice Morse è un albero binario.
Un albero binario è una struttura di dati ad albero in cui ogni nodo ha al massimo due nodi figlio, generalmente distinti come "sinistra" e "destra". I nodi con figli sono nodi principali e i nodi figlio possono contenere riferimenti ai loro genitori. Fuori dall'albero, c'è spesso un riferimento al nodo "root" (l'antenato di tutti i nodi), se esiste. È possibile raggiungere qualsiasi nodo nella struttura dei dati iniziando dal nodo principale e seguendo ripetutamente i riferimenti al figlio sinistro o destro. In un albero binario un grado di ogni nodo è massimo due.
Gli alberi binari sono utili, perché come puoi vedere nella figura, se vuoi trovare un nodo nell'albero, devi solo guardare un massimo di 6 volte. Se si desidera cercare il nodo 24, ad esempio, si inizierà dalla radice.
Questa ricerca è illustrata di seguito:
Puoi vedere che puoi escludere metà dei nodi dell'intero albero al primo passaggio. e metà della sottostruttura sinistra sul secondo. Questo rende ricerche molto efficaci. Se ciò avvenisse su 4 miliardi di elementi, dovresti cercare solo un massimo di 32 volte. Pertanto, più elementi sono contenuti nella struttura, più efficiente può essere la ricerca.
Le eliminazioni possono diventare complesse. Se il nodo ha 0 o 1 figlio, allora è semplicemente una questione di spostare alcuni puntatori per escludere quello da eliminare. Tuttavia, non è possibile eliminare facilmente un nodo con 2 figli. Quindi prendiamo una scorciatoia. Diciamo che volevamo eliminare il nodo 19.
Dal momento che cercare di determinare dove spostare i puntatori sinistro e destro non è facile, ne troviamo uno con cui sostituirlo. Andiamo al sottoalbero di sinistra e andiamo il più a destra possibile. Questo ci dà il prossimo valore più grande del nodo che vogliamo eliminare.
Ora copiamo tutti i contenuti di 18, ad eccezione dei puntatori sinistro e destro, ed eliminiamo il nodo 18 originale.
Per creare queste immagini, ho implementato un albero AVL, un albero auto-bilanciante, in modo che in ogni momento, l'albero abbia al massimo un livello di differenza tra i nodi foglia (nodi senza figli). Ciò evita che l'albero si inclini e mantiene il O(log n)
tempo di ricerca massimo , con il costo di un po 'più di tempo richiesto per inserimenti ed eliminazioni.
Ecco un esempio che mostra come il mio albero AVL si è mantenuto il più compatto ed equilibrato possibile.
In un array ordinato, le ricerche continuerebbero comunque O(log(n))
, proprio come un albero, ma l'inserimento e la rimozione casuali richiederebbero O (n) invece dell'albero O(log(n))
. Alcuni contenitori STL utilizzano queste caratteristiche prestazionali a proprio vantaggio, pertanto i tempi di inserimento e rimozione richiedono un massimo di O(log n)
, il che è molto veloce. Alcuni di questi contenitori sono map
, multimap
, set
, e multiset
.
Il codice di esempio per un albero AVL è disponibile all'indirizzo http://ideone.com/MheW8
L'applicazione principale è alberi di ricerca binari . Si tratta di una struttura di dati in cui la ricerca, l'inserimento e la rimozione sono tutti molto rapidi (sulle log(n)
operazioni)
Un esempio interessante di un albero binario che non è stato menzionato è quello di un'espressione matematica valutata in modo ricorsivo. Fondamentalmente è inutile dal punto di vista pratico, ma è un modo interessante di pensare a tali espressioni.
Fondamentalmente ogni nodo dell'albero ha un valore che è inerente a se stesso o viene valutato ricorsivamente operando sui valori dei suoi figli.
Ad esempio, l'espressione (1+3)*2
può essere espressa come:
*
/ \
+ 2
/ \
1 3
Per valutare l'espressione, chiediamo il valore del genitore. Questo nodo a sua volta ottiene i suoi valori dai suoi figli, un operatore più e un nodo che contiene semplicemente "2". L'operatore plus a sua volta ottiene i suoi valori dai bambini con i valori '1' e '3' e li aggiunge, restituendo 4 al nodo di moltiplicazione che restituisce 8.
Questo uso di un albero binario è simile a invertire la notazione polacca in un certo senso, in quanto l'ordine in cui vengono eseguite le operazioni è identico. Inoltre, una cosa da notare è che non deve necessariamente essere un albero binario, è solo che gli operatori più comunemente usati sono binari. Al suo livello più elementare, l'albero binario qui è in realtà solo un linguaggio di programmazione puramente funzionale molto semplice.
Applicazioni dell'albero binario:
Non credo che ci sia alcun uso per alberi binari "puri". (tranne per scopi didattici) Alberi binari bilanciati, come alberi rosso-neri o alberi AVL sono molto più utili, perché garantiscono operazioni O (logn). Gli alberi binari normali possono finire per essere un elenco (o quasi un elenco) e non sono molto utili nelle applicazioni che utilizzano molti dati.
Gli alberi bilanciati vengono spesso utilizzati per implementare mappe o set. Possono anche essere utilizzati per l'ordinamento in O (nlogn), anche se esistono modi migliori per farlo.
Anche per la ricerca / inserimento / eliminazione possono essere utilizzate tabelle hash , che di solito hanno prestazioni migliori rispetto agli alberi di ricerca binari (bilanciati o meno).
Un'applicazione in cui sarebbero utili alberi di ricerca binaria (bilanciata) sarebbe se fossero necessarie la ricerca / l'inserimento / la cancellazione e l'ordinamento. L'ordinamento potrebbe essere sul posto (quasi, ignorando lo spazio dello stack necessario per la ricorsione), dato un albero bilanciato di build pronto. Sarebbe comunque O (nlogn) ma con un fattore costante più piccolo e senza spazio aggiuntivo necessario (ad eccezione del nuovo array, supponendo che i dati debbano essere inseriti in un array). Le tabelle hash invece non possono essere ordinate (almeno non direttamente).
Forse sono utili anche in alcuni sofisticati algoritmi per fare qualcosa, ma non mi viene in mente nulla. Se ne trovo di più, modificherò il mio post.
Altri alberi come gli alberi B + sono ampiamente utilizzati nei database
Una delle applicazioni più comuni è l'archiviazione efficiente dei dati in forma ordinata per accedere e cercare rapidamente gli elementi memorizzati. Ad esempio, std::map
o std::set
nella libreria standard C ++.
L'albero binario come struttura di dati è utile per varie implementazioni di parser di espressioni e risolutori di espressioni.
Può anche essere utilizzato per risolvere alcuni problemi del database, ad esempio l'indicizzazione.
Generalmente, l'albero binario è un concetto generale di una particolare struttura di dati basata su alberi e possono essere costruiti vari tipi specifici di alberi binari con proprietà diverse.
In C ++ STL e molte altre librerie standard in altre lingue, come Java e C #. Gli alberi di ricerca binaria vengono utilizzati per implementare set e mappa.
Una delle applicazioni più importanti degli alberi binari sono gli alberi di ricerca binaria bilanciati come:
Questo tipo di alberi ha la proprietà che la differenza nelle altezze della sottostruttura sinistra e della sottostruttura destra viene mantenuta ridotta eseguendo operazioni come le rotazioni ogni volta che un nodo viene inserito o eliminato.
Per questo motivo, l'altezza complessiva dell'albero rimane dell'ordine del registro n e le operazioni come la ricerca, l'inserimento e la cancellazione dei nodi vengono eseguite nel tempo O (registro n). L'STL di C ++ implementa anche questi alberi sotto forma di insiemi e mappe.
Sull'hardware moderno, un albero binario è quasi sempre non ottimale a causa della cattiva gestione della cache e dello spazio. Questo vale anche per le varianti (semi) bilanciate. Se li trovi, è dove le prestazioni non contano (o sono dominate dalla funzione di confronto), o più probabilmente per ragioni storiche o di ignoranza.
Un compilatore che utilizza un albero binario per una rappresentazione di un AST, può utilizzare algoritmi noti per analizzare l'albero come postorder, inorder. Il programmatore non ha bisogno di elaborare il proprio algoritmo. Poiché un albero binario per un file sorgente è più alto dell'albero n-ary, la sua costruzione richiede più tempo. Prendi questa produzione: selstmnt: = "if" "(" expr ")" stmnt "ELSE" stmnt In un albero binario avrà 3 livelli di nodi, ma l'albero n-ary avrà 1 livello (di chid)
Ecco perché i sistemi operativi basati su Unix sono lenti.