On This Page

What Is a Linked List?

A linked list is a chain of nodes (objects that bundle data with one or more pointers) where each node holds two things: some data and a reference (pointer) to the next node. Unlike an array, the nodes don't sit next to each other in memory. They can be scattered anywhere โ€” the pointers are what hold the chain together.

Picture a scavenger hunt. Each clue tells you where to find the next clue. You can't jump straight to clue #5 โ€” you have to follow the chain from the beginning. That's the trade-off: you lose random access (the ability to jump to any position in O(1)), but you gain the ability to insert and remove elements without shifting anything.

A Brief History

Linked lists were invented in 1955-56 by Allen Newell, Cliff Shaw, and Herbert A. Simon at the RAND Corporation. They were building the Information Processing Language (IPL) for an early AI program called the Logic Theorist, and they needed a way to represent flexible, changing data. Arrays were too rigid โ€” if you needed to insert something in the middle, you had to shift everything. Their solution: store each piece of data with a pointer to the next piece. The data could live anywhere in memory, and rearranging the list meant changing a few pointers instead of moving data around.

This was a radical idea at the time. Most early computers worked exclusively with fixed-size blocks of sequential memory. The linked list introduced the concept of dynamic data structures โ€” structures whose shape can change at runtime.

Three Flavors

Why Not Just Use Arrays?

Arrays are better in most cases โ€” faster access, better cache performance, simpler code. But linked lists win when:

Real-World Uses

How It Works

Node Structure

Each node is a tiny object with at minimum two fields. In a singly linked list: data and next. In a doubly linked list: data, next, and prev. The list itself just holds a pointer to the first node (the head). Some implementations also track the tail (last node) for O(1) appends, and a size counter so you don't have to traverse the whole list to count elements.

Insertion

At the head: Create a new node. Point its next to the current head. Update head to be the new node. That's it โ€” O(1), no shifting, no matter how long the list is. Order matters here: if you update the head before setting the new node's next, you lose access to the rest of the list.

At the tail: If you have a tail pointer, same deal โ€” O(1). Without one, you have to walk the entire list to find the end: O(n).

In the middle: Walk to the node before where you want to insert. Point the new node's next to the node after. Point the previous node's next to the new node. O(1) for the rewiring itself, but O(n) to find the position.

Deletion

Same idea in reverse. To delete a node, you need access to the node before it (in a singly linked list) so you can bypass the deleted node. In a doubly linked list, you can delete any node you have a reference to in O(1) because you can reach both neighbors through prev and next.

One edge case that catches people: deleting the head node. You can't "unlink" the head from a previous node โ€” there isn't one. You have to update the head pointer itself.

The Dummy Head Trick

Many linked list problems get messy because inserting/deleting at the head is a special case โ€” you need to update the head pointer itself, not some node's next. A sentinel node (also called a dummy head) eliminates this. You create a fake node before the real head, and every operation is now "insert/delete after some node" โ€” no special cases. At the end, the real head is dummy.next.

Memory Overhead

Each node in a singly linked list carries one pointer (8 bytes on a 64-bit machine). A doubly linked list carries two. Plus the object header overhead that languages like Python and Java add. For an integer (4 bytes of actual data), a singly linked node might use 40-60 bytes total in Python. An array storing the same integer uses about 8 bytes. That's 5-7x more memory per element. For large datasets, this matters.

๐Ÿ”ง Practical Tip

In interviews, always ask yourself: "Do I need a dummy head?" If you're modifying the list and the head might change, the answer is usually yes. It simplifies your code and eliminates off-by-one bugs.

Interactive Visualization

Watch how pointers get rewired during insertion and deletion. The arrows show the next references between nodes.

Linked List Operations

Operations & Complexity

Singly Linked List

Operation Time Notes
Access by indexO(n)Must walk from head
SearchO(n)Linear scan
Insert at headO(1)Just rewire one pointer
Insert at tailO(1)**With tail pointer; O(n) without
Insert at positionO(n)O(n) to find, O(1) to insert
Delete headO(1)Move head forward
Delete at positionO(n)Need previous node's reference

