struct nodo { int n; anagrafica *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;
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 | |
p = q; | Dirottamento del puntatore "ufficiale" della lista al nuovo elemento |
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.
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';
Istruzione | Effetto | Descrizione |
nodo *q = p; | Salvo l'indirizzo del nodo da eliminare in q | |
p = p->prossimo; | Avanzamento del puntatore p al nodo che diverrà la nuova testa della lista | |
delete q; | Cancellazione del nodo da eliminare |
Istruzione | Effetto | Descrizione |
nodo *q; for ( q = p; q->prossimo != NULL; q = q->prossimo ); | 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 |
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.
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".
void inserisci_dopo (lista &L, nodo *p, int n) { nodo *q = new nodo; q->n = n; q->prossimo = p->prossimo; p->prossimo = q; }La funzione è in grado di inserire un dato in qualsiasi posizione della lista, tranne in testa, perché è necessario indicare il nodo precedente. Se si adotta la convenzione che l'inserimento in testa è indicato impostando il parametro p a NULL, allora basta gestire il caso particolare all'inizio della funzione. Si noti che la precedente funzione non prende in considerazione l'eventuale aggiornamento del campo ultimo, necessario se la posizione di inserimento è la coda della lista. Lasciamo al lettore la gestione del caso.
Supponiamo ora di voler inserire un elemento in una lista ordinata, mantenendola tale. A meno che non si debba inserire il nuovo elemento in testa alla lista (se è minore di tutti gli elementi già presenti, oppure se la lista è vuota), è necessario individuare il nodo che precede la posizione di inserimento:
void inserisci_ordinato (lista &L, int n) { if ( L.primo == NULL || L.primo->n >= n ) // Se la lista e' vuota o tutti gli elementi sono maggiori di n inserisci_dopo (L, NULL, n); // inserisci in testa alla lista else { nodo *q = L.primo; // punta al primo nodo while ( q->prossimo != NULL && q->prossimo->n < n ) // cerca il nodo che *precede* la posizione corretta per l'inserimento q = q->prossimo; inserisci_dopo (L, q, n); // inserisci dopo la posizione trovata. } }
nodo *q = L.primo; while ( q->prossimo != p ) // Cerca il nodo immediatamente precedente p q = q->prossimo; q->prossimo = p->prossimo; // Salta il nodo p delete p; // Rilascia la memoria allocata per p.Il codice precedente non tiene conto del caso particolare in cui il nodo da cancellare si trova in testa alla lista (e quindi non c'è un nodo precedente), e non aggiorna l'eventuale puntatore all'ultimo nodo. Entrambi i casi sono lasciati come esercizi.
struct nodo { nodo *precedente; int n; nodo *prossimo; };Questa stessa modifica permette, nel caso di cancellazione, di accedere agevolmente al nodo precedente quello da cancellare. Una lista munita di un sistema di puntatori in avanti e all'indietro è detta "doppiamente concatenata". Si noti inoltre che molte delle operazioni sopra definite richiedono la gestione di alcuni casi particolari. L'inserimento nella lista, ad esempio, deve trattare separatamente la testa e, talvolta, la coda. Possiamo fare sì che la testa venga gestita come qualunque altro nodo semplicemente aggiungendo un nodo supplementare, detto "dummy" (fantoccio), oppure "sentinella", senza alcun dato utile al suo interno, il cui puntatore prossimo punta alla vera testa della lista.
Per semplificare l'accesso all'ultimo nodo della lista, possiamo concatenarlo alla sentinella, che diventa quindi un indicatore di "fine della lista". Ecco come si presenta una lista circolare doppiamente concatenata:
Si noti che la sentinella deve sempre essere presente nella lista, anche se questa è vuota. Una lista vuota è rappresentata come segue:
La sentinella è concatenata a sé stessa.