web-dev-qa-db-fra.com

Utilisation de pointeurs pour supprimer un élément de la liste liée individuellement

Dans une récente interview de Slashdot Linus Torvalds a donné un exemple de la façon dont certaines personnes utilisent les pointeurs de manière à indiquer qu’ils ne comprennent pas vraiment comment les utiliser correctement. 

Malheureusement, comme je suis l'une des personnes dont il parle, j'ai également échoué à comprendre son exemple:

J'ai vu trop de personnes qui suppriment une entrée de liste à lien unique en gardant une trace de l'entrée "prev", puis de supprimer l'entrée en faisant quelque chose comme

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

et chaque fois que je vois un code comme celui-ci, je me contente de dire "Cette personne ne comprend pas les pointeurs". Et c'est malheureusement assez commun. Les gens qui comprendre les pointeurs simplement utiliser un "pointeur sur le pointeur d’entrée", et initialisez cela avec l'adresse de la list_head. Et puis comme ils parcourir la liste, ils peuvent supprimer l’entrée sans utiliser aucun conditionnels, en faisant juste

*pp = entry->next

Quelqu'un peut-il fournir un peu plus d'explications sur les raisons pour lesquelles cette approche est meilleure et comment elle peut fonctionner sans déclaration conditionnelle?

44
codebox

Au début, vous faites

pp = &list_head;

et, en parcourant la liste, vous avancez ce "curseur" avec

pp = &(*pp)->next;

De cette façon, vous gardez toujours une trace du point d'où vous venez et pouvez modifier le pointeur qui y habite.

Donc, quand vous trouvez que l'entrée est supprimée, vous pouvez simplement faire

*pp = entry->next

De cette façon, vous prenez en charge les 3 cas Afaq mentionne dans une autre réponse, éliminant ainsi le contrôle NULL sur prev.

32
glglgl

Reconnecter la liste une fois qu'un nœud doit être supprimé est plus intéressant. Considérons au moins 3 cas:

1.Retirer un nœud depuis le début.

2.Retirer un nœud du milieu.

3.Removing un noeud de la fin. 

Supprimer depuis le début

Lors de la suppression du nœud au début de la liste, il n'est pas nécessaire de relier les nœuds, car le premier nœud n'a pas de nœud précédent. Par exemple, supprimer un noeud avec un:

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

Cependant, nous devons fixer le pointeur au début de la liste:

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

Retrait du milieu

La suppression d'un nœud du milieu nécessite que le nœud précédent ignore le nœud en cours de suppression. Par exemple, supprimer le nœud avec b:

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

Cela signifie que nous avons besoin d'un moyen de faire référence au nœud avant celui que nous voulons supprimer.

Retrait de la fin

Supprimer un nœud de la fin nécessite que le nœud précédent devienne la nouvelle fin de la liste (c’est-à-dire qu’il ne pointe rien après). Par exemple, en supprimant le noeud avec c:

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

Notez que les deux derniers cas (milieu et fin) peuvent être combinés en disant que "le nœud précédant celui à supprimer doit indiquer l'endroit où se trouve celui à supprimer." 

11
Afaq

Explication par vidéo

Ce problème a été discuté par Philip Buuck dans cette vidéo sur YouTube . Je vous recommande de regarder cela si vous avez besoin d'explications plus détaillées.


Explication par exemple

Si vous aimez apprendre des exemples, j'en ai préparé un. Disons que nous avons la liste simple liée suivante:

Singly-linked list example

qui est représenté comme suit (cliquez pour agrandir):

Singly-linked list representation

Nous voulons supprimer le noeud avec le value = 8.

Code

Voici le code simple qui fait ceci:

#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 = (node_t*)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;
}

Si vous compilez et exécutez ce code, vous obtiendrez:

56 70 8 1 28 
56 70 1 28

Explication du code

Créons **p 'double' pointeur sur *head pointeur:

Singly-linked list example #2

Analysons maintenant le fonctionnement de void remove_from_list(int val, node_t **head). Il parcourt la liste indiquée par head tant que *p && (**p).value != val.

Singly-linked list example #3

Singly-linked list example #4

Dans cet exemple, la liste donnée contient la variable value que nous souhaitons supprimer (c'est-à-dire 8). Après la deuxième itération de la boucle while (*p && (**p).value != val), (**p).value devient 8, nous cessons donc d’itérer.

Notez que *p pointe vers la variable node_t *next dans node_t qui est avant le node_t que nous voulons supprimer (qui est **p). Ceci est crucial car cela nous permet de changer le pointeur *next du node_t qui se trouve devant le node_t que nous voulons supprimer, en le supprimant de la liste.

Attribuons maintenant l’adresse de l’élément à supprimer (del->value == 8) au pointeur *del.

Singly-linked list example #5

Nous devons corriger le pointeur *p de sorte que **p pointe vers l'élément un élément after*del que nous allons supprimer:

Singly-linked list example #6

Dans le code ci-dessus, nous appelons free(del); il n'est donc pas nécessaire de définir del->next sur NULL, mais si nous souhaitons renvoyer le pointeur sur l'élément 'détaché' de la liste au lieu de le supprimer complètement, nous définirions del->next = NULL:

Singly-linked list example #7

8
patryk.beza

Dans la première approche, vous supprimez un nœud en unlink le dans la liste.

Dans la deuxième approche, vous remplacez le nœud à supprimer par le nœud suivant.

Apparemment, la seconde approche simplifie le code de manière élégante. En définitive, la deuxième approche nécessite une meilleure compréhension de la liste chaînée et du modèle de calcul sous-jacent.

Note: Voici une question de codification très pertinente mais légèrement différente. Bon pour tester sa compréhension: https://leetcode.com/problems/delete-node-in-a-linked-list/

5
ZillGate

Je préfère l'approche du nœud factice, un exemple de mise en page:

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

et ensuite, vous passez au noeud à supprimer (toDel = curr> next)

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

De cette façon, vous n'avez pas besoin de vérifier si c'est le premier élément, car le premier élément est toujours factice et n'est jamais supprimé.

2
Xee

@glglgl:

J'ai écrit suivant exemple simple. J'espère que vous pourrez voir pourquoi cela fonctionne.
Dans la fonction void delete_node(LinkedList *list, void *data), j'utilise *pp = (*pp)->next; et cela fonctionne. Pour être honnête, je ne comprends pas pourquoi cela fonctionne. Je dessine même le diagramme des pointeurs mais je ne le comprends toujours pas. J'espère que vous pourrez clarifier cela.

#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 = (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 = (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 = (Employee *) malloc(sizeof(Employee));
    strcpy(samuel->name, "Samuel");
    samuel->age = 23;

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

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

    Employee *jessie = (Employee *) 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;
}

sortie:

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

Linked List
Susan   14
Sally   19
Jessie  18
1
Lion Lai

Voici un exemple de code complet, utilisant un appel de fonction pour supprimer les éléments correspondants:

  • rem() supprime les éléments correspondants en utilisant prev

  • rem2() supprime les éléments correspondants à l'aide d'un pointeur à l'autre

// 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 = (list_entry *) 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;
}

Voir le code en action ici:

Si vous compilez et utilisez valgrind (un vérificateur de fuite de mémoire), procédez comme suit:
gcc -std=c11 -Wall -Wextra -Werror -o go code.c && valgrind ./go
nous voyons que tout va bien.

0
ajneu