Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
=
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.
=
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.