Fondamenti d’Informatica 1 A.A. 2016/2017 Corsi di Laurea in Ing. Civile e Ing. per l’Ambiente e il Territorio Docente: Giorgio Fumera Università degli Studi di Cagliari Formulazione di algoritmi Sommario I Approccio alla scrittura di programmi I Esempi di formulazione di algoritmi I Proprietà degli algoritmi: correttezza ed efficienza Algoritmi notevoli: ricerca e ordinamento I – – – – – ricerca sequenziale ricerca binaria su sequenze ordinate ordinamento per selezione (selection sort) ordinamento “a bolle” (bubble sort) ordinamento per inserimento (insertion sort) Algoritmi e programmi Algoritmo: descrizione del procedimento per l’esecuzione di una data operazione, espressa in modo non ambiguo, in un linguaggio comprensibile da un dato esecutore, in termini di una sequenza finita di azioni, ciascuna delle quali sia eseguibile dall’esecutore. Programma: rappresentazione di un algoritmo in un linguaggio (di programmazione) comprensibile da un calcolatore (che ne sarà l’esecutore). Approccio alla scrittura di programmi 1. Comprendere il problema da risolvere: quali sono i dati da elaborare e il risultato desiderato? 2. Definire un procedimento risolutivo (algoritmo), espresso inizialmente anche in modo informale, per esempio in linguaggio naturale, ma senza ambiguità 3. Riformulare l’algoritmo in termini delle operazioni esprimibili nel linguaggio di programmazione scelto 4. Tradurre l’algoritmo in un programma codificato in tale linguaggio Formulazione di algoritmi Quando si formula un algoritmo, o se ne verifica la correttezza, o si vuole comprenderne il funzionamento, è sempre utile provare ad eseguirlo (a mente, o con carta e matita) per alcuni possibili valori dei dati di ingresso. Di seguito si presentano alcuni esempi di formulazione di algoritmi (passo 2 dell’approccio descritto sopra). Se l’esecutore di riferimento è un calcolatore, nella formulazione dell’algoritmo si deve prestare attenzione ai risultati intermedi di cui si dovrà “tener traccia” durante l’elaborazione. Tali risultati dovranno infatti essere memorizzati in opportune celle di memoria. Formulazione di algoritmi: esempi 1. Calcolare la somma di una sequenza di numeri 2. Calcolare il massimo di una sequenza di numeri 3. Calcolare il massimo comun divisore (MCD) di due numeri naturali 4. Calcolare il minimo comune multiplo (MCM) di due numeri naturali Formulazione di algoritmi: problemi e istanze Si noti che negli esempi precedenti si richiede non la risoluzione di una specifica istanza di un problema (per esempio, sommare i numeri 5, −7, 4, 9, −2), ma la definizione di un procedimento in grado di risolvere tutte le possibili istanze di tale problema (per esempio, sommare una qualsiasi sequenza di numeri). Più precisamente, per istanza s’intende uno specifico valore dei dati d’ingresso di un problema (per esempio, la sequenza 5, −7, 4, 9, −2, nel caso della somma di una sequenza di numeri). Questa è una caratteristica generale degli algoritmi: un algoritmo deve essere in grado di risolvere tutte le possibili istanze (o un dato sottoinsieme delle istanze) di un problema. Formulazione di algoritmi: livello di dettaglio Negli esempi che seguono gli algoritmi saranno descritti a un livello di dettaglio più alto (cioè, in termini di operazioni molto elementari) rispetto a quanto normalmente si farebbe per descriverli a beneficio di un esecutore umano. Questo è dovuto al fatto che i linguaggi di programmazione consentono di esprimere solo operazioni molto elementari, come si vedrà più avanti. 1. Somma di una sequenza di numeri Dati da elaborare: una sequenza di numeri. Risultato desiderato: la somma di tali numeri. Un possibile algoritmo, espresso in modo informale: sommare il secondo numero al primo, poi sommare il terzo al risultato precedente, e così via fino all’ultimo numero, tenendo traccia di volta in volta solo dell’ultima somma parziale calcolata. Notare che i numeri possono essere scanditi in un qualsiasi ordine (l’operazione di somma è commutativa); dal punto di vista della descrizione e dell’esecuzione dell’algoritmo è però conveniente considerare lo stesso ordine nel quale i numeri si trovano nella sequenza. 1. Somma di un insieme di numeri: esempio Per comprendere meglio l’algoritmo appena descritto, si consideri la sua applicazione a una specifica istanza: la sequenza 5, −7, 4, 9, −2. Somma iniziale: 0 I primo numero: 5 nuova somma parziale: 0 + 5 = 5 I secondo numero: −7 nuova somma parziale: 5 + (−7) = −2 I terzo numero: 4 nuova somma parziale: −2 + 4 = 2 I quarto numero: 9 nuova somma parziale: 2 + 9 = 11 I quinto numero: −2 nuova somma parziale: 11 + (−2) = 9 Risultato: 9. 2. Massimo di una sequenza di numeri Dati da elaborare: una sequenza di numeri. Risultato desiderato: il più grande di tali numeri. Un possibile algoritmo: I assumere inizialmente che il numero maggiore sia il primo della sequenza I scorrere quindi la sequenza dal secondo all’ultimo elemento, confrontare ciascun elemento con il valore più grande osservato fino a quel momento, e aggiornare quest’ultimo se risultasse inferiore all’elemento in esame Notare che durante l’esecuzione dell’algoritmo l’esecutore dovrà tener traccia solo del valore più grande tra quelli già osservati. 2. Massimo di una sequenza di numeri Esempio: calcolare il massimo tra i valori 5, −7, 4, 9, −2. I si assume che il valore maggiore sia 5 (il primo numero della sequenza) I secondo numero: −7 il valore maggiore tra 5 e −7 è 5 I terzo numero: 4 il valore maggiore tra 5 e 4 è 5 I quarto numero: 9 il valore maggiore tra 5 e 9 è 9 I quinto numero: −2 il valore maggiore tra 9 e −2 è 9 Risultato: 9. 3. Massimo comun divisore Dati da elaborare: due numeri naturali m e n. Risultato desiderato: il più grande numero naturale che sia divisore di entrambi, indicato con MCD(m, n). Un possibile algoritmo, espresso a un livello di dettaglio adeguato per un esecutore umano: 1. scomporre m e n nei loro fattori primi 2. moltiplicare tra loro i fattori comuni, ciascuno con il minimo esponente con cui compare nei due numeri Se l’esecutore non fosse in grado di eseguire alcune delle operazioni indicate, come accade in particolare per i calcolatori (per esempio, la scomposizione in fattori primi di un numero), sarebbe necessario descrivere anche il procedimento per la loro esecuzione in termini di operazioni più elementari che l’esecutore sia in grado di eseguire (per esempio, somme, sottrazioni, confronti tra numeri, ecc.). 3. Massimo comun divisore Il MCD può anche essere anche ottenuto con un procedimento più semplice da descrivere, e composto solo da operazioni molto elementari, tenendo conto che: I MCD(m, n) ∈ {1, . . . , min{m, n}} I per definizione, MCD(m, n) è il più grande divisore comune di m e n nell’insieme indicato sopra L’algoritmo è allora il seguente: scandire i valori 1, 2, . . ., min{m, n} dal più grande al più piccolo, e in ogni passo verificare se il valore in esame sia un divisore sia di m che di n: in caso affermativo si è trovato il MCD, altrimenti si procede analizzando il valore successivo. 3. Massimo comun divisore Esempio: calcolare il MCD tra 27 e 18. MCD(27, 18) ∈ {1, . . . , 18}, quindi si considerano i valori 18, 17, 16, . . . : I 18 è un divisore sia di 27 che di 18? no I 17 è un divisore sia di 27 che di 18? no I 16 è un divisore sia di 27 che di 18? no I ... I 10 è un divisore sia di 27 che di 18? no I 9 è un divisore sia di 27 che di 18? SÌ Risultato: MCD(27, 18) = 9. 3. Massimo comun divisore Un altro possibile algoritmo si può ricavare da un teorema dimostrato dal matematico greco Euclide (c. 300 a.C.). Dati due numeri naturali m e n, con m 6= n: MCD(m, n) = MCD(min{m, n}, max{m, n} − min{m, n}) Dimostrazione. Se m > n (la dimostrazione per m < n è analoga): I se k è un divisore comune di m e n, allora è anche divisore di m − n; infatti, per ipotesi esistono due numeri naturali a e b tali che m = k × a e n = k × b, e quindi m − n = k × (a − b) I se k è un divisore comune di m − n e n, allora è anche divisore di m; infatti, per ipotesi esistono due numeri naturali c e d tali che m − n = k × c e n = k × d, e quindi m = k × (c − d) I questo implica che i divisori comuni di (m, n) coincidono con quelli di (m − n, n), e quindi anche il loro MCD sarà identico 3. Massimo comun divisore Applicando ripetutamente il teorema di Euclide a partire dai valori di interesse, m e n, è facile convincersi che dopo un numero finito di passi si otterranno due valori identici, il cui MCD (corrispondente per definizione agli stessi valori) sarà anche uguale al MCD tra m e n. Questo consente di formulare il seguente algoritmo (noto come algoritmo di Euclide): dati due numeri naturali m e n, calcolare ripetutamente una nuova coppia di numeri ottenuti dalla coppia precedente considerando il numero più piccolo e la differenza tra il maggiore e il minore, fino a ottenere due valori identici, che coincideranno con MCD(m, n). 3. Massimo comun divisore Esempio: calcolo di MCD(36, 28) con l’algoritmo di Euclide: MCD(36, 28) I = MCD (28, 8) I = MCD (20, 8) I = MCD (12, 8) I = MCD (8, 4) I = MCD (4, 4) = 4 4. Minimo comune multiplo Dati da elaborare: due numeri naturali m e n. Risultato desiderato: il più piccolo numero naturale che sia multiplo di entrambi, indicato con MCM(m, n). Anche in questo caso si possono formulare almeno due possibili algoritmi: I scomporre m e n nei loro fattori primi, e moltiplicare tra loro tutti i fattori ottenuti, ciascuno elevato al massimo esponente con cui compare nei due numeri I poiché MCM(m, n) ∈ {max{m, n}, . . . , m × n}, scandire tali valori dal più piccolo al più grande e in ogni passo verificare se il valore in esame sia un multiplo sia di m che di n: in caso affermativo si è trovato il MCM, altrimenti si procede analizzando il valore successivo Osservazioni Alcune osservazioni generali sulla formulazione di algoritmi, che possono essere ricavate dagli esempi precedenti: I possono esistere diversi algoritmi in grado di risolvere uno stesso problema I algoritmi diversi per risolvere uno stesso problema possono essere più o meno difficili da formulare e da descrivere (si veda il caso del MCD), e possono richiedere l’esecuzione di un numero diverso di operazioni I in alcuni casi problemi diversi possono essere risolti con algoritmi simili (si veda il caso degli algoritmi per la somma e il calcolo del massimo di una sequenza di numeri, oppure per il calcolo del MCD e del MCM) (cont.) Osservazioni (cont.) I I ogni algoritmo può essere espresso a diversi livelli di dettaglio, ovvero in termini di operazioni più o meno elementari; il livello di dettaglio adeguato corrisponde alle operazioni che l’esecutore è in grado di eseguire nella maggior parte degli algoritmi (compresi gli esempi visti in precedenza) ricorrono due schemi esecutivi, che per questo motivo corrispondono a istruzioni presenti in tutti i linguaggi di programmazione: – scelta tra più alternative: se si verifica una certa condizione, allora si esegua una certa sequenza di operazioni, altrimenti se ne esegua un’altra (istruzione condizionale) – ripetizione di una sequenza di operazioni per un certo numero di volte, o su tutti gli elementi di un certo insieme, o finché una certa condizione è vera (istruzione iterativa) Proprietà degli algoritmi: correttezza Un algoritmo è corretto se produce il risultato desiderato per ogni possibile istanza del problema. Il metodo più “semplice” per verificare la correttezza di un dato algoritmo consiste nell’eseguirlo per tutte le istanze del problema I in pratica questo è impossibile, se il numero d’istanze è molto grande (in teoria può anche essere infinito) I inoltre, un algoritmo non corretto potrebbe non terminare mai per qualche istanza del problema Di norma, la correttezza deve quindi essere dimostrata con procedimenti analoghi alle dimostrazioni dei teoremi matematici. La non correttezza può invece essere dimostrata individuando anche un singolo controesempio, cioè un’istanza per la quale l’algoritmo non produce il risultato desiderato. Proprietà degli algoritmi: efficienza Un algoritmo A è più efficiente di un algoritmo B, se risolve lo stesso problema usando meno risorse. Questa proprietà è anche indicata come complessità computazionale. Risorse necessarie a un calcolatore per l’esecuzione di un algoritmo: I quantità di memoria (complessità spaziale) I tempo d’esecuzione (complessità temporale) Il tempo d’esecuzione dipende però dalle caratteristiche dell’esecutore (calcolatore). Per valutare la complessità temporale indipendentemente dal calcolatore, si considera il numero di operazioni richieste dall’algoritmo. La valutazione dell’efficienza viene di norma svolta nel caso peggiore, cioè considerando l’istanza che richiede la maggior quantità di memoria o il numero maggiore di operazioni. Algoritmi di ricerca e ordinamento Si considerino i seguenti problemi: I determinare se un certo oggetto è presente in una data sequenza di oggetti (ricerca) I determinare se un certo oggetto è presente in una data sequenza, i cui elementi siano ordinati secondo un certo criterio (ricerca in sequenze ordinate) I ordinare gli elementi di una data sequenza, secondo un certo criterio (ordinamento) Per semplicità, di seguito si considerano sequenze di numeri. Gli algoritmi presentati sono però applicabili a dati di qualsiasi natura. Algoritmo di ricerca sequenziale Problema: determinare se un certo numero è presente in una data sequenza di numeri. Dati da elaborare: una sequenza di numeri, e il numero da cercare. Risultato desiderato: una risposta affermativa (“sì”) o negativa (“no”). Un possibile algoritmo: scandire la sequenza dal primo all’ultimo elemento, e confrontare in ogni passo l’elemento in esame con il valore cercato; se i due valori fossero identici, interrompere la scansione della sequenza e rispondere “sì”, concludendo il procedimento; se si è terminata la scansione della sequenza (e quindi nessuno dei suoi elementi corrisponde al valore cercato), rispondere “no”. Esempio Il numero 31 è presente nella sequenza (52, −48, −34, 75, −80)? I 52 è diverso da 31 I −48 è diverso da 31 I −34 è diverso da 31 I 75 è diverso da 31 I −80 è diverso da 31, e non ci sono altri elementi Risultato: no (31 non è presente nella sequenza considerata). Esempio Il numero −34 è presente nella sequenza (52, −48, −34, 75, −80)? I 52 è diverso a −34 I −48 è diverso da −34 I −34 è uguale a −34: si trascurano gli elementi successivi della sequenza, e il risultato è sì (−34 è presente nella sequenza considerata) Algoritmo di ricerca sequenziale: efficienza L’algoritmo di ricerca sequenziale può essere applicato a qualsiasi sequenza, indipendentemente dall’ordine degli elementi. La sua efficienza in termini del numero di operazioni richieste (complessità temporale) può essere facilmente valutata, nel caso peggiore, tenendo conto che: I l’algoritmo consiste in una serie di confronti tra coppie di elementi (l’oggetto da cercare e un elemento della sequenza) I il caso peggiore corrisponde all’istanza che richiede il maggior numero di confronti, a parità della lunghezza della sequenza È facile convincersi che tra tutte le istanze in cui la sequenza ha una data lunghezza N, il caso peggiore è quello in cui l’oggetto cercato si trova nell’ultima posizione della sequenza, oppure non è presente in essa: in questi casi il numero di confronti è pari a N. Algoritmo di ricerca binaria Un algoritmo più efficiente può essere formulato per il caso particolare in cui la sequenza sia ordinata secondo un certo criterio, usando un approccio analogo a quello che si segue nella ricerca di un nome in un elenco telefonico. Informalmente: si apre l’elenco in corrispondenza delle due pagine centrali (approsimativamente); se il nome cercato si trova in tali pagine, la ricerca termina; altrimenti: I se il nome cercato precede (in ordine alfabetico) i nomi nelle pagine considerate, la ricerca prosegue nelle pagine precedenti, nello stesso modo (aprendo l’elenco più o meno a metà di tali pagine, ecc.) I se il nome cercato segue quelli nelle pagine considerate, la ricerca procede nelle pagine successive Algoritmo di ricerca binaria In modo più formale: si confronta l’elemento da cercare (indicato con x ) con quello al centro della sequenza (indicato con c); se x è uguale a c la ricerca termina (la risposta è “sì”); in caso contrario: I se x precede c, la ricerca procede nello stesso modo tra gli elementi che precedono c I se x segue c, la ricerca procede nello stesso modo tra gli elementi che seguono c L’algoritmo termina: I non appena si trova il valore cercato: il risultato è “sì” I quando la sottosequenza da considerare non contiene nessun elemento: il risultato è “no” Nota: se il numero N di elementi della sequenza (o sottosequenza) considerata è pari, si considera come elemento centrale quello in posizione N2 o N2 + 1. Esempio Determinare se il numero 31 sia presente nella sequenza: 1, 6, 8, 10, 18, 25, 32, 34, 37, 52, 60. Di seguito si mostra la sequenza considerata in ogni passo dell’algoritmo, e, in rosso, l’elemento centrale: I primo passo: 1, 6, 8, 10, 18, 25, 32, 34, 37, 52, 60 25 < 31: si prosegue con la sottosequenza a destra I secondo passo: 32, 34, 37, 52, 60 37 > 31: si prosegue con la sottosequenza a sinistra I terzo passo: 32, 34 32 > 31: si prosegue con la sottosequenza a sinistra I quarto passo: la sottosequenza è vuota Risultato: no (31 non è presente nella sequenza considerata). Esempio Determinare se il numero 8 sia presente nella sequenza: 1, 6, 8, 10, 18, 25, 32, 34, 37, 52, 60 I primo passo: 1, 6, 8, 10, 18, 25, 32, 34, 37, 52, 60 25 > 8: si prosegue con la sottosequenza a sinistra I secondo passo: 1, 6, 8, 10, 18 8 = 8: l’algoritmo termina e il risultato è sì (8 è presente nella sequenza considerata) Algoritmo di ricerca binaria: efficienza Il numero maggiore di confronti (caso peggiore) si ha quando l’oggetto da cercare non è presente nella sequenza considerata, oppure quando si trova nell’ultima sottosequenza considerata, e questa risulta composta da un solo elemento. Il corrispondente numero di confronti può essere approssimato tenendo conto che: I dopo ogni confronto la lunghezza della successiva sottosequenza si riduce (circa) della metà I l’ultima sottosequenza sarà sempre composta da un solo elemento Algoritmo di ricerca binaria: efficienza Per una sequenza iniziale di N elementi, il numero di confronti nel caso peggiore (indicato con k), può essere approssimato come segue (assumendo per semplicità che anche una sequenza di lunghezza dispari si riduca esattamente della metà): numero di confronti 1 2 3 ... k lunghezza della sequenza N = N/20 N/2 = N/21 N/4 = N/22 ... ... 1 = N/2k−1 Da cui si ottiene facilmente (trascurando il fatto che k deve essere un intero): k = log2 N + 1 . Confronto tra ricerca sequenziale e binaria Il numero di confronti richiesto nel caso peggiore, per sequenze ordinate di N elementi, è pari a I N, per la ricerca sequenziale I ≈ log2 N + 1, per la ricerca binaria Poiché N ≥ log2 N + 1 per qualsiasi valore intero di N, la ricerca binaria è più efficiente di quella sequenziale. Il vantaggio diventa evidente al crescere di N, come mostra il seguente esempio (nel quale si approssima log2 N + 1 a un intero per troncamento): N 10 100 1.000 1.000.000 109 ... numero di confronti (caso peggiore) ricerca sequenziale: N ricerca binaria: log2 N 10 100 1.000 1.000.000 109 ... +1 4 7 10 20 30 ... Algoritmi di ordinamento Problema: ordinare secondo un certo criterio una data sequenza di oggetti. Dati di partenza: una sequenza di oggetti, e un criterio di ordinamento. Risultato desiderato: gli stessi elementi della sequenza iniziale, ordinati secondo il criterio richiesto. Esistono diversi algoritmi di ordinamento che si differenziano per la loro efficienza. Di seguito si descrivono tre algoritmi (i più semplici da formulare, ma anche i meno efficienti): I ordinamento per selezione (selection sort) I ordinamento “a bolle” (bubble sort) I ordinamento per inserimento (insertion sort) Ordinamento per selezione Idea di base: disporre l’elemento più piccolo nella prima posizione della sequenza, quello immediatamente successivo nella seconda posizione, e così via. L’algoritmo di ordinamento per selezione ottiene questo risultato scandendo le posizioni della sequenza dalla prima alla penultima; per ognuna di tali posizioni: 1. cercare la posizione dell’elemento più piccolo a partire da quello nella posizione considerata (si può usare un algoritmo analogo a quello per la ricerca del massimo) 2. se la posizione di tale elemento non coincide con la posizione considerata, si scambiano gli elementi in tali posizioni Una variante dell’algoritmo consiste nel disporre nell’ultima posizione l’elemento più grande, nella penultima posizione quello immediatamente precedente, e così via. Esempio Ordinare in senso crescente i seguenti numeri, con l’algoritmo di ordinamento per selezione: 52, −34, −48, 58, 75, −80, 22. In rosso si mostra la posizione considerata in ogni passo dell’algoritmo, in grassetto l’elemento più piccolo da tale posizione in avanti; se i due elementi coincidono, non si esegue nessuno scambio. I 52, −34, −48, 58, 75, −80, 22: si scambiano 52 e −80 I −80, −34, −48, 58, 75, 52, 22: si scambiano −34 e −48 I −80, −48, −34, 58, 75, 52, 22: nessuno scambio I −80, −48, −34, 58, 75, 52, 22: si scambiano 58 e 22 I −80, −48, −34, 22, 75, 52, 58: si scambiano 75 e 52 I −80, −48, −34, 22, 52, 75, 58: si scambiano 75 e 58 I −80, −48, −34, 22, 52, 58, 75 Risultato: −80, −48, −34, 22, 52, 58, 75. Ordinamento “a bolle” L’idea di base è la stessa dell’ordinamento per selezione: disporre in prima posizione l’elemento più piccolo, in seconda posizione quello immediatamente successivo, e così via (oppure: disporre nell’ultima posizione l’elemento più grande, nella penultima posizione quello immediatamente precedente, e così via). L’algoritmo di ordinamento “a bolle” ottiene questo risultato non ricercando in ogni passo il corrispondente elemento più piccolo, ma attraverso un’opportuna serie di scambi tra coppie di elementi. Ordinamento “a bolle” Detta N la lunghezza della sequenza, si considerano in successione: I la sequenza iniziale I la sottosequenza nelle ultime N − 1 posizioni I la sottosequenza nelle ultime N − 2 posizioni I ... I la sottosequenza nelle ultime due posizioni Per ciascuna sottosequenza, detta M la sua lunghezza, l’elemento più piccolo viene portato in prima posizione confrontando in successione: I gli elementi in posizione M − 1 e M I gli elementi in posizione M − 2 e M − 1 I ... I gli elementi nelle prime due posizioni e scambiando dopo ogni confronto i due elementi considerati, se non si trovano nell’ordine desiderato. Esempio Ordinare in senso crescente i seguenti N = 7 numeri, con l’algoritmo di ordinamento “a bolle”: 52, −34, −48, 58, 75, −80, 22. In rosso si mostra la sottosequenza considerata in ogni passo dell’algoritmo, in grassetto i due elementi confrontati; se i due elementi non si trovano nell’ordine desiderato, vengono scambiati. I primo passo: si considera l’intera sequenza di N elementi 52, −34, −48, 58, 75, −80, 22: nessuno scambio 52, −34, −48, 58, 75, −80, 22: scambio 52, −34, −48, 58, −80, 75, 22: scambio 52, −34, −48, −80, 58, 75, 22: scambio 52, −34, −80, −48, 58, 75, 22: scambio 52, −80, −34, −48, 58, 75, 22: scambio −80, 52, −34, −48, 58, 75, 22 (cont.) Esempio (cont.) I secondo passo: si considerano gli ultimi N − 1 elementi −80, 52, −34, −48, 58, 75, 22: scambio −80, 52, −34, −48, 58, 22, 75: scambio −80, 52, −34, −48, 22, 58, 75: nessuno scambio −80, 52, −34, −48, 22, 58, 75: scambio −80, 52, −48, −34, 22, 58, 75: scambio −80, −48, 52, −34, 22, 58, 75 I terzo −80, −80, −80, −80, −80, (cont.) passo: si considerano gli ultimi N − 2 elementi −48, 52, −34, 22, 58, 75: nessuno scambio −48, 52, −34, 22, 58, 75: nessuno scambio −48, 52, −34, 22, 58, 75: nessuno scambio −48, 52, −34, 22, 58, 75: scambio −48, −34, 52, 22, 58, 75 Esempio (cont.) I quarto passo: si considerano gli ultimi N − 3 elementi −80, −48, −34, 52, 22, 58, 75: nessuno scambio −80, −48, −34, 52, 22, 58, 75: nessuno scambio −80, −48, −34, 52, 22, 58, 75: scambio −80, −48, −34, 22, 52, 58, 75 I quinto passo: si considerano gli ultimi N − 4 elementi −80, −48, −34, 22, 52, 58, 75: nessuno scambio −80, −48, −34, 22, 52, 58, 75: nessuno scambio −80, −48, −34, 22, 52, 58, 75 I sesto passo: si considerano gli ultimi N − 5 (due) elementi −80, −48, −34, 22, 52, 58, 75: nessuno scambio Risultato: −80, −48, −34, 22, 52, 58, 75. Ordinamento per inserimento L’idea è la seguente: I si assuma che gli elementi nelle prime M posizioni (per un qualunque M ∈ {1, 2, . . . , N − 1}) formino una sottosequenza già ordinata (anche se questa non fosse la loro posizione corretta nell’intera sequenza ordinata) I si può allora inserire nella posizione corretta di tale sottosequenza l’elemento in posizione M + 1 (indicato con x ), attraverso una serie di confronti con gli elementi in posizione M, M − 1, M − 2, . . . : – se x è minore o uguale all’elemento con cui viene confrontato, quest’ultimo deve essere spostato nella posizione successiva della sequenza, e si procede poi con un altro confronto – se x è maggiore dell’elemento con cui viene confrontato, x deve essere inserito nella posizione successiva della sequenza, e i confronti terminano I si procede quindi nello stesso modo con gli elementi in posizione M + 2, M + 3, . . . , N Ordinamento per inserimento Per rendere tale algoritmo applicabile a qualsiasi sequenza, si può sempre considerare come sottosequenza ordinata iniziale quella composta dal solo primo elemento (M = 1). Anche in questo caso si può considerare una variante che consiste nell’inserire l’elemento in posizione N − 1 nella sottosequenza composta dal solo ultimo elemento; poi l’elemento in posizione N − 2 nella sottosequenza (ora ordinata) degli ultimi due elementi, e così via. Esempio Ordinare in senso crescente i seguenti numeri, con l’algoritmo di ordinamento per inserimento: 52, −34, −48, 58, 75, −80, 22. Per ogni passo, in rosso si mostra la sottosequenza già ordinata, e in grassetto l’elemento da inserire in tale sottosequenza: I 52, −34, −48, 58, 75, −80, 22 l’elemento 52 viene spostato di una posizione verso destra, e −34 viene inserito nella prima posizione I −34, 52, −48, 58, 75, −80, 22 gli elementi −34 e 52 vengono spostati di una posizione verso destra, −48 viene inserito nella prima posizione I −48, −34, 52, 58, 75, −80, 22 nessuno spostamento I −48, −34, 52, 58, 75, −80, 22 nessuno spostamento (cont.) Esempio (cont.) I −48, −34, 52, 58, 75, −80, 22 gli elementi −48, . . . , 75 vengono spostati di una posizione verso destra, −80 viene inserito nella prima posizione I −80, −48, −34, 52, 58, 75, 22 gli elementi 52, 58, 75 vengono spostati di una posizione verso destra, 22 viene inserito nella quarta posizione I −80, −48, −34, 22, 52, 58, 75 Risultato: −80, −48, −34, 22, 52, 58, 75. Algoritmi di ordinamento: efficienza La complessità temporale può essere valutata in termini del numero di confronti tra coppie di elementi; dopo ogni confronto, nel caso peggiore ci sarà anche uno scambio. Non è difficile rendersi conto che per l’ordinamento per selezione e l’ordinamento “a bolle” il numero di confronti non dipende dalla sequenza di partenza. Nel caso dell’ordinamento per inserimento, il numero di confronti dipende invece dalla particolare sequenza di partenza, e il caso peggiore è quello in cui tale sequenza sia ordinata nel senso opposto a quello desiderato. Algoritmi di ordinamento: efficienza Esempio: ordinamento per selezione di una sequenza di N elementi I passo 1: per trovare l’elemento più piccolo dell’intera sequenza sono necessari N − 1 confronti (assumendo inizialmente che il più piccolo sia il primo) I passo 2: N − 2 confronti I ... I passo N − 1: un confronto Numero totale di confronti: (N − 1) + (N − 2) + . . . + 1 = N−1 X k=1 k = N(N − 1) 2 Con ragionamenti analoghi si può facilmente ricavare che l’ordinamento per inserimento (nel caso peggiore) e l’ordinamento “a bolle” richiedono lo stesso numero di confronti. Algoritmi di ordinamento: efficienza Per una sequenza di N elementi, il numero di operazioni richieste nel caso peggiore dai tre algoritmi di ordinamento considerati è quindi proporzionale a N 2 . Esistono altri algoritmi di ordinamento più complessi, alcuni dei quali hanno un’efficienza maggiore: I Quick Sort (numero di confronti proporzionale a N 2 ) I Merge Sort (numero di confronti proporzionale a N log2 N) I Heap Sort (numero di confronti proporzionale a N log2 N)