Sei sulla pagina 1di 17

Giuseppe Ferri 15 gennaio 2021

INTORNO ALLA COMPLESSITÀ COMPUTAZIONALE

APPUNTI PER LA QUINTA C

1. Qualità di un algoritmo

Spesso si è interessati a soluzioni di problemi che, nell'impiego delle risorse uma-


ne e di calcolo, vengono ritenute economiche: l'impiego dell'algoritmo più eciente
si traduce in un risparmio di risorse computazionali e, di conseguenza, in un rispar-
mio di tempo e di denaro.
Durante la fase di analisi dei problemi è emerso che, spesso, esistono più algoritmi
per risolvere un problema: quali sono i criteri da seguire per valutare la bontà di
un algoritmo ? La risposta non è immediata.

Di ogni algoritmo dobbiamo considerare due aspetti:

• la sua organizzazione interna, ovvero la struttura data alle sue istruzioni


(organizzazione delle sue strutture di controllo) e le strutture dati utilizzate
(tipi di variabili semplici e strutturate);
• le risorse necessarie per eseguirlo (in particolare la memoria e il processo-
re), strettamente legato allo spazio e al tempo necessari alla sua esecuzione.

1.1. La bontà di un algoritmo.


Ogni algoritmo può essere tradotto in diversi programmi scritti in dierenti lin-
guaggi di programmazione e ogni programma verrà, a sua volta, trasformato in più
processi a tempo di esecuzione.

Per valutare la bontà di un algoritmo, quindi, non ci riferiremo direttamente all'al-


goritmo stesso, ma alle risorse che utilizzerà in quanto entità dinamica a tempo di
esecuzione.

Finora, infatti, l'obiettivo dell'attività di programmazione è stato il seguente:

Dato un problema, scriverne la soluzione sotto forma di algoritmo corretto e fun-


zionante (sia dal punto di vista sintattico, sia da quello semantico) e codicarlo in
un linguaggio di programmazione.

Ora, invece, l'obiettivo è:

Dati uno o più algoritmi che risolvono un problema, confrontarli per individuare il
migliore sulla base di un'analisi qualitativa.
1
INTORNO ALLA COMPLESSITÀ COMPUTAZIONALE 2

1.2.La misura della qualità. Daremo per scontato che gli algoritmi siano cor-
misura della qualità. In certi casi è
retti e cercheremo di assegnare loro una
importante valutre il tempo di esecuzione di un algoritmo, altre volte serve va-
lutare dierenti caratteristiche.

Oltre al fatto che il risultato sia corretto (e lo diamo per scontato) e che possa
essere anche semplice, in certi casi è importante valutare il tempo di esecuzione
dell'algoritmo.
Nel mondo di Internet, se un'immagine non viene visualizzata entro un lasso di
tempo ritenuto ragionevole, non accadranno disastri, ma si avrà un degrado delle
prestazioni. Questo evento, di fatto, renderà il sistema ineciente o poco utilizza-
bile. Siamo pertanto interessati a dare ai nostri algoritmi un'organizzazione interna
tale che il processo corrispondente impieghi il minor numero di risorse durante la
sua esecuzione.
Tra le risorse, particolare attenzione andrà posta a spazio di memoria e tempo di
esecuzione.

1.3. La risorsa spazio. Quando parliamo di risorsa spazio, intendiamo l'area di


