Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Premesse.
Utilizzare al meglio gli strumenti dell’Informatica significa anche conoscere e studiare il
comportamento degli algoritmi nella soluzione di problemi Parleremo quindi di soluzioni di
problemi con l’ausilio del calcolatore, parleremo di algoritmi e di come si possa stabilire la “bontà”
di un algoritmo, di come si possano confrontare fra loro algoritmi differenti per stimarne
l’efficienza Oggi esistono problemi che, anche con il più potente dei computer, richiederebbero
centinaia di anni per essere risolti Ci chiediamo se prima o poi si riuscirà a trovare per ogni
problema un algoritmo efficiente che possa risolverlo in tempi ragionevoli, o se questo obiettivo è
impossibile
A fronte di un certo problema possono essere proposti algoritmi differenti per risolverlo: come
faccio a stabilire quale è il migliore? L’algoritmo migliore è il più efficiente: quello che riesce a
risolvere il problema con il minor utilizzo di risorse (della macchina) e nel minor tempo possibile
Uno stesso algoritmo, eseguito su macchine diverse, impiega tempi diversi! Allora l’efficienza di
un algoritmo è legata alla potenza della macchina su cui lo eseguo? No: una buona misura
dell’efficienza deve prescindere dal calcolatore su cui eseguirò l’algoritmo, altrimenti invece di
misurare l’efficienza dell’algoritmo misurerei l’efficienza della macchina!
L’efficienza di un algoritmo è data dal numero di operazioni elementari compiute dall’algoritmo,
calcolate in funzione della “dimensione dell’input” , ossia in funzione del numero di dati che
dovranno essere elaborati
n → +∞ ⇒ T(n) → +∞
Non per tutti gli algoritmi è così: nell’algoritmo che calcola il massimo tra due numeri, in quello
che calcola l’area del trapezio, nell’algoritmo che esegue delle prove sul troncamento della
divisione tra interi, … i dati di ingresso sono una quantità fissa, non variano di dimensione e di
conseguenza il tempo di esecuzione dell’algoritmo è noto e finito.
Nel calcolo della somma o del massimo su n dati acquisiamo gli n valori dell’array: la dimensione
dei dati varia con n e di conseguenza variabile è il tempo di esecuzione dell’algoritmo.
Notazione O (o-grande).
Sia f(n) la funzione di complessità che cerchiamo. Si dice che f(n) = O(g(n)) ovvero che g(n) è una
limitazione superiore per f(n) se:
Notazione Ω (omega).
Sia f(n) la funzione di complessità che cerchiamo. Si dice che f(n) = Ω(g(n)) ovvero che g(n) è una
limitazione inferiore per f(n) se:
Notazione Θ (theta).
Sia f(n) la funzione di complessità che cerchiamo. Si dice che
∃ c1, c2, n0 > 0: ∀ n ≥ n0 Esempio. n2 , 1000 n2, 1/100 n2 sono tutte Θ(n2)
c1 g(n) ≤ f(n) ≤ c2 g(n) infatti: varia solo la costante
moltiplicativa
24/04/2017
4 Costo computazionale degli algoritmi
Funzioni di riferimento
Consideriamo delle funzioni di riferimento
e calcoliamo la complessità degli algoritmi
confrontandola con queste funzioni.
y = log x
y=x
y = x log x
y = x2
y = 2x
y = ex
Classi di costo
Quando scriviamo un algoritmo, vogliamo stimare la sua complessità cercando di individuare la
funzione g(n) che approssima f(n). In tale modo gli algoritmi vengono suddivisi in classi di
complessità:
Ordini di grandezza:
Avendo a disposizione un elaboratore che esegue 103 operazioni al secondo, l’algoritmo risolutivo
di un problema 2n (ultima colonna) con ingresso di dimensione:
Quando si scrive un algoritmo si deve sempre dare una stima della limitazione superiore. È
opportuno dare una stima della limitazione inferiore e sarebbe importante stimare anche un
comportamento medio tra le due stime, ma questo spesso è difficile.
O(g) limitazione superiore: sono richieste al più g(n) operazioni
Ω(g) limitazione inferiore: sono richieste almeno g(n) operazioni.
24/04/2017
5 Costo computazionale degli algoritmi
Il problema dell’arresto
È legittimo considerare algoritmi che indagano sulle proprietà di altri algoritmi, che sono trattati
come dati. Infatti gli algoritmi sono rappresentabili con sequenze di simboli, che possono essere
presi dallo stesso alfabeto usato per codificare i dati di input. Una stessa sequenza di simboli può
essere quindi interpretata sia come un programma, sia come un dato di ingresso di un altro
programma.
Il problema dell’arresto consiste nel chiedersi se un generico programma termina la sua
esecuzione, oppure va in ciclo, ovvero continua a ripetere la stessa sequenza di istruzioni
all’infinito (supponendo di non avere limiti di tempo e memoria). Il problema dell’arresto è
INDECIDIBILE.
L’algoritmo ARRESTO costituirebbe uno strumento estremamente potente: permetterebbe infatti
di dimostrare congetture ancora aperte sugli interi (esempio: la congettura di Goldbach).
Goldbach() {
n = 2;
do { n = n + 2;
controesempio = true;
for (p = 2; p ≤ n -2; p++) {
q = n – p;
if (Primo(p) && Primo(q)) controesempio = false;
}
} while (!controesempio);
return n;
}
24/04/2017
6 Costo computazionale degli algoritmi
operazioni elementari
assegnazioni (accesso e assegnamento)
confronti
lettura/scrittura
invocazione di funzioni (passaggio parametri e gruppi di istruzioni all’interno della
funzione)
strutture di controllo
Assegnazione.
1) a = 25; Indichiamo con
2) a = sqrt(3*sin(x) + cos(y)/7);
ta
Queste due assegnazioni necessitano di due tempi diversi e
il tempo per una assegnazione.
sicuramente sarà: t1 < t2 perché l’espressione 2) richiede più calcoli.
Sequenza di assegnazioni.
<a1>
<a2> k costante Sommiamo i tempi:
…. il numero di istruzioni
k ta ~ ta
<ak> non dipende da n
k non dipende da n.
Se abbiamo due funzioni F1(n) F2(n) con lo stesso ordine di infinito,
può essere importante stimare anche il valore di k.
Struttura condizionale.
se P
allora <a1> <a1> e <a2> rappresentano
altrimenti <a2> gruppi di istruzioni
Se <a1> e <a2> non dipendono da n:
finese
tp + ta1 tp + ta2 tp
Il predicato P potrà essere:
abbiamo un numero costante di
1) a<b operazioni, come nell’algoritmo del
max(a,b).
2) (a<b) o (non S) e ((a==c) o (c!=h))
tp + ta1 oppure
tp + ta2
24/04/2017
7 Costo computazionale degli algoritmi
Struttura iterativa.
tp: eseguito n+1 volte
Consideriamo un ciclo con incremento fisso e passo 1:
(n volte con P vero, 1 volta con P falso)
ti: eseguito n volte
per i da 1 a n eseguire
iterazione //indipendente da n (n+1)·tp + n * ta ~ n * t
fineper
t = costante: “costo” dell'iterazione
Indichiamo con tp il tempo per il predicato e con ti il tempo
per l'iterazione (per ora indipendente da n): Avremo:
(n+1)·tp + n·ti = n·tp + tp + n·ti ≤ n·tp + n·tp + n·ti = n·( 2·tp + ti) = n * t (n ≥ 1)
1. per i da 1 a n eseguire
2. per k da 1 a n eseguire
2
iterazione n t
fineper
fineper
Esempio 1
A B B
algoritmo 1 n+1 n
RicercaSequenziale( a , k ) {
trovato = false; ta
indice = -1; ta
i = 0; ta
while ((i<n) && (!trovato)) { tp
if (a[i] == k) { tp
trovato = true; ta
indice = i; ta
}
else i = i+1; ta
}
return indice; ta
}
Esempio 2
A B C D
algoritmo n-2+1 n-2 Log n +1 log n
(1) for (i=2; i<n; i++) { tp
(2) j=i; ta
(3) printf("%d", j); ta
(4) while (!(j%2)) { tp
(5) j/=2; ta
(6) printf("%d", j); ta
(7) }
}
___A___ ______B___________________________
____C_____ + ____D______
(n-1)*tp + (n-2)*(2ta + tp (log n + 1) + (log n) (2ta)) < n*tp + n*2ta +n*tp* log n + n*tp +
n*log n * 2ta < c * n*log n
Esempio 3
algoritmo 1 n/2+1 n/2 log n log n
private static void q( int[] v ) {
int n = v.length - 1; ta
for ( int k=n/2; k>0; k=k-1 ) { tp1
int i = k; ta
int j = 2 * i; ta
do {
if ((j < n) && (v[j] < v[j+1])){
j = j + 1; tp
} ta
if ( v[i] < v[j] ) {
int x = v[i]; tp
v[i] = v[j]; ta
v[j] = x; ta
} else { ta
break;
} ta
i = j;
j = 2 * i; ta
} while ( j <= n ); tp2
}
}
In sostanza la dipendenza da n riguarda due cicli come segue:
(n/2+1) tp1 + n/2 * (2ta + log n * (tp2 + tw)) < [perché n/2+1 < n]
n * tp1 + n * ta + n * log n * tp2 + n * log n * tw =
n * (tp1 + ta) + n * log n * (tp2 + tw) < n log n (tp1 + ta + tp2 + tw ) [perché n < n log n]