skip to content

In a circular singly-linked list, why does walking until a NIL next reference never terminate?

level: middleimportance: should knowfreq 52%

answer

  1. What sentinel does a ring not have?
  2. The last node points where?
  3. Compare against what, not NIL?
  4. Test before or after advancing?
  5. Identity, never stored value

basics

~20 s

A circular list has no NIL next reference: the last node points back to the first, so the usual end test never fires. Stop by node identity instead — advance first, then halt on returning to the node you started from.

solid answer

~50 s

Non-circular lists terminate a traversal on a sentinel value: the final node's `next` is NIL, and every loop leans on that. A circular list deliberately removes it — the last node closes the ring by pointing at the first — so no node ever has a NIL successor and the loop spins forever. The replacement condition is node identity against the starting node, but the shape matters: a plain pre-test loop `while curr != start` exits immediately, before visiting anything, because `curr` starts *at* `start`. You need a do-while shape — visit, advance, then test — or equivalently loop while `curr.next != start` and handle the final node after. Guard the empty ring separately, since there is no starting node to compare against, and compare node *identity*, not stored values, since two nodes may hold equal values.

code

pseudocode · 14 lines
pseudocode
// ring of player seats; start is any node in the ring
curr = start
while curr != NIL          // BUG: no node in a ring is NIL
    visit(curr)
    curr = curr.next

// correct: advance first, then test identity
if start == NIL
    return                 // empty ring
curr = start
repeat
    visit(curr)
    curr = curr.next
until curr == start

go deeper

for a junior

Recall that a circular list's last node points back at the first, so no node has a NIL successor and the familiar end-of-list test simply never fires.

for a middle

Explain the correct stopping rule and its loop shape: visit, advance, then compare node identity against the starting node — a pre-test comparison exits before visiting anything.

for a senior

Show the edge cases you would actually get bitten by: guarding the empty ring, moving the handle before unlinking the node it names, and emptying the ring when its only node is removed.

for a principal

Own the tradeoff itself: a ring buys a total advance operation with no wrap-around bounds test, at the price of every traversal having to supply its own termination rule — a good trade when advancing dominates, a poor one when full scans do.

## Where the termination condition comes from Every traversal needs a stopping rule. In a singly- or doubly-linked list the rule is handed to you by the layout: the last node's `next` field holds NIL, a value that is not a node, so `while curr != NIL` distinguishes "more list" from "end of list" without any extra state. The loop is so idiomatic that it gets written reflexively. A circular list is exactly the variant that deletes that guarantee. Its last node's `next` points back to the first node, and if it is circular *and* doubly-linked, the first node's `prev` points back to the last. The ring has no beginning and no end — only whichever node your handle happens to name. Run the reflexive loop over it and `curr` is never NIL, so the traversal cycles the ring forever, doing real work each time round: in a long-running process that is a hang and a growing output, not a crash, which makes it slower to diagnose than a null-reference fault would be. ## The three candidate conditions **Wrong: `while curr != NIL`.** Never fires. This is the bug. **Wrong: `while curr != start` as a pre-test loop.** Terminates instantly and visits nothing, because the loop begins with `curr == start`. This is the classic over-correction — the comparison is right, the loop shape is not. **Right: visit first, then test.** Two equivalent formulations: ``` repeat visit(curr) curr = curr.next until curr == start ``` or, if the loop must be pre-test, walk while `curr.next != start` and handle the last node after the loop. Both visit every node exactly once. ## The details that separate a correct answer from a nearly correct one **Guard the empty ring.** If the handle is NIL there is no starting node, and a do-while shape dereferences it before testing anything. The emptiness check belongs *before* the loop, not inside the condition. **Compare identity, not value.** In a turn-order ring for a four-player board game, two seats can easily carry the same stored value — the same team colour, the same score, the same NIL payload for an eliminated player. Terminating when the current node's *value* matches the start's value stops early and silently skips players. The test must be "is this the same node", not "does this look like the start". **Do not lean on a maintained count.** "Loop `size` times" reads clean and is genuinely used, but it converts a structural invariant into a bookkeeping one: a single insert or delete path that forgets to update `size` turns every traversal into either a short read or an over-run into already-visited nodes. Identity comparison cannot drift, because it reads the same references the traversal is already following. **Removal changes the ring's shape.** Two cases are easy to miss. Removing the node the handle names requires moving the handle to another node first, or every later operation starts from a node that has left the ring. Removing the *only* node — the one whose `next` is itself — must set the handle to NIL, because unlinking it leaves a one-node ring pointing at a departed node, which then traverses forever as a ghost list of one. ## Why choose a ring at all The turn-order case shows the payoff. Play advances forever and wraps: after the fourth seat comes the first. The alternative is a linear list plus an index that is reset when it runs off the end, which means every advance carries a bounds test and every piece of code that advances play must remember to write that test. The ring pushes the wrap into the structure — `curr = curr.next` is total, and there is no end to fall off. Add or eliminate a player and the ring stays correct with no index to fix up. The price is exactly the one this question is about: you have traded the free termination sentinel for an explicit stopping rule that every traversal must supply for itself. That is a fair trade when advancing is the common operation and full passes are rare, and a poor one when the code is dominated by scans that each have to re-derive where to stop. ## Interview shape Name the missing NIL first, then give the identity condition, then volunteer the loop-shape trap. Finishing with the empty-ring guard and the single-node removal case is what makes the answer read as lived rather than recited.

  • Why not just loop as many times as the stored node count?
    It works while the count is accurate, but it replaces a structural invariant with a bookkeeping one. One insert or delete path that forgets to update the count makes every traversal either stop short or run past into already-visited nodes. Identity comparison reads the same references the walk already follows, so it cannot drift.
  • How do you remove the last remaining node from a circular list?
    Set the list's handle to NIL. That node's `next` points at itself, so unlinking it structurally is a no-op — if you leave the handle in place it names a node that has left the ring, and every later traversal walks a one-node ghost list forever. Emptiness has to be represented by the handle, not by the ring.
  • What has to happen if you delete the node the handle currently names?
    Move the handle to another node — usually the successor — before unlinking, or the list ends up anchored to a departed node. In a turn-order ring that is exactly the eliminate-the-current-player case, which is why the handle is normally advanced as part of the removal rather than after it.

A carousel has no last horse. You cannot ride until the horses run out — you have to remember which one you got on and stop when it comes round again.

saying these in an interview costs you the question

  • Writes while curr is not NIL over a circular list
  • Uses a pre-test loop against the start node and visits nothing
  • Terminates on value equality rather than node identity
  • Forgets the empty-ring guard before a do-while traversal
  • Leaves the handle on a node that was just unlinked
  • Assumes a maintained size counter is always trustworthy

context