How do you rewrite a recursive tree walk as a Python `while` loop over an explicit stack?
answer
- the pending work has to live somewhere
- seed a container, loop until empty
- pop, do the work, push the children
- the entry carries what the frame carried
- which end you take from picks the container
basics
~10 sSeed a list with the root, then loop while it is non-empty: pop an item, do that node's work, and push its children. The list holds the pending work the frames used to hold.
solid answer
~40 sMove the pending work off the call stack and into a container you own. Seed a list with the root, write `while stack:` — an empty list is falsy — then `node = stack.pop()`, do the node's work, and `stack.extend(children)`. `list.append()` and `list.pop()` both act on the end and are amortized O(1), which is exactly what a stack needs. Crucially, the entries must carry everything a frame carried: if the recursive call passed a depth, a path or a parent, push a tuple of the node plus those values and unpack it on pop. Use `collections.deque` with `popleft()` when you need to take items from the front instead, because `list.pop(0)` shifts every remaining element and turns a wide walk quadratic. Keep a set of visited keys if the data can contain cycles.
code
python · 21 linesfrom collections import deque
tree = {"root": ["a", "b"], "a": ["a1"], "b": [], "a1": []}
def depth_first(start):
seen, stack = [], [start]
while stack:
node = stack.pop() # amortized O(1) from the end
seen.append(node)
stack.extend(tree[node])
return seen
def breadth_first(start):
seen, queue = [], deque([start])
while queue:
node = queue.popleft() # O(1); list.pop(0) would be O(n)
seen.append(node)
queue.extend(tree[node])
return seen
print(depth_first("root"), breadth_first("root"))go deeper
Be able to write the pattern from memory: seed a list with the root, loop while it is non-empty, pop an item, do the work, push the children. Know that an empty list is falsy so while stack: is the idiomatic test.
Explain what the container replaces — the frames — and choose it deliberately: a list when you take from the end, collections.deque when you take from the front, never list.pop(0) for a queue.
Show that you know what has to be pushed: every parameter the recursive call carried, packed into the entry, plus visited-tracking for structures you did not build and a plan for how the failure identifies itself.
Judge when the rewrite is worth its readability cost across a codebase, and where a recursion over provably shallow data should simply be left alone with the bound documented.
### The transformation A recursive walk over a nested structure keeps its pending work in frames: each call holds one node plus whatever parameters it was given, and the interpreter remembers where to resume. Because CPython never reuses a frame, the only way to walk an arbitrarily deep structure is to move that pending work off the call stack and into a container you control. The shape is always the same: ```python stack = [root] while stack: node = stack.pop() visit(node) stack.extend(children_of(node)) ``` Three things are worth naming in that snippet. `while stack:` tests the list for emptiness through its truthiness — an empty list is falsy — which is the idiomatic spelling and does the same job as the recursion's "no more branches to descend". `stack.pop()` removes and returns the last element, which is amortized O(1) because nothing after it has to move. `stack.extend(...)` pushes all the children in one call, which is both faster and clearer than a loop of `append`. ### What has to go on the stack The frames were not holding only the node. Every parameter the recursive call passed — a depth counter, an accumulated path, a parent reference, a flag saying whether you are inside an already-matched subtree — lived in a frame too, and after the rewrite it has to live in the stack entry. Push a tuple and unpack it on pop: ```python stack = [(root, 0, ())] while stack: node, depth, path = stack.pop() ... stack.extend((child, depth + 1, path + (node,)) for child in children_of(node)) ``` The rule of thumb is exact: the explicit stack must hold everything a frame held. Anything you forget to push is state the loop cannot recover, and it usually shows up later as a subtly wrong result rather than an error. ### list or collections.deque Use a plain `list` for a stack. `list.append()` and `list.pop()` both operate at the end of the list and are amortized O(1); the list over-allocates so appends rarely copy, and popping from the end moves nothing. Reach for `collections.deque` when you take items from the *front*. `list.pop(0)` has to shift every remaining element left by one, so a first-in-first-out walk built on a list degrades to quadratic time on a wide structure — a real and easily-missed performance bug. `collections.deque` is a doubly linked list of blocks with O(1) `append`, `pop`, `appendleft` and `popleft`, so a queue built on it stays linear. A deque also accepts a `maxlen`, which silently discards from the other end when full — handy for a bounded buffer, wrong for a work queue, so do not set it here by accident. For a stack specifically, a deque is not faster than a list, and it is one more import; pick the container by which end you take from, not by reputation. ### Termination and cycles The recursion stopped at its base case, once per branch. The loop stops on one condition — the container is empty — which is the same statement made once instead of once per path. Neither version protects you from a cycle: if the data can point back at an ancestor, the recursion recurses forever and the loop loops forever, and the loop is arguably worse because it will happily consume memory until the process dies rather than tripping the interpreter's nesting guard. Keep a `set` of visited keys and check it before pushing, whenever the structure is data you did not construct yourself. ### What you gain and what you pay You gain unbounded depth: the stack list lives on the heap, so it is limited by memory rather than by the interpreter's nesting limit, and it can hold millions of entries. You also gain speed, because a loop iteration is far cheaper than a function call, and you gain the ability to pause, checkpoint or resume the walk — the work queue is an ordinary object you can inspect, serialize or hand to something else. You pay in readability. The recursive version reads like the definition of the structure; the loop version reads like a machine. You also pay in diagnostics: the frames used to record where you were, and after the rewrite every iteration runs on the same source lines, so you have to put the identifying context into the stack entries and into whatever you log or raise. ### The interview answer Seed a container with the root, loop while it is non-empty, pop, do the node's work, push the children along with every parameter the recursion carried. Use a list when you take from the end, a `collections.deque` when you take from the front, and never `list.pop(0)` for a queue.
- When would you reach for `collections.deque` instead of a list for that container?When you take items from the front. `list.append()` and `list.pop()` at the end are amortized O(1), but `list.pop(0)` shifts every remaining element, so a first-in-first-out walk over a wide structure degrades to quadratic time. `collections.deque` gives O(1) at both ends, so use it for a queue and keep a plain list for a stack — for stack use it is no faster and one more import.
- The recursion passed a depth counter and an accumulated path as parameters — where do those live after the rewrite?On the stack entries. Push a tuple of the node plus every value the recursive call would have passed, and unpack it when you pop. The explicit stack has to hold exactly what the frames held; anything you forget to push is state the loop cannot recover, and it usually surfaces as a quietly wrong result rather than an error.
- How does the loop version terminate compared with the recursion?The recursion stops at its base case, once per branch. The loop stops on one condition, the container being empty, which is the same statement made once instead of once per path. Neither protects against a cycle in the data: the recursion recurses forever, the loop loops forever and eats memory, so track visited keys in a set when the structure is not yours.
saying these in an interview costs you the question
- Uses list.pop(0) as a queue and calls it O(1)
- Pushes only the node, dropping the recursion's other parameters
- Assumes a deque is always faster than a list for a stack
- Thinks the rewrite is unnecessary because Python optimizes tail calls
- Skips visited-tracking on data that can contain cycles