Algoritmi e Strutture Dati HEAP Anno accademico 2004-05 1/32 Heap 128 72 64 8 1 7 6 12 30 3 A={ 128, 64, 72, 8, 1 2 3 4 7, 12, 30, 1, 5 6 7 8 6, 9 3 } 10 A(6) = 12 2/32 Heap: definizione formale Una Heap è un albero binario quasi completo. Quasi significa che possono mancare alcune foglie consecutive a partire dall’ultima foglia di destra. Per ogni nodo i Value(i) ≤ value(parent(i)) Nota 1: il massimo si trova nella radice Nota 2: non c’è nessuna relazione tra il valore di un nodo e quello di un suo fratello 3/32 Memorizzazione di un heap in un vettore 128 72 64 8 1 7 6 12 30 3 4/32 Memorizzazione di un heap in un vettore Radice posizione 1 Per ogni nodo in posizione i: left-child(i) posizione 2i right-child(i) posizione 2i+1 Parent(i) = i/2 5/32 Aggiunta di un elemento (heapify) i Heaps Heap A i B 2i 2i+1 parte del vettore già heapizzato elemento da aggiungere alla sotto heap (verde) 6/32 IDEA: facciamo scendere il nodo i nell’albero fino a trovare la sua posizione. ? i A B A Bi 7/32 Heapify(A,i) Heapify(A,i) l=left(i) r=right(i) if l≤heap-size(A) and A[l]>A[i] then largest=l else largest=i if r≤heap-size(A) and A[r]>A[largest] then largest=r if largesti then Exchange(A[i],A[largest]) Heapify(A,largest) 8/32 Heapify: costo computazionale Caso pessimo: il nodo si sposta fino ad arrivare alle foglie. Heapify impiega tempo costante ad ogni livello per sistemare A[i], A[left(i)] e A[right(i)]. Esegue aggiustamenti locali al massimo height(i) volte dove height(i) = O(log(n)) 9/32 Build-heap(A) Build-heap(A) heap-size(A)=length(A) for i=length(A)/2 downto 1 do heapify(A,i) Analisi approssimativa: ogni chiamata a heapify costa O(log(n)). Chiamiamo heapify O(n) volte, quindi build-heap = O(nlog(n)) Domanda (esercizio): build-heap = (nlog(n)) ? 10/32 PQ implementate con Heap Extract-max(A) if heap-size(A)<1 then "error" max=A[1] A[1]=A[heapsize(A)] heapsize(A)=heapsize(A)-1 Heapify(A,1) return max O(log(n)) 11/32 PQ implementate con Heap max = ?? max = max = Heapify( ) 12/32 PQ implementate con Heap Insert(A,x) heap-size(A)=heap-size(A)+1 i=heap-size(A) while i>1 and A[parent(i)]<x do A[i]=A[parent(i)] i=parent(i) A[i]=x O(log(n)) 13/32 Heap Sort: l’idea. Heap Heapify Heap Heapify ... avanti così... 14/32 Heap Sort Heap-Sort(A) build-heap(A) for i=length(A) downto 2 do exchange(A[1],A[i]) heap-size[A]=heap-size(A)-1 heapify(A,1) O(nlog(n)) È un metodo “in place” 15/32 Quicksort: l’idea Dividi: Dividi il vettore in due parti non vuote. Conquista: ordina le due parti ricorsivamente Combina: fondi le due parti ottenendo un vettore ordinato. A={10,5,41,3,6,9,12,26} mergesort A metà A1={10,5,41,3} A2={6,9,12,26} quicksort Intorno a un Pivot, es 12 A1={10,5,3,6,9,12} A2={41,26} 16/32 Quicksort Quicksort(A,p,r) if p<r then q=partition(A,p,r) Quicksort(A,p,q) Quicksort(A,q+1,r) Nota: Mergesort lavora dopo la ricorsione Quicksort lavora prima della ricorsione Partition è cruciale !!! 17/32 A(p,r) i Pivot j 5 3 2 6 4 1 3 7 i j i j 5 3 2 6 4 1 3 7 3 3 2 6 4 1 5 7 i j i j 3 3 2 6 4 1 5 7 3 3 2 1 4 6 5 7 j i 3 3 2 1 4 6 5 7 <5 ≥5 (n) in place 18/32 Partition(A,p,r) x=A[p] i=p-1 j=r+1 while true do repeat j=j-1 until A[j]<=x repeat i=i+1 until A[i]>=x if i<j then scambia(A[i],A[j]) else return j 19/32 Analisi di QS nel caso ottimo Caso ottimo: partizioni bilanciate T(n) = 2T(n/2) + (n) quindi: T(n) = (nlog(n)) 20/32 Analisi di QS nel caso pessimo Caso pessimo: partizioni sbilanciate T(n) = T(n-1) + (n) ricorsione partition quindi: T(n) = (n2) 21/32 Analisi di QS nel caso... ... non buono ! 90% 10% T(n) ??? 22/32 Albero di ricorsione n + n 1/10 n 1/100 n log10n 9/100 n n + 9/10 n 9/100 n 81/100 n 81/1000 n log10/9n 729/1000 n n + <n (n log(n)) 23/32 Analisi del caso medio di QS: una intuizione. Caso medio: a volte facciamo una buona partition a volte no... buona partition: cattiva partition: 24/32 Caso medio le buone e le cattive partition si alternano... 1 1 cattiva n-1 (n-1)/2 (n-1)/2 buona dopo una cattiva e una buona partizione in successione siamo più o meno nella situazione in cui la cattiva partizione non è stata fatta ! 25/32 QS: distribuzione degli input Abbiamo assunto implicitamente che tutte le sequenze di numeri da ordinare fossero equiprobabili. Se ciò non fosse vero potremmo avere costi computazionali più alti. Possiamo “rendere gli input equiprobabili” ? come procediamo mischiamo la sequenza casualmente prima di ordinare Scegliamo il pivot a caso. 26/32 QS “randomizzato” QSR usa una versione randomizzata della procedura Partition. Randomized-partition(A,p,r) i=random(p,r) exchange(A[p],A[i]) return Partition(A,p,r) Un algoritmo randomizzato non ha un input pessimo, bensì ha una sequenza di scelte pessime di pivot. 27/32 Insertion sort Merge sort Heap sort Quick sort Caso pessimo n2 n log(n) n log(n) n2 Caso medio n2 n log(n) n log(n) n log(n) Caso ottimo n n log(n) n log(n) n log(n) = in place 28/32 È possibile ordinare in meno di n log(n) ??? ovvero in o(n log(n)) 29/32 Limite inferiore di complessità Insertion-sort Merge-sort Heap-sort Quick-sort “Comparison-sort” algoritmi basati su confronti Questi metodi calcolano una soluzione che dipende esclusivamentedall’esito di confronti fra numeri TEOREMA (Lower Bound per algoritmi Comparison-sort): Qualsiasi algoritmo “comparison-sort” deve effettuare nel caso pessimo (n log(n)) confronti per ordinare una sequenza di n numeri. 30/32 lower bound per comparison sort IDEA: con n numeri ho n! possibili ordinamenti. Possiamo scegliere quello giusto tramite una sequenza di confronti. ≤ ≤ > > ≤ > Ogni nodo rappresenta un confronto. 31/32 Esempio: a1:a2 ≤ a2:a3 ≤ a1,a2,a3 ≤ a1,a3,a2 n=3 > a1:a3 {a1,a2,a3} > ≤ > a3,a1,a2 albero dei confronti a1:a3 a2,a1,a3 ≤ a2,a3,a1 > a2:a3 > a3,a2,a1 Ogni nodo bianco rappresenta un confronto. Ogni nodo rosso rappresenta una possibile soluzione. 32/32 Limite inferiore al numero dei confronti 3! = 6 = numero di foglie dell’albero dei confronti. ogni (cammino dalla radice ad una) foglia rappresenta un ordinamento ci sono n! ordinamenti. quanto deve essere alto un albero per avere n! foglie ??? un albero binario alto h ha al massimo 2h foglie dobbiamo avere 2h ≥ n! Formula di Stirling: n! > (n/e)n e=2.17... h ≥ log[(n/e)n] = nlog(n) - nlog(e) = (nlog(n)) 33/32 Il caso pessimo di un qualsiasi algoritmo comparison-sort eseguito su una sequenza di n numeri è dato dall’altezza dell’albero di decisione associato a quell’algoritmo. MA Un albero binario con n! foglie (ordinamenti) ha un altezza (nlog(n)) QUINDI qualsiasi algoritmo comparison-sort, nel caso pessimo, esegue (nlog(n)) confronti. 34/32