Skip to content

Dummy Head Rewiring and Merging

Almost every bug in a linked-list problem lives at the head. Deleting the first node is different from deleting any other node. Building a list needs a special case for the very first element. Merging two lists needs to decide which head wins before the loop can even start.

The dummy head (or sentinel) makes all of that disappear:

Allocate one throwaway node that sits before the real list. Now every real node has a predecessor, so no operation is ever a special case. Return dummy.next at the end.

One extra node in exchange for deleting every if node is head branch you would otherwise write. It is the highest-leverage trick in this phase.

  • Why dummy plus a tail pointer is the standard list builder.
  • How a dummy makes head deletion identical to any other deletion.
  • The prev/curr rewiring discipline, and the assignment-order rule that keeps you from losing the rest of the list.
  • Where a dummy does not help.
  • Three real LeetCode problems solved in the browser: 21, 2, 19.

The dummy node exists to delete a branch. Watch the tail cursor: every append is tail.next = x with no “is the result empty yet?” test anywhere.

listWhy every list-building function starts with a dummyLC 21 · O(m + n)
dummytail
remaininga:1a:3a:5b:2b:4b:6
a1,3,5b2,4,6
setupThe dummy node is the whole technique. Without it, the first append needs a special case ("is the result empty yet?"), and that branch is where linked-list code goes wrong. With it, "tail.next = x" is unconditionally correct, and the real head is just "dummy.next" at the end.
1/8

Note the final step: once one list is exhausted the other is attached whole, in one assignment, because it is already sorted. And note the comparison is <= rather than < — that is what makes the merge stable.

listA fixed gap between two pointers, in one passLC 19 · one pass
dummy12345laglead
n2
setupA single pass, using a fixed gap between two pointers. The dummy matters here more than usual: if the node to remove *is* the head, lag needs somewhere to stand before it.
1/9

The gap is opened from the dummy, not the head. That is deliberate: it puts lag immediately BEFORE the target, which is the only position from which a node can be unlinked.

build_with_dummy.py
class ListNode:
    def __init__(self, val=0, next=None):
        self.val, self.next = val, next
 
 
def build(values):
    dummy = ListNode()          # throwaway node before the real head
    tail = dummy                # always points at the last real node so far
 
    for v in values:
        tail.next = ListNode(v)
        tail = tail.next        # advance -- forgetting this is the classic bug
 
    return dummy.next           # the real head; dummy is discarded

The pair dummy + tail is the whole idiom. dummy never moves, so it still remembers where the list starts; tail walks forward as you append. Without the dummy, the first append needs if head is None: head = node and every later one needs tail.next = node — two cases instead of one.

merge_two_lists.py
def merge_two_lists(a, b):
    dummy = ListNode()
    tail = dummy
 
    while a and b:
        if a.val <= b.val:              # <= keeps the merge stable
            tail.next, a = a, a.next
        else:
            tail.next, b = b, b.next
        tail = tail.next
 
    tail.next = a or b                  # attach whatever remains, in one line
    return dummy.next

Two things worth noticing:

  • tail.next = a or b replaces a second loop. At most one list is non-empty, and it is already sorted and already linked — so splice the whole remainder in O(1)O(1) rather than copying node by node.
  • Relinking, not copying. No new nodes are allocated, so this is O(1)O(1) extra space.

Template 3 — delete a node, including the head

Section titled “Template 3 — delete a node, including the head”
delete_with_dummy.py
def remove_all(head, target):
    dummy = ListNode(0, head)          # dummy points AT the real head
    prev, curr = dummy, head
 
    while curr:
        if curr.val == target:
            prev.next = curr.next      # unlink; prev stays put
        else:
            prev = curr                # only advance prev when we keep curr
        curr = curr.next
 
    return dummy.next

When rewiring, you can destroy the pointer you still need. The safe habit is to save what you are about to overwrite before overwriting it:

swap_pairs_ordering.py
# LC 24 -- swap every two adjacent nodes
def swap_pairs(head):
    dummy = ListNode(0, head)
    prev = dummy
 
    while prev.next and prev.next.next:
        first = prev.next               # name the nodes BEFORE rewiring
        second = first.next
 
        first.next = second.next        # 1. first now points past second
        second.next = first             # 2. second points back at first
        prev.next = second              # 3. prev adopts the new front
 
        prev = first                    # first is now the tail of this pair
    return dummy.next

Naming the nodes first (first, second) and only then reassigning is what makes this readable and correct. Trying to do it with raw prev.next.next.next chains is how people lose ten minutes in an interview.

TimeSpace
Build / merge / delete with a dummyO(n)O(n)O(1)O(1) — relinking, not copying

Merge — a = 124, b = 134 (LC 21’s example). Each row is one iteration; a and b show where each cursor points afterwards.

stepcomparisontaken fromtail now ata headb head
11 ≤ 1 ✓a<= keeps it stable1 (a’s)21
21 ≤ 2 ✗b1 (b’s)23
32 ≤ 3 ✓a243
43 ≤ 4 ✗b344
54 ≤ 4 ✓a4 (a’s)None4
loop ends (a exhausted)tail.next = a or b splices b’s remaining 4

Result 112344.

  • <= rather than < is what makes the merge stable. At steps 1 and 5 the values tie, and taking a first preserves the relative order of equal elements. That matters the moment nodes carry payloads beyond the sort key — and it is the same reason merge_sort uses <=.
  • The half-built list temporarily trails into its source. After step 1, tail is a’s node 1, whose next still points at a’s own 2 — so walking dummy.next at that instant shows 124, not 1. Step 2 overwrites that pointer. Nothing is wrong; it is simply what relinking in place looks like mid-flight, and it is why printing the list inside the loop is a confusing way to debug this.
  • tail.next = a or b is O(1)O(1), not a second loop. At most one list is non-empty, it is already sorted and already linked, so one pointer assignment adopts the whole remainder.
  • The dummy is what removes the special case. Without it, the first iteration would need to decide whether it is setting head or tail.next — a branch inside the hot loop, and the usual source of “works except on the first element”.

Deletion — remove_all([1,1,1], 1). Every node matches, including the head, which is exactly why the dummy exists.

currmatches?actionprev after
1st 1prev.next = curr.nextprev is the dummy, so the head is unlinked with no special casedummy (stays)
2nd 1prev.next = curr.next again, from the same prevdummy (stays)
3rd 1samedummy (stays)

Result: empty list, dummy.next is None. Advance prev unconditionally and the result is [1] — the third node survives, because after unlinking the second, prev had already moved onto a node that is no longer in the list. On [1,2,6,3,4,5,6] with target 6 the same bug is invisible (the two 6s are not adjacent), which is why consecutive matches are the test case that must be in your head.

OperationTimeSpace
Build a list with a dummy tailO(n)O(n)O(1)O(1) extra — nodes are relinked, not copied
Merge two sorted listsO(n+m)O(n + m)O(1)O(1)
Delete all matching nodesO(n)O(n)O(1)O(1)
Merge k sorted lists, pairwise cascadeO(nk)O(nk)O(1)O(1)
Merge k sorted lists, divide and conquerO(nlogk)O(n \log k)O(logk)O(\log k) recursion
Merge k sorted lists, min-heap of headsO(nlogk)O(n \log k)O(k)O(k)

Two things worth being able to say:

  • O(1)O(1) space is the whole point of relinking. Building a new list by copying values is also O(n)O(n) time but O(n)O(n) space, and it hands the caller different node objects — which breaks any problem where node identity matters.
  • The k-list bound is the interesting one. Merging list 1 into 2, then into 3, and so on, re-walks the accumulated prefix every time: O(nk)O(nk). Pairing them up instead — merge 1+2, 3+4, then the results — walks each element logk\log k times, giving O(nlogk)O(n \log k). That is LC 23, and the heap formulation reaches the same bound with O(k)O(k) space and no recursion.