Doubly Linked List

Operation Time Notes
Access by indexO(n)Can start from closer end
SearchO(n)Linear scan in either direction
Insert at head/tailO(1)Rewire two pointers
Insert at positionO(n)O(n) to find, O(1) to insert
Delete any node (given ref)O(1)Can access prev directly
Delete at positionO(n)O(n) to find, O(1) to delete

Space complexity: O(n) โ€” one node per element, plus one pointer (singly) or two pointers (doubly) per node.

Implementation

Singly Linked List in Python

Python
class Node:
    __slots__ = ('val', 'next')  # Save memory by avoiding __dict__

    def __init__(self, val, nxt=None):
        self.val = val
        self.next = nxt


class SinglyLinkedList:
    def __init__(self):
        self.head = None
        self.size = 0

    def insert_head(self, val):
        # New node points to current head, then becomes head
        # Order matters โ€” point first, then update head
        self.head = Node(val, self.head)
        self.size += 1

    def insert_tail(self, val):
        new_node = Node(val)
        if not self.head:
            self.head = new_node
        else:
            # Walk to the end โ€” this is why a tail pointer helps
            curr = self.head
            while curr.next:
                curr = curr.next
            curr.next = new_node
        self.size += 1

    def delete(self, val):
        # Dummy head avoids special-casing deletion of the first node
        dummy = Node(0, self.head)
        prev, curr = dummy, self.head
        while curr:
            if curr.val == val:
                prev.next = curr.next  # Bypass the deleted node
                self.size -= 1
                self.head = dummy.next  # Head might have changed
                return True
            prev, curr = curr, curr.next
        return False

    def find(self, val):
        curr = self.head
        while curr:
            if curr.val == val:
                return curr
            curr = curr.next
        return None

    def to_list(self):
        result, curr = [], self.head
        while curr:
            result.append(curr.val)
            curr = curr.next
        return result

    def reverse(self):
        """Reverse the list in-place by flipping all pointers.
        
        Uses three pointers: prev, curr, next_temp.
        At each step: save next, point curr backward, advance both.
        """
        prev = None
        curr = self.head
        while curr:
            next_temp = curr.next  # Save next before we break the link
            curr.next = prev       # Flip the arrow
            prev = curr            # Advance prev
            curr = next_temp       # Advance curr
        self.head = prev           # prev is now the new head

Singly Linked List in JavaScript

JavaScript
class ListNode {
  constructor(val, next = null) {
    this.val = val;
    this.next = next;
  }
}

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

  insertHead(val) {
    // Point new node to current head, then swap
    this.head = new ListNode(val, this.head);
    this.size++;
  }

  insertTail(val) {
    const node = new ListNode(val);
    if (!this.head) {
      this.head = node;
    } else {
      let curr = this.head;
      while (curr.next) curr = curr.next;
      curr.next = node;
    }
    this.size++;
  }

  delete(val) {
    // Dummy head pattern โ€” no special case for deleting head
    const dummy = new ListNode(0, this.head);
    let prev = dummy, curr = this.head;
    while (curr) {
      if (curr.val === val) {
        prev.next = curr.next;
        this.head = dummy.next;
        this.size--;
        return true;
      }
      prev = curr;
      curr = curr.next;
    }
    return false;
  }

  find(val) {
    let curr = this.head;
    while (curr) {
      if (curr.val === val) return curr;
      curr = curr.next;
    }
    return null;
  }

  toArray() {
    const result = [];
    let curr = this.head;
    while (curr) {
      result.push(curr.val);
      curr = curr.next;
    }
    return result;
  }
}

Doubly Linked List (Python, with Sentinels)

Python
class DNode:
    __slots__ = ('val', 'prev', 'next')
    def __init__(self, val, prev=None, nxt=None):
        self.val = val
        self.prev = prev
        self.next = nxt

