Variabili locali. Variabili parametro. Prototipo di funzione. Prototipo di funzione. Moduli esterni. Moduli esterni

Documenti analoghi
Algoritmi esponenziali

Algoritmi esponenziali

Trasformare array paralleli in array di record

Trasformare array paralleli in array di record

Trasformare array paralleli in array di record

Altri modi per leggere dati in ingresso

Altri modi per leggere dati in ingresso

Gestione dei file. Gestione dei file. Gestione dei file. Gestione dei file Costruiamo un oggetto di tipo FileReader: Gestione dei file

Gestione dei file. Gestione dei file. Gestione dei file. Gestione dei file Costruiamo un oggetto di tipo FileReader:

Casi di prova. Il problema dell ordinamento. Casi di prova. Casi di prova. Casi di prova

Tempo e spazio di calcolo (continua)

Tempo e spazio di calcolo (continua)

Algoritmi e Strutture Dati

Algoritmi e Strutture Dati. Capitolo 1 Un introduzione informale agli algoritmi

Pensiero Algoritmico. Lezione 3 23 Novembre Ripasso. Anatomia di un programma. Anatomia di un programma. Ozalp Babaoglu Università di Bologna

Esempio: il fattoriale di un numero fact(n) = n! n!: Z! N n! vale 1 se n " 0 n! vale n*(n-1)! se n > 0. Codifica:

La Ricorsione. Carla Binucci e Walter Didimo

STRUTTURE DI CONTROLLO DEL C++

Operativamente, risolvere un problema con un approccio ricorsivo comporta

LA RICORSIONE LA RICORSIONE

Fondamenti di Informatica

complessità degli algoritmi

Algoritmi e Strutture Dati

Divide et impera. Divide et impera. Divide et impera. Divide et impera

n n 1 n = > Il calcolo del fattoriale La funzione fattoriale, molto usata nel calcolo combinatorio, è così definita

Informatica (A-K) 5. Algoritmi e pseudocodifica

Fondamenti di Informatica 6. Algoritmi e pseudocodifica

FILE DI TESTO e SOTTOPROGRAMMI

LA RICORSIONE LA RICORSIONE LA RICORSIONE: ESEMPIO LA RICORSIONE: ESEMPIO LA RICORSIONE: ESEMPIO LA RICORSIONE: ESEMPIO

Mergesort. Mergesort. Mergesort. Mergesort. L idea è la seguente: (par. 13.4) dividere l insieme in due parti uguali di n/2 componenti

PROGRAMMAZIONE: I sottoprogrammi

Corso di PHP. Prerequisiti. 3 - Le strutture di controllo. Istruzioni semplici Controllo del flusso di programma

Laboratorio di Python

Rappresentazione degli algoritmi

LA RICORSIONE! Una funzione matematica è definita ricorsivamente quando nella sua definizione compare un riferimento a se stessa

Il primo programma C++

Capitolo 19. Ricorsione

LA RICORSIONE. Una funzione matematica è definita ricorsivamente quando nella sua definizione compare un riferimento a se stessa

liceo B. Russell PROGRAMMAZIONE INDIRIZZO: SCIENTIFICO SCIENZE APPLICATE BIENNIO: SECONDA DISCIPLINA: INFORMATICA

Istruzioni. Una istruzione semplice è qualsiasi espressione seguita da un punto e virgola. Esempio x = 0; y = 1;/* due istruzioni */

Esempio : i numeri di Fibonacci

Esercitazione 5. Procedure e Funzioni Il comando condizionale: switch

Nozioni di base (II Parte)

LE STRUTTURE DATI DINAMICHE: GLI ALBERI. Cosimo Laneve

INFORMATICA A. Titolo presentazione sottotitolo. Laboratorio n 6 Dott. Michele Zanella Ing. Gian Enrico Conti

Algoritmi e Strutture Dati

Un tipico esempio è la definizione del fattoriale n! di un numero n, la cui definizione è la seguente:

Lo sviluppo di un semplice programma e la dimostrazione della sua correttezza

Un tipico esempio è la definizione del fattoriale n! di un numero n, la cui definizione è la seguente:

Assegnazione di una variabile

Testo di riferimento. Problema delle 12 monete. Algoritmi. Complessità degli algoritmi (cenni) Dispense del Corso di Algoritmi e Strutture Dati

Complessità degli algoritmi (cenni)

Operativamente, risolvere un problema con un approccio ricorsivo comporta

Calcolare x n = x x x (n volte)

Funzioni, Stack e Visibilità delle Variabili in C

Algoritmi e Strutture Dati

Laboratorio di Programmazione e Calcolo

Istruzioni di Ciclo. Unità 4. Domenico Daniele Bloisi. Corso di Programmazione e Metodi Numerici Ingegneria Aerospaziale BAER

Implementazione dell albero binario in linguaggio C++

Problemi, istanze, soluzioni

Algoritmi e Strutture Dati

Lezione 5 e 6. Fabio Scotti ( ) Laboratorio di programmazione per la sicurezza. Valentina Ciriani ( ) Laboratorio di programmazione

Prof. Pagani Corrado LINGUAGGIO C: SELEZIONE E CICLI

Introduzione al C. Proprietà degli elementi di un insieme. Claudio Ciccotelli

esegui Ricerca_binaria (metà sinistra di A, v) esegui Ricerca_binaria (metà destra di A, v)

