Scaricare la presentazione
La presentazione è in caricamento. Aspetta per favore
PubblicatoMelchiorre Fadda Modificato 11 anni fa
1
Lez. 51 Universita' di Ferrara Facolta' di Scienze Matematiche, Fisiche e Naturali Laurea Specialistica in Informatica Algoritmi Avanzati Complessita' degli algoritmi e dei problemi: sorting e searching straight insertion ricerca binaria mergesort, heapsort radix sort (detto anche distribution sort) Copyright © 2006-2009 by Claudio Salati.
2
2 COMPLESSITA' DEGLI ALGORITMI Perche' perdere tempo ad analizzare la complessita' di un algoritmo? Puo' essere divertente. Per mettere alla prova la propria capacita' di preveggenza. Perche' si e' studiosi di efficienza. Per scrivere algoritmi piu' efficienti che ci permettano di risparmiare tempo e spazio. (perche' volere risparmiare tempo e spazio? Vedi ad es. S. Penge, Zen e arte della programmazione)
3
3 COMPLESSITA' SPAZIALE E TEMPORALE Si puo' distinguere tra complessita' temporale: occupazione di tempo di CPU per l'esecuzione di un programma complessita' spaziale: occupazione di spazio di memoria per ospitare le strutture dati utilizzate dal programma. Noi ci occuperemo principalmente di complessita' temporale La complessita' viene comunque misurata in funzione delle dimensioni del problema Teorema: se la complessita' spaziale e' O(g(n)) allora la complessita' temporale e' almeno O(g(n)). (non vale il viceversa) Dimostrazione: se ho creato delle strutture dati per risolvere il problema, devo averle scritte e/o lette almeno 1 volta: se le dimensioni delle strutture dati sono O(g(n)), per fare questo non posso avere impiegato meno di O(g(n)) tempo.
4
4 CALCOLO DELLA COMPLESSITA' (temporale) Ipotesi di lavoro: si esegue una istruzione alla volta il costo di un algoritmo dipende dal numero di istruzioni che richiede di eseguire (non dalla lunghezza del suo testo) ogni accesso in memoria ha lo stesso costo il metodo piu' accurato per misurare le prestazioni di un algoritmo e' contare le azioni che sono eseguite durante la sua esecuzione per le operazioni elementari si puo' individuare (si assume) un costo (tempo) fisso/costante le operazioni complesse possono essere scomposte in operazioni elementari una analisi a priori del tempo di calcolo ignora tutti i fattori dipendenti dalla macchina o dal linguaggio di programmazione, e si concentra sulla determinazione dell'ordine di grandezza della complessita' dell'algoritmo
5
5 Complessita' e suo UPPER BOUND Definizione: per complessita' di un algoritmo intendiamo la somma delle frequenze di esecuzione di tutti i suoi statement (in funzione della dimensione del problema che si sta affrontando) Definizione: f(n) = O(g(n)) (si legge: f di n uguale O grande di g di n) se esistono due costanti positive c ed n 0 tali che | f(n) | c * | g(n) | per tutti gli n n 0 una analisi a priori del tempo di calcolo f(n) di un algoritmo puo' essere usata per determinare una funzione g(n) tale che f(n) = O(g(n)) Con l'algoritmo ha un tempo di calcolo O(g(n)) intendiamo che se l'algoritmo e' eseguito su un qualche computer, sullo stesso tipo di dati, per valori crescenti della dimensione n dei dati di ingresso, i tempi risultanti sono sempre minori di c * | g(n) |, con c costante positiva.
6
6 UPPER BOUND Teorema: se A(n) = a m * n m + … + a 1 * n + a 0 e' un polinomio di grado m allora A(n) = O(n m ) Dimostrazione: | A(n) | | a m | * n m + … + | a 1 | * n + | a 0 | = = ( | a m | + | a m-1 | / n + … + | a 0 | / n m ) * n m (per n 1) ( | a m | + | a m-1 | + … + | a 0 | ) * n m scegliendo n 0 = 1 e c = ( | a m | + | a m | + … + | a 0 | ) si ha | A(n) | c * n m, cioe' A(n) = O(n m )QED Conclusioni: se descriviamo la frequenza di esecuzione di uno statement con un polinomio di grado m come A(n) allora il tempo di calcolo dello statement e' O(n m ) tramite la notazione O() esprimiamo una funzione che e' un upper bound della complessita' dell'algoritmo
7
7 COMPLESSITA' DEGLI ALGORITMI Quale e' l'importanza della costante associata all'ordine di complessita'? Consideriamo due algoritmi che risolvono lo stesso problema, il primo di complessita' O(n) ed il secondo di complessita' O(n 2 ). Quale e' il migliore? Se le complessita' effettive sono 10*n e 1*n 2 rispettivamente, il primo algoritmo e' migliore del secondo per tutti gli n>10 Se le complessita' effettive sono 10 4 *n e 1*n 2 rispettivamente, il primo algoritmo e' migliore del secondo solo per tutti gli n>10 4 La costante associata all'ordine di grandezza mi dice fino a quali dimensioni un problema puo' essere considerato piccolo e risolubile (magari convenientemente) con algoritmi banali.
8
8 COMPLESSITA' ASINTOTICA Perche' e' importante la complessita' asintotica (cioe' l'ordine di grandezza della complessita')? Perche' e' lei che determina la dimensione massima dei problemi trattabili con un algoritmo Se le dimensioni dei problemi sono piccole, tutti gli algoritmi richiedono comunque "poco" tempo e sono "equivalenti"! Consideriamo il valore di alcune funzioni al crescere di n: f(n) = n :12345678910 f(n) = n 2 :149162536496481100 f(n) = n 3 :182764125…1000 f(n) = 2 n :2481632641282565121024 Consideriamo in particolare, al crescere di n, il valore della funzione f(n) = n*log(n): f(2)=2f(3)=4.8f(4)=8f(5)=11.6f(6)=15.5f(7)=19.7 f(8)=24f(9)=28.5f(10)=33.2f(100)=664.4
9
9 COMPLESSITA' ASINTOTICA Dati due algoritmi di complessita' asintotica O(n*log(n)) e O(n x )con x>1, esiste un valore n 0 tale che per tutti i valori n>n 0 (problemi di grandi dimensioni) e' migliore il primo, mentre per valori bassi di n diventano significativi, al fine del confronto, il valore di x, il coefficiente del termine di grado massimo ed i termini di grado minore per cui puo' risultare piu' conveniente il secondo algoritmo Un algoritmo di complessita' asintotica O(n*log(n)) e' "quasi lineare" Quindi e' molto buono, perche' e' difficile essere meglio che lineare (se leggo tutti i dati in ingresso almeno una volta ho gia' una complessita' almeno lineare!)
10
10 COMPLESSITA' DEGLI ALGORITMI La dimensione del problema per la quale un algoritmo richiede meno operazioni di un altro dipende dal coefficiente del termine di grado massimo dai termini di ordine minore e dai loro coefficienti Questi elementi dipendono: dall'algoritmo e dalla sua codifica (e.g. iterazione vs. ricorsione) dal calcolatore che si usa e dal suo set di istruzioni dal linguaggio e dal sistema di programmazione utilizzati In una analisi a priori del tempo di calcolo di un algoritmo non vengono considerati fattori dipendenti dalla codifica, dalla macchina e dal linguaggio utilizzati; Si determinano l'ordine di grandezza (l'upper bound), ed eventualmente quei termini che influenzano significativamente il confronto, basandosi sulla sola struttura dell'algoritmo
11
11 LOWER BOUND Definizione: f(n) = (g(n)) (si legge: f di n uguale omega di g di n) se esistono due costanti positive c ed n 0 tali che | f(n) | c * | g(n) | per tutti gli n n 0 Con l'algoritmo ha un tempo di calcolo (g(n)) intendiamo che se l'algoritmo e' eseguito su un qualche computer, sullo stesso tipo di dati, per valori crescenti di dimensione n dei dati di ingresso, i tempi risultanti sono sempre superiori a c * | g(n) |, con c costante positiva. Tramite la notazione esprimiamo un lower bound della complessita' dell'algoritmo La nozione di lower bound si puo' pero' applicare non solo alla complessita' di un algoritmo, ma anche a quella di un problema
12
12 Complessita' dei Problemi e degli Algoritmi Bubblesort e' sia O(n 2 ) che (n 2 ), ma puo' essere (n) se lo si scrive nella forma: while (nella iterazione precedente ci sono stati scambi) {... } Straight selection e' sia O(n 2 ) che (n 2 ) Mergesort e' sia O(n*log(n)) che (n*log(n)) A noi in genere interessano i lower bound dei problemi, non degli algoritmi, infatti il lower bound di un problema rappresenta l'upper bound minimo di tutti gli algoritmi (noti e non noti) che lo risolvono Se il lower bound di un problema e' (f(n)) cio' vuol dire che non esiste (non puo' esistere) nessun algoritmo con upper bound minore di O(f(n))
13
13 Analisi del caso peggiore e Upper bound Il calcolo della complessita' dell'algoritmo viene effettuato contando il numero delle istruzioni eseguite, ponendosi in una situazione che 1.Maggiormente penalizza il tempo di calcolo analisi del caso peggiore 2.Rende medio il tempo di calcolo analisi del caso medio Considerare una condizione media comporta in generale complesse valutazioni probabilistiche, per cui di solito ci si limita ad analizzare il caso peggiore: si cerca cioe' l'upper bound della complessita' di un algoritmo
14
14 Complessita' dell'algoritmo di sort per straight selection Il numero di confronti (all'interno della funzione findMinEl() ) e' indipendente dalla situazione iniziale (dall'ordine iniziale degli elementi nel vettore) Ad ogni iterazione del ciclo esterno della funzione straightSelection() devo effettuare 3 assegnamenti Il caso peggiore si ha quando ad ogni iterazione del ciclo della funzione findMinEl() devo effettuare 1 assegnamento (cio' avviene quando il vettore e' inizialmente ordinato in modo non crescente) il ciclo esterno e' ripetuto n-1 volte nel caso peggiore l'i-esima iterazione prevede (trascurando le operazioni di controllo del ciclo) 3 + (n-i-1) assegnamenti (n-i-1) confronti per un totale di operazioni di … (lasciato per esercizio) e quindi una complessita' O(n 2 )
15
15 Sort di un vettore: Straight insertion Problema: Dato un vettore di N elementi interi, N 1, ordinarlo in modo non decrescente Fino ad ora abbiamo visto 2 algoritmi, basati su due diverse strategie, ma in ogni caso di complessita' quadratica: straight selection straight exchange o bubblesort Vediamo una terza strategia: Straight insertion ALLi-ESIMA ITERAZIONE IL SOTTOVETTORE v[0..i-1] E GIA ORDINATO (ma solo al proprio interno, non rispetto al resto di v) SI PRENDE LELEMENTO DI INDICE i E LO SI INSERISCE NELLA POSIZIONE APPROPRIATA ALLINTERNO DEL SOTTOVETTORE GIA ORDINATO, TRASLANDO IN AVANTI DI UN POSTO NEL VETTORE GLI ELEMENTI CHE SONO MAGGIORI DI LUI C'E' SPAZIO PER FARLO PERCHE' DAL VETTORE E' STATO ESTRATTO L'ELEMENTO DI INDICE i
16
16 Situazione all'i-esima iterazione del ciclo: v[0]..v[i-1]tratto del vettore gia' internamente ordinato in modo non decrescente. Questo sottovettore deriva dal processamento sequenziale degli i precedenti elementi del vettore originale. Non c'e' relazione tra gli elementi del sottovettore v[0]..v[i-1] e quelli del sottovettore v[i]..v[N-1] iindice dell'elemento del vettore che si vuole inserire al posto giusto nel sottovettore ordinato v[0]..v[i-1] Straight Insertion: la strategia v[0].. v[i-1] ordinatov[i+1]v[N-1]... vettore v i v[i]
17
17 Straight Insertion: l'algoritmo void straightInsertion(int n, int v[]) { 1 int i = 1; 2 // { P; i n; ( j: 0<j i-1) v[j-1] v[j] } while (i <= n-1) { 3 int toInsert = 4 whereToInsert(0, i-1, v, v[i]); // { i n-1; ( j: 0 j toInsert-1) v[j] v[i]; // ( j: toInsert j i-1) v[j]>v[i]; } int temp = v[i]; 5 shiftOneRight(toInsert, i-1, v); 6 v[toInsert] = temp; 7 i += 1; 8 // { i n; ( j: 0<j i-1) v[j-1] v[j] } } 9 // { i=n; ( j: 0<j n-1) v[j-1] v[j] } } 10
18
18 Straight Insertion: l'algoritmo int whereToInsert(int low, int high, 1 int v[], int el) { // 0 low high<size | int v[size] // ( j: low<j high) v[j-1] v[j] int scan = low; 2 // ( j: low<j high) v[j-1] v[j] // ( j: low j scan-1) v[j] el // scan high+1 while (scan <= high && v[scan] <= el) scan +=1; 3 // ( j: low<j high) v[j-1] v[j] // ( j: low j scan-1) v[j] el // ( j: scan j high) v[j]>el // scan high+1 return scan; 4 } 5
19
19 Straight Insertion: l'algoritmo void shiftOneRight(int low, int high, int v[]) { 1 // 0<=low, low-1 <= high, // 0<=high<size-1 | int v[size] int scan = high; 2 while (scan >= low) { 3 v[scan+1] = v[scan]; 4 scan -= 1; 5 } 6 } 7
20
20 Straight Insertion: correttezza, terminazione, complessita' Correttezza: lasciata per esercizio Terminazione: lasciata per esercizio Complessita': ad ogni iterazione del ciclo esterno vengono effettuati toInsert confronti per localizzare la posizione in v (nel sottovettore v[0]..v[i-1] ) in cui inserire v[i] ed i-toInsert assegnamenti per spostare di una posizione a destra gli elementi del sottovettore v[toInsert]..v[i] per cui anche questo algoritmo e' quadratico
21
21 RICERCA BINARIA Ma e' veramente necessario impiegare O(i) confronti per localizzare la posizione in v[] (in particolare nel sottovettore v[0]..v[i-1] ) in cui inserire v[i] Il sottovettore v[0]..v[i-1] e' internamente ordinato! Confrontiamo v[i] con v[i/2] se v[i]=v[i/2] possiamo inserire v[i] in posizione i/2 (o i/2+1 : va bene lo stesso) se v[i]<v[i/2] sicuramente non dovremo inserire v[i] a destra di v[i/2] per localizzare la posizione in cui inserire v[i] a sinistra di v[i/2] possiamo applicare ricorsivamente la stessa strategia, confrontando v[i] con v[(i/2)/2] possiamo ovviamente procedere in modo analogo e speculare nel caso v[i]>v[i/2]
22
22 RICERCA BINARIA: l'algoritmo (del dizionario) int whereToInsert(int low, int high, int v[], int el) { // 0 low high<size | int v[size] // ( j: 0<j size-1) v[j-1] v[j] // ( j: 0 j low-1) v[j] el // ( j: high+1 j size) v[j]>el... // low return high+1 // ( j: 0<j size-1) v[j-1] v[j] // ( j: low j return-1) v[j] el // ( j: return j high) v[j]>el }
23
23 Situazione all'i-esima chiamata ricorsiva: v[low..high]tratto del vettore sotto esame. Gli elementi di questo sottovettore sono tra loro ordinati. v[0..low-1]tratti del vettore non significativi. Si assume che v[high+1..size]gli elementi contenuti in questi sotto-vettori, veri o fittizi, siano comunque ordinati e che tutti gli elementi del primo sottovettore siano el tutti gli elementi del secondo sottovettore siano >el RICERCA BINARIA: la strategia...v[high]v[size]... vettore v high...v[low] lowsize
24
24 N.B.: si assume per comodita' che tutti gli elementi del vettore, anche quelli esterni al range di interesse, siano ordinati e di valore opportuno (vedi pagina precedente). Si assume anche che il vettore si prolunghi con un elemento fittizio di indice size (essendo size la dimensione del vettore). Ovviamente v[size] e' assunto >el. Ovviamente non ha senso accedere gli elementi esterni al range di interesse low..high, sia che siano veri, sia che siano fittizi. (e' comunque impossibile sapere a priori se sono veri o fittizi, eccetto v[size] che e' fittizio) RICERCA BINARIA: la strategia
25
25 RICERCA BINARIA: l'algoritmo int whereToInsert(int low, int high, 1 int v[], int el) { int scan = (low + high) / 2; 2 if (v[scan] <= el) { 3 int newLow = scan + 1; 4 if (newLow <= high) 5 return whereToInsert(newLow, high, v, el); 6 else 7 return newLow; 8 // end if } else { // v[scan]>el 9 int newHigh = scan; 10 if (newHigh > low) 11 return whereToInsert(low, newHigh-1, v, el); 12 else 13 return newHigh; 14 // end if } 15 } 16
26
26 RICERCA BINARIA: analisi dell'algoritmo 1 int whereToInsert(int low, int high, int v[], int el) { // ( j: 0 j low-1) v[j] el // ( j: high+1 j size) v[j]>el int scan = (low + high) / 2; // low scan high if (v[scan] <= el) { // ( j: 0 j scan) v[j] el // ( j: high+1 j size) v[j]>el int newLow = scan + 1; // newLow high+1 // ( j: 0 j newLow-1) v[j] el // ( j: high+1 j size) v[j]>el if (newLow <= high) return whereToInsert(newLow, high, v, el); else // newLow = high+1, era low=high // ( j: 0 j newLow-1) v[j] el // ( j: newLow j size) v[j]>el return newLow; // end if } else {...
27
27 RICERCA BINARIA: analisi dell'algoritmo 2 int whereToInsert(int low, int high, int v[], int el) { // ( j: 0 j low-1) v[j] el // ( j: high+1 j size) v[j]>el int scan = (low + high) / 2; // low scan high if (v[scan] <= el) {... } else { // v[scan]>el int newHigh = scan; // ( j: 0 j low-1) v[j] el // ( j: newHigh j size) v[j]>el if (newHigh > low) return whereToInsert(low, newHigh-1, v, el); else // newHigh=low // ( j: 0 j newHigh-1) v[j] el // ( j: newHigh j size) v[j]>el return newHigh; // end if }
28
28 Ricerca binaria: correttezza, terminazione, complessita' Correttezza: la dimostrazione e' per induzione sulla lunghezza del sottovettore da esaminare, cioe' sulla differenza high-low base dell'induzione: se low=high allora anche scan=high=low. Se v[scan] el allora newLow>high e la funzione ritorna correttamente high+1 Se v[scan]>el allora newHigh=low e la funzione ritorna correttamente low (devo inserire el in posizione low, traslando in avanti v[low] ) passo induttivo: vedi lucido successivo.
29
29 Ricerca binaria: correttezza, terminazione, complessita' Correttezza (continua): passo induttivo: supponiamo che l'algoritmo funzioni k n, e dimostriamo che funziona per n+1: (quindi possiamo assumere high>low ) In questo caso low scan<high. Se v[scan] el allora low<newLow high e high-newLow<n, per cui la funzione si comporta correttamente per ipotesi induttiva (ipotesi induttiva applicabile, precondizioni della funzione soddisfatte) Se v[scan]>el allora se newHigh=low la funzione ritorna correttamente low se newHigh>low allora newHigh-1-low<n, per cui la funzione si comporta correttamente per ipotesi induttiva
30
30 Straight Insertion: verso Mergesort Ordine di complessita' della funzione di ricerca binaria: O(log(n)) infatti ad ogni confronto si dimezza la dimensione del vettore da esaminare La complessita' asintotica di Straight Insertion migliora avendo ridotto da lineare a logaritmica la complessita' di whereToInsert() ? No, perche' rimane comunque lineare la complessita' di shiftOneRight() Supponiamo pero' di utilizzare una tecnica simile a quella della ricerca binaria a Straight Insertion nel suo complesso: nella ricerca binaria ad ogni ricorsione dimezzo la dimensione del sottovettore che devo esaminare, e riconduco la soluzione del problema dimensione n a quella del problema di dimensione n/2. applichiamo la stessa tecnica a Straight Insertion.
31
31 Straight Insertion: verso Mergesort Divido il problema di ordinare un vettore di dimensione n in quello di ordinare due sottovettori di dimensione n/2 Risolto questo, inserisco in modo ordinato gli elementi del secondo sottovettore nel primo sottovettore. Perche' dovrebbe essere piu' efficiente che nel caso di Straight Insertion? Dimezzando la dimensione di un problema quadratico, la sua soluzione e' 4 volte piu' facile! L'inserimento puo' tenere conto del fatto che gli elementi del secondo sottovettore sono anch'essi ordinati (mentre nel caso di Straight Insertion l'inserimento avviene a partire da un secondo sottovettore non ordinato): una volta inserito il primo elemento, il secondo dovra' essere inserito nel sottovettore ordinato alla sua destra! Ovviamente dovro' evitare operazioni di traslazione!
32
32 MERGESORT: la strategia Problema: Dato un vettore di N elementi interi, N 1, ordinarlo in modo non decrescente Premessa: P = { N 1, int v[N] } Conseguenza: C = { N 1, int v[N], j=0..N-2 : v[j] v[j+1] } Strategia: 1.si opera in modo ricorsivo; 2.si divide il vettore in due sottovettori: il sottovettore v[0]..v[(N-1)/2] e il sottovettore v[(N-1)/2+1]..v[N-1]; 3.si ordina in modo indipendente ciascuno dei due sottovettori; 4.si fa il merge (fusione) ordinato dei due sottovettori ordinati in un unico vettore globalmente ordinato.
33
33 Mergesort: perche' questa strategia? Perche' questa strategia? Perche' gli algoritmi di sort considerati fino ad ora sono tutti di complessita' O(n 2 ). Quindi, se divido per 2 la dimensione del problema la sua soluzione diventa 4 volte piu' rapida! E risolvere entrambi i sottoproblemi mi costa (al piu') solo la meta' che risolvere il problema originale. (utilizzando uno degli algoritmi quadratici gia' noti) Mi resta ancora meta' del tempo per operare la fusione delle due soluzioni parziali: se la fusione e' facile, globalmente ci guadagno. In realta', ovviamente, anche i due sottoproblemi possono essere risolti utilizzando ricorsivamente la stessa strategia.
34
34 Mergesort: l'algoritmo void mergeSort(int v[], 1 int low, int high) { assert(low<=high); if (low<high) { 2 int middle = (low + high) / 2; 3 // low<=middle<high mergeSort(v, low, middle); 4 // ( j: low<j middle) v[j-1] v[j] mergeSort(v, middle+1, high); 5 // ( j: low<j middle) v[j-1] v[j] // ( j: middle+1<j high) v[j-1] v[j] merge(v, low, middle, high); 7 // ( j: low<j high) v[j-1] v[j] } 8 } 9
35
35 Mergesort: l'algoritmo di merge - 1 void merge(int v[], 1 int low, int middle, int high) { 2 int *pBufferV = malloc((high-low+1) * sizeof(int)); 3 int bufferIndex = 0; 4 int lowScan = low; 5 int highScan = middle + 1; 6 while (lowScan<=middle && highScan<=high) { 7 if (v[lowScan]<=v[highScan]) { 8 pBufferV[bufferIndex] = v[lowScan]; 9 lowScan += 1; 10 } else { 11 pBufferV[bufferIndex] = v[highScan]; 12 highScan += 1; 13 } 14 bufferIndex += 1; 15 } 16 // continua alla pagina seguente
36
36 Mergesort: l'algoritmo di merge - 2 if (lowScan>middle) // && highScan<=high 17 while (highScan<=high) { 18 pBufferV[bufferIndex] = v[highScan]; 19 highScan += 1; 20 bufferIndex += 1; 21 } 22 else // lowScan high 23 while (lowScan<=middle) { 24 pBufferV[bufferIndex] = v[lowScan]; 25 lowScan += 1; 26 bufferIndex += 1; 27 } 28 // end if // continua alla pagina seguente
37
37 Mergesort: l'algoritmo di merge - 3 for (bufferIndex = 0, lowScan = low; 29 lowScan<=high; 30 bufferIndex += 1, lowScan +=1) 31 v[lowScan] = pBufferV[bufferIndex]; 32 // end for free(pBufferV); 33 // punto 1: // avete notato che quando si alloca memoria // bisogna indicare di quanta se ne ha bisogno, // ma quando si libera memoria non occorre // indicare quanta se ne libera! // Strano! o no? // punto 2: // perche' abbiamo utilizzato memoria dinamica // e non memoria automatica, anche se la // disciplina di allocazione che abbiamo // realizzato e' identica a quella che e' usata // per la memoria automatica? } 34
38
38 Mergesort: esercizi 1.Aggiungere a merge() tutte le asserzioni ritenute significative per ottenere una buona documentazione della funzione. Considerare in particolare precondizioni postcondizioni (prima della copiatura finale allindietro, cioe' prima della riga 29) invariante del primo ciclo 2.Perche' nella realizzazione di merge() non ci si e' accontentati di allocare come variabile automatica un vettore di dimensione costante (fissata a tempo di compilazione)? Considerare problemi di efficienza problemi di funzionalita'
39
39 Mergesort: risposte parziali Invariante del primo ciclo (riga 7) di merge() lowScan middle highScan high (N.B. cio', insieme alla negazione della condizione di controllo del ciclo, garantisce che esattamente uno dei due rami dell'if successivo, riga 17, sia eseguito) bufferIndex-0 = (lowScan-low + highScan-(middle+1)) j=0..bufferIndex-2 : pBufferV[j] pBufferV[j+1] j=low..middle-1 : v[j] v[j+1] j=middle+1..high-1 : v[j] v[j+1] ( j=lowScan..middle, k= 0..bufferIndex) : v[j] pBufferV[k] ( j=highScan..high, k= 0..bufferIndex) : v[j] pBufferV[k] j=low..lowScan-1 : v[j] pBufferV[0.. bufferIndex-1] j=middle+1..highScan-1 : v[j] pBufferV[0.. bufferIndex-1]
40
40 Mergesort: correttezza MERGESORT E' CORRETTA: ASSUMIAMO LA CORRETTEZZA DELL'ALGORITMO DI MERGE. LA CORRETTEZZA DI MERGESORT SI STABILISCE PER INDUZIONEMATEMATICA SUL NUMERO DEGLI ELEMENTI DEL VETTORE. BASE: SE LUNGO 1 (low=high) E' GIA' ORDINATO E NON SI FA NIENTE INDUZIONE: SE PIU' LUNGO, CIASCUNA DELLE DUE META' E' ORDINATA PER HP INDUTTIVA, MERGE LAVORA BENE, PER CUI TUTTO LAVORA BENE MERGE E' CORRETTA: TUTTI GLI ELEMENTI DI CIASCUNO DEI SOTTOVETTORI SONO INSERITI NEL VETTORE FINALE: QUELLI NON INSERITI NEL PRIMO LOOP LO SONO NEL SECONDO. GLI ELEMENTI SONO INSERITI IN ORDINE NON DECRESCENTE PERCHE' LO ERANO GIA', E QUINDI PER INDUZIONE SULLA LUNGHEZZA DEL VETTORE RISULTANTE, NEL PRIMO CICLO PERCHE' LO ERANO GIA' NEL SECONDO
41
41 Mergesort: terminazione TERMINAZIONE: MERGESORT TERMINA PERCHE' LE RICORSIONI TERMINANO APPLICANDOSI SEMPRE SU VETTORI DI LUNGHEZZA DIMEZZATA, PER CUI ALLA FINE SI OTTIENE SEMPRE UN VETTORE DI LUNGHEZZA 1 (SE high==low+1 ALLORA (low+high)/2==low E middle+1==high) MERGE TERMINA PERCHE': A OGNI ITERAZIONE DEL PRIMO LOOP VIENE INCREMENTATO DI 1 L'INDICE DI SCANSIONE O DELL'UNO O DELL'ALTRO SOTTOVETTORE IL SECONDO LOOP SI APPLICA SOLO SUL VETTORE (CHE C'E') NON COMPLETAMENTE SCANDITO DAL PRIMO LOOP: IL LOOP TERMINA PERCHE' L'INDICE DI SCANSIONE DEL VETTORE E' INCREMENTATO DI 1 AD OGNI ITERAZIONE ANCHE L'ULTIMO LOOP TERMINA PER LO STESSO MOTIVO
42
42 Mergesort: complessita' Complessita' dell'algoritmo di merge: Ogni iterazione del primo statement while comporta 3 confronti e 3 assegnamenti una iterazione del ciclo while contenuto in ciascuno dei rami dello statement if successivo comporta 1 confronto e 3 assegnamenti il totale complessivo delle iterazioni dei due primi cicli while e' high-low+1 l'ultimo ciclo (di ricopiatura) e' eseguito' high-low+1 volte, e comporta 3 assegnamenti e 1 confronto ad ogni iterazione quindi l'esecuzione dell funzione merge() comporta l'esecuzione di meno di 2*(3+3)*(high-low+1) operazioni pertanto la funzione merge() ha complessita' O(n), essendo evidentemente n= high-low+1 la dimensione del problema
43
43 Mergesort: complessita' Complessita' dell'algoritmo di mergesort: la figura seguente mostra la sequenza delle chiamate ricorsive fatte dalla funzione mergeSort() quando e' applicata ad un vettore di 10 elementi (di indice da 0 a 9) la coppia di valori in ogni nodo rappresenta i valori dei parametri low e high per quell'attivazione di mergeSort() viene anche indicato l'ordine delle operazioni di call e return l'operazione di divide continua fino a che ogni nodo contiene un solo elemento
44
44 Mergesort: complessita' Complessita' dell'algoritmo di mergesort: 0, 9 0, 12, 23, 34, 45, 67, 78, 89, 9 0, 23, 45, 78, 9 0, 45, 9 0, 01, 15, 56, 6 1, 910, 18 2, 57, 811, 1416, 17 3, 36, 48, 69, 712, 1215, 1317, 1518, 16 13, 1014, 114, 15, 2 Call, return
45
45 Mergesort: complessita' Complessita' dell'algoritmo di mergesort: la complessita' della funzione mergeSort() e' descritta dalla seguente relazione ricorsiva: a con a costante, se n=1 T(n) = 2 * T(n/2) + c * n + c' con c e c' costanti, se n>1 espandendo in modo ricorsivo T(n/2) e trascurando la costante c': T(n) = 2 * T(n/2) + c * n = 2 * (2 * T(n/4) + c * n/2) + c * n = = 4 * T(n/4) + 2 * c * n = = 4 * (2 * T(n/8) + c * n/4) + 2 * c * n = = 8 * T(n/8) + 3 * c * n = in generale = 2 p * T(n/2 p ) + p * c * n {
46
46 Parentesi - Esercizio verificare, utilizzando il principio di induzione matematica, la uguaglianza delle due seguenti relazione ricorsive: 1.T(n) = 2 * T(n/2) + c * n 2.T(n) = 2 p * T(n/2 p ) + p * c * n suggerimento: basta assumere l'uguaglianza per p=n e verificare che applicando la definizione 1 si ottiene la relazione 2 per p=n+1
47
47 Mergesort: complessita' Complessita' dell'algoritmo di mergesort: T(n) = 2 p * T(n/2 p ) + p * c * n dove 2 p * T(n/2 p ) e' il costo dell'attivazione ricorsiva di mergeSort() p * c * n e' il costo delle attivazioni della funzione merge() ponendo p = log 2 n (e quindi 2 p =n) T(n) = n * T(1) + log 2 n * c * n = a * n + c * n * log 2 n e quindi T(n) = O(n * log 2 n) = O(n * log(n))
48
48 Mergesort: complessita' Perche la strategia adottata per mergesort funzioni occorre che la suddivisione del vettore avvenga in due sotto-vettori bilanciati, cioe di uguale numero di elementi. Supponiamo infatti di suddividere il vettore in 2 sotto-vettori, il primo di n-1 elementi e il secondo di 1 elemento: La soluzione del primo problema avrebbe costo O((n-1) 2 ) = O(n 2 ) Quindi no si sarebbe guadagnato niente! In effetti, se modifichiamo mergesort suddividendo il vettore in 2 sotto-vettori, il primo di n-1 elementi e il secondo di 1 elemento, quello che otteniamo e la versione ricorsiva di straight-insertion!
49
49 Complessita' di mergesort e straight selection Si e' visto che, considerando il termine di ordine maggiore, le complessita' di mergesort e straight selection sono rispettivamente Tmergesort(n) = c 1 * n * log 2 n Tstraightselection (n) = c 2 * n 2 da cio' deriva che straight selection e' migliore di mergesort quando c 2 * n 2 < c 1 * n * log 2 n cioe' quando c 1 > c 2 * n / log 2 n quando il vettore da ordinare e' abbastanza corto puo' essere piu' conveniente utilizzare straight selection (l'algoritmo asintoticamente peggiore!) N.B.: in realta' quando si calcola la complessita' di un algoritmo di ordinamento per confronti, normalmente si contano solo i confronti tra elementi del vettore
50
50 ORDINAMENTO PER CONFRONTI COMPLESSITA' DEL PROBLEMA QUANTI SONO I POSSIBILI ORDINAMENTI DI UN INSIEME DI n ELEMENTI? n! = n * (n-1) * (n-2) *... * 1 ORDINARE UN INSIEME DI n ELEMENTI SIGNIFICA SELEZIONARE TRA LE n! PERMUTAZIONI DEGLI ELEMENTI DELL'INSIEME QUELLA CHE SODDISFA LA RELAZIONE DI ORDINAMENTO LA SELEZIONE AVVIENE PER CONFRONTI DI 2 ELEMENTI, IL CHE MI PORTA A PRENDERE UNA DECISIONE POSSIAMO QUINDI IMMAGINARE LE n! PERMUTAZIONI COME LE FOGLIE DI UN ALBERO BINARIO DI DECISIONI CHE CI PERMETTE DI SELEZIONARE LA PERMUTAZIONE GIUSTA UN ALBERO BINARIO COMPLETO DI ALTEZZA h HA 2 h FOGLIE (ELEMENTARE PER INDUZIONE, E LO VEDREMO DURANTE IL CORSO) PERCIO' SE LE FOGLIE SONO n!, L'ALTEZZA MINIMA DELL'ALBERO BINARIO DI DECISIONE E' log(n!)
51
51 ORDINAMENTO PER CONFRONTI COMPLESSITA' DEL PROBLEMA UNA APPROSSIMAZIONE DI n! E' (n/e) n E QUINDI log(n!)= n * (log(n) - log(e)) = n*log(n) - 1.44*n = (n*log(n)) SEGUENDO L'ALBERO DI DECISIONE SI DEBBONO OPERARE TANTI CONFRONTI QUANTA E' L'ALTEZZA DELL'ALBERO PERTANTO LA COMPLESSITA' DEL PROBLEMA DI ORDINARE UN VETTORE PER CONFRONTO DI ELEMENTI E' DI ORDINE n*log(n) NON PUO' ESISTERE NESSUN ALGORITMO CHE OPERA PER CONFRONTO CHE SIA MEGLIO DI O(n*log(n)) PERTANTO MERGESORT (IN QUESTO SENSO) E' OTTIMO
52
52 ORDINAMENTO PER CONFRONTI COMPLESSITA' DEL PROBLEMA SI PUO' FARE DI MEGLIO, OPERANDO PER CONFRONTI? NOTA CHE L'OPERAZIONE DI merge RICHIEDE L'UTILIZZO DI UN VETTORE DI APPOGGIO IN CUI OPERARE IL MERGE, E QUINDI UNA COPIATURA ALL'INDIETRO: LAVORO SPAZIO: MERGESORT NON OPERA 'SUL POSTO' COME FANNO GLI ALGORITMI O(n 2 ) VISTI (E.G. BUBBLESORT) MA RICHIEDE UNO SPAZIO DI MEMORIA AGGIUNTIVO DI DIMENSIONE n LA COMPLESSITA' SPAZIALE DI MERGESORT E' MAGGIORE DI QUELLA DI BUBBLESORT: 2*n ANZICHE' n IN PIU' C'E' L'OVERHEAD (anche spaziale) DELLE CHIAMATE DI PROCEDURA E QUELLO DELLA DOPPIA COPIATURA PER IL MERGING Esistono algoritmi di ordinamento per confronto di complessita O(n*log(n)) che operano sul posto? Si', heapsort
53
53 HEAPSORT (TREESORT) Modificando straight -insertion, che e' di complessita' O(n 2 ), abbiamo ottenuto un nuovo algoritmo, mergesort, di complessita' O(n*log(n)) Un primo tentativo per ottenere questo risultato e' stato quello di rendere logaritmico, anziche' lineare, il costo di localizzare la posizione dove inserire il nuovo elemento nel sottovettore gia' ordinato. Ri-consideriamo straight -selection: cosa e' che lo rende quadratico? Il fatto che ad ogni iterazione il costo per identificare il nuovo elemento minimo e' lineare: sarebbe possibile renderlo logaritmico?
54
54 HEAPSORT (TREESORT) NEL PRIMO CICLO DI STRAIGHT SELECTION TRAMITE n-1 CONFRONTI VIENE IDENTIFICATO L'ELEMENTO MINIMO DEL VETTORE NON SI RACCOGLIE NESSUNA ALTRA INFORMAZIONE RIGUARDO AGLI ALTRI n-1 ELEMENTI NON MINIMI! COSA POTREMMO FARE IN ALTERNATIVA CON n-1 CONFRONTI A NOSTRA DISPOSIZIONE? 1.DIVIDIAMO GLI n ELEMENTI IN COPPIE 2.CON n/2 CONFRONTI SI PUO' STABILIRE L'ELEMENTO MINIMO DI OGNI COPPIA, OTTENENDO n/2 ELEMENTI 3.I PASSI (1) E (2) POSSONO ESSERE ITERATI FINO AD OTTENERE L'ELEMENTO MINIMO DEL VETTORE, PIU' UN ALBERO DI RELAZIONI TRA GLI ELEMENTI DEL VETTORE VIA VIA SELEZIONATI IL TUTTO CON n/2 + n/4 + n/8 +... = n-1 CONFRONTI L'ALBERO COSI' COSTRUITO HA OVVIAMENTE ALTEZZA log(n)
55
55 HEAPSORT: esempio vettore = [ 44, 55, 12, 42, 94, 18, 6, 67 ] 6 445512429418667 4412186 126
56
56 HEAPSORT PROCEDIAMO COME IN STRAIGHT SELECTION: 1.ESTRAIAMO DALL'ALBERO L'ELEMENTO MINIMO. PER FARLO OCCORRE CANCELLARLO A TUTTI I LIVELLI DELL'ALBERO SOSTITUENDOLO CON UNA CHIAVE "INDEFINITO". CIO' IMPLICA log(n) OPERAZIONI. 2.SEGUENDO IL PATH DELL'EX ELEMENTO MINIMO RIEMPIAMO I BUCHI LASCIATI DALLA SUA CANCELLAZIONE FACENDO EMERGERE DAL BASSO IL RELATIVO SOSTITUTO. QUESTA OPERAZIONE COSTA ANCH'ESSA log(n), ED IN PARTICOLARE IMPLICA log(n) CONFRONTI. 3.SI E' COSI' SELEZIONATO IL SECONDO ELEMENTO MINIMO MA QUESTA VOLTA IN TEMPO log(n). 4.RIPETENDO I PASSI (1)...(3) FINO A CHE SONO ESTRATTI TUTTI GLI ELEMENTI DEL VETTORE ORIGINARIO ABBIAMO ORDINATO IL VETTORE CON UN NUMERO DI CONFRONTI PARI A: (n-1) + (n-1)*log(n) ED IN GENERALE CON UN COSTO O(n*log(n)) LA COSA NON VIENE GRATIS: BISOGNA MEMORIZZARE E MANTENERE LA STRUTTURA AD ALBERO. OCCORRE TRATTARE LA CHIAVE "INDEFINITO", CHE OLTRE TUTTO CAUSA CONFRONTI INUTILI.
57
57 HEAPSORT: esempio vettore = [ 44, 55, 12, 42, 94, 18, 6, 67 ] vettore ordinato = [ ] 6 445512429418667 4412186 126
58
58 HEAPSORT: esempio vettore = [ 44, 55, 12, 42, 94, 18, 6, 67 ] vettore ordinato = [ 6 ] 44551242941867 441218 12
59
59 vettore = [ 44, 55, 12, 42, 94, 18, 6, 67 ] vettore ordinato = [ 6 ] HEAPSORT: esempio 12 44551242941867 44121867 1218
60
60 vettore = [ 44, 55, 12, 42, 94, 18, 6, 67 ] vettore ordinato = [ 6 ] HEAPSORT: esempio 12 44551242941867 44121867 1218
61
61 vettore = [ 44, 55, 12, 42, 94, 18, 6, 67 ] vettore ordinato = [ 6, 12 ] HEAPSORT: esempio 445542941867 441867 18
62
62 vettore = [ 44, 55, 12, 42, 94, 18, 6, 67 ] vettore ordinato = [ 6, 12 ] HEAPSORT: esempio 18 445542941867 44421867 4218
63
63 HEAP Ma per mantenere questa organizzazione degli elementi del vettore che ne riflette degli ordinamenti parziali e' necessario mantenere, oltre al vettore originario, anche una esplicita struttura dati ad albero? E come fare a costruire questa struttura ad albero? L'algoritmo del torneo per identificare l'elemento minimo di un vettore in linea di principio raccoglie esattamente questa informazione, ma non la traduce in una struttura dati esplicita. In sostanza, sarebbe facile costruire e mantenere la struttura dati che mi rende espliciti tutti i risultati di tutti gli scontri del torneo? Esistono alternative piu' semplici? Si' esiste una struttura dati che mi rappresenta sostanzialmente la stessa informazione dei risultati degli incontri del torneo: questa struttura dati e' chiamata heap.
64
64 HEAP il vettore stesso puo' essere strutturato internamente in modo da mantenere questa informazione: Il vettore stesso puo' essere utilizzato per rappresentare un albero binario. In questo albero binario ogni nodo e' minore o uguale dei suoi figli (se ne ha). Un vettore cosi' strutturato e' chiamato heap infatti: UN ALBERO BINARIO COMPLETO di n elementi PUO' ESSERE RAPPRESENTATO IN UN ARRAY (di indici da 0 a n-1) DOVE I FIGLI SINISTRO E DESTRO DEL NODO i SI TROVANO RISPETTIVAMENTE AGLI INDICI 2*i+1 E 2*i+2 perche' i nostri vettori sono indiciati a partire da 0 se i nostri vettori fossero indiciati a partire da 1 i figli si troverebbero agli indici i*2 e i*2+1
65
65 HEAP N.B.: un array rappresenta sempre un albero binario completo o quasi completo cioe' un albero che ha tutti i nodi a tutte le profondita' salvo eventualmente a quella massima, in cui possono essere assenti le foglie piu a destra Perche un array che rappresenta un albero binario sia uno heap deve anche valere la regola h[i] h[2*i+1] h[i] h[2*i+2] (se questo elemento esiste) per ogni i 0.. (n/2)-1, cioe' per tutti gli elementi che hanno almeno un figlio (n e' la dimensione del vettore, i cui indici vanno da 0 a n-1) N.B. (n/2)-1 = ((n-1)-1)/2 Poiche' i figli del nodo i si trovano agli indici 2*i+1 e 2*i+2, il padre del nodo di indice k si trova all'indice (k-1)/2 Nota che, se larray h e uno heap, h[0] e lelemento minimo dellarray
66
66 HEAP: esempio vettore = [ 4, 12, 6, 14, 22, 18, 16, 44, 55, 23, 42, 94, 19, 26, 67 ] i | 0 i n/2-1 : v[i] v[2*i+1] v[i] v[2*i+2] i | 0 i n-1 : v[0] v[i] v[0]=4 v[7] =44 v[8] =55 v[9] =23 v[10] =42 v[11] =94 v[12] =19 v[13] =26 v[3]=14v[4]=22v[5]=18v[6]=16 v[1]=12v[2]=6 v[14] =67
67
67 HEAPSORT: percolazione (sift) CONSIDERIAMO UNO HEAP IN CUI MANCHI L'ELEMENTO DI INDICE 0 E PENSIAMO DI INSERIRE UN NUOVO ELEMENTO. METTENDOLO IN POSIZIONE 0, QUELLO CHE SI OTTIENE E' UNO HEAP? CIOE' E' h[0] h[1] E h[0] h[2] ? SE NO, BISOGNA FARLO PERCOLARE AL POSTO GIUSTO: Se h[2] < h[0] h[1] ALLORA BISOGNA SCAMBIARE h[0] E h[2] E POI HEAPIFICARE RICORSIVAMENTE IL SOTTOALBERO DI RADICE h[2]. Se h[1]< h[0] h [2] ALLORA BISOGNA SCAMBIARE h[0] E h[1] E POI HEAPIFICARE RICORSIVAMENTE IL SOTTOALBERO DI RADICE h[1]. Se h[0] > h[1] h[0] > h[2] ALLORA BISOGNA SCAMBIARE h[0] E min(h[1], h[2]) E POI HEAPIFICARE RICORSIVAMENTE IL SOTTOALBERO DI RADICE h[1] O h[2] MODIFICATO. PERCHE' SIA MANTENUTA LA PROPRIETA' DELLO HEAP SI DEVE HEAPIFICARE VERSO IL FIGLIO MINORE! OVVIAMENTE TUTTI I DISCORSI SI POSSONO FARE IN MODO DUALE PER HEAP IN CUI VALE LA RELAZIONE ANZICHE'
68
68 Percolazione : esempio facciamo percolare in v l'elemento 44 v[0]= v[3]=55v[4]=94v[5]=18v[6]=12 v[1]=42v[2]=6
69
69 Percolazione : esempio facciamo percolare in v l'elemento 44 v[0]=44 v[3]=55v[4]=94v[5]=18v[6]=12 v[1]=42v[2]=6
70
70 Percolazione : esempio facciamo percolare in v l'elemento 44 v[0]=6 v[3]=55v[4]=94v[5]=18v[6]=12 v[1]=42v[2]=44
71
71 Percolazione : esempio facciamo percolare in v l'elemento 44 v[0]=6 v[3]=55v[4]=94v[5]=18v[6]=44 v[1]=42v[2]=12
72
72 HEAP: procedura di percolazione o sift() fa percolare in un vettore heapificato dall'elemento di indice i+1 fino all'elemento di indice r un elemento inserito nella posizione i la percolazione termina quando un nodo non ha figli o e' minore o uguale di tutti (1 o 2) i suoi figli void sift (int n, int v [], int i, int r) { // int v[n]; 0<=i<=n-1; 0<=r<=n-1; i<=r // N.B.: n in effetti e' irrilevante, conta solo r int j = 2*i+1; // indice di eventuale figlio sx if (j<=r) { // esiste almeno il figlio sx? if (j v[j+1]) j += 1; // e il dx? // j e' l'indice del figlio con valore minore // se v[j]<v[i] li scambia e percola ricors. if (v[i]>v[j]) { // scambia v[i] e v[j] int tmp = v[i]; v[i] = v[j]; v[j] = tmp; sift(n, v, j, r); // percola ricorsivamente } // end if }
73
73 HEAPSORT : procedura di sift() procedura di sift() CORRETTEZZA: E' DIMOSTRATA FACILEMENTE PER INDUZIONE MATEMATICA sull'altezza del nodo da cui si fa percolare, TENENDO CONTO CHE SE i*2+1>r IL SOTTOALBERO DI RADICE i, CHE CONTIENE SOLO L'ELEMENTO i, E' OVVIAMENTE UNO HEAP, E ASSUMENDO CHE TUTTI GLI ELEMENTI DI INDICE >i SIANO RADICI DI SOTTOALBERI HEAP COMPLESSITA': E' LINEARE CON L'ALTEZZA NELL'ALBERO DEL NODO DI INDICE i, IN QUANTO E' T(altezza) = T(altezza-1) + cost
74
74 HEAPSORT: heapificazione iniziale Per applicare la strategia delineata bisogna inizialmente heapificare l'intero vettore in ingresso (che, in generale, non e' uno heap). In realta', qualsiasi vettore di dimensione n e' gia' intrinsecamente uno heap per tutti gli elementi di indice i>n/2-1, per i quali non esiste nessun elemento 2*i+1 e 2*i+2 con cui confrontarsi! Per heapificare il resto del vettore: void buildHeap(int v[], int n) { // int v[n] int k = n/2-1; // v[] e' gia' heapificato nel suffisso k+1..n-1: // - j=k+1..n-1 : 2*j+1 n-1 v[j] v[2*j+1] // - j=k+1..n-1 : 2*j+2 n-1 v[j] v[2*j+2] while (k>=0) { sift(n, v, k, n-1); k -= 1; }
75
75 HEAPSORT: heapificazione iniziale buildHeap(), correttezza: –La dimostrazione di correttezza (per induzione matematica) e' banale tenendo conto della correttezza di sift() e notando che quando sift-iamo l'elemento k tutti i successivi sono gia' heapificati. buildHeap(), complessita': –Poiche' lo heap ha altezza log(n), essendo un albero binario completo o quasi completo, e poiche' la procedura sift() e' invocata n/2 volte, buildHeap() e' di complessita' al piu' O(n*log(n)). –Vedi esercizi.
76
76 HEAPSORT: l'algoritmo Dopo l'applicazione di buidHeap() v[0] e' l'elemento minimo che vogliamo estrarre: Dove lo mettiamo? Al posto dell'elemento in posizione v[n-1]. Poi mettiamo il valore che era precedentemente in v[n-1] in v [0] e lo facciamo percolare al posto giusto utilizzando la funzione sift(), e tenendo conto che lo heap si e' accorciato di un elemento. Ripetiamo questa operazione n-1 volte e il vettore e' ordinato. Ma e ordinato in modo non crescente! Per ordinare il vettore in modo non descrescente avremmo dovuto costruire lo heap utilizzando la relazione anziche la relazione
77
77 Situazione all'i-esima iterazione del ciclo: v[i+i]..v[N-1]tratto del vettore gia' globalmente ordinato in modo non crescente. Tutti gli elementi del sottovettore v[i+1]..v[N-1] sono minori o uguali di tutti gli elementi del sottovettore v[0]..v[i] v[0]..v[i]sottovettore strutturato ad heap secondo la relazione iposizione nel vettore nella quale andare ad inserire l'elemento minimo selezionato nel sottovettore heap Heapsort: la strategia v[i+1].. v[N-1] ordinatov[1]v[i]... vettore v i v[0]
78
78 HEAPSORT: l'algoritmo void heapSort(int n, int v[]) { int k; buildHeap(v, n); // v[0]..v[k] e' uno heap con relazione // j=k+2..n-1 : v[j-1] v[j] // j=0..k, i=k+1..n-1 : v[j] v[i] for(k=n-1; k>=1; k-=1) { int temp = v[0]; v[0] = v[k]; v[k] = temp; sift(n, v, 0, k-1); }
79
79 HEAPSORT: correttezza e complessita' Correttezza: IL VETTORE E' ORDINATO IN MODO NON CRESCENTE Dimostrazione: e' per induzione sulle iterazioni. L'IPOTESI INDUTTIVA E' CHE ALL'ITERAZIONE DI INDICE i IL SOTTOVETTORE v[i+1..n-1] E' ORDINATO IN QUESTO MODO, E CHE I SUOI ELEMENTI SONO TUTTI DI QUELLI DEL SOTTOVETTORE v[0..i] Complessita': buildHeap() E' O(n*log(n)), COSI' E' OVVIAMENTE ANCHE IL RESTO PER CUI TUTTO L'ALGORITMO E' O(n*log(n)) ED E' IN-PLACE OVVIAMENTE SE VOGLIAMO UN VETTORE ORDINATO IN MODO NON DECRESCENTE NON SCAMBIAMO GLI ELEMENTI DOPO HEAPSORT, MA CAMBIAMO DA A LA RELAZIONE CARATTERIZZANTE DELLO HEAP
80
80 Ordinare senza confrontare Si e' visto che operando per confronto non e' possibile ordinare un vettore in tempo migliore che O(n*log(n)). Ma come e' possibile ordinare un vettore senza eseguire confronti? Come si fa a ordinare un mazzo di carte? Di solito per prima cosa si separano cuori, fiori, quadri e picche, e per fare questo non si opera alcun confronto. I confronti entrano in gioco per ordinare tra loro carte dello stesso seme. Che sono normalmente ordinate utilizzando straight insertion. Questo algoritmo ci dice che e' possibile ordinare non solo per confronti, ma anche "per distribuzione". E' possibile operare solo per distribuzione?
81
81 Ordinare per distribuzione Lalgoritmo: Prima distribuisco le carte secondo il loro valore (numero), indipendentemente dal colore, in 13 pacchetti diversi e ordinati tra loro. Il pacchetto dei 2 segue quello degli assi e precede quello dei 3. Poi raccolgo i pacchetti mantenedo il loro ordine. Poi distribuisco le carte cosi' raccolte in base al seme, in 4 pacchetti diversi e ordinati tra loro. Poi raccolgo i pacchetti nel loro ordine: il mazzo risulta ordinato! Note: La sequenza con cui si applicanoi criteri di distribuzione e essenziale: Se avessi distribuito al primo giro in base al colore e al secondo in base al numero non avrei ottenuto un mazzo ordinato. Avrei ottenuto un mazzo con tutti gli assi, seguiti da tutti i 2, seguiti da tutti i 3, … ogni sottomazzetto di egual numero ordinato secondo il colore! Per ottenere il mazzo ordinato devo distribuire prima in base alla cifra piu leggera (il numero), poi in base a quella piu pesante (il colore).
82
82 RADIX SORT (Distribution Sort) void radixSort(int n, int numLen, char* v[]) { // Un intero e' rappresentato come una stringa // di caratteri in base 10. // Tutte le stringhe hanno la stessa lunghezza, // numLen (>= 1). // Per unificare la lunghezza delle stringhe, // dove necessario sono stati aggiunti dei // leading 0 struct distributionSlot { int elsInSlot; char* slot[?]; // cosa e'? See next page. } (struct distributionSlot) radixBins[10]; // un bidone per radice, cioe' per cifra
83
83 char* slot[?] ? Cosa significa char* slot[?]; ? Con questa notazione indichiamo un array di dimensioni indefinite, che possono crescere (e calare?) secondo necessita' (e.g. quando scriviamo un valore in un elemento non ancora esistente). Una struttura dati di questo tipo non esiste come primitiva del C! A un certo punto ci dovremo quindi chiedere: come realizzarla? Nota che la modalita' di realizzazione dovra' garantire che il costo delle operazioni sulla struttura dati sia quello che ipotizzeremo per valutare la complessita' di radixSort() Nello scrivere radixSort() dovremo quindi porre attenzione a quali operazioni sono necessarie per manipolare un oggetto di tipo: char* slot[?]
84
84 RADIX SORT (Distribution Sort) // reset distribution bins for (int dScan = 0; dScan <= 9; dScan += 1) radixBins[dScan].elsInSlot = 0; // end for // distribute/collect for each digit, from // least significant for (dScan = numLen-1; dScan>=0; dScan -= 1) { // gli elementi in v sono ordinati rispetto // al loro suffisso dScan+1..numLen-1 // distribute for (int sScan = 0; sScan < n; sScan += 1) { int iSlot = v[sScan][dScan] - '0'; int last = radixBins[iSlot].elsInSlot; radixBins[iSlot].slot[last] = v[sScan]; radixBins[iSlot].elsInSlot += 1; }
85
85 RADIX SORT (Distribution Sort) // collect // gli elementi in ogni slot sono ordinati // rispetto al loro suffisso dScan+1..numLen-1 int sScan = 0; for (int cScan = 0; cScan <= 9; cScan += 1) { for(int iScan = 0; iScan < radixBins[cScan].elsInSlot; iScan += 1) { v[sScan]= radixBins[cScan].slots[iScan]; sScan += 1; } radixBins[cScan].elsInSlot = 0; } // gli elementi in v sono ordinati // rispetto al loro suffisso dScan..numLen-1 }
86
86 RADIXSORT: correttezza - 1 La procedura ordina le stringhe numeriche in ingresso in modo non decrescente. Dimostrazione: e' per induzione sulle iterazioni del ciclo di distribuzione/raccolta, cioe' in pratica sulla lunghezza delle stringhe. Per stringhe di lunghezza 1 (numLen=1) il ciclo viene eseguito una sola volta: tutte le stringhe "0" sono sistemate nel bidone 0, tutte le stringhe "1" sono sistemate nel bidone 1, e cosi' via. poiche' la fase di raccolta scandisce i bidoni in ordine da 0 a 9, nell'array finale tutte le stringhe "0" precedono tutte le stringhe "1" che precedono tutte le stringhe "2" e cosi' via. Continua alla pagina seguente.
87
87 RADIXSORT: correttezza - 2 Supponiamo ora che la procedura operi correttamente per stringhe di lunghezza n. Per ipotesi induttiva, dopo n iterazioni di distribuzione/raccolta le stringhe sono ordinate nel vettore rispetto ai loro n ultimi digit. L'n+1-esima iterazione processa le stringhe nell'ordine in cui compaiono nel vettore, distribuendole in base alla n+1-esima cifra dal fondo. Pertanto tutte le stringhe che hanno l'n+1-esimo digit che vale '0' si trovano nel bidone 0, e all'interno del bidone queste stringhe sono ordinate in base ai loro ultimi n digit. Lo stesso ragionamento si puo' fare per le stringhe il cui n+1- esimo digit da destra vale '1', '2', … E poiche' la procedura di raccolta scandisce i bidoni e il loro contenuto in ordine, anche il vettore di raccolta risultera' ordinato, rispetto alle ultime n+1 cifre delle stringhe. QED
88
88 RADIXSORT: complessita - prologo Operazioni su char* slot[?] In che modo (con quali operazioni) sono accedute le strutture dati di tipo char* slot[?] ? Scritture (inserimenti) e letture/estrazioni in ordine FIFO E possibile tramite liste implementare una coda FIFO in cui le operazioni di inserzione/rimozione hanno costo O(1) Quindi e legittimo assumere che anche le operazioni che accedono a strutture dati char* slot[?] abbiano costo unitario. In realta durante la fase di collect sarebbe piu conveniente potere raccogliere tutti gli elementi di un bidone tramite una unica operazione di complessita costante (anziche trasferirli uno per uno). Perche questo sia possibile e sufficiente che tra le operazioni sulla lista FIFO ci sia anche quella di concatenazione di liste e che questa operazione abbia complessita costante!
89
89 RADIXSORT: complessita' Complessita': il ciclo di distribuzione/raccolta viene ripetuto numLen volte, cioe' tante volte quanto e' la lunghezza delle stringhe da ordinare il corpo del ciclo ha complessita' O(n) dove n e' il numero di stringhe da ordinare N.B.: trascurando le operazioni necessarie per scandire tutti i bidoni. Questa ipotesi e ragionevole se i bidoni sono normalmente pieni, il che e probabilisticamente vero se il numero delle stringhe (casuali) e molto maggiore di quello dei bidoni. per cui l'algoritmo ha complessita' O( numLen * n) ed e' conveniente rispetto agli algoritmi che operano per confronto quando numLen <log(n) in realta' bisognerebbe tenere conto dei fattori moltiplicativi: e.g. ad ogni iterazione ogni elemento e' copiato due volte!
90
90 RADIXSORT: complessita' Nota che piu bidoni ho a disposizione, piu rapido e lalgoritmo, perche a parita di lunghezza delle stringhe da ordinare il numero di fasi di distribuzione-raccolta (pari al numero di cifre dei numeri) e minore. Dovendo ordinare un vettore di interi senza segno da 64 bit posso utilizzare ad esempio radici da 4, 8, 16 bit (e corrispondentemente 16, 256, 65536 scatole): nel primo caso lalgoritmo effettuera 16 fasi, nel secondo 8, nel terzo 4. In realta pero, detto #bins il numero dei bidoni utilizzati nella distribuzione, la fase di raccolta ha complessita O(#bins+n) perche devono essere scanditi tutti i bidoni, anche se sono vuoti. Normalmente si assume che sia n >> #bins, cosi che O(#bins+n) O(n), ma questo non e vero se #bins diventa molto grande. Da questo discende che non e sempre conveniente moltiplicare il numero dei bidoni, e quindi diminuire numLen. In particolare non e detto che sia conveniente utilizzare tanti bidoni quanti sono i caratteri dellalfabeto in caso di stringhe Unicode (2 16 caratteri), salvo che il numero delle stringhe da ordinare non sia comunque molto maggiore di 2 16.
91
91 RADIXSORT: generalizzazione L'algoritmo di radixsort puo' ovviamente essere generalizzato anche a stringhe non numeriche e di lunghezza diversa: se consideriamo stringhe sull'alfabeto ASCII esteso (256 caratteri) quante radici dobbiamo considerare? Come si gestiscono stringhe di lunghezza diversa? nel caso di stringhe numeriche basta considerarle logicamente estese con degli 0 a sinistra nel caso di stringhe alfabetiche basta considerarle logicamente estese con degli spazi (o dei '\0') a destra Notare che la fase di raccolta puo' essere eliminata utilizzando 2 sequenze di bidoni di distribuzione e palleggiando le stringhe in distribuzione dall'una all'altra Esercizio: riscrivere la procedura di distribution sort perche' possa trattare stringhe alfanumeriche di lunghezza variabile ed evitando la fase di raccolta
92
92 RankSort e sorting parallelo (da: G. StefanescuNational University of Singapore - Parallel & Concurrent Programming, Spring, 2004) Strategia dellalgoritmo di Rank Sort: count the number of numbers that are smaller than a number a in the array this gives the position of a in the sorted array this procedure has to be repeated for all elements of the array Hence the time complexity of Rank Sort is n*(n-1), thus O(n2). Rank Sort e il peggiore algoritmo di ordinamento sequenziale visto nel corso!
93
93 RankSort e sorting parallelo Versione sequenziale dellalgoritmo di Rank Sort: void rankSort(int a[],int sorted[], unsigned n) { /* works well only if there are no repetitions of the numbers * in the array */ for (unsigned i=0; i<n; i+=1) { int rank = 0; for (unsigned j=0; j<n; j+=1) if (a[i] > a[j]) rank += 1; sorted[rank] = a[i]; }
94
94 RankSort e sorting parallelo Versione parallela dellalgoritmo di Rank Sort, utilizzando n processori: void rankSort(int a[],int sorted[], unsigned n) { /* works well only if there are no repetitions of the * numbers in the array */ forall (unsigned i=0; i<n; i+=1) { /* listruzione parallela forall distribuisce * lesecuzione del ciclo su n processori paralleli */ int rank = 0; for (unsigned j=0; j<n; j+=1) if (a[i] > a[j]) rank += 1; sorted[rank] = a[i]; } n processors work in parallel to find the ranks of all numbers of the array. Parallel time complexity is O(n), better than any sequential sorting algorithm.
Presentazioni simili
© 2024 SlidePlayer.it Inc.
All rights reserved.