Trees & Binary Search Trees
Traversals, the recursive shape of tree problems, and why an in-order walk of a BST comes out sorted.
A tree is a graph with hierarchy: one root, child links, and no cycles. That shape makes many problems feel almost too simple once you trust recursion. Solve the left subtree, solve the right subtree, then combine the two answers at the current node.
Binary search trees add ordering. Every value in the left subtree is smaller than the node, and every value in the right subtree is larger. That one invariant turns search, insertion, and validation into guided walks instead of blind traversals.
The mental model
A binary tree node stores a value and up to two child references:
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
Most tree code has the same recursive shape:
def solve(node):
if node is None:
return base_answer
left = solve(node.left)
right = solve(node.right)
return combine(node.val, left, right)
Traversal is the order in which you visit nodes:
- Preorder — node, left, right. Useful when the parent should be processed before children, like copying or serializing a tree.
- Inorder — left, node, right. In a BST, this produces values in sorted order.
- Postorder — left, right, node. Useful when the parent depends on child answers, like deleting or computing subtree sizes.
- Level-order — breadth-first by depth, using a queue.
from collections import deque
def inorder(node):
if not node:
return []
return inorder(node.left) + [node.val] + inorder(node.right)
def level_order(root):
if not root:
return []
out = []
q = deque([root])
while q:
node = q.popleft()
out.append(node.val)
if node.left:
q.append(node.left)
if node.right:
q.append(node.right)
return out
For BSTs, the invariant is global, not just parent-child. Every node in the entire left subtree must be below the current value; every node in the entire right subtree must be above it. That is why bounds are safer than only comparing a node with its immediate children.
Pattern recognition
Variations
Worked problems
The first two problems are traversal mechanics. The third is the BST invariant in its most interview-relevant form.
Maximum Depth of Binary Tree
Given the root of a binary tree, return the number of nodes on the longest path from the root down to any leaf. An empty tree has depth 0.
Approach. Define depth(node) as the depth of the subtree rooted at node. The empty subtree contributes 0. A real node contributes 1 plus the deeper of its two children. This is the canonical tree recursion shape.
Show solution
def max_depth(root):
if root is None:
return 0
return 1 + max(max_depth(root.left), max_depth(root.right))Complexity. O(n) time because every node is visited once. Space is O(h) for the recursion stack, where h is the tree height.
Binary Tree Level Order Traversal
Given a binary tree, return its values grouped by depth from top to bottom. Each inner list should contain the nodes from one level, left to right.
Approach. BFS naturally processes nodes by distance from the root. At the start of each loop, the queue contains exactly one level. Capture its current length, pop that many nodes, and enqueue their children for the next round.
Show solution
from collections import deque
def level_order(root):
if root is None:
return []
ans = []
q = deque([root])
while q:
level = []
for _ in range(len(q)):
node = q.popleft()
level.append(node.val)
if node.left:
q.append(node.left)
if node.right:
q.append(node.right)
ans.append(level)
return ansComplexity. O(n) time and O(w) space, where w is the maximum width of the tree. In the worst case, w can be O(n).
Validate Binary Search Tree
Given a binary tree, decide whether it satisfies the binary-search-tree rule: every node in a left subtree is smaller than its ancestor, and every node in a right subtree is larger.
Approach. A local check against children is not enough. A node deep in the right subtree must still be greater than the original root. Carry an open lower and upper bound as you recurse. Going left tightens the upper bound; going right tightens the lower bound.
Show solution
def is_valid_bst(root):
def valid(node, low, high):
if node is None:
return True
if not (low < node.val < high):
return False
return valid(node.left, low, node.val) and valid(node.right, node.val, high)
return valid(root, float("-inf"), float("inf"))Complexity. O(n) time and O(h) space for the recursion stack. The bounds make the global BST invariant explicit at every node.