STRUTTURE INFORMATIVE L’informazione elaborata da un calcolatore si presenta in una varietà di forme differenti che dipendono in genere dalla natura del problema da risolvere. Accanto a problemi che richiedono il trattamento di dati propriamente numerici, ve ne sono altri di natura non numerica che richiedono elaborazioni su sequenze di caratteri alfabetici, su schemi di flusso o grafi e così via. I dati si presentano quindi strutturati in modi differenti, per ciascuno dei quali è necessario individuare una rappresentazione interna al calcolatore che risulti conveniente per le elaborazioni da eseguire e per lo scambio di informazioni con l’esterno. Di conseguenza è opportuno esaminare i principali tipi di aggregati di dati dotati di una struttura logica, cioè le cosiddette strutture astratte di dati, e i sistemi per la loro rappresentazione nella memoria di un calcolatore, cioè le possibili strutture concrete adatte a contenere le strutture astratte. Con il termine strutture informative, si comprendono, – le strutture astratte, proprie del problema e dipendenti unicamente da questo ( insieme di leggi che definiscono le relazioni esistenti fra i dati di un insieme finito ); – le strutture concrete, interne alla memoria ( insieme di celle contenenti le informazioni e gruppi di regole per il loro ordinamento logico ) STRUTTURE ASTRATTE DI DATI Le principali strutture astratte di dati sono: • Lista lineare • Coda • Pila • Doppia coda • Array ( vettore e matrice ) • Tabella • Grafo • Albero Lista lineare Insieme finito di dati nel quale è definita una legge di ordinamento, cioè è stabilita una corrispondenza biunivoca tra i suoi elementi e l’insieme dei numeri naturali. In base a tale legge, è possibile stabilire: – qual è il primo elemento dell’insieme – qual è l’ultimo elemento dell’insieme – quale di due elementi qualsiasi precede l’altro • Gli elementi della lista devono essere omogenei fra loro • Nelle liste l’accesso ad un suo elemento deve necessariamente avvenire tramite una ricerca sequenziale a partire dal primo elemento della lista • Nel caso in cui gli elementi della lista siano caratteri di un alfabeto, si parla di stringa Le operazioni sulle liste sono di tue tipi: • GLOBALI: riguarda tutti gli elementi della lista; esempi: – concatenazione o fusione di due liste in una sola – suddivisione di una lista in più parti – ordinamento degli elementi secondo un criterio diverso da quello – iniziale • LOCALI: riguarda i singoli elementi della lista; esempi: – lettura e/o modifica di un elemento della lista – inserimento di un nuovo elemento nella lista – eliminazione di un elemento della lista La nozione di lista è generalizzabile: gli elementi della lista possono, a loro volta, essere delle liste i cui elementi possono essere ancora delle liste e così di seguito Coda Si tratta di una lista lineare di lunghezza variabile in cui: – gli inserimenti avvengono solo dopo l’ultimo elemento detto fondo della coda – le eliminazioni avvengono solo dal primo elemento detta testa della coda La coda è gestita in logica First In First Out ( FIFO ) cioè il primo elemento che può essere estratto è il primo ad essere stato inserito: Pila o Stack E' una lista lineare di lunghezza variabile in cui gli inserimenti e le estrazioni avvengono ad un solo estremo ( fondo della pila ). Il primo elemento che può essere estratto è quello che è stato inserito per ultimo: la pila è gestita in logica Last In First Out ( LIFO ) Doppia coda E' una lista lineare di lunghezza variabile in cui gli inserimenti e le estrazioni possono avvenire indifferentemente su entrambi gli estremi Array E' un insieme finito di elementi in corrispondenza biunivoca con un insieme di n-ple di numeri interi ( indici ). Gli indici possono assumere valori compresi in un intervallo determinato: – per n = 1, si parla di vettore ( array monodimensionale ) – per n = 2, si parla di matrice ( array bidimensionale ) Quindi in un array multidimensionale accesso ad un elemento avviene attraverso la n-pla di indici e non in modo sequenziale come avviene nelle liste. In particolare in un vettore l’accesso ad un elemento i avviene attraverso l’indice i, mentre l’accesso ad un elemento della lista avviene tramite una ricerca sequenziale che esamina tutti gli elementi della lista fino al reperimento dell’elemento voluto. Tabella E' un insieme finito di elementi, ciascuno dei quali costituito da una coppia ordinata di dati < chiave, valore> Il primo è il nome o chiave dell’elemento; il secondo sono le informazioni associate alla chiave. L’accesso ad un elemento della tavola avviene tramite la chiave. Le tavole sono utilizzate quando esistono corrispondenze biunivoche tra insiemi non esprimibili tramite formule matematiche RICERCATABELLARE CHIAVE chiave matricola 1232 1423 4562 1233 1424 1425 1510 1516 RICERCA TABELLARE Nome Nicola Giuseppe Carlo Giovanni Pippo Giancarlo Luigi Vittorio valore cognome Rossi Verdi Gentile Piccolo Garibaldo Sconosciuto Mazzini Bainchi VALORE Luogo nascita Roma Milano Torino Firenze Torino Torino Bologna Bari Grafo Il termine grafo è impiegato per indicare una figura costituita da: • un insieme finito di punti detti nodi o vertici • un insieme finito di segmenti, detti lati o archi, che congiungono alcune coppie di nodi appartenenti al primo insieme; gli archi possono essere quindi identificati dai nomi delle coppie di nodi da essi congiunti. il grafo può essere visto come la rappresentazione di una struttura astratta di dati, infatti i nodi rappresentano la struttura contenente le informazioni, gli archi le relazioni fra i dati contenuti nei nodi. Due nodi sono adiacenti se esiste un arco che li congiunge Il cammino è una successione di nodi adiacenti. In particolare: – un cammino semplice è una successione di nodi distinti, ad eccezione eventualmente del primo e dell’ultimo che possono coincidere – un ciclo o circuito è un cammino semplice che congiunge un nodo con se stesso Un grafo è connesso quando per ogni coppia di nodi, è sempre possibile congiungere tali nodi mediante un cammino. grafo connesso grafo orientato Alberi L' albero è un grafo non orientato, connesso e senza cicli che gode delle seguenti proprietà: • Per un albero valgono le seguenti proprietà: – se l'albero ha n nodi allora contiene n-1 archi; – esiste un solo cammino semplice tra ogni coppia di nodi dell’albero; – se si rimuove un arco qualsiasi dell’albero, la struttura risultante non è più connessa, ma composta da due alberi distinti • scelto un nodo arbitrario come radice, si possono ordinare i suoi nodi in livelli – il livello della radice è 1 – il livello di ogni altro nodo è uguale al numero di nodi contenuti nel percorso tra quel nodo e la radice • Con il termine FOGLIA si indicano quei nodi che non appaiono in alcun percorso semplice fra un altro nodo e la radice • Le strutture informative assumono spesso la forma di alberi in cui sia stabilita la radice (detti semplicemente ALBERI) • L’albero si dice ORDINATO se in ciascun livello si considera significativo l’ordine con cui compaiono i nodi; Per gli alberi, si può dare una definizione che non fa riferimento al grafo: Un albero è un insieme costituito da uno o più nodi tale che: – un particolare nodo è designato come radice – i rimanenti nodi, se esistono, possono essere suddivisi in insiemi disgiunti, ciascuno dei quali è a sua volta un albero ( sottoalbero ) E’ da notare che questa definizione è ricorsiva poiché espressa in funzione di altri alberi • grado di un nodo è il numero i sottoalberi del nodo stesso Alberi binari Un albero binario è un insieme di nodi, tale che: – un particolare nodo (se il numero dei nodi è diverso da zero) è designato come radice – i rimanenti nodi, se esistono, possono essere suddivisi in due insiemi disgiunti, ciascuno dei quali è a sua volta un albero binario ( sottoalbero sinistro e destro ) Si noti che l’albero binario NON è un caso particolare di albero, per i seguenti due motivi: – un albero binario può essere vuoto, mentre un albero deve contenere almeno un nodo – ciascuno dei due sottoalberi della radice conserva la propria identità di sottoalbero destro e sinistro anche se l’altro sottoalbero è vuoto Questo vincolo è molto più restrittivo dell’ordinamento dei nodi nei livelli di un albero ordinato. Ad esempio, due alberi binari contenenti un solo nodo, oltre alla radice, sono distinti se nel primo tale nodo è designato come sottoalbero sinistro della radice e nel secondo come sottoalbero destro. Visita degli alberi Visitare un albero equivale ad esaminare tutti i suoi nodi uno per uno in ordine appropriato I metodi di visita di un albero binario si distinguono per il momento in cui si esamina la radice rispetto ai suoi sottoalberi e sono: ordine anticipato, ordine differito e ordine simmetrico Algoritmo di visita di un albero binario in ordine anticipato 1. esamina la radice; 2. visita il sottoalbero sinistro, in ordine anticipato; 3. visita il sottoalbero destro, in ordine anticipato; Algoritmo di visita di un albero binario in ordine differito 1. visita il sottoalbero sinistro, in ordine differito; 2. visita il sottoalbero destro, in ordine differito; 3. esamina la radice; Algoritmo di visita di un albero binario in ordine simmetrico 1. visita il sottoalbero sinistro, in ordine simmetrico; 2. esamina la radice; 3. visita il sottoalbero destro, in ordine simmetrico Vista in ordine anticipato: A B D G H I E C F L M N Vista in ordine differito: GIHDEBLNMFCA Vista in ordine simmetrico: G D H I B E A C L F M N Trasformazione di un albero in albero binario L’importanza dell’albero binario si basa sulla relativa semplicità con cui tale struttura può essere allocata in memoria e nella possibilità di trasformare un albero in albero binario. L'algoritmo è il seguente: Un albero ordinato S si rappresenta come albero binario T, se: • i nodi di nodi di T e S sono gli stessi; • la radice di T coincide con la radice di S; • ogni nodo dell’albero binario T: • ha come figlio sinistro il primo figlio del nodo omonimo dell’albero S; • ha come figlio destro il fratello del nodo omonimo dell’albero S STRUTTURE CONCRETE DI DATI Sappiamo che la struttura della memoria di un calcolatore è estremamente semplice: celle di memoria a ciascuna delle quali è associato un indirizzo. In questo ambito è difficile rappresentare le strutture astratte. La soluzione è quella di realizzare dei programmi che permettano all’utente di impiegare la macchina come se tali strutture fossero proprie della macchina, più semplicemente rappresenteremo le strutture astratte tramite strutture concrete. Le strutture concrete sono: • Vettore • Lista concatenata • Plesso Vettore ( struttura sequenziale ) È la struttura concreta più semplice ed intuitiva. È dotata delle seguenti caratteristiche: – – è un insieme elementi costituiti da una o più celle di memoria con indirizzi crescenti; tutti gli elementi hanno la medesima lunghezza ( stesso numero di celle ). I parametri che caratterizzano una struttura sequenziale o vettore sono: – indirizzo base del vettore ( ib ) cioè l'indirizzo del primo elemento; – lunghezza del vettore, il numero ( m ) dei suoi elementi; – dimensionedell’elemento il numero ( d ) di celle richieste da ciascun elemento. Se consideriamo il vettore vett[10] allora: vett[0] = ib è l’indirizzo del primo elemento vett[ i ] = ib + i * d è l’indirizzo dell’elemento vett[ i ] La lunghezza m deve essere fissata e non può essere modificata: il vettore è una struttura rigida, adatta a contenere serie di dati il cui numero sia noto a priori, o per cui si possa prevedere un limite superiore. Di seguito un esempio di un vettore di 12 elementi, da 6 byte ciascuno, allocato in memoria dall’indirizzo 500 all’indirizzo 571. L’indirizzo del primo elemento x[0] è 500, quello dell’i-esimo elemento è dato da IND ( x[i] ) = 500 + 6 * i In conclusione, i vettori sono caratterizzati da scarsa flessibilità: • l’inserimento di un nuovo elemento tra due elementi richiede la cancellazione di tutti gli elementi che lo seguono e la loro riscrittura in una posizione più avanti • analogo discorso vale per l’eliminazione, se non si vogliono lasciare celle inutilizzate Lista concatenata Permette di assegnare alle celle un ordinamento logico arbitrario, differente dal loro ordinamento fisico, indicato dagli indirizzi. Si tratta di un insieme di elementi disposti in modo arbitrario nella memoria, purché non sovrapposti Ogni elemento è costituito da due parti: • il dato che rappresenta l’elemento della struttura astratta da rappresentare • l’indirizzo dell’elemento successivo della catena (puntatore) L’ultimo elemento contiene un puntatore nullo Il reperimento delle informazioni avviene attraverso una scansione della catena: l’indirizzo di un elemento è noto sotto forma di puntatore contenuto nell’elemento precedente. Di seguito un' esempio di lista concatenata costituita da 10 elementi, ciascuno delle lunghezza di 8 byte ci cui gli ultimi 4 contengono il puntatore all’elemento successivo. Possiamo avere una lista concatenata BIDIREZIONALE che è costituita da elementi dotati anche di puntatori all’elemento precedente e di una lista concatenata CIRCOLARE o CICLICA nelle quali il puntatore dell'ultimo elemento punta al primo elemento. Inserzione di un elemento in una lista concatenata L’inserzione di un elemento K tra gli elementi Hi e Hi + 1 richiede le operazioni: PUN ( K ) ind ( Hi + 1 ) PUN ( Hi ) ind ( K ) oppure PUN ( K ) PUN ( Hi ) Cancellazione di un elemento in una lista concatenata • L’eliminazione di un elemento Hi richiede la scansione fino all’elemento Hi-1 e quindi l’operazione: PUN ( Hi-1 ) ind ( Hi+1 ) che equivale a PUN ( Hi – 1 ) PUN ( Hi ) Nasce un problema: l’elemento Hi è ancora legato a Hi+1 e non si raggiunge più nella scansione: questo comporta uno spreco di memoria. È necessario disporre di un metodo di amministrazione della memoria libera, responsabile della raccolta delle celle che si rendono via via libere. Una possibile soluzione prevede di formare la lista concatenata libera, costituita con le celle libere. Da questa lista concatenata si estraggono gli elementi necessari a memorizzare nuove informazioni, e in essa si reinseriscono nuovi elementi non più necessari, seguendo i procedimenti di eliminazione ed inserzione Vantaggi e svantaggi delle liste concatenate Vantaggi: • memorizzazione compatta di insiemi di dati per cui è richiesto un ordinamento diverso da quello in cui i dati si presentano • inserzione ed eliminazione di elementi molto semplice • occupazione di memoria buona se si usano algoritmi di gestione della memoria libera Svantaggi: • spreco di spazio derivante dai puntatori, tanto più rilevante quanto più sia piccolo il campo contenente l’informazione • necessità di accompagnare ogni operazione con il relativo aggiornamento della lista libera • inefficienza nell’accesso ad un elemento, che richiede la scansione della lista Lista concatenata multipla La lista concatenata multipla è una generalizzazione del caso precedente in cui sia aggiunto un campo che contiene un puntatore verso una lista che a sua volta contiene l’informazione Plesso di Ross Il plesso è costituito da un insieme di elementi disposti in memoria in modo arbitrario, purchè non sovrapposti. Ciascun elemento è una struttura concatenata le cui parole possono contenere: – informazioni – puntatori ad altri elementi È necessario quindi specificare un formato per gli elementi che indichi il significato dei diversi campi in cui è divisa la struttura concatenata. Se tutti gli elementi sono uguali, il formato è esterno al plesso; altrimenti, è indicato come primo campo di ogni elemento Il formato specifica quindi la lunghezza del dato ed il numero di puntatori ad altri elementi. Facciamo un esempio di un plesso e conseguente allocazione di memoria: • Il formato di ogni elemento sia specificato nel primo byte di ciascuna struttura concatenata con la seguente convenzione: – il semibyte (nybble) più significativo indichi la lunghezza, in byte, del dato – il semibyte meno significativo indichi quanti puntatori, ciascuno di due byte, fanno parte dell’elemento. Se l’elemento non contiene puntatori (elemento terminale del plesso), tale semibyte = 0 Memorizzazione delle strutture astratte: liste Le liste possono essere rappresentate sia mediante vettori che liste concatenate, la scelta dipende dalle operazioni che devono essere fatte sulle liste. – Se l’ operazione prevalente la lettura con eventuale modifica, è idoneo il vettore. – Se si eseguono inserimenti, eliminazioni, fusioni di liste va bene la lista concatenata. Memorizzazione delle strutture astratte: code, pile, doppie code In generale possono essere usate sia la rappresentazione con il vettore che quella concatenata; sono comunque necessari uno o più puntatori, modificati dopo ogni inserimento o eliminazione, che identificano il primo, l’ultimo elemento, o entrambi, all’interno della struttura La coda è in genere rappresentata con una lista concatenata circolare con un puntatore che tiene aggiornato l’indirizzo del fondo della coda: in tale elemento viene tenuto l’indirizzo della testa della coda. La memorizzazione di una pila può essere fatta con un vettore. Infatti la testa della pila rimane fissa ed è quindi sufficiente un puntatore sull’ultimo elemento (elemento affiorante della pila) da aggiornare dopo ogni inserimento o eliminazione. Memorizzazione delle strutture astratte: matrici Si usa un vettore accodando gli elementi riga dopo riga oppure colonna dopo colonna. Per una matrice di m righe ed n colonne, l’indirizzo del generico elemento Aij IND ( Aij ) = IND ( A11 ) + ( i -1 ) * n * l + ( j - 1 ) * l dove: IND (A11) è l’indirizzo del primo elemento del vettore l è la lunghezza di ciascun elemento Se la memorizzazione avviene per colonne, basta sostituire nell’equazione m ad n e i a j Se la matrice ha grandi dimensioni, ma ha molti zeri ( matrice sparsa ), una possibilità è quella di usare una doppia famiglia di catene circolari: ogni elemento non nullo appartiene a 2 catene, una di riga ed una di colonna; quindi l’elemento della catena è formato: – dal dato – dai suoi due indici – da due puntatori agli elementi successivi su riga e colonna Memorizzazione delle strutture astratte: tabella Sono memorizzate tipicamente in vettori. I metodi per l’accesso ai singoli elementi possono essere molto diversi in funzione dell’uso previsto per le tavole stesse: la scelta del metodo ha l’obiettivo di minimizzare la lunghezza di ricerca cioè il numero di chiavi esaminate prima di raggiungere l’elemento cercato. I metodi più diffusi sono: – ricerca sequenziale – ricerca binaria – accesso diretto – accesso calcolato (hash) Ricerca sequenziale Gli elementi nella tabella sono allocati senza seguire alcuna regola di ordinamento. La ricerca si effettua scandendo gli elementi della tabella fino al reperimento della chiave desiderata. È una tecnica poco efficiente in cui il tempo medio di ricerca è pari a N/2, se N è la lunghezza della tavola. Una tavola su cui si opera una ricerca sequenziale può essere memorizzata come struttura sequenziale o a catena. Ricerca binaria La tabella deve essere ordinata secondo le chiavi. La ricerca binaria richiede il confronto fra la chiave cercata e quella dell’elemento centrale della tabella. Il risultato del confronto indica se la ricerca ha termine oppure in quale metà della tavola deve continuare. Il procedimento viene iterato Il tempo massimo di ricerca è log2N E’ una tecnica di ricerca veloce, ma è difficile inserire nuovi elementi per la necessità di riordinare le chiavi. Una tabella su cui si opera una ricerca binaria deve essere memorizzata come vettore, dato che l’accesso deve avvenire tramite calcolo dell’indirizzo. Accesso diretto È applicabile solo a tabelle le cui chiavi siano in corrispondenza biunivoca con gli indirizzi di memoria corrispondenti agli elementi della tabella. Esiste quindi una funzione di accesso che, a partire dalla chiave dell’elemento ricercato, permette di ottenere l’indirizzo di memoria. A chiavi diverse devono corrispondono indirizzi diversi. È difficile prevedere l’inserimento di nuovi elementi ( le relative chiavi devono ancora permettere l’identificazione di indirizzi diversi ). Il tempo di accesso è elevato, ma ha scarsa applicabilità a causa delle ipotesi restrittive. Anche in questo caso la tabella viene memorizzata in un vettore. Accesso calcolato (hash) È una generalizzazione dell’accesso diretto essendo basato sulla funzione di accesso e richiedendo che la tavola sia memorizzata in un vettore; tuttavia consente l’inserimento di nuovi elementi Il metodo si basa su una funzione di accesso, che applicata alla chiave fornisce un numero intero minore della lunghezza del vettore, che è a sua volta maggiore del numero di elementi da memorizzare. Questo numero calcolato attraverso la funzione di accesso, è l’indice dell’elemento del vettore. Purtroppo, a coppie di chiavi distinte può corrispondere lo stesso numero: problema della collisione di elementi ( le chiavi sono dette sinonimi ) Nasce il problema della scelta delle posizioni nel vettore corrispondenti agli elementi in collisione La scelta della funzione di accesso ed il criterio di gestione delle collisioni determinano la bontà dell’implementazione della tavola. I sinonimi possono essere memorizzati in catene interne o esterne alla tavola. Memorizzazione di alberi e grafi in catene Possono essere rappresentati da una catena generalizzata, in cui nella catena principale compaiono i dati associati a tutti i nodi, accompagnati da due puntatori: – il primo punta ad una catena secondaria formata dagli elementi che rappresentano sono i nodi adiacenti a quello in esame – il secondo punta all’elemento successivo nella catena principale Memorizzazione di alberi e grafi in plessi Si possono usare anche i plessi: ogni elemento contiene il dato del nodo e tanti puntatori quanti sono i nodi adiacenti a quello in esame. Il plesso è particolarmente adatto a rappresentare alberi binari: in tal caso, si hanno sempre due nodi uscenti da ogni nodo dell’albero e, di conseguenza, il formato degli elementi del plesso è omogeneo e può essere riportato una volta per tutte al di fuori degli elementi del plesso. Approfondimento: la lista concatenata implementata in C++ È possibile realizzare strutture dati in grado di allocare memoria solo quand'è necessaria, per ogni singolo dato da memorizzare. Un esempio sono le liste concatenate, in cui ogni elemento è associato a un puntatore contenente l'indirizzo dell'elemento successivo. Nella figura seguente, è rappresentata una lista concatenata avente quattro elementi (detti nodi); la variabile p punta al primo di questi nodi, mentre ciascun nodo contiene un puntatore al nodo successivo. L'ultimo nodo contiene un indirizzo speciale per il puntatore, detto NULL, corrispondente all'indirizzo zero, ad indicare che non punta a nulla. Dichiarazione e inizializzazione Se si vuole realizzare una lista concatenata in grado di contenere dei numeri interi, si può dichiarare la seguente struttura: struct nodo { int n; nodo *prossimo; }; Per rappresentare una lista vuota, poniamo NULL nel puntatore al primo nodo. Quindi, una lista concatenata potrà essere dichiarata come segue: nodo *p = NULL; Inserimento di un dato Supponiamo di voler inserire un nuovo nodo nella lista, contenente ad esempio il numero 1. Ecco una successione di istruzioni che permettono di aggiungere il nodo: Istruzione Effetto Descrizione nodo *q = new nodo; Creazione del nuovo nodo (in giallo) con l'aiuto di una variabile d'appoggio q q->n = 1; Scrittura del dato nel nuovo nodo q->prossimo = p; Concatenazione del nuovo nodo in testa alla lista Dirottamento del puntatore "ufficiale" della lista al nuovo elemento p = q; L'effetto finale di questa sequenza di istruzioni è quello di avere un nodo in più all'inizio della lista, come si vede megli nella seguente figura, dalla quale è stato rimosso il puntatore q, a questo punto ininfluente: La stessa sequenza di istruzioni funziona anche per inserire il primo elemento in una lista vuota: la verifica è lasciata al lettore. Stampa del contenuto di una lista Per stampare il contenuto di una lista è sufficiente realizzare un ciclo in cui, a differenza della scansione di un array, non si incrementa un indice, ma si sposta un puntatore di nodo in nodo: nodo *q; // Utilizza un nodo ausiliario q = p; // Punta q al primo nodo della lista while ( q != NULL ) { // fintantoche' q punta a un nodo (non siamo alla fine della lista) cout << q->n << '\n'; // scrivi l'informazione contenuta nel nodo q = q->prossimo; // sposta il puntatore q al prossimo nodo. } Più concisamente: for ( nodo *q = p; q != NULL; q = q->prossimo ) cout << q->n << '\n'; Eliminazione del primo elemento della lista Si tratta di effettuare una sorta di "operazione inversa" rispetto all'inserimento. Ripartiamo dalla lista iniziale, quella con quattro elementi, e vediamo come cancellare il primo nodo, contenente l'informazione 3. Istruzione Effetto Descrizione nodo *q = p; Salvo l'indirizzo del nodo da eliminare in p = p>prossimo; Avanzamento del puntatore p al nodo che diverrà la nuova testa della lista q Cancellazione del nodo da eliminare delete q; Si noti che, disponendo di funzioni per l'inserimento di un elemento in testa alla lista e per la cancellazione di un elemento dalla testa della lista, abbiamo realizzato una struttura di tipo LIFO del tutto equivalente alla pila, senza le limitazioni alle dimensioni dovute all'uso dei vettori. Inserimento di un elemento in fondo alla lista Per inserire un nuovo valore, ad esempio 16, in fondo alla coda, dobbiamo dapprima trovare l'ultimo elemento, in modo da concatenare a questo il nuovo nodo. Per farlo abbiamo bisogno di partire dal primo nodo e avanzare finché ci sono nodi successivi. Supponiamo, per ora, che la coda contenga già dei nodi. Istruzione Effetto Descrizione nodo *q; for ( q = >prossimo NULL; q = >prossimo p; q!= q); Ricerca dell'ultimo nodo della lista q->prossimo = new nodo; Creazione del nuovo nodo e assegnazione dell'indirizzo al puntatore che prima era NULL q->prossimo->n = 16; q->prossimo>prossimo = NULL; Inserimento dei dati nel nuovo nodo Ovviamente, queste istruzioni non sono valide se la lista è inizialmente vuota; infatti, in questo caso il nuovo nodo non va concatenato a un nodo precedentemente esistente, ma al puntatore p. Il codice diviene il seguente: if ( p == NULL ) { p = new nodo; p->n = 16; p->prossimo = NULL; } else { nodo *q; for ( q = p; q->prossimo != NULL; q = q->prossimo ); q->prossimo = new nodo; q->prossimo->n = 16; q->prossimo->prossimo = NULL; } Un altro problema è la necessità di un ciclo per la ricerca dell'ultimo elemento, che può rallentare l'esecuzione del programma se sono richiesti molti inserimenti. Per ovviare a questo, si può definire una lista come un'associazione di due puntatori a nodo, uno verso la testa e uno verso la coda della lista: struct lista { nodo *primo, *ultimo; }; Una lista definita in questo modo può essere rappresentata come segue: Con il puntatore all'ultimo elemento a disposizione, non è necessario andarlo a cercare ogni volta con un ciclo; per contro, ovviamente tale puntatore dev'essere mantenuto aggiornato, quindi ogni inserimento in coda alla lista richiede la modifica, e anche l'inserimento in testa, se viene eseguito su una lista vuota. Ricerca di un elemento in una lista Una lista concatenata non permette l'accesso a qualunque dato a partire da un indice, come nel caso del vettore. Per accedere a un elemento, è necessario partire dall'inizio della lista e procedere di nodo in nodo fino al raggiungimento del nodo giusto. Ad esempio, per cercare il valore n nella lista L servono le seguenti istruzioni: nodo *q = L.primo; while ( q != NULL && q->n != n ) q = q->prossimo; Alla fine del ciclo, il puntatore q punterà al nodo contenente il dato cercato. Se tale nodo non esiste, q vale NULL. È chiaro che una strategia come la ricerca binaria non è praticabile. Se la lista è ordinata, è possibile terminare la ricerca non appena si trova un nodo con un valore maggiore di quello cercato. È sufficiente trasformare la condizione "q->n!=n" in "q->n<n". e la lista è concatenata a sé stessa.