Algoritmi Fondamentali Nicola Di Mauro [email protected] 2 Ordinamento Disporre un gruppo di elementi secondo una prefissata relazione d’ordine Dipendente dal tipo di informazione 2 possibilità Crescente Decrescente Altri criteri Una delle attività di elaborazione più importanti Numerica (ordinamento numerico) Alfanumerica (ordinamento lessicografico) Stima: 30% del tempo di calcolo di un elaboratore Obiettivo: efficienza Sfruttare “al meglio” i confronti ed i conseguenti spostamenti degli elementi Piazzare gli elementi prima possibile più vicino alla loro posizione finale nella sequenza ordinata 3 Ordinamento Possibilità di avere elementi costituiti da più componenti Associazione a ciascuno di una chiave Identifica univocamente Stabilisce la sua posizione Unica componente rilevante per l’ordinamento esempio: record / oggetti Supponiamo, nel seguito, di richiedere un ordinamento numerico crescente elaborazione sulla sola chiave 4 Ordinamento Gran varietà di algoritmi Basati su confronti e scambi fra gli elementi Relazione d’ordine, criterio Non esiste uno migliore in assoluto La bontà dipende da fattori connessi ai dati su cui deve essere applicato Dimensione dell’insieme di dati Numerosità Grado di pre-ordinamento dell’insieme di dati Già ordinato, parzialmente, ordine opposto, casuale 5 Ordinamento Algoritmi esterni Usano un array di appoggio Occupazione di memoria doppia Necessità di copiare il risultato nell’array originale Algoritmi interni Eseguono l’ordinamento lavorando sullo stesso array da ordinare Basati su scambi di posizione degli elementi 6 Ordinamento Esterno Enumerativo Ciascun elemento confrontato con tutti gli altri per determinare il numero degli elementi dell’insieme che sono più piccoli in modo da stabilire la sua posizione finale Non verranno trattati nel seguito 7 Ordinamento Interno Per Selezione A bolle Elemento più piccolo localizzato e separato dagli altri, quindi selezione del successivo elemento più piccolo, e così via Coppie di elementi adiacenti fuori ordine scambiate, finché non è più necessario effettuare alcuno scambio Per Inserzione Elementi considerati uno alla volta e inseriti al posto che gli compete all’interno degli altri già ordinati Ordinamento per Selezione 8 Minimi successivi Trovare il più piccolo elemento dell’insieme e porlo in prima posizione Scambio con l’elemento in prima posizione Trovare il più piccolo dei rimanenti (n – 1) elementi e sistemarlo in seconda posizione … Finché si trovi e collochi il penultimo elemento Ultimo sistemato automaticamente Ordinamento per 9 Selezione: Esempio Inizio I II III IV V array(1) 44 44 11 11 11 11 array(2) 33 33 33 22 22 22 array(3) 66 66 66 66 33 33 array(4) 11 11 44 44 44 44 array(5) 55 55 55 55 55 55 array(6) 22 22 22 33 66 66 Ordinamento per 10 Selezione: Algoritmo i←0 fintantoché i < n-1 esegui trova il minimo di lista (i … n-1) scambia la posizione del minimo con lista(i) i←i+1 Algoritmi già noti Ricerca del minimo Scambio Ordinamento per 11 Selezione: in Java ... for (int i = 0; i<n-1; i++) { min = a[i]; p = i; for (int j = i+1; j<n; j++) if (a[j] < min) { min = a[j]; p = j; } a[p] = a[i]; a[i] = min; } ... Ordinamento per 12 Selezione: Complessità Confronti Sempre (n – 1) * n / 2 ~ O(n2) n – 1 al I passo di scansione n – 2 al II passo di scansione … 1 all’(n – 1)-mo passo di scansione Scambi Al più (n – 1) 1 per ogni passo Ordinamento per 13 Selezione: Considerazioni Ogni ciclo scorre tutta la parte non ordinata Numero fisso di confronti Non trae vantaggio dall’eventuale preordinamento Pochi scambi Ordinamento a Bolle Far “affiorare” ad ogni passo l’elemento più piccolo fra quelli in esame Confronto fra coppie di elementi adiacenti e, se sono fuori ordine, scambio, ripetendo il tutto fino ad ottenere la sequenza ordinata Simile alle bolle di gas in un bicchiere Il passo di esecuzione coincide con: Il numero di elementi già ordinati Elemento a cui fermarsi ad ogni passo 14 Ordinamento a Bolle Se in una passata non viene effettuato nessuno scambio, l’insieme è già ordinato L’algoritmo può già terminare 15 Meno di n – 1 passi Miglioramento: Usare un indicatore di scambi effettuati Impostato a vero all’inizio di ogni passata Impostato a falso non appena si effettua uno scambio Si termina se alla fine di un passo è rimasto inalterato Ordinamento a Bolle 16 Esempio Inizio I/1 I/2 I/3 I/4 II array(1) 4 4 4 4 4 1 array(2) 3 3 3 3 1 4 array(3) 6 6 6 1 3 3 array(4) 1 1 1 6 6 6 array(5) 5 2 2 2 2 2 array(6) 2 5 5 5 5 5 Ordinamento a Bolle 17 Esempio II II/1 II/2 II/3 III array(1) 1 1 1 1 1 array(2) 4 4 4 4 2 array(3) 3 3 3 2 4 array(4) 6 6 2 3 3 array(5) 2 2 6 6 6 array(6) 5 5 5 5 5 Ordinamento a Bolle 18 Esempio III III/1 III/2 IV IV/1 Fine array(1) 1 1 1 1 1 1 array(2) 2 2 2 2 2 2 array(3) 4 4 4 3 3 3 array(4) 3 3 3 4 4 4 array(5) 6 5 5 5 5 5 array(6) 5 6 6 6 6 6 Ordinamento a Bolle 19 Algoritmo passo ← 0 (* passo di esecuzione *) ordinato ← falso fintantoché passo < n e non ordinato esegui passo ← passo + 1 ordinato ← vero i←n fintantoché i > passo esegui se vettore(i) < vettore(i – 1) allora scambia vettore(i) con vettore(i– 1) ordinato ← falso i←i–1 Ordinamento a Bolle 20 Codifica . . . p = 0; ordinato = false; while ((p < n) && !ordinato) { p++; ordinato = true; for (int i = n-1; i >= p; i--) if (a[i] < a[i-1]) { t = a[i]; a[i] = a[i-1]; a[i-1] = t; ordinato = false; } } . . . Ordinamento a Bolle 21 Complessità Caso migliore (lista già ordinata): 1 passo n – 1 confronti, 0 scambi Caso peggiore (ordine opposto): n – 1 passi All’i-mo passo n – i + 1 confronti → in tutto (n – 1) * n / 2 ~ O(n2) Come per Selezione n – i + 1 scambi → in tutto (n – 1) * n / 2 ~ O(n2) Molto maggiore della Selezione Caso medio Scambi pari alla metà dei confronti → O(n2) Ordinamento a Bolle 22 Considerazioni Ogni ciclo scorre tutta la parte non ordinata Prestazioni medie inferiori agli altri metodi Nel caso peggiore, numero di confronti uguale all’ordinamento per selezione, ma numero di scambi molto maggiore Molto veloce per insiemi con alto grado di preordinamento Ordinamento per 23 Inserzione Ricerca la giusta posizione d’ordine di ogni elemento rispetto alla parte già ordinata Inizialmente già ordinato solo il primo elemento Elementi da ordinare considerati uno per volta Necessari n – 1 passi Metodo intuitivo Simile all’ordinamento eseguito sulle carte da gioco Ordinamento per 24 Inserzione Determinare la posizione in cui inserire la chiave nella sequenza ordinata, facendo scalare le altre Scansione sequenziale Soluzione completa costruita inserendo un elemento della parte non ordinata nella parte ordinata, estendendola di un elemento parte ordinata ≤x |x| | x | parte non ordinata >x parte non ordinata → Ordinamento per 25 Inserzione: Note Strategia di scelta del prossimo elemento da inserire nella parte ordinata Primo elemento della parte non ordinata Variante [da Dromey] Inserire subito il più piccolo in prima posizione Evita di dover effettuare appositi controlli sull’indice per evitare che esca fuori dall’array Ordinamento per 26 Inserzione: Esempio Inizio I II III IV V array(1) 40 30 30 10 10 10 array(2) 30 40 40 30 30 20 array(3) 60 60 60 40 40 30 array(4) 10 10 10 60 50 40 array(5) 50 50 50 50 60 50 array(6) 20 20 20 20 20 60 Ordinamento per 27 Inserzione: Algoritmo per ogni elemento dal secondo fino all’ultimo esegui inserito ← falso fintantoché non è stato inserito esegui se è minore del precedente allora fai scalare il precedente se sei arrivato in prima posizione allora piazzalo; inserito ← vero fine-se (necessario per evitare ambiguità) altrimenti piazzalo; inserito ← vero fine-se fine-fintantoché fine-perogni Ordinamento per 28 Inserzione: Codifica Java for (int i=1; i <= n-1; ++i) { x = a[i]; j = i - 1; inserito = false; while (!inserito) if (x < a[j]) { a[j+1] = a[j]; j--; if (j < 0) { a[0] = x; inserito = true; } } else { a[j + 1] = x; inserito = true; } } Ordinamento per 29 Inserzione: Algoritmo Cerca il minimo Prima posizione ← minimo Fintantoché c’è una parte non ordinata Considera il primo elemento di tale parte Confrontalo a ritroso con i precedenti, facendoli via via scalare finché sono maggiori Gli elementi via via incontrati scandendo a ritroso la parte ordinata scalano per far spazio all’elemento da inserire Ordinamento per 30 Inserzione: in Java . . . min = a[0]; p = 0; for (int i=1; i <= n-1; ++i) if (a[i] < min) { min = a[i]; p = i; } a[p] = a[0]; a[0] = min; // a[0] e a[1] ordinati for (int i=2; i <= n-1; ++i) { x = a[i]; j = i; while (x < a[j-1]) { a[j] = a[j-1]; j--; } a[j] = x; } . . . Ordinamento per 31 Inserzione: Complessità Sempre n – 1 passi Uno scambio per ogni confronto, salvo (eventualmente) l’ultimo Caso ottimo (lista già ordinata) n – 1 confronti, 0 scambi Come il metodo a bolle Caso pessimo (ordine opposto) i-mo passo i – 1 confronti e scambi -> (n – 1) * n / 2 ~ O(n2) Caso medio: metà confronti e scambi Ordinamento per 32 Inserzione: Considerazioni Per sequenze con distribuzione casuale Molti confronti Molti scambi Caso migliore come l’ordinamento a bolle Valido per Piccole sequenze (n ≤ 25) Sequenze note a priori essere parzialmente ordinate Ogni ciclo scorre una porzione della parte ordinata Ordinamento 33 Considerazioni Scambio più costoso del confronto Confronto operazione base del processore Scambio composto da tre assegnamenti Un assegnamento richiede due accessi alla memoria Ad ogni passo La porzione ordinata cresce di una unità La porzione disordinata decresce di una unità Ordinamento 34 Tecniche Avanzate Si può dimostrare che, in media, per dati casuali, gli elementi devono essere spostati di una distanza pari a n/3 rispetto alla loro posizione originaria Gli algoritmi semplici tendono a spostare solo gli elementi vicini e quindi necessitano di molti spostamenti in più per ordinare la lista Meno efficienza Algoritmi migliori in grado di scambiare, nelle prime fasi di ordinamento, valori che occupano posizioni molto distanti nella lista Ordinamento di Shell 35 (a diminuzione di incremento) Si può ricavare vantaggio dai confronti iniziali piazzando gli elementi più vicini alla loro posizione finale Muovere, inizialmente, elementi su lunghe distanze Sequenza finale ordinata raggiunta più velocemente Ridurre progressivamente le distanze Raffinamento dell’ordine degli elementi 36 Ordinamento di Shell Scelta della distanza Distanza massima possibile su n elementi: n/2 Possibilità che un elemento resti disaccoppiato Primo con l’(n/2 + 1)-mo Secondo con l’(n/2 + 2)-mo … Scelta della riduzione di distanza Dimezzamento progressivo Numero logaritmico di riduzioni n/2; (n/2)/2 = n/4; n/8; n/16; …; n/2log2n Ordinamento di Shell 37 Esempio n=8 | 20 | 35 | 18 | 8 | 14 | 41 | 3 | 39 | Distanza n/2 = 4 4 catene da 2 elementi | 14 | 35 | 3 | 8 | 20 | 41 | 18 | 39 | Distanza n/4 = 2 2 catene da 4 elementi | 3 | 8 | 14 | 35 | 18 | 39 | 20 | 41 | Distanza n/8 = 1 1 catena da 8 elementi | 3 | 8 | 14 | 18 | 20 | 35 | 39 | 41 | 38 Ordinamento di Shell Scelta di un algoritmo per ordinare le catene Disordine nella lista relativo Algoritmo efficiente per strutture parzialmente ordinate Catene corte Algoritmo efficiente per piccoli insiemi di valori Metodi candidati A bolle Per inserzione Meno scambi 39 Ordinamento di Shell Realizzazione del meccanismo per diminuire le distanze inc ← n fintantoché inc > 1 inc ← inc / 2 Ordina le catene con incremento inc per inserzione 40 Ordinamento di Shell Numero di catene da ordinare per ogni definito incremento Pari all’incremento inc ← n fintantoché inc > 1 inc ← inc / 2 per j che varia da 1 a inc Ordina la j-ma catena con incremento inc per inserzione 41 Ordinamento di Shell Accesso ad ogni singola catena per applicare l’ordinamento Nell’ordinamento per inserzione si inizia considerando il secondo elemento Elementi successivi a distanza inc (invece che 1) Posizione del primo elemento di ogni catena: j Posizione del secondo elemento di ogni catena: j + inc posizione ← posizione precedente + inc Fino a quando la posizione non supera la dimensione dell’array (n) k>n 42 Ordinamento di Shell inc ← n fintantoché inc > 1 inc ← inc / 2 per j che varia da 1 a inc k ← j + inc fintantoché k ≤ n x ← a(k) Trova la posizione corrente per x a(corrente) ← x k ← k + inc 43 Ordinamento di Shell Individuazione della posizione corretta per inserire l’elemento x via via considerato Inizialmente x è in posizione k nella lista Il primo elemento da confrontare con x è nella posizione precedente (distanza inc invece che 1) corrente ← k precedente ← corrente – incremento Scorrere in seguito gli altri elementi nella catena corrente ← precedente precedente ← precedente – incremento fintantoché x < a(precedente) esegui … 44 Ordinamento di Shell Necessità di un metodo per garantire la terminazione Nell’ordinamento per inserzione esisteva una sentinella Minimo in prima posizione Metodo basato solo sugli indici Se precedente è ripetutamente decrementato di inc potrà finire sotto j che segna l’inizio della catena corrente Accertarsi che precedente ≥ j 45 Ordinamento di Shell fintantoché (precedente ≥ j) e (x < a(precedente)) esegui … Problemi con l’implementazione Valutazione della condizione di ciclo non cortocircuitata Non si ferma appena è trovata falsa la prima condizione Necessità di definire un indicatore che ricordi se x < a(precedente) è vero o falso while (precedente ≥ j) and (not inserito) do … Ordinamento di Shell 46 Algoritmo Definire il vettore di n elementi Inizializzare l’incremento inc a n Fintantoché l’incremento è maggiore di 1 Dimezza l’incremento Per ciascuna delle inc catene a intervalli inc da ordinare Individua il secondo elemento della catena Fintantoché non è raggiunta la fine della catena Usa il meccanismo di inserzione per l’elemento corrente Passa all’elemento successivo a distanza inc Ordinamento di Shell 47 Programma Java inc = n; while (inc > 1) { inc = inc / 2; for (int j = 0 ; j < inc; ++j) { k = j + inc; // indice successivo nella catena while (k < n) { inserito = false; x = a[k]; corr = k; prec = corr-inc; while ((prec >= j) && !inserito ) { if (x < a[prec]) { a[corr] = a[prec]; corr = prec; prec = prec-inc; } else inserito = true; } // while ((prec >= j) && !inserito ) a[corr] = x; k = k + inc; } // while (k < n) } // for } // while (inc > 1) 48 Ordinamento di Shell | 20 | 35 | 18 | Risultato finale: Penultimo passo: n=7 3 | | 3 | 8 | 14 | 18 | 20 | 35 | 41 | | 8 | 14 | 18 | 20 | 35 | 41 | 3 | Per arrivarci con Shell: 8 | 14 | 41 | 3 confronti, 2 scambi 1 solo passo: n/2 = 3 catene di lunghezza 2 Al passo successivo: n/4 = 1 catena pari a tutto l’array Per arrivarci per inserzione: 11 confronti, 8 scambi | 20 | 35 | 18 | 8 | 14 | 41 | 3 | → 1 confronto, 0 scambi | 20 | 35 | 18 | 8 | 14 | 41 | 3 | → 2 confronti, 2 scambi | 18 | 20 | 35 | 8 | 14 | 41 | 3 | → 3 confronti, 3 scambi | 8 | 18 | 20 | 35 | 14 | 41 | 3 | → 4 confronti, 3 scambi | 8 | 14 | 18 | 20 | 35 | 41 | 3 | → 1 confronto, 0 scambi Ordinamento di Shell 49 Considerazioni Numero di passi esterni logaritmico L’ultimo passo coincide con il normale ordinamento per inserzione/selezione Numero di volte che si può continuare a dimezzare n Vantaggio del preordinamento risultante superiore alla perdita per le ripetizioni dell’ordinamento Scelte migliori per la sequenza di decrementi 2p–1, …, 31, 15, 7, 3, 1 Numero di confronti e scambi proporzionale a n1.2 50 Partizionamento Dato un array non ordinato di n elementi, e un valore x dello stesso tipo degli elementi, partizionare gli elementi in due sottoinsiemi tali che Gli elementi ≤ x siano in un sottoinsieme Gli elementi > x siano nell’altro sottoinsieme Esempio | 4 | 13 | 28 | 18 | 7 | 36 | 11 | 24 | x = 20 51 Partizionamento Si potrebbe ordinare l’array e trovare l’indice dell’elemento che separa i due sottoinsiemi | 4 | 7 | 11 | 13 | 18 | 24 | 28 | 36 | ≤ 20 > 20 Non richiesto Non necessario Il problema sarebbe risolto anche con la seguente configurazione finale | 4 | 13 | 11 | 18 | 7 | 36 | 28 | 24 | ≤ 20 > 20 52 Partizionamento Nota la posizione finale p che divide i due sottoinsiemi Alcuni elementi non vanno spostati Alcuni elementi sono fuori posto Scorrendo l’array da sinistra verso p, i valori ≤ x Scorrendo l’array da destra verso p, i valori > x Per ognuno da una parte, uno dall’altra Scambio La posizione p può essere ricavata Contare gli elementi ≤ x 53 Partizionamento Occorrono 2 scansioni dell’array Determinazione della posizione p Individuazione e scambio degli elementi fuori posto Si termina quando si arriva a p È possibile scandire l’array una sola volta Mentre le due partizioni non si incontrano Estendere le partizioni sinistra e destra scambiando le coppie piazzate male La posizione p sarà individuata automaticamente al termine 54 Partizionamento Scansione effettuata tramite 2 indici i per la partizione sinistra Parte da 1 Incrementato j per la partizione destra Parte da n Decrementato Partizionamento 55 Algoritmo Individuazione dei primi valori fuori posto Mentre l’i-mo elemento è ≤ x e i < j Mentre il j-mo elemento è > x e i < j Decrementare j Mentre le partizioni non si sono incontrate Incrementare i Scambiare i valori nelle posizioni individuate Cercare i successivi valori fuori posto Al termine, j è la posizione p Limite inferiore per i valori > x nella partizione Partizionamento 56 Note Implementative Gestione del caso in cui x è fuori dall’intervallo dei valori assunti dall’array Inserire un controllo supplementare dopo i cicli preliminari se il j-mo elemento è > x allora decrementa j Ciò assicura che ∀k ∈ [1..j]: a[k] ≤ x Se x è maggiore di tutti gli elementi dell’array, al termine dei cicli si ha i = j = n e il controllo non scatta Se x è minore di tutti gli elementi dell’array, al termine dei cicli si ha i = j = 1 e, dopo il controllo, j = 0 Partizionamento 57 Codice Java int partiziona(Tipo[] a, int p1, int p2, Tipo x) i = p1; j = p2; while ((i < j) && (a[i] <= x)) i++; while ((i < j) && (a[j] > x)) j--; if (a[j] > x) j--; while (i < j) { t = a[i]; a[i] = a[j]; a[j] = t; i++; j--; while (a[i] <= x) i++; while (a[j] > x) j--; } return j; } 58 Fusione Fondere due array, già ordinati secondo il medesimo criterio, in un unico array ordinato secondo lo stesso criterio 2 | 5 | 9 | 13 | 24 3 | 4 | 11 | 15 | 22 2 | 3 | 4 | 5 | 9 | 11 | 13 | 15 | 22 | 24 Numero di elementi dell’array risultante pari alla somma degli elementi dei due array dati Necessità di esaminare tutti gli elementi dei due array dati 59 Fusione I due array possono considerarsi come costituiti da sottosequenze da concatenare opportunamente nell’array finale Suddividere ciascuno dei due array dati in una parte già fusa e una ancora da fondere In ogni istante, esaminare il primo elemento della parte da fondere dei due array Inserire il minore nella prima posizione dell’array fuso Avanzare al prossimo elemento dell’array da cui è stato preso 60 Fusione Uno dei due array finirà per primo Copiare fino ad esaurimento i suoi elementi restanti nell’array finale Individuabile dall’inizio confrontando gli ultimi elementi dei due array Se l’ultimo elemento di un array è minore del primo elemento dell’altro, la fusione si riduce alla copiatura degli elementi dell’uno seguita dalla copiatura degli elementi dell’altro Fusione 61 Algoritmo Mentre non è terminato il numero di elementi da considerare in nessuno degli array: Confronta i loro primi elementi degli array da fondere e metti il più piccolo nell’array finale Aggiorna l’indice dell’array appropriato Se è finito il primo array, allora Copia il resto del secondo nell’array finale Altrimenti Copia il resto del primo nell’array finale Fusione 62 Modularizzazione algoritmo merge algoritmo mergecopy Fonde l’array che finisce prima con l’altro algoritmo shortmerge Decide quale array finisce prima, quindi esegue la fusione di conseguenza Fonde le parti degli array ricadenti nello stesso intervallo algoritmo copy Copia gli elementi di un array dalla posizione attuale fino alla fine Fusione 63 Algoritmo merge Definire gli array a[1..m] e b[1..n] Se l’ultimo elemento di a è ≤ all’ultimo elemento di b allora Altrimenti Fondi tutto a con b Copia il resto di b Fondi tutto b con a Copia il resto di a Dà il risultato c[1..n+m] Fusione 64 Algoritmo mergecopy Definire gli array a[1..m] e b[1..n] con a[m] ≤ b[n] Se l’ultimo elemento di a risulta ≤ rispetto al primo elemento di b allora Copia tutto a nei primi m elementi di c Copia tutto b in c, partendo dalla posizione m+1 Altrimenti Fondi tutto a con b in c Copia il resto di b in c partendo dalla posizione in cui è finita la fusione Fusione 65 Algoritmo shortmerge Definire gli array a[1..m] e b[1..n] con a[m] ≤ b[n] Mentre tutto a non è ancora fuso esegui Se il corrente a ≤ b allora Altrimenti Copia il corrente a nella corrente posizione di c Avanza l’indice di a Copia il corrente b nella corrente posizione di c Avanza l’indice di b Avanza l’indice di c Fusione 66 Algoritmo copy Definire gli array b[1..n] e c[1..n+m] e definire dove cominciare in b (al corrente j) e in c (al corrente k) Mentre non è ancora finito b esegui Copia l’elemento dalla corrente posizione in b nella corrente posizione in c Avanza l’indice j di b Avanza l’indice k di c 67 Ordinamento per Fusione Algoritmo di MergeSort L’idea alla base è che l’ordinamento di una lista di n elementi può essere ottenuto Dividendo la lista in due sequenze di n/2 elementi ciascuna Dimensione inferiore (dimezzata) Ordinando singolarmente ogni sequenza Problema di ordine inferiore Risolubile secondo la stessa tecnica Procedura ricorsiva Fondendo le due metà ordinate in un’unica sequenza Lista risultante ordinata Ordinamento per Fusione 68 Esempio Insieme iniziale | 33 | 21 | 7 | 48 | 28 | 13 | 65 | 17 | Suddivisione in 2 sottoinsiemi | 33 | 21 | 7 | 48 | | 28 | 13 | 65 | 17 | Ordinamento di ogni singolo sottoinsieme | 7 | 21 | 33 | 48 | | 13 | 17 | 28 | 65 | Combinazione (fusione) dei sottoinsiemi ordinati | 7 | 13 | 17 | 21 | 28 | 33 | 48 | 65 | Ordinamento per Fusione 69 Esempio 85 24 63 45 17 31 96 50 decomposizioni 85 24 63 45 ordinamenti (semplici) fusioni 17 31 96 50 85 24 63 45 17 31 96 50 24 85 45 63 17 31 50 96 24 45 63 85 17 24 31 17 31 50 96 45 50 63 85 96 70 Ordinamento per Fusione void ordinamentoPerFusione(int[] a) { n = a.length; int[] b = new int[n] = {. . .}; mergesort(a, 0, n-1) } void mergeSort(int[] a, int p1, int p2){ if (p1 < p2) { int q = (p1 + p2) / 2; mergeSort(a, p1, q); mergeSort(a, q+1, p2); merge(a, p1, q, p2) } } Ordinamento per Fusione 71 Merge La procedura di fusione deve lavorare su segmenti diversi di uno stesso vettore Non può essere la stessa definita per la fusione di due vettori distinti Occorre definire una nuova procedura che fonda segmenti adiacenti di uno stesso vettore Ordinamento per Fusione 72 merge() public static void merge (int[] a, int from, int mid, int to) { int n = to - from + 1; int[] b = new int[n]; int i = from; int k = mid + 1; int j = 0; while (i<=mid && k<=to) { if (a[i] < a[k]) { b[j] = a[i]; i++;} else { b[j] = a[k]; k++;} j++; } while (i <= mid) { b[j] = a[i]; i++; j++; } while (k <= to) { b[j] = a[k]; k++; j++; } for (j = 0; j < n; j++) a[from + j] = b[j]; } Ordinamento per Fusione 73 Considerazioni un certo numero di livelli di decomposizioni e fusioni in ciascun livello vengono fusi tutti gli elementi il costo asintotico di ciascun livello di fusioni è N il numero di livelli è log2 N TmergeSort(N) = N log2 N 74 Quicksort [Hoare] Detto anche ordinamento per partizionamento-scambio Basato sull’idea che nei primi passi di ordinamento la distanza fra i dati da muovere deve essere grande Analogamente all’ordinamento di Shell 2 versioni Iterativa Ricorsiva 75 Quicksort & Shellsort L’ordinamento di Shell confronta al primo passo elementi a distanza n/2 Equità nelle distanze di confronto fra i vari elementi Distanza massima riferita al singolo elemento Primo con l’ultimo Secondo col penultimo … Quicksort 76 Partizionamento Poco beneficio Alcuni confronti non generano scambi Al più n/2 scambi Ogni scambio sistema al più una coppia Andrebbe bene se l’array avesse un ordine inverso Meglio un metodo di partizionamento elementi più piccoli | elementi più grandi Aumenta il preordinamento Riduce la dimensione del problema Ogni partizione può essere affrontata separatamente Quicksort 77 Scelta Valore Centrale Il partizionamento richiede un valore in base al quale distinguere gli elementi Scelto a caso Potrebbe essere esterno ai valori della lista Prenderne uno della lista stessa Indifferente quale Elemento mediano Elemento in posizione media Posizione media Limite inferiore + limite superiore DIV 2 Quicksort 78 Dopo il Partizionamento Situazione: Parte sinistra Parte destra Elementi minori dell’elemento mediano Elementi maggiori dell’elemento mediano Escluse interazioni fra partizioni Nessun elemento della parte sinistra potrà mai, nell’ordinamento, finire nella parte destra Ciascuna può essere trattata (ordinata) separatamente Ciascuna contiene meno elementi della lista completa Quicksort 79 Ciclo Per ciascuna delle 2 partizioni ottenute valgono le stesse considerazioni precedenti Riapplichiamo su ciascuna i concetti già esposti Problemi analoghi a quello iniziale (ordinamento) Dimensione minore Si ottengono, per ciascuna, altre 2 partizioni Situazione dopo un certo numero di passi: elementi I partizione < elementi II partizione < … < elementi n-ma partizione Criterio di stop: partizioni di un solo elemento Non ulteriormente partizionabili Già ordinate Quicksort 80 Algoritmo di Base Mentre tutte le partizioni non risultano ancora ridotte a dimensione 1 esegui Scegli la prossima partizione da elaborare Individua un nuovo valore di partizionamento per la partizione scelta (il mediano) Partiziona la partizione attuale in due insiemi più piccoli parzialmente ordinati rispetto al mediano attuale Quicksort 81 Iterativo Ogni passo genera 2 nuove partizioni Si può agire su una sola partizione alla volta Una si elabora L’elaborazione dell’altra resta in sospeso Possibile esistenza, in un certo momento, di partizioni create in attesa di elaborazione Necessità di ricordarne i limiti destro e sinistro Dove iniziano Dove finiscono Salvare l’informazione sui limiti di ciascuna Riferita alle posizioni (indice) dell’array originale Quicksort 82 Ricordare i Compiti Futuri Memorizzazione dei limiti delle partizioni lasciate in sospeso Ogni partizione richiede la memorizzazione di 2 valori che la individuano (gli estremi) Immagazzinati in un array di appoggio Relativi agli indici dell’array di partenza 2 elementi dell’array di appoggio per ogni partizione Scelta della prossima Simulazione di una struttura a pila Last In, First Out Quicksort 83 Fabbisogno di Memoria Man mano che il partizionamento prosegue Il numero delle partizioni di cui salvare l’informazione sui limiti aumenta (di 1) Le partizioni diventano sempre più piccole Dimensione dell’array di servizio Caso peggiore: 2(n–1) elementi Si memorizzano n – 1 partizioni lasciate in sospeso Ogni volta si elabora la partizione più grande Ogni volta la partizione più piccola contiene un solo elemento | partizione da elaborare per prima | x | Ciascuna richiede 2 elementi Troppo Spazio! Quicksort 84 Ottimizzazione Elaborare per prima la partizione più piccola Minor numero di partizioni da tenere in sospeso Partizioni di dimensione 1 non generano altre partizioni Individuate e scartate subito dall’elaborazione In ogni istante esiste una sola partizione in sospeso di cui vanno salvati i limiti (la più grande) Quicksort 85 Ottimizzazione/2 Ad ogni passo si elabora la più grande delle partizioni più piccole Nel precedente caso peggiore: 2 elementi Nuovo caso peggiore: 2 log2 n Partizioni divise sempre a metà Dimensione dimezzata Accettabile Quicksort 86 Salvataggio Limiti con Pila Test per decidere qual è la partizione più grande Se le partizioni cadono a sinistra del medio Altrimenti Elabora la partizione sinistra (che è la più grande) Salva i limiti di quella destra Elabora la partizione destra (che è la più grande) Salva i limiti di quella sinistra La partizione più grande è sempre messa in attesa di elaborazione Gestione a pila Quicksort 87 Partizionamento e Accodamento 1 a 2 a | e f | cd | c 3 b a..c < d..b f..c < a..e | || f ghc f..g < h..c Top dello stack Vettori di servizio d .. b 1 a .. e d .. b 2 h a d 3 .. .. .. c e b Quicksort 88 Fine Ciclo Quando si raggiunge una partizione di dimensione 1 si ricomincia il processo di partizionamento sulla partizione più recentemente memorizzata Limiti della partizione rimossi dalla cima della pila Quando tutte le partizioni sono state ridotte a dimensione 1 non ci saranno più limiti nella pila Pila vuota Terminazione del processo di partizionamento Quicksort 89 Algoritmo Acquisire l’array a[1..n] da ordinare Inizializzare il puntatore al top dello stack al valore 2 Impostare i limiti sup e inf dell’array nello stack a 1..n (si inizia con una sola partizione coincidente con l’intero array) Mentre lo stack non è vuoto esegui Estrai dal top dello stack la prossima partizione (limiti sup e inf del segmento di array corrispondente) Mentre il corrente segmento non è ridotto a dimensione 1 esegui Scegli l’elemento mediano del segmento di array Partiziona il segmento in 2 rispetto al suo valore mediano Salva nello stack i limiti della partizione più grande Segmento corrente ← partizione più piccola Quicksort 90 Codice Java top = 0; stack[top++] = 0; stack[top] = n - 1; while (top > 0) { right = stack[top--]; left = stack[top--]; while (left < right) { medio = (left + right) / 2; mediano = a[medio]; pos = partiziona(a,left,right,mediano); if (pos < medio) { // salva dx e lavora su sx stack[++top] = pos+1; stack[++top] = right; right = pos; // nuovo inizio sx } else { // salva sx e lavora sulla dx stack[++top] = left; stack[++top] = pos; left = pos+1; // nuovo inizio dx } } // while (left < right) } Quicksort 91 Versione Ricorsiva Algoritmo per sua natura ricorsivo Implementazione ricorsiva molto semplice: Strategia basata sull’applicazione dello stesso processo a problemi via via più piccoli Divide l’insieme di dati in 2 partizioni in modo tale che tutti gli elementi della partizione sinistra (prime pos posizioni) siano ≤ a tutti gli elementi della parte destra (posizioni da pos+1 fino ad n) Riapplica lo stesso meccanismo di partizionamento ad entrambe le partizioni (che sono di dimensioni via via più piccole) a[1..pos] e a[pos+1..n] fino ad ottenere segmenti di dimensione 1 A questo punto l’array sarà completamente ordinato Quicksort 92 Ricorsivo Situazione iniziale: dati originali inf = 1 sup = n Dopo il primo partizionamento: partizione sinistra inf | pos partizione destra sup Passi fondamentali dell’algoritmo ricorsivo Se nell’insieme corrente c’è più di 1 elemento Partiziona i dati in partizione sinistra e destra Ripeti il processo di partizionamento per la partizione sinistra Ripeti il processo di partizionamento per la partizione destra Quicksort 93 Ricorsivo/2 Indicatori di indice necessari: inf | pos sup partition(a, inf, sup, mediano, pos) Ricorsione quicksort2(a, inf, pos) quicksort2(a, pos+1, sup) Terminazione della ricorsione Chiamata ad un segmento di 1 unico elemento Test: se inf < sup allora continua Quicksort 94 Algoritmo Ricorsivo Se il segmento corrente da ordinare contiene più di 1 elemento allora Partizionalo in 2 segmenti più piccoli tali che tutti gli elementi del segmento di sinistra siano ≤ di tutti gli elementi del segmento di destra Se il segmento di sinistra è più piccolo del segmento di destra allora Quicksort il segmento di sinistra Quicksort il segmento di destra Altrimenti Quicksort il segmento di destra Quicksort il segmento di sinistra Quicksort 95 Metodo Java void quicksort2(double[] a, int inf, int sup) { int medio = -1; double mediano; if (inf < sup) { medio = (inf + sup) / 2; mediano = a[medio]; partition(a, inf, sup, mediano, medio); if ((medio – inf) < (sup – medio-1)) { quicksort2(a, inf, medio); quicksort2(a, medio+1, sup); } else { quicksort2(a, medio+1, sup); quicksort2(a, inf, medio); } } // if (inf < sup) } Quicksort 96 Considerazioni Suddivisione successiva delle partizioni Struttura risultante ad albero binario Caso peggiore: suddivisione a metà Profondità dell’albero: log2 n Numero di volte che si può spaccare successivamente a metà una lista di n elementi Ciascun livello dell’albero contiene n elementi Complessità: n log2 n Algoritmo ottimo!