Introduzione alla programmazione in C

Indice. Prefazione. 3 Oggetti e Java 53

del Linguaggio C Istruzioni di iterazione

Una funzione matematica è definita ricorsivamente quando nella sua definizione compare un riferimento a se stessa

I cicli. Iterazioni Calcolo della media Istruzioni break e continue

Programmazione ricorsiva.

complessità computazionale

ISTRUZIONI ISTRUZIONI SEMPLICI

FUNZIONI. attribuire un nome ad un insieme di istruzioni parametrizzare l esecuzione del codice

ISTRUZIONI. Le strutture di controllo permettono di aggregare istruzioni semplici in istruzioni più complesse.

<istruzione> ::= <istruzione-semplice> <istruzione> ::= <istruzione-di-controllo> <istruzione-semplice> ::= <espressione> ;

Programmazione ricorsiva: cenni

Soluzione. I Compitino 16/4/09. I Compitino 16/4/09. I Compitino 16/4/09. I Compitino 16/4/09

Esercizi di Algoritmi e Strutture Dati

Il principio di induzione. La Ricorsione. Il fattoriale: iterativo. Il fattoriale: ricorsivo. P (0) P (n) P (n + 1) per ogni n

Fondamenti Teorici e Programmazione

Funzioni, Stack e Visibilità delle Variabili in C

Istruzioni di Controllo

Ricorsione in C. slides credit Prof. Paolo Romano

ESERCITAZIONI DI INTRODUZIONE AGLI ALGORITMI (A.A. 08/09)

Qualsiasi programma in C++ segue lo schema:

Lezione 8 programmazione in Java. Anteprima. La ricorsione. Nicola Drago Dipartimento di Informatica Università di Verona

Università degli Studi di Ferrara

Esercizio. 2 i=i*2) j=j*2)

Un esempio di calcolo di complessità: insertion sort

Pile Le pile: specifiche e realizzazioni attraverso rappresentazioni sequenziali e collegate. Pile e procedure ricorsive

Note sull algoritmo di Gauss

Fondamenti di Informatica T. Linguaggio C: Stack e Ricorsione

Esercizi su ABR. Prof. E. Fachini - Intr. Alg.!1

INFORMATICA. Strutture iterative

Lezione 4. Problemi trattabili e soluzioni sempre più efficienti. Gianluca Rossi

Sviluppo di programmi

Istruzioni di controllo

Testi di esame precedenti a.a. e soluzioni

Transcript:

Variabili parametro Una variabile parametro di scambio nell intestazione della funzione: (char nome[], int n, double &a) è visibile all interno della funzione e non deve essere ridefinita viene inizializzata all atto dell invocazione della funzione viene creata ( nasce ) quando la funzione viene chiamata viene eliminata ( muore ) quando la funzione termina la sua esecuzione. Una variabile locale: Variabili locali int a; double area; è visibile all interno del blocco in cui è definita e non deve essere ridefinita nel blocco deve essere esplicitamente inizializzata viene creata ( nasce ) quando viene eseguito l enunciato che la definisce viene eliminata ( muore ) quando l esecuzione del programma esce dal blocco in cui è stata definita. Prototipo di funzione Prototipo di funzione Poiché il compilatore esamina le righe del codice in sequenza, è necessario che prima della chiamata della funzione ci sia una definizione della funzione stessa. Ciò può essere realizzato in due modi: 1) scrivendo il prototipo della funzione, la sua firma 2) scrivendo il codice della funzione prima della scrittura del main. 1) prototipo #include... double f(double); int main(){... if(f(x)>0)... return 0; double f(double x){ return x*x-3; 2) #include... double f(double x){ return x*x-3; int main(){... if(f(x)>0)... Moduli esterni La progettazione di software richiede anche una generalità nella scrittura del codice. Vogliamo produrre programmi per risolvere problemi riguardanti argomenti specifici (come avviene nelle librerie di programmi). In C++ c è la possibilità di costruire moduli esterni, che contengono algoritmi scritti anche su file fisici diversi da quello del programma che li utilizza: moduli con algoritmi per gestire per vettori, matrici, polinomi, ordinamenti, liste, Moduli esterni Abbiamo visto che i sottoprogrammi possono essere scritti, all interno del file fisico che contiene il main, con due modalità: scrivendo il prototipo di funzione, il main e la funzione, oppure scrivendo la funzione prima del main. Estendiamo queste due modalità ai moduli esterni: 1) il prototipo e un modulo con il codice della funzione da compilare separatamente 2) una istruzione include per inserire il modulo con il codice della funzione. 1

