






















Studia grazie alle numerose risorse presenti su Docsity
Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium
Prepara i tuoi esami
Studia grazie alle numerose risorse presenti su Docsity
Prepara i tuoi esami con i documenti condivisi da studenti come te su Docsity
Trova i documenti specifici per gli esami della tua università
Preparati con lezioni e prove svolte basate sui programmi universitari!
Rispondi a reali domande d’esame e scopri la tua preparazione
Riassumi i tuoi documenti, fagli domande, convertili in quiz e mappe concettuali
Studia con prove svolte, tesine e consigli utili
Togliti ogni dubbio leggendo le risposte alle domande fatte da altri studenti come te
Esplora i documenti più scaricati per gli argomenti di studio più popolari
Ottieni i punti per scaricare
Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium
Array, operazioni su di essi e algoritmi di ordinamento (selection sort, insertion sort, bubble sort, mergesort, quicksort). Testing dei programmi e uso dei file. Abstract Data Types e Pseudo-Generics. Ricorsione. Complessità computazionale. ADT: - Punto, - Lista, - Stack (implementazioni con Array e con liste), - Queue (implementazioni con Array e con liste), - Albero binario - Albero binario di ricerca, Alberi AVL, Heap - Tabelle Hash
Tipologia: Dispense
1 / 30
Questa pagina non è visibile nell’anteprima
Non perderti parti importanti!























