在链表中添加节点时,为什么要使用双指针?

78
下面的两个代码示例都在链表顶部添加一个节点。 但是,第一个代码示例使用双指针,而第二个代码示例使用单指针。 代码示例1:
struct node* push(struct node **head, int data)
{
        struct node* newnode = malloc(sizeof(struct node));
        newnode->data = data;
        newnode->next = *head;
        return newnode;
}

push(&head,1);

代码示例2:

struct node* push(struct node *head, int data)
{
        struct node* newnode = malloc(sizeof(struct node));
        newnode->data = data;
        newnode->next = head;
        return newnode;
}

push(head,1)

两种策略都有效。然而,许多使用链表的程序使用双指针来添加新节点。我知道什么是双指针。但是如果单个指针足以添加新节点,为什么很多实现都依赖于双指针呢?

是否有任何情况下单个指针无法工作,因此我们需要使用双指针?


3
已移除 C++ 标签,这绝对是 C 语言。 - Armen Tsirunyan
7
在 C 语言中,你不需要对 malloc() 的结果进行强制转换。移除强制转换会使代码更易读,并符合惯用写法。 - Kerrek SB
5
基本上,在C语言中,使用malloc()不加强制转型也不会对程序产生太大影响,除非是意外地隐藏了某些错误。但在C++中,这是必须的。 - Flexo
1
哼...如果我编写双向链表,我喜欢使其成为循环列表,并始终具有一个初始的空哨兵节点,head 指向该节点。这使得许多例程更加简单。例如,根本不需要传递或修改 head。它永远不会改变。 - Rudy Velthuis
2
@Kerrek SB:为了维护这位热心的学生,他说“无论是c/c++”,这似乎是缩写形式的“无论是C还是C++”,也就是说他在任何一种语言中都会这样做。这就像有人说“在C/C++/Java/C#中使用大括号来界定一个块”。我们都知道没有C/C++/Java/C#这种语言,但每个人都能理解它。 - Rudy Velthuis
显示剩余5条评论
15个回答

123

有些实现会传递指向指针的参数,以允许直接更改头指针而不是返回新的指针。因此,您可以编写以下代码:

// note that there's no return value: it's not needed
void push(struct node** head, int data)
{
    struct node* newnode = malloc(sizeof(struct node));
    newnode->data=data;
    newnode->next=*head;
    *head = newnode; // *head stores the newnode in the head
}

// and call like this:
push(&head,1);

不需要传递指向头指针的指针的实现必须返回新的头节点,调用者负责自行更新:

struct node* push(struct node* head, int data)
{
    struct node* newnode = malloc(sizeof(struct node));
    newnode->data=data;
    newnode->next=head;
    return newnode;
}

// note the assignment of the result to the head pointer
head = push(head,1);

如果在调用此函数时不执行此赋值操作,您将会泄漏使用malloc分配的节点,并且头指针将始终指向相同的节点。

现在优点应该很清楚了:使用第二种方式,如果调用者忘记将返回的节点分配给头指针,则会发生糟糕的事情。

编辑:

指向指针(双重指针)还允许在同一程序中创建多个用户定义的数据类型(例如:创建2个链表)。

为避免双指针的复杂性,我们可以始终利用结构体(它可以作为内部指针)。

可以按以下方式定义列表:

typedef struct list {
    struct node* root;    
} List;

List* create() {
    List* templ = malloc(sizeof(List));
    templ->root = NULL;
    return templ;
}

在链表函数中,可以按照以下方式使用上述列表(以Push函数为例):

void Push(List* l, int x) {         
    struct node* n = malloc(sizeof(struct node));
    n->data = x;
    n->link = NULL;
    
    printf("Node created with value %d\n", n->data);
    if (l->root == NULL) {
        l->root = n;
    } else {
        struct node* i = l->root;
        while (i->link != NULL){
            i = i->link;
        }
        i->link = n;
    }
}

在你的main()函数中以以下方式声明列表:

List* list1 = create(); 
push(list1, 10);

      

谢谢@Yogi。尽管它被拒绝了,但我还是手动应用了您的编辑。 - R. Martinho Fernandes
struct node* push(struct node* head, int data) { struct node* newnode = malloc(sizeof(struct node)); newnode->data=data; newnode->next=head; head = newnode; } 为什么不这样做? - Amit Tripathi
4
@Amit因为那不会改变任何事情。这个答案中的解释可能有所帮助:https://dev59.com/T2oy5IYBdhLWcg3wq_wk#8403699 - R. Martinho Fernandes

