← back

Tree

Recursive DFS Traversal

Recursively visit left subtree, root, and right subtree in pre/in/post order. The call stack naturally handles backtracking. Most tree problems reduce to a well-chosen recursive traversal.

Most binary tree problems reduce to a single question: can you compute something for the current node using results from the left and right subtrees? If yes, you write a recursive function that trusts its children to return the right answer, combines those answers with information at the current node, and passes the result up to its parent. That is recursive depth-first search on a tree, and it is the single most important technique for tree problems in interviews.

The reason recursion maps so naturally onto trees is that a tree is a recursive data structure. Every node is the root of its own subtree. When you call dfs(node.left), you are solving the exact same problem on a smaller tree. The recursion bottoms out at null nodes, where you return some base value, and then results bubble back up through the call stack, assembling the final answer.

The three traversal orders

When you visit every node in a tree recursively, the order in which you process the current node relative to its children gives rise to three classic traversal orders.

Preorder (root, left, right) processes the current node before its children. This is useful when you need to pass information downward, for example, building a path from root to leaf, or serializing a tree.

Inorder (left, root, right) processes the left subtree first, then the current node, then the right subtree. On a binary search tree, inorder traversal visits nodes in sorted order. This makes it the go-to approach for problems like finding the kth smallest element or implementing an iterator.

Postorder (left, right, root) processes both children before the current node. This is the order you need when the answer at a node depends on answers from its children: computing heights, counting nodes, or checking balance. Most "compute a property of the tree" problems are naturally postorder.

In practice, many problems blend these orders. You might pass bounds downward (preorder flavor) while also returning a boolean upward (postorder flavor). The labels are less important than the underlying question: does this node need information from above, from below, or both?

Max depth: the simplest recursive DFS

LeetCode 104 asks for the maximum depth of a binary tree, the number of nodes along the longest path from the root down to the farthest leaf. This is the "hello world" of recursive tree problems.

The recursive insight is immediate: the max depth of a tree is 1 (for the current node) plus the maximum of the depths of its left and right subtrees. A null node has depth 0.

Let's trace through a small example:

1
2
3
4
5
        3
       / \
      9   20
         /  \
        15   7

We call max_depth(3). This calls max_depth(9) and max_depth(20). The call max_depth(9) has two null children, so both return 0, and max_depth(9) returns 1 + max(0, 0) = 1. The call max_depth(20) recurses to max_depth(15) and max_depth(7), both of which return 1 (same reasoning as node 9). So max_depth(20) returns 1 + max(1, 1) = 2. Back at the root, max_depth(3) returns 1 + max(1, 2) = 3.

The code is almost a direct translation of the English description:

1
2
3
4
def max_depth(root):
    if not root:
        return 0
    return 1 + max(max_depth(root.left), max_depth(root.right))

Three lines. That is the power of recursive DFS: when the problem decomposes cleanly, the code is trivially short.

Validate BST: passing bounds downward

Consider LeetCode 98: a binary search tree has the property that for every node, all values in its left subtree are strictly less than the node's value, and all values in its right subtree are strictly greater. A common mistake is to only check that each node's value is greater than its left child and less than its right child. That check is local and misses cases where a deeper node violates the BST property with respect to an ancestor.

The correct approach is to pass a valid range downward. The root can hold any value, so it starts with the range (-infinity, +infinity). When you go left, the current node's value becomes the new upper bound. When you go right, the current node's value becomes the new lower bound. At every node, you check whether the node's value falls within its inherited range.

Why does this work? Because each node inherits a valid range from all of its ancestors, not just its parent. When you descend left from a node with value 10, you set hi = 10. If you then descend right from that child (say it has value 5), you set lo = 5. Now the grandchild must satisfy 5 < val < 10, which correctly encodes both the parent and grandparent constraints. The bounds accumulate the full ancestry.

1
2
3
4
5
6
7
def is_valid_bst(root, lo=float('-inf'), hi=float('inf')):
    if not root:
        return True
    if root.val <= lo or root.val >= hi:
        return False
    return (is_valid_bst(root.left, lo, root.val) and
            is_valid_bst(root.right, root.val, hi))

This is a preorder-style traversal: you check the current node's validity before recursing into children. The bounds flow downward through the recursion, and a boolean result flows upward. If any node fails the check, the and short-circuits and the whole tree is invalid.

Same tree and symmetric tree: comparing two subtrees simultaneously

Some tree problems ask you to compare two subtrees at the same time. LeetCode 100, "Same tree," checks whether two trees are structurally identical with the same values. Its companion, LeetCode 101, "Symmetric tree," checks whether a single tree is a mirror image of itself.

For same tree, you recurse on both trees in lockstep. Two nodes are the same if they are both null, or if they have the same value and their left children are the same and their right children are the same.

1
2
3
4
5
6
7
8
def is_same_tree(p, q):
    if not p and not q:
        return True
    if not p or not q:
        return False
    return (p.val == q.val and
            is_same_tree(p.left, q.left) and
            is_same_tree(p.right, q.right))

