Docsity
Docsity

Prepara i tuoi esami
Prepara i tuoi esami

Studia grazie alle numerose risorse presenti su Docsity


Ottieni i punti per scaricare
Ottieni i punti per scaricare

Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium


Guide e consigli
Guide e consigli


Algoritmi - Appunti 2024-2025, Dispense di Algoritmi E Strutture Di Dati

Questo documento fornisce un'analisi approfondita degli algoritmi, coprendo vari tipi di algoritmi di ordinamento, strutture dati e tecniche di programmazione. Viene discusso il concetto di complessità computazionale e si presentano esempi pratici di applicazione degli algoritmi in contesti reali. Concetti chiave Algoritmi di Ordinamento: Tecniche per ordinare dati, come QuickSort e MergeSort. Complessità Computazionale: Studio del tempo e delle risorse necessarie per eseguire algoritmi. Strutture Dati: Organizzazione dei dati per un accesso e una manipolazione efficienti, come alberi e grafi. Programmazione Dinamica: Tecnica per risolvere problemi complessi suddividendoli in sottoproblemi più semplici. Grafi: Rappresentazione di relazioni tra oggetti, utilizzata in vari algoritmi per la ricerca di percorsi e flussi. Teorema di Master: Strumento per analizzare la complessità di algoritmi ricorsivi.

Tipologia: Dispense

2022/2023

In vendita dal 04/11/2025

paolo-imbriani
paolo-imbriani 🇮🇹

2 documenti

1 / 102

Toggle sidebar

Questa pagina non è visibile nell’anteprima

Non perderti parti importanti!

bg1
Algoritmi
Universit`a di Verona
Imbriani Paolo -VR500437
Professor Roberto Segala
July 22, 2025
1
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe
pff
pf12
pf13
pf14
pf15
pf16
pf17
pf18
pf19
pf1a
pf1b
pf1c
pf1d
pf1e
pf1f
pf20
pf21
pf22
pf23
pf24
pf25
pf26
pf27
pf28
pf29
pf2a
pf2b
pf2c
pf2d
pf2e
pf2f
pf30
pf31
pf32
pf33
pf34
pf35
pf36
pf37
pf38
pf39
pf3a
pf3b
pf3c
pf3d
pf3e
pf3f
pf40
pf41
pf42
pf43
pf44
pf45
pf46
pf47
pf48
pf49
pf4a
pf4b
pf4c
pf4d
pf4e
pf4f
pf50
pf51
pf52
pf53
pf54
pf55
pf56
pf57
pf58
pf59
pf5a
pf5b
pf5c
pf5d
pf5e
pf5f
pf60
pf61
pf62
pf63
pf64

Anteprima parziale del testo

Scarica Algoritmi - Appunti 2024-2025 e più Dispense in PDF di Algoritmi E Strutture Di Dati solo su Docsity!

Algoritmi

Universit`a di Verona

Imbriani Paolo -VR

Professor Roberto Segala

July 22, 2025

Contents

1 Introduzione agli algoritmi

Ci sono diverse definizioni di ’algoritmo’ ma quella piu semplice per definirloe una sequenza di istruzioni volta a risolvere un problema. In maniera piu semplice, possiamo definirlo come una ricetta. Le ricette sono delle istruzioni per creare dei piatti: queste istruzioni sono precise tuttavia possono avere anche delle varianti. Il problema none trovare la ricetta, ma capire quale sia quella gisuta da utilizzare in base al problema posto.

In questo corso impararemo come decidere. Studiare i diversi approcci e il metodo migliore per affrontare un problema. Capiremo come confrontare gli algoritmi fra di loro.

1.1 Complessit`a

Classifichiamo gli algoritmi in base alla loro complessita ovvero quanto tempo ci mettono per essere completati. Il nostro obiettivoe quello non di misurare il tempo in se (poich´e puo dipendere dal tipo di macchina che utilizziamo) che impiega un programma a finire ma piuttosto capire il numero di istruzioni elementari che vengono impiegate per risolvere il problema.

La complessita none un numero bensı una funzione, poich´e mappa la dimensione del problema in numero di istruzioni da eseguire. Per esempio, quando si parla di dimensione di una matrice ci si viene piu comodo vedere il numero di colonne e righe piuttosto che contare il numero di elementi all’interno della matrice. La dimensione del problema `e un insieme di oggetti, tipicamente un insieme di numeri che ci permette di avere un idea chiara di capire quanto sia grande il problema. Se riusciamo a misurarlo bene, potremmo facilitarci la vita nel risolvere il problema.

