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.
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 Lespressione 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 induzione matematica: –se una proprietà P vale per n=n 0 –e si può provare che, assumendola valida per n, allora vale per n+1 allora P vale per ogni n n 0
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 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); } Attenzione: la codifica non corrisponde alla specifica!! Il 2° caso si applica per n 0, cioè anche per n<0 !! MA COSÌ PUÒ NON TERMINARE!
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 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 lespressione che costituisce il parametro attuale (nellenvironment del main) e si trasmette alla funzione fatt una copia del valore così ottenuto (7).
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 lespres- sione condizionale: ciò impone di valutare una espressione 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, nellenvironment di fatt, lespressione 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 lespres- 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 dellespressione 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 lespressione 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 lespressione 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 lespressione 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 ( (N-1)+N come composta di due termini: ( (N-1)) N Esiste un caso banale assolutamente ovvio: la somma fino a 1 vale 1.
UN ALTRO ESEMPIO Problema: calcolare la somma dei primi N interi Specifica: Considera la somma ( (N-1)+N come composta di due termini: ( (N-1)) N Esiste un caso banale assolutamente ovvio: la somma fino a 1 vale 1. Il primo termine non è altro che la soluzione allo stesso problema in un caso più semplice 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; }
UN TERZO ESEMPIO Problema: calcolare lN-esimo numero di Fibonacci 0, se n=0 fib(n-1) + fib(n-2), altrimenti fib (n) =1, se n=1
UN TERZO ESEMPIO Problema: calcolare lN-esimo numero di Fibonacci Codifica: unsigned fibonacci(unsigned n) { return (n<2) : n ? fibonacci(n-1) + fibonacci(n-2); }
UN TERZO ESEMPIO Problema: calcolare lN-esimo numero di Fibonacci Codifica: unsigned fibonacci(unsigned n) { return (n<2) : n ? fibonacci(n-1) + fibonacci(n-2); } Ricorsione non lineare: ogni invocazione del servitore causa due nuove chiamate al servitore medesimo.
UNA RIFLESSIONE Negli esempi di ricorsione visti finora fattoriale somma dei primi N interi calcolo dellN-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.
UN ESEMPIO DIVERSO Problema: trovare il Massimo Comun Divisore tra N e M m, se m=n MCD(m, n-m),se m<n MCD(m, n) =MCD(m-n, n),se m>n
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 lespressione 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 (lultima!) 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 lMCD è 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 lultima 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)