//file primo.c Moduli esterni: caso 1 #include... double f1(double); double f2(double); int main(){... if(f1(x)==f2(x))... return 0; //file funzioni1.c double f1(double x){ return x*x; double f2(double x){ return -x*x+5; Moduli esterni: caso 1 I due moduli verranno compilati separatamente e successivamente si costruirà un unico eseguibile tramite i due moduli oggetto (se la compilazione ha avuto esito positivo). Per eseguire compilazioni separate si utilizza l opzione -c che costruisce i moduli oggetto di estensione.o g++ -c primo.c funzioni1.c per costruire l eseguibile g++ -o primo primo.o funzioni1.o Moduli esterni: caso 2 //file secondo.c #include funzioni2.h int main(){... if(f1(x)==f2(x))... return 0; //file funzioni2.h double f1(double x){ return x*x; double f2(double x){ return -x*x+5; Moduli esterni: caso 2 L istruzione #include funzioni2.h inserisce il file funzioni2.h e la compilazione è unica (analogamente a ciò che avviene con le altre istruzioni #include) In generale: #include nomefile ; Per costruire l eseguibile, si utilizza la usuale istruzione: g++ -o secondo secondo.o Esercizio Si calcoli la complessità dell algoritmo seguente, considerando i casi a>b, a=b, a<b; specificando il numero di assegnazioni e di confronti nei tre casi. Si supponga n 0. Quale sarebbe il valore di a e b se si stampassero a e b alla fine della struttura iterativa mentre? Esercizio Algoritmo definizione variabili a, b, n, i, k intero continua logico acquisire valori per a, b, n continua vero i 0 mentre i n e continua eseguire i i+1 per k da 1 a i eseguire 2

se Esercizio a > b allora continua falso altrimenti a a - 1 //finese //fineper //finementre Stampare i valori di a e b Soluzione esercizio Caso n = 0. i = 0, quindi il predicato mentre i 0 è falso: il ciclo non viene eseguito. Caso n < 0. se a b il ciclo non termina se a > b il ciclo termina: continua diventa falso. Soluzione esercizio Caso n > 0. Caso a > b. Viene eseguita la prima iterazione del ciclo esterno, viene eseguita una iterazione del ciclo interno (k=1,1); continua diventa falso e il ciclo esterno termina: 2t a + t p + t a + 2 t p + t c + t a + t p costante continua i k=1,1 a>b continua i b non varia, a non varia Soluzione esercizio Caso a <= b. I casi a=b e a<b sono uguali: se a=b allora si esegue l istruzione a a-1 e a diventa minore di b. Il numero di confronti e di assegnazioni nel ciclo interno sono uguali e coincidono con il numero di iterazioni eseguite: k=1 1 k=2 2... k=n n La somma è: 1 + 2 + 3 +... + n = (n+1)*n/2 Soluzione esercizio Pertanto abbiamo: 2t a + (n+1) t p + nt a + (n(n+1)/2 +n) t p + continua i V F i n(n+1)/2 t c + + n(n+1)/2 t a c n 2 a>b a Algoritmi esponenziali a diventa a n*(n+1)/2, b non varia 3

Algoritmi esponenziali Supponiamo che f(n) sia la funzione che rappresenta il numero di operazioni eseguite da un algoritmo e supponiamo che il tempo necessario per compiere una operazione sia un microsecondo: 1µs = 10-6 sec. Vogliamo vedere che per valori di n non elevati gli algoritmi impiegano un tempo troppo elevato per poter essere utilizzati: gli algoritmi esponenziali sono impraticabili. Algoritmi esponenziali Vediamo questa tabella dove riportiamo, al variare di n, il tempo impiegato da alcune funzioni di n (cap. 5) n log 2 (n) 100*n 10*n 2 n 3 2 n 10 2.3 µs 1ms 1ms 1ms 1024µs ~ 1ms 20 2.99µs 2ms 4ms 8ms 1.048sec 60 4.09µs 6ms 36ms 0.21sec 2 60 µs ~ 366 secoli Algoritmi esponenziali 2 60 µs ~ 366 secoli, vediamo come mai 2 60 µs ~ 10 60 0.3 µs ~ 10 18 µs~ 10 12 sec (1.1529 10 12 ) (1µs = 10-6 sec ) Quanti secondi in un anno? 1 minuto = 60 secondi 1 ora = 3600 secondi 1 giorno = 86400 secondi 1 secolo = 3.15 10 9 secondi secondi 1.1529 10 12 secoli = = = 366 secondi in un secolo 3.15 10 9 Algoritmi esponenziali Esistono degli algoritmi che sono intrinsecamente esponenziali: calcolare le permutazioni di n elementi: n! ~ n n Ci sono problemi che sono risolti da un algoritmo deterministico esponenziale, ma il cui algoritmo non deterministico è polinomiale. L algoritmo non deterministico è un algoritmo ideale: può essere simulato pensando di poter eseguire scelte contemporanee ed è rappresentabile su un albero: la profondità dell albero può essere proporzionale alla dimensione dei dati. Tali problemi si chiamano NP (Non deterministico Polinomiale). Algoritmi esponenziali Esempio. Consideriamo la formula logica Algoritmi esponenziali In generale considerando F(x 1, x 2,.., x n ) il problema può essere risolto in un tempo O(2 n ). F(x 1, x 2, x 3 ) = (x 1 e x 2 ) o (non x 3 ) e ci chiediamo se esiste una scelta di valori per le variabili x 1, x 2, x 3 che renda vera F. Un algoritmo deterministico prova tutte le possibilità e poiché il valore di x i può essere vero o falso, le possibilità sono 2 3. L algoritmo non deterministico è in grado di scegliere il valore di x i che porta alla soluzione. Per simulare tale comportamento si può costruire un albero: ogni nodo rappresenta una variabile della formula; da ogni nodo partono due rami che rappresentano il vero e il falso. 4

