Ordinamento dei dati

Uno dei problemi tipici che spesso si deve affrontare quando si trattano collezioni di dati omogenei è l'ordinamento degli stessi.
Una collezione di dati ordinati semplifica notevolmente, ad esempio, le operazioni di ricerca di un elemento nella collezione.

Gli elementi di una collezione possono essere ordinati in due modi:

  • per valori crescenti, ossia quando ogni elemento della collezione precede, ossia è inferiore secondo il criterio dell'ordinamento, tutti gli elementi successivi
  • per valori decrescenti, ossia quando ogni elemento della collezione segue, ossia è maggiore secondo il criterio dell'ordinamento, tutti gli elementi successivi

Gli algoritmi per ordinare gli elementi di una collezione sono diversi e si distinguono per il numero di confronti ed il numero di scambi tra elementi, effettuati per completare l'operazione di ordinamento.

Ovviamente, minore sarà il numero di confronti e scambi di elementi e maggiore sarà l'efficienza dell'algoritmo stesso.
Un'altro elemento di distinzione è la necessità di memoria aggiuntiva (strutture dati aggiuntive) per svolgere il lavoro di ordinamento.
Gli algoritmi che non necessitano di strutture aggiuntive per eseguire l'ordinamento dei dati vengono anche detti algoritmi in-place.

Una ulteriore distinzione riguarda la strategia risolutiva.
Gli algoritmi che utilizzano strategie facilmente individuabili dal ragionamento umano sono detti algoritmi ingenui.
Quelli che invece utilizzano strategie risolutive più complesse, individuate a seguito di considerazioni e studi logico-matematici sono detti algoritmi evoluti.

Esistono, pertanto, diversi algoritmi di ordinamento dei dati che differiscono per gli aspetti finora elencati.
I principali sono:

  • algoritmi ingenui
    • ordinamento per selezione (selection sort)
    • ordinamento per inserzione (insertion sort)
    • ordinamento per scambio o a bolle (bubble sort)
  • algoritmi evoluti
    • ordinamento quicksort
    • ordinamento shell sort
    • ordinamento per fusione di collezioni ordinate (mergesort)

Di seguito esamineremo alcuni di essi approfondendone la logica ed implementandone la strategia in linguaggio Java.

Ordinamento per selezione (selection sort)

L'algoritmo di ordinamento per selezione, noto anche con il nome di selection sort, appartiene alla categoria degli algoritmi ingenui, ossia la cui strategia risolutiva è alquanto naturale e banale.
Esso può essere definito in due varianti, ossia per selezione del minimo o del massimo.
Nel seguito esamineremo nel dettaglio la versione con selezione del minimo.
La versione con selezione del massimo è analoga, con qualche piccola variazione, facilmente intuibile una volta compresa la logica di funzionamento della versione con selezione del minino.

L'algoritmo di ordinamento per selezione del minimo conduce all'ordinamento dei dati della sequenza attraverso i passi descritti di seguito.

Il vettore viene attraversato dal primo all'ultimo elemento alla ricerca del valore minimo.
Individuata la posizione del valore minimo, esso viene scambiato con il primo elemento del vettore, in modo che occupi, da ora in avanti, la prima posizione della sequenza.
Successivamente si procede allo stesso modo, considerando la sequenza formata dagli elementi del vettore successivi al primo.
Il procedimento si ripete fino a quando la sequenza risultante è composta da un solo elemento, che per definizione è ordinata.

Cerchiamo di comprendere meglio quanto descritto, attraverso una rappresentazione grafica.

Supponiamo di dover ordinare il vettore seguente:

5 9 3 1 2 6 7 4 8

Sulla base dell'algoritmo descritto precedentemente, il vettore viene attraversato alla ricerca del valore mininmo.
Tale valore è individuato nel valore 1, alla posizione 4.

5 9 3 1 2 6 7 4 8

Esso viene scambiato con il primo valore della sequenza analizzata, in modo da occupare la posizione più a sinistra nella sequenza considerata.
L'operazione di scambio è di seguito rappresentata graficamente.

