appunti Java pag.154 11. Strutture dati Nella soluzione dei problemi sono già stati utilizzati diversi tipi di strutture dati ma fino ad ora non si è discusso in modo sistematico delle loro caratteristiche e specificità d'uso. Una prima definizione approssimata di struttura dati potrebbe essere la seguente: Una struttura dati è un "contenitore" di informazioni che si caratterizza per i suoi attributi "logici", quali Modalità di accesso, Dimensionamento, Organizzazione dei suoi componenti, o per gli attributi "fisici", quali i Tempi di accesso ad un componente e la Persistenza delle informazioni . Un array ha i seguenti attributi "logici": 1. Modalità di accesso sequenziale se per trovare una informazione lo si deve scorrere dalla prima componente fino a quella desiderata; indicizzato se si conosce la posizione del dato cercato e si usa l'indice per accedervi direttamente. 2. Dimensionamento statico di norma, infatti un array una volta dimensionato nel programma non può essere "fatto crescere". 3. Organizzazione dei componenti disordinata se questi vengono inseriti senza un particolare ordine; ordinata se gli Oggetti inseriti sono dotati di una particolare relazione d’ordine che viene mantenuta dalla struttura: ordine Alfabetico, ordine di Grandezza crescente, decrescente ecc.; in ogni caso un array è sempre aderente alla struttura della memoria RAM in quanto ogni locazione di questa ha un indirizzo numerico e l'array è memorizzato in celle adiacenti (sequenza) a cui si fa corrispondere un indice intero da 0..n. con componenti duplicati o non duplicati a seconda del programma che gestisce la struttura. Ma un array è dotato anche dei seguenti attributi "fisici": 4. Persistenza delle informazioni "breve" o "volatile" in quanto la memoria conserva i dati solo fin che il programma è in esecuzione o il computer è alimentato da corrente. Si dice che è una struttura dati Volatile (e non permanente come invece è il file memorizzato su supporto magnetico). 5. Tempi di accesso ad un componente "molto veloci" in quanto il suo supporto è la memoria RAM (ordine di grandezza dei nanosecondi 10-9 s.). In contrasto con i file di caratteri o byte che hanno per supporto un disco che sono "lenti" (ordine di grandezza dei millisecondi 10-3 s.). In sostanza i parametri per valutare una struttura dati potrebbero essere: appunti Java Accesso sequenziale indicizzato pag.155 Dimensionamento Organizzazione Persistenza volatile statico ordinata permanente dinamico disordinata con duplicati senza duplicati Tempi di accesso lento (10-3) veloce(10-9) Se si analizzano le strutture dati a noi note e si classificano usando gli attributi discussi si ottiene la seguente tabella comparativa: Accesso Array mono o sequenziale e pluridimensionale indicizzato File di caratteri o byte sequenziale e indicizzato Stream o Filtro di caratteri o byte sequenziale e indicizzato String o StringBuffer sequenziale e indicizzato Dimensionamento Organizzazione statico disordinata o ordinata con duplicati dinamico disordinata o ordinata con duplicati dinamico disordinata o ordinata con duplicati dinamico in java disordinata o statico Pascal o C ordinata con duplicati Persistenza volatile Tempo di acc. veloce permanente lento volatile lento o veloce volatile veloce 11.1 Strutture dati di uso comune E' pratica consolidata di chi progetta programmi quella di utilizzare tipi di strutture dati diverse a seconda del problema da risolvere in modo che queste siano aderenti alla specifica situazione. Si è quindi consolidata in informatica la formalizzazione di alcune strutture dati di uso frequente quali liste, pile, code insiemi, alberi, grafi e altre ancora. I linguaggi più moderni forniscono queste strutture dati, con le relative operazioni formalizzate, per sollevare di una parte di lavoro di codifica il programmatore di applicazioni. In Java le strutture dati di utilità sono contenute nel package java.util (in parte in java.io visto in precedenza) e ciascuna di esse è una classe che appartiene ad una ben strutturata gerarchia. Si studieranno in seguito alcune delle classi più importanti del package java.util. 11.2 Liste Una delle strutture dati di più largo uso in programmazione è la Lista che si potrebbe definire come un Abstract Data Type (Tipo di Dato Astratto) nel seguente modo: Una lista è una struttura dati sequenziale costituita da zero o più oggetti. La definizione ci informa che: Una lista può essere vuota; Se non è vuota ha un primo elemento (detto anche testa della lista) e un ultimo elemento; appunti Java pag.156 Ogni suo elemento, esclusi il primo e l'ultimo, è dotato di un predecessore e di un successore; Può essere virtualmente illimitata (su un computer la lista è forzatamente limitata dalle dimensioni della memoria) Si rappresenta, come una successione, nel seguente modo { a1, a2, ... , an} Tra le strutture dati studiate la String è una tipica Lista di caratteri, come pure un File di caratteri o di byte. Il file è stato rappresentato come una sequenza di dati nel seguente modo: ATT INI ...... EOF Analogamente al File si potrebbe immaginare la rappresentazione grafica di una lista come una struttura costituita da Nodi; tali nodi contengono Oggetti (le informazioni) collegati tra loro da "link o reference" mediante i quali è possibile passare da un nodo al suo successore (predecessore) come nella figura. iniz Object Object Object Object null Si nota che una lista deve essere sempre dotata di un "link" particolare che indica la sua "testa" (iniz) e deve essere dotata di un "segno" particolare (null) che ne indica la conclusione. A differenza del file questa struttura, memorizzata in memoria di lavoro, è volatile. Ogni Object deve essere dotati di uno o più link che indicano il suo successore (e/o predecessore); tale link sono rappresentati graficamente con una freccia. Una struttura dati di questo tipo (una classe in Java) di quali operazioni deve essere dotata ? Ragionando sull'Abstract Data Type Lista si dovrebbe disporre di : Costruttore/i Inserzione, Cancellazione e Ricerca: Aggiunta di un oggetto in testa Aggiunta di un oggetto in coda Aggiunta di un oggetto in una posizione qualsiasi Cancellazione di un oggetto noto Cancellazione di un oggetto di posizione nota Ricerca dell'esistenza di un oggetto noto Determinazione della posizione di un oggetto noto Concatenazione: Aggiungere una Lista in coda a una Lista data Altri metodi: Operatore per leggere l'oggetto di testa Operatore per leggere l'oggetto in coda Operatore per leggere un oggetto di posizione nota Operatore per stabilire se la lista è Vuota o meno appunti Java pag.157 Operatore per conoscerne la dimensione Operatore per sapere se durante la lettura si è giunti al termine della Lista (deve consentire lo scorrimento in sequenza dal primo all'ultimo elemento); Se si analizza la struttura di Lista e la si classifica usando gli attributi discussi si ottiene la seguente tabella comparativa: ListaSequenziale Accesso sequenziale Dimensionamento Organizzazione dinamico disordinata o ordinata con duplicati Persistenza volatile Tempo di acc. veloce La classe LinkedList del package java.util rappresenta pienamente tale ADT. Ecco di seguito i suoi metodi: Classe Costruttori Metodi LinkedList Identificatori: LinkedList(); Invocazione: LinkedList L =new LinkedList(); Effetti: Crea un oggetto Lista identificato da L vuoto. Identificatore void add(Object); void add(Index, Object); void addFirst(Object); boolean remove(Object); Object remove(Index); void clear(); boolean contains(Object); int indexOf(Object); boolean addAll(Collection); Object getFirst(); Object getLast(); Object get(Index); boolean isEmpty(); int size(); Iterator iterator(); ListIterator listIterator(): Invocazione: Pckage e Gerarchia Effetti:: java.util java.Object java.util.AbstractCollection java.util.LinkedList Si noti che i metodi iterator() e listIterator() sono metodi particolari che hanno la funzione di creare un Oggetto iteratore (più esattamente si tratta di una interfaccia) che serve per "scorrere" la lista. Un Iterator e un ListIterator differiscono solo lievemente. Il primo è dotato di due metodi e consente di scorrere la sequenza solo dalla testa alla fine, mentre il secondo possiede altri due metodi che consentono lo scorrimento a ritroso. appunti Java pag.158 Interface Iterator Come si ottiene un iterator esempio Metodi Identificatore Invocazione: Effetti:: Pckage e Gerarchia java.util Interface ListIterator Come si ottiene un ListIterator esempio Metodi Identificatore Invocazione: Effetti:: Pckage e Gerarchia java.util Usando un opportuno metodo che ottiene un Iteratore da una Classe di tipo "struttura dati" come ad esempio la LinkedList, un Vector o un Set LinkedList L=new LinkedList(); .... Iterator iter = L.iterator(); boolean hasNext(); Object next(); LinkedList L= ......; ......; Iterator iter=L.iterator(); // crea un iteratore while (iter.hasNext()) { // iter non è finito ? Object o = iter.next(); // leggi l'oggetto .....; } Scorre l'iteratore ottenuto dalla lista dal primo all'ultimo oggetto. java.Object java.util.Iterator Usando un opportuno metodo che ottiene un Iteratore da una Classe di tipo "struttura dati" come ad esempio la LinkedList, un Vector o un Set LinkedList L=new LinkedList(); .... ListIterator iter = L.listIterator(); boolean hasNext(); Object next(); boolean hasPrevious(); Object previous(); ListIterator iter=L.listIterator(); // Attenzione ! Il puntatore è sulla testa della // lista e la chiamata: boolean B = iter.hasPrevious(); // restituisce false e quindi non si può scorrere a // ritroso se prima non si è usato next() Scorre l'iteratore ottenuto dalla lista dal primo all'ultimo oggetto e viceversa. java.Object java.util.ListIterator Per verificare la differenza tra l'interfaccia Iterator e ListIterator si propone un esempio che indica come fare per creare e scorrere una lista dal primo all'ultimo elemento. esempio 1. "Si desidera realizzare una lista nella quale inserire alcuni Oggetti Integer, String e Boolean e quindi stamparli in sequenza." Richiesta: Realizzare un main() che prima inserisca i dati e poi li stampi tramite un ciclo. appunti Java pag.159 Codifica: import java.util.*; public class cap11_es_01 { public static void main(String args[]) { LinkedList L=new LinkedList(); // (1) Integer I=new Integer(99); L.add(I); // (2) String S=new String ("Banana"); L.add(S); // (3) Boolean B=new Boolean(true); L.add(B); // (4) I=new Integer(3); L.add(I); I=new Integer(17); L.add(I); B=new Boolean(false); L.add(B); System.out.println("Lista di "+L.size()+" Oggetti:"); Iterator it=L.iterator(); while (it.hasNext()) System.out.println(""+it.next()); System.out.println("Fine esecuzione."); } } // // // // (5) (6) (7) (8) Esecuzione: l’output della finestra sarà: Lista di 6 Oggetti: 99 Banana true 3 17 false Fine esecuzione. Commento al codice: La nota (1) mostra il costruttore, le (2, 3, 4) la costruzione e l'inserimento di oggetti, la (5) invoca il metodo che restituisce l'Iteratore it con il quale si utilizzano i metodi it.hasNext() (6) e it.next() (7) per realizzare il ciclo di scansione della Lista. Il metodo hasNext() esegue il test per verificare se si è giunti alla fine della lista e next() acquisisce in lettura l'oggetto e sposta il puntatore all’elemento successivo. Sostituendo alle ultime righe le seguenti (6', 7', 8', 9', 10')si ottiene prima un identico risultato (la stampa diretta) e quindi la stampa inversa della lista. ListIterator lit=L.listIterator(); while (lit.hasNext()) System.out.println(""+lit.next()); System.out.println("Fine lettura diretta."); while (lit.hasPrevious()) System.out.println(""+lit.previous()); System.out.println("Fine lettura inversa."); // (6') // (7') // (8') // (9') // (10') appunti Java pag.160 esempio 2. "progettare una sottoclasse di LinkedList chiamata Lista che implementi il solo metodo di inserimento ordinato di Stringhe in modo che ciascuna stringa sia collocata in ordine lessicografico." Richieste: Il metodo oltre a mantenere l'ordine deve inserire anche stringhe ripetute che dovranno essere consecutive. Il main() di prova dovrà stampare la lista con un iteratore per verificare il corretto funzionamento del metodo. L’esercizio chiede di progettare la classe Lista sottoclasse di LinkedList come mostrato dalla figura sottostante. La sottoclasse da progettare non ha una validità generale nel senso che non consente di inserire in modo ordinato Oggetti qualunque ma solo oggetti di tipo String. Si tratta di un esempio molto particolare. Codifica della sottoclasse: import java.util.*; public class Lista extends LinkedList { public void put(String S) { int i=0; boolean trovato=false, esiste=false; while (i<size() && !trovato && !esiste) { (0) String A=(String) get(i); if (A.compareTo(S)>0) trovato=true; (1) else if (A.compareTo(S)==0) esiste=true; (2) else i++; (3) } add(i,S); (4) } LinkedList Lista } Codifica del main: import java.util.*; public class cap11_es_02 { public static void main(String args[]) { Lista L=new Lista(); (5) String S[]={"fico","banana","mela","fico","zucca","pera","pera"}; for (int i=0; i<S.length; i++) L.put(S[i]); (6) System.out.println("\nLista di "+L.size()+" Oggetti:"); (7) Iterator it=L.iterator(); (8) while (it.hasNext()) (9) System.out.println(""+it.next()); (10) System.out.println("\nFine esecuzione."); } } Output: Lista di 7 Oggetti: banana fico fico mela pera pera zucca Fine esecuzione. Commento al codice: appunti Java pag.161 Nel metodo put() la condizione (0) evidenzia che l’inserimento ordinato può essere eseguito solo dopo una ricerca che determina la posizione di inserimento della stringa nella lista. La ricerca ha termine in tre casi distinti (1) se ho trovato una stringa che segue alfabeticamente quella da inserire; (2) se ho trovato una stringa identica a quella da inserire; (3) se sono arrivato al termine. La nota (4) evidenzia il metodo usato per inserire la stringa nella popsizione corretta. Le note da (5) a (10) mostrano che la sottoclasse Lista è autorizzata ad usare i metodi della classe genitrice che ha ereditato. 11.3 Vettori dinamici Tra le altre strutture dati di vasto uso in programmazione vi è sicuramente l'array; ma anche in java si tratta di una struttura dati sostanzialmente statica. Anche se può essere dimensionato dinamicamente durante l'esecuzione, la sua dimensione non è più modificabile pena la perdita delle informazioni allocate fino a quel momento. Un vettore dotato di indice e capace di crescere dinamicamente come la Lista sarebbe ideale per risolvere problemi che devono trattare i dati sia in sequenza (lista) che in modo indicizzato (array). Una struttura con queste caratteristiche deve forzatamente sacrificare "sull'altare della dinamicità " un poco di efficienza ma la sua costruzione è possibile e in effetti tale struttura "ibrida" è presente in java.util sotto forma di Classe con i seguenti metodi: Classe Costruttori Metodi Vector Identificatori: Invocazione: Vector(); Vector(Collection c); Vector(int initCapacity); Vector(int initCapacity, int capacityIncr); Vector V = Vector(); Effetti: Crea un oggetto Vector identificato da V vuoto. Identificatore void add(Object); void add(Index, Object); void insertElementAt(Index, Object); void setElementAt(Index, Object); boolean remove(Object); void removeElementAt(Index); void clear(); boolean contains(Object); int indexOf(Object); boolean addAll(Collection); Object elementAt(Index); Object get(Index); boolean isEmpty(); int size(); int capacity() Invocazione: Pckage e Gerarchia Effetti:: java.util java.Object java.util.AbstractCollection java.util.Vector appunti Java pag.162 Se si analizza le struttura di tipo Vector e la si classifica usando gli attributi discussi si ottiene la seguente tabella comparativa: Accesso sequenziale e indicizzato Vector Dimensionamento Organizzazione dinamico disordinata o ordinata con duplicati Persistenza volatile Tempo di acc. veloce per verificare come operare per creare e scorrere un Vector dal primo all'ultimo elemento proponiamo un esempio. esempio 3. "Si desidera realizzare un Vector nel quale inserire alcuni Oggetti Integer, String e Boolean e quindi stamparli in sequenza." Richiesta: Realizzare un main() che prima inserisca i dati e poi li stampi in un ciclo. Codifica: import java.util.*; public class cap11_es_03 { public static void main(String args[]) { Vector V=new Vector(); // (1) Integer I=new Integer(99); V.add(I); // (2) String S=new String ("Banana"); V.add(S); // (3) Boolean B=new Boolean(true); V.add(B); // (4) I=new Integer(3); V.add(0,I); // (5) I=new Integer(17); V.setElementAt(I,1); // (6) B=new Boolean(false); V.add(B); System.out.println("Capacità="+V.capacity()+" Dimensione="+V.size()); // (7) int i=0; // (8) while (i<V.size()) { // (9) System.out.println("V["+i+"]="+V.get(i)); // (10) i++; } System.out.println("Fine esecuzione."); } } Esecuzione: l’output della finestra sarà: Capacità=10 Dimensione=5 V[0]=3 V[1]=17 V[2]=Banana V[3]=true V[4]=false Fine esecuzione. Commento al codice: La nota (1) mostra il costruttore, le (2, 3, 4) la costruzione e l'inserimento in coda di oggetti, la (5) invoca il metodo che inserisce un elemento nella componente di posizione 0, la (6) utilizza il metodo di sovrascrittura sull'Oggetto di posizione 1, (l'Integer 99 viene sovrascritto in modo distruttivo). Le (7 8, 9, 10) utilizzano i metodi necessari per realizzare il ciclo di scansione del Vector utilizzando l'indice di scorrimento. appunti Java pag.163 11.4 Collezioni e Insiemi Si discuteranno dal punto di vista astratto due strutture dati la Collezione e l'Insieme per vedere le proprietà di cui godono. In particolare un insieme (di oggetti) deve rispettare la nozione che conosciamo dalla matematica e consentire l'esecuzione corretta delle operazione note. Un insieme è una collezione o aggregato di oggetti definiti e distinti tra loro che si dicono elementi dell'insieme. Alla nozione di insieme sono associate simboli e operazioni in particolare si utilizzano i simboli di: appartenenza [ x ε D ] insieme vuoto [ Φ ] sottoinsieme [ A ⊆ B] rappresentazione dell'insieme [ { a,b,c,d}) Ci attendiamo poi che si possano eseguire le operazioni di: Unione [ ∪ ] Intersezione [ ∩ ] Differenza. Ragionando sull'Abstract Data Type Insieme si dovrebbe disporre di : Costruttore/i Inserzione, Cancellazione e Ricerca: Aggiunta di un oggetto all'insieme (solo se non è già presente) Cancellazione di un oggetto noto Ricerca dell'esistenza di un oggetto noto (appartenenza) Sottoinsieme Operazioni: Unione Intersezione Differenza Altri metodi: Cardinalità dell'insieme (la sua dimensione attuale) Operatore per stabilire se si tratta di un insieme Vuoto Operare lo scorrimento (in qualche modo) di tutti gli oggetti per ottenerne una rappresentazione tabulare. La nozione di Insieme utilizza il termine Collezione o Aggregato di oggetti, e si potrebbe pensare che una Collezione sia un termine che designa una entità più generale (o generica) del concetto di insieme. In cosa può consistere tale diversità? Che cosa hanno in comune Collezione e l'Insieme? Potremmo dire che sia la Collezione che l'Insieme hanno la proprietà comune di essere entrambi "contenitori" di oggetti senza ordine (non esiste un primo o un ultimo elemento di un insieme, esso può contenere oggetti non confrontabili). appunti Java pag.164 Contemporaneamente potremmo dire che la differenza tra le due entità consiste nel fatto che l'insieme NON ammette elementi ripetuti mentre una Collezione può ammetterli. Potremmo definire tale struttura dati nel seguente modo: la Collezione è una raccolta di oggetti distinti o duplicati. In altri termini si potrebbe dire che un Insieme è una Collezione che non ammette oggetti duplicati. Ma quali sono le operazioni che ci attendiamo di poter eseguire su una entità (struttura dati) di questo tipo ? Nella seconda definizione si nota che esiste una relazione di generalizzazione tra Insieme e Collezione (un insieme è una collezione particolare) che potrebbe rendere applicabile il concetto di ereditarietà tipico della progettazione di classi. Molte delle operazioni che valgono per una Collezione potrebbero valere anche per un Insieme. Insieme Collection Accesso non sequenziale non indicizzato non sequenziale non indicizzato Dimensionamento Organizzazione dinamico disordinata senza duplicati dinamico disordinata con duplicati Persistenza volatile Tempo di acc. veloce volatile veloce Si analizzerà di seguito la struttura della gerarchia delle Classi e Interfacce di java.util, per progettare se necessario gli ADT che non ci soddisfano completamente. alcune classi e interfacce del package java.util Object Collection Iterator ListIterator AbstractCollection Set List LinkedList Coda Vector HashSet Stack Insieme TreeSet java.util (parte) appunti Java pag.165 Lasciando al lettore l'analisi delle altre Classi del package java.util quali Date, Calendar, Time molto utili nella programmazione di applicazioni, si cercherà di interpretare la "filosofia" con cui le sole classi in grassetto nella gerarchia disegnata sono state progettate. La prima cosa da notare è la gerarchia di Classi: AbstractCollection, LinkedList, Vector, HashSet. e di Interfacce Collection, List e Set. Il progettista ha deciso che le caratteristiche comuni a tutte le strutture dati dinamiche sono "raggruppate" nella classe AbstractCollection che implementa l'intefaccia Collection. In altri termini tutte le strutture dati dinamiche sono collezioni. In altre parole possono essere "pensate" come raccolte indifferenziate di oggetti duplicati o meno. Un HashSet è una Collection cosi come lo è una LinkedList o un Vector. La seconda cosa da notare è che non tutti gli ADT Collection sono Liste. Il progettista ha suddiviso le Collection in strutture dati sequenziali (LinkedList, Vector) e per queste ha realizzato l'interfaccia List e in strutture senza duplicati (HashSet e TreeSet) e per queste ha realizzato l'interfaccia Set. In altre parole una LinkedList e un Vector sono una List, un HashSet è un Set. Tutte sono AbstractCollection e quindi Collection. Se si esamina la classe HashSet, o l'interfaccia Set, sotto sono elencati costruttori e metodi, si nota che le operazioni non sono del tutto coincidenti con quelle attese per una struttura dati di tipo Insieme. Classe Costruttori HashSet Identificatori: Invocazione: Effetti: Metodi Identificatore HashSet(); HashSet(Collection c); HashSet A = new HashSet(); Nel primo caso Crea un oggetto HashSet identificato da A vuoto. Nel secondo crea un set da un Vector eliminando i duplicati. void add(Object); boolean remove(Object); boolean removeAll(Collection); void clear(); boolean equals(HashSet); boolean contains(Object); boolean addAll(Collection); boolean isEmpty(); int size(); Invocazione: Effetti:: Pckage e Gerarchia java.util java.Object java.util.AbstractCollection java.util.HeshSet appunti Java pag.166 Mancano le operazioni "dirette" di Unione e Intersezione e Differenza anche se esistono operazioni di concatenamento tra Collection e di cancellazione di elementi o collezioni. Questa insoddisfazione sarà "risolta" progettando una classe Insieme come sottoclasse di HashSet nella quale siano implementate le operazioni elencate in precedenza per gli insiemi. Per quanto riguarda la Collection, l'interfaccia esistente in java.util può essere soddisfacente in quanto consente tutte le operazioni di inserimento, ricerca e cancellazione e mantiene anche elementi duplicati. 11.4.1 La sottoclasse Insieme Si progetterà di seguito la classe insieme come sottoclasse di HashSet e si farà in modo che questa sia ancora una Collection. La gerarchia del paragrafo precedente mostra la collocazione ereditaria della classe Insieme. Di seguito si è indicato lo schema della classe con i metodi richiesti. HashSet Insieme Insieme() Insieme(Collection c); Insieme unione(Insieme i); Insieme intersezioine(Insieme i); Insieme differenza(Insieme i); boolean contiene(Object o); void metti(Object o); boolean togli(Object o); boolean vuoto(); int cardin(); boolean sottins(Insieme i); boolean identico(Insieme i); void svuota(); Iterator iterator(); La codifica è lasciata come esercizio al lettore esercizio 11.09. Proponiamo un esempio di utilizzo della Classe Insieme progettata. esempio 4. "Assegnare a un insiemi i numeri da 20 a 29 e ad un secondo insieme i numeri 15 a 24 e stamparli. Ottenere Unione, Intersezione e le due differenze e stamparle ancora". Richiesta : realizzare il solo main(). appunti Java pag.167 Codifica: import java.util.*; public class cap11_es_04 { public static void main(String args[]) { Insieme L=new Insieme(),L1=new Insieme(); for (int i=20; i<30; i++) L.metti(new Integer(i)); // (1) for (int i=15; i<25; i++) L1.metti(new Integer(i)); // stampare con Iterator System.out.println("\nL1 ha "+L1.cardin()+" Oggetti:"); Iterator it=L1.iterator(); // (2) while (it.hasNext()) System.out.print(""+it.next()+","); // stampare la Collection con toString() System.out.println("\nL ha "+L.cardin()+" Oggetti:"); System.out.println(L); // (3) // unione Insieme C=L1.unione(L); System.out.println("\nL1 + L="+C); // intersezione C=L1.intersezione(L); System.out.println("\nL1 * L="+C); // prima differenza C=L.differenza(L1); System.out.println("\nL - L1="+C); // seconda differenza C=L1.differenza(L); System.out.println("\nL1 - L="+C); } } Esecuzione: l’output della finestra sarà: L1 ha 10 Oggetti: 20,21,22,23,24,25,26,27,28,29, L ha 10 Oggetti: [15, 16, 17, 18, 19, 20, 21, 22, 23, 24] L1 + L=[15, 16, 17, 18,..,25, 26, 27, 28, 29] L1 * L=[20, 21, 22, 23, 24] L – L1=[15, 16, 17, 18, 19] L1 – L=[25, 26, 27, 28, 29] Le note (1) (2) (3) evidenziano i metodi utilizzati per costruire l’insieme, stamparlo usando un iteratore oppure il metodo toString() di ogni Collection. esempio 5. "Immettere in un insieme i numeri da 1 a 25 e stamparlo, eliminare tutti i numeri dispari e stamparlo ancora". Richiesta : realizzare il solo main(). Codifica: import java.util.*; public class cap11_es_05 { public static void main(String args[]) { Insieme L=new Insieme(); for (int i=1; i<26; i++) L.metti(new Integer(i)); // stampa System.out.println("\nL ha "+L.cardin()+" Oggetti:"); System.out.println(L); // Eliminazione appunti Java pag.168 for (int d=1; d<26; d=d+2) { Integer A=new Integer(d); L.togli(A); } // stampa System.out.println("\nL ha "+L.cardin()+" Oggetti:"); System.out.println(L); } } Esecuzione: l’output della finestra sarà: L ha 25 Oggetti: [1,2,3,..,20,21,22,23,24,25] L ha 12 Oggetti [2,4,6,8,10,12,14,16,18,20,22,24] 11.4.2 L'interfaccia Collection Se si guarda la cima della piramide delle Classi java.util si nota che tutte le strutture date implementano l’interfaccia Collection e quindi possono essere trattate tutte come Collezioni di oggetti. Tutte le Classi che sono Collection debbono implementare i seguenti metodi: Interface Collection Classi che imple- Identificatori: mentano l’interfaccia Collection Metodi Identificatore Package java.util Class AbstractCollection Si ricorda che Abstract collection è genitrice di tutte le struture studiate: LinkedList, Vector, HasSet ecc. boolean add(Object); boolean addAll(Collection); boolean remove(Object); boolean removeAll(Collection); void clear(); boolean equals(Collection); boolean contains(Object); boolean isEmpty(); int size(); esempio 6: “Dopo aver creato una oggetto Vector di Numeri che contenga sicuramente duplicati, ottenere rapidamente, usando i metodi di Collection, da questo un Insieme senza duplicati.” Richieste: Nel main() generare un vector con duplicati, stamparlo, trasformarlo in Insieme e stamparlo di nuovo. appunti Java pag.169 Codifica: import java.util.*; public class cap11_es_06 { public static void main(String args[]) { Vector A=new Vector(); A.add(new Integer(11)); A.add(new Integer(11)); A.add(new Integer(23)); A.add(new Double(3.1)); A.add(new Double(9.9)); A.add(new Double(9.9)); System.out.print("Contiene "+A.size()+" elementi\n Contiene="+A); Insieme I=new Insieme(A); System.out.print("\nContiene "+I.size()+" elementi\nContiene="+I+"\n"); } } Output: Contiene 6 elementi Contiene=[11, 11, 23, 3.1, 9.9, 9.9] Contiene 4 elementi Contiene=[11, 23, 9.9, 3.1] 11.5 Pila e Coda Due ulteriori strutture dati usate in programmazione sono la Coda e la Pila (in inglese Queue e Stack). Queste due strutture dati sono di norma utilizzate come “intermediari” tra un processo produttore di Dati e un processo consumatore. In qualche modo sono analoghe al concetto di Stream o di Filtro visto nel capitolo 9. Infatti uno stream o un filtro sono entità che mettono in comunicazione due processi. Allo stesso modo pila e coda sono utilizzate in programmazione per collegare un processo produttore e uno consumatore: Produttore Consumatore Coda X Y Z T Consumatore Produttore X Y Z Pila Us esempio di processo che usa una Coda è un qualsiasi programma di Stampa; questo è un processo produttore di caratteri ad alta velocità mentre una stampante è un processo consumatore di caratteri molto lento. La Coda di Stampa è l’intermediario che consente di accumulare i caratteri prodotti dal programma e pronti per la stampa in attesa che il processo di stampa li consumi alla sua ridotta velocità. In questo caso la coda funge da “filtro sincronizzatore” dei due processi. Programma Coda di stampa X Y Z T Stampante appunti Java pag.170 Così una Pila è usata per mantenere processi “sospesi” in attesa che altri processi “abbiano completato il lavoro”. Si immagini un “programma P” che chiama una “procedure P1” la quale a sua volta chiama una “procedure P2” e così via come in figura. Processo P 40 10 istr. 1 20 istr. 2 30 chiama P1 40 istr. 3 50 fine P. 40 Processo P1 get Pila 160 istr. xx 170 chiama P2 180 fine P1 180 Processo P2 put Pila 260 istr. yy get Pila 270 istr. zz 280 fine P2 put Pila 180 40 40 In questo esempio la Pila degli indirizzi di programma consente di mantenere l’ordine corretto di esecuzione; il processo principale P viene sospeso (e si memorizza l’indirizzo di rientro 40) in attesa che venga svolto il processo P1, ma questo incontra la chiamata di P2, P1 è sospeso (si memorizza l’indirizzo di rientro 180) in attesa che P2 abbia termine. Quando P2 ha termine il controllo del processo ritorna a P1 e quando questo termina il controllo ritorna la processo principale P. Questo lavoro è svolto da una Pila che come in figura accumula gli indirizzi a cui i processi sospesi e debbono poi essere riavviati. Dagli esempi mostrati si nota che le strutture dati Pila e Coda hanno in lettura un comportamente molto simile a Stream, ma molto diverso rispetto a List, Vector o Insieme. Infatti Pila, Coda, Stream sono strutture nelle quali la lettura è distruttiva, nel senso che leggendo il dato lo si cancella-consuma. List, Vector e Set cono contenitori senza lettura distruttiva, infatti consentono di leggere le informazioni più volte senza eliminarle. 11.5.1 l’ADT Pila Si potrebbe dare le seguente definizione dell’ADT: Si dice Pila una struttura dati sequenziale che consente l’input e l’output di un dato da uno stesso lato della sequenza. appunti Java pag.171 La figura mostra graficamente uno Stack: Push Dato Pop Dato Dato n …. Dato 2 Dato 1 In sostanza l’ultimo elemento che entra nella coda è anche il primo elemento che ne esce. La Pila prende il nome di struttura (LIFO) Last In First Out. Oltre a questo si deve ricordare che la lettura di un dato è distruttiva. Queste sono le operazioni che caratterizzano l’ADT Pila: Costruttore/i Aggiunta di un oggetto in testa Lettura distruttiva dell’oggetto di testa Lettura NON distruttiva dell’oggetto di testa Operatore per stabilire se La Pila è vuota In Java esiste la Classe Stack come sottoclasse di Vector con i seguenti metodi Classe Costruttori Stack Identificatori: Stack(); Invocazione: Stack A = new Stack(); Effetti: Nel primo caso Crea un oggetto Stack identificato da A vuoto. void push(Object); Object pop(); Object peek(); boolean isEmpty(); java.Object java.util.AbstractCollection java.util.Vector java.util.Stack Metodi Identificatore Pckage e Gerarchia java.util esempio 7. “creare e stampare uno Stack di Integer” Codifica: import java.util.*; public class cap11_es_07 { public static void main(String args[]) { Stack A=new Stack(); for (int i=0; i<7; i++) A.push(new Integer(i)); while (!A.isEmpty()) System.out.println(A.pop()); } } appunti Java pag.172 Esecuzione: l’output della finestra sarà: 6 5 4 3 2 1 0 esempio 8. “usare una Pila per verificare se una sequenza di parentesi tonde aperte e chiuse è legittima” Indicazioni: una sequenza corretta di parentesi è quella usata per scrivere espressioni aritmetiche rispettando l’ordine di priorità. La correttezza è indipendente dal contenuto. Es. la sequenza (()())() è corretta la sequenza (()()() è errata perche manca una parentesi chiusa. In ogni caso non basta che il numero di parentesi aperte sia uguale a quelle chiuse, infatti nell’esempio ())(() le parentesi pur essendo equilibrate non sono corrette. Un algoritmo per verificare la correttezza della sequenza, facendo uso di uno Stack, potrebbe essere il seguente: inserisci la sequenza di parentesi in uno StringBuffer Sb; 1. crea uno Stack Vuoto St; 2. Fintanto che (Sb non è vuoto <e> non c’è errore) 3. preleva una parentesi da Sb; 4. Se è una ‘(‘ Allora inseriscila nello Stack; 5. Altrimenti Se lo Stack non è Vuoto Allora preleva la ‘(‘ 6. Altrimenti c’e un Errore 7. Se (Stack è Vuoto <e> non c’e errore) Allora la sequenza è Corretta 8. Altrimenti la sequenza è Errata Codifica: import java.util.*; public class cap11_es_08 { public static void main(String args[]) { StringBuffer Sb=new StringBuffer(args[0]); // input args[0] Stack St=new Stack(); boolean err=false; while (Sb.length()!=0 && !err) { char Ch=Sb.charAt(0);Sb=Sb.deleteCharAt(0); if (Ch=='(') St.push(new Character(Ch)); else if (!St.isEmpty()) St.pop(); else err=true; } if (!err && St.isEmpty()) System.out.println("OK"); else System.out.println("ERRATA"); } } appunti Java pag.173 11.5.2 l’ADT Coda Si potrebbe dare le seguente definizione di Coda: Si dice Coda una struttura dati sequenziale che consente l’input di un dato da un capo e l’output dal capo opposto. La figura mostra graficamente una Coda: Put Dato Get Dato Dato n ….. Dato 2 Dato 1 In sostanza il primo elemento che entra nella coda è anche il primo elemento che ne esce. La Coda prende il nome di struttura (FIFO) First In First Out. Oltre a questo si deve ricordare che la lettura di un dato è distruttiva. Operazioni che caratterizzano l’ADT Coda: Costruttore/i Aggiunta di un oggetto in coda Lettura distruttiva dell’oggetto di testa Lettura NON distruttiva dell’oggetto di testa Operatore per stabilire se la Coda è vuota in Java NON esiste una tale struttura ma la si può facilmente progettare come sottoclasse di LinkedList. Di seguito si fornisce il diagramma della classe Coda progettata come sottoclasse di LinkedList. LinkedList Coda + Coda() + put(Object) : void + get( ) : Object + peek( ) : Object + isEmpty( ) : boolean La progettazione della sottoclasse Coda è lasciata come esercizio vedi 11.18 appunti Java pag.174 esempio 9 :“creare e stampare una CODA di Integer” Codifica: import java.util.*; public class cap11_es_09 { public static void main(String args[]) { Coda A=new Coda(); for (int i=0; i<5; i++) A.put(new Integer(i)); while (!A.isEmpty()) System.out.println(A.get()); } } Esecuzione: l’output della finestra sarà: 0 1 2 3 4 esempio 10. “Date due code di parole ordinate, fonderle in una terza coda mantenendo l’ordine lessicografico”. Richieste: nel main() creare le due code ordinate partendo da due Array di stringhe ordinate usando gli opportuno metodi di coda. Realizzare poi un metodo statico fusione() con gli opportuni parametri e richiamarlo. Codifica: import java.util.*; public class cap11_es_10 { public static Coda fondi(Coda a, Coda b) { Coda C=new Coda(); while (!a.isEmpty() && !b.isEmpty()) { String Sa=(String) a.peek(); String Sb=(String) b.peek(); if (Sa.compareTo(Sb)<=0) {C.put(Sa); a.get();} else { C.put(Sb); b.get();} } if (a.isEmpty()) C.addAll(b); else C.addAll(a); return C; } public static void main(String args[]) { String[] S1={"asino", "cavallo", "somaro"}; String[] S2={"cane", "cavalla", "leone", "liocorno"}; Coda A=new Coda(); for (int i=0; i<S1.length; i++) A.put(new String(S1[i])); System.out.println("Coda A="+A); Coda B=new Coda(); for (int i=0; i<S2.length; i++) B.put(new String(S2[i])); System.out.println("Coda B="+B); Coda C=fondi(A,B); System.out.println("Coda A+B="+C); } } appunti Java pag.175 Esecuzione: l’output della finestra sarà: Coda A=[asino, cavallo, somaro] Coda B=[cane, cavalla, leone, liocorno]; Coda A+B=[asino, cane, cavalla, cavallo, leone, liocorno, somaro] 11.6 Alberi Binari Definizione di Albero Binario 1. Un albero binario è un Nodo vuoto (null) chiamato Radice 2. Oppure è un Nodo Radice dotato di due alberi binari detti sottoalberi sinistro e destro. La difinizione è ricorsiva in quanto la seconda riga definisce un albero binario usando la parola albero binario. Aiutiamoci con alcuni esempi di alberi binari: Albero (A) Albero (C) Albero (B) null 33 33 null null 38 20 99 36 null null Il caso (A) soddisfa alla definizione (1). I casi (B) (C) alla definizione di riga (2) e riga (1). Infatti (B) ha una Nodo Radice (def. 2) e due sottoalberi Vuoti (def. 1). Un albero si caratterizza per il numero di Livelli o Profondità. La profondità di (A) e (B) è 0 quella di (C) è 2. Si ricorda che il livello della radice è il livello 0 e i successivi sono i livelli 1, 2,..n. Le Foglie di un Albero sono i nodi terminali. L’albero (C) ha tre foglie 20, 36, 99. L’albero (B) ha una sola foglia 33 che coincide con la Radice. I Rami sono i collegamenti tra Nodi. 11.6.1 Generazione, Visita e Ricerca in un albero Binario Un albero Binario di norma memorizza nei nodi informazioni con Chiavi non Ripetute o Univoche. Si dice Campo Chiave di una informazione il campo che la caratterizza Univocamente. Ad esempio se le informazioni da memorizzare in un nodo sono Cognomi (non ripetuti) di persone e le relative età, il campo Chiave diviene il Cognome, ciascun nodo conterrà tutta l’informazione ma il campo chiave è il solo Cognome. Il nodo avrà la seguente struttura: appunti Java pag.176 chiave Cognome età Se invece le informazioni da memorizzare sono Cognomi e Nomi che si possono ripetere, occorre definire un campo chiave diverso; per esempio il Codice Fiscale che ha la caratteristica di essere univoco per ogni cittadino. Il nodo avra la seguente struttura: chiave Cod.Fiscale Cognome Nome età Il campo chiave univoco è essenziale sia per generare correttamente l’albero sia per ricercare le informazioni in modo rapido. La visitazione di un albero, la lettura dei suoi nodi, può essere eseguita in varie modalità le due principali sono la visita in Profondità e quella in Ampiezza. Per esempio la visita in profondità con il metodo SRD (Sinistra Radice Destra) per l’albero (C) genera la seguente sequenza di lettura: Albero (C) 33 20, 33, 36, 38, 99 38 20 99 36 Una visita in ampiezza la seguente: Albero (C) 33 33, 20, 38, 36, 99 38 20 36 99 In seguito si tratterà di un solo tipo di albero binario, in particolare di Alberi Binari Ordinati ed Equilibrati, generati e scanditi in modalità SRD. (a) Generare un albero in modalità SRD significa che ogni Nodo ha nel suo sottoalbero sinistro solo CHIAVI Minori della radice e viceversa nel sottoalbero Destro. (b) La lettura di un tale albero produce sempre un elenco con Chiavi Ordinate in modo Crescente. (c) Se l’albero è Equilibrato, ovvero la differenza tra la maggiore e la minore delle sue profondità non è mai superiore ad UNO, il tempo per la ricerca di una Chiave è molto rapida e non impiega mai un numero di confronti tra nodi superiore log 2 N dove N è il numero totale dei Nodi. appunti Java pag.177 L’albero C soddisfa tette le condizioni (a), (b),(c). Se si deve eseguire una ricerca su un numero molto alto di elementi e si confrontano le velocità di ricerca in una Lista Sequenziale rispetto ad un Albero, si possono notare immediatamente i vantaggi offerti dal secondo: Nr. Elementi 8 Lista Sequenziale Numero max.ricerche 8 Albero Binario Bilanciato Numero max.ricerche log 2 8 =3 128 128 log 2 128 =7 1.024 1.024 log 2 1024 =10 1.048.576 1.048.576 log 2 1048576 =20 I vantaggi elencati in (b) e (c), ordinamento immediato delle chiavi e ricerca proporzianale al logaritmo si “pagano” rispetto ad altre strutture dati, in fase di generazione e mantenimento dell’equilibrio dell’albero. Nella sintesi finale cercheremo di completare una tabella di confronto tra tutte le strutture dati trattate introducendo anche il concetto di efficienza e velocità di una struttura dati rispetto alle operazioni di Inserimento di un Nodo, Cancellazione di un Nodo, Ricerca di Un Nodo, Ordinamento delle chiavi e accesso alla struttura. 11.6.2 La Classe TreeSet L’ADT Albero binario e le sue Operazioni: Costruttore/i Aggiunta di un oggetto Cancellazione di un oggetto Ricerca dell’esistenza di un Oggetto Operatore per stabilire se l’albero è vuoto Modalità di visita completa dei nodi Esaminiamo la Classe TreeSet. La prima annotazione da fare è che si tratta ancora di una struttura di tipo Set che ne implementa l’interfaccia come HashSet infatti come questo ammette solo chiavi non ripetute e quindi è ancora un Set. Classe Costruttori TreeSet Identificatori: Invocazione: Effetti: Metodi Identificatore TreeSet(); TreeSet(Collection c); TreeSet A = new TreeSet(); Nel primo caso Crea un identificato da A vuoto. boolean add(Object); void clear(); boolean contain(Objects); boolean isEmpty(); boolean remove(Objects); Iterator iterator() oggetto TreeSet appunti Java Pckage e Gerarchia pag.178 java.util java.Object java.util.AbstractCollection java.util.TreeSet esempio 11. “generare l’albero di Integer di figura (C) inserendo i numeri in ordine casuale e stamparlo con e senza Iterator” Richiesta: realizzare il solo main(). Codifica: import java.util.*; public class cap11_es_11 { public static void main(String args[]) { TreeSet A=new TreeSet(); A.add(new Integer(99)); A.add(new Integer(20)); A.add(new Integer(36)); A.add(new Integer(38)); A.add(new Integer(33)); Iterator it=A.iterator(); System.out.println("Albero A="+A);// usa toString()predefinito while (it.hasNext()) System.out.println(it.next()); // Ciclo con Iterator } } Output: Albero a=[20, 33, 36, 38, 99] 20 33 36 38 99 Per verificare che la ricerca di un elemento in un albero bilanciato è più rapida rispetto ad una lista, mentre la generazione è più lenta realizzeremo due programmi per confrontare le due strutture. esempio 12. “Si vuole realizzare un programma che confronti la velocità di ricerca di una chiave in un albero rispetto ad una lista di Double di grandi dimensioni” Indicazioni: Generare prima un albero e una lista di 70.000 Double con una funzione random() e testare il tempo di ricerca, generare ancora un albero e una lista di 700.000 double e testare ancora il tempo di ricerca di una chiave. Richieste: Il main() dopo la generazione delle due strutture dati deve chiamare la funzione contains(Double) con la stessa chiave e rilevare il tempo di inizio e fine della chiamata stampando il tempo trascorso. Codifica: import java.util.*; public class cap11_es_12 { public static TreeSet Genera_tree(long N) { TreeSet A=new TreeSet(); long i=0; Double NUM=null; appunti Java pag.179 double num=0; while (i<N) { num=(Math.random()*2*N); NUM=new Double(num); if (A.add(NUM)) i++; } return A; } public static LinkedList Genera_list(long N) { LinkedList A=new LinkedList(); long i=0; Double NUM=null; double num=0; while (i<N) { num=(Math.random()*2*N); NUM=new Double(num); if (A.add(NUM)) i++; } return A; } public static void main(String args[]) { TreeSet A=Genera_tree(70000); LinkedList L=Genera_list(70000); // Chiave da cercare Double Key=new Double((70000/2)*0.33333); long t=System.currentTimeMillis(); A.contains(Key); long tf=System.currentTimeMillis(); t=tf-t; long s=System.currentTimeMillis(); L.contains(Key); long sf=System.currentTimeMillis(); s=sf-s; System.out.println("N=70.000\nRicerca Albero ="+t+" Lista="+s); A=Genera_tree(700000); L=Genera_list(700000); // Chiave da cercare Key=new Double((700000/2)*0.33333); t=System.currentTimeMillis(); A.contains(Key); tf=System.currentTimeMillis(); t=tf-t; s=System.currentTimeMillis(); L.contains(Key); sf=System.currentTimeMillis(); s=sf-s; System.out.println("N=700.000\nRicerca Albero ="+t+" Lista="+s); } } Output: N 70.000 700.000 Albero 0 ms (*) 0 ms (*) Lista Circa 40 ms (**) Circa 360 ms(**) appunti Java pag.180 (*) il tempo di ricerca nell’albero è sempre inferiore al millisecondo e non è possibile rilevare tempi inferiori. (**) i tempi possono variare a seconda del computer utilizzati e della sua velocità in ogni caso il rapporto è circa 10/1 nei due casi. esempio 13. “Si vuole realizzare un programma che confronti la velocità di generazione di in un albero rispetto ad una lista di Double di grandi dimensioni” Indicazioni: Generare prima un albero e una lista di 70.000 Double con una funzione random() e testare il tempo impiegato, generare ancora un albero e una lista di 700.000 double e testare ancora il tempo di generazione. Richieste: Il main() deve rilevare il tempo necessario per la generazione delle due strutture dati rilevando il tempo di inizio e fine della chiamata e stampando il tempo trascorso. Codifica: import java.util.*; public class cap11_es_13 { public static TreeSet Genera_tree(long N) { TreeSet A=new TreeSet(); long i=0; Double NUM=null; double num=0; while (i<N) { num=(Math.random()*2*N); NUM=new Double(num); if (A.add(NUM)) i++; } return A; } public static LinkedList Genera_list(long N) { LinkedList A=new LinkedList(); long i=0; Double NUM=null; double num=0; while (i<N) { num=(Math.random()*2*N); NUM=new Double(num); if (A.add(NUM)) i++; } return A; } public static void main(String args[]) { // 70.000 long t=System.currentTimeMillis(); TreeSet A=Genera_tree(70000); long tf=System.currentTimeMillis(); t=tf-t; long s=System.currentTimeMillis(); LinkedList L=Genera_list(70000); long sf=System.currentTimeMillis(); s=sf-s; System.out.println("N=70.000\nGenerazione Albero ="+t+" Lista="+s); // 700.000 t=System.currentTimeMillis(); A=Genera_tree(700000); appunti Java pag.181 tf=System.currentTimeMillis(); t=tf-t; s=System.currentTimeMillis(); L=Genera_list(700000); sf=System.currentTimeMillis(); s=sf-s; System.out.println("N=700.000\nGenerazione Albero ="+t+" Lista="+s); } } Output: N 70.000 700.000 Albero Circa 550 ms Circa 9200 ms Lista Circa 500 ms Circa 6600 ms Anche in questo caso si nota che il rapporto tra i tempi di generazione della lista è circa 10/1, nell’albero tale rapporto tende ad aumentare fino a 20/1 a causa del maggior tempo necessario per riequilibrare un albero via via crescente.