Tipi di dato e strutture dati

Documenti analoghi
I numeri razionali. Specifica: la sintassi. Specifica: la semantica

Esercitazione 11. Liste semplici

Strutture dati. Il che cosa e il come. F. Damiani - Alg. & Lab. 04/05

Specifica: la sintassi. Specifica: la semantica. Specifica: la semantica

Le liste. ADT e strutture dati per la rappresentazione di sequenze. Ugo de' Liguoro - Algoritmi e Sperimentazioni 03/04 - Lez. 5

Sommario. Le strutture dati elementari per implementare sequenze: Vettori Liste

Strutture Dinamiche. Fondamenti di Informatica

Il linguaggio C. Puntatori e dintorni

Strutture dati e loro organizzazione. Gabriella Trucco

Argomenti della lezione. Introduzione agli Algoritmi e alle Strutture Dati. Lista Lineare. Lista Lineare come Tipo di Dato Astratto

Fondamenti di Informatica e Laboratorio T-AB T-15 Strutture dati

Pile e code. ADT e strutture dati per la rappresentazione di sequenze ad accesso LIFO e FIFO

ADT LISTA: altre operazioni non primitive ADT LISTA COSTRUZIONE ADT LISTA COSTRUZIONE ADT LISTA (2)

Strutture Dati Dinamiche

ELEMENTI DI INFORMATICA L-B. Ing. Claudia Chiusoli

Tipi di dato personalizzati Array di struct. Tipi di dato utente. Laboratorio di Programmazione I. Corso di Laurea in Informatica A.A.

La struttura dati CODA

Il linguaggio C. Notate che...

Trasformare array paralleli in array di record

Laboratorio di Informatica

Astrazioni sui dati : Specifica di Tipi di Dato Astratti in Java

Trasformare array paralleli in array di record

L'Allocazione Dinamica della Memoria nel linguaggio C

Algoritmi e Strutture Dati

Fondamenti di Informatica II

4 Le liste collegate 4.0. Le liste collegate. 4 Le liste collegate Rappresentazione di liste 4.1 Rappresentazione di liste

Struttura dati astratta Coda

Una breve introduzione all implementazione in C di algoritmi su grafo

Laboratorio di Programmazione

Prof. E. Occhiuto INFORMATICA 242AA a.a. 2010/11 pag. 1

Linked Lists. Liste linkate (1) liste linkate ( stack, queues ), trees. Liste linkate come strutture

Alberi n-ari: specifiche sintattiche e semantiche. Realizzazioni. Visita di alberi n-ari.

Ogni variabile in C è una astrazione di una cella di memoria a cui corrisponde un nome, un contenuto e un indirizzo.

1) definizione di una rappresentazione 2) specificazione di un algoritmo (dipendente dalla rappresentazione) 3) traduzione in un linguaggio

Costruttori/distruttori. Sovraccarico degli operatori. Costruttori/distruttori. Necessità di un cotruttore

LE STRUTTURE DATI DINAMICHE: GLI ALBERI. Cosimo Laneve

Il linguaggio C Strutture

Tipi di dati strutturati e Linguaggio C. Record o strutture Il costruttore struct in C

La programmazione nel linguaggio C

Programma del corso. Elementi di Programmazione. Introduzione agli algoritmi. Rappresentazione delle Informazioni. Architettura del calcolatore

PILE E CODE. Pile (stack):

3) Descrivere l architettura di un elaboratore tramite astrazione a livelli

Algoritmi e Strutture Dati

Array e puntatori in C

Cosa si intende con stato

Esonero di Informatica I. Ingegneria Medica

Esercizi Strutture dati di tipo astratto

