POLITECNICO DI MILANO ESAME DI INFORMATICA 3 Prof.ssa Sara Comai Anno Accademico 2003/2004 I Prova in itinere - Laurea On Line SOLUZIONI ESERCIZIO 2 (SEMANTICA) 1) Passaggio dei parametri per valore 3 5 5 7 9 5 7 10 3 7 17 3 - Si noti che y è una variabile globale e quindi tutte le modifiche apportate dalla funzione f sono visibili anche nel main. - Si noti che nella riga 17 la variabile x non è visibile (è fuori dal suo scope) e quindi non ha alcun valore. - La riga 7 invece non viene eseguita e quindi non vengono riportati valori. - Queste osservazioni valgono anche per i punti successivi. 2) Passaggio dei parametri per indirizzo 3 5 5 7 9 7 7 10 3 3 17 3
3) Passaggio dei parametri per risultato con valore di default 5 3 5-5 7 5 2 9 10 17 2 - se ad y nella riga 15 viene associato un valore si considera questo valore e non quello di default (che va assegnato nel caso in cui la variabile non sia definita) 4) Passaggio dei parametri per valore-risultato 3 5 5 7 9 5 7 10 3 7 17 7 ESERCIZIO 2 (LISP) (defun flatlist (L) (cond ((null L) nil) ((atom (car L)) (cons (car L) (flatlist (cdr L)))) (append (flatlist (car L)) (flatlist (cdr L))))) La funzione riceve in ingresso una lista: - se la lista è vuota restituisce la lista vuota - se il primo elemento della lista è un atomo allora si prende l'atomo e lo si inserisce con cons nella lista (appiattita) che contiene gli elementi successivi - se il primo elemento della lista è a sua volta una sotto-lista allora lo si appiattisce; si prende la parte rimanente della lista e si appiattisce anche questa. Infine si combinano queste due liste piatte inserendone i valori all'interno di una lista creata con append.
ESERCIZIO 3 (LISTE) La soluzione proposta utilizza l'ereditarietà delle classi e le scelte fatte rappresentano solo uno dei modi possibili per risolvere l'esercizio. Al posto dell'ereditarietà si potevano utilizzare anche delle union in analogia all'implementazione degli alberi con nodi diversi della lezione 15. Il programma non è completo per poter essere eseguito, ma vengono incluse solamente la parti richieste dal testo dell'esercizio: l'organizzazione dei dati in base alla specifica, un insieme di possibili operazioni (estendibili), e l'implementazione della funzione flatlist. #include <iostream.h> enum Atom_id { CHARACTER, INTEGER, GENERIC_NODE enum Boolean { FALSE, TRUE // Definizione di una classe astratta che descrive un nodo generico della lista LISP class LispGenNode { virtual int who_is(void) = 0; // tipo di nodo virtual Boolean isatom(void) = 0; // atomo o sotto-lista // Definizione di una classe concreta che descrive un atomo che contiene un carattere class LispChar : public LispGenNode { virtual int who_is(void) { return CHARACTER; virtual Boolean isatom(void) { return TRUE; char getvalue(void) {return _c; // restituisce il valore dell'atomo char _c; // contiene solamente un carattere // Definizione di una classe concreta che descrive un atomo che contiene un intero class LispInt : public LispGenNode { virtual int who_is(void) { return INTEGER; virtual Boolean isatom(void) { return TRUE; int getvalue(void) { return _i; // restituisce il valore dell'intero int _i; // contiene solamente un intero // Definizione di una classe concreta che descrive un nodo non atomo (sotto-lista) class LispNode : public LispGenNode { virtual int who_is(void) { return GENERIC_NODE; virtual Boolean isatom(void) { return FALSE; LispGenNode* _car; // puntatore a primo elemento della lista LispNode* _cdr; // puntatore ad elementi successivi della lista
// Definizione della classe astratta ListLisp // descrive le operazioni che si possono applicare ad un oggetto di tipo "lista Lisp" class ListLisp { virtual LispGenNode* car(void) =0; // può restituire un atomo // oppure una sotto-lista virtual ListLisp* cdr(void) =0; // restituisce la lista senza il primo nodo virtual ListLisp* cons(lispgennode* atom) = 0; // inserisce un atomo in una lista virtual ListLisp* append(listlisp* l) = 0; // inserisce una lista-lisp in una lista virtual ListLisp* append(lispnode* l) = 0; // inserisce una nodo di tipo Lisp in una lista virtual Boolean nil(void) = 0; // verifica se la lista è vuota o meno virtual ListLisp* flatlist(void) = 0; // appiattisce la lista // altre funzioni... // Definizione della classe concreta che gestisce l'intera lista class List : public ListLisp { virtual LispGenNode* car(void); virtual ListLisp* cdr(void); virtual ListLisp* cons(lispgennode* atom); virtual ListLisp* append(listlisp* l); virtual ListLisp* append(lispnode* n); virtual Boolean nil(void) { if (_head == NULL) return TRUE; else return FALSE; List(void) { _head = NULL; ListLisp* flatlist(void); LispNode* _head; // contiene il puntatore al primo elemento della lista che è sempre // di tipo LispNode (altrimenti sarebbe un atomo e non una lista Lisp) ListLisp* flatlisthelper(listlisp* l); // helper // Implementazione della funzione flatlist // Si noti che siccome la funzione richiede di applicare una ricorsione all'oggetto stesso // per cui viene definita, occorre utilizzare una funzione di tipo "helper" che permetta // di esprimere la ricorsione ListLisp* List::flatList (void){ return flatlisthelper(this); } ListLisp* List::flatListHelper(ListLisp* l) { ListLisp* l1; ListLisp* l2; if (l->nil()) { return l; } else { if (l->car()->isatom()) { l1 = flatlist(l->cdr()); // se la lista è vuota si restituisce la lista stessa // se il primo elemento della lista è un atomo // si applica ricorsivamente flatlist alla parte rimanente // della lista return l1->cons(l->car()); // si inserisce l'atomo in testa alla parte rimanente // della lista } else { // se il primo elemento della lista è una sotto-lista l1 = new List(); // si crea una lista vuota l1 = flatlist(l1->append((lispnode* )l->car())); // si applica ricorsivamente
} l2 = flatlist(l->cdr()); return l1->append(l2); } // flatlist alla sotto-lista che rappresenta il primo // elemento della lista - questa sottolista viene ottenuta // inserendo il primo nodo della lista che viene estratto // con la funzione car nella lista vuota l1 // Si noti che flatlist richiede in ingresso // una lista di tipo ListList, mentre la funzione car // restituisce un nodo e non una lista) // si applica ricorsivamente flatlist alla parte // rimanente della lista // si combinano le due parti Era possibile definire flatlist come funzione esterna (non membro della classe ListLisp) La flatlist poteva essere realizzata navigando direttamente la lista anche se normalmente è più semplice definire dei comandi per la manipolazione delle informazioni della struttura dati e poi utilizzare direttamente questi comandi Anzichè l'ereditarietà era possibile utilizzare il costrutto union per distinguere i diversi tipi di nodo L'utilizzo dei template C++ è adatto quando si vogliono realizzare algoritmi o strutture dati che funzionano per un qualsiasi tipo di dato. In questo esercizio i tipi di dati erano ben definiti dalla specifica Non è corretto utilizzare tipi char per rappresentare interi perchè il tipo char come intero è solo un sottoinsieme di int (char normalmente è rappresentato con 8bit mentre int generalmente da 32bit) La lista LISP non è un albero ma una lista di liste. I puntatori cdr e car possono puntare a tipi differenti: cdr punta sempre ad un nodo contenente cdr e car mentre car può puntare ad un atomo oppure ad un nodo. ESERCIZIO 4 (COMPLESSITA ) La complessità di questo frammento di codice si ottiene moltiplicando la complessità del ciclo for esterno per la complessità del blocco interno. La complessità del ciclo esterno è n volte la complessità delle sue istruzioni istruzioni interne. Il blocco interno contiene due cicli in cascata, quindi bisogna stabilire quale dei due cicli richiede maggiore tempo di esecuzione. Consideriamo il primo ciclo, la variabile di conteggio assume i valori 2, 4, 8, 16, 32 ovvero 2, 2 2, 2 3, 2 4, 2 5. La complessità di questo ciclo è Θ(log 2 (n)). Consideriamo il secondo ciclo, la variabile di conteggio assume i valori 2, 8, 512, che si possono vedere come 2, 2 2 2, 8 8 8, 512 512 512. ovvero come 2, 2^(3), 2^(3 3), 2^(3 3 3),, cioè 2, 2^(3^1), 2^(3^2), 2^(3^3), La complessità di questo ciclo è Θ(log 3 (log 2 (n)))
Anche a livello intuitivo si può vedere che la variabile di conteggio del primo ciclo cresce meno velocemente della variabile di conteggio del secondo e quindi il primo ciclo viene eseguito un numero di volte superiore al secondo. Essendo dominante la complessità del primo ciclo la complessità dell intero frammento di codice è pari a n volte la complessità di questo ciclo: Θ(n log 2 (n)). - il secondo ciclo interno non ha complessità pari a Θ(n 3 ) oppure a Θ(radice cubica di n). - Si noti che una complessità pari a Θ(n 3 ) significa che all'aumentare di n il tempo di esecuzione aumenta di un fattore pari al cubo di n e richiede quindi veramente molto tempo, cosa che invece non si verifica per il secondo ciclo interno che viene eseguito poche volte dato che k cresce molto rapidamente. - non è corretto affermare che la complessità del ciclo esterno è Θ(n) dato che viene eseguito n volte. La sua complessità è n volte la complessità delle sue istruzioni interne. - non è corretto affermare che la complessità sia pari a (n log 2 (n)), senza Θ