使用指针从单链表中删除项目

Posted

技术标签:

【中文标题】使用指针从单链表中删除项目【英文标题】:Using pointers to remove item from singly-linked list 【发布时间】:2012-10-06 13:34:45 【问题描述】:

在最近的Slashdot InterviewLinus Torvalds 中举了一个例子,说明有些人使用指针的方式表明他们并不真正了解如何正确使用它们。

不幸的是,由于我是他所说的人之一,所以我也无法理解他的例子:

我见过太多人通过跟踪“prev”条目来删除单链表条目,然后删除条目,这样做 像

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

每当我看到这样的代码时,我都会说“这个人不 理解指针”。可悲的是,这很普遍。那些 理解指针只需使用“指向入口指针的指针”,并且 用 list_head 的地址初始化它。然后就像他们 遍历列表,他们可以删除条目而不使用任何 条件,只需做

*pp = entry->next

有人可以提供更多解释为什么这种方法更好,以及它如何在没有条件语句的情况下工作?

【问题讨论】:

对于Linus来说,“这个人不懂指针”似乎意味着“这个人不像我那样写代码”…… 【参考方案1】:

一开始,你做

pp = &list_head;

并且,当您遍历列表时,您将这个“光标”前进

pp = &(*pp)->next;

这样,您始终可以跟踪“您来自”的位置,并可以修改那里的指针。

所以当你找到要删除的条目时,你可以这样做

*pp = entry->next

这样,您可以处理 Afaq 在另一个答案中提到的所有 3 个案例,从而有效地消除了 NULLprev 的检查。

【讨论】:

这里真的需要 &* 吗?似乎有点多余。 @FUZxxl 是必需的,因为pp 是指向节点指针的指针。所以我们首先要取消引用它,然后以通常的方式访问next,然后再次获取它的地址。 如果您删除列表的尾节点并且需要跟踪tail,您将如何使用此pp @glglgl,这不是(*pp)->下一个足以让地址存储在pp,为什么&? @ZianLai 不。你希望pp 指向不同的地方,而不是更改pp 指向的数据。【参考方案2】:

如果您喜欢从示例中学习,我准备了一个。假设我们有以下单链表:

表示如下(点击放大):

我们要删除带有value = 8 的节点。

代码

这是执行此操作的简单代码:

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>

struct node_t 
    int value;
    node_t *next;
;

node_t* create_list() 
    int test_values[] =  28, 1, 8, 70, 56 ;
    node_t *new_node, *head = NULL;
    int i;

    for (i = 0; i < 5; i++) 
        new_node = malloc(sizeof(struct node_t));
        assert(new_node);
        new_node->value = test_values[i];
        new_node->next = head;
        head = new_node;
    

    return head;


void print_list(const node_t *head) 
    for (; head; head = head->next)
        printf("%d ", head->value);
    printf("\n");


void destroy_list(node_t **head) 
    node_t *next;

    while (*head) 
        next = (*head)->next;
        free(*head);
        *head = next;
    


void remove_from_list(int val, node_t **head) 
    node_t *del, **p = head;

    while (*p && (**p).value != val)
        p = &(*p)->next;  // alternatively: p = &(**p).next

    if (p)   // non-empty list and value was found
        del = *p;
        *p = del->next;
        del->next = NULL;  // not necessary in this case
        free(del);
    


int main(int argc, char **argv) 
    node_t *head;

    head = create_list();
    print_list(head);

    remove_from_list(8, &head);
    print_list(head);

    destroy_list(&head);
    assert (head == NULL);

    return EXIT_SUCCESS;

如果你编译并运行这段代码,你会得到:

56 70 8 1 28 
56 70 1 28

代码说明

让我们创建**p 'double' 指向*head 指针:

现在让我们分析void remove_from_list(int val, node_t **head) 的工作原理。只要*p &amp;&amp; (**p).value != val,它就会遍历head 指向的列表。

在此示例中,给定列表包含我们要删除的value(即8)。在while (*p &amp;&amp; (**p).value != val) 循环的第二次迭代后,(**p).value 变为 8,因此我们停止迭代。

请注意,*p 指向node_t 内的变量node_t *next,即我们要删除的node_t(即**p)之前。这一点至关重要,因为它允许我们更改要删除的 node_t 前面的 node_t*next 指针,从而有效地将其从列表中删除。

现在让我们将要删除的元素的地址 (del-&gt;value == 8) 分配给 *del 指针。

