PROGRAMMAZIONE AVANZATA JAVA E C Massimiliano Redolfi Lezione 7: Code, Stack, Liste Ricerca 2
Ricerca Se dobbiamo cercare un elemento in un array possiamo trovarci in due situazioni Elementi non ordinati Elementi ordinati Ricerca sequenziale Ricerca binaria 3 Ricerca sequenziale La ricerca sequenziale è molto semplice: si passano tutti gli elementi dell array, dal primo all ultimo, alla ricerca dell elemento chiave: int search_seq(const char *items, int count, char key) for(int i=; i<count; i++) if(items[i] == key) return i; // corrispondenza trovata return -1; // nessuna corrispondenza trovata La ricerca restituisce l indice dell elemento trovato oppure, se nessun elemento corrisponde alla chiave -1. Evidentemente una ricerca richiederà, in media, count/2 confronti. 4
Ricerca sequenziale E possibile migliorare l efficienza dell algoritmo scorrendo l array tramite puntatori e non tramite il sistema di indicizzazione: int search_seq(const char *items, int count, char key) char *p = items; for(int i=; i<count; i++) if(*p++ == key) return i; // corrispondenza trovata return -1; // nessuna corrispondenza trovata 5 Ricerca binaria Se i dati non sono ordinati non c è altra possibilità di ricerca, bisogna scorrere i dati uno alla volta e se i dati sono molti è chiaro che i tempi si allungano (con progressione lineare) Ma se i dati sono ordinati c è un alternativa molto efficiente al posto della ricerca sequenziale, la ricerca binaria! 6
Ricerca binaria Supponiamo di voler cercare il numero 4 all interno di un array ordinato: 1 2 3 4 5 6 7 8 9 L idea è semplice: confrontiamo il valore centrale dell array rispetto alla chiave se è maggiore allora ripeteremo la ricerca nella prima metà altrimenti nella seconda. A questo punto basta ripetere il procedimento ricorsivamente sino a quando non si trova l elemento cercato oppure non ci sono più elementi da cercare. 7 Ricerca binaria Cerco il numero 4: 1 2 3 4 5 6 7 8 9 Poiché 5 è maggiore di 4 cerco nella prima metà: 1 2 3 4 Poiché 2 è minore di 4 cerco nella seconda metà: 3 4 4 Trovato! Ogni volta divido per 2 lo spazio su cui agisco à Il numero di confronti è al più pari a log 2 n 8
int search_bin(char *items, int count, char key) int low=, high=count-1, mid; while(low <= high) mid = (low + high) / 2; if(key < items[mid]) high = mid-1; else if (key > items[mid]) low = mid+1; else return mid; /* Trovato!! */ return -1; /* nessuna corrispondenza */ Ricordarsi che funziona SOLO SU ARRAY ORDINATI! Supposto key = 4 low mid Ricerca binaria high 1 2 3 4 5 6 7 8 9 low mid high 1 2 3 4 5 6 7 8 9 mid lowhigh 1 2 3 4 5 6 7 8 9 low mid high 1 2 3 4 5 6 7 8 9 9 Code, Stack, Liste ed Alberi 1
Rappresentazione astratta dei dati Un programma è composto da due parti fondamentali: gli algoritmi i dati Abbiamo visto che i dati possono essere strutturati in vario modo dai tipi semplici (int, char, ) ad insiemi omogenei (gli array) a tipi di dati complessi come le strutture. 11 Rappresentazione astratta dei dati I dati possono essere rappresentati da due punti di vista distinti: - a livello macchina: in cui ci si concentra sulla rappresentazione fisica del dato (numero di bit, MIPS, ) - a livello astratto: in cui ci si concentra sugli aspetti funzionali legati al dato, tralasciando i dettagli fisici (si ha già un esempio di questo nel passaggio da int a float ) Quando si progetta un sistema, un software è buona cosa immaginare i dati come oggetti astratti, senza preoccuparsi particolarmente dei dettagli fisici che subentreranno solo in un secondo momento, quando si dovranno implementare effettivamente le funzionalità del sistema. 12
astrazione? strutture float int Rappresentazione astratta dei dati Sino a questo momento ci siamo occupati di dati via via sempre più astratti ma sempre legati ad una caratterizzazione fisica del dato (anche le strutture non sono altro che raggruppamenti di tipi ben associabili ad elementi fisici). Il livello successivo trascende questi aspetti astraendo ulteriormente il concetto di dato e comprendendo in questo le funzionalità stesse che permettono di accedere ad un elemento, ovvero le routine di inserimento, estrazione, ricerca del dato stesso. 13 astrazione motori di elaborazione strutture float int Rappresentazione astratta dei dati In questo senso il livello di astrazione che affrontiamo oggi include la logica stessa di accesso al dato in modo indipendente dal tipo trattato. Analizzeremo alcuni tra i principali di motori per l elaborazione dei dati: 1. le code 2. gli stack 3. le liste concatenate 14
Code 15 Code Una coda è un elenco lineare di in cui gli accessi avvengono secondo un ordinamento di tipo FIFO (first-in, firstout). Questo significa che il primo oggetto inserito è anche il primo che verrà estratto. dato inserito E D C B A dato estratto E la tipica situazione della coda in posta od al supermercato chi prima arriva prima viene servito 16
Le operazioni ammesse su una coda sono due: c_ins, c_ret per aggiungere e recuperare un oggetto rispettivamente. Notiamo che non ci sono funzioni ad accesso diretto ad una coda. Code Azione c_ins(a) c_ins(b) c_ret(), ottiene A c_ins(c) c_ins(d) c_ret(), ottiene B c_ret(), ottiene C Contenuto della coda A AB B BC BCD CD D 17 Code Si noti che, come per gli altri motori di elaborazione dei dati, poco ci importa di che cosa siano i dati trattati, quello che ci interessa è come i dati vengono gestiti. Utilità delle code: - buffer - elenchi di task - ritardi - Com è implementabile una coda? 18
Code insert retrive Possiamo vederla come un array di una certa lunghezza (il numero massimo di elementi gestibili dalla coda) e due puntatori o indici: uno punta alla posizione di inserimento, l altra a quella di estrazione 19 Code insert stato iniziale retrive insert c_ins(a) A retrive insert c_ins(b) A B retrive 2
Code insert c_ret() B retrive insert c_ins(c) B retrive C insert c_ins(d) B C D retrive 21 Code insert c_ret() C D retrive insert c_ret() D retrive insert c_ret() retrive 22
Code: un semplice esempio #include <stdio.h> #define MAX_EL 255 char coda[max_el]; int ipos =, rpos = ; void c_ins(char ch) if(ipos >= MAX_EL) printf( Coda piena!\n ); return; coda[ipos] = ch; ipos++; Utilizziamo nell esempio come buffer un array di char globale chiamato coda che contiene al più MAX_EL elementi. ipos e rpos sono rispettivamente gli indici in cui inserire ed estrarre i dati 23 Code: un semplice esempio ch c_ret(void) if(rpos >= ipos) printf( Coda vuota!\n ); return \ ; char ch = coda[rpos]; rpos++; return ch; int main(void) c_ins( A ); c_ins( B ); printf( \nret: %c, c_ret()); c_ins( C ); c_ins( D ); printf( \nret: %c, c_ret()); printf( \nret: %c, c_ret()); printf( \nret: %c, c_ret()); 24
Che fare quando si raggiunge la fine della coda (del buffer)? Code circolari O ci si ferma oppure si ricomincia dall inizio (memorizzando i dati nelle celle che nel frattempo si sono liberate). Quando scrittura e lettura proseguono in questo modo, dalla fine della coda all inizio si parla di code circolari. Come si modifica il sistema precedente per gestire le code circolari? (si devono modificare solo le funzioni c_ins e c_ret) 25 Code circolari: un semplice esempio void c_ins(char ch) // la coda è piena quando un inserimento cancellerebbe un // elemento non ancora letto, cioè quando ipos è uguale a rpos-1 // oppure ipos è alla fine dell array e rpos all inizio if( (ipos+1 == rpos) (ipos+1 == MAX_EL && rpos == ) ) print( Coda piena!\n ); return; coda[ipos++] = ch; if(ipos >= MAX_EL) ipos = ; // riprendi dall inizio 26
char c_ret() // la coda è vuota se i due indici coincidono if(rpos == ipos) printf( Coda vuota\n ); return \ ; Code circolari: un semplice esempio char ch = coda[rpos++]; if(rpos >= MAX_EL) rpos = ; // riprendi dall inizio return ch; 27 Stack 28
Stack Uno stack ha un funzionamento opposto a quello della coda in quanto gli gli accessi avvengono secondo un meccanismo di tipo LIFO (last-in, first-out). Questo significa che il primo oggetto inserito è l ultimo che verrà estratto. Si può immaginare una pila (stack) di piatti, i piatti vengono aggiunti e tolti dalla sommità della pila quindi l ultimo piatto appoggiato sulla pila sarà anche il primo ad essere estratto. 29 Le operazioni ammesse su uno stack sono due: c_ins, c_ret per aggiungere e recuperare un oggetto rispettivamente. Notiamo che non ci sono funzioni ad accesso diretto ad uno stack. Azione Contenuto dello stack inserisci Stack estrai c_ins(a) c_ins(b) c_ret(), ottiene B c_ins(c) c_ins(d) c_ret(), ottiene D c_ret(), ottiene C A BA A CA DCA CA A 3
Stack Per convenzione le funzioni di inserimento ed estrazione dei dati da uno stack sono dette push e pop rispettivamente. Un esempio classico di gestione della memoria di tipo LIFO è rappresentato dallo stack di sistema in cui vengono memorizzate le variabili locali Com è implementabile uno stack? 31 Stack: un semplice esempio #include <stdio.h> #define MAX_EL 255 int stack[max_el]; int tos = ; void push(int i) if(tos >= MAX_EL) printf( Stack pieno!\n ); return; stack[tos++] = i; Utilizziamo nell esempio come buffer un array di int globale chiamato stack che contiene al più MAX_EL elementi. Ci basta un solo indice che indica la cima dello stack (tos) 32
Stack: un semplice esempio int pop(void) if(--tos < ) printf( Stack vuoto!\n ); return tos = ; return stack[tos]; int main(void) push( A ); push( B ); printf( \nret: %c, pop()); push( C ); push( D ); printf( \nret: %c, pop()); printf( \nret: %c, pop()); printf( \nret: %c, pop()); 33 Liste concatenate 34
Liste concatenate Code e stack richiedono che la lettura del dato implichi l eliminazione dello stesso dalla memoria della coda, o dello stack. I dati inoltre sono memorizzati in celle di memoria contigue secondo una dimensione prefissata. Le liste danno invece la possibilità all utente di accedere ad un dato in esse contenute senza rimuoverlo automaticamente. Inoltre mantengono le in modo diverso senza la necessità di preallocare un buffer. Una lista concatenata può essere letta in modo più flessibile in quanto ogni record di informazione contiene un collegamento, un puntatore, al record successivo come in una sorta di catena. 35 Esistono due tipi di liste concatenate: Liste concatenate - liste concatenate semplici: ogni oggetto contiene un puntatore all oggetto successivo - liste concatenate doppie: ogni oggetto contiene due puntatori, uno all oggetto successivo l altro a quello precedente 36
Liste a concatenamento semplice: Liste a concatenamento semplice puntatore puntatore Notiamo che il dato proprio della lista (informazione) è solo una parte dell oggetto che compone l elemento della lista che deve prevedere anche il puntatore all elemento successivo. In genere l oggetto gestito dalla lista può essere visto come una struttura composta da un elemento il cui tipo dipende dall informazione gestita più un puntatore. 37 Liste a concatenamento semplice Supponiamo di realizzare una lista per una rubrica. L informazione è costituita da una struttura address tipo: struct address char nome[5]; char via[1]; char telefono[15]; ; L oggetto complessivo trattato dalla lista sarà: puntatore struct list_item struct address info; // è l informazione vera e propria struct list_item *next; ; 38
Inserire un oggetto alla fine della lista: Prima Liste a concatenamento semplice info info info NUOVO Dopo info info info NUOVO 39 Liste a concatenamento semplice Chiamiamo la funzione list_add, questa dovrà avere in input l elemento da aggiungere più un puntatore all ultimo elemento della lista (in modo da agganciare i due oggetti). struct list_item struct address info; struct list_item *next; ; void list_add(struct list_item *new_item, struct list_item **last_item) if(!*last_item) *last_item = i; // è il primo elemento della lista else (*last_item)->next = i; i->next = NULL; *last_item = i; Si noti che viene passato un puntatore all ultimo elemento della lista in modo da poter modificare il valore dello stesso. 4
Liste a concatenamento semplice Risulta particolarmente semplice visualizzare il contenuto di una lista, è sufficiente scorrerla dal primo all ultimo elemento: void list_show(struct list_item *first_item) struct list_item *p = first_item; int n = ; while(p) printf( \n[%d]: %s, ++n, p->info.nome); p = p->next; 41 Liste a concatenamento semplice: Liste a concatenamento semplice puntatore puntatore Oltre alle operazioni di inserimento e visualizzazione possiamo avere però altre situazioni: - inserire oggetti in una posizione iniziale o mediana - eliminare oggetti all inizio, alla fine o in posizione mediana Vediamo come si svolge concettualmente la cosa (per un esempio di codice si veda l esercitazione libretto_3) 42
Inserire un oggetto all inizio: Prima Liste a concatenamento semplice NUOVO puntatore puntatore Dopo NUOVO info info info 43 Liste a concatenamento semplice Inserire un oggetto in una posizione qualsiasi: Prima NUOVO info info info Dopo info NUOVO info info 44
Eliminare un oggetto all inizio: Prima Liste a concatenamento semplice info info info Dopo cancellato info info 45 Eliminare un oggetto alla fine: Prima Liste a concatenamento semplice info info info Dopo info info cancellato 46
Eliminare un oggetto mediano: Prima Liste a concatenamento semplice info info info Dopo info cancellato info 47 Liste a concatenamento doppio Liste a concatenamento doppio: ogni elemento è legato al successivo ed al precedente Chiaramente valgono tutte le osservazioni precedenti solo che ora i puntatori devono essere aggiornati a coppie 48
Liste a concatenamento semplice Riprendendo l esempio l oggetto trattato dalla lista a concatenamento doppio sarà: struct list_item struct address info; // è l informazione vera e propria struct list_item *next; struct list_item *previous; ; prev info next 49