Pattern 07 of 20
Trees
Recursion on subtrees: depth-first for paths and properties, breadth-first for levels.
When to use it
- The input is a binary tree or a binary search tree
- The answer combines results from the left and right subtrees
- You need nodes level by level
The idea
Solve the problem for one node by assuming it is solved for its children, then combine. Decide what each call returns to its parent and what it records globally; that choice is most of the solution.
Typical cost O(n) time, O(h) stack space where h is the tree height.
How it works
Tree problems are recursion problems. Assume your function already works on the left and right subtrees, then decide how to combine their answers at the current node. If you can state what one call returns to its parent, the code nearly writes itself.
Many questions need two quantities: the value a node reports upward and a global best that may be achieved at that node. Diameter and maximum path sum are the classic cases: each node returns its best downward path, but the answer is the best path that bends through it.
Binary search trees add an ordering invariant. An in-order traversal visits values in sorted order, and every node carries an allowed (low, high) range from its ancestors. Validation, k-th smallest and lowest common ancestor all exploit that.
Breadth-first search is the tool for anything about levels: level averages, right-side view, minimum depth, zigzag order. Process the queue one level at a time by reading its length before the inner loop.
How to attack it
- 1Decide depth-first (paths, subtree properties) or breadth-first (levels, nearest).
- 2Define exactly what the recursive call returns for a subtree.
- 3Write the base case for an empty node first.
- 4Decide whether a separate global answer is needed beyond the return value.
- 5Consider recursion depth on a skewed tree and switch to an explicit stack if needed.
Template
A skeleton to adapt, not a solution to paste. Write it from memory a few times.
from collections import deque
def max_depth(root):
# Post-order: answer for a node from its children's answers.
if root is None:
return 0
return 1 + max(max_depth(root.left), max_depth(root.right))
def diameter(root):
best = 0
def height(node):
nonlocal best
if node is None:
return 0
l, r = height(node.left), height(node.right)
best = max(best, l + r) # global answer: a path bending at node
return 1 + max(l, r) # value reported to the parent
height(root)
return best
def level_order(root):
levels, queue = [], deque([root] if root else [])
while queue:
level = []
for _ in range(len(queue)): # exactly one level per outer loop
node = queue.popleft()
level.append(node.val)
queue.extend(c for c in (node.left, node.right) if c)
levels.append(level)
return levelsVariants
- Post-order aggregation
- Height, size, balance and subtree sums: compute children first, then the node.
- Return value plus global best
- Diameter and maximum path sum report one thing upward and update a separate best at each node.
- Top-down with state
- Pass a running path sum, depth or allowed BST range down to the children.
- Level-order BFS
- Snapshot the queue length (or swap queues) to handle one level at a time.
- Construction and serialization
- Rebuild a tree from two traversals, or encode it with null markers; preorder with markers is unambiguous.
Pitfalls
- Validating a BST by comparing each node only with its direct children instead of its full ancestor range.
- Confusing what the function returns with the global answer, especially in path problems.
- Not handling the empty tree at the top level.
- Using queue.shift() in JavaScript inside a hot loop, which is O(n) per call.
- Stack overflow from recursion on a degenerate tree of 10^5 nodes.
What it costs
| Operation | Cost |
|---|---|
| DFS or BFS over all nodes | O(n) time |
| Recursive DFS stack | O(h): O(log n) balanced, O(n) skewed |
| BFS queue | O(w), the widest level, up to n/2 |
| BST search / insert | O(h) |
61 Trees problems
Easiest first. Problems marked Free can be solved without Pro.
- 1Tree HeightFreeEasy
- 2Root-First Visit OrderFreeEasy
- 3Left-Root-Right ReadingFreeEasy
- 4Children Before ParentEasy
- 5Twin TreesEasy
- 6Mirror-Image CheckEasy
- 7Even-Keeled TreeEasy
- 8Embedded CopyEasy
- 9Root-to-Leaf Target TotalEasy
- 10Left-Hanging LeavesEasy
- 11Flip Every BranchEasy
- 12Locate a Key in a Search TreeEasy
- 13Nearest Shelf LabelEasy
- 14Price Band TotalEasy
- 15Balanced Tree From Sorted PricesEasy
- 16Smallest Gap Between LabelsEasy
- 17Overlay Two TreesEasy
- 18Two Keys That Add UpEasy
- 19Flatten a Search Tree Into a ChainEasy
- 20Most Frequent Search KeysEasy
- 21Floor-by-Floor ListingMedium
- 22Serpentine Floor WalkMedium
- 23Skyline From the EastMedium
- 24Deepest Leftmost ValueMedium
- 25Longest Span Between NodesMedium
- 26Widest FloorMedium
- 27Outline of the TreeMedium
- 28Digit Trails TotalMedium
- 29Every Trail to the TargetMedium
- 30Unchallenged NodesMedium
- 31Downward Segments With Target SumMedium
- 32Longest Uniform ChainMedium
- 33Tree HeistMedium
- 34Coin Balancing MovesMedium
- 35Spread From the SparkMedium
- 36Is It a Search Tree?Medium
- 37K-th Ranked TicketMedium
- 38Shelve a New KeyMedium
- 39Remove a Catalog EntryMedium
- 40Trim a Search Tree to a WindowMedium
- 41Shared Ancestor in a Search TreeMedium
- 42Add Every Larger KeyMedium
- 43Next Larger KeyMedium
- 44Repair Two Swapped KeysMedium
- 45Rebuild From Pre-Order and In-Order LogsMedium
- 46Rebuild From In-Order and Post-Order LogsMedium
- 47Search Tree From a Pre-Order LogMedium
- 48Nearest Common ManagerMedium
- 49Encode a Tree as TextMedium
- 50Decode a Tree From TextMedium
- 51Nodes Exactly K Hops AwayMedium
- 52Column-by-Column ReportHard
- 53Heaviest Path AnywhereHard
- 54Fewest WatchtowersHard
- 55Height After PruningHard
- 56Total Distance From Every NodeHard
- 57Richest Valid Search SubtreeHard
- 58Insertion Orders for the Same TreeHard
- 59Fewest Cameras to Watch Every NodeHard
- 60Rebuild a Tree From a Dashed OutlineHard
- 61Height After Cutting a BranchHard
Solve them in the browser, in Python or JavaScript.