Docsity
Docsity

Prepara i tuoi esami
Prepara i tuoi esami

Studia grazie alle numerose risorse presenti su Docsity


Ottieni i punti per scaricare
Ottieni i punti per scaricare

Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium


Guide e consigli
Guide e consigli


Analisi della Complessità Asintotica degli Algoritmi - Prof. Calamoneri, Dispense di Elementi di Informatica

Una introduzione alla complessità computazionale asintotica degli algoritmi, illustrando le notazioni o, ω e ө. Esplora il concetto di limite asintotico superiore, inferiore e stretto, fornendo esempi e regole per il calcolo della complessità. Anche il tempo di esecuzione degli algoritmi in funzione della loro complessità temporale, evidenziando l'importanza dell'analisi asintotica per la valutazione delle prestazioni degli algoritmi.

Tipologia: Dispense

2011/2012

Caricato il 28/01/2025

peppegmx
peppegmx 🇮🇹

5 documenti

1 / 11

Toggle sidebar

Questa pagina non è visibile nell’anteprima

Non perderti parti importanti!

bg1
Pag 13
2) Complessità asintotica
Per poter valutare l’efficienza di un algoritmo, così da poterlo confrontare con algoritmi diversi
che risolvono lo stesso problema, bisogna essere in grado di valutare l’ordine di grandezza del
suo tempo di esecuzione e delle sue necessità in termini di memoria. In particolare, tale
valutazione ha senso quando la dimensione dell’input è sufficientemente grande.
La disciplina che si occupa della quantificazione di tali grandezze è la complessità
computazionale asintotica:
complessità computazionale: indica che ci occupiamo di valutare il “costo di
esecuzione” dell’algoritmo;
asintotica: indica che siamo interessati a risultati di tipo asintotico, ossia validi per
grandi dimensioni dell’input.
Salvo ove sia diversamente specificato, la grandezza che viene valutata è il tempo di
esecuzione dell’algoritmo.
Prima di entrare nel vivo della valutazione della complessità computazionale asintotica degli
algoritmi è necessario introdurre alcune definizioni.
2.1 Notazione O (limite asintotico superiore)
Date due funzioni f(n), g(n) 0 si dice che
f(n) è un O(g(n))
se esistono due costanti c ed n0 tali che
0 f(n) c g(n) per ogni n n0
pf3
pf4
pf5
pf8
pf9
pfa

Anteprima parziale del testo

Scarica Analisi della Complessità Asintotica degli Algoritmi - Prof. Calamoneri e più Dispense in PDF di Elementi di Informatica solo su Docsity!

2) Complessità asintotica

Per poter valutare l’efficienza di un algoritmo, così da poterlo confrontare con algoritmi diversi che risolvono lo stesso problema, bisogna essere in grado di valutare l’ordine di grandezza del suo tempo di esecuzione e delle sue necessità in termini di memoria. In particolare, tale valutazione ha senso quando la dimensione dell’input è sufficientemente grande.

La disciplina che si occupa della quantificazione di tali grandezze è la complessità computazionale asintotica :

 complessità computazionale: indica che ci occupiamo di valutare il “costo di

esecuzione” dell’algoritmo;

 asintotica: indica che siamo interessati a risultati di tipo asintotico, ossia validi per

grandi dimensioni dell’input.

Salvo ove sia diversamente specificato, la grandezza che viene valutata è il tempo di esecuzione dell’algoritmo.

Prima di entrare nel vivo della valutazione della complessità computazionale asintotica degli algoritmi è necessario introdurre alcune definizioni.

2.1 Notazione O (limite asintotico superiore)

Date due funzioni f(n), g(n) ≥ 0 si dice che

f(n) è un O(g(n))

se esistono due costanti c ed n 0 tali che

0 ≤ f(n) ≤ c g(n) per ogni n ≥ n 0

Esempio 2. Sia f(n) = 3n + 3

f(n) è un O(n^2 ) in quanto, posto c = 6, cn^2 ≥ 3n + 3 per ogni n ≥ 1.

Ma f(n) è anche un O(n) in quanto cn ≥ 3n + 3 per ogni n ≥ 1 se c ≥ 6 , oppure per ogni n ≥ 3 se c ≥ 4.

