Search Tech Journey

Find topics, journeys and posts

back to blog
algorithmsintermediate 60m read

L22 · Trees I — DFS Traversals

Preorder, inorder and postorder as one recursive skeleton with the work line moved, plus the iterative forms and when they are worth writing.

🧩DSAPhase 1 · Core patterns· Session 022 of 130 60 min

🎯 See preorder, inorder and postorder as ONE recursive skeleton with the work line moved, know why inorder-on-a-BST yields sorted order, write the iterative forms, and master the return-vs-update split that unlocks every hard tree problem.

Series: LeetCode — From Basics to Interview-Ready · Session 22 / 65 · Phase 1 · Core patterns

Prerequisites

What this pattern IS

A binary tree is nodes, each holding a value and pointers to a left child and a right child (either may be empty). Depth-first search (DFS) explores it by going as deep as possible down one branch before backtracking — which is exactly what recursion does naturally, because each recursive call dives into a child and the call stack remembers where to resume.

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

The revelation of this session is that preorder, inorder, and postorder are the same recursive walk — visit node, recurse left, recurse right — and differ only in where you place the line that does the work. Do the work before recursing → preorder (root, left, right). Between the two recursions → inorder (left, root, right). After both → postorder (left, right, root). One skeleton, one moved line, three traversals. That's the whole thing, and seeing it collapses three memorised algorithms into one.

Each order has a signature use. Preorder copies or serialises a tree (you see the root before its children, so you can rebuild top-down). Inorder on a binary search tree yields values in sorted order — because a BST puts smaller values left and larger right, so "left, me, right" visits them ascending. Postorder computes things that depend on children first — a subtree's height, whether it's balanced, deleting a tree safely — because you have both children's answers before you handle the node.

Reading an org chart
🌍 Real world
Walk an organisation chart depth-first: dive into a manager's whole team before moving to the next manager. WHEN you note down each person's name — before you look at their reports (preorder), or after you've tallied their whole team (postorder) — changes what you compute, but the walk is identical.
💻 Code world
def dfs(node): visit(node); dfs(node.left); dfs(node.right) # move 'visit' to change the order

Deriving the three orders

From first principles
Start with the question
Why are preorder, inorder and postorder the same algorithm, and why does inorder on a BST come out sorted?
  1. 1
    DFS on a binary tree is always: handle this node, recurse into left subtree, recurse into right subtree.
    forced by · there are only these three actions per node, and DFS fixes left-before-right.
  2. 2
    The only freedom is WHEN the 'handle this node' action fires relative to the two recursions.
    forced by · the two recursions have fixed order (left then right), so the node-visit is the one movable piece.
  3. 3
    Before => preorder (N,L,R); between => inorder (L,N,R); after => postorder (L,R,N).
    forced by · those are the only three positions the node-visit can occupy.
  4. 4
    In a BST, every left descendant is smaller and every right descendant is larger than the node.
    forced by · that's the BST ordering invariant.
  5. 5
    So inorder (all-of-left, then node, then all-of-right) emits everything smaller, then the node, then everything larger — recursively — which is ascending order.
    forced by · the invariant holds at every node, so the emitted sequence is globally sorted.
⇒ Therefore
One skeleton, three visit positions. Inorder + BST = sorted, because 'left, me, right' composes the ordering invariant into a globally ascending sequence.

The templates — annotated Python

# ---- Template A: the three recursive DFS orders -------------
def preorder(node, out):
    if not node: return               # base case: empty subtree
    out.append(node.val)              # WORK here => preorder (N,L,R)
    preorder(node.left, out)
    preorder(node.right, out)
 
def inorder(node, out):
    if not node: return
    inorder(node.left, out)
    out.append(node.val)              # WORK here => inorder (L,N,R)
    inorder(node.right, out)
 
def postorder(node, out):
    if not node: return
    postorder(node.left, out)
    postorder(node.right, out)
    out.append(node.val)              # WORK here => postorder (L,R,N)
 
 
# ---- Template B: iterative PREORDER (explicit stack) --------
def preorder_iter(root):
    if not root: return []
    out, stack = [], [root]
    while stack:
        node = stack.pop()
        out.append(node.val)
        if node.right: stack.append(node.right)   # push right FIRST
        if node.left:  stack.append(node.left)    # so left pops first
    return out
 
 
# ---- Template C: iterative INORDER (go left, then unwind) ---
def inorder_iter(root):
    out, stack, cur = [], [], root
    while cur or stack:
        while cur:                    # dive left, stacking as you go
            stack.append(cur)
            cur = cur.left
        cur = stack.pop()             # leftmost unvisited
        out.append(cur.val)           # visit
        cur = cur.right               # then go right
    return out
 
 
# ---- Template D: the RETURN-vs-UPDATE split (the big one) ---
def max_depth(root):
    if not root: return 0             # base
    l = max_depth(root.left)          # trust: left subtree depth
    r = max_depth(root.right)         # trust: right subtree depth
    return 1 + max(l, r)              # postorder combine
# Many hard problems: RETURN one thing to the parent, UPDATE a
# shared answer with another. See diameter / max-path-sum below.

Two things to lock in. In iterative preorder (B), you push the right child first so the left child pops first — the stack reverses your intended order, so you feed it reversed. And Template D is the shape of essentially every non-trivial tree problem: you do a postorder recursion, and at each node you combine the children's returned values. When "what the parent needs from me" differs from "the global answer this node contributes to", you split them: return one value, update a shared variable with another. That split is the single highest-leverage idea in all of tree algorithms.

Mental modelOne walk, movable pen
You trace the whole tree with your finger (always left before right, diving deep first). Your other hand holds a pen. WHERE along the trace you press the pen — on the way in, in the crook between children, or on the way out — is the only choice, and it names the traversal.
  • Same walk every time; move the WORK line to pick pre/in/post.
  • Inorder on a BST = sorted output — reach for it whenever a BST wants order.
  • Postorder = 'children first', for heights, balance, sums, safe deletion.
  • Hard problems: RETURN what the parent needs, UPDATE a shared global with the rest.
🔔 Fires when you see
Any binary tree question: traversal, aggregate over subtrees, path problems, BST order, or 'compute something that depends on children'.

Memory / mnemonic

Anchor the three orders with "the ROOT's position names it": pre = Root first, in = Root in the middle, post = Root last — read left-to-right as N-L-R, L-N-R, L-R-N. Say "pre-in-post = root first, middle, last" and the visit line's placement falls out instantly.

Two more pegs. "Inorder + BST = sorted" — memorise it as a fact and it solves a whole class of BST problems (kth smallest, validate BST, range sum) on sight. And for iterative preorder, remember "push RIGHT first so LEFT comes out first" — the stack flips you, so you flip it. For the capstone, chant "RETURN up, UPDATE across": the value you hand your parent goes up, the answer you contribute (a path across through you) updates the shared max.

Common misconception
✗ What most people think
Preorder, inorder and postorder are three different algorithms you memorise separately.
✓ What is actually true
They are ONE recursive skeleton with the single work line moved to one of three positions. Learn the skeleton and the position rule, not three separate procedures.
Why the myth is so sticky
Textbooks present them as a list of three, encouraging rote memorisation of each. That hides the fact that only one line moves, which is why students mix them up under pressure.
Prove it to yourself
Write preorder, then produce inorder by MOVING the append line down one position, then postorder by moving it down once more — without rewriting anything else. If you can, you've internalised the skeleton; if you had to recall three templates, you memorised instead of understood.

What interviewers actually ask

Tree DFS is one of the most-asked interview areas because a single traversal skeleton spawns dozens of problems, and the return-vs-update split cleanly separates candidates who understand recursion from those who pattern-match.

  • Maximum Depth of Binary Tree (104) · Easy · Amazon, LinkedIn · probes: the postorder combine (1 + max of children) · follow-up: minimum depth, and the iterative BFS version.
  • Invert Binary Tree (226) · Easy · Amazon, Google · probes: swap children at every node — trivial recursion, famous for a Google rejection tweet · follow-up: do it iteratively.
  • Same Tree (100) / Subtree of Another Tree (572) · Easy · Amazon, Meta · probes: structural recursion comparing two trees in lockstep · follow-up: subtree check reuses same-tree.
  • Validate Binary Search Tree (98) · Medium · Amazon, Meta, Microsoft · probes: the trap — check bounds, not just parent vs child · follow-up: do it via inorder and check ascending.
  • Kth Smallest Element in a BST (230) · Medium · Amazon, Meta · probes: inorder yields sorted, so stop at the k-th · follow-up: what if the BST is modified often (augment nodes with subtree counts).
  • Binary Tree Maximum Path Sum (124) · Hard · Meta, Amazon · probes: the return-vs-update split in full — return the best single downward branch, update a global with left+node+right · follow-up: reconstruct the actual path.

Worked solutions

1. Validate Binary Search Tree (98)

Plain words: is this tree a valid BST — every node greater than all of its left subtree and less than all of its right subtree? The classic bug is checking only node.left.val < node.val < node.right.val, which misses violations deeper down. The fix: pass down the allowed (low, high) range each node must fall within.

def isValidBST(root) -> bool:
    def valid(node, low, high):
        if not node:                       # base: empty is valid
            return True
        if not (low < node.val < high):    # must fit the inherited bounds
            return False
        # left subtree tightens the UPPER bound; right the LOWER
        return valid(node.left,  low, node.val) and \
               valid(node.right, node.val, high)
    return valid(root, float('-inf'), float('inf'))

Complexity: O(n) time, O(h) space (recursion depth). The insight is that the constraint on a node comes from all its ancestors, not just its parent — encoded as a shrinking (low, high) window. Follow-up: an inorder traversal that checks each value exceeds the previous is an equivalent, often cleaner, solution (inorder + BST = sorted).

