07 2 ricorsione

46
Fondamenti di informatica 1 Funzioni ricorsive

Upload: piero-fraternali

Post on 18-Jan-2015

674 views

Category:

Documents


1 download

DESCRIPTION

 

TRANSCRIPT

Page 1: 07 2 ricorsione

Fondamenti di informatica 1

Funzioni ricorsive

Page 2: 07 2 ricorsione

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)!

Page 3: 07 2 ricorsione

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

Page 4: 07 2 ricorsione

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

Page 5: 07 2 ricorsione

Fattoriale iterativoint fattoriale(int n) { int fatt = 1; for (; n > 1; --n) // non serve contatore, uso n fatt *= n; return fatt;}

Page 6: 07 2 ricorsione

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)

Page 7: 07 2 ricorsione

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

Page 8: 07 2 ricorsione

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)

Page 9: 07 2 ricorsione

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

Page 10: 07 2 ricorsione

Fattoriale ricorsivoint fattorialeRic(int n) { if (n == 0) return 1; else return n * fattorialeRic(n - 1);}

Page 11: 07 2 ricorsione

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

Page 12: 07 2 ricorsione

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

Page 13: 07 2 ricorsione

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

Page 14: 07 2 ricorsione

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

Page 15: 07 2 ricorsione

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?

Page 16: 07 2 ricorsione

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

Page 17: 07 2 ricorsione

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

Page 18: 07 2 ricorsione

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

Page 19: 07 2 ricorsione

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);}

Page 20: 07 2 ricorsione

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);}

Page 21: 07 2 ricorsione

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()

Page 22: 07 2 ricorsione

22

num 2volte 3n m 3n m 2n m 1

/ 3/ 4/ 5

Modello a run-time

n m 0

Page 23: 07 2 ricorsione

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

Page 24: 07 2 ricorsione

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 ++

Page 25: 07 2 ricorsione

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

Page 26: 07 2 ricorsione

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

Page 27: 07 2 ricorsione

Progettazione

A C C A V A L L A V A C C A

da a

Page 28: 07 2 ricorsione

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

Page 29: 07 2 ricorsione

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

Page 30: 07 2 ricorsione

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]

Page 31: 07 2 ricorsione

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;}

Page 32: 07 2 ricorsione

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?

Page 33: 07 2 ricorsione

33

FORMULAZIONE RICORSIVA

A B C

Torre di n-1 dischi

Le torri di Hanoi

Page 34: 07 2 ricorsione

34

A B C

Le torri di Hanoi

Page 35: 07 2 ricorsione

35

A B C

Le torri di Hanoi

Page 36: 07 2 ricorsione

36

A B C

Le torri di Hanoi

Page 37: 07 2 ricorsione

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)

Page 38: 07 2 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

Page 39: 07 2 ricorsione

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

Page 40: 07 2 ricorsione

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;}

Page 41: 07 2 ricorsione

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

Page 42: 07 2 ricorsione

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

Page 43: 07 2 ricorsione

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!

Page 44: 07 2 ricorsione

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

Page 45: 07 2 ricorsione

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