1 Record, tabelle, relazioni F. Bombi 1 novembre 2001
2 Record, tabelle, relazioni Sino ad ora abbiamo studiato strutture di dati, quali array e liste che ci consentono di gestire collezioni di dati dello stesso tipo È frequente la necessità di gestire dati di natura diversa in un unico oggetto. A secondo del contesto in cui ci si muove si parla di: – File di Record – Tabelle – Relazioni L’impiego di array di liste, di array di array o di liste di array non risolve il problema
3Record file Il concetto di record nasce con riferimento alla registrazione su supporti fisici, quali nastri o dischi magnetici, di file contenenti insiemi di dati file piatto,record La struttura più semplice che si può adottare, detta file piatto, si compone di una sequenze di record di uguale struttura campi Ogni record è a sua volta composto da campi secondo una struttura predeterminata Ogni campo contiene un‘informazione elementare (un dato numerico, una stringa di caratteri, una data, ecc.) in un formato noto (binario, ASCII) tracciato record L’organizzazione dei record di un file è descritta in un documento detto tracciato record Per semplicità assumiamo che ciascun campo occupi un numero di byte fisso e noto a priori
4 Esempio All’anagrafe di un comune vogliamo conservare i dati anagrafici dei cittadini Utilizziamo un file composto da record Per ogni cittadino prevediamo un record composto dai seguenti campi: – Cognomestringa 20 caratteri – Nomestringa20 caratteri – Codice fiscalestringa16 caratteri – Data di nascitastringa6 caratteri – Comune di nascitaintero4 byte – Sessocarattere1 carattere – Stato civilecarattere1 carattere – Ecc. ecc.
5 Tabelle tabellarighecolonne Possiamo organizzare la stessa informazione in una tabella composta da righe e colonne. Utilizziamo una riga per ogni record e una colonna per ogni campo CognomeNomeCDFData nasc.SessoComuneStato civ.
6 Relazioni ennupla n-tupletuple In termini più matematici possiamo pensare ogni record (o riga della tabella) come una ennupla (in inglese n-tuple o semplicemente tuple) di valori ciascuno appartenente ad un differente insieme (gli insiemi dei cognomi possibili, dei codici fiscali, ecc.) n attributi Gli n elementi di una ennupla vengono detti attributi relazione L’intero insieme di dati è allora una relazione definita come un sottoinsieme del prodotto cartesiano degli insiemi di tutti i possibili valori di ciascun campo
7 Esempio Supponiamo di avere due insiemi A = {a, b, c, d} B = {1, 2, 3} C = A x B Il prodotto cartesiano C = A x B dei due insiemi è l’insieme di tutte le possibili coppie di elementi del primo e del secondo insiemeC={(a,1),(a,2),(a,3),(b,1),…,(d,2),(d,3)} L’insieme D = {(a,2),(b,1),(c,1),(c,3)} relazione AB costituisce una relazione (binaria) su A e B
8 In prima approssimazione… filetabellarelazione recordrigatupla campocolonnaattributo
9 Chiavi e attributi Per definizione, in una relazione (essendo un insieme) non ci possono essere due n-uple uguali. Per coerenza assumiamo che in un file (o in una tabella) non ci siano due record (due righe uguali) chiave In questa trattazione molto semplificata immaginiamo in fine che ciascun record sia univocamente individuato dal valore di uno dei campi detto chiave (o chiave primaria) chiave Dato quindi il valore di una chiave possiamo cercare se esiste (o non esiste) un record individuato da quel valore della chiave L’operazione di ricerca fornisce il valore dei restanti attributi, la ricerca corrisponde quindi alla valutazione di una funzione pensando alla chiave come variabile indipendente e ai restanti attributi variabili dipendenti
10 In Java È facile pensare di utilizzare una classe per rappresentare un record, la classe avrà tanti membri quanti sono i campi del record In prima battuta la classe non richiede metodi e può essere costruita in modo che i membri siano tutti pubblici Tornando per un momento all’esempio dell’anagrafe potremmo usare la classe public class Record { public String cognome; public String nome; public String nome; public String codice fiscale; public String codice fiscale; ecc., ecc. ecc., ecc.} Un file potrà essere rappresentato come una lista o un array di record
11 Un po’ di generalità Coppia Nei prossimi esempi, per trattare il problema in modo un po’ più astratto, assumiamo di voler gestire record composti da soli due campi (che chiameremo Coppia ): chiave Comparable – Il primo campo è la chiave che dovrà essere un oggetto che realizza l’interfaccia Comparable attributo – Il secondo sarà ancora un oggetto che costituisce l’unico attributo presente array Lista Un file (tabella, relazione), sarà rappresentata di un array (quando sappiamo il numero di record presenti) oppure da una Lista (quando vogliamo poter aggiungere e togliere gli elementi a piacimento) attributo record Coppia Ennupla È pressoché immediato generalizzare questo schema quando si debbano trattare record con molti campi pensando all’oggetto attributo composto a sua volta da un record oppure sostituendo la classe Coppia con una classe Ennupla con il numero di campi necessari
12 Ricerca di un record file A questo punto disponiamo di quasi tutti gli strumenti necessari per gestire un file di record ciascuno composto da una coppia chiave-attributo in un array o in una lista Sappiamo: – Inserire o togliere un elemento – Ordinare il file – Eliminare i doppioni Vediamo ora come cercare un elemento dato il valore della chiave Coppia successo insuccesso Pensiamo di organizzare la ricerca mediante un metodo statico che riceva quale parametro l’array o la lista e la chiave da cercare (inserita in un oggetto di tipo Coppia ). Tutti gli algoritmi di ricerca hanno la peculiarità di avere due uscite (a stretto rigore non sarebbero strutturati) in quanto la ricerca può terminare con successo o con insuccesso
13 La classe Coppia public class Coppia implements Comparable { public Comparable chiave; public Object attributo; public Object attributo; public Coppia (Comparable c, Object a) public Coppia (Comparable c, Object a) { chiave = c; attributo = a; } { chiave = c; attributo = a; } public int compareTo (Object x) public int compareTo (Object x) { return chiave.compareTo(((Coppia)x).chiave); } { return chiave.compareTo(((Coppia)x).chiave); } public String toString () public String toString () { return chiave.toString() + ":" + attributo.toString();} { return chiave.toString() + ":" + attributo.toString();}} public Anche se non è necessario (dato che tutti i campi sono public ) è comodo che Comparable la classe realizzi l’inerfaccia Comparable (questo obbliga ad inserire un compareTo() metodo compareTo() ad accesso pubblico che confronta le chiavi. Il costruttore rende più immediato assegnare i valori ai campi al momento della costruzione di un nuovo oggetto. toString() Ogni classe deve avere un metodo toString() per poter stampare gli esemplari della classe
14 Ricerca in un array ifwhile Come già detto l’algoritmo base per cercare un elemento in un array pone un sottile problema di strutturazione in quanto richiede due uscite. Non può essere quindi pensato direttamente come composizione di if e while. Occorre ricorrere ad un’istruzione di salto incondizionato oppure ad uno switch break return Java, per risolvere situazione di questo tipo, mette a nostra disposizione l’istruzione break per uscire da un ciclo prima che sia terminato. In modo analogo possiamo uscire da un ciclo usando l’istruzione return (che termina anche il metodo) Stilisticamente l’uso di switch è in genere sconsigliato da qualche purista
15 Uso di break o di uno switch int i; for (i = 0; i < n; i++) if (x.compareTo(v[i]) == 0) if (x.compareTo(v[i]) == 0) break; break; if (i == n) // non trovato // non trovatoelse // trovato // trovato int i; boolean trovato = false; for (i = 0; !trovato && i < n; i++) if (x.compareTo(v[i]) == 0) if (x.compareTo(v[i]) == 0) trovato = true; trovato = true; if (trovato) // trovato // trovatoelse // non trovato // non trovato break switch Si vuole cercare x fra i primi n elementi del vettore v[]
16 Quanto tempo occorre per trovare un dato fra n? Abbiamo analizzato situazioni di questo tipo in altri casi per cui sappiamo subito dire che dobbiamo fare un numero di cicli pari a: – 1 – 1 nel caso migliore (l’elemento cercato è il primo) – n – n nel caso peggiore (l’elemento cercato è l’ultimo o non è presente) – (n+1)/2 – (n+1)/2 in media (l’elemento cercato è presente e occupa con la stessa probabilità tutti le posizioni possibili) Ad ogni ciclo dobbiamo fare due confronti, uno per verificare se l’elemento è presente e l’altro per verificare se abbiamo esaurito i dati
17 L’uso di una sentinella sentinella È possibile ridurre a metà il numero di confronti utilizzando una sentinella (utile anche in altre circostanze) v[] n Supponendo che il vettore v[] abbia almeno una posizione libera, inseriamo il valore da cercare al posto di indice n A questo punto siamo sicuri che l’elemento cercato compaia nel vettore almeno una volta, possiamo interrompere il ciclo di ricerca quando si trova il dato senza controllare se si è esaurito il vettore n Al termine del ciclo se l’elemento trovato occupa la posizione n la ricerca è fallita
18 int i; v[n] = x; for (i = 0; x.compareTo(v[i]) == 0; i++); if (i == n) // non trovato // non trovatoelse // trovato // trovato tempo taglia Due confronti Sentinella Anche usando la sentinella il comportamento asintotico rimane O(n) Cambia la costante di proporzionalità che lega n il tempo effettivo a n
19 Possiamo fare meglio? divide et impera Se non abbiamo altre informazioni non c’è modo di migliorare le cose. Se sappiamo che il vettore è ordinato possiamo utilizzare una strategia ricorsiva di tipo divide et impera che sfrutta il risultato di ogni confronto per ridurre le dimensioni del problema da risolvere
20 Ricerca per bisezione xv[] is Vogliamo cerca x nel vettore v[] fra gli elementi di indice compreso fra i e s i > s Se i > s l’elemento cercato non è presente insuccesso! (uno dei casi base) m = (i+s)/2v[m] == x Sia m = (i+s)/2 se v[m] == x successo! (l’altro caso base) (Passo ricorsivo) x < v[m]im-1 Se x < v[m] cerchiamo fra i e m-1 x > v[m]m+1s Se x > v[m] cerchiamo fra m+1 e s Anche se apparentemente ci sono due chiamate ricorsive ad ogni passo ne viene attivata una sola per cui è immediato utilizzare l’eliminazione della ricorsione in coda e realizzare l’algoritmo in forma iterativa
21 public static Comparable public static Comparable binaria (Comparable x, Comparable[] v, int n) binaria (Comparable x, Comparable[] v, int n) { int inf = 0; { int inf = 0; int sup = n - 1; int sup = n - 1; int meta; int meta; while (inf <= sup) while (inf <= sup) { meta = (inf + sup)/2; { meta = (inf + sup)/2; int confronto = x.compareTo(v[meta]); int confronto = x.compareTo(v[meta]); if ( confronto == 0) if ( confronto == 0) return v[meta]; return v[meta]; else if (confronto < 0) else if (confronto < 0) sup = meta - 1; sup = meta - 1; else else inf = meta + 1; inf = meta + 1; } return null; return null; } Questa realizzazione dell’algoritmo di ricerca per bisezione o binaria mostra come eliminare la ricorsione con un ciclo while() e gestire i due casi base con due salti (i due return )
22 Quanti confronti si fanno? Per semplificare l’analisi supponiamo che n = 2 k -1k = ln 2 (n+1) n = 2 k -1 e quindi k = ln 2 (n+1) k Consideriamo solo il caso peggiore e analizziamo il problema in funzione di k T(1) = 1 T(k) = 1 + T(k-1) È immediato dimostrare per induzione che la ricorrenza ha come soluzione T(k) = k e quindi T(n) = ln 2 (n+1) O(log n) Il comportamento asintotico è dunque O(log n)
23 Ricerca in una lista Gli algoritmi di ricerca per scansione con o senza sentinella possono essere facilmente riscritti per la ricerca di un elemento in una lista Con una lista non è invece possibile utilizzare l’algoritmo di bisezione Con una l’uso della sentinella è un po’ brigoso perché è necessario prima inserire l’elemento cercato nella lista e poi toglierlo, non disponendo di metodi di confronto fra iteratori (che per altro sarebbe facile costruire) non è poi immediato verificare se la ricerca termina alla fine della lista
24 public static Comparable scansione (Comparable x, Catena l) { for (IteraCatena q = new IteraCatena(l); { for (IteraCatena q = new IteraCatena(l); !q.allaFine(); q.prossimo()) !q.allaFine(); q.prossimo()) if (x.compareTo(q.valore()) == 0) if (x.compareTo(q.valore()) == 0) return (Comparable)q.valore(); return (Comparable)q.valore(); return null; return null; } public static Comparable sentinella (Comparable x, Catena l) { IteraCatena q = new IteraCatena(l); { IteraCatena q = new IteraCatena(l); q.vaiAllaFine(); q.vaiAllaFine(); IteraCatena p = new IteraCatena(q); IteraCatena p = new IteraCatena(q); q.inserisci(x); q.inserisci(x); q.vaiAllInizio(); q.vaiAllInizio(); while (x.compareTo(q.valore()) != 0) while (x.compareTo(q.valore()) != 0) q.prossimo(); q.prossimo(); Comparable tmp = (Comparable)q.valore(); Comparable tmp = (Comparable)q.valore(); q.prossimo(); q.prossimo(); if (q.allaFine()) if (q.allaFine()) tmp = null; tmp = null; p.togli(); p.togli(); return tmp; return tmp; } Con una lista il vantaggio derivante dall’uso di una sentinella è dubbio
25 Esempio di utilizzo ‘:’ Costruiamo ora una semplice applicazione che legge da un file (il nome è passato come argomento dalla riga di comando) i dati relativi ad un insieme di studenti, ciascun record è costituito dal numero di matricola e dal nome separati dal carattere ‘:’ Si legge poi dall’ingresso standard il nome di uno studente per cercare il corrispondente numero di matricola Notare che i dati vengono ordinati per poter utilizzare l’algoritmo di bisezione
26 public class CercaStudente { public static void main (String[] arg) throws IOException { BufferedReader fileDati=new BufferedReader(new FileReader(arg[0])); { BufferedReader fileDati=new BufferedReader(new FileReader(arg[0])); String str; String str; Coppia[] dati = new Coppia[100]; Coppia[] dati = new Coppia[100]; int n = 0; int n = 0; while((str = fileDati.readLine()) != null) while((str = fileDati.readLine()) != null) { StringTokenizer t = new StringTokenizer(str, ":"); { StringTokenizer t = new StringTokenizer(str, ":"); String matricola = t.nextToken(); String matricola = t.nextToken(); String nome = t.nextToken(); String nome = t.nextToken(); dati[n++] = new Coppia(nome, matricola); dati[n++] = new Coppia(nome, matricola); } Ordina.mergesort(dati, n); Ordina.mergesort(dati, n); for (int j = 0; j < n; j++) System.out.println(j + ": " + dati[j]); for (int j = 0; j < n; j++) System.out.println(j + ": " + dati[j]); BufferedReader in = BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); new BufferedReader(new InputStreamReader(System.in)); str = in.readLine(); str = in.readLine(); while (str.length() != 0) while (str.length() != 0) { Coppia x = new Coppia(str, null); { Coppia x = new Coppia(str, null); System.out.println(Cerca.scansione(x, dati, n)); System.out.println(Cerca.scansione(x, dati, n)); System.out.println(Cerca.sentinella(x, dati, n)); System.out.println(Cerca.sentinella(x, dati, n)); System.out.println(Cerca.binaria(x, dati, n)); System.out.println(Cerca.binaria(x, dati, n)); str = in.readLine(); str = in.readLine(); } }}
27 Attenzione Coppia chiave Gli oggetti di tipo Coppia sono confrontabili e il confronto avviene considerando solo il campo chiave Per effettuare la ricerca è necessario costruire un oggetto che contiene la chiave cercata (il valore dell’attributo è inessenziale) null I metodi di ricerca restituiscono o un riferimento null o un riferimento all’elemento che contiene la chiave cercata