E' facile convincersi che, data una funzione f(n) , esistono infinite funzioni g(n) per cui f(n) risulta un O(g(n)). Come sarà più chiaro in seguito, ci interessa tentare di determinare la funzione g(n) che meglio approssima la funzione f(n) dall'alto o, informalmente, la più piccola funzione g(n) tale che f(n) sia O(g(n)).

Esempio 2. Sia f(n) = n^2 + 4n

Ebbene, f(n) è un O(n^2 ) in quanto cn^2 ≥ n^2 + 4n per ogni n se c ≥ 5 , oppure per ogni n ≥ 4/(c-1) se c > 1.

Esempio 2.

Sia f(n) un polinomio di grado m : f(n) = , con am > 0

Dimostriamo, per induzione su m , che f(n) è un O(nm).

Casi base

 m = 0 : f(n) = a 0 , quindi f(n) è un O(1) per qualunque n , per ogni c ≥ a 0

 m = 1 : f(n) = a 0 + a 1 n , quindi f(n) è un O(n) per qualunque n , per ogni c ≥ a 0 + a 1

Ipotesi induttiva

h(n) = è un O(nm-1) se c ≥

Passo induttivo

Dobbiamo dimostrare che

f(n) = è un O(nm)

cioè che

c’nm^ se c’ ≥

Per l’ipotesi induttiva:

 f(n) si può scrivere come f(n) = h(n) + amnm

 cnm-1^ ≥ h(n) per ogni c ≥

