Il modo giusto per rimuovere un elemento da un elenco collegato


10

In questa intervista di Slashdot, Linus Torvalds è citato dicendo:

Ho visto troppe persone che eliminano una voce dell'elenco collegata singolarmente tenendo traccia della voce "precedente" e quindi per eliminare la voce, facendo qualcosa di simile

if (prev)
  prev-> next = entry-> next;
else
  list_head = entry-> next;

e ogni volta che vedo codice del genere, vado semplicemente "Questa persona non capisce i puntatori". Ed è purtroppo abbastanza comune.

Le persone che comprendono i puntatori usano semplicemente un "puntatore al puntatore voce" e lo inizializzano con l'indirizzo di list_head. E poi mentre attraversano l'elenco, possono rimuovere la voce senza usare alcun condizionale, semplicemente facendo un "* pp = entry-> next".

Come sviluppatore di PHP non ho toccato i puntatori dall'Introduzione alla C all'università un decennio fa. Tuttavia, ritengo che questo sia un tipo di situazione che dovrei almeno conoscere. Di cosa sta parlando Linus? Ad essere onesti, se mi chiedessero di implementare un elenco collegato e di rimuovere un elemento, il modo "sbagliato" sopra riportato è il modo in cui lo farei. Cosa devo sapere per codificare come Linus dice meglio?

Lo sto chiedendo qui piuttosto che su Stack Overflow poiché in realtà non ho problemi con questo nel codice di produzione.


1
Quello che sta dicendo è che quando è necessario memorizzare la posizione di prev, invece di memorizzare l'intero nodo, è possibile memorizzare la posizione di prev.next, dal momento che "l'unica cosa che ti interessa. Un puntatore a un puntatore. E se lo fai, eviti lo sciocco if, dal momento che ora non hai il caso imbarazzante di list_headessere un puntatore dall'esterno di un nodo. Il puntatore all'inizio della lista è quindi semanticamente uguale al puntatore al nodo successivo.
Ordous,

@Ordous: capisco, grazie. Perché un commento? Questa è una risposta concisa, chiara e illuminante.
dotancohen,

@Ordous Tutto ciò che è coinvolto nello snippet di codice è un puntatore, quindi il suo punto non può avere nulla a che fare con la memorizzazione dell'intero nodo rispetto alla memorizzazione di un puntatore ad esso.
Doval,

Risposte:


9

Usando le mie abilità con L331 MS Paint:

inserisci qui la descrizione dell'immagine

La soluzione originale è quella di puntare a nodi tramite curr. In tal caso, controlla se il nodo successivo dopo currha il valore di eliminazione e, in tal caso, reimposta il puntatore del currnodo next. Il problema è che non esiste un nodo che punta all'inizio della lista. Ciò significa che deve esserci un caso speciale per verificarlo.

Quello che Linus (probabilmente) propone invece non è di salvare il puntatore sul nodo esaminato corrente, ma piuttosto il puntatore al puntatore sul nodo corrente (etichettato pp). L'operazione è la stessa: se il pppuntatore punta a un nodo con il valore corretto, si reimposta il pppuntatore.

La differenza arriva all'inizio della lista. Sebbene non vi sia alcun nodo che punti in testa alla lista, in realtà c'è un puntatore alla testa della lista. Ed è lo stesso un puntatore a un nodo, proprio come un altro nextpuntatore a nodi . Quindi non è necessaria una clausola speciale per l'inizio dell'elenco.


Ah vedo ora .... impari qualcosa di nuovo ogni giorno.
Lawrence Aiello,

1
Penso che descrivi le cose correttamente, ma suggerirei che la soluzione corretta sia quella di list_headpuntare a qualcosa con un nextnodo che punti al primo elemento dati reale (e abbia previnizializzato sullo stesso oggetto fittizio). Non mi piace l'idea di previndicare qualcosa di diverso, dal momento che tali trucchi possono introdurre un comportamento indefinito tramite aliasing e rendere il codice inutilmente sensibile al layout della struttura.
supercat,

@supercat Questo è esattamente il punto. Invece di previndicare i nodi, punta ai puntatori. Punta sempre a qualcosa dello stesso tipo, vale a dire un puntatore a un nodo. Il tuo suggerimento è essenzialmente lo stesso: previndica qualcosa "con un nextnodo". Se scarti la shell otterrai solo il list_headpuntatore iniziale . O in altre parole - qualcosa che viene definito solo avendo un puntatore al nodo successivo, è semanticamente equivalente a un puntatore a un nodo.
Ordous,

@Ordous: Questo ha senso, anche se presuppone questo list_heade nextconterrà lo stesso "tipo" di puntatore. Non è un problema in C, forse, ma forse problematico in C ++.
supercat,

@supercat Ho sempre pensato che fosse la rappresentazione "canonica" di un elenco collegato, indipendente dalla lingua. Ma non sono abbastanza competente per giudicare se fa davvero la differenza tra C e C ++ e quali sono le implementazioni standard lì.
Ordous,

11

inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine

Esempio di codice

// ------------------------------------------------------------------
// Start by pointing to the head pointer.
// ------------------------------------------------------------------
//    (next_ptr)
//         |
//         v
// [head]----->[..]----->[..]----->[..]----->[to_remove]----->[....]
Node** next_ptr = &list->head;

// ------------------------------------------------------------------
// Search the list for the matching entry.
// After searching:
// ------------------------------------------------------------------
//                                  (next_ptr)
//                                       |
//                                       v
// [head]----->[..]----->[..]----->[..]----->[to_remove]----->[next]
while (*next_ptr != to_remove) // or (*next_ptr)->val != to_remove->val
{
    Node* next_node = *next_ptr
    next_ptr = &next_node->next;
}

// ------------------------------------------------------------------
// Dereference the next pointer and set it to the next node's next
// pointer.
// ------------------------------------------------------------------
//                                           (next_ptr)
//                                                |
//                                                v
// [head]----->[..]----->[..]----->[..]---------------------->[next]
*next_ptr = to_remove->next;

Se abbiamo bisogno di una logica per distruggere il nodo, allora possiamo semplicemente aggiungere una riga di codice alla fine:

// Deallocate the node which is now stranded from the list.
free(to_remove);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.