1 9 3 5 2 6 7 4 8

A questo punto la sequenza costituita dal primo elemento è sicuramente ordinata
Occorre pertanto ripetere l'operazione concentrandosi sulla sottosequenza costituita dagli elementi successivi al primo.

1 9 3 5 2 6 7 4 8

Ripentendo la sequenza di passi utilizzata finora, fino a quando la sottosequenza da ordinare risulta costutuita da un solo elemento, il vettore si modificherà come mostrato di seguito, fino a contenere gli elementi originari ordinati in ordine crescente.

1 9 3 5 2 6 7 4 8
1 2 3 5 9 6 7 4 8
1 2 3 5 9 6 7 4 8
1 2 3 5 9 6 7 4 8
1 2 3 5 9 6 7 4 8
1 2 3 5 9 6 7 4 8
1 2 3 4 9 6 7 5 8
1 2 3 4 9 6 7 5 8
1 2 3 4 9 6 7 5 8
1 2 3 4 5 6 7 9 8
1 2 3 4 5 6 7 9 8
1 2 3 4 5 6 7 9 8
1 2 3 4 5 6 7 9 8
1 2 3 4 5 6 7 9 8
1 2 3 4 5 6 7 9 8
1 2 3 4 5 6 7 9 8
1 2 3 4 5 6 7 8 9
1 2 3 4 5 6 7 8 9
1 2 3 4 5 6 7 8 9

Una implementazione, in linguaggio Java, di tale strategia di ordinamento è mostrata nel codice seguente:

public static void selectioSort(int[] v) { // selezione del minimo
    // ad ogni iterazione si posiziona il minimo 
    // nella prima cella della porzione di vettore
    // esaminata
    for (int i = 0; i < v.length - 1; i++) {
        // si imposta il minimo alla posizione
        // del primo elemento della sequenza
        int posMin = i;
        for (int j = i + 1; j < v.length; j++) {
            // se si trova un elemento più piccolo 
            // del minimo attuale, si cambia la
            // posizione del minimo
            if (v[j] < v[posMin]) {
                posMin = j;
            }
        }
        // se la posizione del minimo non è quella iniziale
        // occorre scambiare l'elemento alla posizione del 
        // minimo con quello iniziale
        if (posMin != i) {
            int t = v[i];
            v[i] = v[posMin];
            v[posMin] = t;
        }
    }
}

Lo stesso algoritmo può essere implementato nella versione con selezione del massimo.
In tal caso, si cerca la posizione dell'elemento massimo, che viene poi posizionato in fondo alla sequenza.
Il procedimento, quindi, si ripete considerando la sottosequenza composta dagli elementi della sequenza precedente, privata dell'ultimo elemento.
In questo caso il vettore ordinato viene costruito a partire dal fondo.

Vediamone una implementazione in linguaggio Java, realizzata apportando una piccola variazione al codice visto sopra.

public static void selectioSort(int[] v) { // selezione del massimo
    // ad ogni iterazione si posiziona il massimo 
    // nell'ultima cella della porzione di vettore
    // esaminata
    int length = v.length;
    while (length > 1) {
        // si imposta il massimo alla posizione
        // del primo elemento della sequenza
        int posMax = 0;
        for (int j = 1; j < length; j++) {
            // se si trova un elemento più grande 
            // del massimo attuale, si cambia la
            // posizione del massimo
            if (v[j] > v[posMax]) {
                posMax = j;
            }
        }
        // se la posizione del massimo non è quella finale della 
        // sequenza occorre scambiare l'elemento alla posizione  
        // del massimo con quello in coda alla sequenza
        if (posMax != length - 1) {
            int t = v[length - 1];
            v[length - 1] = v[posMax];
            v[posMax] = t;
        }

        // a questo punto il massimo della sequenza è posizionato
        // in fondo ad essa, per cui si diminuisce la lunghezza 
        // della sequenza ancora da ordinare e si itera nuovamente
        length--;
    }
}

