










Studia grazie alle numerose risorse presenti su Docsity
Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium
Prepara i tuoi esami
Studia grazie alle numerose risorse presenti su Docsity
Prepara i tuoi esami con i documenti condivisi da studenti come te su Docsity
Trova i documenti specifici per gli esami della tua università
Preparati con lezioni e prove svolte basate sui programmi universitari!
Rispondi a reali domande d’esame e scopri la tua preparazione
Riassumi i tuoi documenti, fagli domande, convertili in quiz e mappe concettuali
Studia con prove svolte, tesine e consigli utili
Togliti ogni dubbio leggendo le risposte alle domande fatte da altri studenti come te
Esplora i documenti più scaricati per gli argomenti di studio più popolari
Ottieni i punti per scaricare
Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium
Dispense riguardanti Algoritmi e strutture dati sui seguenti argomenti: - Analisi di algoritmi - Ricerca in una lista - Algoritmi di ordinamento caratteristiche - Selection Sort - Insertion Sort - Bubble Sort - Tecnica di Divide et impera - Merge Sort - Quick Sort - Union-find
Tipologia: Sintesi del corso
1 / 18
Questa pagina non è visibile nell’anteprima
Non perderti parti importanti!











Mauro Tellaroli
9 Quick sort 14 9.1 Note sullo pseudocodice 7............................... 14 9.2 Analisi tempo...................................... 15 9.3 Analisi spazio...................................... 16 9.4 Note sullo pseudocodice 10.............................. 17
10 Union-find 17 10.1 QuickFind........................................ 17 10.1.1 Analisi Tempo................................. 17 10.2 QuickUnion....................................... 18 10.2.1 Analisi Tempo................................. 18 10.3 QuickFind con bilanciamento sulle union....................... 18 10.3.1 Analisi Tempo................................. 18 10.4 QuickUnion bilancito in altezza............................ 18 10.4.1 Analisi Tempo................................. 18
1 Analisi di algoritmi
Utilizzando il criterio di costo uniforme ciascuna operazione viene contata come singolo passo, indipendentemente dalla dimensione degli operandi coinvolti nell’operazione stessa. Sebbene sia comodo, si tratta di un modello troppo idealizzato: esso assume infatti che registri e memoria siano costruiti da parole di grandezza arbitrariamente grande. A risolvere questo problema `e il criterio di costo logaritmico che assume che il costo di esecuzione delle istruzioni dipenda dalla dimensione degli operandi coinvolti. Quindi eseguire un’operazione su un numero n implica pagare un costo proporzionale a log n.
Data una funzione f (n) definiamo:
Pi`u intuitivamente, per n sufficientemente grande e per opportune costanti c, c 1 , c 2 si ha:
Si puo esprimere il costo di un algoritmo A e la complessita di un problema P attraverso la notazione asintotica:
L’algoritmo controlla in sequenza gli elementi dell’insieme, arrestandosi quando trova l’elemento cercato; non potendosi avvalere di alcun ordinamento tra gli elementi, l’algoritmo pu`o concludere con certezza che l’insieme non contiene alcun elemento corrispondente solo dopo averli verificati tutti.
Pseudocodice 1: Ricerca sequenziale 1 Algoritmo ricercaSequenziale (array A[0...n − 1], elemento x) → booleano 2 i ← 0 3 while i < n and A[i] 6 = x do 4 i ← i + 1 5 if i = n then return False 6 else return True
2.1.1 Analisi della ricerca sequenziale
Caso migliore. L’elemento `e in prima posizione; la ricerca esegue solo un confronto:
Tbest(n) = 1
Caso peggiore. L’elemento `e nell’ultima posizione o non appartiene alla lista; la ricerca controlla tutti gli elementi: Tworst(n) = n
Caso medio. Come visto nel paragrafo 1.4 per calcolare il caso medio abbiamo bisogno della probabilita del verificarsi di un istanza. Un’ipotesi sensatae: se x e nella lista, puo occupare qualsiasi posizione (pos) con la stessa probabilita, e quindi per ogni possibile posizione la probabilita e (^) n^1 ; inoltre se xe in posizione i verr`a trovato dopo i confronti, quindi: { ∀i ∈ [1, n] P(pos(x) = i) = (^) n^1 ∀i T (i) = i
Tavg (n) =
∑^ n
i=
(P(pos(x) = i) · i) =
∑^ n
i=
n
· i) =
n
∑^ n
i=
i =
n
n(n + 1) 2
n + 1 2
La ricerca binaria richiede un accesso diretto agli elementi della lista in base alla posizione ma, soprattutto, richiede una lista gi`a ordinata. Prendiamo un elemento m in posizione centrale dell’array A e confrontiamolo con l’elemento x da noi cercato: se m < x, restringiamo la ricerca alle posizioni successice a m; altrimenti restringiamo a quelle precedenti.
Pseudocodice 2: Ricerca binaria (o dicotomica) 1 Algoritmo ricercaBinaria (array A[0...n − 1], elemento x) → indice 2 sx ← 0 3 dx ← n 4 pos ← − 1 5 while sx < dx and pos = − 1 do 6 m ← (sx + dx)/ 2 7 if x = A[m] then 8 pos ← m 9 else if x < A[m] then 10 dx ← m 11 else 12 sx ← m + 1
13 return pos
2.2.1 Analisi ricerca binaria
Caso migliore. Si verifica quando x `e nella posizione centrale di A (x = A[n/2]):
Tbest(n) = 1
Caso peggiore. Si verifica quando sx = dx, ovvero quando la dimensione del sottoarray esaminato e 1. Per semplicita supponiamo che n sia una potenza di 2. Ad ogni passo il sottoarray e lungo la meta dell’array di partenza. Quindi il primo sottoarray e lungo n 2 , il secondo 2 n 2 , il terzo 2 n 3 ,..., fino all’ultimo che saro lungo 2 ni , dove i e il numero di passi effettuati. Ma come detto all’inzio sappiamo che quest’ultimoe lungo 1 quindi:
n 2 i^
= 1 ⇔ n = 2i^ ⇔ i = log 2 n
Tworst(n) = O(log n)
Caso medio. Supponiamo che n sia una potenza di 2 e che x possa occupare qualsiasi posizione di A con la stessa probabilita ( (^1) n ). L’algoritmo potra poi eseguire al minimo 1 confronto e al massimo dlog 2 ne confronti (1 ≤ i ≤ log n). Analizziamo ora quante volte verranno fatti i confronti in base alla posizione dell’elemento x:
a solo per 1 posizione ( n 2 ); la probabilita `e (^) n^1 ;a per 2 posizioni ( n 4 , 34 n ); la probabilita `e 2 · (^) n^1 ;a per 2^2 posizioni ( n 8 , 38 n , 58 n , 78 n ); la probabilita `e 2^2 · (^1) n ; -...a per 2i−^1 posizioni; la probabilita `e 2i− 1 n.
Quindi nel caso medio il numero di confronti sar`a:
Tavg (n) =
log∑ n
i=
#conf ronti · P(ci siano # confronti) =
Un algoritmo si dice in loco quando `e in grado di trasformare una struttura dati utilizzando soltanto un piccolo e costante spazio di memoria extra. I dati in ingresso sono solitamente sovrascritti con il risultato prodotto durante l’esecuzione dell’algoritmo.
4 Selection Sort
Il principio dell’algoritmo e dividere l’array (lungo n) in 2 parti: una parte A ordinata e una parte B da ordinare. All’inizio B sara lunga n mentre A sara lunga 0. Man mano che si andra avanti si selezionera un elemento in B che andra poi definitivamente messo in A. In questo modo si toglie un elemento da B e lo si mette in A. Alla fine si avra A lunga n e quindi l’array sara tutto ordinato. Una maniera per eseguire l’algoritmo e scegliere l’elemento piu piccolo in B. L’algoritmo e chiamato ”Selection sort” in quanto ad ogni iterazione viene selezionato un elemento che andra poi al suo posto definitivo.
Figura 1: Esempio di esecuzione del Selection sort. Si noti la parte celeste che denota A e la parte rosa che denota B
Pseudocodice 3: Selection Sort 1 Algoritmo selectionSort (array A[0...n − 1]) 2 for i ← 0 to n − 2 do 3 m ← i 4 for k ← i + 1 to n − 1 do 5 if A[k] < A[m] then 6 m ← k
7 scambia A[m] con A[i]
L’unica operazione costosa dell’algoritmo `e il confronto tra 2 elementi. Per questo stimare il numero di confronti eseguiti (C(n)) equivale a stimare il tempo (T (n)). Analizziamo il numero di confronti (Ci) ad ogni iterazione:
Quindi:
C(n) = 1 + 2 + 3 + ... + n − 1 =
n∑− 1
i=
i =
n(n − 1) 2
= Θ(n^2 )
Da notare il fatto che nel selection sort il numero di confronti effettuati `e indipendente dal contenuto dell’array. Vengono sempre eseguiti n(n 2 − 1)confronti:
Tbest(n) = Tworst(n) = Tavg (n) =
n(n − 1) 2
Siccome l’algoritmo e in loco e non viene utilizzata la ricorsione, lo spazioe costante:
S(n) = O(1)
5 Insertion Sort
Anche qui, come nel selection sort si divide l’array in 2 parti: una ordinata A e una non ordinata B. Si parte con A lunga 1 e B lunga n − 1. Ad ogni passo si toglie un elemento da B e lo si inserisce nella giusta e definitiva posizione in A. L’algoritmo `e chiamato ”Insertion sort” in quanto ad ogni iterazione un elemento viene inserito al suo posto nel sottoarray ordinato.
elementi piu grandi si accumulano sul fondo l’ultima parte dell’array sara gia ordinata e non dovra piu essere ricontrollata. L’algoritmoe chiamato ”Bubble sort” siccome le ”bolle” (gli elementi) pi`u grosse vanno in cima all’array.
Pseudocodice 5: Bubble Sort 1 Algoritmo bubbleSort (array A[0...n − 1]) 2 j ← 1 3 do 4 scambio ← f alse 5 for i ← 1 to n − j do 6 if A[j − 1] > A[j] then 7 scambia A[j] con A[j − 1] 8 scambio ← true
9 j ← j + 1 10 while scambio and j < n
Caso migliore. Il caso migliore si verifica con un array gi`a ordinato; in questo caso nel ciclo interno (riga 5) non viene effettuato nessuno scambio e quindi il ciclo principale (riga 3) termina:
Tbest(n) = n − 1
Caso peggiore. Il caso peggiore si verifica con un array ordinato al contrario. Consideriamo le iterazioni del ciclo principale (riga 3); ad ogni iterazione il ciclo interno (riga 5) esegue n − j confronti:
Quindi siccome il ciclo principale viene eseguito n − 1 volte:
Tworst(n) =
n∑− 1
j=
Cj (n) =
n∑− 1
j=
j =
n(n − 1) 2 = O(n^2 )
Siccome l’algoritmo e in loco e non viene utilizzata la ricorsione lo spazioe costante:
S(n) = O(1)
7 Divide et impera
La tecnica divide-et-impera e una tecnica di progettazione di algoritmi il cui principio consiste nel dividere il generico input di dimensione n in un certo numero m di sottoistanze del medesimo problema, ciascuna di dimensione na (circa) per qualche a > 1; quindi richiama ricorsivamente se stesso su tali istanze ridotte e poi ricompone i risultati parziali ottenuti attraverso una funzione di tempo g(n), ottenendo cosı la soluzione del problema principale. Il tempo di calcolo di algoritmi divide-et-impera `e soluzione di un equazione di ricorrenza della seguente forma:
T (n) = mT
( (^) n a
Teorema. La relazione di ricorrenza
T (n) =
mT ( na ) + nc^ se n > 1 b se n = 1
⇒ T (n) =
Θ(nc) se m < ac Θ(nc^ log n) se m = ac Θ(nloga^ m) se m > ac
Utilizziamo il Master theorem (Paragrafo 7.1) per analizzare il tempo dell’algoritmo. Il problema viene diviso in 2 sottoistanze grandi n 2 (assumendo n pari); ricomporre poi i risultati (merge) costa al pi`u n − 1 confronti. Otteniamo quindi un’equazione di ricorrenza:
T (n) = 2T
( (^) n 2
( (^) n 2
Θ(n) pu`o essere scritto come n quindi:
T (n) = 2T
( (^) n 2
Applicando il master theorem otteniamo:
T (n) = Θ(n log n)
Possiamo anche qui considerare la struttura ricorsiva dell’algoritmo e usare il master theorem per stimare S(n). Ad ogni chiamata ricorsiva corrisponde un record di attivazione che ha peso costante (1). Inoltre la chiamata ricorsiva pu`o essere analizzata ricorsivamente come segue:
Dunque:
S(n) =
( (^) n 2
Possiamo vedere 1 come n^0 e quindi:
S(n) =
( (^) n 2
Dal master theorem concludiamo che:
S(n) = Θ(n^0 log n) = Θ(log n)
9 Quick sort
Anche questo algoritmo, come il mergeSort, si basa sulla tecnica divide-et-impera. Se nel mergeSort la parte di divide e semplice e la parte di imperae piu complicata, nel quickSort si invertono le cose. Il principio di funzionamentoe il seguente:
Supponiamo di dover partizionare il seguente array:
28 36 65 3 2 11 60 74 Scegliamo come perno (o pivot) 36 e costruiamo le due sottosequenze:
28 3 2 11 36 65 60 74 Si noti che la posizione del perno dopo la partizione rimarra definitiva. Quindi l’algoritmo puo successivamente riordinare la parte di array sinistra del perno e quella destra.
Pseudocodice 7: Partizionamento del quickSort 1 Algoritmo partiziona (array A, indice i, indice f ) → indice 2 perno ← A[i] 3 sx ← i 4 dx ← f 5 while sx < dx do 6 do 7 dx ← dx − 1 8 while A[dx] > perno 9 do 10 sx ← sx + 1 11 while sx < dx and A[sx] ≤ perno 12 if sx < dx then 13 scambia A[sx] con A[dx]
14 scambia A[i] con A[dx] 15 return dx
Cb(n) = Θ(n log n)
Caso medio. Assumiamo una distribuzione uniforme e quindi la probabilita che il perno si trovi in posizione ke (^) n^1. Applicando poi la definizione di tempo medio:
C(n) =
n
n∑− 1
k=
(C(k) + C(n − k − 1) + n) =
= n +
n
n∑− 1
k=
C(k)
Si pu`o, dopo una serie di passaggi abbastanza complessi, concludere che:
C(n) ≤ 1 .39 log 2 n Una domanda puo sorgere spontanea: perche l’algoritmo e detto quickSort (veloce) se Cw(n) = Θ(n^2 ) come negli algoritmi piu semplici? La risposta e che il caso peggioree estremamente raro; si puo notare cio dal fatto che sia il caso medio che il caso migliore sono dell’ordine di n log n. Inoltre la costante trovata nell’analisi del caso medio, ovvero 1.39, e molto bassa. Le prestazione dell’algoritmo sono molto influenzate dalla scelta del perno. Spesso si utilizzano delle strategie casuali, per cercare di evitare il caso peggiore, come la scelta di un elemento casuale come perno o nel riordinare a caso l’array in partenza. L’algoritmo none stabile.
L’algoritmo e in loco ma viene utilizzata la ricorsione. Bisogna tenere quindi conto dello spazio occupato nello stack dalle chiamate ricorsive. Ogni record di attivazione contiene solo i e f quindi la dimensionee costante. Sara quindi il numero massimo di record di attivazione presenti nello stack contemporaneamente a determinarne lo spazio. Il caso peggiore si verifica quando, come nel caso peggiore del tempo, la suddivisonee sempre sbilanciata. Qui verranno memorizzate nello stack n record di attivazione. Una soluzione per ridurre le chiamate ricorsive `e utilizzare un’iterazione come mostrato nello pseudocodice 9.
Pseudocodice 9: QuickSort con iterazione 1 Algoritmo quickSort (array A, indice i, indice f ) 2 while f − 1 > 1 do 3 m ← partiziona(A, i, f ) 4 quickSort(A, i, m) 5 i ← m + 1
In questo modo esegue le chiamate ricorsive solo sulla parte sinistra dell’array. Il problema peroe che se i sottoarray di destra fossero nulli, l’altezza dello stack resterebbe comunque n. La soluzione a cioe eseguire le chiamate ricorsive solo al sottoarray piu corto dei due. Il caso peggiore che possa capitaree trovare ad ogni passo un sottoarray di lunghezza n/2. In questo caso lo spazio potr`a essere definito con:
Pseudocodice 10: QuickSort con riduzione dell’altezza dello stack 1 Algoritmo quickSort (array A, indice i, indice f ) 2 while f − 1 > 1 do 3 m ← partiziona(A, i, f ) 4 if m − i < f − m then 5 quickSort(A, i, m) 6 i ← m + 1 7 else 8 quickSort(A, m + 1, f ) 9 f ← m
S(n) = S
( (^) n 2
Dove +1 indica lo spazio costante occupato da un record di attivazione. Risolvendo l’equazione si ottiene quindi:
S(n) = Θ(log n)
u piccolo; se loe si esegue la chiamata ricorsiva sul sottoarray A[i...m] altrimenti si esegue sul sottoarray A[m + 1...f ]10 Union-find
Lo scopo di questa struttura dati `e tenere traccia di un insieme di elementi, partizionato in un certo numero di sottoinsiemi disgiunti. Le operazioni fondamentali sono:
La union-find viene implementata attraverso alberi di altezza 1 che rappresentano gli insiemi. Le foglie dell’albero contengono gli elementi dell’insieme mentre la radice ne contiene il nome.
10.1.1 Analisi Tempo