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 e strutture dati merelli 2026, Appunti di Algoritmi E Strutture Di Dati

asd merelli 2026 non completo, attenzione

Tipologia: Appunti

2025/2026

Caricato il 29/12/2025

riccardo-cotani
riccardo-cotani 🇮🇹

5 documenti

1 / 38

Toggle sidebar

Questa pagina non è visibile nell’anteprima

Non perderti parti importanti!

bg1
Pag. 0 a 38
0 | P a g e
Algoritmi e
strutture dati
COTANI RICCARDO
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

Anteprima parziale del testo

Scarica algoritmi e strutture dati merelli 2026 e più Appunti in PDF di Algoritmi E Strutture Di Dati solo su Docsity!

Pag. 0 a 38

Algoritmi e

strutture dati

COTANI RICCARDO

Legenda: Rosso: Argomento principale Viola: Sottotema di primo livello Blu: Sottotema di secondo livello Bianco: Sotto tema di terzo livello Concetto di algoritmo Definizione: Un algoritmo è una procedura computazionale ben definita, questa viene eseguita per risolvere un dato, detto problema computazionale. Questo è composto anche da delle sequenze di passi computazionali, che prende dei valori come input e produce altri valori come output. In una descrizione formale si può dire che: L’algoritmo è un procedimento di calcolo esplicito con un numero di regole e passi finiti. Valutazione di un Algoritmo: nel mondo degli algoritmi e strutture dati, è essenziale valutare un algoritmo che possa essere in grado di risolvere correttamente ed efficacemente un problema:  ALGORITMO CORRETTO: ovvero che per ogni istanza (input), deve saper risolvere questo problema con un output corretto. (concetto di decidibilità fondamenti) o Caso Base: verifica per input minimi o Passo induttivo: se funziona per input piccoli, funziona per input più grandi  DIMOSTRAZIONI MATEMATICHE: parte dell’algoritmo che spiega come quest’ultimo stia seguendo il concetto di correttezza; Infatti, si affida a queste e le descrizioni informali.  ALGORITMO EFFICIENETE: con efficienza si intende che si risolva un dato problema in un tempo finito e senza troppi sprechi di risorse Un esempio di algoritmo corretto, possono essere gli algoritmi di ordinamento, ma questi differiscono per efficienza. Nota: l’efficienza è strettamente collegata al problema dell’arresto, questo perché alcuni problemi non sono risolvibili. Classi di complessità: esistono dei casi, in cui l’algoritmo non può essere efficiente, questo perché ci possono essere due casi:

1. Cosa fa questo algoritmo:  L’algoritmo ordina un array di elementi in ordine crescente.  Prende una lista di numeri o valori confrontabili e li riordina dal più piccolo al più grande. 2. Problema che risolve:  Risolve il problema dell’ordinamento di una sequenza di elementi.  Trasforma un insieme di dati disordinato in una sequenza ordinata secondo un criterio di confronto (ad esempio dal minore al maggiore). 3. Procedimento di calcolo:  Analizza l’array elemento per elemento, a partire dal secondo.  Per ogni elemento A[j], lo considera come una chiave (key).  Confronta key con gli elementi precedenti dell’array.  Sposta in avanti tutti gli elementi più grandi della chiave.  Inserisce la chiave nella posizione corretta, in modo che la parte sinistra dell’array risulti ordinata.  In questo modo, costruisce progressivamente una parte ordinata dell’array.  È il classico algoritmo chiamato Insertion Sort (ordinamento per inserimento).

  1. Strutture dati utilizzate:  Input: un array A contenente i valori da ordinare.  Output: lo stesso array A, ma con i valori disposti in ordine crescente.
  2. Numero di passi (caso length[A] = 7):  Nel miglior caso (array già ordinato), il ciclo interno non esegue nessuno

spostamento e il numero di passi è proporzionale a n → quindi circa 7

passi.  Nel peggior caso (array completamente inverso), ogni elemento deve essere confrontato con tutti i precedenti. Il numero totale di passi è quindi:

Quindi, per un array di 7 elementi, l’algoritmo compie 21 passi nel caso peggiore.