Ordinamento per inserzione (insertion sort)

L'algoritmo di ordinamento per inserzione itera su sottosequenze del vettore da ordinare che crescono, ad ogni iterazione, di un elemento.
Si inizia con la sottosequenza formata dal primo elemento del vettore, ordinata per definizione in quanto costituita da un solo elemento.
Si procede, poi, estendendo la precedente sottosequenza, attraverso l'inserimento dell'elemento che la segue, nella sua posizione corretta, traslando gli elementi successivi ad esso di una posizione verso destra.
Si ripete tale procedimento fino a quando non sono stati esaminati tutti gli elementi del vettore da ordinare.
Analizziamo meglio tale strategia attraverso un esempio grafico.

Supponiamo di dover ordinare il vettore seguente:

5 9 3 1 2 6 7 4 8

Si inizia considerando la sottosequenza formata dal primo elemento del vettore, che risulta ordinata, per ovvie ragioni.

5 9 3 1 2 6 7 4 8

Si procede poi considerando la sottosequenza ottenuta aggiungendo l'elemento seguente alla sottosequenza precedente e verificando la sua posizione corretta all'interno della sottosequenza.

5 9 3 1 2 6 7 4 8

Nel caso in esame, l'elemento si trova già nella sua corretta posizione, per cui non si effettuano spostamenti e si procede con l'iterazione successiva.

5 9 3 1 2 6 7 4 8

Questa volta l'elemento aggiunto non si trova nella sua posizione corretta. Occorre pertanto traslare verso destra tutti gli elementi maggiori dell'elemento aggiunto ed inserire quest'ultimo nella sua corretta posizione.

3 5 9 1 2 6 7 4 8
3 5 9 1 2 6 7 4 8
3 5 9 1 2 6 7 4 8

Anche questa volta l'elemento aggiunto non si trova nella sua posizione corretta. Si ripete pertanto lo stesso procedimento effettuato all'iterazione precedente.

1 3 5 9 2 6 7 4 8
1 3 5 9 2 6 7 4 8

Si continua in modo analogo per i restanti elementi della sequenza.

1 3 5 9 2 6 7 4 8
1 2 3 5 9 6 7 4 8
1 2 3 5 9 6 7 4 8
1 2 3 5 9 6 7 4 8
1 2 3 5 6 9 7 4 8
1 2 3 5 6 9 7 4 8
1 2 3 5 6 9 7 4 8
1 2 3 5 6 7 9 4 8
1 2 3 5 6 7 9 4 8
1 2 3 5 6 7 9 4 8
1 2 3 4 5 6 7 9 8
1 2 3 4 5 6 7 9 8
1 2 3 4 5 6 7 9 8
1 2 3 4 5 6 7 8 9
1 2 3 4 5 6 7 8 9

Una implementazione, in linguaggio Java, di tale strategia di ordinamento è mostrata nel codice seguente:

public static void insertionSort(int[] v) {
    for (int l = 1; l < v.length; l++) {
        int el = v[l];
        int p = l - 1;
        while (p >= 0 && v[p] > el) {
            v[p + 1] = v[p];
            p--;
        }
        if (p != l - 1) {
            v[p + 1] = el;
        }
    }
}

Ordinamento a bolle (bubble sort)

L'algoritmo di ordinamento a bolle o bubble sort è un algoritmo iterativo, che procede confrontando coppie di elementi adiacenti e scambiando tra loro i valori, qualora non fossero nell'ordine corretto.

Esso può processare il vettore in due modi, dal primo all'ultimo elemento oppure nella direzione inversa.

Nel primo caso gli elementi di peso maggiore della collezione vengono spostati verso la fine del vettore e, ad ogni iterazione, l'elemento maggiore verrà posizionato nell'ultima posizione della sequenza.

Nel secondo caso gli elementi di peso minore della collezione vengono spostati verso l'inizio del vettore e, ad ogni iterazione, l'elemento minore verrà posizionato nella prima posizione della sequenza.

