Loading learning content...
Here's the most beautiful property of trees: every subtree is itself a tree.
This might sound obvious, but the implications are profound. When you look at a tree and focus on any internal node, you're looking at a smaller tree. That smaller tree has the same properties, the same structure, and can be processed with the same algorithms as the original. This self-similarity is why recursion and trees are a perfect match—and why tree algorithms are often stunningly elegant.
By the end of this page, you will understand subtrees formally, recognize how tree structure naturally enables recursion, master the 'think in subtrees' mental model, and see why this perspective transforms seemingly complex problems into simple recursive solutions.
The recursive nature of trees isn't just an algorithmic convenience—it's the fundamental reason trees are so powerful. Once you internalize "every internal node is the root of its own subtree," you'll start seeing trees differently. Problems that seemed daunting become tractable because you can reduce them to smaller instances of themselves.
A subtree is a portion of a tree that is itself a complete tree. Every node in a rooted tree is the root of exactly one subtree.
Formal definition:
Given a rooted tree T with root r, and any vertex v in T, the subtree rooted at v consists of:
- The vertex v itself
- All descendants of v
- All edges connecting v to its descendants
Alternative definition:
The subtree rooted at v is the induced subgraph of T containing v and all vertices reachable by moving downward (away from the root) from v.
In the diagram, the subtree rooted at A is highlighted:
Notice that this subtree is a valid tree on its own:
For a tree with n nodes, there are exactly n subtrees — one rooted at each node. The entire tree is the subtree rooted at the root. A single leaf node forms a trivial subtree of size 1.
Subtrees inherit all the fundamental properties of trees, plus they have organizational properties within the larger tree:
For any internal node n with children c₁, c₂, ..., cₖ: the subtree rooted at n consists of n itself, plus the subtrees rooted at c₁, c₂, ..., cₖ. These child subtrees are completely separate from each other. This partition is the key to recursive decomposition.
Trees are recursively defined. This means the very definition of a tree refers to smaller trees:
Recursive definition of a rooted tree:
A rooted tree is either:
- Base case: A single node (which is both the root and a leaf)
- Recursive case: A root node connected to one or more child subtrees, where each child subtree is itself a rooted tree
This definition is circular in the mathematical sense—but it's well-founded because each recursive step involves strictly smaller structures, bottoming out at single nodes.
This recursive structure is why trees and recursion are natural partners:
// The structure of almost every recursive tree function:
function solveForTree(node) {
// Base case: leaf or empty
if (!node || isLeaf(node)) {
return baseResult;
}
// Recursive case: solve for each child subtree
let results = [];
for (const child of node.children) {
results.push(solveForTree(child)); // Recursive call
}
// Combine results from subtrees
return combineResults(node, results);
}
The function's structure mirrors the tree's structure: we solve the problem for a tree by solving it for each subtree and combining.
Recursive definitions enable proofs by structural induction. To prove a property P holds for all trees: (1) prove P for a single-node tree (base case), (2) prove that if P holds for all child subtrees, it holds for the whole tree (inductive step). This proof technique is essential for reasoning about tree algorithms.
The most powerful skill in tree algorithm design is thinking in subtrees. Instead of thinking about the entire tree, you:
This is the "recursive leap of faith" applied to trees.
Example: Computing the maximum depth of a tree
Step 1: What do we need from child subtrees? The maximum depth of each child subtree.
Step 2: Assume we have it (trust recursion) Each child gives us a number: the maximum depth in that subtree.
Step 3: How do we combine at the current node? The maximum depth from the current node is 1 + the maximum of child depths.
Step 4: Base case for leaves? A leaf has depth 0 (no children to go deeper).
function maxDepth(node) {
// Base case
if (!node) return 0;
if (node.children.length === 0) return 0;
// Get information from each child subtree
let maxChildDepth = 0;
for (const child of node.children) {
maxChildDepth = Math.max(maxChildDepth, maxDepth(child));
}
// Combine: add 1 for the current level
return 1 + maxChildDepth;
}
Apply this process consistently and tree problems become formulaic.
Let's apply the four-step subtree thinking process to several classic problems:
Problem 1: Count all nodes in a tree
| Step | Answer |
|---|---|
| Count of nodes in each child subtree |
| Each child returns its subtree size |
| Sum all child counts + 1 (for this node) |
| Return 1 (the leaf itself) |
function countNodes(node) {
if (!node) return 0;
let count = 1; // Count this node
for (const child of node.children) {
count += countNodes(child); // Add child subtree counts
}
return count;
}
Problem 2: Find the sum of all node values
| Step | Answer |
|---|---|
| Sum of values in each child subtree |
| Each child returns its subtree sum |
| Sum all child sums + this node's value |
| Return this node's value |
function sumValues(node) {
if (!node) return 0;
let sum = node.value;
for (const child of node.children) {
sum += sumValues(child);
}
return sum;
}
Problem 3: Check if a value exists in the tree
| Step | Answer |
|---|---|
| Does the value exist in any child subtree? |
| Each child returns true/false |
| Return true if this node matches OR any child returns true |
| Return true if this leaf matches, else false |
function containsValue(node, target) {
if (!node) return false;
// Check this node
if (node.value === target) return true;
// Check child subtrees
for (const child of node.children) {
if (containsValue(child, target)) return true;
}
return false;
}
Recursive tree algorithms fall into two broad patterns based on how information flows:
Bottom-Up (Postorder-style):
Top-Down (Preorder-style):
Bottom-Up Example: Height
function height(node) {
if (!node || node.children.length === 0) return 0;
// First: get heights from all children (recurse)
let maxChildHeight = 0;
for (const child of node.children) {
maxChildHeight = Math.max(maxChildHeight, height(child));
}
// Then: compute this node's height
return 1 + maxChildHeight;
}
Top-Down Example: Compute Depth of Each Node
function computeDepths(node, currentDepth = 0) {
if (!node) return;
// First: process this node
node.depth = currentDepth;
// Then: recurse with updated depth
for (const child of node.children) {
computeDepths(child, currentDepth + 1);
}
}
Ask yourself: 'Does this node need information from its children, or from its parent?' If from children → bottom-up. If from parent → top-down. Some problems require both passes!
In binary trees, each node has at most two children, conventionally called the left child and right child. This creates exactly two special subtrees:
Definitions:
The left subtree of a node is the subtree rooted at its left child (or empty if no left child).
The right subtree of a node is the subtree rooted at its right child (or empty if no right child).
In the diagram:
The left/right distinction has profound implications:
Binary tree recursive pattern:
function binaryTreeAlgorithm(node) {
// Base case: null node
if (!node) {
return baseResult;
}
// Recurse on left and right subtrees
const leftResult = binaryTreeAlgorithm(node.left);
const rightResult = binaryTreeAlgorithm(node.right);
// Combine results
return combine(node.value, leftResult, rightResult);
}
This pattern is ubiquitous in binary tree algorithms:
// Height of binary tree
function height(node) {
if (!node) return -1; // or 0, depending on definition
return 1 + Math.max(height(node.left), height(node.right));
}
// Size of binary tree
function size(node) {
if (!node) return 0;
return 1 + size(node.left) + size(node.right);
}
Many tree problems are explicitly about subtrees. Here are common patterns:
Pattern 1: Find subtrees satisfying a condition
// Count subtrees with sum equal to target
function countSubtreesWithSum(node, target) {
if (!node) return { count: 0, sum: 0 };
const left = countSubtreesWithSum(node.left, target);
const right = countSubtreesWithSum(node.right, target);
const sum = node.value + left.sum + right.sum;
let count = left.count + right.count;
if (sum === target) count++;
return { count, sum };
}
Pattern 2: Check if one tree is a subtree of another
function isSubtree(s, t) {
if (!s) return !t;
if (isSameTree(s, t)) return true;
return isSubtree(s.left, t) || isSubtree(s.right, t);
}
function isSameTree(s, t) {
if (!s && !t) return true;
if (!s || !t) return false;
return s.value === t.value &&
isSameTree(s.left, t.left) &&
isSameTree(s.right, t.right);
}
| Problem Type | Approach | Example |
|---|---|---|
| Aggregate over subtree | Bottom-up recursion | Subtree sum, count, max |
| Compare subtrees | Recursive equality check | Same tree, symmetric tree |
| Find matching subtrees | Check condition at each node | Subtree with given sum |
| Subtree vs path | Distinguish 'entire subtree' from 'downward path' | Path sum vs subtree sum |
| Validate subtree property | Check invariant holds in all subtrees | BST validation |
Don't confuse 'subtree' problems with 'path' problems. A subtree includes all descendants of a node. A path is a sequence of nodes from one node to another. The same tree can have subtrees with sum X while having no path with sum X, or vice versa.
Recursive tree algorithms are elegant but require careful analysis:
Time Complexity:
Most tree algorithms visit each node exactly once, giving O(n) time complexity. However, some naive recursive solutions are much worse:
// INEFFICIENT: O(n²) for unbalanced trees
function isBalanced_Naive(node) {
if (!node) return true;
// height() is O(n), and we call it at each node!
const leftHeight = height(node.left);
const rightHeight = height(node.right);
if (Math.abs(leftHeight - rightHeight) > 1) return false;
return isBalanced_Naive(node.left) && isBalanced_Naive(node.right);
}
// EFFICIENT: O(n) by computing height bottom-up
function isBalanced_Efficient(node) {
function checkHeight(node) {
if (!node) return 0;
const left = checkHeight(node.left);
if (left === -1) return -1; // Left subtree unbalanced
const right = checkHeight(node.right);
if (right === -1) return -1; // Right subtree unbalanced
if (Math.abs(left - right) > 1) return -1; // This node unbalanced
return 1 + Math.max(left, right);
}
return checkHeight(node) !== -1;
}
Space Complexity:
Recursive algorithms use call stack space proportional to the recursion depth:
Mitigation strategies:
Aim for O(n) time where you visit each node once, computing everything you need in a single pass. If you find yourself calling the same recursive function on overlapping subtrees, you may be doing redundant work.
Let's consolidate the power of subtree thinking:
Module Complete:
You now have a complete understanding of tree fundamentals:
With this foundation, you're ready to explore tree properties (depth, height, levels), specific tree types (binary trees, N-ary trees), and traversal algorithms that visit every node systematically.
Congratulations! You've mastered the essential concepts of tree data structures. You understand trees formally (connected acyclic graphs), can speak their vocabulary fluently, classify nodes as leaves or internal, and think recursively about subtrees. This foundation will support everything you learn about trees going forward.