Analisi di Algoritmi Analizzare un algoritmo, sta a significare che si determinano quali sono le risorse necessarie in termini di:

 Spazio di memoria: ossia, la quantità di memoria utilizzata durante il

processo

 Tempo di computazione: ossia il tempo di esecuzione di questo.

Questa analisi ci consente di ottenere varie informazioni tra cui:  Stima del tempo impiegato  Stima il tempo di esecuzione di input molto grandi in tempi ragionevoli  Confrontare l’efficienza di due algoritmi  Ottimizzare le parti critiche Da qui possiamo definire una funzione “T” che ci servirà per la analisi dei vari algoritmi, in termini specifici, questa mette in relazione la dimensione dell’istanza con il tempo, ovvero: T(n) = tempo impiegato dall’algoritmo quando l’istanza ha dimensione n

Modello matematico:

Per indicare il tempo che ci impiega un algoritmo, abbiamo la seguente funzione, dove:  Con ni, intendiamo il numero di volte che il passo i-esimo viene eseguito  Con ci, sarebbe il costo computazionale del passo i-esimo  Con k, si intende il numero totale di passi dell’algoritmo T ( n )= (^) ∑ i = 1 ¿ ¿ k n (^) i c (^) i Esempio pratico del modello: Se un algoritmo ha 3 istruzioni con costi c₁, c₂, c₃ che vengono eseguite n, n² e 1 volte rispettivamente: T(n) = n·c₁ + n²·c₂ + 1·c₃  Operazione 1 (costo c₁ = 2 unità): Inizializzazione - eseguita n volte  Operazione 2 (costo c₂ = 3 unità): Confronto tra elementi - eseguita n² volte

Ipotesi: ogni istruzione corrispondente alla linea “i” dell’algoritmo viene eseguita

in un tempo costante indicato con cᵢ.

Problema dell’ordinamento Il problema dell’ordinamento consiste nell'organizzare gli elementi di un insieme secondo una determinata relazione d'ordine (totale), utilizzando algoritmi specifici come Bubble Sort, Selection Sort o Quick Sort. La complessità di questo problema varia da O(n^2 ) a O(n*log 2 n)

Definizione matematica:

Dati n elementi sui quali si possa considerare un relazione d’ordine totale (ogni coppia di elementi è confrontabile) costruire una permutazione (a’1, a’2, a’3, ..., a’n), vale a dire trovare una disposizione degli elementi in modo tale che si abbia: a’ 1 <= a’ 2 <= a’n.

Istanza del problema:

abbiamo delle istanze (31, 41, 59, 26, 41, 58), e dobbiamo ottenere un output ordinato in maniera crescente, quindi l’output deve essere (26, 31, 41, 41, 58, 59). Per risolvere questo problema, dobbiamo scegliere un algoritmo, la scelta dipende da tre fattori:

  • Dal numero di elementi da ordinare o Per pochi elementi: si utilizzando algoritmi semplici come l’Insertion sort o Per molti elementi: si utilizzano algoritmi efficienti come il Quick sort o il Merge Sort
  • Da quanto gli elementi siano già ordinati o Ordinata parzialmente: se la sequenza è quasi ordinata allora alcuni algoritmi saranno più veloci o Disordinata: se questa fosse disordinata allora potremmo utilizzare altri algoritmi più efficienti
  • Dispositivo di memoria (metodo di accesso) o Memoria interna (RAM): possiamo usare qualsiasi algoritmo o Memoria esterna (disco): servono algoritmi che minimizzano gli accessi o Accesso sequenziale vs accesso casuale

Funzionamento :

istanza iniziale: 31, 41, 59, 26, 41, 58 Step 1:  Ordinati: [31]  Da inserire: 41  Non ordinati: [59, 26, 41, 58]  Risultato : [31, 41] Step 2:  Ordinati: [31, 41]  Da inserire: 59  Non ordinati: [26, 41, 58]  Risultato : [31, 41, 59] Step 3:  Ordinati: [31, 41, 59]  Da inserire: 26 ← qui deve trovare la posizione corretta  Non ordinati: [41, 58]  Risultato : [26, 31, 41, 59]

o Il numero di confronti è pari a i − 1

per l’elemento i-esimo o L’elemento trova la sua posizione circa a metà della sequenza ordinata

Formula generale