1.2 Complessit`a dei costrutti e ordini di grandezza

E doveroso stare attenti a cosa moltiplichiamo:^ se moltiplichiamo due vettori, il numero di operazioni da eseguiree esattamente n. Mentre se fossero orgnanizzate in matrici quadrate con lunghezza del lato √ n la sua complessit`a andrebbe ad aumentare a O ( nn ). Come si rappresentano le istruzioni di un programma? Se sono in serie:

I 1 c 1 ( n ) I 2 c 2 ( n ) .. . Il cl ( n )

Dove la complessita totale allora sara: ∑ l i =

ci ( n )

oppure all’interno di un costrutto if-else:

if cond c cond( n ) I 2 c 1 ( n ) else Il c 2 ( n )

In questo caso, come faccio a sapere la complessita totale di questi istruzioni? Per capirlo, studiamo il caso nel _worst case scenario_ ovvero nel caso peggiore. A volte vengono mostrati anche i casi migliore ma di solito si calcola il tempo peggiore. Quindi ci interessa specialmente il _limite superiore_ dell’algoritmo, quindi piuttosto che dire che la complessita e esattamente **uguale** questo numero, invece noi diremo chee minore o uguale del numero calcolato.

C ( n ) = c cond( n ) + max ( c 1 ( n ) , c 2 ( n ))

Mentre per un while loop? while cond ccond ( n ) I c 0 ( n ) Sia m limite superiore numero di iterazioni che esegue l’algoritmo. La complessita di questo algoritmo quindi sara: C ( n ) = ccond ( n ) + m ( ccond ( n ) + c 0 ( n )) Proviamo ora a calcolare la molteplicazioni tra matrici. Siano due matrici A e B rispettivamente con dimensione n x m e m x l.

1 For i <- 1 to n 2 For j <- 1 to l 3 C [ i ][ j ] <- 0 4 For k <- 1 to m 5 C [ i ][ j ] += A [ i ][ j ] * B [ k ][ j ]

Avere un risultato del tipo 5 m + 1 (riguardo il for piu interno)e inutile perch´e non ci da in- formazioni realmente utili sulla complessit`a dell’algoritmo. Ad ogni modo contando tutti i for, avremo un risultato del tipo:

n (5 ml + 4 l + 2) + n + 1 = 5 nml + 4 nl + 3 n + 1 Ci sono alcuni elementi in questa operazioni che non influiscono realmente sul risultato finale. Indi per cui, possiamo anche ometterlo all’interno del calcolo della complessita di un algoritmo. In realta cio che ci interessa realmente nel risultato finalee:

5 nml + 4 nl + 3 n + 1

Infatti nml e capace di dirci tutto sulla complessita dell’algoritmo e da cosa dipende. Quando

Definition 1.3. f ∈ Θ( g ) ⇐⇒ fO ( g ) ∧ f ∈ Ω( g )

Figure 3: f ∈ Θ( g )

n

x

c 1 g ( n ) , c 2 g ( n ) , f ( n )

Ò Esempio 1. Questa affermazione vale? nO (2 n )

S`ı, perch´e esiste un c che soddisfa nc 2 n.

Quest’altra affermazione vale? 2 nO ( n )

