Verifichiamo
l'efficienza del calcolo dei numeri di Fibonacci
L'esercitazione
non guidata della scorsa lezione prevedeva di calcolare l'n-esimo
numero di Fibonacci utilizzando una funzione ausiliaria, implementata
in modo ricorsivo e iterativo.
Alcuni di voi hanno probabilmente gia' notato la
differenza nel tempo di esecuzione delle due procedure.
Verifichiamo il tempo necessario per calcolare lo stesso numero di Fibonacci utilizzando le due diverse implementazioni della funzione ausiliaria.
Scriviamo due programmi, FibIte e FibRic, per calcolare
i numeri di Fibonacci in maniera iterativa e ricorsiva
/* |
/* |
> time ./FibRic
> time ./FibIte
per ottenere
il tempo di esecuzione.
Alla fine compariranno tre
valori, di cui quello riferito come user è
il
più informativo: real si riferisce al tempo
intercorso tra il lancio del programma e la sua terminazione;
user al tempo di uso effettivo della CPU; e
sys al tempo di uso effettivo della CPU da
parte del
sistema.
Come mai la versione ricorsiva e' meno efficiente?
La spiegazione risiede nel fatto che la formula ricorsiva impiega una doppia ricorsione che causa ripetute invocazioni per il calcolo dello stesso valore.
Ad esempio, per calcolare F(5), verranno invocate ricorsivamente due istanze (indipendenti tra loro) per il calcolo rispettivamente di F(4) e di F(3). A sua volta il calcolo di F(4) richiederà il calcolo di F(3) e di F(2), mentre quello di F(3) richiederà il calcolo di F(2) e di F(1). Già a questo punto dovrebbe essere evidente che si eseguono molte più chiamate di quelle strettamente necessarie, perché si vuole calcolare più volte lo stesso valore (ad esempio, F(3) o F(2)).
La versione iterativa invece calcola ogni valore esattamente una volta.
Imparando a
programmare funzioni ricorsive, può essere utile
osservare quali (e quante) chiamate vengono effettuate dall'algoritmo,
in modo da valutare l'efficienza della soluzione proposta.
A questo scopo basta aggiungere un opportuno comando di stampa all'inizio di ogni funzione per visualizzare il nome della funzione e gli argomenti sui quali è stata invocata.
Nell'esempio
riportato sotto l'istruzione di stampa è evidenziata in colore
blu.
/* |
Passaggio
di Array come parametri di una funzione
Ad esempio, il prototipo di una funzione per il calcolo della somma di tutti gli elementi di un generico array di double dovrà essere:
double somma(double[], int);Notare che nel primo parametro, di tipo array di double, la dimensione non viene specificata (nelle parentesi quadre non compare alcun valore).
La definizione della funzione dovrà
invece sfruttare il secondo
parametro per capire quale sia il limite dell'array:
double somma(double a[], int n) {La funzione main potrà invocare la funzione somma su array di qualsiasi dimensione. Provate a compilare ed eseguire il codice completo riportato sotto.
int i;
double totale=0.0;
for ( i=0 ; i<n ; i++ )
totale += a[i];
return totale;
}
#include <stdio.h> |
Nota: Gli array vengono allocati in porzioni contigue della memoria. Quando un array deve essere passato ad una funzione, invece di copiarne tutti gli elementi in una nuova variabile locale di tipo array, quello che succede è che l'indirizzo base dell'array viene passato per valore. Di fatto questo corrisponde ad un passaggio per indirizzo dell'array, dato che eventuali modifiche si ripercuoteranno sull'array originale. Ad esempio, possiamo definire una funzione che inizializza tutti gli elementi di un generico array di double ad uno stesso valore v nel seguente modo:
void inizializza(double a[], int n, double v) {
int i;
for ( i=0 ; i<n ; i++ )
a[i]=v;
}