Al termine dell'iterazione si ripete il procedimento sulla sottosequenza ottenuta dalla precedente, ignorandone l'ultima cella nel primo caso, la prima cella nel secondo caso.

Il termine bolle nel nome sta, infatti, ad indicare il procedimento per cui, ad ogni iterazione, gli elementi di peso maggiore si spostano verso il fondo della sequenza e quelli di peso minore verso la superficie della stessa.

Analizziamone più in dettaglio la strategia, attraverso un esempio grafico.

Supponiamo di dover ordinare gli elementi del vettore seguente in ordine crescente:

5 9 3 1 2 6 7 4 8

Sulla base dell'algoritmo descritto precedentemente, il vettore viene attraversato in una delle due direzioni, in questo caso dall'inizio verso la fine, confrontando le coppie di elementi adiacenti ed eventualmente scambiandoli di posto, se non ordinati secondo l'ordinamento voluto.
Si inizia pertanto confrontando i primi due elementi evidenziati.

5 9 3 1 2 6 7 4 8

In questo caso i due elementi sono nell'ordine giusto, per cui si procede con la coppia successiva

5 9 3 1 2 6 7 4 8

In questo caso i due elementi non sono nell'ordine giusto, pertanto si procede con lo scambio

5 3 9 1 2 6 7 4 8

Si continua poi, con procedimento analogo, con le coppie successive, fino a che si giunge alla fine del vettore.

5 3 9 1 2 6 7 4 8
5 3 1 9 2 6 7 4 8
5 3 1 9 2 6 7 4 8
5 3 1 2 9 6 7 4 8
5 3 1 2 9 6 7 4 8
5 3 1 2 6 9 7 4 8
5 3 1 2 6 9 7 4 8
5 3 1 2 6 7 9 4 8
5 3 1 2 6 7 9 4 8
5 3 1 2 6 7 4 9 8
5 3 1 2 6 7 4 9 8
5 3 1 2 6 7 4 8 9
5 3 1 2 6 7 4 8 9

Come si può notare, al termine di una iterazione, l'elemento maggiore del vettore è stato posizionato nella sua posizione corretta, ossia in fondo al vettore.
Gli elementi più piccoli, invece, si sono spostati di qualche posizione verso l'inizio del vettore.
A questo punto, si ripete il procedimento sulla sottosequenza del vettore costituita dai suoi elementi ad eccezione dell'ultimo.
Ad ogni iterazione si posiziona, pertanto, l'elemento maggiore in fondo alla sottosequenza, fino a quando essa non risulti costituita da un solo elemento.
Vediamo in che modo, quanto detto, si riflette nell'esempio considerato.

5 3 1 2 6 7 4 8 9
3 5 1 2 6 7 4 8 9
3 5 1 2 6 7 4 8 9
3 1 5 2 6 7 4 8 9
3 1 5 2 6 7 4 8 9
3 1 2 5 6 7 4 8 9
3 1 2 5 6 7 4 8 9
3 1 2 5 6 7 4 8 9
3 1 2 5 6 7 4 8 9
3 1 2 5 6 4 7 8 9
3 1 2 5 6 4 7 8 9
3 1 2 5 6 4 7 8 9

A questo punto termina la seconda iterazione.
Si può notare l'elemento 8 posizionato in fondo alla sottosequenza.
Si ripete considerando la sottosequenza costituita dagli elementi del vettore ad eccezione degli ultimi due.

3 1 2 5 6 4 7 8 9
1 3 2 5 6 4 7 8 9
1 3 2 5 6 4 7 8 9
1 2 3 5 6 4 7 8 9
1 2 3 5 6 4 7 8 9
1 2 3 5 6 4 7 8 9
1 2 3 5 6 4 7 8 9
1 2 3 5 4 6 7 8 9
1 2 3 5 4 6 7 8 9
1 2 3 5 4 6 7 8 9

Fine terza iterazione e nuova iterazione sulla sottosequenza dal primo al sesto elemento.