Problemi e algoritmi. Il che cosa e il come. F. Damiani - Alg. & Lab. 04/05 (da U. de' Liguoro - Alg. & Spe. 03/04)

Esercizi su strutture dati

Calcolare x n = x x x (n volte)

Problemi e algoritmi. Il che cosa ed il come. Il che cosa ed il come. Il che cosa e il come

Gestione dinamica della memoria

Indice PARTE A. Prefazione Gli Autori Ringraziamenti dell Editore La storia del C. Capitolo 1 Computer 1. Capitolo 2 Sistemi operativi 21 XVII XXIX

Lezione 8 Struct e qsort

Alberi. Strutture dati: Alberi. Alberi: Alcuni concetti. Alberi: definizione ricorsiva. Alberi: Una prima realizzazione. Alberi: prima Realizzazione

Alberi ed Alberi Binari

Introduzione alla programmazione

5. Quinta esercitazione autoguidata: liste semplici

Strutture dati. Le liste

La Struttura Dati Pila

Compitino di Laboratorio di Informatica CdL in Matematica 13/11/2007 Teoria Compito A

Funzioni, Stack e Visibilità delle Variabili in C

Università di Roma Tor Vergata L12-1

Informatica 1. Prova di recupero 21 Settembre 2001

Studio degli algoritmi

Algoritmi e strutture dati

La classe std::vector della Standard Template Library del C++

Corso di Fondamenti di Informatica Il sistema dei tipi in C++

Argomenti Avanzati.! I puntatori! Stack! Visibilità delle Variabili

Allocazione dinamica

Le basi del linguaggio Java

COMPLESSITÀ COMPUTAZIONALE DEGLI ALGORITMI

Cognome e Nome : Corso e Anno di Immatricolazione: Modalità di Laboratorio (Progetto/Prova) :

Alberi e alberi binari I Un albero è un caso particolare di grafo

Unità Didattica 4 Linguaggio C. Vettori. Puntatori. Funzioni: passaggio di parametri per indirizzo.

Esercizi riassuntivi (Fondamenti di Informatica 2 Walter Didimo) Soluzioni

L Allocazione Dinamica della Memoria

Introduzione al linguaggio C Puntatori

Quicksort e qsort() Alessio Orlandi. 28 marzo 2010

Le precondizioni e postcondizioni aggiunte al prototipo, consentono di fornire una specificazione precisa della funzione

Strutture dati dinamiche in C (II)

Costanti e Variabili

CORSO DI PROGRAMMAZIONE

Alberi binari e alberi binari di ricerca

Laboratorio di Algoritmi e Strutture Dati. Code con Priorità

Fondamenti di informatica, Sez. Ing. Informatica, Ing. Gestionale, Ing. Ambientale II prova in itinere, 29 Gennaio 2009

Espressioni aritmetiche

Laboratorio di Architettura lezione 5. Massimo Marchiori W3C/MIT/UNIVE

Esercizio 1: funzione con valore di ritorno di tipo puntatore

UNITÀ DIDATTICA 5 LA RETTA

Puntatori in C. Puntatori. Variabili tradizionali Esempio: int a = 5; Proprietà della variabile a: nome: a

dott. Sabrina Senatore

Errori frequenti Cicli iterativi Array. Cicli e array. Laboratorio di Programmazione I. Corso di Laurea in Informatica A.A.

Tipi di dato, Alessandra Giordani Lunedì 7 maggio 2011

Hash Table. Hash Table

Utilizza i tipi di dati comuni a tutto il framework.net Accesso nativo ai tipi.net (C# è nato con.net) Concetti fondamentali:

Linguaggio C: PUNTATORI

Linguaggi, Traduttori e le Basi della Programmazione

Array. Parte 7. Domenico Daniele Bloisi. Corso di Fondamenti di Informatica Ingegneria delle Comunicazioni BCOR Ingegneria Elettronica BELR

La struttura dati LISTA

Transcript:

Tipi di dato e strutture dati 1. Tipi di dato Pressoché tutti i linguaggi di programmazione tipati consentono all utente di definire nuovi tipi. Il C++ fornisce quattro modi per farlo: typedef, enum, struct, class. D altra parte i tipi predefiniti non sono soltanto degli identificatori riferiti a costanti o variabili i cui valori appartengano ad un determinato insieme (in C++ true e false se il tipo è bool, gli interi se il tipo è int, ecc.), ma anche una certa collezione di operazioni e di predicati (funzioni a valori booleani). In generale un tipo di dato è un modello matematico, cioè una struttura algebrica, consistente di un insieme di valori e di una collezione di operazioni su tali valori, che nel caso dei tipi definiti dall utente devono essere programmate. Una rappresentazione diagrammatica (incompleta) dei tipi int e bool è la seguente: + 0, 1, 2,, n, Interi not true, false Booleani Se si parla di modello matematico è perché si intende fare astrazione dalla concreta rappresentazione dei valori e quindi anche dagli algoritmi che realizzano le varie operazioni su di essi. Una descrizione astratta di una collezione di tipi di dato (ciascuno dei quali viene detto Abstract Data Type, o ADT) si suddivide in una specifica sintattica, in cui sono elencati tipi ed operatori, specificandone tipi e numero dei parametri ed il tipo dei valori, ed in una specifica semantica, con cui si descrive assiomaticamente o con pre e post condizioni, il significato degli operatori. Esempio Supponiamo di voler definire il tipo dei numeri razionali. La parte sintattica dell ADT potrebbe avere la forma: NewRatio: void Ratio Assign: Int, Int Ratio Sum: Ratio, Ratio Ratio Invert: Ratio Ratio LessOrEqual: Ratio, Ratio Bool dove ad esempio Assign: Int, Int Ratio indica che la funzione ha due parametri interi e ritorna un razionale, mentre NewRatio: void Ratio indica che NewRatio non ha alcun parametro, e ritorna un razionale.

Una possibile specifica semantica è quella equazionale, costituita cioè da equazioni della forma: LessOrEqual(Assign(a, b), Assign(c, d)) = (ad bc) Più maneggevole è una specifica basata su pre e post condizioni, come ad esempio: Assign(a, b) = r Pre: b > 0 Post: ritorna r = a/b Data una collezione di ADT, è possibile descrivere algoritmi che utilizzino questi tipi di dato. Il vantaggio di tale scrittura è che non dipende dalla realizzazione concreta dei tipi di dato utilizzati, che può essere quella di una libreria costruita da altri, ovvero essere ridefiniti (ad esempio per ragioni di maggior efficienza) senza dover riscrivere il codice degli algoritmi che li utilizzano. 2. Strutture dati Una volta fissata la specifica di un tipo occorre procedere alla sua realizzazione attraverso la definizione di strutture dati e di opportuni algoritmi per la loro manipolazione. In generale le strutture dati sono metodi di rappresentazione in memoria dei valori di un tipo. Poiché le rappresentazioni, in ultima analisi, non sono che sequenze di bit, è fondamentale definire delle regole per interpretarle, e quindi accedervi in lettura e per modificarle. Anche una semplice variabile di tipo intero corrisponde ad una struttura dati: di solito una parola macchina di due o quattro byte organizzati in modo tale che la metà dei valori rappresentabili sia interpretata come valori positivi e l altra metà come negativi (complementazione a 2). Tuttavia si parla di strutture dati, o anche di strutture informative, per riferirsi alla rappresentazione di collezioni di dati anziché a singoli valori di tipo di base. Gli esempi più elementari sono vettori e record. Un vettore è una n-pla di valori di uno stesso tipo T. Supposto che la rappresentazione di un valore di tipo T occupi k byte, un vettore di dimensione n si rappresenta con una sequenza di n k byte a partire da un dato indirizzo b, detto base del vettore. Allora l accesso al vettore avviene sfruttando il sistema di indirizzamento della memoria centrale: per trovare v[i], ossia l i-esima componente del vettore v basta infatti calcolare l indirizzo b + i k, ciò che spiega come mai in C++ il primo elemento di un vettore sia acceduto con v[0]. base = indirizzo del primo elemento V[i] = valore all indirizzo base + i dim(valore) In C++, come già avviene in C, un vettore (array) non è che una costante di tipo puntatore, vale a dire una variabile il cui valore (inalterabile) è l indirizzo di base del vettore: possiamo cioè dire che il tipo di v in int v[10]; V a f k z q d i w sia int*, ed infatti se dichiariamo int* p, l assegnazione p = v è lecita e sensata. L informazione int* è dunque utilizzata dal sistema per il calcolo della dimensione del singolo elemento, onde accedere correttamente ai valori memorizzati nel vettore. Tutto ciò è ancor più

trasparente quando si procede alla allocazione dinamica di un vettore. La dichiarazione ed inizializzazione: int* w = new int[10]; causa infatti un allocazione consecutiva di 10 interi, e l assegnazione a w dell indirizzo di base coincidente con quello del primo elemento. In C tutto ciò corrisponde all istruzione: int* w = (int*) malloc (sizeof(int) * 10); dove la funzione di libreria sizeof calcola la dimensione in byte di un valore in funzione del tipo; malloc alloca il numero di byte richiesto e ritorna un puntatore generico al primo byte di tale blocco; (int*) è un casting, o ridefinizione forzata del tipo, necessaria perché quest ultima informazione va perduta in quanto malloc prende in ingesso un semplice intero. Con new, che invece dipende da un tipo o comunque da un espressione di tipo, il casting non è più necessario. Le differenze tra le dichiarazioni di v e w sono che mentre v è una costante, w è una variabile e dunque può essere assegnata ad altro valore; inoltre la dimensione di v deve essere una costante mentre nel caso di w la costante 10 può essere rimpiazzata con una variabile il cui valore dipenderà dai dati in ingresso e dal momento in cui l istruzione viene eseguita. Naturalmente, come per tutte le allocazioni in memoria dinamica, la deallocazione del vettore cui punta w non è mai automatica, ma deve essere programmata mediante l istruzione: delete [] w; Che le componenti di un vettore debbano avere tutte lo stesso tipo costituisce un limite che si può superare utilizzando i record. Un record è infatti una k-pla di valori (ma con k di solito piuttosto piccolo) v, K 1,v di tipo k T 1, K,Tk rispettivamente. Questa eterogeneità di tipo non rende possibile l accesso basato su indici, per cui si ricorre a nomi o etichette di campo. La sintassi C/C++ per i record è: struct <nome record> { <tipo 1 > <etichetta campo 1 >; <tipo k > <etichetta campo k >; ; Il nome record è in realtà un tipo che viene così definito; ma il C++, come già il C, richiede che sia sempre preceduto dalla parola riservata struct: per evitare questo inconveniente basta premettere typedef e far seguire la definizione della struttura dal nome del tipo: typedef struct <nome record> { <tipo 1 > <etichetta campo 1 >; <tipo k > <etichetta campo k >; <nome tipo>; L accesso ai campi di un record sia in lettura che in scrittura avviene utilizzando un selettore rappresentato dal punto seguito dall etichetta del campo (ed eventualmente da altri operatori d accesso).

Esempi typedef struct ratio { int num; // numeratore int den; // denominatore > 0 Ratio; In questo caso, essendo i due campi dello stesso tipo, avremmo potuto scrivere: int num, den; Nel caso che segue, invece, i tipi dei campi sono differenti: typedef struct polynomial { int degree; // grado 0 double* coeff; // vettore dei coefficienti Polynomial; Si osservi come il tipo del vettore sia un puntatore (a double, se si intende avere coefficienti razionali rappresentati in virgola mobile ed in doppia precisione) e non un array, poiché si intende che la dimensione del vettore che ospita i coefficienti non sia fissata, ma dipenda dal grado, essendo pari a degree + 1. Ciò comporta che, per usare una variabile di tipo Polynomial, occorrerà inizializzarne i campi allocando il vettore dei coefficienti; una soluzione consiste nel creare una funzione con questo compito, ad esempio: Polynomial NewPolynomial (int deg) // Pre : deg 0 // Post: ritorna il polinomio x deg { Polynomial p; p.degree = deg; p.coeff = new double[deg + 1]; for (int i = 0; i < deg; i++) p.coeff[i] = 0; p.coeff[deg] = 1; return p; Quindi si potrà procedere ad una definizione ed inizializzazione come segue: Polynomial p = NewPolynomial(n); Sarà inoltre opportuno definire una funzione per la corretta deallocazione di un polinomio: infatti se la variabile p è una variabile locale (automatica), la sua rimozione dalla pila di sistema non comporta la deallocazione del vettore dei coefficienti; conviene dunque fornire una funzione il cui unico scopo sia quello di liberare la memoria da allocazioni non più utilizzate: void DeletePolynomial (Polynomial p) { delete [] p.coeff;

Tra la specifica di un tipo e la sua realizzazione mediante opportune strutture dati e funzioni deve evidentemente sussistere una relazione di adeguatezza o correttezza, che si può esprimere con il concetto algebrico di omomorfismo: se indichiamo con E la mappa che associa ai valori astratti le rispettive rappresentazioni concrete, allora per ogni funzione astratta f si chiede che: E( f ( v1, K, vn )) = E( f )( E( v1 ), K, E( vn )) dove E ( f ) indica la funzione che realizza (implementa) f. Per garantire la correttezza occorre mantenere vere certe proprietà della rappresentazione per tutta la durata della sua vita nella memoria dell elaboratore: ad esempio r.den > 0 nel caso dei razionali, oppure che la dimensione del vettore dei coefficienti sia pari al grado + 1 nel caso dei polinomi. L insieme delle caratteristiche di una struttura dati che si deve mantenere dopo successive modifiche prende il nome di invariante di struttura. 3. Strutture di puntatori Vettori e record non sono sufficienti per realizzare strutture dati complesse, se non attraverso complicate e rigide codifiche. Il C++ fa invece largo uso dei puntatori per la loro realizzazione, ciò di cui abbiamo già visto un esempio nel caso dei polinomi. Consideriamo ora il problema della rappresentazione di collezioni, cioè di insiemi o multinsiemi di valori. Definiamo innanzitutto un ADT degli insiemi (cosiddetti dinamici, poiché un insieme non è determinato dai suoi valori, ma dall identità di un contenitore): Tipi: Element, Set Operatori NewSet: void Set IsEmpty: Set Bool In: Element, Set Bool Insert: Element, Set Set Delete: Element, Set Set La semantica è definita: NewSet() = IsEmpty(s) = b In(e, s) = b Insert (e, s) = s Delete(e, s) = s Post: b = true sse s = Post: b = true sse e s Post: s = s {e Post: s = s {e Una soluzione ovvia consiste nell utilizzare un vettore parzialmente riempito: v dim proxlibero array

La cui definizione in C++ prende la forma: struct VettRec { int dim, proxlibero; Element *array; ; typedef VettRec* Set; L idea è che il vettore si considera riempito nell intervallo 0..proxlibero 1, onde un eventuale nuovo inserimento utilizzerà array[proxlibero] e comporterà l incremento di proxlibero. Se i valori di tipo Element non sono ordinati o comunque il vettore non è mantenuto ordinato, allora i tempi di accesso in ricerca sono lineari nel caso peggiore: bool In (Element e, Set s) { int i while (i < s.proxlibero && s.array[i]!= e) i++; return i < s.proxlibero; Se invece i valori sono ordinati allora la ricerca è O(log n): bool In (Element e, Set s) { int i = 0, j = s.proxlibero 1; bool trovato = false while (i <= j &&!trovato) { int m = (i + j)/2; if (v[m] == e) trovato = true; else if (v[m] < e) i = m + 1; else j = m 1; return trovato; Tuttavia i tempi di inserimento e cancellazione in un vettore ordinato restano lineari; in particolare è necessario scalare gli elementi che seguono il posto in cui e verrà inserito o eliminato: void Insert (Element e, Set& s) // Pre: s.proxlibero < s.dim oppure e s { int i, j; for (i = 0; i < s.proxlibero && v[i] < e; i++); if (i == s.proxlbero v[i]!= e) { for (j = s.proxlibero; j > i; j--) v[j] = v[j 1]; v[i] = e; s.proxlibero++; // altrimenti v[i] == e: nessun inserimento Pur senza migliorare rispetto a questi problemi, una struttura di puntatori detta lista consente quanto meno di evitare di scalare gli elementi nel caso di inserimento ordinato, e soprattutto di evitare la

restrizione espressa nella precondizione, vale a dire il limite sulla dimensione del vettore (e quindi sulla cardinalità dell insieme). Essenzialmente una lista è una catena di record, ciascuno formato da due campi, uno per l elemento, e l altro per un puntatore al record successivo: l 2 5 9 1 La sua definizione pone tuttavia un problema: nel definire il tipo della lista occorre usare la stessa nozione di lista: infatti una lista realizzata attraverso record si può pensare come una coppia <elemento, lista degli elementi successivi>: info next Non è tuttavia possibile definire un record che contenga un campo il cui valore sia un record dello stesso tipo, perché la dimensione di un valore di questo genere sarebbe infinita. Il problema è invece risolto se il campo che corrisponde alla lista degli elementi successivi è un puntatore, la cui dimensione è addirittura costante (e piuttosto piccola), visto che dipende dall architettura della macchina. In definitiva la definizione in C++ risulta: struct Nodo { Element info; Lista next; ; typedef Nodo* Lista; L ultimo elemento della lista sarà un record il cui campo next non punta ad alcun altro elemento: si utilizza allora una costante il cui valore sia distinto da qualunque indirizzo di memoria: in C/C++ NULL. Utilizzeremo questa stessa costante per indicare la lista vuota. L operazione fondamentale sulle liste è l inserimento in testa, ossia come primo elemento. La funzione Cons effettua l allocazione del nuovo elemento, lo aggiunge davanti alla lista L e ritorna un puntatore alla nuova lista così ottenuta: Lista Cons (Element e, Lista L) { Lista nl = new Nodo; nl->info = e; nl->next = L; return nl; Si osservi la notazione nl->info il cui significato è (*nl).info. Questa funzione ha due inverse, che sono in sostanza le due proiezioni: Element Head (Lista L) // Pre: L non è vuota

{ return L->info; Lista& Tail (Lista L) // Pre: L non è vuota { return L->next; La funzione Tail ritorna un riferimento perché è utile poterla utilizzare come lato sinistro in un assegnazione. La funzione Cons è utilizzata per gli inserimenti in qualunque posizione, come mostra la seguente funzione: void Inserimento (Element e, int i, Lista& L) // Pre: 1 i lunghezza(l) + 1 // Post: e è inserito in L come i-esimo el. { if (i == 1) L = Cons(e, L); else // 1 < i lunghezza(l) + 1 { int j = 1; Lista p = L; while (j < i-1 && p!= NULL) // inv. p punta al j-esimo el. di L { j++; p = Tail(p); if (p!= NULL) // allora j == i-1, Tail(p) = Cons(e, Tail(p)); La stessa funzione si può definire in modo ricorsivo, ottenendo un testo più compatto e di pari efficienza, se si prescinde dall uso della pila di sistema: Lista InsRic (Element e, int i, Lista L) { if (i == 1) return Cons(e, L); else { Tail(L) = InsRic (e, i-1, Tail(L)); return L; Trovare il predecessore di un elemento in una lista comporta una scansione della parte della lista che lo precede. Una realizzazione più efficace utilizza anche un puntatore al predecessore: 1 12 7 3 L In questa realizzazione, detta lista doppia circolare, non solo è fatta di record con puntatori al successivo ed al predecessore, ma ha un elemento fittizio cui L punta detto sentinella, il cui ruolo è sia quello di marcare l inizio e la fine della lista, sia di rendere più semplici le operazioni di inserimento e cancellazione. Ad esempio per inserire un elemento cui punta q dopo quello cui punta p le operazioni da compiere sono:

temp = p->next; p->next = q; q->pred = p; temp->pred = q; q->next = temp; Allora, se l inserimento avviene in testa, anziché in un diverso punto della lista, basta che p punti alla sentinella, e non occorre trattare questo caso a parte, come avviene in Inserimento visto sopra.