La programmazione Gli elaboratori sono macchine capaci di effettuare un numero limitato di operazioni molto semplici, ma a grande velocità. Combinando insieme sequenze di operazioni semplici è possibile realizzare funzioni comunque complesse, che possono soddisfare le esigenze più disparate. È possibile quindi realizzare programmi per l'elaborazione di testi, per la gestione di grosse banche dati, per il controllo di robot e di processi, e così via. 1 Condizione fondamentale affinché si possa risolvere un problema mediante l'elaboratore è che esso sia stato definito con precisione. Non si può infatti dire alla macchina semplicemente: “Calcola la criticità di un reattore” “Evadi gli ordini di un'azienda” “Aggiorna i conti correnti di una banca” “Traduci in lingua inglese” 2 Per le applicazioni di tipo scientifico, si dovranno definire i problemi in termini di sistemi di equazioni, di formule da calcolare che a loro volta dovranno essere poste in termini elementari (i soli comprensibili all'elaboratore). Per le elaborazioni aziendali si dovrà redigere un processo di ragionamento logico assai dettagliato, comparazioni di codici, letture di tabelle, confronti fra quantità, ecc. 3 Quindi per risolvere un determinato problema occorre esaminarlo nei dettagli più minuti. La sequenza di passi che porta alla soluzione di un problema è detto algoritmo. La sequenza di operazioni, incorporate nella logica della macchina, utilizzate per lo svolgimento di una determinata linea risolutiva, si dice programma. 4 L'esperienza quotidiana suggerisce numerosi esempi di algoritmi: una ricetta da cucina, le istruzioni per l'uso di un televisore, le indicazioni per un lavoro a maglia, le regole per calcolare la somma o la moltiplicazione di due numeri. Le operazioni che un algoritmo può prescrivere possono essere di natura assai differente, ma devono possedere, per poter essere considerate tali, delle caratteristiche ben precise. 5 ogni operazione deve avere termine entro un intervallo di tempo finito dall'inizio della sua esecuzione. “Cavalcare un centauro” o “calcolare le cifre decimali di ” non possono essere considerate operazioni: la prima, infatti, non è eseguibile affatto e la seconda non può avere termine in un tempo finito. 6 ogni operazione deve produrre, se eseguita, un effetto osservabile che possa essere descritto. Ad esempio, l'esecuzione dell'operazione di “spostare il tavolo” produce un effetto che può essere descritto specificando la disposizione dei mobili nella stanza prima e dopo lo spostamento. Invece, “pensare” o “pensare al numero 5” non possono essere considerate operazioni poiché non è affatto chiaro come il loro effetto possa essere “osservato” e descritto. 7 ogni operazione deve produrre lo stesso effetto ogni volta che venga eseguita a partire dalle stesse condizioni iniziali (determinismo). Così se X vale 5 e Y vale 10, tutte le volte che eseguiamo la somma di X e Y con quei valori iniziali, il risultato dovrà essere sempre 15. L'esecuzione di un algoritmo da parte di un “esecutore” - uomo o macchina - si traduce in una successione di operazioni che vengono effettuate nel tempo evocando un “processo sequenziale”. 8 Per “processo sequenziale” s’intende una serie di eventi che occorrono uno dopo l'altro, ognuno con un inizio e una fine bene identificabile. Talvolta il processo è fisso, cioè è sempre lo stesso ad ogni diversa esecuzione. Esempio: algoritmo per calcolare l'importo di una fattura: 9 cerca l'aliquota IVA sulla tabella; moltiplica l'importo netto per l'aliquota trovata; somma il risultato all'importo netto. Questo algoritmo è composto da tre istruzioni, che devono essere eseguite in sequenza. L'elencazione, una dopo l'altra, di tutte le istruzioni eseguite, nell'ordine di esecuzione è detta sequenza di esecuzione dell'algoritmo. 10 Più frequentemente lo stesso algoritmo può evocare più processi sequenziali differenti, a seconda delle condizioni iniziali. Esempio: il precedente algoritmo, se è richiesto do dover considerare la possibilità che la merce in esame non sia soggetta a IVA. 11 SE la merce da fatturare è soggetta a IVA ALLORA Cerca la corretta aliquota IVA sulla tabella e moltiplica l'importo per l'aliquota trovata. Somma il risultato all'importo netto. ALTRIMENTI Tieni conto solo dell'importo di partenza. 12 In questo caso, il processo evocato non è fisso, ma dipende dai dati da elaborare, in particolare dal tipo di merce da fatturare: l'algoritmo descrive un insieme costituito da due sequenze di esecuzione diverse. Esistono situazioni, ancora più complesse, in cui il numero di sequenze d'esecuzione descritte da uno stesso algoritmo non solo non è noto a priori, ma addirittura può essere infinito. 13 Ad esempio, l'algoritmo per telefonata: consideriamo effettuare una 1 solleva il ricevitore; 2 componi il numero; 3 SE qualcuno risponde ALLORA 3.1conduci la conversazione 4 ALTRIMENTI 4.1deponi il ricevitore e 4.2RIPETI L'INTERO PROCEDIMENTO. L'algoritmo sequenze corrispondenti descrive infinite d'esecuzione, al fatto che la 14 telefonata riesca al 1, 2, 3, ... tentativo. Si potrà avere un processo ciclico che non avrà mai termine se l'interlocutore non risponde mai al telefono! Si può dire che un algoritmo è un testo in grado di descrivere un insieme di sequenze di esecuzioni. Diversi sono i costrutti linguistici che possono essere utilizzati allo scopo. Ad esempio, l'algoritmo della telefonata potrebbe essere: 15 1. Solleva il ricevitore 2. Componi il numero 3. SE qualcuno risponde ALLORA SALTA al PUNTO 6 4. Deponi il ricevitore 5. TORNA AL PUNTO 1 6. Conduci la conversazione Negli algoritmi fino ad ora visti sono indicate due classi fondamentali di costrutti: costrutti che l'esecuzione di operazioni (come ricevitore”); 16 prescrivono determinate “deponi il costrutti che indicano l'ordine di esecuzione delle operazioni (come “torna al punto 1”). I primi sono detti istruzioni o comandi, i secondi schemi di controllo o istruzioni di controllo. È stato dimostrato (Bohm - Jacopini) che qualsiasi algoritmo può essere realizzato utilizzando due soli costrutti di controllo: costrutto di decisione binaria costrutto di ripetizione (o loop) 17 Il costrutto di decisione binaria è anche detto costrutto IF - THEN ELSE (SE ALLORA ALTRIMENTI) e segue lo schema: IF condizione THEN sequenza A ELSE sequenza B SE la condizione è vera ALLORA esegui la sequenza A, ALTRIMENTI esegui la sequenza B. 18 Graficamente si rappresenta così: INIZIO FALSO VERO C B A FINE SE la condizione C è vera allora esegui la sequenza A ALTRIMENTI esegui la sequenza B 19 Nei casi in cui manca la parte ELSE il costrutto condizionale diventa semplicemente: IF condizione THEN sequenza A SE la condizione è vera ALLORA esegui la sequenza A (ovvero "esegui la sequenza A se e solo se la condizione è vera"). 20 Il meccanismo di ripetizione, detto anche meccanismo DO-WHILE, si può manifestare in due forme diverse. La prima è detta (FINCHÉ-ESEGUI), rappresentazione è: WHILE-DO la cui WHILE condizione DO sequenza istruzioni Continua ad eseguire la sequenza di istruzioni finché la condizione è vera. 21 Se già alla prima verifica la condizione risulta falsa, la sequenza di istruzioni non verrà mai eseguita. Graficamente: INIZIO FALSO C FINE VERO A Finché la condizione C è vera esegui la sequenza A 22 La seconda è detta REPEAT-UNTIL (RIPETI-FINCHÉ) ed è simmetrica alla precedente: REPEAT sequenza istruzioni UNTIL condizione Qui il blocco di istruzioni precede il controllo: pertanto verrà sempre eseguito almeno una volta anche se la condizione risulta falsa alla prima verifica. 23 Graficamente: INIZIO A C VERO FALSO FINE Esegui la sequenza A finché la condizione C è vera 24 Una derivazione interessante del primo dei due costrutti è il ciclo a contatore, che ha la forma: FOR variabile FROM valore_iniz TO valore_fin DO sequenza istruzioni 25 A variabile si attribuiscono tutti i valori compresi tra valore_iniz e valore_fin e, per ogni attribuzione, si esegue la sequenza di istruzioni. Se all'inizio valore_iniz è maggiore di valore_fin, le istruzioni non vengono mai eseguite. L’istruzione o comando è un costrutto di un certo linguaggio, ad esempio l'italiano, che prescrive l'esecuzione di una operazione. 26 Le frasi: sposta il tavolo; mischia le carte; calcola l'IVA del totale; sono istruzioni in italiano, che prescrivono l'esecuzione di tre operazioni diverse. Finora non è stata fatta alcuna ipotesi sulla forma grammaticale di una istruzione. 27 Si richiede solamente che esista un insieme, arbitrario ma ben specificato, di regole grammaticali che permettano di stabilire con certezza se una data sequenza di caratteri sia da considerarsi o meno una istruzione: questo insieme di regole è detto sintassi. L'esistenza di un'istruzione presuppone il fatto che sia eseguita. Affinché ciò risulti possibile, è necessario che l'esecutore dell'algoritmo in cui essa appare ne conosca non solo la sintassi ma anche il significato (semantica). 28 Ad esempio, il segno nell’espressione a b assume il significato di somma algebrica se a e b sono dei numeri, ma potrebbe assumere il significato di concatenamento di stringhe se a e b sono (sequenze di) caratteri. Si dicono elementari quelle istruzioni il cui significato possa ritenersi noto al loro esecutore, non elementari le altre. Il fatto che una istruzione sia considerata elementare oppure no dipende dal particolare esecutore a cui essa è rivolta. 29 Ad esempio l'istruzione: “COMPONI IL NUMERO” sarà considerata elementare soltanto se l'esecutore ha esperienza in fatto di telefonate. 30 In caso contrario, il suo significato dovrà essere definito in termini di operazioni più semplici. Ad esempio: SE la chiamata è extraurbana ALLORA componi il prefisso POI: componi la prima cifra; componi la seconda cifra; componi la terza cifra; ...................... 31 Potremmo ancora specificare che “COMPONI IL PREFISSO” significa: componi la prima cifra del prefisso; componi la seconda cifra del prefisso; .................. e così via. 32 Dal precedente esempio si desume che, quando si deve risolvere un problema e si studia un algoritmo a tale scopo, non necessariamente si pensa ad una successione di istruzioni elementari per l'esecutore. In generale si scompone il problema principale in un certo numero di problemi più semplici, ognuno dei quali viene ulteriormente scomposto per ottenere una sequenza di operazioni atte alla soluzione. 33 L'unione di tutte le sequenze di operazioni così ottenute formerà la soluzione del problema principale ed in definitiva sarà proprio l'algoritmo risolutivo. Supponiamo di voler specificare un algoritmo per effettuare il prodotto di due numeri interi col metodo delle addizioni successive. L'algoritmo potrebbe essere costituito semplicemente dal seguente testo: Si sommi il moltiplicando a se stesso un numero di volte uguale al valore del moltiplicatore. 34 Specifichiamo con maggiore dettaglio le operazioni richieste evitando i costrutti ambigui. Ad esempio, potremmo scrivere: Si sommi il moltiplicando a se stesso, e si decrementi di uno il valore del moltiplicatore. Si sommi ancora il moltiplicando al valore ottenuto dalla precedente somma, e si decrementi di nuovo il valore ottenuto dalla precedente sottrazione. Si ripeta il procedimento fino a che, per decrementi successivi, non si raggiunga il valore uno. 35 Per evitare ambiguità, si è stati costretti ad usare le espressioni: “valore ottenuto dalla precedente somma” e “valore ottenuto dalla precedente sottrazione” per specificare, di volta in volta, di quale valore si sta parlando. 36 Per distinguere tali valori si ricorre a simboli o identificatori per riferirsi senza ambiguità, di volta in volta, al valore desiderato. Ad esempio: 1 Si chiami M il valore del moltiplicando ed N il valore del moltiplicatore e sia M1 il risultato (inizialmente zero). 2 Si ripetano le seguenti operazioni fino a che il valore di N non diventi uguale a 0: 37 2.1 si sommi il valore del moltiplicando M al valore di M1 e si chiami il risultato ancora M1; 2.2si sottragga 1 dal valore di N, e si chiami il risultato ancora N. Alla fine il valore di M1 è il risultato. I simboli M, N e M1 sono detti identificatori di variabili (è conveniente che siano di tipo mnemonico). Si può concludere che per costruire un algoritmo (e quindi un programma) sono necessari solo tre 38 blocchi di base, caratterizzati dall’avere un solo punto d’inizio e un solo punto di fine: 1)blocco di elaborazione 2)blocco di ripetizione (o loop) 3)blocco di decisione binaria. La particolare forma utilizzata per descrivere un algoritmo, in cui si puntualizzano le operazioni effettuate, i costrutti di controllo, le variabili implicate, è detta diagramma di flusso. 39 Se negli anni '60 si preferiva per i diagrammi di flusso la forma grafica, dagli anni '70 in poi si è prevalsa la forma testuale, utilizzando un liguaggio in gergo detto Pascal-like (quello degli esempi), in cui i costrutti di controllo sono espressi in modo non ambiguo. Impostato il diagramma di flusso, è necessario procedere alla sua verifica mediante una prova logica per controllare se la linea operativa tracciata è esatta oppure contiene errori. 40 La prova logica consiste nell'ipotizzare dei dati campione, calcolare su di essi le operazioni del problema, quindi eseguire passo passo le funzioni espresse dal diagramma di flusso fino ad arrivare allo stop. Alla fine si confrontano i risultati calcolati con altri metodi e quelli a cui si è pervenuti col diagramma. Se sono uguali l’algoritmo (con buona probabilità) è esatto. 41 La prova logica si suddivide nelle quattro fasi seguenti: 42 1. Stesura dei dati campione: deve essere fatta tenendo presente tutte le particolarità che i dati del problema possono presentare. 2. Calcolo dei risultati per altra via: si calcolano i risultati per vie diverse da quelle del diagramma di flusso. 3. Prova logica effettiva: si parte dalla posizione di inizio (start) e si elabora, sui dati ipotizzati, eseguendo una a una le operazioni indicate dal diagramma, finché non si raggiunge lo stop. 43 4. Controllo dei risultati: si verifica la corrispondenza fra i risultati della prova logica effettiva e quelli ottenuti per via diversa. I diagrammi di flusso possono presentarsi ad un livello generale, a grandi linee, o ad un livello particolare, dove sono descritte le operazioni elementari. Il diagramma a grandi linee tende ad evidenziare il “grosso del problema”, dando risalto alle operazioni di maggiore importanza. 44 Il diagramma dettagliato è più utile al momento della stesura del programma e successivamente per il test e la manutenzione del programma stesso. Analizziamo gli elementi comuni ai principali linguaggi di programmazione (Pascal, Fortran, C, Basic, ecc.), procedendo mediante esempi. Tutte le informazioni (numeri, caratteri o altro) che devono essere elaborate da un programma devono essere poste in un “contenitore”. 45 Tutti i linguaggi consentono di definire diversi tipi di “contenitori” e di aggregarli insieme in “strutture”. L'insieme di “contenitori” e “strutture è detto “base-dati” del problema. Il riferimento a questi “contenitori” (o “strutture”) avviene mediante nomi o “etichette” scelti dal programmatore e che assumono il nome di “identificatori”. 46 Col termine di “identificatore” si intende una sequenza di uno o più caratteri utilizzati per indicare un'entità ben definita. Per reperire o deporre informazioni stabilite dalla procedura di calcolo si fa riferimento a contenitori identificati da una etichetta. Il simbolo: Am significa appunto “riponi m nel contenitore etichettato A”. 47 Inizializzare un contenitore vuol dire introdurre per la prima volta un elemento. Realizziamo come esempio un diagramma di flusso per la ricerca del massimo fra quattro numeri. Siano N1, N2, N3, N4 e max gli identificatori dei quattro numeri e del massimo che cerchiamo. 48 Ipotizziamo dapprima che il numero più grande sia N1 e lo mettiamo in max; confrontiamo ora il contenuto di max con N2: se N2 è maggiore allora trascriviamo in max tale elemento, se invece N2 è minore non alteriamo il contenuto di max. Ripetiamo il procedimento con N3 e N4. 49 1 Leggi N1, N2, N3, N4 2 max N1 3 IF N2 > max THEN 3.1max N2 4 IF N3 > max THEN 4.1max N3 5 IF N4 > max THEN 5.1max N4 6 stampa “Il massimo è” max Si osservi come il precedente non sia un processo ottimizzato in quanto è valido solo per 4 elementi. Se vogliamo trovare il massimo fra 5 elementi occorrerà aggiungere un altro confronto. 50 Quando si devono effettuare operazioni su elementi simili, cioè della stessa specie, conviene “strutturare” i dati, ad esempio in un “vettore” e utilizzare etichette con indici. Si individua ogni elemento con una etichetta, ad esempio N, più un indice che lo distingue dagli altri dello stesso tipo. Normalmente si rappresenteranno così: N[1], N[2] o, più 51 genericamente, N[I], dove I funge da etichetta dell'indice. 52 Graficamente: N[1] N[I] con I=5 N[8] Usando questa notazione possiamo modificare il precedente diagramma di flusso. 53 1 Per I da 1 a 4 1.1leggi N[I] 2 max N[1] 3 Per I che va da 2 a 4 3.1IF N[I] > max THEN 3.1.1 max N[I] 4 stampa “Il massimo è” max Il due diagrammi differiscono tra loro sostanzialmente: il primo procede direttamente dall'inizio alla fine; il secondo itera perché contiene un ciclo o loop. 54 Meglio ancora generalizzare l’algoritmo rispetto al numero di elementi trattati. Si può ottenere in due modi: fissando come parametro il numero di elementi, cioè come costante simbolica definita in testa all’algoritmo; definendolo come variabile il cui valore è caricato da un dispositivo di ingresso all’inizio del programma. 55 L’esempio precedente si modifica così (il ciclo a contatore è stato cambiato in ciclo while per mostrarne l’equivalenza): 1 Leggi num_dati 2 indice 1 3 While indice num_dati 3.1leggi N[indice] 3.2incrementa indice 4 max N[1] 5 indice 2 6 While indice num_dati 6.1IF N[indice] > max THEN 6.1.1 max N[indice] 6.2incrementa indice 7 stampa “Il massimo è” max 56 Altro esempio: diagramma di flusso che effettua la somma di N numeri. Sia somma il risultato della somma (somma è quindi un accumulatore) e num_dati il numero di elementi su cui operare. Poiché il contenuto iniziale di un contenitore è impredicibile, occorre inizializzare a zero il contenitore somma, in modo da ottenere alla fine la somma corretta. 57 1 Leggi num_dati 2 indice 1 3 While indice num_dati 3.1leggi N[indice] 3.2incrementa indice 4 somma 0 5 indice 1 6 While indice num_dati 6.1somma somma + N[indice] 6.2incrementa indice 7 stampa “La somma è” somma 58 Caratteristiche programma di un buon - La modularità: conviene spezzare il problema da risolvere in tanti problemi parziali. La combinazione delle soluzioni dei sotto-problemi permette di ottenere la soluzione finale. In questa maniera si semplifica anche il lavoro di prova e correzione del programma nel suo complesso. 59 Scrivere programmi modulari significa anche poter riutilizzare, con minime variazioni, i singoli moduli in altri programmi. - La documentazione, ossia la descrizione dettagliata delle caratteristiche del programma e del suo modo di operare. In genere si divide in due parti. Nella prima detta esterna si suole dare tutte le informazioni necessarie per ottenere le migliori prestazioni dal programma. 60 Contiene le modalità d'uso del programma, il significato dei messaggi emessi durante l'esecuzione, altre caratteristiche proprie quali l'occupazione di memoria, i sottoprogrammi richiamati, ecc. La seconda parte, detta interna, è la descrizione degli algoritmi usati, i diagrammi di flusso e i commenti. Tutti i linguaggi di programmazione prevedono la possibilità di inserire commenti all’interno dei programmi. 61 - La leggibilità, frutto di una oculata scelta degli algoritmi risolutivi, di una efficace suddivisione in moduli e di un uso appropriato delle strutture del linguaggio. Caratteristiche più difficilmente definibili e qualificabili sono l'efficienza e la portabilità. La prima è facilmente intuibile: è efficiente un programma che raggiunge lo scopo per il quale è stato realizzato, è veloce, occupa poco spazio di memoria, permette una buona interazione con l'utente. 62 Per portabilità s'intende la possibilità di far "girare" il programma su piattaforme differenti. Per ottenere la portabilità, conviene usare quanto più possibile linguaggi standard di alto livello, limitando al minimo quelle forme non-standard ammesse dal linguaggio (estensioni o dialetti). L'attività più onerosa è il collaudo: nella stesura di un programma si commettono involontariamente molti errori: la prova e la correzione permetteranno di scoprirli. 63 Questa fase è detta debugging (da to debug, “spulciare”). Correggere un programma è tanto più semplice quanto più esso è documentato e il flusso delle istruzioni è continuo, quasi armonico. L'esperienza dimostra che il requisito dell'efficienza in senso stretto è richiesto in un numero di applicazioni molto limitato. È invece rilevante il tempo impiegato nella manutenzione e nell'aggiornamento dei programmi. 64 Il requisito della leggibilità non deve pertanto essere mai sacrificato. Conviene partire dal presupposto che i programmi devono essere compresi prima da altri uomini e solo successivamente dalle macchine! 65 Algoritmi Che cosa s’intende comunemente per algoritmo ? “Un algoritmo è costituito da un insieme finito di passi distinti e non ambigui che, eseguiti a partire da assegnate condizioni iniziali, producono l’output corrispondente e terminano in un tempo finito”. La definizione precedente implica aver identificato uno o più “passi” che si è in grado di eseguire: 66 occorre disporre di una serie di operazioni (elementari o complesse) e di un “esecutore” capace di realizzare tali operazioni. La descrizione di un algoritmo dipende dunque dall’insieme di operazioni disponibili. Esiste un metodo per inventare algoritmi? No, ma esistono strategie che possono venire in soccorso (metodo euristico). 67 Sulla falsariga di quanto ha proposto il matematico Polya per la dimostrazione di teoremi, si possono suggerire i seguenti punti salienti: scomposizione dei problemi complessi in problemi più semplici (si suppone che siano noti un certo numero di problemi elementari di cui si conosce la soluzione); focalizzazione dell’attenzione: riconoscimento di uno o più “passi” che risultano risolutivi (la soluzione può essere ottenuta, ad esempio, ripetendo più volte lo stesso passo); 68 minimizzazione delle differenze tra “descrizione attuale della soluzione” ed “obiettivo”; si noti che ciò implica individuare e tentare di risolvere dapprima il problema – o l’aspetto del problema – più rilevante, rimandando a fasi successive il raffinamento della soluzione; utilizzo delle analogie: se il problema che si sta affrontando può essere ricondotto alla forma di un problema che si è già risolto, si può riutilizzare lo stesso tipo di soluzione. 69 Per la soluzione degli esercizi proposti si suppone di disporre delle seguenti operazioni: somma / sottrazione / prodotto tra numeri interi o numeri reali: il risultato è rispettivamente un numero intero o un reale divisione tra numeri reali: risultato è un numero reale il divisione tra numeri interi: il risultato è la parte intera del quoziente (quota) 70 modulo tra numeri interi: il risultato è il resto della divisione tra numeri interi confronto per >, , =, <, tra numeri interi e numeri reali: il risultato è un valore logico, VERO o FALSO and (“e insieme”) / or (“oppure”) / not (negazione logica) tra valori logici: il risultato è ancora di tipo logico 71 Come si vede, le operazioni ammesse presuppongono l’esistenza di particolari tipi di dato: nel nostro caso, esistono contenitori, variabili o costanti, per numeri interi e per numeri reali. Per semplicità, si può ipotizzare che esistano come elementari le seguenti altre azioni su questi tipi di variabili: Leggi dato, operazione di introduzione di un valore nel contenitore dato da un periferico (per esempio, dalla tastiera); Stampa dato, operazione di visualizzazione del contenuto di 72 dato (per videoterminale). esempio, sul Alcune altre operazioni su dati strutturati (accesso ad elementi di un vettore, ecc.) verranno introdotte in seguito. 73 Esercizio 1 Realizzare il diagramma di flusso per il calcolo dell’area geometrica di un triangolo. 1 Leggi la base 2 Leggi l’altezza 3 Calcola base altezza / 2, risultato in area_triangolo 4 Fine. 74 Esercizio 2 Realizzare un algoritmo per il calcolo della tabellina pitagorica di un numero. Occorre identificare significativo: un passo 1 Calcola numero fattore, risultato in prodotto 75 Stabilisco quante volte devo ripetere l’operazione: 1 Con fattore che assume valori da 1 a 10 1.1Calcola numero fattore, risultato in prodotto Completo con i dettagli: 1 Leggi fattore 2 Con fattore che assume valori da 1 a 10 2.1Calcola numero fattore, risultato in prodotto 2.2Visualizza prodotto 3 Fine 76 Esercizio 3 Realizzare un algoritmo per il calcolo della tabellina pitagorica dei numeri da 1 a 10. Occorre identificare significativo: un passo 1 Calcola la tabellina di numero (operazione già nota) 77 Stabilisco quante volte devo ripetere l’operazione: 1 Con numero che assume valori da 1 a 10 1.1Calcola la tabellina di numero L’algoritmo completo è: 1 Con numero che assume valori da 1 a 10 1.1Con fattore che assume valori da 1 a 10 1.1.1 Calcola numero fattore, risultato in prodotto 1.1.2 Visualizza prodotto 2 Fine 78 Esercizio 4 Trasformare il seguente algoritmo non strutturato in un equivalente algoritmo strutturato: 1 si pone 1 in num_val e 0 in accum 2 con indice che va da 1 a 100 si esegue 2.1si legge un dato intero in varint 2.2se varint < 0 si va a 3. 2.3si somma varint ad accum 2.4si incrementa num_val 3 si decrementa num_val 4 si visualizza accum 5 fine. 79 L’algoritmo contiene un ciclo a contatore basato su indice, ma dal ciclo si esce anche se si verifica l’evento “varint < 0”. Occorre pertanto trasformare il ciclo a contatore in un ciclo basato su evento (ciclo WHILE o ciclo REPEAT-UNTIL). Si esce dal ciclo se sono stati trattati tutti i dati (test su indice) oppure se il dato letto in varint è negativo. Nell’adottare un ciclo basato su evento occorre invece determinare la condizione per continuare il ciclo: 80 si permane nel ciclo finché non sono stati trattati tutti i dati e allo stesso tempo varint è positivo o nullo. 1 si pone 1 in num_val e 0 in accum 2 si pone indice a 0, varint a 0 3 finché (indice < 100) e insieme (varint 0) si esegue 3.1si legge un dato intero in varint 3.2se varint 0 3.2.1si somma varint ad accum 3.2.2si incrementa num_val 3.3si incrementa indice 4 si decrementa num_val 5 si visualizza accum 81 6 fine. All’interno del ciclo si è inserito un test su varint per ottenere un comportamento equivalente a quello dell’algoritmo specificato dal testo dell’esercizio. 82 Esercizio 5 Realizzare un algoritmo per calcolare la media aritmetica di una sequenza forniti da un dispositivo di input. Analisi nel matematica: dominio della la media si calcola effettuando la somma di tutti i numeri e dividendo per la quantità di numeri; la divisione è lecita se il divisore non è nullo. 83 Identifico un passo significativo: 1 Leggi numero 2 Calcola numero + accumulatore, risultato in accumulatore 3 Incrementa quanti_numeri Determino quante volte effettuare il passo significativo, tengo conto di dover effettuare le inizializzazioni e della condizione per effettuare correttamente la divisione. 84 L’algoritmo è: 1 Inizializza accumulatore e quanti_numeri con il valore 0 2 Finché ci sono numeri 2.1Leggi numero 2.2Calcola numero + accumulatore, risultato in accumulatore 2.3Incrementa quanti_numeri 3 Se quanti_numeri > 0 3.1Calcola accumulatore / quanti_numeri, risultato in media 4 Altrimenti 4.1Visualizza “media non calcolabile” 5 Fine. 85 NB: nel realizzare l'algoritmo, si è ipotizzato di poter testare la disponibilità di altri numeri provenienti dal dispositivo di input. Se ciò non risultasse vero, la frase 2Finché ci sono numeri - dovrebbe essere ulteriormente specificata. 86 Esercizio 6 Realizzare un algoritmo per il calcolo del massimo comun divisore (M.C.D.) di due numeri interi positivi. Analisi nel dominio della matematica: Euclide ha già proposto un algoritmo efficiente, che sfrutta la seguente proprietà: se a > b, il M.C.D. tra a e b è anche il M.C.D. tra a-b e b; il procedimento può essere reiterato e termina quando, in seguito a 87 sottrazioni successive, si ottengono due numeri uguali. 88 Algoritmo: Leggi dato_a e dato_b Finché dato_a dato_b Se dato_a > dato_b calcola dato_a - dato_b, risultato in dato_a altrimenti calcola dato_b - dato_a, risultato in dato_b Assegna a massimo_comun_divisore il valore di dato_a Fine. 89 II Soluzione Si può utilizzare il procedimento di scomposizione in fattori primi. Si ipotizza di poter disporre di un vettore vett_primi, di lunghezza adeguata, che contenga i numeri primi almeno fino al più piccolo dei numeri considerati. 1 2 3 5 7 11 ecc. 90 Analizzo il problema nel dominio della matematica: Il M.C.D. è costituito dal prodotto dei numeri primi per i quali i due numeri sono pienamente divisibili. Occorre tener presente che un numero primo può contribuire al M.C.D. con molteplicità superiore ad 1. 91 Identifico un passo significativo: cerca fattore comune di dato_a e dato_b in vett_primi, determina se trovato e restituisce il valore in fatt_comun Se trovato è vero moltiplica il valore precedente di massimo_comun_denominatore per fatt_comun riduci dato_a e dato_b di fatt_comun 92 Nota: il primo passo (“cerca...”) può essere ritenuto un’azione primitiva, se esiste un “modulo” che lo attua. Lo si trasforma in procedura o funzione, in cui dato_a e dato_b sono parametri d’ingresso e trovato e fatt_comun sono parametri d’uscita. Occorre determinare quante volte occorre ripetere il passo significativo, e successivamente le inizializzazioni. Riscrivendo l’algoritmo in modo più formale, si ha: 93 Main Legge dato_a e dato_b Pone 1 in massimo_comun_denominatore Pone VERO in trovato Finché trovato è VERO Cerca_fattore_comune(ingressi: dato_a, dato_b; uscite: trovato, fatt_comun) Se trovato è VERO Calcola fatt_comun massimo_comun_denominat ore, risultato in massimo_comun_denominatore calcola dato_a / fatt_comun, risultato in dato_a 94 calcola dato_b / fatt_comun, risultato in dato_b Visualizza massimo_comun_denomi-natore Fine. Passo alla soluzione del sottoproblema. Devo verificare se un generico elemento di vett_primi divide pienamente dato_a e dato_b. 95 Il passo significativo è: Se ((dato_a MOD vett_primi[indice]) = 0) e insieme ((dato_b MOD vett_primi[indice]) = 0) Pone VERO in trovato Pone vett_primi[indice] in fatt_comun altrimenti Incrementa indice 96 Determino quante volte devo eseguire questo passo: Smetto di cercare appena trovo il fattore comune. Il fattore cercato non può essere superiore al minimo tra dato_a e dato_b. Determino le inizializzazioni: devo ignorare il primo numero primo, cioè 1: pertanto l’indice di scansione del vettore vett_primi deve iniziare da 2. 97 L’algoritmo completo è: Cerca_fattore_comune(ingressi: dato_a, dato_b; uscite: trovato, fatt_comun) Pone 2 in indice Se dato_a < dato_b pone dato_a in minimo_ab altrimenti pone dato_b in minimo_ab 98 Finché (NO trovato) e insieme (vett_primi[indice] minimo_ab) Se ((dato_a MOD vett_primi[indice]) = 0) e insieme ((dato_b MOD vett_primi[indice]) = 0) Pone VERO in trovato Pone vett_primi[indice] in fatt_comun altrimenti Incrementa indice Fine. 99 Conclusioni Ai criteri generali per creare buoni algoritmi si possono aggiungere i seguenti suggerimenti: analizzare il problema nel suo dominio specifico, cercando di evidenziare tutte le specifiche esplicite ma anche quelle implicite; creare un esempio, il più generale possibile; creare esempi dei casi limiti; nella realizzazione dell’algoritmo, cercare di trattare tutti i dati allo stesso modo, evitando di trattare a parte il primo dato; 100 prendere come riferimento i dati dell’esempio tipico e partire non dall’inizio, né dalla fine, ma dal centro, immaginando che i dati precedenti siano stati già trattati: individuare così il passo fondamentale; mettere a posto le inizializzazioni affinché l’algoritmo operi correttamente alla partenza; verificare che con questo vengano trattati correttamente anche i casi limite. Se ciò non accade, tentare di effettuare piccole modifiche per far rientrare anche i casi limiti: se le modifiche sono onerose, valutare 101 l’opportunità di rigettare completamente l’algo-ritmo e di crearne un altro su nuove basi. Data la stretta dipendenza tra azioni e base dati, anche quest’ultima dovrebbe essere valutata con occhio critico. 102