Progettazione del software - Dipartimento di Matematica

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