class DoublyLinkedList:
    def __init__(self):
        # Sentinel nodes eliminate all edge cases
        # head_sentinel <-> ... real nodes ... <-> tail_sentinel
        self.head = DNode(None)
        self.tail = DNode(None)
        self.head.next = self.tail
        self.tail.prev = self.head
        self.size = 0

    def insert_after(self, node, val):
        # Splice new node between node and node.next
        new = DNode(val, node, node.next)
        node.next.prev = new
        node.next = new
        self.size += 1
        return new

    def remove(self, node):
        # Unlink node from both neighbors โ€” O(1) because
        # we have direct access to prev and next
        node.prev.next = node.next
        node.next.prev = node.prev
        self.size -= 1
        return node.val

    def push_front(self, val):
        return self.insert_after(self.head, val)

    def push_back(self, val):
        return self.insert_after(self.tail.prev, val)

    def pop_front(self):
        if self.size == 0: raise IndexError("empty")
        return self.remove(self.head.next)

    def pop_back(self):
        if self.size == 0: raise IndexError("empty")
        return self.remove(self.tail.prev)

    def move_to_front(self, node):
        """Move an existing node to the front. Used in LRU caches."""
        self.remove(node)
        self.insert_after(self.head, node.val)

Variation: Cycle Detection with Floyd's Algorithm

Floyd's cycle detection (also called the tortoise and hare algorithm) uses two pointers moving at different speeds. If there's a cycle, they'll meet. The math behind it: if the slow pointer has entered the cycle, the fast pointer closes the gap by one node per step, so they must collide within one full loop of the cycle.

Python
def has_cycle(head):
    """Detect if a linked list has a cycle using O(1) space.
    
    Slow pointer moves 1 step, fast pointer moves 2 steps.
    If they meet, there's a cycle. If fast hits None, no cycle.
    """
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow is fast:
            return True
    return False

def find_cycle_start(head):
    """Find where the cycle begins (if any).
    
    After slow and fast meet inside the cycle, reset one pointer
    to head. Move both one step at a time. They'll meet at the
    cycle's entry point. This works because of a mathematical
    relationship between the distances traveled.
    """
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow is fast:
            # Reset one pointer to head
            slow = head
            while slow is not fast:
                slow = slow.next
                fast = fast.next
            return slow  # This is the cycle start
    return None  # No cycle

Common Mistakes

โš ๏ธ Mistakes That Trip Up Beginners
  1. Losing the reference to the rest of the list. When you reassign node.next, you sever the link to everything after it. Always save next_temp = curr.next before rewiring pointers. This is the #1 bug in linked list code.
  2. Not handling the empty list case. If head is None, calling head.next crashes. Always check for None before accessing properties. A dummy head node eliminates this class of bugs entirely.
  3. Forgetting to update the head pointer. When you delete the first node or insert before it, the head changes. If you only update node.next pointers but forget to reassign self.head, your list is broken.
  4. Creating infinite loops. In circular list operations, or when rewiring pointers incorrectly, it's easy to create a cycle by accident. If your traversal never terminates, check your pointer assignments for accidental loops.
  5. Using a linked list when an array would be faster. Linked lists have poor cache locality (nodes are scattered in memory), so traversal is much slower than arrays in practice. For most problems, a Python list or JavaScript Array is faster. Only reach for a linked list when the problem specifically requires one, or when you need O(1) middle insertion with a known reference.

Real-World Example: LRU Cache

An LRU (Least Recently Used) cache stores the N most recently accessed items. When the cache is full and a new item arrives, the least recently used item gets evicted. Every major browser, CDN, and database uses this strategy.

The trick: combine a doubly linked list (for O(1) reordering) with a hash map (for O(1) lookup). The most recently used item is at the head, the least recently used is at the tail.

