← 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(logn)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