...

Diapositiva 1 - Mondadori Education

by user

on
Category: Documents
10

views

Report

Comments

Transcript

Diapositiva 1 - Mondadori Education
Informatica 3
V anno
La complessità computazionale
In cerca di buone soluzioni
Trovare buone soluzioni nella risoluzione di un problema si rivela un aspetto
fondamentale tanto nella vita di tutti i giorni, quanto in informatica.
Anche nella progettazione di algoritmi sarà importante trovare buone soluzioni.
Individuare e progettare un algoritmo efficiente si traduce in un risparmio di
risorse computazionali e, di conseguenza, in un risparmio di tempo e denaro.
Un problema, molte soluzioni
Solitamente esistono diversi algoritmi per risolvere uno stesso problema.
Di ogni algoritmo dobbiamo considerare due aspetti:
- la sua organizzazione interna, ovvero la struttura data alle sue istruzioni e le
strutture dati utilizzate
- le risorse necessarie per eseguirlo (memoria e processore), strettamente
legate allo spazio e al tempo necessari alla sua esecuzione.
Ogni algoritmo può essere tradotto in diversi programmi, scritti in
diversi linguaggi di programmazione.
La bontà di algoritmo
Per valutare la bontà di un algoritmo NON ci si sofferma sull’algoritmo in
sé ma sulle risorse che utilizza e al suo tempo di esecuzione.
Il nostro obiettivo sarà quindi quello di:
Dati uno o più algoritmi che risolvono un problema, confrontarli per
individuare il migliore sulla base di un’analisi qualitativa.
Cercheremo quindi di assegnare agli algoritmi una misura di qualità.
In molti casi, sarà importante valutare il tempo di esecuzione, altre volte
serve valutare differenti caratteristiche.
Nello spazio e nel tempo
L’obiettivo è quello di 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, molto importanti si rivelano:
- Spazio di esecuzione (risorsa spazio), cioè l’area di memoria occupata
da un processo durante la sua esecuzione.
- Tempo di esecuzione (risorsa tempo), cioè il tempo in cui il processo
legato all’algoritmo è eseguito.
Con i moderni calcolatori, dotati di grande capacità di memoria, la risorsa
spazio è divenuta meno importante.
Sarà, dunque, più importante concentrarsi sui tempi di esecuzione.
Algoritmi buoni, per cominciare
Gli algoritmi che prenderemo in considerazione saranno algoritmi buoni,
cioè dovranno essere:
- soluzioni semplici, efficaci e, soprattutto, generali
- facilmente modificabili in caso di necessità
- indipendenti dal linguaggio di programmazione che si vuole utilizzare
Costo di un algoritmo
Misureremo il tempo di esecuzione di un algoritmo in numero di
operazioni che l’algoritmo deve compiere per fornire i risultati.
Tale numero è detto costo dell’algoritmo.
Per quantificare il costo di un algoritmo ci concentreremo sull’algoritmo in
sé, tralasciando la sua implementazione, in quanto:
- l’algoritmo è la descrizione generale dell’intero processo di calcolo
- l’algoritmo ignora tutti i dettagli implementativi legati al linguaggio
- il tempo necessario per l’esecuzione dell’algoritmo è indicativo del tempo
di esecuzione del processo corrispondente
Costo istruzioni semplici
La prima regola di valutazione per il calcolo del costo delle istruzioni
di un algoritmo riguarda le istruzioni semplici:
Le istruzioni semplici quali lettura, scrittura, assegnamento hanno un
costo pari a uno.
Costo = 1
Costo costrutti iterativi (1)
I costrutti iterativi quali MENTRE…FINEMENTRE, RIPETI…FINCHÉ 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 delle
singole istruzioni.
(NON sono inserite nel calcolo le operazioni di apertura e 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 perché
occorre calcolare il test di uscita dal ciclo, quindi avremo:
Costo = CostoTest*(K+1) + CostoCorpo*K
Costo costrutti iterativi (2)
I costrutti iterativi quali PER…ESEGUI hanno un costo ottenuto dalla
somma dei seguenti costi:
- costo dovuto all’inizializzazione della variabile del ciclo: pari a uno.
- costo di condizione di fine ciclo: pari al numero di volte che il ciclo viene
eseguito più un costo pari a uno dovuto al test finale che consente di
uscire dal ciclo.
- costo del corpo del ciclo: pari alla somma dei costi delle singole
istruzioni, 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.
Riassumendo,avremo:
Costo = 1 + CostoTest*(K+1) + CostoIncremento*K + CostoCorpo*K
Costo costrutto di selezione
Il costrutto di selezione SE…ALLORA ha un costo pari alla somma dei
costi del test più quello delle singole istruzioni semplici contenute
all’interno di ALLORA.
Analogamente viene calcolato il costo del costrutto di selezione binaria
SE…ALLORA…ALTRIMENTI, dove sarà utilizzato il costo massimo tra i
due rami.
Avremo, dunque:
Costo = 1 + MAX(CostoRamoALLORA, CostoRamoALTRIMENTI)
Costo chiamata sottoprogramma
L’istruzione di chiamata di un sottoprogramma ha costo pari a quello
dell’intero sottoprogramma, tenendo conto delle regole per le precedenti
situazioni, più il costo dell’istruzione di chiamata, che è pari a uno.
Costo = 1 + CostoSottoprogramma
Costo istruzioni composte
L’istruzione composta (per esempio annidamenti di strutture di controllo e
blocchi di istruzioni) ha una complessità pari alla somma dei costi delle
singole istruzioni semplici che la compongono.
Costo =  CostoIstruzioniSemplici
Regole e costi
Utilizzando queste regole per il calcolo dei vari costi, avremo risultati
approssimati perché è consuetudine occuparsi solo di operazioni dal costo
dominante (confronti, moltiplicazioni ecc.) trascurando le istruzioni meno
onerose dal punto di vista esecutivo (addizioni, incrementi, decrementi ecc.).
Pertanto, 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’algoritmo per esso, per ottenere il tempo totale di esecuzione dell’algoritmo.
Complessità computazionale
Il numero di operazioni di un algoritmo è legato anche alla dimensione dei
dati di input (N), perciò risulta da essa definibile.
Usiamo la notazione generica T(N) per indicare la funzione matematica
che indica la relazione esistente tra il numero di operazioni di un
programma e la dimensione N dei dati in input.
La funzione T(N) è chiamata complessità computazionale (in tempo) o
semplicemente complessità dell’algoritmo. La dimensione N dei dati in
input è anche detta dimensione del problema.
Sequenza dati in input
Per effettuare una buona analisi dell’algoritmo, spesso è necessario
valutare la sequenza di dati che si presentano in input e i loro specifici
valori.
Pertanto:
La complessità è legata ai valori dei dati in input e a come i dati sono
disposti, oltre che alla loro dimensione.
Caso ottimo, pessimo e medio
Per valutare le prestazioni di alcuni algoritmi si dovrebbero prevedere
tre tipi di analisi, ovvero:
- analisi del caso ottimo
- analisi del caso pessimo
- analisi del caso medio
La funzione va analizzata nei tre casi in base alla disposizione e al tipo di
dati in ingresso.
Utilizziamo le seguenti notazioni:
Tottimo(N)
Tpessimo(N) Tmedio(N)
Per quale analisi optare?
Per decidere quale considerare delle tre, va analizzato il tipo di applicazione.
Per esempio, in sistemi real-time interessa solo la funzione Tpessimo(N), ma in
genere è la funzione Tmedio(N) quella che riveste maggiore importanza.
Pertanto, parlando di complessità di un algoritmo ci riferiamo implicitamente
all’analisi del caso medio.
Quando l’analisi del caso medio si rivela complicata si ricorre al caso
pessimo.
Possiamo considerare che la funzione Tpessimo(N) fornisca una limitazione
alla complessità dell’algoritmo, nel senso che le altre funzioni di complessità
non possono crescere più di come accade nel caso pessimo.
L’algoritmo ottimo
L’obiettivo è quello di risolvere un problema scrivendo un algoritmo con la
minima complessità possibile sia nel caso medio sia in quello pessimo.
Definiremo tale algoritmo un algoritmo ottimo.
Pertanto, per complessità di un problema si intende la complessità degli
algoritmi ottimi che lo risolvono.
Ordine di grandezza della complessità
Molto spesso risulta difficile giungere a una formulazione della complessità
di un algoritmo.
Confrontando due algoritmi, può anche accadere che il primo esegua
meno operazioni con dimensione dal problema bassa, ma che le cose si
ribaltino quando tale dimensione cresce.
In questi casi entra in gioco l’ordine di grandezza della complessità, cioè la
valutazione complessiva per valori molto grandi delle dimensioni del
problema. Si parla di complessità asintotica.
Complessità asintotica
La complessità asintotica si esprime con la notazione matematica:
lim T(N)
N
che si legge: limite per N che tende a infinito della funzione T(N).
Si ottiene, cioè, un’espressione in funzione di N che indica qual è il
comportamento asintotico dell’algoritmo.
Ordine di grandezza
Introduciamo le seguenti definizioni:
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)), che 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)), significa che g(N) è un limite superiore
alla legge di crescita di T(N), cioè T(N) non cresce più di g(N).
Diremo, inoltre, che:
f(N) è proporzionale a g(N) se f(N) è O(g(N)) e g(N) è O(f(N)).
Pertanto, f(N) e g(N) sono dello stesso ordine (di grandezza).
Grafico ordine di grandezza
Il grafico di T(N) da un certo punto in poi sarà al di sotto di quello di C*g(N)
Costante di proporzionalità
Possiamo, ora affermare che:
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: essendo comportamenti asintotici, sia la
costante 5, sia la costante moltiplicativa 2 (perché si parla di limiti),
risultano irrilevanti, per determinare l’ordine di grandezza. Ciò che
interessa è il termine N2.
Più formalmente:
Classi di complessità
Possiamo individuare alcuni ordini di grandezza per le funzioni T(N) che
individuano le cosiddette classi di complessità (o di computabilità).
Abbiamo i seguenti casi:
- Complessità costante O(1) o O(C)
- Complessità logaritmica O(logN)
- Complessità lineare O(N)
- Complessità NlogN O(NlogN)
- Complessità polinomiale O(NK)
- Complessità esponenziale O(KN)
Complessità costante O(1) o O(C)
La Complessità costante O(1) o O(C) indica la complessità degli algoritmi
che eseguono lo stesso numero di operazioni indipendentemente dalla
dimensione dei dati in input.
Complessità logaritmica O(logN)
La Complessità logaritmica O(logN) indica la complessità degli algoritmi
che eseguono un numero di operazioni proporzionale a log N.
Complessità lineare O(N)
La Complessità lineare O(N) indica la complessità degli algoritmi che
eseguono un numero di operazioni proporzionale a N, cioè proporzionale
alle dimensione del problema.
Complessità NlogN O(NlogN)
La Complessità NlogN O(NlogN) è la classe di molti algoritmi di ordinamento.
Complessità polinomiale O(NK)
La caratteristica della Complessità polinomiale O(NK) è di avere dimensioni
del problema come base da elevare a un esponente K.
Quando K=2 si parla di complessità quadratica.
Quando K=3 si parla di complessità cubica.
Complessità esponenziale O(KN)
La caratteristica della Complessità esponenziale O(KN) è di avere
dimensioni del problema come esponente.
Algoritmi con medesima classe di complessità
Due algoritmi appartenenti alla stessa classe di complessità possono
essere confrontati relativamente al tempo di esecuzione.
Due algoritmi appartengono alla stessa classe di complessità quando:
Siano f1(N) e f2(N) due funzioni con la stessa classe di complessità
computazionale, relative, rispettivamente, agli algoritmi A1 e A2 che
risolvono lo stesso problema.
Diremo che A1 e A2 appartengono alla stessa classe di complessità se:
Efficienza di un algoritmo
Visto quando due algoritmi appartengono alla stessa classe di
complessità, possiamo, ora, valutare l’efficienza di tali algoritmi.
In particolare, diremo che:
1. A1 è più efficiente di A2 se:
2. A2 è più efficiente di A1 se:
Tempo di esecuzione in base alla classe di complessità
Data una classe di complessità, dalla seguente tabella possiamo notare come varia il tempo
di esecuzione di un algoritmo (espresso in secondi) al variare della dimensione N del
problema con un computer che esegue un milione di operazioni elementari al secondo.
Problemi facili e difficili
L’analisi della complessità computazionale degli algoritmi permette di
classificare i problemi.
Dati due problemi A e B, supponiamo che:
- esista un algoritmo per risolvere A in tempo O(N) nel caso pessimo;
- il migliore algoritmo per B abbia complessità O(2N) nel caso pessimo.
Possiamo concludere che:
- B è un problema “più difficile” di A;
- B non ammette soluzioni altrettanto efficienti quanto A.
Problemi trattabili e intrattabili
La classificazione basata sulla complessità computazionale è una
classificazione “fine”, ma in generale è utile classificare i problemi sulla
base di una classificazione più “grossolana”:
- un problema con complessità polinomiale nel caso pessimo, cioè O(NP)
per qualche P, è considerato trattabile
- un problema con complessità almeno esponenziale nel caso pessimo,
ovvero O(CN) per qualche C, è considerato intrattabile.
Gli algoritmi con complessità esponenziale sono da considerare con
diffidenza perché possono richiedere tempi di esecuzione lunghi anche per
valori di N piccoli e indipendentemente dalla velocità dell’elaboratore.
Purtroppo esistono ancora molti problemi, detti intrattabili, per cui NON è
presente un algoritmo NON esponenziale.
Nel caso dei sistemi di crittografia questo è un bene.
Problemi P e NP
La classe di problemi risolvibili in tempo polinomiale è chiamata P.
Esiste una classe di problemi, chiamata NP, per ciascuno dei quali esiste
un algoritmo esponenziale che lo risolve, ma non si sa se esistano
algoritmi polinomiali che lo risolvano.
La classe P è contenuta nella classe NP o coincide con essa, ma è ancora
un problema aperto dimostrare che P è contenuta strettamente in NP.
Complessità computazionale spaziale
In conclusione, accenniamo alla complessità computazionale spaziale,
intesa come dimensione della struttura dati necessaria a supportare
l’esecuzione di un algoritmo.
Generalmente si associa la complessità spaziale all’ordine di grandezza
della memoria e computazionale all’ordine di grandezza del tempo
necessario alla computazione.
In conclusione…
Lo studio qualitativo condotto deve farci riflettere sul tipo di considerazioni
che vanno fatte quando si affronta la progettazione di un algoritmo o
quando si confrontano due o più algoritmi.
Lo studio asintotico, infatti, indaga sulla qualità intrinseca di un algoritmo.
Ci serve per stabilire quale sia l’algoritmo “migliore” al crescere della
dimensione del problema.
Ogni algoritmo deve, comunque, essere esaminato caso per caso, perché
NON è possibile condurre lo studio su schemi preordinati.
Fly UP