Algoritmi e Strutture Dati Laurea in Informatica

Slides:



Advertisements
Presentazioni simili
Camil Demetrescu, Irene Finocchi, Giuseppe F. ItalianoAlgoritmi e strutture dati Copyright © The McGraw - Hill Companies, srl 1 Ordinamenti ottimi.
Advertisements

Camil Demetrescu, Irene Finocchi, Giuseppe F. ItalianoAlgoritmi e strutture dati Copyright © The McGraw - Hill Companies, srl Capitolo 4 Ordinamento:
Camil Demetrescu, Irene Finocchi, Giuseppe F. ItalianoAlgoritmi e strutture dati Copyright © The McGraw - Hill Companies, srl Capitolo 4 Ordinamento:
Camil Demetrescu, Irene Finocchi, Giuseppe F. ItalianoAlgoritmi e strutture dati Capitolo 4 Ordinamento: Heapsort Algoritmi e Strutture Dati.
Algoritmi e Strutture Dati
Capitolo 4 Ordinamento: Selection e Insertion Sort Algoritmi e Strutture Dati.
Capitolo 4 Ordinamento Algoritmi e Strutture Dati.
Camil Demetrescu, Irene Finocchi, Giuseppe F. ItalianoAlgoritmi e strutture dati Copyright © The McGraw - Hill Companies, srl Usa la tecnica del.
Capitolo 4 Ordinamento: Selection e Insertion Sort Algoritmi e Strutture Dati.
Algoritmi e Strutture Dati
Capitolo 4 Ordinamento: Selection e Insertion Sort Algoritmi e Strutture Dati.
Capitolo 4 Ordinamento Algoritmi e Strutture Dati.
Algoritmi e Strutture Dati (Mod. A)
Algoritmi e Strutture Dati 8 crediti Calendario: 1 Ott. – 6 Dic. Aula: LuM250 Orario: Lun, Mar, Mer, Gio Giorni di lezione disponibili 40 ~48.
Ordinamento dell’array
Radix-Sort(A,d) // A[i] = cd...c2c1
Vedremo in seguito che (n log n) è un limite stretto per il problema dellordinamento. Per ora ci limitiamo a dimostrare che: La complessità nel caso pessimo.
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.
Valutare la difficoltà dei problemi
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]
Complessità del problema Se non facciamo ipotesi sul tipo degli elementi della sequenza le uniche operazioni permesse sono confronti e assegnazioni. Problema.
Algoritmi e Strutture Dati Laurea in Informatica Calendario: 2 Marzo – 12 Giugno Aula: LuM250 Orario: Mer, Gio, Ven Numero crediti = 8 (~ 64.
Soluzione 6: Algoritmo Quicksort
TECNICA DIVIDE ET IMPERA
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.
Algoritmi e Strutture Dati
Camil Demetrescu, Irene Finocchi, Giuseppe F. ItalianoAlgoritmi e strutture dati Copyright © The McGraw - Hill Companies, srl Capitolo 4 Ordinamento:
1 Ordinamento (Sorting) INPUT: Sequenza di n numeri OUTPUT: Permutazione π = tale che a 1 ’  a 2 ’  … …  a n ’ Continuiamo a discutere il problema dell’ordinamento:
Camil Demetrescu, Irene Finocchi, Giuseppe F. ItalianoAlgoritmi e strutture dati Copyright © The McGraw - Hill Companies, srl Capitolo 4 Ordinamento:
1 Ordinamento (Sorting) Input: Sequenza di n numeri Output: Permutazione π = tale che: a i 1  a i 2  ……  a i n Continuiamo a discutere il problema dell’ordinamento:
Didattica e Fondamenti degli Algoritmi e della Calcolabilità Sesta giornata Risolvere efficientemente un problema in P: Il problema dell’ordinamento: Insertion.
Capitolo 4 Ordinamento: lower bound Ω(n log n) e MergeSort ((*) l’intera lezione) Algoritmi e Strutture Dati.
Algoritmi e Strutture Dati HeapSort. Select Sort: intuizioni L’algoritmo Select-Sort  scandisce tutti gli elementi dell’array a partire dall’ultimo elemento.
Algoritmi e Strutture Dati Università di Camerino Corso di Laurea in Informatica (12 CFU) I periodo didattico Emanuela Merelli
Rudimenti di Complessità Corso di Programmazione II Prof. Dario Catalano.
Huffman Canonico: approfondimento. Come abbiamo visto, Huffman canonico ci permette di ottenere una decompressione più veloce e con un uso più efficiente.
© 2007 SEI-Società Editrice Internazionale, Apogeo
Insiemi di numeri e insiemi di punti
Algoritmi Avanzati a.a.2014/2015 Prof.ssa Rossella Petreschi
Progettare algoritmi veloci usando strutture dati efficienti
Progettare algoritmi veloci usando strutture dati efficienti
IL CONCETTO DI ALGORITMO
x : variabile indipendente
Complessità ammortizzata degli algoritmi Union Find
Algoritmi e Strutture Dati
Sulla complessità Lezione n°2
Tipo di dato: array Un array è un tipo di dato usato per memorizzare una collezione di variabili dello stesso tipo. Per memorizzare una collezione di 7.
Algoritmi e Strutture Dati
Divide et Impera Quicksort Mergesort
Algoritmi e Strutture Dati Modelli di calcolo e analisi di algoritmi
I RADICALI Definizione di radicali Semplificazione di radicali
Il sistema di numerazione decimale
I numeri relativi DEFINIZIONE. Si dicono numeri relativi tutti i numeri interi, razionali e irrazionali dotati di segno (positivo o negativo). ESEMPI Numeri.
32 = 9 x2 = 9 x = 3 32 = 9 √9 = 3 L’estrazione di radice
Matrici Definizioni Matrici Rettangolari Quadrate 02/01/2019
Ordinamento in tempo lineare
Heapsort.
Algoritmi e Strutture Dati
Corso di Laurea Ingegneria Informatica Fondamenti di Informatica
Progettare algoritmi veloci usando strutture dati efficienti
* 07/16/96 Sez. 2: Ordinamento La consultazione di banche dati è sempre più cruciale in tutte le applicazioni dell’Informatica. Se vogliamo consultare.
Algoritmi e Strutture Dati
Esercizio Dato un albero binario, definiamo altezza minimale di un nodo v la minima distanza di v da una delle foglie del suo sottoalbero, definiamo invece.
Algoritmi e Strutture dati a.a.2010/2011 Prof.ssa Rossella Petreschi
MergeSort Usa la tecnica del divide et impera:
concetti ed applicazioni
Unione per ranghi compressi
Analisi ammortizzata Lezione n°2
Ricerca 01/08/2019 package.
HeapSort Stesso approccio incrementale del selectionSort Tipo di dato
Transcript della presentazione:

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.1s 0.033s 100 10000 664 10s 0.664s 1000 106 9965 1ms 10s 10000 108 132877 0.1s 133s 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.