memoria occupata da un processo durante la sua esecuzione, dove, con il termine
memoria, indichiamo la memoria di lavoro. Ci riferiamo sia a quella utilizzata
per la memorizzazione delle strutture dati (denite nell'algoritmo), sia a quella
strettamente necessaria per la memorizzazione del codice stesso, dei suoi dati si
input e dei risultati intermedi.

1.4. La risorsa tempo. Parlando di risorsa tempo, invece, intendiamo il tempo


di esecuzione del processo legato all'algoritmo.

L'evoluzione tecnologica fornisce ormai computer dotati di grandi capacità di me-


moria a costi relativamente bassi. Per questa ragione, la risorsa spazio è diventata
meno importante nella valutazione dei problemi.
Nel seguito della nostra analisi, quindi, ci occuperemo esclusivamente della valuta-
zione del tempo di esecuzione del processo legato a un algoritmo.
Per farlo, confronteremo tra loro due o più algoritmi di una stessa classe di proble-
mi, sulla base degli stessi parametri rilevanti, in modo da poter eettuare un'analisi
qualitativa degli algoritmi stessi.
La scelta dei parametri deve essere mirata, in quanto da essi dipendono le informa-
zioni sul comportamento dell'algoritmo.

Gli algoritmi considerati saranno sempre algoritmi buoni, cioè dovranno essere:
• soluzioni semplici, ecaci e, soprattutto generali;
• facilmente modicabili in caso di necessità;
• indipendenti dal linguaggio di programmazione che si vuole utilizzare.
• il test è svolto sullo stesso computer, dedicato esclusivamente alla sua
esecuzione;
• l'esecuzione del test avviene più volte con dati di input diversi per dimen-
sione e disposizione.

Queste ultime considerazioni ci conducono ad aermare che non si può assolu-


tamente valutare la bontà di un algoritmo servendosi di unità solari, in quanto,
INTORNO ALLA COMPLESSITÀ COMPUTAZIONALE 3

malgrado notevoli e minuziosi accorgimenti, non si può eettuare un'analisi com-


pleta.

Per questo motivo si dice che occorre considerare esclusivamente l'algoritmo in


sé e non la sua implementazione, in quanto:

1. l'algoritmo è la descrizione generale dell'intero processo di calcolo; l'algo-


ritmo ignora tutti i dettagli implementativi legati al linguaggio di program-
mazione;
2. il tempo necessario per l'esecuzione dell'algoritmo e indicativo del tempo
di esecuzione del processo corrispondente.

1.5. Il modo adatto per valutare il tempo di esecuzione di un algoritmo.


Abbiamo detto che la bontà di un algoritmo corretto si valuta in base al tempo
impiegato per la sua esecuzione e che tale tempo non può essere espresso in unità
solari. È quindi necessario un metodo di valutazione più idoneo.

Per ottenere una valutazione adabile, nel seguito della nostra analisi misureremo
il tempo di esecuzione in numero di operazioni (assegnazioni, scambi, confronti,
operazioni di I/O e così via) che l'algoritmo deve compiere per fornire i risultati.
Chiameremo tale numero costo dell'algoritmo.

Riferendoci, per esempio, a un linguaggio strutturato, possiamo introdurre alcune


regole di valutazione per il calcolo del costo delle istruzioni di un algoritmo.

1. Le istruzioni semplici quali lettura, scrittura, assegnamento hanno un


costo pari a uno.

Costo = 1

2. I costrutti iterativi quali WHILE, DO{ ... } WHILE, hanno un costo pari
alla somma dei costi del test e del corpo del ciclo. In particolare:
 il test del ciclo, essendo un'istruzione semplice, ha complessità pari a
uno;
 il costo del corpo del ciclo, invece, è dato dalla somma dei costi del-
le singole istruzioni. Naturalmente va tenuto conto di quante volte
ciascuna istruzione viene eseguita. Nel calcolo non includiamo mai le
istruzioni di apertura e di chiusura del corpo del ciclo.
Se il ciclo viene iterato K volte, allora il costo è:

Costo = CostoTest * K + CostoCorpo * K

Nei cicli a controllo in testa però, il test viene eseguito K + 1 volte e non K
volte (l'ultimo test, il (K + 1)esimo, è quello di uscita dal ciclo), quindi:

Costo = CostoTest * (K + 1) + CostoCorpo * K

3. I costrutti iterativi come FOR hanno un costo ottenuto dalla somma dei
seguenti costi:
INTORNO ALLA COMPLESSITÀ COMPUTAZIONALE 4

 costo dovuto all'inizializzazione della variabile del ciclo: pari a 1 (ese-


guito una sola volta all'inizio);
 costo di condizione di ne ciclo: pari al numero di volte che il ciclo
viene eseguito più un costo pari a 1 dovuto al test nale che consente
di uscire dal corpo del ciclo;
 costo del corpo del ciclo: pari alla somma dei costi delle singole istru-
zioni, tenendo conto delle K volte che il corpo del ciclo viene eseguito;
 costo di incremento della variabile del ciclo: pari al numero di volte
che il ciclo viene eseguito.

Costo = 1 + CostoTest * (K + 1) + CostoIncremento * K + CostoCorpo


* K

4. Il costrutto di selezione IF... THEN ha costo pari alla somma dei costi
del test (che ormai sappiamo essere uguale all'unità) più quello delle singole
istruzioni semplici contenute all'interno del ramo THEN. Analogamente vie-
ne calcolato il costo dell'istruzione di selezione binaria IF ... THEN ...
ELSE, dove conveniamo di utilizzare il costo massimo tra i due rami.

Costo = 1 + MAX(CostoRamoTHEN, CostoRamoELSE)

5. L'istruzione di chiamata di un sottoprogramma ha costo pari a quello


dell'intero sottoprogramma, tenendo conto delle regole descritte nei prece-
denti punti, più il costo dell'istruzione di chiamata, che è pari a uno.

Costo = 1 + CostoSottoprogramma

6. L'istruzione composta (per esempio annidamenti di strutture di control-


lo e blocchi di istruzioni) ha una complessità pari alla somma dei costi delle
singole istruzioni semplici che la compongono.

Costo = Σ CostoIstruzioniSemplici

1.6. Il costo dominante.


Utilizzando queste regole è possibile eliminare gli inconvenienti, trattati nel para-
grafo precedente, legati al sistema di elaborazione e al compilatore o interprete uti-
lizzato. I risultati ottenuti sono, però, approssimati. Questo perché è consuetudine
occuparsi solo di operazioni dal costo dominante, quali, per esempio, confronti e
moltiplicazioni, trascurando le istruzioni meno onerose dal punto di vista esecutivo,
quali addizioni, incrementi, decrementi. Possiamo ora dire che:

Il tempo di esecuzione di un algoritmo è proporzionale al suo costo.

Infatti, conoscendo il tempo di esecuzione di un'istruzione di costo unitario (tempo


unitario), espresso in secondi, basterà moltiplicare il costo complessivo dell'algo-
ritmo per esso, per ottenere il tempo totale di esecuzione dell'algoritmo.
INTORNO ALLA COMPLESSITÀ COMPUTAZIONALE 5

1. Esempio di costo per istruzioni semplici

Le seguenti istruzioni semplici hanno tutte costo pari a 1.

LEGGI(N) costo pari a 1


SCRIVI(Inserisci un numero) costo pari a 1
I ←− I + 1 costo pari a 1

2. Esempio di costo per costrutto iterativo

Consideriamo il corpo dell'algoritmo Alg1 che calcola la somma dei primi N nu-
meri naturali.

// Algoritmo Alg1
SCRIVI(inserisci un numero)
LEGGI(N)
I ←− 1
Som ←− 0
WHILE (I <= N) DO {
Som ←− Som + I
I ←− I + 1
}

SCRIVI(La somma dei primi, N, numeri è,


Som)

Se N = 10, il costo del solo ciclo è pari a 31, valore ottenuto da (3 * 10) + 1.
Infatti il ciclo è costituito da un test che ha costo uguale a 1 e da un corpo com-
posto da 2 istruzioni semplici, anch'esse di costo unitario: il costo dell'intero ciclo
è, pertanto, uguale a 3. Considerato che il ciclo deve essere eseguito dieci volte, il
costo totale è dato da 3 * 10 = 30. Ma non abbiamo ancora nito! Quando il ci-
clo viene eseguito per la decima volta, la variabile I viene incrementata di un'unità
assumendo, così, il valore 11. La condizione di ciclo viene eseguita un'undicesima
volta in modo da poter vericare la falsità della condizione e, quindi, consentirne
l'uscita. Tale esecuzione comporta l'aggiunta di un'ulteriore unità portando, così,
il valore a 31. Il costo complessivo dell'intero algoritmo (costo del ciclo + costo
delle altre istruzioni) è inne 31 + 5 = 36, includendo anche le 4 istruzioni prima
del ciclo e l'istruzione di scrittura nale.
Generalizzando, quindi, per N qualsiasi il costo è: (3 * N) + 1 + 5 = 3N + 6.

