On this page
Description
Binary tree traversal is recursion shaped like the tree. Handle the empty tree, recurse into the left and right subtrees, and combine. Almost every tree problem takes one of two shapes. In the bottom-up shape, the children report results and the parent combines them (height, size, balance). In the top-down shape, the parent passes rules down to its children (a BST's valid range, the depth so far). Picking the right shape is most of the work.
We'll go from simple to more technical:
- The pattern: null base case, recurse, combine results on the way up.
- A second flavor: pass constraints down instead (validating a BST).
- Where tree walks run in production (DOMs, file systems, permissions).
- The bugs: missing base cases, local-only checks, and recomputing subtrees.
The approach
A tree has no single "front to back" order the way an array does. But every subtree is a smaller tree, so recursion fits it naturally. First decide what an empty tree returns. Then ask both subtrees for their answers, and combine them with the node itself.
The height of a tree is the number of nodes on its longest path down from the root. An empty tree has height 0. Any other tree has height 1 plus the taller of its two subtrees.
The answers come back from the bottom up:
| Node | Left height | Right height | Returns |
|---|---|---|---|
| 1 | 0 | 0 | 1 |
| 3 | 1 | 0 | 2 |
| 12 | 0 | 0 | 1 |
| 9 | 0 | 1 | 2 |
| 8 | 0 | 2 | 3 |
| 6 | 2 | 3 | 4 |
Java
int height(TreeNode node) {
if (node == null) return 0; // an empty tree has height 0
int left = height(node.left);
int right = height(node.right);
return 1 + Math.max(left, right); // combine: this node plus the taller side
}
Python
def height(node: TreeNode | None) -> int:
if node is None:
return 0 # an empty tree has height 0
left = height(node.left)
right = height(node.right)
return 1 + max(left, right) # combine: this node plus the taller side
Size, sum and balance checks are the same shape with a different combine step.
A second flavor: passing constraints down
Some problems need the other direction. Each call receives rules from its ancestors and passes a stricter version to its children. Validating a binary search tree is the clearest case. In a BST, everything in a node's left subtree is smaller than the node, and everything in its right subtree is larger. Comparing a node with its parent isn't enough.
Take a root of 10 whose left child is 5, and give 5 a right child of 12. Each node looks fine next to its parent: 12 is bigger than 5. But 12 sits in 10's left subtree, so it must be smaller than 10. Carrying an allowed range down catches it:
| Node | Allowed range | Inside it? |
|---|---|---|
| 10 | any value | yes |
| 5 | below 10 | yes |
| 12 | between 5 and 10 | no: the tree isn't a valid BST |
Java
boolean isValidBST(TreeNode node, long min, long max) {
if (node == null) return true;
if (node.val <= min || node.val >= max) return false;
return isValidBST(node.left, min, node.val) // left subtree: same lower bound, tighter upper bound
&& isValidBST(node.right, node.val, max); // right subtree: tighter lower bound, same upper bound
}
Python
def is_valid_bst(node: TreeNode | None, lo: float = float("-inf"), hi: float = float("inf")) -> bool:
if node is None:
return True
if not lo < node.val < hi:
return False
return (is_valid_bst(node.left, lo, node.val) # left subtree: same lower bound, tighter upper bound
and is_valid_bst(node.right, node.val, hi)) # right subtree: tighter lower bound, same upper bound
Each step down narrows the range. A node deep in the left subtree still has to respect the root's upper bound, set many calls earlier.
Where you'll see it
Every hierarchy is walked this way: an HTML or JSON document, a file system, a folder tree with permissions. Permission inheritance is the top-down shape. A folder's effective permissions combine the rules of every folder above it, not just its parent's.
Which tool for which job?
| You need | Use |
|---|---|
| A value computed from the children (height, size, sum) | bottom-up: return it from the recursion |
| A rule set by the ancestors (BST range, depth limit) | top-down: pass it as a parameter |
| Two facts about each subtree (balanced and height) | return both from one recursion |
| A very deep or skewed tree | an explicit stack instead of recursion |
| Level by level | BFS with a queue |
| Sorted order of a BST | in-order traversal |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
| No null base case | NullPointerException / AttributeError | handle null/None first |
| Checking a BST node only against its parent | invalid trees pass | carry the (low, high) range down |
Integer.MIN_VALUE/MAX_VALUE as range sentinels | trees holding those values fail | nullable bounds (or long) |
| Calling a height helper at every node | O(n log n) balanced, O(n^2) skewed | compute everything in one pass |
| Recursing on a 100,000-node chain | stack overflow | iterate with a stack |
Check yourself
Try to answer before opening each one - recalling beats re-reading.
1. A recursive tree function reads node.val without checking for null first. What happens?
Show answer
It crashes as soon as the recursion reaches a missing child: a NullPointerException in Java or an AttributeError in Python. Every tree function needs to decide what an empty tree returns before it touches a node's fields.
2. A BST check only tests node.left.val < node.val < node.right.val at each node. Why does it miss invalid trees?
Show answer
It only compares each node with its own children. A node can be fine next to its parent and still break a rule set higher up, like 12 under 5 under 10 above. The tree looks right at every node and is still not a BST.
3. Why pass a shrinking (min, max) range down instead of comparing with one value?
Show answer
A node's allowed range comes from every ancestor at once. The range carries all of those rules down together, getting narrower at each step, so each node is checked against all of its ancestors, not just the nearest one.
Key takeaways
- Most tree problems are bottom-up (combine the children's answers) or top-down (pass rules down as parameters).
- Height, size and sums are bottom-up. BST validation is top-down.
- A node can satisfy its parent and still break an older ancestor's rule. Carry the full range down.
- Decide what an empty tree returns before reading any node's fields.
- Calling a helper recursion at every node turns O(n) into O(n log n), or O(n^2) on a skewed tree.