Lista concatenata Struttura di dati lista concatenata • Consideriamo una nuova modalità di memorizzare i dati in cui l’accesso non avviene più tramite un indice, che individua la posizione del dato nella struttura, ma tramite un indirizzo di memoria. • Per costruire una sequenza di informazioni è necessario che ogni dato della struttura memorizzi l’indirizzo di memoria dell’elemento successivo. • Si costruisce pertanto una struttura di dati collegata, chiamata lista concatenata. Lista concatenata Lista concatenata • Un elemento, o nodo, di una lista concatenata oltre all’informazione memorizza anche il riferimento dell’elemento successivo: per rappresentare il nodo, abbiamo bisogno di un record con due (o più) campi di tipo diverso: • Il nodo sarà composto da : • l’informazione: un elemento di tipo base o un record o un array, … • il riferimento sarà un riferimento ad un nodo: un riferimento ad un elemento dello stesso tipo del nodo. • Si intuisce che le definizione del nodo sarà un po’ particolare: dobbiamo definire il tipo del nodo, ma al suo interno dobbiamo usare quel tipo (il nodo ha un campo riferimento). dato: info riferimento: punt Lista concatenata Lista concatenata • Le informazioni memorizzate non sono più necessariamente contigue in memoria, come nell’array. • La lista concatenata non ha una dimensione stabilita a priori; dobbiamo però individuarne la fine, vale a dire un valore per il riferimento con il quale non si acceda ad alcun nodo. • Tale valore nel C++ è NULL. • inizio 1 Lista concatenata • Vedremo la costruzione di una struttura di dati lista concatenata per rappresentare una sequenza di interi. • Sintassi. struct tipolista {//tipo del nodo int info; tipolista *punt; }; tipolista *nodo; Confronto tra array e lista concatenata // nodo Confronto tra array e lista concatenata Accesso: lista concatenata • Prima di vedere la costruzione di una lista concatenata, mettiamo a confronto le due strutture di dati: • accesso • elemento successivo • inserimento e cancellazione • dimensione • spazio di memoria • L’accesso è sequenziale: per accedere ad un dato si deve scorrere la lista facendo una scansione lineare. • Di fatto si esegue una “ricerca” nella struttura esaminando i nodi fino a trovare il valore cercato. Non si può ritornare indietro: si può solo vedere in avanti. • Per eseguire la scansione della lista si deve iniziare “dal primo” nodo della struttura e passare al successivo. Accesso: lista concatenata Accesso: array • Sia p un puntatore di tipo tipolista, l’accesso al campo info è: (*p).info oppure p→ →info • L’accesso è diretto: per accedere ad un dato si utilizza l’indice dell’array x inizio • La struttura è accessibile da un riferimento inizio che “vede” il primo nodo. x • Se v è il nome dell’array, v[i] rappresenta l’accesso all’i-esimo elemento (v[3]=x). • Con l’accesso diretto non c’è un ordine da rispettare: v[3], v[0], v[5], …: si può tornare indietro. 2 Successivo: lista concatenata Successivo: array • Ogni nodo, tranne l’ultimo, contiene nel campo punt l’indirizzo di memoria del nodo successivo. Sia p un puntatore di tipo tipolista, per passare al nodo successivo si memorizza in p il riferimento al nodo successivo: p = p→ →punt; • Dato un elemento nella posizione i , v[i], il successivo si trova nella posizione i+1; per passare al successivo si fa i+1, quindi v[++i] oppure i = i+1; uso v[i] 4 4 10 10 p Inserimento e cancellazione: lista concatenata Inserimento e cancellazione: lista concatenata • Per inserire un nuovo nodo bisogna: 1) costruire il nuovo nodo 2) agganciarlo nella posizione voluta con assegnazioni sui riferimenti 1) Costruzione del nuovo nodo: • Bisogna trovare una “posizione” nella lista, dopo la quale effettuare l’inserimento del nuovo nodo: questa posizione si ottiene facendo la scansione lineare alla ricerca di un valore z (un campo info) che dovrà essere presente nella lista. tipolista *nuovo = new tipolista; nuovo.info = x; 2) Per poterlo agganciare bisogna sapere dove: in testa, in coda, dopo un altro nodo, ... z a x Inserimento e cancellazione: lista concatenata Inserimento e cancellazione: array • Per cancellare un nodo bisogna assegnare al riferimento punt di un nodo il valore del riferimento successivo: p→punt = p→punt→punt ; • Per inserire un nuovo dato nella i-esima posizione si deve: 1) verificare se la lunghezza dell’array è sufficiente e slittare verso destra i valori da n a i+1 2) con altro array: copiare i valori fino alla posizione i nel nuovo array, inserire il nuovo elemento, copiare i rimanenti valori 1 bis) se non serve la posizione intermedia, si può aggiungere l’elemento alla fine. a x d 3 Inserimento e cancellazione: array Dimensione: lista concatenata e array • Per cancellare un dato dalla i-esima posizione si deve: 1) ricopiare gli elementi a partire dalla posizione i+1 sul precedente 1 bis) se l’elemento da togliere è unico e non interessa l’ordine, si può copiare l’ultimo sull’i-esimo posto. • La lista concatenata non ha limite di dimensione massima. Spazio di memoria: lista concatenata e array Confronto tra array e lista concatenata • La lista concatenata occupa più spazio: ogni nodo è composto da due campi: • l’informazione • il riferimento • Conclusione. • L’array richiede “spostamento” di dati (O(n)) nel caso di inserimento e cancellazione, che per la lista sono O(1); possiede invece accesso diretto, che è O(1), mentre la lista accesso sequenziale, che è O(n). • L’array occupa meno spazio: • c’è solo l’informazione che deve essere memorizzata Costruzione di una lista concatenata • Vogliamo costruire una sequenza di interi utilizzando la struttura di dati lista concatenata. • Utilizziamo un sottoprogramma di nome costruiscilista, che restituisce un puntatore ad un elemento di tipo tipolista. • Nel sottoprogramma utilizziamo un puntatore piniz che “vede” il primo elemento della lista; ogni nuovo elemento viene costruito e agganciato a piniz (la testa della lista). • L’array ha una dimensione fissa. • Si può gestire male la memoria e occuparla tutta. Se viene esaurita la memoria disponibile l’esecuzione si interrompe: Segmentation fault • Pertanto il tipo di problema suggerirà quale struttura di dati sia più idonea: molti accessi e poche modifiche oppure pochi accessi e molte modifiche. Costruzione di una lista concatenata tipolista *costruiscilista(){ tipolista *p, *piniz; int dato; /*tutti gli elementi sono agganciati in testa, pertanto l'ultimo inserito e' il primo della lista */ piniz = NULL; cout<<"per terminare inserire 1000"; cin>>dato; 4 Costruzione di una lista concatenata while(dato !=1000) { p = new tipolista; p->info = dato; p->punt = piniz; piniz = p; cin>>dato; }//fine while return piniz; }//fine costruiscilista Array e riferimenti • Consideriamo la seguente definizione: int v[10]; il compilatore alloca nella memoria uno spazio per 10 componenti intere e mette nella variabile v l’indirizzo di memoria di v[0]. Pertanto v è un puntatore ad un’area di tipo intero. v[0] v[9] v Array e riferimenti Aritmetica dei puntatori • La variabile v contiene il valore &v[0]. • Possiamo accedere a v[0] non solo tramite l’indice ma anche considerando il contenuto di v: *v. • Quando un array è variabile di scambio in un sottoprogramma, viene passato il valore presente nella variabile v: è per questo che le componenti dell’array sono visibili anche al sottoprogramma. • Come parametro formale si può usare, oltre a int v[] anche int * . • Nel linguaggio C++ è possibile accedere ad aree di memoria successive a quelle memorizzate in un puntatore. Consideriamo int *p; p = …; p = p+1; Aritmetica dei puntatori Aritmetica dei puntatori • In generale si ha: p = p + n; l’indirizzo iniziale di p viene incrementato di un numero di byte uguale a: n*(n° byte dell’area puntata da p) • Esempio. short *p; p = &varintera; p p = p+2; • Dopo l’assegnazione p vede l’area successiva. p • L’indirizzo contenuto in p viene incrementato di 2*(2 byte) = 4 byte. • Esempio. double *q; q = &vareale; q = q+3; • L’indirizzo contenuto in q viene incrementato di 3*(8byte) = 24 byte. 5 Aritmetica dei puntatori Costruzione di una lista concatenata: inserimento in testa • Possiamo anche usare l’aritmetica dei puntatori per accedere alle componenti di un array: dal momento che v contiene &v[0] e che *v coincide con v[0], abbiamo che: *(v+1) coincide con v[1] *(v+2) coincide con v[2] ... • Invece di accedere alle componenti tramite un indice si può accedere anche tramite un indirizzo di memoria (non useremo questa tecnica). • Definizione del nodo: Costruzione di una lista concatenata: inserimento in testa Costruzione di una lista concatenata: inserimento in testa • Si aggiunge il primo elemento: • Costruzione degli altri elementi: inserimento in testa: p = new tipolista; p->dato = 1; p->punt = inizio; //NULL inizio = p; 1 • struct tipolista{ int dato; tipolista *punt; }; tipolista *p, *inizio; • Costruzione della lista. • Si parte da lista vuota: • inizio = NULL; inizio p = new tipolista; p->dato = 5; p->punt = inizio; inizio = p; 1 • inizio inizio 5 p p Costruzione di una lista concatenata: inserimento in testa Costruzione di una lista concatenata: inserimento in coda • Quando si costruisce una lista inserendo tutti gli elementi in testa, le istruzioni sono le stesse per il primo e per tutti gli altri elementi. • Si ha quindi una struttura iterativa che termina quando si inserisce l’ultimo elemento: poiché la lista concatenata non possiede un numero prefissato di elementi, si può avere una condizione con un controllo su un valore speciale oppure una scelta per continuare o no. • Per inserire un elemento alla fine della lista, in coda, è necessario avere un puntatore che contiene il riferimento all’ultimo elemento tipolista *p, *inizio, *ultimo; • Costruzione della lista. • Si parte da lista vuota: inizio = NULL; • inizio 6 Costruzione di una lista concatenata: inserimento in coda Costruzione di una lista concatenata: inserimento in coda • Si aggiunge il primo elemento: • Costruzione degli altri elementi: inserimento in coda: p = new tipolista; p->dato = 1; p->punt = NULL; //il primo è anche inizio = p; //l’ultimo ultimo = p; 1 • p = new tipolista; p->dato = 5; p->punt = NULL; inizio ultimo->punt = p; ultimo = p; inizio 1 5 • ultimo p p ultimo Costruzione di una lista concatenata: inserimento in coda Inserimento e cancellazione in una lista concatenata • Quando si costruisce una lista inserendo tutti gli elementi in coda, le istruzioni per inserire il primo sono diverse da quelle per inserire tutti gli altri elementi. • Il primo elemento si aggancia a inizio inizio = p; gli altri si agganciano a ultimo->punt ultimo->punt = p; . • Per poter inserire o cancellare un elemento in una lista occorre trovare il punto in cui eseguire l’operazione: occorre pertanto eseguire una scansione lineare della lista con un puntatore pos di tipo tipolista. • Questa scansione lineare si fa cercando un elemento della lista: si può inserire prima o dopo l’elemento (se è presente), si può cancellare l’elemento successivo, il precedente o l’elemento stesso. Ricerca in una lista concatenata Inserimento in una lista concatenata • Si cerca un elemento elem a partire dall’inizio della lista: • Caso 1. Inserimento (di 1) dopo un elemento presente (6): p->punt=pos->punt; pos = inizio; trovato = false; while((pos!=NULL) && (!trovato)){ if(pos->dato == elem) trovato = true; else pos = pos->punt; }//ricerca lineare pos->punt =p; 2 3 • 6 inizio 1 pos p 7 Inserimento in una lista concatenata Inserimento in una lista concatenata • Caso 2. Inserimento prima di un elemento presente. • Per eseguire questo inserimento si deve eseguire la scansione con due puntatori: pos “cerca” l’elemento, prec “vede” il precedente. 2 6 • inizio • Scansione con due puntatori: while((pos!=NULL) && (!trovato)){ if(pos->dato == elem) trovato = true; else {prec = pos; pos = pos->punt;} } • L’inserimento viene effettuato l’elemento (2) “visto” da prec. prec dopo pos Cancellazione in una lista concatenata Cancellazione in una lista concatenata • Caso 1. Cancellazione dopo un elemento. • Si può eseguire la cancellazione dopo solo se l’elemento trovato non è l’ultimo. • Caso 3. Cancellazione prima di un elemento. • Si esegue la scansione con due puntatori, si scambiano i campi dato (scambiare 2 con 6) dei due riferimenti visti da prec e pos e si effettua la cancellazione dopo prec . if(pos->punt != NULL) pos->punt = pos->punt->punt; • Caso 2. Cancellazione dell’elemento trovato. • Si esegue la scansione con due puntatori e si cancella dopo prec; se l’elemento trovato è il primo della lista (se pos == inizio) allora si deve modificare il valore di inizio. Primo e ultimo elemento in una lista concatenata • Nelle operazioni di inserimento e cancellazione si deve prestare molta attenzione al primo e all’ultimo elemento: • il primo elemento è individuato dal puntatore inizio, quindi: pos == inizio • l’ultimo elemento contiene il riferimento NULL, quindi: pos->punt == NULL • Altre operazioni: costruire una lista ordinata, inserire un elemento in ordine, eseguire la fusione di due liste ordinate, . . . Allocazione statica e dinamica • Si parla di allocazione statica quando lo spazio per le variabili viene riservato prima dell’inizio dell’esecuzione del programma: lo spazio viene allocato durante la compilazione. • Si parla di allocazione dinamica quando lo spazio per le variabili (alcune) viene riservato durante l’esecuzione del programma. • L’allocazione dinamica si ha utilizzando l’operatore new. (par. 11.4) 8 Allocazione statica e dinamica Allocazione statica e dinamica • Le variabili allocate dinamicamente restano accessibili anche quando il sottoprogramma è terminato: abbiamo visto la costruzione di una lista concatenata in un sottoprogramma e la stampa di tale lista in un sottoprogramma diverso. • Le variabili allocate dinamicamente occupano una parte della memoria che si chiama Heap. • Le variabili allocate staticamente occupano una parte di memoria chiamata Stack. • Cosa accade delle variabili che non sono più referenziate? Quando si cancella un elemento da una lista, quell’area allocata non è più visibile poiché non c’è un puntatore che permette di accedervi. Come si può riutilizzare? • Nei linguaggi che utilizzano molta allocazione dinamica, come Java, esiste un programma per la “ripulitura” automatica della memoria: garbage collector. Allocazione statica e dinamica • Il garbage collector scandisce la memoria e marca le aree non più referenziate e le rende nuovamente libere. • Nel linguaggio C++ (Pascal) si deve eseguire una ripulitura manuale della memoria utilizzando l’istruzione delete. • Per usare correttamente tale istruzione si deve conoscere con esattezza quale area vuole cancellare (non tratteremo questo argomento). TDA: Tipo di dati Astratto TDA: Tipo di dati Astratto TDA: Tipo di dati Astratto • Si vuole costruire un nuovo tipo di dato: si deve quindi definire un dominio (i dati) e le funzioni che operano sul dominio (le operazioni che possiamo fare sugli elementi del dominio). • I tipi di dato astratto (ADT: Abstract Data Type) che consideriamo sono dei contenitori di informazioni e si differenziano per le operazioni che possono essere eseguite su quelle informazioni: pila, coda, lista, dizionario, albero. • Una volta stabilito cosa può fare il TDA, dobbiamo realizzarlo e scegliere come vengono effettuate le operazioni: dobbiamo scegliere una struttura di dati. • Una struttura di dati è un modo di organizzare dati ed è caratterizzata da una sua propria modalità di accesso. • Le strutture di dati che abbiamo visto sono: array e liste concatenate. 9 TDA: Tipo di dati Astratto • Poiché un TDA rappresenta in generale un contenitore di informazioni, le funzioni che operano sul dominio dovranno svolgere: • inserimento di un elemento • rimozione di un elemento • ispezione degli elementi contenuti nella struttura: ricerca di un elemento all’interno della struttura • Ci sono delle operazioni che si fanno in maniera efficiente sia con array che con lista concatenata, altre risultano più complesse con una struttura piuttosto che con l’altra. Pila o Stack Pila o Stack Pila o Stack • Una Pila è un TDA ad accesso limitato. • Si può accedere solo al primo elemento della Pila, detto anche testa. • Le sue funzioni sono: • In una Pila gli oggetti possono essere inseriti ed estratti secondo un comportamento definito LIFO: • • • • verifica se la Pila è vuota: isEmpty guarda la testa: top inserisci in testa: push estrai la testa: pop Last In First Out l’ultimo inserito è il primo a uscire Il nome ricorda una “pila di piatti”: l’unico oggetto che può essere ispezionato è quello che si trova in cima alla pila. Pila o Stack Pila o Stack • Le operazioni che caratterizzano questo TDA non sono tutte sempre possibili; possiamo sempre aggiungere un elemento in cima alla Pila, ma se la Pila è vuota: • non possiamo togliere la testa della Pila • non possiamo ispezionare la testa della Pila. • Supponiamo di rappresentare una Pila con un array di interi di 5 componenti: int vett[5]; • Per realizzare il TDA Pila dobbiamo: • rappresentare la situazione Pila vuota • per poter inserire un elemento, sapere quale è la prima posizione libera • Utilizziamo una variabile intera il cui valore indica la prima posizione libera: sp (stack pointer) . • Vediamo come si realizza una Pila mediante la struttura di dati array e poi mediante la lista concatenata. 10 Pila o Stack Pila o Stack int sp; • Pila vuota: sp = 0; • inserire in testa: vett[sp] = valore; sp++; • accedere alla testa: vett[sp-1] • estrarre la testa sp--; • Esempio: • si parte da Pila vuota: sp=0 • inseriamo in testa il valore 6: vett[sp]=6; sp++; // sp=1 4 3 2 1 0 • inseriamo in testa 15: vett[sp]=15; sp++; // sp=2 Pila o Stack 4 3 2 • togliamo la testa: sp--; // sp=1 l’elemento non viene cancellato, ma 15 non è più accessibile sp=1 sp=0 sp = 2 sp = 1 sp = 0 15 6 Pila o Stack • guardiamo la testa: accesso all’elemento vett[sp-1] // 15 4 3 15 6 • Utilizzo di una Pila. • Durante l’esecuzione di un programma nel RuntimeStack sono allocate aree per i descrittori dello stato dei sottoprogrammi che sono sospesi. • Un editor mantiene traccia delle operazioni eseguite: quando si effettua un “undo” per annullare un’operazione, si eliminano le ultime eseguite ripristinando lo stato precedente: l’ultima modifica viene eliminata “estraendola” dalla testa della pila. Stampare una Pila Operazioni su una Pila • Quando si stampa un Pila gli elementi appaiono nell’ordine inverso a quello di inserimento; inoltre la Pila si vuota. • Supponiamo di avere introdotto nella Pila i valori 1, 2, 3 nell’ordine; per stampare la Pila bisogna accedere ad ogni elemento e poiché è accessibile solo la testa, per poter “vedere” gli altri elementi si deve togliere la testa. Poiché la testa è l’ultimo elemento inserito, gli elementi appaiono in ordine inverso. 11 Stampare una Pila stampa testa 3 stampa testa 2 3 2 1 Stampare una Pila stampa testa 1 2 1 1 Pila vuota • Se vogliamo stampare gli elementi nello stesso ordine di inserimento, dobbiamo prendere un’altra Pila e “rovesciare” quella iniziale e stampare la nuova Pila. 3 2 1 2 1 3 Nuova Pila 1 2 3 1 2 3 Nuova Pila Ricerca in una Pila • Non ci sono assiomi di “ricerca elemento” tra gli assiomi dello Stack. • Pertanto se vogliamo eseguire la ricerca di un elemento in una Pila è necessario utilizzare una Pila di appoggio ed estrarre gli elementi dalla Pila in cui eseguire la ricerca. Se la testa coincide con l’elemento cercato allora l’elemento è presente. Se la Pila iniziale si vuota l’elemento non è presente. • Successivamente si reinseriscono nella Pila iniziale gli elementi tolti. Pila o Stack • Esercizio. Si consideri una formula matematica; scrivere un algoritmo per verificare se le parentesi sono o no bilanciate. • Analisi del problema. • La formula {a + (b-c) * [(a + b) - 7]} ha parentesi bilanciate, mentre la formula {a + (b-c}-5) non ha parentesi bilanciate, anche se il numero di tonde e graffe aperte coincide con il numero di quelle chiuse. Quindi non è sufficiente contarle. Pila o Stack Pila o Stack • Se vogliamo realizzare il TDA Pila con una lista concatenata: dovremo realizzare gli assiomi: • verifica se la Pila è vuota • guarda la testa • inserisci in testa • estrai la testa • Possiamo costruire delle funzioni che rappresentano le varie operazioni. • Abbiamo visto la costruzione di una lista concatenata con inserimento in testa. • Se vogliamo realizzare il TDA Pila, Stack, dobbiamo costruire della funzioni che rappresentano gli assiomi: • inserire in testa un nuovo elemento: push • estrarre dalla testa il primo elemento: pop • ispezionare la testa: top • verificare se lo Stack è vuoto: isEmpty 12 Realizzare una Pila, Stack Realizzare una Pila, Stack • La realizzazione della funzioni può utilizzare l’array o la lista concatenata. • Il programma che costruisce la Pila, utilizzerà una struttura iterativa che chiama una funzione “inserisciintesta” (push) senza “sapere” quale è la struttura di dati utilizzata. • Il programma che gestisce la Pila avrà delle funzioni per: estrarre il primo elemento (pop), restituire il valore del primo elemento (top) e verificare se la Pila è vuota oppure no (isEmpty). • Pertanto un programma per gestire il TDA Pila sarà del tipo: • costruzione della Pila finché ci sono dati da inserire chiama inserisciintesta • stampa della Pila: finché la Pila non è vuota chiama stampa la testa chiama estrai la testa Complessità delle funzioni della Pila Complessità delle funzioni della Pila • Vogliamo calcolare la complessità delle operazioni che riguardano la realizzazione degli assiomi della Pila. • La complessità delle operazioni dipende dalla struttura di dati e non dal TDA. • Le operazioni sono: isEmpty, push, pop, top. • Il tempo di esecuzione di ogni operazione su una Pila realizzata con array (di dimensione compatibile) è costante: abbiamo solo un numero costante di assegnazioni, confronti o restituzione di valore. Il tempo non dipende dalla dimensione della struttura dati: quindi O(1). • Anche per la lista concatenata si ha un numero costante di assegnazioni sui puntatori, confronti, restituzione di valore: quindi O(1). Coda o Queue Coda o Queue • Una Coda è un TDA ad accesso limitato. • Si può accedere al primo elemento della Coda, detto testa e all’ultimo elemento detto coda. • Le sue funzioni sono: • • • • verifica se la coda è vuota: isEmpty guarda la testa: front inserisci in coda: enqueue estrai la testa: dequeue 13 Coda o Queue Coda o Queue • In una Coda gli oggetti possono essere inseriti ed estratti secondo un comportamento definito FIFO: First In First Out il primo inserito è il primo a uscire Il nome ricorda una “fila in attesa”: viene estratto l’elemento che si trova “in coda” da più tempo, testa. • Gli assiomi assomigliano a quelli della Pila: • • • • verifica se la Coda è vuota: isEmpty guarda la testa: top, front estrai un elemento (la testa): pop, dequeue inserisci un elemento: push (testa), enqueue (coda) • Solo l’inserimento è diverso: nella Pila si inserisce in testa, nella Coda alla fine. • La Coda si può realizzare su array oppure su lista concatenata. Coda o Queue • La realizzazione della Coda su array è più complessa. • Nella Pila si estraeva e si inseriva da un’unica parte: l’estremo destro dell’array sp-- Coda o Queue estrarre dalla testa inserire in coda • Costruiamo l’array: int vett[6]; int qp; vett[0] sp++ • Nella Coda decidiamo di: inserire a destra (in coda) e di estrarre a sinistra (in testa). Coda o Queue • Per inserire un elemento in coda: v[qp] = valore; qp++; L’array deve avere dimensione opportuna. • Per togliere la testa ed avere la nuova testa nella prima posizione (qp=0) si devono ricopiare all’indietro gli elementi: qp--; for(int i = 0; i<qp; i++) v[i] = v[i+1]; //queue pointer: indica la prima posizione libera qp = 0; //Coda vuota; • Per accedere alla testa: restituire v[0] Coda o Queue • Questa realizzazione, efficiente per la Pila, è poco efficiente per la Coda. • Nella Pila tutte le operazioni sono O(1). • Nella Coda le operazioni sono O(1), tranne togli che è O(n): infatti per mantenere la struttura compatta, si devono sempre spostare tutti gli elementi. • Per realizzare una Coda più efficiente usiamo due indici. 14 Coda o Queue Coda o Queue • Un indice vede il primo elemento, l’altro indice vede l’ultimo. • L’ultimo rappresenta la prima posizione libera in cui inserire un nuovo elemento (inserimento in coda); il primo è la testa. L’array è riempito nella parte centrale. • Abbiamo quindi: int primo, ultimo; primo = 0; //testa ultimo = 0; //prima posizione libera • Coda vuota: primo == ultimo la prima posizione libera è la testa della coda • accedere alla testa (se non è vuota): restituire v[primo] primo ultimo Coda o Queue Coda o Queue • togliere la testa (se non è vuota): primo ++; • inserire un elemento, in coda: v[ultimo] = valore; ultimo++; • In tale modo tutte le operazioni sono O(1) la realizzazione con due indici è più efficiente. • Nella realizzazione con array, sia per la Pila che per la Coda, c’è il problema della dimensione fissa dell’array. • Rimane un problema. • Supponiamo che l’array abbia 10 componenti (i=0, 1, 2, ..., 9), si inizia con coda vuota: primo=0 e ultimo=0. • Eseguiamo 10 operazioni di inserimento: ultimo=10, che rappresenta Coda piena. • Eseguiamo 10 operazioni di estrazione: primo=ultimo, che rappresenta Coda vuota. Coda o Queue Coda o Queue • Ora vogliamo inserire un nuovo dato: poiché ultimo = 10 non si può inserire, anche se la Coda è vuota: in tale modo lo spazio va “perduto” . • Si risolve il problema con la tecnica dell’array circolare: se c’è posto prima si può inserire il nuovo dato. • Quando ultimo coincide con la lunghezza dell’array, si ritorna al valore iniziale, in tale modo si recupera lo spazio lasciato libero dall’eliminazione degli elementi, facendo “il giro”: ultimo primo 15 Coda o Queue Coda o Queue • Aritmetica modulo m. • Siano a ed m due numeri naturali, indichiamo con a mod m il resto della divisione a/m Se m = 10 abbiamo: per a < m a mod m = a per a ≥ m ritroviamo come resti i valori compresi tra 0 e 9 • Se vogliamo realizzare il TDA Coda con una lista concatenata, utilizziamo due riferimenti: primo vede la testa della Coda e ultimo vede l’ultimo elemento della Coda. Si inserisce con ultimo e si estrae con primo. • Gli assiomi da realizzare sono: verifica se è vuota, estrai, inserisci, guarda la testa. • Vedremo un modulo lista.c con tutte le funzioni per costruire i TDA Pila e Coda e tutte le operazioni di inserimento e cancellazione. Lista Lista • La Lista è un TDA che generalizza il concetto di sequenza: gli elementi hanno un ordine posizionale. Nella Lista tutti gli elementi sono accessibili. • La realizzazione con array è poco efficiente, la sua realizzazione “naturale” è con la lista concatenata. • Il modulo lista.c contiene tutte le possibili operazioni per accedere ai vari elementi, inserire e rimuovere elementi in qualunque posizione. Lista Lista • C’è un linguaggio di programmazione LISP (LISt Processor, 1958) basato sul concetto di lista. • È un linguaggio funzionale: il programma è inteso come funzione. • La lista viene definita attraverso i suoi assiomi (funzioni) e le funzioni possono essere composte per costruire altre funzionalità della lista. • Le informazioni elementari che si vogliono rappresentare in una Lista si chiamano atomi. • Dominio del TDA: Dominio = {atomi, lista}= A ∪ L L = {insieme di tutte le liste} A = {insieme degli atomi} costante = λ : lista vuota funzioni = {isEmpty, head, rest, build} (par. 11.2.2) 16 Lista Lista • Nel linguaggio LISP queste funzioni si chiamano: isEmpty null head car rest cdr build cons • Vediamo il comportamento del TDA tramite i suoi assiomi, indipendentemente dalla sua realizzazione. • Funzioni (assiomi) che definiscono la Lista: • 1) isEmpty : L → B rest(l ) = l ' rest: toglie la testa false se l ≠ λ se l ≠ λ head(l ) = a head: restituisce la testa l a Lista l l' a • 2) build : A × L → L build(a, l ) = l ' build: concatenazione atomo-lista costruisce la lista se l = λ • 2) head : L → A Lista • 1) rest : L → L se l ≠ λ true isEmpty(l) = l l' • La Lista viene definita in generale tramite una definizione ricorsiva: • una lista è: • la lista vuota • oppure, dato un atomo e una lista (che può essere λ) è la concatenazione dell’atomo alla lista. • Con questa definizione ricorsiva vediamo come si può costruire la lista. Lista • l = λ = () si parte da lista vuota • a è un atomo; si può costruire la lista (a): (a) = build(a, λ) • aggiungiamo altri elementi: siano b e c atomi (b, a) = build(b,(a)) (c, b, a) = build(c, (b,a)) • Le funzioni si possono comporre e si possono costruire altre funzioni, con le quali si rappresenta l’accesso a tutti gli elementi della lista. Lista • Esempio. • Sia L = (c, b, a) head(L) = c rest(L) = (b, a) componiamo le funzioni: head(rest(build(c, (b, a)))) = b head(build(c,(b, a))) = c 17 Lista Lista • L’insieme degli assiomi è completo: ogni altra funzione della Lista può essere espressa tramite gli assiomi. Si usano definizioni ricorsive. • Esempio. Definiamo la lunghezza della Lista: ln: L → 0 se L = λ len(L) = 1+ len(rest(L)) se L ≠ λ len((c, b, a)) = 1 + len((b, a)) = 1 + 1 + len((a)) = = 1+ 1+ 1+ len(λ ) = 3 • Definiamo la fine della Lista (l’ultimo elemento) end: L → A solo se L ≠ λ head(L) se rest(L) = λ end(L) = end(rest(L)) se rest(L) ≠ λ end((c, b, a)) = end((b, a)) = end((a)) = = head((a)) = a Lista Lista • Definiamo una funzione per aggiungere alla fine un elemento: addToEnd: L × A → L build(a, L) se L = λ addToEnd(L,a) = build(head(L), addToEnd(rest(L), a)) se L ≠ λ addToEnd(λ, a) = build(a, λ) = (a) addToEnd((a), b) = = build(a, addToEnd(λ , b)) = = build(a, (b)) = (a, b) addToEnd((a, b), c) = = build(head(a,b), addToEnd(rest(a,b), c) = = build(a, addToEnd((b), c) = build(a, (b,c)) = = (a, b, c) Lista • Possiamo definire la funzione per togliere l’ultimo elemento (se L ≠ λ): deleteFromEnd: L → L rest(L) se rest(L) = λ deleteFromEnd(L) = se rest(L) ≠ λ build(head(L), deleteFromEnd(rest(L)) • Si può anche definire la funzione per la concatenazione di due liste. 18