lez. 10a1 universita' di ferrara facolta' di scienze matematiche, fisiche e naturali...
TRANSCRIPT
Lez. 10a 1
Universita' di FerraraFacolta' di Scienze Matematiche, Fisiche e Naturali
Laurea Specialistica in Informatica
Algoritmi Avanzati
Strategie per la progettazione di algoritmi:divide & conquer
• problema del min-max• quicksort
Copyright © 2007-2008 by Claudio Salati.
2
DIVIDE-&-CONQUER
• Nella progettazione di un algoritmo la strategia e' quasi sempre quella di suddividere il problema in sottoproblemi piu' semplici.
• Dato un problema P su un insieme di dati di dimensione n una maniera di suddividerlo in problemi piu' semplici e' quella di
considerare problemi analoghi a P ma su insiemi di dati di dimensione minore(cioe' problemi piu' piccoli: piccolo e' facile)
• SI RICONDUCE LA SOLUZIONE DI P, dove #P = n, ALLA SOLUZIONE DI m PROBLEMI P1, P2, …, Pm,
dove i | 1i m : # Pi = n/k
• OVVIAMENTE BISOGNA CHE SIA FACILE:
• OPERARE LA SCOMPOSIZIONE
• RICOMPORRE LE m SOLUZIONI PARZIALI NELLA SOLUZIONE COMPLESSIVA
3
DIVIDE-&-CONQUER
• PERCHE' CONSIDERARE LO STESSO PROBLEMA SOLO PER DIMENSIONI PIU' PICCOLE DOVREBBE ESSERE CONVENIENTE?
• PERCHE' SE IL PROBLEMA E' DI COMPLESSITA' n2 DIVIDERLO PER 2 SIGNIFICA OTTENERE UN PROBLEMA 4 VOLTE PIU' FACILE!
• Non solo, OPERANDO RICORSIVAMENTE SI ARRIVA AD OTTENERE PROBLEMI ELEMENTARI:
e.g., ORDINARE UN VETTORE DI LUNGHEZZA 1 E' MOLTO FACILE, E' GIA' DI PER SE STESSO ORDINATO
4
DIVIDE-&-CONQUER
void divideAndConquer(items data [], int fromItem, int toItem)
{
// risolve il problema per il sottovettore // data[fromItem..toItem];// la risoluzione si suppone sul posto
if (smallEnough(fromItem, toItem))
solve(data, fromItem, toItem);
else { int middle = (fromItem + toItem) / 2; // splitProblem(data, fromItem, toItem, // &middle); divideAndConquer(data, fromItem, middle); divideAndConquer(data, middle + 1, toItem); combine(data, fromItem, toItem, middle);
}
}
5
DIVIDE-&-CONQUER
• IL PARTIZIONAMENTO AVVIENE DI NORMA VERSO PROBLEMI BILANCIATI, DI UGUALE DIMENSIONE
• SBILANCIARE NON CONVIENE
• SUPPONIAMO DI APPLICARE MERGESORT A 2 SOTTOPROBLEMI SBILANCIATI:
• T(n) = T(1) + T(n-1) + T(merge, 1, n-1)
• T(1) E' COSTANTE
• T(merge, 1, n-1) E' O(n)
• E LA COMPLESSITA' TOTALE E' O(n2)
• INFATTI CON LO SBILANCIAMENTO SI E' TRASFORMATO MERGESORT IN STRAIGHT-INSERTION!
6
DIVIDE-&-CONQUER
• SE g(n) E' LA COMPLESSITA' DELLA SOLUZIONE DI UN
PROBLEMA smallEnough()
• E f(n) E' LA COMPLESSITA' DI combine() 2 SOLUZIONI PARZIALI, CIASCUNA DI DIMENSIONE n/2
• ALLORA LA COMPLESSITA' DI divideAndConquer() E'
• T(n) = g(n) per n
smallEnough()
• T(n) = 2 * T(n/2) + f(n) per n !smallEnough()
• La complessita’ della operazione di scomposizione del problema in due sotto-problemi (che nello schema di funzione
coincide con il calcolo di middle) e’ supposta assorbibile
entro la complessita' di combine()
7
DIVIDE-&-CONQUER
• Vogliamo valutare il costo T(n) di risolvere un problema di dimensione n
• SUPPONIAMO DI DIVIDERE UN PROBLEMA DI DIMENSIONE n IN a SOTTOPROBLEMI DI DIMENSIONE n/b CIASCUNO
• E CHE IL LAVORO PER OPERARE LA RICOMBINAZIONE DELLE SOLUZIONI PARZIALI SIA c * nd
(INGLOBANDO IN QUESTO ANCHE L'EVENTUALE COSTO DEL PARTIZIONAMENTO IN SOTTOPROBLEMI)
• E SUPPONIAMO CHE RISOLVERE UN PROBLEMA DI DIMENSIONE 1 ABBIA COSTO COSTANTE k1
• ALLORA a * T(n/b) E' IL COSTO DI RISOLVERE TUTTI I SOTTOPROBLEMI
8
DIVIDE-&-CONQUER
• Definizione ricorsiva di T(n):
T(n) = c * nd + a * T(n/b)
T(1) = k1
• espandiamo ricorsivamente T(n/b)
T(n) = c * nd + a * (c * (n/b)d + a * T(n/b2))
= c * nd * (1 + a/bd) + a2 * T(n/b2)
• e continuando ad espandere ricorsivamente per p=logbn volte
T(n) = c * nd * (1 + a/bd + … + (a/bd)p-1) + ap * T(1)
• ma ap = a = (b ) = (b ) = nlogbn logba logbn logbn logba logba
{
j1p
0jd
dalog1
b
a*n*cn*k)n(T b
9
DIVIDE-&-CONQUER
•
cioe' (se x1)
e
1nn
0j
j1n
0j
jn
0j
j x*ax*a*xax*a*xax*a
)x1(*ax*a*)x1( 1nn
0j
j
x1
x1*ax*a
1nn
0j
j
10
DIVIDE-&-CONQUER
• se x<1 allora
per cui k2 maggiora la sommatoria per qualsiasi n finito
• se x=1 allora la sommatoria vale (n+1)*a
• se x>1 per n sufficientemente grande la sommatoria e' maggiorata da a*xn+1
2
1n
nk
x1
a
x1
x1*alim
11
DIVIDE-&-CONQUER
• N.B.: la sommatoria e' quella dei primi n termini della serie geometrica
• La serie geometrica e la sua sommatoria
• convergono per x < 1
• divergono per x > 1
• Per x = 1 la sommatoria della serie geometrica diverge
• Per x>1 e n abbastanza grande la sommatoria della serie geometrica coincide approssimativamente, a meno di costanti moltiplicative, con il suo ultimo termine (o meglio, con il termine successivo, per tenere conto dei termini precedenti)
• N.B.: nel nostro caso x = a / bd
12
DIVIDE-&-CONQUER
a < bd
• Caso: x = a / bd < 1
•
•
e quindi, poiche' logba < d :
• in questo caso domina il lavoro non ricorsivo di ricomposizione dei risultati parziali
d2
alog1 n*c*kn*k)n(T b
)n(O)n(O)n(T dalogb
)n(O)n(T d
13
DIVIDE-&-CONQUER
a > bd
• Caso: x = a / bd > 1
•
• ma:
• e quindi:
• in questo caso domina il lavoro ricorsivo
p
ddalog
1b
a*n*cn*k)n(T b
dalogb
alognlog
nlog
b
alognlog
d
p
dbdb
b
b
dbb
nbbb
a
b
a
)n(O)n(T alogb
14
DIVIDE-&-CONQUER
a = bd
• Caso: x = a / bd = 1
•
• e quindi:
• in quanto logba=d
• in questo caso il lavoro ricorsivo e quello di ricomposizione si bilanciano, e compare il termine logaritmico
nlog*n*cn*k)n(T bdalog
1b
)nlog*n(O)n(T bd
15
DIVIDE-&-CONQUER
Riassumendo:
• se a bd
• se a = bd )nlog*n(O)n(T bd
)n(O)n(T )alog,dmax( b
16
DIVIDE-&-CONQUER
Nel caso del mergesort:
• a = 2
• b = 2
• d = 1
– quindi: a = bd
– quindi: T(n) = O(n * log2n)
Nel caso dell’algoritmo del torneo:
• a = 2
• b = 2
• d = 0
– quindi: a > bd
– quindi: T(n) = = O(n))O(n alogb
17
DIVIDE-&-CONQUER
In generale, se
allora
salvo che le due quantita' maggiori siano uguali. In questo caso e':
d
kk
22
11 n*c
b
nT*a...
b
nT*a
b
nT*a)n(T
)n(O)n(T)alog,...,alog,alog,dmax( kkb22b11b
))nlog(*n(O)n(T)alog,...,alog,alog,dmax( kkb22b11b
18
DIVIDE-&-CONQUER: esempio
• Immaginiamo di dovere individuare elemento minimo e massimo di un vettore.
• L'algoritmo seguente fornisce una soluzione banale del problema
typedef . . . elemento;struct result { elemento min; elemento max; };struct result easyMinMax(elemento v[], int n) { 1
assert(n>0); struct result res; 2
res.min = res.max = v[0]; 3
for (int i=1; i<n; i+=1) { 4
if (v[i] < res.min) res.min = v[i]; 5
if (v[i] > res.max) res.max = v[i]; 6
} 7
return (res); 8
}
19
DIVIDE-&-CONQUER: esempio
• Per analizzare la complessita' dell'algoritmo ci si concentra sul numero di confronti tra elementi del vettore che esso esegue.
• perche' le altre operazioni sono in numero proporzionale a questi confronti.
• perche' se gli elementi sono oggetti complessi (e.g. stringhe) il costo di questi confronti e' predominante rispetto al costo delle altre operazioni.
• la complessita' di easyMinMax() e' evidentemente 2*(n-1).
• 2*(n-1) rappresenta caso migliore, peggiore, e medio.
• e' anche immediato vedere come easyMinMax() puo' essere migliorata:
se v[i]<res.min non puo' essere v[i]>res.max
quindi il secondo if (riga 6) deve essere eseguito solo se la condizione del primo (riga 5) risulta falsa
20
DIVIDE-&-CONQUER: esempio
typedef . . . elemento;
struct result { elemento min;
elemento max; };struct result betterMinMax(elemento v[], int n) { 1
assert(n>0); struct result res; 2
res.min = res.max = v[0]; 3
for (int i=1; i<n; i+=1) { 4
if (v[i] < res.min) res.min = v[i]; 5
else if (v[i] > res.max) res.max = v[i]; 6
// end if } 7
return (res); 8
} 9
21
DIVIDE-&-CONQUER: esempio
• la complessita' di betterMinMax() non e' piu' sempre la stessa per i casi migliore, peggiore, e medio.
• il caso migliore si ha quando v[] e' ordinato in modo non crescente: in questo caso il numero di confronti e' n-1.
• il caso peggiore si ha quando v[] e' ordinato in modo non decrescente: in questo caso il numero di confronti e' 2*(n-1).
• nel caso medio sara' v[i]<res.min meta' delle volte e quindi il numero medio di confronti sara' 3*(n-1)/2.
• e' possibile fare "di meglio"?
• applichiamo la strategia divide & conquer!
22
DIVIDE-&-CONQUER: esempio
typedef . . . elemento;typedef struct result { elemento min; elemento max; } result;result dAndCMinMax(elemento v[], 1
int from, int to) { assert(0<=from && from<= to && to<=#v); result res; 2
if (to-from <= 1) { 3
// 1 o 2 elementi nel sottovettore if (v[from] <=v [to]) { 4
res.min = v[from]; res.max = v[to]; 5
} else { 6
res.min = v[to]; res.max = v[from]; 7
} 8
} else { 9
// continua alla pagina seguente
23
DIVIDE-&-CONQUER: esempio
// continuazione di dAndCMinMax()
// piu' di 2 elementi nel sottovettore int mid = (from + to) / 2; 10
result r1 = dAndCMinMax(v, from, mid); 11
result r2 = dAndCMinMax(v, mid+1, to); 12
res.min = (r1.min <= r2.min) ? r1.min 13
: r2.min; 14
res.max = (r1.max >= r2.max) ? r1.max 15
: r2.max; 16
} 17
return (res); 18
} 19
24
DIVIDE-&-CONQUER: esempio
• dAndCMinMAX() correttezza:
• per esercizio (elementare per induzione matematica).
• dAndCMinMAX() complessita':
• e' data dalla seguente relazione ricorsiva:
• T(1) = T(2) = 1
• T(n : n>2) = 2 * T(n/2) + 2
• T(n) = 2 * (2 * T(n/4) + 2) + 2 = 4 * T(n/4) + (4+2)
= 4 * (2 * T(n/8) + 2) + (4+2) = 8 * T(n/8) + (8+4+2)
= ...
• e per k tale che 2k=n 22
n*3)22(22)2(T*2)n(T k1k
1k
1i
i1k
25
DIVIDE-&-CONQUER: esempio
• N.B.: la complessita' calcolata per dAndCMinMAX() si applica in tutti i casi, peggiore, migliore e medio.
• In effetti e' dimostrabile che nessun algoritmo basato su confronti puo' richiedere meno di 3*n/2-2 confronti.
• Ma dAndCMinMAX() e' davvero migliore di betterMinMax()?
• in termini di complessita' spaziale e' peggiore: richiede l'allocazione di log(n) record di attivazione ricorsiva.
• nel confronto si potrebbero contare anche le altre operazioni, e allora il vantaggio di dAndCMinMAX() diventerebbe minore.
• c'e' poi da considerare l'overhead temporale delle chiamate ricorsive.
• Cio' dimostra l'importanza, in certe circostanze, di considerare i coefficienti moltiplicativi e i termini di grado inferiore nelle funzioni che descrivono la complessita' degli algoritmi.
26
ALGORITMI DI ORDINAMENTO PER CONFRONTO
Algoritmo O(n2) Corrispondente algoritmo O(n*log(n))
straight insertion mergesort
straight selection heapsort
bubblesort ?
• Straight insertion si basa sulla creazione di un sottovettore localmente ordinato e sull'inserimento in esso, uno dopo l'altro, degli elementi restanti del vettore
• Mergesort si basa sulla creazione di due sottovettori localmente ordinati e sulla loro fusione ordinata
• Straight selection, heapsort e bubblesort si basano sulla creazione di un sottovettore globalmente ordinato e con la sua estensione successiva con gli elementi del restante sottovettore disordinato
• N.B.: tra sottovettore ordinato e sottovettore disordinato esiste una precisa relazione: tutti gli elementi del primo sono di tutti gli elementi del secondo
27
ALGORITMI DI ORDINAMENTO PER CONFRONTO
• Straight insertion si basa
– sulla creazione di 2 sottovettori, un sottovettore localmente ordinato e un sottovettore non ordinato (senza alcuna relazione d’ordine tra gli elementi di un sottovettore e quelli dell’altro)
– sull'inserimento nel sottovettore ordinato, uno dopo l’altro, degli elementi del sottovettore non ordinato
• Mergesort si basa
– sulla creazione di 2 sottovettori, entrambi localmente ordinati (senza alcuna relazione d’rdine tra gli elementi di un sottovettore e quelli dell’altro)
– sul merge ordinato dei 2 sottovettori
– sull’uso della strategia Divide&Conquer
28
ALGORITMI DI ORDINAMENTO PER CONFRONTO
• Straight selection si basa
– sulla creazione di 2 sottovettori, un sottovettore globalmente ordinato e un sottovettore non ordinato (gli elementi del sottovettore non ordinato sono tutti maggiori o uguali degli elementi del sottovettore globalmente ordinato)
– sull'inserimento nel sottovettore ordinato dell’elemento minimo del sottovettore non ordinato
• Heapsort si basa
– sulla creazione di 2 sottovettori, un sottovettore globalmente ordinato e un sottovettore non ordinato (gli elementi del sottovettore non ordinato sono tutti minori o uguali degli elementi del sottovettore globalmente ordinato)
– sul fatto che il sottovettore non ordinato e’ costituito da uno heap basato sulla relazione “maggiore o uguale”
– sull'inserimento nel sottovettore ordinato dell’elemento massimo del sottovettore non ordinato
– sull’uso della differenziazione formale
29
ALGORITMI DI ORDINAMENTO PER CONFRONTO
• Straight exchange e/o bubblesort si basano
– sulla creazione di 2 sottovettori, un sottovettore globalmente ordinato e un sottovettore non ordinato (gli elementi del sottovettore non ordinato sono tutti maggiori o uguali degli elementi del sottovettore globalmente ordinato)
– sull’utilizzo di una successione di scambi con elementi adiacenti del sottovettore non ordinato per portare un elemento del sottovettore non ordinato al proprio posto nel sottovettore ordinato
• Quicksort si basa
– sull’applicazione della strategia Divide&Conquer
– sull’utilizzo, per spostare un elemento verso la sua posizione corretta, di salti il piu’ lunghi possibile
30
QUICKSORT
• Come saranno i due sottovettori in cui viene suddiviso il vettore da ordinare secondo la strategia Divide&Conquer?
• Non possono essere entrambi globalmente ordinati, altrimenti le 2 chiamate ricorsive e la funzione combine() non avrebbero niente da fare, avrebbe fatto gia’ tutto la funzione splitProblem()!
• Quindi almeno inizialmente non potranno essere ordinati, saranno le chiamate ricorsive a ordinarli.
• Ma la relazione d’ordine tra i due sottovettori?
• Ma la relazione d’ordine tra i due sottovettori?
• La si conserva!
• La funzione splitProblem() dovra’ essere tale da suddividere il vettore in 2 sottovettori tali che gli elementi dell’uno sono tutti minori o uguali degli elementi dell’altro.
• La funzione combine() sara’ vuota!
31
QUICKSORT
• Quicksort si basa come bubblesort sullo scambio di elementi disordinati.
• Lo scambio di elementi in quicksort non ha lo scopo di mettere in ordine elementi adiacenti.
• Lo scambio in quicksort non avviene tra elementi adiacenti.
• Gli elementi fanno degli spostamenti maggiori: e' per questo che ci si aspetta un comportamento migliore di quello di bubblesort
• Lo scambio di elementi in quicksort ha lo scopo di creare due sottovettori che abbiano la seguente proprieta':
Proprieta' P: Tutti gli elementi del primo sottovettore sono minori o uguali di tutti gli elementi del secondo sottovettore.
• Per ottenere un vettore ordinato e’ quindi sufficiente ordinare localmente ciascuno dei due sottovettori ottenuti dal partizionamento del vettore.
• L'ordinamento dei due sottovettori e' ovviamente possibile applicando ricorsivamente la procedura.
32
QUICKSORT
i = 0..m-1: v[i] v[m]
i = m+1..N-1: v[i] v[m]
• e quindi:
i = 0..m-1, j = m+1..N-1 : v[i] v[j]
• L'operazione di suddivione del vettore nei due sottovettori separati dall'elemento pivot m e' chiamata partizionamento
v[0] v[1] v[N-1]...vettore v
v[0] ... v[m] v[N-1]...vettore v
33
QUICKSORT: l’elemento pivot
• L’elemento pivot dovrebbe essere l’elemento mediano del vettore v
• In questo modo i due sottovettori alla sua destra e alla sua sinistra sarebbero bilanciati
• Ma come e’ possibile conoscere a priori quale e’ l’elemento mediano del vettore?
• Ci vorrebbe un oracolo.
• In mancanza dell’oracolo ci si affida al calcolo delle probabilita’: si utilizza come pivot un elemento a caso, il piu’ comodo, il primo elemento del (sotto-)vettore.
34
QUICKSORT: l'algoritmo
void quicksort(int v[], int from, int to) {
if (to > from) {
int m = partition(v, from, to);
quicksort(v, from, m-1);
quicksort(v, m+1, to);
}
}
• La funzione partition() effettua il partizionamento del vettore v[] in due sottovettori che soddisfano la proprieta' P intorno all'elemento pivot di indice m
• Per ordinare il vettore int v[n] e' sufficiente chiamare
quicksort(v, 0, n-1);
35
QUICKSORT: funzione partition() .1
int partition(int v[], int from, int to) { assert(to>from); int pivot = v[from]; // seleziono il pivot int fromScan = from+1; int toScan = to; // i=from+1..fromScan-1: v[i]v[from] // i=toScan+1..to: v[i]v[from] for (;;) { while (fromScan <= toScan && v[fromScan] <= pivot) fromScan += 1; while (toScan >= fromScan && v[toScan] >= pivot) toScan -= 1;
// continua alla pagina successiva
36
QUICKSORT: funzione partition() .2
// int partition(): continua if (toScan > fromScan) { int temp = v[fromScan]; v[fromScan] = v [toScan]; v[toScan] = temp; fromScan += 1; toScan -= 1; } else { break; } } assert (toscan == fromscan-1); int m = fromScan - 1; v[from] = v[m]; v[m] = pivot; return(m);}
37
QUICKSORT: correttezza .1
Teorema: La funzione partition() suddivide il sottovettore v[from..to] in tre parti (la prima o la terza eventualmente vuote):
• il sottovettore v[from..m-1]
• l'elemento v[m]
• il sottovettore v[m+1..to]
tali che:
k = from .. m-1: v[k] v[m]
k = m+1 .. to: v[m] v[k]
Dimostrazione:
• Discende immediatamente dall'invariante del ciclo esterno e dal fatto che tale ciclo termina quando toScan=fromScan-1.
• L'elemento v[m] al termine della funzione e' uguale all'elemento v[from] all'inizio di essa.
38
QUICKSORT: correttezza .2
Teorema: La funzione quicksort() ordina il sottovettore v[from..to] in modo non decrescente.
Dimostrazione:
• Per induzione matematica sulla lunghezza del sottovettore v[from..to].
• Ciascuno dei due sottovettori delle chiamate ricorsive e' lungo al piu' to-from, per cui l'ipotesi induttiva e' applicabile.
• Dopo le chiamate ricosive i due sottovettori sono ordinati: la correttezza globale discende dalla correttezza della funzione partition().
39
QUICKSORT: complessita' .1
• L'algoritmo quicksort ha complessita' O(n2).
• Nel caso peggiore infatti ogni partizionamento avviene con uno dei due sottovettori vuoto.
• La chiamata ricorsiva e' quindi per un sottovettore di lunghezza inferiore di un solo elemento a quella del sottovettore della attivazione chiamante.
• Si hanno quindi n attivazioni della funzione e ogni attivazione, relativa ad un sottovettore di lunghezza k, effettua k-1 confronti.
• Nel caso migliore pero' il partizionamento e’ con sottovettori bilanciati: in questo caso l'altezza dell'albero delle chiamate ricorsive e' solo log(n) e il comportamento dell'algoritmo diventa di complessita' n*log(n).
• La domanda quindi e':
• Quale e' il comportamento atteso?
• Quale e' il comportamento medio?
40
QUICKSORT: complessita' media .2
• L'ipotesi e' che tutte le permutazioni della sequenza di elementi da ordinare siano equiprobabili.
• Sia T(n) la complessita' dell'algoritmo nell'ordinamento di un sottovettore di lunghezza n:
• T(0) = T(1) = b (costante)
• T(n) = (n - 1) + T(i-1) + T(n-i), per n 2, con i = 1..n
essendo i la posizione nel sottovettore partizionato dell'elemento pivot.
• Nell'ipotesi fatta i puo' assumere qualunque valore tra 1 e n con uguale probabilita', per cui in media
(1)
1n
0i
n
1i
)i(T*n
2)1n(
))in(T)1i(T(*n
1)1n()n(T
41
• Proviamo se la relazione e' soddisfatta da T(n) = c * n * log(n) (che e' la complessita' "desiderata").
• Sostituiamo nella relazione ricorsiva (1):
(2)
• Quanto vale la sommatoria che compare in (2)?
QUICKSORT: complessita' media .3
?
1n
1j
)jlog(*j*n
c*21n)nlog(*n*c
42
• Consideriamo una generica funzione monotona crescente f(x)
(3)
QUICKSORT: complessita' media .4
n
k
nn
k
)k(fdx)x(f)k(f1
1
11
1
43
Ma
per cui, sottraendo (f(n) - f(1)) dall'integrale di (3):
(4)
consideriamo allora f(x) = x * ln(x)
tenendo conto che
e sostituendo nella disequazione (4)
(vedi pagina seguente)
QUICKSORT: complessita' media .5
)f()nf()k(f)k(fn
k
n
k
11111
1
11
1
1
11nn
k
n
dx)x(f)k(f))(f)n(f(dx)x(f
42
22 x)xln(
xdx)xln(x
44
cioe'
QUICKSORT: complessita' media .6
4
10
4
n)nln(*
2
n
)kln(k
)0)nln(n(4
10
4
n)nln(*
2
n
22
1n
1k
22
4
1
4
n)nln(
2
n)kln(k
221n
1k
45
E dividendo per ln(2)
che sostituiamo nella relazione ricorsiva (2):
quindi, a meno di termini di ordine minore: T(n) = O(n*log(n))
QUICKSORT: complessita' media .7
)2ln(*4
1
)2ln(*4
n)nlog(
2
n)klog(k
221n
1k
n*)2ln(*2
c1n*
)2ln(*2
c1)nlog(*n*c
)2ln(*4
1
)2ln(*4
n)nlog(
2
n
n
c21n
22