Beginner
25 min
Full Guide

Linked Lists

Singly and doubly linked lists, operations, and use cases

What is a Linked List?

A linked list is a linear data structure where elements (called nodes) are not stored in contiguous memory locations. Instead, each node contains data and a reference (or pointer) to the next node in the sequence. This structure allows for efficient insertion and deletion operations.

Unlike arrays, linked lists don't require shifting elements when inserting or deleting, making them ideal for scenarios where frequent modifications are needed.

Linked List vs Array

Linked List Advantages

  • ✓ Dynamic size - grows/shrinks easily
  • ✓ O(1) insertion/deletion at beginning
  • ✓ No memory waste from unused capacity
  • ✓ Easy to implement stacks and queues

Array Advantages

  • ✓ O(1) random access by index
  • ✓ Better cache locality
  • ✓ Less memory per element (no pointers)
  • ✓ Simpler to use and understand

Types of Linked Lists

1. Singly Linked List

Each node points to the next node. Can only traverse forward.

[Data | Next] → [Data | Next] → [Data | null]

2. Doubly Linked List

Each node has pointers to both next and previous nodes. Can traverse both directions.

null ← [Prev | Data | Next] ⇄ [Prev | Data | Next] → null

3. Circular Linked List

Last node points back to the first node, forming a circle.

[Data | Next] → [Data | Next] → [Data | Next] ↺

Singly Linked List Implementation

Node Class

// Node class represents each element in the list
class Node {
  constructor(data) {
    this.data = data;
    this.next = null;
  }
}

// Linked List class
class LinkedList {
  constructor() {
    this.head = null;
    this.size = 0;
  }

  // Get the size of the list - O(1)
  getSize() {
    return this.size;
  }

  // Check if list is empty - O(1)
  isEmpty() {
    return this.size === 0;
  }
}

Insert at Beginning - O(1)

insertAtBeginning(data) {
  const newNode = new Node(data);
  
  // Point new node to current head
  newNode.next = this.head;
  
  // Update head to new node
  this.head = newNode;
  
  this.size++;
}

// Usage
const list = new LinkedList();
list.insertAtBeginning(3);  // List: 3
list.insertAtBeginning(2);  // List: 2 → 3
list.insertAtBeginning(1);  // List: 1 → 2 → 3

Insert at End - O(n)

insertAtEnd(data) {
  const newNode = new Node(data);
  
  // If list is empty, new node becomes head
  if (this.isEmpty()) {
    this.head = newNode;
  } else {
    // Traverse to the end
    let current = this.head;
    while (current.next !== null) {
      current = current.next;
    }
    // Attach new node at the end
    current.next = newNode;
  }
  
  this.size++;
}

// Note: Can optimize to O(1) by maintaining a tail pointer

Insert at Position - O(n)

insertAt(data, index) {
  if (index < 0 || index > this.size) {
    throw new Error('Invalid index');
  }
  
  // Insert at beginning
  if (index === 0) {
    this.insertAtBeginning(data);
    return;
  }
  
  const newNode = new Node(data);
  let current = this.head;
  
  // Traverse to position before insertion point
  for (let i = 0; i < index - 1; i++) {
    current = current.next;
  }
  
  // Insert new node
  newNode.next = current.next;
  current.next = newNode;
  
  this.size++;
}

Delete from Beginning - O(1)

deleteFromBeginning() {
  if (this.isEmpty()) {
    throw new Error('List is empty');
  }
  
  // Move head to next node
  const deletedData = this.head.data;
  this.head = this.head.next;
  
  this.size--;
  return deletedData;
}

Delete from End - O(n)

deleteFromEnd() {
  if (this.isEmpty()) {
    throw new Error('List is empty');
  }
  
  // If only one node
  if (this.head.next === null) {
    const deletedData = this.head.data;
    this.head = null;
    this.size--;
    return deletedData;
  }
  
  // Traverse to second-to-last node
  let current = this.head;
  while (current.next.next !== null) {
    current = current.next;
  }
  
  const deletedData = current.next.data;
  current.next = null;
  
  this.size--;
  return deletedData;
}

Search - O(n)

search(data) {
  let current = this.head;
  let index = 0;
  
  while (current !== null) {
    if (current.data === data) {
      return index;
    }
    current = current.next;
    index++;
  }
  
  return -1;  // Not found
}

contains(data) {
  return this.search(data) !== -1;
}

Display/Print List

print() {
  if (this.isEmpty()) {
    console.log('List is empty');
    return;
  }
  
  const values = [];
  let current = this.head;
  
  while (current !== null) {
    values.push(current.data);
    current = current.next;
  }
  
  console.log(values.join(' → '));
}

