C语言 从单链表中删除一个条目

cqoc49vn  于 2023-03-28  发布在  其他
关注(0)|答案(7)|浏览(129)

所以今天我在看The mind behind Linux | Linus Torvalds,Linus在视频中发布了两段代码,它们都用于删除单链表中的某个元素。
第一个(正常的):

void remove_list_entry(linked_list* entry) {
    linked_list* prev = NULL;
    linked_list* walk = head;
    while (walk != entry) {
        prev = walk;
        walk = walk->next;
    }
    if (!prev) {
        head = entry->next;
    } else {
        prev->next = entry->next;
    }
}

更好的一个:

void remove_list_entry(linked_list* entry) {
    // The "indirect" pointer points to the
    // *address* of the thing we'll update
    linked_list** indirect = &head;

    // Walk the list, looking for the thing that
    // points to the entry we want to remove
    while ((*indirect) != entry)
        indirect = &(*indirect)->next;

    // .. and just remove it
    *indirect = entry->next;
}

所以我不明白第二段代码,当*indirect = entry->next;求值时会发生什么?我不明白为什么它会导致删除某个条目。请有人解释一下,谢谢!

a64a0gku

a64a0gku1#

  • *indirect = entry->next;求值时会发生什么?我不明白为什么它会导致删除某个条目。*

我希望你对双指针有清楚的理解。
假设如下:
节点结构为

typedef struct Node {
    int data;
    struct Node *next;
} linked_list;

链表有5个节点,entry指针指向链表中的第二个节点。内存视图如下所示:

entry -+
   head                          |
      +---+     +-------+     +-------+     +-------+     +-------+     +--------+
      |   |---->| 1 |   |---->| 2 |   |---->| 3 |   |---->| 4 |   |---->| 5 |NULL|
      +---+     +-------+     +-------+     +-------+     +-------+     +--------+

本声明:

linked_list** indirect = &head;

将使indirect指针指向head

entry -+
  head                          |
     +---+     +-------+     +-------+     +-------+     +-------+     +--------+
     |   |---->| 1 |   |---->| 2 |   |---->| 3 |   |---->| 4 |   |---->| 5 |NULL|
     +---+     +-------+     +-------+     +-------+     +-------+     +--------+
       ^
       |
     +---+
     |   |
     +---+
   indirect

while循环

while ((*indirect) != entry)

*indirect将给予第一个节点的地址,因为head指向第一个节点,而entry指向第二个节点,所以循环条件的计算结果为true,并将执行以下代码:

indirect = &(*indirect)->next;

这将使indirect指针指向第一个节点的next指针。内存视图:

entry -+
   head                          |
      +---+     +-------+     +-------+     +-------+     +-------+     +--------+
      |   |---->| 1 |   |---->| 2 |   |---->| 3 |   |---->| 4 |   |---->| 5 |NULL|
      +---+     +-------+     +-------+     +-------+     +-------+     +--------+
                      ^
                      |
                    +---+
                    |   |
                    +---+
                  indirect

现在while循环条件将被评估。因为indirect指针现在指向第一个节点的next*indirect将给予第二个节点的地址,并且由于entry指向第二个节点,因此循环条件评估为false并且循环退出。
以下代码将立即执行:

*indirect = entry->next;

*indirect解引用到第一个节点的next,现在它被分配到entry指针所指向的节点的next。内存视图:

entry -+
   head                          |
      +---+     +-------+     +-------+     +-------+     +-------+     +--------+
      |   |---->| 1 |   |--   | 2 |   |---->| 3 |   |---->| 4 |   |---->| 5 |NULL|
      +---+     +-------+  \  +-------+     +-------+     +-------+     +--------+
                  *indirect \              /
                             +------------+

现在第一个节点的next指向列表中的第三个节点,这样第二个节点就从列表中删除了。
希望这能消除你所有的疑虑。

编辑

  • 大卫在评论中建议添加一些细节-为什么&(*indirect)->next中需要(..)括号?*

indirect的类型是linked_list **,这意味着它可以保存linked_list *类型的指针的地址。*indirect将给予linked_list *类型的指针,->next将给出其next指针。
但是我们不能写*indirect->next,因为运算符->的优先级高于一元运算符*。因此,*indirect->next将被解释为*(indirect->next),这在语法上是错误的,因为indirect是指向指针的指针。因此我们需要()围绕*indirect
此外,&(*indirect)->next将被解释为&((*indirect)->next),这是next指针的地址。

**1)**如果您不知道双指针是如何工作的,请检查以下内容:

让我们举一个例子:

#include <stdio.h>

int main() {
        int a=1, b=2;
        int *p = &a;
        int **pp = &p;

        printf ("1. p : %p\n", (void*)p);
        printf ("1. pp : %p\n", (void*)pp);
        printf ("1. *p : %d\n", *p);
        printf ("1. *pp : %d\n", **pp);

        *pp = &b;  // this will change the address to which pointer p pointing to
        printf ("2. p : %p\n", (void*)p);
        printf ("2. pp : %p\n", (void*)pp);
        printf ("2. *p : %d\n", *p);
        printf ("2. *pp : %d\n", **pp);

        return 0;
}

在上面的代码中,在这个语句-*pp = &b;中,你可以看到,在不直接访问指针p的情况下,我们可以使用双指针pp来改变它所指向的地址,双指针pp指向指针p,因为解引用双指针pp将给予指针p
其输出:

1. p : 0x7ffeedf75a38
1. pp : 0x7ffeedf75a28
1. *p : 1
1. *pp : 1
2. p : 0x7ffeedf75a34   <=========== changed 
2. pp : 0x7ffeedf75a28
2. *p : 2
2. *pp : 2

内存中视图应该是这样的:

//Below in the picture
//100 represents 0x7ffeedf75a38 address
//200 represents 0x7ffeedf75a34 address
//300 represents 0x7ffeedf75a28 address

int *p = &a
      p         a
      +---+     +---+
      |100|---->| 1 |
      +---+     +---+

        int **pp = &p;

      pp        p         a
      +---+     +---+     +---+
      |300|---->|100|---->| 1 |
      +---+     +---+     +---+

*pp = &b;

      pp        p         b
      +---+     +---+     +---+
      |300|---->|200|---->| 2 |
      +---+     +---+     +---+
                ^^^^^     ^^^^^
nwlqm0z1

nwlqm0z12#

该条目并不是真的被“删除”,它只是不再在列表中。如果这是你的链:

A --> B --> C --> D --> E --> ■

如果你想删除C,你实际上只是链接了它,它仍然在内存中,但不再能从你的数据结构中访问。

C 
A --> B --------> D --> E --> ■

最后一行将B的next指针设置为D而不是C。

nnt7mjpx

nnt7mjpx3#

与第一个例子不同,第二个例子循环遍历列表中的条目,而是遍历指向列表中条目的 * 指针。这使得第二个例子可以用你所问的语句得出简单的结论,这在英语中是“设置用于指向我想从列表中删除的条目的指针,以便它现在指向该条目所指向的任何内容”。换句话说,它使指向要删除的条目的指针指向 * 过去 * 要删除的条目。
第一个例子必须有一个特殊的方法来处理你想要删除的条目是列表中的第一个条目的唯一情况。因为第二个例子循环通过指针(以&head开始),所以它没有特殊情况。

n6lpvg4x

n6lpvg4x4#

  • indirect = entry-〉next;这只是将它移动到下一个节点你需要删除条目1所以你必须在条目节点之前指向条目节点的下一个所以你的循环应该在条目之前停止while((*indirect)-〉next!= entry)indirect = &(*indirect)-〉next

(*indirect)-〉Next =entry-〉next
希望对你有帮助

mgdq6dx1

mgdq6dx15#

这个例子是一个很好的操作链表结构的方法,也是一个很好的展示指针功能的方法。
当你从一个单链表中删除一个元素时,你必须让前一个节点指向下一个节点,绕过你要删除的节点。例如,如果你要删除节点E,那么无论是什么列表指针指向E,你都必须让它指向E.next所指向的。
现在,问题是“无论是什么列表指针,它都曾经指向E”有两种可能性。大多数情况下,它是一些前一个节点的next指针指向E。但是如果E恰好是列表中的第一个节点,这意味着列表中没有前一个节点,它是指向E的顶级列表指针-在Linus的示例中,这是变量head
所以在Linus的第一个“正常”示例中,有一个if语句。如果有前一个节点,代码将prev->next设置为指向下一个节点。但是如果没有前一个节点,这意味着它将删除列表头部的节点,因此它将head设置为指向下一个节点。
虽然这不是世界末日,但它是两个独立的赋值和一个if条件,以照顾我们在英语中认为的“无论是什么列表指针,它曾经指向E”。一个好的程序员的关键标志之一是能够准确无误地嗅出不必要的冗余,并用更干净的东西替换它。
在这个例子中,关键的一点是我们想要更新的两个东西,即headprev->next,都是指向列表节点linked_list *的指针,而指针最擅长的事情之一就是指向我们关心的东西,即使这个东西根据情况可能是几个不同的东西中的一个。
因为我们关心的东西是指向linked_list的指针,所以指向我们关心的东西的指针将是指向linked_listlinked_list **的指针。
这正是Linus的“更好”示例中的变量indirect,它是一个指向“任何用于指向E的列表指针”的指针。(或者,在实际代码中,不是E,而是要删除的传入的entry)最初,indirect指针指向head,但后来,在我们开始遍历列表以找到要删除的节点之后,它指向节点(前一个节点)的next指针,该指针指向我们正在查看的节点。因此,在任何情况下,*indirect(即indirect指向的指针)都是我们想要更新的指针。这正是魔术线

*indirect = entry->next;

在“更好”的例子中。
另一件需要注意的事情(尽管这可能会使代码在开始时更加晦涩)是indirect变量也取代了第一个例子中使用的walk变量。也就是说,在第一个例子使用walk的地方,“更好”的例子使用*indirect。但这是有意义的:我们需要遍历列表中的所有节点,寻找entry。因此我们需要一个指针来遍历这些节点-这就是第一个例子中walk变量所做的。但是当我们找到我们想要删除的条目时,指向该条目的指针将是“用于指向E的任何列表指针”-并且它将是要更新的指针。在第一示例中,我们不能将walk设置为prev->next-这只会更新本地walk变量,而不是head或列表中的next指针之一。但是通过使用指针indirect(间接)遍历列表,情况总是*indirect-即,由indirect-指向的指针是指向我们正在查看的节点的 * 原始 * 指针(而不是walk中的副本),这意味着我们可以通过说*indirect = entry->next来 * 有效地更新它。

6qfn3psc

6qfn3psc6#

如果你重写一下,这会容易理解得多

indirect = &(*indirect)->next;

作为

Indirect = &((*indirect)->next);

while循环会给予我们一个next指针的地址,这个指针属于某个节点,而这个节点的next指针指向这个条目,所以最后一个语句实际上是改变了next指针的值,这样它就不再指向这个条目了,在特殊情况下,当条目是head的时候,while循环将被跳过,并且最后一行改变头指针的值,并使其指向条目的下一个节点。

kfgdxczn

kfgdxczn7#

一个注解是,这两种解决方案都假设头指针不指向NULL,并且要删除的条目确实存在于列表中。

相关问题