我们需要修复*p 指针,使**p 指向一个元素之后 *del 元素我们要删除:

在上面的代码中,我们调用free(del),因此没有必要将del-&gt;next设置为NULL,但是如果我们想从列表中返回指向“分离”元素的指针,而不是完全删除它,我们将设置del-&gt;next = NULL:

【讨论】:

【参考方案3】:

删除节点后重新连接列表更有趣。让我们考虑至少 3 种情况:

1.从头删除一个节点。

2.从中间移除一个节点。

3.从末端移除一个节点。

从头删除

当删除列表开头的节点时,没有要执行的节点重新链接,因为第一个节点没有前面的节点。例如,删除节点:

link
 |
 v
---------     ---------     ---------
| a | --+---> | b | --+---> | c | 0 |
---------     ---------     ---------

但是,我们必须将指针固定到列表的开头:

link
 |
 +-------------+
               |
               v
---------     ---------     ---------
| a | --+---> | b | --+---> | c | 0 |
---------     ---------     ---------

从中间移除

从中间移除一个节点需要前面的节点跳过被移除的节点。比如用b去掉节点:

link
 |
 v
---------     ---------     ---------
| a | --+--+  | b | --+---> | c | 0 |
---------  |  ---------     ---------
           |                ^
           +----------------+

这意味着我们需要某种方式来引用我们要删除的节点之前的节点。

从末尾删除

从末尾删除一个节点要求前面的节点成为列表的新末尾(即,在它之后没有指向任何内容)。例如用 c: 删除节点:

link
 |
 v
---------     ---------     ---------
| a | --+---> | b | 0 |     | c | 0 |
---------     ---------     ---------

请注意,最后两种情况(中间和结束)可以通过说“要删除的节点之前的节点必须指向要删除的节点所在的位置。”

【讨论】:

【参考方案4】:

在第一种方法中,您通过从列表中取消链接删除一个节点。

在第二种方法中,您将要删除的节点替换为下一个节点。

显然,第二种方法以一种优雅的方式简化了代码。当然,第二种方法需要更好地理解链表和底层计算模型。

注意:这是一个非常相关但略有不同的编码问题。有利于测试一个人的理解: https://leetcode.com/problems/delete-node-in-a-linked-list/

【讨论】:

【参考方案5】:

我更喜欢虚拟节点方法,一个示例布局:

|Dummy|->|node1|->|node2|->|node3|->|node4|->|node5|->NULL
                     ^        ^
                     |        |
                    curr   curr->next // << toDel

然后,你遍历到要删除的节点(toDel = curr>next)

tmp = curr->next;
curr->next = curr->next->next;
free(tmp);

这样,您不需要检查它是否是第一个元素,因为第一个元素始终是 Dummy 并且永远不会被删除。

【讨论】:

curr-&gt;next-next 是错字吗? @MaxHeiber 好像是这样。我修正了错字。【参考方案6】:

这是我的看法,我发现这样更容易理解。

使用指向指针的指针删除链表中节点的示例。

    struct node 
        int value;
        struct node *next;
    ;

    void delete_from_list(struct node **list, int n)
    
        struct node *entry = *list;

        while (entry && entry->value != n) 
            // store the address of current->next value (1)
            list = &entry->next;
            // list now stores the address of previous->next value
            entry = entry->next;
        
        if (entry) 
            // here we change the previous->next value
            *list = entry->next;
            free(entry);
        
    

假设我们用这些值创建一个列表:

*node   value   next
----------------------------------------
a       1       null
b       2       a
c       3       b
d       4       c
e       5       d

如果我们要删除值为3的节点:

entry = e

while (entry && entry->value != 3) iterations:

    e->value != 3
        list = &e->next
        entry = d

    d->value != 3
        list = &d->next
        entry = c

    c->value == 3
        STOP

if (entry)
        d->next = b         (what was 'list' is dereferenced)
        free(entry)

if (entry) 之后我们有:

    d->next = b

所以列表变成:

*node   value   next
----------------------------------------
a       1       null
b       2       a
c       3       b
d       4       b
e       5       d

最后:

    free(entry)

列表变成:

*node   value   next
----------------------------------------
a       1       null
b       2       a
d       4       b
e       5       d

如果我们想删除第一个节点,它仍然可以工作,因为最初

*list == entry

因此有:

*list = entry->next;

*list 将指向第二个元素。


