Lezione 1 - Algoritmi + strutture dati = programmi Corso — Programmazione Programmazione e progettazione — Strutture dati e sottoprogrammi Marco Anisetti e-mail: [email protected] web: http://homes.di.unimi.it/anisetti/ Scrivere programmi • Descrivere cioè algoritmi con un linguaggio per cui l'esecutore è il calcolatore. • Se si parte dal problema da risolvere serve l'abilità di descrivere la soluzione pensando in termini di algoritmi (Algorithmic Thinking) • Per scrivere un programma abbiamo bisogno di Descrivere i dati Definire le istruzioni che operano sui dati (istruzioni date dall'algoritmo e codificate in un linguaggio) • Flusso Partenza dai dati iniziali Algoritmo (dati + istruzioni) Otteniamo dei dati finali (soluzione alla nostra istanza di problema) Algorithmic Thinking(1) • Un programmatore deve sviluppare l'abilità di capire, eseguire, valutare e creare algoritmi • Capire ed eseguire: Attitudine a seguire sequenze precise di istruzioni senza perdere la pazienza e con estrema diligenza e perseveranza. A volte è veramente tedioso seguire step-by-step un algoritmo complesso, ma non bisogna arrendersi • Valutare: Determinare se un algoritmo risolve il problema per cui è nato • Creare Dato un problema trovare una soluzione algoritmica al problema • Tutto questo deve essere fatto sapendo quel è l'esecutore • Gli algoritmi trattati nel corso sono quelli eseguibili da un calcolatore Algorithmic Thinking(2) • Problem-solver vs Programmer • Disaccoppiare il linguaggio di programmazione dall'algoritmo • Uno strumento agevole per farlo è come abbiamo già intuito il flow chart • I flow chart introducono un certo carico di lavoro dovuto al disegno e alla difficoltà della modifica se non si utilizzano strumenti elettronici • Se fosse possibile programmare usando i flow chart servirebbe uno strumento che li converta nel relativo codice macchina per permetterne l'esecuzione Descrivere i dati • Per il momento non ci siamo preoccupati se non in maniera superficiale della descrizione dei dati • Abbiamo parlato genericamente di variabili e del fatto che occupino una locazione di memoria di dimensione dipendente dal tipo • Abbiamo visto che il tipo determina le istruzioni che possono essere eseguite su di esso • I dati di un problema devono essere descritti con lo stesso rigore e precisione con la quale si determina l'algoritmo per risolvere un problema • I dati possono essere semplici o strutturati ed organizzati in dati complessi Algoritmi + strutture dati = programmi • Nel momento in cui si passa da algoritmo a programma le strutture dati devono essere esplicitate • I problemi affrontati sono talmente semplici che bastano variabili semplici assimilabili a incognite matematiche • Sembra che l'algoritmo dipenda soprattutto dal set di istruzioni che lo compongono • In realtà conta moltissimo anche la struttura dati su cui agisce • Lo stesso problema si può risolvere con algoritmi differenti Lo stesso algoritmo può avere delle incarnazioni differenti se la struttura su cui si appoggia è differente Una struttura dati adeguata può aiutare a risolvere il problema e a scrivere il programma relativo, più agevolmente (embrione della programmazione ad oggetti) Istruzioni e strutture dati(1) • Un programma per funzionare deve essere caricato in memoria e da li può essere eseguito dall'elaboratore • Questo ci suggerisce che anche le istruzioni di un programma risiederanno in memoria assieme alle strutture dati dello stesso programma • Per le competenze che abbiamo raggiunto fino ad ora possiamo dire che la memoria allocata per il programma in esecuzione viene divisa tra codice e dati • In realtà esistono differenti strategie per la gestione della memoria • Un programma in esecuzione (processo) potrebbe aver un bisogno crescente di memoria dati (dati dinamici) • Il segmento dati deve poter crescere, il segmento codice non cresce Segmentazione della memoria(1) • E' importante per un programmatore comprendere cosa succede al programma che scrive nel momento in cui viene eseguito • Per prima cosa quando viene eseguito il programma è stato trasformato in un codice macchina direttamente interpretabile dall'elaboratore • In generale i riferimenti ad indirizzi di memoria locali al processo (variabili) sono espressi in forma relativa (dipende dal traduttore usato per passare al linguaggio macchina) • La gestione della memoria e del caricamento del programma in memoria è a carico del Sistema Operativo (definisce l'indirizzo di base) Approfondimenti nel corso di Sistemi Operativi Segmentazione della memoria(2) • Per le nostre competenze attuali possiamo dire: • Segmento codice: Contiene il codice del programma (ed in alcuni casi le costanti). Normalmente viene condiviso fra tutti i processi che eseguono lo stesso programma. Viene marcato in sola lettura per evitare sovrascritture accidentali (o maliziose) • Il segmento dei dati: Contiene le variabili (a volte diviso in due parti, variabili inizializzate e non inizializzate) Lezione 2 - Tipi da dato elementare Corso — Programmazione Programmazione e progettazione — Strutture dati e sottoprogrammi Marco Anisetti e-mail: [email protected] web: http://homes.di.unimi.it/anisetti/ Università degli Studi di Milano — Dipartimento di informatica Modificatori di tipo Costante • Costante è intuitivamente un valore che non cambia per il lasso di tempo in cui viene usato • Costante è differente da letterale • Un letterale è un carattere che viene inserito in una espressione Esempio x + 5 x variabile 5 letterale • Una costante in programmazione è un modificatore di tipo che indica che una variabile non potrà mai cambiare valore Variabili e costanti • Sia variabili che costanti potrebbero in alcuni linguaggi di programmazione richiedere una loro 00 dichiarazione00 • Se esiste l'obbligo della dichiarazione questa permette di definire sia variabili che costanti e deve necessariamente indicarne un tipo (almeno per le variabili) • La dichiarazione deve avvenire prima dell'utilizzo della variabile o costante • La costante deve essere solitamente inizializzata con il suo valore nel momento che viene dichiarata • A volte come nel caso del Pascal la costante non richiede il tipo perchè è immediatamente deducibile dalla sua inizializzazione Tipi di dato • In generale un linguaggio di programmazione permette il trattamento di diversi tipi di dato • Un tipo di dato è determinato dal range di valori che può assumere un oggetto di quel tipo e da delle operazioni che si possono effettuare su tale tipo • I tipi più comunemente presenti nei linguaggi di programmazione sono catalogabili in tipi semplici, tipi strutturati e tipi puntatore Tipi di dato semplice(1) I tipi semplici solitamente annoverano: • Numeri interi: indica il campo dei numeri interi, ovviamente ristretto dai valori MAXINT e MININT. Gli operatori definiti sono solitamente quelli di +, -, *, div e mod • Numeri reali: I numeri reali formano un continuo, mentre nei calcolatori il tipo reale può contenere un numero finito di valori ciascuno per rappresentare un intervallo del continuo. Questa approssimazione implica un errore che è campo d'indagine per il calcolo numerico. I processi che utilizzano il tipo reale sono detti numerici. Tipi di dato semplice(2) • Carattere: indica il campo finito ed ordinato di simboli (caratteri). Richiede una standardizzazione per associare un codice ad un carattere es ASCII. Esistono i codici sia dei caratteri di stampa che dei caratteri di controllo (es. carriage return, line feed) • Booleani: assumono due valori logici True o False, sono definiti degli operatori come AND OR NOT ecc. Tutti gli operatori di relazione solitamente ritornano un valore booleano. Di solito vengono descritte delle tabelle di verità per valutare i risultati degli operatori. Le espressioni basate su booleani e operatori possono essere valutate con tecniche short-circuit Tipi di dato: numeri reali(1) • Virgola fissa, si stabiliscono a priori le cifre per la parte decimale e per quella intera • Virgola mobile, rappresentazione matematica esponenziale dei numeri mediante mantissa m ed esponente e: x = m ∗ Be , dove B è la base della rappresentazione è può essere 10, 16 o 2 a seconda dell'elaboratore (standard IEEE 754) • La precisione di una aritmetica in virgola mobile può essere definita come ovvero il più piccolo numero positivo tale che 1 6= (1 + ) (se precisione di n cifre decimali = 10−n ), ed è fonte di errori che un programmatore deve considerare Tipi di dato: numeri reali(2) • Alcune proprietà delle operazioni vengono meno proprio per questa imprecisione [ESEMPIO associatività] Consideriamo un'aritmetica a 4 cifre, x = 9. 900, y = 1. 000, z = −0. 999 (x + y) + z = 10. 90 + (−0. 999) = 9. 910 x + (y + z) = 9. 900 + 0. 001 = 9. 901 Approssimazione nei reali: esempio(1) • Soluzione di una equazione di secondo grado (problema della cancellazione) ax 2 + bx + c = 0 • Si risolve matematicamente come noto con d ← sqrt((b ∗ b) − 4 ∗ a ∗ c) x2 ← −(b + d)/(2 ∗ a); x1 ← (d − b)/(2 ∗ a) • Con una aritmetica a 4 cifre e con i seguenti numeri a = 1. 000 ,b = −200. 0, c = 1. 000 si avrebbe: d = sqrt(40000 − 4. 000) = 200. 0 x1 = 400. 0/2. 000 = 200. 0 x2 = 0. 000/2. 000 = 0 • Ma le soluzioni corrette alla quarta cifra sono x1 = 200. 0 e x2 = 0. 005 • Non sempre si possono applicare metodi risolutivi della matematica quando si ha a che fare calcolatori numerici Approssimazione nei reali: esempio(2) • Si può superare questo limite in questo caso usando un algoritmo differente che sfrutta la relazione x1 ∗ x2 = c/a • Quindi si può calcolare una soluzione (con valore maggiore) e poi applicare questa relazione per ricavare l'altra [esempio] d ← sqrt((b ∗ b) − 4 ∗ a ∗ c) if b ≥ 0 then x1 = −(b + d)/(2 ∗ a) else x1 = (d − b)/(2 ∗ a) endif x2 = c/(x1 ∗ a) end Tipi puntatore • Puntatore o riferimento: si riferisce all'indirizzo di memoria dove una determinato oggetto (di un tipo conosciuto) risiede. Vengono usati per questioni di efficienza e per gestire tipi dinamici che possono crescere o decrescere a run-time • Di solito il controllo del tipo è molto stringente sui puntatori per evitare che essi vengano interpretati come altri tipi. • Le operazioni che si fanno sui puntatori sono di solito allocazione, riferimento, assegnamento, controllo di uguaglianza, deallocazione • Esempio: se x contiene l'indirizzo di una variabile intera y, allora si dice che x punta alla variabile intera y e si indica con un operatore di riferimento a seconda del linguaggio (∗x linguaggio C) Ricapitolando • L'associazione tra operatori e tipi viene definita nei manuali dei linguaggi • In questa fase ci interessano considerazioni più generiche • Rappresentazione dei dati in forma interpretabile dalla macchina • I tipi semplici fittano in una locazione della macchina • I tipi strutturati derivano dai tipi base e sfruttano sequenze contigue di locazioni della macchina • La rappresentazione di tipi che formano un continuo come i reali è discretizzata introducendo delle approssimazioni • Tipi puntatore contengono indirizzi di memoria e si riferiscono sempre ad un tipo semplice o strutturato • Costanti sono un modificatore di tipo • La tipizzazione aiuta il programmatore nella codifica Lezione 3 - Flow Chart con RAPTOR Corso — Programmazione Programmazione e progettazione — Strutture dati e sottoprogrammi Marco Anisetti e-mail: [email protected] web: http://homes.di.unimi.it/anisetti/ Università degli Studi di Milano — Dipartimento di informatica Flow chart con RAPTOR • Si tratta di un ambiente grafico che permette il disegno dei flow chart e la loro esecuzione automatica • Si seguono le regole per la definizione del flow chart già viste • Esistono dei formalismi aggiuntivi per determinare dei blocchi che eseguono i seguenti compiti: Input: un trapezio con una freccia entrante Output: un trapezio con una freccia uscente Call: un trapezio con una doppia freccia uscente, determina la chiamata di una funzionalità di libreria • E' possibile rappresentare tutti i costrutti basilari della programmazione strutturata Flow chart con RAPTOR RAPTOR: le variabili(1) • Le variabili hanno un ruolo importante in ogni tipo di programma • RAPTOR gestisce le variabili in modo che queste siano non specificate fino a che non vengono assegnate • RAPTOR non prevede una fase dichiarativa per le variabili • Una variabile si vede cambiare o settare il proprio valore in tre modi: Un valore inserito da una istruzione di input Il valore assegnato relativamente al calcolo di una equazione Valore di ritorno ottenuto da una chiamata a una funzionalità di libreria o chiamata a procedura • Una variabile è identificata dal suo nome (identificatore) che serve seguire delle regole semplici come l'impossibilità che inizi per un numero, che contenga spazi o caratteri non strettamente alfanumerici RAPTOR: le variabili(2) • Alloca virtualmente la memoria per la variabile indicata del relativo identificatore nel momento in cui la trova per la prima volta. • La variabile esiste fino al termine del programma • Quando la variabile viene creata allora si determina anche il tipo • Esistono solamente due tipi base in RAPTOR il tipo numero e il tipo testuale • Le variabili non possono cambiare tipo durante l'esecuzione del programma Le costanti • Sono degli identificatori che si rifanno ad una zona di memoria che non è modificabile dopo che è stato definito il suo contenuto la prima volta • In RAPTOR esistono delle costanti di sistema definite per facilitare il calcolo di espressioni matematiche es pi = π ed e • In generale un linguaggio di programmazione permette di definire delle costanti che rimangono tali per la durata del programma RAPTOR: Statements o istruzioni(1) • Trattandosi di flow chart i tipi di istruzioni sono codificati nel costrutto grafico utilizzato • In RAPTOR esistono costrutti per istruzioni di tipo: Input, Assignment, Call, and Output • Input Statement: Permette di specificare un testo da usare come prompt e il nome della variabile dove verrà salvato l'input • Assignment Statement: Generalmente serve per effettuare un calcolo e salvare il risultato in una variabile. In generale la parte destra potrebbe contenere espressioni di varia complessità Le espressioni matematiche(1) • Definite utilizzando un linguaggio e una notazione (prefissa, infissa, postfissa) • E' composta da operatori e operandi • Gli operandi possono essere delle costanti (numeri) delle funzioni (es funzioni matematiche predefinite) o delle variabili • RAPTOR adotta la notazione infissa e quindi le normali regole di precedenza tra operatori • Nel dettaglio le precedenze in Raptor sono: 1. 2. 3. 4. 5. Le funzioni Ogni cosa nelle parentesi Esponenziali Moltiplicazioni e divisioni da sinistra a destra Somme e sottrazioni da sinistra a destra Le espressioni matematiche(2) • Ogni linguaggio di programmazione definisce delle funzioni aggiuntive agli operatori matematici elementari come ad esempio operatori per il calcolo del modulo della divisione (mod) della radice quadrata (sqrt) del valore assoluto (abs) ecc. • Tali possono richiedere una rappresentazione in termini di funzione con la notazione < nome funzione > (< parametri funzione >) • Le espressioni matematiche sono espressioni che coinvolgono oggetti di tipo numerico • In generale un linguaggio potrebbe permettere espressioni che contengano oggetti di altro tipo Le espressioni generiche • Una espressione è una combinazione di oggetti che avviene attraverso degli operatori che sono definiti per quegli oggetti • In RAPTOR esistono solo due tipi di dati i numeri e il testo • Esiste un operatore che è definito sul testo che è l'operatore ``+'' che permette di concatenare testo • Tale operatore è definito anche per la coppia < numero > < testo > ed agisce convertendo il numero in testo (senza assegnare questa conversione) e concatenando. Altri esempi di operatori ridefiniti sono il * tra < numero > < stringa > in python (ripete la stringa per < numero > di volte) Ricapitolando • RAPTOR permette di programmare usando i costrutti dei flow chart • Come tutti i linguaggi di programmazione ha delle caratteristiche associate alla gestione di variabili e tipi • La forma grafica identifica la tipologia di istruzione • L'istruzione scritta all'interno della forma è pilotata • Le espressioni matematiche vengono gestite in modo classico • Alcuni operatori hanno delle specificità tipo l'operatore ``+'' Lezione 4 - Utilizzo di RAPTOR Corso — Programmazione Programmazione e progettazione — Strutture dati e sottoprogrammi Marco Anisetti e-mail: [email protected] web: http://homes.di.unimi.it/anisetti/ Università degli Studi di Milano — Dipartimento di informatica Ricapitolando • Abbiamo visto come si può usare RAPTOR per scrivere uno dei programmi visti fino ad ora a lezione • RAPTOR permette di valutare un flow chart in modo automatico • Ha delle limitazioni dovute a delle semplificazioni • Aiuta a pensare in termini di algoritmo ed evidenzia il controllo di flusso Lezione 5 - Relazioni di ricorrenza e tipi di dato strutturato (parte I) Corso — Programmazione Programmazione e progettazione — Strutture dati e sottoprogrammi Marco Anisetti e-mail: [email protected] web: http://homes.di.unimi.it/anisetti/ Programmi basati su relazioni di ricorrenza(1) • La ricorrenza prende spunto dal concetto matematico di induzione • La ricorrenza può avere due forme nella programmazione, le ripetizioni (ciclo) e la ricorsione • Vedremo prevalentemente programmi che si basano su ripetizioni semplici nella forma pre-condizionata while cond do I • Le ripetizioni hanno senso se terminano ovvero se I modifica almeno una variabile che compare in cond rendendola non più soddisfatta • Il ciclo sopra può diventare logicamente il seguente: V← v0 ; while p(V) do V ← f(V) • Dove p è un predicato (booleano) e f una funzione • All'inizio della ripetizione V deve avere un valore ben determinato V0 Programmi basati su relazioni di ricorrenza(2) • Esempio: Calcolo fattoriale f (n) = 1 ∗ 2 ∗ 3 ∗ · · · ∗ n = n! (n ≥ 0) • E' calcolabile usando una relazione di ricorrenza • f (n) = n ∗ f (n − 1) per n > 0 • Avendo come condizione iniziale f (0) = 1 Tipi di dato strutturato: array(1) • Vettori o array: struttura che contiene dati omogenei (dello stesso tipo), aventi uno stesso identificatore. L'accesso agli elementi del vettore avviene attraverso indici che definiscono le posizioni nel vettore (accesso diretto) Si tratta di una struttura sequenziale (memorizzata in una struttura di memoria contigua) Si tratta di una struttura statica, ovvero la sua dimensione deve essere definita a priori e non può essere modificata durante l'esecuzione del programma Sequenzialità e staticità sono legati chiaramente da come viene allocato lo spazio per un vettore in memoria Tipi di dato strutturato: array(2) • La gestione di un array avviene tramite tre diverse informazioni: L'indirizzo di base IB dato dalla variabile definita come array stessa, il numero di elementi N, la dimensione del singolo elemento D • Queste informazioni assieme al fatto che si tratta di una struttura dati contigua permette l'accesso diretto tipico degli array • Esempio: se volessi accedere all'elemento J di un array allora dovrei recuperare il dato che si trova in IB + D ∗ (J − 1) • Può avvenire o meno il controllo dello sforamento della dimensione dell'array • Generalmente non sono accettati array di dimensione dinamica non nota in fase di compilazione Tipi di dato strutturato: array(3) • Inizializzazione e caricamento sono due fasi simili in cui un vettore viene riempito dei dati che servono al programma • L'inizializzazione avviene solitamente in fase di dichiarazione, si tratta del primo caricamento di dati nel vettore • Il caricamento è una operazione che prevede l'inserzione di dati in un vettore in modo completo (simile all'inizializzazione) o parziale • Solitamente la notazione per l'array prevede l'uso delle parentesi [] per racchiudere l'indice o la dimensione se si sta valutando la fase dichiarativa Array in RAPTOR • RAPTOR non prevedendo la fase dichiarativa considera un array ogni identificatore seguito da ``[< numero >]'' • Nel momento in cui viene utilizzato per la prima volta ad esempio il vettore v[n], raptor provvede ad inserire anche i valori per v[(n − 1). . 1] ovvero alloca lo spazio per gli elementi del vettore inferiori a n Caricamento di un array • Il caricamento dei dati in un array avviene attraverso un ciclo che ad ogni iterazione inserisce un valore e sposta l'indice al prossimo elemento Utilità di un array • Gli array sono utilizzati spesso in associazione a cicli • Sono indispensabili per risolvere problemi in cui abbiamo aggregazioni di dati da elaborare scandendoli in vario modo • Permettono di aggregare variabili dello stesso tipo e che si possono correlare tra loro distinguendole in base ad indici • Esistono diverse operazioni che ha senso studiare sugli array e che possono tornare utili per la scrittura di un programma Array: esempio • Dato un array V di 10 elementi contenente interi, restituire a video un array A con gli elementi invertiti (ultimo al primo posto etc) • Analisi: La richiesta equivale a scorrere il vettore in senso opposto dall'ultimo elemento al primo dato che devo solo stamparli a video [Pseudcodice] integer v[10]={2,3,6,1,8,3,9,2,5,9} i← 10 while (i>0) do stampa(a[i]) i← i-1 endwhile Matrici • Le matrici sono degli array multidimensionali • Dal punto di vista degli array visti fino ad ora si tratta di vettori che hanno come elementi altri vettori • Per individuare un elemento di una matrice sono necessari due indici • Solitamente il primo indice identifica le righe mentre il secondo le colonne • In RAPTOR come in Pascal una matrice è definita come M[i,j] con i e j indici per righe e colonne Matrici: esempio • Scrivere il programma che data una matrice ritorna gli elementi della diagonale principale • Analisi: gli elementi richiesti sono quelli che stanno nelle posizioni dove i due indici sono uguali [Pseudcodice] integer v[10,10] i← 1 j← 1 while (i<=10 and j<=10) do stampa(v[i,j]) i← i+1 j← j+1 endwhile • Si può scrivere in maniera più compatta? Tipi di dato strutturato: record(1) • La natura dei dati di un problema può essere troppo complessa per essere descritta con variabili semplici o array • Ad esempio una transazione bancaria racchiudono conto corrente, tipo di operazione ed importo • Volendo con 3 variabili semplici si potrebbe risolvere il problema senza però che esse riferiscano espressamente alla stessa transazione • Non si tratta di descrivere un oggetto ma di trovare un modo più opportuno per descrivere una classe di oggetti con caratteristiche comuni (attributi) • Esempio attributi di un'auto possono essere Marca, Tipo, Colore, Numero di telaio. Essi sono comuni alla classe Automobile, ogni automobile è caratterizzata da dei precisi valori per questi attributi Tipi di dato strutturato: record(2) • Un Record è una struttura dati complessa che cattura questo concetto. E' composto da un insieme finito di elementi (campi) di qualsiasi tipo che sono però logicamente connessi • I campi corrispondono agli attributi: ogni campo contiene un valore per un attributo • Ogni record è individuato dal suo identificatore, mentre ogni suo campo è individuato dall'identificatore del record e dall' identificatore del campo stesso • Si può vedere analogamente all'array come un rettangolo suddiviso ma in blocchi che possono essere anche di dimensioni differenti, con gli attributi al posto degli indici e i campi sotto gli attributi fuori dal rettangolo Record: esempio • Consideriamo un array di strutture di tipo anagrafico comprendenti nome e cognome. Scrivere un programma che dato in ingresso il cognome trova tutti i nomi relativi e li stampa a video assieme al cognome [Pseudcodice] type anagrafica = record nome:stringa cognome:stringa end anagrafica an [10] i← 1 leggi(nom) while (i<=10) do if (an[i].nome==nom) then stampa (nom, an[i].cognome) i← i+1 endwhile Controlli sui tipi • I tipi associati a delle variabili si estendono alle espressioni (type system del linguaggio) Operatori aritmetici Overload: significati differenti a seconda del contesto Coercion: promozioni automatiche a tipi differenti per risolvere conflitti Polimorfismo: una funzione polimorfica ha un tipo parametrico o generico • A seconda del linguaggio il tipo di una variabile può essere fisso o meno Valore dinamico ma tipo fisso (binding statico) Tipo associato run-time (binding dinamico) • Equivalenza tra tipi • Controllo statico o dinamico Errore sul tipo avviene quando una funzione (operatore) si aspetta un tipo come argomento e ne trova un altro Controlli sul codice prima che venga tradotto Controlli sui tipi run-time (controllo sforamento indici di un array) Dati strutturati in memoria • Vediamo un esempio di come dovrebbero essere allocate le variabili strutturate in memoria Ricapitolando • L'importanza di poter derivare delle relazioni di ricorrenza • Relazione tra tipo di dato strutturato array e i cicli Cicli che scorrono indici, alcuni linguaggi hanno definito un costrutto ad hoc (compatto) per questo genere di cicli (es for) • Operazioni base di ricerca merge e ordinamento su array • Definizione di array multidimensionali (matrici) • Definizione di tipo strutturato record • Controlli sui tipi • Occupazione di memoria Lezione 6 - Sottoprogrammi Corso — Programmazione Programmazione e progettazione — Strutture dati e sottoprogrammi Marco Anisetti e-mail: [email protected] web: http://homes.di.unimi.it/anisetti/ Università degli Studi di Milano — Dipartimento di informatica Sottoprogrammi e astrazione • Il concetto di astrazione è un elemento fondamentale per risolvere i problemi • E' assodato che il cervello umano non riesce a parallelizzare il pensiero in modo indefinito • La soluzione di un problema passa quindi obbligatoriamente da delle astrazioni che limitano il numero di problemi che vengono gestiti contemporaneamente dal nostro cervello (sottoproblemi). • Questo approccio è alla base della progettazione Top Down e dell'organizzazione procedurale dei programmi • L'astrazione procedurale equivale al raggruppamento di più problemi dettagliati in un unico macro-problema • Un esempio analogo all'approccio modulare Esempio: calcolo delle radici(1) Calcolo delle soluzioni reali di ax 2 + bx + c = 0 Prima approssimazione della soluzione, approccio modulare Esempio: calcolo delle radici(2) Versione più dettagliano il modulo del calcolo delle radici Sottoprogrammi • Nella risoluzione dei problemi e nella stesura dei programmi si possono presentare i seguenti casi: Ripetere identicamente più volte la stessa sequenza di istruzioni Ripetere la stessa sequenza di istruzioni su variabili che possono avere valori o identificatori diversi da quelli assunti la volta precedente Copiare o inserire in un programma un altro programma già realizzato Utilizzare un programma di libreria • Un termine generico per definire il raggruppamento procedurale è quello di sottoprogramma • A tale raggruppamento viene assegnato un nome univoco per il programma chiamato identificatore del sottoprogramma • Alcuni termini più specifici derivano da come questo sottoprogramma si relaziona con il programma principale Sottoprogrammi: classificazione • Macro: un insieme di istruzioni al quale viene assegnato un nome. Il nome della macro viene rimpiazzato dall'insieme delle istruzioni relative prima di passare il sorgente al traduttore che lo codifica in un linguaggio comprensibile alla macchina (sottoprogramma improprio) • Procedura: un insieme di istruzioni alle quale viene dato un nome e che definiscono un' unità di esecuzione a sé stante. Una procedura comunica con il programma che l'ha chiamata attraverso dei parametri passati tra ``()'' dopo il suo nome, generalmente una procedura non restituisce un valore di output (terminologia ALGOL) • Funzione: una procedura che restituisce uno o più valori in output. Simile alle funzioni matematiche da cui prende il nome Struttura a livelli • I sottoproblemi risolti da un sottoprogramma generico possono a loro volta essere suddivisi in sotto-sottoproblemi • Si arriva ad una gerarchia a livelli dove vengono riassunte le relazioni tra sottoprogrammi e dove la radice comune è costituita dal programma principale detto Main [Sottoprogramma] Un problema di tipo procedurale caratterizzato da un algoritmo A che opera su dati D per produrre risultati R si può partizionare in n sottoproblemi procedurali a differenti livelli ove l'i-esimo è caratterizzato da un algoritmo Ai che opera su dati Di ⊂ (D0 + R0 + · · · + Ri−1 ) con D0 ∈ D e R0 insieme vuoto Procedura e funzioni in pseudocodice(1) • Consideriamo il seguente pseudocodice [Procedurale] [Sequenziale] t ← r mod q; r ← q; q← t; Procedure P begin t ← r mod q; r ← q; q← t; end • Da notare l'uso esplicito della definizione del blocco che appartiene alla procedura tramite keyword begin end (altri linguaggi usano le {}) • I blocchi begin end possono essere usati anche in associazione ad altri costrutti in dipendenza dal linguaggio Procedura e funzioni in pseudocodice(2) • La procedura è individuata da una intestazione (P) e da un corpo (racchiuso tra begin end) • L'intestazione contiene l'identificativo della procedura (P) ed eventuali parametri • E' evidente che P è una procedura, se fosse una funzione andrebbe indicato cosa torna e ci dovrebbe essere una istruzione nel corpo che indica il valore da ritornare (possiamo chiamarla return(<cosa>)) oppure si assegna il valore al nome della funzione • Altre cose fondamentali da indicare sono i tipi dei parametri passati e dell'eventuale output Esempio: Function F(x:real):real begin f ← x*x+sqrt(x);end Ci indica come si deve chiamare che prende in ingresso un reale e ritorna un reale Nel suo corpo vediamo che viene usata una funziona matematica predefinita dal linguaggio che è sqrt(< var >) Passaggio di parametri • Passaggio per valore: Viene passata una copia del valore della variabile utilizzata al momento della chiamata. • Passaggio per indirizzo o riferimento: Viene passato un riferimento alla variabile del chiamante, che quindi non viene duplicata nel sottoprogramma ma può essere usata e modificata dal chiamato agendo sulla variabile del chiamante. • Output di una funzione: Si tratta di un valore che viene ritornato esplicitamente dal sottoprogramma (che in questo caso è in sostanza una funzione). • La definizione formale di funzione prevederebbe il ritorno di un solo parametro, tale regola è man mano scomparsa nella programmazione odierna • I parametri vengono definiti nel momento in cui la procedura viene creata (parametri formali) e devono essere inclusi nel momento in cui la procedura viene chiamata (parametri attuali) Importanza dei sottoprogrammi • Fornisce uno strumento potente per scomporre il problema in sottoproblemi più gestibili • Non serve solo per abbreviare il lavoro di codifica ma in modo essenziale per articolare, suddividere e strutturare un programma in componenti fra loro coerenti • La struttura è determinante per la comprensibilità del programma L' abbiamo visto a livello di costrutti Concetto analogo se ci spostiamo a livello di programma • Migliora la leggibilità e la verificabilità del codice • L'estrazione di una parte di programma in un sottoprogramma permette di mettere in risalto le variabili da essa influenzate e di porre in risalto le condizioni da soddisfare per l'ottenimento del risultato intermedio • Mettono in evidenza i campi di influenza delle variabili contenute Concetto di scope • Un qualsiasi oggetto (variabile, costante, procedura, funzione) che è significativo soltanto in una determinata parte di un programma è detto locale • Al contrario se è sempre significativo è detto globale • Le procedure e le funzioni costituiscono degli ambiti di esecuzione separati dal resto del programma e che dialogano con esso tramite l'interfaccia definita dalla loro intestazione (o attraverso delle variabili globali) • Le macro non sono un abito di esecuzione • Per questo motivo non definiscono un ambito locale • Gli oggetti dichiarati all'interno del corpo di una procedura o funzione sono locali a quella • Località o globalità è genericamente detta visibilità • Si dice scope di una variabile il suo ambito di visibilità • Anche blocchi begin end possono definire uno scope Funzione: esempio(1) • Esempio di due funzioni con passaggi di parametri differenti [Passaggio per indirizzo] [Passaggio per valore] procedure swap(var x,y: integer) integer z procedure swap(x,y: integer) begin integer z z=x;x=y;y=z; begin end z=x;x=y;y=z; a=10; end b=5; a=10; swap(a,b); b=5; swap(a,b); Funzione: esempio(2) • Consideriamo al chiamata della swap di prima con i parametri passati per riferimento • Cosa succede se la chiamata fosse swap(i,A[i])? • Se i=2 e A[i] =99 allora il corpo che eseguirebbe z=x;x=y;y=z; esegue: z=2; i=99; A[2]=z; Call and return(1) • Essendo ambiti di esecuzione separati le procedure o funzioni vengono effettivamente chiamate dal programma che ne fa uso e questa operazione scatena una serie di eventi • Come cosa fondamentale un salto all'esecuzione di codice della procedura/funzione, la sospensione dell'esecuzione del chiamante e il relativa salto indietro quando termina Call and return(2) • Esiste una struttura di supporto per memorizzare l'indirizzo di ritorno dopo la chiamata, i parametri passati e utilizzati da procedure o funzioni • Tale struttura è un segmento di memoria del processo chiamata stack • E' una struttura dati astratta che agisce come una pila LIFO (Last In First Out) • Per primo viene inserito l'indirizzo di ritorno (l'indirizzo dell'istruzione successiva alla chiamata) • In seguito i parametri • Poi le eventuali dichiarazioni locali alla procedura o funzione Memoria del processo • Con la gestione dei sottoprogrammi la struttura della memoria si arricchisce di un segmento stack Record di attivazione(1) • La visibilità della procedura o funzione è determinata dallo stack • L'insieme di oggetti che vengono caricati nello stack ad ogni chiamata si chiama record di attivazione • Ogni esecuzione di sottoprogramma ha il suo record di attivazione • Anche il programma principale main ha un record di attivazione nello stack Record di attivazione(2) [Esempio in linguaggio C] void Prima(int a){ int b; } void Seconda(float c){ } int main(){ int num=100; Prima(10); Seconda(num); } Record di attivazione(3) Risoluzione macro Macro espansione a compile time Controllo dei parametri attraverso un metodo grafico(1) • Serve per permettere agli studenti alle prime armi di controllare i valori delle variabili passate e la loro validità • Ad ogni variabile viene associato ad un rettangolo che rappresenta l'area di memoria nella quale viene scritto il valore della variabile. L'identificatore della variabile viene scritto all'esterno. • Bisogna tener presente i seguenti casi: • Variabili ridichiarate: ad ogni ridichiarazione si costruisce un rettangolo dove l'identificatore è quello della variabile ridichiarata con a pedice il nome del sottoprogramma dove avviene la ridichiarazione. Il rettangolo si cancella quando si esce dal sottoprogramma Controllo dei parametri attraverso un metodo grafico(2) • Parametro trasmesso per valore: all'atto della chiamata si crea un nuovo rettangolo con identificatore uguale al nome del parametro formale e valore uguale al valore del parametro attuale corrispondente. Al rientro il parametro viene cancellato • Parametro passato per indirizzo o riferimento: quando avviene la chiamata il rettangolo esiste già, serve solo indicare esternamente l'identificatore del parametro formale che corrisponde all'attuale già esistente. In questo caso l'area di memoria risulta avere due nomi. L'identificatore del parametro formale viene cancellato all'uscita dal sottoprogramma Controllo dei parametri attraverso un metodo grafico(3) Lezione 7 - Chiamate a sottoprogrammi con RAPTOR Corso — Programmazione Programmazione e progettazione — Strutture dati e sottoprogrammi Marco Anisetti e-mail: [email protected] web: http://homes.di.unimi.it/anisetti/ RAPTOR: Statements(2) • Procedure call Statement: Una procedura è un insieme di istruzioni che servono per eseguire un compito al quale insieme viene affidato un nome. Tale nome serve per richiamare la procedura. La chiamata di una procedura sospende l'esecuzione del programma per preoccuparsi dell'esecuzione della procedura, che una volta terminata restituisce il controllo al programma. Le procedure possono avere associati dei parametri che vengono passati come ingressi al codice che esegue la procedura • Si tratta di chiamate a funzionalità di libreria interne a RAPTOR. Alcune di queste funzioni permettono di aprire finestre grafiche, disegnare linee ecc • Output Statement: Scrive in output sulla finestra MasterConsole quanto indicato nello statement RAPTOR: commenti • Come per ogni linguaggio di programmazione è possibile inserire dei commenti per spiegare le scelte implementative • In Raptor per inserire un commento basta cliccare sul tasto destro sopra un elemento del flow chart e scegliere l'opzione comment • Intestazione del programma: Commenti su chi ha scritto il programma quando e una descrizione generale di cosa fa (commento al simbolo start) • Descrizione delle sezione: Commenti alle sezioni più significative del programma, servono per far comprendere la struttura del programma stesso • Descrizione logica: Commenti a istruzioni particolarmente significative e di non semplice lettura RAPTOR: Strutture di controllo(1) • Sono le tipiche strutture di controllo del flow chart, ovvero selezione ed iterazione • Selection Control: Serve per definire delle selezioni basati su una decisione. La decisione avviene nel momento in cui si valuta la condizione • La valutazione della condizione equivale alla valutazione di un predicato booleano e segue tutte le regole dell'algebra di Bool. Essendo in sostanza analoga alla valutazione di una espressione ha rilevanza l'ordine con il quale viene valutata. Le espressioni matematiche vengono valutate con le precedenze già descritte mentre per il resto: 1. 2. 3. 4. 5. Operatori (=! = / =<<=>>=) da sinistra a destra Operatore not da sinistra a destra Operatore and da sinistra a destra Operatore xor da sinistra a destra Operatore or da sinistra a destra RAPTOR: Strutture di controllo(2) • Loop control: Rappresenta ogni tipo di costrutto iterativo tramite una selezione ed un ritorno del ciclo (ellisse con loop). E' possibile definire sia cicli post-condizionati che pre-condizionati • Per costruire cicli pre o post condizionati serve aggiungere prima o dopo la selezione gli statement richiesti. • E' possibile aggiungere sia prima che dopo la condizione ottenendo cicli misti (equivalente di goto) • La selezione deve essere impostata in logica booleana in modo da seguire i cammini predefiniti che non sono modificabili Procedure call in RAPTOR • In RAPTOR la procedure call può essere utilizzata per implementare macro procedure o funzioni • Esistono numerose procedure di sistema per il disegno, tra queste alcune sono delle procedure tipo (Open Graph Window (X Size, Y Size)) altre delle macro tipo (Close Graph Window), ad altre per l'interfaccia con l'utente • Alcune procedure sono definite bloccanti, in quanto includono delle istruzioni che attendono il verificarsi di un determinato evento • Esistono delle funzioni di interfaccia tipo (GetMouseX ) che ritornano dei valori al chiamante ( x ← GetMouseX ) RAPTOR: subcharts e procedure • In RAPTOR il termine procedure comprende sottoprogrammi che siano comparabili a procedure e funzioni dato che il passaggio di parametri è concesso così come il ritorno di valori • Per il momento abbiamo visto la chiamata a delle procedure di libreria presenti in RAPTOR • In seguito proveremo a definirne di nostre partendo con la definizione di subcharts • I subcharts sono dei diagrammi di flusso che vengono richiamati dal diagramma di flusso principale (chiamato main) per essere eseguiti • I subcharts equivalgono alle macro, non avendo parametri di ingresso e non ritornando nulla RAPTOR: subcharts(1) • Per creare un subchart occorre cliccare con il tasto destro del mouse sulla tab del flow chart main • Tra le voci appare Add subchart • Nel dialog box viene richiesto il nome del subcharts. Tale nome deve essere ovviamente univoco all'interno del programma • I subcharts condividono lo stesso scope delle variabili del flow chart main, ed è proprio per questo motivo che equivalgono a delle macro RAPTOR prevede diversi livelli di profilo per i programmatori che lo utilizzano. Se si passa al livello intermedio, nello stesso menu apparirà anche l'opzione ``Add procedure'' Subcharts: esempio RAPTOR: subcharts(2) • I subcharts sono molto utili per dividere il programma in sottoprogrammi che possono essere richiamati più volte • Come le macro hanno il vantaggio di evitare il duplicarsi di codice • Nell'esempio di prima il subcharts viene richiamato 4 volte separatamente • Lo stesso programma scritto senza subcharts sarebbe più difficile da leggere debuggare e sviluppare RAPTOR: procedure(1) • Nell'esempio del Draw Bulls Eye si può notare che ogni chiamata viene preceduta da degli assegnamenti per le variabili x e y che vengono poi usati dal subchart richiamato in seguito • Questo è tipico dell'uso dei subchart e delle macro in genere per generare comportamenti differenti a partire della stessa base di codice • L'equivalente si potrebbe ottenere passando x e y ad una procedura che esegue la macro Draw Bulls Eye • In Raptor è possibile definire delle procedure in maniera molto simile ai subcharts scegliendo l'opzione add procedure nel menu di tasto destro sul tab main • La procedura è molto più flessibile perchè è possibile chiamarla con differenti valori iniziali ad ogni chiamata senza dover settare le variabili prima della chiamata RAPTOR: procedure(2) • Questa flessibilità si ottiene attraverso il passaggio di parametri • I parametri sono delle variabili che prendono valore nel momento della chiamata (parametri di ingresso) oppure cambiano valore nel momento che la procedura termina (parametri di uscita) • Quando si definisce una procedura in Raptor si indicano se i parametri sono da considerarsi di ingresso o di uscita o sia di ingresso che di uscita Passaggio di parametri in RAPTOR • Parametri di ingresso (input): i valori vengono inizializzati nel momento della chiamata e i valori dei parametri attuali vengono passati ai formali (passaggio per valore) • Parametri di uscita (output): il loro valore viene ricopiato nella relativa variabile del programma chiamante (copy-out della call-by-value-result) • Parametri di ingresso e uscita (input/output): vengono trattati come parametri di input all'ingresso e output all'uscita della procedura (simile al passaggio per riferimento o al call-by-value-result) • I parametri vengono definiti nel momento in cui si crea una procedura ma possono anche essere modificati a posteriori con il click del tasto destro sulla tab della procedura Procedure: esempio RAPTOR: scope delle variabili • La procedure di RAPTOR esattamente come tutte le procedure in generale hanno un loro spazio di dichiarazione delle variabili che vivono fino a quando la procedura non termina • Le variabili usate in una procedure di RAPTOR non sono visibili al di fuori della procedura stessa • In RAPTOR le variabili definite (utilizzate) in una procedure o nel flow chart main hanno visibilità solo in quella procedura o nel main a meno che non vengano passate esplicitamente come parametri • Non esistono variabili globali Riassumento • Variabili: Esistono solo due tipi semplici, numero e stringa Le variabili non vengono dichiarate esplicitamente ma implicitamente quando vengono usate in un assegnamento Ogni variabile ha una sua visibilità che equivale al flow chart nella quale è usata (il subchart ha la visibilità del flow chart da cui è chiamato) • Sottoprogrammi: macro (subchart), procedure e funzioni (procedure) Le procedure possono ricevere parametri Hanno una ambito di definizione delle variabili disgiunto dal flow chart chiamante L'unico punto di contatto sono i parametri che vengono passati o ritornati I parametri possono essere definiti come di input di output o di input/output Lezione 8 - Ricorsività Corso — Programmazione Programmazione e progettazione — Strutture dati e sottoprogrammi Marco Anisetti e-mail: [email protected] web: http://homes.di.unimi.it/anisetti/ Università degli Studi di Milano — Dipartimento di informatica Ricorsività • Un oggetto ricorsivo è definito totalmente o parzialmente in base a se stesso • Il concetto di ricorsione nella programmazione è strettamente collegato a quello di induzione o ricorrenza in matematica. Molti concetti matematici sono definiti in modo induttivo o tramite ricorrenze. L'esempio canonico di un concetto matematico definito in modo induttivo è quello di numero naturale Base: 0 è un numero naturale; Passo: se n è un numero naturale, allora anche il successore di n, S(n) (cioè n + 1), è un numero naturale Ricorsività: esempi • Fattoriale: n! è definito come n ∗ (n − 1)! • M.C.D.(A,B): se A > B M.C.D.(B,A), se B = 0 A, se A > B M.C.D.(B,A MOD B) • Numero di Fibonacci di un numero naturale n è: F (n) se n = 0 0, se n = 1 1, se n > 1 F (n − 1) + F (n − 2) Ricorsività: divide et impera • La ricorsione sta alla base di una tecnica molto potente di programmazione, chiamata divide et impera • Essa consiste nello scomporre (divide) un problema in sotto problemi dello stesso tipo, più semplici da risolvere (impera). Questa tecnica è la chiave per la progettazione di molti algoritmi importanti, ed è un ingrediente fondamentale della programmazione dinamica • Virtualmente tutti i linguaggi di programmazione in uso al giorno d'oggi permettono la specificazione diretta di funzioni e procedure ricorsive, utilizzando lo stack per tener traccia delle loro chiamate annidate • Si può dimostrare che ogni sottoprogramma ricorsivo può essere trasformato in un sottoprogramma iterativo mediante l'utilizzo esplicito di uno stack Nucleo della ricorsività • Il potere della ricorsività sta nella possibilità di definire un insieme anche infinito di oggetti con un numero finito di comandi • Si usano algoritmi ricorsivi quando il problema da risolvere, la funzione da valutare o la struttura dati da usare sono intrinsecamente ricorsivi • Un linguaggio permette la ricorsione se permette ad un sottoprogramma di richiamare se stesso • Un sottoprogramma ricorsivo S può essere espresso come una combinazione C di istruzioni Ii non contenenti riferimenti a S e una chiamata ad S S = C[Ii,S] • Se il sottoprogramma S contiene un riferimento esplicito a se stesso allora si parla di ricorsività diretta • Se il sottoprogramma S contiene un riferimento ad una procedura Q che contiene il riferimento a S allora si parla di ricorsività indiretta Ricorsività: problemi(1) • Un sottoprogramma ricorsivo crea dei problemi legati alla terminazione e alla instanziazione • Il problema della terminazione è comune a tutti gli algoritmi, nel caso della ricorsione il caso terminale quando raggiunto fa concludere la procedura o funzione senza ulteriori chiamate ricorsive • Nel caso del fattoriale il caso terminale è 0! = 1 • La chiamata ricorsiva viene generalmente fatta precedere dalla condizione di terminazione B 1. B è vera finchè il caso terminale non è stato raggiunto allora: S if B then C[Ii,S] oppure S C[Ii, if B thenS] 2. B è falsa finchè il caso terminale non è stato raggiunto allora: S if B then nothing else C[Ii,S] oppure S = C[Ii, if B then nothing else S] Ricorsività: problemi(2) • Quando si chiama un sottoprogramma vengono allocate le aree di memoria per variabili costanti locali e parametri passati (record di attivazione) • Questa area di memoria viene deallocata nel momento del'uscita dal sottoprogramma • Nel caso della ricorsione dato che il sottoprogramma si richiama ricorsivamente prima di uscire allora ne esistono più istanze analoghe in memoria (solitamente stessi identificatori ma valori potenzialmente diversi) un record di attivazione per chiamata • Questo conflitto si risolve considerando come attiva solo l'ultima instanza allocata (scope) • Esiste anche un problema di spazio in memoria per garantire la sopravvivenza della ricorsione Record di attivazione: ricorsione(1) [Esempio in linguaggio C] void Prima(int a){ int b; Prima(a-1); } void Seconda(float c){ } int main(){ int num=100; Prima(10); Seconda(num); } Record di attivazione: ricorsione(2) [Nota] Cosa succede se un parametro è passato per indirizzo? Iterazione e ricorsione Calcolo del fattoriale con tecnica iterativa e ricorsiva [Fattoriale iterativo] int fattoriale (int n) { int i, f; f = 1; for (i = 1; i <= n; i++) { f = f * i; } return f; } [Ricorsiva] int fattoriale (int n) { if (n == 0) return 1; else return (n * fattoriale(n - 1)); } Ricorsione multipla • Si ha ricorsione multipla quando un’attivazione di funzione causa più di una attivazione ricorsiva della stessa funzione • n-esimo numero di Fibonacci [Fibonacci ricorsivo] int F(n){ if (n==0) return 0; if (n==1) return 1; if (n>1) return F(n-2) + F(n-1); } Ricorsione e intrattabilità • Un es. di problema intrattabile: la Torre di Hanoi Torre di Hanoi • Tre paletti e un certo numero di dischi di grandezza decrescente • Il gioco inizia con tutti i dischi incolonnati su un paletto in ordine decrescente, in modo da formare un cono. • Lo scopo del gioco e' portare tutti dischi sull'ultimo paletto, potendo spostare solo un disco alla volta e potendo mettere un disco solo su un altro disco piu' grande, mai su uno piu' piccolo. • il numero minimo di mosse necessarie per completare il gioco è 2n − 1, dove n è il numero di dischi. • Di conseguenza, secondo la leggenda, i monaci di Hanoi dovrebbero effettuare almeno 18.446.744.073.709.551.615 mosse prima che il mondo finisca (una mossa al secondo 585 miliardi di anni), essendo n =64 Torre di Hanoi e ricorsione(1) • Problema caratterizzato dalla sua intrattabilità • Soluzione naturale ricorsiva: consideriamo i paletti da sinistra verso destra come P1 , P2 e P3 e i dischi da 1 il più piccolo a n il più grande Sposta i primi n − 1 dischi da P1 a P2 usando P3 come appoggio Sposta il disco n da P1 a P3 Sposta n − 1 dischi da P2 a P3 Torre di Hanoi e ricorsione(2) • Considero il numero di dischi n il numero del palo sorgente s il numero del palo destinazione d e il palo di appoggio a • Partendo da un certo numero di dischi e scegliendo sorgente e destinazione possiamo scrivere la seguente funzione ricorsiva [Hanoi ricorsivo] void hanoi(n, s, d, a) { if (n == 1) muoviDisco(s, d); else { hanoi(n-1, s, a, d); muoviDisco(s, d); hanoi(n-1, a, d, s); } } Iterazione o ricorsione • Come facciamo ad accorgerci se una ricorsione è corretta o ben definita? • Se ad ogni volta che applico la ricorsione sono significativamente più vicino al caso base • Si dice che a definizione non è circolare ma converge • Solitamente le soluzioni ricorsive sono più eleganti, sono più vicine alla definizione del problema • Non è detto che siano più efficienti • L'utilizzo di algoritmi ricorsivi costituisce l'opzione più naturale ed intuitiva per operare su strutture definite in modo ricorsivo (esempio alberi) Lezione 9 - Programmazione strutturata con RAPTOR Corso — Programmazione Programmazione e progettazione — Strutture dati e sottoprogrammi Marco Anisetti e-mail: [email protected] web: http://homes.di.unimi.it/anisetti/