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


Introduzione alle Strutture Dati: Array, Liste, Stack e Queue - Prof. Foresti, Appunti di Algoritmi E Strutture Di Dati

Algoritmi e strutture dati - appunti - Prof. Foresti

Tipologia: Appunti

2017/2018

Caricato il 28/02/2018

matteochannel1
matteochannel1 🇮🇹

4.5

(6)

6 documenti

1 / 18

Toggle sidebar

Questa pagina non è visibile nell’anteprima

Non perderti parti importanti!

bg1
29-9-15
Computer programs → solve problems
Passi da seguire →
formulare il problema;
definire la soluzione;
implementarla;
testarla;
valutarla.
La prima cosa è conoscere il problema da risolvere:
molto probabilmente non sono specificati in modo preciso (es.: creare una ricetta per un
ottimo dolce);
se un problema può essere espresso in termini di un modello formale, è meglio farlo →
quando il problema è personalizzato, possiamo cercare delle soluzioni e scoprire se esistono
già programmi che risolvono il problema (o usare le proprietà del modello per costruire
buone soluzioni);
per modellare bene un problema possiamo sfruttare tutte le branche della matematica.
Quando abbiamo un modello del nostro problema: cerchiamo di definire una soluzione in termini di
modello.
Obbiettivo = trovare una soluzione nella forma di algoritmo.
Algoritmo = sequenza finita di istruzioni ciascuna delle quali ha un preciso significato e può essere
eseguita in un tempo finito, un algoritmo può contenere istruzioni ripetute, ma un algoritmo deve
sempre terminare! (indipendentemente dal suo input).
Definizione alternativa: procedura computazionale ben definita che prende un valore (o insieme di
valori) in input e produce un valore (o un insieme di valori) in output.
(Strumento per risolvere un problema computazionale)
Un algoritmo è corretto se termina e produce un output corretto → risolve il problema!
Istanza o problema → caso specifico di un problema ottenuto quando il problema è riferito ad un
certo output.
Esempio: problema ordinamento di un set di numeri.
Input: 31, 41, 56, 29, 41, 58
Output: 29, 31, 41, 41, 56, 58
Esistono diversi tipi di algoritmo (bubble sort, merge sort, …)
ADT, data type, data structur
Gli algoritmi devono lavorare su dei dati: se sono organizzati, il lavoro funziona meglio.
ABSTRACT DATA TYPE (ADT): modello matematico con una collezione di operazioni definite
su quel modello.
Esempio: insieme di interi; operazioni: unione, intersezione, differenza.
Le ADT sono indipendenti dall'implementazione, sono in genere una generalizzazione dei tipi di
base, che possiamo trattare come tale.
(+) in un solo punto del programma, ho tutte le operazioni sul tipo e le posso cabiare o
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe
pff
pf12

Anteprima parziale del testo

Scarica Introduzione alle Strutture Dati: Array, Liste, Stack e Queue - Prof. Foresti e più Appunti in PDF di Algoritmi E Strutture Di Dati solo su Docsity!

Computer programs → solve problems Passi da seguire →

  • formulare il problema;
  • definire la soluzione;
  • implementarla;
  • testarla;
  • valutarla. La prima cosa è conoscere il problema da risolvere:
  • molto probabilmente non sono specificati in modo preciso ( es.: creare una ricetta per un ottimo dolce);
  • se un problema può essere espresso in termini di un modello formale, è meglio farlo → quando il problema è personalizzato, possiamo cercare delle soluzioni e scoprire se esistono già programmi che risolvono il problema (o usare le proprietà del modello per costruire buone soluzioni);
  • per modellare bene un problema possiamo sfruttare tutte le branche della matematica. Quando abbiamo un modello del nostro problema: cerchiamo di definire una soluzione in termini di modello. Obbiettivo = trovare una soluzione nella forma di algoritmo. Algoritmo = sequenza finita di istruzioni ciascuna delle quali ha un preciso significato e può essere eseguita in un tempo finito , un algoritmo può contenere istruzioni ripetute , ma un algoritmo deve sempre terminare! (indipendentemente dal suo input). Definizione alternativa: procedura computazionale ben definita che prende un valore (o insieme di valori) in input e produce un valore (o un insieme di valori) in output. (Strumento per risolvere un problema computazionale) Un algoritmo è corretto se termina e produce un output corretto → risolve il problema! Istanza o problema → caso specifico di un problema ottenuto quando il problema è riferito ad un certo output. Esempio: problema ordinamento di un set di numeri. Input: 31, 41, 56, 29, 41, 58 Output: 29, 31, 41, 41, 56, 58 Esistono diversi tipi di algoritmo (bubble sort, merge sort, …) ADT, data type, data structur Gli algoritmi devono lavorare su dei dati: se sono organizzati, il lavoro funziona meglio. ABSTRACT DATA TYPE (ADT): modello matematico con una collezione di operazioni definite su quel modello. Esempio : insieme di interi; operazioni: unione, intersezione, differenza. Le ADT sono indipendenti dall'implementazione , sono in genere una generalizzazione dei tipi di base, che possiamo trattare come tale.
  • (+) in un solo punto del programma, ho tutte le operazioni sul tipo e le posso cabiare o

