Roberto Visconti COMPENDIO DI INFORMATICA CAPITOLO 2 estratto da: COMPENDIO DI INFORMATICA ediz. CALDERINI Bologna – 1988 anno di revisione 2013 12/05 1 2.1 GLI ALGORITMI 2.1.1 INTRODUZIONE L'obiettivo di questo capitolo e' quello di "aprire" la discussione relativa alla risoluzione di problemi mediante algoritmo, senza pretendere di chiudere l'argomento, visto che la sua continuazione logica e' costituita dai capitoli 3 e 5, in cui viene discussa in modo piu' ampio ed organico la programmazione in linguaggi evoluti. In questa fase iniziale, si cerchera' di tendere al concetto di algoritmo in modo euristico, cercando di dedurre la sua esistenza dagli esempi che verranno portati qui di seguito. In un secondo momento, ci occuperemo di una definizione piu' rigorosa del termine algoritmo. Ipotizziamo di risolvere un semplice problema esemplificativo, come ad esempio potrebbe il calcolo dell'area del cerchio. Se consideriamo la sua soluzione a livello macroscopico, siamo tentati di affermare che l'area di un cerchio di raggio assegnato si trova con un calcolo. In realta', se analizziamo in modo dettagliato tutte le fasi operative per arrivare alla soluzione, possiamo pensare di suddividere il problema "calcolo dell'area" globale in una somma di tanti piccoli problemi elementari. Infatti, riflettendo attentamente, si puo' dedurre che sono state eseguite una serie di operazioni, che possono essere riassunte nelle seguenti: -Ricerca del valore del raggio del cerchio ; (2.1.1) -Calcolo del valore Area con la formula: Area = raggio x raggio x 3.14 (2.1.2) -Scrittura del risultato numerico in una posizione ed in una forma assegnata, come ad esempio: SOLUZIONE: Area cerchio di raggio cm.10 = cmq.314 (2.1.3) Per arrivare a risolvere il problema nella sua globalita', abbiamo dovuto "spezzettarlo", per cosi' dire, in una somma di tanti problemi piu' "piccoli", in modo che ogni problema fosse risolvibile con una singola azione. Infatti, e' possibile rendersi conto dall'esempio portato che anche una elaborazione banale come quella considerata richiede in generale un insieme di azioni per essere portata a compimento. Se una elaborazione richiede una sola azione per arrivare a compimento, si dice che il problema associato non richiede algoritmo per essere risolto, cioe' il problema e' autorisolvente. 2 Se, dato un problema risolvibile con algoritmo, si riesce a definire un metodo per ridurre l'algoritmo ad una sola azione, allora il problema e' risolto. Non e' necessario che questa condizione si verifichi per risolvere un problema, poiche' la sua risoluzione si ottiene con l'algoritmo completo, che e' la strada comunemente seguita per ottenere le soluzioni. In generale, la risoluzione mediante algoritmo e' applicabile ad una numerosa classe di problemi che interessano l'informatica, oltre che numerose materie tecnico- scientifiche. Normalmente, quando si risolve il problema assegnato, sia esso di natura scientifica, come il calcolo di una incognita in una equazione, o tecnico, come la riparazione di una stampante, sono sempre conosciute le situazioni iniziali del problema, che costituiscono i dati, od, in gergo tecnico, l'INPUT. Nel nostro caso, l'input e' costituito dal valore del raggio inserito nell'azione (2.1.1). E' anche noto l'obiettivo finale, che in gergo viene chiamato OUTPUT. Nel caso dell'esempio, si tratta dell'azione specificata con la scritta: SOLUZIONE: Area del cerchio di raggio cm.10 = cmq. ...... che deve essere riempita con il valore risultante dal calcolo. L'esistenza dell'input e dell'output vincola le azioni che possono essere eseguite sui dati di ingresso, per farli evolvere e diventare dati di uscita. Le regole che vengono determinate per far evolvere un dato di ingresso in una altra forma individata da altre proprieta', e derivante dalla forma precedente, sono chiamate trasformazioni. Queste trasformazioni sono caratterizzate dall'avere un ordine ben preciso e non alterabile per poter costituire un algoritmo. Ad esempio, la successione di regole per il calcolo dell'area del cerchio deve essere: (2.1.1) (2.1.2) (2.1.3) e non puo' essere costituita sequenzialmente, ad esempio, dalla successione: (2.1.2) (2.1.1) (2.1.3) perche' la regola 2.1.2 prevede per il suo utilizzo che in precedenza sia noto il valore del raggio: ma questo fatto e' contenuto nella regola 2.1.1, che deve percio' precedere la regola 2.1.2. 3 Possiamo allora concludere la discussione attuale con la seguente definizione, piu' rigorosa di quella intuitiva precedente: -Per ALGORITMO si intende una successione ordinata di trasformazioni simboliche, che causano l'evoluzione di un INPUT in OUTPUT in modo progressivo. Le azioni con cui viene descritto un algoritmo vengono spesso chiamate passi, per cui diremo che un algoritmo e' costituito da un insieme di passi. In generale, dato un problema, non esiste un solo algoritmo risolutivo di quel problema, ma ne esistono piu' d'uno. In questi casi, una difficolta' aggiuntiva consiste nello scegliere quale e' il migliore degli algoritmi proposti, in base alle condizioni al contorno del problema stesso. Un insieme di istruzioni, per poter costituire un algoritmo, deve rispettare delle caratteristiche fondamentali, che possono essere sintetizzate nelle seguenti proprieta': 1) -FINITEZZA : l'algoritmo deve essere descrivibile in un numero finito di passi; 2) -ESEGUIBILITA' : l'algoritmo deve essere eseguibile in ogni sua parte con le risorse definite dall'utente; 3) -NON AMBIGUITA': le istruzioni costituenti i passi dell'algoritmo devono essere eseguibili univocamente, cioe' deve essere possibile una ed una sola sola interpretazione di ogni passo. Non bisogna confondere la finitezza del numero di passi con cui e' descritto un algoritmo con il numero delle azioni eseguibili, che virtualmente possono anche essere infinite. Difatti, e' possibile, ad esempio, ideare un algoritmo per il calcolo del numero trascendente PIGRECO = 3.1415927..., descritto da un numero finito di azioni, ad esempio : 1. Misurare la circonferenza di un cerchio; 2. Misurare il diametro dello stesso cerchio; 3. Eseguire la divisione dei due numeri trovati. Tuttavia, lo svolgimento di questo algoritmo può proseguire all'infinito, in quanto la terza azione non da' mai resto nullo. E' lasciato al solutore del problema l'imporre delle condizioni al contorno per far si' che l'algoritmo diventi effettivamente eseguibile, ad esempio aggiungendo un quarto passo: 4. Proseguire le operazioni di cui al punto 3 fino a che il resto e' < = 0.001. Si puo' esprimere questo fatto, affermando che un algoritmo deve essere contraddistinto dalla non limitatezza: • del numero dei dati in input; • del numero dei dati in output; • del numero dei passi eseguibili. 4 Il carattere di non ambiguita' deve essere fornito da una specifica chiara e non altrimenti interpretabile. Nel nostro caso, al passo 2 abbiamo la specifica che il cerchio di cui si misura il diametro deve essere lo stesso cerchio di cui si e' misurata la circonferenza al passo 1, e non uno qualsiasi. Questa istruzione non si puo' prestare ad altre interpretazioni, come accade invece per il passo: 2. Misurare il diametro di un cerchio che potrebbe indicare tanto quello di cui si e' misurata la circonferenza, quanto un' altro qualsiasi. Come sintesi di questo breve discorso introduttivo, potremo dire che un algoritmo deve essere finito, eseguibile e non ambiguo. Si vuole qui dare un esempio conclusivo del paragrafo, esponendo a titolo esemplificativo due algoritmi. Il primo, riassuntivo, e' relativo all'esempio trattato, e costituisce un possibile algoritmo per il problema "calcolo area del cerchio". Il secondo, di tipo non numerico, e' relativo alla manutenzione di una parte hardware di un sistema, e precisamente una stampante. Si tratta ovviamente di una esposizione didattica esemplificativa. ALGORITMO 1 : CALCOLO DELL'AREA DEL CERCHIO 1. Ricerca valore del raggio del cerchio; 2. Calcola il valore Area = raggio x raggio x 3.14 3. Scrivi il risultato accanto alla parola "cmq." nella scritta: AREA DEL CERCHIO = cmq. ALGORITMO 2 : DIAGNOSTICA: GUASTI STAMPANTE - CASO: INTRALCIO CARTA NEL SISTEMA DI TRASCINAMENTO 1. Porre lo switch di accensione in posizione OFF; 2. Verificare che la lampada spia corrispondente sia spenta; 3. Aprire il coperchio frontale; 4. Rimuovere il foglio bloccato dal rullo trascina-carta; 5. Chiudere il coperchio; 6. Reinserire la carta nel raccoglicarte anteriore; 7. Porre lo switch di accensione in posizione ON. Come si vede, nel secondo caso l'algoritmo consiste in pratica in un insieme ordinato di prescrizioni, che hanno senso solo se eseguite in una sequenza ben determinata. 5 2.1.2 RAPPRESENTAZIONE DI ALGORITMI : TAVOLE DI FLUSSO Nello studio degli algoritmi, si evidenziano da un punto di vista pratico alcuni fatti importanti: - Gli algoritmi messi a punto per la soluzione di problemi sono molteplici, e prodotti in grande numero da più specialisti. -Le soluzioni messe a punto da uno specialista possono essere utili a molti altri utenti, senza che questi ultimi ripetano le stesse cose sviluppate dallo specialista. Questi fatti, ampiamente provati dalla esperienza, hanno portato alla esigenza di disporre di una rappresentazione "standard" degli algoritmi, che permetta, attraverso un formalismo universalmente conosciuto, la comprensione di un algoritmo anche e soprattutto a coloro che non lo hanno sviluppato. Il primo formalismo impiegato per la rappresentazione di algoritmi, che ha avuto larga diffusione e successo in informatica, e' stato la rappresentazione mediante TAVOLE DI FLUSSO, dette anche DIAGRAMMI DI FLUSSO , con termine inglese FLOW-CHART. Da un punto di vista informatico, la tavola di flusso rappresenta uno strumento che permette la schematizzazione di una idea astratta su carta, fino ad avere una rappresentazione traducibile direttamente in istruzioni di linguaggio di programmazione. L'assunto base e' che naturalmente una idea astratta per la soluzione di un problema non e' direttamente traducibile in linguaggio di programmazione. Nella tavola di flusso e' evidenziato sequenzialmente tutto il flusso logico ed ordinato delle operazioni da svolgere. Ogni operazione e' relativa ad una funzione particolare. Ad esempio, l'operazione di ingresso dei dati da tastiera e' considerata funzionalmente diversa dall'operazione di calcolo del prodotto "raggio x raggio x 3.14". Ogni funzione particolarmente significativa viene identificata con un simbolo grafico diverso, ad esempio: • il rettangolo per indicare una operazione generica di elaborazione, come ad esempio il calcolo dell'area; • il rombo per indicare che nel corso dell' algoritmo viene presa una decisione tra due (o piu') possibili; • la piccola ellisse per indicare l'inizio o la fine di un algoritmo (scrivendo il termine adatto nel suo interno); • il rettangolo adattato a trapezoide per le operazioni di input / output ; e cosi' via. Nella figura 2.1 sono indicati alcuni simboli diffusi a livello internazionale per la rappresentazione degli algoritmi mediante tavole ( o diagrammi) di flusso. 6 Fig. 2.1 Simboli dei diagrammi di flusso (flow chart) Nel caso che il diagramma di flusso non entri in un solo foglio, si puo' interrompere il diagramma stesso in un punto ed usare un connettore ( indicato col simbolo 'cerchio chiuso' O ) in cui disegneremo una lettera di riferimento. In un secondo foglio verrà disegnata la parte ulteriore, cominciando non con un altro INIZIO, ma con un connettore in cui disegneremo la stessa lettera. Ad esempio, in figura 2.2 è visibile la tavola di flusso di un semplice algoritmo per calcolare e stampare il perimetro P di un cerchio partendo dal raggio R: su richiesta, viene calcolata e stampata anche l'area S. In questo esempio, si vede anche l'uso dei connettori: se la prima sezione del programma non è contenuta interamente nel foglio a disposizione, si prosegue con i connettori (in questo caso indicati con le lettere A,B) su un secondo foglio. 7 Fig. 2.2 Esempio di uso dei simboli dei diagrammi di flusso completo di connettori Come si può vedere, ad ogni simbolo corrisponde una funzione ben precisa, utilizzabile da chi sviluppa l'algoritmo. E' utile precisare che una operazione dell'algoritmo puo' essere descritta in più modi, secondo la volontà dell'utente. Praticamente, non esiste un solo modo, in genere, per rappresentare molte delle funzioni necessarie in una tavola di flusso. E' bene quindi abituarsi a considerare in modo aperto le rappresentazioni degli algoritmi, valutandone la chiarezza logica piuttosto che l'aspetto puramente estetico-formale. Ora che possediamo gli strumenti, vediamo in pratica come si applicano, tornando all'esempio del calcolo dell' area del rettangolo. Con riferimento alla fig. 2.3.a, determiniamo l'inizio del flow-chart con il delimitatore START inserito in testa al foglio, per indicare che da qui inizia il diagramma di flusso (in italiano si puo' scrivere INIZIO). Questo diagramma di flusso proseguira' fino ad incontrare un secondo delimitatore, che ne indica la fine, denominato END (oppure FINE). Dopo lo start, verra' disegnato il blocco di ingresso dati, scrivendo nel suo interno le operazioni di ingresso/uscita previste in questa fase: abbreviando con la lettera B la base (e scrivendolo in una apposita legenda contenente la descrizione delle variabili, che completera' la tavola di flusso), scriveremo la lettera H all'interno del blocco. Di seguito, usando una virgola per separarla dalla B, scriveremo la lettera H per indicare che verrà immessa anche l'altezza del rettangolo. Questa forma di scrivere i dati da immettere nel computer prende il nome di lista. Una lettera I inserita nel blocco (in alto a destra) aiutera' i lettori a capire che si tratta di un INPUT invece che di un OUTPUT. 8 Fig. 2.2 Esempio di calcolo dell' area di un rettangolo L'inizio e l'ingresso dati vengono raccordati con un segmento orientato che stabilisce la direzione di trasferimento delle informazioni. E' importante che tale segmento abbia ben evidenziata la direzione con una freccia, in quanto solo in questo modo viene facilitata al massimo la logica di comprensione dell'algoritmo. Nel nostro caso, la freccia andrà verso il basso (da INIZIO all'ingresso dati). Per indicare che si deve eseguire una elaborazione numerica verra' usato il rettangolo (blocco sequenziale) con l'indicazione delle operazioni da svolgere, e cioe': A = B·H Queste operazioni vanno scritte all'interno del blocco. Dall'elaborazione, dovremo procedere verso la stampa su video del risultato, per cui verra' tracciato un segmento orientato verso un blocco di I/O, in cui una lettera O in alto a destra puo' aiutare a far capire che si tratta di una uscita dati, per indicare che il risultato andra' scritto su video, o su stampante. Il blocco terminale di FINE fa' capire che la tavola relativa a questa procedura di calcolo e' terminata. In fig. 2.3.b e' indicato un raffinamento della procedura precedente, in cui e' stato inserito un controllo da parte del programmatore. Subito dopo l'immissione del raggio da INPUT si verifica se la base B e' pari a zero. Per fare questo si e' usato un blocco condizionale in cui viene scritta con chiarezza la formulazione della condizione da verificare. Se la verifica fornisce 'NO' si prosegue l'elaborazione come in precedenza. Se la verifica fornisce 'SI' si assume implicitamente che non ha senso calcolare l'area di un rettangolo con base nulla, e si procede esplicitamente con una strada alternativa che da 9 questo punto "salta" direttamente alla fine della tavola di flusso, evitando di passare per il calcolo dell'area, ritenuto inutile. Questa struttura che abbiamo esaminato in questo esempio prende il nome generico di SALTO (o JUMP od, ancora, BRANCH), ed e' usata diffusamente in informatica. Il suo uso andrebbe comunque limitato allo stretto indispensabile, poiche' il proliferare indiscriminato dei salti fa' perdere abbastanza facilmente, nelle flow-chart, l'intellellegibilita' e la facilita' di comprensione e di modifiche, quando il problema cresce di complessita'. Una volta stabilita la convenzione per la rappresentazione degli algoritmi, si puo' passare all'esame di ogni algoritmo di nostro interesse, relativo alla risoluzione di problemi di qualsiasi genere. Se si analizza un numero adeguatamente grande di tavole di flusso, si vede che alcuni insiemi di simboli si ripetono in gran numero di volte. Questi insiemi di simboli sono riuniti a formare una STRUTTURA logica, che risolve un certo problema. Le strutture fondamentali in informatica sono essenzialmente tre: A) STRUTTURA SEQUENZIALE B) STRUTTURA ALTERNATIVA C) STRUTTURA ITERATIVA Queste strutture sono insiemi di blocchi e, da un punto di vista globale, sono anch'esse pensabili come un unico blocco. Nel seguito della trattazione useremo percio' il termine "blocco" sia per indicare un singolo elemento della tabella in fig. 2.1, sia per indicare un insieme di blocchi. La caratteristica fondamentale di un blocco, sia singolo che globale, e' quella di avere un solo ingresso ed una sola uscita attraverso i quali il blocco stesso si connette al resto della flow- chart. 10 A) STRUTTURA SEQUENZIALE La struttura sequenziale e' costituita da un insieme di operazioni eseguite di seguito l'una dopo l'altra. Come caso particolare, le operazioni possono essere una soltanto. Questa struttura e' costituita da un insieme di simboli, ognuno relativo ad una funzione, raccordati tra loro mediante rette orientate aventi TUTTE la stessa direzione. Un esempio di struttura sequenziale, relativa al calcolo dell'area del cerchio quando e' noto il diametro, e non il raggio, e' visibile in fig. 2.4. Fig. 2.4 Un altro esempio di struttura sequenziale e' costituito dall'algoritmo della manutenzione della stampante, illustrato in precedenza, ed illustrato in fig. 2.5. Questa struttura e' caratterizzata dal fatto che ciascuna operazione, una volta eseguita, non ha piu' influenza diretta nel corso dell'elaborazione sulle parti restanti dell'algoritmo. Il blocco che contiene l'operazione viene chiamato spesso BLOCCO DI PROCESSO, in cui il termine "processo" ha il significato di "procedimento, elaborazione". In questa accezione del termine può indicare, come nel caso di questo esempio, anche azioni pratiche esecutive, come l'apertura di coperchi di macchina, ed essere perciò largamente impiegata sui manuali di installazione di hardware (stampanti, schede grafiche, ecc.). 11 Fig. 2.5 B) STRUTTURA ALTERNATIVA La struttura alternativa (detta anche condizionale) consente di effettuare scelte, lungo l'arco dell'algoritmo, in base al verificarsi o meno di determinate condizioni. Il simbolo piu' usato e' il rombo, come illustrato in fig. 2.6 . Fig.2.6 Le condizioni che originano la scelta vengono scritte all'interno dei quadrilatero. Se la condizione scritta si verifica, si prosegue l'algoritmo lungo il percorso corrispondente al segmento orientato accanto al quale e' scritto 'SI'. Se la condizione scritta non si avvera, si segue invece il percorso corrispondente alla retta orientata accanto alla quale e' scritto 'NO'. Un esempio di uso e' visibile in fig. 2.2 b), dove e' illustrato l'algoritmo relativo al calcolo dell'area del rettangolo, dal quale vogliamo escludere il caso in cui la base B e' nulla. Un 'altro esempio è quello di fig. 2.7 , dove è visualizzato un semplice algoritmo di controllo della divisione tra due numeri. Si immagini di dover dividere un numero N per un numero D. La matematica insegna che non e' possibile dividere un numero assegnato per 0 (in termini differenziali, il risultato divergerebbe ad infinito). Vogliamo allora controllare che la divisione non venga eseguita quando il divisore D e' zero, anche perche' questa operazione darebbe probabilmente una condizione di errore in un elaboratore elettronico. Eseguiamo allora un test sul numero D. Se il numero D=0, si segue il percorso dal lato 'SI', viene scritto sul video "DIVISIONE IMPOSSIBILE", e non viene eseguita l'operazione. Se il numero D non e' zero, si segue il percorso dal lato 'NO', viene eseguita la divisione R=N/D, ed il risultato viene stampato su video. L'osservazione di questo esempio ci porta a due conclusioni importanti: 1)-la struttura condizionale si puo' ottenere in due modi: a) un test ed un solo blocco sequenziale di processo, in un solo ramo; b) un test e due blocchi di processo, uno per ogni ramo. 2)-la struttura condizionale, usata in modo corretto, deve prevedere un solo ingresso generale ed una sola uscita generale. Nell' esempio di fig. 2.7, i punti di ingresso ed uscita dalla struttura alternativa sono indicati con le lettere I, U. 12 C) STRUTTURA ITERATIVA Per struttura iterativa si intende una struttura che permetta l'esecuzione di un blocco sequenziale piu' volte. Il numero di volte viene determinato dal verificarsi o meno di una condizione, all'interno della struttura. L'algoritmo compie dunque una iterazione, cioe' ripete un certo numero di volte una operazione, su condizione. I simboli grafici classici per realizzare una iterazione sono illustrati in fig. 2.9. Le modalita' con cui puo' avvenire una iterazione su controllo sono due: RIPETI-FINCHE' (fig. 2.8), in cui le operazioni indicate nel blocco di processo vengono eseguite almeno una volta, in quanto sono incontrate prima del controllo della condizione. Se la condizione illustrata nel blocco di alternativa e' verificata, si torna ad eseguire con un ritorno all'indietro (loop in gergo tecnico) il blocco di processo, altrimenti si procede oltre il blocco iterativo. Questa struttura ha la proprieta' che il blocco di processo viene eseguito almeno una volta, poiche' viene incontrato prima della condizione. Fig. 2.8 Per loop, od anello, si intende un qualsiasi percorso chiuso che ha origine in un punto P, attraversa un percorso composto da segmenti orientati e da blocchi, e ritorna nel punto P di origine. E' chiaro che un algoritmo composto da un loop senza blocchi alternativi lungo il percorso e' destinato a chiudersi all'infinito su se' stesso: costituisce un cosiddetto loop infinito, ed e' uno dei pericoli da evitare, in fase di programmazione degli elaboratori elettronici, dovuto ad errori di impostazione del blocco condizionale. Difatti, se per errore la condizione nel blocco alternativo non si verifica mai, si cade in loop infinito. FINCHE'-RIPETI (fig. 2.9), composto da un segmento orientato verso un blocco alternativo indicante una condizione. Se la condizione indicata e' verificata, si prosegue ad eseguire l'insieme di operazioni contenute nel blocco di processo, altrimenti si procede oltre il blocco iterativo. Questa struttura ha la proprieta' che il blocco di processo puo' non essere eseguito mai, in quanto prima viene attraversata la condizione, e poi eventualmente il blocco. Anche qui bisogna fare attenzione ad evitare la possibilita' di loop infiniti, studiando la condizione in modo che sicuramente accada almeno una volta. 13 Un esempio di applicazione di queste due strutture e' visibile in figura 2.9.a e figura 2.9.b. In fig. 2.9.a troviamo esposto l'algoritmo relativo ad un pilota di automobile, che vuole raggiungere una determinata velocita'. Il blocco di processo descrive l'azione dell'accelerare, mentre la condizione richiesta e' il raggiungimento della velocita' desiderata. Se la condizione raggiunta e' 'NO', si prosegue con l'accelerare: quando la condizione raggiunta e' 'SI', si esce dal blocco iterativo. L'azione dell'accelerare e' stata ripetuta un certo numero di volte, fino al raggiungimento della condizione. In fig. 2.9.b e' illustrato un algoritmo per staccare tutti i fogli da un quaderno. Per prima cosa si analizza la condizione, e si verifica se nel quaderno ci sono fogli. Se non ci sono fogli, l'azione termina immediatamente, senza dare corso al blocco di processo. Se invece la condizione fornisce 'SI', si esegue il blocco di processo, e si torna attraverso il percorso indicato al punto di inizio, cioe' a verificare la condizione indicata nel blocco alternativo, tramite un loop di ritorno. Queste strutture sono di rilevanza notevole per tutti i problemi di programmazione. E' stato dimostrato il seguente Teorema di JACOPINI - BOHM (1966): -Dato un problema riconducibile alla stesura di un algoritmo; -Se l'algoritmo e' scrivibile facendo uso di istruzioni di salto condizionato ed/od incondizionato; -Allora l'algoritmo puo' essere riscritto facendo uso delle sole strutture SEQUENZIALE ALTERNATIVA - ITERATIVA in opportuno collegamento, eliminando ogni struttura di salto. Questo fatto aiuta notevolmente nella stesura di algoritmi risolutivi di problemi, poiche' in questo modo l'algoritmo risulta di piu' facile comprensione, ed e' molto piu' semplice la manutenzione del programma realizzato da una flow-chart di questo genere. L'argomento trattato verra' ampiamente ripreso e sviluppato, data la sua importanza, nella parte relativa alla programmazione. 14 Quando la durata del blocco iterativo non dipende dal verificarsi di una condizione, ma da due parametri prefissabili a priori (cioe' prima dell'uso del ciclo stesso), si puo' usare il simbolo mostrato in fig. 2.10.a, in cui si inserisce il blocco sequenziale da ripetere all'interno di due delimitatori, che delimitano per l'appunto la durata del blocco di processo. Questa struttura dati prende il nome di CICLO FISSO o semplicemente ciclo, ed è usata molto spesso in informatica. In fig. 2.10.b e' mostrato un algoritmo banale per la stampa dei colori dell'arcobaleno, che sono delimitati dal rosso (estremo inferiore) e dal violetto (estremo superiore). 15 2.1.3 METODOLOGIA DI SVILUPPO DEGLI ALGORITMI: TOP-DOWN SOTTOPROGRAMMI E La risoluzione di un problema concreto puo' in molti casi originare algoritmi notevolmente complessi. La rappresentazione di algoritmi in questo caso puo' non essere di facile comprensione, e vengono richieste nuove tecniche per poter accoppiare al formalismo una semplicita' di uso. Una metodologia di sviluppo degli algoritmi che ha riscosso un notevole successo negli anni e' stato lo sviluppo TOP-DOWN, o ,come si dice in termine italiano, 'sviluppo dall'alto verso il basso'. Il concetto alla base della metodologia TOP-DOWN e' che un problema complesso non va' risolto immediatamente nei suoi passi definitivi, ma viene risolto a grandi blocchi dapprima, scendendo poi nei dettagli particolari in momenti successivi. L'assunto fondamentale e' il seguente: -ogni blocco costituito da un ingresso ed una uscita puo' essere sostituito con un insieme di blocchi equivalenti, il cui ingresso globale e la cui uscita globale siano eguali alle precedenti. Un esempio puo' chiarire meglio questo concetto. Come problema da risolvere mediante algoritmo, ipotizziamo di voler consultare un libro in una biblioteca di 100 volumi. E' necessario conoscere come dato iniziale il titolo del libro. Noto il titolo si puo' passare alla consultazione. Queste due operazioni fondamentali sono indicate nel flow-chart in fig. 2.11.a. In generale, la consultazione non è una operazione semplice, ma potrebbe richiedere più operazioni (richiesta, assenso, diniego, ricerca, ripetizione, ecc.) che possono costituire nel loro insieme una procedura a se' stante. Questa procedura, che rappresenta una sorta di programma nel programma, prende il nome di sottoprogramma, e viene indicata nei flow chart con il simbolo di rettangolo con i bordi ai lati. Senza porci per adesso il problema di come sarà fatta la consultazione, indichiamo che il programma a questo punto eseguirà un sottoprogramma, e lasciamoci il problema specifico per il seguito. Come si potrà intuire, abbiamo spezzato il problema iniziale, apparentemente un po' troppo difficile, in somma di problemi facili. Fig. 2.11.a Questo modo di lavorare prende il nome di 'lavoro a sottoprogrammi' (o, con termine inglese, a subroutine). Con dizione inglese, il sottoprogramma “is a program in the Program”. La procedura di consultazione a questo punto si attua in almeno due fasi: l'addetto alla biblioteca dira' dapprima se il volume e' contenuto nella biblioteca, oppure no. Dopo questa fase, si passera' a cercare il volume nel repertorio (esame degli scaffali). 16 Queste procedure sono descritte nel flowchart di fig. 2.11.b, in cui si e' sostituito il blocco "consultazione" con un insieme di blocchi (blocco condizionale "libro presente?" e subroutine "ricerca libro"). E' importante notare che l'insieme di blocchi fornisce le stesse uscite, a parita' di ingresso, che noi ci aspettiamo ragionevolmente dal precedente blocco di processo. D'altra parte, anche il blocco "ricerca libro" e' ancora da definire con precisione. Possiamo pensare di sostituirlo con una struttura RIPETI-FINCHE' che ci permette di ripetere l'operazione di lettura dei titoli dei libri in biblioteca finche' non viene trovato quello in ricerca. Fig. 2.11.b Questo fatto e' evidenziato nella tavola di flusso di fig. 2.11.c, in cui al blocco "ricerca libro" e' stato sostituito l'insieme di blocchi: "leggi titolo libro" (processo) "titolo giusto ?" (condizionale) che in totale ha lo stesso ingresso, la stessa uscita globale e si comporta nello stesso modo del blocco di processo sostituito. In questo modo si puo' procedere a raffinare ulteriormente un algoritmo, fino a determinare dettagliatamente tutto il suo funzionamento. Il vantaggio che ha questo metodo per la progettazione di algoritmi risolutivi per applicazioni su elaboratori elettronici e' la possibilita' di progettare una procedura all'inizio a grandi linee, senza perdere il controllo della crescita della procedura stessa. In seguito, si passa alla definizione dei dettagli, scendendo sempre piu' nei particolari. 17 La programmazione strutturata viene effettuata ricorrendo ad uno strumento di programmazione particolare, chiamato sottoprogramma (o subroutine ). Un sottoprogramma e' un insieme ordinato e delimitato di istruzioni, che causano l'evoluzione di uno o piu' dati di ingresso in uno o piu' dati di uscita dal sottoprogramma stesso, attraverso una serie di trasformazioni simboliche. I tecnici americani definiscono in sintesi il sottoprogramma come “a program in the Program”. Il dato (od i dati) di ingresso del sottoprogramma e' in genere fornito da un programma principale, chiamato main, in cui il sottoprogramma e' contenuto. Questa "fornitura" di valori e' indicata con i termini trasferimento di variabili, trasferimento di parametri o passaggio di parametri . Il simbolo grafico della subroutine e' indicato, come già visto, nell' esempio di fig. 2.11.a . Il lettore si accorgera' che tale simbolo e' stato usato intenzionalmente per descrivere le situazioni viste in fig. 2.11.a e 2.11.b, dove e' stato esemplificato in breve il concetto di programmazione top-down. Concludendo, la subroutine puo' essere vista come un vero e proprio programma, scritto all'interno di un programma di piu' ampio respiro, che e' detto in generale MAIN (o principale). Nella fig. 2.11.a indichiamo che il blocco "CONSULTAZIONE", contenuto nel programma "BIBLIOTECA", che costituisce il MAIN, e' un insieme di istruzioni (in pratica un programma interno al main, e quindi un "sottoprogramma") che vengono eseguite dopo la fase di ingresso del titolo del libro. Quando si specifica la struttura di questo programma, si "scopre" che e' composto da un blocco condizionale, seguito da un altro insieme di istruzioni, riferentesi alla ricerca del libro, che costituiscono logicamente la procedura "RICERCA LIBRO". La sostituzione del blocco "RICERCA LIBRO" con le azioni relative porta infine alla struttura finale di fig. 2.11.c. Lo sviluppo delle procedure con l'indicazione di sottoprocedure simboliche (che vengono attuate in seguito come subroutine) evita all'inizio una conoscenza troppo approfondita del problema, ed aiuta a comprendere un algoritmo di vasta estensione da un punto di vista globale, per meglio valutarne gli effetti. La scrittura di una procedura con uso intensivo di subroutine prende il nome di programmazione modulare, ed ogni subroutine costituisce un modulo di programma. La procedura finale e' l'insieme ordinato della procedura principale MAIN e di tutte le subroutine. In sintesi: PROCEDURA = {MAIN + SUBROUTINE 1 + SUBROUTINE 2 +...SUBROUTINE N} 18 dove N rappresenta il numero delle subroutine necessarie per la risoluzione dell'algoritmo. I vantaggi principali della programmazione mediante subroutine possono essere sintetizzati come segue: • la stesura di un algoritmo puo' essere fatta in modo globale, senza essere costretti fin dall'inizio a conoscere perfettamente tutti i dettagli di programmazione; • la lettura e la comprensione sia di un flow-chart che di una codifica in linguaggio di programmazione risultano piu' facilmente comprensibili per il minor numero di simboli richiesto; • un algoritmo puo' essere facilmente modificato ed adattato ad altri problemi, col solo cambio di uno o piu' moduli; • il trasferimento dei parametri dal MAIN ai vari sottoprogrammi rende il funzionamento degli stessi in pratica indipendente (da qui il nome modulare). Per via di questo fatto, eventuali errori in uno o piu' moduli possono essere rintracciati piu' facilmente, che nel corso di un unico programma sequenziale di grosse dimensioni. • la disponibilita' di una serie di moduli collaudati e verificati rende possibile la formazione di una biblioteca di subroutine, che l'utente puo' scindere da una particolare applicazione, e fondere in una vera e propria libreria di software, riutilizzandole in altre applicazioni. E' questo il caso, ad esempio, delle procedure generatori di maschere video per l'ingresso facilitato dei dati da terminale video, e generatori di stampe, per la produzione di tabulati su stampante. La programmazione TOP-DOWN rappresenta uno dei punti cardine della programmazione strutturata, per cui si farà ampio riferimento ad essa nella parte dedicata alla programmazione. La programmazione strutturata consiste nello scrivere un programma osservando tre criteri fondamentali: a) progettazione top-down ; b) modularita' ; c) codifica strutturata (uso blocchi SEQUENZIALE - ALTERNATIVO - ITERATIVO). 19 2.1.4 RAPPRESENTAZIONE DI ALGORITMI MEDIANTE PSEUDO LINGUAGGIO La rappresentazione di un algoritmo mediante tavola di flusso e' di reale vantaggio fino a quando conserva due proprieta' fondamentali: • intellegibilita' grafica • possibilita' di tradurre direttamente i simboli in istruzioni di un linguaggio di programmazione Queste proprieta' si mantengono in modo soddisfacente fino a che il problema non cresce molto di complessita' algoritmica. In altri casi, le necessita' grafiche impongono una lettura non facilmente traducibile in istruzioni di programmazione (flow chart su piu' fogli, ecc.). Quando il problema si complica oltre un certo livello, la rappresentazione sotto forma di flow chart perde funzionalita'. Questo fatto e' avvertito particolarmente quando dalla stesura delle operazioni da svolgere non si riesce a passare direttamente (cioe' senza fasi intermedie) alla scrittura delle istruzioni di programma. Si e' imposta, in seguito a queste esigenze, una seconda tecnica rappresentativa, basata sulla formulazione tramite uno pseudo linguaggio di scrittura dell'algoritmo, basato sulla esposizione mediante una serie di regole vicine al linguaggio naturale. Ad esempio, il problema seguente: -stampare un elenco dei primi dieci numeri e dei loro quadrati potrebbe essere esposto con una formulazione seguente: INIZIA PONI contatore = 1 FINCHE' contatore e' minore di 10 ESEGUI STAMPA contatore, quadrato (contatore) FINE. La costruzione di uno pseudo-linguaggio che permetta l'esposizione di un algoritmo deve seguire alcune regole importanti: -Definizione di un insieme di parole chiave (dizionario), che stabiliscono un insieme di azioni possibili. -Ogni azione e' applicabile ad un insieme di dati. In questo modo, risulta possibile descrivere la traformazione che subisce una struttura dati definita all'inizio del programma a mano a mano che viene elaborata dalle azioni definite dall' algoritmo. In definitiva, uno pseudo- linguaggio PL puo' essere visto come un insieme: PL = { parole chiave , relazioni ,sintassi } 20 in cui le parole chiave sono parole riservate per la sola esposizione delle azioni, e non possono essere usate per altri scopi, mentre le relazioni definiscono le strutture dati su cui possono operare le azioni definite dalle parole chiave. Ad esempio, e' una operazione ammessa la formulazione STAMPA contatore mentre non e' ammessa la formulazione STAMPA FINE essendo sia STAMPA che FINE due parole di significato diverso proprie del linguaggio. La sintassi e' relativa alla distinzione tra parole riservate ed operandi (le quantita' da elaborare): in genere, una parola chiave viene contraddistinta da un operando mediante una evidenziazione in grassetto, in sottolineato, oppure in maiuscolo, come nel caso del nostro esempio. Non essendoci una regola precisa, il programmatore e' abbastanza libero di fare la sua scelta in modo piu' congeniale, fermo restando che l'obbiettivo dell'esposizione in pseudo- codice deve essere la chiarezza. Le regole di applicazione sono derivate : -in parte da una formulazione affine al linguaggio naturale, per cui risulta relativamente semplice l'uso di una pseudo codifica per lo sviluppo di un algoritmo; -in parte dalla necessita' di disporre di una formulazione affine ad un linguaggio di programmazione, in modo da rendere la traduzione da pseudo- codifica a programma il meno possibile indolore. Tali regole possono essere, a titolo esemplificativo : A)-Parole chiave- che corrispondono ai passaggi fondamentali dell’algoritmo B)-Operatori- che possono essere aritmetici, logici, di relazione C)-Identificatori- delle variabili o delle risorse dell’algoritmo D)-Indentazione- rientro dei paragrafi che mette in evidenza la gerarchia dell’algoritmo. Analizziamo dettagliatamente questi punti: A)- Le parole chiave debbono essere scritte in lettere. Potrebbero consistere ad esempio nelle seguenti: Inizio, fine Se, allora, altrimenti Per, da , a , passo, esegui Mentre, esegui Ripeti, finchè Caso, di. Leggi ( ) Stampa ( ) L’insieme di tutte le parole costituisce il dizionario dello pseudo-linguaggio. 21 B)- Gli operatori sono: a. Aritmetici: + * / ^ DIV MOD b. Di relazione: c. Logici: < > <= >= >< addizione sottrazione moltiplicazione divisione tra numeri reali elevazione a potenza divisione tra numeri interi resto della divisione tra numeri interi minore maggiore minore o uguale maggiore o uguale diverso (a volte indicato anche con # ) AND OR NOT XOR congiunzione disgiunzione negazione OR esclusivo C)- Gli identificatori assegnano i valori alle variabili a alle risorse. Si esprimono con l’operatore di assegnamento: V 9 V E D)- Indentazione: ogni paragrafo viene fatto rientrare in base all’ordine di esecuzione delle operazioni: SE si verifica una certa condizione ALLORA fai questo ALTRIMENTI fai quest’altro. Lo pseudo-linguaggio e' stato reso, con una notevole interazione teorico- pratica, affine ai linguaggi di programmazione strutturati, e permette di ottenere formulazioni tali da giustificare una rapida conversione in tutti i principali linguaggi come C, PHP, PASCAL e simili. 22 Esaminiamo ora le operazioni da compiere per lo sviluppo di una codifica in pseudolinguaggio (o pseudo- codifica). Queste fasi sono generalmente due: A) - DEFINIZIONE STRUTTURA DEI DATI DA ELABORARE B) - DEFINIZIONE AZIONI ALGORITMO (ELABORAZIONE) Per la fase A), si dispone di un insieme di parole riservate, che permette di definire come deve essere la struttura dei dati da elaborare. Queste parole chiave sono in genere: COSTANTE per indicare un dato che nel corso del programma non variera'. E' una dichiarazione di costante la frase COSTANTE pigreco = 3.1415927 VARIABILE per indicare un dato destinato a variare con l'evoluzione dell'algoritmo. E' una dichiarazione di variabile la frase: VARIABILE i,j,k = intero che designa come numeri interi variabili le lettere simboliche i,j,k. TIPO seguita dalla indicazione del tipo di dato al quale si vuole ricorrere per l'organizzazione dei dati. Ad esempio, la dichiarazione: TIPO stipendio = numero INTERO costituisce un esempio di definizione di tipo del dato "stipendio" come numero intero. La definizione di tipo fa' uso di tipi elementari per costruire tipi piu' complessi. Ad esempio, la frase: TIPO edificio = AGGREGATO di mattoni identifica un dato ("edificio") composto da un insieme di altri dati ("mattoni") di tipo piu' elementare, e correlato ad essi da una parola chiave del dizionario, che permette la costruzione di questo tipo particolare, denominata AGGREGATO. Ogni termine del dizionario deve essere usato con la sintassi che gli e' piu' propria. Questo argomento, accennato per forza di cose in questa sede, sara' ripreso e trattato piu' dettagliatamente nel capitolo dedicato alla struttura dei dati, dove si trova discusso in modo piu' ampio e rigoroso. Per la fase B), si dispone di una serie di costrutti, ognuno relativo ad una azione specifica per la realizzazione di un algoritmo. Per poter programmare una sequenza qualsiasi di algoritmi, si dovrebbe disporre di un numero molto grande di costrutti, teoricamente indefinito. 23 In realta', riflettendo sulla precedente formulazione del teorema di JACOPINI- BOHM, si puo' osservare che, disponendo dei costrutti che permettono l'espressione delle seguenti funzioni: -PROCEDURA SEQUENZIALE -PROCEDURA ALTERNATIVA -PROCEDURA ITERATIVA e' possibile rappresentare ogni algoritmo di interesse pratico. I costrutti che vengono usati per questa funzione sono percio': -ESEGUI <operazione> -RIPETI <azione> FINCHE' <condizione> -FINCHE' <condizione> RIPETI <azione> -SE <condizione> ALLORA <azione> ALTRIMENTI <azione> dove: <operazione> consiste in un insieme di azioni (al limite una); <azione> indica una trasformazione che opera su un dato, rendendolo disponibile in un'altra forma; <condizione> indica la definizione di una condizione logica, in base alla quale e' possibile operare una decisione. E' da notare che questi sono i costrutti piu' usati, ma che non si tratta degli unici, in quanto il programmatore puo' valersi di altre definizioni di costrutti, a seconda degli obbiettivi che vuole raggiungere. Un esempio di uso pratico di programmazione in pseudo- linguaggio lo vediamo applicato al seguente problema, già visto in precedenza a titolo esemplificativo mediante le tavole di flusso: -calcolo area del cerchio, con esclusione del caso in cui il raggio e' nullo. Il problema si risolve conoscendo il raggio (numero reale), e la costante pigreco (3.141593), ottenendo l'area (numero reale). Una codifica in pseudo- linguaggio potrebbe essere: 24 DATI COSTANTE pigreco = 3.1415927; VARIABILE raggio, area = numero reale; INIZIO LEGGI raggio; FINCHE' raggio <> 0 ESEGUI area = raggio * raggio * pigreco; STAMPA area; FINE; FINE; FINE. In questo semplice esempio, osserviamo: -che la parte DATI e' separata distintamente dalla parte programma (algoritmo). Questa seconda parte e' delimitata dai simboli INIZIO e FINE; -che sono state seguite alcune regole sintattiche particolari, quali terminare una frase con il simbolo ; (detto 'terminatore di linea'), porre dopo la parola chiave FINE il punto (.), ecc. Queste regole sono studiate per accostare la formulazione in pseudo- codifica ai linguaggi di programmazione strutturata, come il C ed il PASCAL, e saranno apprese piu' chiaramente dopo l'apprendimento dei linguaggi stessi; -che e' stato seguito un incolonnamento grafico in modo da "strutturare" la formulazione in moduli il piu' possibile indipendenti uno dall'altro. Ad esempio, e' possibile rendersi conto immediatamente che l'insieme di istruzioni usate per il calcolo e la stampa dell' area compete alla sola azione di ESEGUI quando il raggio e' diverso da zero; che l'insieme di istruzioni comprese tra INIZIO e FINE fa' parte del solo algoritmo e non dei dati, e cosi' via. Lo sviluppo di un algoritmo in questa maniera MODULARE costituisce un vantaggio notevole, se si deve sostituire un blocco di un programma con un'altro. Ad esempio, e' sufficiente pensare al cambio del blocco tra INIZIO e FINE con un altro, che effettui il calcolo dell'area del quadrato, conservando la stessa struttura di dati. Basterebbe infatti cambiare il modulo: area = raggio * raggio * pigreco; STAMPA area; con il modulo: area = raggio * raggio; STAMPA ('AREA QUADRATO DI LATO R='); area; per poter usare tutto il resto del programma, senza riscriverlo. La programmazione in pseudo linguaggio richiede, com'e' ovvio dagli esempi, che sia chiaro e definito il problema fin dall'inizio in tutte le sue particolarita', e che tutti i dati del problema siano definiti ed esistenti prima di scrivere la codifica. In genere, lavorando con lo pseudo linguaggio come strumento, non e' attuabile una programmazione di tipo iterativo per approssimazioni successive, resa a volta necessaria dalla non completa disponibilita' di tutti i dati fin dall'inizio. Questa ed altre tematiche di programmazione dei computer saranno piu' chiare dopo aver sviluppato la parte relativa alla programmazione con linguaggi evoluti ed assemblatori. 25 2.2 LA STRUTTURA DEI DATI Proseguendo la trattazione con l'obbiettivo di raggiungere una metodologia di programmazione, ricordiamo il fondamentale assunto di Wirth : ALGORITMI + STRUTTURA DATI = PROGRAMMI In sintesi, un programma P è dato dall' insieme di un algoritmo A ed una struttura dati D secondo la relazione: P={A,D} Abbiamo sufficienti informazioni per quanto riguarda la formulazione degli algoritmi risolutivi di un problema. Gli algoritmi sono relativi ai mezzi ed agli strumenti con i quali vogliamo intervenire sui dati, che costituiscono il problema. Dobbiamo quindi esaminare quali sono le forme che possiamo attribuire ai dati, in modo che risulti piu' facile l'applicazione degli algoritmi ai dati stessi. La forma che prendono i dati prima di essere elaborati tramite gli algoritmi prende il nome di struttura. Supponiamo di dover analizzare gli stipendi di fine mese di una azienda. I numeri 1.545, 1.321, 1.700 costituiscono i dati del problema. La procedura mediante il quale verranno elaborati questi dati costituisce un insieme di regole che deriveremo da algoritmi opportuni. La forma che devono assumere questi numeri per essere trattati con piu' facilita' dagli algoritmi puo' essere quella di lista, tabella, variabile numerica, od altro, a seconda delle nostre esigenze. Nasce quindi l'esigenza di sapere di quali strutture di dati possiamo disporre, per raggiungere questo obbiettivo. 2.2.1 DEFINIZIONI FONDAMENTALI Per approfondire l'argomento relativo alla strutturazione dei dati, e' necessario definire con piu' rigore cosa si intende per "dati" e quali sono le principali grandezze ad essi collegate. Si definisce dato una informazione unitaria, rappresentata in modo univoco in base ad una convenzione. Il dato, nella sua forma elementare, e' un elemento di un insieme. Gli elementi di un insieme sono caratterizzati spesso dall'avere tutti almeno una proprieta' in comune. Ad esempio, il numero 3 costituisce un dato. Si puo' considerare tale dato come elemento dell' insieme dei numeri interi. Tutti i numeri interi sono caratterizzati dalla proprieta' di non avere parti frazionarie. Un insieme di dati puo' essere indicato simbolicamente con una lettera maiuscola racchiusa tra parentesi graffe. Ad esempio, l'insieme dei dati di partenza di un problema si puo' indicare con il simbolo { D }. In informatica, assume particolare rilevanza la possibilita' di mettere in relazione un insieme, come ad esempio l'insieme dei dati di ingresso { D }, con un altro insieme, ad esempio 26 l'insieme delle soluzioni { S }.Due insiemi possono essere messi in relazione tra loro mediante un terzo insieme, detto insieme relazione R , o semplicemente relazione R . Ogni elemento di R e' composto da una coppia ordinata ed univocamente determinata di un elemento del primo insieme ed uno del secondo insieme. La relazione R gode delle seguenti proprieta' : - R ( D X S , cioe' R e' incluso nell'insieme formato da n-ple composte ordinatamente da un elemento di D ed un elemento di S. -una relazione R viene detta "funzione" quando per ogni elemento d | D esiste un solo elemento s | S tale che sia <d,s> | R, cioe' la coppia <d,s> fa' parte della relazione R. Mediante l'insieme R e' possibile realizzare una corrispondenza tra elementi del primo insieme e del secondo insieme: gli elementi sono messi in relazione tra loro. La relazione e' generale per tutti gli insiemi considerati, ed assume quindi un significato simbolico, traducibile in simbolo logico- matematico. In sintesi possiamo dire che una relazione generale pone in relazione tra loro elementi distinti. Il passo successivo consiste nel determinare come distinguere un dato di un certo genere da un altro all'interno di un insieme, ad esempio per distinguere un numero da una stringa di caratteri. Mediante le definizioni precedenti, possiamo costruire una definizione di TIPO dei dati, come segue: TIPO: - Insieme di elementi caratterizzati dalle stesse proprieta'. - Definito da una coppia { D , R } , dove: D = insieme di definizione dei dati aventi un certo tipo (dominio di definizione) R = relazione (o relazioni) intercorrenti tra essi. Se il dominio di definizione e' unico, i dati si dicono SEMPLICI od ELEMENTARI. In questo caso R e' una o piu' relazioni su di esso. Ad esempio, il tipo BOOLEANO conosciuto dall'elettronica digitale costituisce un esempio di tipo elementare, in quanto definito da un insieme di dati: D = {0,1} e le operazioni primitive disponibili sono in genere le seguenti: R = { and, or, not, nand, nor, xor } Se il dominio di definizione e' unico ma R e' una o piu' relazioni n-arie su di esso, i dati si dicono COMPOSITI OMOGENEI. Ad esempio, una stringa lunga 4 caratteri e' un dato omogeneo composito, formato con elementi appartenenti ad un insieme C dato da: C = { A,B,C,D,E,F,G,H,J,K,L,M,N,.....Z} 27 Da questo insieme, si deriva mediante un insieme di relazioni, che puo' essere rappresentato dalle regole della lingua italiana, l'insieme D dato da: D = { ROMA, PANE, REMO, LIRA, BUCO, TIPO, ..... } L'elemento ROMA e' una 4-pla di caratteri; in generale, detto n il numero di caratteri componenti il dato, si parlera' di n-pla di caratteri. L'insieme delle relazioni definibili di solito per questo tipo composito omogeneo e' dato da: R = { accesso a destra della stringa, accesso a sinistra della stringa, confronto tra stringhe } che devono essere n-arie in quanto operano su n caratteri per volta. Se il dominio di definizione e' un insieme di domini di tipi diversi, ed R un insieme di relazioni su di esso, i dati si dicono COMPOSITI ETEROGENEI. Un esempio di questo tipo e' costituito dalle TAVOLE a chiave di accesso, la cui conoscenza va' attualmente oltre lo scopo della presente trattazione. I tipi di dati comunemente piu' usati in informatica sono elementari e compositi omogenei. Si vuole qui accennare ad un tipo di dato particolare, in quanto viene spesso usato come ausilio per l'uso pratico di altri dati. Si tratta del tipo PUNTATORE, che puo' essere definito informalmente come un elemento che collega un indice di riferimento ad un elemento di un insieme ordinato. Mediante l'uso del PUNTATORE risulta possibile, ad esempio, collegare alla serie dei primi 10 numeri naturali i nomi alfanumerici di 10 citta' d' Italia. La struttura esemplificativa dell'uso del puntatore assume allora la forma seguente: 1 2 3 4 PUNTATORE 5 6 7 8 9 10 ROMA MILANO TORINO VENEZIA FIRENZE BOLOGNA TERNI PESCARA NAPOLI PALERMO Associando al puntatore il valore 4, otterremo come risultato quello di "puntare" all'elemento VENEZIA della lista di dati. Associando invece il valore 9 otterremo la scelta dell'elemento NAPOLI dalla lista, e cosi' via. I puntatori risultano molto utili per la gestione di insiemi di dati raccolti in forma di elenco, come pure per indirizzare dei dati localizzati nella memoria di un elaboratore elettronico. 28 2.2.2 STRUTTURE DATI NOTEVOLI In questa parte, il lettore verra' ragguagliato sulle strutture dati piu' frequenti usate nei programmi per elaboratori elettronici. Ricordando le definizioni precedenti, possiamo operare una prima suddivisione essenziale per la struttura dei dati a seconda del tipo. Gli insiemi di tipi fondamentali sono due: A) – ELEMENTARI (o SEMPLICI ) B) – COMPOSITI (o STRUTTURATI ) A) - ELEMENTARI Si tratta di un insieme di tipi che comprende i seguenti tipi terminali (cioè non ulteriormente scomponibili in tipi più semplici): -A.1 -A.2 -A.3 -A.4 INTEGER REAL BOOLEAN CHARACTER Tipo INTEGER: ha come dominio { D } i numeri interi con segno, aventi valore assoluto minore di un limite prefissato (tipicamente -32768 / +32767) dipendente dal linguaggio e dall'elaboratore usato. Le operazioni primitive che possono essere svolte con questo tipo partono dalle fondamentali ( + * - / ) e comprendono un certo numero di funzioni naturali, quali il valore assoluto, ed altre, desumibili dalla consultazione dei manuali. Esempio:il numero 3 e' un tipo intero, come pure il numero -4523. Per i numeri interi esiste il problema della precisione in alcuni calcoli, come 10/3, che pur essendo tra numeri interi forniscono risultati decimali. In questi casi il risultato del calcolo e' approssimato all'intero. Tipo REAL: ha come dominio di definizione { D } un insieme di numeri reali in virgola mobile, che rappresentano un insieme di numeri razionali, limitati da un estremo inferiore ed uno superiore (tipicamente 10 alla 50esima potenza in valore assoluto). I numeri trascendenti, come il numero: 3.1415927.................. ed i numeri periodici provenienti da calcolo, come 10/3, vengono rappresentati con approssimazione numerica dipendente sia del tipo di elaboratore elettronico che dal tipo di precisione dichiarata dall'utente (normalmente semplice o doppia). Tipo BOOLEAN: e' definito su un insieme a due soli valori {D} = {0,1} oppure: { D } = { FALSE , TRUE } 29 e per esso valgono le operazioni primitive AND, OR, NOT, NAND, NOR, XOR, come gia' esemplificato in precedenza. Va' notato che spesso gli operatori NAND e NOR sono assenti, in quanto sono ricavabili mediante relazioni tra gli operatori AND - NOT e OR - NOT. Le operazioni di confronto hanno { D } come dominio dei risultati, nel senso che se formuliamo la relazione: SCRIVI ( 5 < 3 ) l'elaboratore elettronico che ammette tipo booleano (praticamente tutti) rispondera' con la sua logica (poiché è falso che 5 è minore di 3) : FALSE. ( oppure 0 ) Tipo CHARACTER : ha come dominio un alfabeto di caratteri i cui elementi siano rappresentabili in base ad una convenzione (codice ASCII generalmente). Le operazioni definite su questi insiemi verificano condizioni di eguaglianza o diseguaglianza, oppure di precedenza / successione fra caratteri, nonche' la possibilita' di valutare numericamente dei caratteri e convertire dei numeri in caratteri. Oltre a questi quattro tipi, che sono i piu' usati e diffusi, ci sono altri tre tipi di una certa importanza nell'informatica. Esaminiamoli brevemente: Tipo LABEL (ETICHETTA): il dominio { D } e' rappresentato dai numeri interi e/o dai caratteri utilizzabili con l'elaboratore usato. Materialmente, questi numeri hanno il significato di individuare una locazione di memoria ben precisa . Generalmente, il tipo etichetta (o semplicemente etichetta) da' la possibilita' di identificare delle istruzioni ai fini di istruzioni di salto, come nel JUMP del linguaggio ASSEMBLER. In altri linguaggi come PL/1 e PASCAL vengono definite anche delle espressioni numeriche per il tipo etichetta. Tipo PUNTATORE (POINTER): ha un dominio praticamente eguale a quello del tipo etichetta. L'operazione normalmente effettuata e' quella di posizionamento del puntatore. Gia' si e' accennato alle funzioni del puntatore, che "punta" ad una locazione di memoria ben precisa, ed e' quindi utile per la gestione di insiemi di dati organizzati. Nei sistemi evoluti, il tipo pointer e' usato per gestire gli indirizzi della memoria disponibile dei linguaggi interpreti, come il BASIC. Tipo COMPLEX: il suo dominio e' formato da numeri aventi una parte simbolica reale ed una parte simbolica immaginaria. E' diffuso solo nei linguaggi scientifici evoluti (FORTRAN ed alcune versioni avanzate di PASCAL, ad esempio). Per il suo uso e la sua struttura, si rimanda ai manuali del linguaggio. 30 B) COMPOSITI (o STRUTTURATI ) Gli insiemi di dati principali che rientrano in questa categoria sono elencati di seguito: B.1 B.2 B.3 B.4 B.5 B.6 B.7 VETTORI (ARRAY UNIDIMENSIONALI) MATRICI (ARRAY PLURIDIMENSIONALI) STRINGHE LISTE PILE (STACK) CODE ALBERI B.1 VETTORI (ARRAY UNIDIMENSIONALI) Per l'esposizione del tipo VETTORE, corrispondente alla dizione inglese ARRAY ad una dimensione, introdurremo la seguente terminologia: -per identificatore si intende un simbolo associato ad un insieme di dati omogenei e distinti. Ad esempio, per descrivere il contenuto di uno scaffale di libri, potremo assegnare l'iniziale L ad ognuno dei libri stessi ed enumerare l'insieme come segue: L1 L2 L3 L4 L5 L6 L7 L8 L9 L10 L11 L12 ............ Alternativamente, possiamo riferirci all'insieme dei libri con un solo simbolo: L identificando in modo sintetico lo scaffale, ma mantenendo ogni libro la sua distinzione di elemento. -per indice si intende un numero di tipo intero, compreso tra un esetremo inferiore (generalmente 0 od 1) ed un estremo superiore (finito). -per n-pla si intende un insieme di n dati distinti ed omogenei tra loro, come schematizzato in fig. 2.12. -per dimensione del vettore si intende il numero n dei dati distinti ed omogenei. Il VETTORE e' una n-pla di dati di un solo tipo (omogenei) e contrassegnati da un identificatore. Ognuno dei dati e' associato tramite una relazione ad un indice. Quando l'identificatore viene associato ad un indice, identifica in base al valore dell'indice stesso il dato ad esso associato. In sintesi si puo' scrivere per il vettore V : V = { C , R } dove C = { I , D } V i | I → i intero, imin < i < imax V d | D → d : un solo tipo di dati 31 Fig. 2.12 Esempio indicativo di vettore, matrice e valore di componenti. Le operazioni associate ai vettori sono normalmente: accesso al vettore per lettura di un singolo elemento, accesso al vettore per scrittura per singolo elemento. Vengono compiute in un insieme complessivo Dn di vettori (dati + soluzioni), tutti della stessa lunghezza n. Se N e' l'insieme dei primi N interi, e se D e' del tipo intero, reale, booleano o complesso, allora un vettore di dimensione n e' un elemento dell'insieme D n, e si dice componente del vettore un elemento della relazione N -> D, che associa ad ogni intero in esame un dato componente il vettore stesso. Nei casi particolari dei linguaggi assemblatori e macchina, l'indice coincide con l'indirizzo di una locazione di memoria. B.2 MATRICI (ARRAY PLURIDIMENSIONALI) Le matrici, dette anche array od aggregati a piu' dimensioni, costituiscono la generalizzazione del concetto di vettore. Risultano pertanto costituite in modo simile, con la differenza che nelle matrici ad ogni elemento viene associato, tramite una relazione, non piu' un solo indice, ma un insieme { R } di indici. Ogni indice ammette singolarmente un estremo inferiore ed un estremo superiore, per cui la dimensione di una matrice andra' ricavata dall'esame degli indici. Se gli indici sono 2 , si usa la dizione MATRICE RETTANGOLARE o BIDIMENSIONALE. Se gli indici sono 3, si usa la dizione MATRICE SPAZIALE o TRIDIMENSIONALE. Per indici superiori a 3, si specifica direttamente la dimensione. Nell'uso comune, per dimensione si intende il numero di indici diversi, e per ordine si intende il valore massimo di ogni indice. Supponiamo di considerare una matrice definita da due indici, il primo variabile da 1 a 4, ed il secondo da 1 a 3. I termini (4,3) possono essere le dimensioni di una matrice di ordine 2. Spesso si usa un po' impropriamente la dizione 32 "dimensione" anche per "ordine", per cui si potrebbe anche parlare delle dimensioni di una matrice a 2 "dimensioni". Per semplicita' di trattazione, limiteremo la esposizione attuale alle matrici a 2 dimensioni, che sono in pratica di uso molto comune. Da un punto di vista concettuale, si puo' attribuire a tale struttura la stessa organizzazione di una matrice cosi' come e' definita in matematica. Possiamo percio' attribuire alla matrice a 2 dimensioni una struttura teorica seguente: -Un identificatore, che identifica la matrice nel suo complesso; -Due indici (ad es. i, j) che, con il loro campo di variazione, individuano tutti gli elementi della matrice; -Una dimensione, determinata dal prodotto degli indici (ad es. 4x4); -Un tipo omogeneo per gli elementi che costituiscono la matrice. Il tipo di matrici piu' comuni in informatica sono quelle a due dimensioni, per cui ci soffermeremo particolarmente su di esse. MATRICI BIDIMENSIONALI La realizzazione grafica della matrice avviene con il classico modo per righe e colonne, interpretando come: -righe : tutti gli elementi orizzontali, individuati da un indice di riga, che nel nostro caso puo' essere i ; -colonne :tutti gli elementi verticali, individuati da un indice di colonna, che nel nostro caso puo' essere j ; Un elemento generico della matrice viene indicato con una lettera simbolica minuscola, corrispondente all' identificatore (maiuscolo) usato per la matrice nel suo complesso. Questo elemento viene scritto con due suffissi, dei quali il primo individua la riga di appartenenza, ed il secondo la colonna di appartenenza (vedi fig. 2.12.b). Ad esempio, la matrice M con 4 righe e 4 colonne , come nell'esempio precedente, verra' rappresentata con il simbolo: M(4,3) Un suo elemento generico verra' rappresentato con la notazione: m(i,j) in cui i indica la riga di appartenenza, e j la colonna di appartenenza. Ad esempio, la notazione m ( 2, 1) indica l'elemento posto all'incrocio della seconda riga con la prima colonna, cioe' il valore 33. Nello stesso esempio di fig. 2.12.a, il termine m (1 , 3) indica l'elemento 25; il termine m (3,2) indica il valore 8529. Le operazioni che si possono fare con le matrici sono molte, e grande e' la loro importanza in tutto il settore del calcolo tecnico- scientifico. Ricordiamo le principali, che sono: -somma tra matrici -differenza tra matrici -prodotto tra un valore numerico scalare ed una matrice -matrice trasposta ed opposta di una matrice assegnata -azzeramento di una matrice assegnata -prodotto scalare tra matrici -prodotto matriciale -inversione di una matrice quadrata (numero di righe eguale al numero delle colonne) 33 B.3 STRINGHE Il tipo composito stringa e' una n-pla di dati del tipo elementare carattere. L'identificatore di una stringa e' caratterizzato spesso dall'avere un carattere speciale di riconoscimento, come ad esempio il carattere $ nel BASIC. Le operazioni che vengono definite per le stringhe sono normalmente: accesso alla stringa, concatenazione di due o piu' stringhe, esame dei caratteri a destra nella stringa, esame dei caratteri a sinistra, esame dei caratteri interni alla stringa, richiesta della lunghezza della stringa espressa in numero di caratteri. Le operazioni definite possono dare in molti casi un risultato che non conserva in generale la lunghezza n della stringa iniziale, percio' sono definite nell'insieme D * delle stringhe di lunghezza qualunque. B.4 LISTE Le liste costituiscono degli insiemi di dati forniti di etichetta ed aventi formato { d, p }, dove d e' un dato, di tipo elementare o composito, e p un puntatore all'etichetta del dato successivo. Per quanto detto, un elemento e' composto da tre componenti: - ETICHETTA - DATO - PUNTATORE Le liste costituiscono dati compositi. Un esempio di lista semplice puo' essere visto qui di seguito: ETICHETTA DATO PUNTATORE AL PROSSIMO ELEMENTO 1 PASSERI 4 2 FRINGUELLI 5 3 PETTIROSSI 2 4 COLIBRI' 0 5 TORDI 1 Mediante una opportuna gestione del puntatore, osserviamo che abbiamo memorizzato la seguente lista di nomi: PETTIROSSI FRINGUELLI TORDI PASSERI COLIBRI' Il numero del primo elemento reale della lista viene memorizzato in una opportuna locazione di memoria. Nel caso dell'esempio e' il numero 3, che potrebbe essere memorizzato nella variabile PRIMO: PRIMO = 3 Il secondo elemento della lista viene puntato dal puntatore del primo elemento, che contiene il valore 2. Questo valore viene usato come etichetta del secondo elemento della lista, nella 34 zona riservata alle etichette. In questa zona, accanto all'etichetta 2 e' stato memorizzato il secondo elemento effettivo della lista, che e' il dato FRINGUELLI. Questo dato punta a sua volta, con il suo pointer, al dato la cui etichetta e' 5, cioe' TORDI, e cosi' via. All'inizio delle operazioni della lista, deve essere preparato il valore del puntatore iniziale po, con una procedura di assegnazione che prende tecnicamente il nome di INIZIALIZZAZIONE. Questa procedura viene compiuta al di fuori della lista. Nel nostro caso, po = 3. L'ultimo elemento della lista ha un puntatore simbolico, ad esempio 0 , che indica non un altro elemento, ma la fine della lista. Le operazioni che si possono compiere con le liste sono : accesso alla lista (tramite puntatore inizializzato), scrittura e lettura di elementi esistenti, aggiornamento per nuovi elementi, rimozione di un elemento. Le liste sono usate spesso anche per realizzare altre strutture di dati notevoli. B.5 PILE (STACK) La pila e' una lista sequenziale accessibile ad una sola estremita', mediante un registro puntatore ad una locazione di memoria (registro puntatore di pila od anche stack pointer). La pila prende anche il nome di stack o catasta. La lettura e la scrittura avvengono mediante l'ausilio di una locazione di memoria in cui transitano i dati. Il meccanismo di funzionamento e' detto di tipo L.I.F.O. (Last In First Out), che significa alla lettera "il primo elemento che entra e' l'ultimo ad uscire". Il meccanismo di funzionamento della pila e' analogo a quello di una pila di libri: l'ultimo libro in cima alla catasta e' il primo ad essere preso quando si desidera effettuare l'operazione di lettura. Il primo libro, che si trova alla fine in fondo alla catasta, sara' l'ultimo ad essere preso fuori. Per definire la pila da un punto di vista informatico, introduciamo la seguente terminologia: -Una lettera maiuscola, ad. es. L, P, indica un dato; -Una lettera tra parentesi indica il contenuto di una locazione di memoria associata al valore tra parentesi. Con riferimento alla fig. 2.13, si puo' vedere la struttura di una pila, costituita da un insieme contiguo e finito di locazioni di memoria, identificato da un puntatore P, e da una locazione di memoria di transito dei dati, indicata con L. Il contenuto inziale del puntatore di pila (o stack pointer) viene definito all'inizio dal programmatore, mediante una assegnazione di inizializzazione. Le operazioni ammesse sono: -Registrazione di un dato, a cui viene assegnato il primo posto libero sulla sommita' dello stack. Questo valore viene individuato tramite lo stack pointer. In questa occasione, lo stack pointer viene decrementato di 1 per puntare alla prossima locazione utilizzabile di stack. 35 Fig. 2.13 -Lettura e/o cancellazione di un dato, nella posizione indicata dallo stack pointer. In questo caso, lo stack pointer deve incrementarsi di 1 per portarsi alla prima locazione utile, che si trova dopo quella in esame. -Interrogazione sul valore contenuto nello stack pointer, che normalmente e' sottoposto a limitazioni imposte dal programmatore. Nella fig. 2.13 si puo' vedere anche un esempio di stack con dei valori tipici, dove si puo' vedere: L (memoria di transito) posto al valore 10 P (stack pointer) posto al valore 0510 (P) (il contenuto della locazione di memoria associata a P, associato alla locazione 0510, e percio' in pratica riferentesi al valore 52). Nella fig. 2.14.a è indicata la procedura di scrittura di un dato sulla sommita' dello stack, rispetto alla situazione iniziale di fig. 2.13.b. Il dato viene prima scritto su L in transito, quindi viene scritto nella posizione indicata dallo stack pointer. Il vecchio dato 52 e' definitivamente sostituito dal nuovo dato 10 (prima contenuto solo in L). Lo stack pointer si posiziona (automaticamente o su gestione del programmatore) al valore 050F per indicare la prima locazione libera da poter impegnare per la prossima operazione. 36 In fig. 2.14.b e' indicata l'operazione di lettura, sempre rispetto alla situazione iniziale di fig. 2.13.b, in cui il contenuto della locazione puntata dallo stack pointer viene trasferito sulla locazione di memoria di transito L, il cui contenuto originale 10 viene definitivamente perso. Fig. 2.14 Lo stack pointer viene posizionato al valore 0511, per indicare che tutte le locazioni precedenti hanno subito la procedura di lettura, e che si sta' procedendo verso il basso per estrarre altri elementi della pila, fino al primo originariamente memorizzato. Questo meccanismo giustifica la dizione L.I.F.O. precedentemente introdotta. 37 B.6 CODE La coda e' una lista sequenziale a due estremi A-B, in cui e' accessibile: -un estremo A per l'operazione di inserzione elementi; -un estremo B distinto dal precedente per l'operazione di estrazione (lettura) di un elemento dalla coda. Il meccanismo descritto viene indicato tecnicamente con la sigla F.I.F.O (First In First Out), che alla lettera significa "il primo elemento ad entrare e' il primo elemento ad uscire". Le operazioni ammesse, oltre a quelle citate, sono quelle di interrogazione, per determinare se la coda e' vuota o piena di elementi. Una coda e' un tipo composito, costituito dal seguente insieme: C = { L , PX , PY } in cui : C coda (di dati) L lista di elementi omogenei PX puntatore di inizio coda PY puntatore di fine coda Fig. 2.15 Un esempio grafico dell' organizzazione di una coda e' visibile in fig. 2.15.a. In fig 2.15.b e' visualizzata la struttura di un elemento completo della coda, costituito da una parte di dato vero e proprio (informazione) ed una parte puntatore contenente l'etichetta di identificazione del prossimo elemento della coda. 38 B.7 ALBERI La struttura ad albero prevede la composizione di dati, anche non omogenei, con relazioni gerarchiche tra sottinsiemi di dati. La nuova terminologia "gerarchico" ha il significato che alcuni di questi sottinsiemi hanno una priorita' di accesso rispetto ad altri, nel senso che per accedere ad una informazione contenuta in posizione i bisogna attraversare una serie di informazioni grarchicamente prioritarie, situate in posizioni i-1, i-2, i-3 . La migliore rappresentazione grafica di strutture ad albero si ottiene mediante i grafi orientati. Un grafo e' un insieme di dati ordinati e correlati tra di loro. Si rappresentano graficamente racchiusi in cerchi, detti nodi, raccordati tra loro mediante segmenti orientati. Il verso di orientamento dei segmenti puo' essere entrante nel nodo (freccia verso il nodo) od uscente dal nodo (freccia uscente dal nodo). L'albero e' descritto in termini di elementi particolari, dei quali diamo la terminologia: Fig. 2.16.a RADICE : nodo da cui deriva tutto il resto della struttura da albero. Graficamente, si invidua dalla proprieta' di avere solo segmenti uscenti. PERCORSO: insieme di segmenti orientati e di nodi ad essi collegati, delimitato tra due nodi assegnati. RAMO : segmento compreso tra due nodi. LIVELLO : il grado gerarchico che compete ad un nodo contenente una informazione. L'ordine gerarchico e' solitamente ascendente ,come indicato in fig. 2.16.a, nel senso che un nodo con gerarchia 0 predomina su un nodo con gerarchia 1, e cosi' via. FOGLIA : elemento terminale di un percorso, dove normalmente si trova il nodo che effettivamente contiene l'informazione da ritrovare. 39 FRATELLO (o GEMELLO) : nodo interno all'albero, avente in comune con altri nodi, che sono denominati anch'essi fratelli, lo stesso numero di livello. PADRE: nodo interno all'albero, avente segmenti uscenti verso altri nodi. FIGLIO : nodo interno all'albero, avente segmenti DA ALTRI NODI collegati ad esso. Con il termine RADICE si indica genericamente tutto l'insieme dei nodi (e quindi dei dati) costituenti l'albero. Il dato vero e proprio e' situato nelle foglie: i nodi intermedi hanno il compito di individuare il percorso (PATH) per arrivare all'informazione situata nel nodo- foglia. Con riferimento alla fig. 2.16, il percorso di ritrovamento di un dato puo' dare luogo a spostamenti orizzontali e verticali. In fig. 2.16.b troviamo un esempio di rappresentazione di informazioni collegate ad albero, relative ad un esempio banale di un docente che desidera sapere quali materie può insegnare nella scuola. L'albero visualizzato (per forza di esempio stringato e non rigorosamente aderente alla realtà) parte con un nodo RADICE detto DOCENTE (informazione), al livello 0, quindi con un primo elenco di scuole possibili (livello 1), con tre nodi fratelli al livello 1, quindi con altri 6 nodi (livello 2) raggruppati per scuola a due a due, e per ultimo sono specificati (livello 3) dei rami asimmetrici come figli di fisica e matematica del livello 2. Una rappresentazione su carta della struttura seguente, fatta con lo scopo di servirsene in un programma, potrebbe essere la seguente: 40 LABEL DATO FRATELLO FIGLIO 1 DOCENTE 0 300 300 ITT 310 500 310 LICEO 320 600 500 ITALIANO 510 0 320 MEDIA 0 700 510 FISICA 600 800 600 ITALIANO 610 0 610 MATEMATICA 700 810 700 MATEMATICA 710 0 710 ITALIANO 0 0 800 TERMOLOGIA 810 0 810 FISICA 0 0 in cui ogni dato e' identificato da una etichetta che lo contrassegna. Due puntatori (chiamati spesso anche MARKER) individuano se il nodo ha fratelli (altri nodi di pari livello) o figli (nodi di livello gerarchico inferiore). Si indica l'assenza di nodi fratelli o figli con uno zero sul puntatore corrispondente. In questo modo il docente che vuole accedere all'informazione "termologia" e che insegna in un ITIS, deve specificare il percorso: Livelli: DOCENTE ITT FISICA TERMOLOGIA 0 1 2 3 In termini di label, le operazioni algoritmiche da compiere sarebbero: 1. Esame della radice dell'albero DOCENTE; 2. Estrazione del figlio con label 300; 3. Confronto dell'elemento ITT di livello 1 cosi' ottenuto con l'elemento di livello 1 della lista specificata, cioe' ITT; 4. Viene trovata eguaglianza, per cui si prende in esame il nodo figlio 500 e si scarta il nodo fratello 310; 5. Estrazione dell'elemento 500 - ITALIANO (livello 2) e confronto con il livello 2 del percorso indicato, cioe' con FISICA; 6. Il confronto e' negativo, per cui si passa ad estrarre il valore del nodo fratello indicato, cioe' 510; 7. Viene ripetuto il confronto con il nodo fratello trovato, cioe' con 510 - FISICA, e l'eguaglianza viene verificata; 8. Estrazione del nodo figlio di FISICA, che ha la label 800; 9. Confronto tra l'informazione TERMOLOGIA (livello 3), corrispondente alla label 800, e l'informazione al livello 3 del percorso originario. 41 Il confronto e' positivo, per cui viene rilasciato l'accesso alle informazioni. La struttura dei sistemi operativi evoluti, quali UNIX ed MS-DOS, prevede una struttura analoga agli alberi per la gestione del sistema a directory, per gestire tutte le attivita' di accesso- rilascio da disco. Ad esempio, per caricare in memoria il programma TERMOLOGIA.EXE, registrato dalla directory principale DOCENTE con il tramite delle sottodirectory ITT e FISICA, si dovrebbe impartire il comando : PATH\RADICE\ITT\FISICA\TERMOLOGIA essendo il simbolo \ TERMOLOGIA il separatore tra nodi dell'albero in esame. 42 2.2.3. IL TIPO FILE Riteniamo utile definire in questa sede un insieme di dati particolare, come il tipo file, che costituisce con le sue caratteristiche un tipo a se' stante. Il tipo FILE (con dizione italiana corretta si traduce ARCHIVIO) costituisce un insieme composito di dati, usato per il trasferimento di informazioni lungo l'esecuzione di un programma. In pratica si tratta di una struttura che contiene i dati (informazioni) su cui operano gli algoritmi di un programma. Esso e' definito come struttura dati formata da una relazione n-aria su n dominii di dati, e da operazioni di : -identificazione del file, tramite un identificatore opportuno (nome del file); -definizioni degli n dominii ; -accesso in lettura o scrittura ; -inserzione od eliminazione di elementi nell'archivio; -richiesta di informazioni, quali esistenza di elementi, lunghezza dei dati, ecc. Come esempio di file consideriamo il seguente insieme di stringhe, relativo ad un file con identificatore BIBLIOTECA: GIOVANNI ROSSI STORIA D'ITALIA EDIZIONI BARBERA Questo insieme di stringhe costituisce, dalla prima (GIOVANNI) all'ultima (BARBERA), un insieme di dati relativi ad un libro, e tutti riferentesi allo stesso soggetto logico (GIOVANNI ROSSI). I domini di definizione della relazione sono in questo caso l'insieme di tutte le altre stringhe contenenti tutti gli altri libri in biblioteca, e simili come struttura alla stringa riportata (nomecognome- titolo- editore). La stringa dell'esempio costituisce un elemento della relazione, formato da componenti allocate in una n-pla di 66 caratteri (spazi bianchi compresi). La relazione { R } sara' un insieme di elementi simili. Un sottoinsieme dell' insieme FILE, organizzato come nell'esempio, cioe' con informazioni tutte riferite allo stesso soggetto logico, prende il nome di RECORD o REGISTRAZIONE. Le parti logiche in cui e' scomponibile il record, e cioe' nel nostro caso nome, cognome, titolo, editore, vengono detti CAMPI. Ogni campo e' ulteriormente scomponibile in BYTE, che rappresentano i caratteri (ad es. G,I,O,V,A,N,N,I per il primo campo) che costituiscono il campo. I file sono classificabili in base all'uso in due tipi fondamentali: -FILE INTERNI AL PROGRAMMA, di uso molto facile e molto veloci come risposta, ma usati solo quando i dati sono pochi, in quanto richiedono molto uso della memoria centrale dell' elaboratore, e scarsamente flessibili da un punto di vista operativo. -FILE ESTERNI AL PROGRAMMA, residenti normalmente su supporti magnetici (floppy-disk, hard-disk, tamburi e cilindri magnetici, ecc.), atti a contenere elevate quantita' di informazioni, ed a gestire operazioni di accesso - elaborazione di tipo sofisticato senza richiedere spazi di memoria eccessivi alla memoria centrale. 43 La gestione dei files costituisce argomento di grande importanza nella programmazione, per cui questo argomento verra' ripreso e sviluppato ampiamente nella parte relativa al software con i linguaggi evoluti. 44 2.3 APPLICAZIONI DEGLI ALGORITMI 2.3.1 INTRODUZIONE Una volta noti gli algoritmi e la struttura dei dati, si puo' passare all' analisi di problemi- tipo risolti a scopo didattico. Lo scopo di questa parte e' quello di familiarizzare il lettore su argomenti di uso comune in informatica. Per ogni problema, verra' dato in piu' casi lo sviluppo doppio in flow- chart ed in pseudolinguaggio, in modo che sia possibile verificare la convenienza dell'una o dell'altra struttura. In casi in cui vi sara' in pratica eguaglianza di effetto, sara' presentato quello ritenuto graficamente piu' adatto, senza insistere eccessivamente sulla convenienza di rappresentare gli algoritmi in pseudo- codice, od in flow chart. Per la codifica in pseudo- linguaggio, e' necessario introdurre l'insieme PL = { parole chiave, relazioni, sintassi } Formuliamo percio' un dizionario delle parole- chiave, con l'obbiettivo di ridurlo al minimo essenziale; per le pseudo- codifiche, useremo i seguenti termini: PROGRAMMA <nome>, per indicare il nome del programma in modo da vere una indicazione di massima delle sue funzioni; DATI <struttura>, per indicare l'elenco della struttura dei dati su cui opererà l'algoritmo. INIZIO, per indicare l'inizio della parte algoritmo; FINE , per segnalare la fine della parte algoritmo, ed in generale il termine di azioni anche parziali svolte dall'algoritmo, per maggior chiarezza di esposizione; LEGGI <dato> per indicare una acquisizione dati da terminale; PONI <variabile> =<espressione> per indicare l'assegnazione di un particolare valore ad una variabile specificata FINCHE' <condizione> RIPETI <azione> per indicare la ripetizione di una azione fino al persistere della condizione. Se la condizione non si verifica al primo passaggio, non viene mai effettuata la sequenza di azioni specificata dopo il RIPETI. RIPETI <azione> FINCHE' <condizione> per indicare la ripetizione di una azione fino al persistere della condizione. A differenza della presente formulazione, se la condizione non e' verificata anche al primo passaggio, almeno una volta la sequenza di azioni viene eseguita. PER <variabile>:=<espressione iniziale> FINO A <espressione finale> ESEGUI <azioni> , per eseguire un ciclo di azioni compreso tra due estremi iniziali e finali, quando non c'e' bisogno di test nel ciclo. Esempio: Per stampare la tabella dei quadrati dei primi 10 numeri interi. PER I=1 FINO A 10 ESEGUI la STAMPA del QUADRATO (I) SE <condizione> ALLORA <azione> ALTRIMENTI <azione> per effettuare un test di condizionalita' come indicato nella parte relativa alla struttura dei dati; 45 STAMPA <dato> per visualizzare su terminale il valore di un dato, sia esso sotto forma di costante, o di variabile, come si vedra' dagli esempi svolti nel seguito. 2.3.1 CONTATORE Obbiettivo dell'algoritmo: realizzare un automatismo che visualizzi su terminale video il conteggio del numero di volte che viene battuto un numero sulla tastiera. Inserendo un valore condizionale apposito, ad esempio 0, il procedimento termina. Piano di prova:dalla tastiera viene battuto il numero 36; su video si legge 1; dalla tastiera si batte il numero 78; su video si legge 2; dalla tastiera si batte il numero 7; su video si legge 3; dalla tastiera si batte il numero 0; il programma termina. Struttura dei dati: dal punto di vista della funzione, si ricorre ad un ausilio di comune impiego in informatica quando si ha necessita' di un conteggio. In questi casi si usa una variabile semplice numerica (di tipo intero) che si incrementa di 1 ogni volta che viene effettuato il passaggio attraverso il ramo di programma che la contiene. Tecnicamente questa variabile prende il nome di contatore. Il contatore richiede una precauzione d'uso importante: prima di essere usato deve essere posto al valore 1 (fase di inizializzazione). In questo caso, questo ramo di programma conterra' la lettura da terminale del numero in esame. Dal punto di vista del tipo, avremo bisogno dei seguenti dati: -contatore (numero intero) -numero introdotto da tastiera (numero reale) La formulazione in pseudo- linguaggio ci puo' fornire, come esempio risolutivo: PROGRAMMA contatore; DATI contatore:intero; numero:reale; INIZIO PONI contatore :=0; LEGGI numero; FINCHE' numero <> 0 RIPETI contatore :=contatore + 1 STAMPA contatore, numero; FINE; FINE. Un esempio di flow-chart per paragone e' visibile in fig. 2.17. 46 Fig. 2.17 2.3.2 SOMMATORE Obiettivo: Effettuare la somma di una serie di numeri introdotti da tastiera. La somma deve terminare quando si immette da tastiera il numero 0. Piano di prova: si immette da tastiera il numero 3; si legge su terminale il valore 3; si immette da tastiera il numero 7; si legge su video il numero 10; si immette da tastiera il numero 23; si legge su terminale il valore 33; si immette da tastiera il valore 0; la procedura ha termine. Struttura dei dati:per questa funzione si usa una variabile ausiliaria, denominata sommatore. Il sommatore contiene, tramite una somma situata all'interno di un ciclo iterativo, la somma dei numeri che vengono incontrati lungo questo ramo di procedura. Come nota importante per l'uso pratico dei sommatori, e' necesario prevedere all'inizio, subito prima del ciclo di somma, l'azzeramento del sommatore (fase di inizializzazone del sommatore). Questo azzeramento di inizializzazione evita che la variabile usata sia rimasta caricata con dati precedenti, o provenienti da altre parti di programma, e origini quindi errori di calcolo nel sommatore. Viene presentato un possibile algoritmo per la soluzione di questo problema: PROGRAMMA sommatore; DATI sommatore, numero: reale; INIZIO PONI sommatore:=0; FINCHE' numero <>0 RIPETI LEGGI numero; PONI sommatore := sommatore + numero; STAMPA sommatore; FINE; STAMPA sommatore; FINE. 47 2.3.3 RICONOSCIMENTO NUMERI PARI DA DISPARI Obiettivo: immettere un numero da tastiera, controllare se e' pari o dispari, quindi stampare su terminale il risultato del test PARI oppure DISPARI. Piano di prova:si immette 3 da tastiera; si stampa DISPARI su terminale; si immette 18; si stampa PARI; si immette 0; la procedura termina. Struttura dei dati: e' necessario un dato per memorizzare il valore immesso da tastiera, piu' un dato per organizzare il confronto tra la meta' del numero entrato e la parte intera. Un esempio di algoritmo potrebbe essere il seguente: PROGRAMMA riconoscimento numeri pari e dispari; DATI I,N : reali; INIZIO LEGGI N; FINCHE' N<>0 RIPETI N=N/2; I=parte intera di N; SE I=N ALLORA STAMPA 'Numero pari' ALTRIMENTI STAMPA 'Numero dispari'; FINE; FINE; FINE. 48 2.3.4 CARICAMENTO DELLE COMPONENTI DI UN VETTORE Obiettivo: scrivere un programma che permetta l'ingresso in memoria di 130 elementi, memorizzabili in un vettore (tali elementi potrebbero essere articoli di magazzino o grandezze fisiche da elaborare). Struttura dati: e' necessaria una costante per il valore 130, ed un vettore, che verra' usato con la parola chiave ARRAY (equivalente ad AGGREGATO) non compresa nel precedente dizionario. Il vettore verra' gestito con un puntatore, che sara' costituito dalla variabile 'indice', che sara' un numero intero. Pseudo-codifica: l'unica cosa di rilievo da notare e' la dichiarazione di vettore, eseguita ponendo tra parentesi quadre il numero di componenti precise, e facendo seguire il tipo di dati (numeri reali, stringhe, ecc.) che verranno caricati nelle componenti del vettore. PROGRAMMA caricamento di un vettore; DATI nmax : costante; elenco = ARRAY [1..130] di reali; INIZIO PONI nmax :=130; PONI indice :=0; FINCHE' indice <= 130 RIPETI STAMPA ('Componente n.=',indice); LEGGI elenco [indice] FINE; FINE. indice: intero; 2.3.5 LETTURA COMPONENTI DI UN VETTORE Struttura dati : si precisa che N1 e' l'indice dell'elemento iniziale da rivedere, N2 l'indice dell'elemento finale, A e' l'indice dell'elemento che si vuole correggere, e J il puntatore del vettore. PROGRAMMA lettura e correzione componenti di un vettore; DATI N1,N2,J,A: interi; elenco : ARRAY [1..130] di reali INIZIO STAMPA ('LETTURA / CORREZIONE DATI DI UN VETTORE'); PONI J:=0; LEGGI N1; LEGGI N2; FINCHE' N1 <= N2 RIPETI STAMPA ('Elemento n.',J); STAMPA elenco [J]; FINE; STAMPA ('Quale correggi ?'); LEGGI A; FINCHE' A<>0 RIPETI STAMPA ('Nuovo elemento ='); LEGGI elenco [A]; FINE; FINE. 49 2.3.6 ORDINAMENTO DELLE COMPONENTI DI UN VETTORE Obbiettivo: ordinare in modo crescente un vettore le cui componenti siano state immesse in memoria come numeri reali, in modo assolutamente casuale. Questa tematica rientra in una grande branca applicativa dell'informatica, che va' sotto il nome di SORT od ORDINAMENTO, e si affianca alle procedure di SEARCH, o RICERCA, e MERGE, o FUSIONE. Data la loro importanza tecnica, queste tecniche verranno approfondite nella parte relativa alla programmazione su computer. Piano di prova: dato un vettore con cinque componenti assegnate Componente 1 2 3 4 5 Contenuto 5 11 2 8 3,3 ottenere un vettore con le componenti ordinate in modo crescente: Componente 1 2 3 4 5 Contenuto 2 3,3 5 8 11 Pseudo- codifica: si suppone il vettore precedentemente caricato con altra procedura. PROGRAMMA ordinamento di un vettore assegnato DATI elenco: ARRAY [1..130] di reali; I,J,N:interi; scambio:reale; INIZIO PONI N:=130; PER I:=1 FINO A N-1 ESEGUI PER J:=I+1 FINO A N ESEGUI SE elenco[J]<elenco[I] ALLORA scambia elenco[J] ed elenco[I]; FINE; FINE; FINE. 2.3.7 RICERCA DI UNA COMPONENTE DI UN VETTORE Obiettivo: ricercare se un valore immesso da tastiera, e denominato K, e' contenuto tra le componenti di un vettore assegnato. Il metodo presentato e' il piu' semplice, basato sull'esame sequenziale di tutte le componenti, fino a trovare quella interessata. Se non viene trovata, non viene stampato il messaggio di indice trovato. Pseudo-codifica: PROGRAMMA ricerca sequenziale in un vettore DATI I,N,K:reali; elenco:ARRAY[1..130] di reali; INIZIO LEGGI K; PONI N:=130; I:=0; FINCHE' I<=N RIPETI I:=I+1; SE elenco[I]:=K ALLORA STAMPA ('Indice trovato=',I) FINE; FINE. 50 2.3.8 METODI NUMERICI: RISOLUZIONE APPROSSIMATA DI UNA RADICE QUADRATA Obiettivo:eseguire l'estrazione di radice quadrata con metodo approssimato, senza ricorrere alle funzioni dirette (tipo SQR o SQRT) presenti nei vari linguaggi, con una precisione di 1 parte su 1000. Viene impiegato l'algoritmo di Newton, basato su iterazione per approssimazioni successive. L'algoritmo, proposto per la radice quadrata, puo' essere esteso alla radice n-esima di un numero, e diventare percio' di utilita' generale. Piano di prova: introdurre 4 da tastiera; leggere 2 su video; introdurre 5 su video; leggere 2.236 su video; introdurre 189 su video; leggere 13.748 su video; introdurre 0 da tastiera; la procedura termina. Struttura dei dati: prevediamo un numero A introdotto da tastiera, il numero N di cui A e' il quadrato (per cui N e la radice quadrata di A),ed un numero NT da usare come variabile di transito. Tutti e tre sono numeri reali. L'algoritmo, basato su iterazione per approssimazioni successive, fa' uso della relazione: N2 = A da cui si puo' porre N = A/N . Come si vede, nella soluzione compare anche l'incognita N, il che rende impossibile risolvere l'equazione corrente con metodi tradizionali. Si puo' pero' pensare di risolverla con metodi numerici approssimati, ipotizzando una soluzione arbitraria per il valore N a secondo membro, sostituendo tale valore a secondo membro della equazione corrente, dove lo chiameremo NT, ed ottenendo un valore di N, che possiamo assumere come soluzione se soddisfa determinate condizioni. L'equazione diventa allora: N = A / NT Il problema nasce per scegliere un valore opportuno per NT. Supponiamo di aver scelto arbitrariamente il valore N T, con un numero reale qualsiasi. I casi possibili sono tre: -NT scelto ed N sono abbastanza vicini come ordine di grandezza; -NT e' molto maggiore di N; -NT e' molto minore di N; In tutti e tre i casi, il modo migliore di effettuare la stima consiste nel fare la media aritmetica tra il valore NT ed il valore A/NT, per compensare la possibilità di sbaglio nella scelta. Ad esempio, cercando la radice quadrata di 5, possiamo ipotizzare: -di aver scelto 2; -di aver scelto 5 (cioe' il numero stesso); -di aver scelto 0.432; Corrispondentemente, una stima migliore viene fatta con le medie: -media (2) = (2 + 5/2 ) / 2 = 2.25 -media (5) = (5 + 5/5) /2 =3 -media (1) = (0.432 + 5/0.432) = 6 51 Svolgiamo un calcolo completo con il terzo valore, che apparentemente e' il piu' lontano dalla soluzione, che e' 2.236. Il valore della radice sembra essere allora: N = 5 / 6 = 0.833 Questo valore elevato al quadrato ci fornisce circa 0.695, troppo lontano dal valore atteso 5. Si usa allora questo valore 0.833 per calcolare un nuovo valore medio: NT = (0.833 + 5/0.833) /2 = 3.41 e si usa questo valore per un nuovo calcolo di N: N = 5 / 3.41 = 1.46 Questo valore, elevato al quadrato, fornisce 2.132, abbastanza vicino a 5 rispetto al precedente, ma ancora lontano per la precisione voluta. Si usa allora di nuovo, iterando la procedura precedente, questo valore per trovare una nuova media: NT = (1.46 + 5/1.46 )/2 = 2.44 si usa questo valore per un nuovo calcolo di N: N = 5 / 2.44 = 2.047 Una verifica su questo valore al quadrato fornisce, per la differenza D tra valore atteso e valore calcolato: D = ( 5 - 4.191) = 0.809 Proseguendo nelle iterazioni come segue, si ottiene: NT = (2.047 + 5/2.047)/2 = 2.244 N = 5 / 2.244 = 2.227 D = (5 - 4.961) = 0.039 NT = (2.244 + 5/2.244) = 2.236 N = 5 / 2.236 = 2.236 D = (5 - 4.9997) = 0.0003 L'ultima differenza ci segnala che la differenza tra il valore trovato ed il valore teorico e' tanto piccola (minore di una parte corrispondente allo 0.006 %) da poter fare assumere come radice quadrata di 5 il numero approssimato 2.236. Questo procedimento e' utile, a mano, quando si deve estrarre la radice quadrata di valori come 771 oppure 53,4. Per disporne come algoritmo, diamo un esempio di pseudo-codifica: PROGRAMMA calcolo approssimato di radici quadrate; DATI NT,N,D,A : reali; INIZIO LEGGI A,N; PONI D := 1.00; FINCHE' D > 0.001 RIPETI PONI NT=(N+A/N) / 2; PONI N =(A/NT); PONI D =(A - NT * NT); FINE; STAMPA N FINE. 52 2.3.9 QUADRATO DI UN NUMERO CON ALGORITMO DEL BINOMIO OBIETTIVO: Effettuare il quadrato di un intero a due cifre, sommando i prodotti parziali secondo la regola della potenza di un binomio, invece che con la somma di prodotti successivi, con cui viene effettuata normalmente. Matematicamente, si tratta di determinare una relazione R tale che, dette "a" e "b" le cifre componenti il numero, si abbia: 2 (ab) → 2 (a + b) Piano di prova: Ipotizziamo di voler calcolare il quadrato di 18 (a=1; b=8) con le regole solite dell' aritmetica. Otterremmo: 18 x 18 = ─── 144 18─── 324 Lo stesso risultato si puo' ottenere con lo sviluppo: 18 ─── 64 ← Quadrato di "b" (8) 16- ← Doppio prodotto di "a" per "b" ( 2 · 1 · 8 ) 1-- ← Quadrato di "a" (1) ─── 324 A questo risultato si arriva, a differenza del metodo tradizionale, con termini singoli con valori minori dei termini precedenti, consentendo di memorizzare i dati intermedi in un formato piu' corto. Questo metodo e' generalizzabile ad un numero "n" di cifre, e puo' essere dimostrato rigorosamente. Nell' ambito di questo testo, ci riferiremo ad esso con il nome convenzionale di prodpot (prodotto per potenze). Pseudo-codifica: si usa una variabile intera QUAD per totalizzare il risultato finale. PROGRAMMA prodpot; DATI A,B,QUAD:INTERI; INIZIO PONI QUAD=0; LEGGI A; LEGGI B; QUAD=B*B; QUAD=QUAD+A*B*2*10; QUAD=QUAD+A*A*100; STAMPA QUAD FINE. 53 Come nota per coloro che vogliano realizzare un programma C (o PASCAL) con questo algoritmo, si fa' presente: -l'introduzione su video di A,B e' sequenziale, come fosse un unico numero, con uso di due istruzioni consecutive READ A; READLN B. -le moltiplicazioni per 10 e per 100 sono relative al fatto che i numeri in esame rappresentano rispettivamente le decine e le centinaia del valore finale. 54 ESERCITAZIONE SU ALCUNE APPLICAZIONI In questa esercitazione verranno esaminati alcuni semplici problemi, che verranno modellizzati e tradotti in algoritmi pseudocodificati e rappresentati attraverso diagrammi a blocchi. Problema: dati due numeri , trovare il maggiore. Analisi del problema: -Dati di ingresso: numeri A,B -Dati di uscita: maggiore dei due -Procedimento da seguire: se A>B stampare A, altrimenti B. Pseudocodifica: Problema Maggiore INIZIO LEGGI (A,B) SE A>B ALLORA STAMPA ( A) ALTRIMENTI STAMPA (B) FINE. Diagramma a blocchi: 55 Problema: leggere 100 numeri interi e stamparne la somma. Analisi del problema: -Dati di ingresso: 100 numeri interi -Dati di uscita: somma dei 100 numeri -Procedimento: leggere un numero e sommarlo ai precedenti, fino a che i numeri letti sono 100. Pseudocodifica: Programma Somma INIZIO conta somma 0 0 RIPETI LEGGI (numero) somma somma + numero conta conta + 1 FINO A CHE conta > 100 STAMPA somma FINE. 56 Diagramma a blocchi: 57 CAPITOLO 2 - ESERCIZI DI VERIFICA E2.1 - Determinare il tipo dei dati forniti negli esempi seguenti: (a) 3.14 (b) "A" (c) 702 (d) La risposta M od F su un questionario, alla casella (e) AX(2,3) (f) "PARIGI" (g) V(5) (h) 7 (i) "7" MASCHIO / FEMMINA E2.2 - Scrivere degli algoritmi finiti, eseguibili e non ambigui che possano risolvere i seguenti problemi: -visualizzare un elenco dei primi dieci numeri interi e dei loro quadrati; -visualizzare per quindici volte la lettera 'X'; -calcolare la somma dei numeri 3, 19 e 7. E' consigliata, in questo come nei successivi esercizi 2 e 3, la codifica in pseudo- linguaggio. E2.3 - Determinare un algoritmo che divida un numero intero A per un numero intero B, escludendo il caso in cui il numero B sia eguale a zero, e considerando la divisione intera, con resto e segno. Determinare inoltre: -il numero di passi (finito); -le risorse necessarie per l'esecuzione; -la non ambiguita' dei singoli passi: deve essere possibile una sola interpretazione dei passi stessi. E2.4 - Scrivere il flow- chart relativo agli algoritmi degli esercizi precedenti. E2.6 - Scrivere una codifica in pseudo- linguaggio di un algoritmo che sommi tutti i numeri interi compresi tra 15 e 25. 58