















































































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
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
1 / 87
Questa pagina non è visibile nell’anteprima
Non perderti parti importanti!
















































































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:
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:
∀ I di dim. n
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
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
log 2 n i 2 i −^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 )= {
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:
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 )= θ ( f ( n )) se f ( n )= Ω ( n log b^ a +^ ε^ ) con ε > 0 e a ∙ f^ ( b^ n ) ≤ c ∙ f ( n )
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).
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:
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:
Anche questa operazione ha complessità O( h).