Heap
In questa pagina 9
Lo heap è l'implementazione efficiente della coda con priorità (vedi Code con prioritàEntry chiave-valore; ADT coda con priorità (insert, min, removeMin) con chiave minima = priorità massima; esempio di esecuzione; applicazioni; implementazioni con lista non ordinata e ordinata e relativi costi; ordinamento tramite coda con priorità.Code con priorità →): bilancia i costi, con min() in e insert, removeMin() in .
Albero binario completo
Un albero binario di altezza è completo se:
- per ogni livello con ci sono nodi (livello pieno);
- al livello i nodi sono tutti a sinistra delle eventuali mancanze: nessun buco, e al più l'ultimo nodo interno ha solo il figlio sinistro.
Proposizione. Un albero binario completo con nodi ha altezza .
Dimostrazione. I livelli sono pieni e l'ultimo ha almeno un nodo: . Quindi , cioè .
Definizione di heap
Uno heap è un albero binario completo i cui nodi memorizzano entry e che soddisfa la heap-order property: per ogni nodo diverso dalla radice, . Equivale a: ogni nodo ha chiave quella dei figli. (Un max-heap inverte la disuguaglianza.) Il nodo last è il più a destra del livello .
Le due proprietà hanno ciascuna un ruolo: la completezza dà altezza , l'heap-order mette il minimo in cima (min() in ). Chiavi duplicate sono ammesse.
Proprietà (heap con entry):
- la radice contiene una entry con chiave minima;
- le chiavi lungo un cammino dalla radice a una foglia formano una sequenza non decrescente;
- con chiavi distinte, se è l'entry con -esima chiave più piccola ( = radice) e sta a profondità , allora (gli antenati di sono altrettante entry con chiave minore, più stessa);
- con chiavi distinte, l'entry con chiave massima sta in una foglia (per assurdo: se fosse in un nodo interno, un suo figlio avrebbe chiave maggiore);
- per ogni nodo e ogni nodo nel sottoalbero , (per transitività lungo il cammino).
Conseguenza della 5: le uniche entry di cui si conosce con certezza il confronto con sono i suoi antenati (minori) e i suoi discendenti (maggiori). In un heap di 12 entry con chiavi distinte, i discendenti di sono , e (i figli di sono e , ma non esiste): quindi 3 entry hanno certamente chiave maggiore (domanda d'esame ricorrente).
Rappresentazione su array (level numbering)
Un heap sta in un array numerando i nodi per livelli, da sinistra a destra: la radice è , i figli di sono e , il padre di è e . I collegamenti padre-figlio sono impliciti (struttura dati implicita): nessun puntatore, spazio . (Nel testo adottato la numerazione parte da 0: figli , , padre .)
Per un albero completo con nodi e altezza : i nodi del livello occupano le posizioni e quelli del livello le posizioni . Per un albero non completo questo schema può richiedere spazio esponenziale (un cammino di nodi sempre a destra occupa la posizione ).
Inserimento: insert(k, x) e up-heap bubbling
Si pone la nuova entry in posizione (la completezza è salva) e si ripristina l'heap-order risalendo.
Algoritmo insert(k, x)
e <- (k, x); last <- last + 1; P[last] <- e; i <- last
while i > 1 AND P[floor(i/2)].getKey() > P[i].getKey() do
swap(P[i], P[floor(i/2)])
i <- floor(i/2)
return eSe l'array è pieno si alloca prima un array più grande (ad esempio di taglia doppia): costo ammortizzato.
Invariante del while: l'unica coppia padre-figlio che può violare l'heap-order è . Vale all'inizio (la sola entry nuova può violare) e si conserva: lo scambio porta la chiave minore sopra, e la chiave scesa è l'antenato che ora è sopra di essa. Alla fine o il padre è : nessuna violazione.
Costo: il while fa al più iterazioni, con ( numero di entry dopo l'inserimento), e c'è un'istanza con esattamente iterazioni (chiave minore di tutte): .
Rimozione del minimo: removeMin() e down-heap bubbling
Si toglie la radice, si porta in radice l'ultima entry e si ripristina scendendo, scambiando sempre con il figlio di chiave minore.
Algoritmo removeMin()
minentry <- P[1]; P[1] <- P[last]; last <- last - 1; i <- 1
while 2*i <= last do
j <- indice del figlio di P[i] con chiave minima (2i o 2i+1)
if P[i].getKey() <= P[j].getKey() then break
swap(P[i], P[j]); i <- j
return minentryInvariante: è il figlio di chiave minima di e le uniche coppie (padre, figlio) che possono violare l'heap-order sono e . Si scambia con il figlio minore perché solo così il nuovo padre di entrambi è di tutti e due. Costo: al più iterazioni, e c'è un'istanza (l'entry di ha chiave massima) con : .
Rimozione di una entry qualsiasi
removeEntry(P, j): si pone e si decrementa . Sia l'entry spostata, il nuovo padre, i figli. Tre casi:
- : l'heap è già valido;
- : up-heap bubbling a partire da ;
- : down-heap bubbling a partire da .
Ciascun ciclo fa iterazioni: al caso pessimo (ad esempio e con chiave massima).
Esempio svolto
Heap iniziale (, ).
insert(5): in posizione (figlio destro di ); scambio, ora in posizione con padre ; scambio, in posizione con padre ; stop. .insert(3): in posizione (padre ) sale a (padre ) (padre ) . .removeMin()restituisce : si mette in radice, ; figlio minore () scambio posizione , figli e : minore , scambio posizione , figlio : stop. .removeMin()restituisce : radice ; figli : scambio con ; figli di posizione : : scambio con ; .
(Tutti i passaggi sono stati verificati eseguendo un'implementazione.)
Fatti sulle visite di uno heap
Con entry a chiavi distinte, la visita in preorder può restituire le entry in ordine crescente (basta che tutte le chiavi del sottoalbero sinistro siano minori di quelle del destro: per livelli con sinistra , destra ). Inorder e postorder non possono: nell'inorder la radice (chiave minima) è preceduta dai nodi del sottoalbero sinistro, e nel postorder è l'ultima.
Errori comuni
- Scambiare con il figlio sbagliato nel down-heap (si sceglie il minore, non il sinistro).
- Dimenticare di aggiornare , o di spostare in radice l'ultima entry (non una qualsiasi) in
removeMin. - Ritenere che uno heap sia ordinato: lo è solo lungo i cammini; non c'è relazione tra fratelli né tra livelli diversi su rami diversi.
- Scambiare le formule dei figli (, con indici da 1; , con indici da 0).
Versione ripasso
- Albero binario completo: livelli pieni, livello riempito da sinistra; nodi (da ).
- Heap = albero completo + heap-order: . Radice = minimo; cammini non decrescenti; con chiavi distinte l'entry -esima per grandezza ha profondità e il massimo sta in una foglia; ogni nodo i suoi discendenti.
- Array (level numbering): figli , padre , , spazio ; per alberi non completi può servire spazio esponenziale (vedi Code con prioritàEntry chiave-valore; ADT coda con priorità (insert, min, removeMin) con chiave minima = priorità massima; esempio di esecuzione; applicazioni; implementazioni con lista non ordinata e ordinata e relativi costi; ordinamento tramite coda con priorità.Code con priorità →).
- insert: in , up-heap: scambia. Invariante: solo (padre, ) può violare. .
- removeMin: , , down-heap scambiando col figlio minore. Invariante: solo le coppie possono violare. .
- removeEntry(): poi: già valido / up-heap (se ) / down-heap (se ); .
- Esempio: da ,
insert(5);insert(3);removeMine . - Visite (7 entry distinte): preorder può essere crescente; inorder e postorder no.
- Pseudocodice insert:
P[++last] <- e; i <- last; while i > 1 AND P[i/2].key > P[i].key: swap(P[i], P[i/2]); i <- i/2. - Pseudocodice removeMin:
m <- P[1]; P[1] <- P[last--]; i <- 1; while 2i <= last: j <- indice del figlio minimo; if P[i].key <= P[j].key then break; swap(P[i], P[j]); i <- j; return m. - Proprietà dimostrate: altezza da ; il massimo sta in una foglia (per assurdo); entry distinte: ha discendenti (), quindi entry sicuramente maggiori.
- Costi:
min;insert,removeMin,removeEntry; il caso peggiore è una chiave minore di tutte (insert) o massima (removeMin). - Errori: figlio sbagliato nel down-heap;
lastnon aggiornato; heap creduto ordinato; formule dei figli con indici 0/1 mescolati.