Linked list node assignment unexpectedly updates multiple variables

Viewed 324

I am new to linked lists and just started to know the functions behind it. I have a question about assigning nodes. I believe this similar question has been repeated (if so, let me know), but I was watching videos about Linked List and I don't see them explaining of what I am about to ask here.

The question is, when I have a main node head and I assign a temporary node.next inside a method in a class LinkedList, why does head also get assigned as well?

I know you might not follow what I'm asking, but I'll present code and pics to help you understand.

Example code:

public void InsertNodeAtTail(int val)
{
    LinkedListNode node = new LinkedListNode(val);

    if (head == null)
        head = node;
    else
    {
        LinkedListNode current = head;
        while (current.next != null)
        {
            current = current.next;
        }
        current.next = node; //???
    }
}

Basically, this is all done inside a class LinkedList and I am inserting a value at the list's tail.

Where I placed the comment //??? is the line I'm curious about.

I ran a console program in VS2019 and placed breakpoints to understand what's going on. When a breakpoint hits after that line with comment which is }, current.next and head.next get assigned simultaneously. And I expect current.next to ONLY be assigned, not head.

Pics:

Before: https://prnt.sc/rwahho

So, both head.next and current.next are null.

After: https://prnt.sc/rwaiub

Both are assigned! Again I was expecting current.next to ONLY be assigned.

So, why did they both get assigned? Are there any explanation for this to happen? If there is any website or article you know to help me understand this, please let me know.

PS: I've added head.next = current after that line with comment and I ended up getting an endless list.

2 Answers

InsertNodeAtTail will take a value, create a node from it, traverse the list until it gets to the end, and then insert the node.

A linked list is comprised of nodes that have a value and a next property (which points to the next node in the list). The beginning of the list is defined by a node called the head. Traversing the list involves starting at the head and then repeatedly moving to the .Next node (in a loop), until a null value is found (the last node in the list will have a null value for .Next).

The first thing the code does is check to see if head is null. If it is, then it means we have an empty list, so there is no "head" (or "tail"), and therefore the value being inserted becomes the head.

If the head is not null, then we create a node called current which we will use to traverse the list. We start by assigning it to the head node, and we check to see if its .Next property is null.


Answer

At this moment in time, both head and current are referring to the same node. If .Next is null, then we assign our new node to the .Next property. Since both current and head are referring to the same node, both their .Next values are changed.


The next time we call InsertNodeAtTail, current will be assigned to head.Next (since it's not null because we just inserted a node here previously). Then the new value will be inserted by assigning it to that node's .Next property, and the head will remain unchanged (because current is no longer referring to head.


VISUAL REPRESENTATION

So to start with we have a NULL head, so the new node is assigned to head, with a null value for its .Next:

    head          
      ║
      ▼
╔═══════════╗ .Next
║ 1st value ║═══════► NULL
╚═══════════╝

The next time we run, we create a current node and point it to HEAD, and examine its .Next property. Since .Next is null, we insert the new value there. At this moment, both head and current are referring to the same node, so any change to one appears in the other as well:

    head          
      ║
      ▼
╔═══════════╗ .Next   ╔═══════════╗ .Next
║ 1st value ║═══════► ║ 2nd value ║═══════► NULL
╚═══════════╝         ╚═══════════╝
      ▲
      ║
   current

Then next time we run, current will move the the last node in the list (head.Next is not null, so current gets assigned to that node), and we will insert the node there. This time, head will remain unchanged, since current is no longer referring to it:

    head          
      ║
      ▼
╔═══════════╗ .Next   ╔═══════════╗ .Next   ╔═══════════╗ .Next    
║ 1st value ║═══════► ║ 2nd value ║═══════► ║ 3rd value ║═══════► NULL
╚═══════════╝         ╚═══════════╝         ╚═══════════╝
                            ▲
                            ║
                         current

If this still seems confusing, you might want to read about Reference Types.

A simple way to think about it is: head and current both "refer to a location in memory". This means that when we make a change to a property of current (like setting current.Next to some value), we are actually making a change to the object at the address that it's referring to.

So if we do current.Next = someNode, then head.Next will also equal someNode.

But when we make a change to current itself (and not the thing it refers to), by assigning it to a whole new node, then we are changing the address in memory to which it refers.

So if we do an assignment like current = head.Next, then any change to head.Value will not affect current.Value because they're referring to different nodes.


Another way to think about it

Pretend we're looking at houses, and we have two realtors (head and current). We've asked one of them to look at a property (head = newNode;), and we've asked the other one to look at the same property address (current = head), and then asked them each to describe it to us. In this case, we'll get the same answer from each of them (it has 2 beds, 1 bath, is 800 square feet, etc).

And if someone comes and adds a swimming pool to the property (current.Next = anotherNode), the next time we ask either of them about it, they will both tell us that there was a new pool added.

Next imagine that we asked one of them to look at a different address (current = current.Next). This time, when we ask each realtor to describe the current property they're looking at, we'll get two completely different descriptions, because they're looking at two different addresses.

This happens when variables are aliased. LinkedListNode current = head; doesn't create a deep copy of head's memory, it merely gives you an additional variable to use to access it. This is necessary because reassignments need to happen to current in order for it to walk the linked list--we'd lose track of the class' head node if we used it directly in an assignment like head = head.next.

So, as an object in memory, we can assume head looks like this (for a single-element linked list):

.------.
| val  |
| next -> null
`------`
   ^
   |
[variable `head`]

Then, we execute LinkedListNode current = head; (notice that there is no new keyword--no new memory has been allocated). The program's state is now:

.------.
| val  |
| next --> null
`------`
   ^ ^
   | |
   | +----[variable `current`]
   |
[variable `head`]

You can see that two variables point to the same memory location. The line while (current.next != null) is false because next is null, so we never enter the loop body for this degenerate linked list.

Finally, we execute current.next = node;:

      [variable: `node`]
             |
             v
.------.  .------.
| val  |  | val  |
| next -->| next --> null
`------`  `------`
   ^ ^
   | |
   | +----[variable `current`]
   |
[variable `head`]

We've mutated the object that both current and head are references to. This is what your debugger is showing you and it's perfectly normal. When the block ends, head still points to the logical head of the list and we added our new tail. current seems pointless here, but if we'd had a longer list, the while loop would have been executed and current would have stopped aliasing head, enabling us to traverse to the end of the list without losing track of head.

This would look something like, for example, adding a new node to a 3-element list after executing current.next = node;:

                        [variable: `node`]
                                 |
                                 v
.------.  .------.  .------.  .------.
| val  |  | val  |  | val  |  | val  |
| next -->| next -->| next -->| next --> null
`------`  `------`  `------`  `------`
   ^                   ^
   |                   |
   |                   +----[variable `current`]
   |
[variable `head`]
Related