web-dev-qa-db-ja.com

ポインターを使用して片方向リンクリストから項目を削除する

最近の Slashdot Interview で、Linus Torvaldsは、一部の人々がポインタを正しく使用する方法を本当に理解していないことを示す方法でポインタを使用する方法の例を示しました。

残念ながら、私は彼が話している人の一人なので、彼の例も理解できませんでした。

「prev」エントリを追跡することによって単一リンクリストエントリを削除し、次にエントリを削除して、次のようなことをする人があまりにも多いのを目にしました

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

そして、そのようなコードを見るときはいつでも、「この人はポインタを理解していません」に行きます。悲しいことに、それは非常に一般的です。ポインタを理解している人は、「エントリポインタへのポインタ」を使用し、list_headのアドレスで初期化します。そして、リストをトラバースするとき、を実行するだけで、条件を使用せずにエントリを削除できます

*pp = entry->next

なぜこのアプローチが優れているのか、そして条件付きステートメントなしでそれがどのように機能するのかについて誰かがもう少し説明できますか?

53
codebox

最初に、あなたは

pp = &list_head;

そして、リストをトラバースするときに、この「カーソル」を次のように進めます。

pp = &(*pp)->next;

このようにして、「どこから来たのか」を常に追跡し、そこにあるポインタを変更できます。

したがって、削除するエントリを見つけたら、次のようにすることができます

*pp = entry->next

このようにして、3つのケースすべてに対処しますAfaq別の回答での言及、NULLprevチェックを効果的に排除します。

32
glglgl

ノードが削除されると、リストを再接続する方が興味深いです。少なくとも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 |
---------     ---------     ---------

最後の2つのケース(中間と終了)は、 "削除するノードの前のノードは、削除するノードが存在する場所を指している必要がある"

11
Afaq

ビデオによる説明

この問題は このYouTubeビデオPhilip Buuck で議論されています。詳細な説明が必要な場合は、こちらをご覧になることをお勧めします。


例による説明

例から学ぶのが好きなら、私が用意しました。次の単一リンクリストがあるとします。

Singly-linked list example

これは次のように表されます(クリックして拡大):

Singly-linked list representation

_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 = (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;
}
_

このコードをコンパイルして実行すると、次のようになります。

_56 70 8 1 28 
56 70 1 28
_

コードの説明

_**p_ポインタへの_*head_ 'double'ポインタを作成しましょう:

Singly-linked list example #2

次に、void remove_from_list(int val, node_t **head)の動作を分析します。 *p && (**p).value != valである限り、headが指すリストを反復処理します。

Singly-linked list example #3

Singly-linked list example #4

この例では、指定されたリストに、削除するvalue(_8_)が含まれています。 while (*p && (**p).value != val)ループの2回目の反復の後、_(**p).value_は_8_になるため、反復を停止します。

_*p_が_node_t *next_内の変数_node_t_を指すことに注意してください。つまり、before_node_t_削除したい(つまり、_**p_)。削除する_*next_の前にある_node_t_の_node_t_ポインターを変更して、リストから効果的に削除できるため、これは非常に重要です。

次に、削除する要素のアドレス(_del->value == 8_)を_*del_ポインターに割り当てます。

Singly-linked list example #5

_*p_ポインタを修正して、_**p_が1つの要素after_*del_ elementを指すようにした削除します:

Singly-linked list example #6

上記のコードではfree(del)を呼び出しているため、_del->next_をNULLに設定する必要はありませんが、要素へのポインターを「切り離された」要素に返す場合は、リストを完全に削除する代わりに、_del->next = NULL_を設定します。

Singly-linked list example #7

8
patryk.beza

最初のアプローチでは、リストからノードをunlinkして削除します。

2番目の方法では、削除対象のノードを次のノードに置換します。

どうやら、2番目のアプローチは、エレガントな方法でコードを簡素化します。確かに、2番目のアプローチでは、リンクリストと基礎となる計算モデルをよりよく理解する必要があります。

:これは非常に関連性がありますが、わずかに異なるコーディングの質問です。理解をテストするのに適しています: https://leetcode.com/problems/delete-node-in-a-linked-list/

5
ZillGate

私はダミーノードアプローチ、レイアウト例を好みます:

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

次に、削除するノードに移動します(toDel = curr> next)

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

このようにすると、最初の要素が常にダミーであり、削除されないため、それが最初の要素であるかどうかを確認する必要はありません。

2
Xee

@glglgl:

以下の簡単な例を書きました。それが機能する理由をご覧いただければ幸いです。
関数void delete_node(LinkedList *list, void *data)では、*pp = (*pp)->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 = (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;
}

出力:

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

次に、完全なコード例を示します。関数呼び出しを使用して、一致する要素を削除します。

  • 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 = (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;
}

ここで実際のコードを参照してください:

Valgrind(メモリリークチェッカー)を次のようにコンパイルして使用する場合:
gcc -std=c11 -Wall -Wextra -Werror -o go code.c && valgrind ./go
すべてが良好であることがわかります。

0
ajneu