Linguaggio C - Dipartimento di Ingegneria dell`Informazione

Linguaggio C
Problemi di Ricerca e
Ordinamento: Algoritmi e
Complessità.
1
Complessità degli Algoritmi
•
Si definisce Complessità di un Algoritmo C(A) la
funzione dei parametri rilevanti per A che
determina il numero di operazioni necessarie per
l’esecuzione di A. Quindi:
C ( A) = f (n1 , n2 ,..., nk )
•
Questo modello non dipende dal tipo di Hardware
o implementazione dell’Algoritmo (Software), ma si
concentra esclusivamente sulla natura del
Problema che viene affrontato.
2
Complessità degli Algoritmi
•
•
•
In generale, l’unico parametro rilevante (o
comunque il più significativo) è la dimensione del
data set su cui lavora l’Algoritmo (ad esempio: il
numero di elementi memorizzati in un vettore);
E’ possibile indicare degli ordini (o classi) di
complessità principali ai quali in genere gli
Algoritmi appartengono;
Tra questi ordini principali, vale la seguente
relazione di dominanza:
α
Θ(1) < Θ(ln 2 (n)) < Θ(n ) < Θ(2
β *n
) ∀α, β > 0
3
Problema della Ricerca
•
•
Dato un insieme di informazioni (semplici o
strutturate) memorizzate nella Struttura Dati D
(Vettore, Matrice, Lista, Albero, …) l’obiettivo è
quello di determinare se un dato elemento “target”
k appartiene o meno all’insieme.
Due possibili approcci sono:
– La Ricerca Sequenziale,
– La Ricerca Binaria.
4
Ricerca Sequenziale
•
•
•
•
Quando non esistono ipotesi di alcun tipo sui valori
memorizzati in D, l’unica possibilità è quella di scandire D
elemento per elemento.
Appena il valore target k viene trovato la ricerca termina con
successo.
Se l’insieme è stato esaminato tutto senza aver trovato k la
ricerca termina con fallimento.
La Ricerca Sequenziale è un approccio possibile per tutte le
Strutture Dati fin qui esaminate, e la complessità esprime il
numero massimo di confronti che devono essere effettuati, che
in questo caso è direttamente proporzionale alla dimensione n
del data set, quindi:
Θ(n)
5
Ricerca Sequenziale
typedef int boolean;
#define TRUE 1
#define FALSE 0
#define N … //Dimensione del Vettore
…
int V[N];
…
boolean Ric_Seq(int *V, int k)
{
boolean Trovato = FALSE;
int i = 0;
while (!Trovato && i<N) {
if (V[i]==k) Trovato = TRUE;
else i++;
}
return Trovato;
}
6
Ricerca Binaria
•
•
Se i valori in D sono ordinati, allora l’esito di un solo test può
escludere più di un elemento di D dallo spazio della ricerca
(si pensi all’elenco telefonico…);
Inoltre, se la Struttura Dati D è un Vettore, diviene
conveniente applicare la cosiddetta Ricerca binaria (o
dicotomica):
– Il target k viene confrontato con l’elemento di posizione
mediana (ossia D[n/2]),
– Se è “uguale” la ricerca termina con successo, altrimenti
• Se è “maggiore” la ricerca prosegue nella metà destra
di D,
• Altrimenti la ricerca prosegue nella metà sinistra di D.
7
Ricerca Binaria
•
•
•
•
Ogni volta che il valore target k viene confrontato con il valore
mediano di D, l’insieme sul quale proseguire la ricerca si
dimezza.
Il numero massimo di confronti coincide con l’altezza di un
Albero binario bilanciato contenente lo stesso numero n di
elementi di D,
L’altezza di un Albero binario bilanciato è legata alla
profondità massima dell’albero stesso, che cresce in maniera
logaritmica su n,
Segue quindi che la complessità dell’Algoritmo di Ricerca
Binaria è direttamente proporzionale al logaritmo sul numero
di elementi n contenuti nella struttura D, ossia:
Θ(log 2 (n))
8
Ricerca Binaria
•
•
•
•
L’Algoritmo di Ricerca Binaria presenta una complessità
logaritmica solo in determinate condizioni, ad esempio:
– Utilizzo in un Vettore ordinato,
– Utilizzo in un Albero binario di ricerca bilanciato.
In una Lista, ad esempio, non essendo possibile
accedere ai dati in tempo costante (Θ(1)), la complessità
degenera a quella lineare (Θ(n)) indipendentemente
dall’ordinamento.
In un Albero binario di ricerca non bilanciato, la proprietà
relativa alla profondità massima non è in generale
rispettata, e questo può portare la complessità, nei casi
limite, a degenerare a quella lineare.
Esistono comunque delle tecniche che permettono di
preservare il bilanciamento di un Albero binario di ricerca
durante l’inserimento dei dati.
9
Ricerca Binaria in un Vettore
typedef int boolean;
#define TRUE 1
#define FALSE 0
#define N … //Dimensione del Vettore
…
int V[N];
boolean Trovato;
…
Trovato = Ric_Bin(V,N,k);
…
boolean Ric_Bin(int *V, int n, int k)
{
if (n>0) {
if (k == V[n/2]) return TRUE;
else
if (k < V[n/2]) return Ric_Bin(V,n/2,k);
else return Ric_Bin(&V[n/2+1],n-n/2-1,k);
}
else return FALSE;
}
10
Ordinamento
•
Si possono individuare due possibili approcci:
– Mantenimento dell’ordine di un insieme,
– Creazione di un insieme ordinato a partire da uno non
ordinato.
•
Nel primo caso, si opera in maniera tale da preservare
l’ordine man mano che vengono aggiunti nuovi elementi
nell’insieme già ordinato (ordinamento per inserzione).
La complessità dell’ordinamento per inserzione dipende
strettamente dal tipo di struttura dati utilizzata:
•
11
Ordinamento per inserzione
•
Nel caso di un Albero binario bilanciato, l’ordinamento è
preservato dalla struttura stessa dell’albero, e dato che
inserire un nuovo elemento ha complessità logaritmica,
inserire n elementi costerà al più:
Θ(n log 2 n)
•
Per restituire in uscita gli elementi ordinati è sufficiente
visitare l’albero con visita simmetrica, con costo lineare,
ossia pari a Θ(n).
12
Ordinamento per inserzione
•
•
•
Nel caso di una Lista concatenata ordinata, trovare la
posizione del nuovo elemento implica dover scandire la
struttura, con un costo lineare.
L’inserimento nella posizione corretta è reso efficiente
dall’utilizzo dei puntatori, rendendo, dal punto di vista
della complessità, irrilevante (Θ(1)) tale operazione.
In conclusione, supponendo di dover inserire n elementi,
nel suo complesso la procedura costerà al più:
2
Θ( n )
13
Ordinamento per inserzione
•
•
•
•
Se la struttura utilizzata è un Vettore, trovare la posizione dove
inserire il nuovo elemento ha costo Θ(log2(n)), se si utilizza
l’algoritmo di ricerca binaria.
Nel caso del Vettore però è necessario spostare fisicamente gli
elementi maggiori del nuovo elemento, in modo da fargli posto.
L’operazione di spostare gli elementi del Vettore, ha un costo
lineare con la dimensione del Vettore, quindi l’inserimento di
ogni nuovo elemento costa in media log2(n) + n, dove n è la
dimensione corrente del vettore.
Dovendo inserire un totale di n elementi, la complessità sarà
pari a n*(log2(n) + n), ossia nlog2(n) + n2 e questo significa che
la complessità dell’intera procedura sarà al più:
2
Θ( n )
14
Considerazioni sulla
complessità dell’Ordinamento
•
•
•
Quando una procedura di ordinamento effettua solo
confronti e spostamenti, il numero minino di operazioni
necessarie è proporzionale a Θ(nlog2(n)), dove n è la
dimensione della struttura.
L’Albero binario di ricerca si rivela una struttura molto
efficiente oltre che per la ricerca, anche per
l’ordinamento, essendo questo garantito per costruzione.
L’Algoritmo di ordinamento per inserzione in una Lista o
in un Vettore appartengono alla medesima classe di
complessità (hanno lo stesso comportamento asintotico),
ma si può affermare che in una Lista la procedura è più
efficiente.
15
Ordinamento per affioramento
(Bubble Sort)
•
•
•
•
•
In talune situazioni può rendersi necessario ordinare un
insieme di elementi disordinato, ad esempio un vettore.
Consideriamo le procedure che effettuano confronti e
spostamenti per l’ordinamento di un Vettore.
Uno degli Algoritmi più semplici, ma non il più efficiente, è
il cosiddetto Bubble Sort.
L’idea è quella di confrontare tra loro gli elementi di tutte
le coppie consecutive e scambiare gli elementi della
coppia quando questi non sono ordinati.
Il procedimento è iterativo, e alla fine di ciascuna
iterazione un elemento del Vettore si troverà nella sua
posizione finale.
16
Bubble Sort: un esempio
•
•
•
•
•
•
Consideriamo il vettore V = {6,10,4,1,8} e supponiamo di
volerlo ordinare in modo crescente.
La prima coppia esaminata è 6,10 e siccome 10 > 6 allora
non effettuo lo scambio,
La seconda coppia da prendere in esame è 10,4 e questa
volta effettuo lo scambio …
Alla fine della prima iterazione ho esaminato tutte le
coppie consecutive di V, e V = {6,4,1,8,10}.
Adesso l’Algoritmo procederà sulle coppie del
sottovettore V1 = {6,4,1,8} …
Al termine di ciascuna iterazione, l’elemento massimo del
vettore affiora fino ad occupare l’ultima posizione, e in
generale gli altri elementi tendono ad assumere la loro
posizione finale.
17
Bubble Sort: analisi della
complessità
•
•
Quanti confronti e spostamenti si effettuano al più?
In generale, se il Vettore V è di dimensione n, allora il
numero massimo di confronti e spostamenti equivale al
numero di coppie consecutive coinvolte nelle varie
iterazioni, ossia:
n2 − n
(n − 1) + (n − 2) + ... + 1 = ∑ (n − 1) =
= Θ( n 2 )
2
•
Il Bubble Sort ha quindi complessità polinomiale di ordine 2.
18
Bubble Sort
void BubleSort(int *V, int size)
{
int i,j;
for (i=size; i>=1; i--)
for (j=0; j<i; j++)
if (V[j] > V[j+1]) scambia(&V[j], &V[j+1]);
}
NOTA: l’Algoritmo potrebbe essere modificato
in modo da terminare nel caso il vettore V sia
già ordinato alla generica iterazione i.
19
Quicksort (ordinamento veloce)
• Alla base del Quicksort vi è un procedimento che porta un
elemento del Vettore (pivot o perno) scelto secondo un
qualche criterio (generalmente il primo elemento del
Vettore) ad occupare la sua posizione finale.
• Contemporaneamente effettua degli spostamenti in maniera
tale che tutti gli elementi minori di pivot siano a sinistra, e gli
elementi maggiori a destra.
• La procedura descritta viene chiamata di partizionamento, e
se si applica ricorsivamente ai sottovettori (partizioni) così
determinati, si otterrà l’ordinamento sperato.
• Alla base dell’Algoritmo Quicksort vi è dunque la procedura
di partizionamento del Vettore.
20
Quicksort (ordinamento veloce)
• La procedura Partiziona utilizza due indici, l e r, per scandire
simultaneamente V dal primo elemento in poi, e dall’ultimo
all’indietro, ed effettuando lo scambio dei due elementi in
esame se l’ordine non è rispettato, finché l < r.
• All’inizio tengo fisso l e faccio scorrere r all’indietro; dopo aver
effettuato uno scambio, tengo fisso r e faccio scorrere l.
• Esempio, sia V = {34,27,64,25,18,29,76,81} e 34 il pivot,
– {34,27,64,25,18,29,76,81} quando V[l] = 34 e V[r] = 29,
viene effettuato lo scambio,
– {29,27,64,25,18,34,76,81} adesso faccio scorrere l …
– {29,27,64,25,18,34,76,81} effettuo lo scambio …
– {29,27,34,25,18,64,76,81} adesso faccio scorrere r …
– {29,27,34,25,18,64,76,81} effettuo lo scambio …
– {29,27,18,25,34,64,76,81} adesso faccio scorrere l …
– {29,27,18,25,34,64,76,81} l == r, la partizione è fatta!
21
Quicksort: analisi della complessità
• Supponiamo che la dimensione del Vettore e la distribuzione degli
elementi contenuti siano tali da generare dei partizionamenti
sempre bilanciati (caso “migliore”).
• Quanti partizionamenti devo effettuare?
• La prima suddivisione produce 2 sottovettori di n/2 elementi
ciascuno, la seconda 4 sottovettori di n/4 elementi ciascuno …
• L’ultima suddivisione, diciamo la p-esima, genera 2p sottovettori di
n/2p = 1 elementi ciascuno, quindi p = log2(n).
• La scansione dei singoli sottovettori è lineare; la suddivisione del
Vettore V viene fatta in n passi, la seconda suddivisione prevede
la scansione di 2 sottovettori di dimensione n/2, che in totale da n
passi, poi 4 sottovettori di dimensione n/4 che in totale da ancora
n passi …
• In generale, se p è il numero di partizionamenti del Vettore, il
numero totale di passi sarà n*p, quindi in questo caso la
complessità del Quicksort sarà:
Θ(np) = Θ(n log 2 (n))
22
Quicksort: analisi della complessità
• L’algoritmo del Quicksort è considerato ottimo tra quelli
che operano confronti e spostamenti, ma il
comportamento dell’algoritmo dipende strettamente da
come sono distribuiti i dati e anche dalla scelta del
pivot.
• Se per esempio gli elementi sono già ordinati, il
partizionamento sarà fortemente sbilanciato (caso
“pessimo”) e la complessità degenera a quella
quadratica, come nel Bubble Sort.
• Esistono metodi stabili la cui complessità è sempre
proporzionale a Θ(nlog2(n)) (es: Mergesort).
23
Quicksort
int Partiziona(int *V, int l, int r)
{
while (l < r) {
while (V[l] <= V[r] && l < r) r--;
if (l < r)
{
scambia(&V[l],&V[r]);
while (V[l] <= V[r] && l < r) l++;
if (l < r) scambia(&V[l],&V[r]);
}
}
return l; //l == r sarà l’indice relativo al pivot
}
24
Quicksort
int main()
{
…
Quicksort(V,0,size-1);
…
}
void Quicksort(int *V, int l, int r)
{
int perno;
if (l < r)
{
perno = Partiziona(V,l,r);
Quicksort(V,l,perno-1);
Quicksort(V,perno+1,r);
}
}
25