Algoritmi esponenziali Se potessimo esplorare l albero in modo da poter percorrere contemporaneamente i due rami V e F, in n passi arriveremmo alle foglie. Quindi l algoritmo non deterministico è O(n). x 1 x 2 x 3 V x 1.............. F x 2 x 2 x 3 x 3 x 3 x 3 Complessità asintotica Le considerazioni fatte sulla complessità valgono solo se n e F(n) Se invece n si mantiene limitato anche un algoritmo esponenziale può essere utilizzato; per lo stesso motivo anche la costante moltiplicativa, che solitamente trascuriamo nella notazione O-grande, può invece essere fondamentale nella scelta. Complessità asintotica Esempio. f 1 (n) = 1000 * n f 2 (n) = 10 * n 2 f 1 è O(n) f 2 è O(n 2 ) Quindi: se n è preferibile f 1 se n 10 è preferibile f 2, infatti: 1000 * n 1000*10 = 10 4 10 * n 2 10*100 = 10 3 Ordinamento per inserimento Ordinamento per inserimento L idea è quella di inserire una componente in ordine rispetto ad una sequenza di componenti già ordinate. Esempio. Vogliamo inserire 6 nella sequenza: 2 5 7 9 10 La cosa più efficiente è partire dall ultima posizione e slittare verso destra le componenti che sono maggiori si 6: in tale modo quelle più piccole restano ferme : 2 5 7 7 9 10 ora c è posto per inserire 6. Ordinamento per inserimento Per effettuare l ordinamento si parte dalla seconda componente che si inserisce in ordine rispetto alla prima, poi si considera la terza, che si inserisce in ordine rispetto alle prime due, in generale: si vuole inserire la k-esima componente in ordine rispetto alle k-1 già ordinate. Poiché il numero di componenti non aumenta, per poter slittare in avanti le componenti che precedono nella sequenza, è necessaria una variabile di appoggio x per salvare il valore: 2 5 9 11 3 1 20 x 2 3 5 9 11 1 20 5

