【问题标题】:What is the reason for using a double pointer when adding a node in a linked list?在链表中添加节点时使用双指针的原因是什么?
【发布时间】:2011-11-08 10:56:48
【问题描述】:

下面的两个代码示例都在链表的顶部添加了一个节点。 但是第一个代码示例使用双指针,而第二个代码示例使用单指针

代码示例 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)

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

是否存在单指针不工作需要双指针的情况?

【问题讨论】:

  • 删除了 C++ 标签。这绝对是C
  • 在 C 中你不会转换 malloc() 的结果。去掉演员表,它会更容易阅读和更惯用。
  • @EAGER_STUDENT - Explaination。基本上在c 中,除了意外隐藏错误之外,它永远不会做任何事情。在c++ 中是必需的。
  • 嗯...如果我编写一个双向链表,我喜欢让它循环并且总是有一个初始的、空的哨兵节点,头指向它。这使得许多例程变得更加简单。例如。根本不需要传递或修改头部。它永远不会改变。
  • @EAGER_STUDENT:没有称为“C/C++”的语言。转换malloc()的结果是C和C++的区别之一。

标签: c pointers linked-list


【解决方案1】:

一些实现传递一个指向指针参数的指针,以允许直接更改头指针而不是返回新指针。因此你可以写:

// 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分配的节点,头指针总是指向同一个节点。

现在好处应该很明显了:用第二种,如果调用者忘记将返回的节点分配给头指针,就会发生不好的事情。

【讨论】:

  • 谢谢@Yogi。我手动应用了您的修改,即使它被拒绝了。
  • struct node* push(struct node* head, int data) { struct node* newnode = malloc(sizeof(struct node)); newnode->data=data; newnode->next=head; head = newnode; } 为什么不是这个?
  • @Amit 因为这不会改变任何事情。此答案中的解释可能会有所帮助:stackoverflow.com/questions/8403447/…
【解决方案2】:

虽然前面的答案已经足够好了,但我认为从“按值复制”的角度来思考要容易得多。

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

通过使用双指针,您将能够更新原始指针的值。双指针仍将按值复制,但这没关系。您真正关心的是修改原始指针,从而绕过函数的作用域或堆栈。

希望这不仅能回答您的问题,还能回答其他与指针相关的问题。