Symmetric tree is almost the same problem, but mirrored. A tree is symmetric if its left subtree is a mirror of its right subtree. Two subtrees are mirrors if their roots have the same value, the left child of one mirrors the right child of the other, and vice versa.

1
2
3
4
5
6
7
8
9
10
def is_symmetric(root):
    def is_mirror(a, b):
        if not a and not b:
            return True
        if not a or not b:
            return False
        return (a.val == b.val and
                is_mirror(a.left, b.right) and
                is_mirror(a.right, b.left))
    return is_mirror(root.left, root.right) if root else True

The only difference from same tree is swapping which children you compare. These two problems illustrate how a tiny change in the recursive structure encodes a fundamentally different question.

The general pattern

Whenever you face a binary tree problem, ask yourself two questions. First: what information do I need from my children to compute the answer at this node? That determines the return type of your recursive function. For max depth, children return an integer (their depth). For validate BST, children return a boolean (whether the subtree is valid). For more complex problems, children might return a tuple, for example, (is_balanced, height) when checking whether a tree is balanced.

Second: what information do I need to pass down from above? For max depth, nothing. For validate BST, you pass bounds. For path sum problems, you pass the running sum.

Once you answer these two questions, the code writes itself: define the base case, recurse on children, combine results, and return.

Inorder traversal and BST sorted order

A binary search tree has a special relationship with inorder traversal: visiting nodes in left-root-right order yields the values in sorted (ascending) order. This property is the basis for several interview problems.

For LeetCode 230, finding the kth smallest element in a BST, perform an inorder traversal and return the kth value you encounter. You can do this with a simple counter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def kth_smallest(root, k):
    count = 0
    result = None

    def inorder(node):
        nonlocal count, result
        if not node or result is not None:
            return
        inorder(node.left)
        count += 1
        if count == k:
            result = node.val
            return
        inorder(node.right)

    inorder(root)
    return result

The same idea powers a BST iterator: maintain a stack of nodes representing the "pending" inorder traversal. Each call to next() advances the traversal by one step.

Iterative DFS with an explicit stack

Recursive DFS uses the call stack implicitly. For very deep trees, particularly skewed ones with O(n)O(n) height, this can cause a stack overflow. The alternative is to manage your own stack explicitly.

The iterative version of preorder traversal is straightforward. Push the root onto a stack. While the stack is not empty, pop a node, process it, then push its right child followed by its left child (right first so that left is processed first).

1
2
3
4
5
6
7
8
9
10
11
12
13
def preorder_iterative(root):
    if not root:
        return []
    result = []
    stack = [root]
    while stack:
        node = stack.pop()
        result.append(node.val)
        if node.right:
            stack.append(node.right)
        if node.left:
            stack.append(node.left)
    return result

Iterative inorder is slightly trickier. You need to push all the left descendants of the current node before processing anything, then process the node, then move to its right child:

1
2
3
4
5
6
7
8
9
10
11
12
def inorder_iterative(root):
    result = []
    stack = []
    current = root
    while current or stack:
        while current:
            stack.append(current)
            current = current.left
        current = stack.pop()
        result.append(current.val)
        current = current.right
    return result

In interviews, the recursive version is almost always preferred for clarity. Use iterative DFS only when explicitly asked, when the tree might be extremely deep, or when the problem naturally fits a stack-based approach (like the BST iterator).

Complexity analysis

Time complexity: O(n)O(n). Every node is visited exactly once. Whether you use preorder, inorder, or postorder, the total work is proportional to the number of nodes.

Space complexity: O(h)O(h), where h is the height of the tree. This is the maximum depth of the recursion stack (or the explicit stack in the iterative version). For a balanced tree, h = O(log⁡n)O(\log n). For a completely skewed tree (essentially a linked list), h = O(n)O(n). This is the worst case you should mention in interviews.

The space is used regardless of whether you recurse or iterate. The explicit stack in the iterative version uses the same O(h)O(h) memory. The difference is that the explicit stack lives on the heap rather than the call stack, which can handle larger sizes and avoids stack overflow errors.

Recognizing this pattern

You're probably looking at recursive DFS on a tree when:

  • The problem asks you to compute a single property (height, count, boolean, sum) over an entire tree where the value at each node depends on its subtrees.
  • Subtree-local information bubbles up cleanly, so you can describe the answer at a node purely as a combination of the left and right return values plus the node itself.
  • The recursion shape is the tree itself, not a DP over (node, state) pairs with overlapping subproblems.
  • Information either flows down as arguments (bounds, paths) or up as return values, but you do not need explicit memoization keyed on multiple dimensions.

Common templates:

Practice Problems (92)

