簡體   English   中英

使用指針從單向鏈表中刪除項目

[英]Using pointers to remove item from singly-linked list

在最近的Slashdot 采訪中, 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

這樣,您就可以處理Afaq在另一個答案中提到的所有 3 種情況,有效地消除了對prevNULL檢查。

如果你喜歡從例子中學習,我准備了一個。 假設我們有以下單鏈表:

單鏈表示例

表示如下(點擊放大):

單鏈表表示

我們要刪除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指針:

單鏈表示例#2

現在讓我們分析一下void remove_from_list(int val, node_t **head)工作的。 只要*p && (**p).value != val它就會遍歷head指向的列表。

單鏈表示例#3

單鏈表示例#4

在這個例子中,給定的列表包含我們想要刪除的value (即8 )。 while (*p && (**p).value != val)循環的第二次迭代之后(**p).value變為8 ,因此我們停止迭代。

請注意, *p指向變量node_t *nextnode_t那就是之前node_t我們想刪除(這是**p )。 這是至關重要的,因為它允許我們改變*next的指針node_t是在前面node_t ,我們要刪除,有效地從列表中刪除。

現在讓我們將要刪除的元素的地址( del->value == 8 )分配給*del指針。

單鏈表示例#5

我們需要修復*p指針,以便**p指向我們要刪除的*del元素之后的一個元素:

單鏈表示例#6

在上面的代碼中,我們調用了free(del) ,因此沒有必要將del->next設置為NULL ,但是如果我們想從列表中返回指向“分離”元素的指針而不是完全刪除它,我們將設置del->next = NULL

單鏈表示例#7

一旦要刪除節點,重新連接列表更有趣。 讓我們考慮至少 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 |
---------     ---------     ---------

請注意,最后兩種情況(中間和結束)可以通過說“要刪除的節點之前的節點必須指向要刪除的節點所在的位置”來組合。

在第一種方法中,您通過從列表中取消鏈接來刪除節點。

在第二種方法中,您要刪除的節點替換為下一個節點。

顯然,第二種方法以優雅的方式簡化了代碼。 當然,第二種方法需要更好地理解鏈表和底層計算模型。

注意:這是一個非常相關但略有不同的編碼問題。 有利於測試自己的理解: 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);

這樣,您就不需要檢查它是否是第一個元素,因為第一個元素始終是 Dummy 並且永遠不會被刪除。

這是一個完整的代碼示例,使用函數調用來刪除匹配的元素:

  • 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
我們看到一切都很好。

@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

這是我的看法,我發現這樣更容易理解。

使用指向指針的指針刪除鏈表中節點的示例。

    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);

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM