Skip to content
DSA Patterns

Learn

Trees

Binary tree interview patterns: the four traversals, the return-value recursion template, the global-plus-return trick for path problems, and the BST invariant.

4 min readUpdated 2 Sept 2026

#Trees#Recursion#BST#DFS

Trees are the largest section on the sheet — 32 problems — because almost every one is the same recursion wearing a different question. Get the template and the traversal orders solid and the section collapses into a handful of ideas.

The traversals

def inorder(node):
    if not node:
        return
    inorder(node.left)
    visit(node)              # preorder puts this first, postorder last
    inorder(node.right)

The name says where the visit happens relative to the recursive calls. What matters is which one each question needs:

FBGADIpreorderF B A D G IinorderA B D F G IpostorderA D B I G F
The name says where the node is visited relative to its children. Inorder on a BST yields sorted order — the most useful tree fact in interviews.
OrderSequenceUse it for
Preordernode, left, rightCopying/serialising a tree — the root arrives first
Inorderleft, node, rightBSTs — this yields sorted order
Postorderleft, right, nodeAnything needing children's results first: height, deletion
Level orderbreadth-firstAnything per level — see BFS

"Inorder on a BST is sorted" is the single most useful tree fact in interviews. It answers

Validate BST, Kth Smallest in a BST and BST Iterator directly.

The recursion template

Nearly every tree problem fits this shape: handle the empty node, recurse both ways, combine.

def solve(node):
    if not node:
        return base_case          # 0, None, True — whatever the identity is
    left = solve(node.left)
    right = solve(node.right)
    return combine(node.val, left, right)
def max_depth(node):
    if not node:
        return 0
    return 1 + max(max_depth(node.left), max_depth(node.right))

Getting the base case right is the problem most of the time. It is the identity element for whatever you are combining: 0 for a sum or depth, True for a universal check, -inf for a maximum.

Return one thing, track another

The trick behind every "path" problem, and the thing that separates a clean solution from a tangle. Some questions need the recursion to return one value while the answer is a different value computed at each node. Keep the answer in an enclosing variable.

def diameter_of_binary_tree(root):
    best = 0

    def depth(node):
        nonlocal best
        if not node:
            return 0
        left, right = depth(node.left), depth(node.right)
        best = max(best, left + right)      # the answer: a path THROUGH this node
        return 1 + max(left, right)         # the return: a path DOWN from this node

    depth(root)
    return best

Binary Tree Maximum Path Sum is the same skeleton with one addition — clamp negative

subtree contributions to zero with max(0, …), since a negative branch is never worth including.

Lowest common ancestor

Elegant enough to be worth memorising. Return the node if it is either target; otherwise recurse both sides. If both sides return something, this node is the LCA; if only one does, pass it up.

def lowest_common_ancestor(root, p, q):
    if not root or root is p or root is q:
        return root
    left = lowest_common_ancestor(root.left, p, q)
    right = lowest_common_ancestor(root.right, p, q)
    if left and right:
        return root
    return left or right

On a BST it is simpler still: walk down while both targets are on the same side, and the first node that splits them is the answer — O(height), no recursion needed.

Validating a BST

The classic wrong answer checks left.val < node.val < right.val locally. That passes a tree where a deep left descendant is larger than the root. The BST property is about

ranges, not neighbours, so pass bounds down:

def is_valid_bst(node, low=float("-inf"), high=float("inf")):
    if not node:
        return True
    if not low < node.val < high:
        return False
    return (is_valid_bst(node.left, low, node.val)
            and is_valid_bst(node.right, node.val, high))

The alternative — inorder traverse and check the sequence is strictly increasing — is equally valid and often easier to explain.

Construction and serialisation

Construct from Preorder and Inorder rests on one observation: preorder's first element is

the root, and finding it in the inorder list splits that list into the left and right subtrees. Build a value → index map for the inorder array first, or the repeated linear search makes it O(n²).

Serialize and Deserialize is preorder with explicit null markers. Nulls are what make the

string unambiguous — without them a single traversal cannot reconstruct the shape.

Complexity

O(n) time for a full traversal. Space is O(h) for the recursion stack: O(log n) on a balanced tree, O(n) on a degenerate one — which is why "what if the tree is a linked list" is a fair follow-up. Morris traversal achieves O(1) space by temporarily rewiring pointers, and is worth knowing exists.

Mistakes that cost the round

  • Checking the BST property locally instead of with inherited bounds.
  • Confusing the answer with the return value in path problems — a path through a node

cannot be extended upward, only a path down can.

  • Wrong base case. return 0 where the identity should be -inf breaks maximum

problems on all-negative trees.

  • Missing null markers when serialising.
  • Ignoring stack depth on a skewed tree.

What to drill

Start with the easy tier — Maximum Depth, Invert, Symmetric, Same Tree — until the template is automatic, then:

  1. Diameter of Binary Tree — return one thing, track another.
  2. Validate Binary Search Tree — the bounds argument.
  3. Lowest Common Ancestor — both the general and BST versions.
  4. Construct Binary Tree from Preorder and Inorder — the split insight.
  5. Serialize and Deserialize Binary Tree — null markers.
  6. Binary Tree Maximum Path Sum — the Hard version of the diameter trick.

All 32 are on the 22 DSA Patterns sheet.

Frequently asked

Which tree traversal should I use?

Inorder for a BST, because it yields values in sorted order. Postorder when a node's answer depends on its children — heights, deletions, bottom-up aggregation. Preorder for copying or serialising, since the root arrives first. Level order (BFS) for anything phrased per level.

How do you validate a binary search tree?

Pass a valid range down the recursion: the left subtree inherits an upper bound of the current node's value, the right subtree a lower bound. Checking only that each node sits between its immediate children is the classic wrong answer — it accepts trees where a deep descendant violates the ordering against an ancestor.

Why does the diameter problem return a different value than it records?

Because a path through a node — left depth plus right depth — cannot be extended further up the tree, while the value the parent needs is the longest path going straight down. So the recursion returns 1 + max(left, right) and separately records left + right in an enclosing variable as the running answer.

Related