TIPO DI DATO ASTRATTO Tipo di dato astratto Esempio: Vogliamo rappresentare un sistema di prenotazione di esami universitari. Dati immagazzinati: prenotazioni esami Operazioni supportate: • prenota(esame, data, studente) • cancella(esame,studente) • modifica(esame,vecchiaData,nuovaData,studente) Condizioni di errore: • Prenota un esame inesistente • Prenota un esame per una data inesistente • Cancella una prenotazione inesistente • Modifica una prenotazione inesistente Un tipo astratto di dato è uno strumento che si utilizza per esprimere i dati, in maniera indipendente dal linguaggio di programmazione Tipo di dato astratto Nella definizione di un programma mirato alla risoluzione di un determinato problema, la scelta delle strutture dati è di fondamentale importanza per un’efficiente risoluzione, almeno tanto quanto la scelta dell'algoritmo risolutore. Molto spesso, anzi, l'algoritmo in grado di risolvere il problema stesso dipende strettamente dalle strutture dati adottate. Ciò implica che i tempi necessari per la risoluzione di un problema (ossia la complessità computazionale dell'algoritmo) siano strettamente legati alla struttura dati adottata (vedi problema parentesi annidate e gestione delle chiamate a funzione). Algoritmi + Strutture Dati = Programmi Usando i tipi di dato visti fino ad ora, in determinati contesti ci si può imbattere in certi limiti, quali l’occupazione di memoria (dimensione fisica e logica dei vettori), velocità di esecuzione (gestire cancellazione o inserimento in un insieme ordinato di elementi implementati tramite vettore) o la chiarezza della soluzione (come mantengo le informazioni relative a un albero genealogico e come implemento l’accesso ai dati). Tipo di dato astratto Progettare un tipo di dato astratto (ADT) significa: – in astratto, definirne le proprietà e le operazioni ammissibili – in pratica, definirne la struttura dati e scrivere le funzioni che realizzano le operazioni previste Punti chiave: • L’oggetto dovrebbe essere manipolabile solo tramite le operazioni previste per esso • Nessuno dovrebbe poter accedere alla struttura interna dell’oggetto in modo diretto E’ uno strumento che si utilizza per esprimere i dati, in maniera indipendente dal linguaggio di programmazione. Tipo di dato astratto E’ un oggetto matematico composto dalle componenti: a) un insieme di valori detto il DOMINIO DEL TIPO b) un insieme di OPERAZIONI che vengono applicate ai valori del dominio oppure che hanno come risultato valori del dominio Tutte le componenti di un tipo astratto, sono indipendenti dalla rappresentazione e dall’uso del tipo stesso nei linguaggi di programmazione. Distinguiamo tra tipi astratti e tipi concreti, intendendo con quest’ultimo i tipi di dato resi disponibili direttamente nel linguaggio C, come ad esempio il tipo array, il tipo struttura, i puntatori, i file eccetera. Tipo di dato astratto • Possibilità di creare astrazioni che corrispondano a entità esistenti nel mondo “non informatico” • Incapsulamento e riuso dell’astrazione • Progettazione per assemblaggio di componenti e astrazioni • Separazione fra interfaccia e implementazione • Possibilità di modificare l’implementazione senza alterare il resto Organizzazione tipica: • Un file header (.h) che contiene la dichiarazione del tipo e le dichiarazioni delle operazioni (funzioni) • Un file sorgente (.c) che include il proprio header (per importare la definizione di tipo) e contiene le definizioni delle operazioni ADT generici: la Pila Tipo Pila Dati: una sequenza S di n elementi Operazioni: isEmpty() -> result restituisce true se S è vuota, false altrimenti push(elem e) aggiunge e come elemento affiorante di S pop() -> elem toglie da S l’elemento affiorante e lo restituisce top() -> elem restituisce l’elemento affiorante di S (senza modificare S) ADT generici: la Coda Tipo Coda Dati: una sequenza S di n elementi Operazioni: isEmpty() -> result restituisce true se S è vuota, false altrimenti enqueue(elem e) aggiunge e come ultimo elemento di S dequeue() -> elem toglie da S il primo elemento e lo restituisce first() -> elem restituisce il primo elemento di S (senza modificare S) ADT generici: il Dizionario Tipo Dizionario Dati: una insieme S di coppie (elem, chiave) Operazioni: insert(elem e, chiave k) aggiunge a S una nuova coppia (e,k) delete(chiave k) cancella da S la coppia con chiave k search(chiave k) -> elem se la chiave k è presente in S restituisce l’elemento e ad essa associato, null altrimenti Tecniche di rappresentazione dei dati Rappresentazioni indicizzate: – I dati sono contenuti in array Rappresentazioni collegate: – I dati sono contenuti in record collegati fra loro mediante puntatori Pro e contro Rappresentazioni indicizzate: – Pro: accesso diretto ai dati mediante indici – Contro: dimensione fissa (riallocazione array richiede tempo lineare) Rappresentazioni collegate: – Pro: dimensione variabile (aggiunta e rimozione record in tempo costante) – Contro: accesso sequenziale ai dati Tipo di dato astratto Lista Come ogni tipo di dato astratto, anche la lista è definita in termini di: – Dominio dei suoi elementi (dominio-base): qualsiasi – Operazioni di costruzione sul tipo lista – Operazioni di selezione sul tipo lista Il vantaggio del loro utilizzo è la semplicità della gestione. Di contro le prestazioni delle operazioni necessarie a gestire tali strutture dati non sono eccelse. Se n è il numero di informazioni contenute, la complessità computazionale (ossia i tempi di esecuzione) degli algoritmi necessari alla loro gestione è direttamente proporzionale al numero di informazioni da gestire. Esistono altre strutture dati, come ad esempio alberi, che possono offrire prestazioni superiori (ad esempio proporzionale a log n), ma a costo di una più difficile gestione. Operazioni su Lista Ad esempio: head: list -> D (selettore “testa”) tail: list -> list (selettore “coda”) cons: D x list -> list (costruttore) empty: list -> boolean (test di lista vuota) Esempi: head( [ 6,7,11,21,3,6 ] ) -> 6 tail( [ 6,7,11,21,3,6 ] ) -> [ 7,11,21,3,6 ] cons( 6, [ 7,11,21,3,6 ] ) -> [ 6,7,11,21,3,6 ] empty( [ 6,7,11,21,3,6 ] ) -> false empty( [ ] ) -> true Ma tante altre operazioni potrebbero essere implementate: inserimento in coda, inserimento indicizzato, rimozione dalla coda, rimozione indicizzata, ... Realizzazione dell’ADT Lista Pochissimi linguaggi forniscono il tipo lista fra quelli predefiniti (LISP, Prolog). Per tutti gli altri linguaggi, l’ADT lista si costruisce a partire da altre strutture dati. Possiamo implementare concretamente l’ADT Lista usando in alternativa: • Rappresentazione indicizzata (vettori, memoria statica) • Rappresentazione collegata (puntatori e struct, memoria dinamica) Implementazione LISTE LINEARI mediante vettori Sono necessari • un vettore per memorizzare gli elementi della lista uno dopo l’altro (rappresentazione sequenziale) • una variabile primo per memorizzare l’indice del vettore in cui è inserito il primo elemento • una variabile lunghezza per indicare da quanti elementi è composta la lista Esempio: Lista [‘a’, ‘b’, ‘c’, ‘a’, ‘f ’] primo lunghezza 0 5 Le componenti del vettore con indice successivo a (primo+lunghezza) non sono significative 0 1 2 3 4 5 6 7 8 9 a b c a f Inconvenienti La dimensione (massima) della lista rappresentata mediante vettore è fissata a priori. Ma questo non corrisponde pienamente alla definizione di LISTA Le seguenti operazioni sono dispendiose: • Inserimento di un elemento in testa o interno alla lista • Cancellazione di un elemento in testa o interno In entrambi i casi è necessario spostare tutti o parte degli elementi della struttura dati Esempio: eliminazione dalla testa Tail( [‘a’, ‘b’, ‘c’, ‘a’, ‘f ’] ) primo 0 lunghezza 5 0 1 2 3 4 5 6 7 8 9 a b c a f primo 0 lunghezza 4 0 1 2 3 4 5 6 7 8 9 b c a f Implementazione LISTE LINEARI mediante vettori Altre possibili implementazioni: • Meomorizzo due indici, (primo, ultimo) che si incrementano sempre (spreco memoria) e prima o poi esaurisco lo spazio disponibile; se l’applicazione lo permette, posso regolarmente riposizionare i due indici a 1 • Tengo due indici: implementazione circolare. In momenti differenti la coda occupa posizioni differenti dell’anello; problema: dimensione massima dell’anello (overflow) Esempio: Lista [‘a’, ‘b’, ‘c’, ‘a’, ‘f ’] primo 0 ultimo 4 0 1 2 3 4 5 6 7 8 9 a b c a f Esempio: eliminazione dalla testa Tail( [‘a’, ‘b’, ‘c’, ‘a’, ‘f ’] ) primo 0 ultimo 4 0 1 2 3 4 5 6 7 8 9 a b c a f primo 1 ultimo 4 0 1 2 3 4 5 6 7 8 9 a b c a f Implementazione LISTE LINEARI mediante puntatori e struct Ciascun nodo della lista è una struttura di due campi: – il valore dell’elemento – un puntatore al nodo successivo della lista (NULL nel caso dell'ultimo elemento) value testa next value l next value i value t next s next value a Lista semplice next Implementazione LISTE LINEARI mediante puntatori e struct Ciascun nodo della lista è una struttura di due campi: – il valore dell’elemento – un puntatore al nodo successivo della lista (NULL nel caso dell'ultimo elemento) – un puntatore al nodo precedente della lista (NULL nel caso del primo elemento) prev value l testa next s i t Lista doppiamente collegata Implementazione LISTE LINEARI mediante puntatori e struct Ciascun nodo della lista è una struttura di due campi: – il valore dell’elemento – un puntatore al nodo successivo della lista (primo nel caso dell'ultimo elemento) – un puntatore al nodo precedente della lista (ultimo nel caso del primo elemento) prev testa value l next s i t Lista circolare doppiamente collegata Confronto (sd) lista concatenata e (sd) array La lista concatenata ha una natura essenzialmente sequenziale e quindi per accedere ad un elemento è necessario scorrere sequenzialmente la lista; gli elementi dell'array sono invece accessibili direttamente specificando un indice (provare a scrivere la ricerca binaria in una lista ordinata!). Per contro, la lista concatenata risulta più flessibile nella soluzione di molti problemi, perché non è necessario specificarne a priori la lunghezza e finché c’è memoria libera è possibile allocare nuovi elementi. La lista concatenata presenta un certo overhead di occupazione di memoria dovuto alla necessità di memorizzare oltre ai campi informazione anche i campi puntatore. Un ultimo vantaggio della lista concatenata dipende dal fatto che alcune operazioni (come rimuovere o inserire un elemento) necessitano solo lo spostamento di alcuni puntatori, mentre in un array le stesse operazioni necessitano costosi spostamenti di tutti gli elementi che stanno a destra del punto in cui è avvenuto l’eliminazione o l’inserimento dell'elemento. Occorrerà sempre tenere ben presenti questi elementi al fine di utilizzare la giusta struttura dati, tenendo conto della natura del problema e degli obiettivi prioritari (efficienza, occupazione di memoria, flessibilità . . .). Uso delle strutture dati semplici Anche realtà più complesse possono essere rappresentate usando strutture dati più semplici come quelle studiate (liste, vettori, matrici, …). Esempio: se volessimo rappresentare un certo numero di informazioni fra cui esiste una relazione «a grafo» potremmo scegliere una struttura dati concreta come appare nel disegno seguente: Uso delle strutture dati semplici Oppure, se il grafo fosse non ordinato, potremmo scegliere una delle rappresentazioni concrete seguenti: Altre strutture dati astratte Analoghe considerazioni valgono per altri tipi di dato astratto generici, quali: • PILA (stack) • CODA • ALBERO BINARIO • GRAFO • …