1 2 3 5 4 6 7 8 9
1 2 3 5 4 6 7 8 9
1 2 3 5 4 6 7 8 9
1 2 3 5 4 6 7 8 9
1 2 3 4 5 6 7 8 9
1 2 3 4 5 6 7 8 9
1 2 3 4 5 6 7 8 9

Fine quarta iterazione e nuova iterazione sulla sottosequenza dal primo al quinto elemento.

1 2 3 4 5 6 7 8 9
1 2 3 4 5 6 7 8 9
1 2 3 4 5 6 7 8 9
1 2 3 4 5 6 7 8 9
1 2 3 4 5 6 7 8 9

Fine quinta iterazione e nuova iterazione sulla sottosequenza dal primo al quarto elemento.

La versione dell'algoritmo descritta continuerebbe ad iterare fino a quando la sottosequenza non risulterà composta da un solo elemento, ordinata per definizione.
Tuttavia si può osservare che l'ultima iterazione non ha effettuato scambi tra gli elementi.
Questo ci porta a concludere che il vettore risulta già ordinato, per cui una ottimizzazione dell'algoritmo può arrestarsi anche alla prima iterazione che non effettua alcuno scambio.
Gli elementi del vettore, come si può vedere, risultano ordinati dal più piccolo al più grande.
Vediamone, a questo punto, una implementazione in linguaggio Java:

public static void bubbleSort(int[] v) {
    // la prima sequenza da esaminare
    // coincide con tutto il vettore
    int l = v.length;

    // esamino la sequenza fino a che essa
    // sia costituita da più di un elemento
    while (l > 1) {
        for (int j = 0; j < l - 1; j++) {
            // confronto gli elementi adiacenti
            // e li scambio nel caso in cui non
            // fossero nell'ordine corretto
            if (v[j] > v[j + 1]) {
                int t = v[j];
                v[j] = v[j + 1];
                v[j + 1] = t;
            }
        }
        // diminuisco la dimensione della 
        // sequenza da esaminare
        l--;
    }
}

L'implementazione appena vista non considera il caso in cui il processamento di una sequenza non effetti alcuno scambio, per cui essa continua fino a che la sequenza da esaminare non diventi di dimensione 1.

Di seguito riportiamo la versione ottimizzata dell'algoritmo, che termina le iterazioni anche nel caso in cui una sottosequenza non abbia alcun elemento da scambiare di posto, quindi sia già ordinata.

public static void bubbleSortOttimizzato(int[] v) {
    // la prima sequenza da esaminare
    // coincide con tutto il vettore
    int l = v.length;
    boolean scambio = true;

    // esamino la sequenza fino a che essa
    // sia costituita da più di un elemento
    // e nella precedente iterazione
    // ci sia stato almeno uno scambio
    while (l > 1 && scambio) {
        // suppongo che non ci saranno scambi
        scambio = false;
        for (int j = 0; j < l - 1; j++) {
            // confronto gli elementi adiacenti
            // e li scambio nel caso in cui non
            // fossero nell'ordine corretto
            if (v[j] > v[j + 1]) {
                int t = v[j];
                v[j] = v[j + 1];
                v[j + 1] = t;
                // segnalo lo scambio di elementi
                scambio = true;
            }
        }
        // diminuisco la dimensione della 
        // sequenza da esaminare
        l--;
    }
}

Quicksort

... non ancora disponibile

Mergesort

La strategia di ordinamento di questo algoritmo si basa sul principio divide et impera ed utilizza una logica ricorsiva.

Nel dettaglio, la sequenza iniziale viene scomposta in due sequenze più piccole, che vengono ordinate con la stessa logica e successivamente utilizzate per ricostruire la sequenza iniziale, questa volta ordinata, attraverso la fusione delle stesse.
Il principio divide et impera sostiene, infatti, che la scomposizione di un problema complesso in sottoproblemi di dimensione inferiore, ne semplifica la risoluzione.
Inoltre, abbiamo già visto, nella sezione degli esercizi, l'algoritmo per la fusione di due sequenze ordinate in un'unica sequenza ordinata.
Tale algoritmo non presenta delle particolari complessità.
Utilizzeremo, quindi, la tecnica della fusione per costruire sottosequenze ordinate del vettore iniziale, partendo da sottosequenze formate da un solo elemento (ordinate per definizione).