Python
class LRUCache:
    """LRU Cache with O(1) get and O(1) put.
    
    Hash map: key -> node (for instant lookup)
    Doubly linked list: maintains access order
      - Head side = most recently used
      - Tail side = least recently used (eviction candidate)
    """
    def __init__(self, capacity):
        self.capacity = capacity
        self.cache = {}  # key -> DNode

        # Sentinel nodes (dummy head/tail)
        self.head = DNode(None)
        self.tail = DNode(None)
        self.head.next = self.tail
        self.tail.prev = self.head

    def get(self, key):
        if key not in self.cache:
            return -1
        node = self.cache[key]
        # Move to front โ€” this item was just accessed
        self._remove(node)
        self._add_front(node)
        return node.val

    def put(self, key, value):
        if key in self.cache:
            self._remove(self.cache[key])
        node = DNode(value)
        node.key = key  # Store key so we can clean up the dict on eviction
        self._add_front(node)
        self.cache[key] = node

        if len(self.cache) > self.capacity:
            # Evict the least recently used (tail side)
            lru = self.tail.prev
            self._remove(lru)
            del self.cache[lru.key]

    def _remove(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev

    def _add_front(self, node):
        node.next = self.head.next
        node.prev = self.head
        self.head.next.prev = node
        self.head.next = node

This is exactly the approach used by Python's functools.lru_cache, Redis's key eviction, and Linux's page cache. The doubly linked list is what makes O(1) eviction possible โ€” you always know which node is least recently used (it's right next to the tail sentinel).

Interview Patterns

Linked list problems test your ability to manipulate pointers without losing track of nodes. The patterns are fewer than arrays, but the pointer logic can get tricky fast.

๐Ÿ’ก Fast and Slow Pointers (Floyd's Tortoise and Hare)

Move one pointer one step at a time and another two steps at a time. If there's a cycle, they'll meet. If there's no cycle, the fast pointer hits null. Also used to find the middle of a list in one pass โ€” when fast reaches the end, slow is at the middle.

Works for: cycle detection, finding cycle start, finding the middle node, checking palindrome.

๐Ÿ’ก Reverse a Linked List

The bread and butter of linked list interviews. Use three pointers: prev, curr, and next_temp. At each step, save the next node, point current node backward, then advance. You're literally flipping arrows one by one.

Can be done iteratively (O(1) space) or recursively (O(n) space from the call stack). Interviewers usually prefer iterative.

๐Ÿ’ก Dummy Head Node

Create a fake node that points to the head of your list. Build your result off this dummy node. At the end, return dummy.next. This eliminates every "what if the head changes?" edge case and lets you write cleaner code under pressure.

Almost mandatory for: merge two sorted lists, remove nth from end, partition list.

๐Ÿ’ก Merge / Zip Two Lists

Walk both lists with one pointer each. At each step, pick the smaller node (for merge) or alternate (for zip). Attach it to your result. When one list runs out, attach the rest of the other.

This pattern is the basis for merge sort on linked lists, which is actually the preferred sort since linked lists don't have random access (so quicksort's partition step is awkward).

๐Ÿ’ก Two Pointers with a Gap

To find the Nth node from the end in one pass: advance the first pointer N steps ahead. Then move both pointers forward together. When the first pointer hits null, the second pointer is at the Nth node from the end.

Classic problem: Remove Nth Node From End of List (LC #19). The gap technique avoids needing to know the list's length.

๐Ÿ’ก Recursive Decomposition

Many linked list problems have clean recursive solutions. The idea: solve the problem for head.next (the rest of the list), then combine with head. Reversing a list recursively: reverse everything after head, then make head's next point back to head.

Be careful: recursion uses O(n) stack space. For lists longer than ~1,000 nodes in Python (or ~10,000 in other languages), use iteration instead.

Practice Problems

Do these in order. The first three are warm-ups. The last three will test you.

# Problem Difficulty Key Pattern
206 Reverse Linked List Easy Three-pointer reversal
21 Merge Two Sorted Lists Easy Dummy head + two-pointer merge
141 Linked List Cycle Easy Fast/slow pointers
19 Remove Nth Node From End Medium Two pointers with gap
143 Reorder List Medium Find middle + reverse + merge
146 LRU Cache Medium Doubly linked list + hash map
25 Reverse Nodes in k-Group Hard Reverse sublists + pointer bookkeeping