0

I am reading the book "C Programming: A Modern Approach" by KN King, where in chapter 17.5 on page 432 they define a function for deleting a node from a singly linked list:

The following function, delete_from_list, uses the strategy that we've just outlined. When given a list and an integer n, the function deletes the first node containing n. If no node contains n, delete_from_list does nothing. In either case, the function returns a pointer to the list.

struct node *delete_from_list(struct node *list, int n)
{
  struct node *cur, *prev;

  for (cur = list, prev = NULL;
       cur != NULL && cur->value != n;
       prev = cur, cur = cur->next)
     ;
  if (cur == NULL)
    return list;               /* n was not found */
  if (prev == NULL)
    list = list->next;         /* n is in the first node */
  else
    prev->next = cur->next;    /* n is in some other node */
  free(cur);
  return list;
}

The book refers to this function in an exercise on page 454:

Exercises

  1. Modify the delete_from_list function so that is uses only one pointer variable instead of two (cur and prev).

My question is about the following solution I found on Github:

struct node *delete_from_list(struct node **list, int n) {
    struct node *item = *list;
    while (item) {
        if (item->value == n) {
            *list = item->next;
            free(item);
            break;
        }
        list = &item->next;
        item = item->next;
    }
    return *list;
}

I understand what a pointer to a pointer is. But I don't understand what happens after the line struct node **list. I would very much appreciate it if anybody could explain this solution line by line with visualization.

trincot
  • 317,000
  • 35
  • 244
  • 286
Fary
  • 77
  • 4
  • Pointers are commonly used to emulate *pass by reference* in C. Remember that in C all arguments are passed by value, meaning the value in the call is copied into the functions local argument variable. Modifying the local argument variable only modifies this local copy, not the original value that was used in the call. By passing a pointer to a variable (using the pointer-to operator `&`) you can modify the original variable by dereferencing the pointer. – Some programmer dude Apr 18 '23 at 06:00
  • As for the function itself and what it does, I suggest some [rubber duck debugging](https://en.wikipedia.org/wiki/Rubber_duck_debugging). Explain the code in detail to a rubber duck, other inanimate object, or even a friend, while at the same time draw everything that happens. Use labeled boxes for variable, and arrows for pointers. Erase and redraw the arrows as the pointers in the code are modified. That is a great way to visualize pointers and lists. – Some programmer dude Apr 18 '23 at 06:02
  • 1
    I do not think the solution is correct. The original function returns a pointer to the new first node (list head). The modified function returns a pointer to the node following the deleted node or a pointer to NULL if `n` was not found. – nielsen Apr 18 '23 at 07:46
  • The "modern approach" is probably to avoid heap fragmented linked lists entirely, since they are very slow and very error prone :) It is some remain from before the dawn of high-end CPUs like 486 or newer... There are still some uses for linked lists but most likely never with fragmented malloc calls. – Lundin Apr 18 '23 at 10:05

1 Answers1

1

I would very much appreciate it if anybody could explain this solution line by line with visualization.

Let's take an example linked list with values 1, 2, and 3, where head points to its first node, and we have a value n that equals 2. We can picture the state of the list in the main program as follows:

      ┌───────┐   ┌─────────────┐  ┌─────────────┐  ┌──────────────┐
      │ head ────►│ ┌─────────┐ │  │ ┌─────────┐ │  │ ┌──────────┐ │
      └───────┘   │ │ value 1 │ │  │ │ value 2 │ │  │ │ value 3  │ │
                  │ └─────────┘ │  │ └─────────┘ │  │ └──────────┘ │
                  │ ┌─────────┐ │  │ ┌─────────┐ │  │ ┌──────────┐ │
                  │ │ next────────►│ │ next────────►│ │ next NULL│ │
                  │ └─────────┘ │  │ └─────────┘ │  │ └──────────┘ │
                  │             │  │             │  │              │
                  └─────────────┘  └─────────────┘  └──────────────┘

With this solution code, the caller must pass a pointer to a pointer to a node, and so if the head variable in the main program is a pointer to the first node, we need to pass its address, like so:

delete_from_list(&head, n);

Let's now picture how the function sees its own list variable:

      ┌───────┐
      │ list  │
      └───│───┘
          ▼
      ┌───────┐   ┌─────────────┐  ┌─────────────┐  ┌──────────────┐
      │ head ────►│ ┌─────────┐ │  │ ┌─────────┐ │  │ ┌──────────┐ │
      └───────┘   │ │ value 1 │ │  │ │ value 2 │ │  │ │ value 3  │ │
                  │ └─────────┘ │  │ └─────────┘ │  │ └──────────┘ │
                  │ ┌─────────┐ │  │ ┌─────────┐ │  │ ┌──────────┐ │
                  │ │ next────────►│ │ next────────►│ │ next NULL│ │
                  │ └─────────┘ │  │ └─────────┘ │  │ └──────────┘ │
                  │             │  │             │  │              │
                  └─────────────┘  └─────────────┘  └──────────────┘

The first statement is:

struct node *item = *list;

So we get:

      ┌───────┐   ┌───────┐
      │ list  │   │ item  │
      └───│───┘   └───│───┘
          ▼           ▼
      ┌───────┐   ┌─────────────┐  ┌─────────────┐  ┌──────────────┐
      │ head ────►│ ┌─────────┐ │  │ ┌─────────┐ │  │ ┌──────────┐ │
      └───────┘   │ │ value 1 │ │  │ │ value 2 │ │  │ │ value 3  │ │
                  │ └─────────┘ │  │ └─────────┘ │  │ └──────────┘ │
                  │ ┌─────────┐ │  │ ┌─────────┐ │  │ ┌──────────┐ │
                  │ │ next────────►│ │ next────────►│ │ next NULL│ │
                  │ └─────────┘ │  │ └─────────┘ │  │ └──────────┘ │
                  │             │  │             │  │              │
                  └─────────────┘  └─────────────┘  └──────────────┘