Procederemo poi a ritroso fondendo le sottosequenze ottenute, fino a ricostruire la sequenza originale con gli elementi ordinati. Approfondiamo attraverso un esempio grafico.
Supponiamo di dover ordinare gli elementi del vettore seguente in ordine crescente:

5 9 3 1 2 6 7 4 8

Sulla base dell'algoritmo descritto precedentemente, il vettore viene diviso in due metà e l'algoritmo viene richiamato su ciascuna metà, in modo ricorsivo, fino a giungere a delle sequenze di dimensione uno.

5 9 3 1 2
6 7 4 8
5 9 3
1 2
6 7
4 8
5 9
3
1
2
6
7
4
8

A questo punto le sequenze ordinate possono essere unite tra loro, utilizzando l'algoritmo della fusione, procedendo a ritroso, fino a giungere alla fusione delle due sequenze iniziali, in cui il vettore era stato diviso, che ricostruiranno il vettore iniziale ordinato.

5
9
3
1 2
6 7
4 8
5 9
3
1 2
4 6 7 8
3 5 9
1 2
4 6 7 8
1 2 3 5 9
4 6 7 8
1 2 3 4 5 6 7 8 9
Nota:
Notare l'evidente minore quantità di passi rappresentati graficamente per l'algoritmo mergesort, rispetto agli altri visti in precedenza.
L'efficienza di questo algoritmo può, infatti, essere verificata analizzando i tempi medi di ordinamento di sequenze piuttosto consistenti.

public static void mergeSort(int[] v) {
        // chiamo la funzione ricorsiva, che contiene
        // i parametri per poter eseguire la ricorsione
        // e quindi determinare la sua terminazione
        mergeSortR(v, 0, v.length - 1);
    }

    // funzione ricorsiva
    public static void mergeSortR(int[] v, int iIn, int iFin) {
        // se l'indice iniziale non è minore di quello finale
        // vuol dire che la sequenza da ordinare è composta da 
        // uno o zero elementi, quindi è ordinata per definizione
        if (iIn < iFin) {
            // calcolo l'indice  mediano della sequenza
            int iMed = (iIn + iFin) / 2;
            // richiamo ricorsivamente la funzione 
            // sulle metà sinistra e destra
            mergeSortR(v, iIn, iMed);
            mergeSortR(v, iMed + 1, iFin);
            // alla fine eseguo la fusione delle 
            // due sequenze ordinate
            merge(v, iIn, iMed, iFin);
        }
    }

    public static void merge(int[] v, int iIn, int iMed, int iFin) {
        // effettua la fusione delle due sequenze
        // la prima con indice iniziale iIn e finale iMed
        // la seconda con indice iniziale iMed+1 e finale iFin
        // utilizzando un terzo vettore t che alla fine 
        // viene ricopiato nel vettore iniziale
        int iIn2 = iMed + 1;
        int l = iMed - iIn + 1 + iFin - iIn2 + 1;
        int i1so = iIn;
        int[] t = new int[l];
        for (int i = 0; i < l; i++) {
            if (iIn > iMed) {
                t[i] = v[iIn2];
                iIn2++;
            } else if (iIn2 > iFin) {
                t[i] = v[iIn];
                iIn++;
            } else if (v[iIn] < v[iIn2]) {
                t[i] = v[iIn];
                iIn++;
            } else if (v[iIn2] < v[iIn]) {
                t[i] = v[iIn2];
                iIn2++;
            } else {
                t[i] = v[iIn2];
                iIn2++;
                i++;
                t[i] = v[iIn];
                iIn++;
            }
        }
        
        // ricopio la sequenza ordinata
        // nel vettore iniziale
        for (int i = 0; i < l; i++) {
            if (v[i + i1so] != t[i]) {
                v[i + i1so] = t[i];
            }
        }
    }