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.nextat 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.
What you’ll learn
Section titled “What you’ll learn”- Why
dummyplus atailpointer is the standard list builder. - How a dummy makes head deletion identical to any other deletion.
- The
prev/currrewiring 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.
Visual intuition
Section titled “Visual intuition”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.
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.
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.
The cue
Section titled “The cue”Template 1 — build a list
Section titled “Template 1 — build a list”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 discardedThe 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.
Template 2 — merge two sorted lists
Section titled “Template 2 — merge two sorted lists”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.nextTwo things worth noticing:
tail.next = a or breplaces a second loop. At most one list is non-empty, and it is already sorted and already linked — so splice the whole remainder in rather than copying node by node.- Relinking, not copying. No new nodes are allocated, so this is extra space.
Template 3 — delete a node, including the head
Section titled “Template 3 — delete a node, including the head”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.nextThe assignment-order rule
Section titled “The assignment-order rule”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:
# 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.nextNaming 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.
| Time | Space | |
|---|---|---|
| Build / merge / delete with a dummy | — relinking, not copying |
Dry run
Section titled “Dry run”Merge — a = 1→2→4, b = 1→3→4 (LC 21’s example). Each row is one iteration; a and b
show where each cursor points afterwards.
| step | comparison | taken from | tail now at | a head | b head |
|---|---|---|---|---|---|
| 1 | 1 ≤ 1 ✓ | a — <= keeps it stable | 1 (a’s) | 2 | 1 |
| 2 | 1 ≤ 2 ✗ | b | 1 (b’s) | 2 | 3 |
| 3 | 2 ≤ 3 ✓ | a | 2 | 4 | 3 |
| 4 | 3 ≤ 4 ✗ | b | 3 | 4 | 4 |
| 5 | 4 ≤ 4 ✓ | a | 4 (a’s) | None | 4 |
| — | loop ends (a exhausted) | tail.next = a or b splices b’s remaining 4 | — | — | — |
Result 1→1→2→3→4→4.
<=rather than<is what makes the merge stable. At steps 1 and 5 the values tie, and takingafirst preserves the relative order of equal elements. That matters the moment nodes carry payloads beyond the sort key — and it is the same reasonmerge_sortuses<=.- The half-built list temporarily trails into its source. After step 1,
tailis a’s node1, whosenextstill points at a’s own2— so walkingdummy.nextat that instant shows1→2→4, not1. 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 bis , 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
headortail.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.
curr | matches? | action | prev after |
|---|---|---|---|
1st 1 | ✓ | prev.next = curr.next — prev is the dummy, so the head is unlinked with no special case | dummy (stays) |
2nd 1 | ✓ | prev.next = curr.next again, from the same prev | dummy (stays) |
3rd 1 | ✓ | same | dummy (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.
Complexity
Section titled “Complexity”| Operation | Time | Space |
|---|---|---|
| Build a list with a dummy tail | extra — nodes are relinked, not copied | |
| Merge two sorted lists | ||
| Delete all matching nodes | ||
Merge k sorted lists, pairwise cascade | ||
Merge k sorted lists, divide and conquer | recursion | |
Merge k sorted lists, min-heap of heads |
Two things worth being able to say:
- space is the whole point of relinking. Building a new list by copying values is also time but 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: . Pairing them up instead — merge 1+2, 3+4, then the results — walks each element times, giving . That is LC 23, and the heap formulation reaches the same bound with space and no recursion.
The variant map
Section titled “The variant map”| Variant | The dummy’s job | Canonical problem |
|---|---|---|
| Build an output list | dummy + tail appender | 2 Add Two Numbers |
| Merge two sorted lists | Same, choosing the smaller head each step | 21 |
| Delete nodes | Gives the head a predecessor | 19 · 82 · 203 |
| Delete a whole run of duplicates | prev skips the entire run | 82 |
| Split into two lists | Two dummies, one per output | 86 Partition List · 328 |
| Swap or reverse in groups | prev anchors each group | 24 · 25 · 92 |
Practice — real LeetCode problems
Section titled “Practice — real LeetCode problems”LC 21 — Merge Two Sorted Lists · Easy
Section titled “LC 21 — Merge Two Sorted Lists · Easy”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 . Space — nodes are relinked, not copied.
Two details:
<=rather than<. With equal values, taking fromlist1first 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’sorreturns the first truthy operand, so this attaches whichever list is non-empty, orNoneif both are. It replaces a whole second loop with one 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 , 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 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.
LC 2 — Add Two Numbers · Medium
Section titled “LC 2 — Add Two Numbers · Medium”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 . Space for the output, which is required, so 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 , one pass. Space .
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.
LeetCode problem set
Section titled “LeetCode problem set”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.
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 step
- 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 digit
- 19Remove Nth Node From End of ListmediumBoth pointers start at the dummy so head-removal is uniform
- 24Swap Nodes in PairsmediumName the nodes before rewiring; `prev` anchors each pair
- 61Rotate ListmediumFind the length, take `k % length`, then relink at the new break
- 82Remove Duplicates from Sorted List IImediumDelete **entire** runs -- the head can go, so a dummy is essential
- 86Partition Listmedium**Two** dummies; remember `greater.next = None` or you build a cycle
Interview follow-ups
Section titled “Interview follow-ups”| They ask | What they’re checking | The answer |
|---|---|---|
| “Why a dummy node?” | Whether you can articulate it | It gives the head a predecessor, so insertion and deletion have no special case; you return dummy.next |
| “What’s the space complexity?” | Precision | — one sentinel node, and existing nodes are relinked rather than copied |
| “Could you do it recursively?” | Flexibility | Usually yes, at stack space; the iterative dummy version is |
“Why <= and not < when merging?” | Care | It makes the merge stable, preserving relative order of equal elements |
| “How do you avoid losing the rest of the list?” | Method | Name the nodes you need in local variables before reassigning any next |
| “When does a dummy not help?” | Judgement | Pure 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 instinct | Fast/slow pointers; and remember to terminate every list you split off |
Edge-case checklist
Section titled “Edge-case checklist”- Empty list —
head 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 tail —
n = 1in LC 19; check your loop leavesfaston the last node. - Both lists empty (LC 21) — must return
None, not a dangling dummy. - One list empty — the
tail.next = a or bsplice covers it. - Final carry (LC 2) —
[5] + [5]gives[0,1]; theor carrycondition. - All nodes identical —
[1,1,1]with the delete template; tests thatprevdoes 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.
Self-check
Section titled “Self-check”-
What does the dummy node actually buy you?
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.
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.
-
In the merge, why `if a.val <= b.val` rather than `<`?
Stability only becomes visible when nodes carry payloads beyond the sort key, which is exactly when it matters. Same reason merge sort uses <=.
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 <=.
-
`tail.next = a or b` after the loop — what is it doing, and why is it not a second loop?
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.
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.
-
In `remove_all`, when should `prev` advance?
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.
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.
-
Why walk `dummy.next` inside the merge loop and see the wrong list?
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.
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.
-
Merging k sorted lists by folding each into an accumulator is O(nk). What gets you O(n log 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.
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.
Recall card
Section titled “Recall card”- 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. - Do —
dummy = ListNode(); tail = dummy, build withtail.next = node; tail = tail.next, and returndummy.next, neverdummy. - Delete —
dummy = ListNode(0, head), thenprev.next = curr.nexton a match, and advanceprevonly when the node is kept. - Merge —
<=for stability; after the loop,tail.next = a or bsplices the remainder in . - Relink, never copy — 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 , merge , all space.
klists: via pairing or a heap, not 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. Returndummy.next. dummy+tailis the standard list builder; always advancetail.- When merging, splice the remainder with
tail.next = a or binstead of a second loop. - When deleting,
prevadvances 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 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.coffeeCtapch.feedbackHeading
pch.feedbackSubheading