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