450 likes | 644 Views
DEFINIZIONE DI NUOVE FUNZIONI & STRATEGIE DI COMPOSIZIONE. La capacità di definire nuove funzioni permette: di definire nuove operazioni di introdurre variabili per denotare i dati in modo simbolico di esprimere la ripetizione di una espressione per un numero (prefissato o meno) di volte.
E N D
DEFINIZIONE DI NUOVE FUNZIONI & STRATEGIE DI COMPOSIZIONE La capacità di definire nuove funzioni permette: • di definire nuove operazioni • di introdurre variabili per denotare i dati in modo simbolico • di esprimere la ripetizione di una espressione per un numero (prefissatoo meno) di volte.
STRATEGIE DI COMPOSIZIONE Tre grandi approcci: 1) la composizione di funzioni; 2) le espressioni condizionali; 3) la ricorsione. Le funzioni definibili in termini di un insieme prescelto di primitive e delle precedenti strategie di composizione costituiscono un insieme detto delle funzioni ricorsive generali.
1) COMPOSIZIONE DI FUNZIONI I parametri in una chiamata di funzione pos- sono consistere/comprendere altre funzioni Es: f(x) + g(f(x), q(x + f(y))) • x1 = f(x) • x2 = f(x) (mossa evitabile da un automa “intelligente”) • x3 = f(y) • x4 = x + x3 • x5 = q( x4 ) • x6 = g( x2,x5 ) • x7 = x1 + x6
2) ESPRESSIONE CONDIZIONALE • L’espressione condizionale riflette la consuetudine matematica di definire le funzioni per elencazione di casi. • Esempio: abs: N -> N abs(x) vale x se x 0 abs(x) vale -x se x 0
3) LA RICORSIONE • La ricorsione consiste nella pos-sibilità di definire una funzione in termini di se stessa. • È basata sul principio di induzionematematica: • se una proprietà P vale per n=n0 • e si può provare che, assumendola valida per n, allora vale per n+1 allora P vale per ogni nn0
LA RICORSIONE • Operativamente, risolvere un problema con un approccio ricorsivo comporta • di identificare un “caso base” la cui soluzione sia “ovvia” • di riuscire a esprimere la soluzione al caso generico n in termini dello stesso problema in uno o più casi più semplici (n-1, n-2, etc).
LA RICORSIONE: ESEMPIO Esempio !: N N n! vale 1 se n 0 n! vale n*(n-1)! se n > 0 Codifica: int fact(int n) { return n==0 ? 1 : n*fact(n-1); }
LA RICORSIONE: ESEMPIO Attenzione: la codifica non corrisponde alla specifica!! Il 2° caso si applica per n0,cioè anche per n<0 !!MA COSÌ PUÒ NON TERMINARE! Esempio !: N N n! vale 1 se n 0 n! vale n*(n-1)! se n > 0 Codifica: int fact(int n) { return n==0 ? 1 : n*fact(n-1); }
LA RICORSIONE: ESEMPIO Esempio !: N N n! vale 1 se n 0 n! vale n*(n-1)! se n > 0 Codifica: int fact(int n) { /* return n==0 ? 1 : n*fact(n-1); */ return n>0 ? n*fact(n-1) : 1; } Nuova codifica
ESEMPIO: FATTORIALE • Servitore & Cliente: int fatt(int n) { return (n>0) ? n* fatt(n-1) : 1; } main(){ int z = 5; int fz = fatt(z+2); int f6 = fatt(6); }
ESEMPIO: FATTORIALE Si valuta l’espressione che costituisce il parametro attuale (nell’environment del main) e si trasmette alla funzione fatt una copia del valore così ottenuto (7). • Servitore & Cliente: int fatt(int n) { return (n>0) ? n*fatt(n-1) : 1; } main(){ int z = 5; int fz = fatt(z+2); int f6 = fatt(6); }
ESEMPIO: FATTORIALE • Servitore & Cliente: int fatt(int n) { return (n>0) ? n*fatt(n-1) : 1; } main(){ int z = 5; int fz = fatt(z+2); int f6 = fatt(6); } La funzione riceve una copia del valore 7 e la lega al simbolo n. Poi valuta l’espres- sione condizionale: ciò impone di valutare unaespressione che contiene una nuova chiamata di funzione.
ESEMPIO: FATTORIALE • Servitore & Cliente: int fatt(int n) { return (n>0) ? n*fatt(n-1) : 1; } main(){ int z = 5; int fz = fatt(z+2); int f6 = fatt(6); } Si valuta quindi, nell’environment di fatt, l’espressione n-1 (che vale 6), e si effettua una nuova chiamata al servitore fatt, pas- sandogli una copia del valore 6.
ESEMPIO: FATTORIALE • Servitore & Cliente: int fatt(int n) { return (n>0) ? n*fatt(n-1) : 1; } main(){ int z = 5; int fz = fatt(z+2); int f6 = fatt(6); } Il (nuovo) servitore riceve quindi una copia del valore 6 e, come sopra, valuta l’espres- sione condizionale. Ciò lo porta a dover fare una nuova chiamata passando 5.
ESEMPIO: FATTORIALE • Servitore & Cliente: int fatt(int n) { return (n>0) ? n*fatt(n-1) : 1; } main(){ int z = 5; int fz = fatt(z+2); int f6 = fatt(6); } … la cosa prosegue, servitore dopo servitore.. ...
ESEMPIO: FATTORIALE • Servitore & Cliente: int fatt(int n) { return (n>0) ? n*fatt(n-1) : 1; } main(){ int z = 5; int fz = fatt(z+2); int f6 = fatt(6); } Prima o poi, dato che il valore passato cala ogni volta, si giunge a invocare fatt con parametro 1. In questo caso, la valutazione dell’espressione dà come risultato 1.
ESEMPIO: FATTORIALE • Servitore & Cliente: int fatt(int n) { return (n>0) ? n*fatt(n-1) : 1; } main(){ int z = 5; int fz = fatt(z+2); int f6 = fatt(6); } Ciò chiude la sequenza di chiamate ricorsive. Il controllo torna al servitore precedente, che può finalmente valutare l’espressione n*1 (valutando n nel suo environment, dove vale 2) ottenendo 2.
ESEMPIO: FATTORIALE • Servitore & Cliente: int fatt(int n) { return (n>0) ? n*fatt(n-1) : 1; } main(){ int z = 5; int fz = fatt(z+2); int f6 = fatt(6); } Il valore 2 viene restituito al servitore pre- cedente, che a sua volta può così valutare l’espressione n*2 (valutando n nel suo environment, dove vale 3) ottenendo 6.
ESEMPIO: FATTORIALE • Servitore & Cliente: int fatt(int n) { return (n>0) ? n*fatt(n-1) : 1; } main(){ int z = 5; int fz = fatt(z+2); int f6 = fatt(6); } … la cosa prosegue ...
ESEMPIO: FATTORIALE • Servitore & Cliente: int fatt(int n) { return (n>0) ? n*fatt(n-1) : 1; } main(){ int z = 5; int fz = fatt(z+2); int f6 = fatt(6); } Prima o poi, a forza di retrocedere, si torna al primo servitore attivato, che può quindi valutare l’espressione n*720 (valutando n nel suo environment, dove vale 7), giun- gendo così a trovare il valore 5040.
ESEMPIO: FATTORIALE • Servitore & Cliente: int fatt(int n) { return (n>0) ? n*fatt(n-1) : 1; } main(){ int z = 5; int fz = fatt(z+2); int f6 = fatt(6); } Il valore 5040, restituito dal servitore fatt, può quindi essere usato per inizializzare la variabile fz.
UN ALTRO ESEMPIO Problema: calcolare la somma dei primi N interi Specifica: Considera la somma (1+2+3+...+(N-1)+N come composta di due termini: • (1+2+3+...+(N-1)) • N Esiste un caso banale assolutamente ovvio: • la somma fino a 1 vale 1.
UN ALTRO ESEMPIO Il primo termine non è altro che la soluzione allo stesso problema inun caso più semplice Problema: calcolare la somma dei primi N interi Specifica: Considera la somma (1+2+3+...+(N-1)+N come composta di due termini: • (1+2+3+...+(N-1)) • N Esiste un caso banale assolutamente ovvio: • la somma fino a 1 vale 1. Il secondo termine è un valore già noto.
UN ALTRO ESEMPIO Problema: calcolare la somma dei primi N interi Codifica: int sommaFinoA(int n){ return (n==1) ? 1 : sommaFinoA(n-1) + n; }
0, se n=0 fib (n) = 1, se n=1 fib(n-1) + fib(n-2), altrimenti UN TERZO ESEMPIO Problema: calcolare l’N-esimo numero di Fibonacci
UN TERZO ESEMPIO Problema: calcolare l’N-esimo numero di Fibonacci Codifica: unsigned fibonacci(unsigned n) { return (n<2) : n ? fibonacci(n-1) + fibonacci(n-2); }
UN TERZO ESEMPIO Ricorsione non lineare: ogniinvocazione del servitore causadue nuove chiamate al servitore medesimo. Problema: calcolare l’N-esimo numero di Fibonacci Codifica: unsigned fibonacci(unsigned n) { return (n<2) : n ? fibonacci(n-1) + fibonacci(n-2); }
UNA RIFLESSIONE • Negli esempi di ricorsione visti finora • fattoriale • somma dei primi N interi • calcolo dell’N-esimo numero di Fibonacci • si inizia a sintetizzare il risultato solo dopo che si sono aperte tutte le chiamate, “a ritroso”, mentre le chiamate si chiudono.
UNA RIFLESSIONE • Le chiamate ricorsive decompongono via via il problema,ma non calcolano nulla • Il risultato viene sintetizzato a partire dalla fine, perché prima occorre arrivare al caso “banale”: • il caso “banale” fornisce il valore di partenza • poi, e solo poi, si sintetizzano, “a ritroso”, i successivi risultati parziali.
UNA RIFLESSIONE Ciò indica che tali soluzioni • sono sintatticamente ricorsive • e danno luogo a un processo computa-zionale effettivamente ricorsivo.
m, se m=n MCD(m, n) = MCD(m-n, n), se m>n MCD(m, n-m), se m<n UN ESEMPIO DIVERSO Problema: trovare il Massimo Comun Divisore tra N e M
UN ESEMPIO DIVERSO Problema: calcolare il Massimo Comun Divisore tra N e M Codifica: int mcd(int m, int n){ return (m==n) : m ? (m>n) ? mcd(m-n, n) : mcd(m, n-m); }
UN ESEMPIO DIVERSO • Servitore & Cliente: int mcd(int m, int n){ return (m==n) : m ? (m>n) ? mcd(m-n, n) : mcd(m, n-m);} main(){ int m = mcd(36,15); }
UN ESEMPIO DIVERSO • Servitore & Cliente: int mcd(int m, int n){ return (m==n) : m ? (m>n) ? mcd(m-n, n) : mcd(m, n-m);} main(){ int m = mcd(36,15); } Si valutano i parametri attuali e si invoca la funzione con parametri 36 e 15
UN ESEMPIO DIVERSO • Servitore & Cliente: int mcd(int m, int n){ return (m==n) : m ? (m>n) ? mcd(m-n, n) : mcd(m, n-m);} main(){ int m = mcd(36,15); } Si legano i parametri 36 e 15 ai parametri formali m e n, e si valuta l’espressione condizionale.
UN ESEMPIO DIVERSO • Servitore & Cliente: int mcd(int m, int n){ return (m==n) : m ? (m>n) ? mcd(m-n, n) : mcd(m, n-m);} main(){ int m = mcd(36,15); } Poiché 36 15, si invoca nuovamente la funzione con parametri m-n (21) e n (15).
UN ESEMPIO DIVERSO • Servitore & Cliente: int mcd(int m, int n){ return (m==n) : m ? (m>n) ? mcd(m-n, n) : mcd(m, n-m);} main(){ int m = mcd(36,15); } Il nuovo servitore, poiché 21 15, fa la stessa cosa e invoca nuovamente la funzione con parametri m-n (6) e n (15).
UN ESEMPIO DIVERSO • Servitore & Cliente: int mcd(int m, int n){ return (m==n) : m ? (m>n) ? mcd(m-n, n) : mcd(m, n-m);} main(){ int m = mcd(36,15); } Il nuovo servitore, poiché 6 15, invoca un ulteriore servitore con parametri m (6) e n-m (9).
UN ESEMPIO DIVERSO • Servitore & Cliente: int mcd(int m, int n){ return (m==n) : m ? (m>n) ? mcd(m-n, n) : mcd(m, n-m);} main(){ int m = mcd(36,15); } Poiché 6 9, si ha una nuova chiamata con parametri m (6) e n-m (3).
UN ESEMPIO DIVERSO • Servitore & Cliente: int mcd(int m, int n){ return (m==n) : m ? (m>n) ? mcd(m-n, n) : mcd(m, n-m);} main(){ int m = mcd(36,15); } Poiché 6 3, si ha una nuova invocazione (l’ultima!) con parametri m-n (3) e n (3).
UN ESEMPIO DIVERSO • Servitore & Cliente: int mcd(int m, int n){ return (m==n) : m ? (m>n) ? mcd(m-n, n) : mcd(m, n-m);} main(){ int m = mcd(36,15); } Poiché 3 = 3, il servitore termina e restituisce come risultato 3.
UN ESEMPIO DIVERSO • Perché questo esempio è “diverso” ? • il risultato viene sintetizzato via via che le chiamate si aprono, “in avanti” • quando le chiamate si chiudono non si fa altro che riportare indietro, fino al cliente, il risultato ottenuto.
UNA RIFLESSIONE • La soluzione ricorsiva individuata per l’MCD è sintatticamente ricorsiva... • … ma dà luogo a un processo computa-zionale diverso dal precedente: • un processo computazionale ITERATIVO • Il risultato viene sintetizzato in avanti • ogni passo decompone e calcola • e porta in avanti il nuovo risultato parziale
UNA RIFLESSIONE Ogni processo computazionale ITERATIVO calcola a ogni passo un risultato parziale • dopo k passi, si ha a disposizione il risultato parziale relativo al caso k • questo non è vero nei processi computa-zionali ricorsivi • là, finché non si sono aperte tutte le chiamate, non è disponibile nessun risultato!
RICORSIONE TAIL Una ricorsione che realizza un processo computazionale ITERATIVO è una ricorsione solo apparente • la chiamata ricorsiva è sempre l’ultima istruzione • i calcoli sono fatti prima • la chiamata serve solo, dopo averli fatti, per proseguire la computazione • questa forma di ricorsione si chiama RICORSIONE TAIL(“ricorsione in coda”)