Traversing Singly Linked Lists in JavaScript
Introduction to Singly Linked Lists
A singly linked list is a linear data structure where each element (node) contains data and a reference to the next node in the sequence. Unlike arrays, nodes are not stored in contiguous memory locations.
Key Characteristics:
- Each node has only one pointer (to the next node)
- Traversal is unidirectional (forward only)
- No direct access to previous nodes
- Dynamic size allocation
[10] -> [20] -> [30] -> [40] -> nullStructure:
- Head: First node in the list
- Tail: Last node (points to null)
- Next: Pointer to the next node
What is Traversal?
Traversal in a singly linked list means visiting each node sequentially from the head to the tail, processing the data in each node.
Why Traverse Singly Linked Lists?
- Search for specific values
- Display all elements in order
- Process or transform data
- Calculate aggregate values (sum, count, average)
- Validate list integrity
- Convert to other data structures
Traversal Direction
Since singly linked lists only have forward pointers, traversal is always unidirectional from head to tail.
Node Structure
class Node {
constructor(value = null) {
this.value = value; // Data stored in the node
this.next = null; // Reference to next node (only forward)
}
}
class SinglyLinkedList {
constructor() {
this.head = null; // First node
this.tail = null; // Last node (optional for efficiency)
this.size = 0; // Track list size
}
// Helper method to add nodes at the end
append(value) {
const newNode = new Node(value);
if (!this.head) {
this.head = newNode;
this.tail = newNode;
} else {
this.tail.next = newNode;
this.tail = newNode;
}
this.size++;
return this;
}
// Helper method to add nodes at the beginning
prepend(value) {
const newNode = new Node(value);
newNode.next = this.head;
this.head = newNode;
if (!this.tail) {
this.tail = newNode;
}
this.size++;
return this;
}
}Iterative Traversal
Basic Iterative Traversal
function traverseIterative(head) {
let current = head;
const values = [];
// Move forward through each node
while (current !== null) {
values.push(current.value);
current = current.next; // Move to next node
}
return values;
}
// Example usage
const list = new SinglyLinkedList();
list.append(10).append(20).append(30).append(40);
console.log(traverseIterative(list.head)); // Output: [10, 20, 30, 40]Iterative Traversal with Index Tracking
function printListIterative(head) {
if (!head) {
console.log("Singly linked list is empty");
return;
}
let current = head;
let position = 0;
console.log("=== Singly Linked List Contents ===");
while (current !== null) {
console.log(`Position ${position}: ${current.value}`);
current = current.next;
position++;
}
console.log("=== End of List ===");
}Search in Singly Linked List
function findElement(head, target) {
let current = head;
let position = 0;
while (current !== null) {
if (current.value === target) {
return {
found: true,
position,
node: current
};
}
current = current.next;
position++;
}
return { found: false, position: -1, node: null };
}
// Example usage
const searchResult = findElement(list.head, 30);
console.log(searchResult); // { found: true, position: 2, node: Node }Recursive Traversal
Basic Recursive Traversal
function traverseRecursive(head, values = []) {
// Base case: reached end of singly linked list
if (head === null) {
return values;
}
// Process current node
values.push(head.value);
// Recursive call for next node (forward only)
return traverseRecursive(head.next, values);
}
// Example usage
console.log(traverseRecursive(list.head)); // Output: [10, 20, 30, 40]Recursive Print Function
function printListRecursive(head, position = 0) {
// Base case: no more nodes
if (head === null) {
if (position === 0) {
console.log("Singly linked list is empty");
}
return;
}
// Process current node
console.log(`Position ${position}: ${head.value}`);
// Recursive call for next node
printListRecursive(head.next, position + 1);
}Reverse Print (Using Recursion)
function printReverse(head) {
// Base case
if (head === null) {
return;
}
// First recur to the end
printReverse(head.next);
// Then print current node (prints in reverse order)
console.log(head.value);
}
// Note: This is the only way to print in reverse for singly linked lists
// without using additional data structuresAdvanced Traversal Techniques
Floyd's Cycle Detection (Two-Pointer)
function detectCycle(head) {
if (!head || !head.next) return false;
let slow = head; // Moves one step
let fast = head; // Moves two steps
// If there's a cycle, fast and slow will meet
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) {
return true; // Cycle detected
}
}
return false; // No cycle
}Find Middle Element (Two-Pointer)
function findMiddle(head) {
if (!head) return null;
let slow = head;
let fast = head;
// When fast reaches end, slow will be at middle
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}Traversal with Callback Function
function traverseWithCallback(head, callback) {
let current = head;
let index = 0;
while (current !== null) {
// Call the callback with current node data
callback(current.value, index, current);
current = current.next;
index++;
}
}
// Usage examples
traverseWithCallback(list.head, (value, index) => {
console.log(`Node ${index}: ${value}`);
});
traverseWithCallback(list.head, (value) => {
if (value > 25) {
console.log(`Large value found: ${value}`);
}
});Conditional Traversal
function traverseUntilCondition(head, condition) {
let current = head;
const result = [];
while (current !== null && !condition(current.value)) {
result.push(current.value);
current = current.next;
}
return {
values: result,
stoppedAt: current ? current.value : null,
reachedEnd: current === null
};
}
// Example: Stop when we find a value greater than 25
const result = traverseUntilCondition(list.head, value => value > 25);
console.log(result); // { values: [10, 20], stoppedAt: 30, reachedEnd: false }Common Use Cases for Singly Linked Lists
1. Calculate Sum
function calculateSum(head) {
let sum = 0;
let current = head;
while (current !== null) {
sum += current.value;
current = current.next;
}
return sum;
}2. Count Nodes
function countNodes(head) {
let count = 0;
let current = head;
while (current !== null) {
count++;
current = current.next;
}
return count;
}3. Find Maximum Value
function findMax(head) {
if (!head) return null;
let max = head.value;
let current = head.next;
while (current !== null) {
if (current.value > max) {
max = current.value;
}
current = current.next;
}
return max;
}4. Convert to Array
function singlyLinkedListToArray(head) {
const array = [];
let current = head;
while (current !== null) {
array.push(current.value);
current = current.next;
}
return array;
}5. Filter Elements
function filterSinglyList(head, predicate) {
const filtered = [];
let current = head;
while (current !== null) {
if (predicate(current.value)) {
filtered.push(current.value);
}
current = current.next;
}
return filtered;
}
// Example: Get all even numbers
const evenNumbers = filterSinglyList(list.head, value => value % 2 === 0);6. Find Nth Node from End
function findNthFromEnd(head, n) {
if (!head || n <= 0) return null;
let first = head;
let second = head;
// Move first pointer n steps ahead
for (let i = 0; i < n; i++) {
if (!first) return null; // List is shorter than n
first = first.next;
}
// Move both pointers until first reaches end
while (first !== null) {
first = first.next;
second = second.next;
}
return second;
}Time and Space Complexity
Iterative Traversal
- Time Complexity: O(n) - must visit each node once
- Space Complexity: O(1) - uses only a few variables
- Best for: Large lists, memory-constrained environments
Recursive Traversal
- Time Complexity: O(n) - must visit each node once
- Space Complexity: O(n) - function call stack grows with list size
- Best for: Smaller lists, when code readability is prioritized
Comparison Table
| Operation | Iterative | Recursive |
|---|---|---|
| Time | O(n) | O(n) |
| Space | O(1) | O(n) |
| Stack Overflow Risk | No | Yes (large lists) |
| Code Readability | Good | Excellent |
| Memory Usage | Minimal | Higher |
Edge Cases and Error Handling
Handling Empty Singly Linked Lists
function safeTraverse(head) {
// Check for empty singly linked list
if (head === null) {
console.log("Cannot traverse: Singly linked list is empty");
return [];
}
const values = [];
let current = head;
while (current !== null) {
values.push(current.value);
current = current.next;
}
return values;
}Cycle Detection in Singly Linked Lists
function hasCycle(head) {
if (!head || !head.next) return false;
let slow = head;
let fast = head;
// Floyd's Cycle Detection Algorithm
while (fast && fast.next) {
slow = slow.next; // Move one step
fast = fast.next.next; // Move two steps
if (slow === fast) {
return true; // Cycle detected
}
}
return false; // No cycle found
}
function safeTraverseWithCycleDetection(head) {
if (hasCycle(head)) {
throw new Error("Cannot traverse: Cycle detected in singly linked list");
}
return traverseIterative(head);
}Robust Traversal with Validation
function robustTraverse(head, options = {}) {
const {
maxNodes = 1000,
onError = console.error,
validateNode = (node) => node && typeof node.value !== 'undefined'
} = options;
if (!head) {
return { success: false, error: "Empty singly linked list" };
}
const values = [];
let current = head;
let nodeCount = 0;
const visited = new Set();
try {
while (current !== null) {
// Cycle detection
if (visited.has(current)) {
throw new Error("Cycle detected in singly linked list");
}
visited.add(current);
// Max nodes protection
if (nodeCount >= maxNodes) {
throw new Error(`Exceeded maximum nodes limit: ${maxNodes}`);
}
// Validate node structure
if (!validateNode(current)) {
throw new Error(`Invalid node at position ${nodeCount}`);
}
values.push(current.value);
current = current.next;
nodeCount++;
}
return { success: true, values, nodeCount };
} catch (error) {
onError(error.message);
return { success: false, error: error.message, nodeCount };
}
}Practical Examples
Example 1: Task Management System
class Task {
constructor(id, description, priority) {
this.id = id;
this.description = description;
this.priority = priority;
this.completed = false;
this.next = null;
}
}
function displayTasks(head) {
console.log("📋 Task List");
console.log("=============");
if (!head) {
console.log("No tasks found");
return;
}
let current = head;
let taskNumber = 1;
let completedCount = 0;
while (current !== null) {
const status = current.completed ? "✅" : "⏳";
const priority = "⭐".repeat(current.priority);
console.log(`${taskNumber}. ${status} ${current.description} ${priority}`);
if (current.completed) completedCount++;
current = current.next;
taskNumber++;
}
console.log(`\nCompleted: ${completedCount}/${taskNumber - 1} tasks`);
}
// Create task list
let taskHead = new Task(1, "Review code", 3);
taskHead.next = new Task(2, "Write documentation", 2);
taskHead.next.next = new Task(3, "Deploy to production", 3);
taskHead.next.completed = true; // Mark second task as completed
displayTasks(taskHead);Example 2: Student Grade System
class Student {
constructor(name, grade, subject) {
this.name = name;
this.grade = grade;
this.subject = subject;
this.next = null;
}
}
function analyzeGrades(head) {
console.log("📚 Student Grade Analysis");
console.log("==========================");
if (!head) {
console.log("No students found");
return;
}
let current = head;
let totalGrades = 0;
let count = 0;
let highestGrade = -1;
let lowestGrade = 101;
let topStudent = "";
while (current !== null) {
console.log(`${current.name} (${current.subject}): ${current.grade}%`);
totalGrades += current.grade;
count++;
if (current.grade > highestGrade) {
highestGrade = current.grade;
topStudent = current.name;
}
if (current.grade < lowestGrade) {
lowestGrade = current.grade;
}
current = current.next;
}
console.log("==========================");
console.log(`Average Grade: ${(totalGrades / count).toFixed(2)}%`);
console.log(`Highest Grade: ${highestGrade}% (${topStudent})`);
console.log(`Lowest Grade: ${lowestGrade}%`);
}
// Create student list
let studentHead = new Student("Alice", 95, "Math");
studentHead.next = new Student("Bob", 87, "Science");
studentHead.next.next = new Student("Charlie", 92, "History");
analyzeGrades(studentHead);Example 3: Shopping Cart Implementation
class CartItem {
constructor(product, price, quantity) {
this.product = product;
this.price = price;
this.quantity = quantity;
this.next = null;
}
getTotalPrice() {
return this.price * this.quantity;
}
}
function processShoppingCart(head) {
console.log("🛒 Shopping Cart Summary");
console.log("========================");
if (!head) {
console.log("Cart is empty");
return { total: 0, itemCount: 0 };
}
let current = head;
let grandTotal = 0;
let itemCount = 0;
while (current !== null) {
const itemTotal = current.getTotalPrice();
console.log(`${current.product} x${current.quantity} - $${itemTotal.toFixed(2)}`);
grandTotal += itemTotal;
itemCount++;
current = current.next;
}
console.log("========================");
console.log(`Total Items: ${itemCount}`);
console.log(`Grand Total: $${grandTotal.toFixed(2)}`);
return { total: grandTotal, itemCount };
}
// Create shopping cart
let cart = new CartItem("Laptop", 999.99, 1);
cart.next = new CartItem("Mouse", 29.99, 2);
cart.next.next = new CartItem("Keyboard", 79.99, 1);
processShoppingCart(cart);Advanced Traversal Patterns
1. Comparing Two Singly Linked Lists
function compareSinglyLists(head1, head2) {
let current1 = head1;
let current2 = head2;
while (current1 !== null && current2 !== null) {
if (current1.value !== current2.value) {
return false;
}
current1 = current1.next;
current2 = current2.next;
}
// Both should be null if lists are equal in length and content
return current1 === null && current2 === null;
}2. Merge Two Sorted Singly Linked Lists
function mergeSortedLists(head1, head2) {
const dummy = new Node(0);
let current = dummy;
let ptr1 = head1;
let ptr2 = head2;
while (ptr1 !== null && ptr2 !== null) {
if (ptr1.value <= ptr2.value) {
current.next = ptr1;
ptr1 = ptr1.next;
} else {
current.next = ptr2;
ptr2 = ptr2.next;
}
current = current.next;
}
// Attach remaining nodes
current.next = ptr1 || ptr2;
return dummy.next;
}3. Remove Duplicates from Sorted Singly Linked List
function removeDuplicates(head) {
if (!head) return head;
let current = head;
while (current && current.next) {
if (current.value === current.next.value) {
current.next = current.next.next; // Skip duplicate
} else {
current = current.next;
}
}
return head;
}Performance Optimizations
1. Early Exit Optimization
function findFirstMatch(head, predicate) {
let current = head;
let index = 0;
while (current !== null) {
if (predicate(current.value)) {
return {
value: current.value,
index,
found: true,
node: current
};
}
current = current.next;
index++;
}
return { value: null, index: -1, found: false, node: null };
}
// Usage: Find first even number
const result = findFirstMatch(list.head, value => value % 2 === 0);2. Batch Processing During Traversal
function processInBatches(head, batchSize, processor) {
let current = head;
let batch = [];
let batchNumber = 1;
while (current !== null) {
batch.push(current.value);
if (batch.length === batchSize) {
console.log(`Processing batch ${batchNumber}:`);
processor(batch);
batch = [];
batchNumber++;
}
current = current.next;
}
// Process remaining items
if (batch.length > 0) {
console.log(`Processing final batch ${batchNumber}:`);
processor(batch);
}
}
// Example processor
const batchProcessor = (batch) => {
const sum = batch.reduce((acc, val) => acc + val, 0);
console.log(` Batch sum: ${sum}`);
};Complete Singly Linked List Implementation
class Node {
constructor(value) {
this.value = value;
this.next = null;
}
}
class SinglyLinkedList {
constructor() {
this.head = null;
this.tail = null;
this.size = 0;
}
// Add to end
append(value) {
const newNode = new Node(value);
if (!this.head) {
this.head = newNode;
this.tail = newNode;
} else {
this.tail.next = newNode;
this.tail = newNode;
}
this.size++;
return this;
}
// Add to beginning
prepend(value) {
const newNode = new Node(value);
newNode.next = this.head;
this.head = newNode;
if (!this.tail) {
this.tail = newNode;
}
this.size++;
return this;
}
// Iterative traversal methods
forEach(callback) {
let current = this.head;
let index = 0;
while (current !== null) {
callback(current.value, index, current);
current = current.next;
index++;
}
}
map(callback) {
const result = [];
this.forEach((value, index) => {
result.push(callback(value, index));
});
return result;
}
filter(predicate) {
const result = [];
this.forEach((value, index) => {
if (predicate(value, index)) {
result.push(value);
}
});
return result;
}
find(predicate) {
let current = this.head;
let index = 0;
while (current !== null) {
if (predicate(current.value, index)) {
return { value: current.value, index, node: current };
}
current = current.next;
index++;
}
return null;
}
// Convert to array
toArray() {
return this.map(value => value);
}
// String representation
toString() {
const values = this.toArray();
return values.join(' -> ') + ' -> null';
}
// Recursive traversal
toArrayRecursive(node = this.head) {
if (node === null) return [];
return [node.value, ...this.toArrayRecursive(node.next)];
}
// Get middle element
getMiddle() {
if (!this.head) return null;
let slow = this.head;
let fast = this.head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
// Reverse the singly linked list
reverse() {
let previous = null;
let current = this.head;
this.tail = this.head;
while (current !== null) {
const nextNode = current.next;
current.next = previous;
previous = current;
current = nextNode;
}
this.head = previous;
return this;
}
}
// Demo usage
const list = new SinglyLinkedList();
list.append(10).append(20).append(30).append(40).append(50);
console.log("Original list:", list.toString());
console.log("Array representation:", list.toArray());
console.log("Doubled values:", list.map(x => x * 2));
console.log("Even numbers:", list.filter(x => x % 2 === 0));
console.log("First number > 25:", list.find(x => x > 25));
console.log("Middle element:", list.getMiddle().value);
console.log("Recursive array:", list.toArrayRecursive());Traversal Patterns Summary
1. Basic Iterative Pattern
let current = head;
while (current !== null) {
// Process current.value
current = current.next; // Move forward (only direction possible)
}2. Basic Recursive Pattern
function traverse(node) {
if (node === null) return; // Base case
// Process node.value
traverse(node.next); // Recursive call (forward only)
}3. Two-Pointer Pattern (Floyd's Algorithm)
let slow = head;
let fast = head;
while (fast && fast.next) {
slow = slow.next; // One step
fast = fast.next.next; // Two steps
}4. Search Pattern
let current = head;
let position = 0;
while (current !== null) {
if (current.value === target) {
return { found: true, position };
}
current = current.next;
position++;
}Singly Linked List vs Other List Types
Advantages of Singly Linked Lists
- Memory Efficient: Only one pointer per node
- Simple Implementation: Easier to understand and implement
- Dynamic Size: Can grow/shrink during runtime
- Insertion at Head: O(1) time complexity
Limitations of Singly Linked Lists
- Unidirectional: Can only traverse forward
- No Random Access: Must traverse from head to reach any element
- No Backward Traversal: Cannot move to previous nodes directly
- Extra Memory: Pointer overhead compared to arrays
Real-World Applications
1. Music Playlist (Forward Play Only)
class Song {
constructor(title, artist, duration) {
this.title = title;
this.artist = artist;
this.duration = duration; // in seconds
this.next = null;
}
}
function playPlaylist(head) {
console.log("🎵 Now Playing Playlist (Forward Only)");
console.log("======================================");
if (!head) {
console.log("Playlist is empty");
return;
}
let current = head;
let totalDuration = 0;
let songNumber = 1;
while (current !== null) {
const minutes = Math.floor(current.duration / 60);
const seconds = current.duration % 60;
console.log(`${songNumber}. ${current.title} by ${current.artist} (${minutes}:${seconds.toString().padStart(2, '0')})`);
totalDuration += current.duration;
current = current.next;
songNumber++;
}
const totalMinutes = Math.floor(totalDuration / 60);
const totalSeconds = totalDuration % 60;
console.log(`\nTotal playlist duration: ${totalMinutes}:${totalSeconds.toString().padStart(2, '0')}`);
}2. Browser Forward History
class HistoryPage {
constructor(url, title, timestamp) {
this.url = url;
this.title = title;
this.timestamp = timestamp;
this.next = null;
}
}
function displayForwardHistory(head, limit = 10) {
console.log("🌐 Browser Forward History");
console.log("===========================");
if (!head) {
console.log("No forward history available");
return;
}
let current = head;
let count = 0;
while (current !== null && count < limit) {
const date = new Date(current.timestamp).toLocaleString();
console.log(`${count + 1}. ${current.title}`);
console.log(` URL: ${current.url}`);
console.log(` Visited: ${date}`);
console.log("");
current = current.next;
count++;
}
if (current !== null) {
console.log("... (more pages in forward history)");
}
}Common Algorithms Using Traversal
1. Reverse a Singly Linked List
function reverseSinglyList(head) {
let previous = null;
let current = head;
while (current !== null) {
const nextNode = current.next;
current.next = previous; // Reverse the link
previous = current;
current = nextNode;
}
return previous; // New head
}2. Remove Nth Node from End
function removeNthFromEnd(head, n) {
const dummy = new Node(0);
dummy.next = head;
let first = dummy;
let second = dummy;
// Move first pointer n+1 steps ahead
for (let i = 0; i <= n; i++) {
first = first.next;
}
// Move both pointers until first reaches end
while (first !== null) {
first = first.next;
second = second.next;
}
// Remove the nth node from end
second.next = second.next.next;
return dummy.next;
}3. Check if Singly Linked List is Palindrome
function isPalindrome(head) {
if (!head || !head.next) return true;
// Find middle using two pointers
let slow = head;
let fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
}
// Reverse second half
let secondHalf = reverseSinglyList(slow);
let firstHalf = head;
// Compare both halves
while (secondHalf !== null) {
if (firstHalf.value !== secondHalf.value) {
return false;
}
firstHalf = firstHalf.next;
secondHalf = secondHalf.next;
}
return true;
}Best Practices for Singly Linked List Traversal
✅ Do's
- Always check for
nullbefore accessing node properties - Use descriptive variable names (
current,next,previous) - Handle empty lists gracefully
- Implement cycle detection for untrusted data
- Use iterative approach for large lists to avoid stack overflow
- Keep track of previous node when modification is needed
❌ Don'ts
- Don't forget to move to the next node (
current = current.next) - Don't assume the list is not empty
- Don't create infinite loops
- Don't use recursion for very large lists
- Don't modify the list structure during traversal unless intended
🚀 Performance Tips
- Use two-pointer techniques for finding middle, detecting cycles
- Implement early exit conditions when searching
- Consider tail pointer for O(1) append operations
- Use dummy nodes to simplify edge case handling
Memory Management
Traversal with Memory Cleanup
function traverseAndCleanup(head, shouldDelete) {
let current = head;
let previous = null;
let newHead = head;
while (current !== null) {
if (shouldDelete(current.value)) {
if (previous === null) {
// Deleting head node
newHead = current.next;
} else {
previous.next = current.next;
}
const nodeToDelete = current;
current = current.next;
// Clean up the deleted node
nodeToDelete.next = null;
nodeToDelete.value = null;
} else {
previous = current;
current = current.next;
}
}
return newHead;
}Real-Life Analogy: Assembly Line
Think of a singly linked list like an assembly line in a factory:
- 🏭 Start of Line (Head): Where products enter
- 📦 Workstations (Nodes): Each processes one item and passes it forward
- ➡️ Conveyor Belt (Next Pointers): Items can only move in one direction
- 🏁 End of Line (Null): Where products exit
Key Points:
- Workers can only see the current item and pass it forward
- No going backward to previous workstations
- Must process items in sequence
- Each workstation knows only about the next one
This perfectly represents how singly linked list traversal works - forward movement only, one node at a time!
Conclusion
Singly linked list traversal is a fundamental operation that forms the foundation for many algorithms and data manipulations. The unidirectional nature of singly linked lists makes traversal straightforward but requires careful consideration of:
- Forward-only movement: Plan your algorithms accordingly
- Memory efficiency: Choose iterative over recursive for large lists
- Edge cases: Handle empty lists and cycles properly
- Performance: Use appropriate algorithms like two-pointer techniques
Master the basic traversal pattern first, then build upon it to solve complex problems. Remember: in singly linked lists, you can only move forward, so make each traversal count!
Core Pattern to Remember:
let current = head;
while (current !== null) {
// Process current node
current = current.next; // Move forward (only direction available)
}