capitolo 2 : algoritmi e strutture dati

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