implementare a piacimento.

  • (+) uso un ADT come un tipo di base nel programma generale.
  • (-) alcune operazioni coinvolgono più ADT e le devo specificare bene per tutte le ADT coinvolte. Esempio : una lista di numeri interi ha operazioni come:
  • svuota;
  • prendi il 1° elemento;
  • prendi l'ultimo elemento;
  • inserisci un elemento. Ci sono vari modi per implementare questo ADT → cambio implementazione → riscrivo le funzioni per le operazioni di base. DATA TYPE: di una variabile è l'insieme di soluzioni che può assumere, e varia da linguaggio a linguaggio. I tipi di base si possono comparare. Esempi : interi, caratteri, boolean, … DATA STRUCTURE: collezione di variabili (anche di tipi diversi) connesse in vario modo, usata per rappresentare una ADT. Componente base di una struttura dati è la cella (box che contiene un valore). Strutture dati → combinano e danno un nome ad aggregati di celle con un riferimento ( puntatore ) ad altre. Alcuni esempi di DATA STRUCTURES molto usati:
  • ARRAY: sequenza di celle tutte dello stesso tipo (cell type) può essere pensato come un mapping da un index set al dominio del cell type. Es.: a b c ... ... z cells 1 2 3 ... ... n index
  • RECORD: collezione di celle (chiamate anche campo ) che possono avere tipo diverso. Es.: nome: christian marcias ddn: 17-11- address: via percali, 27
  • le relazioni fra le celle, oltre ad essere espresse come array o record, si possono realizzare come:
  • puntatori → cella che indica un'altra cella;
  • cursore → cella il cui contenuto è un intero che fa riferimento ad una specifica cella di un array indicandone il vettore dell'indice. Studio di complessità Dato un problema → esistono diversi algoritmi in grado di risolverlo. Quale scegliere? 1. uno che sia facile da capire ed implementare;
  1. uno che usa bene le risorse, che è efficiente soprattutto per il tempo che impiega a trovare una soluzione → indipendente dalla macchina su cui gira, ma per il suo progetto. Una risorsa più importante è spesso il tempo. Vogliamo valutare quanto impiega un algoritmo a trovare una soluzione.