【讨论】:

    【解决方案3】:

    正如@R. Martinho Fernandeshis answer 中指出的那样,使用pointer to pointer 作为void push(struct node** head, int data) 中的参数允许您直接从push 函数中更改head 指针,而不是返回新指针。

    还有另一个很好的例子说明了为什么使用pointer to pointer 代替单个指针可以缩短、简化和加速您的代码。您询问了 添加 一个新节点到列表中,与从单链表中 删除 节点相比,该节点可能通常不需要指针到指针。您可以在没有指针的情况下实现从列表中删除节点,但它不是最理想的。我描述了细节here。我建议您也观看解决问题的this YouTube video

    顺便说一句:如果你算上Linus Torvaldsopinion,你最好学习如何使用指针到指针。 ;-)

    Linus Torvalds: (...) 在光谱的另一端,我实际上希望更多的人了解真正核心的低级编码。不是大而复杂的东西,比如无锁名称查找,但只是很好地使用了指针到指针等。例如,我见过太多人通过跟踪“上一个”条目来删除单链表条目,然后删除条目,执行类似的操作

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

    每当我看到这样的代码时,我都会说“这个人不懂指针”。遗憾的是,这很常见。

    了解指针的人只是使用“指向入口指针的指针”,并使用 list_head 的地址对其进行初始化。然后当他们遍历列表时,他们可以在不使用任何条件的情况下删除条目,只需执行“*pp = entry->next”。 (...)


    其他可能有用的资源:

    【讨论】:

      【解决方案4】:

      在您的特定示例中,不需要双指针。但是,如果您要执行以下操作,则可能需要它:

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

      【讨论】:

      • @a6h:不客气...................................... ..................................................... ..................................................... .....................................
      【解决方案5】:

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

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

      观察 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;
      }
      

      解释起来很简单(基本)。我们的 main 函数中有一个指针,它指向列表的第一个节点(根)。在insert() 函数中,我们传递根节点的地址,并使用该地址到达列表的末尾并向其添加一个节点。因此我们可以得出结论,如果我们在函数(不是主函数)中有变量的地址,我们可以从该函数中永久更改该变量的值,这将反映在主函数中。

      OBSERVATION 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 中的地址发生了变化。因此,现在root(在insert()函数中)和A(在main()函数中)存储不同的地址。

      所以正确的最终程序应该是,

      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的地址(&amp;A)*root存储指针A存储的地址,**root存储A存储地址的值。用简单的语言root=&amp;A*root= A**root= *A

      2.如果我们写*root= 1528,那么这意味着存储在root中的地址的值变为1528,因为存储在root中的地址是指针A的地址(&amp;A)因此现在A=1528(即存储在A 中的地址为1528)并且此更改是永久性的。

      每当我们更改*root 的值时,我们确实在更改存储在root 中的地址的值,并且由于root=&amp;A(指针A 的地址)我们间接更改A 的值或存储在A.

      所以现在如果A=NULL(列表为空)*root=NULL,那么我们创建第一个节点并将其地址存储在*root,即间接我们将第一个节点的地址存储在A。如果 list 不为空,则一切都与之前使用单指针的函数相同,只是我们将 root 更改为 *root,因为存储在 root 中的内容现在存储在 *root 中。

      【讨论】:

        【解决方案6】:

        让我们用这个简单的例子:

        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 var
                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);
        
        
                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持有的值,代表z变量的地址,在main函数中声明。

        在 my_func() 中,当我们改变 p 的值以保存 z 的值时,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 var
            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);基本上,我们将 h 指针的地址传递给双指针 p,在函数签名中声明为参数。
        3. 在 my_func() 中我们正在执行:*p = z;我们正在取消引用双指针 p,一级。基本上,这就像你会做的那样翻译:h = z;

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

        您可以同时举两个例子并加以区分。 因此,回到您的问题,您需要双指针才能修改您直接从该函数传入的指针。

        【讨论】:

          【解决方案7】:

          想想像 [HEAD_DATA] 这样的 head 的内存位置。

          现在在第二种情况下,调用函数的 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]

          您只是用本地指针中的新内存地址替换了先前内存的内存地址。 main_head(指针)仍然指向旧的 [HEAD_DATA]

          【讨论】:

            【解决方案8】:

            在 C 中处理链表的标准方法是让 push 和 pop 函数自动更新头指针。

            C 是“按值调用”,意味着参数的副本被传递到函数中。如果您只传递头指针,则调用者不会看到您对该指针所做的任何本地更新。两种解决方法是

            1) 传递头指针的地址。 (指向头指针的指针)

            2) 返回一个新的头指针,并依赖调用者更新头指针。

            选项 1) 是最简单的,尽管一开始有点混乱。

            【讨论】:

              【解决方案9】:

              如果你花时间编写一个工作节点插入函数,答案会更明显;你的不是。

              您需要能够写入在头部上以将其向前移动,因此您需要一个指向头部的指针的指针,以便您可以取消引用它以获取指向头部的指针并更改它。

              【讨论】:

                【解决方案10】:

                假设您必须进行某些更改并且这些更改应该反映在调用函数中。

                例子:

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

                同样我们传递链表头的内存地址。

                这样,如果添加了任何节点并且 Head 的值发生了更改,那么该更改会反射回来,我们不必在调用函数内部手动重置 Head。

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

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

                【讨论】:

                  【解决方案11】:

                  当我们在函数中将指针作为参数传递并希望在同一个指针中更新时,我们使用双指针。

                  另一方面,如果我们将指针作为参数传递给函数并在单个指针中捕获它,则必须将结果返回给调用函数才能使用结果。

                  【讨论】:

                    【解决方案12】:

                    我认为关键在于它可以更轻松地更新链表中的节点。您通常必须跟踪以前和当前的指针,您可以使用双指针来处理这一切。

                    #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/callocnullptr 而不是 NULL 等。 )。
                    【解决方案13】:

                    假设我在卡片 1 上记下了您的家庭地址。现在,如果我想将您的家庭地址告诉其他人,我可以将地址从 card-1 复制到 card-2 并给 card-2,或者我可以直接给 card-1。无论哪种方式,该人都会知道地址并可以联系到您。但是当我直接给card-1时,可以在card-1上更改地址,但是如果我给card-2,则只能更改card-2上的地址,而不是在card-1上。

                    将指针传递给指针类似于直接访问 card-1。传递指针类似于创建地址的新副本。

                    【讨论】:

                      【解决方案14】:

                      我认为您的困惑可能是因为这两个函数都有一个名为head 的参数。这两个head 实际上是不同的东西。第一个代码中的head 存储了头节点指针的地址(它本身存储了头节点结构的地址)。而第二个head 直接存储头节点结构的地址。而且由于这两个函数都返回新创建的节点(应该是新的头),我认为没有必要采用第一种方法。此函数的调用者负责更新他们拥有的头部引用。我认为第二个已经足够好并且看起来很简单。我会选择第二个。

                      【讨论】:

                        【解决方案15】:

                        命名约定 - Head 是造成混淆的原因。

                        头是尾,尾是头。尾巴摇摇头。

                        Head 只是一个 Pointer,Data 是 Null - 而 Tail 只是 Data,Pointer 是 Null。

                        所以你有一个指向结构指针的指针。结构指针指向链接列表中的第一个节点结构。 这个指向第一个结构节点指针的指针称为 Head。最好叫 startptr 或 headptr。

                        当您抓住 startptr 时,您就抓住了链表。那么就可以遍历所有的struct节点了。

                        【讨论】:

                          猜你喜欢
                          • 1970-01-01
                          • 1970-01-01
                          • 1970-01-01
                          • 1970-01-01
                          • 2015-06-15
                          • 1970-01-01
                          • 1970-01-01
                          • 2021-07-07
                          相关资源
                          最近更新 更多