Sommario
Costo di un programma
Problema: dato un array a di interi ordinati in modo crescente, determinare se l'intero k è presente in a.
Es. array a: [1,3,9,17,34,95,96], intero k: 50.
Soluzione: consideriamo il seguente funzione che realizza l'algoritmo di ricerca sequenziale:
public static boolean ricercaSequenziale(int[] a, int k) { for (int i = 0; i < a.length; i++) if (a[i]==k) return true; return false; }
La funzione ricercaSequenziale
realizza un algoritmo corretto
per risolvere il problema (nota, esso in realtà non sfrutta l'ordinamento
dell'array).
Ma quanto costa la sua computazione? Esistono funzioni/algoritmi più efficienti?
Il costo computazionale di una funzione/programma è un costo definito in termini di risorse di calcolo.
Le risorse di calcolo fondamentali sono due:
Esistono altre risorse di calcolo che possono essere rilevanti in casi particolari (non trattate in questo corso), ad esempio
A che serve stabilire il costo computazionale di una funzione/programma/algoritmo?
A confrontare diverse funzioni/programmi/algoritmi che risolvono lo stesso
problema, in modo da scegliere il più efficiente.
Concentriamoci sul tempo di calcolo.
La prima risposta che viene in mente è facciamo una prova: scegliamo una particolare configurazione dei dati in ingresso ed eseguiamo il programma misurando il tempo.
Es. Configurazione dei dati in ingresso (input): array a: [1,3,9,17,34,95,96],
intero k: 50. Risultato calcolato(output): false
. Tempo
cronometrato: 10 µsec.
Che cosa sappiamo dopo questo esperimento? Quasi niente!!!
Conosciamo un numero corrispondente ad un dato input. Da cosa è influenzato questo numero?
Questa misura non è soddisfacente per capire il costo computazionale insito dell'algoritmo che la nostra funzione realizza (approssimato). Noi vogliamo conoscere questo costo in modo (necessariamente) approssimato:
Una misura del costo computazionale soddisfacente deve:
Cioè siamo interessati al costo (o complessità) asintotico nel caso peggiore.
Esempio
Ad esempio diremo che ricercaSequenziale
ha un costo lineare,
cioè il costo è una funzione che varia come
a n + b
dove n e' la dimensione dell'array (dim. dell'input) e a e b sono due fattori costanti indipendenti dalla dim. dell'input.
Vedremo che ricercaBinaria
ha un costo logaritmico,
cioè il costo e' una funzione che varia come
a log(n) + b
dove n è la dimensione dell'array (dim. dell'input) e a e b sono due fattori costanti indipendenti dalla dim. dell'input.
Ne segue che ricercaBinaria
e' un algoritmo migliore
(piu' efficiente) di ricercaSequenziale
.
Ci concentriamo sul costo in tempo.
Idea fondamentale del modello astratto di macchina proposto: ogni istruzione atomica ha costo unitario.
if (<condizione>) <parte if> else <parte else>
costo dell'istruzione if-else
è: c1
+ cif se la condizione è vera e c1
+ celse se la condizione è falsa,
dove c1 è il costo della
condizione, cif
il costo della parte if e celse il costo della parte
else
for
, si possono ridurre
a questa):
while (<condizione>) <corpo del ciclo>
costo dell'istruzione while è: SUMi = 1..n_rip(ci+di)
dove:
{ <ist_1> ... <ist_n> }
costo dell'istruzione blocco e': c1+ ... + cn
Nota: stiamo assumendo che
v[i]==10
e
v[i]==10*10*10*10/4*10*10*10/3*10*10/2*10*10/10*10
abbiano entrambe costo 1, nonostante la seconda sia sicuramente piu' costosa
delle prima.
Il punto e' che il suo costo non dipende dalla dimensione dell'input!!!
Esempio
public static boolean ricercaSequenziale(int[] a, int k) { for (int i = 0; i < a.length; i++) if (a[i]==k) return true; return false; }
Es1 input a: [1,3,9,17,34,95,96], k:9
Costo:
int i = 0;
)i < a.length
)a[i] == k
)i++;
)return true;
)Es2 input a:[1,3,9,17,13,95,96], k:50
Costo:
int i = 0;
)i < a.length
)a[i] == k
)i++;
)return false;
)Caso peggiore
Il passo successivo e' liberarsi della particolare configurazione dei dati in ingresso.
Per fare questo, nel seguito concentreremo la nostra attenzione sul caso peggiore.cioè la configurazione dell'input piu' sfavorevole per il programma/algoritmo. Perché?Quale e' il caso peggiore per ricercaSequenziale
?
L'elemento cercato k non occorre nell'array a.
Costo per un array di dimensione n (dimensione dell'input):
int i = 0;
)i < a.length
)a[i] == k
)i++;)
return true;
)Nota 3 n + 3 è una funzione (non un semplice numero) ed n,
cioè la dimensione dell'array, è la dimensione dell'input!
La ricerca sequenziale ha un costo lineare.
Alternative al caso peggiore
ricercaSequenziale
dice che il costo e' costante!)Caso medio (cenni)
Vogliamo considerare il costo nei casi reali o comuni.
Idea: uso teoria delle probabilità
Esempio: Consideriamo ricercaSequenziale
assumiamo che k appartenga effettivamente al vettore, e consideriamo una distribuzione uniforme, tutte le configurazioni dell'input equiprobabili.
In altre parole, k si trova in posizione i-esima (1 <= i <= n) con probabilità 1/n, dove n e' la dimensione dell'array.
Costo nel caso k si trovi in posizione i-esima:
int i = 0;
)i < a.length
)a[i] == k
)i++;)
return true;
)Ora calcoliamo la media:
SUM i=1..n 1/n (3 i + 1) = 1/n (3 (SUM i=1..n i) + n), si ricorda che SUM i=1..n i = ((n+1) n) / 2, quindi abbiamo 3/2(((n+1) n)/n) + n/n = (3n + 5)/2, cioè ancora costo lineare.
Nota: quale è il problema di questo tipo di analisi?
La scelta della distribuzione!!! In generale è molto difficile scegliere la distribuzione in modo da rispecchiare i "casi reali"!
Dimensione dell'input
Il costo di un programma/algoritmo e' una funzione dell'input.
Esempio: Il costo (nel caso peggiore) di ricercaSequenziale
e' 3n+3, dove n, la dimensione dell'array (cioè il
num. dei suoi elementi), è il parametro che caratterizza la dimensione
dell'input.
La scelta del parametro che caratterizza la dimensione dell'input e' spesso semplice:
Tuttavia ci sono casi più complessi:
Esempio
public static int fattoriale(int n) { int fatt = 1; int i = 1; while (i<=n) { fatt = fatt*i; i++; } return fatt; }
Costo:
int fatt = 1;
)int i = 1;
)i <= n
)fatt = fatt * i;)
i++;)
return fatt;
)Cioè il calcolo del fattoriale è lineare?
Attenzione quale è il parametro che caratterizza la dimensione dell'input?
Nota: abbiamo assunto che fatt = fatt*i;
costi 1, ma
in realta' essa dipende dall'input! (il fattoriale cresce ad ogni iterazione
e quindi cresce il costo dell' istruzione). Per tenere conto di questo potremo
adottare una macchina astratta piu' sofisticata che associa alle istruzioni/condizioni
atomiche costi logaritmici. Tuttavia, tranne in casi particolari (vedi corso
di Algoritmi e Strutture Dati) questa analisi piu' sofisticata non e' necessaria.
0 | 1 | 1 | 1 | 1 |
0 | 0 | 2 | 2 | 2 |
0 | 1 | 0 | 3 | 3 |
0 | 0 | 1 | 0 | 4 |
0 | 1 | 2 | 1 | 0 |
Per farlo possiamo usare il seguente programma:
public static void main(String[] args) { int n=5; for (int riga =1; riga<=n; riga++) { for (int col =1; col<=n; col++) System.out.print(riga%col+" "); System.out.println(); } }
Determinarne il costo.
Esercizio 2 Si consideri la funzione:
public static int somma(int m, int n) { // m, n >= 0 while (n > 0) { m = m+1; n = n-1; } return m; }
Determinarne il costo. (Suggerimento si chiarisca inizialmente quale parametro caratterizza la dimensione dell'input.)
Studio del comportamento asintotico
Idea: trascurare costanti moltiplicative e termini di ordine inferiore.
Esempio:
Come fare formalmente?
Come nel calcolo infinitesimale!
Notazione O (O-grande)
per tutti gli n >=n0.
Intuitivamente questo significa:
Considerando O-grande siamo interessati all'andamento asintotico della funzione di costo del programma.
Esempio 1: Consideriamo una funzione f(n) = (n + 1)2.
allora abbiamo che (n +
1)2 <= 2n2,
per ogni n >= 3, quindi
applicando la definizione di O-grande, ponendo c = 2
e n0 = 3 abbiamo che f(n)
è O(n2).
Abbiamo che f (n) = 2n4 + 5n3 - 4n2 + 4 < 2n4 + 5n4 + 4n4 = 11n4 quindi applicando la definizione di O-grande, ponendo c = 11, ed n0 = 1, abbiamo che f (n) è O(n4).
Osservazioni sulla notazione O
4 n2 + 16 n + 18 è O(n2)
5n5 + 4n3 + n + 10 è O(n5)
Costo di un programma/algoritmo
Definizione: Un algoritmo A ha costo (o complessità) O(g(n)) se la quantità di tempo (spazio) sufficiente per eseguire A su un input di dimensione n nel caso peggiore è O(g(n)) .
Funzioni di costo
O(1) funzione di costo costante (che non dipende dai dati in ingresso).
O(n) funzione di costo lineare
O(log(n)) funzione di costo logaritmica
O(n log(n)) funzione di costo n log n (a volte detta "quasi-lineare")
O(nk) funzione di costo polinomiale cioè limitata da un polinomio di grado k (cioè, aknk + ak - 1nk - 1 +...+ a1n + a0, o più semplicemente aknk ) dove k è una costante.
O(kn) funzione di costo esponenziale cioè limitata da una funzione esponenziale kn, con k una costante maggiore di 1
...
Funzioni di costo (valori)
O-grande |
nome
|
n = 10
|
n = 100
|
n = 1000
|
ordine grandezza (n=1000)
|
O(1) |
costante
|
1
|
1
|
1
|
1 microsec
|
O(log n) |
logaritmica
|
2.3
|
4.6
|
6.9
|
|
O(n) |
lineare
|
10
|
100
|
1000
|
1 millisec
|
O(n log n ) |
n log n
|
23
|
460
|
6907
|
|
O(n2) |
quadratica
|
100
|
10000
|
1000000
|
1 sec
|
O(n3) |
cubica
|
1000
|
1000000
|
1000000000
|
16 min
|
O(2n) |
esponenziale
|
1024
|
1.26E30
|
1,07E301 |
???
|
Valutazione semplificata del costo: scomposizione dei costi
Valutazione semplificata del costo: operazioni dominanti
ricercaSequenziale
tramite l'individuazione dell'operazione dominante.
Ricerca binaria
Sia a un vettore di n elementi stringa a0,..., an - 1 ordinati in modo crescente ed k l'intero cercato in a.
left l'indice del primo elemento di a
right l'indice dell'ultimo elemento di a
med l'indice dell'elemento centrale pari ad
(left + right)/2.
false
.true
Ricerca binaria: implementazione
/* verifica se il valore k e' contenuto all'interno del array a ordinato */ public static boolean ricercaBinaria(int[] a, int k) { int left = 0, right = a.length-1; while (left<=right) { int med = (left+right)/2; if (a[med]==k) return true; else if (a[med]<k) left = med+1; else // a[med]>k right = med-1; } return false; }
Ricerca binaria: costo
Esiste una operazione dominante? Quale?
Tutte le operazioni all'interno del ciclo while
: es. condizione
(left <= right)
oppure istruzione med = (left+right)/2,
ecc.
Quante volte viene eseguito il ciclo while
nel caso peggiore?
Ad ogni iterazione vengono scartati metà degli elementi dell'array.
Quindi quanti elementi devono essere ancora analizzati ad ogni iterazione?
Vediamo: iter 0, n elem; iter 1, n/2 elem; iter 2, n/4
elem; iter 3, n/8;... iter n/2i...
Quante volte posso iterare?
Fino a quando n/2i >= 1. Cioè
2i <=n;
cioè i <= log2(n).
Il numero di iterazioni è al massimo pari a log(n).
Quindi considerando che il costo di ciascuna iterazione è costante,
facendo riferimento al costo dei sottoprogrammi ripetuti,abbiamo che il costo
di ricercaBinaria
è O(log(n)).
Esempio: Sia l'array a = [3, 5, 7 ,11, 17 ,19, 23, 31] e l'elemento
k cercato 6.
Il costo della ricerca è log2(8)
= 3.
Esercizio 4 Si consideri la funzione
La funzione prende come parametro una matricepublic static void stampaRigheMatrice(short[][] A) { for (int i=0; i<A.length; i++) { // scandisce righe for (int j=0; j<A[0].length; j++) // scandisce elementi riga i System.out.print(A[i][j]+" "); // stampa elemento riga System.out.println(); // fine riga } }
A
di dimensione n
x m e la stampa su System.out
per righe.
Determinare il costo di stampaRigheMatrice
utilizzando la notazione
O.
Esercizio 5 Si consideri il seguente metodo che prende come parametri due matrici
A
e B
entrambe di dimensione n
x n e restituisce una nuova matrice ottenuta
sommando gli elementi corrispondenti di A
e B
.
Determinare il costo dipublic static double[][] sommaMatrici(double[][] A, double[][] B) { double[][] C = new double [A.length][A[0].length]; for (int i=0; i<A.length; i++) for (int j=0; j<A[0].length; j++) C[i][j] = A[i][j] + B[i][j]; return C; }
sommaMatrici
utilizzando la notazione O.
Esercizio 6 Si consideri il seguente metodo che prende come parametri due matrici
A
e B
entrambe di dimensione n
x n e restituisce una nuova matrice ottenuta come
prodotto di A
e B
.
Determinare il costo dipublic static double[][] prodottoMatrici(double[][] A, double[][] B) { double[][] C = new double [A.length][A[0].length]; for (int i=0; i<A.length; i++) for (int j=0; j<A[0].length; j++) { C[i][j]=0; for (int k=0; k<A[0].length; k++) C[i][j] += A[i][k] * B[k][j]; } return C; }
prodottoMatrici
utilizzando la notazione O.
Esercizio 7 Si determini il costo di tutti i metodi della classe
TestoCriptato
public class TestoCriptato { // rappresentazione degli oggetti della classe private int chiave; private String testo; // costruttori public TestoCriptato(String testoNonCriptato) { chiave = 0; testo = testoNonCriptato; } public TestoCriptato(String testoNonCriptato, int chiave) { this.chiave = chiave; testo = codifica(testoNonCriptato,chiave); } // altri metodi pubblici public String getTestoCriptato() { return testo; } public String getTestoDecriptato(int chiave) { if (chiave == this.chiave) return decodifica(testo, chiave); else return null; } public boolean estChiave(int chiaveCandidata) { return chiaveCandidata == chiave; } public void setChiave(int chiave, int nuovaChiave) { if (chiave == this.chiave) { this.chiave = nuovaChiave; testo = codifica(decodifica(testo,chiave),nuovaChiave); } } // metodi ausiliari private static String codifica(String testo, int chiave) { String testoRis; char c; int ci; testoRis = ""; for (int i = 0; i < testo.length(); i++) { c = testo.charAt(i); ci = (int)c; ci = ci + chiave; c = (char)ci; testoRis = testoRis + String.valueOf(c); } return testoRis; } private static String decodifica(String testo, int chiave) { String testoRis; char c; int ci; testoRis = ""; for (int i = 0; i < testo.length(); i++) { c = testo.charAt(i); ci = (int)c; ci = ci - chiave; c = (char)ci; testoRis = testoRis + String.valueOf(c); } return testoRis; } }
Esercizio 8 Scrivere un metodo static int numeroDuplicati(int[] A)
che restituisce il numero di duplicati nell'array A
,
ovvero il numero di valori distinti che compaiono più volte come elementi
dell'array.
Determinare il costo del metodo.
Costo rispetto allo spazio
Fino ad adesso abbiamo focalizzato l'attenzione principalmente sul costo rispetto al tempo. Cosa cambia quando consideriamo invece lo spazio?
La definizione di O-grande e di delimitazione superiore di costo di un programma/algoritmo date precedentemente naturalmente sono ancora valide se invece di considerare quantità di tempo consideriamo quantità di spazio.
Modello di costo per lo spazio: mentre la quantità di tempo è calcolata considerando istruzioni/condizioni atomiche con costo unitario, quando passiamo allo spazio consideriamo unitario la quantità di memoria necessaria per memorizzare una variabile di qualsiasi tipo, incluse naturalmente quelle allocate nei record di attivazione delle funzioni.
Due considerazioni importanti:
Costo rispetto allo spazio
Esempio:
public static boolean ricercaSequenziale(int[] a, int k) { for (int i = 0; i < a.length; i++) if (a[i]==k) return true; return false; }
Costo in tempo O(n).
Costo in spazio?
O(1)! Spazio costante! perchè?
Esempio (implementazione ricorsiva di ricerca sequenziale):
public static boolean ricercaSequenzialeR(int[] a, int k, int i) { // i denota la pos iniziale del vettore if (i >= a.length) return false; else if (a[i]==k) return true; else return ricercaSequenzialeR(a,k,i+1); }
In realtà, alcuni compilatori sono in grado di riconoscere che questo è un tipo di ricorsione molto semplice (la funzione che invoca se stessa ricorsivamente non usa i risultati i tale invocazione), detta "tail recursion", e in presenza di tale tipo di ricorsione non allocano un nuovo record di attivazione, ma riusano il record di attivazione del chiamante.
Costo rispetto allo spazio
Esempio:
public static boolean ricercaBinaria(int[] a, int k) { int left = 0, right = a.length-1; while (left<=right) { int med = (left+right)/2; if (a[med]==k) return true; else if (a[med]<k) left = med+1; else // a[med]>k right = med-1; } return false; }
Costo in tempo O(log(n)).
Costo in spazio?
O(1)! Spazio costante! perchè?
Esempio (implementazione ricorsiva di ricerca binaria):
public static boolean ricercaBinariaR(int[] a, int k, int left, int right) { if (left > right) return false; else { int med = (left+right)/2; if (a[med]==k) return true; else if (a[med]<k) return ricercaBinariaR(a,k,med+1,right); else // a[med]>k return ricercaBinariaR(a,k,left,med-1); } }
Costo in tempo O(log(n)).
Costo in spazio?
O(log(n))! perchè?