Rappresenta un lower-bound asintotico, è più debole di ϴ(g(n)). Da notare:

  1. f(n) deve essere asintoticamente non negativa (non negativa per un n abbastanza grande);
  2. possiamo ammettere le costanti nel definire g(n), perché per qualsiasi costante del termine dominante di g(n), esiste una costante c(c1 o c2, rispettive) da poter usare nella dominanza. Es.: Ulteriori notazioni asintotiche includono:
  • o piccolo : o(g(n)) = {f(n) : per ogni c>0 esiste una costante n0>0 tale per cui 0<=f(n)<=c*g(n), per ogni n>=n0}
  • w piccolo : w(g(n)) = {f(n) : per ogni c>0 esiste una costante n0>0 tale per cui 0<=c*g(n)<=f(n), per ogni n>=n0} f(n) Є w(g(n)) <=> g(n) Є o(f(n)) Proprietà delle relazioni asintotiche
  • transitività : f(n)= ϴ(g(n)) e g(n)= ϴ(h(n)) → f(n)= ϴ(h(n).

Lo stesso vale per O, Ω, o, w.

  • Riflessività : f(n)= ϴ(f(n)), f(n)=O(f(n)), f(n)=Ω(f(n)).
  • Simmetria : f(n)= ϴ(g(n)) ↔ g(n)= ϴ(f(n)), f(n)=O(g(n)) ↔ g(n)= Ω(f(n)) f(n)=o(g(n)) ↔ g(n)=w(f(n)) → la relazione d'ordine fra le due funzione data dalle relazioni asintotiche stabilisce solo un ordine parziale (non tutte le coppie di funzioni sono confrontabili secondo le notazioni asintotiche). Scelta dell'algoritmo In termini di efficienza preferiamo l'algoritmo che, nel worst case , presenta un O grande asintotico più bassa → risparmio, soprattutto se l'input è grande, molto tempo perché la curva di crescita del tempo computazionale è meno rigida. ATTENZIONE! Se lavoro sempre su input piccolo, anche la costante è di rilievo e la notazione asintotica non vale! Attenzione va posta anche alla memoria usata (complessità di spazio). Sarebbe sempre auspicabile avere algoritmi chiari e comprensibili agli altri! Analisi di un algoritmo Casi base:
  • 1: costo costante → non dipende dalla dimensione dell'input. Es.: lettera del 1° elemento di un array.
  • Log n: costo logaritmico → il problema viene risolto trasformandolo in problemi più piccoli, riducendone la dimensione ( es.: non guardo tutti i dati, algoritmi su alberi binari).
  • N: costo lineare → un piccolo e costante ammontare di calcolo è eseguito su ciascun input. Es.: lettura di tutti gli elementi in una lista.
  • N log N: spezza il problema in parti più piccole, i cui risultati vengono riconfermati.
  • N^2: costo quadratico (n^3, n^4, … ) → processa tutte le coppie di dati in input.
  • 2^n: costo esponenziale → processa tutte le possibili combinazioni di dati in input (è il caso più costoso). Regola della somma Dati due algoritmi A1 con costo O(f(n)) e A2 con costo O(g(n)), quanto costa la loro esecuzione in sequenza? Costa O(f(n))+O(g(n))=O(max(f(n), g(n)), vince il più grande. Dimostrazione: A1 è O(f(n)) → Ǝ c1, n1: A1(n)<=c1f(n), per ogni n<=n1. A2 è O(g(n)) → Ǝ c2, n2 : A2(n)<=c2g(n), per ogni n<=n2.

corpo del ciclo; nel caso il numero di iterazioni non è noto, la si può stimare facendo sempre una valutazione pessimistica. Esempio: 6-10- Chiamate a funzione/procedura

  • Se le procedure e funzioni chiamate sono non ricorsive, si calcola per primo il costo della funzione/procedura che non ne invoca altre e si usa poi questo risultato per calcolare il costo delle funzioni e procedure chiamanti. Regola generale : partiamo dall'interno, dall'istruzione più semplice e dalla funzione/procedura che non ne chiama altre, e procediamo poi verso l'esterno per avere il risultato voluto.
  • Se le procedure/funzioni invece sono ricorsive, è necessario esprimere il corso con termini

di un'equazione di ricorrenza e poi risolvere tale equazione. Un'equazione di ricorrenza esprime il costo t(n) di una chiamata con input di dimensione n in funzione del costo t(n) che la funzione fa di sé stessa ma su un input di dimensione k. Es.: Esistono diversi modi per risolvere le equazioni di ricorrenza:

  1. metodo della sostituzione → prova con una soluzione e verifica se è corretta. Es.:
  2. metodo dell'albero di ricorsione → molto utile se non si ha idea della approssimazione da cui partire. Ad ogni modo, nell'albero rappresenta il costo di un sottoproblema (una chiamata). Sommando il costo dei nodi allo stesso livello e poi, per tutti i livelli, le somme ottenute, abbiamo una buona stima del costo dell'algoritmo.

Lista → sequenza ordinata di 0 o più elementi di un dato tipo; a1, a2, a3, … , an n=lunghezza della lista n=0 lista vuota. È possibile dire se un elemento precede/segue un altro elemento nella lista. Operazioni su lista L:

  • INSERT(x, p, L) → inserisce x nella posizione p della lista L, spostando avanti gli altri elementi. a1, a2, … , an → a1, … , ap-1, ax, ap+1, … , an
  • LOCATE(x, L) → trova l'elemento x nella lista L e ne restituisce la posizione, ritorna la ?? lista altrimenti (se x compare più volte, ritorna una sola ???).
  • RETRIEVE(p, L) → ritorna l'elemento nella posizione p della lista L.
  • DELETE(p, L) → rimuove l'elemento in posizione p della lista L.
  • NEXT(p, L) → ritorna la posizione successiva a p nella lista L.
  • PREVIOUS(p, L) → ritorna la posizione precedente a p nella lista L.
  • FIRST(L) → ritorna la prima posizione nella lista L.
  • MAKENULL(L) → svuota la lista L.
  • … Può essere implementata in vati modi: array, puntatori, … Lista implementata con un array Gli elementi sono memorizzati come celle contigue dell'array. 1° elemento lista 2° elemento … … Ultimo elem. last lista vuota La variabile last tiene traccia della posizione dove si trova l'ultimo elemento della lista nell'array.
  • integer A[max_length] → è un array che implementa una lista;
  • integer last → all'inizio; sarà 0 se la lista è vuota;
  • insert(x, p, A) → costo O(n):
  • locate(x, A) → costo O(n);
  • delete(p, A) → costo O(n);
  • le cifre operazionali sono banali: retrieve(p, A) → A[p]; next(p, A) → p+1; previous(p, A) → p-1; first(A) → A[1]; makenull(A) → last=0. → costo O(n) NOTA: bisogna sempre controllare se il valor di p sia entro il range Lista implementata usando i puntatori → linked list Ogni elemento della lista, oltre al contenuto, ha un puntatore al suo successore nella lista. L'ordine degli elementi è data dai loro puntatori.
  • head: primo elemento nella lista, non ha predecessori;
  • tail: ultimo elemento della lista, tail.next=NIL (=NULL). Data una lista L, la variabile L.head è un puntatore al suo primo elemento (può essere implementato come un elemento della lista con contenuto vuoto).
  • insert(x, p, L) → costo=O(n);
  • locate(x, L) → costo=O(n);
  • delete(p, L) → costo=O(1): trascura l'elemento p.next. NOTA: posso usare locate per trattare la posizione dove inserire/cancellare un elemento key==cella in cui è contenuto l'elemento, oppure con = memorizza l'elemento in quella cella. Es.: (p.next).key=x L'implementazione con array o con puntatori hanno vantaggi e svantaggi. La soluzione da usare va scelta caso per caso valutando le esigenze specifiche:
  • costo delle operazioni diverse.
  • Gli array richiedono di conoscere a priori la massima dimensione della lista e potrebbero causare uno spreco di spazio. Liste direttamente linkate Ciasun elemento ha due puntatori: all'elemento successivo e all'elemento precedente. Per comodità si possono usare le sentinelle, che sono elementi vuoti che permettono di gestire con più facilità le liste. In generale si usa una sentinella soltanto con un contenuto vuoto a cui punta il next della tail e il previous della head → la lista diventa circolare. Il riferimento alla sentinella sostituisce l'head.
  • PUSH(x, S): → check if(S.top>=max_length) → error “stack full” S.top=S.top+ S[S.top]=x
  • POP(S): if(empty(S)) → error “underflow” else → S.top=S.top-1 → return S[S.top-1]
  • TOP(S): if(empty(S)) → error “underflow” else → return S[S.top]
  • MAKENULL(S) S.top=
  • EMPTY(S): if(S.top=0) → return TRUE else → return FALSE Implementazione con puntatori: Ogni elemento punta a quello inserito prima di lui, c'è un puntatore che punta all'elemento TOP. 13-10- Coda – queue Queue (coda) → tipo particolare di lista dove le operazioni di inserimento vengono eseguite da un'estremità (detta tail o coda) mentre le operazioni di cancellazione vengono eseguite dall'altra estremità (detta head o testa). Le code implementano una strategia FIFO (first in first out). Operazioni su una coda Q
  • ENQUEUE(x, Q) → inserisce l'elemento x nella coda Q come ultimo elemento.
  • DEQUEUE(Q) → rimuove l'elemento in testa alla coda (eventualmente lo ritorna).
  • FRONT(Q) → restituisce il primo elemento nella coda (la head).
  • MAKENULL(Q) → svuota la coda Q.
  • EMPTY(Q) → restituisce TRUE solo se la coda Q è vuota. Essendo una lista, la coda può essere implementata con array o puntatori. Implementazione con array La coda è un array a[1, … , max_length] di elementi dello stesso tipo, con una variabile head e una variabile tail che tengono traccia del primo e dell'ultimo (rispettivamente) elemento nella coda in termini del suo indice nell'array. Cosa succede se continuo a mettere e togliere cose? Prima o poi arrivo a max_length come indice e non posso più inserire nulla, ma i primi posti

dell'array sono magari vuoti! Soluzione → pensare all'array come chiuso su sé stesso: array circolare → la prima posizione viene subito dopo l'ultima. head Implementazione con puntatori È una classica lista con due puntatori in più: head e tail (anche lei può essere circolare). Stack e ricorsione Stack usato dai linguaggi di prog. per gestire la ricorsione. Stack dei record di attivazione Contiene tutto quello che serve a ciascuna procedura chiamata per operare correttamente, soprattutto quando si torna al chiamante. Ogni procedura/funzione chiamata aggiunge un record di attivazione al top dello stack. Ricorsione → permette di descrivere in modo finito un insieme potenzialmente infinito di oggetti, descrivendo ciascuno in termini di sé stesso direttamente o indirettamente. Per terminare, la ricorsione deve avere un caso base, cioè un caso deve essere sempre raggiunto e che è definito in modo non ricorsivo. La ricorsione ha poi un passo ricorsivo. NOTA : ogni chiamata ricorsiva aggiunge un record di attivazione nello stack se la ricorsione dura troppo potrebbe esaurire lo spazio dedicato allo stack in memoria. Le procedure/funzioni ricorsive possono essere riformulate in termini iterativi se questa operazione richiede l'uso esplicito di uno stack che simula lo stack di attivazione, si dice che siamo in presenza di una funzione intrinsecamente ricorsiva. La tail recursion è un caso particolare di ricorsione facile da eliminare e si ha quando: il parametro di attivazione è passato per valore o se l'ultima istruzione è la chiamata ricorsiva.

  • una sequenza di nodi n1, n2, … , nk tale per cui ni è genitore di ni+1 per 1<k<i è detta cammino (path) da n1 a nk;
  • la lunghezza di un cammino è pari al numero di nodi che la compongono meno uno (cioè è pari al numero di archi percorsi);
  • un nodo è un ancestor (predecessore) di un altro nodo se esiste un path dal primo al secondo, il secondo nodo è detto descendant (discendente) del primo;
  • ogni nodo è ancestor e descendant di sé stesso (path lungo zero);
  • si parla di proper ancestor e proper descendant quando il path che collega i due nodi ha lunghezza almeno 1;
  • l' altezza di un nodo è la lunghezza del cammino più dal nodo ad una foglia;
  • l'altezza dell'albero è pari all'altezza della rat ;
  • la profondità di un nodo nell'albero è pari alla lunghezza dell'unico cammino che la congiunge alla radice dell'albero. Operazioni su un albero T
  • PARENT(n, T) → ritorna il genitore del nodo n nell'albero T.
  • LEFTHOST_CHILD(n, T) → ritorna il nodo più a sinistra fra i figli del nodo n dell'albero T.
  • RIGHT_SIBLING(n, T) → ritorna il fratello di destra del nodo n dell'albero T.
  • LABEL(n, T) → ritorna l'etichetta del nodo n dell'albero T.
  • CREATE(v, T1, … , Tn) → crea un albero che ha come radice il nodo con etichetta v e che ha come figli le radici degli alberi T1, … , Tn.
  • ROOT(T) → ritorna il nodo radice dell'albero T.
  • MAKENULL(T) → rende l'albero T vuoto. Un albero può essere rappresentato in vari modi: array, figlio sinistra-fratello destra, lista di figli, … Albero implementato con array I nodi sono rappresentati dalle celle di un array, che contiene, per ciascun nodo, l'indice della cella che rappresenta il suo genitore. L'albero è quindi un array T[1, … , n] dove n è il numero di nodi e T[i] indica che il nodo j-esimo è il genitore del nodo i-esimo in T. T[i]=0 se il nodo i è la radice dell'albero T. Es.:

Come tutte le cose, questa rappresentazione ha vantaggi e svantaggi:

  • (+) efficiente trovare il genitore;
  • (-) costoso trovare i figli di un nodo;
  • (-) si perde l'ordine dei figli di ciascun nodo, se non si addita una convenzione per cui i figli di ciascun nodo seguono un dato ordine nell'array T. Albero implementato con figlio sinistro-fratello destro Per ciascun nodo n si tiene traccia di due cose:
  • n.leftchild → il suo figlio più a sinistra (NIL se è foglia);
  • n.rightsibling → il suo fratello di destra (NIL se è il figlio più a destra di suo padre). Possono essere due array oppure due puntatori per il nodo n. Es.:
  • (+) mantengo l'ordine dei figli;
  • (+) trovo velocemente i figli di un nodo;
  • (-) costa molto ritrovare il padre di un nodo → devo scandire l'intero array e costa quindi O(n).