skip to content

Why must a directory-size rollup over a file tree use postorder rather than preorder?

level: middleimportance: must knowfreq 64%

answer

  1. which value is defined in terms of which
  2. a parent's sum needs results, not just visits
  3. compare with computing a node's depth
  4. aggregation goes up, propagation goes down
  5. children must return before the parent combines

basics

~20 s

A directory's total depends on its children's totals, so every child call must return before the parent can sum them. Postorder guarantees that order; preorder would have the parent aggregate values that do not exist yet.

solid answer

~50 s

The rollup is a bottom-up aggregate: `total(d)` is `d`'s own bytes plus the totals of all its entries, so the parent's answer is *defined* in terms of results it does not have until the recursion returns. Postorder — recurse into every child, then compute the node — is exactly that dependency order. Preorder computes the node before descending, which for a top-down quantity like a node's depth or its inherited path is fine, but for an aggregate leaves the parent nothing to add. The distinction is direction of data flow, not which traversal happens to visit more nodes: both visit all `n` nodes and both cost O(n). You can get the same rollup without recursion, but only by respecting the same dependency — process nodes in reverse level order, or keep a per-node counter of unresolved children and finish a node when it hits zero.

code

pseudocode · 8 lines
pseudocode
TOTAL-SIZE(node):
    sum = OWN-BYTES(node)
    for child in CHILDREN(node):
        sum = sum + TOTAL-SIZE(child)
    return sum

// moving the combine before the loop would sum
// child totals that have not been computed yet

go deeper

for a junior

Be ready to state the rule plainly: a parent's aggregate needs its children's results, so the children are handled first. Recognise that the combine step goes after the loop, not before it.

for a middle

Explain the dependency in terms of the defining equation, and contrast it with a top-down quantity such as depth that genuinely wants preorder. Be able to reproduce the same order iteratively with reverse level order or unresolved-child counters.

for a senior

Show the efficiency judgment too: one linear pass yields every subtree total, while per-directory recomputation is quadratic on deep trees, and a single changed file invalidates only its ancestor chain.

for a principal

Own when to cache aggregates on the nodes at all: cached totals buy O(h) updates and instant reads, at the cost of an invalidation path every writer must honour, which is a correctness contract across a whole codebase.

## The question behind the question Interviewers ask this because the tempting wrong answer is "any order works, since every traversal visits every node". That is true about *coverage* and false about *dependency*. Traversal order decides when a node's own computation happens relative to its children's, and an aggregate that reads child results has a hard ordering constraint that coverage alone does not satisfy. ## The dependency, stated precisely For a directory tree, define `total(d)` = the bytes stored directly at `d` plus `total(c)` summed over every entry `c` of `d`. The right-hand side mentions `total(c)`. Any evaluation strategy that computes `total(d)` before all of its `total(c)` values exist is computing with holes. Postorder is the traversal defined by "visit a node only after all of its children have been visited", so it is the traversal that satisfies the dependency by construction. In an n-ary shape, postorder means: loop over the whole children list, recursing into each, and only then combine — the generalization of "left, right, node" to "all children in order, then node". Notice what does *not* matter: the order among siblings. Addition is associative and commutative, so any order over the children list gives the same sum. What matters is only that the parent comes last. If the aggregate were order-sensitive — concatenating child names into a path string, say — sibling order would matter too, but the parent-after-children constraint is the same either way. ## The mirror image: quantities that want preorder Preorder is not "the wrong traversal"; it is the right traversal for the opposite data flow. Top-down quantities are defined in terms of the *parent's* value: - a node's depth = parent's depth + 1 - a node's full path = parent's path plus the node's own name - an inherited permission or an inherited style = the parent's, unless the node overrides it Each of those is computed on the way *in*, so preorder — node first, then descend — is the natural fit, and computing them in postorder would ask the child for a value the parent has not produced. A useful way to say this in an interview: **postorder for aggregation, preorder for propagation.** Many real tree passes need both, which is why you see one walk that carries a value down as a parameter and returns an aggregate up as a result. ## Cost, and what the ordering does not buy you Both orders visit `n` nodes and follow `n - 1` child links, so the walk is O(n) either way, with O(1) work per node when the combine step is a sum. Choosing postorder is not a performance decision — it is a correctness decision. The performance decision hiding nearby is a different one: if you need the total for *every* directory, one postorder pass gives you all `n` of them in linear time, whereas computing each directory's total independently by re-walking its subtree is O(n · h) in the worst case and quadratic on a deep chain. That is the actual efficiency trap in this problem, and the one worth volunteering. ## Doing it without recursion An iterative version must reproduce the dependency explicitly. Two standard shapes: - **Two passes.** Walk the tree once in any order, recording each node's parent and pushing nodes onto a list; then process that list in reverse, so every node is handled after all of its descendants. A level-order walk processed in reverse works for the same reason: a child never appears before its parent going down, so it never appears after it coming back up. - **Unresolved-child counters.** Give each node a counter set to its number of children. Start from the leaves (counter zero), add each finished node's total into its parent, decrement the parent's counter, and when a counter reaches zero that parent is itself ready. This is the same dependency order expressed as a readiness queue rather than a call order. Both are O(n). Neither escapes the dependency; they just make it visible. ## Incremental rollups A senior-flavoured extension the interviewer may reach for: one file changes size — what has to be recomputed? Only the totals of that file's ancestors, because no other node's aggregate mentions it. That is O(h) work rather than a fresh O(n) pass, and it is a direct consequence of the same dependency structure that forced postorder in the first place. It is also why cached subtree aggregates are worth keeping on the nodes: the invalidation set is exactly the ancestor chain. ## Failure modes to name Saying "traversal order doesn't matter, we visit everything"; computing the parent's sum before the loop over children rather than after; assuming each directory already stores its own total (it stores only its own bytes — the total is what you are deriving); and confusing aggregation with propagation, which produces code that tries to push totals downward.

  • Which tree quantity would you compute in preorder instead, and why?
    Anything defined in terms of the parent: a node's depth, its full path, an inherited permission or style. Those flow downward, so you compute the node's value from the parameter handed in by its parent and pass it along to the children. Postorder would be wrong for them in the same way preorder is wrong for a rollup — you would be reading a value that does not exist yet.
  • If you need the total for every directory, what is the trap in computing each one separately?
    Re-walking each directory's subtree recomputes the same descendants once per ancestor, which is O(n times h) and quadratic on a deep chain. One postorder pass produces all n totals in O(n), because each node's result is computed once and consumed by exactly one parent.
  • One file's size changes. What has to be recomputed?
    Only the totals of that file's ancestors, up to the root — O(h) updates, not a fresh full pass. No other node's aggregate references that file, so nothing else can be stale. That property is what makes caching subtree aggregates on the nodes worthwhile.

saying these in an interview costs you the question

  • Says any traversal order works since all nodes get visited
  • Adds the parent's total before recursing into children
  • Confuses bottom-up aggregation with top-down propagation
  • Assumes each directory already stores its total size
  • Recomputes each directory's total by re-walking its subtree

context