890 likes | 1.34k Views
Algoritmi e complessità. La complessità Complessità in tempo e spazio Complessità asintotica Algoritmi e complessità Ricerca e ordinamento La macchina di Turing e le classi di complessità. La complessità. La complessità.
E N D
Algoritmi e complessità • La complessità • Complessità in tempo e spazio • Complessità asintotica • Algoritmi e complessità • Ricerca e ordinamento • La macchina di Turing e le classi di complessità
La complessità • L’analisi di complessità definisce le risorse teoricamente consumate da un algoritmo • complessità temporale: tempo necessario all’esecuzione dell’algoritmo • complessità spaziale: memoria necessaria all’esecuzione dell’algoritmo • Poiché ad ogni algoritmo corrispondono più implementazioni (più programmi), lo studio della complessità non definisce esattamente il tempo e la memoria usata: si concentra sulle proprietà che sono indipendenti dell’implementazione fornendo un’idea di quanto sia efficiente un algoritmo
Che cosa si misura? 1 • Complessità temporale • Si contano le istruzioni eseguite dall’algoritmo • Poiché le istruzioni potrebbero essere di natura diversa, si individuano quelle che incidono principalmente sul tempo di esecuzione • Le operazioni in virgola mobile: le più lente da eseguire per una CPU; sono predominanti se il loro numero è paragonabile al numero delle altre istruzioni • Le istruzioni di controllo(gli if) e le istruzioni più frequenti (sono predominanti se sono in numero molto superiore alle altre istruzioni) • Le istruzioni di accesso alla memoria secondaria e alle periferiche: sono decine di migliaia di volte più lente delle istruzioni svolte nella memoria principale; se un’applicazione ne richiede molte, queste potrebbero essere predominanti (ad es., nei database, l’analisi di complessità è concentrata sugli accessi al disco)
Che cosa si misura? 2 • Complessità spaziale • Si misurano le “posizioni” di memoria occupate dai dati necessari allo svolgimento dell’algoritmo • La complessità spaziale sarà misurata relativamente alla memoria principale se i dati dell’algoritmo possono essere allocati in memoria principale, in base all’occupazione di memoria secondaria quando le strutture dati dell’algoritmo sono troppo grandi per poter risiedere nella memoria centrale
Complessità asintotica • Lo studio della complessità si concentra su i casi in cui il problema è grande: • non importa se un programma di contabilità impiega 1 o 100 millisecondi a calcolare il bilancio • cambia molto se il programma della segreteria impiega 1 o 10 secondi a trovare i dati di uno studente nell’archivio • Complessità asintotica • Definisce le risorse usate da un algoritmo al crescere della dimensione del problema affrontato • Ad esempio: come cambia il tempo di accesso ai dati quando cresce il numero degli studenti nell’archivio della segreteria
Complessità temporale asintotica 1 • Formalmente, si usa il concetto matematico di ordine di grandezza • sia n la dimensione del problema, cioè la dimensione dell’input dell’algoritmo • siaT(n) il tempo impiegato per l’esecuzione dell’algoritmo quando l’ingresso ha dimensione n • sia f(n) una qualsiasi funzione di n, ad esempio 3, n, n2, n5, 2n • Si dice che la complessità asintotica dell’algoritmo è dell’ordine di f(n) e si scrive O(f(n)) se esiste una costante tale che T(n)f(n) • Osservazione importante: in base alla definizione data, algoritmi che differiscono solo per una costante moltiplicativa hanno lo stesso ordine di complessità • Esempio: due algoritmi che richiedono 4n e 7n operazioni sono entrambi O (n)
Complessità temporale asintotica 2 • Informalmente… • L’ordine O(f(n)) fornisce una misura della complessità temporale di ogni programma che implementa l’algoritmo • Esempio: Calcolare la somma degli elementi di un array • n: numero di elementi dell’array • complessità: O (n)
Complessità media e relativa al caso peggiore • Un algoritmo può richiedere un numero di operazioni diverse per ingressi di dimensione uguale: • complessità media: complessità valutata su tutti i possibili ingressi • complessità nel caso peggiore: complessità dell’algoritmo per l’ingresso che richiede più operazioni • Di solito, quando si parla di complessità, ci si riferisce alla complessità nel caso peggiore
Complessità asintotica: array e liste • Array • n: numero degli elementi dell’array • Ricerca/inserimento/cancellazione di un elemento • Complessità O (n) • Liste semplici • n:numero delle posizioni nella lista • Ricerca/cancellazione di un elemento • Complessità O (n) • Inserimento di un elemento all’inizio della lista (in testa) • Complessità O (1)
6 3 8 1 4 7 9 Complessità asintotica: alberi binari • Alberi binari di ricerca • n: numero dei nodi dell’albero • Inserire, eliminare o ricercare un elemento in un albero binario bilanciato • Complessità: O (log2n)
Complessità asintotica: tabelle hash • Problema Memorizzare in maniera opportuna un insieme di dati tipicamente sotto forma di record in modo da poter reperire un qualsiasi elemento dell’insieme con un numero “piccolo” di tentativi • Cosa significa “piccolo” ? • Indipendente (o quasi) dalla dimensione della tabella su cui si effettua la ricerca, quindi con una complessità in tempo pari ad O (1)
Funzioni hash 1 • h: K → {0, 1, 2, …, m–1} • K: insieme dei valori distinti che possono essere assunti dalle chiavi dei record • m: dimensione del vettore in cui si intende memorizzare la tabella • Ipotesi:K sottoinsieme dei numeri naturali • Possibile funzione di accesso: h(k) k MOD m, kK • Valore della funzione sempre compreso fra 0 em–1
Funzioni hash 2 • Se K non è un sottoinsieme dei numeri naturali • Esempio: insieme di stringhe alfanumeriche • La funzione hash si applica a numeri • Per utilizzarla in corrispondenza di una chiave non numerica occorre associare alla chiave un valore numerico • Necessità di definire funzioni hash generali • Associazione di un valore numerico ad una chiave di qualunque tipo • Applicazione della funzione hash a tale valore • Esempio: si utilizza la somma dei codici ASCII dei caratteri che costituiscono la stringa
Collisioni 1 • Associazione, da parte di una trasformazione, della stessa posizione a chiavi distinte • Sinonimi • Esempio:[10,12,20,23,27,30,31,39,42,44,45,49,53,57,60] • h(chiave) (chiave MOD 15) • Posizione 0 ← 30, 45, 60 • Posizione 8 ← 23, 53 • Posizione 12←12, 27, 42, 57 • Ciascuna posizione dell’array può contenere al più un elemento; occorre… • Ridurre al massimo le collisioni • Gestirle quando si verificano
Collisioni 2 • Funzioni di hashing perfetto(che evitano i duplicati) sono rare, anche per tabelle grandi • Esempio: paradosso del compleanno Dato un gruppo di 23 persone, ci sono più del 50% di probabilità che due di esse siano nate nello stesso giorno dell’anno • In altre parole, se scegliamo una funzione aleatoria (a valori casuali) che trasforma 23 chiavi in un indirizzo di una tabella di 365 elementi, la probabilità che due chiavi NON collidano è solo 0.4927 (meno della metà) • Individuare una funzione di accesso che porti ad un numero ridotto di collisioni è un problema complesso
h(chiave) (chiave MOD 19) Collisioni 3 • Tuttavia… numero di collisioni ridotto drasticamente se accettiamo uno spreco del 25% di memoria extra • Esempio: array di 19 elementi (indicizzati da 0 a 18) • Posizione 0 ← 57 • Posizione 8 ← 27 • Posizione 1 ← 20, 39 • Posizione 10 ←10 • Posizione 3 ← 60 • Posizione 11 ← 30, 49 • Posizione 4 ← 23, 42 • Posizione 12 ←12, 31 • Posizione 6 ← 44 • Posizione 15 ← 53 • Posizione 7 ← 45 • Collisioni non eliminate del tutto
Gestione delle collisioni 1 • Uso di liste concatenate destinate alla memorizzazione degli elementi che, in inserimento, hanno portato ad una collisione • Ricerca di un elemento di chiavek • Si calcolah(k) • Se si verifica una collisione allora si accede alla lista associata alla posizioneh(k) e la si scandisce
Gestione delle collisioni 2 • Il costo dell’operazione di ricerca realizzata in modo lineare relativamente alle liste di elementi in collisione si mantiene pressoché indipendente da n(numero degli elementi contenuti nella tabella) • Inserimento/cancellazione costano O (1) • Metodo non adatto a reperire sottoinsiemi di dati con chiave che soddisfi una data relazione
Complessità asintotica fattoriale • Sia dato un programma che prende in ingresso i partecipanti ad una competizione e genera (ad esempio per stamparle) tutte le possibili classifiche finali • n: numero di partecipanti • Complessità: O (n!) • Si osservi che n! è un numero molto grande anche per n relativamente piccoli 20! 2.432.902.008.176.640.0002.41018
Complessità asintotica polinomiale • Sia dato un programma che ha come ingresso due array a, b e cerca tutte le coppie (i,j) tali che a[i]b[j] • n: dimensione di a • m: dimensione di b • Complessità: O (nm) void search(int a[], int b[], int alength, int blength) { … for(i0;ialength;i){ for(j0;jblength;j){ if(a[i]b[j]) printf(“Trovata corrispondenza: a[%d]b[%d]%d”, i, j, a[i]); } } }
Algoritmi facili e difficili • In base alla loro complessità temporale asintotica, gli algoritmi sono tipicamente divisi in classi • Algoritmi a complessità costanteO (1) o lineareO (n) • Molto veloci, “scalabili” • Algoritmi a complessità polinomiale O(na) per un qualche valore a • Usabili se l’esponente a è piccolo • Algoritmi a complessità esponenziale O(an) per un qualche valore a (1) • Usabili solo per n molto piccoli
Algoritmi a complessità esponenziale • Fondamentale: perché gli algoritmi a complessità esponenziale sono considerati quasi inusabili? • Perché richiedono talmente tante operazioni che probabilmente anche i calcolatori futuri non saranno in grado di eseguire in tempi ragionevoli le loro possibili implementazioni (programmi) per dati in ingresso ad alta dimensionalità
Esempio • Si consideri il programma che genera tutte le classifiche finali di una competizione con n partecipanti, complessità O (n!) (è esponenziale, perché ) • Con 20 concorrenti le classifiche sono 20! 2.41018 • Un computer che generi 1 miliardo di classifiche al secondo, circa 31016 l’anno, impiegherebbe circa 79 anni per generare tutte le classifiche richieste • Tendenzialmente, i computer diverranno sempre più veloci e fra dieci anni forse saranno abbastanza veloci da realizzare in un mese quello per cui adesso occorrono 79 anni ma… • …comunque, fra dieci anni per risolvere il problema con 21 partecipanti occorreranno ancora 21 mesi e per 25 partecipanti circa 531300 anni!!
La ricerca dicotomica 1 • Per “cercare” un elemento in un vettore ordinato esiste un metodo detto ricerca binaria o dicotomica • Si confronta il valore val da ricercare con l’elemento centrale del vettore A[length/2] • Se val è minore dell’elemento mediano, si ripete la ricerca sulla metà sinistra del vettore, altrimenti si ricerca nella metà destra
0 2 4 5 8 9 13 16 20 23 27 30 34 35 0 2 4 5 8 9 13 16 20 23 27 30 34 35 0 2 4 5 8 9 13 16 20 23 27 30 34 35 0 2 4 5 8 9 13 16 20 23 27 30 34 35 La ricerca dicotomica 2 • Esempio: ricerca del numero 23 Si confronta 23 con 13 Ci si concentra sulla metà destra (da ind. 8 a ind. 14): si confronta 23 con 27 Ci si concentra sulla metà sinistra (da ind. 8 a ind. 10): si confronta 23 con 20 Ci si concentra sulla metà destra (da ind. 9 a ind. 9): trovato!!
Implementazione della ricerca dicotomica int search(int val, int A[], int from,int to) { int center(fromto)/2; if (from to) return 1; if (fromto) { if(A[from]val) {return from;} return 1;} // si esegue solo se A[from]!val //si esegue solo se (fromto) if (valA[center]){ return search(val,A,from,center1);} if (valA[center]){ return search(val,A,center1,to);} return center; }
Complessità della ricerca dicotomica • La ricerca dicotomica divide il vettore in due ad ogni passo: • dopo p passi la dimensione del vettore è • nel caso peggiore, la ricerca si ferma quando è 1, cioè quando plog2n • Quindi la ricerca dicotomica è O (log2n)
Mergesort 1 • Il Mergesort è un algoritmo basato sul paradigma del divide et impera • Una strategia divide et impera consiste nel suddividere un problema in sottoproblemi, nel risolvere i sottoproblemi, e nel ricomporli per ottenere la soluzione del problema originale • Il Mergesort è composto da due fasi: • una fase di divisione del vettore da ordinare in sottovettori • una fase di ricomposizione dei risultati (merge)
6 1 2 8 3 4 7 5 Divisione 3 4 7 5 6 1 2 8 3 4 5 7 1 2 6 8 1 2 3 4 5 6 7 8 Mergesort 2 • Idea • Dato un vettore da ordinare, lo si divide in due sottovettori di ugual dimensione, si ordinano i sottovettori e poi si “fondono” insieme Ordinamento Ordinamento Fusione
6 1 2 8 3 4 7 5 3 4 7 5 6 1 2 8 6 1 2 8 3 4 7 5 6 3 1 4 2 7 8 5 Mergesort: la divisione ricorsiva • Come si ordinano i due sottovettori ? • Applicando ricorsivamente la divisione fino a quando il vettore contiene un solo elemento: in tal caso l’ordinamento è banale Divisione
1 2 3 4 5 6 7 8 3 4 5 7 1 2 6 8 1 6 2 8 5 7 3 4 6 3 1 4 2 7 8 5 Mergesort: la fusione ricorsiva 1 • I sottovettori ordinati verranno poi ricorsivamente fusi Fusione
i j k Mergesort: la fusione ricorsiva 2 • La fusione viene realizzata utilizzando due indici che scorrono i due sottovettori da fondere: • Ad ogni passo si confrontano i due elementi indicati dagli indici i e j, A[i], A[j] • Si copia l’elemento minore in un vettore d’appoggio e si incrementa l’indice corrispondente • Si torna al passo 1. fino a quando i due vettori non sono stati completamente visitati 1 2 3 4 5 6 7 8
Complessità del Mergesort • Il Mergesort ha complessità O(nlog2n) sia nel caso medio che nel caso pessimo • Mergesort è un algoritmoottimo! • La sua complessità asintotica è la migliore possibile • Comunque… • …esistono algoritmi che per alcuni ingressi fanno meglio di nlog2n(ad es., Bubblesort su vettori ordinati) • …esistono altri algoritmi con complessità nlog2n nel caso pessimo Heapsort
Quicksort 1 • Quicksort, come Mergesort, è un algoritmo divide et impera • Idea • Si divide il vettore A in due sottovettori, che contengono rispettivamente tutti gli elementi maggiori e minori di (per esempio) A[0], cioè il primo elemento del vettore detto perno • Si ripete ricorsivamente la divisione…
4 2 1 6 3 8 7 5 3 2 1 4 6 8 7 5 3 2 1 6 8 7 5 1 2 3 5 6 7 8 1 2 5 7 8 Quicksort 2 Si ripartisce il vettore rispetto ad A[1] 4 Si divide rispetto a 3 Si divide rispetto a 6
Si scorrono i, j confrontando con 4 4 2 8 6 3 1 7 5 j i Si scambiano gli elementi 4 2 8 6 3 1 7 5 i j Si scorrono i, j confrontando con 4 4 2 1 6 3 8 7 5 j i Quicksort: l’operazione perno 1 • Come si divide il vettore? • Si usano due indici i, j che scorrono il vettore da sinistra e da destra, rispettivamente • L’indice i scorre fino a quando A[i]A[1] • L’indice j scorre fino a quando A[j]A[1] • Si effettua lo scambio fra A[i] e A[j] e quindi si procede come sopra
4 2 1 6 3 8 7 5 4 2 1 3 6 8 7 5 3 2 1 4 6 8 7 5 Quicksort: l’operazione perno 2 • Alla fine si scambia il perno con l’elemento in posizione j Si scambiano gli elementi i j Si scambia A[j] con il perno j
Implementazione void perno(int A[], int from, int to) { int ifrom1, jto; while(ij){ while(A[i]A[from]) i; while(A[j]A[from]) j; if(ij) scambia(A,i,j); } scambia(A,from,j);}
Complessità del Quicksort • Il Quicksort ha complessità media O (nlogn) • Il caso pessimo si verifica quando il perno finisce in fondo o in testa al vettore • In tal caso, Quicksort ha complessità pari ad O (n2)
Mergesort vs Quicksort • Mergesort ha il vantaggio di avere complessità sempre O (nlog n) • Quicksort ha il vantaggio di non richiedere un vettore di appoggio: ordina il vettore “in loco” (minore complessità spaziale) • In media, Quicksort si comporta “bene” e, per questo motivo, in pratica spesso è preferito a Mergesort
Heap 1 • Ospita gli elementi dell’insieme A di cardinalità n, su cui è definita una relazione d’ordine totale “” • Lo heap (mucchio) è un albero binario • Proprietà 1 L’albero è quasi perfettamente bilanciato • È completo fino al livellok1, cioè contiene il numero massimo di nodi, 2k1, mentre al livello k contiene un numero di nodi (foglie) compreso tra 1 e 2k I nodi a livello massimo sono tutti addossati a sinistra • Proprietà 2 Ogni nodo contiene un elemento dell’elemento contenuto nel padre
23 38 18 22 17 5 10 28 12 63 Heap 2 • Noto il valore di n, la forma dell’albero è fissata dalla Proprietà 1 • L’allocazione degli elementi nei nodi può variare, nel rispetto della Proprietà 2 • L’elemento massimo dell’insieme è allocato nella radice • Nello heap, i sottoalberi di ciascun nodo sono ancora heap • Lo heap può essere allocato in un array
Heap 3 • Con l’allocazione lineare… • A[1] è l’elemento contenuto nella radice dello heap • Per ogni A[i], gli elementi corrispondenti ai figli sinistro e destro, se esistono, sono memorizzati in A[2i] e A[2i1] • Se 2in e/o 2i1n il figlio sinistro e/o destro di A[i] non esiste nell’albero • A[2i]A[i] e A[2i1]A[i], quando tali elementi sono definiti
Heapsort 1 • Lo heap trova la sua applicazione più elegante nel metodo di ordinamento noto come Heapsort • Si estrae l’elemento massimo dallo heap (quello nella radice, o in prima posizione nella rappresentazione lineare) • Si ricostruisce lo heap • …fino a quando non ci sono più elementi nello heap (ovvero gli elementi del vettore sono ordinati)
12 28 10 12 23 38 18 22 5 17 10 18 63 23 38 22 17 28 5 Heapsort 2 • Come si ricostruisce lo heap, dopo l’estrazione della radice? Continua…
18 12 10 38 23 38 22 5 17 28 28 23 18 22 17 10 12 Si scambia A[1] con A[2] 5 Heapsort 3 • Si considera il massimo fra i due figli della radice e, se max{A[2],A[3]}A[1], si effettua lo scambio Continua…
38 12 10 38 23 18 22 5 17 28 18 23 28 22 17 10 12 Si scambia A[2] con A[5] 5 Heapsort 4 • Si considera il massimo fra i due figli di A[2] e, se max{A[4],A[5]}A[2], si effettua lo scambio Continua…
12 18 23 28 22 17 5 5 10 38 23 28 22 17 18 12 10 Heapsort 5 • Si estrae A[1] che è l’elemento più grande… e si ricomincia il procedimento di ricostruzione