skip to content

In an explicit-stack rewrite of a recursive tree walk, why do children come out reversed?

level: middleimportance: must knowfreq 58%

answer

  1. what does pop hand you back?
  2. the recursion called children in list order
  3. all siblings go on before any comes off
  4. last one pushed is first one explored
  5. push the children in reverse index order

basics

~20 s

A stack hands back the most recently pushed item, so children pushed left to right are popped right to left and siblings are visited in the opposite order from the recursion. Push the children in reverse index order to restore it.

solid answer

~50 s

The recursion issues its child calls in list order, and each one finishes before the next begins. The stack rewrite pushes all the children first and then pops, and pop is last-in-first-out — so the child pushed last is explored first, and every sibling group is visited in reverse. The traversal is still depth-first; only sibling order flips. That is invisible for an order-independent aggregate like a total stock count, which is exactly why it survives review, and it bites the moment order matters: the first matching category wins, the report is emitted in a fixed sequence, ties are broken by position, or a bounded scan stops early. The fix is one line — push the children from the last index down to the first — and the test that catches it must assert visit order, not just the final total.

code

pseudocode · 10 lines
pseudocode
stack = empty
push(stack, root)
total = 0
while stack is not empty:
    node = pop(stack)
    total = total + node.stock
    for i in 0..length(node.children)-1:
        push(stack, node.children[i])
    ...
return total

go deeper

for a junior

Remember the one rule that causes this: a stack returns the most recently pushed item, so a group pushed in order comes back reversed. Be able to say which child is explored first.

for a middle

Explain the two schedules side by side — recursion finishes each child before starting the next; the rewrite pushes all children then pops — and give the one-line fix along with what still differs in peak memory.

for a senior

Show how you would have caught it: a fixture with distinguishable siblings asserting visit order, and the class of consumers that silently depend on traversal order in a real system.

for a principal

Weigh whether hand-rolled stack walks belong in the codebase at all, given that their defects pass aggregate tests. Decide where they are justified and what review or test standard makes them safe to own.

## The two schedules are not the same A recursive stock counter over a product catalogue loops over a category's children and calls itself on each one. The schedule is: descend fully into child 0 and return, then descend fully into child 1, and so on. At any instant the live frames are exactly the path from the root to the node being processed. The explicit-stack rewrite has a different schedule. It pops a node, does that node's work, and pushes **all** of its children at once. Because a stack returns the most recently pushed item, the last child pushed is the next one popped — the group is consumed in reverse. ``` stack = empty push(stack, root) total = 0 while stack is not empty: node = pop(stack) total = total + node.stock for i in 0..length(node.children)-1: push(stack, node.children[i]) return total ``` This code is not broken. It is a correct depth-first walk that touches every node exactly once — it just walks siblings right to left, which is not what the recursion it replaced did. ## Why the bug hides If all you compute is a total, order is irrelevant: addition gives the same answer whatever sequence you visit in, so the rewrite passes every test that checks the number. The reversal only becomes visible when the traversal's *order* is part of the contract: - **First match wins** — "find the first category with zero stock" now returns a different category. - **Emitted sequence** — a report, an export, or a stream of records changes order, and something downstream that assumed catalogue order breaks. - **Tie-breaking** — two categories with equal stock, and the reported winner flips. - **Bounded work** — "scan until you have collected 50 items" collects a different 50. - **Snapshot comparison** — golden-file tests start failing for reasons nobody can explain. This is the classic bug in the whole "convert this recursion to an explicit stack" exercise, and interviewers ask about it precisely because the naive rewrite looks obviously equivalent. ## The fix, and its cost Push the children from the highest index down to the lowest: ``` for i in length(node.children)-1 downto 0: push(stack, node.children[i]) ``` Now the leftmost child is on top and pops first, matching the recursion. It is one line, but it is a line that reads backwards from the intent it implements, which is why it deserves a comment and a test. Alternatively, hand the frontier a different discipline: consuming the frontier oldest-first instead of newest-first preserves sibling order but abandons depth-first behaviour entirely and explores level by level, which is a different traversal with different peak-memory characteristics — not a drop-in replacement for a recursion you were trying to preserve. ## The other thing that changed: peak memory The recursion holds one frame per level of the current path — depth-many frames. The stack rewrite holds every unexplored sibling along that path: up to roughly the branching factor times the depth of entries. For a wide catalogue that is a materially larger count. It is usually still the better deal, for two reasons. Each entry is a node reference, not a full frame with parameters, locals and a return address, so the per-item cost is far smaller. And the structure lives in memory that grows on demand rather than in the fixed-size region reserved for a thread's call stack, so "too deep" stops being a hard crash and becomes ordinary memory pressure you can observe. But if you claim the rewrite "uses less memory", be ready to say which memory: fewer bytes, in a place with a much more forgiving limit, but more live entries. ## Reviewing one of these Three questions catch nearly every defect in a hand-rolled stack walk. Does the pop order reproduce the recursion's visit order, or was it silently reversed? Is any node pushed more than once, or pushed before its predecessor's work is recorded? And does a test exist that distinguishes the orders — a fixture whose siblings are individually identifiable, asserting the sequence of visits rather than an order-independent sum? A test that only checks the total is a test that would pass on the reversed version.

  • Does the reversal change the total stock this walk computes?
    No — every node is still visited exactly once and addition does not care about order, which is precisely why the defect survives testing. It changes the answer only when order is part of the contract: first-match searches, emitted report sequence, positional tie-breaking, or a scan that stops after collecting a fixed number of items.
  • Which holds more live items, the recursion's frames or the explicit stack's entries?
    The explicit stack usually holds more items — every unexplored sibling along the current path, roughly branching factor times depth, against the recursion's depth-many frames. Each entry is far smaller than a frame, though, and it sits in memory that grows on demand rather than in the fixed region reserved for a thread's call stack.
  • How would you catch this reversal in review or in a test?
    Build a fixture whose siblings are individually identifiable and assert the sequence of visited nodes, not an order-independent aggregate. In review, read the push loop against the recursion it replaces and ask whether pop order reproduces the original visit order — a walk that only sums will look correct either way.

Loading plates onto a stack in menu order means the last course served is the starter.

saying these in an interview costs you the question

  • Says the stack version visits nodes in the same order as the recursion
  • Claims the reversal makes the computed total wrong
  • Fixes the order by sorting the output instead of the pushes
  • Assumes the rewrite is equivalent because a total test passes
  • Says the stack version stops being depth-first

context