2. Kth Smallest Element in a BST (230)

Plain words: find the k-th smallest value. Since inorder on a BST emits values ascending, walk inorder and stop at the k-th. The iterative form lets you stop early without traversing the whole tree.

def kthSmallest(root, k: int) -> int:
    stack, cur = [], root
    while cur or stack:
        while cur:                         # dive to the smallest
            stack.append(cur)
            cur = cur.left
        cur = stack.pop()                  # next in sorted order
        k -= 1
        if k == 0:                         # the k-th smallest
            return cur.val
        cur = cur.right
    return -1                              # k out of range (shouldn't happen)

Complexity: O(h + k) time (dive to the smallest, then k steps), O(h) space. This is iterative inorder (Template C) with an early stop — the reason to know the iterative form is exactly this "stop partway" capability that recursion makes awkward. Follow-up: if the tree is modified often, augment each node with its subtree size to answer k-th in O(h).

3. Binary Tree Maximum Path Sum (124)

Plain words: find the maximum sum of any path, where a path is any node-to-node route through connected nodes (it needn't touch the root). This is the return-vs-update split in its purest, hardest form.

def maxPathSum(root) -> int:
    best = float('-inf')
    def gain(node):                        # returns best DOWNWARD branch
        nonlocal best
        if not node:
            return 0
        left  = max(gain(node.left),  0)   # ignore negative branches
        right = max(gain(node.right), 0)
        # UPDATE: path THROUGH this node bends left+node+right
        best = max(best, node.val + left + right)
        # RETURN: parent can only extend ONE branch, not both
        return node.val + max(left, right)
    gain(root)
    return best

Complexity: O(n) time, O(h) space. The crux: a node returns node.val + max(left, right) because its parent can only continue the path through one child, but it updates best with node.val + left + right because the best path might peak at this node using both children. Clamping negatives to 0 means "drop a branch that only hurts". Return-up, update-across — this exact split reappears in diameter (543) and many hard tree problems.

Try itSolve Diameter of Binary Tree (543) with the same split

Compute the diameter — the number of edges on the longest path between any two nodes. Reuse Template D's shape: each call returns its height (1 + max of children), and updates a shared max with left_height + right_height (the path bending through this node). It's Max Path Sum with heights instead of sums — proving the split is one reusable idea, not a bag of tricks.

💡 Hint · Return the height; update a global with left-height + right-height (edges on the path through the node).

Recursive versus iterative DFS

The tradeoff
Recursive DFS versus iterative (explicit stack) DFS versus BFS for tree problems.
Recursive DFS
+ you gain Shortest, clearest code; the visit-position rule makes pre/in/post trivial; natural for combine/return-vs-update.
− you pay O(h) call-stack memory; overflows on a pathological (list-like) tree deeper than ~1000.
pick when Almost always — most trees are shallow (balanced => O(log n) depth); default here.
Iterative DFS (explicit stack)
+ you gain No recursion-limit risk; lets you stop early mid-traversal (k-th smallest).
− you pay More code, especially inorder/postorder; you manage the stack by hand.
pick when Very deep/skewed trees, or when you must pause/resume/stop-early a traversal.
BFS (queue, level order)
+ you gain Processes by level; needed for level-order and shortest-path-in-unweighted problems.
− you pay O(width) memory (can be O(n) at the widest level); not depth-first.
pick when Level-order traversal, minimum depth, or anything defined per level (L23).
What a senior engineer actually does
Default to recursive DFS — it's the cleanest and most trees are shallow. Switch to iterative when depth threatens the recursion limit or you need early-stop. Reach for BFS only when the problem is inherently level-based; that's the next session.

Where this shows up in production

Tree DFS is everywhere data is nested: the DOM in a browser (rendering, event bubbling), the abstract syntax tree in every compiler and linter (postorder to evaluate expressions, preorder to emit code), file-system walks (os.walk is DFS), JSON/XML parsing and serialisation (preorder to serialise, so you can rebuild top-down), and dependency resolution. Inorder-on-a-BST underlies ordered maps and database index scans that must return keys in sorted order. The honest framing: whenever a structure is recursive, DFS is the natural traversal, and which order you pick is dictated by whether you need parents before children (pre), sorted BST output (in), or children before parents (post).

You can now…
  • Write pre/in/post order by moving one work line in a single skeleton.
  • Explain why inorder on a BST yields sorted output.
  • Pick the right order: pre to serialise, in for BST order, post for child-dependent work.
  • Write iterative preorder and inorder with an explicit stack.
  • Validate a BST with inherited (low, high) bounds, not just parent comparison.
  • Find the k-th smallest in a BST by early-stopping an inorder walk.
  • Apply the return-vs-update split to diameter and maximum path sum.
Recall · L22 · click to reveal
★ = stretch question

Practice queue

Key points