Algoritmi di ordinamento Selection Sort Quick Sort Lower bound alla complessità degli algoritmi di ordinamento
Selection Sort L’elemento minimo viene messo in posizione 0 SelectionSort(dati[]) { for (i=0; i<dati.length-1; i++) { min = <Seleziona min. in dati[i], …. , dati[dati.length-1]> <Scambia min con dati[i]; } L’elemento minimo viene messo in posizione 0 Si itera il procedimento sulle posizioni successive
Selection Sort/2 Versione ricorsiva SelectionSort(dati[], i) { min = <Seleziona min. in dati[i], …. , dati[dati.length-1]> <Scambia min con dati[i]; SelectionSort(dati[], i+1) ; } …… SelectionSort(dati[], 0) ; Versione ricorsiva
Selection Sort/3 Ordinamento del vettore di interi {5, 2, 3, 8, 1}
Come ordinare oggetti diversi da numeri Ordinare un vettore i cui elementi sono oggetti complessi. Es. oggetti della classe: class Persona { String cognome; String CF; public Persona (String cognome, String CF) { this.cognome = cognome; this.CF = CF; } Come ordinare un array di tali oggetti rispetto al cognome ?
Come ordinare oggetti diversi da numeri/2 Occorre: Dichiarare che tra gli oggetti della classe (Persona nell’esempio) è definito un ordinamento Dichiarare rispetto a quale o a quali membri della classe è definito l’ordinamento (il cognome nel nostro caso) Definire la regola che stabilisce l’ordinamento tra due oggetti della classe (nel nostro caso: due oggetti di tipo persona sono ordinati alfabeticamente secondo i rispettivi cognomi) In C++ si possono sovraccaricare gli operatori In Java si può dichiarare che la classe (Persona) implementa l’interfaccia Comparable (non è la sola possibilità)
Come ordinare oggetti diversi da numeri/3 Il passo 1 si traduce così: class Persona implements Comparable { …… } I passi 2 e 3 consistono nell’implementare l’unico metodo previsto dall’interfaccia Comparable: int compareTo(Object o) compareTo definisce le regole che stabiliscono l’ordinamento tra oggetti della classe (nel nostro caso, l’ordinamento è quello alfabetico sui cognomi)
Come ordinare oggetti diversi da numeri/4 Quindi: class Persona implements Comparable { String cognome; String CF; public Persona (String cognome, String CF) { this.cognome = cognome; this.CF = CF; } public int compareTo (Object pers) { return cognome.compareTo(((Persona)pers).cognome); Nota: occorre fare il cast perché compareTo vuole un Object
Selection Sort/4 public void selectionsort(Comparable[] data) { int i, j, least; for (i = 0; i < data.length-1; i++) { for (j = i+1, least = i; j < data.length; j++) if (data[j].compareTo(data[least]) < 0) least = j; swap(data, least, i); /* Scambia gli oggetti in pos. i e least */ } Es.: versione ricorsiva
Selection Sort - Tempo di esecuzione Supponiamo che l’array contenga n elementi Alla i-esima iterazione occorre trovare il massimo di n-i+1 elementi e sono quindi necessari n-i confronti Vi sono n-1 cicli Costo = Si osservi che tale costo non dipende dall’ eventuale ordinamento parziale dell’array (cfr. Insertion Sort)
Quick Sort quicksort(array[]) { if (array.length>1) { Scegli bound; /* subarray1 e subarray2 */ while (ci sono elementi in array) if (generico elemento < bound) inserisci elemento in subarray1; else inserisci elemento in subarray2; quicksort(subarray1); quicksort(subarray2); }
Quick Sort/2 Array subarray1 < bound subarray2 >= bound
Partizionamento dell’array [8 5 4 7 6 1 6 3 8 12 10] con quicksort
Partizionamento dell’array [8 5 4 7 6 1 6 3 8 12 10] con quicksort
Quick Sort/3 void quicksort(Comparable[] data, int first, int last) { int lower = first + 1, upper = last; swap(data, first, (first+last)/2); /* Questo serve solo perché così, in pratica è spesso più veloce */ Comparable bound = data[first]; while (lower <= upper) { while (data[lower].compareTo(bound) < 0) lower++; while (bound.compareTo(data[upper]) < 0) upper--; if (lower < upper) swap(data, lower++, upper--); else lower++; /* 1 */ } /* End while */ swap(data, upper, first); if (first < upper-1) /* se first == upper-1 il sottoarray ha solo 2 elementi ed è ordinato */ quicksort(data, first, upper-1); if (upper+1 < last) quicksort(data, upper+1, last); }
Quick Sort/4 void quicksort(Comparable[] data) { if (data.length < 2) return; int max = 0; /* Trova max. e mettilo alla fine; serve per evitare che lower cresca oltre la dim. dell’ array (non è detto che accada ma può succedere) */ for (int i = 1; i < data.length; i++) if (data[max].compareTo(data[i]) < 0) max = i; swap(data, data.length-1, max); // largest el is now in its quicksort(data, 0, data.length-2); // final position; }
Analisi del Quick Sort Costo = O(No. confronti) Costo O(n2) nel caso peggiore Costo O(n log n) nel caso migliore e medio In pratica l’algoritmo è efficiente Scelta pivot fondamentale
Quick Sort – Caso peggiore No. confronti per sotto-array Array n-1 n-2 2 1 n-1 volte L’elemento di pivot è sempre il minimo Costo = O(n-1+n-2+...+2+1) = O(n2)
Quick Sort – Caso migliore No. confronti per sotto-array n potenza di 2 per semplicità Array n-1 n/2-1 2 1 log n+1 volte n/4-1 Costo =
Efficienza algoritmi di ordinamento Merge Sort (e Heap Sort): O(n log n) Quick Sort, Selection Sort, Insertion Sort: O(n2) Quick Sort: O(n log n) nel caso migliore Selection Sort: O(n2) in tutti i casi Insertion Sort: O(n) nel caso migliore Domanda: qual è l’efficienza massima (complessità minima) ottenibile nel caso peggiore -> Lower bound
Ordinamento – limiti inferiori Osservazione fondamentale: tutti gli algoritmi devono confrontare elementi Dati ai, ak, tre casi possibili: ai < ak, ai > ak, oppure ai=ak Si assume per semplicità che tutti gli elementi siano distinti Si assume dunque che tutti i confronti abbiano la forma ai < ak, e il risultato del confronto sia vero o falso Nota: se gli elementi possono avere lo stesso valore allora si considerano solo confronti del tipo ai <= ak
Alberi di decisione a1:a2 < > a2:a3 a1:a3 < > > < Albero di decisione per Insertion Sort sull’insieme {a1, a2, a3} a1:a2 < > a2:a3 a1:a3 < > > < a1,a2,a3 a1:a3 a2,a1,a3 a2:a3 > < > < a1,a3,a2 a3,a1,a2 a2,a3,a1 a3,a2,a1 Un albero di decisione rappresenta i confronti eseguiti da un algoritmo su un dato input Ogni foglia corrisponde ad una delle possibili permutazioni
Alberi di decisione/2 a1:a2 < > a2:a3 a1:a3 < > > < Albero di decisione per Insertion Sort sull’insieme {a1, a2, a3} a1:a2 < > a2:a3 a1:a3 < > > < a1,a2,a3 a1:a3 a2,a1,a3 a2:a3 > < > < a1,a3,a2 a3,a1,a2 a2,a3,a1 a3,a2,a1 Vi sono n! possibili permutazioni -> l’albero deve contenere n! foglie L’esecuzione di un algoritmo corrisponde ad un cammino sull’albero di decisione corrispondente all’input considerato
Alberi di decisione/3 Riassumendo: Albero binario Deve contenere n! foglie Il più lungo cammino dalla radice ad una foglia (altezza) rappresenta il No. confronti che l’algoritmo deve eseguire nel caso peggiore Teorema: qualunque albero di decisione che ordina n elementi ha altezza Ώ(n log n) Corollario: nessun algoritmo di ordinamento ha complessità migliore di Ώ(n log n) Nota: esistono algoritmi di ordinamento con complessità più bassa, ma richiedono informazioni aggiuntive
Dimostrazione teorema Un albero di decisione è binario Albero binario di altezza h non ha più di 2h foglie 1 1=21-1 2 2= 22-1 3 4= 23-1 h 2h-1 Dobbiamo avere: 2h-1 > No. foglie = n! h-1 > log(n!)
Dimostrazione teorema/2 n! > (n/e)n (approssimazione di Stirling) h-1 > log(n/e)n = n log(n/e) = n logn – n loge = Ώ(n log n) Corollario: gli algoritmi Merge Sort e Heap Sort hanno complessità asintotica ottima