Singly Linked List
Understand the structure of singly linked lists, implement core operations, and compare their performance with doubly linked lists and arrays.
Singly Linked List
A singly linked list is a sequence of nodes connected in one direction. Each node stores a value and a next reference to the following node.
The list keeps a head reference to its first node and a tail reference to its last node. The tail's next points to null, which marks the end of the list.
Think of the structure like a scavenger hunt:
- You only know where the first clue is.
- Each clue reveals a message and gives you directions to find the next clue.
- The final clue points to nothing (
null), signaling that the hunt is over.
Key Property: Unidirectional
In a singly linked list, connections only go forward: each node points to the next, never to the previous one. To find any element, you must start from the head and traverse forward one link at a time.
Anatomy of a Singly Linked List
| Component | Description |
|---|---|
| Node | Stores a value and a next reference. |
| Head | References the first node. |
| Tail | References the last node. Its next is null. |
| Length | Tracks the number of nodes for fast boundary checks. |
Visualizing a Node
Each node is an independent object with two properties:
Defining the Node and List
To implement a singly linked list in code, we define two classes:
Node: Holds the data and the reference to the next node.SinglyLinkedList: Manages the chain, trackinghead,tail, andlength.
class Node {
constructor(val) {
this.val = val;
this.next = null;
}
}
class SinglyLinkedList {
constructor() {
this.head = null;
this.tail = null;
this.length = 0;
}
}Tracking tail and length gives us two immediate performance advantages:
- Appending items to the end runs in constant time instead of requiring a full traversal.
- Checking the number of items or validating index boundaries runs in time.
Core Operations
1. Adding to the End (push)
The push method appends a new node to the end of the list.
Step-by-Step Logic
- Create a new node with the provided value.
- If the list is empty (
!this.head), point bothheadandtailto the new node. - Otherwise, set the current
tail.nextto point to the new node, and updatetailto be the new node. - Increment
lengthby 1.
push(val) {
const newNode = new Node(val);
if (!this.head) {
this.head = newNode;
this.tail = newNode;
} else {
this.tail.next = newNode;
this.tail = newNode;
}
this.length++;
return this;
}Time Complexity: — Direct pointer update through the tail pointer.
2. Adding to the Beginning (unshift)
The unshift method prepends a new node to the start of the list, making it the new head.
Step-by-Step Logic
- Create a new node with the provided value.
- If the list is empty, set both
headandtailto the new node. - Otherwise, set the new node's
nextpointer to reference the currenthead. - Update
headto reference the new node. - Increment
lengthby 1.
unshift(val) {
const newNode = new Node(val);
if (!this.head) {
this.head = newNode;
this.tail = newNode;
} else {
newNode.next = this.head;
this.head = newNode;
}
this.length++;
return this;
}Time Complexity: — Unlike arrays where prepending requires shifting every element to the right (), adding to the head of a linked list is instant.
3. Removing from the Beginning (shift)
The shift method removes the node at the head of the list and returns it.
Step-by-Step Logic
- If the list is empty, return
null. - Save the current
headnode in a temporary variable. - Advance
headtohead.next. - Decrement
lengthby 1. - If
lengthreaches 0, settailtonull. - Sever the removed node's
nextpointer and return it.
shift() {
if (!this.head) return null;
const removedNode = this.head;
this.head = this.head.next;
this.length--;
if (this.length === 0) {
this.tail = null;
}
removedNode.next = null;
return removedNode;
}Time Complexity: — Constant time. In an array, removing from index 0 takes time because all remaining elements must shift left.
4. Removing from the End (pop)
The pop method removes the node at the tail of the list and returns it.
Why is Pop O(n) in a Singly Linked List?
Even though we have a tail pointer, removing the tail requires pointing the second-to-last node to null and making it the new tail. Because singly linked lists only point forward, we must traverse the entire list from the head to locate that second-to-last node.
Step-by-Step Logic
- If the list is empty, return
null. - Start two pointers at
head:currentandnewTail. - Loop while
current.nextexists: advancenewTailtocurrent, andcurrenttocurrent.next. - When the loop finishes,
currentis the tail, andnewTailis the second-to-last node. - Set
tailtonewTailand disconnect itsnextpointer (tail.next = null). - Decrement
lengthby 1. - If
lengthreaches 0, reset bothheadandtailtonull. - Return
current.
pop() {
if (!this.head) return null;
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;
}Time Complexity: — Must traverse to the second-to-last node.
5. Accessing by Index (get)
The get method retrieves the node at a given 0-based index.
Unlike arrays, where index lookups are instant () using memory offsets, a singly linked list must count forward from head step by step.
Step-by-Step Logic
- Validate the index: if
index < 0orindex >= this.length, returnnull. - Start a pointer
currentatthis.head. - Loop
indextimes, movingcurrent = current.next. - Return
current.
get(index) {
if (index < 0 || index >= this.length) return null;
let current = this.head;
for (let i = 0; i < index; i++) {
current = current.next;
}
return current;
}Time Complexity: — Requires sequential traversal from the head.
6. Updating by Index (set)
The set method modifies the value stored in the node at a specific index.
Step-by-Step Logic
- Call
get(index)to find the target node. - If found, update its value and return
true. - If not found (index out of bounds), return
false.
set(index, val) {
const foundNode = this.get(index);
if (foundNode) {
foundNode.val = val;
return true;
}
return false;
}Time Complexity: — Inherited from the traversal in get.
7. Inserting at Any Position (insert)
The insert method inserts a new node at any valid index (0 through length).
Step-by-Step Logic
- If
index < 0orindex > this.length, returnfalse. - If
index === 0, callunshift(val)to insert at the beginning. - If
index === this.length, callpush(val)to append at the end. - For middle positions:
- Locate the node right before the target index using
get(index - 1). - Create the new node.
- Point the new node's
nexttoprev.next. - Re-route
prev.nextto point to the new node.
- Locate the node right before the target index using
- Increment
lengthand returntrue.
insert(index, val) {
if (index < 0 || index > this.length) return false;
if (index === 0) return Boolean(this.unshift(val));
if (index === this.length) return Boolean(this.push(val));
const prev = this.get(index - 1);
const newNode = new Node(val);
newNode.next = prev.next;
prev.next = newNode;
this.length++;
return true;
}Time Complexity: — Finding the position takes traversal, but the pointer rewiring itself is instant ().
8. Removing from Any Position (remove)
The remove method deletes the node at a given index and returns it.
Step-by-Step Logic
- If
index < 0orindex >= this.length, returnnull. - If
index === 0, delegate toshift(). - If
index === this.length - 1, delegate topop(). - For middle positions:
- Locate the previous node:
prev = this.get(index - 1). - Identify the node to remove:
removedNode = prev.next. - Bypass the removed node:
prev.next = removedNode.next. - Clear
removedNode.next = null.
- Locate the previous node:
- Decrement
lengthand return the removed node.
remove(index) {
if (index < 0 || index >= this.length) return null;
if (index === 0) return this.shift();
if (index === this.length - 1) return this.pop();
const prev = this.get(index - 1);
const removedNode = prev.next;
prev.next = removedNode.next;
removedNode.next = null;
this.length--;
return removedNode;
}Time Complexity: — Locating the node takes , while unlinking is .
9. Reversing the List (reverse)
Reversing a singly linked list in-place is one of the classic data structure algorithms. It inverts the direction of all pointer links so that the head becomes the tail, the tail becomes the head, and all intermediate links point backward.
The Three-Pointer Algorithm
To flip links without losing references to the rest of the list, we maintain three pointers as we traverse:
prev: Starts atnull.current: Starts atthis.head.next: Temporarily preserves the upcoming node (current.next).
Step-by-Step Loop
- Swap
this.headandthis.tail. - Loop while
currentis notnull:- Store
next = current.next(saves remaining chain). - Reverse pointer:
current.next = prev. - Shift
prevforward:prev = current. - Shift
currentforward:current = next.
- Store
- Return the reversed list.
reverse() {
let current = this.head;
this.head = this.tail;
this.tail = current;
let prev = null;
let next = null;
while (current) {
next = current.next; // 1. Save next node
current.next = prev; // 2. Reverse pointer direction
prev = current; // 3. Step prev forward
current = next; // 4. Step current forward
}
return this;
}Time Complexity: — Single pass through all nodes.
Space Complexity: — Done entirely in-place with three temporary pointer variables.
10. Displaying the List (print)
A helper method to print the elements of the list sequentially for debugging and inspection:
print() {
const values = [];
let current = this.head;
while (current) {
values.push(current.val);
current = current.next;
}
console.log(values.join(" -> ") + " -> null");
}Complete Implementation
Here is the complete, cohesive SinglyLinkedList implementation with all methods unified:
class Node {
constructor(val) {
this.val = val;
this.next = null;
}
}
class SinglyLinkedList {
constructor() {
this.head = null;
this.tail = null;
this.length = 0;
}
push(val) {
const newNode = new Node(val);
if (!this.head) {
this.head = newNode;
this.tail = newNode;
} else {
this.tail.next = newNode;
this.tail = newNode;
}
this.length++;
return this;
}
unshift(val) {
const newNode = new Node(val);
if (!this.head) {
this.head = newNode;
this.tail = newNode;
} else {
newNode.next = this.head;
this.head = newNode;
}
this.length++;
return this;
}
shift() {
if (!this.head) return null;
const removedNode = this.head;
this.head = this.head.next;
this.length--;
if (this.length === 0) {
this.tail = null;
}
removedNode.next = null;
return removedNode;
}
pop() {
if (!this.head) return null;
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;
}
get(index) {
if (index < 0 || index >= this.length) return null;
let current = this.head;
for (let i = 0; i < index; i++) {
current = current.next;
}
return current;
}
set(index, val) {
const foundNode = this.get(index);
if (foundNode) {
foundNode.val = val;
return true;
}
return false;
}
insert(index, val) {
if (index < 0 || index > this.length) return false;
if (index === 0) return Boolean(this.unshift(val));
if (index === this.length) return Boolean(this.push(val));
const prev = this.get(index - 1);
const newNode = new Node(val);
newNode.next = prev.next;
prev.next = newNode;
this.length++;
return true;
}
remove(index) {
if (index < 0 || index >= this.length) return null;
if (index === 0) return this.shift();
if (index === this.length - 1) return this.pop();
const prev = this.get(index - 1);
const removedNode = prev.next;
prev.next = removedNode.next;
removedNode.next = null;
this.length--;
return removedNode;
}
reverse() {
let current = this.head;
this.head = this.tail;
this.tail = current;
let prev = null;
let next = null;
while (current) {
next = current.next;
current.next = prev;
prev = current;
current = next;
}
return this;
}
print() {
const values = [];
let current = this.head;
while (current) {
values.push(current.val);
current = current.next;
}
console.log(values.join(" -> ") + " -> null");
}
}
// Example Usage
const list = new SinglyLinkedList();
list.push(10).push(20).push(30);
list.print(); // 10 -> 20 -> 30 -> null
list.unshift(5);
list.print(); // 5 -> 10 -> 20 -> 30 -> null
list.insert(2, 15);
list.print(); // 5 -> 10 -> 15 -> 20 -> 30 -> null
list.reverse();
list.print(); // 30 -> 20 -> 15 -> 10 -> 5 -> nullPerformance Summary
| Operation | Time Complexity |
|---|---|
| Push at tail | |
| Pop from tail | |
| Unshift at head | |
| Shift from head | |
| Get or set by index | |
| Insert or remove by index | |
| Search | |
| Reverse |
Singly Linked List vs. Arrays
Understanding the performance trade-offs between linked lists and arrays allows you to choose the right data structure for your specific use case.
| Operation | Singly Linked List | Array / Dynamic Array | Architectural Explanation |
|---|---|---|---|
| Access (by index) | Arrays calculate physical memory addresses via arithmetic offset; linked lists must traverse sequentially from head. | ||
| Search (by value) | Both must inspect elements one by one in unsorted collections. | ||
Insertion at Head (unshift) | Linked list rewires a single pointer; arrays must shift every subsequent element one position to the right. | ||
Insertion at Tail (push) | Linked list updates tail.next; arrays append to pre-allocated capacity (occasional resize is ). | ||
| Insertion in Middle | Linked list spends traversing to the position; array spends shifting elements. | ||
Deletion at Head (shift) | Linked list advances head; array must shift every remaining item left. | ||
Deletion at Tail (pop) | Singly linked list must traverse from head to find the second-to-last node; array decrements its size counter. | ||
| Deletion in Middle | Linked list requires traversal; array requires shifting. | ||
| Memory Allocation | Node by node | Contiguous block | Linked-list nodes do not need to sit next to one another; array elements do. |
| Memory Overhead | Higher | Lower | Each linked-list node stores a next reference in addition to its value. |
Applications and When to Use a Singly Linked List
Optimal Use Cases
- Frequent changes at the head: Prepending and removing nodes both take time.
- Stacks: Use
unshiftandshiftto add and remove from the same end in time. - Queues: Use
pushat the tail andshiftat the head; both operations take time. - Dynamic collections: The list grows and shrinks one node at a time.
- Building other structures: Singly linked lists can support chaining in hash maps and adjacency lists in graphs.
When to Avoid
- Frequent Random Index Lookups: If your application constantly reads values by index (e.g.,
data[450]), arrays provide instant access whereas linked lists require slow traversal. - Strict Memory Limitations: Every node needs an additional
nextreference. - Backward Traversal: A singly linked list cannot move directly from a node to its predecessor.
Key Takeaways
- Every node stores a value and one
nextreference. - Links move from
headtotail; there is no direct backward traversal. - Push, unshift, and shift run in time when the list tracks its tail.
- Pop takes because the list must traverse from the head to find the node before the tail.
- Index-based access takes because linked lists do not provide random access.
- Reversing the list takes time and auxiliary space.