(1) 注意说:

list = &entry->next;

同说:

list = &(entry->next);

【讨论】:

【参考方案7】:

这是一个完整的代码示例,使用函数调用删除匹配的元素:

rem() 删除匹配的元素,使用 prev

rem2() 删除匹配元素,使用指针到指针

// code.c

#include <stdio.h>
#include <stdlib.h>


typedef struct list_entry 
    int val;
    struct list_entry *next;
 list_entry;


list_entry *new_node(list_entry *curr, int val)

    list_entry *new_n = malloc(sizeof(list_entry));
    if (new_n == NULL) 
        fputs("Error in malloc\n", stderr);
        exit(1);
    
    new_n->val  = val;
    new_n->next = NULL;

    if (curr) 
        curr->next = new_n;
    
    return new_n;



#define ARR_LEN(arr) (sizeof(arr)/sizeof((arr)[0]))

#define     CREATE_LIST(arr) create_list((arr), ARR_LEN(arr))

list_entry *create_list(const int arr[], size_t len)

    if (len == 0) 
        return NULL;
    

    list_entry *node = NULL;
    list_entry *head = node = new_node(node, arr[0]);
    for (size_t i = 1; i < len; ++i) 
        node = new_node(node, arr[i]);
    
    return head;



void rem(list_entry **head_p, int match_val)
// remove and free all entries with match_val

    list_entry *prev = NULL;
    for (list_entry *entry = *head_p; entry; ) 
        if (entry->val == match_val) 
            list_entry *del_entry = entry;
            entry = entry->next;
            if (prev) 
                prev->next = entry;
             else 
                *head_p    = entry;
            
            free(del_entry);
         else 
            prev = entry;
            entry = entry->next;
        
    



void rem2(list_entry **pp, int match_val)
// remove and free all entries with match_val

    list_entry *entry;
    while ((entry = *pp))  // assignment, not equality
        if (entry->val == match_val) 
            *pp =   entry->next;
            free(entry);
         else 
            pp  =  &entry->next;
        
    



void print_and_free_list(list_entry *entry)

    list_entry *node;
    // iterate through, printing entries, and then freeing them
    for (;     entry != NULL;      node = entry, /* lag behind to free */
                                   entry = entry->next,
                                   free(node))           
        printf("%d ", entry->val);
    
    putchar('\n');



#define CREATELIST_REMOVEMATCHELEMS_PRINT(arr, match_val) createList_removeMatchElems_print((arr), ARR_LEN(arr), (match_val))

void    createList_removeMatchElems_print(const int arr[], size_t len, int match_val)

    list_entry *head = create_list(arr, len);
    rem2(&head, match_val);
    print_and_free_list(head);



int main()

    const int match_val = 2; // the value to remove
    
        const int arr[] = 0, 1, 2, 3;
        CREATELIST_REMOVEMATCHELEMS_PRINT(arr, match_val);
    
    
        const int arr[] = 0, 2, 2, 3;
        CREATELIST_REMOVEMATCHELEMS_PRINT(arr, match_val);
    
    
        const int arr[] = 2, 7, 8, 2;
        CREATELIST_REMOVEMATCHELEMS_PRINT(arr, match_val);
    
    
        const int arr[] = 2, 2, 3, 3;
        CREATELIST_REMOVEMATCHELEMS_PRINT(arr, match_val);
    
    
        const int arr[] = 0, 0, 2, 2;
        CREATELIST_REMOVEMATCHELEMS_PRINT(arr, match_val);
    
    
        const int arr[] = 2, 2, 2, 2;
        CREATELIST_REMOVEMATCHELEMS_PRINT(arr, match_val);
    
    
        const int arr[] = ;
        CREATELIST_REMOVEMATCHELEMS_PRINT(arr, match_val);
    

    return 0;

在此处查看实际代码:

gcc https://wandbox.org/permlink/LxgMddqCZWyj7lMI 叮当https://wandbox.org/permlink/5aEkxh24sGfAecwF

如果像这样编译和使用 valgrind(内存泄漏检查器):gcc -std=c11 -Wall -Wextra -Werror -o go code.c &amp;&amp; valgrind ./go 我们看到一切都很好。

【讨论】:

void rem2(list_entry **pp, int match_val) 的完美实现它解决了 Linus Torvalds 对许多开发人员没有很好地理解指针的担忧,特别是指针指针的微妙之处。他用它作为一个例子,许多人不知道如何只使用两个分支条件删除链接的多个元素,因为它需要使用指向指针的指针。【参考方案8】:

