DEV Community

Timevolt
Timevolt

Posted on

May the Traversal Be With You: Tree Traversals Explained - Recursive vs Iterative

The Quest Begins (The "Why")

I still remember the first time I stared at a binary tree diagram on a whiteboard during an interview and felt my brain short‑circuit. The interviewer asked, “Can you print the nodes in sorted order without using any extra libraries?” I fumbled with a recursive solution, got it working, but then they hit me with the follow‑up: “Now do it without recursion.” My heart sank. I’d spent hours visualizing call stacks, but the idea of replacing that with an explicit stack felt like trying to defeat a final boss with a wooden spoon.

Why does this matter? Because tree traversals are the bread‑and‑butter of so many problems: validating BSTs, finding the kth smallest element, serializing trees, you name it. If you can’t move through a tree confidently, you’ll keep tripping over edge cases and waste precious interview minutes. So I embarked on a quest to truly understand how traversal works—not just memorize code, but see why the recursive and iterative versions are two sides of the same coin.

The Revelation (The Insight)

Here’s the “aha!” moment: recursion is just an implicit stack. When you call a function, the language pushes the current state (local variables, return address) onto the call stack and jumps to the new frame. When the call returns, it pops that state and continues. An iterative traversal does exactly the same thing, but you manage the stack yourself with a simple list or array.

Take inorder traversal (left‑root‑right) as our example. The recursive version looks like this:

def inorder_recursive(node):
    if not node:
        return
    inorder_recursive(node.left)
    visit(node)          # e.g., print node.val
    inorder_recursive(node.right)
Enter fullscreen mode Exit fullscreen mode

What’s happening under the hood?

  1. We dive left as far as we can, pushing each node onto the call stack.
  2. When we hit a None, we start popping: the most recent node is the leftmost unvisited node → we “visit” it.
  3. Then we try its right subtree, repeating the process.

If we replace the call stack with our own explicit stack, we get the iterative version:

def inorder_iterative(root):
    stack = []
    curr = root
    while curr or stack:
        # Go as left as possible, saving nodes on the way
        while curr:
            stack.append(curr)
            curr = curr.left
        # curr is None → pop the last node we saved
        curr = stack.pop()
        visit(curr)               # process the node
        # Now explore the right subtree
        curr = curr.right
Enter fullscreen mode Exit fullscreen mode

The outer while loop keeps going until both the current pointer and the stack are empty—exactly the condition that the recursive version would have when the call stack is empty. The inner while pushes left children, mimicking the recursive descent. When we can’t go left anymore, we pop, visit, and then swing right. That’s it—no magic, just a faithful recreation of the call stack.

Why does this always produce left‑root‑right? Because we always process a node only after we’ve exhausted its entire left subtree (the inner loop finishes) and before we touch its right subtree (we set curr = curr.right after the visit). The stack guarantees we return to the correct parent after each left‑down journey.

Wielding the Power (Code & Examples)

Before: The Struggle

Early on, I tried to “cheat” the iterative version by only pushing the root and then looping:

# DON’T DO THIS – it misses nodes!
def bad_inorder(root):
    stack = [root]
    while stack:
        node = stack.pop()
        visit(node)
        if node.right:
            stack.append(node.right)
        if node.left:
            stack.append(node.left)
Enter fullscreen mode Exit fullscreen mode

This gives a preorder‑like order (root‑right‑left) and completely skips the left‑first requirement. I spent an hour wondering why my output was jumbled until I drew the stack on paper and saw that I was never revisiting a node after exploring its left side.

After: The Victory

The correct iterative version (shown above) is short, clear, and it mirrors the recursive logic perfectly. Let’s see it in action on two classic interview problems.

Problem 1: Validate a Binary Search Tree

A BST is valid if an inorder traversal yields a strictly increasing sequence.

def is_valid_bst(root):
    stack = []
    curr = root
    prev_val = float('-inf')
    while curr or stack:
        while curr:
            stack.append(curr)
            curr = curr.left
        curr = stack.pop()
        if curr.val <= prev_val:   # violation!
            return False
        prev_val = curr.val
        curr = curr.right
    return True
Enter fullscreen mode Exit fullscreen mode

We keep track of the last visited value (prev_val). If we ever see a node ≤ prev_val, the tree breaks BST property. The algorithm runs in O(n) time—each node is pushed and popped once—and uses O(h) auxiliary space, where h is the tree height (worst‑case O(n) for a skewed tree, O(log n) for a balanced one).

Problem 2: Kth Smallest Element in a BST

Because inorder gives sorted order, we can stop early after k visits.

def kth_smallest(root, k):
    stack = []
    curr = root
    while curr or stack:
        while curr:
            stack.append(curr)
            curr = curr.left
        curr = stack.pop()
        k -= 1
        if k == 0:
            return curr.val
        curr = curr.right
    return None  # k is larger than number of nodes
Enter fullscreen mode Exit fullscreen mode

Again, each node is touched at most once → O(n) time, O(h) space. The early exit often makes it faster in practice.

Common Traps to Avoid

Trap Why it’s wrong Fix
Forgetting to reset curr after popping You’ll stay stuck on the same node, infinite loop After curr = stack.pop(), always set curr = curr.right (or None if no right child)
Using pop(0) on a list (queue) instead of pop() (stack) Turns the algorithm into level‑order traversal Use list.append and list.pop() for LIFO behavior
Not handling the empty tree (root is None) Leads to AttributeError on curr.val Guard with if not root: return ... at the start

Why This New Power Matters

Mastering the link between recursion and an explicit stack does more than let you pass interview whiteboard puzzles. It gives you a mental model for any depth‑first search: graph DFS, expression tree evaluation, even parsing nested JSON. You’ll start to see stacks everywhere—function calls, undo/redo mechanics, backtracking algorithms—and you’ll know exactly how to swap between the two forms whenever you need to avoid recursion limits or want explicit control over memory.

It’s also a confidence booster. When you can look at a problem, sketch the recursive solution in your head, and then immediately translate it to an iterative version without second‑guessing, you feel like you’ve leveled up from a apprentice to a seasoned adventurer. The tree is no longer a mysterious forest; it’s a map you can navigate with a trusty stack in your pack.

Your Turn: Embark on Your Own Quest

Grab a binary tree (draw one on paper or code a simple class) and try this:

  1. Write the recursive post‑order traversal (left‑right‑root).
  2. Convert it to an iterative version using a stack.
  3. Test it on a tree where you know the answer (e.g., [1, null, 2, 3] → post‑order [3, 2, 1]).

If you get stuck, draw the stack step‑by‑step—just like we did for inorder. Share your code or your “aha!” moment in the comments; I’d love to see how you tackled the challenge.

Happy traversing, and may your stacks never overflow! 🚀

Top comments (0)