Riepilogando: la complessità T(N) di un algoritmo è una funzione che lega la dimen-


sione N del problema al numero di operazioni eseguite dall'algoritmo per risolvere
quel problema.
Poiché T(N) è una funzione, possiamo rappresentarla tramite un diagramma car-
tesiano, ponendo sull'asse delle ascisse la dimensione N del problema e su quello
delle ordinate il numero di operazioni. La complessità dell'algoritmo Alg1 si può
rappresentare come in gura:
INTORNO ALLA COMPLESSITÀ COMPUTAZIONALE 6

Otteniamo una retta perché la complessità T(N) = 3N + 6 rappresenta l'equazio-


ne di una retta. Per convenzione considereremo sempre N maggiore o uguale a 1,
intendendo che deve esserci almeno un dato in input (N è, infatti, la dimensione dei
dati in input).

È più corretto parlare di dimensione del problema invece che di dimensione dei
dati in input, perché la dimensione N varia da problema a problema e spesso non è
facilmente individuabile.
Consideriamo, per esempio, il seguente problema: trovare la somma dei primi N
numeri primi. Supponiamo che il valore di input di N sia 100. Qual è la dimensione
N del problema ? Si potrebbe pensare che sia 1 (in quanto viene fornito solo un nu-
mero, 100), ma non è così: se venisse fornito un numero più grande di 100, sarebbe
necessario trovare più numeri primi e fare quindi più operazioni. La dimensione
del problema è quindi il valore dell'unico numero dato, cioè 100 e, in generale, è N.
Ricordiamo che, malgrado possa sembrare banale, spesso non è facile individuare
la dimensione di un problema.

