What is the reason for using a double pointer when adding a node in a linked list?

79,692

Solution 1

Some implementations pass a pointer to pointer parameter to allow changing the head pointer directly instead of returning the new one. Thus you could write:

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

The implementation that doesn't take a pointer to the head pointer must return the new head, and the caller is responsible for updating it itself:

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

If you don't do this assignment when calling this function, you will be leaking the nodes you allocate with malloc, and the head pointer will always point to the same node.

The advantage should be clear now: with the second, if the caller forgets to assign the returned node to the head pointer, bad things will happen.

Edit:

Pointer to pointer(Double pointers) also allows for creation for multiple user defined data types within a same program(Example: Creating 2 linked lists)

To avoid complexity of double pointers we can always utilize structure(which works as an internal pointer).

You can define a list in the following way:

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

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

In link list functions use the above List in following way: (Example for Push function)

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

In your main() function declare the list in follow way:

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

      

Solution 2

Although the previous answers are good enough, I think it's much easier to think in terms of "copy by value".

When you pass in a pointer to a function, the address value is being copied over to the function parameter. Due to the function's scope, that copy will vanish once it returns.

By using a double pointer, you will be able to update the original pointer's value. The double pointer will still be copied by value, but that doesn't matter. All you really care is modifying the original pointer, thereby bypassing the function's scope or stack.

Hope this answers not just your question, but other pointer related questions as well.

Solution 3

As @R. Martinho Fernandes pointed out in his answer, using pointer to pointer as an argument in void push(struct node** head, int data) allows you to change the head pointer directly from within push function instead of returning the new pointer.

There is yet another good example which shows why using pointer to pointer instead a single pointer may shorten, simplify and speed up your code. You asked about adding a new node to the list which probably typically doesn't need pointer-to-pointer in contrast to removing the node from the singly-linked list. You can implement removing node from the list without pointer-to-pointer but it is suboptimal. I described the details here. I recommend you also to watch this YouTube video which addresses the problem.

BTW: If you count with Linus Torvalds opinion, you would better learn how to use pointer-to-pointer. ;-)

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". (...)


Other resources that may be helpful:

Solution 4

In your particular example there is no need for the double pointer. However it can be needed, if, for example, you were to do something like this:

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

Solution 5

Observation and Finding, WHY...

I decided to do some experiments and make some conclusion,

OBSERVATION 1- If the linked list is not empty then we can add the nodes in it (obviously at the end) by using a single pointer only.

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

Its simple to explain (Basic). We have a pointer in our main function which points to the first node (root) of the list. In the insert() function we pass the address of the root node and using this address we reach the end of the list and add a node to it. So we can conclude that if we have address of a variable in a function (not the main function) we can make permanent changes in the value of that variable from that function which would reflect in the main function.

OBSERVATION 2- The above method of adding node failed when the list was empty.

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

If you keep on adding elements and finally display the list then you would find that the list has undergone no changes and still it is empty. The question which struck my mind was in this case also we are passing the address of the root node then why modifications are not happening as permanent modifications and list in the main function undergoes no changes. WHY? WHY? WHY?

Then I observed one thing, when I write A=NULL the address of A becomes 0. This means now A is not pointing to any location in memory. So I removed the line A=NULL; and made some modification in the insert function.

some modifications,(below insert() function can add only one element to an empty list, just wrote this function for testing purpose)

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

the above method also fails because in the insert() function root stores same address as A in the main() function but after the line root= (struct LinkedList *)malloc(sizeof(struct LinkedList)); the address stored in root changes. Thus now , root (in insert() function) and A (in main() function) store different addresses.

So the correct final program would be,

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

But we dont want two different functions for insertion, one when list is empty and other when list is not empty. Now comes double pointer which makes things easy.

One thing I noticed which is important is that pointers store address and when used with '*' they give value at that address but pointers themselves have their own address.

Now here is the complete program and later explain the concepts.

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

following are the observations,

1. root stores the address of pointer A (&A) , *root stores the address stored by pointer A and **root stores the value at address stored by A. In simple language root=&A, *root= A and **root= *A.

2. if we write *root= 1528 then it means that value at address stored in root becomes 1528 and since address stored in root is the address of pointer A (&A) thus now A=1528 (i.e. address stored in A is 1528) and this change is permanent.

whenever we are changing value of *root we are indeed changing value at address stored in root and since root=&A ( address of pointer A) we are indirectly changing value of A or address stored in A.

so now if A=NULL (list is empty) *root=NULL , thus we create the first node and store its address at *root i.e. indirectly we storing the address of first node at A. If list is not empty , everything is same as done in previous functions using single pointer except we have changed root to *root since what was stored in root is now stored in *root.

Share:
79,692
Admin
Author by

Admin

Updated on January 31, 2022

Comments

  • Admin
    Admin over 2 years

    The two code examples below both add a node at the top of a linked list. But whereas the first code example uses a double pointer the second code example uses a single pointer

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

    code example 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)
    

    Both strategies work. However, a lot of programs that use a linked list use a double pointer to add a new node. I know what a double pointer is. But if a single pointer would be sufficient to add a new node why do a lot of implementations rely on double pointers?

    Is there any case in which a single pointer does not work so we need to go for a double pointer?