Использование указателей для удаления элемента из одноуровневого списка
В недавнем интервью Slashdot Линус Торвальдс привел пример того, как некоторые люди используют указатели таким образом, что они не понимают, как правильно их использовать.
К сожалению, поскольку я один из людей, о которых он говорит, я также не смог понять его пример:
Я видел слишком много людей, которые удаляют запись с одиночным соединением, отслеживая запись "prev", а затем удаляют запись, делая что-то вроде
if (prev)
prev->next = entry->next;
else
list_head = entry->next;
и всякий раз, когда я вижу такой код, я просто иду "Этот человек не понимает указателей". И это, к сожалению, довольно распространено. Люди, которые понимают указатели, просто используют "указатель на указатель ввода" и инициализируют это с адресом list_head. И затем, когда они пересекают список, они могут удалить запись без каких-либо условностей, просто делая
*pp = entry->next
Может ли кто-нибудь дать немного больше объяснений, почему этот подход лучше, и как он может работать без условного заявления?
Ответы
Ответ 1
В начале вы делаете
pp = &list_head;
и, по мере прохождения списка, вы продвигаете этот "курсор" с помощью
pp = &(*pp)->next;
Таким образом, вы всегда отслеживаете точку, откуда "вы пришли", и можете изменять указатель, живущий там.
Итак, когда вы обнаружите, что запись будет удалена, вы можете просто сделать
*pp = entry->next
Таким образом, вы заботитесь обо всех трех случаях Afaq упоминает в другом ответе, эффективно устраняя проверку NULL
на prev
.
Ответ 2
Повторное подключение списка после удаления node более интересно. Рассмотрим хотя бы 3 случая:
1. Восстановить node с самого начала.
2.Удаление a node из середины.
3.Удаление node с конца.
Удаление с самого начала
При удалении node в начале списка нет повторения узлов, которые должны выполняться, поскольку первый node не имеет предшествующего node. Например, удалив node с помощью:
link
|
v
--------- --------- ---------
| a | --+---> | b | --+---> | c | 0 |
--------- --------- ---------
Однако мы должны исправить указатель на начало списка:
link
|
+-------------+
|
v
--------- --------- ---------
| a | --+---> | b | --+---> | c | 0 |
--------- --------- ---------
Удаление из середины
Удаление node из середины требует, чтобы предыдущий node пропустил удаление node. Например, удаление node с помощью b:
link
|
v
--------- --------- ---------
| a | --+--+ | b | --+---> | c | 0 |
--------- | --------- ---------
| ^
+----------------+
Это означает, что нам нужно каким-то образом ссылаться на node до того, который мы хотим удалить.
Удаление с конца
Удаление node с конца требует, чтобы предыдущий node стал новым концом списка (т.е. ничего не указывает после него). Например, удаление node с помощью c:
link
|
v
--------- --------- ---------
| a | --+---> | b | 0 | | c | 0 |
--------- --------- ---------
Обратите внимание, что последние два случая (средний и конец) можно комбинировать, говоря, что "node, предшествующий удаляемому, должен указывать, где должен быть удален тот, который нужно удалить".
Ответ 3
Если вам нравится учиться на примерах, я подготовил один. Допустим, у нас есть следующий односвязный список:
![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
указатель:
![Singly-linked list example #2]()
Теперь давайте проанализируем, как работает void remove_from_list(int val, node_t **head)
. Он перебирает список, указанный в head
, до тех пор, пока *p && (**p).value != val
.
![Singly-linked list example #3]()
![Singly-linked list example #4]()
В этом примере данный список содержит value
, который мы хотим удалить (то есть 8
). После второй итерации цикла while (*p && (**p).value != val)
(**p).value
становится 8
, поэтому мы прекращаем итерацию.
Обратите внимание, что *p
указывает на переменную node_t *next
внутри node_t
, которая находится перед node_t
, который мы хотим удалить (то есть **p
). Это очень важно, поскольку позволяет нам изменить указатель *next
на node_t
, который находится перед node_t
, который мы хотим удалить, эффективно удаляя его из списка.
Теперь давайте назначим адрес элемента, который мы хотим удалить (del->value == 8
), указателю *del
.
![Singly-linked list example #5]()
Нам нужно исправить указатель *p
так, чтобы **p
указывал на один элемент после элемента *del
, который мы собираемся удалить:
![Singly-linked list example #6]()
В приведенном выше коде мы вызываем free(del)
, поэтому нет необходимости устанавливать для del->next
значение NULL
, но если мы хотим вернуть указатель на элемент, "отсоединенный" из списка, вместо того, чтобы полностью удалить его, мы установит del->next = NULL
:
![Singly-linked list example #7]()
Ответ 4
В первом подходе вы удалите node из unlink из списка.
Во втором подходе вы замените подлежащий удалению node следующим node.
По-видимому, второй подход упрощает код элегантным способом. Определенно, второй подход требует лучшего понимания связанного списка и базовой вычислительной модели.
Примечание. Вот очень важный, но немного другой вопрос с кодировкой. Хорошо для тестирования одного понимания:
https://leetcode.com/problems/delete-node-in-a-linked-list/
Ответ 5
Я предпочитаю подход Dummy node, пример макета:
|Dummy|->|node1|->|node2|->|node3|->|node4|->|node5|->NULL
^ ^
| |
curr curr->next // << toDel
а затем вы переходите к node для удаления (toDel = curr > next)
tmp = curr->next;
curr->next = curr->next-next;
free(tmp);
Таким образом, вам не нужно проверять, является ли это первым элементом, потому что первый элемент всегда является Dummy и никогда не удаляется.
Ответ 6
@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
Ответ 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 = (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
мы видим, что все хорошо.