VariantThe dummy’s jobCanonical problem
Build an output listdummy + tail appender2 Add Two Numbers
Merge two sorted listsSame, choosing the smaller head each step21
Delete nodesGives the head a predecessor19 · 82 · 203
Delete a whole run of duplicatesprev skips the entire run82
Split into two listsTwo dummies, one per output86 Partition List · 328
Swap or reverse in groupsprev anchors each group24 · 25 · 92

Problem. Merge two sorted linked lists into one sorted list by splicing their nodes together. Return the head of the merged list.

Constraints. 0 <= len(each list) <= 50, -100 <= Node.val <= 100, both lists sorted ascending.

Examples. [1,2,4] + [1,3,4] gives [1,1,2,3,4,4] · [] + [] gives [] · [] + [0] gives [0]

Editorial — approach, complexity, follow-ups

Walk both lists, always attaching the smaller head. The dummy means you never have to decide which list provides the first node — the loop body handles it like any other step.

Time O(n+m)O(n + m). Space O(1)O(1) — nodes are relinked, not copied.

Two details:

  • <= rather than <. With equal values, taking from list1 first makes the merge stable (equal elements keep their relative order across lists). It does not change the output for this problem, but it is the correct habit and it matters when nodes carry payloads.
  • tail.next = list1 or list2. Python’s or returns the first truthy operand, so this attaches whichever list is non-empty, or None if both are. It replaces a whole second loop with one O(1)O(1) splice.

Both-empty returning [] and one-empty returning the other are handled with no explicit guards, which is the dummy earning its keep.

Follow-ups you should expect: “Merge k lists (LC 23)?” — either merge pairwise in O(nlogk)O(n \log k), or use a heap of the current heads; see K-way Merge. “Do it recursively?” — merge(a, b) returns the smaller head with head.next = merge(rest, other); elegant but O(n+m)O(n + m) stack space. “Merge descending?” — flip the comparison. “What if the inputs were unsorted?” — you would have to sort first; the merge itself relies on sortedness.

Problem. Two non-empty linked lists represent non-negative integers with digits stored in reverse order, one digit per node. Add them and return the sum as a linked list in the same format.

Constraints. 1 <= len(each list) <= 100, 0 <= Node.val <= 9, no leading zeros except the number 0 itself.

Examples. [2,4,3] + [5,6,4] gives [7,0,8] (342 + 465 = 807) · [0] + [0] gives [0] · [9,9,9,9,9,9,9] + [9,9,9,9] gives [8,9,9,9,0,0,0,1]

Editorial — approach, complexity, follow-ups

Reverse digit order is deliberate and helpful: the list heads are the least significant digits, which is exactly the order you add in by hand. Walk both lists together, carrying as you go.

Time O(max(n,m))O(\max(n, m)). Space O(max(n,m))O(\max(n, m)) for the output, which is required, so O(1)O(1) auxiliary.

The load-bearing detail is or carry in the loop condition. Consider [5] + [5]: both lists are exhausted after one step but carry == 1, and the answer is [0, 1]. Without or carry, the loop ends and you return [0]. The [9,9,9,9,9,9,9] + [9,9,9,9] case is the same trap at scale — its answer is one digit longer than either input.

divmod(total, 10) returning (carry, digit) in one call is tidier than two separate // and % operations, and harder to get backwards.

Follow-ups you should expect: “What if the digits were in forward order (LC 445)?” — reverse both lists first, or use two stacks, or recurse to align the least significant digits. “Add three lists?” — the same loop with carry possibly exceeding 1, so divmod still handles it. “Subtract instead?” — borrowing, plus deciding which number is larger first.

LC 19 — Remove Nth Node From End of List · Medium

Section titled “LC 19 — Remove Nth Node From End of List · Medium”

Problem. Given the head of a linked list, remove the nth node from the end and return the head. Try to do it in one pass.

Constraints. 1 <= list length <= 30, 1 <= n <= list length.

Examples. [1,2,3,4,5], n = 2 gives [1,2,3,5] · [1], n = 1 gives [] · [1,2], n = 1 gives [1]

Editorial — approach, complexity, follow-ups