⊣ 94 Binary Tree Inorder Traversal
⊣ 98 Validate Binary Search Tree
⊣ 100 Same Tree
⊣ 101 Symmetric Tree
⊣ 104 Maximum Depth of Binary Tree
⊣ 110 Balanced Binary Tree
⊣ 112 Path Sum
⊣ 113 Path Sum II
⊣ 114 Flatten Binary Tree to Linked List
⊣ 144 Binary Tree Preorder Traversal
⊣ 145 Binary Tree Postorder Traversal
⊣ 173 Binary Search Tree Iterator
⊣ 199 Binary Tree Right Side View
⊣ 226 Invert Binary Tree
⊣ 230 Kth Smallest Element in a BST
⊣ 257 Binary Tree Paths
⊣ 331 Verify Preorder Serialization of a Binary Tree
⊣ 386 Lexicographical Numbers
⊣ 404 Sum of Left Leaves
⊣ 450 Delete Node in a BST
⊣ 501 Find Mode in Binary Search Tree
⊣ 508 Most Frequent Subtree Sum
⊣ 530 Minimum Absolute Difference in BST
⊣ 558 Logical OR of Two Binary Grids Represented as Quad-Trees
⊣ 559 Maximum Depth of N-ary Tree
⊣ 563 Binary Tree Tilt
⊣ 572 Subtree of Another Tree
⊣ 589 N-ary Tree Preorder Traversal
⊣ 590 N-ary Tree Postorder Traversal
⊣ 606 Construct String from Binary Tree
⊣ 617 Merge Two Binary Trees
⊣ 652 Find Duplicate Subtrees
⊣ 654 Maximum Binary Tree
⊣ 669 Trim a Binary Search Tree
⊣ 671 Second Minimum Node In a Binary Tree
⊣ 700 Search in a Binary Search Tree
⊣ 701 Insert into a Binary Search Tree
⊣ 783 Minimum Distance Between BST Nodes
⊣ 814 Binary Tree Pruning
⊣ 865 Smallest Subtree with all the Deepest Nodes
⊣ 872 Leaf-Similar Trees
⊣ 897 Increasing Order Search Tree
⊣ 938 Range Sum of BST
⊣ 951 Flip Equivalent Binary Trees
⊣ 965 Univalued Binary Tree
⊣ 971 Flip Binary Tree To Match Preorder Traversal
⊣ 988 Smallest String Starting From Leaf
⊣ 1022 Sum of Root To Leaf Binary Numbers
⊣ 1026 Maximum Difference Between Node and Ancestor
⊣ 1028 Recover a Tree From Preorder Traversal
⊣ 1038 Binary Search Tree to Greater Sum Tree
⊣ 1110 Delete Nodes And Return Forest
⊣ 1123 Lowest Common Ancestor of Deepest Leaves
⊣ 1145 Binary Tree Coloring Game
⊣ 1161 Maximum Level Sum of a Binary Tree
⊣ 1261 Find Elements in a Contaminated Binary Tree
⊣ 1302 Deepest Leaves Sum
⊣ 1315 Sum of Nodes with Even-Valued Grandparent
⊣ 1325 Delete Leaves With a Given Value
⊣ 1339 Maximum Product of Splitted Binary Tree
⊣ 1376 Time Needed to Inform All Employees
⊣ 1379 Find a Corresponding Node of a Binary Tree in a Clone of That Tree
⊣ 1382 Balance a Binary Search Tree
⊣ 1448 Count Good Nodes in Binary Tree
⊣ 1519 Number of Nodes in the Sub-Tree With the Same Label
⊣ 1600 Throne Inheritance
⊣ 1719 Number Of Ways To Reconstruct A Tree
⊣ 1766 Tree of Coprimes
⊣ 1932 Merge BSTs to Create Single BST
⊣ 1993 Operations on Tree
⊣ 2003 Smallest Missing Genetic Value in Each Subtree
⊣ 2096 Step-By-Step Directions From a Binary Tree Node to Another
⊣ 2236 Root Equals Sum of Children
⊣ 2265 Count Nodes Equal to Average of Subtree
⊣ 2331 Evaluate Boolean Binary Tree
⊣ 2385 Amount of Time for Binary Tree to Be Infected
⊣ 2440 Create Components With Same Value
⊣ 2467 Most Profitable Path in a Tree
⊣ 2872 Maximum Number of K-Divisible Components
⊣ 3067 Count Pairs of Connectable Servers in a Weighted Tree Network
⊣ 3203 Find Minimum Diameter After Merging Two Trees
⊣ 3249 Count the Number of Good Nodes
⊣ 3319 K-th Largest Perfect Subtree Size in Binary Tree
⊣ 3327 Check if DFS Strings Are Palindromes
⊣ 3331 Find Subtree Sizes After Changes
⊣ 3372 Maximize the Number of Target Nodes After Connecting Trees I
⊣ 3373 Maximize the Number of Target Nodes After Connecting Trees II
⊣ 3558 Number of Ways to Assign Edge Weights I
⊣ 3589 Count Prime-Gap Balanced Subarrays
⊣ 3715 Sum of Perfect Square Ancestors
⊣ 3812 Minimum Edge Toggles on a Tree
⊣ 3841 Palindromic Path Queries in a Tree