PROGRAMMAZIONE AVANZATA JAVA E C Massimiliano Redolfi Lezione 5: Algoritmi di ordinamento PAJC Algoritmi di ordinamento prof. Massimiliano Redolfi – PAJC 2 PAJC Algoritmi: ordinamento Orinamento: disporre una sequenza di informazioni in ordine crescente o decrescente. La definizione è semplice ed intuitiva, vedremo tuttavia che realizzare un algoritmo di ordinamento efficiente ci porterà a considerare molte strade alternative via via più interessanti e sofisticate. Nota: la libreria standard mette a disposizione una funzione qsort per l’ordinamento che è utile nella maggior parte dei casi… oggi noi vogliamo capire come arrivare a realizzare qsort! utilizzarla è banale (o quasi). prof. Massimiliano Redolfi – PAJC 3 PAJC Algoritmi: ordinamento Gli algoritmi che analizzeremo si concentrano sull’ordinamento di oggetti ad acesso casuale quali array o file ad accesso diretto. Il valore, detto chiave dell’ordinamento, rispetto a cui si effettua l’ordinamento è solitamente composto da una parte delle informazioni di cui sono composti gli oggetti dell’array. La chiave è quella parte di informazioni che determina la posizione relativa degli elementi (es: data una rubrica la chiave di ordinamento potrebbe essere il cognome della persona, oppure il numero di telefono e così via) Quindi dato un insieme di dati questi possono anche essere ordinati secondo chiavi diverse. prof. Massimiliano Redolfi – PAJC 4 PAJC Ordinamento: metodi Esistono tre algoritmi generali per ordinare un array: • scambio • selezione • inserimento Per comprendere i tre metodi si pensi ad un mazzo di carte che si vuole ordinare… Scambio Si devono disporre tutte le carte allineate sul tavolo e quindi scambiare tra loro le carte non ordinate, procedendo sino ad ordinare l’intero mazzo. prof. Massimiliano Redolfi – PAJC 5 PAJC Ordinamento: metodi Selezione Si spargono le carte sul tavolo poi si seleziona la carta con il valore più basso, la si raccoglie e la si tiene in mano. Quindi, dalle carte che rimangono sul tavolo, si seleziona la carta più bassa, la si raccoglie e la si posizione dietro quella già selezionata. Si procede in tal modo sino a quando tutte le carte non sono state raccolte dal tavolo, a quel punto in mano si hanno le carte ordinate. Inserimento Si tengono tutte le carte in mano, poi si posa una carta alla volta inserendola nella posizione corretta. Quando non si hanno più carte in mano sul tavolo si ha il mazzo ordinato. prof. Massimiliano Redolfi – PAJC 6 PAJC Ordinamento: valutazione degli algorimti Come scegliere un algoritmo? Dobbiamo definire dei criteri di valutazione: • velocità di ordinamento nel caso generale • prestazioni (velocità/memoria occupata/…) nei casi più o meno favorevoli • comportamento naturale dell’algoritmo • comportamento nel caso di elementi con la stessa chiave prof. Massimiliano Redolfi – PAJC 7 PAJC Ordinamento: valutazione degli algorimti Velocità: evidentemente è fondamentale che un algoritmo sia veloce, ed è altrettanto chiaro che il tempo di elaborazione sarà proporzionale al numero di elementi da ordinare. La velocità è strettamente legata al numero di confronti e scambi che l’algoritmo deve fare, ricordando che le operazioni di scambio richiedono più tempo. Vedremo che a seconda dell’algoritmo scelto al crescere del numero di elementi da ordinare i tempi di elaborazione possono crescere in modo drammatico (esponenziale) oppure rimanere contenuti (logaritmico) prof. Massimiliano Redolfi – PAJC 8 PAJC Ordinamento: valutazione degli algorimti Casi limite: i tempi di elaborazione nei casi limite sono importanti per valutare il comportamento nelle condizioni estreme dell’algoritmo, soprattutto se si ipotizza che tali situazioni si presentino con una certa frequenza. Comportamento naturale: un algoritmo si comporta in modo naturale se interviene in modo maggiore su un array non ordinato rispetto a quanto faccia su un array quasi ordinato. In tal caso le prestazioni peggiori dell’algoritmo corrispondono al caso in cui l’array è ordinato al contrario. prof. Massimiliano Redolfi – PAJC 9 PAJC Ordinamento: valutazione degli algorimti Chiavi identiche: spesso nell’ordinamento vi è una chiave primaria ed una secondaria da utilizzare nel caso esistano chiavi primarie identiche (es: chiave primaria: cognome, chiave secondari: nome). Ora per comprendere l’importanza del comportamento dell’algoritmo in questi casi si pensi di avere un elenco di cognomi/nomi ordinato e di inserire un nuovo elemento. La lista a questo punto dovrà essere riordinata, ma chiaramente non è necessario riordinare le chiavi secondarie… cioè non serve che l’algoritmo scambi gli elementi che hanno la stessa chiave primaria… OK! iniziamo ad analizzare alcuni algoritmi di ordinamento, partendo dai più semplici! prof. Massimiliano Redolfi – PAJC 10 PAJC Ordiamento Bubble sort prof. Massimiliano Redolfi – PAJC 11 PAJC Bubble sort E’ l’algoritmo di ordinamento più noto (per il nome semplice e simpatico) ma anche uno dei peggiori!! Ma visto che è facile da capire è utile analizzarlo… Bubble sort appartiene alla classe degli algoritmi di scambio. Il concetto su cui si sviluppa l’algoritmo è: confrontare due elementi adiacenti e scambiarli se necessario, ripetendo l’operazione sino a che l’array non è ordinato. Il concetto di bolla sta in questo: ogni elemento da ordinare è una sorta di bolla a se stante che si scontra con le vicine alla ricerca del proprio posto. prof. Massimiliano Redolfi – PAJC 12 PAJC Bubble sort: versione molto semplice dell’algoritmo void bubble(char *items, int count) Ordinamento di un array di caratteri (una stringa) contenente count elementi { int a, b; char t; for(a=1; a < count; a++) for(b=count-1; b >= a; b--) { L’algoritmo si compone di due cicli: -il ciclo esterno (ripetuto per count-1 volte) assicura che ogni elemento si trovi nella posizione corretta anche nel caso peggiore if(items[b-1] > items[b]) { t = items[b-1]; items[b-1] = items[b]; items[b] = t; } - il ciclo interno esegue gli scambi ed i confronti } } vediamo come funziona… prof. Massimiliano Redolfi – PAJC 13 PAJC Bubble sort: ordiniamo una stringa immessa dall’utente int main(void) { char s[255]; printf(“Inserisci una testo: “); gets(s); bubble(s, strlen(s)); printf(“\nIl testo ordinato è: %s\n”, s); Inserimano la stringa: dcab I passi che vengono eseguiti sono: 0. 1. 2. 3. dcab adcb abdc abcd return 0; } vediamo bubble.prj prof. Massimiliano Redolfi – PAJC 14 PAJC Bubble sort: ordiniamo una stringa immessa dall’utente void bubble(char *items, int count) Inserimano la stringa: dcab { I passi che vengono eseguiti sono: int a, b; char t; for(a=1; a < count; a++) for(b=count-1; b >= a; b--) { if(items[b-1] > items[b]) { t = items[b-1]; items[b-1] = items[b]; items[b] = t; } } 0. 1. 2. 3. dcab adcb abdc abcd In pratica mentre il ciclo interno ordina gli elementi il ciclo esterno ripete l’ordinamento count-1 volte in modo da essere certi di aver effettuato tutti i confronti Effettivamente si potrebbe terminare non appena nel ciclo interno non si effettuano scambi… } prof. Massimiliano Redolfi – PAJC 15 PAJC Bubble sort Ma quanti confronti e quanti scambi richiede l’algoritmo? Situazione Confronti peggiore migliore media Scambi ordine di n2 (n2 – n) / 2 0 ordine di n2 Bubble sort è quindi un algoritmo n2 poiché il tempo di esecuzione è proporzionale al quadrato del numero di elementi da confrontare. Il tempo di esecuzione cresce quindi in modo esponenziale al crescere del numero di elementi ➔ è un algoritmo MOLTO INEFFICIENTE prof. Massimiliano Redolfi – PAJC 16 PAJC Tempo di esecuzione Bubble sort Numero di elementi prof. Massimiliano Redolfi – PAJC 17 PAJC Bubble sort Si può notare come la lettere a passi dalla terza alla prima posizione in un solo passaggio mentre la d impieghi diversi cicli. Questa è una caratterisctica generale di Bubble sort: un elemento fuori posizione che al termine occuperà la prima posizione andrà nella posizione corretta al primo passaggio mentre un elemento fuori posizione che al termine occuperà la parte finale dell’elenco raggiungerà la propria posizione più lentamente. Idea! perché non scambiare la testa con la coda ad ogni ciclo? invece di leggere l’array sempre nella stessa direzione possiamo alternare la direzione di lettura ➔ algoritmo Shaker sort (ha comunque tempi dell’ordine di n2) prof. Massimiliano Redolfi – PAJC 18 PAJC Bubble sort void bubble(char *items, int count) { Numero di confronti: (n2 – n / 2) // dichiarazioni for(a=1; a < count; a++) for(b=count-1; b >= a; b--) // confronta e scambia Come contare il numero di operazioni? } Partiamo dal ciclo interno, questo viene eseguito per count-1 volte alla prima iterazione quindi per n-2 volte e così sino ad a=count-1 per cui viene eseguito 1 volta. Quindi (posto n = count) op = (n-1) + (n-2) + … + (n-(n-1)) = (n * (n-1)) / 2 = (n2 – n) / 2 prof. Massimiliano Redolfi – PAJC 19 PAJC Ordiamento Selezione prof. Massimiliano Redolfi – PAJC 20 PAJC Ordinamento per Selezione Selezione Si spargono le carte sul tavolo poi si seleziona la carta con il valore più basso, la si raccoglie e la si tiene in mano. Quindi, dalle carte che rimangono sul tavolo, si seleziona la carta più bassa, la si raccoglie e la si posizione dietro quella già selezionata. Si procede in tal modo sino a quando tutte le carte non sono state raccolte dal tavolo, a quel punto in mano si hanno le carte ordinate. prof. Massimiliano Redolfi – PAJC 21 PAJC Ordinamento per Selezione In altri termini un algoritmo di selezione seleziona l’elemento con la chiave più basso in un array e lo scambia con il primo elemento, quindi fra gli n-1 elementi rimanenti trova l’elemento più basso e lo scambia con il secondo e così via. Lo scambio prosegue sino agli ultimi due elementi Supponiamo di applicare il metodo all’array DCAB avremo: - situazione iniziale: DCAB - passo 1: ACBD - passo 2: ABDC - passo 3: ABCD prof. Massimiliano Redolfi – PAJC 22 PAJC void select_sort(char *items, int count) { int a, b, c; Ordinamento per Selezione char t; for(a=0; a < count-1; a++) { c = a; a e b sono gli indici che utilizzo per scorrere l’array, c indica la posizione dell’elemento più basso trovato durante la ricerca t = items[a]; for(b=a+1; b <count; b++) { if(items[b] < t) { t = items[b]; c = b; } // if } // for b if(c != a) { // è stato trovato un el < di a items[c] = items[a]; items[a] = t; L’algoritmo si compone di due cicli: -il ciclo esterno ripetuto per count-1 volte per confrontare ogni elemento dell’array - il ciclo interno ricerca tra gli elementi restanti se c’è un elemento con chiave più bassa dell’elemento a anche in questo caso il numero di confronti è pari a (n2 – n) / 2. Troppi!! } } // for a } prof. Massimiliano Redolfi – PAJC 23 PAJC Ordiamento Inserimento prof. Massimiliano Redolfi – PAJC 24 PAJC Ordinamento per Inserimento Inserimento Si tengono tutte le carte in mano, poi si posa una carta alla volta inserendola nella posizione corretta. Quando non si hanno più carte in mano sul tavolo si ha il mazzo ordinato. L’algoritmo ordina per prima cosa i primi due elementi quindi inserisce il terzo nella posizione corretta rispetto ai primi due e così via. Supponiamo di applicare il metodo all’array DCAB avremo: - situazione iniziale: DCAB - passo 1: CDAB - passo 2: ACDB - passo 3: ABCD prof. Massimiliano Redolfi – PAJC 25 PAJC void insert_sort(char *items, int count) { int a, b; char t; for(a=0; a < count; a++) { Ordinamento per Inserimento a e b sono gli indici che utilizzo per scorrere l’array t = items[a]; for(b=a-1; b >= 0; b--) { { if(t < items[b]) break; items[b+1] = items[b]; } items[b+1] = t; } // for b } // for a } prof. Massimiliano Redolfi – PAJC L’algoritmo si compone di due cicli: -il ciclo esterno tocca tutti gli elemtni - il ciclo interno verifica dove inserire il nuovo elemento, effettivamente sposta tutti gli elementi di una posizione in avanti in modo da creare un ‘buco’ dove inserire il nuovo elemento Se l’elenco è già ordinato abbiamo n-1 confronti altrimenti siamo nell’ordine di n2 26 PAJC Ordinamento per Inserimento Si noti quindi come l’algoritmo di inserimento si comporti in modo diverso dai precedenti offrendo prestazioni in media migliori (anche se di poco). L’algoritmo presenta inoltre altri due vantaggi: - si comporta in modo naturale: manipola di meno gli array già ordinati, quindi risulta ottimo per elenchi già parzialmente ordinati - non scambia gli oggetti che hanno lo stesso valore nella chiave, ideale per inserire nuovi valori in una lista ordinata Si noti tuttavia che un’operazione di inserimento implica lo spostamento dell’intero array precedentemente ordinato e questa è un’operazione particolarmente pesante in termini computazionali… prof. Massimiliano Redolfi – PAJC 27 PAJC Ordiamento algoritmi avanzati prof. Massimiliano Redolfi – PAJC 28 PAJC Algoritmi avanzati Gli algoritmi precedenti hanno un forte svantaggio: il loro tempo di esecuzione è dell’ordine di n2 e questo ne limita l’uso a casi molto semplici. Come migliorare gli algoritmi per farli andare in modo più veloce? scrivere gli algoritmi in assembler in modo da ottimizzare a livello di codice macchina le operazioni eseguite studiare algoritmi intrinsecamente più efficienti prof. Massimiliano Redolfi – PAJC 29 PAJC Algoritmi avanzati scrivere gli algoritmi in assembler in modo da ottimizzare a livello di codice macchina le operazioni eseguite E’ una strada che in genere permette di migliorare di un certo fattore l’efficienza di un algoritmo ma se l’algoritmo di partenza è inefficienti anche i risultati dell’ottimizzazione saranno modesti. In definitiva riscrivere in assembler permette di eseguire le operazioni in modo più veloce ma le operazioni devono comunque essere eseguite. prof. Massimiliano Redolfi – PAJC 30 PAJC Algoritmi avanzati studiare algoritmi intrinsecamente più efficienti Se invece si riesce a riprogettare l’algoritmo trovando alternative che riducono il numero di operazioni eseguite ecco che il sistema complessivo sarà più veloce indipendentemente dal linguaggio scelto per implementare l’algoritmo stesso. Nel caso dell’ordinamento abbiamo due algoritmi particolarmente interessanti: shell sort e quick sort prof. Massimiliano Redolfi – PAJC 31 PAJC Ordiamento shell sort prof. Massimiliano Redolfi – PAJC 32 PAJC Shell sort Il nome deriva dal suo ideatore D.L. Shell anche se spesso si associa il nome shell (conchiglia) a come viene rappresentato visivamente il funzionamento dell’algoritmo. Il punto di partenza è l’algoritmo di inserimento, l’obiettivo è ridurre gli incrementi, contenere il numero di spostamenti di blocchi dell’array. Per arrivare a tale risultato l’idea di base è: invece di iniziare a confrontare direttamente elementi contigui confrontiamo inizialmente elementi lontani, separati da un certo intervallo (gap) e quindi riduciamo man mano il gap stesso fino al confronto finale. prof. Massimiliano Redolfi – PAJC 33 PAJC Shell sort Supponiamo di voler ordinare l’array: F D A C B E (gap = 3, 2, 1) Passo 1 F D A C B E Passo 2 C B A F D E Passo 3 A B C E D F Result A B C D E F prof. Massimiliano Redolfi – PAJC 34 PAJC Shell sort void shell_sort(char *items, int count) { int i, j, k, gap; char c, a[5] = {9,5,3,2,1}; for(k=0; k < 5; k++) { gap = a[k]; for(i = gap; i<count; i++) { Si noti come l’algoritmo è riconducibile all’algoritmo di inserimento, in questo caso però non si agisce direttamente su elementi contigui ma su elementi separati da un certo gap x = items[i]; for(j = i-gap; j >= 0; j-=gap) { if(x < items[j]) break; items[j+gap] = items[j]; } // for j items[j+gap] = x; } // for i } // for k } Il tempo di esecuzione è proporzionale a n1.2 prof. Massimiliano Redolfi – PAJC 35 PAJC Efficienza degli algoritmi shell sort prof. Massimiliano Redolfi – PAJC 36 PAJC Ordiamento quick sort prof. Massimiliano Redolfi – PAJC 37 PAJC Quick sort L’algoritmo quick sort progettato da C.A.R. Hoare è in genere considerato il miglior algoritmo di ordinamento disponibile. Quick sort è una derivazione del sistema di ordinamento a scambio. L’idea base per migliorare il semplice algoritmo a scambio è il concetto di partizione. prof. Massimiliano Redolfi – PAJC 38 PAJC Quick sort Il concetto è semplice: dato un array da ordinare si sceglie un valore della chiave (detto comparando) e si suddividono gli elementi dell’array in due insiemi (o sezioni). Tutti gli elementi maggiori od uguali andranno in un insieme e tutti quelli minori nell’altro. Ripetendo il processo per ogni insieme si arriva all’ordinamento dell’intero array. prof. Massimiliano Redolfi – PAJC 39 PAJC Quick sort Esempio: F E D A C B comparando = d B C A D E F comparando = c A comparando = e C B D E F comparando = c A B prof. Massimiliano Redolfi – PAJC C comparando = c D E F 40 PAJC Quick sort Esempio: F E D A C B comparando = d B C A D E F comparando = c A comparando = e C B D E F comparando = c A B C comparando = c D E F prof. Massimiliano Redolfi – PAJC 41 PAJC Quick sort Si evidenziano immediatamente due caratteristiche dell’algoritmo: 1. la natura ripetitiva, ricorsiva dell’algoritmo di ordinamento 2. la necessità di stabilire un metodo per identificare il comparando prof. Massimiliano Redolfi – PAJC 42 PAJC Quick sort: comparando Per quanto concerne il comparando la scelta è particolarmente importante in quanto, in caso di una scelta non opportuna (ad esempio si sceglie sempre il valore massimo della partizione) allora l’algoritmo degenera e raggiunge tempi di esecuzione dell’ordine di n2 In genere la scelta del metodo da utilizzare per determinare il comparando è funzione dei dati, solitamente si adottano due approcci: - utilizzare valori medi di ogni sezione (utile quando gli elementi sono distribuiti in modo uniforme) - scegliere in modo casuale il valore (utile quando i valori sono simili o molto vicini tra loro, oppure non si hanno informazioni prestabilite sugli stessi) prof. Massimiliano Redolfi – PAJC 43 PAJC Ricorsione La ricorsione è un processo che definisce qualcosa in termini di se stesso. In C una funzione può richiamare se stessa, quando un’istruzione all’interno del corpo di una funzione richiama la funzione stessa tale funzione viene detta ricorsiva. Un semplice esempio di funzione ricorsiva è dato dalla funzione fattoriale che dato un intero ne calcola il fattoriale: n ! n! = n * (n-1) * (n-2) * … * 2 * 1 prof. Massimiliano Redolfi – PAJC 44 PAJC Ricorsione: fact /* VERSIONE STANDARD */ int fact(int n) { int answer = 1; for(int i = 1; i<=n; i++) answer *= i; return answer; } /* VERSIONE RICORSIVA */ int factr(int n) { if(n==1) return 1; // uscita dalla ricorsione int answer = n * factr(n-1); // chiamata ricorsiva return answer; } prof. Massimiliano Redolfi – PAJC 45 PAJC Quick sort Vediamo ora l’algoritmo di quick sort… prof. Massimiliano Redolfi – PAJC 46 PAJC Quick sort void qs(char *items, int left, int right) { int i = left, j = right; char x, y; Si noti che ora devo definire gli estremi dell’intervallo su cui opera l’algoritmo x = items[(i+j) / 2]; do { scelta del comparando while((items[i] < x) && (i < right)) i++); while((items[j] > x) && (j > left)) j--); sezionamento dell’array if(i<=j) { y = items[i]; items[i] = items[j]; items[j] = y; i++; j--; ricorsione } } while (i <= j); if(left < j) qs(items, left, j); if(right > i) qs(items, i, right); Il tempo di esecuzione in genere è proporzionale a n log n } prof. Massimiliano Redolfi – PAJC 47 PAJC Quick sort Si noti che per mantenere la compatibilità con le interfaccie definite dagli altri algoritmi possiamo definire la funzione quick_sort nel seguente modo: void quick_sort(char *items, int count) { qs(items, 0, count-1); } prof. Massimiliano Redolfi – PAJC 48 PAJC Quale algoritmo scegliere?? prof. Massimiliano Redolfi – PAJC 49 PAJC Quick sort Sebbene l’algoritmo quick sort sia l’ottimale nella maggior parte dei casi esistono situazioni in cui non rappresenta la scelta ottimale. Ad esempio avendo pochi valori da ordinare l’overload dovuto alle chiamate ricorsive può determinare il degrado delle prestazioni ragione per la quale quando si ha un insieme ridotto di elementi si utilizzano algoritmi diversi (come shell sort). In genere anche nel caso di elementi già parzialmente ordinati quick sort potrebbe non essere la soluzione ideale. In sintesi: quick sort è il miglior algoritmo di carattere generale, ciò non esclude che in talune situazioni possa convenire utilizzare alte tecniche. prof. Massimiliano Redolfi – PAJC 50