Algoritmi e Strutture Dati Laurea in Informatica Calendario: 27 Febbraio – 13 Giugno Aula: LuM250 Orario: Mer 12.30-14.15 Gio, Ven 14.30-16.15 Numero crediti = 9 (~ 72 ore) ~ 52 ore di teoria, ~20 ore di esercizi Docenti: Paolo Baldan (5crediti) Michele Scquizzato (4 crediti)
Modalità d’Esame Prova intermedia per l’assegnazione di un bonus fino a 3 punti (che si somma al voto dell’esame finale) Prova scritta (5 appelli - indispensabile iscriversi nella lista di esame che verrà attivata su UNIWEB) Registrazione, con possibile orale (su base volontaria). Indispensabile per conseguire la lode.
Materiale didattico Testo: Introduzione agli Algoritmi e Strutture Dati (3° ed). T.H.Cormen, C.E.Leiserson, R.L.Rivest, C.Stein. McGraw-Hill. Trad. di: Introduction to Algorithms and Data Structures (3° ed). T.H.Cormen, C.E.Leiserson, R.L.Rivest, C.Stein. MIT Press. Pagina del corso: www.math.unipd.it/~baldan/Algoritmi + Moodle con altro materiale
Programma Le prime 5 parti del testo (con qualche omissione): Fondamenti: notazione per gli algoritmi e primi esempi di algoritmi e di analisi degli algoritmi Ordinamento e statistiche d’ordine Strutture dati fondamentali Tecniche avanzate di progettazione ed analisi degli algoritmi Strutture dati avanzate
INTRODUZIONE Esempio1: problema dell’ordinamento I problemi computazionali, gli algoritmi che li risolvono e le tecniche per sviluppare tali algoritmi Esempio1: problema dell’ordinamento Input: a1,a2,...,an Output: a'1,a'2,...,a'n permutazione (riarrangiamento) di a1,a2,...,an tale che a'1 a'2 ... a'n
TECNICA INCREMENTALE
Soluzione1: Algoritmo Insertion-Sort. j 1 n j A key 1 n j A i Usare questo esempio per illustrare la sintassi della pseudocodifica 1 n j A i key 1 n j A i key 1 n j A 1 n A j
// inseriamo A[j] nella sequenza // ordinata A[1..j-1] i = j - 1 Insertion-Sort(A) n = A.length for j = 2 to n key = A[j] // inseriamo A[j] nella sequenza // ordinata A[1..j-1] i = j - 1 while i > 0 and A[i] > key A[i+1] = A[i] i = i – 1 A[i+1] = key Usare questo esempio per illustrare la sintassi della pseudocodifica
// inseriamo A[j] nella // sequenza ordinata // A[1..j-1] i = j – 1 Insertion-Sort(A) n = A.length for j = 2 to n key = A[j] // inseriamo A[j] nella // sequenza ordinata // A[1..j-1] i = j – 1 while i > 0 and A[i] > key A[i+1] = A[i] i = i – 1 A[i+1] = key void Insertion-Sort(vector<T> A) { int i, j, n = A.size(); T key; for (j = 1; j < n; j++) key = A[j]; // inseriamo A[j] nella // sequenza ordinata // A[0..j-1] i = j – 1; while (i >= 0 && A[i] > key) A[i+1] = A[i]; i--; } A[i+1] = key; Usare questa slide per evidenziare similarità e differenze con il linguaggio C++.
A key 5 2 8 4 7 1 3 6 Insertion-Sort(A) n = A.length for j = 2 to n // inserisce A[j] nella // sequenza ordinata // A[1..j-1] key = A[j] i = j – 1 while i > 0 and A[i] > key A[i+1] = A[i] i = i – 1 A[i+1] = key 5 # 8 4 7 1 3 6 2 # 5 8 4 7 1 3 6 2 5 8 4 7 1 3 6 2 5 # 4 7 1 3 6 8 2 5 # 4 7 1 3 6 2 5 8 4 7 1 3 6 2 5 8 # 7 1 3 6 4 2 # 5 8 7 1 3 6 2 4 5 8 7 1 3 6 2 4 5 8 # 1 3 6 7 2 4 5 # 8 1 3 6 2 4 5 7 8 1 3 6
A key 2 4 5 7 8 1 3 6 Insertion-Sort(A) n = A.lenght for j = 2 to n // inserisce A[j] nella // sequenza ordinata // A[1..j-1] key = A[j] i = j – 1 while i > 0 and A[i] > key A[i+1] = A[i] i = i – 1 A[i+1] = key 2 4 5 7 8 # 3 6 1 # 2 4 5 7 8 3 6 1 2 4 5 7 8 3 6 1 2 4 5 7 8 # 6 3 1 2 # 4 5 7 8 6 1 2 3 4 5 7 8 6 1 2 3 4 5 7 8 # 6 1 2 3 4 5 # 7 8 1 2 3 4 5 6 7 8
Correttezza InsertionSort Analisi di Insertion-Sort: correttezza Insertion-Sort(A) n = A.length for j = 2 to n key = A[j] i = j - 1 while i > 0 and A[i] > key A[i+1] = A[i] i = i – 1 A[i+1] = key 1 n j A Introdurre il concetto di invariante di un ciclo ed illustrare il suo uso nella dimostrazione di correttezza di InsertionSort. 1 n j A i 1 n j A i 1 n j A i 1 n j A Correttezza InsertionSort
Analisi di Insertion-Sort: complessità Insertion-Sort(A) // n = A.length // for j = 2 to n // key = A[j] // i = j -1 // while i > 0 and A[i] > key // A[i+1] = A[i] // i = i – 1 // A[i+1] = key // Tempo richiesto per eseguire l’algoritmo calcolato come tempo richiesto per eseguire ciascuna istruzioni elementare moltiplicato per il numero di volte che l’istruzione viene ripetuta.
caso migliore:
caso peggiore:
caso medio:
TECNICA DIVIDE ET IMPERA
Soluzione2: Algoritmo Merge-Sort 0 1 2 2 3 4 5 6 7 8 9 5 2 8 0 4 7 1 9 3 2 6 5 2 8 0 4 7 1 9 3 2 6 5 2 8 0 4 7 1 9 3 2 6 5 2 8 0 4 7 0 2 4 5 7 8 1 9 3 2 6 1 2 3 6 9 5 2 8 0 4 7 2 6 1 9 3 2 5 8 5 2 8 0 4 7 0 4 7 1 3 9 1 9 3 2 6 2 6 5 2 8 0 4 7 1 9 3 6 2 5 2 2 5 8 8 0 4 0 4 7 7 1 9 1 9 3 3 2 2 6 6 2 5 4 9 1 5 5 2 2 4 4 1 1 9 9
Merge-Sort(A,p,r) if p < r q = (p+r)/2 Merge-Sort(A,p,q) Merge-Sort(A,q+1,r) Merge(A,p,q,r)
A[p..q] A[q+1..r] 0 2 4 5 7 8 1 2 3 6 9 L 0 1 2 2 3 4 5 7 8 6 9 0 1 2 2 3 4 5 6 7 8 9 0 1 2 2 3 4 5 7 8 6 9 0 1 2 2 3 4 5 6 7 8 9 0 1 2 2 3 4 5 6 7 8 9 0 1 2 2 3 4 5 6 7 8 9 0 1 2 4 5 7 8 2 3 6 9 0 2 4 5 7 8 1 2 3 6 9 0 1 2 2 3 4 5 7 8 6 9 2 4 5 7 8 1 2 3 6 9 0 1 2 4 5 7 8 2 3 6 9 0 1 2 2 4 5 7 8 3 6 9 R A[p..r]
Merge(A,p,q,r) n1 = q – p + 1 n2 = r – q for i = 1 to n1 L[i] = A[p + i – 1] for j = 1 to n2 R[j] = A[q + j] L[n1 + 1] = R[n2 + 1] = i = j = 1 for k = p to r if L[i] R[j] A[k] = L[i] i = i + 1 else A[k] = R[j] j = j + 1
Analisi di Merge-Sort: correttezza Merge-Sort(A,p,r) if p < r // altrimenti A[p..r] è ordinato q = (p+r)/2 Merge-Sort(A,p,q) Merge-Sort(A,q+1,r) Merge(A,p,q,r) non ordinati 1 p r n A ordinati 1 p r n q A ordinati 1 p r n A
Merge(A,p,q,r) n1 = q – p + 1 n2 = r – q for i = 1 to n1 L[i] = A[p + i – 1] for j = 1 to n2 R[j] = A[q + j] L[n1 + 1] = R[n2 + 1] = i = j = 1 for k = p to r if L[i] R[j] A[k] = L[i] i = i + 1 else A[k] = R[j] j = j + 1 1 p r n q A 1 p r n A L ∞ R n1 n2 1 p r n A L ∞ R n1 n2 k i j 1 p r n A L ∞ R n1 n2 k i j
Merge(A,p,q,r) // complessità // n1 = q – p + 1 // n2 = r – q // for i = 1 to n1 // L[i] = A[p + i – 1] // for j = 1 to n2 // R[j] = A[q + j] // L[n1 + 1] = R[n2 + 1] = // i = j = 1 // for k = p to r // if L[i] R[j] // A[k] = L[i] // i = i + 1 // else A[k] = R[j] // j = j + 1 //
Merge-Sort(A,p,r) //complessità // if p < r // q = (p+r)/2 // Merge-Sort(A,p,q) // Merge-Sort(A,q+1,r) // Merge(A,p,q,r) //
Dunque esiste N tale che per ogni n > N. Qualunque siano i valori delle costanti a', b', c', a", b" e c" l’algoritmo Merge-Sort è superiore a Insertion-Sort per array di dimensione sufficientemente grande.
Possiamo dire che “cresce come” n2 mentre “cresce come” n log2 n. IS n2 ns MS n log2 n ns 10 100 33 0.1s 0.033s 100 10000 664 10s 0.664s 1000 106 9965 1ms 10s 10000 108 132877 0.1s 133s 106 1012 2·107 17m 20ms 109 1018 3·1010 70anni 30s 109 1018 3·1010 30s
dunque esiste N tale che per ogni n > N. Qualunque siano i valori delle costanti a', b', c', a", b" l’algoritmo Insertion-Sort è superiore a Merge-Sort per array (quasi) ordinati e sufficientemente grandi.
Insertion-Sort è anche superiore a Merge-Sort per array piccoli in quanto le costanti a', b', c' in sono generalmente molto maggiori delle costanti a", b" e c" in . Questo suggerisce una modifica di Merge-Sort in cui le porzioni di array di dimensione minore di una certa costante k si ordinano con Insertion-Sort invece di usare ricorsivamente Merge-Sort.
Soluzione3: Algoritmo Merge-Ins-Sort Merge-Ins-Sort(A,p,r) if p < r if r-p+1 < 32 InsertSort(A,p,r) else q = (p+r)/2 Merge-Ins-Sort(A,p,q) Merge-Ins-Sort(A,q+1,r) Merge(A,p,q,r)
Complessità e Notazione Asintotica Quando si confrontano algoritmi, determinare il tempo di esecuzione È complicato Contiene dettagli inutili che vorremmo ignorare Dipende da costanti non note vogliamo darne una visione più astratta (tasso di crescita)
Paragonare tra loro algoritmi Abbiamo una scala di complessità: vogliamo inserire ogni algoritmo in questa scala
Un algoritmo può richiedere tempi diversi per input della stessa taglia. Ad esempio il tempo per ordinare n oggetti può dipendere dal loro ordine iniziale. complessità massima complessità media complessità minima
Nell’analizzare la complessità tempo di un algoritmo siamo interessati a come aumenta il tempo al crescere della taglia n dell’input. Siccome per valori “piccoli” di n il tempo richiesto è comunque poco, ci interessa soprattutto il comportamento per valori “grandi” di n (il comportamento asintotico)
Inoltre, siccome la velocità del processore influisce sul tempo calcolo per una costante moltiplicativa noi valuteremo la complessità a meno di una tale costante. Questo giustifica le seguenti definizioni:
Notazione asintotica O (limite superiore asintotico) O(g(n))
Scriveremo f(n) = O(g(n)) per dire che f(n) è una delle funzioni dell’insieme O(g(n)) si legge: f(n) è “o grande” di g(n) Se f(n) = O(g(n)) rappresenta il tempo calcolo richiesto da un algoritmo diciamo che O(g(n)) è un limite superiore asintotico per la complessità tempo di tale algoritmo.
esempi infatti per c = 4 ed n0 = 5 Vedremo che in generale per a2 > 0 a_2n^2+a_1n+a_0 \leq cn^2 a_2+a_1/n+a_0/n^2 \leq c c=2a_2 a_1/n+a_0/n^2 \leq a_2 a_1/n+a_0/n^2 \leq (|a_1|+|a_0|)/n \leq a_2 Vero per n \geq (|a_1|+|a_0|)/a_2 infatti per c = 3 ed n0 = 1
Notazione asintotica . (limite inferiore asintotico)
Scriveremo f(n) = (g(n)) per dire che f(n) è una delle funzioni dell’insieme (g(n)). si legge: f(n) è “omega” di g(n) Se f(n) = (g(n)) rappresenta il tempo calcolo richiesto da un algoritmo diciamo che (g(n)) è un limite inferiore asintotico per la complessità tempo di tale algoritmo.
esempi infatti per c = 1 ed n0 = 10 Vedremo che in generale se a2 > 0 a_2n^2+a_1n+a_0 \geq cn^2 a_2+a_1/n+a_0/n^2 \geq c c=a_2/2 a_2/2 \geq -a_1/n-a_0/n^2 -a_1/n-a_0/n^2 \leq (|a_1|+|a_0|)/n \leq a_2/2 Vero per n \geq 2(|a_1|+|a_0|)/a_2 infatti per c = 1 ed n0 = 1
Notazione asintotica . (limite asintotico stretto)
Scriveremo f(n) = (g(n)) per dire che f(n) è una delle funzioni dell’insieme (g(n)). si legge: f(n) è “theta” di g(n) Se f(n) = (g(n)) rappresenta il tempo calcolo richiesto da un algoritmo diciamo che (g(n)) è un limite asintotico stretto per la complessità tempo di tale algoritmo.
esempi Dunque per c1 = 1, c2 = 4 ed n0 = 10 Dunque
per ogni n n0 allora altrimenti per ogni n n0. Assurdo! per ogni n n0 allora altrimenti per ogni n n0. Assurdo!
Metodo del limite Spesso è possibile determinare dei limiti asintotici calcolando il limite di un rapporto. Ad esempio se allora per ogni > 0 esiste n0 tale che per n ≥ n0 Preso 0 < < k e posto c1 = k e c2 = k + e quindi
Se diciamo che ed in questo caso Se diciamo che ed in questo caso Attenzione: quando il limite del rapporto non esiste questo metodo non si può usare.
In generale per ogni funzione polinomiale di grado k con coefficiente ak > 0. Inoltre
Per 0 < h < k e 1 < a < b :
Useremo le notazioni asintotiche anche all’interno delle formule. In questo caso le notazioni O(f(n)), Ω(f(n)) e ϴ(f(n)) stanno ad indicare una qualche funzione appartenente a tali insiemi e di cui non ci interessa conoscere la forma esatta ma solo il comportamento asintotico. Ad esempio T(n)=n2+O(n) significa che T(n) è la somma di n2 e di una funzione che cresce al più linearmente.
Valutare la difficoltà dei problemi esiste un algoritmo che risolve il problema con questa complessità limite superiore: O(n2)
Valutare la difficoltà dei problemi ogni algoritmo che risolve il problema ha complessità maggiore o uguale di questa limite inferiore: (n)
Un limite superiore per il problema dell’ordinamento Abbiamo visto che Insert-Sort per ordinare n oggetti richiede O(n2) operazioni Quindi O(n2) è un limite superiore
Vedremo in seguito che (n log n) è un limite stretto per il problema dell’ordinamento. Per ora ci limitiamo a dimostrare che: La complessità nel caso pessimo di ogni algoritmo di ordinamento sul posto che confronta e scambia tra loro soltanto elementi consecutivi dell’array è (n2). Quindi il problema di ordinare sul posto un array scambiando tra loro soltanto elementi consecutivi ha complessità (n2).
Se l’array è ordinato non ci sono inversioni. Sia A[1..n] un array Se i < j e A[i] > A[j] diciamo che la coppia di indici (i, j) è una inversione i j k 8 3 3 Se l’array è ordinato non ci sono inversioni. Se l’array è ordinato in senso opposto e gli elementi sono tutti distinti allora ogni coppia (i, j) di indici con i < j è una inversione e quindi ci sono esattamente n(n-1)/2 inversioni.
Come cambia il numero di inversioni quando facciamo uno scambio tra due elementi consecutivi A[i] ed A[i+1] dell’array? i i+1 x y Consideriamo tutte le coppie di indici ( j, k) con j < k e vediamo quante e quali di esse possono cambiare di stato da inversioni a non inversioni o viceversa quando scambiamo A[i] con A[i+1].
Se j e k sono entrambi diversi da i e i+1 la coppia ( j, k) non cambia di stato e quindi il numero di inversioni di questo tipo non cambia. j i i+1 k u y x v
Consideriamo le due coppie (i, k) e (i+1, k) con k > i+1 ossia x y v i i+1 k y x v (i, k) è inversione dopo lo scambio se e solo se (i+1, k) lo era prima e (i+1, k) è inversione se e solo se (i, k) lo era prima. Quindi le due coppie si scambiano gli stati ma il numero totale di inversioni non cambia.
Consideriamo le coppie (j, i) e (j,i+1) con j < i ossia u x y j i i+1 u y x La situazione è simmetrica di quella precedente e quindi anche in questo caso il numero totale di inversioni non cambia.
Rimane soltanto da considerare la coppia (i, i+1) che con lo scambio cambia di stato se i due elementi sono diversi. In conclusione con lo scambio di due elementi consecutivi dell’array il numero totale di inversioni aumenta o diminuisce di 1 (o rimane invariato se i due elementi scambiati erano uguali).
Nel caso pessimo in cui l’array è ordinato in senso inverso e gli elementi sono tutti distinti le inversioni iniziali sono n(n-1)/2. Occorrono quindi almeno n(n-1)/2 scambi tra elementi consecutivi per ridurre tale numero a 0. Siccome n(n-1)/2 = (n2) rimane dimostrato il limite inferiore.
Esercizio: Abbiamo dimostrato che scambiando due elementi diversi consecutivi il numero totale di inversioni aumenta o diminuisce di 1. Quindi se prima dello scambio il numero di inversioni totale era pari, dopo lo scambio esso risulta dispari e viceversa. Mostrare che questo cambiamento della parità del numero totale di inversioni avviene anche se si scambiano due elementi diversi non consecutivi.
Soluzione delle ricorrenze Metodo di sostituzione: Si assume che la soluzione sia di un certo tipo, ad esempio dove k1, k2 e k3 sono delle costanti Si sostituisce la soluzione nella ricorrenza e si cercano dei valori delle costanti per i quali la ricorrenza è soddisfatta. Se le cose non funzionano si riprova con un altro tipo di soluzione.
Esempio: assumiamo sostituendo si ottiene: Le costanti k1, k2 e k3 devono essere le stesse a sinistra e a destra.
per n = 1 si ottiene: mentre per n > 1 da cui e dunque è la soluzione.
Soluzione delle ricorrenze Metodo dell’esperto: Fornisce direttamente le soluzioni asintotiche di molte ricorrenze del tipo: dove n/b significa anche n/b o n/b
Teorema dell’esperto: Se T(n) = aT(n/b)+f(n) è una ricorrenza con a ≥ 1 e b > 1 costanti e dove n/b può essere anche n/b o n/b allora : per qualche costante > 0 per qualche costante ε > 0 ed esistono k < 1 ed N tali che a f(n/b) k f(n) per ogni n N
Intuizione:
Come usare il Teorema dell’esperto T(n) = aT(n/b)+f(n) Togliere eventuali arrotondamenti per eccesso o per difetto Calcolare Calcolare il limite Se il limite è finito e diverso da 0 siamo nel Caso 2 e
Se il limite è 0 potremmo essere nel Caso 1 Se il limite è 0 potremmo essere nel Caso 1. Per esserne sicuri occorre trovare un valore positivo ε per il quale risulti finito il limite nel qual caso possiamo concludere Se per ogni ε positivo tale limite risulta infinito il teorema dell’esperto non si può usare.
Se il limite è ∞ potremmo essere nel Caso 3 Se il limite è ∞ potremmo essere nel Caso 3. Per esserne sicuri occorre trovare un ε positivo per il quale risulti diverso da 0 il limite Se è 0 per ogni ε positivo non si può usare il teorema dell’esperto. Altrimenti prima di concludere bisogna studiare la disequazione a f(n/b) ≤ k f(n)
Se tale disequazione è soddisfatta per qualche costante k strettamente minore di 1 e per tutti i valori di n da un certo valore N in poi possiamo concludere che Altrimenti il teorema dell’esperto non si può usare.
Esempi: Trascurando gli arrotondamenti entrambe sono della forma: Con a=b=2 ed f(n)=(n) siccome e quindi possiamo applicare il Caso 2 e concludere
Esempio: In questo caso e quindi Caso 1? Se Per e Quindi e si applica il Caso 1
Esempio: In questo caso e quindi Caso 3? Se e Per e Quindi Inoltre Si applica il Caso 3:
Esempio: ma e quindi per qualunque Dunque non si può usare il metodo dell’esperto. Neanche la seconda condizione è soddisfatta ma e quindi non esiste nessun k < 1 tale che per ogni n > N
Soluzione: Algoritmo Heap-Sort Un array A[1..n] può essere interpretato come un albero binario: A[1] è la radice, A[2i] e A[2i+1] sono i figli di A[i] A[ i / 2 ] è il padre di A[i]
Albero binario quasi completo a1 a2 a12 a3 a4 a5 a6 a7 a8 a9 a10 a11 1 2 3 4 5 6 7 8 9 10 11 12 a1 a2 a12 a3 a4 a5 a6 a7 a8 a9 a10 a11 Albero binario quasi completo a10 a11 a9 a12 a2 a8 a4 a5 a6 a7 a3 a1 12 102 112 1002 1012 1102 1112 10002 10012 10102 10112 11002
Proprietà di un heap (mucchio) Diciamo che A[1..n] è un (è ordinato a) max-heap se ogni elemento A[i] soddisfa la seguente proprietà: “A[i] è maggiore o uguale di ogni suo discendente in A[1..n]” Per brevità indicheremo questa proprietà con H(i)
Un max-heap 1 2 3 4 5 6 7 8 9 10 11 12 9 8 2 7 5 4 3 6 1 6 1 3 2 8 4 5 7 9 1 2 3 4 5 6 7 8 9 10 11 12
Costruzione di un max-heap 1 2 3 4 5 6 7 8 9 10 11 12 6 5 5 6 4 8 9 2 7 3 1 9 5 6 8 5 9 6 9 8 5 8 7 7 6 2 9 2 4 2 5 1 6 2 3 8 9 2 7 4 5 6 7 4 3 7 1 4 8 9 10 11 12
Ordinamento dell’array 1 2 3 4 5 6 7 8 9 10 11 12 8 2 2 9 6 3 1 8 1 7 6 3 7 3 7 1 1 7 9 8 2 7 5 4 3 6 1 5 3 1 6 3 5 4 3 3 4 3 2 7 3 1 4 1 2 6 2 1 2 1 9 1 8 7 2 3 5 7 4 4 5 6 7 4 3 6 1 2 8 9 10 11 12
Max-Heapfy(A,i) l = 2i, r =2i+1 m = i if l A.heapsize and A[l] > A[m] m = l if r A.heapsize and A[r] > A[m] m = r if m i t = A[i], A[i] = A[m], A[m] = t Max-Heapfy(A,m)
Build-Max-Heap (A) A.heapsize = A.length for i = A.lenght/2 downto 1 Max-Heapfy(A,i)
Heap-Sort (A) Build-Max-Heap(A) for i = A.length downto 2 t = A[i], A[i] = A[1], A[1] = t A.heapsize = A.heapsize - 1 Max-Heapfy(A,1)
Abbiamo visto che la complessità nel caso pessimo di ogni algoritmo di ordinamento sul posto che confronta e scambia tra loro elementi consecutivi dell’array è (n2). Per ottenere algoritmi più efficienti dobbiamo quindi operare confronti e scambi tra elementi “distanti” dell’array. L’algoritmo Heap-Sort confronta elementi non consecutivi e possiamo quindi sperare che la sua complessità sia minore.
In effetti Heap-Sort richiede tempo O(n log n) per ordinare un array di n elementi (vedi Libro 6.2, 6.3, 6.4)
Implementazione di code con priorità Gli heap binari si possono usare, oltre che per ordinare un array, anche per implementare delle code con priorità. Le code con priorità sono delle strutture dati in cui è possibile immagazzinare degli oggetti x a cui è attribuita una priorità x.key ed estrarli uno alla volta in ordine di priorità.
Le operazioni fondamentali sulle code con priorità sono: Insert(S, x): aggiunge x alla coda S Maximum(S): ritorna x S con x.key massima Extract-Max(S): toglie e ritorna x S con x.key massima. Possono inoltre essere definite anche: Increase-Key(S,x,p): aumenta la priorità di x Change-Key(S,x,p): cambia la priorità di x
Heap-Maximum(A) // A è un max-heap if A.heapsize < 1 error “underflow” else return A[1] Heap-Extract-Max(A) // A è un max-heap if A.heapsize < 1 error “underflow” else max = A[1] A[1] = A[A.heapsize] A.heapsize = A.heapsize - 1 Max-Heapfy(A,1) return max
Heap-Insert 1 2 3 4 5 6 7 8 9 10 11 12 13 12 9 9 12 8 7 12 7 5 7 4 12 4 4 12 13 3 6 1 2 6 1 3 2 8 4 5 7 9 12 10 11
Per realizzare Heap-Insert e Heap-Increase-Key ci serve una Max-Heapfy diversa che invece della proprietà: “A[i] è maggiore o uguale di ogni suo discendente” usa la proprietà simmetrica: “A[i] è minore o uguale di ogni suo ascendente” entrambe, se vere per ogni elemento dell’array, ci assicurano l’ordinamento a max-heap di A.
La nuova versione Max-HeapfyR ricostruisce lo heap quando tutti gli elementi dell’array sono minori o uguali dei loro ascendenti tranne al più quello in posizione i. Max-HeapfyR(A,i) // solo A[i] può non soddisfare la proprietà while i >1 and A[⌊i/2⌋].key < A[i].key scambia A[⌊i/2⌋] con A[i] // solo A[⌊i/2⌋] può non soddisfarla i = ⌊i/2⌋
Heap-Increase-Key(A,i,p) // A max-heap if p < A[i].key error “la nuova priorità è minore” else A[i].key = p Max-HeapfyR(A,i) Max-Heap-Insert(A,x) // A max-heap A.heapsize = A.heapsize+1 A[A.heapsize] = x Max-HeapfyR(A,A.heapsize)
Possiamo facilmente realizzare anche una Heap-Change-Key nel modo seguente: Heap-Change-Key(A,i,p) // A max-heap if p < A[i].key A[i].key = p Max-Heapfy(A,i) else Max-HeapfyR(A,i)
Soluzione 6: Algoritmo Quicksort Si basa sulla partizione dell’array rispetto ad un suo elemento scelto come “pivot”. L’operazione viene quindi ripetuta sulle due parti così ottenute. 1 2 3 4 5 6 7 8 9 10 11 12 9 9 6 4 6 9 2 8 8 4 4 3 6 4 1 2 9 2 7 5 7 4 8 4 3 3 6 7 7 1 1 9 5 7 5 i j i i j i i j j i i j j j j j j j
Quicksort(A,p,r) if p < r q = Partition(A,p,r) Quicksort(A,p,q-1) Quicksort(A,q+1,r) 1 p r n A non ordinati 1 p r n A q 1 p r n A q 1 p r n A q 1 p r n A ordinati
Partition(A,p,r) x = A[r] i = p -1 for j = p to r -1 if A[j] < x i = i+1 scambia A[i] e A[j] scambia A[i+1] e A[r] return i+1 1 p r n A 1 p r n A i x 1 p r n A i x j 1 p r n A i x 1 p r n A i x
Array ordinato o ordinato in senso inverso Quicksort (A,p,r) // Complessità massima if p < r q = Partition(A,p,r) Quicksort(A,p,q-1) Quicksort(A,q+1,r) Array ordinato o ordinato in senso inverso
Quicksort(A,p,r) // Complessità minima if p < r // q = Partition(A,p,r) // Quicksort(A,p,q-1) // Quicksort(A,q+1,r) //
Quicksort (A,p,r) // Complessità media if p < r then q = Partition(A,p,r) Quicksort(A,p,q-1) Quicksort(A,q+1,r)
Per n > 1 e moltiplicando per n otteniamo Per n = 2 Per n > 2 e dunque
dividendo per n(n+1) ponendo otteniamo
la cui soluzione è
Infine Quindi
La complessità media O(n log n) di Quick-Sort vale soltanto se tutte le permutazioni dell’array in ingresso sono ugualmente probabili. In molte applicazioni pratiche questo non è vero!!! Vi sono applicazioni in cui le permutazioni quasi ordinate sono molto più probabili e questo può aumentare la complessità media fino ad O(n2).
Randomized-Partition(A,p,r) i = Random(p,r) scambia A[i] e A[r] return Partition(A,p,r) Randomized-Quicksort(A,p,r) if p < r then q = Randomized-Partition(A,p,r) Randomized-Quicksort(A,p,q-1) Randomized-Quicksort(A,q+1,r)
Complessità del problema Problema dell’ordinamento Input: sequenza a1,a2,...,an di elementi su cui è definito un ordine Output: a'1,a'2,...,a'n permutazione di a1,a2,...,an tale che a'1 ≤ a'2 ≤ ... ≤ a'n Se non facciamo ipotesi sul tipo degli elementi della sequenza le uniche operazioni permesse sono confronti e assegnazioni.
Siccome siamo interessati ad un limite inferiore possiamo contare solo alcune delle operazioni. Se un certo limite inferiore vale per il tempo richiesto per eseguire tali operazioni a maggior ragione varrà per il tempo calcolo totale. Noi conteremo solo i confronti e dimostreremo che nel caso pessimo il numero di confronti è Ω(n log n). Per fare questo è utile rappresentare la struttura di un algoritmo mediante un albero delle decisioni.
1:2 ≤ > 2:3 1:3 ≤ > ≤ > 1:3 2:3 ≤ > > ≤ Esempio. Albero delle decisioni di Insertion-Sort con un array di 3 elementi. Insertion-Sort(A) n = A.length for j = 2 to n i = j – 1 while i ≥ 1 and A[i]>A[i+1] scambia A[i] con A[i+1] i = i – 1 1:2 ≤ > 2:3 1:3 ≤ > ≤ > 1:3 2:3 ≤ > > ≤
1:2 a,b,c 2:3 a,b,c 1:3 b,a,c 1:3 a,c,b 2:3 b,c,a Esempio. Albero delle decisioni di Insertion-Sort con un array di 3 elementi. 1:2 a,b,c ≤ > 2:3 a,b,c 1:3 b,a,c ≤ > ≤ > 1:3 a,c,b 2:3 b,c,a a,b,c (1,2,3) b,a,c (1,2,3) ≤ > > ≤ a,c,b (1,2,3) c,a,b (1,2,3) b,c,a (1,2,3) c,b,a (1,2,3) Se l’algoritmo è corretto le foglie devono essere etichettate con ogni permutazione possibile dell’input. Perché?
Le permutazioni di 1,2,. ,n sono n Le permutazioni di 1,2,...,n sono n! e quindi l’albero delle decisioni deve avere almeno n! foglie. Ma un albero binario con N foglie deve avere altezza almeno pari a log2(N). Esercizio: Dimostrarlo per induzione su N. Dunque nel caso pessimo l’algoritmo deve eseguire almeno log2(n!) confronti.
Ma e quindi per ogni algoritmo generale di ordinamento. Possiamo concludere che Ω(n log n) è un limite inferiore per la complessità del problema dell’ordinamento.
L’algoritmo di ordinamento Heapsort risolve il problema dell’ordinamento con complessità massima Dunque O(n log n) è limite superiore per la complessità del problema dell’ordinamento. Siccome limite superiore e inferiore coincidono (n log n) è limite stretto per il problema dell’ordinamento.
Considerazione sul limite inferiore Ω(n log n) per l’ordinamento ATTENZIONE: Il limite inferiore Ω(n log n) da noi dimostrato vale solo per algoritmi di ordinamento generali, ossia algoritmi che non fanno alcuna ipotesi sul tipo degli elementi da ordinare: le uniche operazioni ammesse su tali elementi sono confronti e assegnazioni.
Il limite inferiore Ω(n log n) vale anche per ordinare numeri reali sui quali, oltre a confronti ed assegnazioni, si possono usare anche le quattro operazioni aritmetiche. In questo caso la dimostrazione del limite inferiore è molto più difficile e si basa su alcuni risultati di geometria algebrica. La dimostrazione si può trovare nel testo di Geometria Computazionale di F. Preparata.
Ordinamento in tempo lineare Il limite inferiore Ω(n log n) vale per tutti gli algoritmi di ordinamento generali, ossia per algoritmi che non fanno alcuna ipotesi sul tipo degli elementi della sequenza da ordinare. Se facciamo opportune ipotesi restrittive sul tipo degli elementi possiamo trovare algoritmi più efficienti. Naturalmente il limite inferiore banale Ω(n) vale comunque per tutti gli algoritmi di ordinamento.
Algoritmo Counting-Sort Assume che gli elementi dell’array siano interi compresi tra 0 e k con k costante. Per ordinare un array A Counting-Sort richiede un secondo array B in cui mette la sequenza ordinata e un array ausiliario C[0..k].
1 2 3 4 5 6 7 8 A 1 4 2 1 2 2 C 2 3 1 4 C 2 4 7 8 1 3 1 2 3 4 5 7 6 8 B 1 2 3 4 5 6 7 8 1 1 2 2 2 4
Counting-Sort(A,B,k) // A contiene a1,...,an for i = 0 to k C[i] = 0 for j = 1 to A.length x = A[j], C[x] = C[x] + 1 // C[x] è il numero di elementi aj = x for i = 1 to k C[i] = C[i] + C[i-1] // C[x] è il numero di elementi aj ≤ x for j = A.length downto 1 // i = C[x] è la posizione in B dove // mettere il prossimo aj = x x = A[j], i = C[x], B[i] = x C[x] = C[x] - 1
Complessità: TCS(n,k) = (n+k) Counting-Sort(A,B,k) // Complessità for i = 0 to k // C[i] = 0 // for j = 1 to A.length // x = A[j], C[x] = C[x] + 1 // for i = 1 to k // C[i] = C[i] + C[i-1] // for j = A.length downto 1 // x = A[j], i = C[x], B[i] = A[j] // C[x] = C[x] - 1 // Complessità: TCS(n,k) = (n+k) Se k = O(n) allora TCS(n,k) = (n)
Osservazione: Nell’ultimo ciclo for dell’algoritmo gli elementi dell’array A vengono copiati nell’array B partendo dall’ultimo Cosa succede se partiamo dal primo?
A 1 4 2 1' 2' 0' 2" 3 5 6 7 8 C 2 4 7 8 1 3 1 3 6 7 B 1 2 3 4 5 6 7 8 1' 1 2 4
Succede che l’algoritmo è ancora corretto ma gli elementi uguali vengono ricopiati in ordine inverso. Quando un algoritmo di ordinamento mantiene l’ordine iniziale tra due elementi uguali si dice che esso è stabile. L’algoritmo Counting-Sort (con l’ultimo ciclo for decrescente) è stabile.
Algoritmo Radix-Sort Assume che i valori degli elementi dell’array siano interi rappresentabili con al più d cifre in una certa base b. Ad esempio interi di d = 5 cifre decimali (b = 10), interi di d = 4 byte (cifre in base b = 256) o stringhe di d caratteri (b = 256). Per ordinare l’array si usa d volte un algoritmo di ordinamento stabile (ad esempio Counting-Sort) per ordinare l’array rispetto a ciascuna delle d cifre partendo dalla meno significativa.
4 3 2 1 7 5 6 9 8 4 3 2 1 7 5 6 9 8 4 3 2 1 7 5 6 9 8 4 3 2 1 7 5 6 9 8 4 3 2 1 7 5 6 9 8 A[1] A[2] A[3] A[4] A[5] A[6] A[7] A[8]
Radix-Sort(A,d) // A[i] = cd...c2c1 for j = 1 to d // A è ordinato rispetto alle cifre cj-1...c1 “usa un algoritmo stabile per ordinare A rispetto alla j-esima cifra” // A è ordinato rispetto alle cifre cj...c1 // A è ordinato
Radix-Sort(A,d) // Complessità for j = 1 to d “usa Counting-Sort per ordinare A rispetto alla j-esima cifra” Complessità: dove b è la base della numerazione e d è il numero di cifre dei numeri da ordinare.
Dovendo ordinare n numeri di m bit ciascuno possiamo scegliere r < m e suddividere i numeri in d=m/r “cifre” di r bit ciascuna In questo caso la base della numerazione è b=2r e Radix-Sort richiede tempo La funzione ha un minimo per r tale che Quindi il valore ottimo di r dipende soltanto da n ed è approssimativamente log2 n.