Topicsβ€ΊLinked Listβ€ΊDummy Head / Sentinel Node
πŸ“–

Dummy Head / Sentinel Node

Linked List
~20 min read
5 practice problems

Overview

The dummy head (also called a sentinel node) is a technique where you add an extra node before the real head of a linked list. This node does not hold meaningful data; it exists only to simplify pointer logic. By using a dummy, you avoid special-case code for an empty list or for operations that change the head (e.g. insert at front, delete the first node).

Why it matters: Without a dummy, you often need separate branches: "if the list is empty" or "if we're deleting the head." With a dummy, every node (including the original head) has a "previous" node, so the same pointer-update logic applies everywhere.


When to Use a Dummy Head

Use a dummy head when:

  • Inserting or deleting at the front β€” the head pointer would otherwise change; with a dummy, you only update dummy.next.
  • Building a new list β€” you can append to tail while keeping tail = dummy initially; no "is this the first node?" check.
  • Removing nodes that match a condition β€” e.g. "remove all nodes with value X"; a single pass with prev and curr works, and prev can start at the dummy.
  • Merging or rearranging lists β€” you build the result from a dummy and return dummy.next.

How It Works

  1. Create a dummy node: const dummy = new ListNode(-1); (or any placeholder value).
  2. Set dummy.next = head so the real list hangs off the dummy.
  3. Use a pointer (e.g. prev or tail) that starts at dummy and moves along the list as you do your logic.
  4. When done, return dummy.next as the new head (which may be null if the list became empty).

Key idea: The dummy is the "previous" node for the original head, so operations at the front are no longer special.


Example 1: Remove All Nodes With a Given Value

Problem: Given the head of a linked list and an integer val, remove all nodes whose value equals val and return the new head.

Without a dummy, you'd need a loop to skip past any leading nodes with value val, then a second loop for the rest. With a dummy, one pass suffices.

function removeElements(head: ListNode | null, val: number): ListNode | null {
  const dummy = new ListNode(-1);
  dummy.next = head;
  let prev: ListNode = dummy;
  let curr: ListNode | null = head;

  while (curr !== null) {
    if (curr.val === val) {
      prev.next = curr.next;
      curr = curr.next;
    } else {
      prev = curr;
      curr = curr.next;
    }
  }
  return dummy.next;
}
  • prev always points to the last "kept" node (initially the dummy).
  • When curr.val === val, we skip curr by setting prev.next = curr.next; no special case when the node to remove is the head.
  • Time: O(n), Space: O(1).

Example 2: Merge Two Sorted Lists (Building a New List)

Problem: Merge two sorted linked lists and return the head of the merged list.

We build the result list by repeatedly appending the smaller of the two current heads. A dummy gives us a single place to append and avoids "is this the first node?" checks.

function mergeTwoLists(
  l1: ListNode | null,
  l2: ListNode | null
): ListNode | null {
  const dummy = new ListNode(-1);
  let tail = dummy;

  while (l1 !== null && l2 !== null) {
    if (l1.val <= l2.val) {
      tail.next = l1;
      l1 = l1.next;
    } else {
      tail.next = l2;
      l2 = l2.next;
    }
    tail = tail.next;
  }
  tail.next = l1 !== null ? l1 : l2;
  return dummy.next;
}
  • tail starts at the dummy and moves to the last node we appended.
  • We always set tail.next to the chosen node and then advance tail; no branch for "first node."
  • Time: O(n + m), Space: O(1) (we only change pointers).

Example 3: Insert a Node at the Head

Problem: Insert a new node with value val at the beginning of the list and return the new head.

With a dummy, "insert at head" is the same as "insert after dummy."

function insertAtHead(head: ListNode | null, val: number): ListNode | null {
  const dummy = new ListNode(-1);
  dummy.next = head;
  const newNode = new ListNode(val);
  newNode.next = dummy.next;
  dummy.next = newNode;
  return dummy.next;
}

Even when head is null, dummy.next becomes the new node and newNode.next becomes null, so the result is correct. No separate "if (head === null)" branch.


Example 4: Remove the Nth Node From the End (Two Pointers + Dummy)

Problem: Remove the nth node from the end of the list and return the head. (If we need to remove the first node, the head changes.)

A dummy ensures that when we use a "previous" pointer, it exists even when the node to remove is the head.

function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
  const dummy = new ListNode(-1);
  dummy.next = head;
  let fast: ListNode | null = dummy;
  let slow: ListNode | null = dummy;

  for (let i = 0; i <= n && fast !== null; i++) {
    fast = fast.next;
  }
  while (fast !== null) {
    fast = fast.next;
    slow = slow!.next;
  }
  if (slow?.next != null) {
    slow.next = slow.next!.next;
  }
  return dummy.next;
}
  • fast moves n+1 steps ahead of slow, so when fast hits the end, slow is the predecessor of the nth-from-end node.
  • Deleting is slow.next = slow.next.next. If the node to delete is the head, slow is the dummy, so we correctly update dummy.next and return the new head.
  • Time: O(n), Space: O(1).

Example 5: Swap Nodes in Pairs

Problem: Given a linked list, swap every two adjacent nodes and return the new head. (If the list has an odd length, the last node stays.)

We need a "previous" pointer for the pair. Using a dummy, the first pair is handled like every other pair.

function swapPairs(head: ListNode | null): ListNode | null {
  const dummy = new ListNode(-1);
  dummy.next = head;
  let prev: ListNode = dummy;

  while (prev.next !== null && prev.next.next !== null) {
    const first = prev.next;
    const second = prev.next.next;
    prev.next = second;
    first.next = second.next;
    second.next = first;
    prev = first;
  }
  return dummy.next;
}
  • prev points to the node before the current pair. We swap first and second and then set prev = first (the node that is now second in the pair) to move to the next pair.
  • Time: O(n), Space: O(1).

Edge Cases to Handle

  • Empty list β€” head === null. With a dummy, dummy.next is null; after your logic, returning dummy.next correctly yields null.
  • Single node β€” e.g. remove that node; prev stays at dummy, prev.next becomes null, so dummy.next is null.
  • All nodes removed β€” e.g. remove all nodes with value val and every node had that value; again dummy.next becomes null.
  • Head changes β€” any operation that would change the head is handled because the "previous" of the old head is the dummy; you never need to reassign a separate head variable.

Common Mistakes

  1. Forgetting to return dummy.next β€” You must return the node after the dummy, not head or dummy.
  2. Losing the reference to the list β€” Keep prev (or tail) and update it correctly so you don't break the list when rewiring.
  3. Using the dummy's value β€” The dummy's value is irrelevant; only dummy.next and the fact that it's a valid "previous" node matter.
  4. Not advancing prev (or tail) β€” In remove/build logic, advance the pointer that represents "last kept node" or "last appended node" so the next iteration sees the right place.

Time and Space Complexity

  • Time: Usually one pass, O(n) (or O(n + m) for two lists).
  • Space: O(1) β€” only a fixed number of pointers (dummy, prev, curr, etc.); the dummy is one node.

Summary

  • A dummy head is an extra node before the real head so that the first node has a "previous" node like any other.
  • Use it to avoid special cases for empty list and for operations at the front.
  • Pattern: Create dummy β†’ set dummy.next = head β†’ run your logic with a pointer starting at dummy β†’ return dummy.next.
  • Apply this in remove-by-value, merge two lists, insert at head, remove nth from end, swap pairs, and many other linked-list problems.

For more patterns that build on this, see Fast and Slow Pointers and Reverse In-Place.