Algoritmi di ordinamento in C Giovanni Cintolo ([email protected]) 15 luglio 2007 - versione 1 Per semplicità in questi algoritmi ci riferiremo solamente all'ordinamento di un array di numeri interi. Ovviamente, gli algoritmi elencati potranno essere usati per ordinare dati di un qualsiasi tipo su cui sia denita una relazione d'ordine. 1 Merge sort 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 : si suddivide il problema in sottoproblemi via via più piccoli, sino a che questi ultimi diventino di semplice risoluzione e poi si combinano le soluzioni al ne di risolvere il problema dato. Il merge sort opera quindi dividendo l'insieme da ordinare in due metà e procedendo all'ordinamento delle medesime ricorsivamente. Quando si sono divise tutte le metà si procede alla loro fusione (merge appunto) costruendo un insieme ordinato. 1.1 Simulazione 1 Presa 1 da http://en.wikipedia.org/wiki/merge_sort. 1 1.2 Spiegazione Esaminiamo il caso dell'ordinamento crescente. Consente di dividere l'array in tanti sottoarray, per poi operarci in ognuno tramite la funzione merge. Funzione mergesort In input vengono forniti due sottoarray ordinati. Il compito di questa funzione è ordinarli tra di loro. Funzione merge • Si crea un vettore di appoggio (buf ) di dimensione DIM (è una costante simbolica, che rappresenta la dimensione del vettore di partenza) dove posizioneremo gli elementi in ordine corretto, una variabile primo che punterà all'indice del primo sottoarray (v[iniziale..med] ), cioè iniziale, e una variabile secondo che punterà all'indice del secondo sottoarray (v[med + 1..nale] ), cioè med + 1. L'utilità della variabile appoggio è di indicare in quale indice del vettore buf posizionare il valore. • Il ciclo while permette di scorrere i due sottoarray, prenderne il minore e salvarlo nel vettore buf. Se l'elemento viene preso in v[primo], bisognerà incrementare solo primo, viceversa per v[secondo]. In ogni caso bisogna incrementare anche appoggio, visto che abbiamo appena inserito l'elemento minore in buf[appoggio]. • Alla ne del ciclo while ci saranno due casi: 1. E' nito prima il primo sottoarray. In questo caso non bisogna fare niente, poiché signica che nella porzione v[secondo..nale] gli elementi sono già nella loro giusta collocazione. La prima porzione ordinata sarà invece nel vettore buf[iniziale..appoggio - 1] e verrà copiata in v tramite il ciclo for. 2. E' nito prima il secondo sottoarray, quindi si copiano da v in v stesso tutti gli elementi, no a med, cominciando dal fondo. A questo punto risulterà ordinata solo la porzione nale di v, mentre la prima parte ordinata sarà presente nel vettore buf[iniziale..appoggio - 1], e, grazie ad un ciclo for, la si copia in v. 1.3 Algoritmo Ordinamento crescente void merge(int v[], int iniziale, int med, int finale){ int buf[DIM]; int primo, secondo, appoggio, i; primo = iniziale; secondo = med + 1; 2 } appoggio = iniziale; while(primo <= med && secondo <= finale){ if(v[primo] <= v[secondo]){ buf[appoggio] = v[primo]; primo++; } else{ buf[appoggio] = v[secondo]; secondo++; } appoggio++; } if(secondo > finale) for(i = med; i >= primo; i--) { v[finale] = v[i]; finale--; } for(i = iniziale; i < appoggio; i++) v[i] = buf[i]; void mergesort(int v[], int iniziale, int finale){ int med; } if(iniziale < finale){ med = (iniziale + finale) / 2; mergesort(v, iniziale, med); mergesort(v, med + 1, finale); merge(v, iniziale, med, finale); } L'unica dierenza sta nel cambiare l'operatore di confronto tra v[primo] e v[secondo]. Ordinamento decrescente void merge(int v[], int iniziale, int med, int finale){ int buf[DIM]; int primo, secondo, appoggio, i; primo = iniziale; secondo = med + 1; appoggio = iniziale; while(primo <= med && secondo <= finale){ if(v[primo] >= v[secondo]){ buf[appoggio] = v[primo]; primo++; 3 } else{ buf[appoggio] = v[secondo]; secondo++; } appoggio++; } } if(secondo > finale) for(i= med; i>=primo; i--) { v[finale] = v[i]; finale--; } for(i = iniziale; i < appoggio; i++) v[i] = buf[i]; void mergesort(int v[], int iniziale, int finale){ int med; } if(iniziale < finale){ med = (iniziale + finale) / 2; mergesort(v, iniziale, med); mergesort(v, med + 1, finale); merge(v, iniziale, med, finale); } 2 Insertion sort L'insertion sort, in italiano ordinamento a inserimento, non è molto diverso dal modo in cui un essere umano, spesso, ordina un mazzo di carte. Partendo con la mano sinistra vuota e le carte sul tavolo, se ne preleva una alla volta e la si inserisce in posizione corretta sulla mano sinistra. 2.1 Spiegazione Verrà esaminato il caso dell'ordinamento crescente. • Si utilizzano tre variabili: ind_sist che tiene traccia dell'indice del prossi- mo elemento da sistemare, ind_contr che memorizza l'indice dell'elemento da controllare e val_sist, in cui viene salvato il valore dell'elemento da sistemare. • Inizialmente ind_contr punta all'indice 0 e ind_sist al successivo, mentre val_sist memorizza il valore dell'elemento di indice ind_sist del vettore. Se il secondo elemento è minore del primo i due vengono scambiati grazie al 4 ciclo while e al successivo assegnamento v[ind_contr + 1] = val_sist. Nel caso i due elementi siano già ordinati, l'assegnamento appena citato viene comunque eettuato, ma senza modicare l'array, visto che l'elemento del vettore di indice ind_contr + 1 sarà uguale a val_sist. • Il compito del ciclo while è quello di posizionare il valore che dobbiamo sistemare (val_sist ) all'interno dell'array. Finché non si trova la posizione corretta bisognerà spostare, un posto alla volta, gli elementi che sono maggiori di val_sist. • Ad ogni iterazione del ciclo for manteniamo ordinato il sottoarray [0..ind_sist - 1], per poi inserirvi ogni elemento nella sua collocazione appropriata. 2.2 Algoritmo Ordinamento crescente void insertionsort(int v[], int dim){ int ind_sist, ind_contr, val_sist; } for(ind_sist = 1; ind_sist < dim; ind_sist++){ val_sist = v[ind_sist]; ind_contr = ind_sist - 1; while(ind_contr >= 0 && v[ind_contr] > val_sist){ v[ind_contr + 1] = v[ind_contr]; ind_contr--; } v[ind_contr + 1] = val_sist; } Ordinamento decrescente void insertionsort(int v[], int dim){ int ind_sist, ind_contr, val_sist; } for(ind_sist = 1; ind_sist < dim; ind_sist++){ val_sist = v[ind_sist]; ind_contr = ind_sist - 1; while(ind_contr >= 0 && v[ind_contr] < val_sist){ v[ind_contr + 1] = v[ind_contr]; ind_contr--; } v[ind_contr + 1] = val_sist; } 5 3 Selection sort L'ordinamento per selezione (selection sort) è un algoritmo di ordinamento che opera in modo simile all'ordinamento per inserzione; seleziona il numero minore nella sequenza di partenza e lo sposta nella sequenza ordinata. 3.1 Spiegazione Esaminiamo il caso dell'ordinamento crescente. • Si scorre il vettore dal basso verso l'alto. • Si cerca l'indice del più piccolo elemento dell'array. • Nella prima iterazione si scambia l'elemento più piccolo con l'elemento in prima posizione, nella seconda quello più piccolo con l'elemento in seconda posizione e così via. Quando l'elemento di indice i dovrà essere scambiato con l'elemento di indice i_min sarà inutile eettuare questa operazione se i è uguale a i_min. • Alla ne di ogni iterazione la porzione di array tra gli indici 0 e i è ordinata e quindi dovremo operare solo nella porzione tra i + 1 e dim - 1. 3.2 Algoritmo 3.2.1 Versione iterativa Ordinamento crescente Si mantiene ordinata la parte bassa dell'array. void selectionSort(int v[], int dim){ int i, j, i_min, temp; } for(i = 0; i < dim - 1; i++){ i_min = i; for(j = i + 1; j < dim; j++) if(v[j] < v[i_min]) i_min = j; if(i != i_min){ temp = v[i]; v[i] = v[i_min]; v[i_min] = temp; } } 6 Ordinamento decrescente Si mantiene ordinata la parte bassa dell'array. void selectionSort(int v[], int dim){ int i, j, i_max, temp; } 3.2.2 for(i = 0; i < dim - 1; i++){ i_max = i; for(j = i + 1; j < dim; j++) if(v[j] > v[i_max]) i_max = j; if(i != i_max){ temp = v[i]; v[i] = v[i_max]; v[i_max] = temp; } } Versione ricorsiva Ordinamento crescente Si mantiene ordinata la parte alta dell'array. void selectionsort(int v[], int dim){ int i = dim - 1, j, i_max, temp; } if(dim > 0){ i_max = i; for(j = i - 1; j >= 0; j--) if(v[j] > v[i_max]) i_max = j; if(i != i_max){ temp = v[i]; v[i] = v[i_max]; v[i_max] = temp; } selectionsort(v, dim - 1); } 4 Bubblesort Il bubblesort (letteralmente: ordinamento a bolle) è un semplice algoritmo iterativo di ordinamento, anche se non molto eciente. Il nome dell'algoritmo è dovuto al fatto che, durante l'applicazione del procedimento, i valori vengono spostati all'interno dell'array con una dinamica che ricorda il movimento delle bollicine in un bicchiere di champagne. In particolare, 7 alcuni elementi attraversano l'array velocemente (come bollicine che emergono dal fondo del bicchiere), altri più lentamente (a dierenza di quanto avviene nel caso del bicchiere di champagne, tuttavia, alcuni elementi salgono ma altri scendono). Ogni passaggio prevede che gli elementi dell'array siano confrontati a due a due, procedendo in un verso stabilito a propria scelta. Quindi nel caso si voglia ordinare in senso crescente l'array si hanno due possibilità: spostare l'elemento più grande verso l'alto o spostare il più piccolo verso il basso. 4.1 Spiegazione Esaminiamo il caso dell'ordinamento crescente, spostando l'elemento più grande verso l'alto (algoritmo 4.2.2). • Saranno confrontati il primo e il secondo elemento, poi il secondo e il terzo, poi il terzo e il quarto e così via no al confronto fra penultimo e ultimo elemento. • Per ogni confronto, se i due elementi adiacenti confrontati non sono ordi- nati, vengono scambiati. • Durante ogni iterazione l'elemento più grande raggiunge la sua collo- cazione denitiva. Quindi, alla prima iterazione, il maggiore risiederà nell'ultima posizione dell'array. • Se in un passaggio non viene eettuato nessuno spostamento di elementi, signica che l'array è ordinato. 4.2 Algoritmo Nei casi in cui non viene proposto anche l'ordinamento decrescente, basterà semplicemente cambiare il confronto tra i due elementi adiacenti. 4.2.1 Versione iterativa Ordinamento crescente verso il basso. Ad ogni passaggio, l'elemento più piccolo si muove void bubblesort(int v[], int dim){ int i = 0, j, ordinato; int temp; do{ ordinato = 1; for(j = dim - 1; j > i; j--) if(v[j - 1] > v[j]){ temp = v[j]; v[j] = v[j - 1]; 8 v[j - 1] = temp; ordinato = 0; } } i++; }while(i < dim - 1 && !ordinato); Ordinamento decrescente verso il basso. Ad ogni passaggio, l'elemento più grande si muove void bubblesort(int v[], int dim){ int i = 0, j, ordinato; int temp; } 4.2.2 do{ ordinato = 1; for(j = dim - 1; j > i; j--) if(v[j - 1] < v[j]){ temp = v[j]; v[j] = v[j - 1]; v[j - 1] = temp; ordinato = 0; } i++; }while(i < dim - 1 && !ordinato); Versione iterativa - Altra soluzione L'algoritmo precedente lo si può scrivere più semplicemente con due for. In questo caso però l'elemento più grande si sposta verso l'alto. La condizione di terminazione del secondo for sarebbe anche potuta essere j < dim - 1. Ci si accorge però che una volta spostato l'elemento maggiore nella parte alta dell'array, possiamo operare in un suo sottoarray. Sarebbe, quindi, inutile eettuare ulteriori confronti nella parte superiore dell'array, che risulta essere già ordinata. Ordinamento crescente void bubblesort(int v[], int dim){ int i, j, ordinato = 0; int temp; for(i = 1; i < dim && !ordinato; i++){ ordinato = 1; for(j = 0; j < dim - i; j++){ if(v[j] > v[j + 1]){ temp = v[j]; v[j] = v[j + 1]; 9 } 4.2.3 } } } v[j + 1] = temp; ordinato = 0; Versione ricorsiva Ad ogni passaggio, l'elemento più grande si muove verso l'alto. Come nella soluzione precedente, quella iterativa, una volta che abbiamo spostato l'elemento più grande nell'ultima posizione dell'array, possiamo operare su un suo sottoarray, visto che la parte nale risulterà ordinata. . Ordinamento crescente void bubblesort(int v[], int dim){ int j, ordinato = 0; int temp; } if(dim > 1){ ordinato = 1; for(j = 1; j < dim; j++){ if(v[j - 1] > v[j]){ temp = v[j]; v[j] = v[j - 1]; v[j - 1] = temp; ordinato = 0; } } if(!ordinato) bubblesort(v, dim - 1); } 5 Counting sort Il counting sort è un'algoritmo di ordinamento per valori numerici interi e non è basato su confronti e scambi, ma per posizionare ordinatamente ogni elemento dell'array basterà calcolare quanti sono i suoi elementi minori. Si assume che ogni input sia compreso tra 0 e M (rappresenta il massimo numero dell'array). 5.1 Spiegazione Esaminiamo il caso dell'ordinamento crescente. L'array ordinato sarà salvato in un altro array (ris ). 10 • Si prepara un nuovo array occ di dimensione M + 1, che conterrà le occor- renze di ogni elemento all'interno dell'array e, quindi, lo si inizializza a 0. L'indice x dell'array occ rappresenta le occorrenze del numero x all'interno del vettore v. • Si contano le occorrenze di ogni numero all'interno del vettore. Ovvia- mente non necessariamente tutti i numeri compresi tra 0 e M sono contenuti al suo interno e quindi alcuni elementi del vettore occ potrebbero rimanere a 0. • A questo punto si ha la necessità di conoscere in che posizione bisognerà inserire l'elemento. Per fare questo ad ogni elemento del vettore delle occorrenze gli si sommano tutte le occorrenze dei numeri precedenti. Quindi per sapere dove collocare il numero x dell'array v, cioè sapere quanti sono gli elementi minori di x, basterà guardare il valore all'indice x nel vettore delle occorrenze. • Ora nel vettore ris, che deve essere un vettore fornito in input, anch'esso di dimensione dim, verranno posizionati in ordine gli elementi del vettore v. Si scorrerà il vettore v dal basso verso l'alto. Per ogni suo elemento si valuta quanto sono i suoi minori, tramite occ[v[i]], e per posizionarlo correttamente bisogna sottrarre -1, poiché gli indici dei vettori cominciano da 0. A questo punto sappiamo la posizione precisa in cui collocare l'elemento v[i]. Non bisogna dimenticarsi di diminuire le occorrenze di quel numero, cioè occ[v[i]], poiché se all'interno dell'array v ci sono elementi uguali, questi non potranno essere collocati nella stessa posizione, ma dovranno essere adiacenti. 5.2 Algoritmo Ordinamento crescente void countingsort(int v[], int dim, int ris[]){ int i, occ[M + 1]; } for(i = 0; i < M + 1; i++) occ[i] = 0; for(i = 0; i < dim; i++) occ[v[i]]++; for(i = 1 ; i < M + 1; i++) occ[i] += occ[i-1]; for(i = dim - 1; i >= 0; i--){ ris[occ[v[i]] - 1] = v[i]; occ[v[i]]--; } 11 Ordinamento decrescente void countingsort(int v[], int dim, int ris[]){ int i, occ[M + 1]; } for(i = 0; i < M + 1; i++) occ[i] = 0; for(i = 0; i < dim; i++) occ[v[i]]++; for(i = M - 1 ; i >= 0; i--) occ[i] += occ[i+1]; for(i = dim - 1; i >= 0; i--){ ris[occ[v[i]] - 1] = v[i]; occ[v[i]]--; } Riferimenti bibliograci [1] Prof.ssa A. Raaetà, Dispensa Laboratorio di Programmazione, Università Ca' Foscari - Venezia [2] Merge sort, http://it.wikipedia.org/wiki/merge_sort [3] Insertion sort, http://it.wikipedia.org/wiki/insertion_sort [4] Selection sort, http://it.wikipedia.org/wiki/selection_sort [5] Bubble sort, http://it.wikipedia.org/wiki/bubble_sort [6] Counting sort, http://it.wikipedia.org/wiki/counting_sort [7] Deitel H. M. e Deitel P. J., Corso Completo di Programmazione, Apogeo, 2 ed. 12