Fasi dello sviluppo:
Elencare gli elementi di un insieme secondo una sequenza stabilita da una relazione d'ordine. Possiamo classificare gli algoritmi di ordinamento in base ad alcune proprietà: Un algoritmo è Stabile se due elementi con la medesima chiave mantengono lo stesso ordine con cui si presentavano prima dell'ordinamento. Un algoritmo ordina in loco se in ogni dato istante al più è allocato un numero costante di variabili, oltre all'array da ordinare(es. se uso array aggiuntivo per salvare gli elementi da ordinare non è in loco) Un algoritmo è Adattivo se il numero di operazioni eƯettuate dipende dall’input. Infine, un algoritmo può essere Interno, se i dati sono contenuti nella memoria RAM, o Esterno se i dati sono residenti su disco o su nastro. Tutti gli algoritmi che vedremo ordinano per confronti. Algoritmi semplici: Numero di operazioni quadratico rispetto alla taglia dell’input: O(n^2 )
Analisi:
Progettazione, definiamo funzioni corrispondenti agli step individuati e, per ognuna, seguiamo gli step di specifica, progettazione, codifica e verifica: input_array(a, n) Input sequenza s in un array a di dimensione n ordina_array(a, n) Ordina array a di dimensione n (per motivi di eƯicienza l’array di input e di
. output sono lo stesso array) output_array(a, n) Output sequenza s1 contenuta in array a di dimensione n Progettazione (funzione ordina_array): Il Selection Sort è un algoritmo di ordinamento di una sequenza di interi: eƯettua una visita totale delle posizioni dell’array, per ogni posizione visitata individua l’elemento che dovrebbe occupare quella posizione, in questo modo, se i è la posizione corrente (0<=i<n), tutti gli elementi nelle posizioni comprese tra 0 ed i-1 rispettano l’ordinamento. L’elemento che deve occupare la posizione i sarà il minimo tra quelli nelle posizioni comprese tra i ed n-1. RaƯinamento del programma principale: • minimo(a, n) • scambia(i, j). Analisi della funzione minimo:
Visita totale: ad ogni passo gli elementi che precedono l’elemento corrente sono tra di loro ordinati, pur non occupando ancora la loro posizione definitiva. Si inserisce l’elemento corrente nella posizione che garantisce il mantenimento dell’ordinamento, dunque gli elementi precedenti maggiori sono spostati in avanti. Il primo elemento è già un sotto-array ordinato. Progettazione: for(i = 1; i < n; i++), inserisci l’elemento in posizione i nell’array ordinato a[0..i-1] RaƯinamento del programma principale: definiamo una sotto-funzione che inserisca un elemento in un array già ordinato.
Algoritmo iterativo: Si eƯettuano n-1 visite dell’array. Alla visita i-esima, si confrontano elementi adiacenti dal primo al (n-i)-esimo elemento (visito sempre in elemento in meno rispetto alla visita precedente). Elementi adiacenti che non risultano ordinati vengono scambiati. Ad ogni passo l’elemento più grande viene portato nella sua posizione finale… Dopo il passo i-esimo, gli elementi tra le posizioni n-i ed n-1 risultano ordinati e nelle loro posizioni finali.
Analisi: Progettazione: Si scorre l’array di input finché non si trova l’elemento o non si raggiunge la fine dell’array: se l’elemento è stato trovato allora si restituisce la posizione corrente, altrimenti si restituisce -1. Si utilizzano un indice per scorrere l’array (int i=0;) e una variabile booleana ci dice se abbiamo trovato l’elemento (preferibile rispetto all’uso dell’istruzione break). La condizione di permanenza nel ciclo sarà: (i<n && !trovato). Short circuit evaluation Invece dello schema precedente, i programmatori C spesso usano il seguente: while( i<n && a[i]!=elem) i++; Infatti quando i=n la condizione a sinistra dell’operatore && è falsa e la condizione a destra dell’operatore non viene valutata (se venisse valutata a[i] sarebbe indefinito). Nota che in molti linguaggi di programmazione non c’è la short circuit evaluation.
Se l’array è ordinato in senso crescente, non è necessario arrivare alla fine dell’array per stabilire che l’elemento non è stato trovato, ci si può fermare appena si trova un elemento maggiore (o uguale) di quello dato.
Consiste nel dividere l’array in due metà e confrontare l’elemento da cercare con l’elemento centrale.
La fase di testing consiste nell’esercitare il programma con dati di test per verificare che il suo comportamento sia conforme a quello atteso. Oracolo: output atteso, quello che ci si aspetta il programma produca Debugging: Individuazione e correzione del difetto che ha causato il malfunzionamento Test suite: un insieme di casi di test per un programma, individuati per classi (array vuoto, ordinato…). Più alta è la fase in cui si introduce il difetto, maggiore è la diƯicoltà nel rimuoverlo.
Per automatizzare il testing si possono usare i file per leggere dati di input e scrivere i dati di output. Usiamo 2 file per dati di input: un file “input.txt”, contenente gli elementi di input, e un file “oracle.txt” contenente l’output atteso. Un file “output.txt” risultante dall’esecuzione del programma, che contiene l’output eƯettivo e una indicazione dell’esito del test (PASS 1 / FAIL 0). In C, il termine stream indica una sorgente di input o una destinazione per l’output. Molti programmi piccoli ottengono il loro input da uno stream (ad es. la tastiera) e lo inviano ad un altro stream (ad esempio il video). Gli stream rappresentano file memorizzati da qualche parte (hard disk o altri tipi di memoria a lungo termine) e sono associati a periferiche (schede di rete, stampanti, etc).
FILE *fopen(const char *filename, const char *mode) int fclose(FILE *stream) La funzione fopen() prende come parametri due tringhe: filename è il nome del file da aprire, può contenere informazioni riguardanti la sua posizione, e mode è una stringa di modalità che specifica quali operazioni abbiamo intenzione di compiere sul file. Stringhe mode per file di testo: "r" Apre il file in lettura (il file deve esistere) "w" Apre il file in scrittura (non è necessario che il file esista) "a" Apre il file in accodamento (non è necessario che il file esista) "r+" Apre il file in lettura e scrittura (il file deve esistere) "w+" Apre il file in lettura e scrittura - tronca il file se esiste "a+" Apre il file in lettura e scrittura - accoda se il file esiste Il valore di ritorno è un puntatore a file, che verrà dato come parametro a tutte le funzioni che opereranno su quel file. fclose() prende come unico parametro il file da chiudere e ritorna 0 in caso di successo, o un valore costante EOF in caso di fallimento. Il carattere EOF cambia in base al sistema operativo.
char *fgets(char *s, int size, FILE *stream); int fscanf(FILE *stream, const char *format, ...); int fprintf(FILE *stream, const char *format, ...) Tutte e tre le funzioni precedenti prendono come parametro un puntatore a file. fgets() prende come parametro il file (lo stream) da cui leggere, la stringa s in cui scrivere quello che viene letto dallo stream fino al carattere newline e un intero size che indica che la funzione può leggere fino a size-1 caratteri. Ritorna la stringa s in caso di successo, NULL in caso di errore o se si raggiunge la fine del file senza aver letto alcun carattere. fscanf() prende come parametro il file da leggere, legge da stream fino ad un carattere di spazio e non lo memorizza. Restituisce il numero di dati letti e scritti con successo. fprintf() scrive su stream. Restituisce il numero di caratteri scritti; -1 in caso di errore.
int sprintf(char *restrict buƯer, const char *restrict format, ... ); int sscanf(const char *str, const char *format, ...) Le funzioni di sopra possono leggere e scrivere dati usando una stringa come se fosse un flusso. sprintf() scrive caratteri in una stringa. Prende come primo argomento il buƯer (stringa) in cui andare a scrivere. Restituisce il numero di caratteri memorizzati
programmatore, seguendo il principio dell’Incapsulamento (information hiding).
Strutture: tipo di dati composito che include un elenco di variabili fisicamente raggruppate in un unico blocco di memoria. Vantaggio: migliore leggibilità dei programmi. Per rendere più leggibile il codice, puoi definire una struct e creare un alias per essa utilizzando typedef: typedef struct point Point; Oppure usare in combinazione il typedef e la definizione della struttura: Puntatori È possibile allocare memoria per una struttura attraverso le funzioni malloc o calloc: Point *p = malloc(sizeof(Point)); In questo modo allochiamo spazio per una struttura di tipo Point e assegniamo un puntatore alla variabile p. In questo caso, per accedere ai campi della struttura usiamo l’operatore freccia -> : Implementazione – nel file punto.h
L’implementazione della struttura del tipo punto è nell’header file, visibile quindi al modulo client, che potrebbe accedere direttamente ai campi della struct senza usare gli operatori dell’ADT. Per “nascondere” la rappresentazione della struttura del tipo di dato, nell’header file punto.h dichiariamo: typedef struct punto *Punto; Non posiamo evitarlo perché all’atto della compilazione del modulo client, il compilatore non saprebbe quanta memoria allocare per una dichiarazione del tipo: Punto p; Invece, essendo il tipo punto un puntatore, il compilatore sa quanta memoria deve allocare per una variabile di quel tipo, indipendentemente dalla dimensione dell’elemento puntato. Il tipo Punto viene poi implementato come un puntatore alla struttura nel file punto.c in modo da non renderla visibile al client tramite l’include dell’header file punto.h :
I generics sono uno strumento che permette la definizione di un tipo parametrizzato, che viene esplicitato successivamente in fase di compilazione (o linkaggio). Obiettivo: Implementare algoritmi e ADT che siano in grado di funzionare, di volta in volta, con il tipo di dati desiderato. Esempio: problema dell’ordinamento
Istanziare un nodo: man mano che costruiamo la lista, creiamo dei nuovi nodi da aggiungere alla lista. I passi per creare un nodo sono:
Il modo più semplice per inserire un nuovo elemento in una lista L è inserire l’elemento in un nuovo nodo da aggiungere in testa alla lista.
Il modo più semplice per eliminare un elemento in una lista L è eliminarlo in testa alla lista.
Dati una lista l e un elemento val, restituisce la posizione della lista in cui appare la prima occorrenza dell’elemento, o -1 se l’elemento non è presente. Richiede una visita finalizzata della lista. Possiamo ottenere sia l’Item ricercato (restituendolo), sia la sua posizione (salvandola nella variabile pos, passata come puntatore), implementando la funzione con il seguente prototipo: Item searchItem(List list, Item item, int *pos).
Per eliminare un elemento in una lista concatenata L:
Per inserire un elemento in una data posizione della lista concatenata L:
Per invertire una lista concatenata L:
Diverse scelte progettuali possibili:
La coda è una struttura dati lineare a dimensione variabile. Si può accedere direttamente solo alla head della lista. Non è possibile accedere ad un elemento diverso da head, se non dopo aver eliminato tutti gli elementi che lo precedono (cioè quelli inseriti prima). Tra le possibili implementazioni, le più usate sono realizzate tramite: Lista concatenata e Array.
È possibile utilizzare gli operatori di: Rimozione dalla testa (avviene in tempo costante), Aggiunta in coda (richiede un tempo lineare, costante se aggiungi un puntatore all’ultimo elemento della lista). Per motivi di eƯicienza, conviene avere accesso sia al primo elemento sia all’ultimo, modificando il tipo lista come un puntatore ad una struct che contiene:
La coda è implementata come un puntatore ad una struct queue che contiene due elementi:
Adesso tail<head. In questo ordine circolare il successore di p è (p+1)%N: ogni volta che si inserisce un elemento tail avanza: tail=(tail+1)%N. Ogni volta che si rimuove un elemento head avanza: head=(head+1)%N. La coda è piena (e non si può eseguire enqueue) se il successore di tail in questo ordine circolare è head: (tail+1)%n==head. Quando la coda è vuota, i valori di head e tail coincidono: tail==head. Abbiamo visto due implementazioni diverse dell’ADT coda:
Ogni volta che viene invocata una funzione viene creata dinamicamente una struttura dati detta record di attivazione: si crea di una nuova istanza della funzione chiamata, viene allocata la memoria per i parametri e per le variabili locali, si eƯettua il passaggio dei parametri, si trasferisce il controllo alla funzione chiamata e si esegue il codice della funzione. È il “mondo della funzione” e contiene:
i calcoli sono fatti prima. La chiamata serve solo, dopo averli fatti, per proseguire la computazione. Questa forma di ricorsione si chiama «Ricorsione Tail» (ricorsione in coda).
Alcune strutture dati sono inerentemente ricorsive, come le Sequenze o le Strutture ad albero… La formulazione ricorsiva di algoritmi su di esse risulta più naturale. Es. Lista linkata, Def: Una lista è… una lista vuota, oppure… un elemento seguito da una lista.
Analisi della complessità: stima del costo degli algoritmi in termini di risorse di calcolo: tempo di esecuzione, spazio di memoria. Variabili che influenzano il tempo di esecuzione: La macchina usata, La dimensione dei dati, La configurazione dei dati (es. array già ordinato). Vogliamo ottenere un modello astratto per la valutazione del tempo, che:
Concentrandoci sul comportamento al crescere della dimensione n dei dai all’infinito (trascurando costanti moltiplicative e termini di ordine inferiore, ad esempio, se il tempo richiesto da un algoritmo su tutti gli input di dimensione n è al massimo 5 n^3 + 3n, la complessità temporale asintotica è O(n^3 )), le classi più frequenti sono:
Considerate f e g funzioni dai naturali ai reali positivi diremo che:
Studiare la complessità di un problema è diverso dallo studiare la complessità di un algoritmo. Un problema può essere risolto da svariati algoritmi. Per poter dire che un problema ha complessità O(g(n)) (ipotizziamo di parlare del caso peggiore) basta trovare un qualsiasi algoritmo che lo risolva con O(g(n)). Per poter aƯermare che un problema è Ω(g(n)) occorre invece dimostrare matematicamente che tutti i possibili algoritmi (inventati o non) lo risolvano alla meglio come Ω(g(n)).