S`ı perch´e esiste un c che soddisfa 2 n ≤ 2 n.

Ò Esempio 1. Questa affermazione vale? fO ( g ) ⇐⇒ g ∈ Ω( g ) Proviamo a dimostrare. Sappiamo che esiste un c e n tale che ∀ n > n f ( n ) ≤ cg ( n ). Isoliamo la g: g ( n ) ≥ (^1) c f ( n )

Esiste quindi un c’ che soddisfa la disuguaglianza.

c ′^ =^1 c

Esempio 3

f 1 ∈ O ( g ) , f 2 ∈ O ( g ) =⇒ f 1 + f 2 ∈ O ( g ) Dobbiamo dimostrare che esiste c 1 e c 2 , n 1 e n 2 tali che:

n > n 1 | f 1 ( n ) ≤ c 1 g ( n )

n > n 2 | f 2 ( n ) ≤ c 2 g ( n ) Quindi n = max ( n 1 , n 2 )

f 1 ( n ) + f 2 ( n ) ≤ ( c 1 + c 2 ) g ( n )

Esempio 4

f 1 ∈ O ( g 1 ) , f 2 ∈ O ( g 2 ) | f 1 f 2 ∈ O ( g 1 g 2 ) Quindi esiste c 1 e c 2 , n 1 e n 2 tali che:

n > n 1 | f 1 ( n ) ≤ c 1 g ( n )

n > n 2 | f 2 ( n ) ≤ c 2 g ( n )

f 1 ( n ) f 2 ( n ) ≤ c 1 g 1 ( n ) c 2 g 2 ( n ) f 1 ( n ) f 2 ( n ) ≤ c 1 c 2 g 1 ( n ) g 2 ( n ) Quindi: c = c 1 c 2

n = max ( n 1 , n 2 )

1.3 Ordini di grandezza per le funzioni

L’algoritmo di ricerca A termina entro n. Immaginiamo che l’algoritmo A sia il seguente:

1 For i <- 0 to length ( a ) - 1 2 if a [ i ] = x 3 ret i

arrivare e un array ordinato al contrario con complessita uguale a:

1 + 2 + 3 + ... + n = n ( n^ 2 + 1) ∈ Θ( n^2 )

Questo algoritmo, quanto spazio di memoria usa in pi`u rispetto allo spazio occupato dai dati?

Lo spazio di memoria di questo algoritmo rimane costante a prescindere dalla dimensione del problema. Un algoritmo del genere si dice che ordina in loco se la quantita di memoria extrae costante. Si parla di stabilit`a di un algoritmo di ordinamento quando l’ordine relativo di elementi uguali non viene scambiato dall’algoritmo. Quindi:

Se ai = aj i < j, mantiene l’ordinamento

2 Concetto di ”Divide et impera”

2.1 Fattoriale e funzioni ricorsive

1 Fatt ( n ) 2 if n = 0 3 ret 1 4 else 5 ret n * fatt ( n - 1)

L’argomento della funzione ci fa capire la complessit`a dell’algoritmo:

T ( n ) =

1 se n = 0 T ( n − 1) + 1 se n > 0

Con problemi ricorsivi si avra una complessita con funzioni definite ricorsivamente. Questo si risolve induttivamente: T ( n ) = 1 + T ( n − 1) = 1 + 1 + T ( n − 2) = 1 + 1 + 1 + T ( n − 3) = 1 + 1 + ︸ ︷︷... + 1︸ i

  • T ( ni )

La condizione di uscita `e: ni = 0 n = i

= 1 + 1 + ︸ ︷︷... + 1︸ n

  • T ( nn )

= n + 1 = Θ( n )

Questo si chiama passaggio iterativo.

Esempio 1

T ( n ) = 2 T

(⌊ (^) n 2

  • n

Questa funzione si pu`o riscrivere come:

T ( n ) =

Costante se n < a 2 T

