Questi due sembrano molto simili e hanno una struttura quasi identica. Qual è la differenza? Quali sono le complessità temporali per le diverse operazioni di ciascuna?
Questi due sembrano molto simili e hanno una struttura quasi identica. Qual è la differenza? Quali sono le complessità temporali per le diverse operazioni di ciascuna?
Risposte:
Heap garantisce solo che gli elementi ai livelli più alti sono maggiori (per heap max) o più piccoli (per heap min) rispetto agli elementi ai livelli inferiori, mentre BST garantisce l'ordine (da "sinistra" a "destra"). Se vuoi elementi ordinati, scegli BST. di Dante non è un geek
Heap è meglio su findMin / findMax (O (1)), mentre BST è buono in tutti i risultati (O (logN)). Inserisci è O (logN) per entrambe le strutture. Se ti interessa solo findMin / findMax (ad es. Relativo alla priorità), vai con heap. Se vuoi tutto ordinato, vai con BST.
Entrambi gli alberi binari di ricerca e cumuli binari sono strutture di dati basati su alberi.
Gli heap richiedono che i nodi abbiano una priorità rispetto ai loro figli. In un heap massimo, i figli di ciascun nodo devono essere inferiori a se stesso. Questo è l'opposto di un minimo heap:
Gli alberi di ricerca binaria (BST) seguono un ordinamento specifico (pre-ordine, in ordine, post-ordine) tra i nodi fratelli. L'albero deve essere ordinato, a differenza di heap:
Sommario
Type BST (*) Heap
Insert average log(n) 1
Insert worst log(n) log(n) or n (***)
Find any worst log(n) n
Find max worst 1 (**) 1
Create worst n log(n) n
Delete worst log(n) log(n)
Tutti i tempi medi su questa tabella sono gli stessi dei tempi peggiori, tranne per Inserimento.
*
: ovunque in questa risposta, BST == BST bilanciato, poiché uno sbilanciato fa schifo asintoticamente**
: usando una banale modifica spiegata in questa risposta***
: log(n)
per heap albero puntatore, n
per heap di array dinamicoVantaggi dell'heap binario rispetto a un BST
l'inserimento del tempo medio in un heap binario è O(1)
, poiché BST lo è O(log(n))
. Questa è la caratteristica killer di heap.
Ci sono anche altri cumuli che raggiungono O(1)
ammortizzati (più forti) come il mucchio di Fibonacci , e anche il caso peggiore, come la coda Brodal , anche se potrebbero non essere pratici a causa di prestazioni non asintotiche: https://stackoverflow.com/questions/30782636 / are-fibonacci-mucchi-o-Brodal-code-utilizzati nella pratica ovunque
gli heap binari possono essere implementati in modo efficiente su array dinamici o alberi basati su puntatori, BST solo alberi basati su puntatori. Quindi per l'heap possiamo scegliere l'implementazione dell'array più efficiente in termini di spazio, se possiamo permetterci latenze di ridimensionamento occasionali.
la creazione dell'heap binario è il O(n)
caso peggiore , O(n log(n))
per BST.
Vantaggio di BST rispetto all'heap binario
la ricerca di elementi arbitrari è O(log(n))
. Questa è la caratteristica killer dei BST.
Per l'heap, è O(n)
in generale, ad eccezione dell'elemento più grande che è O(1)
.
Vantaggio "falso" dell'heap rispetto a BST
heap è O(1)
trovare max, BST O(log(n))
.
Questo è un malinteso comune, perché è banale modificare un BST per tenere traccia dell'elemento più grande e aggiornarlo ogni volta che tale elemento può essere modificato: all'inserimento di uno più grande scambiare, alla rimozione trovare il secondo più grande. https://stackoverflow.com/questions/7878622/can-we-use-binary-search-tree-to-simulate-heap-operation (menzionato da Yeo ).
In realtà, questa è una limitazione degli heap rispetto ai BST: l' unica ricerca efficiente è quella per l'elemento più grande.
L'inserto heap binario medio è O(1)
fonti:
Argomento intuitivo:
In un heap binario, aumentare il valore in un determinato indice è anche O(1)
per lo stesso motivo. Ma se vuoi farlo, è probabile che vorrai mantenere un indice aggiuntivo aggiornato sulle operazioni dell'heap https://stackoverflow.com/questions/17009056/how-to-implement-ologn-decrease- key-operation-for-min-heap-based-prior-queu ad es. per Dijkstra. Possibile senza costi aggiuntivi.
La libreria standard GCC C ++ inserisce benchmark su hardware reale
Ho confrontato C ++ std::set
( Red-black tree BST ) e std::priority_queue
( dynamic array heap ) per vedere se avevo ragione sui tempi di inserimento, e questo è quello che ho ottenuto:
Quindi chiaramente:
il tempo di inserimento dell'heap è sostanzialmente costante.
Possiamo vedere chiaramente i punti di ridimensionamento della matrice dinamica. Dato che calcoliamo una media di ogni 10k inserti per poter vedere qualsiasi cosa sopra il rumore del sistema , tali picchi sono in effetti circa 10k volte più grandi di quanto mostrato!
Il grafico ingrandito esclude essenzialmente solo i punti di ridimensionamento dell'array e mostra che quasi tutti gli inserti sono inferiori a 25 nanosecondi.
BST è logaritmico. Tutti gli inserti sono molto più lenti dell'inserto heap medio.
Analisi dettagliata di hashmap BST vs: https://stackoverflow.com/questions/18414579/what-data-structure-is-inside-stdmap-in-c/51945119#51945119
La libreria standard GCC C ++ inserisce il benchmark su gem5
gem5 è un simulatore di sistema completo e quindi fornisce un orologio infinitamente accurato con m5 dumpstats
. Quindi ho provato ad usarlo per stimare i tempi per i singoli inserti.
Interpretazione:
l'heap è ancora costante, ma ora vediamo più in dettaglio che ci sono alcune righe e ogni riga superiore è più sparsa.
Ciò deve corrispondere alle latenze di accesso alla memoria eseguite per inserti sempre più alti.
TODO Non riesco davvero a interpretare pienamente il BST in quanto non sembra così logaritmico e un po 'più costante.
Con questo maggiore dettaglio, tuttavia, possiamo vedere anche alcune linee distinte, ma non sono sicuro di cosa rappresentino: mi aspetto che la linea di fondo sia più sottile, poiché inseriamo la parte inferiore inferiore?
Benchmark con questa configurazione Buildroot su una CPU HPI aarch64 .
BST non può essere implementato in modo efficiente su un array
Le operazioni di heap devono solo fare il bubble up o down di un singolo ramo di un albero, quindi gli O(log(n))
swap nel caso peggiore, nella O(1)
media.
Mantenere un BST bilanciato richiede rotazioni dell'albero, che possono cambiare l'elemento superiore per un altro, e richiederebbero lo spostamento dell'intero array intorno ( O(n)
).
Gli heap possono essere implementati in modo efficiente su un array
Gli indici padre e figlio possono essere calcolati dall'indice corrente come mostrato qui .
Non ci sono operazioni di bilanciamento come BST.
Elimina min è l'operazione più preoccupante in quanto deve essere dall'alto verso il basso. Ma si può sempre fare "percolando" un singolo ramo dell'heap come spiegato qui . Questo porta ad un caso peggiore O (log (n)), poiché l'heap è sempre ben bilanciato.
Se stai inserendo un singolo nodo per ognuno di quelli rimossi, perdi il vantaggio dell'inserto medio asintotico O (1) che heap fornisce in quanto l'eliminazione dominerebbe e potresti anche usare un BST. Dijkstra tuttavia aggiorna i nodi più volte per ogni rimozione, quindi stiamo bene.
Cumuli di array dinamici rispetto a cumuli di alberi di puntatori
Gli heap possono essere implementati in modo efficiente su heap di puntatori: https://stackoverflow.com/questions/19720438/is-it-possible-to-make-efficient-pointer-based-binary-heap-implementations
L'implementazione di array dinamici è più efficiente in termini di spazio. Supponiamo che ogni elemento heap contenga solo un puntatore a un struct
:
l'implementazione dell'albero deve memorizzare tre puntatori per ciascun elemento: padre, figlio sinistro e figlio destro. Quindi l'utilizzo della memoria è sempre 4n
(3 puntatori ad albero + 1 struct
puntatore).
I BST degli alberi necessiterebbero inoltre di ulteriori informazioni di bilanciamento, ad esempio il rosso-nero.
l'implementazione di array dinamici può avere dimensioni 2n
subito dopo il raddoppio. Quindi in media lo sarà 1.5n
.
D'altra parte, l'heap dell'albero ha un migliore inserimento nel caso peggiore, perché copiare l'array dinamico di supporto per raddoppiarne le dimensioni prende il O(n)
caso peggiore, mentre l'heap dell'albero fa solo nuove piccole allocazioni per ciascun nodo.
Tuttavia, il raddoppio dell'array di supporto viene O(1)
ammortizzato, quindi si riduce alla massima latenza. Menzionato qui .
Filosofia
I BST mantengono una proprietà globale tra un genitore e tutti i discendenti (a sinistra più piccoli, a destra più grandi).
Il nodo superiore di un BST è l'elemento intermedio, che richiede una conoscenza globale da mantenere (sapendo quanti elementi sempre più piccoli ci sono).
Questa proprietà globale è più costosa da mantenere (log n insert), ma offre ricerche più potenti (log n search).
Heap mantiene una proprietà locale tra genitore e figli diretti (genitore> figli).
La nota di testa di un heap è il grande elemento, che richiede solo conoscenza locale per mantenere (conoscendo il tuo genitore).
Elenco doppiamente collegato
Un elenco doppiamente collegato può essere visto come sottoinsieme dell'heap in cui il primo elemento ha la massima priorità, quindi confrontiamoli anche qui:
O(1)
caso peggiore dato che abbiamo puntatori agli elementi e l'aggiornamento è davvero sempliceO(1)
medio, quindi peggio dell'elenco collegato. Scambio per avere una posizione di inserimento più generale.O(n)
per entrambiUn caso d'uso per questo è quando la chiave dell'heap è il timestamp corrente: in tal caso, le nuove voci andranno sempre all'inizio dell'elenco. Quindi possiamo persino dimenticare del tutto il timestamp esatto e mantenere la posizione nell'elenco come priorità.
Questo può essere usato per implementare una cache LRU . Proprio come per le applicazioni heap come Dijkstra , vorrai mantenere una hashmap aggiuntiva dalla chiave al nodo corrispondente dell'elenco, per trovare quale nodo aggiornare rapidamente.
Confronto tra diversi BST bilanciati
Anche se i tempi di inserimento e di ricerca asintotici per tutte le strutture di dati che sono comunemente classificati come "BST bilanciati" che ho visto finora è lo stesso, BBST diversi hanno compromessi diversi. Non l'ho ancora studiato completamente, ma sarebbe bene riassumere questi compromessi qui:
Guarda anche
Domanda simile su CS: Qual è la differenza tra un albero di ricerca binario e un heap binario?
Con la struttura dei dati si devono distinguere i livelli di preoccupazione.
Le strutture di dati astratte (oggetti memorizzati, le loro operazioni) in questa domanda sono diverse. Uno implementa una coda di priorità, l'altro un set. Una coda di priorità non è interessata a trovare un elemento arbitrario, ma solo quello con la priorità più grande.
L' implementazione concreta delle strutture. Qui a prima vista entrambi sono alberi (binari), con diverse proprietà strutturali. Sia l'ordinamento relativo delle chiavi che le possibili strutture globali differiscono. (Un po 'impreciso, in una BST
chiave viene ordinato da sinistra a destra, in un heap sono ordinati dall'alto verso il basso.) Come osserva correttamente IPlant, anche un heap dovrebbe essere "completo".
C'è un'ultima differenza nell'implementazione di basso livello . Un albero di ricerca binario (non bilanciato) ha un'implementazione standard usando i puntatori. Al contrario, un heap binario ha un'implementazione efficiente utilizzando un array (proprio a causa della struttura limitata).