La forma correcta de eliminar un elemento de una lista vinculada

10

En esta entrevista de Slashdot, se cita a Linus Torvalds diciendo:

He visto a demasiadas personas que eliminan una entrada de la lista enlazada individualmente haciendo un seguimiento de la entrada "anterior", y luego eliminan la entrada, haciendo algo como

if (anterior) anterior-
  > siguiente = entrada-> siguiente;
else
  list_head = entry-> next;

y cada vez que veo un código así, solo digo "Esta persona no entiende los punteros". Y lamentablemente es bastante común.

Las personas que entienden los punteros simplemente usan un "puntero al puntero de entrada" e inicializan eso con la dirección de list_head. Y luego, a medida que atraviesan la lista, pueden eliminar la entrada sin usar ninguna condición, simplemente haciendo un "* pp = entrada-> siguiente".

Como desarrollador de PHP, no he tocado punteros desde Introducción a C en la universidad hace una década. Sin embargo, siento que este es un tipo de situación con la que al menos debería estar familiarizado. ¿De qué está hablando Linus? Para ser honesto, si me pidieran implementar una lista vinculada y eliminar un elemento, la forma "incorrecta" anterior es la forma en que lo haría. ¿Qué necesito saber para codificar como Linus dice mejor?

Estoy preguntando aquí en lugar de sobre Stack Overflow, ya que en realidad no tengo problemas con esto en el código de producción.

dotancohen
fuente
1
Lo que dice es que cuando necesita almacenar la ubicación del prev, en lugar de almacenar el nodo completo, puede almacenar la ubicación de prev.next, ya que eso es lo único que le interesa. Un puntero a un puntero. Y si haces eso, evitas lo tonto if, ya que ahora no tienes el extraño caso de list_headser un puntero desde fuera de un nodo. El puntero al encabezado de la lista es semánticamente igual que el puntero al siguiente nodo.
Ordous
@ Ordous: Ya veo, gracias. ¿Por qué un comentario? Esa es una respuesta concisa, clara e iluminadora.
dotancohen
@Ordous Todo lo que está involucrado en ese fragmento de código es un puntero, por lo que su punto no puede tener nada que ver con almacenar el nodo completo en lugar de almacenar un puntero en él.
Doval

Respuestas:

9

Usando mis habilidades de pintura L331 MS:

ingrese la descripción de la imagen aquí

La solución original es apuntar a los nodos vía curr. En ese caso, verifica si el siguiente nodo currtiene el valor de eliminación y, de ser así, restablece el puntero del currnodo next. El problema es que no hay ningún nodo que apunte al encabezado de la lista. Eso significa que tiene que haber un caso especial para verificarlo.

Lo que propone Linus (probablemente) en su lugar no es guardar el puntero en el nodo examinado actual, sino más bien el puntero al puntero en el nodo actual (etiquetado pp). La operación es la misma: si el pppuntero apunta a un nodo con el valor correcto, reinicia el pppuntero.

La diferencia viene al comienzo de la lista. Si bien no hay ningún nodo que apunte al encabezado de la lista, de hecho, hay un puntero al encabezado de la lista. Y es lo mismo un puntero a un nodo, tal como otro nextpuntero de nodos . Por lo tanto, no hay necesidad de una cláusula especial para el comienzo de la lista.

Ordous
fuente
Ah, ya veo ... aprendes algo nuevo todos los días.
Lawrence Aiello
1
Creo que describe las cosas correctamente, pero sugeriría que la solución adecuada es list_headapuntar a algo con un nextnodo que apunte al primer elemento de datos real (y se haya previnicializado en ese mismo objeto ficticio). No me gusta la idea de prevseñalar algo de diferente tipo, ya que estos trucos pueden introducir comportamientos indefinidos mediante alias y hacer que el código sea innecesariamente sensible al diseño de la estructura.
supercat
@ Supercat Ese es exactamente el punto. En lugar de prevapuntar a Nodos, apunta a punteros. Siempre apunta a algo del mismo tipo, es decir, un puntero a un nodo. Su sugerencia es esencialmente la misma: prevapunte a algo "con un nextnodo". Si descarta el shell, solo obtiene el list_headpuntero inicial . O, en otras palabras, algo que se define solo al tener un puntero al siguiente nodo, es semánticamente equivalente a un puntero a un nodo.
Ordous
@Ordous: Eso tiene sentido, aunque presupone eso list_heady nexttendrá el mismo "tipo" de puntero. No es un problema en C, quizás, pero quizás problemático en C ++.
supercat
@supercat Siempre asumí que esa es la representación "canónica" de una lista vinculada, independiente del idioma. Pero no soy lo suficientemente competente para juzgar si realmente hace una diferencia entre C y C ++, y cuáles son las implementaciones estándar allí.
Ordous
11

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

Ejemplo de código

// ------------------------------------------------------------------
// 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;

Si necesitamos algo de lógica para destruir el nodo, entonces podemos agregar una línea de código al final:

// Deallocate the node which is now stranded from the list.
free(to_remove);

fuente