(⌊ (^) n 2

  • n se na

Se la complessita fosse gia data bisognerebbe soltanto verificare se `e corretta. Usando il metodo di sostituzione: T ( n ) = cn log n

sostituiamo nella funzione di partenza:

T ( n ) = 2 T

(⌊ (^) n 2

  • n ≤ 2 c

(⌊ (^) n 2

log

⌊ (^) n 2

  • n ≤ (^)  2 c n ^2

log n 2 + n = cn log ncn log 2 + n ? ≤ cn log n se ncn log 2 ≤ 0

c ≥ (^) n log 2 n = (^) log 2^1

Il metodo di sostituzione dice che quando si arriva ad avere una disequazione corrispon- dente all’ipotesi, allora la soluzione `e corretta se soddisfa una certa ipotesi.

Esempio 2

T ( n ) = T

(⌊ (^) n 2

+ T

(⌈ (^) n 2

  • 1 ∈ O ( n )

T ( n ) ≤ cn T ( n ) = T

(⌊ (^) n 2

+ T

(⌈ (^) n 2

c

(⌊ (^) n 2

  • c

(⌈ (^) n 2

= c

(⌊ (^) n 2

⌈ (^) n 2

= cn + 1

? ≤ cn

Il metodo utilizzato non funziona perche rimane l’1 e non si puo togliere in alcun modo.

Si pu`o togliere l’approssimazione per difetto per ottenere un maggiorante:

n

1 +^34 +

)log 4 n − 1 )

  • 3log^4 nc

n

i =

) i )

  • c 3 log^4 n

Per capire l’ordine di grandezza di 3log^4 n^ si pu`o scrivere come:

3 log^4 n^ = n (log n^^3 log^4 n ) = n log^4 n ·log n^^3 = n log^4

Quindi la complessitae: = O ( n ) + O ( n log^4 3 ) Si ha che una funzione e uguale al termine noto della funzione originale e l’altra chee uguale al logaritmo dei termini noti. Se usassimo delle variabili uscirebbe:

T ( n ) = aT

(⌊ (^) n b

  • f ( n ) = O ( f ( n )) + O ( n log b^ a )

2.2 Master Theorem o Teorema dell’esperto

Data una relazione di occorrenza di questa forma:

T ( n ) = aT

(⌊ (^) n b

  • f ( n ) Distinguiamo tre casi:

f ( n ) ∈ O ( n log b^ aϵ ) =⇒ T ( n ) ∈ Θ( n log b^ a )

f ( n ) ∈ Θ( n log b^ a ) =⇒ T ( n ) ∈ Θ( f ( n ) log n )

f ( n ) ∈ Ω( n log b^ a + ϵ ) =⇒ T ( n ) ∈ Θ( f ( n ))

Esempio 1

T ( n ) = 9 T

( (^) n 3

  • n a = 3 , b = 3 , f ( n ) = n

Basta che trovo un ϵ che mi dia n.

n log b^ a^ = n log^3 9 = n^2 ∗ n −^12

In questo caso ϵ = n −^12 e ci troviamo nel PRIMO CASO e la soluzione `e T ( n ) ∈ Θ( n^2 ).

Esempio 2

T ( n ) = T

( (^2) n 3

a = 1 , b =^32 , f ( n ) = n^0

n log b^ a^ = n log^321 = n^0

Ci troviamo nel SECONDO CASO e la soluzione `e T ( n ) ∈ Θ(log n ))

Esempio 3

T ( n ) = 3 T

( (^) n 4

  • n log n a = 3 , b = 4 , f ( n ) = n log n

Ci troviamo nel TERZO CASO quindi basta qualsiasi valore di ϵ basta che sia contenuto tra log 3 4 ≤ ϵ ≤ 1. La soluzione `e T ( n ) ∈ Θ( n log n ).

Esempio 4

T ( n ) = 2 T

( (^) n 2

  • n log n a = 2 , b = 2 , f ( n ) = n log n

n log n ∈ Ω( n 1+

? ϵ )

log n ∈ Ω( ) NON VALE Poich´e un logaritmo e sempre piu piccolo di un polinomio. Questo `e un caso dove il teorema non si applica

2.3 Merge Sort (A, n)

Questo algoritmo di ordinamento ricorsivo utilizza il concetto di divide et impera. Concettualmente, un merge sort funziona come segue:

  1. Dividi l’array non ordinato in n sottoarray, ognuno contenente un elemento (un array di

Propriet`a Heap:

∀ nodo, il contenuto `e ≥ del contenuto dei figli

La complessita dell’algoritmoe in base al numero di livelli dell’albero.

−→ albero con n livelli:

#Nodi = 2^0 + 2^1 + 2^2 + ... + 2 n −^1 =^1 −^2

n 1 − 2 = 2

n (^) − 1

−→ albero con n nodi: #Livelli = log 2 n #Foglie = n 2

Le foglie di un albero sono la met`a dei nodi dell’albero. 1 extractMax ( H ) // O ( log n ) 2 h [1] <- H [ H. heap_size () ] 3 H. heap_size () -- 4 heapify (H , 1) 1 heapify (H , 1) // O ( log n ) 2 l <- left [ i ] // 3 r <- right [ i ] 4 if ( l < h. heap_size () AND H [ l ] > H [ i ] 5 largest <- l 6 else 7 largest <- i 8 if m <= h. heap_size () and H [ r ] > H [ largest ] 9 largest <- r 10 if largest! = i 11 swap ( H [ i ] , H [ largest ]) 12 heapify (H , largest )

Creare uno heap da un array: 1 buildHeap ( A )

2 A. heap_size () <- length [ A ] 3 for i <- length [ A ]/2 down to 1 4 heapify (A , i )

Immagino tutte le foglie come heap con un solo nodo. L’indice del primo nodo che non `e heap corrisponde a length 2 ( A )su questo allora chiamo heapify.

Scegliamo un elemento a caso in base a quello comparato rispetto all’elemento perno tale che:

sxpernodx

Questo algoritmo non e **stabile** ma lavora **in loco**. La sua complessita?

T ( n ) = T (partition) + T ( q ) + T ( nq )

Se il quicksort e perfettamente diviso in due, allora la sua complessita e _O_ ( _n_ log _n_ ). Se invece l’arraye gia ordinato la sua equazione di ricorrenza sara:

= n + T (1) + T ( n − 1) = Θ( n^2 )

Tuttavia non ci aspettiamo che questo caso sia frequente e quindin nella stra grande maggioranza dei casi allora: T ( n ) = n + T ( cn ) + T ((1 − c ) n )

Un equazione di questo tipo sappiamo che ha come complessit`a Θ( n log n ).

1 rand_Partition (A , p , r ) 2 i <- rand ( p .. r ) // Ora l ' elemento perno e ' un elemento a caso 3 scambio ( A [ p ] , A [ i ] 4 ret partition (A , p , r )

T ( n ) = n + n^1 ( T (1) + T ( n − 1) +^1 n ( T (2) + ( T − 2)) +... + 1 n ( T^ ( n^ −^ 2) +^ T^ (2)) +

n ( T^ ( n^ −^ 1) +^ T^ (1) = = n + n^1

i

( T ( i ) + T ( ni ))

= n + n^2

i

T ( i ) ∈ O ( n log n )

Qualsiasi algoritmo che lavora per confronti deve fare almeno O ( n log n ).

3 Algoritmi di ordinamento in tempo lineare

3.1 Algoritmi non basati su confronti

3.1.1 Counting Sort Tuttavia possiamo trovare algoritmi che come tempo di esecuzione hanno tempo lineare. Come? Non lavorando a confronti.

Come ordinare n numeri con valori da 1 a k? 1 countingSort (A , k ) 2 for i <- 1 to k 3 C [ i ] <- 0 4 for j <- 1 to len ( A ) 5 C [ A [ j ]]++ 6 for i <- 2 to k 7 C [ i ] <- C [i -1]+ C [ i ] 8 for j <- len ( A ) down to 1 9 B [ C [ A [ j ]]] <- a [ j ] 10 C [ A [ J ]] - -

La complessita di questo algoritmoe O ( n + k ) dove n `e la lunghezza dell’array e k e il range di valori.

3.1.2 Radix Sort Il radix sort e un algoritmo di ordinamento che ordina gli elementi confrontando i singoli bit. Quello che fae ordinare per la cifra meno significativa, poi per la seconda cifra meno significativa e cosı via. 1 radixSort (A , d ) // O ( d ( n + k ) ) 2 for i <- 1 to d 3 countingSort (A , n ) La complessita di questo algoritmo e Θ( _d_ ( _n_ + _k_ )) dove _d_e il numero di cifre e k e il range di valori. Se si vuole invece ordinare _n_ valori da 1 a _n_^2 −1, le costanti nascoste all’interno del Θ sono molto alte e quindi none un algoritmo efficiente. Tuttavia si posson rappresentare i numeri in base n e quindi ottenere un algoritmo lineare.

3.1.3 Bucket Sort Il Bucket Sort e un algoritmo di ordinamento applicabile quando la distribuzione dei datie nota. Quindi su un array di n elementi distribuiti uniformemente su [0 , 1), si puo dividere l’intervallo in _n_ sottointervalli con probabilita (^1) n e poi ordinare i singoli sottointervalli chia- mate anche ”Bucket”. Infatti se i dati sono distribuiti uniformemente allora la complessita dell’algoritmoe lineare: Θ( n ) Questo perch´e in ogni bucket ci si aspetta un valore costante e quindi indipendente dal valore di n.

Il caso pessimo peroe quando tutti gli elementi ricadono nello stesso bucket. La probabilit`a