18

虽然前面的答案已经足够好了,但我认为更容易从“按值复制”方面思考。

当您向函数传递指针时,地址值将被复制到函数参数中。由于函数的作用域,该副本将在返回后消失。

通过使用双重指针,您将能够更新原始指针的值。双重指针仍将按值复制,但这并不重要。你真正关心的是修改原始指针,从而绕过函数的范围或堆栈。

希望这不仅回答了您的问题,也回答了其他有关指针的问题。


7
正如@R. Martinho Fernandes他的回答中指出的那样,在void push(struct node** head, int data)中使用指向指针的指针作为参数,可以直接从push函数内部更改head指针,而无需返回新指针。
还有一个很好的例子,说明为什么使用指向指针的指针而不是单个指针可能会缩短、简化和加速您的代码。您问如何将新节点添加到列表中,这可能通常不需要指向指针,与从单链表中删除节点相反。您可以实现不使用指向指针从列表中删除节点,但这是次优的。我在这里描述了详细信息。我建议您也观看这个YouTube视频,其中讨论了这个问题。
顺便说一句:如果你考虑Linus Torvalds意见,你最好学习如何使用指向指针的指针。;-)

Linus Torvalds: (...) At the opposite end of the spectrum, I actually wish more people understood the really core low-level kind of coding. Not big, complex stuff like the lockless name lookup, but simply good use of pointers-to-pointers etc. For example, I've seen too many people who delete a singly-linked list entry by keeping track of the "prev" entry, and then to delete the entry, doing something like

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

and whenever I see code like that, I just go "This person doesn't understand pointers". And it's sadly quite common.

People who understand pointers just use a "pointer to the entry pointer", and initialize that with the address of the list_head. And then as they traverse the list, they can remove the entry without using any conditionals, by just doing a "*pp = entry->next". (...)


其他可能有帮助的资源:


6
在您的特定示例中,不需要双指针。然而,如果您像这样做某些事情,则可能需要它:
struct node* push(struct node** head, int data)
{
    struct node* newnode = malloc(sizeof(struct node));
    newnode->data=data;
    newnode->next=*head;
    //vvvvvvvvvvvvvvvv
    *head = newnode; //you say that now the new node is the head.
    //^^^^^^^^^^^^^^^^
    return newnode;
}

3

观察和发现,* 为什么...

我决定做一些实验并得出结论,

观察 1- 如果链表不为空,则我们可以仅使用单个指针将节点添加到其中(显然是在末尾)。

int insert(struct LinkedList *root, int item){
    struct LinkedList *temp = (struct LinkedList*)malloc(sizeof(struct LinkedList));
    temp->data=item;
    temp->next=NULL;
    struct LinkedList *p = root;
    while(p->next!=NULL){
        p=p->next;
    }
    p->next=temp;
    return 0;
}


int main(){
    int m;
    struct LinkedList *A=(struct LinkedList*)malloc(sizeof(struct LinkedList));
    // Now we want to add one element to the list so that the list becomes non-empty
    A->data=5;
    A->next=NULL;
    cout<<"enter the element to be inserted\n"; cin>>m;
    insert(A,m);
    return 0;
}

这很容易解释(基础知识)。我们在主函数中有一个指针,它指向列表的第一个节点(根节点)。在insert()函数中,我们传递根节点的地址,并使用此地址到达列表的末尾并添加一个节点。因此,我们可以得出结论:如果我们在函数中(而不是主函数)具有变量的地址,则可以从该函数对该变量的值进行永久更改,这将反映在主函数中。

**观察2-当列表为空时,上述添加节点的方法失败了。

int insert(struct LinkedList *root, int item){
    struct LinkedList *temp = (struct LinkedList*)malloc(sizeof(struct LinkedList));
    temp->data=item;
    temp->next=NULL;
    struct LinkedList *p=root;
    if(p==NULL){
        p=temp;
    }
    else{
      while(p->next!=NULL){
          p=p->next;
      }
      p->next=temp;
    }
    return 0;
}


