Algorithm tutorials
Tree Basics and Traversals
Tree vocabulary, how to root a tree given as an edge list, bottom-up work in reverse traversal order, ancestor checks with entry/exit indices, and the binary-tree traversals interviewers expect.
Intuition
A tree is a connected undirected graph without cycles. The following are equivalent ways to recognize one with n vertices: connected with n - 1 edges; acyclic with n - 1 edges; exactly one simple path between any two vertices; every edge is a bridge. That unique path is what makes trees so pleasant: there is never a choice of route, so many questions reduce to one traversal.
Choosing a root turns the tree into a hierarchy:
- the parent of
vis the next vertex on the path fromvto the root; the root has none; - children, siblings, ancestors (vertices on the path to the root) and descendants follow;
- depth of
v= number of edges from the root; height of the tree = maximum depth; - the subtree of
v=vplus all its descendants; - a leaf has no children (in an unrooted tree, degree at most 1; "at most" because a single-vertex tree has a degree-0 leaf).
Binary-tree words used in interviews: a full binary tree has 0 or 2 children everywhere; a complete one fills every level except possibly the last, which is filled from the left; a perfect one has all leaves at the same depth. Textbooks disagree on these names, so define them when you use them.
When to use it in interviews / OAs
- The input is
nvertices andn - 1edges (or aparentarray): root it, then compute depths, subtree sizes or sums. - A
TreeNodewithleftandright: almost every question is one of the four traversals with some bookkeeping. - "Level by level", "right side view", "zigzag", "average of each level": BFS with one loop per level.
- "Is
uan ancestor ofv" asked many times: entry/exit indices from one DFS. - Rebuilding a tree from two traversal sequences.
Template
Rooting a tree given as edges
An explicit stack yields a preorder in which every parent comes before its children, so the reversed order visits children first. That gives bottom-up computations (subtree sizes, sums, DP) with no recursion at all.
import sys
from collections import deque
from dataclasses import dataclass
def root_tree(n: int, edges: list[tuple[int, int]], root: int = 0):
adj: list[list[int]] = [[] for _ in range(n)]
for u, v in edges:
adj[u].append(v)
adj[v].append(u)
parent, depth, order = [-1] * n, [0] * n, []
stack = [root]
while stack:
u = stack.pop()
order.append(u) # preorder: a parent always precedes its children
for v in adj[u]:
if v != parent[u]: # the only neighbor that is not a child
parent[v] = u
depth[v] = depth[u] + 1
stack.append(v)
return parent, depth, order
def subtree_sizes(parent: list[int], order: list[int]) -> list[int]:
size = [1] * len(parent)
for v in reversed(order): # children before parents
if parent[v] != -1:
size[parent[v]] += size[v]
return size
def ancestor_checker(parent: list[int], order: list[int]):
# each subtree is a contiguous block of the preorder
size = subtree_sizes(parent, order)
tin = [0] * len(order)
for i, v in enumerate(order):
tin[v] = i
def is_ancestor(u: int, v: int) -> bool: # True also when u == v
return tin[u] <= tin[v] < tin[u] + size[u]
return is_ancestor
Binary tree traversals
@dataclass
class TreeNode:
val: int
left: "TreeNode | None" = None
right: "TreeNode | None" = None
def inorder_iter(root: TreeNode | None) -> list[int]:
out, stack, node = [], [], root
while stack or node:
while node: # walk left as far as possible
stack.append(node)
node = node.left
node = stack.pop()
out.append(node.val)
node = node.right
return out
def preorder_iter(root: TreeNode | None) -> list[int]:
out, stack = [], [root] if root else []
while stack:
node = stack.pop()
out.append(node.val)
if node.right: # push right first so left is processed first
stack.append(node.right)
if node.left:
stack.append(node.left)
return out
def postorder_iter(root: TreeNode | None) -> list[int]:
# root-right-left preorder, reversed, is left-right-root
out, stack = [], [root] if root else []
while stack:
node = stack.pop()
out.append(node.val)
if node.left:
stack.append(node.left)
if node.right:
stack.append(node.right)
return out[::-1]
def level_order(root: TreeNode | None) -> list[list[int]]:
levels, q = [], deque([root] if root else [])
while q:
levels.append([])
for _ in range(len(q)): # exactly the nodes of the current level
node = q.popleft()
levels[-1].append(node.val)
if node.left:
q.append(node.left)
if node.right:
q.append(node.right)
return levels
Rebuilding from preorder and inorder
The first preorder value is the root; its position in the inorder splits the left and right subtrees. A value-to-index map makes each split O(1). Values must be distinct.
def build_tree(preorder: list[int], inorder: list[int]) -> TreeNode | None:
sys.setrecursionlimit(max(1000, 2 * len(preorder) + 50))
pos = {v: i for i, v in enumerate(inorder)}
it = iter(preorder)
def build(lo: int, hi: int) -> TreeNode | None: # inorder[lo:hi]
if lo >= hi:
return None
node = TreeNode(next(it))
mid = pos[node.val]
node.left = build(lo, mid) # left subtree consumes the next preorder values
node.right = build(mid + 1, hi)
return node
return build(0, len(inorder))
Complexity
Rooting, subtree sizes, every traversal and the rebuild are O(n) time. Extra space is O(n) for adjacency and order, or O(h) for the stacks of binary-tree traversals, where h is the height (h = n for a skewed tree). After the O(n) preprocessing, each ancestor query is O(1).
Common pitfalls
- Forgetting the parent check in an undirected adjacency list: the traversal walks back up and loops forever (or double counts).
- Recursion depth. A skewed tree with
10^4nodes breaks Python's default limit of about 1000 frames. Use the iterative forms, or raise the limit asbuild_treedoes. n = 1. No edges at all; the single vertex is both root and leaf. Many "leaf" or "center" algorithms special-case it.- Depth vs. height. Depth is measured from the root down to a node; height is measured from a node down to its deepest leaf. Agree on edges vs. nodes too.
- Duplicate values break the preorder + inorder rebuild, since the split position becomes ambiguous.
- Level-order without freezing the level size. Read
len(q)before the inner loop; it changes as children are appended.
Interview variations
- Bottom-up DP in reverse order. Subtree sums, "count nodes whose value equals the subtree average", "minimum cameras", "distribute coins": compute children before parents.
- Rerooting. "Sum of distances from every node": one bottom-up pass for subtree sizes, one top-down pass that moves the root across each edge.
- Distance-K nodes in a binary tree. Add parent pointers (one traversal), then BFS from the target as in a general graph.
- Serialize / deserialize. Preorder with null markers is enough to rebuild a binary tree uniquely.
- Side views and zigzag. Level-order with a per-level rule (last element, alternate direction).
- Morris traversal. Inorder in
O(1)extra space by temporarily threading right pointers; rarely required, good to mention. - Other rebuilds. Inorder + postorder works like the template from the back; preorder + postorder only determines a full binary tree.