Open a gap of n nodes between two pointers, then slide both until the fast one reaches the last node. The slow pointer is then exactly n + 1 from the end — the predecessor of the node to remove, which is what you need in a singly linked list.

Time O(L)O(L), one pass. Space O(1)O(1).

The crucial choice is starting both pointers at dummy rather than at head. Landing on the predecessor is only possible because dummy gives the head one. ([1], 1) returning [] is the case that proves it: removing the only node means the head itself goes, and without a dummy you would need a separate if n == length: return head.next branch.

The loop condition while fast.next (not while fast) is what leaves fast on the final node rather than past it, keeping slow one before the target. Off-by-one here is the other common failure.

This is a fast-and-slow-pointer problem too — but note the pointers move at the same speed with a fixed gap, unlike cycle detection where they move at different speeds.

Follow-ups you should expect: “Two passes?” — count the length, then walk length - n steps; simpler to explain, and worth offering first if the one-pass version is not clicking. “What if n exceeded the length?” — the constraints forbid it, but you should say what you would do (validate and return head unchanged). “Remove the nth from the start?” — trivial with a dummy. “Remove all nodes with a given value (LC 203)?” — the delete template above.

Generated from the problem database, so each entry carries its sheet membership and reported companies. Tick them off as you go — progress is saved in this browser, and the Export button writes it to a file you can keep.

8 problems
2 easy6 medium0 hard

Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.

  • 21Merge Two Sorted Listseasy`dummy` + `tail`, then splice the remainder in one stepNeetCode 150Blind 75LeetCode Top Interview 150amazonmicrosoftapplebloomberg
  • 83Remove Duplicates from Sorted ListeasyKeep one of each run; no dummy strictly needed since the head survives
  • 2Add Two Numbersmedium`or carry` in the loop condition emits the final digitNeetCode 150LeetCode Top Interview 150
  • 19Remove Nth Node From End of ListmediumBoth pointers start at the dummy so head-removal is uniformNeetCode 150Blind 75LeetCode Top Interview 150
  • 24Swap Nodes in PairsmediumName the nodes before rewiring; `prev` anchors each pair
  • 61Rotate ListmediumFind the length, take `k % length`, then relink at the new breakLeetCode Top Interview 150
  • 82Remove Duplicates from Sorted List IImediumDelete **entire** runs -- the head can go, so a dummy is essentialLeetCode Top Interview 150
  • 86Partition Listmedium**Two** dummies; remember `greater.next = None` or you build a cycleLeetCode Top Interview 150
They askWhat they’re checkingThe answer
“Why a dummy node?”Whether you can articulate itIt gives the head a predecessor, so insertion and deletion have no special case; you return dummy.next
“What’s the space complexity?”PrecisionO(1)O(1) — one sentinel node, and existing nodes are relinked rather than copied
“Could you do it recursively?”FlexibilityUsually yes, at O(n)O(n) stack space; the iterative dummy version is O(1)O(1)
“Why <= and not < when merging?”CareIt makes the merge stable, preserving relative order of equal elements
“How do you avoid losing the rest of the list?”MethodName the nodes you need in local variables before reassigning any next
“When does a dummy not help?”JudgementPure traversal, and in-place reversal — there the head is expected to change and you already track prev
“How would you detect a bug like a cycle?”Debugging instinctFast/slow pointers; and remember to terminate every list you split off
  • Empty listhead is None; the dummy makes most templates handle it for free.
  • Single node([1], 1) for LC 19 returns []; the head-removal case.
  • Removing the head — the whole reason for the dummy.
  • Removing the tailn = 1 in LC 19; check your loop leaves fast on the last node.
  • Both lists empty (LC 21) — must return None, not a dangling dummy.
  • One list empty — the tail.next = a or b splice covers it.
  • Final carry (LC 2) — [5] + [5] gives [0,1]; the or carry condition.
  • All nodes identical[1,1,1] with the delete template; tests that prev does not advance on a match.
  • Forgetting to terminate a split list (LC 86) — creates a cycle and hangs, rather than failing cleanly.
  • Forgetting tail = tail.next — silently drops everything but the last appended node.
