skip to content

Why is a three-branch delete (empty, head, scan) a dummy-head refactor and not just style?

level: seniorimportance: should knowfreq 40%

answer

  1. Count the branches and what triggers each
  2. Which branch writes outside the chain
  3. Two input shapes drive two-thirds of the code
  4. Give position zero a predecessor
  5. One node per list buys one code path

basics

~10 s

All three branches exist because the first node has no predecessor. A dummy head supplies one, collapsing them into a single scan-and-relink and deleting the head-reference reassignment where the real defects live.

solid answer

~50 s

Two of the three paths exist for one input shape each — the empty list and the target-is-first case — and only the third is exercised by ordinary traffic, so the code most likely to be wrong is the code least likely to be covered. Worse, the head branch is the only one that writes state outside the chain: it reassigns the list's head reference, and forgetting or misordering that write is the recurring defect. It also gets copied into every other mutating routine. With a dummy head the whole routine is `prev = sentinel; while prev.next != null; if match then prev.next = prev.next.next`, which covers empty, first, middle and last identically. In review I would price the cost honestly — one node per list, callers get `sentinel.next`, traversals start one node in — and I would insist the empty and single-element cases stay in the test suite, since they now run the same path as everything else.

code

pseudocode · 12 lines
pseudocode
delete_first_match(list, target):
    if list.head == null
        return
    if list.head.value == target
        list.head = list.head.next
        return
    prev = list.head
    while prev.next != null
        if prev.next.value == target
            prev.next = prev.next.next
            return
        prev = prev.next

go deeper

for a junior

Be able to point at the empty-list and first-node branches and say that a dummy head removes both by giving the first node a predecessor.

for a middle

Rewrite the routine as a single scan with the anchor as the starting predecessor, and trace empty, single-element and last-element inputs through it out loud.

for a senior

Make the review argument: inverted coverage, the one branch that writes outside the chain, duplication across every mutating routine, and the honest cost of one node per list.

for a principal

Own the standard: is the anchor a house convention for every list-shaped structure, or does the fleet's memory profile and the team's familiarity argue for keeping explicit branches with mandatory edge-case tests?

## Read the shape, not the lines The routine in the fragment has three paths. Path one returns early when the chain is empty. Path two matches the first node and reassigns the list's head reference. Path three walks with a trailing reference and relinks. Three paths, one job. The reason there are three is structural, not stylistic: in a bare singly-linked list every node is reached through its predecessor, and the first node has none. Path two is the code that stands in for the predecessor that does not exist, and path one is the code that stands in for a chain that has no first node either. Both are consequences of the same missing node. ## Why a reviewer should push on it **Coverage is inverted.** Path three runs on almost every input; paths one and two run on exactly two input shapes. The branches that get the least incidental exercise are the ones with the fiddliest job. Take a ledger of signed amounts kept newest-first: reversing the most recent entry is always the target-is-first case, so on that workload path two is the hot path and path three is the rarity — and the code review, the profiler and the test fixtures usually assume the opposite. **It is the only path that writes outside the chain.** Path two assigns to the list's head reference. That is the one line whose omission produces a list still pointing at an unlinked node, or a list that reports itself empty because the reference was moved too early. Every list-implementation bug story features this line. **It multiplies.** The same two guards reappear in insert-at, splice, filter-in-place, and anything else that mutates. Five routines, ten branches, ten chances to get the outside-the-chain write wrong. ## What the refactor actually produces With a permanent anchor node in front of the chain, the list handle points at the anchor and the first real element is `sentinel.next`. Delete becomes one path: - set `prev` to the sentinel - while `prev.next` is not null: if `prev.next` is the target, set `prev.next = prev.next.next` and stop; otherwise advance `prev` Empty list: the loop never executes. Target is the first node: `prev` is the sentinel and the general relink applies. Target is the last node: the relink writes null, which the loop condition already understands. The head reference is never written, because it never has to move. ## What it does not buy, and say so first The refactor changes no complexity: the search is still a linear walk and the relink is still constant time. It costs one node per list and adds a discipline — callers are handed `sentinel.next`, never the anchor, and every traversal, count and search starts one node in. A reviewer who sells the change as a performance win has misread it; the return is branch count and bug surface. ## The tests that must survive it Because every input now runs the same path, it is tempting to trim the cases. Do the opposite — keep them as the regression evidence that the collapse was sound: empty list; single element that matches; single element that does not match; target at the front; target at the back; target absent; duplicates present, where only the first is unlinked. If the structure also maintains a size counter or a tail reference, each of those cases must assert them too, since the anchor does nothing to keep them honest. ## When to leave the three branches alone There are honest reasons to decline: - **Many tiny lists.** One anchor node per list is invisible for a few long lists and is a real memory line when a service holds millions of one- and two-element chains. - **The structure is built once and never mutated.** With no mutating routines to unify, the anchor pays a cost for a benefit nobody collects. - **Nodes escape to callers.** If surrounding code hands out raw node references and relinks them externally, you cannot guarantee the anchor stays private, and a leaked anchor is worse than the branches you removed. In those cases the right review comment is not "add a sentinel" but "the empty and single-element cases are the untested majority of this routine — assert them". Naming the cost, the alternative and the decline conditions is what makes the argument a senior one rather than a preference.

  • Which test cases must survive the refactor?
    Empty list, single element matching, single element not matching, target at the front, target at the back, target absent, and duplicates where only the first is unlinked. They now all run the same path, which is a reason to keep them as evidence the collapse was sound, not a reason to trim them.
  • Does the refactor change the routine's complexity?
    No. Finding the target is still a linear walk and the relink is still constant time. The return is fewer branches, one less place to forget the head-reference write, and a routine whose rare inputs run the same code as its common ones. Selling it as a performance win misreads it.
  • When would you leave the three branches in place?
    When the service holds millions of tiny lists and one extra node each is a real memory line; when the structure is built once and never mutated, so there is nothing to unify; or when surrounding code hands out raw node references, making a private anchor impossible to guarantee. Then ask for explicit empty and single-element assertions instead.

saying these in an interview costs you the question

  • Argues the refactor improves asymptotic complexity
  • Says the branches are fine because tests pass
  • Calls sentinels a wasted node with no bug argument
  • Keeps the head-reference reassignment after refactoring
  • Assumes the head case is rare in every workload

context