Per un array di dimensione n, il numero totale di confronti è:

  • Ottimo: ∑ i = 1 n − 1 ≈ n
  • Pessimo: (^) ∑ i − 1 n ( n − 1 )

≈ n 2

  • Medio: ∑ i − 1 2 n ( n − 1 )

≈ n 2

Notazioni Asintotiche

Notazione O grande (limite superiore asintotico)

Se f(n) = O(g(n)) allora g(n) è un limite asintotico superiore per f(n), intendiamo che la funzione f(n) cresce come o meno di g(n) a meno di un fattore costante per valori di n sufficientemente grandi. Per questo, la notazione O(n) viene usata nell’analisi del costo computazionale nel caso pessimo. Graficamente:

Esempio pratico: l’algoritmo di ricerca in un array non ordinato nel caso peggiore, deve esaminare tutti gli n elementi, ovvero il suo limite asintotico superiore.

Definizione Formale e l’abuso di notazione

A livello formale, O(g(n)) non indica una sola funziona, ma rappresenta un insieme di funzioni. Questa è l’insieme di tutte le funzioni f(n) che soddisfano la seguente condizione: Esistono due costanti positive c e n 0 , tali che 0 <= f(n) <= c * g(n) per ogni n >= n 0. Di conseguenza la scrittura f(n) = O(g(n)) è tecnicamente un abuso di notazione, la forma matematica più corretta sarebbe f(n) ∈ O(g(n)), ma per convezione si utilizza l’uguale. Esempio matematico: Provare che f(n) = 3n^2 + 10n = O(n^2 ) Dobbiamo dimostrare l’esistenza di costanti positive c e n 0 tali che 0 <= f(n) < cn^2 per ogni n >= n 0 Per dimostrare questo possiamo scegliere c = 4 e n 0 = 1w0; infatti: 3n^2 + 10n <= cn^2 cn^2 – 3n^2 – 10n >= 0 Raccogli n^2 (c – 3)n^2 – 10n >= 0 e scegliamo c = 4 n^2 – 10n >= 0

La notazione Ω-grande definisce un limite inferiore asintotico , una sorta di "pavimento" per la crescita della funzione. L'algoritmo non potrà mai essere più veloce di così, per input sufficientemente grandi, questa viene usata quando dobbiamo descrivere la complessità di un algoritmo nel caso migliore (ottimo). Questo indica che vengono usate le risorse minime necessarie per risolvere il problema. Esempio pratico: Qualsiasi algoritmo che deve ordinare un array deve, come minimo, leggere tutti gli n elementi. Pertanto, la sua complessità sarà sempre Ω(n). Ω(g(n)), rappresenta l’insieme di funzioni definito come (g (n)) = {f (n) | esistono delle costanti positive c e n0 tali che 0 ≤ cg (n) ≤ f (n) Esempio matematico: Dimostrare che 3n² + 10n = Ω (n²). Per fare questo dobbiamo dimostrare l’esistenza di costanti positive c e n 0 tali che 0 <= cn² <= f(n) per ogni n >= n 0 Per dimostrare tale esistenza, possiamo prendere come valore di c il coefficiente del termine dominante di f(n), ovvero c = 3, mentre n 0 lo possiamo prendere come 0. Se facciamo la diseguaglianza troviamo che: 3n^2 <= 3n^2 + 10n 0 <= (3n^2 - 3n^2 ) + 10n Quest'ultima affermazione è vera per tutti gli n ≥ 0. Pertanto, possiamo scegliere n₀ = 0. Poiché abbiamo trovato una coppia di costanti valide (c = 3, n₀ = 0), la dimostrazione è completa.

Θ-grande (limite stretto):

NB: ricorda che f (n) = Θ(g (n)) è un “abuso” di notazione

