STRUTTURE INFORMATIVE STRUTTURE ASTRATTE DI DATI

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.