tipo di dato astratto - programmazione A

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
• …