07 2 ricorsione
DESCRIPTION
TRANSCRIPT
Fondamenti di informatica 1
Funzioni ricorsive
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)!
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
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
Fattoriale iterativoint fattoriale(int n) { int fatt = 1; for (; n > 1; --n) // non serve contatore, uso n fatt *= n; return fatt;}
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)
Progettare con la ricorsione
• E' un nuovo esempio del procedimento divide-et-impera che spezza un problema in sotto-problemi
• 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
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)
Ricorsione nei sottoprogrammi
• Dal latino re-currere– ricorrere, fare ripetutamente la stessa azione
• Un sottoprogramma P invoca se stesso
– Direttamente• P invoca P
– oppure
– Indirettamente• P invoca Q che invoca P
P
P
Q
Fattoriale ricorsivoint fattorialeRic(int n) { if (n == 0) return 1; else return n * fattorialeRic(n - 1);}
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
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
Il modello a runtime: esempioint 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;}
13
val = 3ris = n = 3temp = 3*
n = 1temp = 1*
n = 2temp = 2*
n = 0temp = ?
temp: cella temporanea per memorizzare il risultato della funzione chiamataassumiamo val = 3
1
1
2
6
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
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?
Definizione ricorsiva della serie
• I numeri di Fibonacci – Modello a base di molte dinamiche
evolutive delle popolazioni• F = {f0, ..., fn} – f0 = 1– f1 = 1– Per n > 1, fn = fn–1 + fn–2
• Notazione "funzionale": F(i) = fi
casi base (due !)
1 passo induttivo
F(3)
F(2) F(1)
+
F(1) F(0)
+
1 1
1
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
Un altro esempio: MCD à-la-Euclide
• Il MCD tra M e N (M, N naturali positivi)– se M=N allora MCD è N– 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
12
12
18
6
18
6 6
1 caso base
2 passi induttivi
MCD: iterativo & ricorsivoint 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);}
Funzione esponenziale (intera)
• Definizione iterativa:
– 1) xy = 1 se y = 0– 2) xy = x * x * … x
(y volte) se y > 0
• Definizione ricorsiva:– 1) xy = 1 se y = 0– 2) xy = x * x(y-1)
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:int esp (int x, int y) { if ( y == 0 ) return 1; else return x * esp(x, y-1);}
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
num 2volte 3n m 3n m 2n m 1
/ 3/ 4/ 5
Modello a run-time
n m 0
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
Costruzione di una stringa invertita
• Data un stringa s1 produrre una seconda stringa s2 che contiene i caratteri in ordine inverso
A B C
AB C +
ABC ++
Costruzione di una stringa invertitastring 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/reference/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
Palindromi in versione ricorsiva
• Un palindromo è tale se:• la parola è di lunghezza 0 o 1;– oppure
• 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
• Il passo induttivo riduce la dimensione del problema!
Caso base
Passo induttivo
Progettazione
A C C A V A L L A V A C C A
da a
Codicebool 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
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 sotto-vettori a destra o a sinistra dell’elemento centrale
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]
Codicebool 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;}
Le torri di Hanoi
A B C
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
FORMULAZIONE RICORSIVA?
33
FORMULAZIONE RICORSIVA
A B C
Torre di n-1 dischi
Le torri di Hanoi
34
A B C
Le torri di Hanoi
35
A B C
Le torri di Hanoi
36
A B C
Le torri di Hanoi
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)
Prototipo della funzione
hanoi(int altezza, char da, char a, char usando)
Altezza della piramide da spostare
Piolo di partenza Piolo di arrivo
Piolo di transito
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
Codicevoid 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;}
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
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
Calcolo numeri di fibonacciint fibo(int n) {
if (n == 0 || n == 1)return 1;
elsereturn (fibo(n - 1) + fibo(n - 2));
}
• Drammaticamente inefficiente!• Calcola più volte l'i-esimo numero di
Fibonacci!
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
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
Check this out
• http://stackoverflow.com/questions/360748/computational-complexity-of-fibonacci-sequence