Linked Lists
Beginner 7 min read

The dummy-node technique

In one line

Put a fake node in front of the real head so that every real node has a predecessor; then inserting or deleting at the head needs no special case and you simply return dummy.next.

What it is

A dummy node (also called a sentinel) is an extra node that holds no real data and sits before the first real node. Your code works with dummy.next as the head, and at the end returns dummy.next, which is the correct head even if the original head was removed or replaced.

Most linked list bugs come from the head being special: it has no previous node, so 'delete the node after prev' does not work for it, and an empty result list has no tail to append to. A dummy gives every node, including the first, a predecessor, and gives an empty list a tail. Doubly linked lists often use two sentinels, one at each end.

Why interviewers ask

Interviewers like to see clean, short code without fragile if-statements for the head. Using a dummy node in Remove Nth Node From End, Remove Linked List Elements, Merge Two Lists or Partition List signals experience, reduces bugs under pressure and makes your code easier to explain.

How it works

  1. 1Create dummy = ListNode(0, head). Now dummy.next is the real head.
  2. 2Walk with a pointer that starts at dummy, not at head, and always look at cur.next. Deleting is cur.next = cur.next.next, which works even when cur.next is the real head.
  3. 3When building a new list, start with tail = dummy and append with tail.next = node; tail = node. The first append is no longer a special case.
  4. 4For removal from the end, place two pointers at dummy with a gap of n + 1 nodes; when the leading one reaches None, the trailing one is just before the node to remove.
  5. 5Return dummy.next as the new head. Never return dummy itself or the old head variable.
  6. 6In a doubly linked list, keep head and tail sentinels that always exist; every real node then has non-null prev and next, so insert and delete are four pointer updates with no checks.

Worked example · Remove the 2nd node from the end of 1 -> 2 -> 3 -> 4 -> 5

  1. Create dummy -> 1 -> 2 -> 3 -> 4 -> 5. Set slow = fast = dummy, n = 2.
  2. Move fast n + 1 = 3 steps: dummy -> 1 -> 2 -> 3. fast is at node 3; slow is at dummy.
  3. Move both until fast is None: (slow 1, fast 4), (slow 2, fast 5), (slow 3, fast None).
  4. slow is node 3, just before node 4, the 2nd from the end. Set 3.next = 5. Result: 1 -> 2 -> 3 -> 5.
  5. Edge case: n = 5 (remove the head). fast moves 6 steps and ends at None at once, slow stays at dummy, dummy.next = 2. Returning dummy.next gives 2 -> 3 -> 4 -> 5 with no special code.

Picture it

A bookend at the start of a shelf. Every book now has something to its left, so 'slide the book next to this one out' works the same for the very first book, and an empty shelf still has a fixed place to start adding books.

Reference code

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def remove_nth_from_end(head, n):
    dummy = ListNode(0, head)             # handles removing the real head
    slow = fast = dummy
    for _ in range(n + 1):
        fast = fast.next                  # gap of n nodes between slow and fast
    while fast:
        slow, fast = slow.next, fast.next
    slow.next = slow.next.next            # slow is just before the target
    return dummy.next

def partition(head, x):
    """Nodes < x first, then nodes >= x, keeping relative order."""
    less = lt = ListNode()
    more = mt = ListNode()
    while head:
        if head.val < x:
            lt.next, lt = head, head
        else:
            mt.next, mt = head, head
        head = head.next
    mt.next = None                        # cut the old link to avoid a cycle
    lt.next = more.next
    return less.next

remove_nth_from_end works even when the head is removed, because slow can stay at the dummy. partition builds two lists behind two dummies, joins them, and sets mt.next = None so the old links cannot form a cycle. Both are O(n) time and O(1) extra space.

Key patterns

Delete matching nodes

When: Remove all nodes with a value, or remove duplicates from a sorted list

How: cur = dummy; if cur.next matches, cur.next = cur.next.next, else cur = cur.next.

Build a result list

When: Merge, partition, add two numbers

How: tail = dummy; append with tail.next = node; tail = tail.next; return dummy.next.

Gap pointers from the dummy

When: Remove the n-th node from the end

How: Start both at dummy, advance fast n + 1 steps, then move both; slow ends just before the target.

Two sentinels in a doubly linked list

When: LRU cache, deque, browser history

How: Fixed head and tail sentinels so every real node has prev and next; no null checks on insert or delete.

Multiple dummies

When: Partition or odd-even split

How: One dummy per output list; join them at the end and terminate the last list with None.

Real-world use

Linux kernel list_head

Kernel lists are circular doubly linked lists whose head is a sentinel list_head with no data; an empty list is just the head pointing to itself, so add and delete never check for null.

C++ std::list

Common implementations of std::list, including libstdc++, use a sentinel node that marks both the beginning and the end, which is what end() points to, so insertion and erase need no head or tail special cases.

Python OrderedDict and lru_cache

The pure-Python OrderedDict in CPython's collections module keeps a sentinel root node in a circular doubly linked list, and functools.lru_cache uses a root sentinel the same way, so moving or removing entries is always the same few pointer updates.

In a project

A moderation tool filters a linked list of comments, removing any flagged ones. Bug reports show that when the first comment is flagged, the whole thread disappears or the flagged comment stays visible.

  1. Read the code: it starts with cur = head and checks cur.next, so the head is never checked, and there is a separate branch for the head that mishandles consecutive flagged comments.
  2. Rewrite with dummy = ListNode(0, head) and cur = dummy; while cur.next exists, skip flagged nodes with cur.next = cur.next.next, otherwise advance.
  3. Return dummy.next as the new head of the thread.
  4. Add tests: all comments flagged, first only, last only, consecutive flagged, empty thread.

Impact: The bug class disappears because there is no head special case left, the function shrinks to a few lines, and the new tests lock in the behaviour for every edge case.

Trade-offs

Use it when

  • The head may be removed or replaced
  • You are building a new list node by node
  • You maintain a doubly linked list with frequent inserts and deletes at the ends

Avoid it when

  • Pure read-only traversals, where there is nothing to rewire
  • Memory-critical embedded code where an extra node per list matters (rare)
  • Code that exposes nodes to callers who might mistake the sentinel for real data; hide it behind an API

Cost: O(1) extra space (one node per dummy); no change to the time complexity of the algorithm.

Common mistakes

  • Returning head instead of dummy.next, which returns a removed node when the head was deleted.
  • Starting the walk at head instead of dummy, so the first node is never checked for removal.
  • Advancing cur after a deletion, which skips the next node and misses consecutive matches.
  • Forgetting to set the last tail's next to None when building lists from existing nodes, creating a cycle.
  • Moving fast only n steps instead of n + 1 in remove-nth-from-end, so slow stops on the target instead of before it.

Interview questions

Quick revision

  • dummy = ListNode(0, head); work from dummy; return dummy.next.
  • Every real node now has a predecessor: no head special case.
  • Delete: cur.next = cur.next.next, do not advance after deleting.
  • Build: tail = dummy; tail.next = node; tail = node.
  • Remove n-th from end: fast moves n + 1 from dummy.
  • Doubly linked list: head and tail sentinels, no null checks.
  • Terminate built lists with tail.next = None.

Check yourself

1. Why return dummy.next instead of head?

2. In remove-nth-from-end starting both pointers at dummy, how many steps does fast move first?

3. While deleting all nodes equal to 6 from 6 -> 6 -> 1, what goes wrong if you advance cur after each deletion?

4. What does a doubly linked list with head and tail sentinels guarantee?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free