在最近的Slashdot Interview中,Linus Torvalds给出了一个例子,说明一些人如何使用指针,表明他们并不真正理解如何正确使用它们。
不幸的是,由于我是他所谈论的人之一,我也没理解他的例子:
我见过太多人通过跟踪“prev”条目删除单链表项,然后删除条目, 类似
if (prev) prev->next = entry->next; else list_head = entry->next;
每当我看到这样的代码时,我就会去“这个人没有 理解指针“。这很遗憾很常见 了解指针只需使用“指向入口指针的指针”,和 使用list_head的地址初始化它。然后就像他们一样 遍历列表,他们可以删除条目而不使用任何 条件,只需做
*pp = entry->next
有人可以提供更多解释,为什么这种方法更好,以及如何在没有条件陈述的情况下工作?
答案 0 :(得分:32)
一开始,你做
pp = &list_head;
并且,当您遍历列表时,使用
推进此“光标”pp = &(*pp)->next;
通过这种方式,您始终可以跟踪“您来自”的位置,并可以修改生活在那里的指针。
因此,当您找到要删除的条目时,您可以执行
*pp = entry->next
这样,您可以在另一个答案中处理所有3个 Afaq 提及的案例,从而有效取消对NULL
的{{1}}检查。
答案 1 :(得分:11)
要删除节点后重新连接列表会更有趣。让我们考虑至少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 :(得分:8)
Philip Buuck this YouTube video已讨论过此问题。如果您需要更详细的解释,我建议您观看。
如果你喜欢从例子中学习,我准备了一个。假设我们有以下单链表:
表示如下(点击放大):
我们要删除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
'double'指向*head
指针的指针:
现在让我们分析void remove_from_list(int val, node_t **head)
的工作原理。只要head
,它就会遍历*p && (**p).value != val
指向的列表。
在此示例中,给定列表包含我们要删除的value
(8
)。在while (*p && (**p).value != val)
循环(**p).value
的第二次迭代变为8
后,我们停止迭代。
请注意,*p
指向node_t *next
内的变量node_t
之前我们要删除的node_t
(**p
{1}})。这一点至关重要,因为它允许我们更改我们要删除的*next
前面node_t
的{{1}}指针,从而有效地将其从列表中删除。
现在让我们将要删除的元素的地址(node_t
)分配给del->value == 8
指针。
我们需要修复*del
指针,以便*p
指向我们要删除的 **p
元素之后的一个元素:
在上面的代码中,我们调用*del
,因此没有必要将free(del)
设置为del->next
,但是如果我们想要将指针返回到'detached'元素中列表而不是完全删除它,我们将设置NULL
:
答案 3 :(得分:5)
在第一种方法中,您通过取消链接从列表中删除节点。
在第二种方法中,您用下一个节点替换要删除的节点。
显然,第二种方法以优雅的方式简化了代码。当然,第二种方法需要更好地理解链表和基础计算模型。
注意:这是一个非常相关但略有不同的编码问题。适合测试一个人的理解: https://leetcode.com/problems/delete-node-in-a-linked-list/
答案 4 :(得分:2)
我更喜欢Dummy节点方法,一个示例布局:
|Dummy|->|node1|->|node2|->|node3|->|node4|->|node5|->NULL
^ ^
| |
curr curr->next // << toDel
然后,您遍历要删除的节点(toDel = curr&gt; next)
tmp = curr->next;
curr->next = curr->next-next;
free(tmp);
这样,你不需要检查它是否是第一个元素,因为第一个元素总是Dummy而且永远不会被删除。
答案 5 :(得分:1)
@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
答案 6 :(得分:1)
这是我的看法,我发现用这种方式更容易理解。
使用指向指针的指针删除链表中节点的示例。
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 :(得分:0)
这是一个完整的代码示例,使用函数调用来删除匹配元素:
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
我们看到一切都很好。