La ricorsione
Introduzione Nel corpo di un sottoprogramma è possibile attivare sottoprogrammi dichiarati esternamente o localmente al sottoprogramma, ma è anche possibile riattivare il sottoprogramma stesso. In quest’ultimo caso si parla di sottoprogrammi ricorsivi, caratteristica prevista da alcuni linguaggi di programmazione. Parliamo, quindi, di ricorsività diretta quando cerchiamo di definire qualcosa riferendoci (ossia “ricorrendo”) alla sua stessa definizione. In generale un oggetto si può definire ricorsivo quando viene definito in termini di se stesso.
Un esempio Proviamo a calcolare la seguente potenza: 78. Dalla matematica sappiamo che la potenza an è così definita: an = 1 se n = 0 con a 0 an = a * a * ..... a se n > 0 dove a è un numero intero o reale diverso da zero e n è un numero intero non negativo. La potenza di numero può essere espressa anche dalla seguente definizione (matematica) ricorsiva: an = a * an-1 se n> 0 dove an-1 = a* an-2 an-2 = a * an-3 ecc. Ritornando al nostro esempio abbiamo che: 78 = 7 * 7 * 7 * 7 * 7 * 7 * 7 * 7 Dalla precedente definizione, quindi, è vero che 78 = 7 * 77 dove 77 = 7 * 76 e 76 = 7 * 75 e così via Pertanto, 78 richiama 77, 77 richiama 76 e così sino al termine del calcolo. Quindi, partendo dal valore 71, si può giungere alla determinazione di 78 attraverso il procedimento inverso.
Gli elementi del procedimento ricorsivo Possiamo affermare che per attivare un procedimento ricorsivo: il problema può essere scomposto in sottoproblemi dello stesso tipo, ognuno dipendente dall’altro in base ad una scala gerarchica; è necessario conoscere la soluzione di un caso particolare del problema (nel nostro caso 70) indispensabile per terminare il problema (condizione di terminazione); si devono conoscere le relazioni funzionali che legano il problema ai sottoproblemi simili. Abbiamo pertanto tre elementi caratteristici: Una condizione che permette di verificare se si è di fronte ad un caso particolare risolvibile banalmente o se è necessario procedere per più passate. La soluzione del caso banale. La soluzione più generale che contiene una o più chiamate ricorsive nel quale una relazione funzionale lega il problema ai sottoproblemi simili.
Un altro esempio Proviamo a calcolare il fattoriale di un numero N. Sappiamo che il fattoriale di un numero naturale N maggiore di zero è uguale a: N! = N * (N - 1)! Più precisamente possiamo dare la seguente definizione (matematica) ricorsiva di fattoriale: Fatt(0) = 1 se N=0 Fatt(N) = Fatt(N-1)*N se N >0 Riferendoci a 7! avremo:. 7! = 7 * 6 * 5 * 4 * 3 * 2 * 1 = 5040 che è uguale a 7! = 7 * 6! = 7 * 6 * 5! = 7 * 6 * 5 * 4! = 7 * 6 * 5 * 4 * 3! = 7 * 6 * 5 * 4 * 3 * 2! = 7 * 6 * 5 * 4 * 3 * 2 * 1! = 7 * 6 * 5 * 4 * 3 * 2 * 1 * 0! = 7 * 6 * 5 * 4 * 3 * 2 * 1 * 1 = 5040 Questo problema è stato espresso in termini ricorsivi in quanto risponde esattamente ai tre requisiti enunciati pocanzi. Infatti: sappiamo che calcolare N! dipende esclusivamente dal calcolo di (N-1)! abbiamo il caso particolare che 0! = 1 abbiamo una relazione funzionale N! = N * (N-1)! che lega il problema principale al sottoproblema.
La funzione Fattoriale( ) FUNZIONE Fattoriale(Num : INTERO) : INTERO INIZIO SE (Num = 0) ALLORA Fatt 1 ALTRIMENTI Fatt Num * Fattoriale(Num -1) FINESE RITORNO (Fatt) FINE Eseguiamo passo dopo passo le funzioni in modo da apprendere completamente il meccanismo della ricorsione.
Il procedimento di calcolo per la funzione Fattoriale( ) Calcoliamo Fatt(5). Considerato che Num è diverso da zero, si inizia il processo con l’istruzione: Fatt Num * Fattoriale(Num -1) che corrisponde a eseguire (a) Fatt 5 * 4! (cioè calcola 5!) Ma noi non conosciamo ancora il valore di 4! per cui la moltiplicazione non può ancora essere eseguita. Per questo motivo si esegue di nuovo l’algoritmo per calcolare 4!. Ora Num vale 4, la condizione Num = 0 è ancora falsa, così viene eseguita nuovamente l’istruzione contenuta nel ramo ALTRIMENTI che corrisponde a calcolare: (b) Fatt 4 * 3! (cioè calcola 4!) Il procedimento prosegue con le seguenti istruzioni (c) Fatt 3 * 2! (cioè calcola 3!) (d) Fatt 2 * 1! (cioè calcola 2!) (e) Fatt 1 * 0! (cioè calcola 1!) A questo punto Num = 0 per cui viene eseguita l’istruzione contenuta nel ramo ALLORA cioè Fatt 1 che non richiede ulteriori esecuzioni dell’algoritmo. Il valore 1 assegnato alla variabile Fatt ci permette di procedere in quanto non richiede ulteriori esecuzioni dell’algoritmo. Il proseguimento avviene a ritroso così da terminare l’esecuzione delle istruzioni (e) (d) (c) (b) (a) rimaste sospese; quindi: nell’istruzione (e) 0! viene sostituito con 1. Ciò permette il calcolo di 1! che è uguale a 1; nell’istruzione (d) 1! viene sostituito con 1. Ciò permette il calcolo di 2! che è uguale a 2; nell’istruzione (c) 2! viene sostituito con 2. Ciò permette il calcolo di 3! che è uguale a 6; nell’istruzione (b) 3! viene sostituito con 6. Ciò permette il calcolo di 4! che è uguale a 24; nell’istruzione (a) 4! viene sostituito con 24. Ciò permette il calcolo di 5! che è uguale a 120.
Le chiamate ricorsive Graficamente il procedimento è il seguente: 5! 1° proc. ricorsivo 5* 4! 5 * 24 = 120 5° calcolo 2° proc. ricorsivo 4 * 3! 4 * 6 = 24 4° calcolo 3° proc. ricorsivo 3 * 2! 3 * 2 = 6 3° calcolo 4° proc. ricorsivo 2 * 1! 2 * 1 = 2 2° calcolo 5° proc. ricorsivo 1* 0! 1 * 1 = 1 1° calcolo
Ricorsione indiretta Si parla di ricorsione indiretta quando nella definizione di una funzione compare la chiamata a un'altra funzione la quale direttamente o indirettamente chiama la funzione iniziale. FUNZIONE ping(N:INTERO) INIZIO SE (n < 1) RITORNO (1) ALRIMENTI RITORNO( pong(n - 1) ) FINE FUNZIONE pong(N:INTERO) SE (n < 0) RITORNO 0 ALTRIMENTI RITORNO ( ping(n/2) ) In questo esempio i metodi sono cooperanti nel senso che si invocano ripetutamente a vicenda (indirettamente), dando luogo a un caso particolare di ricorsione indiretta chiamata ricorsione mutua.
Problemi con la ricorsione Quando si inizia a scrivere funzioni ricorsive si incorre in alcuni problemi dovuti alla mancanza di familiarità con questo tipo di meccanismo. Per analizzare questi problemi consideriamo la funzione somma(N) che restituisce la somma dei primi N numeri interi. Ad esempio, se N vale 4 deve restituire 1+2+3+4 = 10. 1 se N = 1 somma(N) = N + somma(N-1) se N > 1 Pseudocodice FUNZIONE somma (N:INTERO) INIZIO SE(N = 1) RITORNO (1) ALTRIMENTI RITORNO ( N + somma(N -1)) FINE
Ricorsione infinita (1) In una ricorsione infinita vengono attivate infinite chiamate della funzione senza che esse vengano mai chiuse. Ciò può verificarsi: quando i valori del parametro non si semplificano. Per esempio, vediamo che cosa succede nell'implementazione della funzione somma( ) quando il valore del parametro non si semplifica. FUNZIONE somma (N:INTERO) INIZIO SE(N = 1) RITORNO (1) ALTRIMENTI RITORNO ( N + somma(N) ) FINESE FINE Esempio di chiamate ricorsive somma(5) ( 5 +somma(5)) ( 5 + ( 5 + somma(5)) ) ( 5 + ( 5 + ( 5 + somma(5))) ) …. ricorsione infinita non viene mai risolta somma(5) il valore di N non si semplifica
Ricorsione infinita (2) Ricorsione infinita quando manca la clausola di chiusura per terminare. Per esempio, vediamo che cosa succede nell'implementazione della funzione somma( ) quando omettiamo la gestione dei casi base: FUNZIONE somma (N:INTERO) INIZIO RITORNO ( N + somma(N -1) ) FINE Esempio di chiamate ricorsive somma(5) ( 5 +somma(4)) ( 5 + ( 4 + somma(3)) ) ( 5 + ( 4 + ( 3 + somma(2))) ) ( 5 + ( 4 + ( 3 + ( 2 + somma(1)))) ) ( 5 + ( 4 + ( 3 + ( 2 + 1 + somma(-1)))) ) ( 5 + ( 4 + ( 3 + ( 2 + 1 + (-1 + somma(-2))))) )……. ricorsione infinita il parametro N viene decrementato all’infinito Manca la clausola di chiusura
Ricorsione infinita nella ricorsione indiretta (1) in una ricorsione indiretta (le due funzioni si chiamano continuamente a vicenda) può verificarsi uno dei problemi visti in precedenza (senza cioè che si semplifichi il parametro o senza che esista la clausola di terminazione). Consideriamo, per esempio, le due seguenti funzioni mutuamente ricorsive: FUNZIONE pong(N:INTERO) INIZIO SE (n < 0) RITORNO (0) ALTRIMENTI RITORNO ( ping(n -1) ) FINE Esempio di chiamate ricorsive ping(2) ( pong(1)) ( ping(0) ) (pong(-1) ) 0 FUNZIONE ping(N:INTERO) INIZIO SE (n < 0) RITORNO (1) ALRIMENTI RITORNO( pong(n - 1) ) FINE
Ricorsione infinita nella ricorsione indiretta (2) Se per un errore scrivessimo: FUNZIONE pong(N:INTERO) INIZIO SE (n < 0) RITORNO (0) ALTRIMENTI RITORNO ( ping(n) ) FINE FUNZIONE ping(N:INTERO) INIZIO SE (n < 1) RITORNO (1) ALTRIMENTI RITORNO( pong(n) ) FINE il valore di N non si semplifica Avremmo: ping(2) ( pong(2)) ( ping(2) ) (pong(2 ) ricorsione infinita
Ricorsione alle olimpiadi di informatica Considera il seguente problema proposto alla selezione scolastica delle olimpiadi di informatica: (il testo è stato adattato allo pseudocodice). Considera la seguente funzione: FUNZIONE ES3( x: INTERO ) INIZIO SE ( x ≤ 1 ) ALLORA RITORNO ( 0 ) ALTRIMENTI RITORNO (1 + ES3( x DIV 2 ) ) FINESE FINE Dove X DIV 2 è la divisione intera di x per 2 quindi, ad esempio: 7 DIV 2 restituisce 3. Che cosa restituisce la chiamata ES3( 10 ) ?. a) 1 b) 2 c) 3 d) nessuna delle precedenti
Ricorsione nelle funzioni di ricerca (1) Algoritmo ricorsivo di ricerca di un elemento in un vettore ordinato Pseudocodice FUNZIONE RicercaBinariaRic (VAL Vet: Vettore,VAL x:INTERO, VAL Inizio: INTERO, VAL Fine: INTERO): BOOLEANO Mezzo: INTERO INIZIO SE(Inizio > Fine) ALLORA RITORNO (Falso) FINESE Mezzo (Inizio + Fine) DIV 2 SE(Vet[Mezzo] = x) RITORNO (Vero) ALTRIMENTI SE(Vet[Mezzo] > x) RITORNO ( RicercaBinariaRic(Vet, x, Inizio, Mezzo -1) ) RITORNO (RicercaBinariaRic(Vet, x, Mezzo + 1, Fine)) FINE
Ricorsione nelle funzioni di ricerca (2) Analisi dell’algoritmo Si cerca l’elemento in mezzo al vettore ((Inizio + Fine) DIV 2 ). Se non lo si trova e l’elemento da cercare è minore dell’elemento presente in mezzo al vettore, si effettua la ricerca con una chiamata ricorsiva della funzione nella prima metà del vettore passando come parametri l’indice di inizio e quello di mezzo meno uno. Se, invece, l’elemento da cercare è maggiore dell’elemento presente in mezzo al vettore, si effettua la ricerca con una chiamata ricorsiva della funzione nella seconda metà del vettore passando come parametri l’indice di mezzo più uno e l’indice di fine del vettore. Continuando con le invocazioni ricorsive si può arrivare: a un vettore di tre elementi: se non è quello di mezzo, si continua la ricerca su due vettori di un elemento ciascuno a un vettore di due elementi Fine = Inizio+1: abbiamo i valori Mezzo=Inizio=Fine-1; si continua fra Inizio, Inizio-1 oppure fra Fine, fine a un vettore di un elemento: se non è quello da cercare, si continua fra Inizio, Inizio-1 oppure fra Fine+1, Fine Prima o poi si arriva al caso: Fine=Inizio -1 che corrisponde alla ricerca in un vettore di zero elementi. E’ questo il caso base in cui si deve ritornare Falso. Esempio di chiamata iniziale della funzione Per effettuare la ricerca dell’elemento 25 nel vettore V di 100 elementi, si invocherà la funzione nel seguente modo: RicercaBinariaRic(V , 25 , 1, 100)
Ricorsione nelle funzioni di ordinamento (1) Algoritmo BubbleSort ricorsivo per l’ordinamento degli elementi di un vettore Vediamo la versione ricorsiva dell’algoritmo BubbleSort per l’ordinamento degli elementi di un vettore. FUNZIONE BubbleSortRic(REF Vet: Vettore, Fine: INTERO) i, Comodo:INTERO INIZIO SE(Fine ≠ 0) ALLORA PER i = 0 A Fine-1 ESEGUI SE(Vet[i] > Vet[i+1]) Comodo Vet[i] Vet[i] Vet[i+1] Vet[i+1] Comodo FINESE FINEPER BubbleSortRic(Vet, Fine-1) FINE
Ricorsione nelle funzioni di ordinamento (2) Analisi dell’algoritmo Nell’ordinamento a bolle dobbiamo far risalire gli elementi più piccoli fino alla cima del vettore. Il ciclo for pone l’elemento maggiore alla fine del vettore. Dopo aver fatto questo, resta da ordinare il resto del vettore cioè tutti gli altri elementi tranne l'ultimo. Nella chiamata ricorsiva viene specificato dove il vettore termina. Dove, cioè, finisce la parte di vettore ancora da ordinare. Facciamo il ciclo di confronti per mettere l’elemento maggiore alla fine. Successivamente effettuiamo l’ invocazione ricorsiva su tutto il vettore tranne l’ultimo elemento. La parte di vettore da ordinare diventa così sempre più piccola a ogni invocazione ricorsiva. Alla prima invocazione la variabile Fine vale la lunghezza del vettore -1 poi diminuisce di uno, poi ancora di uno, e così via. Quando la variabile Fine vale 0, la parte di vettore da ordinare è il solo primo elemento (che è già ordinato!). E’ questo il caso base in cui la funzione termina. Esempio di chiamata iniziale della funzione Per effettuare l’ordinamento del vettore V di 100 elementi, si invocherà la funzione nel seguente modo: BubbleSortRic(V, 100)