07 2 ricorsione

1,144 views

Published on

0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total views
1,144
On SlideShare
0
From Embeds
0
Number of Embeds
2
Actions
Shares
0
Downloads
16
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide

07 2 ricorsione

  1. 1. Fondamenti di informatica 1 Funzioni ricorsive
  2. 2. Definizioni induttive • Sono comuni in matematica nella definizione di proprietà di sequenze numerabili • Esempio: numeri pari – 0 è un numero pari – se n è un numero pari anche n+2 è un numero pari • Esempio: il fattoriale di un naturale N (N!) – se N=0 il fattoriale N! è 1 – se N>0 il fattoriale N! è N * (N-1)!
  3. 3. Dimostrazioni per induzione • Dimostriamo che (2 x n)2 = 4 x n2 (distributività del quadrato rispetto alla moltiplicazione) 1) se n=1 : vero (per verifica diretta) 2) suppongo sia vero per n'=k (ip. di induz.) e lo dimostro per n=k+1: (2n)2 = (2(k+1))2 = (2k+2)2 = (2k)2 + 8k + 4 = (per ipotesi di induzione) 4k2 + 8k + 4 = 4(k2 + 2k + 1) = 4(k+1)2 = 4n2 1) è il caso base, 2) è il passo induttivo
  4. 4. Iterazione e ricorsione • Sono i due concetti informatici che nascono dal concetto di induzione: applicare un'azione un insieme numerabile e finito di volte • L'iterazione si realizza mediante la tecnica del ciclo • Per il calcolo del fattoriale: – 0! = 1 – n! = n (n - 1)(n - 2)…. 1 – realizzo un ciclo che parte dal dato richiesto e applica il passo di induzione fino a raggiungere il caso base
  5. 5. Fattoriale iterativo int fattoriale(int n) { int fatt = 1; for (; n > 1; --n) // non serve contatore, uso n fatt *= n; return fatt; }
  6. 6. Progettare con la ricorsione • Esiste un CASO BASE, che rappresenta un sotto-problema facilmente risolvibile • Esempio: se N=0, so N! in modo "immediato" (vale 1) • Esiste un PASSO INDUTTIVO che ci riconduce (prima o poi) al caso base • L'algoritmo ricorsivo esprime la soluzione al problema (su dati di una "dimensione" generica) in termini di operazioni semplici e della soluzione allo stesso problema su dati "più piccoli" (che, su dati sufficientemente elementari, si suppone risolto per ipotesi) • Esempio: per N generico esprimo N! in termini di N (che è un dato direttamente accessibile) moltiplicato per (è una operazione semplice) il valore di (N-1)! (che so calcolare per ipotesi induttiva)
  7. 7. Progettare con la ricorsione • E' un nuovo esempio del procedimento divideet-impera che spezza un problema in sottoproblemi • Con le funzioni non ricorsive abbiamo spezzato un problema in tanti sotto-problemi diversi più semplici • Con la ricorsione spezziamo il problema in tanti sotto-problemi identici applicati a dati più semplici
  8. 8. Fattoriale con la ricorsione • 1) n! = 1 se n = 0 • 2) n! = n * (n - 1)! se n > 0 – riduce il calcolo a un calcolo più semplice – ha senso perché si basa sempre sul fattoriale del numero più piccolo, che io conosco – ha senso perché si arriva a un punto in cui non è più necessario riusare la definizione 2) e invece si usa la 1) – 1) è il caso base, 2) è il passo induttivo (ricorsivo)
  9. 9. Ricorsione nei sottoprogrammi • Dal latino re-currere – ricorrere, fare ripetutamente la stessa azione • Un sottoprogramma P invoca se stesso – Direttamente P • P invoca P – oppure – Indirettamente • P invoca Q che invoca P P Q
  10. 10. Fattoriale ricorsivo int fattorialeRic(int n) { if (n == 0) return 1; else return n * fattorialeRic(n - 1); }
  11. 11. Simulazione del calcolo di FattRic(3) 3 = 0? No calcola fattoriale di 2 e moltiplica per 3 2 = 0? No calcola fattoriale di 1 e moltiplica per 2 1 = 0? No calcola fattoriale di 0 e moltiplica per 1 0 = 0? Si fattoriale di 0 è 1 fattoriale di 1 è 1 per fattoriale di 0, cioè 1 1 = 1 fattoriale di 2 è 2 per fattoriale di 1, cioè 2 1 = 2 fattoriale di 3 è 3 per fattoriale di 2, cioè 3 2 = 6
  12. 12. Esecuzione di funzioni ricorsive • In un certo istante possono essere in corso diverse attivazioni dello stesso sottoprogramma – Ovviamente sono tutte sospese tranne una, l'ultima invocata, all'interno della quale si sta svolgendo il flusso di esecuzione • Ogni attivazione esegue lo stesso codice ma opera su copie distinte dei parametri e delle variabili locali
  13. 13. Il modello a runtime: esempio int fattorialeRic(int n) { if (n == 0) return 1; else { int temp = n * fattorialeRic(n - 1); return temp; } } int main() { int numero; cin >> numero; int ris = fattorialeRic(numero); cout << "Fattoriale ricorsivo: " << ris << endl; return 0; } assumiamo val = 3 13 val = 3 ris = n=3 temp = n=2 temp = n=1 temp = n=0 temp = 6 3* 2 2* 1 1* 1 ? temp: cella temporanea per memorizzare il risultato della funzione chiamata
  14. 14. Terminazione della ricorsione • … se ogni volta la funzione richiama se stessa… perché la catena di invocazioni non continua all'infinito? • Quando si può dire che una ricorsione è ben definita? • Informalmente: – Se per ogni applicazione del passo induttivo ci si avvicina alla situazione riconosciuta come caso base, allora la definizione è ben formata e la catena di invocazioni termina
  15. 15. Un altro esempio: la serie di Fibonacci • Fibonacci (1202) partì dallo studio sullo sviluppo di una colonia di conigli in circostanze ideali • Partiamo da una coppia di conigli • I conigli possono riprodursi all'età di un mese • Supponiamo che dal secondo mese di vita in poi, ogni femmina produca una nuova coppia • e inoltre che i conigli non muoiano mai… – Quante coppie ci sono dopo n mesi?
  16. 16. Definizione ricorsiva della serie • I numeri di Fibonacci F(3) – Modello a base di molte dinamiche evolutive delle popolazioni • F = {f0, ..., fn} F(1) + F(2) F(1) + F(0) 1 1 1 – f0 = 1 casi base (due !) – f1 = 1 1 passo induttivo – Per n > 1, fn = fn–1 + fn–2 • Notazione "funzionale": F(i) = fi
  17. 17. Numeri di Fibonacci in C++ int fibo(int n) { if (n == 0 || n == 1) return 1; else return (fibo(n - 1) + fibo(n - 2)); } Ovviamente supponiamo che n>=0
  18. 18. Un altro esempio: MCD à-la-Euclide • Il MCD tra M e N (M, N naturali positivi) – se M=N allora MCD è N 1 caso base – se M>N allora esso è il MCD tra N e M-N – se N>M allora esso è il MCD tra M e N-M 30 2 passi induttivi 18 12 12 18 6 6 6
  19. 19. MCD: iterativo & ricorsivo int euclideIter(int m, int n) { while( m != n ) if ( m > n ) m = m – n; else n = n – m; return m; } int euclideRic (int m, int n) { if ( m == n ) return n; if ( m > n ) return euclideRic(m–n, n); else return euclideRic(m, n–m); }
  20. 20. Funzione esponenziale (intera) • Definizione iterativa: – 1) xy = 1 se y = 0 – 2) xy = x * x * … x (y volte) se y > 0 • Codice iterativo: int esp (int x, int y) { int e = 1; for (int i = 1; i <= y; i++ ) e *= x; return e; } • Codice ricorsivo: • Definizione ricorsiva: – 1) xy = 1 se y = 0 – 2) xy = x * x(y-1) se y > 0 int esp (int x, int y) { if ( y == 0 ) return 1; else return x * esp(x, y-1); }
  21. 21. Ricorsione e passaggio per reference (incrementare m volte una var del chiamante) void incrementa(int &n, int m) { if (m != 0) { n++; incrementa(n, m - 1); } } int main() { cout << "Inserire due numeri" << endl; int numero, volte; cin >> numero >> volte; incrementa(numero, volte); cout << numero; return 0; } • n è un sinonimo della variabile del chiamante… ciò vale in modo ricorsivo .. • Per cui n si riferisce sempre alla variabile numero del main()
  22. 22. Modello a run-time num 2 / 4 /3 5 volte 3 n m 3 n m 2 n m 1 n m 0 22
  23. 23. Terminazione (ancora!) • Attenzione al rischio di catene infinite di chiamate • Occorre che le chiamate siano soggette a una condizione che prima o poi assicura che la catena termini • Occorre anche che l'argomento sia "progressivamente ridotto" dal passo induttivo, in modo da tendere prima o poi al caso base
  24. 24. Costruzione di una stringa invertita • Data un stringa s1 produrre una seconda stringa s2 che contiene i caratteri in ordine inverso A B C B C + C A + B + A
  25. 25. Costruzione di una stringa invertita string inversione(string s) { // caso base if (s.size() == 1) return s; // passo induttivo return inversione(s.substr(1,s.size()-1)) + s[0]; } string substr (size_t pos = 0, size_t len = npos) const; • Restituisce una nuova stringa costruita con len caratteri a partire da pos • http://www.cplusplus.com/ref erence/string/string/substr/ int main() { string s1 = "Hello world!!"; string s2; s2 = inversione(s1); cout << s2; return 0; } • NB: Soluzione non ottimale che crea una stringa temporanea per ogni carattere della stringa da invertire
  26. 26. Palindromi in versione ricorsiva • Un palindromo è tale se: • la parola è di lunghezza 0 o 1; – oppure Caso base • il primo e l'ultimo carattere della parola sono uguali e inoltre la sotto-parola che si ottiene ignorando i caratteri estremi è a sua volta un palindromo Passo induttivo • Il passo induttivo riduce la dimensione del problema!
  27. 27. Progettazione A da C C A V A L L A V A C C A a
  28. 28. Codice bool palindroma(string par, int da, int a) { if (da >= a) return true; else return (par[da] == par[a] && palindroma(par, da+1, a-1)); } • Notare la regola del cortocircuito • Evita il passo ricorsivo se si trovano due caratteri discordi int main() { string parola; cout << "Inserisci la parola" << endl; cin >> parola; bool risultato = palindroma(parola,0,parola.size()-1); if (risultato) cout << "La parola " << parola << " è palindroma" << endl; else cout << "La parola " << parola << " NON è palindroma" << endl; return 0; } • Notare che il primo passo richiede di inizializzare la ricorsione con i valori degli estremi di partenza
  29. 29. Ricerca Binaria • Scrivere un programma che implementi l’algoritmo di ricerca dicotomica in un vettore ordinato in senso crescente, con procedimento ricorsivo. • Dato un valore val da trovare e un vettore array con due indici low, high, che puntano rispettivamente al primo e ultimo elemento; – L’algoritmo di ricerca dicotomica prevede che se l’elemento f non è al centro del vettore cioè in posizione “m = (low+high)/2” allora deve essere ricercato ricorsivamente soltanto in uno dei due sottovettori a destra o a sinistra dell’elemento centrale
  30. 30. Progettazione • Se low > high, allora l’elemento cercato f non è presente nel vettore (caso base) • Se (val == array [ (low+high) / 2 ]), allora f è presente nel vettore. (caso base) • Altrimenti (passo induttivo) – Se (f > array[ (low+high) / 2 ]) la ricerca deve continuare nel sottovettore individuato dagli elementi con indici nell’intervallo *m +1, high+ – Se (f < array[ (low+high) / 2 ]) allora la ricerca deve continuare nel sottovettore individuato dagli elementi con indici nell’intervallo *low, m - 1]
  31. 31. Codice bool BinarySearch(int array[], int low, int high, int val) { int m; if (low > high) return false; else { m = (low + high) / 2; if (val == array[m]) return true; else if (val > array[m]) return BinarySearch(array, m + 1, high, val); else return BinarySearch(array, low, m - 1, val); } } int main() { int sequenza[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int valore; cin >> valore; bool trovato = BinarySearch(sequenza,0,size-1,valore); if (trovato) cout << "Risultato trovato " << endl; else cout << "Risultato non presente" << endl; return 0; }
  32. 32. Le torri di Hanoi Stampare le mosse necessarie per spostare tutta la torre da A a C muovendo un cerchio alla volta e senza mai mettere un cerchio più grosso su uno più piccolo Torre di n dischi A B C FORMULAZIONE RICORSIVA?
  33. 33. Le torri di Hanoi FORMULAZIONE RICORSIVA Torre di n-1 dischi A 33 B C
  34. 34. Le torri di Hanoi A 34 B C
  35. 35. Le torri di Hanoi A 35 B C
  36. 36. Le torri di Hanoi A 36 B C
  37. 37. Progettazione ricorsiva • Spostare la torre di 1 elemento da non viola mai le regole e si effettua con un passo elementare (caso base) • Per spostare la torre di N elementi, p.e. da A a C – sposto la torre di N-1 cerchi da A a B (ricorsione) – sposto il cerchio restante in C – sposto la torre di N-1 elementi da B a C (ricorsione)
  38. 38. Prototipo della funzione hanoi(int altezza, char da, char a, char usando) Piolo di partenza Altezza della piramide da spostare Piolo di arrivo Piolo di transito
  39. 39. Algoritmo • Se devi spostare una piramide alta N da x a y transitando da z • Sposta una piramide alta N-1 da x a z, transitando per y • Sposta il disco N-esimo da x a y –  stampa la mossa • Sposta una piramide alta N-1 da z a y, transitando per x
  40. 40. Codice void hanoi (int altezza, char da, char a, char transito) { if (altezza > 0) { hanoi (altezza-1, da, transito, a); cout << "Sposta cerchio da " << da << " a "<< a <<endl; hanoi (altezza-1, transito, a, da); } } int main() { hanoi (3, 'A', 'C', 'B'); return 0; }
  41. 41. Hanoi: soluzione iterativa • Non è così evidente… • Stabiliamo un "senso orario" tra i pioli: 1, 2, 3 e poi ancora 1, ecc. • Per muovere la torre nel prossimo piolo in senso orario bisogna ripetere questi due passi: – sposta il disco più piccolo in senso orario – fai l'unico altro spostamento possibile con un altro disco
  42. 42. Ricorsione o iterazione? • • • • Spesso le soluzioni ricorsive sono eleganti Sono vicine alla definizione del problema Però possono essere inefficienti Chiamare un sottoprogramma significa allocare memoria a run-time N.B. è sempre possibile trovare un corrispondente iterativo di un programma ricorsivo
  43. 43. Calcolo numeri di fibonacci int fibo(int n) { if (n == 0 || n == 1) return 1; else return (fibo(n - 1) + fibo(n - 2)); } • Drammaticamente inefficiente! • Calcola più volte l'i-esimo numero di Fibonacci!
  44. 44. Soluzione con memoria di supporto • La prima volta che calcolo un dato numero di Fibonacci lo memorizzo in un array • Dalla seconda volta in poi, anziché ricalcolarlo, lo leggo direttamente dall'array • Mi occorre un valore "sentinella" con cui inizializzare l'array che mi indichi che il numero di Fibonacci corrispondente non è ancora stato calcolato – Qui posso usare ad esempio 0
  45. 45. Codice long fib(int n, long memo[]) { if (memo[n] != 0) return memo[n]; memo[n] = fib(n-1,memo) + fib(n-2, memo); return memo[n]; } • • • • const int MAX = 10; int main() { int n; long memo[MAX]; for (int i = 2; i < MAX; i++) memo[i] = 0; memo[0] = 1; memo[1] = 1; // casi base cout << "Inserire intero: " << endl; cin >> n; cout << "fibonacci di " << n << " = " << fib(n, memo); return 0; } Drastica riduzione della complessità (aumento di efficienza) Questa soluzione richiede un tempo lineare in n La soluzione precedente richiede un tempo esponenziale in n Il prezzo è il consumo di memoria in qtà proporzionale a N
  46. 46. Check this out • http://stackoverflow.com/questions/360748/c omputational-complexity-of-fibonaccisequence

×