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, Appunti di Algoritmi E Strutture Di Dati

Una definizione informale di algoritmo e l'importanza di progettare algoritmi efficienti ed efficaci. Inoltre, viene presentato il metodo di calcolo dei numeri di Fibonacci e le notazioni asintotiche. Vengono inoltre descritte le tipiche complessità degli algoritmi e i metodi di analisi, con un focus sulla ricerca sequenziale e binaria. Viene infine presentato il Teorema Master, basato sulla tecnica divide et impera, per risolvere problemi di dimensione n.

Tipologia: Appunti

2021/2022

In vendita dal 17/09/2023

mariapiasacco
mariapiasacco 🇮🇹

3 documenti

1 / 87

Toggle sidebar

Questa pagina non è visibile nell’anteprima

Non perderti parti importanti!

bg1
ALGORITMI E STRUTTURE DATI
Definizione informale di Algoritmo
Un algoritmo è una sequenza di passi di calcolo che, ricevendo in
ingresso un valore restituisce in uscita un altro valore.
Lo scopo è progettare algoritmi efficienti, oltre che efficaci, per
risolvere problemi e valutare le strutture dati più adatte:
Efficacia = produrre il risultato desiderato in modo corretto;
Efficienza = tempo di esecuzione + occupazione di memoria.
I numeri di Fibonacci
Espansione di una popolazione di conigli sotto appropriate
condizioni:
Indicando con 𝐹𝑛 il numero di coppie dell'anno
n
, abbiamo la
seguente relazione di ricorrenza:
Fn=
{
Fn1+Fn2, n 3
1, n=1,2
Per calcolare 𝐹𝑛 proviamo un approccio numerico. Tuttavia, questa
metodica non opera solamente con numeri interi e ci si potrebbe
imbattere in errori di arrotondamento che è consigliabile evitare.
Partendo dal presupposto che l’algoritmo di Fibonacci sia ricorsivo,
immaginiamo una seconda soluzione:
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

Anteprima parziale del testo

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

ALGORITMI E STRUTTURE DATI

Definizione informale di Algoritmo

Un algoritmo è una sequenza di passi di calcolo che, ricevendo in ingresso un valore restituisce in uscita un altro valore.

Lo scopo è progettare algoritmi efficienti, oltre che efficaci, per risolvere problemi e valutare le strutture dati più adatte:

 Efficacia = produrre il risultato desiderato in modo corretto;  Efficienza = tempo di esecuzione + occupazione di memoria.

I numeri di Fibonacci

Espansione di una popolazione di conigli sotto appropriate condizioni:

Indicando con 𝐹𝑛 il numero di coppie dell'anno n, abbiamo la seguente relazione di ricorrenza:

Fn ={ Fn −^1 + Fn −^2 ,^ n^ ^3 1 , n = 1 , 2

Per calcolare 𝐹𝑛 proviamo un approccio numerico. Tuttavia, questa metodica non opera solamente con numeri interi e ci si potrebbe imbattere in errori di arrotondamento che è consigliabile evitare. Partendo dal presupposto che l’algoritmo di Fibonacci sia ricorsivo, immaginiamo una seconda soluzione:

Guardando l'algoritmo fibonacci2, in prima approssimazione possiamo dire:

 Se n ≤ 2 , eseguo una sola linea di codice;  Se n = 3, sono eseguite due linee di codice per la chiamata fibonacci2(3) più una linea di codice per la chiamata fibonacci2(2) ed una per fibonacci2(1). Totale = 4 linee di codice;  Se n = 4. Totale = 7 linee di codice e così via.

Ad ogni chiamata vengono eseguite due linee di codice, più quelle per le chiamate ricorsive:

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

In generale, il tempo richiesto da un algoritmo ricorsivo è pari al tempo speso all’interno della chiamata più il tempo speso nelle chiamate ricorsive.

Per poter risolvere la relazione di ricorrenza è utile il cosiddetto albero della ricorrenza. È un albero con i nodi corrispondenti alle chiamate ricorsive e figli di un nodo corrispondenti alle sottochiamate.

Il numero presente nei nodi descrive il numero di linee di codice eseguite nella chiamata, con queste regole:

 Le foglie (nodi senza figli) hanno etichetta 1;  I nodi interni hanno etichetta 2.

Diremo pertanto che f(n) = O(g(n)) se f(n) < c g(n) per qualche costante c e n abbastanza grande. Dal punto di vista del tempo di esecuzione abbiamo O(n).

Possiamo sfruttare il concetto delle potenze ricorsive, cioè calcolare la n-esima potenza della matrice elevando al quadrato la ( n/2) -esima potenza:

T ( n )= O ( 1 )+ T ( n 2 )

Iterando, si ottiene che: T^ (^ n^ )^ ^ kc^ + T^ (

n 2 k^ )

Se scegliamo 𝑘 = log 2 (𝑛) otteniamo:

T ( n ) ≤ c ∙ log 2 n + T ( 1 )= O ( log 2 n )

Sia f(n) il tempo di esecuzione o l'occupazione di memoria di un algoritmo su input di dimensione n. La notazione asintotica è un'astrazione utile per descrivere l'ordine di grandezza di f(n) ignorando i dettagli non influenti.

Notazione asintotica O

Se una funzione f(n) O(g(n)), allora f(n) cresce al più come g(n). Per comodità f(n) = O(g(n)).

Notazione asintotica Ω

Se una funzione f(n) Ω(g(n)), allora f(n) cresce almeno come g(n). Per comodità scriveremo f(n) = Ω(g(n)).

Notazione asintotica 𝜃

Se una funzione f(n) 𝜃(g(n)), allora f(n) cresce esattamente come g(n). Per comodità scriveremo f(n) = 𝜃(g(n)).

Date due funzioni f(n) e g(n), si ha che: f(n) = 𝜃(g(n)) se e solo se f(n) = O(g(n)) e f(n) = Ω(g(n)).

 La notazione 𝜃 gode della proprietà simmetrica: f(n) = 𝜃(g(n)) se g(n) = 𝜃(f(n));  Le notazioni O e Ω godono della proprietà simmetrica trasposta: f(n) = O(g(n)) se g(n) = Ω(f(n));

Sia 𝑡(𝐼) il tempo di esecuzione dell'algoritmo sull'istanza 𝐼 di dimensione n:

 Caso peggiore (worst case): T (^) worst ( n )= max∀ I didim .n { tempo ( I )}  Caso migliore (best case): T (^) best ( n )= min∀ I di dim .n { tempo ( I )}  Caso medio (average case): consideriamo P{I} la probabilità che si verifichi l’istanza I:

T avg ( n )= ∑

∀ I di dim. n

{ P^ {^ I^ }^ ∙^ tempo ( I^ )}

L'analisi che si usa maggiormente è il worst case.

Ricerca sequenziale

Ricerca di un elemento x in una lista L:

T (^) best ( n )= (^1) , x è al primo elemento; T (^) worst ( n )= n , x non presente in L o ultimo elemento;

T (^) avg ( n )= 1

n ∑ i = 1

n i =^1 n

n ( n + 1 ) n = n +^1 n

Ricerca binaria

Confronta x con l’elemento centrale di L e prosegue nella metà sinistra o destra in base all’esito del confronto.

T (^) best ( n )= (^1) , x è l’elemento centrale; T (^) worst ( n )= n , x non presente in L o trovato nell’ultimo confronto;

T (^) avg ( n )= 1

n ∑ i = 1

log 2 n i 2 i −^1 =^1

2 (^

2 log^2 n ( log 2 n − 1 ) + 1 )=log 2 n − 1 + 1

n

Analisi di algoritmi ricorsivi

L’analisi di algoritmi ricorsivi si basa sulle relazioni di ricorrenza, che indicano il tempo richiesto per una procedura più il tempo per le chiamate ricorsive.

Nel caso dell’algoritmo di ricerca binaria, lo possiamo riscrivere in modo ricorsivo:

Nel caso peggiore:

T ( n )= 2 + T (^) ([ n − 2 1 ])

In generale, la relazione di ricorrenza è nella forma di:

Teorema Master: si basa sulla tecnica divide et impera. Consiste in:

 Dividere il problema (di dimensione n) in a sottoproblemi di dimensione n/ b (divide);  Risolvere i sottoproblemi ricorsivamente;  Ricombinare le soluzioni (impera).

La relazione di ricorrenza è data:

T ( n )= {

aT ( n b )+ f ( n ) , se n > 1

1 , se n = 1

L’albero di ricorsione appena visto ha le seguenti proprietà:

 I sottoproblemi al livello i hanno dimensione n/bi;  Il contributo di un nodo al livello i al tempo di esecuzione è f(n/bi);  i = logb n è il numero di livelli dell’albero;  Il numero di nodi al livello i è ai.

Mettendo tutto insieme possiamo riscrivere la relazione:

T ( n )= ∑

i = 0

log b n ai^ f (^) ( n bi^ )

Il teorema Master consta tre soluzioni della relazione di ricorrenza iniziale:

T ( n )= θ (^ n log b^ a^ )^ se f ( n )= O (^ n log b^ aε^ )^ con ε > 0

T ( n )= θ ( n log b^ a^ log ( n )) se f ( n ) = θ ( n log b^ a^ )

T ( n )= θ ( f ( n )) se f ( n )= Ω ( n log b^ a +^ ε^ ) con ε > 0 e a ∙ f^ ( b^ n ) ≤ c ∙ f ( n )

STRUTTURE DATI ELEMENTARI

La struttura dati rappresenta l'organizzazione dei dati che permette di supportare le operazioni di un tipo di dato usando meno risorse di calcolo possibile.

Dizionario: è una collezione di elementi a cui sono associate chiavi prese da un dominio totalmente ordinato.

Pila: tipo di dati accessibile con modalità LIFO (Last In, First Out).

Coda: tipo di dati accessibile con modalità FIFO (First In, First Out).

Meno riallocazioni e uso limitato di memoria. Inserimento e cancellazione: O(1).

Rappresentazioni collegate

Ricerca binaria non possibile (anche se ordinato) → O( n); Inserimento → O(1); Cancellazione → O(1), ma con ricerca diventa O( n).

ALBERI

Un albero è una coppia T = (N, A) costituita da un insieme N di nodi e da un insieme A ⊆ N × N di coppie di nodi, detti archi.

In un albero ogni nodo v (tranne la radice) ha un solo genitore u tale che u, v ∈ A. Un nodo u può avere zero o più figli v tali che u, v ∈ A e il loro numero è detto grado del nodo.

Un nodo senza figli è chiamato foglia, mentre i nodi che non sono né foglie né la radice sono chiamati nodi interni. Parleremo inoltre di antenati e discendenti di un nodo intendendo i nodi raggiungibili salendo o scendendo, rispettivamente. La profondità (livello) di un nodo è il numero di archi che bisogna attraversare per raggiungerlo partendo dalla radice. Nodi con lo stesso genitore sono detti fratelli. L'altezza di un albero è la massima profondità a cui si trova una foglia.

Versioni di albero

Esistono alcune classi di alberi con vincoli strutturali che ne facilitano la rappresentazione o operazioni efficienti. Ad esempio, un albero d-ario è un albero in cui tutti i nodi tranne le foglie hanno grado massimo d.

Un livello si dice saturo se ha il massimo numero possibile di nodi.

Un albero d-ario si dice pieno se tutti i livelli sono saturi, tranne eventualmente l'ultimo, mentre si dice completo se è pieno e i nodi sull'ultimo livello sono tutti disposti il più a sinistra possibile.

Rappresentazioni indicizzate di alberi

Vettore padre: dato T = ( N, A) con n nodi numerati da 0 a n – 1 si utilizza un array P di dimensione n le cui celle contengono la coppia ( info, parent) con P[ v]. info contenente il dato del nodo v e P[ v]. parent = u se e solo se ( u, v) ∈ A, mentre se v è la radice allora P[ v]. parent = null, con v ∈ [ 0 , n – 1].

Vettore posizionale: dato T = ( N, A) un albero d-ario completo con n nodi numerati da 1 a n si utilizza un array P di dimensione n + 1 tale che P[ v] contiene il dato nodo v. Al figlio i-esimo di v assegno la posizione nell’array:

d ( v – 1) + i + 1 con i ∈ [1, d]

Visite di alberi

È utile attraversare gli alberi visitandone tutti i nodi, una ed una sola volta, che partendo dalla radice visita tutti i discendenti. T( n) = O( n) e S( n) = O( n).

Visita in profondità

Partendo dall'algoritmo generico, possiamo utilizzare un tipo di dato Pila ed ottenere la visita in profondità o depth-first search (DFS). L’algoritmo di visita in profondità (DFS) parte da r e procede visitando nodi di figlio in figlio fino a raggiungere una foglia. Retrocede poi al primo antenato che ha ancora figli non visitati (se esiste) e ripete il procedimento a partire da uno di quei figli.

Visita in profondità ricorsiva

Esiste anche una versione di DFS ricorsiva. Non usiamo più la Pila, ma di fatto il procedimento è lo stesso mediante la ricorsione. Tre varianti:

 Visita in preordine; Visita la radice → visita il sottoalbero sinistro in ordine anticipato → visita il sottoalbero destro in ordine anticipato → lista dei nodi:

F, B, A, D, C, E, G, I, H

 Visita simmetrica; Visita il sottoalbero sinistro in ordine simmetrico → visita la radice → visita il sottoalbero destro in ordine simmetrico → lista dei nodi: A, B, C, D, E, F, G, H, I  Visita in postordine; Visita il sottoalbero sinistro in ordine posticipato → visita il sottoalbero destro in ordine posticipato → visita la radice → lista dei nodi: A, C, E, D, B, H, I, G, F

Alberi ed espressioni

Ogni nodo che contiene un operatore è radice di un sottoalbero. Ogni foglia contiene un valore costante o una variabile.

Notazione polacca (sintassi) denota formule matematiche:

 Gli operatori si trovano tutti a sinistra degli argomenti (prefissa);  Notazione polacca inversa (postfissa): (((b x c) + a) – e) x b – ((c + a)/d)  - x – + a x b c e b / + c a d (visita anticipata);  a b c x + e – b x c a + d / - (visita posticipata).

Visita in ampiezza

Partendo dall'algoritmo generico, possiamo utilizzare un tipo di dato Coda ed ottenere la visita in ampiezza (BFS). L’algoritmo di visita in ampiezza parte da r e procede visitando nodi per livelli successivi. Un nodo sul livello i può essere visitato solo se tutti i nodi sul livello i − 1 sono stati visitati.

Search

Grazie alla proprietà di ricerca l'operazione search è molto semplice. Traccia un cammino nell'albero partendo dalla radice: su ogni nodo, usa la proprietà di ricerca per decidere se proseguire nel sottoalbero sinistro o destro.

La complessità è T^ (^ n^ )= O^ (^ h )^.^ Per come costruiamo l’albero l’ultimo livello ha massimo 2h^ nodi. Poiché deve risultare 2 h^ ≤n , si ottiene che h = O ( log 2 n ), nel caso peggiore, perché l’altezza è limitata.

Insert

Un nuovo nodo viene sempre inserito come foglia in un BST. L'inserimento può essere implementato con questi due passi:

 Cerca il nodo v che diventerà genitore del nuovo nodo;  Crea il nuovo nodo u con elemento e con chiave k ed appendilo come figlio sinistro o destro di v rispettando la proprietà di ricerca.

Il passo 1 equivale ad un'operazione di ricerca, è O( h). Il passo 2, se implemento l'albero tramite puntatori ai figli, richiede solo di

modificare un numero costante di puntatori e quindi è O(1). Complessivamente, l'inserimento è O( h).

Max

Per poter valutare la complessità della cancellazione, dobbiamo prima introdurre altre due operazioni. La prima che vediamo è la ricerca del nodo con valore massimo nel sottoalbero di un nodo u. Grazie alla proprietà di ricerca, basta scendere verso destra nel sottoalbero di u finché è possibile. Questa operazione è chiaramente O( h).

Pred

La seconda operazione che ci serve è quella che permette di ricavare il predecessore di un nodo. Il predecessore di un nodo u è un nodo v avente massima chiave chiave( u). Per trovare il predecessore, distinguiamo due casi:

  1. u ha un figlio sinistro: in questo caso pred( u) è il massimo del sottoalbero sinistro di u;
  2. u non ha un figlio sinistro: pred( u), se esiste, è il più basso antenato di u (ovvero l'antenato con massima profondità nell'albero) il cui figlio destro è anch'esso antenato di u. Per trovarlo, risaliamo da u verso la radice fintanto che non incontriamo una svolta a sinistra.

Anche questa operazione ha complessità O( h).