最近の Slashdot Interview で、Linus Torvaldsは、一部の人々がポインタを正しく使用する方法を本当に理解していないことを示す方法でポインタを使用する方法の例を示しました。
残念ながら、私は彼が話している人の一人なので、彼の例も理解できませんでした。
「prev」エントリを追跡することによって単一リンクリストエントリを削除し、次にエントリを削除して、次のようなことをする人があまりにも多いのを目にしました
if (prev) prev->next = entry->next; else list_head = entry->next;
そして、そのようなコードを見るときはいつでも、「この人はポインタを理解していません」に行きます。悲しいことに、それは非常に一般的です。ポインタを理解している人は、「エントリポインタへのポインタ」を使用し、list_headのアドレスで初期化します。そして、リストをトラバースするとき、を実行するだけで、条件を使用せずにエントリを削除できます
*pp = entry->next
なぜこのアプローチが優れているのか、そして条件付きステートメントなしでそれがどのように機能するのかについて誰かがもう少し説明できますか?
最初に、あなたは
pp = &list_head;
そして、リストをトラバースするときに、この「カーソル」を次のように進めます。
pp = &(*pp)->next;
このようにして、「どこから来たのか」を常に追跡し、そこにあるポインタを変更できます。
したがって、削除するエントリを見つけたら、次のようにすることができます
*pp = entry->next
このようにして、3つのケースすべてに対処しますAfaq別の回答での言及、NULL
のprev
チェックを効果的に排除します。
ノードが削除されると、リストを再接続する方が興味深いです。少なくとも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つのケース(中間と終了)は、 "削除するノードの前のノードは、削除するノードが存在する場所を指している必要がある"
この問題は このYouTubeビデオ の Philip Buuck で議論されています。詳細な説明が必要な場合は、こちらをご覧になることをお勧めします。
例から学ぶのが好きなら、私が用意しました。次の単一リンクリストがあるとします。
これは次のように表されます(クリックして拡大):
_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'ポインタを作成しましょう:
次に、void remove_from_list(int val, node_t **head)
の動作を分析します。 *p && (**p).value != val
である限り、head
が指すリストを反復処理します。
この例では、指定されたリストに、削除する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
_ポインターに割り当てます。
_*p
_ポインタを修正して、_**p
_が1つの要素after_*del
_ elementを指すようにした削除します:
上記のコードではfree(del)
を呼び出しているため、_del->next
_をNULL
に設定する必要はありませんが、要素へのポインターを「切り離された」要素に返す場合は、リストを完全に削除する代わりに、_del->next = NULL
_を設定します。
最初のアプローチでは、リストからノードをunlinkして削除します。
2番目の方法では、削除対象のノードを次のノードに置換します。
どうやら、2番目のアプローチは、エレガントな方法でコードを簡素化します。確かに、2番目のアプローチでは、リンクリストと基礎となる計算モデルをよりよく理解する必要があります。
注:これは非常に関連性がありますが、わずかに異なるコーディングの質問です。理解をテストするのに適しています: https://leetcode.com/problems/delete-node-in-a-linked-list/
私はダミーノードアプローチ、レイアウト例を好みます:
|Dummy|->|node1|->|node2|->|node3|->|node4|->|node5|->NULL
^ ^
| |
curr curr->next // << toDel
次に、削除するノードに移動します(toDel = curr> next)
tmp = curr->next;
curr->next = curr->next-next;
free(tmp);
このようにすると、最初の要素が常にダミーであり、削除されないため、それが最初の要素であるかどうかを確認する必要はありません。
@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
次に、完全なコード例を示します。関数呼び出しを使用して、一致する要素を削除します。
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
すべてが良好であることがわかります。