int main(){
    int m;
    struct LinkedList *A=NULL; //initialise the list to be empty
    cout<<"enter the element to be inserted\n";
    cin>>m;
    insert(A,m);
    return 0;
}

如果您不断添加元素并最终显示列表,则会发现列表没有发生任何更改,仍然为空。

我想到的问题是,在这种情况下,我们仍然传递了根节点的地址,为什么修改不会作为永久修改发生,并且主函数中的列表没有发生任何更改。为什么?为什么?为什么?

然后我观察到一件事情,当我写A=NULL时,A的地址变为0。这意味着现在A没有指向内存中的任何位置。因此,我删除了A=NULL;这行,并对插入函数进行了一些修改。

一些修改(下面的insert()函数只能向空列表添加一个元素,只是为了测试目的编写此函数):

int insert(struct LinkedList *root, int item){
    root= (struct LinkedList *)malloc(sizeof(struct LinkedList));
    root->data=item;
    root->next=NULL;
    return 0;
}


int main(){
    int m;
    struct LinkedList *A;
    cout<<"enter the element to be inserted\n";
    cin>>m;
    insert(A,m);
    return 0;
}

上述方法也失败了,因为在insert()函数中,根节点存储的地址与main()函数中的A相同,但是在root= (struct LinkedList *)malloc(sizeof(struct LinkedList));这一行之后,root中存储的地址发生了变化。因此,现在insert()函数中的rootmain()函数中的A存储不同的地址。

因此,正确的最终程序应该是:

int insert(struct LinkedList *root, int item){
    root->data=item;
    root->next=NULL;
    return 0;
}


int main(){
    int m;
    struct LinkedList *A = (struct LinkedList *)malloc(sizeof(struct LinkedList));
    cout<<"enter the element to be inserted\n";
    cin>>m;
    insert(A,m);
    return 0;
}

但我们不想为插入编写两个不同的函数,一个是当列表为空时,另一个是当列表不为空时。现在双指针出现了,使事情变得容易。

我注意到的一件重要的事情是指针存储地址,当与 '*' 一起使用时,它们会给出该地址上的值,但指针本身也有自己的地址。

现在这里是完整的程序,稍后解释概念。

int insert(struct LinkedList **root,int item){
    if(*root==NULL){
        (*root)=(struct LinkedList *)malloc(sizeof(struct LinkedList));
        (*root)->data=item;
        (*root)->next=NULL;
    }
    else{
        struct LinkedList *temp=(struct LinkedList *)malloc(sizeof(struct LinkedList));
        temp->data=item;
        temp->next=NULL;
        struct LinkedList *p;
        p=*root;
        while(p->next!=NULL){
            p=p->next;
        }
        p->next=temp;
    }
    return 0;
}


int main(){
    int n,m;
    struct LinkedList *A=NULL;
    cout<<"enter the no of elements to be inserted\n";
    cin>>n;
    while(n--){
        cin>>m;
        insert(&A,m);
    }
    display(A);
    return 0;
}

以下是观察结果:
1. root 存储指针 A 的地址(&A),*root 存储指针 A 所存储的地址,**root 存储指针 A 所指向的值。简单来说,root=&A,*root=A,**root=*A。
2. 如果我们写入 *root=1528,则表示存储在 root 中的地址的值变为 1528,由于存储在 root 中的地址是指针 A 的地址(&A),因此现在 A=1528(即存储在 A 中的地址为 1528),并且这种更改是永久性的。
每当我们更改 *root 的值时,实际上我们正在更改存储在 root 中的地址的值,并且由于 root=&A(指针 A 的地址),因此我们间接地更改了 A 的值或存储在 A 中的地址。

现在,如果A=NULL(列表为空)*root=NULL,因此我们创建第一个节点并将其地址存储在*root中,即间接地将第一个节点的地址存储在A中。如果列表不为空,则除了使用单个指针执行先前函数时更改了根为*root之外,其他所有内容都相同,因为现在存储在根中的内容现在存储在*root中。


1

把头部的内存位置想象成[HEAD_DATA]。

现在在您的第二种情况下,调用函数的main_head是指向该位置的指针。

main_head--->[HEAD_DATA]

在您的代码中,它将指针main_head的值发送到函数(即head_data的内存位置的地址),然后将其复制到函数中的local_head中。 所以现在

local_head---> [HEAD_DATA]

main_head---> [HEAD_DATA]

