A singly linked list is a collection of nodes where each node contains a value and a reference (or pointer) to the next node in the sequence. The list maintains a reference to the first node head and the last node tail, and it keeps track of the number of nodes, length.
Arrays and objects are excellent data structures for most applications. However, when you start dealing with large datasets (in the order of hundreds of thousands or more) having the right data structure becomes crucial.
Singly linked lists serve as the foundation for other data structures like stacks, queues, graphs, and trees.
Now that we've talked about the why and the when I guess we should go into the how.
First, let's define the Node class, which will represent each element in the linked list.
class Node {
constructor(value) {
this.value = value;
this.next = null;
}
}
Next, we'll define the SinglyLinkedList class, which will manage the nodes and provide methods to manipulate the list.
class SinglyLinkedList {
constructor() {
this.head = null;
this.tail = null;
this.length = 0;
}
}
function push(value) {
const newNode = new Node(value);
if (this.length === 0) {
this.head = newNode;
this.tail = this.head;
} else {
this.tail.next = newNode;
this.tail = newNode;
}
this.length++;
return this;
}
Explanation: The push method creates a new node with the given value. If the list is empty, we set both the head and tail to the new node. Otherwise, we append the new node to the end of the list by updating the next property of the current tail and then setting the new node as the tail. The length is incremented, and the updated list is returned.
function pop() {
if (!this.head) return undefined;
let current = this.head;
let newTail = current;
while (current.next) {
newTail = current;
current = current.next;
}
this.tail = newTail;
this.tail.next = null;
this.length--;
if (this.length === 0) {
this.head = null;
this.tail = null;
}
return current;
}
Explanation: The pop method first checks if the list is empty and returns undefined if it is. Otherwise, it traverses the list to find the second-to-last node which we set as the tail and updates its next property to null, effectively unlinking the previous tail, allowing it to be garbage collected. The length is decremented, and in case the list became empty, the head and tail are set to null. The removed node is returned.
As for array, the shift method is used to remove a node from the beginning of the list. Unlike with arrays this process doesn't involves reassign every node's index so here it's pretty much where linked list excels:
function shift() {
if (!this.head) return undefined;
let current_head = this.head;
this.head = current_head.next;
this.length--;
if (this.length === 0) {
this.tail = null;
}
return current_head;
}
See how few operations there are?
Explanation: We first check if the list is empty and returns undefined if it is. Otherwise, we store the head before overriding it with its next node then we decrement the length and finally we return the node we removed. Also don't forget to clean up the tail if there is no more items as we only took care of the head by setting it to current_head.next which is null when there is only one element.
Next stop unshift, the opposite of shift, adding a new node to the beginning of the list, again this is where linked list excel, let's see how:
function unshift(value) {
let new_node = new Node(value);
if (!this.head) {
this.head = new_node;
this.tail = this.head;
} else {
new_node.next = this.head;
this.head = new_node;
}
this.length++;
return this;
}
Explanation: Quite simple again, we create a new node with the given value. If the list is empty, we set both the head and tail to the new node. Otherwise, we set the next property of the new node to the current head and update the head to be the new node. The length is incremented, and the updated list is returned.
How about the get method? With arrays we can easily get an item by its index arrindex but with a linked list it's a little harder as nodes are not referenced by index, they're like free agents linked to each other.
function get(index) {
if (index < 0 || index >= this.length) {
return undefined;
}
let item = this.head;
for (let i = 0; i < index; i++) {
item = item.next;
}
return item;
}
Explanation: Because nodes are not indexed we need to traverse the whole list to find our node and to prevent our traversal to run out of the list we need a guard clause, returning undefined if the index parameter is out of bounds. Otherwise we will define a variable item starting from the head (which will be returned if the index parameter is 0) then we will loop through the list going from item.next to item.next until the index parameter is superior to the current iterator i meaning our item variable is currently storing the desired item.
function set(index, value) {
const item = this.get(index);
if (!item) return false;
item.value = value;
return true;
}
Explanation: Here it's quite simple we use our previously defined get method to find the item, if the item is undefined we return false otherwise we update the value of the item and return true.
function insert(value, index) {
if (index >= this.length) {
return !!this.push(value);
} else if (index <= 0) {
return !!this.unshift(value);
}
const previous_item = this.get(index - 1);
const next_item = previous_item.next;
const new_item = new Node(value);
previous_item.next = new_item;
new_item.next = next_item;
this.length++;
return true;
}
Explanation: Let's recap, there are a few scenarios we need to handle:
function remove(index) {
if (index < 0 || index >= this.length) return undefined;
else if (index === 0) return this.shift();
else if (index === this.length - 1) return this.pop();
const previous = this.get(index - 1);
const next_item = previous.next;
previous.next = next_item.next;
this.length--;
return next_item;
}
Explanation: So here again we have some edge cases to handle:
The length is decremented, and the removed node is returned.
Last but not least, the reverse function this one is totally optional and is mostly good to know for interviews as it could randomly show up.
function reverse() {
let temp = null;
let current = this.head;
let prevNode = null;
while (current != null) {
temp = current.next;
current.next = prevNode;
prevNode = current;
current = temp;
}
this.head = prevNode;
return this;
}
This is a tough one so I'll try my best to guide you through:
Now that we have everything set up we traverse the list, reversing each node's next pointer to point to the previous node:
4. Store current.next in temp so we don't lose the rest of the list.
5. Set current.next = prevNode — this is the reversal step. On the first iteration, the head becomes the tail so it points to null.
6. Advance prevNode = current and current = temp to move forward.
7. After the loop, prevNode holds the new head. We assign it to this.head and return the list.
Congrats 🎉 you made it through I hope you learned a lot, I certainly did and if you have any suggestions, improvements or anything that comes to your mind don't hesitate to write! I'll reply as soon as possible!
Happy coding! 🚀
Honestly i am no one, i've just been coding for 3 years now and i like to document every solutions to every problem i encounter. Extracting as much code snippets and tutorials i can so that if i ever need it again i just have to pop back here and get a quick refresher.
Feel free to me through this learning journey by providing any feedback and if you wanna support me: