Fondamenti di informatica un approccio a oggetti con Java Luca Cabibbo Complessità Capitolo 22 Marzo 2006 1 Complessità Fondamenti di informatica Contenuti 2 Complessità dei metodi determinazione sperimentale della complessità analisi di complessità analisi asintotica operazione dominante Complessità degli algoritmi e dei problemi Giustificazione dell’analisi asintotica Discussione Complessità Fondamenti di informatica Introduzione Una importante qualità del software è l’efficienza un programma è tanto più efficiente quanto minori sono le risorse di calcolo necessarie per la sua esecuzione le risorse di calcolo usate da un programma sono, tra l’altro, il processore, le memorie (centrale e secondarie), la rete di calcolatori Questo capitolo studia il problema determinare l’efficienza dei metodi rispetto all’impiego della risorsa processore l’efficienza di un metodo viene espressa mediante una funzione che descrive il costo computazionale del metodo, ovvero il numero di “operazioni elementari” eseguite durante l’esecuzione del metodo, contato rispetto alla dimensione dei dati di ingresso del metodo il costo computazionale di un metodo è una stima (approssimata) della funzione che descrive il tempo di esecuzione del metodo rispetto ai dati di ingresso 3 Complessità Fondamenti di informatica Complessità dei metodi Un metodo è tanto più efficiente quanto minori sono le risorse di calcolo necessarie per la sua esecuzione la risorsa di calcolo su cui viene concentrata l’attenzione è il processore, il cui uso è misurato dal tempo impiegato nell’esecuzione del metodo Il tempo di esecuzione di un metodo viene considerato un costo speso nella sua esecuzione, chiamato costo computazionale o complessità computazionale del metodo intuitivamente, l’analisi di complessità di un metodo M ha lo scopo di determinare una funzione tM(I), chiamata funzione di costo del metodo, che descrive il tempo di esecuzione di M in modo parametrico rispetto all’insieme I di dati di ingresso le funzioni di costo vengono normalmente determinate in modo approssimato, per diverse ragioni 4 Complessità Fondamenti di informatica Obiettivi della determinazione della complessità Prima di studiare come determinare la complessità computazionale dei metodi, è importante comprendere le finalità della funzione di costo la funzione di costo dei metodi è significativa in senso assoluto conoscendo la funzione di costo di un metodo è possibile capire se il tempo di esecuzione di un metodo su certi dati di ingresso è accettabile (perché limitato superiormente da un periodo di tempo ragionevolmente piccolo) oppure non accettabile (perché limitato inferiormente da un periodo di tempo molto grande) la funzione di costo dei metodi è significativa anche in senso relativo sulla base della complessità, è possibile ad esempio decidere quale metodo scegliere per risolvere un problema, tra due o più metodi che risolvono quel problema 5 Complessità Fondamenti di informatica Determinazione della complessità Come è possibile determinare la complessità computazionale di un metodo? nell’approccio sperimentale il tempo di esecuzione di un metodo viene misurato con un cronometro l’analisi di complessità ha invece l’obiettivo di determinare la funzione di costo dei metodi in modo analitico (e approssimato), superando alcuni dei limiti dell’approccio sperimentale 6 Complessità Fondamenti di informatica Determinazione sperimentale della complessità Prima di introdurre l’analisi di complessità (una tecnica analitica per la valutazione della complessità) viene discussa la possibilità di misurare sperimentalmente la complessità dei metodi vengono anche illustrati i limiti e le problematiche nella determinazione sperimentale della complessità, che giustificano le ipotesi che vengono fatte nell’analisi di complessità La complessità temporale di un metodo può essere determinata in modo sperimentale eseguendo più volte il metodo, usando ogni volta un diverso insieme di dati di ingresso, e misurando con un cronometro il tempo impiegato per ciascuna esecuzione del metodo 7 Complessità Fondamenti di informatica Dimensione dei dati di ingresso In generale, la funzione di costo di un metodo non viene espressa in modo parametrico rispetto all’insieme di dati di ingresso, ma piuttosto in modo parametrico rispetto alla dimensione dei dati di ingresso la dimensione di un insieme di dati è una misura dello spazio di memoria necessario a memorizzare quell’insieme di dati ad esempio, la complessità di un metodo che calcola la somma o il massimo tra gli elementi di un array (passato come parametro) può essere ragionevolmente descritta in funzione della lunghezza dell’array, trascurando invece il valore dei singoli elementi dell’array 8 Complessità Fondamenti di informatica Determinazione mediante un cronometro Il risultato della determinazione sperimentale della complessità di un metodo può essere visualizzato disegnando un grafico in cui ciascun punto descrive una diversa esecuzione del metodo l’ascissa del grafico indica la dimensione N dell’insieme dei dati di ingresso (un numero naturale) l’ordinata del grafico indica il tempo di esecuzione (ad esempio, in millisecondi) Esecuzione del metodo M t (ms) 160 140 120 100 80 60 40 20 0 N 0 9 20 40 60 80 100 120 140 160 nel grafico, un punto di coordinate (N,T) indica che il tempo dell’esecuzione del metodo per un insieme di dati di ingresso di dimensione N è stato T Complessità Fondamenti di informatica Problemi della misurazione sperimentale La determinazione sperimentale della complessità di un metodo presenta un certo numero di problemi la determinazione sperimentale è poco oggettiva, perché dipende dal calcolatore utilizzato il tempo di esecuzione di un metodo dipende, oltre che dall’insieme di dati di ingresso, anche dalla configurazione hardware/software del calcolatore utilizzato, nonché dal carico del calcolatore al momento della sperimentazione a parità di configurazione e carico del calcolatore, il tempo di esecuzione di un metodo non dipende solo dalla dimensione dei dati di ingresso, ma anche dal particolare valore dei dati di ingresso la misurazione sperimentale della complessità di un metodo è possibile solo se il metodo è stato completamente implementato si ha un calcolatore a disposizione per eseguire il metodo 10 Complessità Fondamenti di informatica Dipendenza dall’insieme dei dati di ingresso Un problema relativo al voler esprimere il tempo di esecuzione di un metodo rispetto alla dimensione dei dati di ingresso, è che in realtà il tempo di esecuzione non dipende solo dalla dimensione dei dati di ingresso, ma anche dal particolare valore dei dati di ingresso infatti, il grafico può contenere più punti relativi a insiemi di dati di ingresso che hanno la stessa stessa dimensione ma per cui il tempo di esecuzione è diverso Esecuzione del metodo M t (ms) 160 140 120 100 80 60 40 20 0 N 0 11 20 40 60 80 100 Complessità 120 140 esperimenti relativi a due insiemi di dati di ingresso che hanno la stessa dimensione 160 Fondamenti di informatica Dipendenza dal calcolatore utilizzato Un problema della misurazione sperimentale della complessità è che l’esecuzione dello stesso metodo su calcolatori differenti può richiedere tempi di esecuzione diversi, a causa delle diverse configurazioni dei calcolatori utilizzati (nonché del loro carico) t (ms) Esecuzione del metodo M sul calcolatore A 160 140 120 100 80 60 40 20 0 140 ms N 0 20 40 60 80 100 120 140 160 stesso insieme di dati di ingresso t (ms) Esecuzione del metodo M sul calcolatore B 160 140 120 100 80 60 40 20 0 100 ms N 0 12 20 40 60 80 100 120 Complessità 140 160 Fondamenti di informatica Analisi di complessità Per superare i limiti e i problemi della determinazione sperimentale della complessità dei metodi, viene adottato un approccio analitico, chiamato analisi di complessità, per esprimere la funzione di costo dei metodi in modo indipendente dal particolare calcolatore dalla particolare scelta dei dati di ingresso ma (per quanto possibile) dipendente dalla dimensione dei dati di ingresso 13 Complessità Fondamenti di informatica Operazioni elementari e costo computazionale Nell’analisi di complessità, il costo computazionale di un metodo è dato dal numero di “operazioni elementari” eseguite durante dell’esecuzione del metodo, espresso in funzione della dimensione dei dati di ingresso per il metodo il modello di costo più semplice è quello in cui sono considerate “elementari” le seguenti operazioni ogni istruzione semplice la valutazione di una condizione per semplicità, per il momento viene trascurata la possibilità di invocare metodi e costruttori 14 Complessità Fondamenti di informatica Esempio — somma degli elementi di un array Determinare, usando il semplice modello di costo proposto, la funzione di costo del seguente metodo che calcola la somma degli elementi di un array dati di interi /* Calcola la somma degli elementi di dati. */ public static int somma(int[] dati) { // pre: dati!=null int n; // lunghezza di dati int s; // somma degli elementi di dati int i; // indice per la scansione di dati /* calcola la somma degli elementi di dati */ n = dati.length; s = 0; for (i=0; i<n; i++) s += dati[i]; return s; } 15 Complessità Fondamenti di informatica Dimensione dei dati di ingresso /* Calcola la somma degli elementi di dati. */ public static int somma(int[] dati) { ... } L’insieme dei dati di ingresso per il metodo somma è costituito dall’array dati di interi la dimensione di questo insieme di dati può essere quantificata dal numero di elementi dell’array dati, ovvero dalla lunghezza di dati Nell’analisi di complessità, la dimensione dell’insieme dei dati di ingresso dei metodi viene solitamente espressa mediante un numero naturale indicato dal simbolo N, opportunamente accompagnato da una descrizione della sua interpretazione ad esempio, in questo caso si vuole determinare la funzione di costo del metodo somma rispetto alla lunghezza N dell’array dati 16 Complessità Fondamenti di informatica Funzione di costo /* Calcola la somma degli elementi di dati. */ public static int somma(int[] dati) { ... } Lo scopo dell’analisi di complessità di un metodo è determinare la funzione di costo del metodo rispetto alla dimensione dei dati di ingresso del metodo in questo caso, bisogna determinare la funzione tsomma(N) che esprime il costo computazione dell’esecuzione del metodo somma rispetto alla lunghezza N dell’array dati in generale, per un particolare valore N, tsomma(N) indica il numero unità di costo spese durante l’esecuzione di somma quando dati ha lunghezza N nel modello di costo considerato, ciascuna unità di costo corrisponde all’esecuzione di una operazione elementare dato che si sta considerando la complessità temporale dei metodi, per un particolare valore N, il numero tsomma(N) è da considerarsi commisurato al tempo di esecuzione di somma quando dati ha lunghezza N 17 Complessità Fondamenti di informatica Determinazione della funzione di costo Quante operazioni elementari vengono eseguite nell’esecuzione del metodo somma quando dati ha lunghezza N? /* Calcola la somma degli elementi di dati. */ public static int somma(int[] dati) { // pre: dati!=null int n; // lunghezza di dati int s; // somma degli elementi di dati int i; // indice per la scansione di dati /* calcola la somma degli elementi di dati */ n = dati.length; s = 0; for (i=0; i<n; i++) s += dati[i]; return s; } 18 bisogna identificare le operazioni elementari nel corpo del metodo, e contare quante volte ciascuna operazione elementare venga eseguita (in funzione di N) Complessità Fondamenti di informatica Determinazione della funzione di costo Quante operazioni elementari vengono eseguite nell’esecuzione del metodo somma quando dati ha lunghezza N? n = dati.length è una operazione elementare viene eseguita una volta s = 0 è una operazione elementare viene eseguita una volta return s è una operazione elementare viene eseguita una volta rimane da determinare il costo computazione dell’esecuzione dell’istruzione ripetitiva 19 Complessità Fondamenti di informatica Determinazione della funzione di costo In generale, relativamente a una esecuzione di una istruzione ripetitiva for l’inizializzazione viene eseguita una sola volta se il corpo viene eseguito N volte, allora la condizione viene valutata N+1 volte e l’incremento viene eseguito N volte Relativamente all’istruzione ripetitiva del metodo somma se dati ha lunghezza N, allora il corpo dell’istruzione ripetitiva for viene eseguito N volte i = 0 è una operazione elementare viene eseguita una volta i < n è una operazione elementare viene eseguita N+1 volte i++ è una operazione elementare viene eseguita N volte s += dati[i] è una operazione elementare viene eseguita N volte 20 Complessità Fondamenti di informatica Determinazione della funzione di costo /* Calcola la somma degli elementi di dati. */ public static int somma(int[] dati) { // pre: dati!=null int n; // lunghezza di dati int s; // somma degli elementi di dati int i; // indice per la scansione di dati /* calcola la somma degli elementi di dati */ n = dati.length; s = 0; for (i=0; i<n; i++) s += dati[i]; return s; } Complessivamente, la funzione di costo tsomma(N) è data da tsomma(N) = 1 + 1 + 1 + (N+1) + N + N + 1 = 3N+5 costo di costo di costo di n = dati.length s = 0 i=0 21 costo di i<N Complessità costo di costo di i++ s += dati[i] costo di return s Fondamenti di informatica Discussione La funzione di costo di un metodo descrive il costo di esecuzione del metodo in termini di generiche “unità di costo,” corrispondenti al costo (temporale) di esecuzione delle operazioni elementari ad esempio, la funzione di costo tsomma(N) = 3N+5 può essere interpretata come segue per calcolare la somma degli elementi di un array di lunghezza N sono necessarie 3N+5 unità di costo, ovvero bisogna eseguire 3N+5 operazioni elementari intuitivamente, questo significa che il tempo di esecuzione di somma è in qualche modo commisurato a 3N+5 in effetti, l’analisi di complessità ha lo scopo di determinare l’andamento del tempo di esecuzione di un metodo, anche se solo in modo approssimato 22 Complessità Fondamenti di informatica Funzione di costo e tempo di esecuzione In generale, non è possibile determinare esattamente, in modo analitico, il tempo di esecuzione (in secondi) di un metodo, per diversi motivi il tempo di esecuzione di una operazione elementare dipende dalla particolare operazione elementare considerata ad esempio, il tempo di esecuzione dell’operazione i++ è (intuitivamente) diverso da quello di s += somma[i] il tempo di esecuzione di ogni operazione varia da calcolatore a calcolatore, anche in modo dipendente dalla configurazione e dal carico del calcolatore inoltre, a parità di calcolatore, configurazione e carico, il tempo di esecuzione di una operazione può dipendere, in modo complesso, dalla particolare sequenza di operazioni eseguita dal calcolatore nell’approccio analitico, è quindi possibile determinare il tempo di esecuzione di un metodo solo in modo approssimato 23 Complessità Fondamenti di informatica Modelli di costo e approssimazioni Ogni possibile modello di costo introduce comunque delle approssimazioni ad esempio, il semplice modello di costo considerato assegna costo computazionale 1 a qualsiasi operazione elementare in realtà il tempo di esecuzione di una operazione elementare op è (circa) uguale a un qualche valore top, che può essere determinato sperimentalmente con riferimento a un particolare calcolatore confondere top con 1 vuol dire accettare approssimazioni a meno di fattori costanti nell’analisi di complessità sono in effetti ritenute accettabili le approssimazioni a meno di fattori costanti (e a meno di termini di ordine inferiore), che sono le approssimazioni che vengono effettivamente introdotte dai modelli di costo utilizzati in pratica 24 Complessità Fondamenti di informatica Un modello di costo per Java D’ora in poi verrà fatto riferimento al modello di costo descritto sinteticamente nella seguente tabella Operazione s Costo ts di una esecuzione di s istruzione semplice (senza invocazioni di metodo) — espressione, assegnazione, incremento, istruzione return e new blocco — { s1 s2 ... sK } 1 ts1 + ts2 + ... + tsK istruzione condizionale — if (cond) s1 else s2 tcond + ts1 — se cond è true tcond + ts2 — se cond è false istruzione ripetitiva — while (cond) s — nell’ipotesi che il corpo del while venga eseguito N volte tcond(1) + ts(1) + tcond(2) + ts(2) + ... + tcond(N) + ts(N) + tcond(N+1) invocazione di metodo o costruttore r.M(p1, ..., pK) 25 tr + tp1 + ... + tpK + tM Complessità Fondamenti di informatica Un modello di costo per Java Istruzioni semplici l’esecuzione di una istruzione semplice (ovvero, che non contiene invocazione di metodi o costruttori) ha costo unitario intuitivamente, viene assegnato costo unitario a ciascuna operazione che corrisponde all’esecuzione di un numero finito e costante di operazioni “primitive” — le operazioni primitive comprendono le operazioni aritmetiche, l’accesso a un elemento di un array e il calcolo di una espressione e la restituzione del suo valore da parte di un metodo è considerato unitario anche il costo dell’esecuzione di una operazione new nel caso di creazione di un oggetto è necessario però considerare anche il costo di esecuzione del costruttore Blocco il costo dell’esecuzione di un blocco di operazioni è pari alla somma dei costi di esecuzione delle operazioni che costituiscono il blocco 26 Complessità Fondamenti di informatica Un modello di costo per Java Istruzioni condizionali il costo dell’esecuzione di una istruzione if-else è dato dal costo della valutazione della condizione più il costo dell’esecuzione della parte (if oppure else) che viene eseguita il costo dell’esecuzione di una istruzione if o di una cascata di istruzioni condizionali può essere determinato in modo simile Istruzioni ripetitive il costo dell’esecuzione di una istruzione while è dato dalla somma dei costi delle valutazioni della condizione più la somma dei costi delle esecuzioni del corpo se il corpo di una istruzione ripetitiva while viene eseguito N volte allora la condizione viene valutata N+1 volte è possibile che ciascuna valutazione della condizione ed esecuzione del corpo abbia costo diverso il costo dell’esecuzione di una istruzione for o do-while può essere determinato in modo simile 27 Complessità Fondamenti di informatica Un modello di costo per Java Invocazione di metodo (o costruttore) il costo dell’esecuzione di una invocazione di metodo è data dalla somma del costo del calcolo dell’oggetto ricevitore e dei parametri attuali, più il costo dell’esecuzione del metodo nel caso in cui l’oggetto ricevitore o un parametro attuale sia semplicemente una variabile, un letterale oppure una espressione semplice (che non contenga nessuna invocazione di metodo), il costo del calcolo del ricevitore o del parametro attuale viene considerato nullo bisogna invece tenere in considerazione il costo del calcolo dell’oggetto ricevitore e dei parametri attuali in particolare nella determinazione del costo di una cascata o composizione di invocazioni di metodi 28 Complessità Fondamenti di informatica Esempio — somma degli elementi di un array Determinare la funzione di costo del metodo somma (rispetto alla lunghezza N di dati) con riferimento al modello di costo proposto /* Calcola la somma degli elementi di dati. */ public static int somma(int[] dati) { // pre: dati!=null int n; // lunghezza di dati int s; // somma degli elementi di dati int i; // indice per la scansione di dati /* calcola la somma degli elementi di dati */ n = dati.length; s = 0; for (i=0; i<n; i++) s += dati[i]; return s; } 29 in questo caso, il modello di costo non è in contraddizione con il semplice modello di costo considerato in precedenza, e la funzione di costo è data ancora da tsomma(N) = 3N + 5 Complessità Fondamenti di informatica Esempio — somma degli elementi di una matrice Determinare la funzione di costo del metodo sommaMatrice che calcola la somma degli elementi dell’array bidimensionale mat, rispetto al numero R di righe e al numero C di colonne di mat /* Calcola la somma degli elementi di mat. */ public static int sommaMatrice(int[][] mat) { // pre: mat!=null && mat è rettangolare int r; // numero di righe di mat int s; // somma degli elementi di mat int i; // indice per la scansione delle righe di mat /* calcola la somma degli elementi di mat */ r = mat.length; s = 0; for (i=0; i<r; i++) /* somma a s la somma degli elementi della riga * i-esima di mat */ s += somma(mat[i]); return s; } 30 Complessità Fondamenti di informatica Somma degli elementi di una matrice Nel metodo sommaMatrice l’operazione r=mat.length viene eseguita una volta e ha costo unitario l’operazione s=0 viene eseguita una volta e ha costo unitario l’operazione return s viene eseguita una volta e ha costo unitario l’istruzione ripetitiva viene eseguita una sola volta bisogna determinare il costo dell’istruzione ripetitiva 31 Complessità Fondamenti di informatica Somma degli elementi di una matrice Relativamente all’esecuzione dell’istruzione ripetitiva il corpo dell’istruzione ripetitiva viene eseguito R volte (R è il numero di righe di mat), con i che varia tra 0 (compreso) e R (escluso) l’operazione i = 0 viene eseguita una volta e ha costo unitario l’operazione i < r viene eseguita R+1 volte, e ciascuna esecuzione ha costo unitario l’operazione s += somma(mat[i]) viene eseguita R volte, con i che varia tra 0 (compreso) e R (escluso) bisogna determinare il costo delle esecuzioni di questa operazione l’operazione i++ viene eseguita R volte, e ciascuna esecuzione ha costo unitario 32 Complessità Fondamenti di informatica Somma degli elementi di una matrice Relativamente alle esecuzioni dell’operazione s += somma(mat[i]), che viene eseguita con i che varia tra 0 (compreso) e R (escluso) per ogni singola invocazione del metodo somma, il costo della valutazione dei parametri e dell’oggetto ricevitore è nullo per ogni singola esecuzione del metodo somma, il costo dell’esecuzione del metodo è tsomma(N) = 3N + 5, in cui N è il numero di elementi di mat[i] essendo mat una matrice rettangolare, tutte le righe di mat hanno la stessa lunghezza, C ciascuna esecuzione di somma ha costo 3C + 5 33 Complessità Fondamenti di informatica Somma degli elementi di una matrice Il costo complessivo del metodo sommaMatrice è dato dai seguenti termini 3 per le operazioni fuori dall’istruzione ripetitiva 1 per l’inizializzazione dell’istruzione ripetitiva R+1 per la condizione dell’istruzione ripetitiva R per l’incremento dell’istruzione ripetitiva (3C + 5)•R per il corpo dell’istruzione ripetitiva Sommando questi termini, è possibile concludere che tsommaMatrice(R,C) = 2R + 5 + (3C + 5)•R = 3RC + 7R +5 nel caso in cui mat sia quadrata, se N è il numero di elementi di mat allora R = C = √N e la funzione di costo rispetto a N è tsommaMatrice(N) = 3N + 7√N +5 34 Complessità Fondamenti di informatica Esercizio — somma della diagonale principale Determinare la funzione di costo di sommaDiagonale, che calcola la somma degli elementi della diagonale principale della matrice quadrata mat, rispetto al numero N di elementi di mat /* Calcola la somma della diagonale principale di mat. */ public static int sommaDiagonale(int[][] mat) { // pre: mat!=null && mat è quadrata int r; // numero di righe e colonne di mat int s; // somma della diagonale principale di mat int i; // indice per la scansione di mat /* calcola la somma della diagonale principale di mat */ r = mat.length; s = 0; for (i=0; i<r; i++) s += mat[i][i]; return s; } Soluzione tsommaDiagonale(N) = 3√N +5 35 Complessità Fondamenti di informatica Dipendenza dall’insieme di dati di ingresso Si consideri ora il problema di determinare la funzione di costo del seguente metodo di ricerca di un valore chiave nell’array di interi dati rispetto alla lunghezza N di dati /* Verifica se dati contiene un elemento uguale a chiave. */ public static boolean ricerca(int[] dati, int chiave) { // pre: dati!=null int n; // lunghezza di dati int i; // indice per la scansione di dati boolean contiene; // dati contiene un elemento // uguale a chiave /* cerca un elemento uguale a chiave in dati */ n = dati.length; contiene = false; for (i=0; i<n; i++) if (dati[i]==chiave) contiene = true; return contiene; } 36 Complessità Fondamenti di informatica Dipendenza dall’insieme di dati di ingresso /* Verifica se dati contiene un elemento uguale a chiave. */ public static boolean ricerca(int[] dati, int chiave) { // pre: dati!=null int n; // lunghezza di dati int i; // indice per la scansione di dati boolean contiene; // dati contiene un elemento // uguale a chiave /* cerca un elemento uguale a chiave in dati */ eseguita 1 volta n = dati.length; eseguita 1 volta contiene = false; eseguite (1, N+1, N) volte for (i=0; i<n; i++) eseguita N volte if (dati[i]==chiave) eseguita K volte (0≤K≤N) contiene = true; eseguita 1 volta return contiene; } In questo caso, la funzione di costo di ricerca è tricerca(N) = 3N + K + 5 37 in cui K indica il numero di elementi di dati che sono uguali a chiave (0≤K≤N) Complessità Fondamenti di informatica Caso peggiore e caso migliore Il costo tricerca(N) = 3N + K + 5 di esecuzione del metodo ricerca dipende non solo dalla la lunghezza N di dati, ma anche dal numero K di elementi di dati uguali a chiave tricerca(N) = 3N + K + 5 In questa formula, K è compreso tra 0 e N K vale 0 se nessun elemento di dati è uguale a chiave in questo caso, 3N + 5 indica il costo dell’esecuzione di ricerca nel caso migliore (nel senso di caso che conduce al costo minimo, e quindi migliore, e non nel senso di caso che produce il miglior risultato) K vale N se tutti gli elementi di dati sono uguali a chiave in questo caso, 3N + N + 5 = 4N + 5 indica il costo dell’esecuzione di ricerca nel caso peggiore (nel senso di caso che conduce al costo massimo, e quindi peggiore) 38 Complessità Fondamenti di informatica Analisi di caso peggiore e di caso migliore Un possibile modo per limitare la dipendenza della funzione di costo dall’insieme di dati di ingresso alla sola dimensione di tale insieme consiste nel determinare la funzione di costo nel caso peggiore o nel caso migliore nell’analisi di caso peggiore si fa riferimento a quei valori per l’insieme di dati di ingresso per cui il costo di esecuzione è massimo (a parità di dimensione) per ogni N, tM(N) indica il costo massimo di esecuzione di M tra tutti gli insiemi di dati di ingresso di dimensione N similmente per l’analisi di caso migliore 3N + 5 — nel caso migliore tricerca(N) = 39 4N + 5 — nel caso peggiore Complessità Fondamenti di informatica Analisi di caso peggiore Nell’analisi di complessità, la funzione di costo viene solitamente determinata rispetto al caso peggiore nell’analisi di caso peggiore, la funzione di costo indica il costo massimo di esecuzione del metodo, rispetto alla dimensione dei dati di ingresso in effetti, l’analisi di complessità si pone solitamente l’obiettivo di determinare la quantità massima (ovvero, sicuramente sufficiente) di risorse che vanno impiegate nell’esecuzione di un metodo o programma solo in alcuni casi, vengono effettuate analisi di caso migliore (o “medio”), indicando esplicitamente che non si tratta di analisi di caso peggiore Quindi, nel caso del metodo ricerca, si arriva alla conclusione che la funzione di costo è tricerca(N) = 4N + 5, da interpretarsi come il costo computazionale per l’esecuzione di ricerca è 4N + 5 nel caso peggiore, rispetto alla lunghezza N di dati 40 Complessità Fondamenti di informatica Esempio — coppia di elementi uguali Determinare la funzione di costo del seguente metodo, che verifica se l’array a contiene una coppia di elementi uguali /** Verifica se a contiene una coppia di elementi uguali. */ public static boolean coppiaUguali(int[] a) { // pre: a!=null int n; // lunghezza di a boolean trovataCoppia; // a contiene una coppia // di elementi uguali int i, j; // indici per la scansione di a /* cerca una coppia di elementi uguali esaminando * comunque tutte le coppie di elementi distinti */ n = a.length; trovataCoppia = false; for (i=0; i<n; i++) for (j=i+1; j<n; j++) if (a[i]==a[j]) trovataCoppia = true; return trovataCoppia; } 41 Complessità Fondamenti di informatica Coppia di elementi uguali trovataCoppia = false; n = a.length; for (i=0; i<a.length; i++) ... return trovataCoppia; Per questa porzione di codice valgono le seguenti considerazioni l’operazione trovataCoppia = false viene eseguita una volta l’operazione n = a.length viene eseguita una volta l’operazione return trovataCoppia viene eseguita una volta relativamente all’istruzione ripetitiva più esterna il corpo di questa istruzione ripetitiva viene eseguito N volte (N è la lunghezza di a), con i che varia tra 0 (compreso) e N (escluso) l’operazione i = 0 viene eseguita una volta l’operazione i < n viene eseguita N+1 volte l’operazione i++ viene eseguita N volte 42 Complessità Fondamenti di informatica Coppia di elementi uguali for (j=i+1; j<n; j++) ... Per questa porzione di codice valgono le seguenti considerazioni questa istruzione ripetitiva (più interna) viene eseguita N volte la prima volta con i che vale 0, la seconda con i che vale 1, ..., l’ultima volta con i che vale N–1 relativamente all’esecuzione di questa istruzione ripetitiva nel caso in cui i che vale I il corpo di questa istruzione ripetitiva viene eseguito N–1–I volte (N–1 volte quando I vale 0, ..., 0 volte quando I vale N–1), con j che varia tra I+1 (compreso) e N (escluso) l’operazione j = i+1 viene eseguita una volta l’operazione j < n viene eseguita N–1–I+1 = N–I volte l’operazione j++ viene eseguita N–1–I volte 43 Complessità Fondamenti di informatica Coppia di elementi uguali for (j=i+1; j<n; j++) ... Per questa porzione di codice valgono le seguenti considerazioni complessivamente (ovvero, considerando tutte le esecuzioni di questa istruzione ripetitiva) N–1 (1) = N volte l’operazione j = i+1 viene eseguita ΣI=0 N–1 (N–I) = l’operazione j < n viene eseguita ΣI=0 N2 – N(N–1)/2 = (N2 + N)/2 volte N–1 (N–1–I) = (N2 – N)/2 l’operazione j++ viene eseguita ΣI=0 volte il corpo di questa istruzione ripetitiva viene eseguito ΣI=0N–1 (N–1–I) = (N2 – N)/2 volte Nel calcolo delle sommatorie è stata utilizzata la formula di Gauss (si noti che viene spesso applicata con M = N–1) Σk=0M k = M (M+1) / 2 44 Complessità Fondamenti di informatica Coppia di elementi uguali if (a[i]==a[j]) ... Per questa porzione di codice valgono le seguenti considerazioni questa istruzione condizionale viene eseguita una volta per ciascuna esecuzione del corpo dell’istruzione ripetitiva più interna in ciascuna esecuzione di questa istruzione condizionale, la condizione a[i]==a[j] viene valutata una volta, e la parte if viene eseguita solo se la condizione si è verificata complessivamente, questa istruzione condizionale viene eseguita (N2 – N)/2 volte la condizione a[i]==a[j] viene valutata complessivamente (N2 – N)/2 volte la parte if viene eseguita un numero di volte compreso tra 0 (se gli elementi di a sono tutti distinti) e (N2 – N)/2 (se gli elementi di a sono tutti uguali) 45 Complessità Fondamenti di informatica Coppia di elementi uguali trovataCoppia = true; Per questa porzione di codice valgono le seguenti considerazioni questa istruzione è la parte if dell’istruzione condizionale complessivamente, l’istruzione condizionale viene eseguita (N2 – N)/2 volte, e la sua parte if un numero di volte compreso tra 0 e (N2 – N)/2 l’operazione trovataCoppia = true viene eseguita un numero di volte compreso tra 0 (se gli elementi di a sono tutti distinti, caso migliore) e (N2 – N)/2 (se gli elementi di a sono tutti uguali, caso peggiore) 46 Complessità Fondamenti di informatica Coppia di elementi uguali È ora possibile determinare la funzione di costo del metodo coppiaUguali l’operazione trovataCoppia = false viene eseguita (complessivamente) una volta l’operazione n = a.length viene eseguita una volta l’operazione return trovataCoppia viene eseguita una volta l’operazione i = 0 viene eseguita una volta l’operazione i < n viene eseguita N+1 volte l’operazione i++ viene eseguita N volte l’operazione j = i+1 viene eseguita N volte 2 l’operazione j < n viene eseguita (N + N)/2 volte 2 l’operazione j++ viene eseguita (N – N)/2 volte 2 la condizione a[i]==a[j] viene valutata (N – N)/2 volte 2 l’operazione trovataCoppia = true viene eseguita (N – N)/2 volte (nel caso peggiore) 47 tcoppiaUguali(N) = 2N2 + 2N + 5 Complessità Fondamenti di informatica Esercizio Analizzare la funzione di costo del seguente metodo ricercaSeq /* Verifica se dati contiene un elemento uguale a chiave. */ public static boolean ricercaSeq(int[] dati, int chiave) { // pre: dati!=null int n; // lunghezza di dati int i; // indice per la scansione di dati boolean contiene; // dati contiene un elemento uguale a chiave /* cerca un elemento uguale a chiave in dati */ n = dati.length; contiene = false; i = 0; while (!contiene && i<n) { if (dati[i]==chiave) contiene = true; i++; } return contiene; } 48 discutere inoltre il caso peggiore e il caso migliore per il metodo Complessità Fondamenti di informatica Analisi asintotica Il calcolo esatto della funzione di costo tM(N) di un metodo M (in cui viene stabilito esattamente quante volte viene eseguita ciascuna operazione) è nella maggioranza dei casi molto laborioso il livello di dettaglio descritto dalla funzione di costo è veramente necessario? quanto è importante calcolare con precisione quante “operazioni elementari” vengono eseguite? quanto è importante stabilire l’insieme delle “operazioni elementari” (ed eventualmente il loro costo di esecuzione unitario)? se è stata fatta la scelta di dare lo stesso costo a operazioni molto diverse tra loro (come i++ e s += dati[i]) — ritenendo accettabile la semplificazione di fattori costanti — come è possibile semplificare l’analisi senza introdurre ulteriori approssimazioni? 49 Complessità Fondamenti di informatica Analisi asintotica di complessità L’analisi asintotica di complessità si pone l’obiettivo di determinare la funzione di costo in modo asintotico, ovvero a meno di fattori moltiplicativi costanti a meno di termini additivi di ordine inferiore descrivendo l’andamento della funzione di costo per N (la dimensione dei dati di ingresso) tendente all’infinito Nell’analisi asintotica, il costo computazionale dei metodi viene determinato in modo più approssimato (ovvero, meno preciso) rispetto a quanto fatto finora i risultati ottenuti mediante l’analisi asintotica sono comunque significativi nella maggioranza dei casi il modello di costo mostrato in precedenza è già approssimato a meno di fattori moltiplicativi costanti — per essere indipendente dal calcolatore usato i termini additivi di ordine inferiore sono asintoticamente meno importanti dei fattori moltiplicativi costanti 50 Complessità Fondamenti di informatica Comportamento asintotico della funzione di costo L’analisi asintotica di complessità di un metodo M si pone l’obiettivo di determinare l’andamento asintotico TM(N) della funzione di costo tM(N) del metodo M rispetto al modello di costo basato sul conteggio delle operazioni eseguite (che è il modello di costo che è stato considerato finora) nel seguito, saranno usato i simboli tM(N) per indicare la funzione di costo basata sul conteggio delle operazioni (notare la ‘t’ minuscola) TM(N) per indicare la funzione di costo asintotico calcolata mediante l’analisi asintotica (notare la ‘T’ maiuscola) T M(N) tM(N) N 51 Complessità Fondamenti di informatica Comportamento asintotico della funzione di costo La funzione di costo asintotico TM(N) di un metodo M indica il costo di M rispetto alla dimensione N dei dati di ingresso per M, calcolato al crescere della dimensione N e trascurando fattori moltiplicativi costanti e termini additivi di ordine inferiore T M(N) tM(N) N 52 l’analisi asintotica viene svolta rispetto a un modello di costo asintotico se possibile, rispetto a una “istruzione dominante” del metodo prima di mostrare questi strumenti, vengono introdotte le basi matematiche dell’analisi asintotica Complessità Fondamenti di informatica Notazione O La notazione O (si legge ‘o’ grande) è il principale strumento matematico per l’analisi asintotica Date le funzioni f(N) e g(N), si dice che f(N) è O( g(N) ) — scritto anche f(N) ∈ O(g(N)) oppure f(N) = O(g(N)) e letto f è ‘o’ grande di g oppure f(N) è ordine di g(N) — se esistono due costanti positive C e N0 tali che f(N) ≤ C•g(N) per tutti gli N ≥ N0 f(N) = O( g(N) ) vuole dire che la funzione f cresce (in modo asintotico) al più quanto la funzione g a meno di fattori costanti e termini di ordine inferiore, e per N sufficientemente grande Ad esempio la funzione 3N + 5 è O( N ) infatti, 3N + 5 ≤ C• N per tutti gli N maggiori di N0 è vera, ad esempio, scegliendo C=4 e N0=5 2 la funzione 3N + 5 è anche O( N ) la funzione log10 N è O( N ) 53 Complessità Fondamenti di informatica Notazione O In pratica, f(N) è O( g(N) ) se il limite per N tendente all’infinito di f(N)/g(N) esiste e ha un valore finito (eventualmente zero) Ad esempio la funzione 3N + 5 è O( N ) infatti, limN→∞ (3N + 5)/N è finito (vale 3) 2 la funzione 3N + 5 è O( N ) 2 infatti, limN→∞ (3N + 5)/N è finito (vale zero) la funzione log10 N è O( N ) infatti, limN→∞ (log10 N)/N è finito (vale zero) Va osservato che nell’analisi asintotica è possibile dire che f(N) è O( g(N) ), leggendolo f(N) è ordine di g(N), anche se limN→∞ f(N)/g(N) = 0, ovvero anche se f(N) ha un ordine di grandezza diverso (inferiore) da quello di g(N) 54 Complessità Fondamenti di informatica Notazione O Esempi di funzioni f(N) che sono O( g(N) ) g(N) g(N) f(N) f(N) N N g(N) g(N) f(N) f(N) N N 55 Complessità Fondamenti di informatica Notazione Θ La notazione Θ (si legge teta grande) è lo strumento matematico che permette di descrivere il fatto che due funzioni crescono asintoticamente nello stesso modo Date le funzioni f(N) e g(N) si dice che f(N) è Θ( g(N) ) — scritto anche f(N) = Θ( g(N) ) e letto f è teta grande di g — se f(N) è O(g(N)) e g(N) è O(f(N)) f(N) è Θ( g(N) ) se esistono tre costanti positive C1 , C2 e N0 tali che C1•g(N) ≤ f(N) ≤ C2•g(N) per tutti gli N ≥ N0 f(N) = Θ( g(N) ) vuol dire che le funzioni f(N) e g(N) crescono asintoticamente nello stesso modo a meno di fattori costanti e termini di ordine inferiore, e per N sufficientemente grande 56 Complessità Fondamenti di informatica Funzioni usate nell’analisi asintotica Nell’analisi asintotica viene fatto in genere riferimento ad alcune funzioni che hanno una forma particolarmente “semplice”, tra cui 1 — funzione costante log N — funzione logaritmica (per log N si intende log2 N) √ N N — funzione lineare N log N 2 N — funzione quadratica N 2 — funzione esponenziale Osservazioni queste funzioni sono state elencate in ordine asintoticamente crescente (in senso stretto) 2 le funzioni √ N, N e N sono casi particolari delle funzioni polinomiali Nk — che crescono tutte meno di 2N 57 Complessità Fondamenti di informatica Analisi asintotica e notazione Θ L’obiettivo dell’analisi asintotica di complessità di un metodo M è determinare una funzione di costo asintotico TM(N) per M che approssimi la funzione di costo tM(N) in modo asintotico (e che sia possibilmente “semplice”) in linea di principio, l’analisi asintotica di complessità ha lo scopo di determinare una funzione TM(N) tale che tM(N) sia Θ(TM(N)) ma senza prima calcolare tM(N) !!! ovvero, bisogna determinare una funzione (“semplice”) TM(N) che abbia un andamento simile a quello del costo computazionale tM(N) di M a meno di fattori costanti, termini di ordine inferiore, e per N sufficientemente grande la notazione Θ formalizza, nell’analisi asintotica di complessità, la nozione di “quantità di risorse (da impiegare nell’esecuzione di un metodo o programma) a meno di fattori costanti e termini di ordine inferiore” 58 Complessità Fondamenti di informatica Analisi asintotica e notazione O In pratica, non sempre è possibile determinare una funzione di costo asintotico TM(N) per un metodo M che approssimi la funzione di costo tM(N) in modo asintotico, ovvero tale che tM(N) sia Θ( TM(N) ) è invece normalmente possibile determinare una funzione di costo asintotico TM(N) per M che sia una maggiorazione della funzione di costo tM(N) in modo asintotico, ovvero tale che tM(N) sia O( TM(N) ) la notazione O formalizza, nell’analisi asintotica di complessità, la nozione di “quantità sufficiente di risorse (da impiegare nell’esecuzione di un metodo o programma) a meno di fattori costanti e termini di ordine inferiore” 59 Complessità Fondamenti di informatica Analisi asintotica e notazione O Ad esempio, l’analisi asintotica si pone l’obiettivo di concludere che il metodo ricerca ha costo asintotico lineare, Tricerca(N) = O(N) tricerca(N) = 4N + 5 il metodo coppiaUguali ha costo asintotico quadratico, TcoppiaUguali(N) = O( N2 ) 2 tcoppiaUguali(N) = 2N + 2N + 5 il metodo sommaDiagonale ha costo asintotico TsommaDiagonale(N) = O( √N ) tsommaDiagonale(N) = 3√N +5 60 Complessità Fondamenti di informatica Analisi asintotica e notazione O La funzione di costo asintotico TM(N) deve essere determinata in modo da avere un comportamento asintotico il più possibile simile a quello di tM(N) ovvero, la notazione O dovrebbe essere utilizzata per caratterizzare la funzione di costo quanto più possibile, trascurandone solo i fattori moltiplicativi costanti e i termini di ordine inferiore si consideri ad esempio il metodo ricerca sebbene sia matematicamente lecito scrivere tricerca(N) = 4N + 5 = O( N3 ) per indicare che il costo computazionale di ricerca è al più cubico in N è chiaramente preferibile scrivere Tricerca(N) = O(N), per indicare che il costo computazionale di ricerca è lineare (e non cubico) rispetto a N In pratica, la notazione O viene solitamente preferita alla notazione Θ, anche se spesso la notazione O viene usata attribuendogli il significato della notazione Θ 61 Complessità Fondamenti di informatica Funzioni usate nell’analisi asintotica L’andamento asintotico delle funzioni “semplici” usate nell’analisi asintotica è mostrato dal seguente grafico 2^n n^2 1 log n n n 62 il grafico conferma che, per N sufficientemente grande, trascurare un termine additivo di ordine inferiore è una approssimazione meno grave rispetto al trascurare un fattore moltiplicativo costante Complessità Fondamenti di informatica Analisi asintotica di complessità L’analisi asintotica di complessità di un metodo M si pone dunque l’obiettivo di determinare il costo asintotico TM(N) di M, in modo tale che la funzione di costo tM(N) sia O ( TM(N) ) evidentemente, l’analisi asintotica non viene svolta determinando prima tM(N) e poi TM(N) piuttosto, l’analisi asintotica di complessità di un metodo viene svolta determinando direttamente la funzione di costo asintotico TM(N) La funzione di costo asintotico di un metodo può essere determinata utilizzando un modello di costo asintotico la differenza principale tra il modello di costo basato sul conteggio delle operazioni e il modello di costo asintotico è che, in quest’ultimo, il costo di una sequenza formata da un numero finito e costante di operazioni non è dato dalla somma dei costi delle operazioni, ma piuttosto dal solo costo dell’operazione di costo massimo 63 Complessità Fondamenti di informatica Un modello di costo asintotico Operazione s Costo asintotico Ts di s istruzione semplice (senza invocazioni di metodo) — 1 espressione, assegnazione, incremento, istruzione return e new blocco — { s1 s2 ... sK } istruzione condizionale — if (cond) s1 else s2 max ( Ts1, Ts2, ..., TsK ) max(Tcond, Ts1) – se cond è true max(Tcond, Ts2) – se cond è false istruzione ripetitiva — while (cond) s — Tcond(1) + Ts(1) + Tcond(2) + Ts(2) + nell’ipotesi che il corpo del while ... + Tcond(N) + Ts(N) + Tcond(N+1) venga eseguito N volte invocazione di metodo o costruttore r.M(p1, ..., pK) 64 max ( Tr, Tp1, ..., TpK, TM ) Complessità Fondamenti di informatica Esempio — costo asintotico del metodo somma Determinare la funzione di costo asintotico del metodo somma (rispetto alla lunghezza N di dati) /* Calcola la somma degli elementi di dati. */ public static int somma(int[] dati) { // pre: dati!=null int n; // lunghezza di dati int s; // somma degli elementi di dati int i; // indice per la scansione di dati /* calcola la somma degli elementi di dati */ n = dati.length; s = 0; for (i=0; i<n; i++) s += dati[i]; return s; } 65 Complessità Fondamenti di informatica Costo asintotico del metodo somma n = dati.length; s = 0; for (i=0; i<n; i++) s += dati[i]; return s; Il costo asintotico del metodo somma è dato dal costo della sequenza di istruzioni che ne forma il corpo il corpo di somma è formato da due assegnazioni, da una istruzione ripetitiva e da una istruzione return le due assegnazioni e l’istruzione return non contengono invocazioni di metodi, e quindi hanno costo unitario il costo dell’istruzione ripetitiva è sicuramente maggiore di (o uguale a) 1 l’istruzione ripetitiva ha sicuramente costo massimo tra le operazioni che formano il corpo del metodo somma, e quindi il costo asintotico del metodo somma è uguale al costo asintotico della sola istruzione ripetitiva 66 Complessità Fondamenti di informatica Costo asintotico del metodo somma for (i=0; i<n; i++) s += dati[i]; Il costo asintotico del metodo somma è dato dal costo asintotico della sola istruzione ripetitiva nell’esecuzione dell’istruzione ripetitiva (N è la lunghezza di dati) l’inizializzazione i=0 ha costo unitario e viene eseguita una sola volta la condizione i<n (che ha costo unitario) viene valutata N+1volte il blocco formato dal corpo dell’istruzione ripetitiva s+=dati[i] e dall’incremento i++ (che ha costo unitario) viene eseguito N volte ogni singola esecuzione del blocco formato dal corpo dell’istruzione ripetitiva s+=dati[i] (che ha costo unitario) e dall’incremento i++ (che ha costo unitario) ha complessivamente costo asintotico unitario 67 Complessità Fondamenti di informatica Costo asintotico del metodo somma for (i=0; i<n; i++) s += dati[i]; Il costo asintotico del metodo somma è dato dal costo asintotico della sola istruzione ripetitiva il costo asintotico dell’istruzione ripetitiva è il massimo tra il costo dell’inizializzazione e la somma del costo delle valutazioni della condizione e delle esecuzioni del corpo e dell’incremento il costo dell’inizializzazione è minore del costo delle valutazioni della condizione e delle esecuzioni del corpo e dell’incremento, e pertanto può essere trascurato le valutazioni della condizione hanno complessivamente costo N+1 le esecuzioni di corpo e incremento hanno costo asintotico complessivo N l’istruzione ripetitiva ha costo asintotico 2N+1 il metodo somma ha costo asintotico Tsomma(N) = 2N+1 = 68 Complessità Fondamenti di informatica O(N) Esercizio Determinare il costo asintotico per tutti i metodi che sono stati già analizzati rispetto al modello di costo basato sul conteggio delle operazioni 69 Complessità Fondamenti di informatica Operazione dominante Nella determinazione della funzione di costo asintotico basata sul modello di costo asintotico, solitamente si procede identificando i blocchi di operazioni (di lunghezza finita e costante) che vengono eseguiti in sequenza determinando, per ciascun blocco di operazioni, il costo asintotico della sola operazione più costosa infatti, il costo asintotico dell’intero blocco di operazioni è dato dal costo della sola operazione più costosa Questa idea viene formalizzata dalla nozione di operazione dominante di un metodo intuitivamente, una operazione dominante di un metodo è una operazione il cui costo di esecuzione è asintoticamente uguale al costo di esecuzione dell’intero metodo l’analisi del costo asintotico di un metodo può essere semplificata identificando una operazione dominante del metodo e determinando il costo della sola operazione dominante, trascurando il costo delle altre operazioni 70 Complessità Fondamenti di informatica Operazione dominante Sia M un metodo la cui funzione di costo è tM(N) una operazione o istruzione D di M è una operazione dominante se, per ogni N e nell’ipotesi di caso peggiore, indicando con tMD (N) il costo dell’esecuzione della sola operazione D nell’ambito dell’esecuzione di M, risulta tM (N) = O( tMD (N) ) ovvero, l’operazione D è dominante se il costo complessivo dell’esecuzione di D nell’ambito dell’intera esecuzione di M è asintoticamente non inferiore al costo complessivo di M Il costo asintotico dell’esecuzione di un metodo che ha una operazione dominante è pari al costo asintotico dell’esecuzione della sola operazione dominante se è possibile identificare una operazione dominante del metodo, allora l’analisi del costo asintotico del metodo può essere semplificata, riducendola alla determinazione del costo di esecuzione della sola operazione dominante un metodo può avere zero, una o più operazioni dominanti 71 Complessità Fondamenti di informatica Esempio — operazioni dominanti del metodo ricerca Si consideri nuovamente il seguente metodo (che ha costo tricerca(N) = 4N + 5) nella figura sono evidenziate le operazioni dominanti del metodo /* Verifica se dati contiene un elemento uguale a chiave. */ public static boolean ricerca(int[] dati, int chiave) { // pre: dati!=null int n; // lunghezza di dati int i; // indice per la scansione di dati boolean contiene; // dati contiene un elemento // uguale a chiave /* cerca un elemento uguale a chiave in dati */ n = dati.length; contiene = false; for (i=0; i<n; i++) if (dati[i]==chiave) contiene = true; return contiene; } 72 Complessità Fondamenti di informatica Operazioni dominanti del metodo ricerca Nel metodo ricerca sono operazioni dominanti tutte le operazioni il cui contributo al costo asintotico è O(N) la condizione dell’istruzione ripetitiva è dominante perché viene valutata N+1 volte e ciascuna valutazione ha costo unitario l’incremento dell’istruzione ripetitiva è dominante perché viene eseguito N volte e ciascuna esecuzione ha costo unitario il corpo dell’istruzione ripetitiva è dominante perché viene eseguito N volte e ciascuna esecuzione ha costo unitario la condizione dell’istruzione condizionale è dominante perché viene valutata N volte e ciascuna valutazione ha costo unitario la parte if dell’istruzione condizionale è dominante perché (nel caso peggiore) viene eseguita N volte e ciascuna esecuzione ha costo unitario 73 Complessità Fondamenti di informatica Esempio — operazioni dominanti di coppiaUguali Nella seguente figura sono evidenziate le operazioni dominanti per il metodo coppiaUguali (che ha costo asintotico quadratico) /** Verifica se a contiene una coppia di elementi uguali. */ public static boolean coppiaUguali(int[] a) { // pre: a!=null int n; // lunghezza di a boolean trovataCoppia; // a contiene una coppia // di elementi uguali int i, j; // indici per la scansione di a /* cerca una coppia di elementi uguali esaminando * comunque tutte le coppie di elementi distinti */ n = a.length; trovataCoppia = false; for (i=0; i<n; i++) for (j=i+1; j<n; j++) if (a[i]==a[j]) trovataCoppia = true; return trovataCoppia; } 74 Complessità Fondamenti di informatica Identificazione dell’operazione dominante Se un metodo è formato solo da operazioni semplici (ovvero, se non contiene nessuna invocazione di metodo o costruttore) allora ciascuna operazione del metodo ha costo unitario il costo complessivo di ciascuna operazione (semplice) è uguale al numero di volte che l’operazione viene eseguita l’operazione dominante del metodo è l’operazione che viene eseguita più spesso solitamente, in questo caso, le operazioni dominanti appartengono alle istruzioni ripetitive più annidate del metodo, come nel caso del metodo coppiaUguali Tuttavia, se un metodo non è formato solo da operazioni semplici (ovvero, se contiene invocazioni di metodi o costruttori) allora il costo complessivo di una operazione è solitamente dato dal prodotto del numero di volte che l’operazione viene eseguita per il costo di ciascuna esecuzione dell’operazione l’operazione dominante del metodo non è necessariamente l’operazione che viene eseguita più spesso 75 Complessità Fondamenti di informatica Esercizi Identificare le operazioni dominanti in tutti i metodi che sono stati analizzati Determinare il costo asintotico per tutti i metodi che sono stati analizzati, basandosi sull’identificazione di una operazione dominante 76 Complessità Fondamenti di informatica Esercizio /* Verifica se dati contiene un elemento uguale a chiave. */ public static boolean ricercaSeq(int[] dati, int chiave) { // pre: dati!=null int n; // lunghezza di dati int i; // indice per la scansione di dati boolean contiene; // dati contiene un elemento uguale a chiave /* cerca un elemento uguale a chiave in dati */ n = dati.length; contiene = false; i = 0; while (!contiene && i<n) { if (dati[i]==chiave) contiene = true; i++; } return contiene; } Discutere le seguenti affermazioni (che sono entrambe vere) l’operazione dati[i]==chiave è dominante l’operazione contiene=true non è dominante 77 Complessità Fondamenti di informatica Complessità degli algoritmi e dei problemi Le tecniche di analisi di complessità, che sono state introdotte per l’analisi dei metodi, possono essere usate anche per l’analisi di complessità degli algoritmi uno stesso algoritmo può essere implementato da metodi diversi la complessità di un algoritmo può essere definita come la complessità del metodo più efficiente che implementa l’algoritmo In pratica, per analizzare la complessità degli algoritmi viene considerato un linguaggio di programmazione astratto e un modello di costo generico per questo linguaggio non vengono presi in considerazione tutti i dettagli della programmazione in uno specifico linguaggio inoltre, nei casi più semplici (come quelli che considereremo), è possibile ragionare correttamente anche con algoritmi descritti in modo informale e incompleto 78 Complessità Fondamenti di informatica Esempio — complessità di un algoritmo Determinare la complessità del seguente algoritmo, che verifica se gli elementi di un array di interi sono tutti uguali viene ipotizzato che gli elementi dell’array sono tutti uguali viene confrontato ogni elemento dell’array con ogni altro elemento dell’array di indice maggiore se viene trovata almeno una coppia di elementi diversi, allora è possibile concludere che non è vero che gli elementi dell’array sono tutti uguali L’algoritmo ha complessità quadratica infatti l’operazione dominante è il confronto tra coppie di elementi (in cui il primo elemento ha indice minore del secondo) il numero di coppie in cui il primo elemento ha indice minore del secondo è quadratico nella lunghezza dell’array 79 Complessità Fondamenti di informatica Complessità di problemi Uno stesso problema può essere risolto da più algoritmi la complessità di un problema è la complessità dell’algoritmo più efficiente tra quelli che risolvono il problema L’analisi di complessità permette di scegliere il “migliore” algoritmo tra gli algoritmi noti che risolvono un problema si supponga ad esempio di dover risolvere un problema P e di conoscere gli algoritmi A e B che risolvono P 2 si supponga inoltre che l’algoritmo A sia Θ(N ) e che l’algoritmo B sia Θ(N log N) è possibile in questo caso concludere che B è asintoticamente migliore di A, e quindi decidere di risolvere il problema P implementando l’algoritmo B Va osservato che non sempre sono noti tutti gli algoritmi che risolvono un certo problema, e quindi bisogna accontentarsi di poter scegliere il migliore algoritmo tra quelli noti 80 Complessità Fondamenti di informatica Esempio — complessità degli algoritmi e dei problemi Si consideri il problema di verificare se gli elementi di un array di interi sono tutti uguali un primo algoritmo è quello considerato in precedenza un secondo algoritmo è il seguente viene ipotizzato che gli elementi dell’array sono tutti uguali viene confrontato ogni elemento dell’array con l’elemento che lo segue se viene trovata almeno una coppia di elementi consecutivi diversi, allora è possibile concludere che gli elementi dell’array non sono tutti uguali 81 Complessità Fondamenti di informatica Complessità degli algoritmi e dei problemi Il primo algoritmo ha complessità quadratica Il secondo algoritmo ha complessità lineare l’operazione dominante è il confronto tra coppie di elementi vengono confrontati solo coppie di elementi consecutivi la complessità è lineare In questo caso è possibile concludere che il secondo algoritmo è asintoticamente migliore del primo inoltre, è possibile osservare che tutti gli algoritmi che risolvono questo problema devono (nel caso peggiore) accedere a tutti gli elementi dell’array, e quindi hanno costo asintotico almeno lineare rispetto al numero di elementi dell’array perciò, non esistono algoritmi di costo sub-lineare per il problema in esame anche il problema ha complessità lineare 82 Complessità Fondamenti di informatica Complessità dei metodi delle API di Java Un problema che finora non è stato considerato è quello dell’analisi della complessità dei metodi delle API di Java o di altre operazioni di librerie di classi predefinite ad esempio, qual è la complessità dei metodi substring e indexOf della classe String? La documentazione delle API di Java riporta la complessità delle operazioni solo in alcuni casi (in particolare, nei casi in cui la complessità è particolarmente significativa) tuttavia, in molti casi la documentazione non riporta la complessità dei metodi conoscere l’algoritmo usato da un certo metodo (o quelli noti per risolvere un certo problema) può essere sufficiente per determinare la complessità asintotica del metodo non è necessario conoscere in dettaglio il codice del metodo è spesso sufficiente una conoscenza approssimativa dell’algoritmo implementato dal metodo 83 Complessità Fondamenti di informatica Giustificazione dell’analisi asintotica La rilevanza dell’analisi asintotica di complessità può essere mostrata mediante alcuni dati numerici la seguente tabella mostra il costo computazionale effettivo (basato sul conteggio delle operazioni) per cinque classi di metodi caratterizzate da complessità asintotica differente, per alcuni possibili valori della dimensione N dei dati di ingresso viene mostrato il tempo di esecuzione in secondi, ipotizzando che il tempo di esecuzione di ciascuna operazione sia 1µs (10–6 s) 84 t(N) \ N 10 100 1 000 10 000 40 N 0.000 4 0.004 0.04 0.4 20 N log N 0.000 6 0.013 0.2 2.65 2 N2 0.000 2 0.02 2 200 N4 0.01 100 2N 0.001 1018 anni Complessità 277 giorni 7 610 anni 10289 anni Fondamenti di informatica Giustificazione dell’analisi asintotica La seguente tabella propone dati analoghi a quelli della precedente tabella mostrando per le cinque classi di metodi la dimensione massima dell’insieme di dati di ingresso che può essere elaborato in un secondo, un minuto e un’ora ipotizzando che il tempo di esecuzione di ciascuna operazione sia 1µs 85 t(N) 1 secondo 1 minuto 1 ora 40 N 25 000 1 500 000 90 000 000 20 N log N 4 096 166 666 7 826 087 2 N2 707 5 477 42 426 N4 31 88 244 2N 19 25 31 Complessità Fondamenti di informatica Giustificazione dell’analisi asintotica La seguente tabella mostra il miglioramento che può essere ottenuto in termini di incremento della dimensione massima M dell’insieme di dati di ingresso che può essere elaborato in una stessa quantità di tempo usando un calcolatore 256 volte più veloce oppure usando lo stesso calcolatore e una quantità di tempo 256 volte maggiore 86 t(N) nuova dimensione massima 40 N 256 M 20 N log N 256 M ((log M) / (7 + log M)) 2 N2 16 M N4 4M 2N M+8 Complessità Fondamenti di informatica Discussione sull’analisi asintotica Le precedenti tabelle mostrano che le funzioni usate nell’analisi asintotica si comportano in modo simile per N piccolo, ma in modo molto diverso per N grande le funzioni “semplici” usate nell’analisi asintotica sono sostanzialmente diverse, e caratterizzano delle classi di complessità significative i problemi trattabili (ovvero, per cui è ragionevole realizzare e utilizzare una soluzione automatica) sono quelli caratterizzati da una funzione di costo asintotico al più polinomiale i problemi intrattabili (ovvero, per cui non è ragionevole realizzare e utilizzare una soluzione automatica, quantomeno per N grande) sono quelli caratterizzati da una funzione di costo asintotico esponenziale (o maggiore) problemi con costo esponenziale sono ad esempio quelli che richiedono di determinare una permutazione degli elementi dell’insieme di dati di ingresso che sia ottimale rispetto a un criterio non banale 87 Complessità Fondamenti di informatica Discussione Vengono ora discussi brevemente alcuni aspetti legati all’analisi di complessità e all’implementazione di algoritmi efficienti l’efficienza di un metodo e la semplicità di realizzazione del metodo sono aspetti contrastanti ovvero, la realizzazione di un metodo che risolve un problema in modo efficiente può richiedere più tempo che non la realizzazione di un metodo che risolve lo stesso problema in modo poco efficiente se bisogna realizzare un metodo che risolve un problema se il metodo deve essere eseguito “poche volte” oppure solo su insiemi di dati di ingresso “piccoli” allora può essere opportuno implementare un algoritmo semplice anche se poco efficiente se il metodo deve essere eseguito “spesso” (ad esempio, perché inserito in una libreria di metodi) oppure su insiemi di dati di ingresso “grandi” allora è solitamente opportuno implementare l’algoritmo più efficiente tra quelli noti 88 Complessità Fondamenti di informatica Discussione 89 l’analisi asintotica trascura i fattori moltiplicativi costanti e i termini di ordine inferiore se bisogna identificare un algoritmo per risolvere un problema in modo molto efficiente, allora è opportuno considerare anche i fattori moltiplicativi costanti, eseguendo l’analisi di complessità basata sul conteggio delle operazioni (o su un modello di costo ancora più sofisticato) e non l’analisi asintotica l’analisi di caso peggiore è in alcuni casi poco realistica in alcuni casi è opportuno eseguire delle analisi di caso medio (in cui il costo computazionale viene calcolato in modo medio rispetto a tutti i possibili insiemi di dati di ingresso) oppure altre forme di analisi in alcuni casi è utile (o necessario) considerare la complessità computazionale anche rispetto ad altre risorse di calcolo come ad esempio la complessità spaziale (che stima l’occupazione di memoria per l’esecuzione di un programma o metodo) Complessità Fondamenti di informatica Discussione 90 i modelli di costo proposti trascurano il costo delle attivazioni dei metodi in realtà, il costo dell’attivazione di un metodo può essere non trascurabile se bisogna implementare un algoritmo in modo molto efficiente, può essere opportuno limitare il numero di attivazioni di metodi (ad esempio, implementando algoritmi ricorsivi mediante metodi iterativi) Complessità Fondamenti di informatica