Introduction
When learning Data Structures and Algorithms, one of the first things you realise is that they are not just abstract concepts, they are the hidden logic that makes everyday software efficient.
As I continue my DSA study journey, this post is the next step in my ongoing series. The previous one explored hashmaps, stacks, and queues. These three structures handle data efficiently using different patterns of access and order. You can check that one out here.
In this blog, we move on to another fundamental structure that underpins many others, the Linked List.
At first glance, a linked list might look similar to an array since both store collections of items. But the difference lies in how they’re connected. Instead of every item sitting next to each other in memory like an array, a linked list connects elements through pointers, forming a flexible chain that can grow and shrink without rearranging everything.
By the end of this blog, you will understand how linked lists work, how to create one in JavaScript, and how common operations like inserting and deleting elements actually happen under the hood, as we do not have a built-in one in JavaScript.
What is a Linked List?
A linked list is a linear data structure made up of individual elements called nodes, where each node stores two things:
- A value, which is the data itself.
- A pointer (or reference) to the next node in the sequence.
That pointer acts as a link, joining each node to the next one, which is why it’s called a linked list. Unlike arrays, the nodes aren’t stored next to each other in memory. Instead, each one "knows" where the next is located.
You can imagine it like a chain of connected train carriages.
Each carriage holds some cargo (the value) and a connector (the pointer) that links it to the next carriage.
If you want to add or remove a carriage, you simply adjust the connectors without moving every other one in the chain.
This makes linked lists more flexible than arrays when it comes to inserting or deleting elements. You don’t have to shift indexes or worry about reallocating memory, you just redirect links between nodes.
However, this flexibility comes with a trade‑off. Because elements aren’t indexed by position, finding an element means starting at the head and moving through each node one by one. That’s why linked lists have O(n) access time instead of the O(1) access you get with arrays.
They can also get a bit tricky. When you’re updating pointers between nodes, you often need to store a temporary reference. Without that extra step, it’s easy to accidentally break a link and lose part of the list.
Before we begin coding, let’s break down the two building blocks of a linked list: the Node and the LinkedList class that manages them.
Building the Linked List
To understand how a linked list works, we’ll start from the smallest building block, the Node.
Each node stores a value and a reference (or pointer) to the next node in the list.
In JavaScript, that looks like this:
class Node {
constructor(value) {
this.value = value;
this.next = null;
}
}
value holds the actual data you want to store.
next is a pointer to the next node in the list.
If next is null, that means the node is the end of the list.
So, if we were to manually connect a few nodes:
const node1 = new Node("A");
const node2 = new Node("B");
const node3 = new Node("C");
node1.next = node2;
node2.next = node3;
console.log(node1);
The result would look something like this:
A -> B -> C -> null
Each node knows where the next one is, that’s all a linked list really is at its core.
Now that we understand how nodes link together, we’ll create a LinkedList class to manage them, adding new nodes, checking the list’s size, and performing operations like append, prepend, and delete.
Let’s start with the basic structure:
class LinkedList {
length = 0;
head = null;
size() {
return this.length;
}
isEmpty() {
return this.length === 0;
}
}
Here’s what each part does:
head stores the first node in the list.
length keeps track of how many nodes exist.
size() returns the total number of nodes.
isEmpty() is a quick way to check if the list has no elements.
We’ve now set up the foundation. Next, we’ll add methods that actually make the linked list come alive, starting with how to append new nodes to the end.
Append (Add to End)
Let’s start by adding a new node to the end of our linked list, this process is often called append.
Appending means we create a new node, then place it after the last element currently in the list.
If the list is empty, this new node simply becomes the head.
Here’s what it looks like in code:
append(value) {
let node = new Node(value);
if (this.head == null) {
this.head = node;
this.length++;
return;
}
let curr = this.head;
while (curr.next != null) {
curr = curr.next;
}
curr.next = node;
this.length++;
}
First, we create a node using the Node class, then we check if the list is empty. If so, the new node becomes the head, as this is where we access the linked list through its head.
If the linked list isn't empty, we loop through the nodes using a while loop until we reach the last one, the node where next is null. This means that we have reached the end of the linked list.
Then, we set that node’s next pointer to our new node and increase the length.
A quick example:
const list = new LinkedList();
list.append("A");
list.append("B");
list.append("C");
console.log(list.head);
// Node { value: 'A', next: Node { value: 'B', next: Node { value: 'C', next: null } } }
Results:
A -> B -> C -> null
Time Complexity: O(n) we must traverse the entire list to find the last node.
Space Complexity: O(1) we create only one new node regardless of list size.
Appending is one of the most common list operations, and while it’s linear, it’s still efficient compared to rearranging an array’s memory like you would for insertions there.
Prepend (Add to Start)
While append adds a node to the end of the list, prepend adds one to the beginning, right before the current head.
This operation is actually faster than appending because we don’t need to traverse the entire list.
We simply create a new node and point it to the existing head. Then, we update the head to the new node.
Here’s the implementation:
prepend(value) {
let node = new Node(value);
if (this.head == null) {
this.head = node;
this.length++;
return;
}
node.next = this.head;
this.head = node;
this.length++;
}
We create a new node with the given value. If the list is empty, that node becomes the head.
If not, we point the new node's next to the current head and then resign the head to the new node. Finally, we increase the length to keep the count accurate.
Example:
const list = new LinkedList();
list.append("B");
list.append("C");
list.prepend("A");
console.log(list.head);
// Node { value: 'A', next: Node { value: 'B', next: Node { value: 'C', next: null } } }
Results:
A -> B -> C -> null
Time Complexity: O(1) we add the node directly at the start, no traversal needed.
Space Complexity: O(1) only one new node created.
Compared to arrays, where inserting at the start requires shifting every other element, linked lists make this process effortless.
Delete (Remove a Node)
Now that we can add items to our linked list, it’s time to remove them.
The delete() method lets us remove a specific node by its value.
Deleting in a linked list can be a little more involved than adding, because we need to handle a few special cases:
The list might be empty.
The node we want to remove might be the head.
Or it might be somewhere in the middle or end.
Here’s what the method looks like:
delete(value) {
if (this.head == null) {
return "Error: linked list is empty";
}
if (this.head.value == value) {
let head = this.head;
this.head = head.next;
head.next = null;
this.length--;
return head;
}
let curr = this.head;
while (curr.next != null) {
if (curr.next.value == value) {
break;
}
curr = curr.next;
}
if (curr.next == null) {
return "Value wasn't in linked list";
}
let returningNode = curr.next;
curr.next = returningNode.next;
returningNode.next = null;
this.length--;
return returningNode;
}
We first check if the list is empty; if so, there is nothing to delete, so we return an error message.
Next, we check the head node matches the value we want to delete. If it does we point to the next node and detach the old one.
Otherwise, we loop through each node until the next one matches the value we want to remove. Once it’s found, we reconnect links by skipping over the target node, effectively "snipping" it out of the list.
Example:
const list = new LinkedList();
list.append("A");
list.append("B");
list.append("C");
list.delete("B");
console.log(list.head);
// Node { value: 'A', next: Node { value: 'C', next: null } }
Result:
A -> C -> null
Time Complexity: O(n) in the worst case, we might need to traverse the entire list to find the node.
Space Complexity: O(1) we only adjust pointers, no new data is stored.
Deleting elements in a linked list is flexible and efficient once you grasp the logic, but it’s also where most pointer mistakes happen.
A single wrong assignment can disconnect part of your list. Always check references carefully when removing nodes.
Get First and Get Last
Once we’ve added and deleted elements, the next step is being able to access specific nodes within the list.
The two simplest operations for this are getFirst() and getLast(). Retrieving the first and last nodes without modifying the list.
Let’s start with getting the first node:
getFirst() {
return this.head;
}
That’s as simple as it gets. The head of the list is the first node. If the list is empty, this will just return null.
Now for the last node:
getLast() {
if (this.head === null) {
return null;
}
let curr = this.head;
while (curr.next !== null) {
curr = curr.next;
}
return curr;
}
We start at the head, we keep moving through the nodes using each next pointer until we find the one whose next is null. That node is the last in the list.
Example:
const list = new LinkedList();
list.append("A");
list.append("B");
list.append("C");
console.log(list.getFirst().value); // A
console.log(list.getLast().value); // C
getFirst() → Time: O(1), Space: O(1)
getLast() → Time: O(n), Space: O(1), since we traverse through each node to reach the end.
Now that we’ve built all the core methods, let’s look at the full linked list in action.
Below is the complete class setup, including everything we’ve learned from creating nodes to appending, prepending, and deleting.
class Node {
constructor(value) {
this.value = value;
this.next = null;
}
}
class LinkedList {
length = 0;
head = null;
size() {
return this.length;
}
isEmpty() {
return this.length === 0;
}
append(value) {
let node = new Node(value);
if (this.head == null) {
this.head = node;
this.length++;
return;
}
let curr = this.head;
while (curr.next != null) {
curr = curr.next;
}
curr.next = node;
this.length++;
}
prepend(value) {
let node = new Node(value);
if (this.head == null) {
this.head = node;
this.length++;
return;
}
node.next = this.head;
this.head = node;
this.length++;
}
delete(value) {
if (this.head == null) {
return "Error: linked list is empty";
}
if (this.head.value == value) {
let head = this.head;
this.head = head.next;
head.next = null;
this.length--;
return head;
}
let curr = this.head;
while (curr.next != null) {
if (curr.next.value == value) {
break;
}
curr = curr.next;
}
if (curr.next == null) {
return "Value wasn't in linked list";
}
let returningNode = curr.next;
curr.next = returningNode.next;
returningNode.next = null;
this.length--;
return returningNode;
}
getFirst() {
return this.head;
}
getLast() {
if (this.head === null) {
return null;
}
let curr = this.head;
while (curr.next !== null) {
curr = curr.next;
}
return curr;
}
}
Now that the full class is ready, let’s test it out with a few operations:
const list = new LinkedList();
list.append("A");
list.append("B");
list.append("C");
console.log("First:", list.getFirst().value); // A
console.log("Last:", list.getLast().value); // C
console.log("Size:", list.size()); // 3
list.prepend("Start");
list.delete("B");
console.log("After Updates:");
console.log("First:", list.getFirst().value); // Start
console.log("Last:", list.getLast().value); // C
console.log("Is Empty:", list.isEmpty()); // false
console.log("Size:", list.size()); // 3
Results:
First: A
Last: C
Size: 3
After Updates:
First: Start
Last: C
Is Empty: false
Size: 3
When you run this code, you can see how nodes connect and adjust as data is added or removed.
There’s no array indexing or shifting, everything happens through simple pointer management between nodes.
This chain‑like structure is what makes linked lists both simple and powerful, especially when dealing with dynamic collections of data.
Conclusion
And that’s our overview of Linked Lists, a simple but powerful data structure that forms the base for many others like stacks, queues, and graphs.
Now that you know how to build and manage one in JavaScript, you’ve taken another solid step in understanding how data really moves under the hood.
If you enjoyed this blog and want to keep learning, check out these other blogs:
Thanks for reading and happy coding as you keep building your DSA skills!
Top comments (0)