RICORSIONE & ITERAZIONE Riconsideriamo l’esempio del Massimo Comun Divisore visto tempo addietro: m, se m=n MCD(m, n-m), se m<n MCD(m, n) = MCD(m-n, n), se m>n
RICORSIONE & ITERAZIONE Questo esempio era stato trasposto nella funzione seguente: int mcd(int m, int n){ return (m==n) : m ? (m>n) ? mcd(m-n, n) : mcd(m, n-m); } L’esempio era particolare perché il risultato veniva sintetizzato in avanti, anziché all’in- dietro come nei processi ricorsivi.
RICORSIONE & ITERAZIONE Ogni processo computazionale che computi “in avanti”, per accumulo, costituisce una ITERAZIONE ossia è un processo computazionale iterativo. Ogni soluzione sintatticamente ricorsiva che dia luogo a un processo computazio-nale iterativo costituisce una ricorsione solo apparente: una ricorsione tail.
RICORSIONE & ITERAZIONE La caratteristica fondamentale di un processo computazionale ITERATIVO è che a ogni passo è disponibile un risultato parziale dopo k passi, si ha a disposizione il risultato parziale relativo al caso k questo non è vero nei processi computazionali ricorsivi, in cui nulla è disponibile finché non si è disgregato il problema fino al caso elementare.
IL RAGIONAMENTO ITERATIVO Si basa sulla disponibilità di una variabile, detta accumulatore, destinata a esprimere in ogni istante la soluzione corrente Si imposta identificando quell’operazione di modifica dell’accumulatore che lo porta a esprimere, dal valore relativo al passo k, il valore relativo al passo k+1.
ESEMPIO: CALCOLO DEL FATTORIALE Definizione: n! = 1 * 2 * 3 *… * n Detto vk = 1 * 2 * 3 *… * k: 1! = v1 = 1 (k+1)! = vk+1 = (k+1) * vk per k1 n! = vn per k=n
ESEMPIO: CALCOLO DEL FATTORIALE int fact(int n){ return factIter(n,1,1); } int factIter(int n, int v, int k){ return (k==n) ? v : factIter(n, (k+1)*v, k+1);
Invariante di programma: è sempre vero che qui k>0 INVARIANTI Un invariante di programma è una relazione sempre vera in un dato punto del program- ma. Esempio: double power(double b, int k){ return (k<=0) ? 1 : powerIt(b,k,b,1); } Invariante di programma: è sempre vero che qui k>0
INVARIANTI DI CICLO Un invariante di ciclo è una relazione sem- pre vera, in un dato punto del programma, a ogni iterazione. Identificare un invariante di ciclo è una forma di progetto. Invarianti diversi suggeriscono di norma algoritmi diversi, che quasi sempre hanno diversa efficienza.
PROBLEMA: CALCOLO DI bk Un approccio iterativo Posto bk = vk, si può scrivere: b0 = v0 = 1 per i=0 bi = vi = b * bi-1 = b * vi-1 per i>0 in particolare: bk = vk per i=k Un possibile invariante: bk = vi * bk-i
PROBLEMA: CALCOLO DI bk Perché bk = bk-i *vi è un invariante? Al generico passo 0<i<k, bk = vi * bk-i Moltiplicando e dividendo per b: bk = (vi *b) * bk-i-1 = vi+1 * bk-(i+1) che è l’invariante al passo (i+1)-esimo. In particolare: per i=0, bk = v0 * bk = bk purché v0 =1 per i=k, bk = vk * b0 = vk condizione iniziale
PROBLEMA: CALCOLO DI bk Come usarlo per progettare l’algoritmo? inizialmente, v = v0 =1 a ogni passo si deve trasformare l’invariante bk = vi * bk-i nella forma bk = vi+1 * bk-(i+1) che deve assumere al passo successivo ciò si ottiene ponendo, a ogni passo v’ = b * v i’ = i + 1
CALCOLO DI bk : L’INVARIANTE double powerIt(double b, int k, double v, int i){ return (i==k) ? v : powerIt(b,k,v*b,i+1); } double power(double b, int k){ return (k==0) ? 1 : powerIt(b,k,1,0); i’ = i+1 V’ = Vi+1 = Vi* b V0=1 i=0
PROGETTARE bk PER INVARIANTI Partendo da relazioni diverse si ottengono approcci diversi, con diversa efficienza. Un diverso invariante: k=0 b0 = 1 k>0 k pari bk = (b2) k/2 k dispari bk = b * bk-1 b*b: non richiede di saper fare potenze richiede di saper fare un prodotto
PROGETTARE bk PER INVARIANTI Come usarlo per progettare l’algoritmo? a ogni passo si deve riscrivere bk in una delle due forme date ciò si ottiene ponendo, a ogni passo se k è pari: b’ = b * b k’ = k/2 se k è dispari: b’ = b k’ = k-1 e moltiplicando per b richiede una operazione dopo la fase di modifica di b e k soluzione ricorsiva
PROGETTARE bk PER INVARIANTI boolean odd(int n){ return n%2==1; } double pow(double b, int k){ return (k==0) ? 1 : odd(k) ? pow(b, k-1) * b : pow(b*b, k/2); ricorsione non-tail (Complessità dell’ordine di log2 k)
PROGETTARE bk PER INVARIANTI UN APPROCCIO ITERATIVO Un ulteriore invariante: bk = t * vn k=0 n=0, t=1 bk = t * v0 = 1 k>0 se n è pari: bk = t * (v2) n/2 se n è dispari: bk = t * v * vn-1
PROGETTARE bk PER INVARIANTI Progetto dell’algoritmo: a ogni passo si deve trasformare l’invariante bk = t * vn in una delle due forme date ciò si ottiene ponendo: se n è pari n’ = n/2, t’ = t, v’ = v2 se n è dispari n’ = n-1, t’ = t*v, v’ = v Interessante: b e k in realtà non si usano!
PROGETTARE bk PER INVARIANTI boolean odd(int n){return n%2==1;} double powIt(double b, int k, double t, double v, int n){ return (n==0) ? t : odd(n) ? powIt(b,k,t*v,v,n-1) : powIt(b,k,t,v*v,n/2); } Come previsto, b e k non servono! Quindi li possiamo togliere…!!
PROGETTARE bk PER INVARIANTI boolean odd(int n){return n%2==1;} double powIt(double t, double v, int n){ return (n==0) ? t : odd(n) ? powIt(t*v, v, n-1) : powIt(t, v*v, n/2); } double power(double b, int k){ return (k==0) ? 1 : powIt(1,b,k);
ESERCIZIO: MOLTIPLICAZIONE Obiettivo: calcolare p = x* y Sfruttiamo l’invariante: y = Q * B + R dove B è un intero positivo Q (quoziente) = y/B R (resto) = y%B
ESERCIZIO: MOLTIPLICAZIONE Obiettivo: calcolare p = x* y Sostituendo: p = x * y = x* (Q * B + R) = = x * (B * (y/B)) + x*(y%B) Caso particolare: y=0 p = x * y = x*0 = 0
ESERCIZIO: MOLTIPLICAZIONE Approccio ricorsivo: si applica direttamente la relazione trovata p = x*B * (y/B) ) + x*(y%B) Ad esempio, scegliendo B=2: int MulNatR(int x, int y){ return (y==0) ? 0 : MulNatR(x*2, y/2) + x*(y%2); } Occorre fare un’operazione dopo la chiamata ricorsiva ricorsione non-tail
ESERCIZIO: MOLTIPLICAZIONE Approccio ricorsivo: si applica direttamente la relazione trovata p = x*B * (y/B) ) + x*(y%B) Ad esempio, scegliendo B=2: int MulNatR(int x, int y){ return (y==0) ? 0 : MulNatR(x*2, y/2) + x*(y%2); } Operazione primitiva che suppo- niamo di saper già fare (è una moltiplicazione per 0 o per 1) Operazioni primitive che supponiamo di sa- per già fare (moltiplicazione/divisione per 2)
ESERCIZIO: MOLTIPLICAZIONE Verso un approccio iterativo Cerchiamo un invariante di ciclo p = x * y + z Ponendo y=Q*B+R e trasformando: p = (x*B) * Q + (x*R + z) = x’ * y’ + z’ dove si è posto y’ = Q = y/B, x’ = x*B, z’ = z + x*R Caso particolare: y=0 p = z
ESERCIZIO: MOLTIPLICAZIONE Invariante di ciclo: p = x * y + z Trasformazione: p = x’ * y’ + z’ y’ = Q = y/B, x’ = x*B, z’ = z + x*R int MulNatIt(int x, int y, int z){ return(y==0) ? z : MulNatIt(x*2, y/2, z+x*(y%2)); } Operazioni primitive: supponiamo di saper già moltiplicare, dividere e modulare per 2.
ESERCIZIO: MOLTIPLICAZIONE Perché supponiamo di saper già moltiplicare, e dividere per 2 (trovando anche il resto) ? Perché l’elaboratore è intrinsecamente capace di farlo nella propria ALU: moltiplicazione per 2 = shift a sinistra (<<) divisione per 2 = shift a destra (>>) moltiplicazione per (y%2) = 0, se y è pari (y%2 vale 0) y, se y dispari (y%2 vale 1)
ESERCIZIO: MOLTIPLICAZIONE Il codice finale che ne risulta: int MulNatIt(int x, int y, int z){ return (y==0) ? z : odd(y): MulNatIt(x<<1, y>>1, z+x) : MulNatIt(x<<1, y>>1, z); } boolean odd(int n){return n%2==1;} y%2 = 1 x/2 y/2 y%2 = 0
UNA RIFLESSIONE DI FONDO L’impostazione funzionale è sempre costruttiva. Ma si può sempre solo creare? Perché creare una versione nuova di un accumulatore ad ogni passo, quando l’elaboratore di Von Neumann permette la modifica del contenuto di una cella di memoria?
UNA PROPOSTA È possibile riusare una stessa area dati senza bisogno di crearne una nuova ad ogni passo computazionale? Ci sono controindicazioni?
VARIABILI NEI LINGUAGGI IMPERATIVI Una variabile in un linguaggio imperativo non è solo un sinonimo per un dato come in matematica è un’astrazione della cella di memoria associata a due diverse informazioni: il contenuto (R-value) l’indirizzo a cui si trova (L-value) 3.22 a x
ESPRESSIONI CON EFFETTI COLLATERALI Le espressioni che contengono variabili, oltre a denotare un valore, possono a volte comportare effetti collaterali sulle variabili coinvolte. Un effetto collaterale è una modifica del valore della variabile (R-value) causato da particolari operatori: operatore di assegnamento operatori di incremento e decremento
variabile = espressione ASSEGNAMENTO L’assegnamento è un particolare tipo di espressione come tale denota comunque un valore!! con un effetto collaterale: quello di cambiare il valore della variabile. Sintassi variabile = espressione Esempi di espressioni di assegnamento: j = 0 k = j + 1
L’espressione di assegnamento denota il valore dell’ espressione variabile = espressione denota il valore dell’ espressione ma cambia anche il valore della variabile: il nuovo valore della variabile è quello denotato dalla espressione.
Se k valeva 2, l’espressione k = 7 denota il valore 7 ESEMPIO Se k valeva 2, l’espressione k = 7 denota il valore 7 e cambia il valore di k, che d’ora in poi vale 7 (non più 2)
L’assegnamento è distruttivo ESEMPIO Se k valeva 2, l’espressione j = k+1 denota il valore 3 e cambia il valore di j, che d’ora in poi vale 3 (qualunque valore avesse prima) L’assegnamento è distruttivo
ESPRESSIONI DI ASSEGNAMENTO Il valore denotato dall’espressione di assegnamento può essere usato in altre espressioni. Ad esempio, 3 + (k=7) denota il valore 10 e cambia in 7 il valore di k
ASSEGNAMENTO & VARIABILI Una variabile in una espressione di assegnamento: è intepretata come il suo R-value, se compare a destra del simbolo = è intepretata come il suo L-value, se compare a sinistra del simbolo = 3.22 a x
Se x valeva 2, l’espressione x = x + 1 denota il valore 3 ESEMPIO Se x valeva 2, l’espressione x = x + 1 denota il valore 3 e cambia in 3 il valore di x il simbolo x a destra dell’operatore = denota il valore attuale (R-value) di x, cioè 2 il simbolo x a sinistra dell’operatore = denota la cella di memoria associata a x (L-value), a cui viene assegnato il valore dell’espressione di destra (3) l’espressione nel suo complesso denota il valore della variabile dopo la modifica, cioè 3.
ASSEGNAMENTO: ASSOCIATIVITÀ Come tutti gli operatori, anche l’operatore di assegnamento deve avere una sua associatività k = j = 1 Prima k=j, o prima j=1 ? l’operatore di assegnamento è associativo a destra: ciò consente espressioni di assegnamento multiplo
ASSEGNAMENTO: ASSOCIATIVITÀ Esempi k = j = 1 interpretato come k = (j = 1) i = j = k = 0 interpretato come i = (j = (k=0)) i = k + 5 = 6 NO: k+5 non ha un L-value! Nota: anche volendo, sarebbe stato impossibile farlo associativo a sinistra, in quanto ciò avrebbe reso molte espressioni prive di significato. Ad esempio: k = j = 2 interpretato come (k=j) = 2 ??? Equivarrebbe a scrivere 1 = 2 !!!!
INCREMENTO (++) E DECREMENTO (--) Gli operatori di incremento e decremento sono usabili in due modi come pre-operatori: ++v come post-operatori: v++ prima incremento, poi uso prima uso, poi incremento
ESEMPI int i, j, k = 5; i = ++k /* i vale 6, k vale 6 */ i = k++ /* i vale 5, k vale 6 */ int i=4, j, k = 5; j = i + k++; /* j vale 9, k vale 6 */ j = ++k - k++; /* in cerca di guai! */
j = ++k - k++; /* in cerca di guai! */ Detti x = ++k e y = k++, ATTENZIONE…!! int k = 6; j = ++k - k++; /* in cerca di guai! */ Detti x = ++k e y = k++, è certo che l’espressione venga valutata come j = x - y (da sinistra a destra) è certo che alla fine k valga 8 ma non si sa se venga calcolato prima x o prima y, e qui la cosa fa molta differenza! se prima x, poi y j = 7 - 7 = 0 se prima y, poi x j = 8 - 6 = 2
recupera l’ R-value della variabile c UN ESEMPIO main() { int f, c = 20; f = 32 + c * 9 / 5; } L’espressione f = 32 + c * 9 / 5 recupera l’ R-value della variabile c calcola il corrispondente valore Fahrenheit e lo assegna alla variabile f (interpretata come L-value effetto collaterale) scarta il valore denotato dall’espressione di assegnamento (che non viene più utilizzato) Ad esempio, se c vale 20, l’espressione vale 68... … quindi a f viene asse- gnato il valore 68. L’espressione f=68 denota ancora 68, che però viene scartato.
ISTRUZIONI Le istruzioni esprimono azioni che, una volta eseguite, comportano una modifica permanente dello stato interno del pro-gramma o del mondo circostante. Le strutture di controllo permettono di aggregare istruzioni semplici in istruzioni più complesse.
ISTRUZIONI Una istruzione C è espressa dalle seguenti produzioni: <istruzione> ::= <istruzione-semplice> <istruzione> ::= <istruzione-di-controllo> <istruzione-semplice> ::= <espressione> ; Quindi, qualsiasi espressione seguita da un punto e virgola è una istruzione semplice.
ESEMPI DI ISTRUZIONI SEMPLICI x = 0; y = 1; /* due istruzioni */ x = 0, y = 1; /* una istruzione */ k++; 3; /* non fa nulla */ ; /* istruz. vuota*/
ISTRUZIONI DI CONTROLLO Una istruzione di controllo può essere: una istruzione composta (blocco) una istruzione condizionale (selezione) una istruzione di iterazione (ciclo) come specificato dalla produzione: < istruzione-di-controllo > ::= <blocco> | <selezione> | <iterazione>
ISTRUZIONI DI CONTROLLO Le istruzione di controllo sono alla base della programmazione strutturata (Dijkstra, 1969). Concetti chiave: concatenazione o composizione selezione o istruzione condizionale ramifica il flusso di controllo in base al valore vero o falso di una espressione (“condizione di scelta”) ripetizione o iterazione esegue ripetutamente un’istruzione finché rimane vera una espressione (“condizione di iterazione”)
TEOREMA DI JACOPINI-BÖHM Le strutture di concatenazione, iterazione e selezione costituiscono un insieme completo in grado di esprimere tutte le funzioni calcolabili. Dunque, l’uso di queste sole strutture di controllo non limita il potere espressivo. La dimostrazione del teorema è basata sulla Turing-equivalenza di un “mini-linguaggio” che fornisca solo tali strutture di controllo.
Lo scope dei simboli che compaiono entro il blocco è il blocco stesso [ <dichiarazioni e definizioni> ] { <istruzione> } } dopo un blocco non occorre il punto e virgola (esso termina le istruzioni semplici, non separa istruzioni) Lo scope dei simboli che compaiono entro il blocco è il blocco stesso
ESEMPIO DI BLOCCO main() { /* INIZIO BLOCCO */ const float F1=9.0, F2=5, SH=32; int c, f, temp = 20; char scala = 'C'; c = (scala != 'F') ? temp : (F2 / F1 * (temp - SH)) ; f = (scala != 'F') ? (SH+temp*F1/F2) : temp; } /* FINE BLOCCO */
Il qualificatore const rende queste variabili non modificabili ..una nota “en passant”... main() { /* INIZIO BLOCCO */ const float F1=9.0, F2=5, SH=32; int c, f, temp = 20; char scala = 'C'; c = (scala != 'F') ? temp : (F2 / F1 * (temp - SH)) ; f = (scala != 'F') ? (SH+temp*F1/F2) : temp; } /* FINE BLOCCO */ Il qualificatore const rende queste variabili non modificabili
ESEMPIO DI BLOCCHI ANNIDATI main() { /* INIZIO BLOCCO ESTERNO */ const float F1=9.0, F2=5, SH=32; int c, f, temp = 20; { /* INIZIO BLOCCO INTERNO */ char scala = ‘C’; c = (scala != 'F') ? temp : (F2 / F1 * (temp - SH)) ; f = (scala != 'F') ? (SH+temp*F1/F2) : temp; } /* FINE BLOCCO INTERNO */ } /* FINE BLOCCO ESTERNO */
ISTRUZIONI CONDIZIONALI <selezione> ::= <scelta> | <scelta-multipla> la seconda non è essenziale, ma migliora l’espressività. l’espressione condizionale ternaria (.. ? … : …) fornisce già un mezzo per fare scelte, ma è poco leggibile in situazioni di medio/alta complessità. L’istruzione di scelta fornisce un altro modo per esprimere alternative.
ISTRUZIONE DI SCELTA SEMPLICE <scelta> ::= if <condizione> <istruzione1> [ else <istruzione2> ] condizione vera falsa istruzione2 istruzione1 Una espressione logica o relazionale, che viene valutata al momento della esecuzione dell’istruzione if.
ISTRUZIONE DI SCELTA SEMPLICE <scelta> ::= if <condizione> <istruzione1> [ else <istruzione2> ] La parte else è opzionale: se omessa, in caso di condizione falsa si passa subito all’istruzione che segue l’if. condizione vera falsa istruzione2 istruzione1
ESEMPIO DI ISTRUZIONE if <istruzione1> e <istruzione2> sono ciascuna una singola istruzione Qualora occorra specificare più istruzioni, si deve quindi utilizzare un blocco. if (n > 0) { /* inizio blocco */ a = b + 5; c = (x<3) ? a : b; } /* fine blocco */ else n = b;
ISTRUZIONE if ANNIDATE Come caso particolare, <istruzione1> o <istruzione2> potrebbero essere un altro if Occorre attenzione ad associare le parti else (che sono opzionali) all’ if corretto if (n > 0) if (a>b) n = a; else n = b; /* riferito a if(a>b) */ { if (a>b) n = a; } else n = b; /* riferito a if(n>0) */ Regola semantica: l’else è sempre associato all’if più interno Se ciò non soddisfa occor- re inserire esplicitamente un blocco.
ISTRUZIONE DI SCELTA MULTIPLA espressione di selezione caso A istruzioni1 caso B istruzioni2 default istruzioni … break Consente di scegliere fra molte istruzioni (alternative o meno) in base al valore di una espressione di selezione. L’espressione di sele-zione deve denotare un valore numerabile (intero, carattere,…).
ISTRUZIONE DI SCELTA MULTIPLA switch (selettore) { case <etichetta1> : < istruzioni> [ break; ] case <etichetta2> : < istruzioni> [ break; ] … [ default : < istruzioni> ] } Il valore dell’espressione selettore viene confron- tato con le etichette dei vari casi: l’esecuzione prosegue dal ramo corrispondente (se esiste). Se nessuna etichetta corri- sponde, si prosegue col il ramo default. Se neanche quello esiste, si prosegue con l’istruzione successiva allo switch.
ISTRUZIONE DI SCELTA MULTIPLA switch (selettore) { case <etichetta1> : < istruzioni> [ break; ] case <etichetta2> : < istruzioni> [ break; ] … [ default : < istruzioni> ] } Il valore dell’espressione selettore viene confron- tato con le etichette dei vari casi: l’esecuzione prosegue dal ramo corrispondente (se esiste). Le etichette sono costanti dello stesso tipo del selettore. Attenzione: <istruzioni> denota una sequenza di istruzioni (non occorre un blocco)
ISTRUZIONE DI SCELTA MULTIPLA espressione di selezione caso A istruzioni1 caso B istruzioni2 default istruzioni … break I vari rami non sono mutua- mente esclusivi: imboccato un ramo, si eseguono anche tutti i rami successivi... … a meno che non ci sia il comando break a forzare esplicitamente l’uscita.
ISTRUZIONI DI ITERAZIONE <while> | <for> | <do-while> Per il Teorema di Jacopini-Böhm, una struttura di controllo iterativa sarebbe sufficiente: averne di più migliora l’espressività del linguaggio. Le istruzioni di iterazione: hanno un solo punto di ingresso e un solo punto di uscita nel flusso del programma perciò possono essere interpretate come una singola azione in una computazione sequenziale.
In generale, non è noto quante volte l’istruzione sarà ripetuta. ISTRUZIONE while <while> ::= while( <condizione> ) <istruzione> L’istruzione viene ripetuta per tutto il tempo in cui la condi- zione rimane vera. condizione vera falsa istruzione Se la condizione è falsa, l’itera- zione non viene eseguita neppure una volta. In generale, non è noto quante volte l’istruzione sarà ripetuta.
<while> ::= while( <condizione> ) <istruzione> ISTRUZIONE while <while> ::= while( <condizione> ) <istruzione> Prima o poi, direttamente o indirettamente, l’istruzione deve modificare la condizione: altrimenti, l’iterazione durerà per sempre! condizione vera falsa istruzione Perciò, quasi sempre istruzione è un blocco, al cui interno si modifica qualche variabile che compare nella condizione.
particolarmente adatta alle verifiche dopo un input ISTRUZIONE do...while <do-while> ::= do <istruzione> while( <condizione> ); condizione vera falsa istruzione È una “variazione sul tema” della precedente: la condizione viene verificata dopo aver eseguito l’istruzione. Se la condizione è falsa, l’itera- zione viene comunque ese- guita almeno una volta. particolarmente adatta alle verifiche dopo un input
È una evoluzione dell’istruzione while ISTRUZIONE for È una evoluzione dell’istruzione while rispetto a cui mira a eliminare alcune frequenti sorgenti di errore: mancanza delle necessarie inizializza- zioni delle variabili mancanza della fase di modifica del ciclo (rischio di ciclo senza fine)
ISTRUZIONE for <for> ::= for( <espr-i> ; <cond> ;<espr-m>) <istruzione> condizione vera falsa istruzione espr-inizializzazione espr-modifica Struttura del while
ISTRUZIONE for <for> ::= for( <espr-i> ; <cond> ;<espr-m>) <istruzione> condizione vera falsa istruzione espr-inizializzazione espr-modifica Espressione di inizia- lizzazione: valutata una e una sola volta prima di iniziare l’itera- zione. Espressione di inizia- lizzazione: valutata una e una sola volta prima di iniziare l’itera- zione.
ISTRUZIONE for <for> ::= for( <espr-i> ; <cond> ;<espr-m>) <istruzione> condizione vera falsa istruzione espr-inizializzazione espr-modifica Condizione: valutata a ogni interazione, per decidere se proseguire (come in un while) Se manca si assume vera! Condizione: valutata a ogni interazione, per decidere se proseguire (come in un while)
ISTRUZIONE for <for> ::= for( <espr-i> ; <cond> ;<espr-m>) <istruzione> condizione vera falsa istruzione espr-inizializzazione espr-modifica Espressione di modifica: valutata a ogni interazione, dopo aver eseguito l’istru- zione. Condizione: valutata a ogni interazione, per decidere se proseguire (come in un while)
UN ESEMPIO Il solito problema: calcolo del fattoriale Dall’approccio ricorsivo… ...all’approccio sintatticamente ricorsivo, ma computazionalmente iterativo (ricorsione tail)... ...all’approccio iterativo tramite istruzioni di iterazione.
IL FATTORIALE ITERATIVO TRAMITE ESPRESSIONE CONDIZIONALE... int factIter(int n, int i, int v){ /* inizialmente, v = 1 */ /* invariante di ciclo: v = i! */ return (i==n) ? v : factIter(n,i+1,(i+1)*v); } Chiamata: factIter(n,0,1)
... IL FATTORIALE ITERATIVO TRAMITE ISTRUZIONE CONDIZIONALE... int factIter(int n, int i, int v){ /* inizialmente, v = 1 */ /* invariante di ciclo: v = i! */ if (i==n) return v; else return factIter(n,i+1,(i+1)*v); } Chiamata: factIter(n,0,1)
...IL FATTORIALE ITERATIVO TRAMITE ISTRUZIONE DI ITERAZIONE: while int fact(int n){ int v=1; /* inizialmente, v = 1 */ int i=0; /* inizialmente, i = 0 */ while (i<n) { /* invariante: v = i! */ v = (i+1)*v; i = i+1; } return v; I valori iniziali, prima forniti dl- l’esterno, divengono ora varia- bili locali della funzione interfaccia utente più pulita
int v=1; /* inizialmente, v = 1 */ int i=0; /* inizialmente, i = 0 */ ...IL FATTORIALE ITERATIVO TRAMITE ISTRUZIONE DI ITERAZIONE: do...while int fact(int n){ int v=1; /* inizialmente, v = 1 */ int i=0; /* inizialmente, i = 0 */ do { /* invariante: v = i! */ v = (i+1)*v; i = i+1; } while (i<n); return v; } La condizione è ora verificata dopo ogni interazione anziché prima. Problema: che succede se si invoca fact(-3) ???
...IL FATTORIALE ITERATIVO TRAMITE ISTRUZIONE DI ITERAZIONE: for int fact(int n){ int v=1; /* inizialmente, v = 1 */ int i; for (i=0;i<n; i+1) v = (i+1)*v; /* invariante: v = i! */ return v; } Le fasi di inizializzazione e di modifica della variabile di con- trollo del ciclo sono ben in evi- denza nella struttura del for.
ITERAZIONE & RICORSIONE TAIL Poiché la ricorsione tail dà luogo a un processo computazionale di tipo iterativo, deve essere possibile trasformare un ciclo in ricorsione tail e viceversa. COME FARLO? il corpo del ciclo rimane immutato il ciclo diventa un if con, in fondo, la chiamata tail-ricorsiva.
ITERAZIONE & RICORSIONE TAIL il corpo del ciclo rimane immutato il ciclo diventa un if con, in fondo, la chiamata tail-ricorsiva. Naturalmente, può essere necessario aggiungere nuovi parametri nell’intestazione della funzione tail- ricorsiva, per “portare avanti” le variabili di stato.
ESEMPIO: Massimo Comun Divisore La soluzione tail-ricorsiva già vista... int mcd(int m, int n){ if (m!=n) if (m>n) return mcd(m-n, n); else return mcd(m, n-m); else return m; } … opportunamente riscritta... if (m!=n) { if (m>n) m=m-n; else n=n-m; return mcd(m,n); } else return m;
ESEMPIO: Massimo Comun Divisore … opportunamente riscritta... int mcd(int m, int n){ if (m!=n) { if (m>n) m=m-n; else n=n-m; return mcd(m,n); } else return m; } … traslata in ciclo: while (m!=n) return m;
Vincolo: deve essere c < (a+b) Rappresentazione delle informazioni: ESERCIZIO 1 Dati tre valori a b c che rappresentano le lunghezze di tre segmenti, valutare se posso-no essere i tre lati di un triangolo, e se sì deci-derne il tipo (scaleno, isoscele, equilatero). Vincolo: deve essere c < (a+b) Rappresentazione delle informazioni: la variabile booleana triangolo indica se i tre seg- menti possono costituire un triangolo le variabili booleane scaleno, isoscele e equil indicano il tipo di triangolo.
ESERCIZIO 1 Specifica: se a+b>c triangolo = vero se a=b=c { equil=isoscele=vero scaleno=falso } altrimenti se a=b o b=c o a=c { isoscele=vero; equil=scaleno=falso } { scaleno=vero; equil=isoscele=falso } altrimenti triangolo = falso
ESERCIZIO 1 main (){ float a=1.5, b=3.0, c=4.0; int triangolo, scaleno, isoscele, equil; triangolo = (a+b>c); if (triangolo) { if (a==b && b==c) { equil=isoscele=1; scaleno=0; } else if (a==b || b==c || a==c) { isoscele=1; scaleno=equil=0;} else { scaleno=1; isoscele=equil=0;} }
ESERCIZIO 1 Non si può usare l’istruzione di scelta multipla (switch) perché le condizioni da verificare sono uguaglianze fra variabili, non semplici confronti con etichette predefinite. main (){ float a=1.5, b=3.0, c=4.0; int triangolo, scaleno, isoscele, equil; triangolo = (a+b>c); if (triangolo) { if (a==b && b==c) { equil=isoscele=1; scaleno=0; } else if (a==b || b==c || a==c) { isoscele=1; scaleno=equil=0;} else { scaleno=1; isoscele=equil=0;} } Attenzione! Una espressione come a==b==c sarebbe stata formalmente lecita, ma avreb- be avuto tutt’altro significato!
ESERCIZIO 2 Dati due valori positivi X e Y, calcolarne la divisione intera X/Y come sequenza di sottrazioni, ottenendo quoziente e resto. Invariante di ciclo: X = Q * Y + R, con R 0 inizialmente, Q=0, R=X (R>Y) a ogni passo, Q’=Q+1, R’=R-Y (R>Y) alla fine, X = Q(n) * Y + R (n) (0<R<Y) che è la definizione di divisione intera.
ESERCIZIO 2 Specifica: sia Q il quoziente, inizialmente pari a 0 sia R il resto, inizialmente pari a X while (R Y) incrementare il quoziente Q decrementare R di una quantità Y Codifica main(){ int x = 20, y = 3, q, r; for (q=0, r=x; r>=y; q++, r=r-y); } Notare l’uso di una espressione concatenata per concatenare due assegnamenti e inizializzare così due variabili. Idem per le operazioni di modifica
OPERATORI DI ASSEGNAMENTO “COMPATTI” Il C introduce una forma particolare di assegnamento che ingloba anche un’operazione aritmetica o bit a bit: l-espr = <espressione> è “quasi equivalente” a l-espr = l-espr <espressione> dove indica un operatore fra +, –, *, /, %, >>, <<, &, ^, |
OPERATORI DI ASSEGNAMENTO “COMPATTI” Perché “quasi” equivalente ? nel primo caso, l-espr viene valutata una sola volta nel secondo, invece, viene valutata due volte Quindi, le due forme sono equivalenti solo se la valutazione di l-espr non comporta effetti collaterali
OPERATORI DI ASSEGNAMENTO “COMPATTI” Esempi k += j equivale a k = k + j k *= a + b equivale a k = k*(a+b)*/ v[i++] *= n non equivale a v[i++] = v[i++]*n */
ESERCIZIO 3 Dati tre valori a, b, c, rappresentanti i coeffi-cienti di un’equazione di secondo grado a x2 + b x + c = 0, calcolarne le radici (reali). Specifica: Calcolare il valore delta = b2 - 4ac Se delta0 calcolare d = delta calcolare le due radici x1, x2 = - (b d) / 2a altrimenti (radici complesse: halt)
#include <math.h> main (){ float a=1.0, b=2.0, c=-15.0; ESERCIZIO 3 #include <math.h> main (){ float a=1.0, b=2.0, c=-15.0; float delta, d, x1, x2; delta = b*b-4*a*c; if (delta>=0){ d = sqrt(delta); x1 = -(b+d)/(2*a); x2 = -(b-d)/(2*a); } Direttiva al preprocessore: include la libreria matematica (fornisce la funzione sqrt)
Scrivere una funzione che verifichi se un naturale N è primo. ESERCIZIO 4 Scrivere una funzione che verifichi se un naturale N è primo. Specifica di I° livello (Crivello di Eratostene): Occorre provare a dividere N per tutti i numeri K N: se nessuno risulta essere un divisore, allora N è primo Specifica di II° livello: Se N è 1, 2 o 3, allora è primo senz’altro. Altrimenti, se è un numero pari, non è primo. Se invece N è dispari e >3, occorre tentare tutti i possibili divisori da 3 in avanti, fino a N.
ESERCIZIO 4 #include <math.h> int isPrime(int n) { int max,i; if (n>=1 && n<=3) return true; /* 1,2,3 ok */ if (n%2==0) return false; /* numeri pari no */ max = sqrt(n); for(i=3; i<=max; i+=2) if (n%i==0) return false; return true; }
ESERCIZIO 5 Scrivere una funzione radice che calcoli la radice quadrata (intera) di un naturale N. Specifica di I° livello: int radice(int n); restituisce il massimo intero X tale che X*X N Specifica di II° livello: Considera un naturale X dopo l’altro a partire da 1, e calcolane il quadrato X*X: fermati appena tale quadrato supera N. Il precedente numero considerato (X-1) è il risultato.
for(x=0; x*x <= n; x++); return x-1; } ESERCIZIO 5 int radice(int n) { int x; for(x=0; x*x <= n; x++); return x-1; } Il corpo del ciclo è vuoto: in effetti, l’elaborazione consiste solo nell’incrementare x per un opportuno numero di volte.
ESERCIZIO 6 Scrivere una funzione che, dato un carattere C, restituisca il corrispondente maiuscolo. Specifica di I° livello: char maiuscolo(char c); restituisce il maiuscolo di C Specifica di II° livello: Se C non è una lettera minuscola, restituiscilo tale e quale. Altrimenti, per calcolare il corrispondente maiu- scolo, sfrutta l’ ordinamento della codifica dei caratteri: – ogni carattere è associato a un intero – le lettere da ‘A’ a ‘Z’ sono in sequenza – le lettere da ‘a’ a ‘z’ sono in sequenza
char maiuscolo(char c) { if (c<'a' || c>'z') return c; ESERCIZIO 6 char maiuscolo(char c) { if (c<'a' || c>'z') return c; else return c – 'a' + 'A'; } Aritmetica fra caratteri: possibile perché le operazioni vengono svolte sul corrispondente codice (ASCII o UNICODE) Attenzione! Una espressione come 'a’<c<'z' è lecita, ma ha tutt’altro significato!