3. Esempio di costo per costrutto di selezione

IF (y >= 0)
THEN {
R ←− 1
Z ←− 2
}
Il costo del costrutto IF ... THEN precedente è pari a 3: 1 per il test della con-
dizione +2 per le istruzioni contenute nel ramo THEN.
INTORNO ALLA COMPLESSITÀ COMPUTAZIONALE 7

IF (Y >= 0)
THEN
{ R ←− 1 }
ELSE {
Z ←− 2
P ←− 0
R ←− 2
}
Il costo del costrutto IF ... THEN ... ELSE precedente è pari a 4: 1 per il test
della condizione, 3 per le istruzioni del ramoELSE (si sceglie il ramo ELSE poiché
le istruzioni in esso contenute sono in numero maggiore rispetto a quelle del ramo
THEN).

4. Esempio di costo per chiamata di un sottoprogramma

Riprendiamo l'esempio dell'algoritmo Alg1 e modichiamolo, col supporto di una


funzione Somma:

int Somma (int X, int Y) {


int S;
S ←− X + Y
return S;
}

// ALGORITMO Alg1Bis

SCRIVI(Inserisci un numero) // (1)

LEGGI(N) // (2)

I ←− 1 // (3)

Som ←− 0 // (4)

WHILE (I <= N) DO { // (5)

Som ←− Somma(I, Som) // (6)

I ←− I + 1 // (7)

}
SCRIVI(La somma dei primi, N, numeri è, Som) // (8)

PerN = 10 abbiamo:
Costo di Alg1Bis = 5 + CostoCostruttoWHILE =
= 5 + (3 + CostoChiamata + CostoFunzioneSomma) * 10 + 1 =
= 5 + (3 + 1 + 2) * 10 + 1 =
= 66
...dove:
5 è il costo delle istruzioni (1), (2), (3), (4) e (8)
3 è il costo delle istruzioni (5), (6) e (7)
INTORNO ALLA COMPLESSITÀ COMPUTAZIONALE 8

2+ il costo delle istruzioni della funzione Somma()

PerN qualsiasi abbiamo:


Costo di Alg1Bis = 5 + CostoCostruttoWHILE =
= 5 + (3 + CostoChiamata + CostoFunzioneSomma) * N + 1 =
= 5 + (3 + 1 + 2) * N + 1 =
= 6N + 6

5. Esempio di costo per istruzione composta

IF (X > 0)
THEN {
IF (Y > 0)
THEN
{ R ←− 1 }
ELSE {
R ←− 2
T ←− 1
}
Z ←− 3
}
Costo istruzione composta = Costo Test IF esterno + Costo Ramo THEN IF
Esterno =
= 1 + Costo IF Interno + Costo Istruzione Semplice =
= 1 + (1 + 2) + 1 = 5

6. Costo per costrutto iterativo WHILE ... DO

// ALGORITMO A1
int I, N;
I ←− 0
WHILE (I < N) DO {
I ←− I + 1
}
Il ciclo WHILE si compone di un test(I < N) e di un corpo costituito da una
operazione di assegnazione I ←− I + 1. Per ogni test positivo si esegue un'asse-
gnazione, quindi i costi sono:

• assegnazione esterna al ciclo: 1;


• numero di test del ciclo: N + 1 (compreso il test nale);
• assegnazioni interne al ciclo: 1 * N
Costo di A1 = CostoAssegnazioneEsterna + CostoCostruttoWHILE =
= 1 + ((1 + 1) * N + 1) = 1 + 2 * N + 1 = 2 + 2 * N
INTORNO ALLA COMPLESSITÀ COMPUTAZIONALE 9

7. Costo per costrutto iterativo DO ... WHILE

// ALGORITMO A2
int I, J, N;
I ←− 0
DO
{
I ←− I + 1
J ←− J * 3 + 42
}
WHILE(I >= N);
Il corpo del ciclo si compone di due passi base.
I costi sono:

• assegnazione esterna al ciclo: 1;


• numero di test: N;
• assegnazioni interne: 2 * N.
Costo di A2 = CostoAssegnazioneEsterna + CostoCostruttoWHILE =
= 1 + ((1 + 2) * N) = 1 + 3 * N

8. Costo per costrutto iterativo FOR...: calcolo dei primi N numeri pari

int SommaPrimiNumPari(int N) {
int M, I, P;
M ←− 0
FOR(I ←− 1; I <= N; I++) DO {
P ←− 2 * I
M ←− M + P
}
return M
}

Costo SommaIPrimiNumPari = CostoAssegnazioneEsterna + CostoCicloFOR =


= 1 + (1 + N + 1 + 2 * N + N) + CostoRETURN = 1 + (2 + 4N) + 1 =
= 4 * N + 4

Per la funzione SommaPrimiNumPari individuiamo i seguenti costi:

• assegnazione esterna al ciclo: 1;


• assegnazione iniziale cicloFOR: 1;
• numero di test del ciclo FOR: N + 1;
• incrementi del ciclo FOR: N;
• corpo del ciclo FOR: 2 * N;
• istruzione RETURN: 1.
INTORNO ALLA COMPLESSITÀ COMPUTAZIONALE 10

9. Costo per costrutto iterativo WHILE ... DO

// ALGORITMO A3
int I, J, N;
I ←− 0
WHILE (I < 2 * N) DO {
I ←− I + 1
J ←− J * 3 + 4367
}
Il test del ciclo WHILE viene eseguito 2 * N + 1 volte (compreso il test nale) e il
corpo è costituito da due istruzioni. Pertanto i costi sono:

• assegnazione esterna al ciclo: 1;


• numero di test: 2 * N + 1 (compreso il test nale);
• assegnazioni interne: 2 * (2 * N).
Costo di A3 = CostoAssegnazioneEsterna + CostoCostruttoWHILE =
= 1 + ((1 + 2) * (2 * N) + 1) = 1 + 3 * 2 * N + 1 = 2 + 6 * N

10. Costo per costrutti iterativi annidati

