Penso che ci siano diverse domande sepolte in questo argomento:
- Come si implementa in
buildHeapmodo che funzioni in tempo O (n) ?
- Come si mostra che
buildHeapviene eseguito in tempo O (n) se implementato correttamente?
- Perché la stessa logica non funziona per far funzionare l'heap sort in O (n) time anziché O (n log n) ?
Come si implementa in buildHeapmodo che funzioni in tempo O (n) ?
Spesso, le risposte a queste domande si concentrano sulla differenza tra siftUpe siftDown. Fare la scelta corretta tra siftUped siftDownè fondamentale per ottenere prestazioni O (n)buildHeap , ma non fa nulla per aiutare a capire la differenza tra buildHeape heapSortin generale. Infatti, adeguate implementazioni di entrambi buildHeape heapSortsaranno solo utilizzare siftDown. L' siftUpoperazione è necessaria solo per eseguire inserimenti in un heap esistente, quindi sarebbe utilizzata per implementare una coda di priorità utilizzando un heap binario, ad esempio.
Ho scritto questo per descrivere come funziona un heap max. Questo è il tipo di heap generalmente utilizzato per l'heap ordinamento o per una coda di priorità in cui valori più alti indicano una priorità più alta. È utile anche un heap minimo; ad esempio, quando si recuperano elementi con chiavi intere in ordine crescente o stringhe in ordine alfabetico. I principi sono esattamente gli stessi; cambia semplicemente l'ordinamento.
La proprietà heap specifica che ciascun nodo in un heap binario deve essere almeno grande quanto entrambi i suoi figli. In particolare, ciò implica che l'elemento più grande nell'heap è alla radice. Setacciare verso il basso e setacciare verso l'alto sono essenzialmente la stessa operazione in direzioni opposte: spostare un nodo offensivo fino a quando non soddisfa la proprietà heap:
siftDown scambia un nodo che è troppo piccolo con il suo figlio più grande (spostandolo così verso il basso) fino a quando non è almeno grande quanto entrambi i nodi sottostanti.
siftUp scambia un nodo che è troppo grande con il suo genitore (spostandolo così verso l'alto) fino a quando non è più grande del nodo sopra di esso.
Il numero di operazioni richieste siftDowned siftUpè proporzionale alla distanza che il nodo potrebbe dover spostare. Perché siftDownè la distanza dal fondo dell'albero, quindi siftDownè costoso per i nodi nella parte superiore dell'albero. Con siftUp, il lavoro è proporzionale alla distanza dalla cima dell'albero, quindi siftUpè costoso per i nodi nella parte inferiore dell'albero. Sebbene entrambe le operazioni siano O (log n) nel peggiore dei casi, in un heap, solo un nodo è nella parte superiore mentre metà dei nodi si trova nel livello inferiore. Quindi non dovrebbe essere troppo sorprendente che, se dobbiamo applicare un'operazione per ogni nodo, preferiremmo siftDownsopra siftUp.
La buildHeapfunzione accetta una matrice di elementi non ordinati e li sposta fino a quando tutti soddisfano la proprietà heap, producendo così un heap valido. Ci sono due approcci che si potrebbero adottare per buildHeapusare le operazioni siftUpe siftDownche abbiamo descritto.
Inizia nella parte superiore dell'heap (l'inizio dell'array) e chiama siftUpogni elemento. Ad ogni passaggio, gli elementi precedentemente setacciati (gli elementi prima dell'articolo corrente nell'array) formano un heap valido e il set-up dell'oggetto successivo lo posiziona in una posizione valida nell'heap. Dopo aver setacciato ciascun nodo, tutti gli elementi soddisfano la proprietà heap.
Oppure, andare nella direzione opposta: iniziare alla fine dell'array e spostarsi indietro verso la parte anteriore. Ad ogni iterazione, setacci un elemento verso il basso fino a quando non si trova nella posizione corretta.
Per quale implementazione buildHeapè più efficiente?
Entrambe queste soluzioni produrranno un heap valido. Non sorprende che la più efficiente sia la seconda operazione che utilizza siftDown.
Lascia che h = log n rappresenti l'altezza dell'heap. Il lavoro richiesto per l' siftDownapproccio è dato dalla somma
(0 * n/2) + (1 * n/4) + (2 * n/8) + ... + (h * 1).
Ogni termine nella somma ha la distanza massima che un nodo alla data altezza dovrà spostare (zero per il livello inferiore, h per la radice) moltiplicato per il numero di nodi a quella altezza. Al contrario, la somma per chiamare siftUpsu ciascun nodo è
(h * n/2) + ((h-1) * n/4) + ((h-2)*n/8) + ... + (0 * 1).
Dovrebbe essere chiaro che la seconda somma è maggiore. Il primo termine da solo è hn / 2 = 1/2 n log n , quindi questo approccio ha al massimo complessità O (n log n) .
Come possiamo dimostrare che la somma per l' siftDownapproccio è davvero O (n) ?
Un metodo (ci sono altre analisi che funzionano anche) è trasformare la somma finita in una serie infinita e quindi usare la serie di Taylor. Potremmo ignorare il primo termine, che è zero:

Se non sei sicuro del motivo per cui ciascuno di questi passaggi funziona, ecco una giustificazione per il processo in parole:
- I termini sono tutti positivi, quindi la somma finita deve essere inferiore alla somma infinita.
- La serie è uguale a una serie di potenze valutata in x = 1/2 .
- Quella serie di potenze è uguale (a tempi costanti) alla derivata delle serie di Taylor per f (x) = 1 / (1-x) .
- x = 1/2 è all'interno dell'intervallo di convergenza di quella serie di Taylor.
- Pertanto, possiamo sostituire la serie Taylor con 1 / (1-x) , differenziare e valutare per trovare il valore della serie infinita.
Poiché la somma infinita è esattamente n , concludiamo che la somma finita non è più grande, ed è quindi O (n) .
Perché l'heap sort richiede tempo O (n log n) ?
Se è possibile eseguire buildHeapin tempo lineare, perché l'ordinamento heap richiede tempo O (n log n) ? Bene, l'heap sort consiste in due fasi. Innanzitutto, chiamiamo buildHeapl'array, che richiede tempo O (n) se implementato in modo ottimale. Il passaggio successivo consiste nell'eliminare ripetutamente l'elemento più grande nell'heap e inserirlo alla fine dell'array. Poiché eliminiamo un articolo dall'heap, c'è sempre un punto aperto subito dopo la fine dell'heap in cui possiamo archiviare l'articolo. Quindi l'heap sort raggiunge un ordinamento rimuovendo in successione il prossimo oggetto più grande e inserendolo nell'array a partire dall'ultima posizione e spostandosi verso la parte anteriore. È la complessità di quest'ultima parte che domina nell'ordinamento degli heap. Il ciclo è simile al seguente:
for (i = n - 1; i > 0; i--) {
arr[i] = deleteMax();
}
Chiaramente, il ciclo esegue O (n) volte ( n - 1 per essere precisi, l'ultimo elemento è già in atto). La complessità di deleteMaxper un heap è O (log n) . In genere viene implementato rimuovendo la radice (l'elemento più grande rimasto nell'heap) e sostituendolo con l'ultimo elemento nell'heap, che è una foglia, e quindi uno degli elementi più piccoli. Questa nuova radice quasi sicuramente violerà la proprietà dell'heap, quindi devi chiamare siftDownfino a quando non la sposti in una posizione accettabile. Ciò ha anche l'effetto di spostare l'elemento successivo più grande fino alla radice. Si noti che, diversamente da buildHeapdove per la maggior parte dei nodi stiamo chiamando siftDowndalla parte inferiore dell'albero, ora stiamo chiamando siftDowndalla parte superiore dell'albero su ogni iterazione!Sebbene l'albero si stia restringendo, non si restringe abbastanza velocemente : l'altezza dell'albero rimane costante fino a quando non si è rimossa la prima metà dei nodi (quando si cancella completamente lo strato inferiore). Quindi per il prossimo trimestre, l'altezza è h - 1 . Quindi il lavoro totale per questa seconda fase è
h*n/2 + (h-1)*n/4 + ... + 0 * 1.
Nota l'interruttore: ora il caso di lavoro zero corrisponde a un singolo nodo e il caso di lavoro h corrisponde alla metà dei nodi. Questa somma è O (n log n) proprio come la versione inefficiente di buildHeapciò è implementata usando siftUp. Ma in questo caso, non abbiamo scelta poiché stiamo cercando di ordinare e richiediamo che il prossimo articolo più grande venga rimosso successivamente.
In breve, il lavoro per l'heap sort è la somma delle due fasi: O (n) tempo per buildHeap e O (n log n) per rimuovere ciascun nodo in ordine , quindi la complessità è O (n log n) . Puoi provare (usando alcune idee della teoria dell'informazione) che per un ordinamento basato sul confronto, O (n log n) è il migliore che potresti sperare comunque, quindi non c'è motivo di essere deluso da questo o aspettarti che l'heap sort raggiunga il O (n) limite di tempo che lo buildHeapfa.