pch.quizTag Dummy head and merging — self-check
  1. What does the dummy node actually buy you?

    pch.quizShowAnswer

    B — It removes the head as a special case: `prev.next = …` works uniformly, including when the node being unlinked or prepended is the head itself — In remove_all([1,1,1], 1) every node matches including the head, and the dummy is what lets one line handle it. Returning `dummy.next` at the end is how the real head is recovered.

  2. In the merge, why `if a.val <= b.val` rather than `<`?

    pch.quizShowAnswer

    B — Because `<=` takes from `a` on a tie, which keeps the merge stable — equal elements retain their original relative order — Stability only becomes visible when nodes carry payloads beyond the sort key, which is exactly when it matters. Same reason merge sort uses <=.

  3. `tail.next = a or b` after the loop — what is it doing, and why is it not a second loop?

    pch.quizShowAnswer

    B — Splicing whichever list still has nodes in O(1): at most one is non-empty, and it is already sorted and already linked, so one pointer assignment adopts the entire remainder — This is the payoff of relinking rather than copying — the remainder needs no work at all, which is what keeps the merge O(1) in space.

  4. In `remove_all`, when should `prev` advance?

    pch.quizShowAnswer

    B — Only when the current node is KEPT — after an unlink, `prev` is already the predecessor of the next node and may have to unlink that one too — Advancing unconditionally turns [1,1,1] with target 1 into [1] — consecutive matches are missed. Non-adjacent duplicates hide the bug entirely, so that test case has to be deliberate.

  5. Why walk `dummy.next` inside the merge loop and see the wrong list?

    pch.quizShowAnswer

    B — Because the node just adopted still points at its own original successor until the next iteration overwrites that pointer — the half-built list temporarily trails into its source — Nothing is broken; it is what in-place relinking looks like mid-flight. It is also why printing the list inside the loop is a misleading way to debug these problems.

  6. Merging k sorted lists by folding each into an accumulator is O(nk). What gets you O(n log k)?

    pch.quizShowAnswer

    B — Pairwise divide and conquer (merge 1+2, 3+4, then the results) or a min-heap of the k list heads — each element is then touched log k times instead of k — The accumulator version re-walks the growing prefix every round. This is LC 23, and the heap formulation reaches the same bound with O(k) space and no recursion.

  • Cue — building, merging, or deleting from a linked list where the head might change. If you are writing if not head or ... to special-case the first node, use a dummy instead.
  • Dodummy = ListNode(); tail = dummy, build with tail.next = node; tail = tail.next, and return dummy.next, never dummy.
  • Deletedummy = ListNode(0, head), then prev.next = curr.next on a match, and advance prev only when the node is kept.
  • Merge<= for stability; after the loop, tail.next = a or b splices the remainder in O(1)O(1).
  • Relink, never copyO(1)O(1) extra space, and node identity is preserved.
  • Save before you overwrite when rewiring; a, b = b, a-style tuple assignment evaluates the right side first, which is what makes the one-liners safe.
  • Cost — build/delete O(n)O(n), merge O(n+m)O(n+m), all O(1)O(1) space. k lists: O(nlogk)O(n \log k) via pairing or a heap, not O(nk)O(nk) by folding.
  • Test cases that catch the real bugs — deleting the head, deleting consecutive matches, one empty list, both empty.
  • A dummy head gives the real head a predecessor, which deletes every is this the first node? special case. Return dummy.next.
  • dummy + tail is the standard list builder; always advance tail.
  • When merging, splice the remainder with tail.next = a or b instead of a second loop.
  • When deleting, prev advances only when you keep a node — otherwise consecutive matches survive.
  • Name the nodes before rewiring. The order of assignments is what makes or breaks pointer surgery.
  • Two dummies to split a list — and terminate the second one, or you create a cycle.
  • Everything here is O(1)O(1) extra space, because you relink existing nodes rather than allocating new ones.

Next: Copy, Flatten and Reorder — the linked-list problems that need a hash map or a recursive descent rather than pure pointer work.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading