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 . È un albero binario (Alberi binariAlbero binario e albero binario proprio; interfaccia; relazioni tra nodi, foglie e altezza (m = n-m+1, h+1 <= m <= 2^h, 2h+1 <= n <= 2^(h+1)-1) con dimostrazioni; visita inorder; parse tree e valutazione di espressioni; heightSum come esempio di calcolo di un'informazione più ricca.Alberi binari →) tenuto in un array: la struttura implicita sfrutta l'indicizzazione degli array (Array e stringhe in CArray di dimensione fissa in memoria contigua, inizializzazione, nessun controllo sugli indici, passaggio a funzioni con la lunghezza, matrici; stringhe come array di char terminati da '\0' e funzioni di string.h.Array e stringhe in C →); il codice C è in Alberi e heap in CAlbero binario in C con nodo struct e figli left/right; funzioni ricorsive (conteggio, altezza, visita inorder, liberazione in postorder); inserimento in un albero binario di ricerca con Nodo **; min-heap su array con indici da 1 (insert, removeMin, bottomUp).Alberi e heap in C →. Non va confuso con lo heap della memoria dinamica (Gestione della memoria in CSegmenti di memoria di un processo (codice, dati statici, stack, heap); durata delle variabili; allocazione dinamica con malloc, calloc, realloc e free; errori classici: memory leak, dangling pointer, double free, buffer overflow; strumenti di controllo.Gestione della memoria in C →): sono due cose diverse con lo stesso nome.
Albero binario completo
Definizione (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.
Esempio. Con i livelli e sono pieni ( nodi) e il livello ha da a nodi tutti a sinistra: va da a .
Proposizione (altezza di un albero binario completo). Un albero binario completo con nodi ha altezza .
Esempio. dà , dà e dà .
Dimostrazione. I livelli sono pieni e l'ultimo ha almeno un nodo: . Qui si usa la somma geometrica finita (Notazione asintoticaDefinizioni di O, Omega, Theta e o piccolo con le costanti c ed n0; esempi con costanti esplicite; proprietà (polinomi, esponenziali, logaritmi, somme, implicazioni tra notazioni); sommatorie notevoli; terminologia (logaritmica, lineare, polinomiale, esponenziale).Notazione asintotica →, Serie notevoli - geometrica, telescopica, armonicaLe serie di cui si conosce il carattere e da usare come termine di paragone: geometrica (converge a 1/(1-q) se |q|<1), telescopiche (somma b_1 - lim b_n, come Mengoli), armonica generalizzata (1/n^alpha converge se e solo se alpha>1).Serie notevoli - geometrica, telescopica, armonica →), da cui è il numero minimo di nodi (livelli pieni più un solo nodo nell'ultimo), mentre è il massimo (tutti i livelli pieni). Applicando a (funzione crescente: Esponenziale e logaritmoLa funzione esponenziale a^x (base positiva diversa da 1) e la sua inversa, il logaritmo in base a, con grafici e proprietà.Esponenziale e logaritmo →) si ottiene , cioè .
Il grafico mostra l'altezza (e quindi il numero massimo di scambi di insert e removeMin) in funzione di : cresce così lentamente che per vale e per un milione .
Grafico interattivo: Altezza di un albero binario completo, floor(log2 n): 3 per n = 11 (gli heap degli esempi), 5 per n = 63; ogni volta che n raddoppia l'altezza cresce di 1
Definizione di heap
Definizione (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 .
Esempio. L'array è uno heap: , e .
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). 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).
Esempio. Nello heap la radice è il minimo e il cammino è non decrescente.
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)
Formula (level numbering di uno heap su array). 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 .)
Esempio. In i figli di sono e , e il padre di è .
Perché proprio e . I livelli precedenti al livello contengono nodi, quindi il livello comincia alla posizione . Il nodo in posizione è il -esimo del suo livello (contando da a sinistra). I suoi due figli sono il -esimo e il -esimo del livello , che comincia alla posizione : le loro posizioni sono e , cioè e se è la posizione del padre. Esempio: ha figli e ; ha e .
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
Proprietà (insert e up-heap bubbling). Si pone la nuova entry in posizione (la completezza è salva) e si ripristina l'heap-order risalendo.
Esempio. Su , insert(5) mette in posizione e lo fa salire fino alla : .
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 (il ragionamento è quello dell'array estendibile in Liste, pile e codeRipasso degli ADT elementari: lista index-based (array) e position-based (lista doppiamente concatenata con sentinelle), pila LIFO, coda FIFO, deque, iteratori; interfacce, implementazioni e costi; array estendibile e coda circolare.Liste, pile e code →).
Traccia di insert(5) su :
| passo | confronto | azione | |||
|---|---|---|---|---|---|
| 0 | 12 | 5 | scambio | ||
| 1 | 6 | 5 | scambio | ||
| 2 | 3 | 5 | falso | stop |
Due scambi (su un massimo di ): .
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
Proprietà (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.
Esempio. Su , removeMin() restituisce e lo heap diventa .
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 : .
Traccia di removeMin() su (): restituisce , mette in radice e , ottenendo .
| passo | figli di | figlio minore | confronto | azione | |
|---|---|---|---|---|---|
| 1 | 1 | , | scambio, | ||
| 2 | 2 | , | scambio, | ||
| 3 | 5 | (e non esiste) | scambio, | ||
| 4 | 10 | nessuno () | fine |
Risultato , tre scambi, come sopra. Se si scambiasse con il figlio maggiore (ad esempio al passo 1) il nuovo padre di sarebbe e l'heap-order verrebbe violato.
Rimozione di una entry qualsiasi
Proprietà (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 .
Esempio. Su , removeEntry(P, 2) porta in posizione e lo fa scendere (down-heap): .
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. Con nodi, (da ).
- Heap = albero completo con heap-order: . Radice = minimo; lungo ogni cammino le chiavi non decrescono; con chiavi distinte il massimo sta in una foglia; ogni nodo è dei suoi discendenti. Chiavi duplicate ammesse.
- Array (level numbering): radice , figli di in e , padre , ; spazio . Per alberi non completi lo stesso schema può richiedere 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(, ): si pone la entry in e si risale:
while i > 1 AND P[floor(i/2)].key > P[i].key, scambia ei <- floor(i/2). Invariante: l'unica coppia che può violare è (padre, ). . - removeMin():
minentry <- P[1]; P[1] <- P[last]; last--; poi down-heap scambiando sempre con il figlio di chiave minore, perché così il nuovo padre è di entrambi. . - removeEntry(): ; poi, se la chiave è maggiore del padre, si scende (down-heap) con ; se è minore del padre, si risale (up-heap) con ; altrimenti l'heap è già valido. .
- Esempio: da ,
insert(5)dà con scambi; poiinsert(3)dà . Il successivoremoveMin()restituisce e lascia . - Visite con entry distinte: il preorder può dare le chiavi in ordine crescente; inorder e postorder no, perché la radice minima è preceduta (inorder) o seguita (postorder) dal sottoalbero sinistro.
- Proprietà dimostrate: il massimo sta in una foglia (per assurdo: un figlio avrebbe chiave maggiore); con entry distinte ha discendenti (, , ), quindi entry sono certamente maggiori.
- Pseudocodice removeMin:
m <- P[1]; P[1] <- P[last--]; i <- 1; while 2i <= last: j <- figlio minimo; if P[i].key <= P[j].key then break; swap(P[i], P[j]); i <- j;return m. - Costi:
min;insert,removeMin,removeEntry; il caso peggiore è una chiave minore di tutte (insert) o massima (removeMin). - Level numbering, perché e : i livelli precedenti al livello contengono nodi, quindi il livello inizia alla posizione . Il nodo in posizione ha figli nelle posizioni e . Esempio: ha figli e ; ha e .
- Spazio: per un albero completo con nodi i livelli occupano le posizioni e l'ultimo ; per un albero non completo lo schema può richiedere spazio esponenziale (un cammino di nodi tutto a destra arriva alla posizione ).
- Esempio di removeMin: su restituisce ; va in radice e lo heap diventa con scambi.
- Errori tipici: scambiare con il figlio sbagliato nel down-heap;
lastnon aggiornato; credere che uno heap sia ordinato; mescolare le formule dei figli con indici 0 e 1.