Sei sulla pagina 1di 36

13 Ordinamento e ricerca

Obiettivi del capitolo


Studiare alcuni algoritmi di ordinamento e ricerca
Osservare che algoritmi che risolvono lo stesso problema possono avere prestazioni
molto diverse
Capire la notazione O-grande
Imparare a stimare le prestazioni di algoritmi e a confrontarle
Imparare a misurare il tempo desecuzione di un programma
Una delle operazioni che capita pi comunemente di dover eseguire nellelaborazione dei
dati lordinamento: per esempio, spesso necessario stampare un elenco di dipendenti in
ordine alfabetico o in ordine di stipendio. Studieremo in questo capitolo diversi metodi per
lordinamento, mettendo a confronto le loro rispettive prestazioni. Questa non intende
minimamente essere una esposizione completa del tema dellordinamento, sul quale tor-
nerete pi avanti nel corso dei vostri studi di informatica. Il riferimento [1] presenta unot-
tima rassegna dei molti metodi di ordinamento che si hanno a disposizione.
Una volta che una sequenza di oggetti stata ordinata, si possono trovare molto rapi-
damente oggetti specifici. Studieremo lalgoritmo di ricerca binaria, che esegue questo
tipo di consultazione veloce.
13.1 Ordinamento per selezione
In questo paragrafo illustreremo il primo algoritmo di ordinamento. Un algoritmo di ordi-
namento (sorting algorithm) sposta gli elementi di una raccolta in modo che vi risultino
poi memorizzati in un qualche ordine specifico. Per rimanere su esempi semplici, conside-
reremo lordinamento di un array di numeri interi, prima di passare a ordinare stringhe o
dati pi complessi. Considerate il seguente array a:
Una prima, elementare fase quella di trovare lelemento pi piccolo. In questo caso
lelemento pi piccolo 5, memorizzato in a[3]. Dovremmo spostare 5 allinizio dellar-
ray, in a[0], dove, naturalmente, c gi un elemento memorizzato, precisamente 11. Di
conseguenza, non possiamo semplicemente spostare a[3] in a[0] senza spostare 11 da
Lalgoritmo di ordinamento
per selezione ordina un array
cercando ripetutamente
lelemento minore
della regione terminale non
ancora ordinata e spostando
tale elemento allinizio
della regione stessa.
capito13new.pmd 16/07/2007, 16.59 523
524 CAPITOLO 13
qualche altra parte. Non sappiamo ancora dove dovrebbe andare a finire il numero 11, ma
sappiamo con sicurezza che non deve stare in a[0]. Ce lo togliamo semplicemente di torno
scambiandolo con a[3].
Ora il primo elemento si trova nel posto giusto. Nella figura precedente, la zona colorata
indica la porzione dellarray che gi stata ordinata, mentre la parte rimanente ancora da
ordinare.
Successivamente, prendiamo il pi piccolo degli elementi rimanenti, esaminando cio
a[1]...a[4]. Tale valore minimo, 9, si trova gi nella posizione giusta: in questo caso non
dobbiamo fare nulla e possiamo semplicemente estendere di una posizione verso destra la
porzione dellarray che risulta essere gi ordinata.
Ripetiamo il processo. Il valore minimo della regione non ordinata 11, che deve essere
scambiato col primo valore della regione non ordinata, 17.
Ora la regione non ordinata composta da due soli elementi, ma continuiamo ad applicare
la stessa strategia vincente. Il valore minimo 12 e lo scambiamo con il primo valore, 17.
Questo ci lascia con una regione non ancora elaborata di lunghezza 1, ma naturalmente
una regione di lunghezza 1 sempre ordinata. Abbiamo finito.
Proviamo a scrivere il codice per questo algoritmo. Per questo programma, come pure
per gli altri programmi di questo capitolo, utilizzeremo un metodo ausiliario per generare
un array con valori casuali, che inseriamo in una classe ArrayUtil in modo da non essere
costretti a ripeterlo in ogni esempio di codice. Per visualizzare il contenuto di un array,
invochiamo invece il metodo statico toString della classe java.util.Arrays e stampia-
mo la stringa da esso restituita.
Questo algoritmo ordiner un array di numeri interi. Se la velocit non fosse un pro-
blema o se, semplicemente, non ci fossero a disposizione metodi di ordinamento migliori,
potremmo interrompere qui la discussione sullordinamento. Tuttavia, come mostrer il
paragrafo successivo, questo algoritmo, pur essendo assolutamente corretto, ha prestazio-
ni davvero deludenti quando viene eseguito su grandi insiemi di dati.
In Argomenti avanzati 13.1 viene presentato lalgoritmo di ordinamento per inseri-
mento (insertion sort), un altro algoritmo di ordinamento molto semplice e analogamen-
te inefficiente.
capito13new.pmd 16/07/2007, 16.59 524
ORDINAMENTO E RICERCA 525
File SelectionSorter.java
/**
Questa classe ordina un array,
usando lalgoritmo di ordinamento per selezione.
*/
public class SelectionSorter
{
/**
Costruisce un ordinatore per selezione.
@param anArray larray da ordinare
*/
public SelectionSorter(int[] anArray)
{
a = anArray;
}
/**
Ordina larray gestito da questo ordinatore.
*/
public void sort()
{
for (int i = 0; i < a.length 1; i++)
{
int minPos = minimumPosition(i);
swap(minPos, i);
}
}
/**
Trova lelemento minimo in una parte terminale dellarray.
@param from la prima posizione in a che va considerata
@return la posizione dellelemento minimo presente
nellintervallo a[from]...a[a.length 1]
*/
private int minimumPosition(int from)
{
int minPos = from;
for (int i = from + 1; i < a.length; i++)
if (a[i] < a[minPos]) minPos = i;
return minPos;
}
/**
Scambia due elementi nellarray.
@param i la posizione del primo elemento
@param j la posizione del secondo elemento
*/
private void swap(int i, int j)
{
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
private int[] a;
}
capito13new.pmd 16/07/2007, 16.59 525
526 CAPITOLO 13
File SelectionSortDemo.java
import java.util.Arrays;
/**
Questo programma applica lalgoritmo di ordinamento
per selezione a un array contenente numeri casuali.
*/
public class SelectionSortDemo
{
public static void main(String[] args)
{
int[] a = ArrayUtil.randomIntArray(20, 100);
System.out.println(Arrays.toString(a));
SelectionSorter sorter = new SelectionSorter(a);
sorter.sort();
System.out.println(Arrays.toString(a));
}
}
File ArrayUtil.java
import java.util.Random;
/**
Questa classe contiene metodi utili per
la manipolazione di array.
*/
public class ArrayUtil
{
/**
Costruisce un array contenente valori casuali.
@param length la lunghezza dellarray
@param n il numero di valori casuali possibili
@return un array contenente length numeri
casuali compresi fra 0 e n - 1
*/
public static int[] randomIntArray(int length, int n)
{
int[] a = new int[length];
for (int i = 0; i < a.length; i++)
a[i] = generator.nextInt(n);
return a;
}
private static Random generator = new Random();
}
Visualizza (esempio):
[65, 46, 14, 52, 38, 2, 96, 39, 14, 33, 13, 4, 24, 99, 89, 77, 73, 87, 36, 81]
[2, 4, 13, 14, 14, 24, 33, 36, 38, 39, 46, 52, 65, 73, 77, 81, 87, 89, 96, 99]
capito13new.pmd 16/07/2007, 16.59 526
ORDINAMENTO E RICERCA 527
Auto-valutazione
1. Perch nel metodo swap serve la variabile temp? Cosa succederebbe se assegnassimo
semplicemente a[i] a a[j] e a[j] a a[i]?
2. Quali sono i passi compiuti dallalgoritmo di ordinamento per selezione nellordinare
la sequenza 6 5 4 3 2 1?
13.2 Misurazione delle prestazioni dellalgoritmo
di ordinamento per selezione
Per rilevare le prestazioni di un programma, potreste semplicemente eseguirlo e usare un
cronometro per misurare quanto tempo impiega, ma la maggior parte dei nostri programmi
viene eseguita molto rapidamente e non sarebbe facile misurare i tempi in modo accurato
in questo modo. Inoltre, anche quando un programma impiega una quantit di tempo per-
cepibile per la sua esecuzione, una certa quantit di quel tempo viene semplicemente usata
per caricare il programma dal disco alla memoria (cosa per la quale non dovremmo pena-
lizzarlo) o per visualizzare i risultati sullo schermo (la cui velocit dipende dal modello
del computer, anche per macchine con CPU identiche). Utilizzeremo invece una classe
StopWatch, che funziona proprio come un cronometro vero: potete farlo partire, fermarlo
e leggere il tempo trascorso. La classe usa il metodo System.currentTimeMillis, che
restituisce il numero di millisecondi che sono trascorsi dalla mezzanotte del giorno 1 gen-
naio 1970. Naturalmente, non ci interessa il numero assoluto di secondi trascorsi da
quellistante particolare, ma la differenza fra due conteggi di questo genere ci fornisce la
durata di un intervallo temporale, misurata in millisecondi. Ecco il codice della classe
StopWatch:
File StopWatch.java
/**
Un cronometro accumula il tempo mentre in azione.
Potete avviare e arrestare ripetutamente il cronometro.
Potete utilizzare un cronometro per misurare il tempo
di esecuzione di un programma.
*/
public class StopWatch
{
/**
Costruisce un cronometro fermo e senza tempo accumulato.
*/
public StopWatch()
{
reset();
}
/**
Fa partire il cronometro, iniziando ad accumulare il tempo.
*/
public void start()
{
if (isRunning) return;
isRunning = true;
capito13new.pmd 16/07/2007, 16.59 527
528 CAPITOLO 13
startTime = System.currentTimeMillis();
}
/**
Ferma il cronometro. Il tempo non viene pi accumulato
e il tempo trascorso viene sommato al tempo totale.
*/
public void stop()
{
if (!isRunning) return;
isRunning = false;
long endTime = System.currentTimeMillis();
elapsedTime = elapsedTime + endTime startTime;
}
/**
Restituisce il tempo totale trascorso.
@return il tempo totale trascorso
*/
public long getElapsedTime()
{
if (isRunning)
{
long endTime = System.currentTimeMillis();
return = elapsedTime + endTime startTime;
}
else
return elapsedTime;
}
/**
Ferma il cronometro e azzera il tempo totale trascorso.
*/
public void reset()
{
elapsedTime = 0;
isRunning = false;
}
private long elapsedTime;
private long startTime;
private boolean isRunning;
}
Ed ecco come utilizzeremo il cronometro per misurare le prestazioni dellalgoritmo di
ordinamento.
File SelectionSortTimer.java
import java.util.Scanner;
/**
Questo programma misura il tempo richiesto
per ordinare con lalgoritmo di ordinamento
capito13new.pmd 16/07/2007, 16.59 528
ORDINAMENTO E RICERCA 529
per selezione un array di dimensione
specificata dallutente.
*/
public class SelectionSortTimer
{
public static void main(String[] args)
{
Scanner in = new Scanner(System.in);
System.out.print(Enter array size: );
int n = in.nextInt();
// costruisce un array casuale
int[] a = ArrayUtil.randomIntArray(n, 100);
SelectionSorter sorter = new SelectionSorter(a);
// usa il cronometro per misurare il tempo
StopWatch timer = new StopWatch();
timer.start();
sorter.sort(a);
timer.stop();
System.out.println(Elapsed time:
+ timer.getElapsedTime() + milliseconds);
}
}
Visualizza (esempio):
Enter array size: 100000 100000 100000 100000 100000
Elapsed time: 27880 milliseconds
Avviando la rilevazione del tempo immediatamente prima dellordinamento e arrestando-
la subito dopo, non si tiene conto del tempo che occorre per inizializzare larray o del
tempo durante il quale il programma attende che lutente digiti il valore n.
Ecco i risultati di alcune semplici esecuzioni di prova:
n nn nn Millisecondi
10 000 772
20 000 3051
30 000 6846
40 000 12 188
50 000 19 015
60 000 27 359
Queste rilevazioni sono state ottenute con un processore Pentium a 1.2 GHz, con sistema
operativo Linux e Java 5.0. Su un altro computer i numeri effettivi potrebbero essere diver-
si, ma la relazione fra i numeri resterebbe la stessa. La Figura 1 mostra un grafico delle
rilevazioni: come potete vedere, raddoppiando le dimensioni dellinsieme dei dati, il tem-
po che occorre per ordinarli pi del doppio.
capito13new.pmd 16/07/2007, 16.59 529
530 CAPITOLO 13
Auto-valutazione
3. Quanti secondi sarebbero necessari, approssimativamente, per ordinare un insieme di
dati contenente 80000 valori?
4. Osservate il grafico della Figura 1: a quale curva matematica assomiglia?
13.3 Analisi delle prestazioni dellalgoritmo
di ordinamento per selezione
Proviamo a conteggiare le operazioni che il programma deve eseguire per ordinare un
array con lalgoritmo di ordinamento per selezione. In effetti noi non sappiamo quante
operazioni macchina vengono generate per ciascuna istruzione Java e neppure quali di
queste istruzioni impiegano pi tempo di altre, per possiamo fare una semplificazione: ci
limiteremo a conteggiare il numero di volte che un elemento dellarray viene visitato.
Ciascuna visita richiede allincirca la stessa quantit di lavoro di altre operazioni, quali
lincremento di indici o il confronto di valori.
Supponiamo che n sia la dimensione dellarray. Come prima cosa, dobbiamo trovare il
pi piccolo fra n numeri: per ottenere questo risultato, dobbiamo visitare n elementi del-
larray. Poi scambiamo gli elementi, cosa che richiede due visite (potreste argomentare
che esiste una certa probabilit che non si debbano scambiare i valori: questo vero, e si
potrebbe raffinare il calcolo per tenere conto di questa osservazione, ma, come vedremo
fra un momento, se lo facessimo non altereremmo la conclusione complessiva). Nel passo
successivo dobbiamo visitare soltanto n 1 elementi per trovare il minimo. Nel passo an-
cora seguente, vengono visitati soltanto n 2 elementi, e lultimo passo visita soltanto due
elementi tra i quali deve trovare il minimo. Ciascun passo richiede 2 visite per scambiare
gli elementi; di conseguenza, il numero totale delle visite :
Figura 1
Tempo impiegato
dallordinamento per
selezione
capito13new.pmd 16/07/2007, 16.59 530
ORDINAMENTO E RICERCA 531
n + 2 + (n 1) + 2 + . . . + 2 + 2 = n + (n 1) + . . . + 2 + (n 1) 2
= 2 + . . . + (n 1) + n + (n 1) 2
=
+
- + -
n n
n
( )
( )
1
2
1 1 2
perch
1 2 1
1
2
+ + + - + =
+
( )
( )
n n
n n
Sviluppando le moltiplicazioni e raccogliendo n a fattore comune, troviamo che il numero
delle visite :
1
2
5
2
3
2
n n +
Otteniamo, cio, unequazione di secondo grado in n. Questo spiega perch il grafico della
Figura 1 assomiglia a una parabola.
Ora semplifichiamo ulteriormente lanalisi. Quando inseriamo un valore elevato per n
(per esempio, 1000 o 2000) allora 1/2n
2
500000 oppure 2000000. Il termine di grado
inferiore, 5/2n 3, non contribuisce pi di tanto, vale appena 2497 oppure 4997, una
goccia nel mare rispetto alle centinaia di migliaia o addirittura ai milioni di visite corri-
spondenti al termine 1/2n
2
. Ignoreremo semplicemente questi termini di grado inferiore.
Poi, ignoriamo anche il fattore costante 1/2: non ci interessa il conteggio effettivo delle
visite per un singolo valore di n, ma vogliamo soltanto confrontare i rapporti dei conteggi
per diversi valori di n. Per esempio, possiamo dire che ordinare un array di 2000 numeri
richiede 4 volte il numero di visite che sono necessarie per ordinare un array di 1000
numeri:
1
2
2000
1
2
1000
4
2
2

=
Il fattore 1/2 si elide in confronti di questo genere: diremo semplicemente che il numero
delle visite dellordine di n
2
". In questo modo, possiamo agevolmente vedere che il nu-
mero di visite quadruplica quando le dimensioni dellarray raddoppiano: (2n)
2
= 4n
2
.
Per indicare che il numero delle visite dellordine di n
2
, gli informatici teorici spesso
usano la notazione O-grande (big-Oh, in inglese). Il numero delle visite O(n
2
): si tratta
di una comoda abbreviazione.
In generale, lespressione f(n) = O(g(n)) significa che f non cresce pi rapidamente di
g, oppure, in modo pi formale: per tutti i valori di n maggiori di una certa soglia, si ha
f(n)/g(n) C per un qualche valore costante C. Solitamente la funzione g viene scelta
molto semplice, come n
2
nel nostro esempio.
Per trasformare unespressione esatta come
1
2
5
2
3
2
n n +
Gli informatici teorici usano
la notazione O-grande
f(n) = O(g(n)) per esprimere
il fatto che la funzione f non
aumenta pi rapidamente
della funzione g.
capito13new.pmd 16/07/2007, 17.00 531
532 CAPITOLO 13
nella corrispondente notazione O-grande, basta semplicemente individuare il termine che
aumenta pi rapidamente, n
2
, e ignorare il suo coefficiente costante, 1/2, indipendente-
mente dal fatto che sia grande o piccolo.
Abbiamo osservato prima che il numero effettivo delle operazioni del processore e
il numero effettivo di microsecondi che il computer dedica a esse allincirca propor-
zionale al numero delle visite degli elementi. Forse per ciascuna visita a un elemento
servono una decina di operazioni macchina (incrementi, confronti, letture e scritture
nella memoria): il numero delle operazioni macchina quindi approssimativamente
101/2n
2
. Ancora una volta, il coefficiente non ci interessa, per cui possiamo dire che
il numero delle operazioni macchina, e quindi il tempo necessario per lordinamento,
dellordine di n
2
ovvero O(n
2
).
Rimane il triste fatto che il raddoppio delle dimensioni dellarray quadruplica il tempo
necessario per ordinarlo. Quando la dimensione dellarray aumenta di un fattore 100, il
tempo di ordinamento aumenta di un fattore 10 000. Per ordinare un array con un milione
di voci (per creare, ad esempio, lelenco telefonico di una citt), si impiega un tempo
10 000 volte pi lungo di quello che occorrerebbe per ordinare 10 000 voci. Se 10 000 voci
si possono ordinare in tre quarti di secondo (come nel nostro esempio), allora lordina-
mento di un milione di voci richiede pi di 2 ore. Questo un problema: vedremo nel
prossimo paragrafo come si possano migliorare in modo spettacolare le prestazioni del
processo di ordinamento scegliendo un algoritmo pi sofisticato.
Auto-valutazione
5. Se aumentate di dieci volte la dimensione dellinsieme dei dati, come aumenta il tem-
po richiesto per ordinare linsieme con lalgoritmo di ordinamento per selezione?
6. Quanto deve valere n perch 1/2 n
2
sia maggiore di 5/2 n 3?
Argomenti avanzati 13.1
Ordinamento per inserimento
Lordinamento per inserimento un altro semplice algoritmo di ordinamento. In questo
algoritmo si suppone che la parte iniziale
a[0] a[1] ... a[k]
di un array sia gi ordinata (quando lalgoritmo inizia il suo lavoro, k vale 0). Espandiamo
questa parte iniziale ordinata inserendovi nella giusta posizione il successivo elemento
dellarray, a[k + 1]. Giunti al termine dellarray, il processo di ordinamento completato.
Ad esempio, supponiamo di iniziare con il seguente array:
La parte iniziale, di lunghezza 1, ovviamente gi ordinata. Aggiungiamo ora a tale por-
zione ordinata lelemento a[1], il cui valore 9. Tale elemento deve essere inserito prima
dellelemento di valore 11, per cui il risultato :
Successivamente, aggiungiamo lelemento a[2], il cui valore 16: per caso, non c biso-
gno di spostare tale elemento.
Lordinamento per selezione
un algoritmo O(n
2
):
il raddoppio della dimensione
dellinsieme dei dati
quadruplica il tempo
di elaborazione.
capito13new.pmd 16/07/2007, 17.01 532
ORDINAMENTO E RICERCA 533
Ripetiamo il procedimento, inserendo lelemento a[3], di valore 5, nella prima posizione
della porzione iniziale.
Infine, lelemento a[4], di valore 7, viene inserito nella posizione corretta e lordinamento
completo.
La classe seguente realizza lalgoritmo di ordinamento per inserimento.
public class InsertionSorter
{
/**
Costruisce un ordinatore per inserimento.
@param anArray larray da ordinare
*/
public InsertionSorter(int[] anArray)
{
a = anArray;
}
/**
Ordina larray gestito da questo ordinatore.
*/
public void sort()
{
for (int i = 1; i < a.length; i++)
{
int next = a[i];
// cerca la posizione in cui inserire, spostando
// in posizioni di indice superiore tutti gli elementi
// di valore maggiore
int j = i;
while (j > 0 && a[j 1] > next)
{
a[j] = a[j 1];
j;
}
// inserisci lelemento
a[j] = next;
}
}
private int[] a;
}
Quanto efficiente questo algoritmo? Indichiamo con n la dimensione dellarray, per il cui
ordinamento eseguiamo n 1 iterazioni. Durante la k-esima iterazione abbiamo una por-
zione gi ordinata di k elementi e dobbiamo inserirvi un nuovo elemento; ciascuno di tali
inserimenti richiede di visitare gli elementi della porzione iniziale ordinata finch non
stata trovata la posizione in cui inserire il nuovo elemento, dopodich dobbiamo spostare
verso posizioni di indice maggiore i rimanenti elementi della parte gi ordinata.
capito13new.pmd 16/07/2007, 17.01 533
534 CAPITOLO 13
Di conseguenza, vengono visitati k + 1 elementi dellarray, per cui il numero totale di
visite :
2 3
1
2
1 + + + =
+
n
n n ( )
Possiamo quindi concludere che lordinamento per inserimento un algoritmo O(n
2
), con
unefficienza dello stesso ordine di quella dellordinamento per selezione.
Lordinamento per inserimento ha, per, uninteressante caratteristica: le sue presta-
zioni sono O(n) se larray gi ordinato (si veda lEsercizio R13.3). Tale propriet utile
nei casi pratici, dal momento che spesso capita di dover ordinare insiemi di dati gi par-
zialmente ordinati.

Argomenti avanzati 13.2


O-grande, e
In questo capitolo abbiamo usato in modo un po approssimativo la notazione O-grande,
per descrivere il modo in cui cresce una funzione. Parlando in modo pi corretto, lespres-
sione f(n) = O(g(n)) significa che f non cresce pi velocemente di g, ma possibile che f
cresca molto pi lentamente, per cui tecnicamente corretto dire che f(n) = n
2
+ 5n 3
O(n
3
) o anche O(n
10
).
Gli informatici teorici hanno inventato ulteriori notazioni che descrivono in modo pi
accurato il modo in cui crescono le funzioni. Lespressione
f(n) = (g(n))
significa che f cresce almeno tanto velocemente quanto cresce g oppure, in modo pi for-
male: per tutti i valori di n maggiori di una certa soglia, si ha f(n)/g(n) C per un qualche
valore costante C (il simbolo la lettera omega maiuscola dellalfabeto greco). Ad
esempio, f(n) = n
2
+ 5n 3 (n
2
) o anche (n).
Lespressione
f(n) = (g(n))
significa che f e g crescono con la stessa velocit, cio vero sia che f(n) = O(g(n)) sia che
f(n) = (g(n)) (il simbolo la lettera theta maiuscola dellalfabeto greco).
La notazione fornisce la pi precisa descrizione dellandamento di una funzione
crescente. Ad esempio, f(n) = n
2
+ 5n 3 (n
2
) ma non (n) n (n
3
).
Le notazioni e sono molto importanti per effettuare unanalisi degli algoritmi con
una certa precisione, ma pratica comune parlare semplicemente di O-grande, pur fornen-
do per tale notazione la stima pi precisa possibile.

13.4 Ordinamento per fusione (MergeSort)


In questo paragrafo imparerete lalgoritmo di ordinamento per fusione, che molto pi
efficiente dellordinamento per selezione, anche se lidea su cui si basa molto semplice.
Supponiamo di avere un array di 10 numeri interi. Proviamo per una volta a essere ottimi-
sti e speriamo che la prima met dellarray sia gi perfettamente ordinata e che lo sia anche
la seconda met, come in questo caso:
Lordinamento per inserimento
un algoritmo O(n
2
).
capito13new.pmd 16/07/2007, 17.01 534
ORDINAMENTO E RICERCA 535
A questo punto facile fondere i due array ordinati in un solo array ordinato, semplice-
mente prelevando un nuovo elemento dal primo o dal secondo sottoarray, scegliendo ogni
volta lelemento pi piccolo:
In effetti, probabile che abbiate eseguito proprio questo tipo di fusione quando vi siete
trovati con un amico a dover ordinare una pila di fogli. Avete spartito la pila in due mucchi,
ciascuno di voi ha ordinato la sua met e poi avete fuso insieme i vostri risultati.
Tutto questo sar anche divertente, ma non sembra che possa risolvere il problema per
il computer, che si trova a dover ancora ordinare la prima e la seconda met dellarray,
perch non pu certo chiedere a qualche amico di dargli una mano. Scopriamo, per, che,
se il computer continua a suddividere larray in sottoarray sempre pi piccoli, ordinando
ciascuna met e fondendole poi insieme, i passaggi che deve eseguire sono assai meno
numerosi di quelli richiesti dallordinamento per selezione.
Proviamo a scrivere una classe MergeSorter che realizzi questa idea. Quando un og-
getto di tipo MergeSorter ordina un array, esso crea due array, ciascuno la met dellarray
originario, e li ordina ricorsivamente. Quindi, fonde insieme i due array ordinati:
public void sort()
{
if (a.length <= 1) return;
int[] first = new int[a.length / 2];
int[] second = new int[a.length first.length];
System.arraycopy(a, 0, first, 0, first.length);
System.arraycopy(a, first.length, second,
0, second.length);
MergeSorter firstSorter = new MergeSorter(first);
MergeSorter secondSorter = new MergeSorter(second);
firstSorter.sort(); firstSorter.sort(); firstSorter.sort(); firstSorter.sort(); firstSorter.sort();
secondSorter.sort(); secondSorter.sort(); secondSorter.sort(); secondSorter.sort(); secondSorter.sort();
merge(first, second); merge(first, second); merge(first, second); merge(first, second); merge(first, second);
}
Il metodo merge noioso ma abbastanza semplice: lo troverete nel codice che segue.
Lalgoritmo di ordinamento
per fusione ordina un array
dividendolo a met,
ordinando ricorsivamente
ciascuna met e fondendo,
poi, le due met ordinate.
capito13new.pmd 16/07/2007, 17.02 535
536 CAPITOLO 13
File MergeSorter.java
/**
Questa classe ordina un array, usando lalgoritmo
di ordinamento per fusione.
*/
public class MergeSorter
{
/**
Costruisce un ordinatore per fusione.
@param anArray larray da ordinare
*/
public MergeSorter(int[] anArray)
{
a = anArray;
}
/**
Ordina larray gestito da questo ordinatore
per fusione.
*/
public void sort()
{
if (a.length <= 1) return;
int[] first = new int[a.length / 2];
int[] second = new int[a.length first.length];
System.arraycopy(a, 0, first, 0, first.length);
System.arraycopy(a, first.length, second,
0, second.length);
MergeSorter firstSorter = new MergeSorter(first);
MergeSorter secondSorter = new MergeSorter(second);
firstSorter.sort();
secondSorter.sort();
merge(first, second);
}
/**
Fonde due array ordinati per generare larray che
deve essere ordinato da questo ordinatore per fusione.
@param first il primo array ordinato
@param second il secondo array ordinato
*/
private void merge(int[] first, int[] second)
{
// il prossimo elemento da considerare nel primo array
int iFirst = 0;
// il prossimo elemento da considerare nel secondo array
int iSecond = 0;
// la prossima posizione libera nellarray a
int j = 0;
// finch n iFirst n iSecond oltrepassano la fine,
// sposta in a lelemento minore
while (iFirst < first.length && iSecond < second.length)
{
if (first[iFirst] < second[iSecond])
{
capito13new.pmd 16/07/2007, 17.02 536
ORDINAMENTO E RICERCA 537
a[j] = first[iFirst];
iFirst++;
}
else
{
a[j] = second[iSecond];
iSecond++;
}
j++;
}
// notate che soltanto una delle due copiature
// seguenti viene eseguita
// copia in a tutti i valori rimasti nel primo array
System.arraycopy(first, iFirst, a, j,
first.length iFirst);
// copia in a tutti i valori rimasti nel secondo array
System.arraycopy(second, iSecond, a, j,
second.length iSecond);
}
private int[] a;
}
File MergeSortDemo.java
import java.util.Arrays;
/**
Questo programma applica lalgoritmo di ordinamento
per fusione a un array che contiene numeri casuali.
*/
public class MergeSortDemo
{
public static void main(String[] args)
{
int[] a = ArrayUtil.randomIntArray(20, 100);
System.out.println(Arrays.toString(a));
MergeSorter sorter = new MergeSorter(a);
sorter.sort();
System.out.println(Arrays.toString(a));
}
}
Visualizza (esempio):
[8, 81, 48, 53, 46, 70, 98, 42, 27, 76, 33, 24, 2, 76, 62, 89, 90, 5, 13, 21]
[2, 5, 8, 13, 21, 24, 27, 33, 42, 46, 48, 53, 62, 70, 76, 76, 81, 89, 90, 98]
Auto-valutazione
7. Perch soltanto una delle due invocazioni di arraycopy presenti al termine del meto-
do merge fa qualcosa?
8. Eseguite manualmente lalgoritmo di ordinamento per fusione sullarray 8 7 6 5 4 3 2 1.
capito13new.pmd 16/07/2007, 17.02 537
538 CAPITOLO 13
13.5 Analisi dellalgoritmo di ordinamento per fusione
Lalgoritmo di ordinamento per fusione sembra molto pi complesso dellalgoritmo di
ordinamento per selezione e ci si immagina che possa impiegare molto pi tempo per
eseguire tutte queste ripetute suddivisioni. Invece, i tempi che si registrano con lordina-
mento per fusione sono molto migliori di quelli dellordinamento per selezione:
n nn nn Ordinamento per fusione Ordinamento per selezione
(millisecondi) (millisecondi)
10 000 31 772
20 000 47 3051
30 000 62 6846
40 000 80 12 188
50 000 97 19 015
60 000 113 27 359
La Figura 2 mostra un grafico che mette a confronto i due insiemi di misurazioni delle
prestazioni: il miglioramento strepitoso. Per capirne il motivo, proviamo a stimare il
numero di visite agli elementi dellarray che sono necessarie per ordinare un array me-
diante lalgoritmo di ordinamento per fusione. Affrontiamo come prima cosa il processo
di fusione che si verifica dopo che la prima e la seconda met sono state ordinate.
Ciascun passo del processo di fusione aggiunge allarray a un elemento, che pu veni-
re da first o da second; nella maggior parte dei casi bisogna confrontare gli elementi
delle due met per stabilire quale prendere. Conteggiamo questa operazione come 3 visite
per ogni elemento (una per a e una ciascuna per first e second), ovvero 3n visite in
totale, essendo n la lunghezza dellarray a. Inoltre, allinizio dobbiamo copiare tutti gli
elementi dallarray a agli array first e second, rendendo necessarie altre 2n visite, per un
totale di 5n.
Se chiamiamo T(n) il numero di visite necessarie per ordinare un array di n elementi
mediante il processo di ordinamento per fusione, otteniamo:
T(n) = T(n/2) + T(n/2) + 5n
perch lordinamento di ciascuna met richiede T(n/2) visite. In realt, se n non pari,
abbiamo un sottoarray di dimensione (n 1)/2 e un altro di dimensione (n + 1)/2: sebbene
questo dettaglio si dimostrer irrilevante ai fini del risultato del calcolo, supporremo per
ora che n sia una potenza di 2, diciamo n = 2
m
. In questo modo tutti i sottoarray si possono
dividere in due parti uguali.
Sfortunatamente, la formula
T(n) = 2T(n/2) + 5n
non ci fornisce con chiarezza la relazione fra n e T(n). Per capire tale relazione, valutiamo
T(n/2) usando la stessa formula:
T(n/2) = 2T(n/4) + 5n/2
Quindi
T(n) = 2 2T(n/4) + 5n + 5n
capito13new.pmd 16/07/2007, 17.02 538
ORDINAMENTO E RICERCA 539
Facciamolo di nuovo:
T(n/4) = 2T(n/8) + 5n/4
quindi
T(n) = 2 2 2T(n/8) + 5n + 5n + 5n
Generalizzando da 2, 4, 8 a potenze arbitrarie di 2:
T(n) = 2
k
T(n/2
k
) + 5nk
Ricordiamo che abbiamo assunto n = 2
m
; di conseguenza, per k = m,
T(n) = 2
m
T(n/2
m
) + 5nm
= nT(1) + 5nm
= n + 5n log
2
n
Siccome n = 2
m
, abbiamo che m = log
2
(n).
Per determinare lordine di crescita della funzione, eliminiamo il termine di grado
inferiore, n, rimanendo con 5n log
2
(n). Eliminiamo il fattore costante 5. Si trascura di
solito anche la base del logaritmo, perch tutti i logaritmi sono correlati da un fattore
costante. Per esempio:
log
2
x = log
10
x/log
10
2 log
10
x 3.32193
Di conseguenza, possiamo dire che lordinamento per fusione un algoritmo O(n log(n)).
Figura 2
Tempo di esecuzione
dellordinamento per
fusione (rettangoli)
confrontato con il
tempo di esecuzione
dellordinamento per
selezione (cerchi)
capito13new.pmd 16/07/2007, 17.02 539
540 CAPITOLO 13
Lalgoritmo di ordinamento per fusione, con prestazioni O(n log(n)), migliore del-
lalgoritmo di ordinamento per selezione, avente prestazioni O(n
2
)? Ci potete scommette-
re che lo . Ricordate che con lalgoritmo O(n
2
) lordinamento di un milione di valori
richiedeva 100
2
= 10000 volte il tempo che era necessario per ordinare 10000 valori. Con
lalgoritmo O(n log(n)), il rapporto :
1000000 1000000
10000 10000
100
6
4
150
log
log
=

=
Supponiamo per un momento che per ordinare un array di 10000 numeri lordinamento
per fusione impieghi lo stesso tempo che impiega lordinamento per selezione, cio tre
quarti di secondo sulla nostra macchina di prova (in realt, molto pi veloce). Allora
impiegherebbe circa 0.75 150 secondi, ovvero meno di 2 minuti, per ordinare un milione
di numeri. Confrontate questo tempo con lordinamento per selezione, che ci metterebbe
pi di 2 ore per eseguire lo stesso compito. Come potete vedere, anche se vi servissero
diverse ore per imparare un algoritmo migliore, sarebbe tempo ben speso.
In questo capitolo abbiamo appena cominciato a scalfire la superficie di questo inte-
ressante argomento. Esistono molti algoritmi di ordinamento, alcuni dei quali hanno pre-
stazioni persino migliori di quelle dellordinamento per fusione, e la cui analisi pu essere
una bella sfida. Se state seguendo un corso di studi in informatica, rivedrete questi impor-
tanti argomenti in un corso successivo.
Tuttavia, quando scrivete programmi in Java, non avete bisogno di realizzare un vo-
stro algoritmo di ordinamento: la classe Arrays contiene metodi statici sort per ordinare
array di numeri interi e di numeri in virgola mobile. Ad esempio, potete ordinare un array
di numeri interi scrivendo semplicemente cos:
int[] a = ...;
Arrays.sort(a);
Tale metodo sort usa lalgoritmo Quicksort, sul quale troverete maggiori informazioni in
Argomenti avanzati 13.3.
Auto-valutazione
9. Sulla base dei dati temporali presentati nella tabella allinizio di questo paragrafo per
lalgoritmo di ordinamento per fusione, quanto tempo ci vuole per ordinare un array di
100 000 valori?
10. Immaginate che il vostro programma Java elabori larray double[]values. Come pen-
sate di ordinarlo?
Argomenti avanzati 13.3
Lalgoritmo Quicksort
Quicksort un algoritmo comunemente usato che ha il vantaggio, rispetto allordinamento
per fusione, di non aver bisogno di array temporanei per ordinare e fondere i risultati parziali.
Lalgoritmo quicksort, come lordinamento per fusione, si basa sulla strategia di di-
videre per vincere (divide and conquer, divide et impera in latino). Per ordinare la
porzione a[from] ... a[to] dellarray a, si dispongono dapprima gli elementi in modo che
nessun elemento nella porzione a[from] ... a[p] sia maggiore di elementi dellaltra porzione
a[p + 1] ... a[to]. Questo passo viene detto suddivisione o partizionamento della porzione.
Lordinamento per fusione
un algoritmo O(n log(n)).
La funzione n log(n) cresce
molto pi lentamente di n
2
.
La classe Arrays realizza
il metodo di ordinamento che
dovreste sempre usare nei
vostri programmi Java.
capito13new.pmd 16/07/2007, 17.02 540
ORDINAMENTO E RICERCA 541
Ad esempio, supponete di iniziare con questa porzione
Ecco una possibile suddivisione della porzione. Notate che le due parti non sono ancora
state ordinate.
Vedrete pi avanti come ottenere tale suddivisione. Nel prossimo passo, ordinate ciascuna
porzione, applicando ricorsivamente lo stesso algoritmo alle due porzioni. Ci ordina lin-
tera porzione originaria, perch il maggiore elemento della prima porzione al massimo
uguale al minore elemento della seconda porzione.
Quicksort implementato ricorsivamente, come segue:
public void sort(int from, int to)
{
if (from >= to) return;
int p = partition(from, to);
sort(from, p);
sort(p + 1, to);
}
Torniamo al problema di come suddividere una porzione di array. Scegliete un elemento
nella porzione e chiamatelo pivot (cardine). Esistono diverse varianti dellalgoritmo quick-
sort: nella pi semplice, sceglierete come pivot il primo elemento della porzione, a[from].
Ora create due regioni: a[from] ... a[i], contenente valori non maggiori del pivot, e a[j]
... a[to], contenente valori non minori del pivot. La regione a[i + 1] ... a[j 1]
contiene valori che non sono ancora stati analizzati (osservate la Figura 3). Allinizio, le
zone sinistra e destra sono vuote, cio i = from 1 e j = to + 1.
A questo punto incrementate i finch a[i] < pivot e decrementate j finch a[j] >
pivot. La Figura 4 mostra i e j quando il procedimento si arresta.
Ora scambiate i valori che si trovano nelle posizioni i e j, estendendo ancora una volta
entrambe le zone. Proseguite finch i < j. Ecco il codice per il metodo partition:
private int partition(int from, int to)
{
int pivot = a[from];
int i = from 1;
Figura 3
Suddividere
una porzione
capito13new.pmd 16/07/2007, 17.02 541
542 CAPITOLO 13
int j = to + 1;
while (i < j)
{
i++; while (a[i] < pivot) i++;
j; while (a[j] > pivot) j;
if (i < j) swap(i, j);
}
return j;
}
In media, lalgoritmo quicksort ha prestazioni O(n log(n)). Poich pi semplice, nella
maggior parte dei casi viene eseguito pi velocemente dellordinamento per fusione. C
un solo aspetto sfortunato nellalgoritmo quicksort: il suo comportamento nellesecuzione
di caso peggiore O(n
2
). Inoltre, se come elemento pivot viene scelto il primo elemento
della regione, il comportamento di caso peggiore si ha quando linsieme gi ordinato:
una situazione, in pratica, piuttosto frequente. Scegliendo con pi cura lelemento pivot,
possiamo rendere estremamente improbabile levenienza del caso peggiore: gli algoritmi
quicksort messi a punto in tal modo sono usati molto frequentemente, perch le loro
prestazioni sono generalmente eccellenti. Ad esempio, come gi detto, il metodo sort
della classe Arrays usa un algoritmo quicksort.
Un altro miglioramento che viene solitamente messo in atto consiste nellutilizzo
dellordinamento per inserimento quando larray di piccole dimensioni, perch il
numero totale di operazioni richieste dallordinamento per inserimento in tali casi
inferiore. La libreria Java usa questo accorgimento quando la lunghezza dellarray
inferiore a 7.

Note di cronaca 13.1


Il primo programmatore
Prima che esistessero le calcolatrici tascabili e i personal computer, navigatori e ingegneri
usavano addizionatrici meccaniche, regoli calcolatori e tavole di logaritmi e di funzioni
trigonometriche per accelerare i calcoli. Sfortunatamente, le tavole, i cui valori dovevano
essere calcolati a mano, erano notoriamente imprecise. Il matematico Charles Babbage
(1791-1871) ebbe lintuizione che se fosse stato possibile costruire una macchina che pro-
ducesse automaticamente tavole stampate, si sarebbero evitati sia gli errori di calcolo sia
quelli di composizione tipografica. Babbage si dedic a progettare una macchina a questo
scopo, che chiam Difference Engine, perch utilizzava differenze successive per calcola-
re funzioni polinomiali. Per esempio, considerate la funzione f(x) = x
3
. Scrivete i valori per
f(1), f(2), f(3) e cos di seguito. Poi scrivete le differenze fra valori successivi:
Figura 4
Estendere le porzioni
capito13new.pmd 16/07/2007, 17.02 542
ORDINAMENTO E RICERCA 543
1
7
8
19
27
37
64
61
125
91
216
Ripetete il processo, scrivendo le differenze fra valori successivi della seconda colonna, e
poi ripetetelo unaltra volta:
1
7
8 12
19 6
27 18
37 6
64 24
61 6
125 30
91
216
Ora le differenze sono costanti. Potete trovare i valori della funzione mediante uno schema
di addizioni: dovete conoscere i valori al bordo dello schema e la differenza costante.
Questo metodo era molto attraente, perch le macchine addizionatrici meccaniche gi si
conoscevano da tempo. Erano costituite da ruote dentate, con dieci denti per ruota, che
rappresentavano le cifre, e opportuni meccanismi per gestire il riporto da una cifra alla
successiva. Al contrario, le macchine moltiplicatrici meccaniche erano fragili e poco affi-
dabili. Babbage costru un prototipo del Difference Engine (Figura 5) che ebbe successo e,
Figura 5
Difference Engine
di Babbage
capito13new.pmd 16/07/2007, 17.02 543
544 CAPITOLO 13
con denaro suo e alcuni fondi messi a disposizione dal governo, pass a produrre la mac-
china per stampare le tavole. Tuttavia, per problemi di finanziamento e per le difficolt che
si incontrarono per costruire la macchina con la precisione meccanica che era necessaria,
non venne mai portata a termine.
Mentre stava lavorando al Difference Engine, Babbage concep unidea molto pi
grandiosa, che chiam Analytical Engine. Il Difference Engine era stato concepito per
eseguire un insieme limitato di calcoli: non era pi avanzato di una calcolatrice tascabile
dei nostri giorni. Ma Babbage si rese conto che una macchina del genere avrebbe potuto
essere resa programmabile immagazzinando programmi insieme ai dati. La memoria in-
terna dellAnalytical Engine doveva essere costituita da 1000 registri di 50 cifre decimali
ciascuno. Programmi e costanti dovevano essere memorizzati su schede perforate: una
tecnica che allepoca era molto diffusa sui telai per tessere stoffe decorate.
Ada Augusta, contessa di Lovelace (1815-1852), unica figlia di Lord Byron, fu amica
e protettrice di Charles Babbage. Ada Lovelace fu una delle prime persone a capire il
potenziale di una macchina del genere, non soltanto per calcolare tavole matematiche, ma
per elaborare dati che non fossero numeri. Viene considerata da molti il primo program-
matore del mondo. Il linguaggio di programmazione Ada, un linguaggio sviluppato per
essere utilizzato nei progetti del Dipartimento della Difesa degli USA (vedere Note di
cronaca 9.2) stato chiamato cos in suo onore.

13.6 Effettuare ricerche


Immaginate di aver bisogno di trovare il numero di telefono di un amico. Cercate il suo
nome nella guida del telefono e naturalmente lo trovate rapidamente, perch la guida del
telefono in ordine alfabetico. probabile che non abbiate mai pensato a quanto impor-
tante che la guida del telefono sia ordinata. Per apprezzare questo fatto, provate a immagi-
nare di avere fra le mani un numero di telefono e di aver bisogno di sapere a chi corrispon-
de. Potreste, naturalmente, chiamare quel numero, ma supponiamo che nessuno risponda
alla chiamata. Potreste scorrere la guida del telefono, un numero dopo laltro, fino a quan-
do trovate quello che vi interessa. Questo comporterebbe, ovviamente, unenorme quanti-
t di lavoro e dovreste essere davvero disperati per imbarcarvi in unimpresa del genere.
Questo esperimento mentale fa capire la differenza fra cercare in un insieme di dati ordi-
nati e cercare fra dati non ordinati. I prossimi due paragrafi esamineranno formalmente
questa differenza.
Se volete trovare un numero in una sequenza di valori che si presentano in un ordine
arbitrario, non potete fare nulla per accelerare la ricerca: dovete semplicemente scorrere
tutti gli elementi esaminandoli uno a uno fino a quando trovate una corrispondenza con
lelemento cercato, oppure arrivate in fondo allelenco. Questa si chiama ricerca lineare o
sequenziale.
Quanto tempo richiede una ricerca lineare? Nellipotesi che lelemento v sia presente
nellarray a di lunghezza n, la ricerca richiede in media la visita di n/2 elementi. Se lele-
mento non presente nellarray, bisogna visitare tutti gli elementi per verificarne lassen-
za. In ogni caso, una ricerca lineare un algoritmo O(n).
Ecco una classe che esegue una ricerca lineare in un array a di numeri interi per trova-
re il valore v: il metodo search restituisce lindice della prima corrispondenza trovata,
oppure 1 se v non presente in a.
Una ricerca lineare esamina
tutti i valori di un array finch
trova una corrispondenza
con quanto cercato, oppure
raggiunge la fine dellarray.
Una ricerca lineare trova
un valore in un array
in O(n) passi.
capito13new.pmd 16/07/2007, 17.02 544
ORDINAMENTO E RICERCA 545
File LinearSearcher.java
/**
Una classe per eseguire ricerche lineari in un array.
*/
public class LinearSearcher
{
/**
Costruisce loggetto di tipo LinearSearcher.
@param anArray un array di numeri interi
*/
public LinearSearcher(int[] anArray)
{
a = anArray;
}
/**
Trova un valore in un array usando lalgoritmo
di ricerca lineare.
@param v il valore da cercare
@return lindice in cui si trova il valore, oppure 1
se non presente nellarray
*/
public int search(int v)
{
for (int i = 0; i < a.length; i++)
{
if (a[i] == v)
return i;
}
return 1;
}
private int[] a;
}
File LinearSearchDemo.java
import java.util.Arrays;
import java.util.Scanner;
/**
Questo programma utilizza lalgoritmo di ricerca lineare.
*/
public class LinearSearchDemo
{
public static void main(String[] args)
{
int[] a = ArrayUtil.randomIntArray(20, 100);
System.out.println(Arrays.toString(a));
LinearSearcher searcher = new LineareSearcher(a);
Scanner in = new Scanner(System.in);
boolean done = false;
while (!done)
{
capito13new.pmd 16/07/2007, 17.02 545
546 CAPITOLO 13
System.out.print(Enter number to search for,
+ 1 to quit: );
int n = in.nextInt();
if (n == 1)
done = true;
else
{
int pos = searcher.search(n);
System.out.println(Found in position + pos);
}
}
}
}
Visualizza (esempio):
[46, 99, 45, 57, 64, 95, 81, 69, 11, 97, 6, 85, 61, 88, 29, 65, 83, 88, 45, 88]
Enter number to search for, 1 to quit: 11 11 11 11 11
Found in position 8
Auto-valutazione
11. Immaginate di dover cercare un numero telefonico in un insieme di 1 000 000 di dati.
Quanti dati pensate di dover esaminare, mediamente, per trovare il numero?
12. Perch nel metodo search non si pu usare un ciclo generalizzato di questo tipo:
for (int element : a)?
13.7 Ricerca binaria
Cerchiamo ora un elemento in una sequenza di dati che sia stata precedentemente ordina-
ta. Naturalmente potremmo ancora eseguire una ricerca lineare, ma possiamo far di me-
glio, come si vedr.
Consideriamo il seguente array ordinato a: linsieme dei dati
e noi vorremmo sapere se il valore 15 si trova in tale insieme. Restringiamo la nostra
ricerca chiedendoci se il valore si trova nella prima o nella seconda met dellarray. Lul-
timo valore nella prima met dellinsieme, a[3], 9. pi piccolo del valore che stiamo
cercando, quindi dovremo cercare nella seconda met dellinsieme, cio nella porzione
evidenziata in grigio
Ora, lultimo valore della prima met di questa porzione 17; quindi il valore che cerchia-
mo deve essere localizzato nella zona qui evidenziata
capito13new.pmd 16/07/2007, 17.02 546
ORDINAMENTO E RICERCA 547
Lultimo valore della prima met di questa brevissima porzione 12, che pi piccolo del
valore che stiamo cercando, per cui dobbiamo esaminare la seconda met:
banale constatare che non abbiamo trovato il numero cercato, perch 15 17. Se voles-
simo inserire 15 nella sequenza, avremmo dovuto inserirlo appena prima di a[5].
Questo processo di ricerca si chiama ricerca binaria o per bisezione perch a ogni
passo dimezziamo la dimensione della zona da esplorare: tale dimezzamento funziona
soltanto perch sappiamo che la sequenza dei valori ordinata.
La classe seguente realizza la ricerca binaria allinterno di un array ordinato di numeri
interi. Il metodo search restituisce la posizione dellelemento cercato se la ricerca ha
successo, oppure 1 se v non viene trovato in a.
File BinarySearcher.java
/**
Una classe per eseguire ricerche binarie in un array.
*/
public class BinarySearcher
{
/**
Costruisce un oggetto di tipo BinarySearcher.
@param anArray un array ordinato di numeri interi
*/
public BinarySearcher(int[] anArray)
{
a = anArray;
}
/**
Trova un valore in un array ordinato,
utilizzando lalgoritmo di ricerca binaria.
@param v il valore da cercare
@return lindice della posizione in cui si trova
il valore, oppure 1 se non presente
*/
public int search(int v)
{
int low = 0;
int high = a.length 1;
while (low <= high)
{
int mid = (low + high) / 2;
int diff = a[mid] v;
if (diff == 0) // a[mid] == v
return mid;
else if (diff < 0) // a[mid] < v
low = mid + 1;
else
high = mid 1;
}
Una ricerca binaria cerca
un valore in un array ordinato
determinando se il valore
si trova nella prima o nella
seconda met dellarray,
ripetendo poi la ricerca
in una delle due met.
capito13new.pmd 16/07/2007, 17.02 547
548 CAPITOLO 13
return 1;
}
private int[] a;
}
Proviamo a stabilire quante visite di elementi dellarray sono necessarie per portare a
termine una ricerca. Possiamo usare la stessa tecnica che abbiamo adottato per lanalisi
dellordinamento per fusione e osservare che, poich esaminiamo lelemento di mezzo,
che conta come una sola visita, e poi esploriamo o il sottoarray di sinistra o quello di
destra, possiamo scrivere:
T(n) = T(n/2) + 1
Utilizzando la stessa equazione, si ha:
T(n/2) = T(n/4) + 1
Inserendo questo risultato nellequazione originale, otteniamo:
T(n) = T(n/4) + 2
Generalizzando, si ottiene:
T(n) = T(n/2
k
) + k
Come nellanalisi dellordinamento per fusione, facciamo lipotesi semplificativa che n
sia una potenza di 2, n = 2
m
, dove m log2(n). Otteniamo quindi
T(n) = 1 + log
2
(n)
Di conseguenza, la ricerca binaria un algoritmo O(log(n)).
Questo risultato ha senso anche dal punto di vista intuitivo. Supponiamo che n valga
100: dopo ciascuna ricerca, la dimensione dellintervallo da esplorare viene divisa a met,
dando luogo a ricerche in array aventi le seguenti dimensioni: 50, 25, 12, 6, 3 e 1. Dopo
sette confronti abbiamo finito: questo coincide con la nostra formula, dal momento
che log
2
(100) 6.64386 e, in effetti, la pi piccola potenza intera di 2 maggiore di 100
2
7
= 128.
Dal momento che una ricerca binaria tanto pi veloce di una ricerca lineare, vale la
pena ordinare prima un array e poi ricorrere a una ricerca binaria? Dipende. Se effettuate
una sola ricerca nellarray, allora pi efficiente sostenere solo il costo O(n) di una ricerca
lineare invece del costo O(n log(n)) di un ordinamento e quello O(log(n)) di una ricerca
binaria. Se, per, si devono eseguire molte ricerche sullo stesso array, allora vale davvero
la pena di eseguire prima lordinamento.
La classe Arrays contiene un metodo statico binarySearch che realizza lalgoritmo
di ricerca binaria, a dire il vero con un utile miglioramento: se un valore non viene trovato
nellarray, il valore restituito non 1, ma k1, dove k la posizione prima della quale
andrebbe inserito lelemento. Ad esempio:
int[] a = { 1, 4, 9 };
int v = 7;
Una ricerca binaria trova
un valore in un array
eseguendo O(log(n)) passi.
capito13new.pmd 16/07/2007, 17.02 548
ORDINAMENTO E RICERCA 549
int pos = Arrays.binarySearch(a, v);
// restituisce 3; v andrebbe inserito prima della posizione 2
Auto-valutazione
13. Immaginate di dover cercare un valore in un array ordinato di 1 000 000 elementi. Usan-
do lalgoritmo di ricerca binaria, quanti elementi pensate di dover esaminare, media-
mente, per trovare il valore che cercate?
14. Perch utile che il metodo Arrays.binarySearch segnali la posizione in cui andrebbe
inserito un elemento che non stato trovato?
15. Perch Arrays.binarySearch restituisce k1 e non k per segnalare che un valore non
presente e che andrebbe inserito prima della posizione k?
13.8 Ordinare dati veri
In questo capitolo abbiamo visto come ordinare array di numeri interi ed effettuare ricer-
che in essi. Naturalmente, nella programmazione reale raro che si debbano fare ricerche
in un insieme di numeri interi. Tuttavia, facile modificare queste tecniche per esplorare
dati veri.
La classe Arrays contiene un metodo statico sort che in grado di ordinare array di
oggetti, ma la classe Arrays non sa come confrontare oggetti di tipo arbitrario. Immagina-
te, ad esempio, di dover ordinare array di oggetti di tipo Coin: potreste ordinarli in base al
loro nome oppure in base al loro valore e il metodo Arrays.sort non pu prendere questa
decisione per voi, per cui richiede che gli oggetti appartengano a classi che realizzano
linterfaccia Comparable, che ha un unico metodo:
public interface Comparable
{
int compareTo(Object otherObject);
}
Linvocazione
a.compareTo(b)
deve restituire un numero negativo se a precede b, 0 se a e b sono uguali e un numero
positivo se a segue b.
Molte classi della libreria standard di Java, come String e Date, realizzano linterfac-
cia Comparable.
Potete realizzare linterfaccia Comparable anche nelle vostre classi. Per esempio, per
ordinare una raccolta di monete, la classe Coin dovrebbe realizzare questa interfaccia e
definire un metodo compareTo:
public class Coin implements Comparable
{
public int compareTo(Object otherObject)
{
Coin other = (Coin) otherObject;
if (value < other.value) return 1;
if (value == other.value) return 0;
return 1;
Il metodo sort della classe
Arrays ordina oggetti
di classi che realizzano
linterfaccia Comparable.
capito13new.pmd 16/07/2007, 17.02 549
550 CAPITOLO 13
}
. . .
}
Quando realizzate il metodo compareTo dellinterfaccia Comparable, dovete essere certi
che il metodo definisca una relazione dordine totale, con le tre seguenti propriet:
Antisimmetrica: Se a.compareTo(b) 0, allora b.compareTo(a) 0
Riflessiva: a.compareTo(a) = 0
Transitiva: Se a.compareTo(b) 0 e b.compareTo(c) 0, allora a.compareTo(c) 0
Una volta che la vostra classe Coin realizza linterfaccia Comparable, potete semplice-
mente usare un array di monete come argomento del metodo Arrays.sort:
Coin[] coins = new Coin[n];
// aggiungi monete
...
Arrays.sort(coins);
Se le monete sono memorizzate in un oggetto di tipo ArrayList, utilizzate invece il meto-
do Collections.sort, che usa lalgoritmo di ordinamento per fusione:
ArrayList<Coin> coins = new ArrayList<Coin>();
// aggiungi monete
...
Collections.sort(coins);
In pratica, dovreste usare i metodi di ordinamento e ricerca presenti nelle classi Arrays e
Collections, e non quelli scritti da voi. Gli algoritmi della libreria sono stati ben collau-
dati e ottimizzati, per cui lobiettivo principale di questo capitolo non stato quello di
insegnarvi a realizzare praticamente algoritmi di ordinamento e ricerca. Al contrario, ave-
te appreso una cosa pi importante: algoritmi diversi possono avere prestazioni ben diver-
se, per cui utile conoscere meglio la progettazione e lanalisi di algoritmi.
Auto-valutazione
16. Perch il metodo Arrays.sort non pu ordinare un array di oggetti di tipo Rectangle?
17. Cosa bisogna fare per ordinare in ordine di saldo crescente un array di oggetti di tipo
BankAccount?
Errori comuni 13.1
Il metodo compareTo compareTo compareTo compareTo compareTo pu restituire qualsiasi valore intero, non solo 1, 0 o 1
Linvocazione a.compareTo(b) pu restituire qualsiasi numero intero negativo per segna-
lare che a precede b, non necessariamente il valore 1. Di conseguenza, la verifica
if (a.compareTo(b) == 1) // ERRORE !
, in generale, errata. Dovete scrivere, invece
if (a.compareTo(b) < 0) // va bene
La classe Collections
contiene un metodo sort che
in grado di ordinare vettori.
capito13new.pmd 16/07/2007, 17.02 550
ORDINAMENTO E RICERCA 551
Perch mai un metodo compareTo dovrebbe voler restituire un numero diverso da 1, 0 o
1? A volte, comodo restituire semplicemente la differenza tra due numeri interi. Ad
esempio, il metodo compareTo della classe String confronta i caratteri che si trovano in
posizioni corrispondenti:
char c1 = charAt(i);
char c2 = other.charAt(i);
Se i caratteri sono diversi, allora il metodo pu semplicemente restituire la loro differenza:
if (c1 != c2) return c1 c2;
Se c1 precede c2, questa differenza certamente un numero negativo, ma non necessa-
riamente il numero 1.

Argomenti avanzati 13.4


Linterfaccia Comparable parametrica
A partire dalla versione 5.0 di Java, linterfaccia Comparable un tipo parametrico, simile
a ArrayList:
public interface Comparable<T>
{
int compareTo(T other);
}
Il parametro specifica il tipo degli oggetti che una certa classe accetta per fare confronti e,
solitamente, si tratta della classe stessa. Ad esempio, la classe Coin realizzer probabil-
mente linterfaccia Comparable<Coin>, in questo modo:
public class Coin implements Comparable<Coin> <Coin> <Coin> <Coin> <Coin>
{
. . .
public int compareTo(Coin Coin Coin Coin Coin other)
{
if (value < other.value) return 1;
if (value == other.value) return 0;
return 1;
}
. . .
}
Indicare il tipo di parametro ha un vantaggio davvero significativo: non c bisogno si
usare un cast per convertire un parametro di tipo Object nel tipo desiderato.

Argomenti avanzati 13.5


Linterfaccia Comparator Comparator Comparator Comparator Comparator
A volte si vuole ordinare un array o un vettore di oggetti che non appartengono a una
classe che realizza linterfaccia Comparable, oppure si vuole ordinare larray in un modo
diverso da quello indotto dal metodo compareTo: ad esempio, pu darsi che si vogliano
ordinare monete per nome invece che per valore.
capito13new.pmd 16/07/2007, 17.02 551
552 CAPITOLO 13
Non volete dover modificare il codice di una classe soltanto per poter invocare
Arrays.sort: esiste, fortunatamente, unalternativa. Una versione del metodo Arrays.sort
non richiede che gli oggetti appartengano a una classe che realizza linterfaccia Compar-
able: potete fornire oggetti di qualsiasi tipo, ma dovete anche fornire un comparatore di
oggetti, che ha il compito, appunto, di confrontare gli oggetti che volete ordinare. Logget-
to comparatore deve appartenere a una classe che realizzi linterfaccia Comparator, che ha
un unico metodo, compare, che confronta due oggetti.
A partire dalla versione 5.0 di Java, linterfaccia Comparator un tipo parametrico, il
cui parametro specifica il tipo dei parametri del metodo compare. Ad esempio, linterfac-
cia Comparator<Coin> questa:
public interface Comparator<Coin>
{
int compare(Coin a, Coin b);
}
Se comp un esemplare di una classe che realizza Comparator<Coin>, linvocazione
comp.compare(a, b)
deve restituire un numero negativo se a precede b, 0 se a e b sono uguali, e un numero
positivo se a segue b.
Ad esempio, ecco una classe Comparator per monete:
public class CoinComparator implements Comparator<Coin>
{
public int compare(Coin a, Coin b)
{
if (a.getValue() < b.getValue()) return 1:
if (a.getValue() == b.getValue()) return 0;
return 1;
}
}
Per ordinare un array di monete in base al loro valore, invocate:
Arrays.sort(coins, new CoinComparator());

Riepilogo del capitolo


1. Lalgoritmo di ordinamento per selezione ordina un ar-
ray cercando ripetutamente lelemento minore della re-
gione terminale non ancora ordinata e spostando tale
elemento allinizio della regione stessa.
2. Gli informatici teorici usano la notazione O-grande f(n)
= O(g(n)) per esprimere il fatto che la funzione f non
aumenta pi rapidamente della funzione g.
3. Lordinamento per selezione un algoritmo O(n
2
): il
raddoppio della dimensione dellinsieme dei dati qua-
druplica il tempo di elaborazione.
4. Lordinamento per inserimento un algoritmo O(n
2
).
5. Lalgoritmo di ordinamento per fusione ordina un array
dividendolo a met, ordinando ricorsivamente ciascuna
met e fondendo, poi, le due met ordinate.
6. Lordinamento per fusione un algoritmo O(n
log(n)). La funzione n log(n) cresce molto pi lenta-
mente di n
2
.
7. La classe Arrays realizza il metodo di ordinamento che
dovreste sempre usare nei vostri programmi Java.
8. Una ricerca lineare esamina tutti i valori di un array fin-
ch trova una corrispondenza con quanto cercato, oppu-
re raggiunge la fine dellarray.
capito13new.pmd 16/07/2007, 17.02 552
ORDINAMENTO E RICERCA 553
9. Una ricerca lineare trova un valore in un array in O(n)
passi.
10. Una ricerca binaria cerca un valore in un array ordinato
determinando se il valore si trova nella prima o nella se-
conda met dellarray, ripetendo poi la ricerca in una
delle due met.
11. Una ricerca binaria trova un valore in un array eseguen-
do O(log(n)) passi.
12. Il metodo sort della classe Arrays ordina oggetti di
classi che realizzano linterfaccia Comparable.
13. La classe Collections contiene un metodo sort che
in grado di ordinare vettori.
Ulteriori letture
[1] Michael T. Goodrich e Roberto Tamassia: Data Structures and Algorithms in Java, 3rd
edition, John Wiley & Sons, 2003.
Classi, oggetti e metodi presentati nel capitolo
java.lang.Comparable<T>
compareTo
java.lang.System
currentTimeMillis
java.util.Arrays
binarySearch
sort
toString
Esercizi di ripasso
** Esercizio R13.1. Attenzione al rischio di errori per scarto di 1. Scrivendo lalgoritmo di ordi-
namento per selezione del Paragrafo 13.1, un programmatore deve stare attento a scegliere <
invece di <=, a.length invece di a.length 1 e from invece di from + 1. Questo un fertile
terreno di coltura per la proliferazione degli errori per scarto di 1. Eseguite passo per passo il
codice dellalgoritmo con array di lunghezza 0, 1, 2 e 3 e controllate accuratamente che tutti i
valori degli indici siano corretti.
* Esercizio R13.2. Qual la differenza fra cercare e ordinare?
** Esercizio R13.3. Per le seguenti espressioni, qual lordine di grandezza della crescita di
ciascuna?
a. n
2
+ 2n + 1
b. n
10
+ 9n
9
+ 20n
8
+ 145n
7
c. (n + 1)
4
d. (n
2
+ n)
2
e. n + 0.001n
3
f. n
3
1000n
2
+ 10
9
g. n + log (n)
h. n
2
+ n log (n)
i. 2
n
+ n
2
j. (n
3
+ 2n)/(n
2
+ 0.75)
java.util.Collections
binarySearch
sort
java.util.Comparator<T>
compare
capito13new.pmd 23/07/2007, 10.26 553
554 CAPITOLO 13
* Esercizio R13.4. Abbiamo calcolato che il numero effettivo di visite nellalgoritmo di ordina-
mento per selezione
T(n) = 1/2n
2
+ 5/2n 3
Abbiamo poi stabilito che questo metodo caratterizzato da una crescita O(n
2
). Calcolate i
rapporti effettivi
T(2000)/T(1000)
T(4000)/T(1000)
T(10000)/T(1000)
e confrontateli con
f(2000)/f(1000)
f(4000)/f(1000)
f(10000)/f(1000)
dove f(n) = n
2
* Esercizio R13.5. Supponiamo che lalgoritmo A impieghi 5 secondi per gestire un insieme di
1000 dati. Se lalgoritmo A un algoritmo O(n), quanto tempo impiegher per gestire un insie-
me di 2000 dati? E uno di 10000?
** Esercizio R13.6. Supponiamo che un algoritmo impieghi 5 secondi per gestire un insieme di
1000 dati. Riempite la tabella seguente, che mostra la crescita approssimativa del tempo di
esecuzione in funzione della complessit dellalgoritmo.
O(n) O(n
2
) O(n
3
) O(n log n) O(2
n
)
1000 5 5 5 5 5
2000
3000 45
10000
Per esempio, dal momento che 3000
2
/1000
2
= 9, se lalgoritmo fosse O(n
2
) impiegherebbe un
tempo 9 volte superiore, cio 45 secondi, per gestire un insieme di 3000 dati.
** Esercizio R13.7. Ordinate le seguenti espressioni O-grande in ordine crescente.
O(n)
O(n
3
)
O(n
n
)
O(log (n))
O(n
2
log (n))
O(n log (n))
O(2
n
)
O n ( )
O n n ( )
O(n
log(n)
)
capito13new.pmd 16/07/2007, 17.02 554
ORDINAMENTO E RICERCA 555
* Esercizio R13.8. Qual landamento O-grande dellalgoritmo standard che trova il valore mi-
nimo di un array? E di quello che trova sia il minimo che il massimo?
* Esercizio R13.9. Qual landamento O-grande del seguente metodo?
public static int count(int[] a, int c)
{
int count = 0;
for (int i = 0; i < a.length; i++)
{
if (a[i] == c) count++;
}
return count;
}
** Esercizio R13.10. Il vostro compito consiste nel togliere tutti i duplicati da un array. Per esem-
pio, se larray contiene i valori
4 7 11 4 9 5 11 7 3 5
allora dovrebbe essere modificato in modo da contenere
4 7 11 9 5 3
Ecco un semplice algoritmo per risolvere il problema. Esaminate a[i] e contate quante volte
ricorre in a: se il conteggio maggiore di 1, eliminatelo. Qual il tasso di crescita del tempo di
esecuzione di questo algoritmo?
** Esercizio R13.11. Considerate il seguente algoritmo per eliminare tutti i duplicati da un array.
Ordinate larray; per ciascuno dei suoi elementi, esaminate il suo vicino per vedere se presen-
te pi di una volta; in caso positivo, eliminatelo. Questo algoritmo pi veloce di quello del-
lesercizio precedente?
*** Esercizio R13.12. Mettete a punto un algoritmo O(n log(n)) per togliere i duplicati da un array
nel caso in cui larray risultante debba avere lo stesso ordinamento di quello originale.
*** Esercizio R13.13. Perch lalgoritmo di ordinamento per inserimento significativamente pi
veloce dellordinamento per selezione quando larray gi ordinato?
*** Esercizio R13.14. Considerate la seguente modifica che migliora le prestazioni dellalgoritmo
di ordinamento per inserimento visto in Argomenti avanzati 13.1. Per ogni elemento dellarray,
invocate Arrays.binarySearch per determinare la posizione in cui va inserito. Questo miglio-
ramento ha un impatto significativo sullefficienza dellalgoritmo?
Esercizi di programmazione
* Esercizio P13.1. Modificate lalgoritmo di ordinamento per selezione in modo da ordinare un
array di numeri interi in ordine decrescente.
* Esercizio P13.2. Modificate lalgoritmo di ordinamento per selezione in modo da ordinare un
array di monete in base al loro valore.
** Esercizio P13.3. Scrivete un programma che generi automaticamente la tabella dei tempi delle
esecuzioni di prova dellordinamento per selezione. Il programma deve chiedere i valori mini-
mo e massimo di n e il numero di misurazioni, per poi attivare tutte le esecuzioni di prova.
capito13new.pmd 16/07/2007, 17.02 555
556 CAPITOLO 13
* Esercizio P13.4. Modificate lalgoritmo di ordinamento per fusione in modo da ordinare un
array di stringhe in ordine lessicografico.
*** Esercizio P13.5. Scrivete un programma per consultare lelenco del telefono. Leggete un insie-
me di dati di 1000 nomi e numeri di telefono da un file che contiene i numeri in ordine casuale.
Gestite la ricerca in base al nome e anche in base al numero di telefono. Utilizzate una ricerca
binaria per entrambe le consultazioni.
** Esercizio P13.6. Scrivete un programma che misuri le prestazioni dellalgoritmo di ordina-
mento per inserimento descritto in Argomenti avanzati 13.1.
*** Esercizio P13.7. Scrivete un programma che ordini un esemplare di ArrayList<Coin> in ordi-
ne decrescente, in modo che la moneta di valore maggiore si trovi allinizio del vettore. Usate
una classe che implementa Comparator.
** Esercizio P13.8. Considerate lalgoritmo di ricerca binaria del Paragrafo 13.7: se non trova
lelemento cercato, il metodo search restituisce 1. Modificate il metodo in modo che, se a non
viene trovato, venga restituito invece il valore k1, dove k la posizione prima della quale
lelemento dovrebbe essere inserito (che il comportamento di Arrays.binarySearch).
** Esercizio P13.9. Realizzate senza ricorsione il metodo sort dellalgoritmo di ordinamento per
fusione, nellipotesi che la lunghezza dellarray sia una potenza di 2. Prima fondete le regioni
adiacenti di dimensione 1, poi le regioni adiacenti di dimensione 2, quindi le regioni adiacenti
di dimensione 4 e cos via.
*** Esercizio P13.10. Realizzate senza ricorsione il metodo sort dellalgoritmo di ordinamento
per fusione, nellipotesi che la lunghezza dellarray sia un numero arbitrario. Procedete fon-
dendo regioni adiacenti la cui dimensione sia una potenza di 2 e fate attenzione allultima
regione, che pu avere dimensione inferiore.
*** Esercizio P13.11. Usate lordinamento per inserimento e la ricerca binaria dellEsercizio P13.8
per ordinare un array secondo quanto descritto nellEsercizio R13.14. Realizzate tale algoritmo
e misuratene le prestazioni.
* Esercizio P13.12. Scrivete una classe Person che realizzi linterfaccia Comparable, confron-
tando persone in base al loro nome. Chiedete allutente di inserire dieci nomi e generate dieci
oggetti di tipo Person. Usando il metodo compareTo, determinate la prima e lultima persona
dellinsieme, e stampatele.
** Esercizio P13.13. Ordinate un vettore di stringhe in ordine crescente di lunghezza. Suggeri-
mento: fornite un oggetto di tipo Comparator.
*** Esercizio P13.14. Ordinate un vettore di stringhe in ordine crescente di lunghezza, in modo
che stringhe della stessa lunghezza vengano disposte in ordine lessicografico. Suggerimento:
Fornite un oggetto di tipo Comparator.
Progetti di programmazione
*** Progetto P13.1. Scrivete un programma per gestire unagenda di appuntamenti. Create una
classe Appointment che memorizzi una descrizione dellappuntamento, il giorno dellappunta-
mento, lora di inizio e lora di fine. Il programma deve conservare gli appuntamenti in un
vettore ordinato. Gli utenti possono aggiungere appuntamenti e stampare tutti gli appuntamenti
capito13new.pmd 16/07/2007, 17.02 556
ORDINAMENTO E RICERCA 557
di un dato giorno. Quando viene aggiunto un nuovo appuntamento, utilizzate una ricerca bina-
ria per stabilire in quale posizione del vettore va inserito. Non aggiungetelo se in conflitto con
altri appuntamenti.
***G Progetto P13.2. Realizzate una animazione grafica degli algoritmi di ordinamento e di ricerca.
Costruite un array e inseritevi un insieme di numeri casuali compresi tra 1 e 100, poi disegnate
ciascun elemento dellarray sotto forma di barretta verticale, come nella Figura 6. Ogni volta
che lalgoritmo modifica larray, mostrate una finestra di dialogo e attendete che lutente prema
il relativo pulsante, poi invocate il metodo repaint.
Realizzate le animazioni dellordinamento per selezione, dellordinamento per fusione e
della ricerca binaria. Nellanimazione della ricerca binaria evidenziate lelemento in fase di
ispezione e i valori di from e to.
Figura 6
Animazione grafica
capito13new.pmd 16/07/2007, 17.02 557
capito13new.pmd 16/07/2007, 17.02 558