Qual è la differenza tra un albero di ricerca binario e un heap binario?


91

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:


63

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.

di xysun


Credo BST è migliore in findMin & FindMax stackoverflow.com/a/27074221/764592
Yeo

10
Penso che questo sia solo un malinteso comune. Un albero binario può essere facilmente modificato per trovare min e max come indicato da Yeo. Questa è in realtà una limitazione dell'heap: l' unica ricerca efficiente è min o max. Il vero vantaggio del mucchio è O (1) inserto media come spiego: stackoverflow.com/a/29548834/895245
Ciro Santilli新疆改造中心法轮功六四事件

Secondo questo video , puoi avere valori più grandi a un livello inferiore, purché il più grande non discenda da quello inferiore.
whoan,

L'heap è ordinato da radice a foglia e BST è ordinato da sinistra a destra.
Deep Joshi,

34

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:

Mucchio massimo binario

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:

Albero di ricerca binario

O(ceppon)
O(1)O(ceppon)


1
O(ceppon)

32

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, nper heap di array dinamico

Vantaggi 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:

  • i livelli dell'albero inferiore hanno esponenzialmente più elementi rispetto ai livelli superiori, quindi quasi sicuramente nuovi elementi andranno in fondo
  • l'inserimento dell'heap inizia dal basso , il BST deve iniziare dall'alto

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:

inserisci qui la descrizione dell'immagine

  • codice di riferimento
  • sceneggiatura
  • dati della trama
  • testato su Ubuntu 19.04, GCC 8.3.0 in un laptop Lenovo ThinkPad P51 con CPU: CPU Intel Core i7-7820HQ (4 core / 8 thread, base 2,90 GHz, cache 8 MB), RAM: 2x Samsung M471A2K43BB1-CRC (2x 16GiB , 2400 Mbps), SSD: Samsung MZVLB512HAJQ-000L7 (512 GB, 3.000 MB / s)

Quindi chiaramente:

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.

inserisci qui la descrizione dell'immagine

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 structpuntatore).

    I BST degli alberi necessiterebbero inoltre di ulteriori informazioni di bilanciamento, ad esempio il rosso-nero.

  • l'implementazione di array dinamici può avere dimensioni 2nsubito 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:

  • inserimento:
    • posizione:
      • lista doppiamente collegata: l'elemento inserito deve essere il primo o l'ultimo, poiché abbiamo solo puntatori a quegli elementi.
      • heap binario: l'elemento inserito può finire in qualsiasi posizione. Meno restrittivo dell'elenco collegato.
    • tempo:
      • elenco doppiamente collegato: il O(1)caso peggiore dato che abbiamo puntatori agli elementi e l'aggiornamento è davvero semplice
      • heap binario: O(1)medio, quindi peggio dell'elenco collegato. Scambio per avere una posizione di inserimento più generale.
  • ricerca: O(n)per entrambi

Un 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:

  • Albero rosso-nero . Sembra essere il BBST più comunemente usato dal 2019, ad esempio è quello utilizzato dall'implementazione GCC 8.3.0 C ++
  • Albero AVL . Sembra essere un po 'più equilibrato di BST, quindi potrebbe essere meglio trovare la latenza, al costo di reperti leggermente più costosi. Wiki riassume: "Gli alberi AVL sono spesso confrontati con alberi rosso-neri perché entrambi supportano lo stesso insieme di operazioni e impiegano [lo stesso] tempo per le operazioni di base. Per applicazioni ad alta intensità di ricerca, gli alberi AVL sono più veloci degli alberi rosso-nero perché sono più rigorosamente bilanciati. Analogamente agli alberi rosso-neri, gli alberi AVL sono bilanciati in altezza. Entrambi non sono, in generale, né bilanciati in peso né in mu per ogni mu <1/2; vale a dire, i nodi fratelli possono avere enormemente diverso numero di discendenti ".
  • WAVL . Il documento originale menziona i vantaggi di quella versione in termini di limiti alle operazioni di ribilanciamento e rotazione.

Guarda anche

Domanda simile su CS: Qual è la differenza tra un albero di ricerca binario e un heap binario?


1
Bella risposta. Le applicazioni comuni di heap sono elementi mediani, k min, top k. Per queste operazioni più comuni rimuovere min quindi insert (di solito abbiamo un piccolo heap con poche operazioni di insert puro). Quindi, in pratica, sembra che per questi algoritmi non superi il BST.
yura,

1
Risposta eccezionale !!! Usando il deque come struttura heap sottostante, è possibile ridurre drasticamente i tempi di ridimensionamento, sebbene sia ancora O (n) nel caso peggiore poiché deve riallocare una matrice (più piccola) di puntatori in blocchi.
Bulat,

13

Con la struttura dei dati si devono distinguere i livelli di preoccupazione.

  1. 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.

  2. 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 BSTchiave 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".

  3. 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).


1

Oltre alle risposte precedenti, l'heap deve avere la proprietà della struttura heap; l'albero deve essere pieno e lo strato più in basso, che non può essere sempre pieno, deve essere riempito dall'estrema sinistra all'estrema destra senza spazi vuoti.

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.