都指向同一个位置,但本质上相互独立。 因此,当您写入local_head = newnode时, 你所做的是

local_head--/-->[HEAD_DATA]

local_head-----> [NEWNODE_DATA]

您只需用新的内存地址替换以前的内存地址。

主要头(指针)仍然指向旧的[HEAD_DATA]。


1
在C语言中处理链表的标准方式是让push和pop函数自动更新头指针。
C语言是“传值调用”的,这意味着参数的副本被传递到函数中。如果你只传递头指针,那么对该指针所做的任何本地更新都不会被调用者看到。有两种解决方法:
1) 传递头指针的地址。(指向头指针的指针)
2) 返回一个新的头指针,并依赖调用者来更新头指针。
选项1) 是最简单的,尽管最初可能有点令人困惑。

1

让我们来看一个简单的例子:

void my_func(int *p) {

    // Allocate space for an int
    int *z = (int *) malloc(sizeof(int));

    // Assign a value
    *z = 99;

    printf("my_func - value of z: %d\n", *z);

    printf("my_func - value of p: %p\n", p);
    // Change the value of the pointer p. Now it is not pointing to h anymore
    p = z;
    printf("my_func - make p point to z\n");
    printf("my_func - addr of z %p\n", &*z);
    printf("my_func - value of p %p\n", p);
    printf("my_func - value of what p points to: %d\n", *p);
    free(z);
}

int main(int argc, char *argv[])
{
    // Our variable
    int z = 10;

    int *h = &z;

    // Print the value of z
    printf("main - value of z: %d\n", z);

    // Print the address of val
    printf("main - addr of z: %p\n", &z);

    // Print the value of h.
    printf("main - value of h: %p\n", h);

    // Print the value of what h points to
    printf("main - value of what h points to: %d\n", *h);

    // Change the value of var z by dereferencing h
    *h = 22;

    // Print value of val
    printf("main - value of z: %d\n", z);

    // Print value of what h points to
    printf("main - value of what h points to: %d\n", *h);


    my_func(h);

    // Print value of what h points to
    printf("main - value of what h points to: %d\n", *h);

    // Print value of h
    printf("main - value of h: %p\n", h);

    return 0;
}

输出:

main - value of z: 10
main - addr of z: 0x7ffccf75ca64
main - value of h: 0x7ffccf75ca64
main - value of what h points to: 10
main - value of z: 22
main - value of what h points to: 22
my_func - value of z: 99
my_func - value of p: 0x7ffccf75ca64
my_func - make p point to z
my_func - addr of z 0x1906420
my_func - value of p 0x1906420
my_func - value of what p points to: 99
main - value of what h points to: 22
main - value of h: 0x7ffccf75ca64

我们有一个名为 my_func 的签名:

void my_func(int *p);

如果您查看输出,最终h指向的值仍然是22,而h的值相同,尽管在my_func中它已经改变了。为什么?

嗯,在my_func中,我们正在操作p的值,这只是一个本地指针。 调用之后:

my_func(ht);

在main()函数中,p将保存h所持有的值,该值表示在main函数中声明的指向变量z的地址。
在my_func()函数中,当我们将p的值更改为保存指向内存位置的指针z的值时,我们没有更改传入的h的值,而只是更改了本地指针p的值。基本上,p不再保存h的值,而是将保存z指向的内存位置的地址。
现在,如果我们稍微修改一下我们的示例:
#include <stdio.h>
#include <stdlib.h>

void my_func(int **p) {
    // Allocate space for an int
    int *z = (int *) malloc(sizeof(int));

    // Assign a value
    *z = 99;

    printf("my_func - value of z: %d\n", *z);

    printf("my_func - value of p: %p\n", p);
    printf("my_func - value of h: %p\n", *p);

    // Change the value of the pointer p. Now it is not pointing to h anymore
    *p = z;
    printf("my_func - make p point to z\n");
    printf("my_func - addr of z %p\n", &*z);
    printf("my_func - value of p %p\n", p);
    printf("my_func - value of h %p\n", *p);
    printf("my_func - value of what p points to: %d\n", **p);

    // We are not deallocating, because we want to keep the value in that
    // memory location, in order for h to access it.
    /* free(z); */
}

