Heapsort Dato un insieme S di n elementi totalmente ordinato, l'algoritmo di ordinamento detto HeapSort ha le seguenti caratteristiche: T(n) = O(n log(n)) Alg. Ordinamento ottimale Ordina in loco (niente spazio aggiuntivo) Usa la una struttura di dati heap. Definizione: Heap binario massimo (minimo) = albero binario in cui ogni nodo figlio ha una chiave minore (maggiore) o uguale alla chiave del proprio nodo padre.
Heap L albero è quasi completo : completo su tutti i livelli tranne eventualmente sul livello più basso che è riempito da sinistra a destra L'Altezza dell albero : lunghezza del più lungo cammino discendente dalla radice ad una foglia 16 i=1 2 14 3 10 4 5 6 7 8 7 9 3 8 9 10 2 4 1
Heap Proprietà di ordinamento parziale dello heap: ogni nodo interno contiene un valore maggiore (minore) uguale del valore contenuto nei figli. Da ciò segue che: 1. L elemento più grande (piccolo) dello heap è memorizzato nella radice. 2. Ogni nodo interno contiene un valore maggiore (minore) uguale del valore contenuto in tutti i suoi discendenti.
Heap Heapsize: numero di elementi nello heap Diverso in genere dalla lunghezza del vettore che lo contiene i=1 2 3 4 5 6 7 8 9 10 Length[A] 16 14 10 8 7 9 3 2 4 1 Heap-size[A] Heap-size[A] Length[A] Un albero binario quasi completo può essere descritto da un vettore in cui il figlio sinistro (left(i)) ed il figlio destro (right(i) di un nodo di posizione i si trovano nelle posizioni 2i e 2i+1 (se minori di heapsize) rispettivamente Il padre è in posizione floor(i/2), se tale valore è 1.
Altezza albero binario 2 14 16 i=1 3 10 Altezza di un nodo: numero di archi sul più lungo cammino dal nodo ad una foglia H = 3 4 8 5 7 6 9 3 7 Altezza dell'albero: massima altezza di un nodo 8 9 10 2 4 1 H = altezza albero binario; N = numero di elementi nell'albero Se l albero è completo: N = 1 +2 + 2 2 + + 2 H = 2 H *( 1 + (1/2) + + (1/2) H )= 2 H *(2-(1/2) H ) = 2 H+1 1 Se l albero non è completo: 2 H -1 < N < 2 H+1 1 H = (log(n)) n k = 0 x k = x n+ 1 1 x 1
Heapify Supponiamo che A sia uno heap. Alteriamo il valore di A[1]. L array che otteniamo non è più un heap. I sottoalberi con radice in A[right(1)] ed A[left(1)] sono ancora heap. Dobbiamo scrivere una procedura che permuti gli elementi A[1], A[2],, A[heapsize[A]] in modo da ricostruire uno heap. Heapify(A,i) l left(i) r right(i) If (l heapsize(a)) and (A[l] > A[i]) then largest l else largest i If (r heapsize(a) ) and (A[r] > A[largest]) then largest r If (largest i) then scambia(a[i], A[largest]) Heapify(A, largest)
Heapify in C // n è l'heapsize. Attenzione: gli indici partono da 0 void heapify(int *a, int n, int i) { int l = 2 * i + 1;//figlio sinistro in posizione 2i+1 int r = l + 1;//figlio destro in posizione 2i+2 int m; } m = (l < n && a[l] > a[i])? l : i; if(r < n && a[r] > a[m]) m = r; if(m!= i) { swap(a,i,m); heapify(a,n,m); }
Buildheap La procedura heapify può essere usata in modo bottom-up per convertire un vettore A[1 n], in uno heap di lunghezza n. Build-heap(A) heapsize(a) lenght(a) for i lenght[a]/2 down to 1 do heapify(a,i) Complessità - Numero di nodi ad altezza h al più parte intera sup. di n (si prova per ( h+ 1) induzione) 2 - Altezza di uno heap con n elementi: parte intera inf. di log 2 n log(n) T (n) h=0 h n 2 (h + 1) n h=0 h 2 (h + 1) n h=0 h 2 (h ) 2 n O(n) h=0 h x h = x 1 x 2
Heapsort Heapsort(A) buildheap(a) for i = length(a) down to 2 do scambia(a[1], A[i]) heapsize(a) = heapsize(a)-1 heapify(a, 1) Tempo di esecuzione : O(n log(n)) Ordinamento in loco
Coda con priorità Una coda con priorità è una struttura dati che rappresenta insieme finito S di oggetti sul quale è definita una funzione priorità p(s) (esempio l'ordine sulle chiavi) Coda con priorità massima (minima); L'elemento con maggiore (minore) priorità è sempre in testa alla coda Sulla coda sono definite le seguenti operazioni: 1.MAXIMUM(A): ritorna l elemento con massima priorità. 2.EXTRACT_MAX(A): estrae e ritorna l elemento con massima priorità - La coda va risistemata dopo l'estrazione 3.INSERT(A, p): inserisce un nuovo elemento p nella coda. 4. Increase_key(A,i,k): assegna al nodo di indice i la nuova priorità k
Coda con priorità Con l'implementazione dello Heap mediante vettore A possiamo realizzare una coda a priorità efficiente MAXIMUM(A) ha un tempo di esecuzione costante, O(1) MAXIMUM(A) return A[1] EXTRACT_MAX(A) ha un tempo di esecuzione O(log(n)), dovuto alla chiamata Heapify. EXTRACT_MAX(A) if heapsize(a) < 1 then error heap underflow max A[1] A[1] A[heapsize(A)] heapsize[a] heapsize(a) 1 Heapify(A,1) return max
Coda con priorità HEAP-INSERT(A, key) : Inserisce il nuovo elemento con chiave key nella coda (alla fine) e deve far sì che sia rispettata la proprietà della coda. Ha un tempo di esecuzione O(log(n)), nel caso peggiore si risale l albero dalla foglia alla radice. HEAP-INSERT(A, key) heapsize(a) heapsize(a) + 1 i heapsize(a)// inserisce nell'ultimo livello prima posizione da sinistra while i > 1 and A[parent(i)] < key // risale finché non trova una chiave >= key A[i] A[parent(i)] i parent(i) end while A[i] key
Coda con priorità Increase_key(A,p,k) : Accedendo direttamente al nodo di indice p, ne cambia il valore della chiave. - Dopo deve far sì che sia rispettata la proprietà della coda - Risale lo heap e scambia il nodo con suo padre, fintanto che il padre ha chiave inferiore. NOTA 1: per la coda con minima priorità, la funzione da implementare è decrease_key. NOTA 2: attenzione alla implementazione delle funzioni che danno parent(i), left(i) e right(i) - Se gli indici dell'array che contiene lo heap, e quindi la coda, partono da 1, allora parent(i) = i/2, left(i)= 2i, right(i)= 2i +1 - E se partono da 0?
Misurare il tempo di esecuzione Nel file di intestazione time.h sono contenuti i prototipi che operano sulla data, sull'ora e sull'orologio interno time_t time (time_t *tp) Restituisce il tempo corrente espresso come numero di secondi trascorsi dal primo gennaio 1970. Per avere il tempo di esecuzione in secondi usiamo il seguente codice: time_t t1, t2; double sec; t1 = time(null); t2 = time(null); sec= difftime(t2,t1); difftime restituisce la differnza in secondi tra t2 e t1
Esercizi Esercizio 1. Scrivere un programma C che riceva le richieste di sussidio e le memorizzi in una coda a priorità. Ogni richiesta è formata da nome (stringa) e da un indicatore economico (scalare) che rappresenta anche la priorità della richiesta. Minore l'indicatore, maggiore la priorità. Il programma deve fornire poi all'utente 4 possibili funzionalità: 1. Inserire una nuova richiesta 2. Visualizzare il numero di richieste esistenti 3. Estrarre la richiesta con minimo indicatore economico 4. Visualizzare la richiesta con minimo indicatore Il programma deve uscire solo quando l'utente digita 'esci' Esercizio 2. Implementare una versione della coda con priorità che utilizzi un vettore, anziché uno heap, per implementare le operazioni richieste. Confrontare i tempi di esecuzione delle due versioni di coda con 100000 operazioni di inserimento e 100000 operazioni di estrazione del massimo partendo da una coda vuota.
Esercizi Esercizio 3. Implementare in C l'algoritmo Heapsort, e fare in modo che la funzione Heapsort(int*a, int*b, int heapsize) prenda in input un secondo vettore di interi b che al termine dell'algoritmo dovrà contenere la permutazione degli indici di 'a' corripondente all'ordinamento. Esempio: Valori letti a: [22] [33] [77] [55] [1] [9] Valori ordinati [1] [9] [22] [33] [55] [77] Permutazione ordinamento b: [4] [5] [0] [1] [3] [2]