Se f(n) = Θ(g(n)) allora g(n) è un limite asintotico stretto, per f(n) : f(n), a meno di un fattore costate, questa cresce esattamente come g(n), infatti Θ-grande va a descrivere la crescita esatta della funzione. Θ(g(n)) = { f(n) | esistono delle costanti positive c 1 , c 2 e n 0 tali che 0 ≤ c 1 g(n ≤ f(n) ≤ c 2 g(n) } Graficamente:

Teorema:

Per ogni coppia di funzioni f(n) e g(n) si ha che: f(n) =Θ(g(n) ), se e soltanto se f(n) = O( g(n) ) e f(n) = Ω( g(n) ) Esempio matematico: f(n) = 3n² + 10n = Θ(n²) Abbiamo dimostrato che f (n) = O(n^2 ) e f (n) = Ω(n^2 ), questo basta per concludere che f (n) = Θ(n^2 ).

Le notazioni Θ-piccolo e Ω-piccolo

Siano f(n) e g(n) due funzioni non negative; f(n) = ω(g(n)) se per ogni costante positiva c esiste una costante n0 > 0 tale che 0 ≤ cg(n) < f(n) per ogni n ≥ n 0 Intuitivamente, nella notazione ω, la funzione f(n) diventa arbitrariamente grande rispetto a g(n) quando n tende all’infinito Diciamo che f(n) domina g(n):

lim

n → ∞ f ( n ) g ( n ) = ∞

Per la situazione Fibonacci, invece la ricorrenza del tempo di esecuzione è T(n) = T(n – 1) + T(n – 2), specchio della sua definizione matematica, questo porta a una complessità esponenziale O(2n), rendendolo molto inefficiente perché ricalcola più volte gli stessi valori. Metodi per Risolvere le ricorrenze: per risolvere una ricorrenza significa trovare una formula chiusa (non ricorsiva) come O(n log n), per descrivere il comportamento dell’algoritmo. Per risolvere una cosa del genere, possiamo utilizzare 3 metodi principali, ossia il metodo iterativo, il metodo della sostituzione e master theorem. Metodo Iterativo: questo, consiste nello srotolare, la ricorrenza più volte fino a trasformarla in una sommatoria, che può poi essere calcolata; Il metodo iterativo è una tecnica anche visuale per organizzare questa somma. Metodo degli Alberi di Ricorsione: Questo metodo è una tecnica visuale, spesso usata per supportare il metodo iterativo, che aiuta a comprendere il costo di un algoritmo ricorsivo. Consiste nel disegnare un albero dove ogni nodo rappresenta il costo di un singolo sottoproblema all'interno delle chiamate ricorsive. L'obiettivo è calcolare il costo totale dell'algoritmo sommando i costi di tutti i nodi, solitamente aggregandoli per livello. Esempio: T(n) = 2T(n/2) + n Analizziamo una ricorrenza comune, T(n) = 2T(n/2) + n (simile a quella del Merge Sort), assumendo T(1) = 1.

  1. Costruzione dell'Albero  Livello 0 (Radice): La chiamata iniziale T(n) ha un costo locale pari a f(n) = n (per dividere/combinare) e genera a=2 sottoproblemi.  Livello 1: La radice ha 2 figli, ognuno per un sottoproblema T(n/2). Il costo locale di ciascuno di questi nodi è f(n/2) = n/2.  Livello 2: Ognuno dei 2 nodi precedenti genera 2 figli (4 nodi totali), ognuno per un sottoproblema T(n/4) con costo locale n/4.  Foglie: Si continua così fino a raggiungere il caso base T(1).
  2. Analisi dei Costi per Livello Per calcolare il costo totale, sommiamo i costi di tutti i nodi aggregandoli per livello:  Costo Livello 0: 1 nodo × n = n  Costo Livello 1: 2 nodi × n/2 = n  Costo Livello 2: 4 nodi × n/4 = n

 Costo Livello i: 2^i nodi × n/2^i = n Notiamo che il costo di ogni livello è costante e pari a n.

  1. Calcolo del Costo Totale Per trovare il costo totale T(n), dobbiamo sommare i costi di tutti i livelli (dalla radice alle foglie).  Determinare l'Altezza (k): L'albero si ferma quando la dimensione del problema diventa 1. Questo accade al livello k tale che n/2^k = 1. Risolvendo per k, si ottiene 2^k = n, e quindi k = log₂(n).  Numero di Livelli: L'albero ha k+1 livelli (dal livello 0 al livello k = log₂(n)).  Sommare i Costi: Il costo totale è la somma dei costi di ogni livello: T(n) = somma da i=0 a log₂(n) di n (sommiamo 'n' per k+1 volte) T(n) = n × (log₂(n) + 1) = n log₂(n) + n  Conclusione: In notazione asintotica, il costo dell'algoritmo è T(n) = Θ(n log₂(n)). Metodo della Sostituzione: Richiedere di capire la soluzione e poi usare il principio di induzione matematica per dimostrare che l’ipotesi è corretta. Dividi et impera:

Paradigma del Divide et Impera, ritroviamo tre passaggi fondamentali:

  1. Dividere il problema in a sottoproblemi di dimensione n/b.
  2. Risolvere i sottoproblemi in modo ricorsivo.
  3. Combinare i risultati per ottenere la soluzione finale. L’equazione generale di ricorrenza è la seguente: T ( n )= 2 T ( n 2 )
  • n Dove n, rappresenta il costo della combinazione dei risultati.

Dove delta è una costante positiva, che misura di quanto f(n) è più lenta o più veloce rispetto al temine n(logb a) Strutture dati Le strutture dati, le definiamo con due concetti molto importati:

  • La ADS, in italiano una struttura dati Astratta, ovvero una struttura dati teorica che descrive cosa vogliamo.
  • E la DS, in italiano struttura dati, questa invece a differenza delle prime sono le implementazioni e rappresentano il come lo facciamo

ADS – Strutture dati Astratte:

definiamo una struttura dati astratta come l’organizzazione di un insieme di oggetti tale da poter essere rappresentata con strutture dati a sostegno delle operazioni che permetteranno la gestione degli oggetti in modo efficiente; queste vanno a specificare esattamente quello che voglio e non viene fatta l’effettiva implementazione. Esempio: La List ADT (Abstract Data Type) è una collezione sequenziale si elementi che supportano dei set di operazioni senza specificare le implementazioni interne. Per restituire una via allo stoccaggio ordinato, lo stoccaggio, l’accesso a questo e la modifica dei dati. Operazioni: L'ADT List deve memorizzare i dati richiesti nella sequenza e deve avere le seguenti operazioni: get(): restituisce un elemento dalla lista in una posizione qualsiasi. insert(): inserisce un elemento in una posizione qualsiasi della lista. remove(): rimuove la prima occorrenza di un elemento da una lista non vuota. removeAt(): rimuove l'elemento in una posizione specificata da una lista non vuota.

replace(): sostituisce un elemento in una posizione qualsiasi con un altro elemento. 4size(): restituisce il numero di elementi nella lista. isEmpty(): restituisce true se la lista è vuota; altrimenti, restituisce false. isFull(): restituisce true se la lista è piena, altrimenti, restituisce false. Applicabile solo in implementazioni a dimensione fissa (ad esempio, liste basate su array). Altri esempi La Pila: Insieme di elementi gestito secondo una politica LIF

  • l'ultimo elemento inserito è il primo estratto La Coda: Insieme di elementi gestiti secondo una politica FIFO
  • il primo elemento inserito è il primo estratto DS - Strutture dati Definiamo ora, le strutture dati; queste sono l’implementazione effettiva delle ADT, vanno a identificare il “come viene fatto” e di come i dati sono organizzati concretamente. Tecniche di rappresentazione dei dati Le tecniche di rappresentazione si basano su come i dati vengono organizzati fisicamente in memoria per un uso efficiente. Esistono due metodologie principali:
  1. Rappresentazioni Indicizzate (Array) I dati sono contenuti in locazioni di memoria contigue, tipicamente un array.  Pro : Accesso diretto ai dati in tempo costante mediante indici.  Contro: Dimensione fissa. La riallocazione dell'array (ad esempio per ingrandirlo) è un'operazione costosa che richiede tempo lineare ().
  2. Rappresentazioni Collegate (Puntatori/Record) I dati sono contenuti in record (o nodi) distinti, collegati tra loro mediante puntatori. La locazione di memoria non è consecutiva.  Pro: Dimensione variabile e flessibile. L'aggiunta e la rimozione di record hanno un costo costante () se si possiede già il puntatore alla posizione desiderata.  Contro: Accesso sequenziale ai dati. Per raggiungere l'n-esimo elemento, è necessario attraversare i primi elementi (costo ).