SlideShare a Scribd company logo
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 iterativo
int 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 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
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

• P invoca P

– oppure

– Indirettamente
• P invoca Q che invoca P

P
Q
Fattoriale ricorsivo
int 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:
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
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

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
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
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
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);
}
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);
}
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()
Modello a run-time
num 2 / 4
/3
5
volte 3
n
m 3
n
m 2
n
m 1
n
m 0

22
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

B

C

+
C

A

+

B

+

A
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
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!
Progettazione
A

da

C

C

A

V

A

L

L

A

V

A

C

C

A

a
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
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
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]
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;
}
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?
Le torri di Hanoi

FORMULAZIONE RICORSIVA

Torre di
n-1
dischi

A
33

B

C
Le torri di Hanoi

A
34

B

C
Le torri di Hanoi

A
35

B

C
Le torri di Hanoi

A
36

B

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

Piolo di partenza
Altezza della
piramide da spostare

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
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;
}
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 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!
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/c
omputational-complexity-of-fibonaccisequence

More Related Content

07 2 ricorsione

  • 1. Fondamenti di informatica 1 Funzioni ricorsive
  • 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. 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. 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. Fattoriale iterativo int fattoriale(int n) { int fatt = 1; for (; n > 1; --n) // non serve contatore, uso n fatt *= n; return fatt; }
  • 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. 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. 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. 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. Fattoriale ricorsivo int fattorialeRic(int n) { if (n == 0) return 1; else return n * fattorialeRic(n - 1); }
  • 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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!
  • 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. 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. 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. 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. 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. Le torri di Hanoi FORMULAZIONE RICORSIVA Torre di n-1 dischi A 33 B C
  • 34. Le torri di Hanoi A 34 B C
  • 35. Le torri di Hanoi A 35 B C
  • 36. Le torri di Hanoi A 36 B C
  • 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. 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. 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. 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. 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. 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. 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. 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. 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. Check this out • http://stackoverflow.com/questions/360748/c omputational-complexity-of-fibonaccisequence