merge sort, quick sort, counting sort

annuncio pubblicitario
Algoritmi e strutture dati
2006/2007 prof. C. Sansone
MERGE SORT, QUICK SORT,
COUNTING SORT
Roberto Bifulco 885/269
Barbara Migliaccio 885/225
Table of contents
Introduzione..........................................................................................................................................................................4
Capitolo I
Merge sort.............................................................................................................................................................................5
I.1Funzionamento........................................................................................................................................................... 5
I.2Pseudo-codice............................................................................................................................................................. 6
I.3Analisi di correttezza.................................................................................................................................................. 7
I.4Analisi di complessità.................................................................................................................................................8
Capitolo II
Quick sort........................................................................................................................................................................... 13
II.1Funzionamento........................................................................................................................................................ 13
II.2Pseudo-codice..........................................................................................................................................................14
II.3Analisi di correttezza...............................................................................................................................................16
II.4Analisi di complessità............................................................................................................................................. 16
Capitolo III
Counting sort...................................................................................................................................................................... 18
III.1Funzionamento.......................................................................................................................................................18
III.2Pseudo-codice........................................................................................................................................................ 19
III.3Analisi di complessità............................................................................................................................................ 19
Capitolo IV
Modalità di testing.............................................................................................................................................................. 20
IV.1Tool per il testing...................................................................................................................................................20
IV.2Valutazione del tempo di esecuzione.................................................................................................................... 20
IV.3Valutazione del numero di operazioni................................................................................................................... 21
IV.4Verifica di correttezza............................................................................................................................................21
Capitolo V
Realizzazione degli algoritmi............................................................................................................................................. 23
V.1Merge sort............................................................................................................................................................... 23
V.2Quick sort................................................................................................................................................................ 24
V.3Counting sort...........................................................................................................................................................25
Capitolo VI
Testing delle prestazioni..................................................................................................................................................... 26
VI.1Merge sort.............................................................................................................................................................. 26
VI.1.1Analisi del tempo di esecuzione....................................................................................................................26
VI.1.2Analisi del numero di operazioni.................................................................................................................. 28
VI.1.3Confronto con i risultati teorici..................................................................................................................... 28
VI.2Quick sort...............................................................................................................................................................30
VI.2.1Analisi del tempo di esecuzione....................................................................................................................30
VI.2.2Analisi del numero di operazioni.................................................................................................................. 32
VI.2.3Confronto con i risultati teorici..................................................................................................................... 33
VI.3Counting sort......................................................................................................................................................... 34
VI.3.1Analisi del tempo di esecuzione....................................................................................................................34
VI.3.2Analisi del numero di operazioni.................................................................................................................. 34
VI.3.3Confronto con i risultati teorici..................................................................................................................... 35
VI.4Confronto fra i tre algoritmi.................................................................................................................................. 36
Appendice A: Codice sorgente........................................................................................................................................... 37
TestBench.cpp...............................................................................................................................................................37
mergesort.cpp................................................................................................................................................................42
quicksort.cpp................................................................................................................................................................. 44
countingsort.cpp............................................................................................................................................................45
Appendice B: il tool TestBench..........................................................................................................................................46
Introduzione
Introduzione
Il problema dell’ordinamento di un insieme è un problema classico dell’informatica che ha una
valenza indiscutibile in ambito applicativo. Nelle pagine seguenti esamineremo una serie di
algoritmi che risolvono il problema in modo efficiente, quali: il Mergesort, il Quicksort e il
Countingsort.
Alcuni di essi operano esclusivamente sulla base del confronto dei valori dell’insieme da ordinare,
mentre altri risolvono il problema in modo ancora più efficiente utilizzando alcune informazioni
aggiuntive sull’insieme da ordinare (ad esempio sulla presenza di elementi duplicati, sul valore
minimo e il valore massimo all’interno dell’insieme, o altre informazioni che potrebbero consentire
di introdurre delle ottimizzazioni nell’algoritmo, in grado di ridurre in modo significativo la
complessità dell’algoritmo stesso).
Detta n la dimensione dell'input, l’algoritmo QUICKSORT consente di raggiungere una
complessità di O(n log(n)) nel caso medio, mentre nel caso più sfavorevole raggiunge una
complessità di Θ(n2).
L’algoritmo MERGESORT consente di raggiungere una complessità di Θ (n log(n)) anche nel caso
peggiore. E' possibile dimostrare che il limite inferiore alla complessità computazionale del
problema dell’ordinamento mediante confronti (senza dunque poter sfruttare altre informazioni
sull’insieme da ordinare) è proprio pari a n log(n)
L’algoritmo COUNTINGSORT è invece basato su altri criteri e strategie, diverse dal confronto fra i
valori degli elementi dell’insieme da ordinare, e sfrutta pertanto altre informazioni sui dati in input;
grazie a questo riesce ad ottenere una complessità lineare di Θ (n).
Questo testo è diviso in capitoli, presentati brevemente nel seguito:
➢
Capitolo I, Capitolo II, Capitolo III: descrizione degli algoritmi Mergesort, Quicksort e
Countingsort da un punto di vista teorico, con analisi di correttezza e complessità e
presentazione dello pseudo-codice dell'algoritmo;
➢
Capitolo IV: vengono presentate le problematiche affrontate per la valutazione delle
prestazioni degli algoritmi e le soluzioni adottate;
➢
Capitolo V: illustrazione di una possibile implementazione degli algoritmi in C++;
➢
Capitolo VI: presentazione dei risultati sperimentali ottenuti, confronto fra gli algoritmi e
verifica delle ipotesi teoriche.
Sono poi presenti due appendici utili a comprendere la soluzione adottata per eseguire il testing,
vengono quindi presentati il tool di testing ed il relativo codice sorgente.
Mergesort, Quicksort, Countingsort
3
Capitolo I
Merge sort
Capitolo I
Merge sort
L'algoritmo mergesort è un algoritmo ricorsivo che usa il paradigma del "divide et impera". Questa
tecnica consiste in tre passaggi fondamentali:
➢
Divide : il problema viene diviso in un certo numero di sotto-problemi di uguale grandezza;
➢
Impera : i sotto-problemi vengono risolti in modo ricorsivo;
➢
Combina : le soluzioni dei sotto-problemi vengono combinate per generare la soluzione del
problema originale.
L’intuizione su cui si basa questo potente algoritmo di “ordinamento per fusione” è la seguente:
potendo disporre di due sequenze già ordinate, è molto facile fonderle in una sola sequenza
completamente ordinata, basta scorrerle entrambe estraendo di volta in volta l’elemento minimo
dalle due sequenze originali, per collocarlo in fondo alla sequenza completamente ordinata che si
sta costruendo.
Il punto di partenza di questo ragionamento è costituito dal fatto che una sequenza composta da un
solo elemento è di fatto una sequenza ordinata.
Partendo da questi presupposti, quindi, è possibile realizzare un algoritmo che, dopo aver suddiviso
l’insieme iniziale A in n sottoinsiemi costituiti da un solo elemento ciascuno, procede a riaggregare i
sottoinsiemi fondendoli ordinatamente fino a ricostruire un’unica sequenza ordinata.
I.1 Funzionamento
Per ordinare un array A[p..r] con il mergesort conformemente al paradigma del "divide et impera",
agiremo in questo modo:
➢
Divide : Dividiamo la sequenza degli n elementi da ordinare in due sottosequenze di n/2
elementi ciascuna. Troviamo q, elemento a metà dell'array;
➢
Impera : Ordiniamo le due sottosequenze A[p..q] e A[q + 1..r] in modo ricorsivo utilizzando
l’algoritmo merge sort;
➢
Combina
A[p..r].
:
Si
fondono
i
sottoarray
ordinati
per
ottenere
l'array
ordinato
L’elemento chiave dell’algoritmo è l'operazione di merge (ultimo passaggio) che combina due
sotto-sequenze ordinate per produrre una singola sequenza ordinata. Questa ripetutamente compara
i primi elementi in testa alle due sotto-sequenze e mette in output il più piccolo valore finché non
rimane nessun elemento. Se la scansione di uno dei vettori è arrivata all'ultima componente, si
copiano i rimanenti elementi dell'altro nel vettore di output. Il merge ha complessità lineare e non
dipende dall’input: infatti, se due sottoarray hanno ciascuno n componenti, abbiamo 2*n confronti
(nel caso peggiore) e altrettante copie, quindi: time(n) = 2*n+2*n = 4*n = Θ(n).
Mergesort, Quicksort, Countingsort
4
Capitolo I
Merge sort
Complessivamente potremmo schematizzare l’intero procedimento di ordinamento di una sequenza
di 4 elementi [6,2,9,5] come nel diagramma esemplificativo rappresentato nella figura seguente.
Le due fasi di divisione spezzano la sequenza di input; le due fasi di merge combinano le sottosequenze ordinate generate nella precedente fase.
I.2 Pseudo-codice
Merge-Sort(A, p, r)
1
If (p < r)
2
then q = [(p+r)/2]
3
Merge-Sort(A, p,q)
4
Merge-Sort(A, q+1, r)
5
Merge(A, p, q, r)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Merge(A, p, q, r)
n1q-p+1
n2 r-q
creo i vettori L[1…n1+1] e R[1…n2+1]
for i 1 to n1
do L[i] A[p+i-1]
for j 1 to n2
do R[j] A[q+j]
L[n1+1]  infinito
R[n2+1] infinito
i 1
j 1
for k p to r
do If L[i]<= R[j]
then A[k]  L[i]
i  i+1
else A[k]  R[j]
j  j+1
Mergesort, Quicksort, Countingsort
5
Capitolo I
Merge sort
L’algoritmo è suddiviso in due procedure distinte: la procedura ricorsiva MERGESORT e la
procedura iterativa MERGE.
In sostanza l’idea di fondo di questo algoritmo è quella di suddividere sempre a metà l’array
iniziale, in modo tale che i due sotto-array ottenuti abbiano un egual numero di elementi (a meno di
uno, nel caso di insiemi di cardinalità non rappresentabile come una potenza di 2 o comunque
dispari). Nel MERGESORT durante il processo di suddivisione l’insieme non viene in alcun modo
modificato, i suoi elementi non sono spostati né confrontati fra di loro. Semplicemente si opera una
suddivisione dicotomica ricorsiva della sequenza iniziale da ordinare, fino ad ottenere n sequenze di
dimensione 1.
Di questa suddivisione ricorsiva si occupa la procedura ricorsiva MERGESORT, che riceve in input
l’intera sequenza A insieme con i due indici p e r che consentono di identificare rispettivamente il
primo e l’ultimo elemento della sotto-sequenza su cui deve operare la procedura. La procedura
individua l’indice q dell’elemento che si trova a metà della sotto-sequenza delimitata dagli elementi
p e r ed innesca due chiamate ricorsive della stessa procedura sulla prima metà della sequenza
(dall’elemento di indice p fino all’elemento di indice q) e sulla seconda metà (dall’elemento di
indice q +1 fino all’elemento di indice r ).
La procedura ricorsiva si ripete fino a quando la sotto-sequenza da suddividere non è composta da
un solo elemento: quando p = r la procedura termina senza compiere alcuna operazione.
Nel backtracking della ricorsione sta il vero e proprio punto di forza di questo algoritmo.
Durante il backtracking infatti vengono fusi a due a due i sottoinsiemi ottenuti con il processo di
suddivisione ricorsiva. Durante la fusione gli elementi vengono posizionati in modo da ottenere da
due sottosequenze ordinate un’unica sequenza ordinata. Infatti le sequenze composte da un solo
elemento da cui inizia il procedimento di fusione sono di per sé ordinate.
I.3 Analisi di correttezza
Ricordiamo che l’algoritmo Merge(A, p, q, r) assume che i vettori A[p …… q] e A[q+1 …… r]
siano ordinati e genera il vettore A[p …… r] ordinato.
Per valutare la Correttezza dell’algoritmo utilizziamo il Principio di induzione completa.
➢
Passo base: Se A è un vettore di un solo elemento,dunque p=r e pertanto l’algoritmo non
viene eseguito. Il vettore è pertanto banalmente ordinato.
➢
Passo induttivo: Ipotizzo la proprietà vera non solo per n-1, bensì vera da 1 a n-1(induzione
completa). Mergesort ordina correttamente n/2 elementi, quindi le due chiamate a Mergesort
sono corrette per ipotesi induttiva (ho 2 vettori di n/2 elementi ordinati).
➢
Conclusione: assumendo la correttezza di merge, ho la fusione ordinata dei due vettori.
Dunque ho un vettore totalmente ordinato.
Valutiamo ora la correttezza della funzione Merge, consideriamo soltanto l'ultimo ciclo, la cui
invariante è che il vettore A[p...k-1] è ordinato ed in L[i] ed R[j] ci sono i minimi dei vettori L ed R.
➢
Passo base: Se k=p il vettore A è vuoto e, poiché si ipotizzano L ed R ordinati, essendo
i=j=1, L[i] e R[j] rappresentano i minimi dei rispettivi vettori.
➢
Passo induttivo: Ipotizzo la proprietà vera per k=n-1, per k=n vengono valutati i minimi
R[j] ed L[i], il minore fra i due viene posto in A[k], poiché A[1..k-1] era ordinato con gli
elementi di R ed L, anche A[1...k] sarà ordinato, inoltre l'aggiornamento degli indici i e j
Mergesort, Quicksort, Countingsort
6
Capitolo I
Merge sort
assicura che il minimo di L ed R sia ancora in posizione i e j.
➢
Conclusione: all'uscita dal ciclo, k = n+1, ma, il vettore ha una lunghezza n, quindi A[1...n]
è ordinato correttamente, mentre in L[i] ed R[j] rimangono le sole sentinelle.
I.4 Analisi di complessità
Il mergesort gira nel caso peggiore con complessità rispetto al tempo Θ (n log 2 n). I fattori costanti
non sono buoni, così non si usa mergesort per piccoli array. Siccome la funzione merge fa copie dei
sottoarray, non opera in-place (cioè l’ordinamento non viene fatto sul posto) . Mergesort si affida
pesantemente alla funzione di merge che ha complessità lineare rispetto al tempo.
Passiamo ora ai dettagli dell'analisi.
Il numero di attivazioni della procedura merge-sort dipende dal numero di componenti del vettore
da ordinare. Per un'analisi del mergesort è conveniente disegnare un albero per rappresentare le
chiamate ricorsive.
Per semplicità, consideriamo un vettore iniziale che abbia n elementi, con n potenza di 2 (n=2k).
[ k=log2n ]. Ecco una tabella riassuntiva delle attivazioni:
Livello
(1)
(2)
(3)
...
(i)
...
log2n+1=k+1
Attivazioni di mergesort:
Attivazioni
1 attivazione su un vettore di n (=2k) componenti
2 attivazioni su 2 vettori di n/2 (=2k-1) componenti
4 attivazioni su 4 vettori di n/4 (=2k-2) componenti
2i-1 attivazioni su 2i-1 vettori di n/(2i-1) (=2k-i+1) elementi;
2k (=n) attivazioni su 2k vettori di 1 componente.
1 + 2 + 4 + ... + 2k = 2k+1 - 1 = 2^(log2n+1) - 1 = 2 x 2^(log2n) - 1 = 2n - 1 = O(n)
Mergesort, Quicksort, Countingsort
7
Capitolo I
Merge sort
Operazioni di confronto:
Ad ogni livello h (h >1) si fa il merge di 2h-1 vettori di lunghezza n/2h-1 [si veda la tabella sopra]
poiche', detta L la lunghezza dei due vettori da fondere, il numero di confronti del merge è (nel caso
peggiore) 2L, il merge di due vettori al livello h richiede 2*(n/2h-1) confronti ma, poiché al livello h
ci sono 2h-1 vettori da fondere, il numero totale di confronti a tale livello e' fisso e vale 2n.
Pertanto, il numero globale di confronti nei k livelli vale: time(n) = 2*n*k = Θ(n*log2n) e
Costomerge = n + Θ (n*log2n) = Θ (n*log2n) .
Dunque il costo dell’algoritmo mergeSort è espresso dalla seguente equazione di ricorrenza:
dove con f(n) abbiamo indicato il costo della procedura di merge. Poiché f (n) = O(n), per il teorema
Master abbiamo che: T(n) = O(n log(n))
Il problema dell'ordinamento di n elementi ha delimitazione inferiore complessità pari a
n*log2n.Ciò implica che l’algoritmo Merge-Sort è un algoritmo di ordinamento ottimale.
Possiamo quindi concludere che la complessità computazionale di MERGESORT È O(n log(n)).
Non esistono casi più favorevoli di altri: l’algoritmo adotta lo stesso comportamento a prescindere
dalla disposizione iniziale degli elementi nella sequenza da ordinare.
Mergesort, Quicksort, Countingsort
8
Capitolo II
Quick sort
Capitolo II
Quick sort
Uno dei più popolari algoritmi di ordinamento è il quicksort che ha complessità O(nlogn) in media
e O(n2) nel caso peggiore. Con le giuste precauzioni il caso peggiore si può evitare. Quicksort ha
inoltre il vantaggio di ordinare sul posto e funziona bene anche in ambienti con memoria virtuale.
Quicksort, come Mergesort, è basato sul paradigma “Divide et impera”, ossia sull’idea di poter
suddividere il problema in sotto-problemi di uguale natura, ma via via sempre più semplici da
risolvere; ovviamente tale strategia è vantaggiosa solo se lo sforzo necessario successivamente per
ricomporre le soluzioni dei sotto-problemi ed ottenere la soluzione del problema iniziale, è inferiore
all’impegno necessario per risolvere il problema nel suo complesso con un algoritmo diretto.
Questo tipo di strategia si presta molto bene ad essere implementata mediante un algoritmo
ricorsivo, che viene applicato ad istanze sempre più piccole, fino ad arrivare ad istanze elementari
del problema originale, che possano essere risolte in via diretta con una sola operazione, senza
dover innescare altre chiamate ricorsive.
Nello specifico la suddivisione del problema in due sotto-problemi analoghi operata da
QUICKSORT è basata sull’idea di separare ogni volta gli elementi “piccoli” da quelli “grandi”,
scegliendo in modo arbitrario ogni volta una soglia per definire un metro per distinguere tra
elementi grandi ed elementi piccoli. L’elemento che svolge il ruolo di soglia è chiamato pivot,
perché è proprio una sorta di “perno” attorno al quale si spostano gli elementi della sequenza da
riordinare.
II.1 Funzionamento
In particolare per ordinare un array A[p..r] con il Quicksort agiremo in questo modo:
➢
Divide : Partizioniamo l’array A[p..r] due sotto-array A[p..q-1] e A[q + 1..r] (eventualmente
vuoti) tali che ogni elemento di A[p..q-1] sia minore o uguale ad A[q] che, a sua volta, è
minore o uguale a ogni elemento di A[q + 1..r] .Calcoliamo l’indice q come parte di questa
procedura di partizionamento;
➢
Impera : Ordiniamo i due sotto-array A[p..q-1] e A[q + 1..r] richiamando ricorsivamente
l’algoritmo quicksort;
➢
Combina : Poiché i sotto-array sono ordinati sul posto, non occorre alcun lavoro per
combinarli: l’intero array A[p..r] è ordinato.
Complessivamente potremmo schematizzare l’intero procedimento di ordinamento come nel
diagramma esemplificativo rappresentato nella figura seguente.
Mergesort, Quicksort, Countingsort
9
Capitolo II
Quick sort
II.2 Pseudo-codice
QuickSort(A, p, r )
1 if p < r
2
then q  Partition(A, p, r )
3
QuickSort(A, p, q - 1)
4
QuickSort(A, q + 1, r )
Dunque il quicksort lavora partizionando l'array che deve essere ordinato e ricorsivamente ordina
ogni parte (l'algoritmo è ricorsivo nel senso che si richiama su ciascun sotto-vettore fino a quando
non si ottengono sotto-vettori di lunghezza 1: a questo punto il vettore di partenza risulta ordinato) .
L’elemento chiave dell’algoritmo è la procedura Partition, che riarrangia il sotto-array A[p..r] sul
posto.
Mergesort, Quicksort, Countingsort
10
Capitolo II
Quick sort
Partition(A, p, r )
1 x  A[r ] . elemento pivot
2 i  p - 1 . dim di A[mid + 1..right]
3 for j  p to r - 1
4
do if A[j ] <= x
5
then i  i + 1
6
scambia A[i ] con A[j ]
7 scambia A[i + 1] con A[r ]
8 return i + 1
In Partition un elemento dell'array è selezionato come valore pivot. I valori più piccoli del pivot
sono sistemati a sinistra del pivot, mentre i più grandi a destra. Il tempo di esecuzione di Partition
per un array di dimensione n è Θ (n).
In figura a), il pivot selezionato è 3. Gli indici sono a entrambi gli estremi dell'array. Un indice
comincia sulla sinistra e seleziona un elemento che è più grande del pivot, mentre un altro indice
comincia sulla destra e seleziona un elemento minore del pivot. In questo caso, i numeri 4 e 1 sono
selezionati. Questi elementi sono allora scambiati, come mostrato in figura b). Questo processo si
ripete finché tutti gli elementi a sinistra del pivot sono minori o uguali al pivot, e tutti gli elementi
alla destra del pivot sono >= del pivot. Quicksort ordina ricorsivamente i due sotto-array e alla fine
risulterà l'array mostrato in figura c).
a)
p
4
2
b)
p
1
2
c)
1
3
pivot
3
2
r
1
5
r
4
5
3
4
5
Come il processo avanza, può essere necessario muovere il pivot in modo che l'ordine corretto
rimanga. In questa maniera quicksort segue nell'ordinare l'array. Se siamo fortunati il pivot
selezionato sarà la media di tutti i valori, dividendo in maniera equa l'array. Se è questo il caso
l'array è diviso ad ogni passo a metà, e Partition deve esaminare tutti e n gli elementi: la
complessità sarà O(nlogn). Per trovare un valore pivot, Partition potrebbe semplicemente
selezionare il primo elemento (A[p]). Tutti gli altri valori dovrebbero essere comparati al valore
pivot, e messi o a destra o a sinistra del pivot. Comunque, c'è un caso che fallisce miseramente. Si
supponga che l'array è all'inizio in ordine. Partition selezionerà sempre il valore più basso come
pivot e dividerà l'array con un elemento nella parte sinistra, e r-p elementi nell'altra. Ogni chiamata
ricorsiva a quicksort diminuirà la grandezza dell'array. Perciò n chiamate ricorsive saranno richieste
per fare l'ordinamento, risultando di complessità O(n2).
Una soluzione a questo problema è selezionare casualmente un elemento come pivot. Questo
renderebbe estremamente sfortunata l'occorrenza del caso peggiore.
Mergesort, Quicksort, Countingsort
11
Capitolo II
Quick sort
II.3 Analisi di correttezza
Per l’analisi di Correttezza dell’algoritmo utilizziamo il Principio di induzione completa.
➢
➢
➢
Passo base : Il vettore formato da un solo elemento, p=q=r, è banalmente ordinato.
Passo induttivo : Ipotizzo la proprietà vera per q, e che q sia al centro del vettore. Si
ottiene: A[1..q-1] e A[q + 1..r] ordinati e A[q] posto opportunamente.
Conclusione : Ho un vettore totalmente ordinato.
II.4 Analisi di complessità
Il tempo di esecuzione del QuickSort dipende da come lavora la Partition ed in particolare dal
fatto che il partizionamento sia bilanciato o sbilanciato. Ricordiamo che il partizionamento
sbilanciato si verifica quando la Partition produce due sotto-problemi con n - 1 e zero elementi,
rispettivamente.
La procedura effettua ad ogni passo un numero di confronti proporzionale alla dimensione del
vettore. Queste dimensioni però non sono fisse a ogni passo, ma dipendono da come è stato scelto il
pivot al passo precedente. Il numero di attivazioni di quicksort dipende quindi dalla scelta del pivot.
Caso peggiore:
Ad ogni passo si sceglie un pivot tale che un sotto-vettore abbia lunghezza 0 Supponiamo che
questo sbilanciamento si verifichi in ogni chiamata ricorsiva; la ricorrenza che definisce il tempo di
esecuzione del QuickSort è:
Sommando i costi ad ogni livello di ricorsione, otteniamo la serie aritmetica il cui valore è Θ(n2).
Basta applicare il metodo della sostituzione per dimostrare che la soluzione della ricorrenza è:
T(n) = T(n - 1) + Θ (n) è T(n) = Θ(n2).
Il tempo di esecuzione Θ(n2) si ha quando l’array è già completamente ordinato.
Caso migliore:
Ad ogni passo si sceglie un pivot tale che il vettore si dimezzi (cioe' si emula il merge sort). Nel
caso di bilanciamento massimo, la procedura Partition genera due sottoproblemi, ciascuno di
dimensione non maggiore di n/2 (uno con dimensione n/2 e l’altro di dimensione n/2 – 1). In questo
caso QuickSort viene eseguito molto più velocemente; la ricorrenza per il tempo di esecuzione è:
T(n) <= 2T(n/2) + Θ (n)
la cui soluzione, per il teorema Master, è:
T(n) = O(n logn)
Mergesort, Quicksort, Countingsort
12
Capitolo II
Quick sort
Caso medio:
La scelta casuale del pivot rende probabile la divisione in due parti aventi circa lo stesso numero di
elementi. Pertanto il tempo del QuickSort nel caso medio è molto più vicino al caso migliore che
non al caso peggiore. Per spiegarne il motivo, dobbiamo capire come il partizionamento influisce
sul comportamento dell’algoritmo.
Supponiamo che Partition produca sempre una ripartizione proporzionale 9-a-1 (in apparenza molto
sbilanciata). In questo caso otteniamo una ricorrenza:
T(n) <= T(9/10n) + T(1/10n) + cn
dove abbiamo incluso la costante c nascosta nel termine Θ (n).
Mostriamo un esempio:
Sommando i costi di ciascun livello abbiamo che T(n) <=cn(h + 1) dove h = log10/9n è l’altezza
dell’albero. Da cui:
T(n) = O(n log10/9 n) = O(n logn)
In effetti anche una ripartizione 99-a-1 determina un tempo di esecuzione pari a O(n logn). Il
motivo è che una qualsiasi ripartizione con proporzionalità costante produce un albero di ricorsione
di profondità Θ (logn); il costo di ciascun livello è O(n). Quindi, il tempo di esecuzione è O(n logn).
Se eseguiamo QuickSort su un input casuale, è poco probabile che il partizionamento avvenga
sempre nello stesso modo ad ogni livello. E’ logico supporre, invece, che qualche ripartizione sarà
ben bilanciata e qualche altra sarà molto sbilanciata
Nel caso medio Partition produce una combinazione di ripartizioni “buone” e di ripartizioni
“cattive” distribuite a caso nell’albero. Si può quindi assumere che le ripartizioni buone e cattive si
alternino nei vari livelli dell’albero.
Mergesort, Quicksort, Countingsort
13
Capitolo III
Counting sort
Capitolo III
Counting sort
Il Counting sort è un algoritmo sorprendentemente semplice e potente in quanto è in grado di
ordinare un vettore di n elementi in tempo lineare. Questo è possibile in quanto l’algoritmo non
opera esclusivamente sul confronto di elementi, ma fa delle ipotesi aggiuntive.
Il Counting sort suppone che ciascuno degli n elementi da ordinare sia un intero compreso tra 0 e k.
Quando k = O(n), l’ordinamento viene effettuato in un tempo Θ (n).
È bene osservare che negli algoritmi visti nelle pagine precedenti non avevamo mai posto un
vincolo così forte sul valore degli elementi dell’insieme.
III.1 Funzionamento
Il concetto che sta alla base del counting sort è determinare, per ogni elemento x in input, il numero
di elementi minori di x. Questa informazione può essere utilizzata per inserire l’elemento x
direttamente nella sua posizione nell’array di output.
Ad esempio, se ci sono 13 elementi minori di x, allora x deve andare nella posizione 14. Questo
schema va leggermente modificato per gestire la presenza di più elementi con lo stesso valore, per
evitare che siano inseriti nella stessa posizione.
Un’importante proprietà dell’algoritmo Counting Sort è la stabilità: gli elementi con lo stesso valore
compaiono nell’array di output nello stesso ordine in cui si trovano nell’array di input. Ovvero,
l’uguaglianza di due numeri viene risolta applicando la seguente regola: il numero che si presenta
per primo nell’array di input sarà inserito per primo nell’array di output. E’ bene notare che se
invece di procedere dall’elemento di indice maggiore a quello minore durante l’assegnazione al
vettore di output, si procedesse dal minore al maggiore, si perderebbe la proprietà di stabilità.
Complessivamente potremmo schematizzare l’intero procedimento di ordinamento di una sequenza
come nel diagramma esemplificativo rappresentato nella figura seguente:
Mergesort, Quicksort, Countingsort
14
Capitolo III
Counting sort
III.2 Pseudo-codice
L’algoritmo è composto da una sequenza di cicli privi di nidificazione. Nel codice, supponiamo che
l’input sia un array A[1.. n], occorrono altri due arrai: B[1.. n] conterrà l’output ordinato e C[0.. k] è
d’appoggio.
Counting-Sort (A, B, k)
for i  0 to k
// Θ(k)
do C[i]  0
//azzera gli elementi di C
for j  1 to length[A]
//Θ(n)
do C[A[j]]  C[A[j]]+1 //C[i] = numero di elementi = i
for i  1 to k
//Θ(k)
do C[i] = C[i] + C[i-1] //C[i] = numero di elementi ≤ i
for j  length[A] downto 1 //Θ(n)
do B[C[A[j]]] = A[j]
//inserisce gli elementi al giusto posto in B
C[A[j]] = C[A[j]]–1
III.3 Analisi di complessità
Esaminando l’algoritmo si osserva che vi sono due cicli di lunghezza k e due di lunghezza n. I cicli
for di riga 1 e 5 costano Θ(k) mentre quelli di riga 3 e 7 costano Θ (n). Pertanto il costo complessivo
è Θ(k + n). Nel caso in cui k = O(n) allora Θ(k + n) = Θ (n).
Mergesort, Quicksort, Countingsort
15
Capitolo IV
Modalità di testing
Capitolo IV
Modalità di testing
Il testing degli algoritmi viene condotto per verificare le analisi teoriche compiute in precedenza. Il
testing, inoltre, consente di valutare i termini costanti di tempo, che in una analisi asintotica
vengono trascurati. La definizione di un “banco di prova” per un algoritmo deve dunque tener conto
delle problematiche di rilevazione delle prestazioni dello stesso, oltre che garantirne il
funzionamento corretto. Per consentire la raccolta di tali dati, si è realizzato un tool software che
esaminasse i tre algoritmi presentati sia verificando il tempo effettivo di esecuzione, che il numero
di operazioni necessarie a compiere l'ordinamento. I dati così raccolti sono poi stati esaminati
graficamente per effettuare un'analisi comparativa fra i tre algoritmi.
IV.1 Tool per il testing
Il tool di testing realizzato in “testbench.cpp” compie le seguenti operazioni:
➢
valutazione dei tempi di esecuzione dell'algoritmo;
➢
controllo del numero di operazioni eseguite dall'algoritmo;
➢
verifica di correttezza dell'ordinamento.
IV.2 Valutazione del tempo di esecuzione
La valutazione dei tempi è un'operazione molto delicata. In un moderno computer, infatti, i tempi di
calcolo sono influenzati da un gran numero di fattori. I tempi ottenuti sono da intendersi come
affetti da un'incertezza variabile e difficilmente quantificabile, che, tuttavia, si può considerare
sempre uguale per tutti gli algoritmi eseguiti (A patto di mantenere il sistema operativo in uno stato
utente sempre uguale). La lettura dei risultati sperimentali potrebbe dunque mostrare qualche
leggera incongruenza rispetto ai risultati teorici.
Un altro importante fattore da considerare è la scala di grandezza con cui sono campionati i tempi
di inizio e fine dell'algoritmo. Gli elaboratori moderni eseguono operazioni in tempi nell'ordine dei
nanosecondi. Il campionamento eseguito dal programma di test è fatto invece in millisecondi.
Questa differenza di ordini di grandezza comporta che per input di dimensione contenuta il tempo
di esecuzioni risulti nullo. Allo stesso modo, input differenti, ma non troppo diversi nella
dimensione, potrebbero presentare stesso tempo di esecuzione.
Quanto detto finora, in alcuni casi, può dar luogo a risultati talvolta inesatti, ad esempio, due vettori
di dimensioni simili e non troppo grandi, potrebbero presentare un tempo di ordinamento nullo per
il più grande dei due, e non nullo per il più piccolo. Questo risultato è da intendersi come affetto da
una imprecisione dovuta ad una delle variabili non controllabili dell'ambiente di esecuzione
dell'algoritmo.
Le imprecisioni presentate potrebbero far dubitare dell'effettiva utilità del test condotto, tuttavia, i
risultati sono comunque indicativi del comportamento generale dell'algoritmo, anche se non ne
identificano precisamente le prestazioni. Inoltre, il tempo di esecuzione, confrontato fra algoritmi
differenti, nonostante le imprecisioni, permette di comprendere il peso delle costanti di tempo delle
Mergesort, Quicksort, Countingsort
16
Capitolo IV
Modalità di testing
operazioni adoperate dall'algoritmo, non valutabili in altro modo.
I tempi sono valutati facendo un semplice campionamento del tempo di sistema (fornito dalle
librerie del sistema operativo), subito prima e subito dopo l'esecuzione dell'ordinamento. Il tempo di
ordinamento è ottenuto come differenza fra i due tempi ottenuti.
IV.3 Valutazione del numero di operazioni
Per una valutazione più precisa delle prestazioni del singolo algoritmo, si è introdotto anche un
controllo sul numero di operazioni eseguite. La scelta di questo controllo è dettata dalla necessità di
rendere indipendente la stima delle prestazioni dal tempo, che è una variabile troppo influenzata da
fattori esterni. Il numero di operazioni è un indice preciso per la stima del comportamento asintotico
dell'algoritmo per via sperimentale, ma, a differenza del tempo, non aiuta molto a comprendere le
differenze in termini delle costanti di tempo delle singole operazioni.
L'introduzione del conteggio delle operazioni all'interno dell'algoritmo comporta chiaramente un
incremento del tempo effettivo di esecuzione. Tuttavia, tale incremento, essendo costante e
proporzionale proprio al numero di operazioni eseguite, non falsa i risultati ottenuti.
Per esporre il modo di conteggio delle operazioni, esaminiamo il seguente frammento di codice:
int n1 = q-p+1;
int n2 = r-q;
int *left = new int[n1];
int *right = new int[n2];
for(int i=0; i<n1 ; i++){
left[i] = a[p+i];
}
for(int j=0; j<n2 ; j++){
right[j] = a[q+j+1];
}
mopNumber++;
mopNumber++;
mopNumber++;
Ciascun gruppo di operazioni è incluso in un “blocco” che viene conteggiato singolarmente. Quindi,
tutte le operazioni prima del primo ciclo for vengono conteggiate come un'unica operazione,
formando il primo blocco, i successivi due blocchi sono i due cicli for. Da notare che questa scelta è
stata fatta poiché sarebbe superfluo considerare ciascuna operazione singolarmente, in quanto non
verrebbe fornita alcuna informazione aggiuntiva. Le istruzioni presenti in un blocco, infatti, sono
istruzioni che vengono sempre eseguite tutte, se la prima istruzione di quel blocco è eseguita,
quindi, conteggiarle singolarmente aggiungerebbe una costante moltiplicativa sempre uguale in
ogni ripetizione dell'algoritmo, non portando, di fatto, informazioni aggiuntive.
IV.4 Verifica di correttezza
La verifica di correttezza dell'algoritmo non fa altro che controllare che il vettore da ordinare,
all'uscita dall'algoritmo sia effettivamente ordinato. Per farlo, viene adoperata la funzione
isSorted():
Mergesort, Quicksort, Countingsort
17
Capitolo IV
Modalità di testing
bool isSorted(int A[], int dimension){
if(dimension<2)
return true;
for(int i=1; i<dimension ; i++){
if(A[i-1]>A[i])
return false;
}
return true;
}
Per verificare la correttezza di tale funzione, distringuiamo due casi fondamentali:
➢
dimension < 2 : in questo caso il vettore ha 1 o zero elementi, quindi è certamente ordinato e
la funzione restituisce sempre un risultato positivo.
➢
dimension >=2 : in questo caso viene eseguito il ciclo for. L'invariante di ciclo è che il
vettore A[0...i-1] è correttamente ordinato.
Passo base: alla prima esecuzione, risulta essere i=1, quindi il vettore ha un unico elemento
ed è ordinato. Nel ciclo, si esamina il secondo elemento del vettore, se è minore del primo,
allora il vettore non è ordinato, e la funzione restituisce un risultato negativo. In caso
contrario il ciclo prosegue.
Passo induttivo: supponendo che per i=j-1 l'invariante sia valida, verifichiamo che lo sia
anche per i=j. Quando si esamina A[j-1], se è maggiore di A[j], la funzione restituisce un
risultato negativo, se invece risulta che A[j-1] è minore di A[j] il ciclo prosegue. Ma poiché
A[0...j-1] è un vettore ordinato, e A[j-1] ed A[j] sono fra loro in ordine, il vettore A[0...j]
sarà anch'esso ordinato.
Passo conclusivo: All'uscita del ciclo i=dimension, ossia, poiché è ancora valida
l'invariante, il vettore A[0...dimension-1] è ordinato, ma questo vettore è tutto il vettore
restituito dalla funzione di ordinamento, che risulta correttamente ordinato, quindi la
funzione restituisce un risultato positivo.
Mergesort, Quicksort, Countingsort
18
Capitolo V
Realizzazione degli algoritmi
Capitolo V
Realizzazione degli algoritmi
Gli algoritmi sono stati realizzati in linguaggio C++, basandosi sullo pseudo-codice presentato in
precedenza. In alcuni punti, sono state apportate piccole modifiche per risolvere problemi
realizzativi.
V.1 Merge sort
unsigned long long mergesort(int a[], int p, int r){
if( p < r ){
int q = (p+r)/2;
mopNumber++;
mergesort(a,p,q);
mergesort(a,q+1,r);
merge(a,p,q,r);
}
return mopNumber;
}
Riguardo l'implementazione di mergesort, ci si è scostati poco dalla realizzazione in pseudo-codice.
Da notare che la funzione restituisce il numero di operazioni compiute. Questa soluzione
chiaramente non è necessaria nell'algoritmo, ma risulta utile per valutare le operazioni compiute.
void merge(int a[], int p, int q, int r){
int n1 = q-p+1;
int n2 = r-q;
// Creazione vettori di supporto
int *left = new int[n1];
int *right = new int[n2];
// Popolamento vettori di supporto
for(int i=0; i<n1 ; i++){
left[i] = a[p+i];
}
for(int j=0; j<n2 ; j++){
right[j] = a[q+j+1];
}
// Merge ordinato dei vettori
int i=0;
int j=0;
int k=p;
while(i<n1 && j<n2){
if(left[i]<right[j]){
a[k] = left[i];
i++;
} else {
a[k] = right[j];
j++;
}
k++;
}
Mergesort, Quicksort, Countingsort
mopNumber++;
mopNumber++;
mopNumber++;
mopNumber++;
mopNumber++;
mopNumber++;
mopNumber++;
19
Capitolo V
Realizzazione degli algoritmi
while(i<n1){
a[k] = left[i];
i++;
k++;
}
while(j<n2){
a[k] = right[j];
j++;
k++;
}
mopNumber++;
mopNumber++;
// Deallocazione vettori di supporto
delete[] left;
delete[] right;
}
La funzione merge presenta una sostanziale differenza rispetto alla versione in pseudo-codice. Si
noti infatti l'assenza di sentinelle. Questa scelta è stata compiuta per evitare di dover scegliere un
valore massimo per la creazione della sentinella. In alternativa si è adoperato un controllo sugli
indici dei vettori L ed R. Tale controllo sostituisce il controllo sul valore di k, in quando la somma
delle dimensioni massime degli indici i e j è pari alla dimensione massima dell'indice k. Quando si
esce dal primo ciclo while, uno dei due vettori di supporto è stato completamente percorso, quindi, i
restanti elementi da inserire appartengono tutti all'altro vettore. Questo è il significato dei due
successivi cicli while. L'analisi di complessità fatta sullo pseudo-codice rimane valida in quanto il
numero di iterazioni rimane sempre lo stesso, pari a r – p +1 . Infatti, in questa versione
dell'algoritmo sono compiute n1 + n2 iterazioni, e sostituendo i valori delle due variabili si ha:
n1 + n2 = q – p + 1 + r – q = r – p + 1.
V.2 Quick sort
unsigned long long quicksort(int A[],int p, int r){
if(p<r){
int q = partition(A,p,r);
quicksort(A,p,q-1);
quicksort(A,q+1,r);
}
opNumber++;
return opNumber;
}
Le considerazioni fatte per la funzione mergesort sulle modifiche necessarie per tenere il conteggio
dello operazioni, sono valide anche per quicksort.
Mergesort, Quicksort, Countingsort
20
Capitolo V
Realizzazione degli algoritmi
int partition(int A[], int p, int r){
int x = A[r];
int i = p-1;
for(int j=p; j<=r-1 ; j++){
if(A[j] <= x){
i = i+1;
swap(&A[i],&A[j]);
}
}
swap(&A[i+1],&A[r]);
return i+1;
}
opNumber++;
opNumber++;
opNumber++;
void swap(int *a,int *b){
int temp = *a;
*a = *b;
*b = temp;
}
opNumber++;
La realizzazione della funzione partition è conforme allo pseudo-codice.
V.3 Counting sort
unsigned long long countingsort(int A[],int lengthA,int B[], int k){
int *C = new int[k+1];
copNumber++;
for(int i=0; i<k+1 ; i++){
C[i] = 0;
}
for(int j=0; j < lengthA ; j++){
C[A[j]] = C[A[j]] + 1;
}
for(int i=1 ; i <= k ; i++){
C[i] = C[i] + C[i-1];
}
for(int j=lengthA-1 ; j>=0 ; j--){
B[C[A[j]]-1] = A[j];
C[A[j]] = C[A[j]]-1;
}
copNumber++;
copNumber++;
copNumber++;
copNumber++;
delete[] C;
return copNumber;
}
Le considerazioni fatte per le funzioni mergesort e quicksort sulle modifiche necessarie per tenere il
conteggio dello operazioni, sono valide anche per countingsort. L'algoritmo è per il rimanente
conforme allo pseudo-codice.
Mergesort, Quicksort, Countingsort
21
Capitolo VI
Testing delle prestazioni
Capitolo VI
Testing delle prestazioni
Utilizzando il tool Testbench si sono raccolti dati sperimentali sui tre algoritmi presentati. Nel
seguito è compiuta un'analisi degli algoritmi, verificando prima la coincidenza fra risultati
sperimentali e analisi teorica, e, in seguito, effettuando un'analisi comparativa fra i tre algoritmi. I
grafici presentati sono stati realizzati tramite la funzione spline.
Il sistema su cui sono stati raccolti i dati presenta la seguente configurazione:
Sistema operativo
Microsoft Windows XP Versione 2002 service pack 2
CPU
Intel T2250 @ 1,73 Ghz, 1,73 Ghz (dual core)
Memoria RAM
2,0 GB SDRAM
VI.1 Merge sort
Per analizzare le prestazioni del mergesort, si è eseguito l'algoritmo su tre tipi di vettore: generato
casualmente, ordinato, contro ordinato. L'algoritmo è stato eseguito 15 volte su ogni tipo di vettore,
ad ogni ripetizione la dimensione dell'input veniva raddoppiata. La prima esecuzione ha avuto in
input un vettore di 5.000 unità, la quindicesima, un vettore di 81.920.000 unità.
VI.1.1 Analisi del tempo di esecuzione
I grafici ottenuti, esaminando il tempo di esecuzione sono presentati nel seguito, sulle ordinate è
presente il tempo in millisecondi, mentre sulle ascisse la dimensione del vettore:
65000
60000
55000
50000
45000
40000
35000
Contro-ordinato
30000
Ordinato
Random
25000
20000
15000
10000
5000
0
0
25000000
Mergesort, Quicksort, Countingsort
50000000
75000000
100000000
22
Capitolo VI
Testing delle prestazioni
Dal grafico è evidente come in tutti e tre i casi l'andamento del tempo rispetto alla grandezza
dell'input sia rappresentato da una retta di pendenza ridotta. E' tuttavia interessante notare come nel
caso di vettore casuale, il tempo impiegato per l'ordinamento sia leggermente maggiore, rispetto
agli altri casi, ordinato e contro-ordinato, per i quali il tempo di ordinamento è esattamente lo
stesso.
VI.1.2 Analisi del numero di operazioni
I grafici ottenuti, esaminando il tempo di esecuzione sono presentati nel seguito, sulle ordinate è
presente il numero di operazioni eseguite, mentre sulle ascisse la dimensione del vettore:
3000000000
2750000000
2500000000
2250000000
2000000000
1750000000
Contro-ordinato
1500000000
Ordinato
Random
1250000000
1000000000
750000000
500000000
250000000
0
0
5000000
10000000
15000000
20000000
25000000
Anche il numero di operazioni cresce con l'input secondo una legge rappresentata da una retta.
Come era facile aspettarsi osservando i risultati dei tempi, il vettore random ha bisogno di un
maggior numero di operazioni per essere ordinato. E' interessante notare come, anche se in numero
molto ridotto, il vettore ordinato richieda più operazioni del vettore contro-ordinato. Questa
caratteristica non era risultata nel grafico temporale, dove, evidentemente, visto il numero ridotto di
operazioni, il campionamento del tempo al millisecondo non è sufficiente ad evidenziare la
differenza.
VI.1.3 Confronto con i risultati teorici
Quanto evidenziato dai dati sperimentali è in completo accordo con l'analisi teorica compiuta, nel
grafico riportato di seguito, è stata riportata anche la funzione nLog(n), dove n è la grandezza
dell'input.
Mergesort, Quicksort, Countingsort
23
Capitolo VI
Testing delle prestazioni
3000000000
2750000000
2500000000
2250000000
2000000000
1750000000
Contro-ordinato
1500000000
Ordinato
Random
nLog(n)
1250000000
1000000000
750000000
500000000
250000000
0
0
5000000
10000000
15000000
20000000
25000000
E' possibile vedere dal grafico come l'andamento teorico individuato combaci perfettamente con i
risultati raccolti sperimentalmente. Il grafico riporta il numero di operazioni sulle ordinate, tuttavia,
un risultato analogo si sarebbe ottenuto considerando i tempi.
Mergesort, Quicksort, Countingsort
24
Capitolo VI
Testing delle prestazioni
VI.2 Quick sort
L'analisi sperimentale del quicksort è risultata più complessa rispetto a quella compiuta per il
mergesort. Sebbene siano entrambi algoritmi ricorsivi, il quicksort effettua un numero
sensibilmente maggiore di chiamate rispetto al mergesort se il vettore è già ordinato (o controordinato), quindi, se si creano delle partizioni del vettore sbilanciate. Questa profonda ricorsione
comporta un rischio non remoto di stack overflow nel caso di input di dimensione considerevole.
Per questo motivo si è dovuto sensibilmente ridurre l'ordine di grandezza dei vettori su cui sono
stati effettuati i test.
Per analizzare le prestazioni del quicksort, si è eseguito l'algoritmo su tre tipi di vettore: generato
casualmente, ordinato, contro ordinato. L'algoritmo è stato eseguito 7 volte su ogni tipo di vettore,
ad ogni ripetizione la dimensione dell'input veniva raddoppiata. La prima esecuzione ha avuto in
input un vettore di 500 unità, la settima, un vettore di 32.000 unità. Dimensioni maggiori del
vettore, sul sistema adoperato per eseguire i test, hanno causato errori di stack overflow.
VI.2.1 Analisi del tempo di esecuzione
I grafici ottenuti, esaminando il tempo di esecuzione sono presentati nel seguito, sulle ordinate è
presente il tempo in millisecondi, mentre sulle ascisse la dimensione del vettore:
16
15
14
13
12
11
10
9
8
Random
7
6
5
4
3
2
1
0
0
5000
10000
15000
20000
25000
30000
35000
Nel caso di un vettore generato casualmente, il tempo di esecuzione rilevato non è indicativo poiché
la dimensione ridotta del vettore rende le operazioni troppo veloci per un campionamento del
tempo fatto al millisecondo. Tuttavia, quando il vettore è generato casualmente, è improbabile che
presenti sbilanciamento nel partizionamento. E' stato dunque eseguito un test con vettori di
dimensione pari a quelli usati per il mergesort, ma generati soltanto casualmente, e, come era lecito
aspettarsi, non si sono verificati stack overflow. I risultati sono presentati nel seguente grafico:
Mergesort, Quicksort, Countingsort
25
Capitolo VI
Testing delle prestazioni
275000
250000
225000
200000
175000
150000
Random
125000
100000
75000
50000
25000
0
0
10000000
20000000
30000000
40000000
50000000
60000000
70000000
80000000
90000000
Si può vedere come l'andamento del tempo rispetto alla dimensione dell'input è tendente al lineare,
specialmente per grandi dimensioni dell'input. La parte iniziale non presenta questo andamento per
via delle interferenze causate dal sistema operativo nella gestione delle chiamate ricorsive, che
introducono ritardi non strettamente dipendenti dall'algoritmo. Per grandi dimensioni dell'input, il
contributo di queste interferenze è proporzionalmente inferiore, quindi si assume un andamento
lineare caratteristico del funzionamento dell'algoritmo.
1400
1300
1200
1100
1000
900
800
700
Contro-ordinato
600
Ordinato
500
400
300
200
100
0
0
5000
10000
15000
20000
25000
30000
35000
Il grafico mostra come nei casi di partizionamento sbilanciato il tempo di esecuzione sia molto più
grande rispetto al parizionamento bilanciato. Un vettore casuale di 32.000 elementi è infatti
ordinato in 16 millisecondi, mentre un vettore ordinato sempre di 32.000 elementi è ordinato in
circa 1400 millisecondi. L'andamento del tempo rispetto alla dimensione dell'input è di tipo
quadratico.
Mergesort, Quicksort, Countingsort
26
Capitolo VI
Testing delle prestazioni
VI.2.2 Analisi del numero di operazioni
I grafici ottenuti, esaminando il tempo di esecuzione sono presentati nel seguito, sulle ordinate è
presente il numero di operazioni eseguite, mentre sulle ascisse la dimensione del vettore:
Anche per il numero di operazioni si osserva la grande differenza nel caso di partizionamento
bilanciato o sbilanciato:
1100000
1000000
900000
800000
700000
600000
Random
500000
400000
300000
200000
100000
0
0
2000
4000
6000
8000
10000
12000
14000
16000
550000000
500000000
450000000
400000000
350000000
300000000
Contro-ordinato
Ordinato
250000000
200000000
150000000
100000000
50000000
0
0
2500
5000
7500
10000
12500
15000
17500
In questo caso è anche ben visibile la differenza di andamento asintotico fra caso migliore (vettore
random) e caso peggiore (vettore ordinato/contro-ordinato). Nel primo caso l'andamento è descritto
da una retta, nel secondo la dipendenza del numero di operazioni dall'input è di tipo quadratico.
Mergesort, Quicksort, Countingsort
27
Capitolo VI
Testing delle prestazioni
VI.2.3 Confronto con i risultati teorici
Quanto evidenziato dai dati sperimentali è in completo accordo con l'analisi teorica compiuta, nel
grafico riportato di seguito è stata riportata anche la funzione nLog(n), dove n è la grandezza
dell'input. Nel caso migliore infatti, il numero di operazioni assume tale andamento rispetto alla
dimensione dell'input:
1200000
1100000
1000000
900000
800000
700000
600000
Random
nLog(n)
500000
400000
300000
200000
100000
0
0
1000
2000
3000
4000
5000
6000
7000
8000
9000
10000 11000 12000 13000 14000 15000 16000
550000000
500000000
450000000
400000000
350000000
300000000
Contro-ordinato
250000000
Ordinato
n^2
200000000
150000000
100000000
50000000
0
0
2000
4000
6000
8000
10000
12000
14000
16000
Nel caso peggiore, quindi con partizioni sbilanciate, l'andamento del numero di operazioni rispetto
alla dimensione dell'input è di tipo quadratico, come è mostrato nel precedente grafico.
Mergesort, Quicksort, Countingsort
28
Capitolo VI
Testing delle prestazioni
VI.3 Counting sort
Per analizzare le prestazioni del countingsort, si è eseguito l'algoritmo su tre tipi di vettore: generato
casualmente, ordinato, contro ordinato. L'algoritmo è stato eseguito 15 volte su ogni tipo di vettore,
ad ogni ripetizione la dimensione dell'input veniva raddoppiata. La prima esecuzione ha avuto in
input un vettore di 5.000 unità, la quindicesima, un vettore di 81.920.000 unità.
VI.3.1 Analisi del tempo di esecuzione
I grafici ottenuti, esaminando il tempo di esecuzione sono presentati nel seguito, sulle ordinate è
presente il tempo in millisecondi, mentre sulle ascisse la dimensione del vettore:
4000
3750
3500
3250
3000
2750
2500
2250
Contro-ordinato
Ordinato
2000
1750
Random
1500
1250
1000
750
500
250
0
0
20000000
40000000
60000000
80000000
100000000
Il tempo di esecuzione ha un andamento lineare rispetto alla dimensione dell'input sommata al
massimo elemento da ordinare. Si può notare come i tempi in caso di vettore random siano quasi il
doppio rispetto al caso di vettore ordinato.
VI.3.2 Analisi del numero di operazioni
I grafici ottenuti, esaminando il tempo di esecuzione sono presentati nel seguito, sulle ordinate è
presente il numero di operazioni eseguite, mentre sulle ascisse la dimensione del vettore:
Mergesort, Quicksort, Countingsort
29
Capitolo VI
Testing delle prestazioni
170000000
160000000
150000000
140000000
130000000
120000000
110000000
100000000
90000000
80000000
70000000
Contro-ordinato
Ordinato
Random
60000000
50000000
40000000
30000000
20000000
10000000
0
0
5000000
10000000
15000000
20000000
25000000
Dall'analisi del numero di operazioni eseguite emergono due dati molto interessanti. Nei casi di
vettori ordinato e contro-ordinato il numero di operazioni eseguite è praticamente lo stesso, mentre
nei tempi il vettore contro-ordinato è più veloce di circa il 15%. Quindi le operazioni compiute nel
caso di vettore ordinato richiedono più tempo.
Secondo dato molto interessante è che il vettore generato casualmente è quello che richiede meno
operazioni per essere ordinato, meno della metà rispetto agli altri casi, ma impiega il doppio del
tempo. Questo risultato evidenzia il ruolo giocato dalle costanti di tempo, che una semplice analisi
asintotica non può mostrare. La spiegazione di questa differenza è data dai diversi ordini di
grandezza degli elementi da ordinare. Nel caso di vettore ordinato il massimo fra gli elementi da
ordinare era infatti 20'479'999, mentre nel caso di vettore random era 32'700. Questa differenza
spiega la differenza di numero di operazioni e rafforza l'osservazione fatta sulle costanti di tempo.
VI.3.3 Confronto con i risultati teorici
Quanto evidenziato dai dati sperimentali è in completo accordo con l'analisi teorica compiuta,
infatti, non esiste un caso migliore o peggiore, ma tutti gli andamenti sono di tipo lineare, come
facilmente si può osservare dai grafici presentati.
Mergesort, Quicksort, Countingsort
30
Capitolo VI
Testing delle prestazioni
VI.4 Confronto fra i tre algoritmi
Il diagramma riportato nel seguito fa un confronto fra i tre algoritmi nei casi di vettore generato
casualmente, per compararne le rispettive costanti di tempo, non evidenziate dall'analisi asintotica:
275000
250000
225000
200000
175000
150000
Countingsort
Quicksort
Mergesort
125000
100000
75000
50000
25000
0
0
25000000
50000000
75000000
100000000
Dai risultati ottenuti è facile vedere come fra tutti il countingsort sia in assoluto l'algoritmo che
impiega minor tempo. Tuttavia, non va dimenticato che non esegue un ordinamento sul posto, e che
necessita della conoscenza del massimo valore fra gli elementi da ordinare. Il tempo ridotto del
countingsort si spiega facilmente ricordando che è un algoritmo iterativo e che quindi non necessita
di continue chiamate a funzione come fanno mergesort e quicksort.
Questi ultimi due presentano costanti di tempo differenti, in particolare il quicksort utilizza
operazioni più veloci, ma facendo ordinamento sul posto, per input molto grandi risulta
sensibilmente più lento del mergesort. Tuttavia, per input di dimensione fino a circa 10'500'000, il
quicksort risulta più efficiente del mergesort.
Mergesort, Quicksort, Countingsort
31
Appendice A: Codice sorgente
Appendice A: Codice sorgente
TestBench.cpp
void main(){
Selection selection = menuSelection();
fileInitialization();
int dimension = selection.vectorLength;
do{
int *a;
switch(selection.vectorGenerationType){
case 1: a = getRandomVector(dimension);
break;
case 2: a = getSortedVector(dimension);
break;
case 3: a = getRSortedVector(dimension);
break;
}
_SYSTEMTIME *stime = new _SYSTEMTIME;
//Counting sort datas
int *b = new int[dimension];
for(int i=0; i<dimension ; i++){
b[i] = 0;
}
int aMax = a[0];
for(int i=0; i<dimension ; i++){
if(aMax < a[i])
aMax = a[i];
}
//ORDINAMENTO E VERIFICA DEI TEMPI
GetSystemTime(stime);
long startTime = stime->wMinute * 60 * 1000 + stime->wSecond * 1000 + stime->wMilliseconds;
switch(selection.algorithm){
case 1: operationNumber = mergesort(a,0,dimension-1);
break;
case 2: operationNumber = quicksort(a,0,dimension-1);
break;
case 3: operationNumber = countingsort(a,dimension,b,aMax);
break;
}
Mergesort, Quicksort, Countingsort
32
Appendice A: Codice sorgente
GetSystemTime(stime);
long endTime = stime->wMinute * 60 * 1000 + stime->wSecond * 1000 + stime->wMilliseconds;
long totalTime = endTime - startTime;
// VERIFICA ORDINAMENTO
bool sorted;
if(selection.algorithm==3)
sorted = isSorted(b,dimension);
else
sorted = isSorted(a,dimension);
if(!sorted)
printf("\n !!!Errore di ordinamento!!!\n");
delete[] a;
delete[] b;
//Stampa su file
printOnFile(dimension,totalTime);
if(totalTime == 0)
printf("\nInput size: %d
Time: %d
Operations: %u",dimension,totalTime,operationNumber);
printf("\nInput size: %d
Time: %d Operations: %u",dimension,totalTime,operationNumber);
else
dimension = dimension * selection.inputIncreaseFactor;
selection.repetitionNumber--;
}while(selection.repetitionNumber > 0);
printf("\n\n");
system("PAUSE");
}
Mergesort, Quicksort, Countingsort
33
Appendice A: Codice sorgente
bool isSorted(int A[], int dimension){
if(dimension<2)
return true;
for(int i=1; i<dimension ; i++){
if(A[i-1]>A[i])
return false;
}
return true;
}
int *getRandomVector(int dimension){
int *a = new int[dimension];
for(int i=0; i<dimension ; i++)
a[i] = rand();
return a;
}
int *getSortedVector(int dimension){
int *a = new int[dimension];
for(int i=0; i<dimension ; i++)
a[i] = i;
return a;
}
int *getRSortedVector(int dimension){
int *a = new int[dimension];
for(int i=0; i<dimension ; i++)
a[i] = dimension-i;
return a;
}
Mergesort, Quicksort, Countingsort
34
Appendice A: Codice sorgente
Selection menuSelection(){
printf("========= Sort Algorithms Test Bench =========\n\n");
printf("Il programma realizza un testing dei tempi degli algoritmi MergeSort, QuickSort, Counting Sort.");
printf(" L'utente specifica i dati relativi alla generazione dell'input. Viene inoltre specificato quante");
printf(" volte va eseguito il test sull'algoritmo e, ad ogni ripetizione, il fattore moltiplicativo di");
printf(" crescita dell'input. I risultati del test vengono poi memorizzati nel file ResultsLog.txt .\n");
Selection selection;
do{
printf("\n Algoritmo di ordinamento (1.MergeSort,2.QuickSort,3.CountingSort): ");
scanf("%d", &selection.algorithm);
}while(selection.algorithm > 3 || selection.algorithm < 1);
do{
printf("\n Tipo di generazione vettore (1.Random,2.Ordinato,3.Contro-ordinato): ");
scanf("%d", &selection.vectorGenerationType);
}while(selection.vectorGenerationType > 3 || selection.vectorGenerationType < 1);
do{
printf("\n Inserire la grandezza dell'input: ");
scanf("%d", &selection.vectorLength);
}while(selection.vectorLength < 1);
do{
printf("\n Inserire il fattore moltiplicativo dell'input: ");
scanf("%d", &selection.inputIncreaseFactor);
}while(selection.inputIncreaseFactor < 1);
do{
printf("\n Inserire il numero di ripetizioni del test: ");
scanf("%d", &selection.repetitionNumber);
}while(selection.repetitionNumber < 1);
return selection;
}
Mergesort, Quicksort, Countingsort
35
Appendice A: Codice sorgente
void fileInitialization(){
FILE *f = fopen("ResultsLog.txt", "w");
char text1[] = "La seguente tabella riporta: (Dimensione input, tempo di ordinamento, numero operazioni)\n\n";
fwrite(text1,sizeof(text1)-1,1,f);
fclose(f);
}
void printOnFile(int dimension, long totalTime){
FILE *f = fopen("ResultsLog.txt", "a");
char *buffer = new char[80];
char *string1 = _itoa(dimension,buffer,10);
int i=0;
char stopChar = string1[i];
while(stopChar!='\0'){
stopChar = string1[i];
i++;
}
fwrite(string1,i-1,1,f);
char text2[] = " ";
fwrite(text2,sizeof(text2)-1,1,f);
char *string2 = _ltoa(totalTime,buffer,10);
i=0;
stopChar = string2[i];
while(stopChar!='\0'){
stopChar = string2[i];
i++;
}
fwrite(string2,i-1,1,f);
char text3[] = " ";
fwrite(text3,sizeof(text3)-1,1,f);
char *string3 = _ultoa(operationNumber,buffer,10);
i=0;
stopChar = string3[i];
while(stopChar!='\0'){
stopChar = string3[i];
i++;
}
fwrite(string3,i-1,1,f);
fwrite("\n",1,1,f);
fclose(f);
}
Mergesort, Quicksort, Countingsort
36
Appendice A: Codice sorgente
mergesort.cpp
unsigned long long mergesort(int a[], int p, int r){
if( p < r ){
int q = (p+r)/2;
mopNumber++;
mergesort(a,p,q);
mergesort(a,q+1,r);
merge(a,p,q,r);
}
return mopNumber;
}
void merge(int a[], int p, int q, int r){
int n1 = q-p+1;
int n2 = r-q;
// Creazione vettori di supporto
int *left = new int[n1];
int *right = new int[n2];
mopNumber++;
// Popolamento vettori di supporto
for(int i=0; i<n1 ; i++){
left[i] = a[p+i];
mopNumber++;
}
for(int j=0; j<n2 ; j++){
right[j] = a[q+j+1];
mopNumber++;
}
// Merge ordinato dei vettori
int i=0;
int j=0;
int k=p;
mopNumber++;
while(i<n1 && j<n2){
if(left[i]<right[j]){
a[k] = left[i];
i++;
mopNumber++;
} else {
a[k] = right[j];
j++;
mopNumber++;
}
k++;
mopNumber++;
}
Mergesort, Quicksort, Countingsort
37
Appendice A: Codice sorgente
while(i<n1){
a[k] = left[i];
i++;
k++;
mopNumber++;
}
while(j<n2){
a[k] = right[j];
j++;
k++;
mopNumber++;
}
// Deallocazione vettori di supporto
delete[] left;
delete[] right;
}
Mergesort, Quicksort, Countingsort
38
Appendice A: Codice sorgente
quicksort.cpp
unsigned long long opNumber = 0;
unsigned long long quicksort(int A[],int p, int r){
if(p<r){
int q = partition(A,p,r);
opNumber++;
quicksort(A,p,q-1);
quicksort(A,q+1,r);
}
return opNumber;
}
int partition(int A[], int p, int r){
int x = A[r];
int i = p-1;
opNumber++;
for(int j=p; j<=r-1 ; j++){
if(A[j] <= x){
i = i+1;
swap(&A[i],&A[j]);
opNumber++;
}
opNumber++;
}
swap(&A[i+1],&A[r]);
return i+1;
}
void swap(int *a,int *b){
int temp = *a;
opNumber++;
*a = *b;
*b = temp;
}
Mergesort, Quicksort, Countingsort
39
Appendice A: Codice sorgente
countingsort.cpp
unsigned long long copNumber = 0;
unsigned long long countingsort(int A[],int lengthA,int B[], int k){
int *C = new int[k+1];
copNumber++;
for(int i=0; i<k+1 ; i++){
C[i] = 0;
copNumber++;
}
for(int j=0; j < lengthA ; j++){
C[A[j]] = C[A[j]] + 1;
copNumber++;
}
for(int i=1 ; i <= k ; i++){
C[i] = C[i] + C[i-1];
copNumber++;
}
for(int j=lengthA-1 ; j>=0 ; j--){
B[C[A[j]]-1] = A[j];
copNumber++;
C[A[j]] = C[A[j]]-1;
}
delete[] C;
return copNumber;
}
Mergesort, Quicksort, Countingsort
40
Appendice B: il tool TestBench
Appendice B: il tool TestBench
Il tool TestBench è stato sviluppato appositamente per i tre algoritmi presentati nel testo. Il tool
permette di scegliere quale algoritmo testare e come generare l'input. I dati raccolti sono poi
mostrati a video e salvati in un file di log. Nel seguito è mostrato un esempio di funzionamento:
1. Si sceglie l'algoritmo da testare:
2. Si seleziona il modo di generazione dell'input
3. Si indica la dimensione iniziale dell'input:
4. Si specifica il fattore moltiplicativo dell'input, ossia, la variabile che viene moltiplicata alla
dimensione dell'input iniziale, per eseguire ogni successiva ripetizione dell'algoritmo.
Mergesort, Quicksort, Countingsort
41
Appendice B: il tool TestBench
5. Si indica il numero di ripetizioni dell'algoritmo da eseguire (Ad ogni ripetizione l'input
cresce secondo il fattore specificato al punto 4)
6. Vengono stampati a video i risultati ed allo stesso tempo viene creato il file di
ResultsLog.txt
Mergesort, Quicksort, Countingsort
42
Scarica