Doubly Linked List Implementation

// Node for doubly linked list
class DoublyNode {
  constructor(data) {
    this.data = data;
    this.next = null;
    this.prev = null;
  }
}

class DoublyLinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.size = 0;
  }

  // Insert at beginning - O(1)
  insertAtBeginning(data) {
    const newNode = new DoublyNode(data);
    
    if (this.isEmpty()) {
      this.head = this.tail = newNode;
    } else {
      newNode.next = this.head;
      this.head.prev = newNode;
      this.head = newNode;
    }
    
    this.size++;
  }

  // Insert at end - O(1) with tail pointer!
  insertAtEnd(data) {
    const newNode = new DoublyNode(data);
    
    if (this.isEmpty()) {
      this.head = this.tail = newNode;
    } else {
      this.tail.next = newNode;
      newNode.prev = this.tail;
      this.tail = newNode;
    }
    
    this.size++;
  }

  // Delete from beginning - O(1)
  deleteFromBeginning() {
    if (this.isEmpty()) {
      throw new Error('List is empty');
    }
    
    const deletedData = this.head.data;
    
    if (this.head === this.tail) {
      this.head = this.tail = null;
    } else {
      this.head = this.head.next;
      this.head.prev = null;
    }
    
    this.size--;
    return deletedData;
  }

  // Delete from end - O(1) with tail pointer!
  deleteFromEnd() {
    if (this.isEmpty()) {
      throw new Error('List is empty');
    }
    
    const deletedData = this.tail.data;
    
    if (this.head === this.tail) {
      this.head = this.tail = null;
    } else {
      this.tail = this.tail.prev;
      this.tail.next = null;
    }
    
    this.size--;
    return deletedData;
  }

  isEmpty() {
    return this.size === 0;
  }
}

Common Linked List Problems

1. Reverse a Linked List

function reverseList(head) {
  let prev = null;
  let current = head;
  
  while (current !== null) {
    // Save next node
    const nextNode = current.next;
    
    // Reverse the link
    current.next = prev;
    
    // Move pointers forward
    prev = current;
    current = nextNode;
  }
  
  return prev;  // New head
}

// Example: 1 → 2 → 3 → null becomes 3 → 2 → 1 → null

2. Detect Cycle (Floyd's Cycle Detection)

function hasCycle(head) {
  let slow = head;
  let fast = head;
  
  while (fast !== null && fast.next !== null) {
    slow = slow.next;        // Move 1 step
    fast = fast.next.next;   // Move 2 steps
    
    if (slow === fast) {
      return true;  // Cycle detected
    }
  }
  
  return false;  // No cycle
}

// If there's a cycle, fast will eventually meet slow

3. Find Middle Element

function findMiddle(head) {
  let slow = head;
  let fast = head;
  
  while (fast !== null && fast.next !== null) {
    slow = slow.next;        // Move 1 step
    fast = fast.next.next;   // Move 2 steps
  }
  
  return slow;  // Slow will be at middle
}

// When fast reaches end, slow will be at middle

4. Merge Two Sorted Lists

function mergeSortedLists(l1, l2) {
  const dummy = new Node(0);
  let current = dummy;
  
  while (l1 !== null && l2 !== null) {
    if (l1.data < l2.data) {
      current.next = l1;
      l1 = l1.next;
    } else {
      current.next = l2;
      l2 = l2.next;
    }
    current = current.next;
  }
  
  // Attach remaining nodes
  current.next = l1 !== null ? l1 : l2;
  
  return dummy.next;
}

5. 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 n+1 steps ahead
  for (let i = 0; i <= n; i++) {
    first = first.next;
  }
  
  // Move both until first reaches end
  while (first !== null) {
    first = first.next;
    second = second.next;
  }
  
  // Remove the nth node
  second.next = second.next.next;
  
  return dummy.next;
}

Time Complexity Summary

Operation Singly Linked Doubly Linked Array
Access by index O(n) O(n) O(1)
Insert at beginning O(1) O(1) O(n)
Insert at end O(n)* O(1) O(1)
Delete from beginning O(1) O(1) O(n)
Delete from end O(n) O(1) O(1)
Search O(n) O(n) O(n)

* Can be O(1) if maintaining tail pointer

💡 When to Use Linked Lists

  • ✓ Frequent insertions/deletions at the beginning
  • ✓ Unknown or dynamic size requirements
  • ✓ Implementing stacks, queues, or graphs
  • ✓ When you don't need random access
  • ✗ Avoid when you need fast index-based access
  • ✗ Avoid when memory overhead is a concern