PERCORSI ABILITANTI SPECIALI Università di Pisa Algoritmi di ordinamento Classe A042 Informatica Algoritmica e problem solving per l’insegnamento Linguaggi di programmazione per l’insegnamento Laboratorio didattico-pedagogico integrato per l'insegnamento dell'informatica 1 a.a. 2013-14 Anastasia Scalioti Indice: 1. Ipotesi di lavoro ............................................................................................................................... 3 1.1 Prerequisiti................................................................................................................................. 3 1.2 Obiettivi ...................................................................................................................................... 3 1.3 Metodologie didattiche .............................................................................................................. 3 2. Algoritmi di Ordinamento ................................................................................................................ 4 2.1.Introduzione ............................................................................................................................... 4 2.2- Ordinamento ............................................................................................................................. 5 2.3. Ordinamento per selezione ....................................................................................................... 6 2.4. Insertion sort ............................................................................................................................. 7 2.5.Mergesort ................................................................................................................................... 8 2.6.Quicksort .................................................................................................................................... 9 Allegato1 ............................................................................................................................................ 12 2 1. Ipotesi di lavoro In base alle Linee Guida ministeriali vigenti si suppone che l’ unità didattica sugli “Algoritmi di ordinamento” venga trattata nella classe 4a di un Istituto Tecnico settore Informatico. Si suppone che la classe sia costituita da 24 studenti. L’unità didattica si articola nei seguenti argomenti: 1. selection sort; 2. insertion sort; 3. mergesort; 4. insertionsort. 1.1 Prerequisiti Concetto di algoritmo; Basi di programmazione nel linguaggio C; Concetto di iterazione, array, funzione complessità computazionale; Paradigma del “divide et impera” e ricorsione; Saper codificare la scansione, il confronto e lo scambio di elementi di un array; 1.2 Obiettivi Classificare gli algoritmi di ordinamento; Comprendere tecniche avanzate di codifica; Scegliere l’algoritmo adeguato alla situazione; Apprendere strategie non banali; 1.3 Metodologie didattiche Nello svolgere l’argomento dell’ordinamento si adotta la metodologia del problem posing, del problem solving e del cooperative learning, in modo da indirizzare i ragazzi verso una tipologia di apprendimento attivo, che li coinvolga, li entusiasmi e li diverta anche; al fine di aumentare la fiducia in se stessi verranno indicati traguardi raggiungibili e di compiti realizzabili. I ragazzi vengono suddivisi in gruppi di 4: inizialmente viene proposto loro il problema di ordinare dei cubetti, dei libri numerati, delle carte da gioco o dei cd, di contare il numero di passi effettuati e confrontare quindi la velocità delle strategie provate dai singoli gruppi per effettuare l’ordinamento; viene chiesto loro di pensare a “come” velocizzare il metodo (brainstorming); in un secondo tempo, vengono presentati gli argomenti dell’unità didattica con l’ausilio di cubetti numerati che aiutino a capire i singoli passi dell’algoritmo, visto che i ragazzi 3 amano poco le trattazioni teoriche; si partirà dall’argomento più semplice, il selection sort, per arrivare a quelli che richiedono la ricorsione, di non facile comprensione; prima ancora di passare a scrivere il codice, vengono invitati a rifare con i cubetti i passi del singolo algoritmo trattato, in modo da verificare che abbiano recepito la strategia; per divertirli (è fondamentale nell’apprendimento quanto l’aspetto motivazionale) si presenterà anche un apposito video al link https://www.youtube.com/user/AlgoRythmics; nel passare a scrivere il codice vengono usate metodologie interattive, quali quelle proposte dai Koans o da Code Hunt, che oltre a stimolare i ragazzi grazie alla suddivisione in livelli di difficoltà crescente, costituiscono strumenti divertenti per apprendere i concetti della programmazione; verranno proposti sistemi on-line di esercizi con valutazione e testing dei programmi sottoposti: http://cms.di.unipi.it/. verrà adottato il linguaggio C; l’apprendimento dei ragazzi sarà valutato in base all’acquisizione dei concetti e del metodo, alla capacità di scrivere correttamente le funzioni (di confronto, di scambio, ricorsive), usare un appropriato linguaggio tecnico, usare con dimestichezza il computer; sarà apprezzata la capacità di essere autonomi, sia nello svolgimento degli esercizi, sia nel riuscire a capire gli errori fatti in fase di compilazione; sarà valutato positivamente l’impegno e l’interesse, esplicitati attraverso l’attenzione e le domande. 2. Algoritmi di Ordinamento 2.1.Introduzione L’importanza dello studio e dell’analisi degli algoritmi si basa sull’evidente constatazione che per risolvere uno stesso problema o per eseguire un medesimo compito esistono in generale svariati procedimenti che si diversificano per comodità, velocità e sicurezza. La necessità di ordinare un insieme di dati viene riscontrata in molte situazioni della vita quotidiana: fornire un elenco di nomi in ordine alfabetico, ordinare per autori dei CD, delle canzoni , dei film e così via. Supponiamo di avere un insieme di oggetti e di voler stabilire se un dato oggetto è presente o meno nell’elenco e quale posizione occupa. Il procedimento più immediato è quello di esaminare uno per uno gli oggetti dell’elenco fino a trovare il dato, o comunque fino ad esaurire il campo di ricerca. Questo è un algoritmo di ricerca sequenziale. Ogni oggetto esaminato ci obbliga a leggere tutto l’elenco quando il nostro oggetto non vi è incluso; laddove è presente richiede la lettura di circa metà elenco in media. Se esiste una relazione di ordine totale la ricerca diventa più rapida: è decisamente più semplice cercare un elemento in un insieme ordinato, anziché in uno disordinato. Ad esempio, se si deve cercare un nominativo su una lista non ordinata si impiega un tempo 4 maggiore rispetto alla ricerca in una rubrica telefonica. Il problema dell’ordinamento, pertanto, riveste una fondamentale importanza anche per il problema della ricerca: i due aspetti sono strettamente connessi. 2.2- Ordinamento Nell’ambito degli algoritmi non numerici fanno parte da leone per vastità di letteratura quelli di ordinamento (sorting) rivolti a disporre una fila di oggetti secondo un certo ordine rispetto a qualche caratteristica; tipicamente si tratta di sistemare numeri in un certo ordine o parole in ordine alfabetico. Ordinare un array significa confrontare tra di loro i suoi elementi ed effettuare opportuni scambi in modo tale da disporne il contenuto in ordine crescente o decrescente a seconda del tipo di ordinamento voluto. Esistono numerose tipologie di algoritmi di ordinamento più o meno complessi e più o meno veloci. Le diverse strategie, generalmente, possono differire anche di molto tra di loro. In prima approssimazione si potrebbe affermare che la «complessità» dell’algoritmo è direttamente proporzionale alla velocità di ordinamento del medesimo. I moderni elaboratori sono così veloci che su piccoli array diventa praticamente ininfluente la scelta del tipo di algoritmo, per cui le prestazioni di un buon sort diventano significativamente apprezzabili solo se questo viene applicato a un vettore con un numero di elementi dell’ordine dei milioni. In ogni caso, visto che le due operazioni fondamentali su cui questi tipi di algoritmi si basano sono il confronto e lo scambio di elementi, risulta abbastanza chiaro che adottando una strategia che minimizzi il numero complessivo di queste si ottiene un ordinamento più veloce. Ovviamente, la velocità del sort dipende anche dalla distribuzione iniziale dei valori nell’array, e questo non è tanto legato al numero di confronti quanto a quello degli scambi che saranno effettuati. Si tenga infatti presente che, in termini di velocità, un’operazione di scambio è più «costosa» di un’operazione di confronto. C’è da precisare che alcuni algoritmi operano direttamente sulla struttura dati che contiene gli elementi da ordinare , mentre altri utilizzano una seconda struttura di “appoggio”. Si definiscono pertanto “in sito” (in place) gli algoritmi che operano direttamente sul vettore originale, mentre si dicono “non in sito”quelli che producono un nuovo vettore ordinato, lasciando inalterato il vettore di origine. I primi consentono una minore occupazione dello spazio di memoria, in quanto non generano vettori duplicati. Analizzeremo i seguenti algoritmi di ordinamento: selection sort, insertiion sort, merge sort e quicksort. I primi due appartengono alla cosiddetta categoria dei “metodi ingenui”perché applicano metodi elementari, mentre gli altri due appartengono alla categoria dei “metodi avanzati”, in quanto 5 consentono di migliorare le prestazioni dei metodi ingenui, a scapito, però, della leggibilità del codice; sono di difficile comprensione e sfruttano tecniche ricorsive. 2.3. Ordinamento per selezione L’ordinamento per selezione, detto selection sort, consiste nell’uso ripetuto di una routine di ricerca del minimo: al passo generico i = 0, 1,…,n-1 viene selezionato l’elemento che occuperà la posizione i della sequenza ordinata; al termine del passo i gli i + 1 elementi selezionati fino a quel momento coincidono con i primi i + 1 elementi della sequenza ordinata, cioè sono parte della soluzione. Ad esempio, per ordinare il vettore [ 21, 34, 18, 9, 14, 41, 3, 38] si dovrà : 1. cercare il più piccolo elemento dell’array 21 34 18 9 14 41 3 38 21 38 2. scambiare l'elemento più piccolo con l'elemento in prima posizione; 3 34 18 9 14 41 3. incrementare l’indice e ricominciare il ciclo: si ripete la stessa operazione nel sottovettore che inizia per 34; si trova il minimo, 9, e lo si scambia con il 34: 3 34 18 9 14 41 21 38 3 9 18 34 14 41 21 38 21 38 4. incrementare l’indice e ripetere il ciclo nel sottovettore che inizia per 18: 3 9 14 34 18 41 5. incrementare l’indice e ripetere il ciclo nel sottovettore che inizia dal 34: 3 9 14 18 34 41 21 38 3 9 14 18 21 41 34 38 3 9 14 18 21 34 41 38 3 9 14 18 21 34 38 41 E così via: 6 Possiamo notare che sono stati effettuati molti scambi necessari solo a liberare lo spazio per il numero trovato, mentre il numero che in origine occupava tale posizione non viene analizzato, ma soltanto spostato. L’algoritmo ha complessità n2, cioè ha complessità quadratica rispetto al numero di elementi da ordinare. Tale complessità è sempre raggiunta a livello di tempo, qualunque sia la sequenza iniziale. 2.4. Insertion sort L’algoritmo per inserimento, relativamente semplice, funziona con lo stesso meccanismo usato da molte persone per ordinare una mano di bridge o ramino: si inizia con la mano sinistra vuota, si prende una carta alla volta e la si inserisce nella corretta posizione dopo averla confrontata con ogni altra carta, da destra a sinistra. I passi sono i seguenti: 1. individuare la posizione che deve occupare rispetto a quelli già presenti; 2. predisporre un posto libero; 3. effettuare l’inserimento. Supponiamo, ad esempio, di voler disporre in un vettore in ordine crescente i seguenti numeri: 21, 34, 18, 9, 14, 41, 3, 38. Il primo valore (21) viene inserito nella prima cella 21 Inserendo il secondo numero, 34, non c’è da effettuare altre operazioni. Quando si va ad inserire il numero 18, invece, occorre scalare i numeri precedenti per lasciare il posto al 18, che dovrà essere posizionato in cima. Avremo quindi i seguenti passi: 21 18 34 21 34 21 34 Per il numero 9 si procede come per il 18: 9 18 21 34 Il 14 va inserito tra il 9 ed il 18, lasciando uno spazio tra i due, il 41 non richiede spostamenti, mentre il 3 richiede lo spostamento di tutti gli altri valori ; infine il 38 va posizionato prima del 41. 9 14 18 21 34 9 14 18 21 34 41 3 9 14 18 21 34 41 3 9 14 18 21 34 38 41 7 L’algoritmo possiamo descriverlo nei seguenti passi: confrontare l’elemento da inserire con quello presente nell’ultima posizione; spostare gli elementi verso destra finchè non si trova la posizione in cui inserire il nuovo elemento (viene creato un “buco” che viene spostato da destra a sinistra); inserire il nuovo elemento in quella posizione- Tale metodo si presta per ordinare numeri non ancora presenti in memoria. Nel caso in cui si volesse ordinare un vettore di numeri già presenti, si dovrà provvedere a prelevare il numero da ordinare dall’ultima posizione, per liberare un “buco” e consentire un eventuale shift a destra; oppure si usa un ordinamento “esterno”usando un vettore nuovo per poter posizionare i numeri ordinati. Nell’Allegato 1 è presentata una versione in place dell’algoritmo. Tale algoritmo risulta efficiente solo nel caso in cui si debba ordinare un piccolo numero di elementi. Anche l’insertion sort è un algoritmo di ordinamento con complessità quadratica rispetto al numero degli elementi da ordinare, come il selection sort, ma si differenzia da quest’ultimo in quanto può richiedere un tempo significativamente inferiore per certe sequenze di elementi; se la sequenza iniziale è già in ordine o non è ordinato solo un numero trascurabile di elementi, l’insetion sort ha complessità dell’ordine di n, mentre la complessità del selection sort rimane sempre quadratica. 2.5.Mergesort Gli algoritmi precedenti, facilmente codificabili, che usano strategie vicine a quelle della vita quotidiana, presentano un’elevata complessità di calcolo risultando, pertanto, poco efficienti nel caso di problemi di dimensioni notevoli. Il mergesort (ed anche il quicksort) risultano più efficienti, ma la strategia adottata e la codifica non sono di facile comprensione. Il merge sort è un algoritmo di ordinamento abbastanza rapido, che utilizza un processo di risoluzione ricorsivo. L'idea alla base del merge sort è il procedimento Divide et Impera, che consiste nella suddivisione del problema in sottoproblemi via via più piccoli. Il merge sort, inventato da John von Neumann nel 1945, concettualmente opera nel seguente modo: 1. la sequenza viene divisa (divide) in due metà (se la sequenza contiene un numero dispari di elementi, viene divisa in due sottosequenze di cui la prima ha un elemento in più della seconda); 2. ognuna di queste sottosequenze viene ordinata, applicando ricorsivamente l'algoritmo (impera); 8 3. le due sottosequenze ordinate vengono fuse (ricombina - merge). Per fare questo, si estrae ripetutamente il minimo delle due sottosequenze e lo si pone nella sequenza in uscita, che risulterà ordinata; Lo pseudocodice è il seguente: Mergesort (a, sinistra, destra): IF (sinistra<destra){ Per implementare l’algoritmo è necessario specificare come fondere le due sotto-sequenze ordinate; il metodo che opera in loco risulta piuttosto complicato; l’uso di un array addizionale, invece, centro=(sinistra+destra):2; Mergesort(a, sinistra, centro); sicuramente meno complicato, si basa sulla stessa strategia che si adotta per fondere due mazzi di carte ordinati in modo crescente: ad ogni passo per stabilire la carta di valore minimo nei due mazzi Mergesort (a, centro+1, destra); Fusione (a, sinistra, centro, basta confrontare le due carte in cima ai mazzi stessi ed inserirla in fondo al nuovo mazzo. destra); Esempio di funzionamento Supponendo di dover ordinare la sequenza [11 3 16 2 1 5 9 0], l'algoritmo procede ricorsivamente dividendola in metà successive, fino ad arrivare alle coppie [11 3] [16 2] [1 5] [9 0] A questo punto si fondono (merge) in maniera ordinata gli elementi, riunendo le metà: [3 11] [2 16] [1 5] [0 9] Al passo successivo, si fondono le coppie di array di due elementi: [2 3 11 16] [0 1 5 9] Infine, fondendo le due sequenze di quattro elementi, si ottiene la sequenza ordinata: [0 1 2 3 5 9 11 16] L'esecuzione ricorsiva all'interno del calcolatore non avviene nell'ordine descritto sopra. Tuttavia, si è formulato l'esempio in questo modo per renderlo più comprensibile. La complessità è dell’ordine di n logn e non dipende dalla disposizione iniziale dei valori negli elementi dell’array. 2.6.Quicksort 9 Quicksort, l’algoritmo di ordinamento per distribuzione (letteralmente “ordinamento rapido”), è un ottimo algoritmo ricorsivo "in place" che si basa sul paradigma "divide et impera" come il merge sort. Si differenzia da quest’ultimo in quanto la fase di decomposizione è più evoluta, mentre quella di ricombinazione è più immediata. I pregi di tale algoritmo sono: facilità di implementazione; è un algoritmo general purpose, che ha un buon comportamento in un’ampia varietà di situazioni ed in molti casi richiede meno risorse di qualsiasi altro algoritmo; è un ordinamento in sito, quindi non richiede vettori temporanei di appoggio; ha una buona efficienza (dipendente dall’elemento scelto come pivot); le sue prestazioni sono le migliori tra quelle degli algoritmi basati sul confronto. Il principio di funzionamento prevede di 1. definire un elemento, chiamato pivot o perno, nel caso che la sequenza abbia almeno due elementi; gli elementi di valore inferiore al valore di tale perno vengono posti a sinistra e gli elementi di valore superiore vengono posti a destra: la posizione di pivot determina due sottovettori (decomposizione); 2. le sotto-sequenze vengono ordinate ricorsivamente nello stesso modo, individuando un nuovo perno, spostando gli elementi e ricercando un nuovo punto di pivot; il procedimento viene ripetuto fino ad ottenere sottovettori di dimensione unitaria (ricorsione); 3. i sottovettori di dimensione unitaria, che risultano ordinati, vengono concatenati (implicitamente) in un’unica sequenza ordinata (ricombinazione). La realizzazione ricosiva è la seguente: Quicksort ( a, sinistra, destra): 0 ≤ sinistra, destra≤ n-1 se (inizio < fine) { scegli pivot nell’intervallo [sinistra…destra]; perno = Distribuzione (a, sinistra, pivot, destra); quicksort (a, sinistra, pivot – 1); quicksort (a, pivot+1, destra); } E’ fondamentale la scelta della partizione per individuare il nuovo punto di pivot. Se si considera il vettore [42, 69, 12, 54, 88, 24, 95, 67] e consideriamo come pivot corrente la posizione 3, il valore della cella 3, cioè 54, sarà il perno: 10 si cercano gli elementi maggiori del perno che si trovano alla sua sinistra; si cercano gli elementi minori del perno (54) che si trovano nella sezione di destra; si esegue lo “scambio a coppie”finchè si individua una posizione tale per cui tutti gli elementi a sinistra sono inferiori a perno e tutti gli elementi a destra sono superiori a perno. Operativamente con due indici scorriamo il vettore da sinistra verso destra e poi da destra verso sinistra, in modo da effettuare le ricerche e gli scambi suddetti. Avremo: [42, 24, 12, 54, 88, 69, 95, 67] Si divide il vettore in due sottovettori: [42, 24, 12, 54] e [88, 69, 95, 67] Prendiamo come valore del perno il 23 nel primo sottovettore ed il 69 nel secondo sottovettore, quindi avremo i seguenti scambi: [12 , 24, 42, 54] e [67, 69, 95, 88 ] Poiché non ci sono altri scambi da fare, si individua i nuovi pivot futuro (le vecchie posizioni 1 e5): [ 12, 24] [42, 54] [ 67, 69] [95, 88] Prendiamo come perno il primo elemento dei quattro minivettori; solo nell’ultimo c’è da fare lo scambio, quindi si avranno 8 vettori costituiti da un solo elemento: tutti gli elementi si trovano nella posizione corretta e l’algoritmo termina: [ 12 ] [ 24 ] [ 42 ] [ 54 ] [ 67 ] [ 69 ] [ 88 ] [ 95 ] [ 12 24 42 54 67 69 88 95] L’algoritmo è tanto più efficiente quanto più l’elemento scelto partiziona perfettamente il vettore: occorrerebbe trovare l’elemento mediano, ma l’algoritmo si complicherebbe. Si potrebbe anche pensare di generare a caso l’elemento perno, ma la scelta più semplice è quella del primo elemento del sottovettore oppure dell’elemento centrale: anche se non viene individuato l’elemento ottimale , tali operazioni non introducono elaborazioni aggiuntive e richiedono solo [N log N] passi. Gli svantaggi sono dati dal fatto che non è stabile; nel caso peggiore (il pivot coincide con l’elemento massimo) ha un comportamento quadratico ed è particolarmente fragile: un semplice errore nella sua implementazione può passare inosservato, ma causare in certe situazioni un drastico peggioramento delle prestazioni. Una possibile implementazione nel linguaggio C è proposta nell’Allegato 1. 11 Allegato1 Esempio1:Selection sort #include <stdio.h> #include <stdlib.h> void selection_sort(int x[], int n) { int min=0; int i=0; int j ; int app; for (j=0; j<n-1; j++) { min=j; for (i=j+1; i<n; i++) { if( x[i]<x[min]) min= i; } app=x[j]; x[j]=x[min]; x[min]=app; } } void stampa_array (int x[], int n) { int i; for (i=0;i<n; i++) { printf("%d ", x[i]); } printf("\n"); } int main() { int a[8]={35,17,28,54,13,19,75,8}; stampa_array(a,8); selection_sort(a,8); stampa_array(a,8); } Esempio 2 : Insertion sort #include <stdio.h> #include <stdlib.h> void insertion_sort(int x[], int n) { int i, j, app; for (i=1; i<n; i++) { app = x[i]; j = i-1; 12 while ((j>=0) && (x[j]>app)) { x[j+1] = x[j]; j--; } x[j+1] = app; } } void stampa_array (int x[], int n) { int i; for (i=0;i<n; i++) { printf("%d ", x[i]); } printf("\n"); } int main() { int a[8]={35,17,28,54,13,19,75,8}; stampa_array(a,8); insertion_sort(a,8); stampa_array(a,8); } Esempio3:Mergesort #include <stdio.h> #include <stdlib.h> void merge(int a[], int left, int center, int right) { int i, j, k; int b[10]; i = left; j = center+1; k = 0; while ((i<=center) && (j<=right)) { if (a[i] <= a[j]) { b[k] = a[i]; i++; } else { b[k] = a[j]; j++;} k++; } while (i<=center) { b[k] = a[i]; i++; k++; } while (j<=right) { b[k] = a[j]; j++; k++; } for (k=left; k<=right; k++) a[k] = b[k-left]; 13 } void merge_sort(int a[], int left, int right) { int center; if (left<right) { center = (left+right)/2; merge_sort(a, left, center); merge_sort(a, center+1, right); merge(a, left, center, right); } } void stampa_array (int x[], int n) { int i; for (i=0;i<n; i++) { printf("%d ", x[i]); } printf("\n"); } int main() { int n=8; int a[8]={35,17,28,54,13,19,75,8}; stampa_array(a,8); merge_sort(a,0,n-1); stampa_array(a,8); } Esempio4: Quicksort #include <stdio.h> #include <stdlib.h> void scambia(int *a, int *b) { int c; c=*a; *a=*b; *b=c; } void quick_sort(int v[],int sin, int des) { int i, j, media; media = (v[sin]+v[des])/2; i=sin; j=des; do { 14 while(v[i]<media) i = i + 1; while(media<v[j]) j = j - 1; if(i<=j) { scambia(&v[i], &v[j]); i = i + 1; j = j - 1; } } while (j>=i); if (sin<j) quick_sort(v,sin, j); if (i<des) quick_sort(v,i, des); } void stampa_array (int x[], int n) { int i; for (i=0;i<n; i++) { printf("%d ", x[i]); } printf("\n"); } int main() { int n=8; int a[8]={35,17,28,54,13,19,75,8}; stampa_array(a,8); quick_sort(a,0,n-1); stampa_array(a,8); } 15