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: Introduzione agli Alberi e ai Grafi, Dispense di Algoritmi E Strutture Di Dati

Appunti presi dalle lezioni del corso di Algoritmi e Strutture Dati della prof Sara Foresti, dell'Università di Milano, corso di Informatica per la Comunicazione Digitale.

Tipologia: Dispense

2015/2016

In vendita dal 30/09/2016

AleSpero
AleSpero 🇮🇹

5

(1)

4 documenti

1 / 47

Toggle sidebar

Questa pagina non è visibile nell’anteprima

Non perderti parti importanti!

bg1
Passi per risolvere un problema
Formulazione del problema
1.
Definire la soluzione
2.
Implementare la soluzione
3.
Testing
4.
Valutare la soluzione
5.
Algoritmo
Algoritmo -> "Metodo per risolvere un problema"
Input Output
Un insieme di valori in input ALGORITMO
Un insieme di valori in output
2 caratteristiche fondamentali:
Un algoritmo è corretto se produce un output corretto per il problema, ossia lo risolve.
Esempio:
Problema -> Ordinare un insieme di numeri interi
Istanza -> S = {5, 1, 4, 6}
Output atteso -> S' = {1, 4, 5, 6}
Corretto
1.
Efficiente (si valuta sia in termini di tempo, che spazio)
2.
Comprensibile (questione più legata ai linguaggi di programmazione)
3.
Un algoritmo deve essere
Input
1.
Qualità (come è stato scritto l'algoritmo)
2.
Complessità delle istruzioni (istruzioni di base)
3.
Complessità dell'algoritmo
4.
Fattori che influiscono sul tempo
n=10
T(n)=T(10)
Caso migliore
1.
Nel controllare la complessità dell'algoritmo si controllano 3 casi:
Appunti di Alessandro Sperotti
Algoritmi e Strutture Dati
martedì 29 settembre 2015
13:30
Algoritmi e strutture dati Pagina 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

Anteprima parziale del testo

Scarica Algoritmi e Strutture Dati: Introduzione agli Alberi e ai Grafi e più Dispense in PDF di Algoritmi E Strutture Di Dati solo su Docsity!

Passi per risolvere un problema

**1. Formulazione del problema

  1. Definire la soluzione
  2. Implementare la soluzione
  3. Testing
  4. Valutare la soluzione**

Algoritmo

Sequenza finita di istruzioni, ciascuna delle quali ha un preciso significato , e può essere eseguito in un tempo finito Algoritmo - > " Metodo per risolvere un problema " Input Output

  • Un insieme di valori in input ALGORITMO
  • Un insieme di valori in output 2 caratteristiche fondamentali: Un algoritmo è corretto se produce un output corretto per il problema, ossia lo risolve. Per testare l'efficienza di un algoritmo bisogna testarlo su più casi, con valori diversi, non solo nel caso specifico del problema Esempio: Problema - > Ordinare un insieme di numeri interi Istanza - > S = {5, 1, 4, 6} Output atteso - > S' = {1, 4, 5, 6}
  1. Corretto
  2. Efficiente (si valuta sia in termini di tempo , che spazio )
  3. Comprensibile (questione più legata ai linguaggi di programmazione) Un algoritmo deve essere
  4. Input
  5. Qualità (come è stato scritto l'algoritmo)
  6. Complessità delle istruzioni (istruzioni di base)
  7. Complessità dell'algoritmo

Fattori che influiscono sul tempo

Esprimiamo la complessità dell'algoritmo (numero istruzioni) attraverso la funzione T(n) con n che è la dimensione dell'input n= T(n)=T(10)

  1. Caso migliore Nel controllare la complessità dell'algoritmo si controllano 3 casi: Appunti di Alessandro Sperotti

Algoritmi e Strutture Dati

martedì 29 settembre 2015 13:

  1. Caso migliore
  2. Caso medio
  3. Caso Peggiore (il più importante) Per valutare che l'algoritmo abbia un tempo efficiente anche nel caso di input con grandi numeri si valuta la complessità asintotica (tendenza di crescita di n) Strutture dati ADT (Abstract Data Type) L'ADT è un modello matematico per i dati con un set di operazioni definite su quel modello
  4. Si sa esattamente come organizzare i dati
  5. Si pone ad "alto livello", slegato da linguaggi di programmazione Vantaggi: Data Type (Tipo di Dato) L' insieme di valori che può assumere una variabile del tipo specificato Data Structure (Struttura Dati) Collezione di variabili (anche di tipo diverso ) collegate in vario modo. (usata per rappresentare ADT) Varie strutture dati: Array: struttura dati che contiene dati tutti dello stesso tipo. Ogni array possiede un indice che identifica ogni cella, ed il contenuto della cella stessa.

A[i] - > i-esima cella

  • Record: Struttura dati che può contenere dati di diversi tipi. Puntatore: è una cella che contiene un indirizzo di un'altra cella: permette di accedere ad un'altra cella in memoria.

Notazioni Asintotiche

Theta grande

O grande

2 N, kN^ - > costo esponenziale (vengono provate tutte le combinazioni dell'input per risolvere il problema)

Regola della Somma

La regola della somma vale per

Regola del Prodotto

(vale anche per omega grande e theta grande) Costo Algoritmo - > Regole calcolo Considerando di aver n input, guardiamo le regole di calcolo durante il calcolo di costo dell'algoritmo

  • Singola Istruzione base: O(1)
  • Sequenza di istruzioni: massimo costo fra istruzioni nella sequenza
  • Costrutto if - then - else - > valore condizione O(1) + il ramo con il maggior costo (if o then)
  • Ciclo: valore condizione uscita dal ciclo: costo singolo algoritmo x numero iterazioni Esempio Esercizi

Costo di una chiamata a Funzione/Procedura Vi sono due casi da distinguere: Se la funzione chiama un'altra funzione il costo totale è uguale al costo della funzione + il costo di ogni altra funzione chiamata a.

  1. La funzione non è ricorsiva: Il costo della funzione è uguale al costo del pezzo di codice che compone la funzione.
  2. La funzione è ricorsiva: consideriamo una funzione fattoriale di n: If(n>1) Return 1; Else Return n*fattoriale(n-1) In questo caso il costo si calcola tramite l' equazione di ricorrenza della funzione, ossia la seguente. T(n) = c + T(n-1) Ma come si risolvono queste equazioni? Metodo di sostituzione Supponiamo di avere un'equazione di ricorrenza: Supponiamo inoltre che la complessità di T(n) sia Allora dobbiamo verificare che Sostituiamo T(n) in 2T(n/2): Sostituendo abbiamo trovato che il risultato coincide con quello trovato nella definizione: Quindi la proposizione è corretta. Albero di ricorsione Supponiamo questa equazione di ricorrenza: Possiamo osservare che ci saranno 3 chiamate ricorsive: quindi disegnamo un "albero" Ogni livello viene tutto diviso per 4, quindi sappiamo che per calcolare il livello i-esimo usiamo la seguente formula: Per calcolare il numero di livelli pongo = n Per calcolare il costo di ogni livello utilizziamo la formula Per calcolare il costo complessivo facciamo la somma del costo di ogni livello: Per ogni "livello" dell'albero ne calcoliamo il costo: ALGO 2° Lezione martedì 6 ottobre 2015 13:

L[q+1]=L[q] //i valori dopo last For q=last downto p (da last fino a p) //shifto in avanti L[p]=x //inserisco il valore nella pos last=last+1 //liberata 9 12 36 18 24 7 10 last= insert(13,4,L) max.length= Costo totale = if L[q]=x return(q) for q=1 to last return "elemento non in lista" Costo totale: O(n)

  • locate(x,L) - > cerca l'elemento specificato all'interno di L e ne restituisce la posizione
  • delete(p,L) Return "posizione non valida" If(p<last) OR (p<1) L[q]=L[q+1] //e sovrascrivo il valore da canc. for q=p to last //shifto indietro i valori last =last- 1 Return "errore" if(p>last) OR (p<1) return (L[p]) Il seguente algoritmo ha costo
  • Retrieve(p,L)
  • Next(p,L)
  • Previous(p,L)
  • First(L) tutte queste istruzioni hanno complessità
  • Makenull(L) - > last= Implementazione tramite puntatori
  • Il primo elemento sarà il contenuto reale della lista, l'elemento in se. (element.key)
  • Il secondo elemento sarà un puntatore che conterrà l'elemento successivo della lista. (element.next) Consideriamo ogni cella come composta da 2 elementi: Il primo elemento della lista si chiama head , mentre l'ultimo elemento della lista si chiama tail. (testa-coda)
  • Insert(x,p,L) - > a differenza dell'insert per gli array, p è un puntatore ad una cella della lista: In sostanza, la cella precedente punta al nuovo elemento, e quest'ultimo punta alla cella successiva

temp=p.next allocate(p.next) p.next.Key=x p.next.next=temp while p!=NULL if p.key==x p=p.next p=L.head return p

  • Locate(x,L) p=L.head p.next=p.next.next Return If(p.next).key=x p=p.next while p.next!=NULL
  • Locate&delete(x,L) Lista doppiamente linkata In una lista doppiamente linkata non abbiamo solo un puntatore a next , ma anche a previous Nelle liste doppiamente linkate è frequente l'uso di sentinelle , ossia elementi lasciati vuoti per gestire più facilmente le liste. Queste sentinelle contengono soltanto il puntatore all'elemento successivo e a quello precedente : utilizzando una sentinella possiamo considerare una lista linkata come "circolare" p.previous.nest=p.nest If p.previous != NULL p.next.prevoius=p.previous If p.next!=NULL
  • Delete(p,L) Stack Lo stack è un tipo particolare di lista dove operazioni di insert e delete vengono eseguite a una sola estremità ( TOP dello stack) LIFO - > - Last In First Out Come le liste, anche lo stack è implementabile tramite array o puntatori : Implementazione tramite Array L'array S[s1, … , max_length] contiene tutti elementi dello stesso tipo e contiene una variabile top che contiene l'indice dell' ultimo elemento inserito. S.top = S.top+1; S[S.top]=x;
  • Push(x,S) - > inserimento elemento al top (elemento in cima)

Coda - Queue Una coda è un tipo particolare di lista: si differenzia dalla lista per le operazioni di inserimento e rimozione di elementi. Al contrario della strategia LIFO utilizzata dallo stack, per la coda si utilizza la FIFO (First In First Out) In una coda gli inserimenti sono effettuati nell'estremità tail della coda, mentre le cancellazioni sono eseguite all'altra estremità della coda ( head ) An An- 1 An- 2 … A Tail head Operazioni

  • Enqueue (x,Q) inserisce x nella coda
  • Dequeue (Q) rimuove l'elemento in posizione head
  • Front (Q) restituisce il primo elemento nella coda
  • Empty (Q) Controlla se la coda è vuota
  • Makenull (Q) svuota la coda Per l'implementazione è possibile farla sia tramite array che tramite liste linkate. Implementazione tramite array in una coda Q[1…. Max.length] Per gestire la coda creiamo una variabile head e una tail , che rispettivamente puntano alla testa e alla coda della coda ( Nel caso di un operazione di dequeue , eliminiamo il contenuto attuale di head (opzionale) e spostiamo la variabile head di una posizione (più specificatamente, aumenta di valore) Nel caso di un operazione di enqueue , viene inserito un elemento in posizione n+1, e viene spostata la tail di una posizione in avanti (dalla posizione n alla posizione n+1) È possibile però che tail raggiunga max.length: ciò significa che la coda è piena? NO , perché nel caso di eliminazioni head sia in posizioni superiori a 1 (ossia non all'inizio dell'array) per ovviare a questa soluzione bisogna considerare la coda come una struttura circolare. Implementazione di enqueue Q[Q.tail]=x Q.tail - > variabile tail Q.tail= If Q.tail=Q.max.length AND empty=FALSE //coda circolare Else Q.tail=Q.tail + 1 //inserimento CODA PIENA! If Q.tail==Q.head Costo= o(1) Implementazione di dequeue x=Q.[Q.head] ALGO 3° Lezione mercoledì 7 ottobre 2015 10:

x=Q.[Q.head] Q.head= If Q.head == Q.max.length Else Q.head = Q.head + 1 //rimozione Implementazione tramite puntatori È una classica lista con due puntatori in più: head e tail. Anche con questo tipo di implementazione la coda può essere circolare. Stack e Ricorsione Lo stack viene spesso utilizzato dai linguaggi di programmazione per gestire la ricorsione : Stack dei record di attivazione : contiene tutto ciò che serve a ciascuna procedura chiamata per operare correttamente. Nella ricorsione ogni funzione/procedura aggiunge un record di attivazione al top dello stack

  • Ricorsione: permette di destrivere in modo gfinito un insieme potenzialmente infinito di oggetti Per fare si che la ricorsione termini ci deve essere un caso base (banale), ossia un caso che viene sempre raggiunto e non in modo ricorsivo. Ogni "passo" della ricorsione si dice "passo ricorsivo"
  • Il parametro di attivazione è passato per argomento
  • La chiamata ricorsiva è l'ultima istruzione Le funzioni ricorsive possono essere riformulate in modo iterativo. Un caso particolare è la tail recursion , ossia una funzione ricorsiva ove è facile eliminare la ricorsione. Si ha questo tipo di ricorsione quando:
  • Lefthost_child(n,T) - > ritorna il nodo più a sinistra tra i figli del nodo n nell'albero T
  • Right_sibling(n,T) - > ritorna il fratello di destra del nodo n nell'albero T
  • Label(n,T) - > ritorna l'etichetta del nodo n nell'albero T Create(v,T1,…,Tn) - > crea un albero che ha come radice il nodo v e ha come figli le radici degli alberi T1,T2…)
  • Root(T) - > ritorna la root dell'albero T
  • Makenull(T) - > rende l'albero T nullo

Albero implementato con Array

Un albero implementato tramite array è un array T[1, … , n] dove n è il numero di nodi e T[i] = j indica che il nodo j-esimo è il genitore del nodo i-esimo in T. in altre parole: I nodi sono rappresentati dalla celle di un array, che per ciascuno di essi contengono l'indice della cella del proprio genitore.

Pro e contro

  • è efficiente trovare il genitore Pro :
  • Costoso trovare i figli di un nodo Si perde l'ordine dei figli di ciascun nodo, se non si adotta una convenzione per cui i figli di ciascun nodo seguono un dato ordine dell'array T

Contro:

Albero implementato con liste di figli

Ogni nodo ha associata la lista dei suoi figli. Qualsiasi implementazione di lista va bene per rappresentare i figli.

Algoritmi di visita

Vi sono vari tipi di metodi per "visitare" un albero, distinguiamoli in varie classi : ○ Pre-ordine (GDBACFEIHJ) ○ In-ordine (ABCDEFGHIJ) ○ Post-ordine (ACBEFDHJIG)

  • In Profondità : partendo dalla radice, si cerca di scendere sempre più in profondità.
  • In Ampiezza Visita l'albero livello per livello, partendo dal nodo radice

Preordine

  1. Viene visitato n
  2. Visito in preordine i figli di n da sinistra verso destra (T1,T2, ecc) L'algoritmo di visita in preorder si sviluppa in questo modo.

Implementazione

Label(n) Pre_order(c) c=c.next While c!=NULL C = n.children (lista dei figli?)

  1. Visita, in-order, il primo figlio a sinistra di n (il più a sinistra)
  2. Visito il nodo n (root del sottoalbero)
  3. Visito in-order, il 2°,3° fino al k° figlio di n da sinistra verso destra

In-order

Implementazione

IN_ORDER(n) If c != NULL //se la lista dei figli non è vuota procede con la funzione In_order(c) Label(n) //visita la radice c=n.children

ALGO 5° Lezione

martedì 20 ottobre 2015 13:

rispettivamente ai due figli. In un albero binario gli elementi sono ordinati a. Il valore dei nodi nel sottoalbero sinistro di n è MINORE di n b. Il valore dei nodi nel sottoalbero destro di n è MAGGIORE di n

  • Un albero binario di ricerca , se Quindi, la visita in order dell'albero produce l'elenco dei valori ordinati Il vantaggio degli alberi binari di ricerca è che per cercare un elemento x non è necessario visitare l'albero: basta sfruttare le proprietà e l'ordinamento degli elementi che compongono l'albero.

Tree_Search(n,k)

K è l'elemento da cercare all'interno di un albero. Return(n) If n.key==k & n==NULL Return (tree_search(n.left,k)) If n.key>k Return(tree_search(n.right,k)) Else

Tree_minimum(n)

n=n.left While n.left != NULL Return(n)

Tree_maximum(n)

n=n.right While n.right != NULL Return(n)

Tree_successor(n)

Tree_minimum(n.right) If n.right != NULL y=n.p //n.p - > genitore n=y y=y.p While y != NULL & n ==y.right Return y Inserimento Visito l'albero cercando v: inserisco v come una nuova foglia. TREE_INSERT(R,z) y=NULL

y=NULL x=T.root y=x x=x.left If z.key <z.key Else x=x.right While x!=NULL z.p=y T.root = z If y==NULL y.left = z Elseif z.key <y.key Else y.right = z

Cancellazione

TREE_DELETE(T,z) Translplant(T,z,z.right) if z.left==NULL Translplant(T,z,z.left) if z.right=NULL else y=TreeMinimum(z.right) Transplant(T,y,y.right) y.right=z.right y.right.p=y if y.p!=z Transplant(T,z,y) y.left=z.left y.left.p=y TRANSPLANT(T,w,v) R=v if w-p=NULL u.p.left=v elseif u=u.p.left u.p.right=v else n.p=w.p if v!=NULL

posizione in cui viene inserito un certo elemento è fissata , e non si può decidere a piacere dove inserire un nodo. Per ovviare a questo problema si utilizzano le rotazioni.

Rotazione

Vi sono due tipi di rotazioni: rotazione singola e rotazione doppia. In questo caso una rotazione singola è utile per bilanciare i nodi x o z (rispettivamente per l'immagine a sinistra e a destra). Nel caso dell'immagine di sinistra eseguendo la rotazione il nodo k2 "scende" e diventa un figlio di k1, il quale diventa root. A suo modo il nodo x "sale" arrivando allo stesso livello di k2. Facciamo un esempio, bilanciando un albero formato da 7 nodi: Tuttavia se volessi bilanciare y la rotazione singola non basta, bensì dovrò adoperare la rotazione doppia.

Una rotazione doppia è semplicemente l'insieme di due rotazioni singole. Grazie a questo tipo di rotazione riusciamo a bilanciare il nodo y presente nell'immagine precedente. Ecco un esempio per chiarire bene il tutto:

Inserimento albero AVL del valore x

  1. Inserisco x in T seguendo la regola del Binary Search Tree Partendo dalla nuova foglia seguo il path verso la root e controllo se la proprietà di bilanciamento AVL è soddisfatta

a. Se concorde: rotazione singola b. Se discorde: rotazione doppia

  1. Al primo nodo sbilanciato applico la rotazione
  2. Cancellazione come da normale albero AVL
  3. Partendo dal nodo rimosso e salendo verso la root, controllo il bilanciamento AVL

Cancellazione di x da un albero AVL

  1. Tutte le foglie sono allo stesso livello
  2. Hanno un grado molto elevato B-Tree Possiamo considerarli come degli alberi "molto piatti e larghi" Con n=numero di valori e m = grado dell'albero Diamo una definizione ai B tree:
  • La radice o è una foglia oppure è un nodo con almeno 2 figli
  • Ogni nodo interno ha fra ceiling[ e m figli
  • Tutti i path della root a una foglia sono lunghi uguale Un B-tree di grado m soddisfa la seguenti proprietà: (da completare)

Inserimento in albero B