// ALGORITMO A4
int I, J, N;
I ←− 0
WHILE (I < N) DO {
FOR(J ←− 1; J <= N; J++) DO {
SCRIVI(CIAO!)
}
I ←− I + 1
}
Abbiamo un ciclo FOR WHILE, una istruzione di output per ogni ciclo
per ogni ciclo
FOR WHILE. Pertanto, i costi sono:
e una assegnazione per ogni ciclo

• assegnazione esterna: 1 +
• test WHILE: N + 1 +
• ciclo WHILE: N * (
• assegnazione iniziale ciclo FOR: 1 +
• controlli ciclo FOR: N + 1 +
• incrementi ciclo FOR: N +
• corpo ciclo FOR: N +
• assegnazione ciclo WHILE: 1)
Costo A4 = costo inizializzazione esterna al ciclo + CostoCostruttoWHILE
= 1 + ((CostoCostruttoFOR + 1) * N + 1) = 2 + (CostoCostruttoFOR) * N
+ N =
= 2 + (1 + N + 1 + N + N + 1) * N + 1) * N + N =
= 2 + 3N + 3 * N2 + N = 2 + 4 * N + 3 * N2 .
INTORNO ALLA COMPLESSITÀ COMPUTAZIONALE 11

11. Costo per costrutti iterativi annidati

int Funz(int N, int M) {


int X, Y, I;
X ←− 0
FOR(I ←− 1; I <= N; I++) DO {
FOR(J ←− 1; J <= M; J++) DO {
X ←− X + 1
Y ←− X
}
}
RETURN X
}
Abbiamo un ciclo FOR esterno e un ciclo FOR interno, due istruzioni per ogni ciclo
FOR interno e una istruzione RETURN. Pertanto i costi sono:

assegnazione esterna: 1 +
assegnazione iniziale FOR esterno: 1 +
test FOR esterno: N + 1 +
corpo ciclo FOR esterno N * (
− assegnazione iniziale ciclo FOR interno: 1 +
− controlli cicloFOR interno: M + 1 +
− incremento ciclo FOR interno: M +
− corpo ciclo FOR interno: 2M ) +
incremento ciclo FOR esterno: N
istruzione RETURN: 1

Costo di Funz = 1 + (CostoCostruttoPEREsterno) + 1 =


= 2 + (1 + N + 1 + N + N * CostoCostruttoPERInterno) =
= 4 + 2N + N * (1 + M + 1 + M + 2M) = 4 + 4N + 4N * M
INTORNO ALLA COMPLESSITÀ COMPUTAZIONALE 12

Un esempio di determinazione della complessità computazio-


nale

Consideriamo i tre algoritmi di seguito riportati, aventi N come dimensione del


problema.

Il primo algoritmo Alg2 calcola N5 :

// ALGORITMO Alg2
int X, N, I;
SCRIVI(Inserisci il valore N)
LEGGI(N)
X ←− N
I ←− 1
WHILE (I <= 5) DO {
X ←− X * N
I ←− I + 1
}
SCRIVI(N)
L'algoritmo Alg2 ha complessità pari a 18 qualunque sia N, poiché richiede 18 ope-
razioni elementari; possiamo esprimere la complessità come TAlg2 (N) = 18. Infatti,
oltre alle istruzioni iniziali e a quella nale, di costo complessivo pari a 5, il ciclo
(che comprende un test e due istruzioni, con costo ciclo = 3) viene ripetuto 4 volte.
Il test viene ripetuto un'altra volta, prima di constatare che la condizione è diven-
tata falsa. Quindi TAlg2 (N) = 5 + 4 * 3 + 1 = 18.

Il secondo algoritmo Alg3 calcola XN :

// ALGORITMO Alg3 int Q, X, N, I;