Ordinamento per inserimento Bisogna non uscire dall array se si deve inserire un valore prima della prima componente. Ci sono varie strategie per realizzare ciò, una di queste è sfruttare la valutazione pigra dei predicati : for(k=1; k<=n-1; k++) {//v[0] è già //in ordine x=v[k]; i=k; while((i!=0) && (x < v[i-1])){ v[i]=v[i-1]; i--; //fine while v[i]=x; //fine for Ordinamento per inserimento Complessità. Caso favorevole: array già ordinato 1 3 8 10 20 il predicato del ciclo interno è sempre falso, ( x < v[i-1] è falso), quindi il numero di operazioni è proporzionale a n : Ω(n). Caso peggiore: array in ordine inverso 20 10 9 7 4 1 il ciclo interno viene sempre eseguito per valori crescenti di i=k: 1 + 2 + 3 +. + (n-1) = n (n-1)/2 : O(n 2 /2) Una scomposizione di un problema P in sottoproblemi P 1, P 2,, P n si dice ricorsiva se almeno uno dei sottoproblemi è formalmente simile a P e di dimensione inferiore. Esempio. Fattoriale di un numero naturale. Per definizione 0! = 1 (caso base) n! = n * (n-1)! Vediamo che n! viene definito tramite (n-1)! che è un numero più piccolo di n. Per capire una definizione ricorsiva, bisogna espanderla : si ricopia la formula sostituendo al posto di n il numero n-1 e si prosegue fino al caso base : n! = n*(n-1)! = n* [(n-1) * (n-2)!] =... = = n*(n-1)*... * (n-n)! Poiché (n-n)! = 0! = 1 per definizione, si ottiene: n! = n*(n-1)*... * 1 Ci chiediamo se è una buona definizione. È una buona definizione perché esiste una dimensione del problema, una condizione, che non necessita di ulteriori scomposizioni; il problema viene risolto direttamente: 0! = 1 il problema per n=0 è risolto senza utilizzo della ricorsione. 6

Possiamo scrivere algoritmi ricorsivi, algoritmi che richiamano se stessi. Il main non può essere ricorsivo: il main è richiamato dal Sistema Operativo. Un generico algoritmo ricorsivo avrà una struttura del tipo: se condizione allora risolvi direttamente altrimenti ricorsione oppure se condizione allora ricorsione In questo caso può esserci o meno una alternativa: se la condizione è falsa non si esegue nulla. Se la ricorsione non termina, si hanno infinite chiamate per l algoritmo e si può occupare tutta la memoria: questo è un errore grave, come quello di costruire un ciclo infinito. La scrittura di un algoritmo ricorsivo è semplice se si sta realizzando una formula matematica come quella del fattoriale: 0! =1, n! = n*(n-1)! Cosa accade veramente? Facciamo uno schema della scomposizione: n! intestazione della funzione fattoriale(n intero) definizione variabili f intero se n==0 allora f 1 altrimenti f n * fattoriale(n-1) //finese restituire f n* (n-1)* (n-1)! (n-2)!........ 1* 0! =1 I vari prodotti n*, (n-1)*, restano sospesi perché il controllo passa al sottoprogramma chiamato. Dopo l ultima chiamata che restituisce 1, si può ritornare indietro ed eseguire i prodotti sospesi. Come può una funzione rimanere sospesa e poi, quando si riattiva, eseguire i prodotti giusti? La funzione ricorsiva avrà una scrittura del tipo: int fattoriale(int n){ int f; if(n == 0) f = 1; else f = n * fattoriale(n-1); return f; //fine fattoriale ricorsivo Nella funzione appare: n nell intestazione n-1 nella chiamata. 7

Una parte della memoria, RunTimeStack, mantiene le descrizioni delle attivazioni dei sottoprogrammi (funzioni): funzione M main. chiama M. quando si esegue l istruzione di chiamata di una funzione, il controllo passa alla funzione e quando la funzione è terminata, il controllo ritorna al chiamante Con la ricorsione la funzione è sempre la stessa, ma la gestione nel RunTimeStack è analoga: nelle varie copie della funzione sono memorizzati i parametri e le istruzioni di quella chiamata: f f f ritorno chiama f. chiama f. main chiama f.. il PC (contatore di programma) contiene l indirizzo della prossima istruzione da eseguire Che cosa deve essere memorizzato per poter eseguire le operazioni? Si deve memorizzare: quali sono le operazioni da eseguire prima e dopo la chiamata quale è il valore delle variabili a quel livello di chiamata. Vediamo un esempio di questa memorizzazione calcolando ricorsivamente 5! 1 * 0! 2 * 1! 3 * 2! ritorno 1 2*1 = 2 3*2 = 6 Quando un sottoprogramma termina, l area allocata ritorna libera. 4 * 3! 5 * 4! 4*6 = 24 5*24 =120 Complessità di un algoritmo ricorsivo Il tempo di un algoritmo ricorsivo si ottiene sommando vari tempi: il tempo delle operazioni eseguite nell algoritmo (esclusa la ricorsione) il tempo della chiamata della funzione il tempo dell algoritmo di dimensione inferiore Il tempo delle operazioni della parte non ricorsiva si calcola contando confronti, assegnazioni, cicli: T(n). Complessità di un algoritmo ricorsivo Il tempo per effettuare una chiamata è O(1): infatti si effettua un passaggio di parametri, in chiamata, e il ritorno di un valore, quando il metodo è terminato; questo equivale ad un numero finito di assegnazioni e pertanto è costante. Il tempo dell algoritmo di dimensione inferiore è T(dimensioneinferiore) 8

Complessità di un algoritmo ricorsivo Esempio. Complessità dell algoritmo ricorsivo per il calcolo di n! Sia T(n) il tempo per calcolare n!: possiamo contare il numero di moltiplicazioni, dal momento che questa è l operazione fondamentale: costante se n=0 T(n) = costante + T(n-1) se n>0 Se contiamo solo le moltiplicazioni la prima costante è 0 e la seconda costante è 1. Complessità di un algoritmo ricorsivo Se vogliamo con precisione contare tutte le operazioni avremo: per la prima costante t c + t a + t ritorno per la seconda costante: t c + t prodotto + t chiamata + t a + t ritorno Otteniamo così la formula: T(n) = c + T(n-1) Complessità di un algoritmo ricorsivo Analogamente a quanto fatto con la definizione, espandiamo la formula: T(n) = c + T(n-1) = c + (c + T(n-2)) = 2c + T(n-2) = = 2c + (c + T(n-3)) = 3c + T(n-3) = = 3c + (c + T(n-4)) = 4c + T(n-4) =. = n c + T(n-n) = n c + T(0) = = (n+1) c O(n) Se avessimo contato le moltiplicazioni, avremmo avuto: T(n) = 1 + T(n-1) =. = n + T(0) = n e iterazione Avremmo anche potuto calcolare il fattoriale in maniera iterativa; la scomposizione iterativa del fattoriale è diversa: f f*1 f f*2 n!....... f f*n e iterazione Anche la scrittura dell algoritmo cambia; possiamo scrivere delle istruzioni del tipo: intestazione della funzione fattiterativo(n intero) definizione variabili f, i intero f 1 per i da 1 a n eseguire f f * i //fineper restituire f e iterazione La complessità non cambia: abbiamo infatti una struttura iterativa che viene eseguita n volte: t a + (n+1) t p + n t a + t ritorno quindi sempre c n operazioni. 9

Leonardo da Pisa (detto Fibonacci, 1175-1240) fu un illustre matematico che si interessò di vari problemi, alcuni dei quali oggi potremmo chiamarli dinamica delle popolazioni, ossia lo studio di come si evolvono le popolazioni. Problema astratto. Consideriamo: un isola deserta: sistema isolato una coppia di conigli genera un altra coppia ogni anno i conigli si riproducono solo dopo due anni dalla loro nascita i conigli sono immortali (n + ) Quante coppie ci sono dopo n anni? Indichiamo con F n il numero di conigli dopo n anni e proviamo a calcolarli a partire dal primo anno: F 1 = 1 coppia iniziale F 2 = 1 la stessa coppia F 3 = 1 + 1 = 2 F 1 + la coppia nata da F 1 ( 2 anni) F 4 = 2 + 1 = 3 F 3 + la coppia nata da F 2 ( 2 anni) F 5 = 3 + 1 + 1 = 5 F 4 + le due coppie nate da F 3 ( 2 anni).. In generale si avrà F n = F n-1 + F n-2 dove F n-1 rappresenta le coppie presenti l anno precedente ed F n-2 rappresenta una nuova coppia per ogni coppia di almeno 2 anni. I numeri si calcolano facilmente sommando i valori dei due posti precedenti: n 1 2 3 4 5 6 7 8 9 10 11 12 F(n) 1 1 2 3 5 8 13 21 34 55 89 144 L algoritmo più immediato da scrivere è quello che ricopia la definizione e pertanto è un algoritmo ricorsivo, che avrà una scrittura del tipo: intestazione funzione fibonacci (n intero) definizione variabili fib intero se n ==1 oppure n ==2 allora fib 1 altrimenti fib fibonacci (n-1) + fibonacci (n-2) //finese restituire fib Possiamo rappresentare le chiamate ricorsive con una struttura di albero, come abbiamo fatto con il fattoriale. Un albero è un insieme di punti, detti nodi, a cui è associata una struttura d ordine che gode delle seguenti proprietà: esiste uno ed un solo nodo che precede tutti gli altri, detto radice ogni nodo, esclusa la radice, ha un unico predecessore immediato. Ogni nodo con successore si chiama padre. Ogni nodo con predecessore si chiama figlio. I nodi senza successore si chiamano foglie. L arco che collega un nodo padre a un nodo figlio si chiama ramo. Possiamo rappresentare l albero per n = 5. 10

F 5 F 4 F 3 F 3 F 2 F 2 F 1 F 2 F 1 F 5 è la radice, F 1 e F 2 sono foglie; le foglie non hanno ulteriori chiamate ricorsive e restituiscono il valore 1 e nel ritorno si eseguono le somme. Si può dimostrare che il numero delle foglie dell albero della ricorsione per la costruzione di F n coincide con il valore di F n. Se vogliamo contare le chiamate della funzione, dobbiamo anche aggiungere il numero dei nodi interni, che corrisponde al numero delle chiamate ricorsive. Si può dimostrare che tale numero è uguale al numero delle foglie meno uno. Possiamo concludere che: la complessità dell algoritmo ricorsivo cresce come F(n). Osservando l albero della ricorsione si nota che molti valori F n sono calcolati più volte: nel caso di F 5, F 2 viene calcolato 3 volte. Si possono pertanto memorizzare tali valori in un array e calcolarli una volta sola. Si dovrà però dare una dimensione all array, stabilendo un numero massimo di elementi da calcolare. intestazione funzione fibonacci2 (n intero) definizione variabili fib[nummax], i intero fib[1] 1 fib[2] 1 per i da 3 a n eseguire fib[i] fib[i-1] + fib[i-2] //fineper restituire fib[n] Quale complessità ha l algoritmo che utilizza l array? tempo O(n): ciclo che viene eseguito n volte spazio O(n): si utilizza un array di nummax componenti per calcolare F n con n<=nummax Si può scrivere un algoritmo ancora più efficiente. Osserviamo nuovamente il calcolo dei valori F n, ed osserviamo che ad ogni passo si utilizzano solo i valori precedenti, che possono essere salvati in due variabili scalari: F 1 = 1 F 2 = 1 F 3 = F 2 + F 1 F 4 = F 3 + F 2 F 5 = F 4 + F 3 queste somme sono del tipo: f f + valoreprecedente 11

Si ottiene così il seguente algoritmo: intestazione funzione fibonacci3 (n intero) definizione variabili fib, i, prec, prec1 intero prec 1 //F1 fib 1 //F2 per i da 3 a n eseguire prec1 fib //salviamo F 2, prima di fib fib + prec // F 3 = F 2 + F 1 prec prec1 // perché servirà nel //fineper //calcolo di F 4 restituire fib Quale complessità ha l algoritmo che utilizza le sole variabili scalari? tempo spazio O(n): ciclo che viene eseguito n volte O(1): si utilizza un numero costante di locazioni di memoria Per calcolare la complessità dell algoritmo ricorsivo dobbiamo capire come il valore di F(n) cresce, andando all infinito. Possiamo stimare il valore utilizzando un algoritmo numerico. Si cerca una funzione che soddisfi la relazione di ricorrenza F n = F n-1 + F n-2 e si prova con a n, a 0; l equazione diventa: a n = a n-1 + a n-2 da cui raccogliendo a n-2 si ottiene: a n-2 ( a 2 a 1) = 0 Poiché a 0 cerchiamo le soluzioni dell equazione ( a 2 a 1) = 0 e troviamo le due radici reali: φ = (1 + 5 ) / 2 ~ 1.618 φ = (1-5 ) / 2 ~ - 0.618 φ è la sezione aurea. Si può dimostrare che F n = (φ n -φ n ) / 5 Esiste quindi un algoritmo numerico con il quale calcolare il numero F n. Però tale algoritmo non può essere preciso, dal momento che F n è un numero naturale e la radice di 5 è un numero irrazionale: quindi una qualunque applicazione di tale algoritmo fornisce solo una approssimazione. La complessità di tempo e di spazio è O(1). Utilizziamo la formula F n = (φ n -φ n ) / 5 per stimare come F(n) + L algoritmo ricorsivo ha complessità O(F n ); dal momento che: φ < 1 si ha che φ n 0 1< φ < 2 si ha che φ n < 2 n e pertanto l algoritmo cresce in maniera esponenziale con limitazione superiore 2 n : tempo O(2 n ). 12

F 4 F 3 F 3 F 2 F 2 F 1 F 2 F 1 F 5 La torre di Hanoi La leggenda narra che dei sacerdoti di un tempio di Brahma lavorino per spostare una pila di 64 dischi d oro da un piolo ad un altro, utilizzandone uno di appoggio e seguendo delle regole; alla fine del lavoro ci sarà la fine del mondo (par. 8.2). La complessità di spazio è O(n); infatti le chiamate ricorsive si espandono in profondità, non sono contemporanee: F 5, F 4, F 3, F 2, ritorno, F 1, ritorno F 2, calcola F 3, ritorno, A B C La torre di Hanoi La configurazione finale dovrà essere: A B C La torre di Hanoi La regola è la seguente: si può spostare un solo disco alla volta non si può mettere un disco grande su uno piccolo. La soluzione più intuitiva è quella ricorsiva: se spostiamo la pila di n-1 dischi da A a B, possiamo muovere il primo disco da A a C e poi spostare la pila di n-1 dischi da B a C. Indichiamo con H(n, A, B,C) il problema di Hanoi di dimensione n. La torre di Hanoi La torre di Hanoi La scomposizione ricorsiva sarà perciò: H(n-1, A, C, B) H(1, A, B, C) //muove un disco H(n-1, B, A, C) Possiamo scrivere le chiamate ricorsive nel caso n=3. Applichiamo l espansione della formula ricorsiva e vediamo come si muovono i dischi. H(3,A,B,C) H(2,A,C,B) H(1,A,B,C) H(2,B,A,C) H(1,A,B,C) H(1,A,C,B) H(1,C,A,B) H(1,B,C,A) H(1,B,A,C) H(1,A,B,C) 13

La torre di Hanoi Complessità. Quanti sono gli spostamenti ei dischi? Per spostare un disco da un piolo ad un altro, ed ottenere la stessa configurazione, si deve spostare 2 volte la pila di dischi che gli sta sopra; quindi ogni disco si muove un numero di volte che è doppio rispetto al disco che gli sta immediatamente sotto. Contiamo gli spostamenti a partire dal primo: La torre di Hanoi disco 1 1 spostamento 2 2 3 2*2 = 4 = 2 2 4 2*4 = 8 = 2 3 5 2*8 = 16 = 2 4 n 2 n-1 sommiamo gli spostamenti 1 + 2 + 2 2 + 2 3 + 2 4 + + 2 n-1 = 2 n -1 quindi O(2 n ) l algoritmo è esponenziale La torre di Hanoi Si può anche scrivere un algoritmo iterativo, che rimane esponenziale, osservando il movimento dei dischi: disco1 A C disco2 A B C disco3 A C B A C I dischi pari percorrono ciclicamente in ordine alfabetico i pioli, i dischi dispari li percorrono in ordine inverso. Trasformare array paralleli in array di record Trasformare array paralleli in array di record Un array è una struttura di dati omogenea: gli elementi dell array sono tutti dello stesso tipo (che è il tipo dell array). A volte è necessario gestire informazioni di tipo diverso ma riferite allo stesso concetto. Supponiamo di voler memorizzare delle informazioni riguardanti gli impiegati di una ditta: nome, stipendio, età. Possiamo pensare di costruire tre array distinti, uno per ogni tipo di informazione. Trasformare array paralleli in array di record char nome[1000][31]; double stipendio[1000]; int eta[1000]; Per avere informazioni nome stipendio eta sull impiegato i-esimo accediamo alla componente i-esima di ciascuno dei tre array. 14

Trasformare array paralleli in array di record I tre array sono strettamente correlati tra loro: devono avere la stessa lunghezza, un algoritmo che elabora le informazioni su un impiegato deve avere i tre array tra i suoi parametri, se si volesse aggiungere un ulteriore informazione, si dovrebbe tenere presente l organizzazione comune ai tre array (l iesimo impiegato sta all i-esimo posto). Linguaggi come C++ mettono a disposizione la possibilità di considerare l impiegato come un concetto e di considerarlo un unica informazione suddivisa in tre campi. Trasformare array paralleli in array di record Questo tipo di informazione nei linguaggi di programmazione si chiama record e rappresenta una collezione di elementi di tipo diverso. La parola record (registrazione) è una parola antica dei linguaggi di programmazione, così come la parola file (archivio). Sono parole nate in riferimento alla registrazione di dati su supporti fisici come i nastri magnetici (o i dischi); l archivio che contiene l insieme delle informazioni registrate prendeva il nome di file di dati (par. 10.1). Trasformare array paralleli in array di record Noi possiamo realizzare un concetto impiegato ed utilizzare una classe, che è un concetto del C++, oppure una struttura, che è un concetto del C; i campi nome, stipendio, eta saranno le informazioni che caratterizzano l impiegato: struct impiegato{ char nome[31]; double stipendio; int eta; ; Trasformare array paralleli in array di record La struttura impiegato è perciò un unica informazione suddivisa in tre campi: nome stipendio eta char[31] double int I valori che rappresentano le informazioni degli impiegati potranno essere memorizzati in un array. Trasformare array paralleli in array di record Costruiamo un array ditta le cui componenti sono di tipo impiegato: impiegato ditta[100]; In tale modo invece di avere tre array paralleli abbiamo un array di record, detto anche tabella, e per accedere all i-esimo impiegato utilizzeremo la componente i-esima dell array: ditta[i] Trasformare array paralleli in array di record Il record rappresenta quindi una collezione di elementi (campi) di tipo diverso. Per accedere ai singoli campi dell i-esimo impiegato si scrive il nome della i-esima componente (record), seguito da un punto e dal nome del campo: ditta[i].nome ditta[i].stipendio ditta[i].eta 15

Trasformare array paralleli in array di record Sintassi. nomerecord.nomecampo I valori del record saranno memorizzati in un file fisico e memorizzati secondo l ordine di lettura: nome, stipendio, età: Gestione dei file N S E N S E N S E 1 impiegato 2 impiegato ultimo impiegato Gestione dei file Finora abbiamo visto programmi che utilizzano solo i flussi di ingresso standard e questi flussi erano collegati attraverso la ridirezione ai corrispondenti file fisici. Vogliamo definire nel programma un file di lettura o di scrittura, per poter leggere più di un file e poter gestire file diversi da cin e cout. Il file definito nel programma prende il nome di file formale; ad esso sarà associato un file fisico contenente i valori da acquisire. Gestione dei file I file formali diversi dai file standard devono essere definiti tramite un tipo: si deve dichiarare se il file è in lettura o in scrittura. Per poter accedere ad un file questo deve essere esplicitamente aperto e collegato al file fisico. Quando il file non è più in uso, esso deve essere esplicitamente chiuso. Il nome del file formale è noto a livello di programma, il nome del file fisico è noto al file system (par. 13.7). Gestione dei file #include <fstream.h> int main(){ ifstream dati; //lettura ofstream risultati; //scrittura dati.open("nomefiledat"); risultati.open("nomefileris"); dati>>... ; risultati<<...; dati.close(); risultati.close(); return 0; Gestione dei file Il file (fisico) per la lettura deve essere costruito (come i file fisici collegati a cin). La fine di un file viene individuata da un funzione booleana di nome eof (end-of-file): la testina di lettura individua la fine del file e la funzione eof restituisce true se il file è finito e restituisce false altrimenti. eof: false eof: true 16

Gestione dei file Il file fisico in scrittura, se non esiste viene creato: viene costruito un file fisico con il nome indicato nella stringa ed in maniera sequenziale vengono inseriti i dati corrispondenti alle istruzioni di scrittura. Se il file esiste già, esso viene cancellato e poi costruito nuovamente, vale a dire sovrascritto: la testina di scrittura si riposiziona all inizio del file per inserire i nuovi dati. Gestione dei file È necessario chiudere il file: out.close(); non viene segnalato alcun errore ma può succedere che il programma termini prima che i dati siano stati inseriti e pertanto il contenuto del file rimane incompleto. Se è presente l istruzione close, il file viene chiuso e solo dopo il programma termina; pertanto il file viene chiuso solo se tutti i dati sono stati scritti nel file. Scelte multiple: switch Scelte multiple: switch La struttura switch si usa al posto di una situazione in cui ci siano scelte del tipo else if annidate (par. 13.7.2). Esempio. int x, y; cin>>x; if (x == 1) y = 1; else if (x == 2) y = 4; else if (x == 4) y = 16; Scelte multiple: switch Sintassi. Viene valutata una espressione (di tipo intero o carattere: switch(espressione){ case valore1: istruzione1; case valore2: istruzione2; case valore3: istruzione3; dove valore1, valore2, valore3 sono valori costanti e diversi tra loro. Scelte multiple: switch Semantica. Il valore dell espressione viene cercata tra i valori presenti nella case e se viene trovata si esegue l istruzione indicata e tutte le successive. Se si vuole che le istruzioni siano in alternativa, come avviene con l annidamento delle if, si deve utilizzare la parola chiave break. 17

Scelte multiple: switch switch(espressione){ case costante1: istruzione; break; case costante2: istruzione; break; case costante3: istruzione; Se il valore dell espressione non è presente nelle case, non vi è alcuna segnalazione di errore. Per gestire meglio il caso valore non presente, si può introdurre un default. Scelte multiple: switch int k; cin>>k; switch(k){ case val1: istruzione; break; case val2: istruzione; break; case val3: istruzione; break; default: cout<<"il valore di k" << "non e presente \n"; Scelte multiple: switch Se non si usa break, l ordine delle case fa variare le operazioni da eseguire. Esempio. cin>>x; switch (x){ case 1: y = 1; case 2: y = 4; case 4: y = 16; //se x = 1 y = 16 vengono eseguite tutte le assegnazioni su y. Scelte multiple: switch Il valore 1 è dell ultima case: switch (x){ case 4: y = 16; case 2: y = 4; case 1: y = 1; //se x = 1 y = 1 Questa struttura è utile quando si devono scegliere metodi diversi da eseguire in alternativa, come un menu che propone delle scelte: la lettura del menu è sicuramente più chiara che non l elenco delle varie else if. Ciclo do Ciclo do Capita a volte di dover eseguire il corpo di un ciclo almeno una volta, per poi ripeterne l esecuzione se è verificata una particolare condizione. Esempio: leggiamo un valore in ingresso e vogliamo che esso sia positivo: se il valore non è positivo vogliamo poter ripetere la lettura. 18

Ciclo do Ciclo do Sintassi. do { //iterazione while(condizione); Semantica. L iterazione viene eseguita la prima volta; viene valutata la condizione: se è vera si esegue nuovamente l iterazione, se è falsa il ciclo termina. Equivale a scrivere: iterazione; while(condizione){ iterazione; oppure boolean continua =true; while(continua){ iterazione; if(!condizione) continua=false; Ciclo do Esercizio. Calcolare la somma 1 +1./2 + 1./3 + 1./4 + + 1./n + fino a quando 1./n > t con t valore stabilito (ad esempio t = 10-6 ). Provare a scrivere l algoritmo usando sia il ciclo while che il ciclo do. 19