Dummy nodes are fictitious or temporary nodes that are inserted into a data structure or algorithm to assist in handling special cases or simplifying complex scenarios.
For the most part, we will use dummy nodes as a reference to a head node. By introducing a dummy node before the head, we create a stable reference point.
Let's see how we can use a dummy node to solve a problem that we have seen before, "Remove Twos from a Linked List." Below is the problem description from the previous assignment:
// Given the head of a linked list, remove all
// occurrences of the value 2 from the linked list.
// Input: 1 -> 2 -> 3 -> 2 -> 4 -> null
// Output: 1 -> 3 -> 4 -> null
// Input: 2 -> 3 -> 2 -> null
// Output: 3 -> null
// Input: null
// Output: null
If you remember the solution to the problem, you might remember that while iterating through the linked list, when we had a matching value, we also had to check if prev
was null
. If that was the case, we couldn't use prev.next = curr.next
because this would raise an error. Instead, we had to update the head
node to curr.next
. This scenario happened when deleting a 2
from the beginning of the linked list.
This is the part of the code we are referring to:
function deleteTwos(head) {
let prev = null;
let curr = head;
if (!head) {
return head;
}
while (curr) {
if (curr.val === 2) {
#highlight
if (!prev) {
head = curr.next;
#endhighlight
} else {
prev.next = curr.next;
}
} else {
prev = curr;
}
curr = curr.next;
}
return head;
}
Instead of starting with prev = null
, let's create a "dummy" node with a next
pointer that points to the head
node, and assign prev
to this dummy node. With this setup, instead of reassigning head
when we want to delete the first node, we can reassign prev.next
, just like when we delete other nodes. dummy.next
will always point to the current head
node, even if we delete the initial head
node.
Let's see the algorithm for this problem:
dummy
, prev
and curr
.
dummy
to a new node and its next
pointer to head
.
prev
to the dummy
.
curr
to the head
of the linked list.
curr
on each iteration.
curr
has a value of 2
(our target value):
prev.next
to curr.next
to bypass curr
and remove it from the list.
curr
has a value that is not 2
:
prev
pointer to curr
.
curr
pointer to the next node.
dummy.next
, which references the head node of our updated linked list.
Notice that step two has been simplified relative to the algorithm we used for the previous solution to this problem.
Here's a step-by-step walkthrough of removing occurrences of the value 2
from the linked list 2 -> 2 -> 3 -> 2 -> 1
. The output should be a linked list 3 -> 1
. Since we are removing several nodes from the beginning, the "dummy" node will come in handy here.
We initialize three pointers, dummy
, prev
, and curr
. Next, we set the dummy's next
pointer to head
, prev
to dummy
, and curr
to the head of the linked list (1
).
We begin iterating through the linked list, checking the value of curr
on each iteration. At this step, curr
's node has a value of 2
, so we want to remove that node from the list. To do that, we first reassign the next
pointer of the prev
node to curr.next
. Because dummy
points to the same node as prev
, an update to prev.next
is also an update to dummy.next
.
Next, we slide the curr
pointer to point to the next node.
As a result, the head
variable references a node that has been removed from the list. We needed head
for the setup of this solution, but we won't use it in our iterations or thereafter.
We continue traversing the linked list. At the beginning of the third step, our list looks like this.
The current node has the value 2
. So, once again, we reassign the next
pointer of the prev
node to the curr.next
.
Next, we slide the curr
pointer.
At the beginning of the fourth step, this is the state of the list.
We've encountered the node with the value 3
. Since it is not the target node, we slide the prev
and curr
pointers. The prev
pointer is updated to point to the node which contains the value 3
and the curr
pointer is moved to the next node, which contains the value 2
. Notice that dummy
hasn't moved in this case, and its next
pointer points to the new head of the linked list (node with the value 3
).
Let's see the list at the beginning of the fifth step.
Once again, we encounter the node with the value 2
. We reassign the next
pointer of the prev
node to the curr.next
, thus removing this node from the list.
Then, we move the curr
pointer to the next node, which contains the value 1.
At the beginning of this step, the list looks like this.
Now, the value of the current node is 1
. It is not the target value, so we slide the prev
and curr
pointers. prev
now points to the node with the value 1
, and curr
now points to null
. Since curr
points to null
, we are at the end of the list, and we return dummy.next
as the final result, the new head node with the value 3
.
After removing all occurrences of the value 2
, the resulting linked list is 3 -> 1
.
Now, let's take a look at the code implementation of the algorithm:
function deleteTwos(head) {
let dummy = new Node();
dummy.next = head;
let prev = dummy;
let curr = head;
while (curr) {
if (curr.val === 2) {
prev.next = curr.next;
} else {
prev = curr;
}
curr = curr.next;
}
return dummy.next;
}
class ListNode {
constructor(val = 0, next = null) {
this.val = val;
this.next = next;
}
}
function deleteTwos(head) {
let dummy = new ListNode();
dummy.next = head;
let prev = dummy;
let curr = head;
while (curr) {
if (curr.val === 2) {
prev.next = curr.next;
} else {
prev = curr;
}
curr = curr.next;
}
return dummy.next;
}
// Helper function to format the linked list into a string
function stringifyList(head) {
let curr = head;
let result = "";
while (curr !== null) {
result += curr.val + " -> ";
curr = curr.next;
}
result += "null";
return result;
}
// Test case 1
const head1 = new ListNode(1);
head1.next = new ListNode(2);
head1.next.next = new ListNode(3);
head1.next.next.next = new ListNode(2);
head1.next.next.next.next = new ListNode(4);
console.log("Input: ", stringifyList(head1));
console.log("Output:", stringifyList(deleteTwos(head1)));
// Input: 1 -> 2 -> 3 -> 2 -> 4 -> null
// Output: 1 -> 3 -> 4 -> null
// Test case 2
const head2 = new ListNode(2);
head2.next = new ListNode(3);
head2.next.next = new ListNode(2);
console.log("Input: ", stringifyList(head2));
console.log("Output:", stringifyList(deleteTwos(head2)));
// Input: 2 -> 3 -> 2 -> null
// Output: 3 -> null
Throughout this assignment, we have seen how to use dummy nodes to solve problems with linked lists. Using a dummy node, we can ensure that its next pointer always points to the head node, eliminating the need to handle certain edge cases where the head node changes or is removed from the list.