@glglgl:

我写了一个简单的例子。希望你能看看它为什么有效。 在函数void delete_node(LinkedList *list, void *data) 中,我使用*pp = (*pp)-&gt;next;,它可以工作。老实说,我不明白它为什么会起作用。我什至画了指针图,但还是不明白。希望你能澄清一下。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct _employee 
    char name[32];
    unsigned char age;
 Employee;

int compare_employee(Employee *e1, Employee *e2)

    return strcmp(e1->name, e2->name);

typedef int (*COMPARE)(void *, void *);

void display_employee(Employee *e)

    printf("%s\t%d\n", e->name, e->age);

typedef void (*DISPLAY)(void *);

typedef struct _node 
    void *data;
    struct _node *next;
 NODE;

typedef struct _linkedlist 
    NODE *head;
    NODE *tail;
    NODE *current;
 LinkedList;

void init_list(LinkedList *list)

    list->head = NULL;
    list->tail = NULL;
    list->current = NULL;


void add_head(LinkedList *list, void *data)

    NODE *node = malloc(sizeof(NODE));
    node->data = data;
    if (list->head == NULL) 
        list->tail = node;
        node->next = NULL;
     else 
        node->next = list->head;
    
    list->head = node;


void add_tail(LinkedList *list, void *data)

    NODE *node = malloc(sizeof(NODE));
    node->data = data;
    node->next = NULL;
    if (list->head == NULL) 
        list->head = node;
     else 
        list->tail->next = node;
    
    list->tail = node;


NODE *get_node(LinkedList *list, COMPARE compare, void *data)

    NODE *n = list->head;
    while (n != NULL) 
        if (compare(n->data, data) == 0) 
            return n;
        
        n = n->next;
    
    return NULL;


void display_list(LinkedList *list, DISPLAY display)

    printf("Linked List\n");
    NODE *current = list->head;
    while (current != NULL) 
        display(current->data);
        current = current->next;
    


void delete_node(LinkedList *list, void *data)

    /* Linus says who use this block of code doesn't understand pointer.    
    NODE *prev = NULL;
    NODE *walk = list->head;

    while (((Employee *)walk->data)->age != ((Employee *)data)->age) 
        prev = walk;
        walk = walk->next;
    

    if (!prev)
        list->head = walk->next;
    else
        prev->next = walk->next; */

    NODE **pp = &list->head;

    while (((Employee *)(*pp)->data)->age != ((Employee *)data)->age) 
        pp = &(*pp)->next;
    

    *pp = (*pp)->next;


int main () 

    LinkedList list;

    init_list(&list);

    Employee *samuel = malloc(sizeof(Employee));
    strcpy(samuel->name, "Samuel");
    samuel->age = 23;

    Employee *sally = malloc(sizeof(Employee));
    strcpy(sally->name, "Sally");
    sally->age = 19;

    Employee *susan = malloc(sizeof(Employee));
    strcpy(susan->name, "Susan");
    susan->age = 14;

    Employee *jessie = malloc(sizeof(Employee));
    strcpy(jessie->name, "Jessie");
    jessie->age = 18;

    add_head(&list, samuel);
    add_head(&list, sally);
    add_head(&list, susan);

    add_tail(&list, jessie);

    display_list(&list, (DISPLAY) display_employee);

    NODE *n = get_node(&list, (COMPARE) compare_employee, sally);
    printf("name is %s, age is %d.\n",
            ((Employee *)n->data)->name, ((Employee *)n->data)->age);
    printf("\n");
    
    delete_node(&list, samuel);
    display_list(&list, (DISPLAY) display_employee);

    return 0;

输出:

Linked List
Susan   14
Sally   19
Samuel  23
Jessie  18
name is Sally, age is 19.

Linked List
Susan   14
Sally   19
Jessie  18

【讨论】:

以上是关于使用指针从单链表中删除项目的主要内容,如果未能解决你的问题,请参考以下文章

C语言使用二级指针借助递归工作栈删除单链表中所有值为x的元素

删除单链表中的循环[重复]

单链表和双链表中节点删除的时间复杂度

单链表实现“插入”和“删除”操作

单链表节点类型与单链表数据类型的区别

009实现一个算法来删除单链表中的一个结点,仅仅给出指向那个结点的指针(keep it up)