Der richtige Weg, um ein Element aus einer verknüpften Liste zu entfernen

9

In diesem Slashdot-Interview wird Linus Torvalds mit folgenden Worten zitiert:

Ich habe zu viele Leute gesehen, die einen einfach verknüpften Listeneintrag löschen, indem sie den Eintrag "prev" verfolgen und dann den Eintrag löschen, indem sie so etwas tun

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

und wenn ich solchen Code sehe, gehe ich einfach zu "Diese Person versteht keine Zeiger". Und es ist leider ziemlich häufig.

Personen, die Zeiger verstehen, verwenden einfach einen "Zeiger auf den Eintragszeiger" und initialisieren diesen mit der Adresse des Listenkopfs. Und dann, wenn sie die Liste durchlaufen, können sie den Eintrag entfernen, ohne Bedingungen zu verwenden, indem sie einfach "* pp = entry-> next" ausführen.

Als PHP-Entwickler habe ich seit Einführung in C an der Universität vor einem Jahrzehnt keine Hinweise mehr berührt . Ich bin jedoch der Meinung, dass dies eine Art Situation ist, mit der ich zumindest vertraut sein sollte. Worüber spricht Linus? Um ehrlich zu sein, wenn ich gebeten würde, eine verknüpfte Liste zu implementieren und ein Element zu entfernen, würde ich den oben genannten "falschen" Weg einschlagen. Was muss ich wissen, um zu codieren, wie Linus am besten sagt?

Ich frage hier eher als bei Stack Overflow, da ich im Produktionscode eigentlich kein Problem damit habe.

dotancohen
quelle
1
Was er sagt ist, dass, wenn Sie die Position des prevKnotens speichern müssen, anstatt den gesamten Knoten zu speichern, Sie einfach die Position von speichern können prev.next, da dies das einzige ist, woran Sie interessiert sind. Ein Zeiger auf einen Zeiger. Und wenn Sie das tun, vermeiden Sie das Dumme if, da Sie jetzt nicht den unangenehmen Fall haben list_head, ein Zeiger von außerhalb eines Knotens zu sein. Der Zeiger auf den Kopf der Liste ist dann semantisch der gleiche wie der Zeiger auf den nächsten Knoten.
Ordentlicher
@Ordous: Ich verstehe, danke. Warum ein Kommentar? Das ist eine präzise, ​​klare und aufschlussreiche Antwort.
Dotancohen
@Ordous Alles, was an diesem Code-Snippet beteiligt ist, ist ein Zeiger, daher kann sein Punkt nichts mit dem Speichern des gesamten Knotens oder dem Speichern eines Zeigers darauf zu tun haben.
Doval

Antworten:

9

Verwenden meiner L331 MS Paint-Kenntnisse:

Geben Sie hier die Bildbeschreibung ein

Die ursprüngliche Lösung besteht darin, auf Knoten über zu zeigen curr. In diesem Fall prüfen Sie, ob der nächste nachfolgende Knoten currden Löschwert hat, und setzen in diesem Fall den currKnotenzeiger zurück next. Das Problem ist, dass es keinen Knoten gibt, der auf den Kopf der Liste zeigt. Das heißt, es muss einen Sonderfall geben, um dies zu überprüfen.

Was Linus (wahrscheinlich) stattdessen vorschlägt, ist nicht, den Zeiger auf den aktuell untersuchten Knoten zu speichern, sondern den Zeiger auf den Zeiger auf den aktuellen Knoten (beschriftet pp). Die Operation ist dieselbe - wenn der ppZeiger auf einen Knoten mit dem richtigen Wert zeigt, setzen Sie den ppZeiger zurück.

Der Unterschied steht ganz am Anfang der Liste. Während es keinen Knoten gibt, der auf den Kopf der Liste zeigt, gibt es tatsächlich einen Zeiger auf den Kopf der Liste. Und es ist genauso ein Zeiger auf einen Knoten wie ein Zeiger eines anderen Knotens next. Daher ist für den Beginn der Liste keine spezielle Klausel erforderlich.

Ordentlich
quelle
Ah, ich verstehe jetzt ... du lernst jeden Tag etwas Neues.
Lawrence Aiello
1
Ich denke, Sie beschreiben die Dinge richtig, aber ich würde vorschlagen, dass die richtige Lösung darin besteht, list_headauf etwas mit einem nextKnoten zu zeigen, der auf das erste reale Datenelement zeigt (und prevauf dasselbe Dummy-Objekt initialisiert hat). Ich mag die Idee nicht, prevauf etwas anderes zu verweisen, da solche Tricks undefiniertes Verhalten über Aliasing einführen und Code unnötig empfindlich für das Strukturlayout machen können.
Supercat
@supercat Das ist genau der Punkt. Anstatt prevauf Knoten zu zeigen, zeigt es auf Zeiger. Es zeigt immer auf etwas vom gleichen Typ, nämlich einen Zeiger auf einen Knoten. Ihr Vorschlag ist im Wesentlichen der gleiche - prevzeigen Sie auf etwas "mit einem nextKnoten". Wenn Sie die Shell verwerfen, erhalten Sie nur den Anfangszeiger list_head. Oder mit anderen Worten - etwas, das nur durch einen Zeiger auf den nächsten Knoten definiert wird, entspricht semantisch einem Zeiger auf einen Knoten.
Ordentlicher
@Ordous: Das macht Sinn, obwohl es das voraussetzt list_headund nextdie gleiche "Art" von Zeigern enthält. Vielleicht kein Problem in C, aber vielleicht problematisch in C ++.
Supercat
@supercat Ich habe immer angenommen, dass dies die "kanonische" Darstellung einer verknüpften Liste ist, sprachunabhängig. Aber ich bin nicht kompetent genug, um zu beurteilen, ob es wirklich einen Unterschied zwischen C und C ++ macht und welche Standardimplementierungen es gibt.
Ordentlicher
11

Geben Sie hier die Bildbeschreibung ein Geben Sie hier die Bildbeschreibung ein Geben Sie hier die Bildbeschreibung ein Geben Sie hier die Bildbeschreibung ein Geben Sie hier die Bildbeschreibung ein

Codebeispiel

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

Wenn wir eine Logik benötigen, um den Knoten zu zerstören, können wir am Ende einfach eine Codezeile hinzufügen:

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

quelle