SCRIVI(Inserisci la base)
LEGGI(X)
SCRIVI(Inserisci l'esponente)
LEGGI(N)
Q ←−1
I ←− N
WHILE (I > 0) DO {
Q ←− Q * X
I ←− I  1
}
SCRIVI(Q)
L'algoritmo Alg3 ha complessità pari a 3N + 8, possiamo esprimerla come TAlg3 (N)
= 3N + 8. Infatti, oltre alle istruzioni di inizializzazione e di I/O (costo totale =
7), il ciclo (costo totale = 3, compreso il test) viene ripetuto N volte prima di uscire.
Allora TAlg3 (N) = 7 + 3N + 1 = 3N + 8.
INTORNO ALLA COMPLESSITÀ COMPUTAZIONALE 13

Il terzo algoritmo Alg4 calcola XN con un metodo diverso.

// ALGORITMO Alg4
int Q, X, N, I;
SCRIVI(Inserisci la base)
LEGGI(X)
SCRIVI(Inserisci l'esponente)
LEGGI(N)
Q ←− 1
I ←− N
WHILE (I > 1) DO {
IF ((I MOD 2) = 0)
THEN {
X ←− X * X
I ←− I / 2
}
ELSE {
Q ←− Q * X
i ←− i − 1
}
}
Q ←− Q * X
SCRIVI(Q)

Determinare la complessità dell'algoritmo Alg4 non è semplice come per i due


algoritmi precedenti, anzi è abbastanza macchinoso, ma proviamoci ugualmente!
Dobbiamo fare le seguenti considerazioni: supponiamo che N sia una potenza di
2. Poiché nell'algoritmo Alg4 la variabile I rappresenta l'esponente (I ←− N),
K
possiamo generalizzare che I = 2 per un qualsiasi valore di K > 0. Dividere I
per 2 (I ←− I / 2), quindi, è equivalente a sottrarre 1 da K. Pertanto N / 2 =
(2
K−1 ). Ogni iterazione decrementa K di 1. Alla ne del ciclo i = 1, cosicché k

= 0. Perciò il numero di cicli è k: Alg4 fa esattamente log2 N iterazioni. Infatti:


log2 N = log2 (2K ) = K * log2 2 = K. Il costo del corpo del ciclo è Test del ciclo
+ Costo del costrutto IF = 1 + (1 + 2) = 4. Quindi il costo totale del ciclo
è 4 * log2 N + 1, dove l'1 nale si riferisce all'ultimo test. Il costo totale di Alg4
è pertanto: 4 * log2 N + 8, considerando anche le istruzioni di inizializzazione e di
I/O. Avremo perciò TAlg4 (N) = 4 * log2 N + 8. Se N non è una potenza di 2, si
dimostra che TAlg4 (N) è proporzionale a 4 * log2 N.

Qui e nel seguito indicheremo con log il logaritmo in base 2, ovvero in generale
log N sta per log2 N.
INTORNO ALLA COMPLESSITÀ COMPUTAZIONALE 14

Possiamo esprimere le funzioni di complessità dei tre algoritmi considerati con i


diagrammi visibili nella gura.
INTORNO ALLA COMPLESSITÀ COMPUTAZIONALE 15

2. Ordine di grandezza e classi di computabilità


2.1. La complessità asintotica.
Come abbiamo visto dai precedenti esempi, alcune volte può risultare molto dici-
le arrivare all'esatta formulazione della complessità di un algoritmo. Confrontando
due algoritmi, inoltre, può accadere che il primo esegua meno operazioni dell'altro
quando la dimensione del problema è bassa, ma che le cose si ribaltino quando tale
dimensione cresce.

In questi casi, allora, conviene fare riferimento all'ordine di grandezza della comples-
sità, cioè valutare la complessità per valori molto grandi delle dimensioni del pro-
blema. Si parla di complessità asintotica.

Riprendiamo i graci delle funzioni T(N) degli algoritmi Alg2, Alg3 e Alg4. No-
tiamo che, per valori di N 1 e l'ascissa del punto P1, l'algoritmo Alg4
compresi tra
compie un numero di operazioni inferiore all'algoritmo Alg2, pur avendo comples-
sità asintotica superiore. Al crescere di N, però, è TAlg3 che cresce di più.

La nozione di complessità asintotica si esprime in notazione matematica nel modo


seguente:

lim T(N)
N→∞

che si legge: limite per N che tende a innito della funzione T(N). In denitiva,
si ottiene un'espressione in funzione di N che indica quale è il comportamento
asintotico dell'algoritmo.
INTORNO ALLA COMPLESSITÀ COMPUTAZIONALE 16

2.2. Le classi di complessità o di computabilità.


Introduciamo ora le seguenti denizioni.

Siano f(N) e g(N) due funzioni. Si dice che f(N) è di ordine di grandezza
g(N) e si scrive O(g(N)) e si legge:
O grande di g di N se esiste una costante C > 0 tale che, per tutti i valori nel
dominio di N, f(N) < C * g(N).

Pertanto, dire che T(N) è O(g(N)), signica che g(N) è un limite superiore alla
legge di crescita di T(N), cioè T(N) non cresce più di g(N).

Il graco di T(N) da un certo punto in poi starà, per le ragioni appena esposte,
al di sotto di quello di C * g(N). Diremo inoltre che:

f(N) è proporzionale a g(N) se f(N) è O(g(N)) e g(N) è O(f(N)). Si dice anche


che f(N) e g(N) sono dello stesso ordine (di grandezza).

Potremo allora fare aermazioni del tipo:

T(N), ossia la complessità computazionale di un algoritmo X, è proporzionale a


N2 , ovvero è dell'ordine di N2 : O(N2 )

Consideriamo T(N) = 2N2 + 5: risultano irrilevanti (ai ni della determinazione


dell'ordine di grandezza) sia la costante 5, sia la costante moltiplicatica 2 (poiché
stiamo parlando di limiti). Ciò che interessa è il termine N2 .

Più formalmente: lim T(N)


2 = COSTANTE
N→∞ N

Programmi e algoritmi saranno quindi valutati confrontando le loro funzioni T(N)


e tralasciando eventuali costanti. Pertanto:

• algoritmi con funzioni di complessità N, 2N, 3N,


2N + 3, 2N + 200,
oppure
3N + 4.000 sono tutti O(N), cioè sotto tutti proporzionali a
N;
• algoritmi con funzioni di complessità come 5N2 , 8N2 + 67 sono tutti O(N2 ),
INTORNO ALLA COMPLESSITÀ COMPUTAZIONALE 17

cioè sono tutti proporzionali a N2 .


Possiamo individuare alcuni ordini di grandezza per le funzioni T(N) che indivi-
duano le cosiddette classi di complessità (o computabilità). Abbiamo quindi i
seguenti casi:

Classe di complessità Descrizione


Complessità costante O(1) o O(C) Indica la complessità degli algoritmi che
eseguono lo stesso numero di operazioni
indipendentemente dalla dimensione dei
dati di input. Un classico esempio è dato
da un algoritmo senza cicli, in cui l'ese-
cuzione non dipende dalla dimensione del
problema.
Complessità logaritmica O(logN) Indica la complessità degli algoritmi che
eseguono un numero di operazioni propor-
zionale a log N. Un esempio è l'algoritmo
di ricerca binaria.
Complessità lineare O(N) Indica la complessità degli algoritmi che
eseguono un numero di operazioni propor-
zionale a N, cioè proporzionale alla dimen-
sione del problema. Per esempio, hanno
complessità lineare la ricerca sequenziale,
la lettura e la stampa degli elementi di un
array, l'algoritmo di verica di un numero
primo.
Complessità NlogN O(NlogN) È la classe di molti algoritmi di ordina-
mento. Per esempio, ha complessità NlogN
l'algoritmo MergeSort (un altro famoso
algoritmo di ordinamento).
Complessità polinomiale O(NK ) La sua caratteristica è avere le dimensioni
del problema come base da elevare a un
esponente K. Quando K = 3 si parla inve-
ce di complessità cubica. È di questo tipo
l'algoritmo che eettua la moltiplicazione
di due matrici quadrate di dimensione N.
Complessità esponenziale O(KN ) La sua caratteristica è avere la dimensione
del problema come esponente. Ne è un
esempio un algoritmo che deve produrre
tutte le possibili stringhe di lunghezza N
su un alfabeto di 10 simboli.

Potrebbero piacerti anche