skip to content

In a linked-node queue, what breaks in the tail reference when the last element is dequeued?

level: middleimportance: should knowfreq 50%

answer

  1. two references, two ends
  2. trace the queue down to one element
  3. dequeue updates only one of them
  4. which node does the other still name
  5. both ends must empty together

basics

~20 s

Dequeuing the last element empties the head reference but leaves the tail pointing at the node just removed. A later enqueue then links onto that detached node while head stays empty, so the item is lost and the queue looks permanently empty.

solid answer

~50 s

A linked queue holds two references so both ends are O(1): `head` for removal and `tail` so enqueue can append without walking the list. The trap is the empty-to-one-element boundary. Dequeue naturally touches only `head` — advance it and return the value — so when the last element leaves, `head` becomes empty while `tail` still points at the node that was just unlinked. If enqueue decides "is the queue empty?" by testing `tail`, it takes the append branch, links the new node onto the detached one and moves `tail` there. `head` is never set, so the queue reports empty forever and every subsequent arrival vanishes. The fix is that the two ends must reach the empty state together: clear `tail` when the last node leaves, and test emptiness with one consistent condition in both operations.

code

pseudocode · 16 lines
pseudocode
// singly linked queue, fields: head, tail

enqueue(v):
  node = Node(v)
  if tail == null:
    head = node
    tail = node
  else:
    tail.next = node
    tail = node

dequeue():
  if head == null: return EMPTY
  v = head.value
  head = head.next
  return v

go deeper

for a junior

Know that a linked queue keeps a reference to each end, one for removal and one for appending, and that both operations are then constant time. Be able to draw the nodes and say which reference moves on which operation.

for a middle

Trace the queue down to a single element and out. Explain that dequeue naturally touches only the front reference, that the back one is then stale, and what a following enqueue does with it depending on which field it tests for emptiness.

for a senior

Treat it as a review finding: name the empty-to-one-element transition as the boundary any queue implementation must be tested at, and require one consistent emptiness test rather than two fields checked in two places.

for a principal

Argue the general rule — redundant references to one structure are duplicated state, and duplicated state needs an invariant someone owns. Weigh that maintenance burden against a contiguous alternative that carries no cross-references at all.

## Why two references at all A singly linked list gives you O(1) removal at the front for free: hold `head`, read its value, move `head` to `head.next`. Appending is the awkward direction — to add at the back you must reach the last node, and reaching it from `head` alone means walking every node, which is O(n) per enqueue. That would reintroduce a linear queue operation, the very defect the linked realization is chosen to avoid. So a linked queue keeps a second reference, `tail`, pointing at the last node. Enqueue becomes `tail.next = node; tail = node` — constant time. **Two references, two O(1) ends.** That is the entire reason for the extra field, and it is the first thing to say when asked why the structure carries it. ## The boundary that breaks Two references describing one list means two things that can disagree, and the place they disagree is the transition between an empty queue and a one-element queue. Trace it. The queue holds exactly one ticket, so `head` and `tail` both point at that single node. Dequeue does what dequeue does: it reads the value and advances `head` to `head.next`, which is nothing. Now `head` is empty — correct, the queue *is* empty — and `tail` still points at the node that was just handed out. The node is no longer in the queue by any reasonable definition, but a live reference still names it. Nothing has failed yet. The failure arrives on the next enqueue, and its severity depends on how enqueue tests for emptiness: - **Guarding on `tail`.** `if tail == null` is false, because `tail` is stale. So enqueue takes the append branch: it sets `stale.next = node` and moves `tail` to the new node. `head` is never assigned. The queue now reports empty on every dequeue while silently accumulating a chain of nodes hanging off a node nobody can reach. Every ticket enqueued after that point is lost, without an error, and the count of lost items grows with traffic. - **Guarding on `head`.** `if head == null` is true, so enqueue sets both `head` and `tail` to the new node and the structure self-heals. The stale `tail` was still wrong for the whole interval, and it kept the removed node reachable — a small retention leak, and a landmine for anyone who later writes code that consults `tail` on an empty queue. That asymmetry is what makes this such a good code-reading question: the same missing line is either a catastrophic silent data-loss bug or a benign-looking wart, depending on a detail three lines away in a different operation. ## The fix, and the principle under it The mechanical fix is one line in dequeue: after advancing `head`, if `head` is now empty, clear `tail` as well. The principle is worth more than the line. **Two references to one structure are two representations of the same fact, and every operation must leave them consistent.** Emptiness is one fact; it should have one test. Either derive emptiness from a single field that both operations agree on — `head`, or an explicit `count` — or maintain both ends in lockstep at every transition. A queue with a `count` field can test `count == 0` in both operations and will never *consult* the stale tail, but the stale reference still exists and still holds a removed node alive, so clearing it is cheap insurance rather than an optional tidy-up. ## Where the cost model actually lands With both references maintained, a linked queue offers O(1) worst-case enqueue and dequeue — genuinely worst-case, with no growth-and-copy step deferred anywhere. Its space cost is one node header and one reference per element on top of the payload, and its elements are scattered in memory rather than contiguous, so traversal chases pointers. Against a contiguous ring with equal asymptotics, that usually loses on constants and locality while winning on never needing to reallocate or choose a capacity. The asymptotic table does not decide between them; footprint and allocation behaviour do. ## What weak answers sound like "Dequeue only touches `head`, so `tail` cannot possibly be wrong" — the exact reasoning that produces the bug. "You only need one reference, just walk to the end" — correct and O(n), which forfeits the reason to use the structure. "It only shows up under load" — it shows up the first time the queue empties completely, which on a low-traffic intake path is constantly.

  • What is the minimal fix?
    One line in dequeue: after advancing `head`, if `head` is now empty, clear `tail` too. Better still, make emptiness a single fact — test the same field, or a `count`, in both operations — so the two references cannot disagree in the first place. Two representations of one fact are what created the bug.
  • Does keeping an explicit count field make the fix unnecessary?
    It removes the ambiguity but not the dangling reference. With a count, both operations test `count == 0` consistently and the stale tail is never consulted, so the data-loss path closes. The tail still names a node the queue no longer owns, though, keeping it alive and waiting for the next person who reads it. Clearing it is one line.
  • Why not drop the tail reference and walk to the end on enqueue?
    Because that makes enqueue O(n) — every append traverses the whole queue — and a linear enqueue defeats the reason to build a queue on linked nodes at all. Filling a queue of n items would cost O(n^2). The tail reference is what buys constant time at the far end, and its price is exactly the consistency obligation this bug illustrates.

saying these in an interview costs you the question

  • One reference is enough; just walk to the end
  • Dequeue touches only head, so tail cannot go stale
  • The bug only appears under heavy load
  • Appending to a singly linked list is O(1) by nature
  • Testing emptiness with a different field per operation is fine

context