Heap Ordinamento e code di priorità Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Heap: definizione Definizione. Uno Heap (binario) è un albero binario finito i cui vertici sono etichettati da elementi di un insieme linearmente ordinato (chiavi), che soddisfa: 1. 2. Se v è figlio di v' allora chiave(v) ≤ chiave(v'); L’albero è completo salvo al più l’ultimo livello, che è riempito da sinistra. Uno heap binario può essere realizzato come un vettore H di dimensione pari alla cardinalità dell’albero e tale che: • H[1] sia la radice dell’albero • H[2i] e H[2i+1] siano rispettivamente il figlio sinistro e quello destro di H[i]. Ne segue che l’elemento la cui etichetta è massima tra tutte quelle dei vertici dell’albero, è la radice ossia H[1]. Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Heap: esempio 16 9 14 7 3 4 2 H= 16 1 8 10 1 9 14 7 4 8 10 3 2 2 4 5 6 8 9 10 3 7 1 Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Left, Right, Parent Per muoversi all’interno di uno heap rappresentato come un vettore sono utili le seguenti funzioni: // fissato uno heap h e la sua dimensione HeapSize(h) Parent(i) = i / 2 // se i ≠ 0 Left(i) = if 2i < HeapSize(h) then 2i else i Right(i) = if 2i+1 < HeapSize(h) then 2i+1 else i Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Inserimento2 Per inserire un nuovo elemento in uno heap, Insert lo aggiunge come foglia dell’albero; quindi la fa risalire lungo il ramo cui è stato aggiunto sinché non sia ricostruito lo heap. Insert (h: heap; x: key) HeapSize(h) := HeapSize(h)+1 i := HeapSize(h) h[i] = x while i > 1 and h[Parent(i)] < h[i] do exchange(h[i], h[Parent(i)]) i := Parent(i) Nota: la complessità è dell’ordine della lunghezza del ramo ossia O(lg n) Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Heap: Inserimento (1) 16 9 14 7 3 4 2 1 8 10 15 Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Heap: Inserimento (2) 16 9 14 7 3 15 2 1 8 10 4 Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Heap: Inserimento (3) 16 15 14 7 3 9 2 1 8 10 4 Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Heap: Inserimento (4) 16 15 14 7 3 9 2 1 8 10 4 Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Estrazione L’estrazione da uno heap avviene dalla radice. Consta di due fasi: 1) l’elemento più a destra dell’ultimo livello rimpiazza la radice; 2) l’elemento ora in radice viene fatto discendere lungo l’albero finché non sia maggiore di entrambi i figli; nel discendere si sceglie sempre il figlio col valore massimo della chiave. ExtractMaximum (h: heap) h[1] := h[HeapSize(h)] Fase (1) HeapSize(h) := HeapSize(h) - 1 Heapify(h,1) // Fase (2) Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Ricostruzione dello heap La fase (2), di cui si incarica Heapify, è più generale, poiché questa funzione può iniziare il suo lavoro in qualunque punto i dello heap, purché i sottoalberi con radice in Left(i) e Right(i) siano a loro volta degli heap: Heapify (h: heap; i: index) largest := index of max{h[i],h[Left(i)],h[Right(i)]} if largest ≠ i then exchange(h[i],h[largest]) Heapify(h,largest) Nota: se n è la cardinalità dell’albero con radice in i, allora Heapify è O(lg n) (caso peggiore). Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Heap: Estrazione (1) 16 15 14 7 3 9 2 1 8 10 4 Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Heap: Estrazione (2) 4 15 14 7 3 9 2 8 10 1 Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Heap: Estrazione (3) 15 4 14 7 3 9 2 8 10 1 Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Heap: Estrazione (4) 15 9 14 7 3 4 2 8 10 1 Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Heap: Estrazione (5) 15 9 14 7 3 4 2 8 10 1 Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Usi di uno heap La struttura heap può essere impiegata per: • implementare code di priorità; • realizzare un algoritmo di ordinamento ottimo, HeapSort. Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Code di priorità (ADT) Una coda di priorità è un insieme finito S di oggetti su cui è definita una funzione priorità: S → Nat. Le operazioni definite su di essa sono illustrate nella seguente specfica ADT: datatype PriorityQueue, Element; constructors: EmptyQueue: PriorityQueue Insert: PriorityQueue, Element -> PriorityQueue ExtractMaximum: PriorityQueue -> PriorityQueue observations: Maximum: PriorityQueue -> Element semantics: Insert(S,x) = S ∪ {x} Maximum(S) = x tale che Priorità(x) = max {Priorità(y)| y ∈ S} ExtractMaximum(S) = S \ {Maximum(S)} Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Code di priorità: implementazione Si possono implementare con gli heap: la funzione priorità si implementa codificando ogni elemento come una coppia 〈elemento, priorità〉, e strutturando lo heap in base alla seconda coordinata di ciascuna coppia (la chiave). Le funzioni Insert e ExtractMaximum sono quelle viste; la funzione Maximum è semplicemente: Maximum (h: heap) return h[1] // il massimo è sempre in radice La funzione, non essendo distruttiva, non richiede infatti alcuna ricostruzione dello heap, ed ha complessità O(1). Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Heapsort (1) Si può sfruttare la struttura dati heap per costruire un algoritmo di ordinamento simile, per struttura, al SelectSort con selezione del massimo. 1 i n V V[1..i] è uno heap V[i+1..n] è ordinato ∀ x ∈ V[1..i] ∀ y ∈ V[i+1..n]. x ≤ y HeapSort(v: array) BuildHeap(v) // riorganizza v in uno heap for i := Length(v) downto 2 do exchange(v[1],v[i]) HeapSize(v) := HeapSize(v) - 1 Heapify(v,1) Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 HeapSort (2) BuildHeap. Se v[1..n] è un vettore qualsiasi, BuildHeap(v) lo riorganizza in modo che sia uno heap. Pensando h[n/2 +1 .. n] come le foglie dell’albero che diventerà uno heap, la condizione che Left(n/2 +1 ) e Right(n/2 +1 ) siano heap è trivialmente soddisfatta, onde possiamo iterare Heapify da n/2 a 1: BuildHeap(v: array) for i := length(v)/2 downto 1 do Heapify(v,i) Nota: un confine superiore alla complessità di BuildHeap è O(n lg n) (si itera una procedura O(lg n) per ciascuno degli n vertici). Questa stima grossolana si può raffinare sino a mostrare che in realtà BuildHeap è lineare. Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Costruzione di uno Heap 2 6 18 3 42 44 94 12 55 79 2 6 18 3 42 12 55 44 94 79 1 2 3 4 5 6 7 8 9 10 Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Costruzione di uno Heap 2 6 18 3 42 44 94 12 55 79 2 6 18 3 42 12 55 44 94 79 1 2 3 4 5 6 7 8 9 10 Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Costruzione di uno Heap 2 6 18 3 79 44 94 12 55 42 2 6 18 3 79 12 55 44 94 42 1 2 3 4 5 6 7 8 9 10 Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Costruzione di uno Heap 2 6 18 94 79 44 3 12 55 42 2 6 18 94 79 12 55 44 3 42 1 2 3 9 10 4 5 6 7 8 Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Costruzione di uno Heap 2 6 55 94 79 44 3 12 18 42 2 6 55 94 79 12 18 44 3 42 1 2 3 9 10 4 5 6 7 8 Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Costruzione di uno Heap 2 94 55 44 79 6 3 12 18 42 2 94 55 44 79 12 18 6 3 42 1 2 8 9 10 3 4 5 6 7 Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Costruzione di uno Heap 94 79 55 44 42 6 3 12 2 94 79 55 44 42 12 18 1 18 2 3 4 5 6 7 6 3 2 8 9 10 Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Heapsort (3) A differenza del SelctSort, che è O(n2), HeapSort ha complessità O(n log n), quindi è un algoritmo ottimo per l’ordinamento. Ciò si deve all’efficienza della selezione del massimo nel semivettore sinistro, che ha complessità logaritmica: HeapSort(v: array) BuildHeap(v) // O(n log n) (o meglio O(n)) for i := Length(v) downto 2 do // O(n) cicli exchange(v[1],v[i]) HeapSize(v) := HeapSize(v) - 1 Heapify(v,1) // O(log n) Allora: O(n) + O(n) O(lg n) = O(n) + O(n lg n) = O(n lg n). Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9 Riferimenti Bertossi: Cap. 7 Cormen et alii.: Cap. 7 Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 9