int main(int argc, char *argv[])
{
    // Our variable
    int z = 10;

    int *h = &z;

    // Print value of z
    printf("main - value of z: %d\n", z);

    // Print address of val
    printf("main - addr of z: %p\n", &z);

    // Print value of h.
    printf("main - value of h: %p\n", h);

    // Print value of what h points to
    printf("main - value of what h points to: %d\n", *h);

    // Change the value of var z by dereferencing h
    *h = 22;

    // Print value of val
    printf("main - value of z: %d\n", z);

    // Print value of what h points to
    printf("main - value of what h points to: %d\n", *h);


    my_func(&h);

    // Print value of what h points to
    printf("main - value of what h points to: %d\n", *h);

    // Print value of h
    printf("main - value of h: %p\n", h);
    free(h);

    return 0;
}

我们有以下输出:
main - value of z: 10
main - addr of z: 0x7ffcb94fb1cc
main - value of h: 0x7ffcb94fb1cc
main - value of what h points to: 10
main - value of z: 22
main - value of what h points to: 22
my_func - value of z: 99
my_func - value of p: 0x7ffcb94fb1c0
my_func - value of h: 0x7ffcb94fb1cc
my_func - make p point to z
my_func - addr of z 0xc3b420
my_func - value of p 0x7ffcb94fb1c0
my_func - value of h 0xc3b420
my_func - value of what p points to: 99
main - value of what h points to: 99
main - value of h: 0xc3b420

现在,我们实际上已经通过my_func改变了h的值:

  1. 更改函数签名
  2. 从main()中调用:my_func(&h); 基本上是将声明为函数签名参数的双重指针p的地址传递给h指针。
  3. 在my_func()中,我们执行了:*p = z; 我们对双重指针p进行了一级解引用。基本上这被翻译为:h = z;

现在,p的值保存了h指针的地址。h指针保存了z的地址。

你可以拿这两个示例并进行比较。

回答你的问题,你需要使用双重指针才能直接从该函数对传入的指针进行修改。


0

想象一种情况,你需要进行某些更改,并且这些更改应该在调用函数中反映出来。

例如:

void swap(int* a,int* b){
  int tmp=*a;
  *a=*b;
  *b=tmp;
}

int main(void){
  int a=10,b=20;

  // To ascertain that changes made in swap reflect back here we pass the memory address
  // instead of the copy of the values

  swap(&a,&b);
}

同样,我们传递了列表头的内存地址。

这种方式,如果添加任何节点并且头的值被更改,那么这个更改会反映回来,我们不必在调用函数内手动重置头。

因此,这种方法减少了内存泄漏的可能性,因为如果我们忘记在调用函数中更新头,则会丢失对新分配的节点的指针。

除此之外,第二个代码将更快地工作,因为我们直接使用内存,没有浪费时间复制和返回。


0

我认为重点在于它使得更新链表中的节点更加容易。通常情况下,您需要跟踪前一个和当前指针,但是使用双指针可以处理所有这些。

#include <iostream>
#include <math.h>

using namespace std;

class LL
{
    private:
        struct node 
        {
            int value;
            node* next;
            node(int v_) :value(v_), next(nullptr) {};
        };
        node* head;

    public:
        LL() 
        {
            head = nullptr;
        }
        void print() 
        {
            node* temp = head;
            while (temp) 
            {
                cout << temp->value << " ";
                temp = temp->next;
            }
        }
        void insert_sorted_order(int v_) 
        {
            if (!head)
                head = new node(v_);
            else
            {
                node* insert = new node(v_);
                node** temp = &head;
                while ((*temp) && insert->value > (*temp)->value)
                    temp = &(*temp)->next;
                insert->next = (*temp);
                (*temp) = insert;
            }
        }

        void remove(int v_)
        {
            node** temp = &head;
            while ((*temp)->value != v_)
                temp = &(*temp)->next;
            node* d = (*temp);
            (*temp) = (*temp)->next;
            delete d;
        }

        void insertRear(int v_)//single pointer
        {
            if (!head)
                head = new node(v_);
            else
            {
                node* temp = new node(v_);
                temp->next = head;
                head = temp;
            }
        }
};

我已经编辑了您的帖子以修复代码格式。但是,您的代码是C++,而此问题的标签是C。请考虑编辑您的代码,以便仅使用纯C语法(例如,使用new而不是malloc/calloc,使用nullptr而不是NULL等)。 - rayryeng

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接