Execution enters the loop, but the if condition is false, so the next executed statement is:

list = &item->next;

Now the local list variable will point to the address of the next member of the node that item points to:

                  ┌───────┐
                  │ item  │
                  └───│───┘
                      ▼
      ┌───────┐   ┌─────────────┐  ┌─────────────┐  ┌──────────────┐
      │ head ────►│ ┌─────────┐ │  │ ┌─────────┐ │  │ ┌──────────┐ │
      └───────┘   │ │ value 1 │ │  │ │ value 2 │ │  │ │ value 3  │ │
                  │ └─────────┘ │  │ └─────────┘ │  │ └──────────┘ │
                  │ ┌─────────┐ │  │ ┌─────────┐ │  │ ┌──────────┐ │
                  │ │ next────────►│ │ next────────►│ │ next NULL│ │
                  │ └─────────┘ │  │ └─────────┘ │  │ └──────────┘ │
                  │   ▲         │  │             │  │              │
                  └───│─────────┘  └─────────────┘  └──────────────┘
                      │
                  ┌───│───┐
                  │ list  │
                  └───────┘

The last statement in the loop's body is:

item = item->next;

So we get:

                                   ┌───────┐
                                   │ item  │
                                   └───│───┘
                                       ▼
      ┌───────┐   ┌─────────────┐  ┌─────────────┐  ┌──────────────┐
      │ head ────►│ ┌─────────┐ │  │ ┌─────────┐ │  │ ┌──────────┐ │
      └───────┘   │ │ value 1 │ │  │ │ value 2 │ │  │ │ value 3  │ │
                  │ └─────────┘ │  │ └─────────┘ │  │ └──────────┘ │
                  │ ┌─────────┐ │  │ ┌─────────┐ │  │ ┌──────────┐ │
                  │ │ next────────►│ │ next────────►│ │ next NULL│ │
                  │ └─────────┘ │  │ └─────────┘ │  │ └──────────┘ │
                  │   ▲         │  │             │  │              │
                  └───│─────────┘  └─────────────┘  └──────────────┘
                      │
                  ┌───│───┐
                  │ list  │
                  └───────┘

In the next iteration the if condition is true, so then we execute this:

*list = item->next;

Which mutates the list as follows:

                                   ┌───────┐
                                   │ item  │
                                   └───│───┘
                                       ▼
      ┌───────┐   ┌─────────────┐  ┌─────────────┐  ┌──────────────┐
      │ head ────►│ ┌─────────┐ │  │ ┌─────────┐ │  │ ┌──────────┐ │
      └───────┘   │ │ value 1 │ │  │ │ value 2 │ │  │ │ value 3  │ │
                  │ └─────────┘ │  │ └─────────┘ │  │ └──────────┘ │
                  │ ┌─────────┐ │  │ ┌─────────┐ │  │ ┌──────────┐ │
                  │ │ next────────┐│ │ next────────►│ │ next NULL│ │
                  │ └─────────┘ │ ││ └─────────┘ │  │ └──────────┘ │
                  │   ▲         │ ││             │┌►│              │
                  └───│─────────┘ │└─────────────┘│ └──────────────┘
                      │           └───────────────┘
                  ┌───│───┐
                  │ list  │
                  └───────┘

Now the found node can be freed with free(item):

                                   ┌───────┐
                                   │ item  │
                                   └───│───┘
                                       ▼
      ┌───────┐   ┌─────────────┐                   ┌──────────────┐
      │ head ────►│ ┌─────────┐ │                   │ ┌──────────┐ │
      └───────┘   │ │ value 1 │ │                   │ │ value 3  │ │
                  │ └─────────┘ │                   │ └──────────┘ │
                  │ ┌─────────┐ │                   │ ┌──────────┐ │
                  │ │ next─────────────────────────►│ │ next NULL│ │
                  │ └─────────┘ │                   │ └──────────┘ │
                  │   ▲         │                   │              │
                  └───│─────────┘                   └──────────────┘
                      │
                  ┌───│───┐
                  │ list  │
                  └───────┘

Finally the function returns *list, but that is of little use: the caller should not use that return value for assignment to its own head variable, or things would go bad. The caller should just rely on the function to adapt its head if necessary (not the case in this example).

This is what the caller ends up with:

      ┌───────┐   ┌─────────────┐                   ┌──────────────┐
      │ head ────►│ ┌─────────┐ │                   │ ┌──────────┐ │
      └───────┘   │ │ value 1 │ │                   │ │ value 3  │ │
                  │ └─────────┘ │                   │ └──────────┘ │
                  │ ┌─────────┐ │                   │ ┌──────────┐ │
                  │ │ next─────────────────────────►│ │ next NULL│ │
                  │ └─────────┘ │                   │ └──────────┘ │
                  │             │                   │              │
                  └─────────────┘                   └──────────────┘

Remarks

In my opinion this is not really a "fair" solution, as it changes the signature of the function:

  • The caller must pass a double pointer instead of single pointer, and
  • The caller should not use the returned pointer to assign it back to their own list pointer. Instead, the function returns a pointer to the successor node, which in most cases is not interesting information, so the caller could just ignore the return value.

The original function would have to be called like this:

head = delete_from_list(head, n);

...while this function should be called like this:

delete_from_list(&head, n);

For the above reasons, I am not sure this was the intended solution for the exercise. For other ideas to solve this, see this Q&A.

trincot
  • 317,000
  • 35
  • 244
  • 286