appunti java – Capitolo 11 pag. 1 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 – Capitolo 11 Accesso sequenziale indicizzato pag. Dimensionamento Organizzazione Persistenza statico ordinata volatile dinamico disordinata permanente con duplicati senza duplicati 2 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 – Capitolo 11 pag. 3 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 Z ...... 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 – Capitolo 11 pag. 4 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. Package e Gerarchia java.util java.Object java.util.AbstractCollection java.util.LinkedList Si riportano costruttori e metodi: Classe LinkedList Costruttori LinkedList(); Metodi Invocazione: LinkedList L = LinkedList(); Effetti: Crea un oggetto Lista identificato da L vuoto. void add(Object); Appende l’elemento alla fine della lista void add(Index, Object); Appende l’elemento nella posizione specificata void addFirst(Object); Inserisce l’oggetto intesta boolean remove(Object); Rimuove l’oggetto specificato Object remove(Index); Rimuove l’elemento specificato in Index e restituisce l’oggetto Lista modificato void clear(); Rimuove tutti gli elementi della lista boolean contains(Object); Verifica se l’oggetto è presente nella lista int indexOf(Object); Restituisce l’indice della prima occorrenza dell’elemento specificato, -1 se non è presente boolean addAll(Collection); Appende alla fine della lista gli elementi della Collection Object getFirst(); Restituisce il I° elemento della lista Object getLast(); Restituisce l’ultimo elemento della lista Object get(Index); Restituisce l’ elemento della posizione specificata boolean isEmpty(); Verifica se la lista è vuota int size(); Restituisce la dimensione della lista Iterator iterator(); Crea un oggetto Iterator per scandire la lista ListIterator listIterator(): Crea un oggetto ListIterator per scandire la lista in entrambi i versi appunti java – Capitolo 11 pag. 5 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. Package e Gerarchia java.util java.Object java.util.Iterator Interface Iterator Creazione di un Si invoca un metodo che genera un Iteratore a partire da una Classe di iterator tipo "struttura dati" come ad esempio LinkedList, Vector, Set, … Metodi Esempio LinkedList L=new LinkedList(); .... Iterator iter = L.iterator(); boolean hasNext(); Restituisce True se si hanno ancora elementi Object next(); Restituisce il successivo elemento della lista Esempio: LinkedList L= ......; ......; Iterator iter=L.iterator(); // crea un iteratore while (iter.hasNext()) { // iter non è finito ? Object o = iter.next(); // leggi l'oggetto .....; } Effetti: Scorre l'iteratore ottenuto dalla lista dal primo all'ultimo oggetto. Interface ListIterator Creazione di un Si invoca un metodo che genera un Iteratore a partire da una Classe di ListIterator tipo "struttura dati" come ad esempio LinkedList, Vector, Set, … Metodi Esempio LinkedList L=new LinkedList(); .... ListIterator iter = L.listIterator(); boolean hasNext(); Restituisce True se si hanno ancora elementi Object next(); Restituisce il successivo elemento della lista boolean hasPrevious(); Restituisce True se esiste un elementoche lo precede Object previous(); Restituisce l’ elemento precedente della lista Esempio: 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() Effetti: Scorre l'iteratore ottenuto dalla lista dal primo all'ultimo oggetto e viceversa. appunti java – Capitolo 11 pag. 6 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 in un ciclo. 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 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 – Capitolo 11 pag. 7 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 { 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) } 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 – Capitolo 11 pag. 8 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: una tale struttura "ibrida" esiste ed è presente in java.util sotto forma di Classe. Di seguito si riportano la gerarchia e la tabella contenente costruttori e metodi principali della classe Vector. Package e Gerarchia Classe Costruttori Metodi N.B.: la descrizione è consultabile nella tabella della classe LinkedList java.util java.Object java.util.AbstractCollection java.util.Vector Vector Vector(); Vector(Collection c); Vector(int initCapacity); Vector(int initCapacity, int capacityIncr); Invocazione: Vector V = Vector(); Effetti: Crea un oggetto Vector identificato da V vuoto. 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() Appende l’oggetto alla fine Rimuove la dell’oggetto I° occorrenza Appende la Collection alla fine appunti java – Capitolo 11 pag. 9 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 – Capitolo 11 pag. 10 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 – Capitolo 11 pag. 11 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 – Capitolo 11 pag. 12 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. Di seguito si riportano la gerarchia e la tabella contenente costruttori e metodi principali della classe HashSet. Package e Gerarchia java.util java.Object java.util.AbstractCollection java.util.HeshSet Classe HashSet Costruttori HashSet(); HashSet(Collection c); Invocazione: HashSet A = new HashSet(); Effetti:Nel primo caso Crea un oggetto HashSet identificato da A vuoto. Nel secondo crea un set da un Vector eliminando i duplicati. Metodi void add(Object); boolean remove(Object); boolean removeAll(Collection); void clear(); boolean equals(HashSet); boolean contains(Object); boolean addAll(Collection); boolean isEmpty(); int size(); appunti java – Capitolo 11 pag. 13 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 – Capitolo 11 pag. 14 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 – Capitolo 11 pag. 15 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, appartengono al package e implementano i metodi sottoelencati: Interface Collection Classi che imple- Identificatori: mentano l’interfaccia Collection Metodi Package Identificatori: Class AbstractCollection Si ricorda che Abstract collection è genitrice di tutte le strutture 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(); java.util 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 – Capitolo 11 pag. 16 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 – Capitolo 11 pag. 17 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 – Capitolo 11 pag. 18 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 – Capitolo 11 pag. 19 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 – Capitolo 11 pag. 20 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 – Capitolo 11 pag. 21 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 – Capitolo 11 pag. 22 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 – Capitolo 11 pag. 23 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 – Capitolo 11 pag. 24 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 – Capitolo 11 Pckage e Gerarchia java.util pag. 25 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 – Capitolo 11 pag. 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(**) 26 appunti java – Capitolo 11 pag. 27 (*) 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 – Capitolo 11 pag. 28 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. appunti java- Capitolo 11 29 pag. 11.7 Sintesi sugli attributi delle strutture dati Non rimane infine che costruire una tabella di sintesi che metta a confronto tutte le strutture Dati studiate. La tabella sarà più dettagliata di quella originaria in quanto si cercherà di dare conto anche dei diversi tempi di inserimento/generazione di una struttura, cancellazione e ricerca di un dato. Struttura Dati Array (semi stat.) Mod. Accesso Sequen. Indiciz. Dim.nto Organizzazione Statico(*) Lista (LinkedList) Sequen. Dinamico Vector Sequen. Indiciz. Dinamico Albero (TreeSet) Insieme (HashSet) File Testo Iterator Con Key Iterator Con Key Sequen. Dinamico Dinamico Disordinata Ordinata(u) Duplicati Disordinata Ordinata(u) Duplicati Disordinata Ordinata(u) Duplicati Ordinata Senza Dup. Senza Dup. Dinamico Disodinata Perm.te File Dati Sequen. Indiciz. Dinamico Disordinata Ordinata(u) Duplicati Perm.te Sequen. Dinamico Volatile Sequen. Dinamico Ord. Di ins. Duplicati Ord. Di ins. Duplicati Strutture Filtro Coda Pila (Stack) Persistenza Volatile Lettura Dato Volatile Ripetibile Veloce (10-8) [°] Volatile Ripetibile Veloce (10-8) [°] Volatile Ripetibile Veloce (10-8) [°] Ripetibile Veloce (10-8) [°] Ripetibile Lenta (10-3) [°] Ripetibile Lenta (10-3) [°] Volatile Volatile Ripetibile Veloce (10-8) [°] Distruttiva Veloce (10-8) [°] Distruttiva Veloce (10-8) [°] Inserimento Cancell. Dato Dato In coda Ovunq. ma lento In mezzo (Lento) Sovrascr. In coda Ovunq. rapido In mezzo Ricerca un Dato O(n/2) O(lgn) (u) Generaz. struttura Rapida O(n/2) Rapida In coda In mezzo Sovrascr. In Ordine Ovunq. rapido O(n/2) O(lgn) (u) Rapida Ovunq. V. media O(lgn) V. media Senza Ordine Ovunq. rapido V. media In Coda Ricop. lentissima O(n/2) Molto Lenta In Coda Sovrascr. Ricop. lentissima O(n/2) O(lgn) (u) Molto Lenta In Coda In Testa rapida Rapida In Testa In Testa rapida Rapida (*) l’array in java è allocato dinamicamente ma non può crescere in modo dinamico. (u) significa che per ottenere l’ordinamento o una ricerca di complessità logaritmica è necessario un algoritmo dell’utente. O(lgn) appunti java- Capitolo 11 pag. 30 [°] la velocità di accesso 10-8 indica una tipica velocità di lettura in RAM, 10-3 una lettura su supporto esterno. Rapido, Semi Rapido, Lento, Molto Lento, Lentissimo, Si riferiscono rispettivamente: Rapido=velocità di semplice scrittura in Ram, Semi Rapido=scrittura in RAM con risistemazione di parte della struttura, Lenta=riscrittura in RAM di buona parte della struttura, Molto Lento=scrittura di un dato su dispositivo magnetico in sequenza, Lentissima = Riscrittura completa della struttura su disp. esterno. O(n/2) e O(lgn) significano rispettivamente che gli algoritmi di ricerca di un dato sono proporzionale a n/2 o al logn. appunti java- Capitolo 11 pag. 31 11.E – Esercizi Esercizi su Liste 1. Si desidera progettare un programma che "Acquisisca il suo input da un testo contenuto in una Stringa e inserisca ciascuna parola del testo in una Lista”. Indicazioni: Fare uso si uno StreamTokenizer per isolare le Parole del testo. Richieste: Progettare una classe opportuna ScanPar_01 che ottenga le funzionalità desiderate; Testarne il funzionamento con un main() opportuno. 2. Generalizzare la classe progettata nell’esercizio 1) e chiamarla ScanPar_02 in modo che “a partire da un Testo contenuto indifferentemente sia in una Stringa che in un File di testo, ottenga sempre la lista delle parole”. La classe dovrebbe fornire anche la funzionalità aggiuntiva restituire un array di parole. 3. Generalizzare la classe progettata nell’esercizio 1) e chiamarla ScanPar_03 in modo che “restituisca le parole anche in ordine Lessicografico sia in un array che in una lista”. 4. Generalizzare la classe progettata nell’esercizio 1) e chiamarla ScanPar_04 e progettarne una seconda di nome Nodo in modo che “restituisca un elenco Ordinato di Oggetti Nodo costituito delle parole contenute nel testo con a fianco il numero di volte con cui ogni parola compare”. 5. Generalizzare la classe progettata nell’esercizio 1) e chiamarla ScanPar_05 inserendo un metodo che “consenta di stampare l’indice analitico di una parola assegnata del testo scandito”. Per indice analitico si intende la restituzione della parola con a fianco i numeri delle righe sulle quali la parola si trova nel testo. Es. se sul problema 5 si cerca la parola testo si deve ottenere: testo : 3, 5, 6 Esercizi sui Vector 6. Progettare una classe Misto che “Generi un Vector casuale sia per la distribuzione che per i valori delle componenti che devono essere Integer o Double in un range identico” . Es. se si chiedono 5 elementi questi saranno casualmente Integer o Double e avranno valori Integer nell’intervallo [Min-Max] e Double nello stesso intervallo [Min.00000 – Max] 7. Generalizzare la classe del precedente esercizio 6) implementando un metodo che restituisca il Vector anche ordinato in modo crescente. 8. Generalizzare la classe del precedente esercizio 7) implementando “un metodo di ricerca Dicotomina di un valore Integer o Double assegnato”. Richieste: Se il numero è presente ne restituisce la posizione se no la posizione corretta di inserimento. 9. Dopo aver generato un Vector Ordinato di grandi dimensioni [5.000, 10.000, 20.000 numeri] usando le classi precedenti, si desidera realizzare un main() che confronti i tempi a) di Ordinamento del vettore, b) di Ricerca Dicotomica della classe( 8) c) di ricerca interna alla Classe Vector. Esercizi su Insiemi 10. “Progettare e implementare la sottoclasse Insieme della classe HashTree come indicato nel capitolo”. pag. 32 appunti java- Capitolo 11 11. Per testare la classe 10) “Generare un Insieme con i numeri da 1 a 25 e stamparlo, eliminare tutti i numeri multipli di 2 e 3 e stamparlo ancora" 12. Per testare la classe 10) "Generare due insiemi con le parole dei due testi distinti contenuti in due stringhe, stamparli, ottenere Unione, Intersezione e le due Differenze e stamparle ancora" 13. Costrure una classe che "che restituisca in un Insieme i numeri primi compresi tra 2 e N, con N scelto dall’utente. Si utilizzi l’algoritmo di Eratostene. " 14. Costrure una classe che "che restituisca in un Insieme i numeri primi compresi tra 2 e N, con N scelto dall’utente. Si utilizzi un algoritmo DIVERSO da quello di Eratostene " 15. Costruire un programma “che confronti la velocità di esecuzione dei due algoritmi precedenti” Esercizi su collezioni 16. “Progettare un programma main con il minimo numero di righe di codice che consenta di ottenere un Insieme e mostrarlo, partendo da una Lista di 20 elementi Integer generarati casualmente nel range [0..9] e quindi con elementi ripetuti”. 17. “Progettare un programma main con il minimo numero di righe di codice che consenta di ottenere un Vector, partendo da un Insieme di elementi Integer generarati casualmente nel range [0..9]. Stampare il Vector ottenuto dopo aver aggiunto due Integer uguali a 9”. Esercizi su Pile, Code e Liste 18. “Progettare una classe Coda come illustrato nel relativo paragrafo del testo”. 19. “Progettare un programma main che utilizzi uno Stack per ottenere una lista Decrescente da una lista Crescente di Numeri”. 20. “Progettare una Classe che ordini in modo Crescente una LinkedList qualsiasi di Double e restituisca la Lista Ordinata.” Vincoli: Si utilizzi l’algoritmo di Fusione descritto di seguito. Primo : se si dispone di due liste ordinate in modo crescente è sempre possibile fonderle per ottenere una lista ordinata. Procedura L3=fondi(L1, L2); L1=[2, 6, 8] L1=[2, 6, 8] L1=[6, 8] L1=[8] L1=[8] L1=[8] L1=[] L2=[1,6, 7] L2=[6, 7] L2=[6, 7] L2=[6, 7] L2=[7] L2=[ ] L2=[ ] L3=[ ] L3=[1 ] L3=[1, 2] L3=[1, 2, 6 ] L3=[1, 2, 6, 6 ] L3=[1, 2, 6, 6, 7 ] L3=[1, 2, 6, 6, 7,8 ] Secondo : Se una lista ha più di un elemento e non è ordinata dividerla in due liste L1,L2 =dimezza(L); Terzo : applica alla lista L il seguente algoritmo ricorsivo che restituisce la lista ordinata LF=MergeSort(L); public static LinkedList MergeSort(LinkedList L) { if (L.size()>1) { LinkedList K[]=dimezza(L); K[0]=MergeSort(K[0]); K[1]=MergeSort(K[1]); L=fondi(K[0],K[1]); } return L; pag. 33 appunti java- Capitolo 11 } Richieste : a. costruire una classe Fusione che contenga il metodo statico MergeSort() e i metodi statici privati fondi() e dimezza(); b. realizzare il main di prova che richiami un metodo statico genera(N) che restituisce una lista casuale di Double e quindi applichi MergeSort(). 21. “Progettare una Classe che ordini in modo Crescente un Vector qualsiasi di Double restituendo il risultato in un Vector.” Vincoli: Si utilizzi l’algoritmo Bubble Sort. 22. “Progettare una main che confronti la velocita dei due ordinamenti precedenti realizzati su List e Vector casuali di grandi dimensioni 5.000 o 10.000 elementi”. 23. “Progettare una Classe che consenta di disporre di un metodo per Valutare un’espressione aritmetica qualsiasi che abbia gli operatori +,-,*,/,^ usando uno Stack”. Indicazioni: L’algoritmo da realizzare è il seguente: si supponga di avere la seguente stringa di caratteri che rapresenta un’espressione aritmetica: 3+4/2-7*2^2 = 3+4/2-7*4 = 3 + 2 – 28 = 23. Si noterà che per VALUTARE l’espressione si devono eseguire le singole operazioni rispettando le priorità degli operatori. L’operatore con massima priorità è la potenza ^ seguono a pari merito * e / e quindi la priorità più bassa riguarda gli operatori + e - . Come realizzare il rispetto delle priorità degli operatori ? Se rispettare la priorità significa rimandare le operazioni a priorità inferiore allora si può utilizzare lo Stak il cui funzionamento simila un rinvio temporaneo. Ecco le operazioni logiche da svolgere con l’uso di due Stak, uno di char e il secondo di numeri. )ESP)ressione 3+4/2-7*2^2 +4/2-7*2^2 4/2-7*2^2 StakNum [ empty ] [3] StackOp [ empty ] [ empty ] Ris 0 0 [3] [+] 0 [+] [/, +] [/, +] [-, +] 0 0 0 0 [-, +] [*, -, +] [*, -, +] [^, *, -, +] [^, *, -, +] [*, -, +] 0 0 0 0 0 0 /2-7*2^2 2-7*2^2 -7*2^2 7*2^2 [4, [4, [2, [2, 3 3 4, 3 ] ] ] *2^2 2^2 ^2 2 [7, [7, [2, [2, [2, [4, 2, 2, 7, 7, 2, 7, 3 3 2, 2, 7, 2, 3] ] ] 3 3 2, 3 ] ] 3] ] Algoritmo Inizializza Oggetti Fintanto che (ESP Non è vuota) (A) Leggi un operando e mettilo nello SN (B) Leggi un Op.re confrontalo con l’Op.reSO Se (SO vuoto o Op.reSO<=Op.re) impila l’Op.re, altrimenti Fintanto che (Op.reSO > Op.re) estrai l’Op.reSO, estrai Due operandi dallo SN esegui l’operazione e impila il risultato nello SN. Poi impila l’Op.re Letto (A) (B) (A) (B) – ha prior. Inferiore a / quindi esegue e impila (A) (B) (A) (B) (A) (C)ESP è vuota: Fintanto che (SO non à vuoto) preleva Op.reSO, preleva 2 Num da SN, esegui operazione e Impila in SN (2^2)=4 pag. 34 appunti java- Capitolo 11 [28, 2, 3 ] [-26, 3 ] [-23 ] [] [ [ [ [ -, +] +] ] ] 0 0 0 -23 (C) 7*4=28 (C) 2-28=-26 (C) 3-26=-23 (D) SO è vuoto metti in SN in Ris 24. “Generalizzare la classe precedente introducendo nell’espressione aritmetica anche le parentesi tonde che indicano una ulteriore priorità”. Indicazioni: ripetere l’algoritmo precedente con la seguente variante opportunamente collocata; immettere le parentesi aperte sempre in SO, se si in contra una parentesi chiusa eseguire tutte le operazioni impilate fino alla parentesi aperta. Esercizi su Alberi 25. “Costruire un programma che acquisisca da tastiera (o da file di testo usando streamTokenizer) parole in ordine casuale, le inserisca in un Albero e stampi l’albero per verificarne l’ordinamento Lessicografico”. Richiesta: realizzare il tutto anche in un solo main(). 26. “Realizzare un programma come quello dell’esercizio 3) utilizzando un Albero”. 27. “Realizzare un programma come quello dell’esercizio 7) utilizzando un Albero”.