InspectorDanno
InspectorDanno

Reputation: 995

How does the append function of a linked list update a nested node while overwriting the tail?

I'm confused how append() is able to update a nested node in a linked list, despite overwriting this.tail.

How can this.tail.next update both this.tail and whatever this.tail.next is referencing, in the first case, this.head, and so forth? Especially when this.tail is overwritten to newNode right afterwards. Shouldn't that overwrite this.head entirely? How is the next key "saved" despite the entire this.tail being set to newNode?

Separately, I understand that this.tail.next is referencing the node before - but how does the references next key get updated if all we are doing is setting this.tail.next, and not its reference?

To illustrate this point - I set up a variable, potatoes, and another variable, fish, referencing potatoes. Despite updating potatoes, it is clear fish does not get updated as well, even though it is only a reference to potatoes.

let potatoes = 5
let fish = potatoes
potatoes = 2
console.log(fish)

class LinkedList {
  constructor(value) {
    this.head = {
      value,
      next: null
    }
    this.tail = this.head
    this.length = 1
  }
  append(value) {
    const newNode = {
      value,
      next: null
    }
    this.tail.next = newNode
    this.tail = newNode
    this.length++
    return this
  }
 }
 
const linkedList = new LinkedList(5)
linkedList.append(2)
linkedList.append(8)

console.log(linkedList)

Upvotes: 0

Views: 170

Answers (2)

trincot
trincot

Reputation: 350310

How can this.tail.next update both this.tail and whatever this.tail.next is referencing, in the first case, this.head, and so forth?

this.head has a (deep) reference to this.tail via the next property chain. So when you mutate this.tail.next, then this change can be "seen" from any other node that has a next chain that eventually ends up at this.tail.

Especially when this.tail is overwritten to newNode right afterwards. Shouldn't that overwrite this.head entirely?

No, assigning to this.tail does not affect the object that this.tail was referencing before that assignment. The only way to alter that object is to change the value of one of its properties.

How is the next key "saved" despite the entire this.tail being set to newNode?

As said above, the object that this.tail referred to was not altered by this assignment. The assignment just "switches" this.tail to (reference) a different object.

Separately, I understand that this.tail.next is referencing the node before - but how does the reference's next key get updated if all we are doing is setting this.tail.next, and not its reference?

When we do this.tail.next = newNode we are setting a reference to what newNode refers to. At that moment the linked list has become one node longer. When we then follow up with this.tail = newNode we make sure that this.tail references the last node in the list, which is an invariant we want to restore.

It may help to visualise the example you have presented in your script.

When const linkedList = new LinkedList(5) is executed, we set this.head to a new node, and we get this state:

 linkedList (this)
  ↓
┌───────────┐    ┌───────────┐
│ head: ───────> │ value: 5  │
│           │    │ next: null│
└───────────┘    └───────────┘ 

The left box is the linked list instance, and the right box is the (first) Node instance.

The constructor continues with this.tail = this.head. This merely copies the reference to the new object into a new tail property:

 linkedList (this)
  ↓
┌───────────┐    ┌───────────┐
│ head: ───────> │ value: 5  │
│ tail: ───────> │ next: null│
└───────────┘    └───────────┘ 

I skip the length property management here, as it is not relevant to the question.

Then linkedList.append(2) is executed. First newNode gets a reference to a new node which is for the moment is not related to the list in any way:

 linkedList (this)
  ↓
┌───────────┐    ┌───────────┐    ┌───────────┐
│ head: ───────> │ value: 5  │    │ value: 2  │
│ tail: ───────> │ next: null│    │ next: null│
└───────────┘    └───────────┘    └───────────┘
                                    ↑
                                   newNode

...and this reference is assigned to this.tail.next as well:

 linkedList (this)
  ↓
┌───────────┐    ┌───────────┐    ┌───────────┐
│ head: ───────> │ value: 5  │    │ value: 2  │
│ tail: ───────> │ next: ───────> │ next: null│
└───────────┘    └───────────┘    └───────────┘
                                    ↑
                                   newNode

So the effect of this.tail.next = newNode is reflected by middle horizontal arrow. Note that indeed newNode and this.tail.next (if you follow the arrows) refer to the same node.

Now we execute this.tail = newNode. This is the (simple) effect of that:

 linkedList (this)
  ↓
┌───────────┐    ┌───────────┐    ┌───────────┐
│ head: ───────> │ value: 5  │    │ value: 2  │
│ tail: ──────┐  │ next: ───────> │ next: null│
└───────────┘ │  └───────────┘ ┌> └───────────┘
              └────────────────┘    ↑
                                   newNode

Note that with such an assignment mutates the linked list instance, but not any node instance. The append method finishes, and the newNode variable gets out of scope and is no longer relevant.

Let's continue with linkedList.append(8). Again a newNode variable gets a reference to a new node:

 linkedList (this)
  ↓
┌───────────┐    ┌───────────┐    ┌───────────┐    ┌───────────┐
│ head: ───────> │ value: 5  │    │ value: 2  │    │ value: 8  │
│ tail: ──────┐  │ next: ───────> │ next: null│    │ next: null│
└───────────┘ │  └───────────┘ ┌> └───────────┘    └───────────┘
              └────────────────┘                     ↑
                                                    newNode

...and again we mutate the node that tail is referring to with this.tail.next = newNode:

 linkedList (this)
  ↓
┌───────────┐    ┌───────────┐    ┌───────────┐    ┌───────────┐
│ head: ───────> │ value: 5  │    │ value: 2  │    │ value: 8  │
│ tail: ──────┐  │ next: ───────> │ next: ───────> │ next: null│
└───────────┘ │  └───────────┘ ┌> └───────────┘    └───────────┘
              └────────────────┘                     ↑
                                                    newNode

...and the only thing that remains is to let tail reference the last node (the invariant) with this.tail.next = newNode:

 linkedList (this)
  ↓
┌───────────┐    ┌───────────┐    ┌───────────┐    ┌───────────┐
│ head: ───────> │ value: 5  │    │ value: 2  │    │ value: 8  │
│ tail: ──────┐  │ next: ───────> │ next: ───────> │ next: null│
└───────────┘ │  └───────────┘    └───────────┘ ┌> └───────────┘
              └─────────────────────────────────┘    ↑
                                                    newNode

Hope this clarifies it.

Upvotes: 2

Invizi
Invizi

Reputation: 1298

Here is why it changes both the tail.next and tail to the appended Node.

// This is setting the new Node to be the next Node to the tail Node.
this.tail.next = newNode

// Now it's setting the new node as the tail.
this.tail = newNode

To answer the reference question, In JavaScript only Arrays and Objects can be referenced, so in your pototoe example, it's not being referenced, but is assigning the value of the other variable.

// Numbers, Strings and booleans, can't be referenced.
let potatoes = 5
let fish = potatoes
potatoes = 2
console.log("Number:", fish)

let orange = "5"
let raspberry = orange
orange = "2"
console.log("String:", raspberry)

let pineapple = false
let watermelon = pineapple
pineapple = true
console.log("Boolean:", watermelon)

// Arrays and Objects can.
let pear = [5]
let apple = pear
pear[0] = 2
console.log("Array:", apple[0])

let banana = {
  value: 5
}
let grape = banana
banana.value = 2
console.log("Object:", grape.value)

Upvotes: 1

Related Questions