Abbiamo visto come, in entrambe le notazioni esposte in precedenza, per ogni funzione f(n) sia possibile trovare più funzioni g(n). In effetti O(g(n)) e Ω(g(n)) sono insiemi di funzioni , e dire “ f(n) è un O(g(n) ” oppure “ f(n) = O(g(n)” ha il significato di “ f(n) appartiene a O(g(n)) ”.

Tuttavia, poiché i limiti asintotici ci servono per stimare con la maggior precisione possibile la complessità di un algoritmo, vorremmo trovare – fra tutte le possibili funzioni g(n) – quella che più si avvicina a f(n).

Per questo cerchiamo la più piccola funzione g(n) per determinare O e la più grande funzione g(n) per determinare Ω.

La definizione che segue formalizza questo concetto intuitivo.

2.3 Notazione Ө (limite asintotico stretto)

Date due funzioni f(n) , g(n) ≥ 0 si dice che

f(n) è un Ө(g(n))

se esistono tre costanti c 1 , c 2 ed n 0 tali che

c 1 g(n) ≤ f(n) ≤ c 2 g(n) per ogni n ≥ n 0

In altre parole, f(n) è Ө(g(n)) se è contemporaneamente O(g(n)) e Ω(g(n)).

Esempio 2. Sia f(n) = 3n + 3

f(n) è un Ө(n) ponendo, ad esempio, c 1 = 3, c 2 = 4, n 0 = 3.

Esempio 2. Sia f(n) un polinomio di grado m: f(n) = , con am > 0

La dimostrazione che f(n) è un Ө(nm) discende dagli esempi 2.3 e 2.5.

2.4 Algebra della notazione asintotica

Per semplificare il calcolo della complessità asintotica degli algoritmi si possono sfruttare delle semplici regole che dapprima enunciamo chiarendole con degli esempi, ed in un secondo momento dimostriamo.

Regole sulle costanti moltiplicative

1A : Per ogni k > 0 e per ogni f(n) ≥ 0 , se f(n) è un O(g(n)) allora anche k f(n) è un O(g(n)).

1B : Per ogni k > 0 e per ogni f(n) ≥ 0 , se f(n) è un Ω(g(n)) allora anche k f(n) è un Ω(g(n)).

1C : Per ogni k > 0 e per ogni f(n) ≥ 0 , se f(n) è un Ө(g(n)) allora anche k f(n) è un Ө(g(n)).

Informalmente, queste tre regole si possono riformulare dicendo che le costanti moltiplicative si possono ignorare.

Regole sulla commutatività con la somma

2A : Per ogni f(n), d(n) > 0 , se f(n) è un O(g(n)) e d(n) è un O(h(n)) allora f(n)+d(n) è un O(g(n)+h(n)) = O(max(g(n),h(n))).

2B : Per ogni f(n), d(n) > 0 , se f(n) è un Ω(g(n)) e d(n) è un Ω(h(n)) allora f(n)+d(n) è un Ω(g(n)+h(n)) = Ω(max(g(n),h(n))).

2C : Per ogni f(n), d(n) > 0 , se f(n) è un Ө(g(n)) e d(n) è un Ө(h(n)) allora f(n)+d(n) è un Ө(g(n)+h(n)) = Ө(max(g(n),h(n))).

Informalmente, queste tre regole si possono riformulare dicendo che le notazioni asintotiche commutano con l’operazione di somma.

Regole sulla commutatività col prodotto

3A : Per ogni f(n), d(n) > 0 , se f(n) è un O(g(n)) e d(n) è un O(h(n)) allora f(n)d(n) è un O(g(n)h(n)).

3B : Per ogni f(n), d(n) > 0 , se f(n) è un Ω(g(n)) e d(n) è un Ω(h(n)) allora f(n)d(n) è un Ω(g(n)h(n)).

Regola 2A. Per ogni f(n), d(n) > 0 , se f(n) è un O(g(n)) e d(n) è un O(h(n)) allora f(n)+d(n) è un O(g(n)+h(n)) = O(max(g(n),h(n))).

Dim. Se f(n) è un O(g(n)) e d(n) è un O(h(n)) allora esistono quattro costanti c’ e c” , n’ 0 ed n” 0 tali che:

f(n) ≤ c’g(n) per ogni n ≥ n’ 0 e d(n) ≤ c”h(n) per ogni n ≥ n” 0

allora:

f(n) + d(n) ≤ c’g(n) + c”h(n) ≤ max(c’, c”)(g(n) + h(n)) per ogni n ≥ max(n’ 0 , n” 0 )

Da ciò segue che f(n) + d(n) è un O(g(n)+h(n)).

Infine:

max(c’, c”)(g(n) + h(n)) ≤ 2 max(c’, c”) max(g(n), h(n)).

Ne segue che f(n) + d(n) è un O(max(g(n), h(n))). CVD

Regola 3A. Per ogni f(n), d(n) > 0 , se f(n) è un O(g(n)) e d(n) è un O(h(n)) allora f(n)d(n) è un O(g(n)h(n)).

Dim. Se f(n) è un O(g(n)) e d(n) è un O(h(n)) allora esistono quattro costanti c’ e c” , n’ 0 ed n” 0 tali che:

f(n) ≤ c’g(n) per ogni n ≥ n’ 0 e d(n) ≤ c”h(n) per ogni n ≥ n” 0

allora:

f(n)d(n) ≤ c’c”g(n)h(n) per ogni n ≥ max(n’ 0 , n” 0 )

Da ciò segue che f(n)d(n) è un O(g(n)h(n)). CVD

Le dimostrazioni delle altre regole che coinvolgono le notazioni Ω e Ө sono lasciate per esercizio.

2.5 Valutazione della complessità computazionale di un

algoritmo

Vediamo ora come calcolare effettivamente la complessità computazionale di un algoritmo, adottiando il criterio della misura di costo uniforme descritto nel par. 1.4.2.

Prima di procedere, facciamo due importanti considerazioni.

Innanzi tutto, osserviamo che è ragionevole pensare che la complessità computazionale, intesa come funzione che rappresenta il tempo di esecuzione di un algoritmo, sia una funzione monotona non decrescente della dimensione dell'input. Questa osservazione ci conduce a constatare che, prima di passare al calcolo della complessità, bisogna definire quale sia la

dimensione dell'input. Trovare questo parametro è, di solito, abbastanza semplice: in un algoritmo di ordinamento esso sarà il numero di dati, in un algoritmo che lavora su una matrice sarà il numero di righe e di colonne, in un algoritmo che opera su alberi sarà il numero di nodi, ecc.. Tuttavia, vi sono casi in cui l’individuazione del parametro non è banale; in ogni caso, è necessario stabilire quale sia la variabile (o le variabili) di riferimento prima di accingersi a calcolare la complessità.

In secondo luogo, vogliamo sottolineare che la notazione asintotica viene sfruttata pesantemente per il calcolo della complessità computazionale degli algoritmi, quindi - in base alla definizione stessa – tale complessità computazionale potrà essere ritenuta valida solo asintoticamente.

In effetti, esistono degli algoritmi che per dimensioni dell'input relativamente piccole hanno un certo comportamento, mentre per dimensioni maggiori un altro. Sottolineiamo che siamo interessati all'andamento asintotico della complessità.

Per poter valutare la complessità di un algoritmo, esso deve essere formulato in un modo che sia chiaro, sintetico e non ambiguo.

Si adotta il cosiddetto pseudocodice , che è una sorta di linguaggio di programmazione “informale” nell’ambito del quale:

 si impiegano, come nei linguaggi di programmazione, i costrutti di controllo (for, if

then else, while, ecc.);

 si impiega il linguaggio naturale per specificare le operazioni;

 si ignorano i problemi di ingegneria del software;

 si omette la gestione degli errori, al fine di esprimere solo l’essenza della soluzione.

Non esiste una notazione universalmente accettata per lo pseudocodice. In queste dispense (come del resto nel libo di testo) useremo l’indentazione per rappresentare i diversi livelli dei blocchi di istruzioni, useremo il simbolo  per indicare un’assegnazione, il simbolo = per verificare che il contenuto di 2 variabili sia lo stesso e il simbolo ≠ per verificare che il contenuto di 2 variabili sia differente.

Le regole generali che si adottano per valutare la complessità computazionale di un algoritmo sono le seguenti:

 le istruzioni elementari (operazioni aritmetiche, lettura del valore di una variabile,

assegnazione di un valore a una variabile, valutazione di una condizione logica su un

numero costante di operandi, stampa del valore di una variabile, ecc.) hanno

complessità Ө(1) ;

Esempio 2. Calcolo del massimo in un vettore disordinato contenente n valori.

Funzione Trova_Max (A: vettore)

1 max  A[1]

2 for i = 2 to n do

3 if A[i] > max

4 then max  A[i]

5 stampa max

Ө( 1 ) (n – 1) iterazioni più Ө( 1 ) Ө( 1 ) Ө( 1 ) Ө( 1 )

La complessità dell’istruzione 1 è Ө(1).

L’iterazione viene eseguita (n – 1) volte, e ciascuna iterazione (costituita dalle istruzioni 2, 3 e 4) ha complessità Ө(1) + Ө(1) + Ө(1) = Ө(1) poiché l’incremento del contatore (istr. 2), la valutazione della condizione (istr. 3) e l’assegnazione (istr. 4) sono istruzioni elementari.

La complessità dell’istruzione 5 è Ө(1).

Quindi, detta T(n) la complessità temporale di questo algoritmo, essa vale:

T(n) = Ө(1) + (n – 1) Ө(1) + Ө(1) = Ө(n)

Concludiamo questo argomento mostrando come variano i tempi di esecuzione di un algoritmo in funzione della sua complessità temporale.

Ipotizziamo di disporre di un sistema di calcolo in grado di effettuare una operazione elementare in un nanosecondo ( 109 operazioni al secondo), e supponiamo che la dimensione del problema sia n = 106 (un milione):

 complessità O(n) - tempo di esecuzione: 1 millesimo di secondo;

 complessità O(n log n) - tempo di esecuzione: 20 millesimi di secondo;

 complessità O(n^2 ) - tempo di esecuzione: 1000 secondi = 16 minuti e 40 secondi.

C’è un'altra situazione interessante da considerare: che succede se la complessità cresce esponenzialmente, ad esempio quando è O(2n)?

E’ abbastanza ovvio che i tempi di esecuzione diventano rapidamente proibitivi: un tale tipo di problema su un input di dimensione n = 100 richiede per la sua soluzione mediante il sistema di calcolo di cui sopra ben 1,2610^21 secondi, cioè circa 310^13 anni.

Si può pensare che l’avanzamento tecnologico, per quanto formidabile, possa rendere abbordabile un tale problema? Purtroppo la risposta è no. Infatti, poniamoci la seguente domanda: supponendo di avere un calcolatore estremamente potente che riesce a risolvere un problema di dimensione n = 1000 , avente complessità O(2n) , in un determinato tempo T , quale dimensione n’ = n + x del problema riusciremmo a risolvere nello stesso tempo utilizzando un calcolatore mille volte più veloce?

Possiamo scrivere la seguente uguaglianza:

T = =

Si ha quindi:

Ossia

x = log 1000 ≈ 10

Dunque, con un calcolatore mille volte più veloce riusciremmo solo a risolvere, nello stesso tempo, un problema di dimensione 1010 anziché di dimensione 1000.

In effetti esiste un’importantissima branca della teoria della complessità che si occupa proprio di caratterizzare i cosiddetti problemi intrattabili , ossia quei problemi la cui complessità computazionale è tale per cui essi non sono né saranno mai risolubili per dimensioni realistiche dell’input.