Progettazione del software Progettazione del software Progettazione del software • Per adattabilità si intende che il software potrebbe essere utilizzato a lungo e deve quindi prevedere cambiamenti, per tener conto di nuove esigenze. • Per riusabilità si intende che il software potrebbe essere usato per applicazioni diverse, ma simili, e quindi usato senza doverlo riscrivere completamente. • Quando si realizza del software ci si deve preoccupare, oltre che del fatto che il programma risolva il problema richiesto, anche che il programma rispetti alcuni principi: robustezza, adattabilità e riusabilità. • Per robustezza si intende che il software sia in grado di trattare anche input imprevisti, come ricevere in ingresso un dato negativo o nullo mentre è stato scritto per ricevere solo dati positivi; ad esempio il prezzo di un prodotto, un valore n per un elenco di dati, … Progettazione del software • La programmazione orientata agli oggetti aiuta nella realizzazione di tali principi mettendo a disposizione del progettista: astrazione, incapsulamento e modularità. (par. 3.1) • Più avanti parleremo di altre due caratteristiche della programmazione orientata agli oggetti: ereditarietà e polimorfismo. • Queste due caratteristiche permetteranno di realizzare il riutilizzo del codice. 1 Progettazione del software Progettazione del software • Per astrazione si intende la capacità di scomporre un problema complesso nelle sue parti fondamentali mettendo in evidenza le funzionalità di tali parti; il concetto di TDA permette di definire le funzionalità senza entrare nel particolare della loro realizzazione. • Per modularità si intende la capacità di poter progettare separatamente ed in maniera indipendente le diverse componenti che rappresentano le unità di funzionamento, unità che però devono poter interagire tra loro per ottenere la soluzione del problema. La modularità facilita sia il riutilizzo del software che i test per il buon funzionamento. • Per incapsulamento si intende la possibilità di non rivelare i dettagli interni della realizzazione. Esempio di progetto Esempio di progetto • Si vuole progettare una classe che rappresenti un conto bancario. Diamo alla classe il nome ContoBancario (BankAccount). Dobbiamo prima di tutto individuare quali sono le funzioni che questo concetto deve rappresentare: in un conto bancario noi possiamo versare dei soldi, prelevare dei soldi e richiedere il saldo. 2 Esempio di progetto Esempio di progetto • Pertanto il comportamento che deve avere un oggetto di tipo ContoBancario sarà rappresentato dai metodi: - versare denaro: deposito - prelevare denaro: prelievo - conoscere il saldo: rendiSaldo con i quali realizzare le operazioni richieste. • Da che cosa è rappresentato un conto bancario? Qual è lo “stato” di un conto bancario? • Un conto bancario è rappresentato dalla quantità di denaro presente al momento in cui lo si guarda: il saldo attuale. • La classe ContoBancario avrà come dato (campo, forma) il saldo: una variabile di istanza (campo d’esemplare) di nome saldo. (paragrafi 3.2-3.6) Esempio di progetto • Una variabile di istanza ha: un nome, un accesso, un tipo di dato, viene inizializzata esplicitamente o per default. • Come nome della variabile si sceglie un nome che ne ricordi il significato; come tipo un valore reale (ci sono i decimali). • L’accesso è opportuno che sia private: Esempio di progetto • Le variabili di esemplare sono di solito private perché in tale modo possono essere lette o modificate soltanto da metodi della classe a cui appartengono. • Sono nascoste (hidden) a chi utilizza la classe, e possono essere lette o modificate soltanto mediante l’invocazione di metodi pubblici della classe (incapsulamento o information hiding). • L’incapsulamento ha dei vantaggi. private double saldo; 3 Esempio di progetto Esempio di progetto • Il vantaggio fondamentale è quello di impedire l’accesso incontrollato allo stato di un oggetto, impedendo così anche che l’oggetto venga (accidentalmente o deliberatamente) ad assumere uno stato incoerente. • Il progettista della classe ContoBancario potrebbe definire (ragionevolmente) che soltanto un saldo non negativo rappresenti uno stato valido per un conto bancario. • Dato che il valore del saldo può essere modificato soltanto invocando i metodi per depositare o prelevare denaro, il progettista può impedire che diventi negativo, magari segnalando una condizione d’errore. • Se invece fosse possibile assegnare direttamente un valore al saldo dall’esterno, sarebbe impossibile gestire gli eventuali errori (il saldo potrebbe diventare negativo anche senza prelievi). Esempio di progetto Esempio di progetto • Ciascun oggetto, o esemplare o istanza, della classe ha una propria copia delle variabili di esemplare. • Possiamo decidere di aprire un conto con saldo iniziale 0, oppure con un saldo iniziale uguale ad un valore assegnato. Definiremo pertanto due costruttori. • Non c’è alcuna relazione tra le varabili di esemplare di oggetti diversi. Ciascuna può essere modificata indipendentemente dall’altra, così come ogni correntista ha un suo conto bancario ed un suo saldo. public ContoBancario(){/*primo costruttore*/ saldo = 0; } //secondo costruttore public ContoBancario(double saldoiniziale){ saldo = saldoiniziale;} • Per lo stesso motivo le uniche operazioni sono “depositare” e “prelevare” e non esiste un metodo con cui “decidere” il valore del proprio saldo. 4 Esempio di progetto Esempio di progetto • Anche in questa classe si hanno due costruttori e il compilatore effettua la risoluzione dell’ambiguità. • Se non trova un costruttore che corrisponda ai parametri forniti nell’invocazione, segnala un errore semantico • Per ogni metodo dobbiamo pensare a quale sarà la sua intestazione (firma: signature). // NON FUNZIONA! ContoBancario a = new ContoBancario("tanti soldi"); cannot find symbol symbol : constructor Contobancario (java.lang.String) location : class ContoBancario Esempio di progetto • Il metodo deposito deve permettere di depositare denaro; quindi avrà un parametro, double denaro e non dovrà restituire nulla. • Il metodo prelievo deve permettere di prelevare denaro; in maniera analoga a deposito avrà un parametro double denaro e non dovrà restituire nulla. • Il metodo rendiSaldo deve permettere di conoscere il saldo e quindi dovrà restituire un valore double che rappresenta il saldo e non avrà parametri. • • • • • l’accesso il tipo di dati restituito il nome gli eventuali parametri il corpo del metodo • Dal momento che stiamo progettando il comportamento dell’oggetto, la sua interfaccia pubblica, i tre metodi avranno accesso public. Esempio di progetto public class ContoBancario{ private double saldo; public ContoBancario(){//primo costruttore saldo = 0; } //secondo costruttore public ContoBancario(double saldoiniziale){ saldo = saldoiniziale; } 5 Esempio di progetto public void deposito(double denaro){ //realizzazione del metodo } public void prelievo(double denaro){ //realizzazione del metodo } public double rendiSaldo(){ realizzazione del metodo } }//fine classe Esempio di progetto public void deposito(double denaro ){ double nuovosaldo = saldo + denaro; saldo = nuovosaldo; } public void prelievo(double denaro){ saldo = saldo - denaro; } public double rendiSaldo(){ return saldo; } } //fine classe Esempio di progetto • Realizzazione dei metodi di Contobancario: • quando si deposita o si preleva una somma di denaro, il saldo del conto si incrementa o si decrementa con quella somma • il metodo rendiSaldo restituisce il valore del saldo corrente, memorizzato nella variabile saldo. • Per semplicità, questa prima realizzazione non impedisce che un conto assuma saldo negativo. Esempio di progetto /*Costruiamo una classe di prova che costruirà oggetti di tipo ContoBancario*/ public class ProvaContoBancario{ public static void main (String[] arg){ ContoBancario cliente1 = new ContoBancario(); ContoBancario cliente2 = new ContoBancario(1000); cliente1.deposito(5000); cliente2.prelievo(300); 6 Esempio di progetto //verifichiamo lo stato dell’oggetto System.out.println(" il primo cliente " + " possiede " + cliente1.rendiSaldo()+ " euro "); Conseguenza dell’incapsulamento • Poiché la variabile saldo di ContoBancario è private, non vi si può accedere da metodi che non siano della classe /* codice interno ad un metodo che non appartiene a ContoBancario */ double b = cliente1.saldo; // ERRORE System.out.println(" il secondo " + " cliente possiede "+ cliente2.rendiSaldo()+ " euro "); }//fine main errore semantico segnalato dal compilatore saldo has private access in ContoBancario double b = cliente1.rendiSaldo(); // ESATTO } I parametri dei metodi public void deposito(double denaro){ double nuovosaldo = saldo + denaro; . . . } • Vediamo cosa succede quando invochiamo il metodo cliente1.deposito(5000); • L’esecuzione del metodo dipende da due valori: • il riferimento all’oggetto cliente1 (parametro implicito) • il valore 5000 (parametro esplicito) • Quando viene eseguito il metodo, il suo parametro esplicito denaro assume il valore 5000: public void deposito(double denaro) I parametri dei metodi • Nel metodo vengono utilizzate due variabili: • denaro: è il parametro esplicito del metodo • saldo: è la variabile di esemplare della classe. • Sappiamo che di tale variabile esiste una copia per ogni oggetto. • Alla variabile saldo di quale oggetto si riferisce il metodo deposito? Come si fa a sapere quale saldo viene incrementato? • Il parametro implicito che invoca il metodo deposito è cliente1, pertanto è il saldo di cliente1. 7 Parola chiave this Parola chiave this • Per facilitare la comprensione e la lettura del codice riguardo all’appartenenza della variabile di istanza ad un oggetto, si può usare la parola chiave this. Si può scrivere: • Ogni metodo ha sempre uno ed un solo parametro implicito (dello stesso tipo della classe a cui appartiene il metodo). • La vera scrittura del metodo dovrebbe essere: this.saldo = this.saldo + denaro; • Con l’invocazione del metodo: cliente1.deposito(5000); il nome del parametro implicito cliente1 viene assegnato a this. public void deposito(double denaro){ this.saldo = this.saldo + denaro; /* this assume il tipo del parametro implicito */ } Parola chiave this • Solitamente non si usa scrivere this, perché la parola chiave this è uguale per tutti gli oggetti. (analogamente, null rappresenta un riferimento nullo per tutti gli oggetti). • Quando un metodo agisce su una variabile di esemplare, (deposito incrementa il saldo) è il compilatore che costruisce un riferimento alla variabile di esemplare (saldo) dell’oggetto rappresentato dal parametro implicito (cliente1). Utilizzare stringhe 8 Utilizzare stringhe • Consideriamo: String vuota = ""; "" String s = null; String p; System.out.println(""la stringa vuota ha lunghezza " + vuota.length()); • E le altre due stringhe? Cosa accade se scriviamo s.length() p.length() la lunghezza è 0, o si verifica un errore? Utilizzare stringhe • Attenzione: gli errori sono diversi: • La stringa s è stata inizializzata ma la sua referenza è null e pertanto l’esecuzione del programma si interrompe segnalando un errore di tipo NullPointerException. • La stringa p non è stata inizializzata e quindi non si può utilizzare il metodo length: errore in compilazione. Confrontare stringhe Confrontare stringhe • Abbiamo visto che per confrontare numeri interi si usa l’operatore di uguaglianza == . • Ci chiediamo ora se lo stesso operatore == si può usare anche con le stringhe. (par. 5.2.3) • Abbiamo visto che anche i numeri reali si possono confrontare con lo stesso operatore ==. • A volte è preferibile usare un confronto del tipo Math.abs(a-b) < epsilon (con epsilon=10-n e n compatibile con la precisione float o double), dato che i reali sono rappresentati in maniera approssimata. String s1 = "ciao""; String s2 = "ci"" + "ao""; //s2 = "ciao"" String s3 = s1; • Il risultato dei confronti if (s1 == s2) if (s1 == s3) sarà vero in entrambi i casi? Ci sarà un errore in compilazione? 9 Confrontare stringhe Confrontare stringhe • Consideriamo il confronto if (s1 == s2) il compilatore non segnala errore; ma quale è il suo significato? • Le variabili oggetto s1 ed s2 contengono ciascuna il riferimento al proprio oggetto: il primo costruito con l’assegnazione della costante “ciao”, il secondo con la concatenazione delle due costanti “ci” e “ao”. • I due oggetti sono distinti e così pure i riferimenti. Confrontare stringhe • Consideriamo il secondo confronto if(s1 == s3) sarà sicuramente vero perché c’è stata l’assegnazione s3 = s1, e pertanto i due riferimenti sono uguali: vedono lo stesso oggetto. • Per confrontare due stringhe e vedere se il valore contenuto sia uguale si deve usare il metodo equals. ciao ciao s1 s2 s3 Confrontare stringhe • Possiamo quindi scrivere: if (s1.equals(s2)) System.out.prinln(""le due stringhe sono uguali""); • Se invece vogliamo sapere se una stringa precede o segue un’altra stringa usiamo il metodo compareTo. if (s1.compareTo(""mondo"") < 0) System.out.println(""ciao "+ " precede mondo""); 10 Confrontare stringhe Confrontare stringhe • Il metodo equals. • È un metodo predicativo, restituisce false se i dati dei due oggetti sono diversi e restituisce true se sono uguali. • Il metodo compareTo. • È un metodo che restituisce un valore intero: >0 se s1 segue s2 s1.compareTo(s2) =0 se s1 è uguale s2 <0 se s1 precede s2 • La stringa s1 invoca il metodo compareTo: Confrontare stringhe Confrontare stringhe • Nel confronto tra stringhe, si effettua un confronto lessicografico secondo l’ordine dei caratteri UNICODE. • Il confronto inizia a partire dal primo carattere delle stringhe e procede confrontando a due a due i caratteri nelle posizioni corrispondenti, finché una delle stringhe termina oppure due caratteri sono diversi. if(s1.compareTo(s2) > 0) //s1 è più grande di s2: s1 segue if(s1.compareTo(s2) < 0) //s1 è più piccola di s2: s1 precede if(s1.compareTo(s2) == 0) //s1 è uguale a s2 • Pertanto: • se una stringa termina, essa precede l’altra • se terminano entrambe e l’ultimo carattere è uguale, sono uguali • altrimenti, l’ordinamento tra le due stringhe è dato dall’ordinamento alfabetico tra i due caratteri diversi. 11 Confrontare stringhe c | a | r | t |o | l | i | n |a c|a|r|t|o|l|a|i|o c|a|r|t|a c|a|r car < carta < cartolaio < cartolina Confrontare stringhe • Il confronto lessicografico genera un ordinamento simile a quello di un dizionario: infatti l’ordinamento tra caratteri in Java è diverso da quello alfabetico, perché tra i caratteri non ci sono solo lettere (e i numeri precedono le lettere), ma: • tutte le lettere maiuscole precedono tutte le lettere minuscole • il carattere “spazio” precede tutti gli altri caratteri Altri metodi sulle stringhe Altri metodi sulle stringhe • Un metodo importante è charAt. • Questo metodo ha per argomento un numero intero n (che deve essere compreso tra 0 e la lunghezza della stringa meno 1, altrimenti si genera un errore in esecuzione) e restituisce il carattere corrispondente alla posizione n. • Esempio. • Può essere utile considerare un singolo carattere invece di una stringa di lunghezza unitaria. • Infatti char è un tipo base mentre String è un oggetto: una variabile di tipo char occupa meno spazio in memoria di una stringa di lunghezza unitaria e le elaborazioni su variabili di tipo char sono più veloci di quelle su stringhe. String s = "ciao""; //lunga 4 char car = s.charAt(3); //car='o' car = s.charAt(0); //car ='c'; 12 Altri metodi sulle stringhe Altri metodi sulle stringhe • Se si vogliono considerare tutti i caratteri di una stringa per elaborarli, si utilizza un ciclo del tipo: • Una variabile di tipo char può essere argomento del metodo print e può essere concatenata con una stringa tramite l’operatore + e verrà convertita a stringa. • Una variabile di tipo char può anche essere confrontata con una costante di tipo char tramite l’operatore ==: String s = "Pippo"; for (int i=0; i < s.length(); i++){ char car = s.charAt(i); // elabora car } char car = s.charAt(1); if(car == 'i') System.out.println(car + "e' uguale a i "); Altri metodi sulle stringhe Scomporre stringhe • Se invece si vuole conoscere quale è la posizione di un carattere nella stringa si usa il metodo indexOf. • Può essere utile individuare in una stringa delle sottostringhe individuate da spazi. • Supponiamo di leggere una riga di testo composta da più elementi e di volerli poi considerare come valori per variabili di tipo diverso. Lo Scanner non considera spazi e tabulazioni con i quali separiamo le singole “parole” (token). • Per separare le varie componenti si può usare la classe StringTokenizer, del pacchetto java.util String s = "ciao""; int indice = s.indexOf('i'); //indice = 1 • Se il carattere non è presente il metodo restituisce -1 13 Scomporre stringhe Scomporre stringhe • Supponiamo che la linea di testo sia così composta: ciao 20 3.5 fine • Possiamo ora utilizzare sull’oggetto t i metodi: • nextToken(): restituisce una stringa che è il token, se c’è, ed avanza al prossimo, altrimenti dà errore; • hasMoreTokens(): restituisce true se ci sono ancora token nella stringa e false se sono finiti; • countToken(): restituisce un intero che rappresenta il numero di token nella stringa. Scanner in = new Scanner(System.in); String line; line = in.nextLine(); • Costruiamo un StringTokenizer: oggetto di tipo StringTokenizer t = new StringTokenizer(line); Scomporre stringhe Scomporre stringhe • Esempio. Vogliamo leggere i tutti i singoli token: • Se tra i token ci sono valori che vogliamo interpretare come numeri, dobbiamo utilizzare dei metodi che trasformano la stringa in numero (analogamente a quanto fanno i metodi nextInt, nextDouble, …): import java.util.StringTokenizer; . . . StringTokenizer t = new StringTokenizer(line); while(t.hasMoreTokens()){ String s = t.nextToken(); //utilizzare s } int a = Integer.parseInt(s); double b = Double.parseDouble(s); • Si tratta di metodi statici: nell’invocazione non c’è il nome di un oggetto ma c’è il nome di una classe. Sono le classi involucro: una per ogni tipo base. 14 Scomporre stringhe • Questi metodi possono essere utili anche per gestire un ciclo di lettura per variabili numeriche, che termina quando si inserisce una stringa con il significato di “fine dati”. boolean finito=false; System.out.println(""per terminare batti: " + "fine""); while(!finito){ String st = in.next(); //t.nextToken() if(st.equals(""fine"")) finito =true; else{ double a = Double.parseDouble(st); //elabora a }//fine else } Metodi statici • Un metodo statico non ha un parametro implicito; viene chiamato anche metodo di classe. (par. 8.6) • I metodi che hanno parametro implicito (che vengono invocati da un oggetto) si chiamano anche metodi di esemplare o di istanza. • I metodi statici si usano, ad esempio, quando si vogliono raggruppare istruzioni che coinvolgono numeri: i metodi della classe Math sono statici. Metodi statici Metodi statici • Un metodo statico viene definito in una classe; per utilizzarlo da un ambiente diverso dalla classe nella quale è definito, si invoca scrivendo il nome di tale classe. • Sintassi. NomeClasse.nomemetodo(…) • Anche il metodo main è statico: infatti quando la JVM attiva il main, non è stato creato ancora alcun oggetto, di conseguenza il primo metodo attivato deve essere statico. 15 Metodi statici Metodi statici • Spesso usiamo gruppi di istruzioni che risolvono problemi specifici e che non creano oggetti. • Allo scopo di aumentare la leggibilità del codice e di permetterne il riutilizzo, può essere utile costruire un metodo contenente quelle istruzioni; tale metodo non è legato ad un oggetto e pertanto può essere un metodo di classe. • Consideriamo le istruzioni per stampare un array. int v[] = new int [...]; //acquisire le componenti di v ... // stampare le componenti di v for(int i=0; i<v.length; i++){ System.out.print(v[i] + '\t'); if((i+1)%5 == 0) System.out.println(); } System.out.println(); Metodi statici Metodi statici • Supponiamo di dover stampare nuovamente l’array perché abbiamo fatto delle modifiche, oppure di avere un altro array da stampare; invece di scrivere più volte le stesse istruzioni, è conveniente costruire un metodo per eseguire la stampa: tale metodo sarà static: public static void stampa(int[] t){ for(int i=0; i<t.length;i++){ //istruzioni per la stampa } }//fine stampa • Dove scriviamo il codice di questo metodo? • I caso. Possiamo scrivere il metodo nella classe che contiene il main. • II caso. Possiamo costruire una classe che contiene metodi per l’uso di array, in analogia con i metodi della classe System: questa classe possiede metodi che effettuano, ad esempio, copie di array (o di parte di array) in altri array. 16 Metodi statici • I caso. Il metodo sta nella classe del main. public class Usoarray{ public static void main (String[] arg){ int[] v = new int [100]; int[] a = new int [20]; //leggi v e a . . . stampa(v); //invocazione del metodo stampa(a); }//fine main public static void stampa(int[] t){ //codice del metodo di stampa } }//fine classe Usoarray Metodi statici • II caso. Costruiamo una classe AlgoritmiArray in cui inseriamo tutti i metodi per un “comodo” uso di array: lettura, stampa, raddoppio, ridimensiona, copia,…. public class AlgoritmiArray{ public static void stampa (int[] t){ …… } public static void copia (int[] t, int[]w){ …… } //altri metodi }//fine classe AlgoritmiArray • Per invocare il metodo da fuori della classe: AlgoritmiArray.stampa(v); Metodi statici • Quando definiamo un array v dobbiamo scegliere la dimensione dell’array; per poter eseguire prove diverse con lo stesso array consideriamo una dimensione “abbastanza grande” e successivamente acquisiamo un valore n, che indica il numero effettivo delle componenti che l’array deve avere in quella applicazione. Tale numero dovrà essere compatibile con la dimensione: n ≤ dimensione (oppure n < dimensione a seconda dell’utilizzo oppure no della componente di indice 0). Metodi statici • Se è n < v.length, il nostro array ha delle componenti libere che vanno dall’indice successivo a quello dell’ultima componente fino a v.length-1. • Si dice allora che l’array è riempito solo in parte. 17 Metodi statici Metodi statici • Può però accadere che abbiamo bisogno di utilizzare l’array per un numero di componenti maggiori di v.length. • In questo caso utilizzeremo la tecnica del raddoppio con la quale andremo a costruire un nuovo array w di dimensione doppia di quello iniziale, nel quale copieremo i valori del primo e del quale andremo poi a copiare la referenza nella variabile v. //raddoppio di un array int w[] = new int [v.length*2]; //copiare v in w for (int k=0; k<v.length; k++) w[k]=v[k]; //copiare la referenza v=w; • Attenzione: copiare la referenza si può in Java perché v è un riferimento ad un array di interi (di dimensione qualunque); non è possibile fare ciò in altri linguaggi. Raddoppio di un array Metodi statici • Se voglio costruire un metodo per eseguire il raddoppio, dovrò fare in modo che la nuova referenza venga restituita: 00 00 11 11 44 w public static int[] raddoppio (int[] t){ //istruzioni per il raddoppio return t; } v 44 99 18 Metodi statici Metodi statici • L’invocazione del metodo (all’interno della classe che lo contiene) sarà: • È comodo gestire un’array “tutto pieno” perché l’ultima componente ha indice v.length-1. • Possiamo allora “buttare via” gli elementi non in uso. Vogliamo un metodo per “ridimensionare” l’array vale a dire un metodo al quale passare l’array e il numero di componenti inserite e che ci restituisca la nuova referenza. if(n == v.length) v = raddoppio(v); se invece siamo AlgoritmiArray fuori dalla classe v = AlgoritmiArray.raddoppio(v); Metodi statici public static int[] ridimensiona (int[] t , int n){ int[] w = new int[n]; for(int k=0; k<n; k++) w[k]= t[k]; //copia la referenza t=w; return t; } • Costruiremo anche metodi per: inserire elementi, togliere elementi, ecc.; in tale modo non ci accorgeremo che l’array è a dimensione fissa. if( n<v.length) v = ridimensiona(v,n); Metodi statici • La classe System mette a disposizione un comodo metodo per copiare i valori di un array in un altro. Molto probabilmente tale metodo sarà più efficiente del nostro. Perché non lo usiamo? • Perché noi stiamo imparando a scrivere algoritmi e pertanto dobbiamo scriverli da soli. • Però possiamo anche sapere come si usa (anche se non lo useremo) il metodo arraycopy della classe System. (par. 7.7) 19 Il metodo System.arraycopy double[] primo = new double[10]; // inseriamo i dati nell’array ... double[] secondo = new double[primo.length]; System.arraycopy(primo, inizio, secondo, inizio2, quanti); • Il metodo System.arraycopy consente di copiare un porzione di un array in un altro array (grande almeno quanto la porzione che si vuol copiare). Il metodo System.arraycopy System.arraycopy(primo,inizio,secondo,inizio2,quanti); primo secondo inizio2 inizio • Se vogliamo copiarlo tutto: System.arraycopy(primo, 0, secondo, 0, primo.length); quanti Inserimento e rimozione di elementi in un array Inserimento e rimozione di elementi in un array • L’eliminazione di un elemento da un array richiede due algoritmi diversi. • Caso I. L’ordine tra gli elementi dell’array non è importante e l’elemento da eliminare è unico: è sufficiente copiare l’ultimo elemento dell’array nella posizione dell’elemento da eliminare; si può successivamente ridimensionare l’array, oppure tenere l’array riempito parzialmente, diminuendo di 1 il numero degli elementi. 20 Inserimento e rimozione di elementi in un array //array tutto pieno double[] v = {1, 2.3, 4.5, 5.6}; int indice = 1; //eliminare 2.3 v[indice] = v[v.length - 1]; //la componente v[3]che vale 5.6 //va al posto di 2.3 v = ridimensiona(v,v.length-1); //array riempito solo in parte int n=4; v[indice] = v[n - 1]; n--; Inserimento e rimozione di elementi in un array //array tutto pieno double[] v = {1, 2.3, 4.5, 5.6}; int indice = 1; for(int i=indice; i<v.length-1; i++) v[i] = v[i+1]; //copia v[2] su v[1]; v[3] su v[2], ... v = ridimensiona(v,v.length-1); //analogamente con array riempito solo //in parte Inserimento e rimozione di elementi in un array • Caso II. Se invece l’ordine tra gli elementi deve essere mantenuto, l’algoritmo è più complesso: tutti gli elementi il cui indice è maggiore dell’indice dell’elemento da rimuovere devono essere spostati nella posizione con indice immediatamente inferiore (copiare il successivo sul precedente a partire dal primo indice che deve rimanere); si può successivamente ridimensionare l’array, oppure tenere l’array riempito parzialmente, diminuendo di 1 il numero degli elementi. Inserimento e rimozione di elementi in un array Senza ordinamento e unico indice Con ordinamento indice i trasferimenti vanno eseguiti dall’alto in basso 21 Inserimento e rimozione di elementi in un array Inserimento e rimozione di elementi in un array • Per inserire un elemento in un array si può: • aggiungerlo alla fine • in una posizione stabilita: in questo caso bisogna “fargli spazio”. Tutti gli elementi il cui indice è maggiore dell’indice della posizione voluta devono essere spostati nella posizione con indice immediatamente superiore: copiare un elemento sul successivo a partire dall’ultimo elemento dell’array. Infine introdurre l’elemento. double[] v = {1, 2.3, 4.5, 5.6}; //vogliamo inserire una nuova componente in //modo che l’array diventi //1, 2.3, 3.5, 4.5, 5.6 Inserimento e rimozione di elementi in un array Inserimento e rimozione di elementi in un array int indice = 2; for (int i = v.length - 1; i > indice; i--) v[i] = v[i - 1]; v = aumenta(v, v.length + 1); //v.length + 1 = 5 ora v ha una // componente in più, che rimane libera //copiamo le componenti di v a partire //dall’ultima indice //copia v[3] su v[4], v[2] su v[3], . . . //inserimento del nuovo valore v[indice] = 3.5; //l’array è: 1, 2.3, 3.5, 4.5, 5.6 //analogamente con un array riempito solo in //parte i trasferimenti vanno eseguiti dal basso in alto 22 Passaggio dei parametri Passaggio dei parametri • Nel metodi per array abbiamo visto che l’array era un parametro esplicito di un metodo ma era anche un valore di ritorno. • Ricordiamo che l’array è una struttura di dati, ma che in Java un array è realizzato come oggetto e pertanto il nome dell’array (variabile oggetto) contiene la referenza all’oggetto creato e tale referenza è uno scalare (l’area di memoria assegnata dal compilatore può contenere un solo valore). Passaggio dei parametri Passaggio dei parametri • In tutti i linguaggi di programmazione si hanno sottoprogrammi (funzioni, procedure, metodi) che rappresentano specifici problemi e che possono essere chiamati (invocati) da altri sottoprogrammi. • Nel metodo chiamante abbiamo le istruzioni con cui effettuiamo l’invocazione: • Tra il metodo “chiamante” e il metodo “chiamato” si ha un passaggio di informazioni. cliente1.deposito(500); v = ridimensiona(v,n); • Nella classe che contiene il metodo chiamato abbiamo la firma: public void deposito(double denaro) public static int[] ridimensiona (int[] t, int n) 23 Passaggio dei parametri Passaggio dei parametri • Si usano nomi diversi per indicare i parametri espliciti di un metodo: • argomenti o parametri effettivi, quando ci si riferisce ai parametri del metodo nell’istruzione di chiamata che compare nel metodo chiamante • parametri formali quando ci si riferisce ai parametri della firma del metodo chiamato. • L’argomento ha un valore che viene passato; il parametro formale assume un valore solo quando il metodo viene chiamato. • Il passaggio dei parametri avviene secondo due modalità dette: (Argomenti avanzati 8.1) • per valore • per indirizzo • Ci sono linguaggi, come C++, Pascal, che hanno entrambi questi passaggi: si utilizza un simbolo o una parola chiave per indicare le due modalità. • In altri linguaggi si ha un solo tipo di passaggio: in Fortran si ha solo il passaggio per indirizzo, in Java solo quello per valore. Passaggio dei parametri Passaggio dei parametri • Passaggio per valore. chiamante somma chiamato somma double somma=500; public void deposito(double somma) cliente1.deposito(somma); • Si tratta di due variabili diverse. Quando si attiva il metodo, il valore della variabile somma del chiamante viene “copiato” nella variabile somma del chiamato. • Se si effettua un passaggio per valore, la variabile del chiamato può anche assumere un nuovo valore (apparire a sinistra del simbolo di assegnazione); tale nuovo valore non apparirà nel chiamante, vale a dire: il valore nel chiamante non viene modificato. • È buona regola non modificarlo. (Consigli 8.3) • Si dice anche che la variabile di scambio (parametro esplicito) è in ingresso. 24 Passaggio dei parametri • Passaggio per indirizzo. chiamante chiamato a sub(a); void sub(int &a) // in C++ • Si tratta della stessa variabile. Quando si attiva il metodo, viene passato l’indirizzo di memoria della variabile del chiamante. Passaggio dei parametri • Vediamo cosa accade con gli array. • Il riferimento dell’array è passato per valore. Possiamo variare i valori contenuti nell’array? • Sì infatti, attraverso il riferimento possiamo accedere ai campi dell’array. • Esempio. Passaggio dei parametri • In tale modo l’area di memoria è visibile ad entrambi i metodi; pertanto ogni modifica fatta sulla variabile a nel chiamato si ritrova nel chiamante: la variabile ritorna modificata. • In Java si ha solo il passaggio per valore. • Quando passiamo un parametro esplicito, esso non torna indietro modificato. • Se vogliamo ottenere una modifica dobbiamo restituire quel valore come tipo di ritorno del metodo. Passaggio dei parametri //metodo chiamato public static void modifica(int[] t){ t[0] = 5; } //nel chiamante la componente 0 // ora vale 5 //chiamante int[] v = {1,2,3}; modifica(v); 25 Passaggio dei parametri • Quando utilizziamo un metodo per eseguire il raddoppio dell’array, non possiamo definirlo void e pensare che il nuovo riferimento “ritorni” al chiamante: l’area doppia è attiva nel chiamato, ma il riferimento nuovo non è visibile al chiamante. • Perciò dobbiamo definire un metodo per il raddoppio con il tipo “riferimento ad array”: Meccanismo di chiamata public static int[] raddoppio(int[]t) Meccanismo di chiamata • Una parte della memoria, RunTimeStack, mantiene le descrizioni delle attivazioni dei metodi: metodo M main ……. chiama M ……. quando si esegue l’istruzione di invocazione di un metodo, il “controllo” passa al metodo e quando il metodo è terminato, il controllo ritorna al chiamante Meccanismo di chiamata • Il main inizia la sua esecuzione; nell’ambiente riservato al main la variabile b viene inizializzata con il suo valore. Con la chiamata del metodo M si attiva un nuovo ambiente per M main { int b = 1; chiama M(b); ……. } metodo M(int a){ int x = 2; ... ritorno } 26 Meccanismo di chiamata main b 1 Ambiente riservato al main: c’è l’area di memoria per la variabile b. Con l’assegnazione b prende il suo valore. Meccanismo di chiamata metodo M x 2 a 1 main Sino a questo momento solo il main è attivo Meccanismo di chiamata main b 1 Quando il metodo termina (ritorno), l’ambiente creato per M viene eliminato. Quando termina il main tutto lo spazio è nuovamente libero. b 1 Il nuovo ambiente viene posto in cima formando una pila. Si ha un assegnamento di valori al parametro formale a e un assegnamento a quello locale x . Durante l’esecuzione del metodo M è attivo solo il suo blocco: la variabile b del main non è usabile. Blocchi annidati • Anche quando si hanno blocchi di codice annidati si costruisce una pila di ambienti nei quali le variabili locali sono note. • La differenza con l’ambito d’uso delle variabili dei metodi è che nel caso dei blocchi la visibilità si amplia, infatti sono visibili sia le variabili del blocco esterno che quelle del nuovo blocco; nelle chiamate dei metodi, invece, sono visibili solo le variabili del metodo in esecuzione in quel momento. 27 Blocchi annidati {int b=1; ... {//qui b è nota int a=b; int x=2; …. } //qui b è nota //a e x non sono note b=1 x=2 a=1 b=1 Esercizi sull’uso di cicli e array • Problema1. Dati n numeri (distinti oppure no) a1, a2, … an determinare quanti sono minori di a1. • Dopo aver risolto il Problema1 risolvere: • Problema2. Dati n numeri a1, a2, …,an determinare quanti sono minori di a1, a2, ,…, an b=1 } Cosa studiare nel libro Cosa studiare nel libro • Capitolo 7: tutto tranne paragrafo 7.8, Argomenti avanzati 7.2, 7.3 e 7.5, Consigli per la produttività 7.1 • Capitolo 8: tutto tranne i paragrafi 8.9 e 8.10 e Argomenti avanzati 8.2 • Capitolo 9: tutto tranne i paragrafi 9.4 e dal 9.6 alla fine e Argomenti avanzati 9.2 • Capitolo 10: tutto tranne i paragrafi 10.8.3 e dal 10.9 alla fine e Argomenti avanzati 10.1, 10.2, 10.6, 10.7 • Capitolo 11: tutto tranne i paragrafi 11.5 e 11.7 e Argomenti avanzati 11.1 • Capitolo 12: tutto tranne il paragrafo 12.5 • Capitolo 13: tutto tranne Argomenti avanzati 13.4 e 13.5 • Capitolo 14: tutto tranne il paragrafo 14.3 e Argomenti avanzati 14.1 e 14.2 • Capitolo 15: tutto tranne i paragrafi 15.2 e dal 15.5 alla fine e Consigli per la qualità 15.1 • Capitolo 16: No • Appendici: B, C, E, F, G, H, I 28 Ricerca lineare (selezione, scansione) Algoritmi di Ricerca • Consideriamo un insieme di elementi (numeri, caratteri, stringhe, oggetti di un tipo di dato,…) e vogliamo risolvere il problema di stabilire se un elemento dato appartiene oppure no all’insieme. (par. 13.6) • L’idea è quella (analoga a quella vista per la somma e il prodotto) di considerare uno ad uno gli elementi (dal primo all’ultimo) e di confrontarli con l’elemento dato. Ricerca lineare Ricerca lineare • Come possiamo considerarli tutti una ed una sola volta? • Cosa significa essere uguali per numeri, stringhe, oggetti di tipo qualunque? • Quale tipo di risposta vogliamo: è presente, non è presente oppure vogliamo poter dire “è in una certa posizione all’interno dell’insieme dato”? • Se vogliamo anche indicazione sul ”dove si trova”, abbiamo bisogno di “mettere in fila” gli elementi. • Pertanto, consideriamo una struttura di dati in cui inserire gli elementi: array; gli elementi sono considerati in sequenza: sappiamo che esiste un primo elemento, per ognuno c’è il successivo, esiste l’ultimo. • Supponiamo, per ora, che questi elementi siano dei numeri: sappiamo cosa significa dire che due numeri a e b sono uguali o diversi. Vedremo poi cosa vuole dire che due oggetti (dello stesso tipo) sono uguali e come facciamo ad eseguire un confronto tra oggetti. 29 Ricerca lineare Ricerca lineare • Problema. Sia A un array di numeri (interi) e sia b un elemento dello stesso tipo di A. Vogliamo rispondere alla domanda: b è un elemento di A? Se c’è, dove si trova? • Analisi. • Sia A = (a1, a2, … an); esiste un indice i (compreso tra 1 ed n) tale che ai = b? • I caso. Gli elementi di A sono tutti distinti tra loro: se esiste l’indice i tale che sia ai = b, esso è unico. • Dobbiamo esaminare una sequenza di valori e per ognuno di essi ci chiediamo se ai == b • Abbiamo quindi una struttura iterativa. • Quale predicato governa la struttura? 1. Dobbiamo esaminare tutti i numeri: avremo un indice che “scorre” gli elementi dal primo all’ultimo, passando al successivo. 2. Ci fermiamo quando lo abbiamo trovato (se c’è è unico). Ricerca lineare Ricerca lineare • Abbiamo un predicato composto che diventa falso non appena uno dei due predicati che lo compongono risulta falso. • La ricerca dell’elemento prosegue (finché ci sono numeri) e (finché non è stato trovato) • Progetto. Per il primo predicato utilizziamo un indice intero che descrive tutti gli elementi; • per il secondo predicato utilizziamo una variabile booleana “trovato” che viene inizializzata falsa, per considerare il caso di elemento non presente, e che diventa vera se l’elemento è presente. • Poiché abbiamo una struttura iterativa, dovremo occuparci del problema della terminazione del ciclo. 30 Ricerca lineare • Scriviamo lo schema dell’algoritmo, usando un linguaggio di pseudocodifica: • diamo dei nomi alle variabili: a array, b l’elemento da cercare (dello stesso tipo), i l’indice per accedere agli elementi dell’array, n il numero effettivo di componenti dell’array sulle quali effettuare la ricerca, trovato la variabile booleana. • indichiamo quali sono le operazioni di acquisizione e di stampa dei dati • rappresentiamo esattamente la struttura iterativa: predicato e corpo del ciclo. Ricerca lineare Algoritmo ricerca lineare definizione variabili a array, b intero n, i intero trovato booleano acquisire e stampare: n, le n componenti di a e il valore b // inizializzazione delle variabili che governano //la struttura iterativa: c’è un legame con il predicato i←0 trovato ← falso Ricerca lineare (selezione) Ricerca lineare (selezione) mentre i ≠ n e non trovato eseguire i ← i+1 se a[i] == b allora trovato ← vero //finescelta //fine ciclo mentre se trovato //P e Q: quale è diventato falso? allora stampa “trovato al posto “ i altrimenti stampa “ elemento non presente” //fine scelta //fine algoritmo • Per verificare l’algoritmo, possiamo rappresentare il tracciato (tracing) dei valori assunti dalle variabili durante l’esecuzione del programma su una particolare scelta di dati (processo): costruiamo una tabella dopo aver assegnato dei valori ad n, A e b. • Quando si “prova” un programma, questo tracciato si ottiene eseguendo della stampe dei valori della variabili. • Esempio. n=5 A = (7, 1, -3, 0, 4) b=0 31 Tracing n i trovato 5 0 F i ≠ n non trovato P i a[i] a[i] == b trovato sì V V 1 7 no F sì V V 2 1 no F sì V V 3 -3 no F sì V V 4 0 sì V iterazione 1° 2° 3° 4° Tracing b=0 sì F F il ciclo termina i=4 trovato = V Codifica • Gli array in Java hanno un indice che inizia da 0. • In molti problemi (formule matematiche) la sequenza inizia da 1; è meglio iniziare da 1 o da 0 se si vuole scrivere l’algoritmo nel linguaggio di programmazione? • Iniziare da 1 significa lasciare un posto vuoto a sinistra della prima componente che mai andrà utilizzato, diversamente da quelli vuoti a destra (nel caso in cui n<dimensione). n i trovato 5 0 F i ≠ n non trovato P i a[i] a[i] == b trovato sì V V 1 7 no F sì V V 2 1 no F sì V V 3 -3 no F sì V V 4 0 no F sì V V 5 4 no F iterazione 1° 2° 3° 4° 5° b = 15 no V F il ciclo termina i=5 trovato = F Codifica • Vantaggi con inizio i=1: • si rispetta la formula matematica; posizione ed ordinale coincidono (il primo è al posto 1, il secondo al posto 2, …); però iniziando da 1 il numero massimo n di elementi sarà n=dimensione-1. • Vantaggi con inizio i=0: • in previsione di utilizzare le tecniche di “raddoppio”, l’array non perde posti a sinistra ed n coincide con la dimensione dell’array; però bisogna ricordare che il primo elemento è al posto 0, il secondo al posto 1, … 32 Codifica • Cosa cambia nell’algoritmo nella codifica in Java? • Gli elementi saranno: a0, a1, …, an-1 e pertanto cambia l’intervallo di variabilità dell’indice i (inizia una posizione fuori dall’intervallo) i ← -1 mentre i ≠ n-1 e non trovato eseguire … Codifica Codifica // inizio algoritmo di ricerca ... //definizione delle variabili ... //acquisire e stampare i dati iniziando //dall’indice 0 trovato = false; i = -1; while((i != n-1) && (!trovato)){ i++; if(a[i] == b) trovato = true; } Varianti /*il predicato del ciclo e’ composto: dobbiamo sapere quale dei due (o tutti e due) all’uscita e’ falso */ • Per scrivere una cosa generale, possiamo definire due variabili intere inizio, fine a cui attribuire i valori per rappresentare l’indice del primo (0 oppure 1) e dell’ultimo elemento dell’array; l’inizializzazione sarà: if(trovato) System.out.println("trovato al posto " + i); else System.out.println("elemento non " + " presente" ); • Possiamo anche iniziare con l’indice a partire dal primo valore; come cambiano allora il predicato e la posizione dell’incremento? indice=inizio-1. • Abbiamo visto un legame tra inizializzazione, condizione e incremento i++: “non è sempre così facile” … 33 Varianti //l’array ha componenti: a[inizio], …, a[fine] i ← inizio trovato ← falso mentre i ≠ fine+1 e non trovato eseguire se a[i] == b allora trovato ← vero altrimenti i ← i+1 //finescelta //fine ciclo mentre //con inizio = 0 e fine = n-1 //oppure inizio = 1 e fine = n Varianti • Poiché il primo elemento è subito disponibile, la prima istruzione dell’iterazione sarà il confronto tra a[i] e b. • Non abbiamo messo l’incremento di i come ultima istruzione dell’iterazione ma in alternativa: tale incremento viene eseguito solo se il predicato è falso. • Potevamo mettere l’incremento in sequenza alla if, come ultima istruzione? Varianti Varianti • Se l’incremento di i è in sequenza con if esso viene sempre eseguito; pertanto nel caso in cui l’elemento sia presente (trovato=vero), l’indice i non indica più la posizione, ma quella successiva. • Per “sistemare” le cose: • Dobbiamo necessariamente utilizzare una variabile logica nel predicato? L’utilizzo delle variabili logiche serve a migliorare la lettura dell’algoritmo; una variabile logica occupa poco posto e non necessita di confronti (si ha solo un accesso). • Esercizio. Riscrivere l’algoritmo senza fare uso della variabile logica trovato. • si stampa i-1, • oppure si assegna ad altra variabile posiz il valore dell’indice in cui si trova l’elemento, • oppure … l’importante è ricordare che una operazione in sequenza ad una if viene eseguita sempre: con predicato vero e con predicato falso. • Suggerimento: inserire nel predicato il confronto a[i] != b 34 Ricerca lineare: elementi ripetuti Ricerca lineare: elementi ripetuti • II caso. Supponiamo che gli elementi di A non siano tutti distinti tra loro: un elemento può essere presente in più punti dell’array. • Il problema diventa quello di determinare in quali posizioni si trova l’elemento: trovare il valore di tutti gli indici i per i quali si ha: ai = b • Analisi. • Per risolvere questo problema, cosa dobbiamo cambiare nell’algoritmo precedente? • La struttura iterativa deve proseguire fino alla fine della sequenza: la valutazione della variabile trovato non è più necessaria; avremo un ciclo che esamina tutte le componenti: in questo caso si può utilizzare il ciclo for. • Possiamo o stampare gli indici in cui si verifica l’uguaglianza oppure mantenerli per un problema successivo: in questo secondo caso abbiamo bisogno di memorizzare i valori degli indici, utilizzando un array. Ricerca lineare: elementi ripetuti Ricerca lineare: elementi ripetuti • Chiamiamo posiz l’array che conterrà i valori degli indici: sarà di tipo intero e avrà la stessa dimensione dell’array a. Si dovrà anche definire una variabile, k, che descrive gli elementi dell’array posiz. • Nell’iterazione ci sarà l’incremento di k ogni volta che il confronto a[i]==b è vero. • Dobbiamo anche dare una risposta al caso “non trovato”: se k non varia dal suo valore iniziale significa che l’elemento non è presente. Algoritmo ricerca2 definizione variabili a[100], posiz[100], b, k, n, i, primo, ultimo acquisire e stampare valori per b, n, a inizializzare primo e ultimo k←0 per i da primo a ultimo eseguire se a[i] == b allora k ← k+1 posiz[k] ← i //finescelta //fineciclo intero 35 Ricerca lineare: elementi ripetuti se k==0 allora stampa “elemento non presente” altrimenti stampare i primi k valori dell’array posiz: posiz[1], …, posiz[k] //finescelta /* l’algoritmo non dipende da quale valore (i=0, oppure i=1) abbiamo scelto per la memorizzazione del primo elemento */ Ricerca lineare: elementi ripetuti • Codifica e implementazione. • Scegliere i valori per primo e ultimo e acquisire i dati (iniziando con l’indice 0 oppure 1); il ciclo da usare è il ciclo for. • Esercizio. Come cambia l’algoritmo se “sappiamo” che b è presente (ricerca lineare certa)? • Soluzione: si può omettere il predicato i ≠ fine+1 perché sicuramente il ciclo termina con trovato=vero. Noi però useremo sempre l’algoritmo che utilizza i due predicati. Ricerca lineare: elementi ripetuti Ricerca lineare: array ordinato • Esercizio: tradurre in Java il progetto di algoritmo e scegliere dei casi di prova significativi: • III caso. Supponiamo ora che gli elementi dell’array siano ordinati (ordine crescente). Vediamo come cambia l’algoritmo di ricerca. • elemento non presente (posiz è vuoto) • elemento presente al primo e all’ultimo posto • elementi tutti uguali (posiz contiene tutti gli indici di a) • elemento presente una, due, … volte in posizioni alterne • Eseguire la verifica con valori noti. • Analisi. • In questo caso ha senso solo considerare il caso di elementi distinti: aprimo< … < ai < ai+1 < …. < aultimo (nel caso di elementi ripetuti si cerca il primo e si esaminano i successivi fino a trovare un elemento diverso). 36 Ricerca lineare: array ordinato Ricerca lineare: array ordinato • Per prima cosa si deve verificare se la ricerca è compatibile con i dati: ha senso eseguire la ricerca solo se aprimo ≤ b ≤ aultimo • La struttura iterativa del primo algoritmo, cambia per tenere conto di questa nuova ipotesi di dati ordinati: dato l’ordinamento, se si trova un valore dell’indice i per il quale si ha ai > b allora è inutile proseguire (ai+2 > ai+1 > ai > b) • Esempio. a = (1, 3, 5, 6, 9, 11, 23, 45, 56, 57, 70, 102) b=7 dopo il confronto con il valore 9 , i confronti successivi sono inutili. • Volendo costruire un algoritmo il più efficiente possibile, possiamo ritenere che le operazioni inutili siano “errate”. Ricerca lineare: array ordinato Ricerca lineare: array ordinato • Il predicato era composto: (finché ci sono elementi) e (finché non è trovato). • Vediamo come cambia l’iterazione. • Nel caso in cui non sia più trovabile, dobbiamo interrompere la ricerca: abbiamo un terzo predicato. • Possiamo usare ancora una variabile booleana trovabile il cui valore è vero, se la ricerca è possibile, e che diventa falso se ai > b. • Il predicato della struttura iterativa è composto da tre predicati uniti con and. • Esercizio: codifica e implementazione. se ai < b allora passare al successivo altrimenti se ai == b allora trovato diventa vero altrimenti non è più trovabile //caso ai > b 37 Ricerca binaria (dicotomica) • Supponiamo di cercare un nome in un elenco ordinato di nomi: vocabolario, elenco telefonico. Sfruttiamo l’ordine lessicografico (alfabetico) e incominciamo a “dividere” l’elenco in due parti pensando alla iniziale del nome, poi alla lettera successiva, ... non iniziamo dalla prima parola dell’elenco se cerchiamo un nome che inizia per M. • Analogamente se pensiamo dei numeri appartenenti ad un intervallo sulla retta. Ricerca binaria (dicotomica) • Caso b < aim • Gli elementi di destra non si guardano a1 b aim aim • Analisi. • Consideriamo l’array a = (a1, a2, … an) a1 aim an con im = (1+n)/2 indice di mezzo • Esaminiamo aim : se b = aim allora abbiamo trovato l’elemento, altrimenti vediamo se risulta b<aim oppure b>aim e proseguiamo la ricerca solo nella parte “possibile”. Ricerca binaria (dicotomica) • Indichiamo con is e id gli indici estremi della parte di array che stiamo guardando: ais ≤ b ≤ aid an • Caso b > aim • Gli elementi di sinistra non si guardano a1 Ricerca binaria (dicotomica) b • Tali valori vengono inizializzati con i valori dell’indice del primo (1 o 0) e dell’ultimo (n o n-1) elemento. an 38 Ricerca binaria (dicotomica) • Si confronta b con aim e se risulta aim≠b cambiamo il valore degli estremi is e id: questo è il modo per “non guardare più” una parte di array: se b < aim cambierà id se b > aim cambierà is • Perciò il valore di id diminuisce e quello di is aumenta: per proseguire la ricerca dovrà essere sempre is ≤ id. Ricerca binaria (dicotomica) • Algoritmo ricerca binaria definizione variabili a array , b, n, is, id, im trovato acquisire e stampare n, a, b se a[1] <= b e b <= a[n] allora //eseguire la ricerca is ←1 id ← n trovato ← falso intero logico Ricerca binaria (dicotomica) • Esempio. a = ( 1 , 5, 6, 8, 11, 15) n = 6 b=5 1) is = 1 id = 6 im = (1+6)/2 = 7/2 = 3 aim = a3 = 6 e 6≠5 aim > b id varia e assume il valore im-1: id=2 2) is = 1 id = 2 im = (1+2)/2 = 3/2 = 1 aim = a1 = 1 e 1≠5 aim < b is varia e assume il valore im+1: is=2 3) aim = a2 = 5 e 5=5 trovato= vero Ricerca binaria (dicotomica) mentre non trovato e is <= id eseguire im ← (is+id)/2 se a[im] == b allora trovato ← vero altrimenti //scegliere dove proseguire se b < a[im] allora id ← im-1 altrimenti is ← im+1 //finese //finese //finementre 39 Ricerca binaria (dicotomica) se trovato allora stampa “trovato al posto “ im altrimenti “non trovato //finese altrimenti stampa “b è esterno ad a” //finese //fine algoritmo Algoritmi di ricerca nel libro • Gli algoritmi sono presentati come metodi che restituiscono un valore intero che è la posizione dell’elemento, se questo è presente, oppure è -1 se l’elemento non è presente. • Attenzione. • Nell’algoritmo di ricerca lineare viene usato un ciclo for dal quale si esce con un return i; //se trovato: si esce dal ciclo return -1; //se non trovato Algoritmi di ricerca nel libro Algoritmi di ricerca nel libro • Nell’algoritmo di ricerca binaria c’è un ciclo while dal quale si esce con un criterio analogo: • Non è molto “elegante” uscire da una struttura iterativa con “return”: è contro lo stile della programmazione strutturata, perché il ciclo ha due uscite, ma soprattutto è una tecnica da usare con molta cautela: troppi return rischiano di far perdere il controllo di ciò che l’algoritmo sta eseguendo. return mid; // im se trovato return -1; //se non trovato • Costruiremo una “nostra” classe Ricerca con i metodi di ricerca e cicli con una sola uscita. 40