Recursion
Last updated: May 2, 2026Table of Contents
- 0) Quick Reference
- Core Principle
- Quick Tips
- 1) Concepts
- 1-1) Core Ideas
- 1-2) Complexity Analysis
- 1-3) Related Concepts
- 2) Patterns
- 2-1) Basic Operation
- 2-2) Top-Down Recursion
- 2-3) Bottom-Up Recursion
- 2-4) Pass State to Next Recursion
- 2-5) Any-True Status in Recursion
- 2-6) Cartesian Product Construction
- 3) Advanced Techniques
- 3-1) Memoization
- 3-2) Divide & Conquer
- 3-3) Recursion to Iteration (Unfold Recursion)
- 4) Complete LeetCode Examples
- 4-1) Symmetric Tree (LC 101)
- 4-2) One Edit Distance (LC 161)
- 4-3) Merge Two Sorted Lists (LC 21)
- 4-4) Subtree of Another Tree (LC 572)
Recursion
0) Quick Reference
When should I use recursion?
- When a problem has overlapping subproblems that reduce the scope
- When you can clearly define a base case and recursive case
- When the problem naturally decomposes into smaller instances of itself
- For tree/graph traversal or backtracking problems
Quick Decision Guide
| Use Case | Pattern | Key Idea |
|---|---|---|
| Need info from parent nodes | Top-Down | Pass context down while traversing |
| Need results from children | Bottom-Up | Solve children first, combine results |
| Need to split & merge results | Divide & Conquer | Partition problem, solve parts, merge |
| Need to explore all possibilities | Backtracking | DFS with decision making |
| Multiple recursive calls, same subproblems | Memoization | Cache results to avoid redundant work |
Core Principle
For a problem F(X) where X is the input:
1. Break down into smaller scopes: x₀, x₁, ..., xₙ ∈ X
2. Recursively solve: F(x₀), F(x₁), ..., F(xₙ)
3. Combine results to solve F(X)
Quick Tips
- When in doubt: Write down the recurrence relationship (how F(n) relates to F(n-1), F(n-2), etc.)
- For redundant calls: Apply memoization (cache intermediate results)
- For stack overflow: Use tail recursion or convert to iteration
1) Concepts
1-1) Core Ideas
1-2) Complexity Analysis
Time Complexity: Think of recursion as a tree structure:
fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \ / \ / \
fib(2) fib(1) fib(1) fib(0)
/ \
fib(1) fib(0)
Given a recursion algorithm: O(T) = R × O(S)
- R = number of recursion invocations
- O(S) = time complexity of work per call
- For Fibonacci without memoization: O(2^n) (exponential)
Space Complexity:
Recursion-Related Space (Call Stack):
- Local variables in recursive function calls
- Input parameters
- Output variables
- Stack overflow risk: when allocated stack space reaches system limit
Recursion-Independent Space (Heap):
- Global variables
- Memoization cache (stores intermediate results)
- Important: Count memoization space when analyzing overall complexity
1-3) Related Concepts
Recursion is used in:
- DFS (Depth-First Search) — tree/graph traversal
- Backtracking — exploring all possibilities with pruning
- Tree problems — natural fit for recursive algorithms
- Dynamic Programming — with memoization optimization
2) Patterns
2-1) Basic Operation
Endless loop through elements in list (common in backtracking/generation):
# Example: LC 22 (Generate Parentheses)
_list = ["(", ")"]
for x in _list:
_tmp = tmp + x
help(_tmp)
2-2) Top-Down Recursion
Definition: Start from the root and make decisions at each node based on information passed down from parent nodes. Also known as “preorder” approach.
Time Complexity:
- Usually O(n) where n is the number of nodes
- Can be O(n²) if same subproblems are solved repeatedly without memoization
Space Complexity:
- O(h) where h is the height of recursion tree (call stack)
- O(n) additional space if memoization is used
Use Cases:
- When you need to pass information from parent to child
- Tree traversal with accumulated state
- Path-based problems
- Validation problems
Pros:
- Intuitive and easy to understand
- Natural for problems requiring parent-to-child information flow
- Good for early termination conditions
Cons:
- May do redundant calculations without memoization
- Can have higher space complexity due to call stack
Pattern:
def topDown(node, parentInfo):
# Base case
if not node:
return baseResult
# Use parentInfo to make decision
currentResult = processWithParentInfo(node, parentInfo)
# Pass updated info to children
newParentInfo = updateParentInfo(parentInfo, node)
leftResult = topDown(node.left, newParentInfo)
rightResult = topDown(node.right, newParentInfo)
# Combine results
return combineResults(currentResult, leftResult, rightResult)
Common LeetCode Problems:
- LC 104: Maximum Depth of Binary Tree
- LC 110: Balanced Binary Tree
- LC 112: Path Sum
- LC 113: Path Sum II
- LC 124: Binary Tree Maximum Path Sum
- LC 236: Lowest Common Ancestor
- LC 257: Binary Tree Paths
- LC 404: Sum of Left Leaves
- LC 437: Path Sum III
Example - Path Sum (LC 112):
def hasPathSum(self, root, targetSum):
def topDown(node, currentSum):
if not node:
return False
currentSum += node.val
# Leaf node check
if not node.left and not node.right:
return currentSum == targetSum
# Continue to children with updated sum
return (topDown(node.left, currentSum) or
topDown(node.right, currentSum))
return topDown(root, 0)
2-3) Bottom-Up Recursion
Definition: Start from leaf nodes and build up the solution by combining results from child nodes. Also known as “postorder” approach.
Time Complexity:
- Usually O(n) where n is the number of nodes
- Generally more efficient as each node is visited exactly once
Space Complexity:
- O(h) where h is the height of recursion tree (call stack)
- Usually no additional space needed for memoization
Use Cases:
- When solution depends on results from subtrees
- Tree property calculations (height, diameter, etc.)
- Aggregation problems
- Dynamic programming on trees
Pros:
- More efficient - each subproblem solved exactly once
- Natural for problems requiring child-to-parent information flow
- Often leads to cleaner code
- Better performance in most cases
Cons:
- Can be less intuitive for some problems
- May need to return multiple values from recursive calls
Pattern:
def bottomUp(node):
# Base case
if not node:
return baseResult
# Get results from children first
leftResult = bottomUp(node.left)
rightResult = bottomUp(node.right)
# Process current node using children results
currentResult = processNode(node, leftResult, rightResult)
return currentResult
Common LeetCode Problems:
- LC 104: Maximum Depth of Binary Tree
- LC 110: Balanced Binary Tree
- LC 543: Diameter of Binary Tree
- LC 124: Binary Tree Maximum Path Sum
- LC 968: Binary Tree Cameras
- LC 979: Distribute Coins in Binary Tree
- LC 1120: Maximum Average Subtree
- LC 1130: Minimum Cost Tree From Leaf Values
- LC 1372: Longest ZigZag Path in a Binary Tree
Example - Maximum Depth (LC 104):
def maxDepth(self, root):
def bottomUp(node):
if not node:
return 0
# Get depths from children
leftDepth = bottomUp(node.left)
rightDepth = bottomUp(node.right)
# Current depth is max of children + 1
return max(leftDepth, rightDepth) + 1
return bottomUp(root)
Example - Balanced Binary Tree (LC 110):
def isBalanced(self, root):
def bottomUp(node):
if not node:
return True, 0 # (isBalanced, height)
# Check left subtree
leftBalanced, leftHeight = bottomUp(node.left)
if not leftBalanced:
return False, 0
# Check right subtree
rightBalanced, rightHeight = bottomUp(node.right)
if not rightBalanced:
return False, 0
# Check current node balance
isCurrentBalanced = abs(leftHeight - rightHeight) <= 1
currentHeight = max(leftHeight, rightHeight) + 1
return isCurrentBalanced, currentHeight
balanced, _ = bottomUp(root)
return balanced
Comparison Table:
| Aspect | Top Down | Bottom Up |
|---|---|---|
| Direction | Root → Leaves | Leaves → Root |
| Information Flow | Parent → Child | Child → Parent |
| When to Use | Need parent context | Need subtree results |
| Efficiency | May have redundancy | Usually more efficient |
| Intuition | More intuitive for path problems | More intuitive for aggregation |
| Memoization Need | Often needed | Rarely needed |
2-4) Pass State to Next Recursion
Pass accumulated state/context as parameters to child recursive calls. Useful when you need to track information from parent nodes.
Example: LC 404 (Sum of Left Leaves)
// LC 404 - Sum of Left Leaves
// IDEA: Pre-order traversal, pass isLeft flag to track if node is left child
private int processSubtree(TreeNode subtree, boolean isLeft) {
// Base case: empty subtree
if (subtree == null) {
return 0;
}
// Base case: leaf node
if (subtree.left == null && subtree.right == null) {
return isLeft ? subtree.val : 0;
}
// Recursive case: process left and right subtrees
return processSubtree(subtree.left, true) + processSubtree(subtree.right, false);
}
Key Insight: By passing isLeft as a parameter, we track parent context without needing global state.
2-5) Any-True Status in Recursion
When you need to find ANY true result among recursive calls, use OR logic. Stop early if any recursive call returns true.
Example: LC 572 (Subtree of Another Tree)
// LC 572 - Subtree of Another Tree
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
// Check if subtree rooted at 'root' matches 'subRoot'
// Use OR: if ANY recursive call returns true, short-circuit and return true
return isSameTree(root, subRoot)
|| isSubtree(root.left, subRoot)
|| isSubtree(root.right, subRoot);
}
private boolean isSameTree(TreeNode node1, TreeNode node2) {
if (node1 == null || node2 == null) {
return node1 == null && node2 == null;
}
return node1.val == node2.val
&& isSameTree(node1.left, node2.left)
&& isSameTree(node1.right, node2.right);
}
Key Insight: Using OR (||) allows early exit when a true result is found, avoiding unnecessary recursive calls.
2-6) Cartesian Product Construction
Definition: Recursively generate all possible structures by dividing a range, building all sub-results for each partition, and combining them via Cartesian product. This is a form of Divide & Conquer where the “combine” step enumerates all left × right combinations.
Time Complexity: O(4^n / n^(3/2)) — Catalan number growth
Space Complexity: O(4^n / n^(3/2)) — storing all generated structures
Use Cases:
- Generate all structurally unique trees (BST, full binary trees)
- Enumerate all ways to parenthesize/split an expression
- Any problem where you partition a range and combine all sub-results
Pattern:
1. Pick each element i in [start, end] as the "root" / split point
2. Recursively build all left results from [start, i-1]
3. Recursively build all right results from [i+1, end]
4. Cartesian product: for each left × right, construct and collect result
5. Base case: empty range → return [null/None] (one empty result, NOT empty list)
// Template: Recursive Construction via Cartesian Product
private List<TreeNode> build(int start, int end) {
List<TreeNode> res = new ArrayList<>();
if (start > end) {
res.add(null); // CRITICAL: null = valid empty subtree
return res;
}
for (int i = start; i <= end; i++) {
List<TreeNode> lefts = build(start, i - 1);
List<TreeNode> rights = build(i + 1, end);
for (TreeNode l : lefts)
for (TreeNode r : rights)
res.add(new TreeNode(i, l, r));
}
return res;
}
Key Insight: Base case must return [null] (list containing null), NOT empty list. Otherwise Cartesian product loses all trees with empty left/right subtrees.
Optimization: Add memoization with Map<Pair<Integer,Integer>, List<TreeNode>> to avoid recomputing overlapping subproblems.
Common LeetCode Problems:
- LC 95: Unique Binary Search Trees II
- LC 96: Unique Binary Search Trees (Catalan count)
- LC 241: Different Ways to Add Parentheses
- LC 894: All Possible Full Binary Trees
- LC 1382: Balance a Binary Search Tree
Example — LC 95: Unique Binary Search Trees II:
def generateTrees(n):
if n == 0: return []
def generate(start, end):
if start > end:
return [None]
all_trees = []
for i in range(start, end + 1):
for left in generate(start, i - 1):
for right in generate(i + 1, end):
root = TreeNode(i)
root.left = left
root.right = right
all_trees.append(root)
return all_trees
return generate(1, n)
3) Advanced Techniques
3-1) Memoization
Idea: Cache results of recursive calls to avoid redundant work when the same subproblem is encountered multiple times.
When to Use:
- When recursive calls repeat (overlapping subproblems)
- When time complexity without memoization is exponential
- Trade space for time (use a hash map for cache)
Example 1: Fibonacci
# Without memoization: O(2^n) — exponential
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# With memoization: O(n) — linear
def fibonacci(n):
cache = {}
def helper(n):
if n in cache:
return cache[n]
if n < 2:
res = n
else:
res = helper(n - 1) + helper(n - 2)
cache[n] = res
return res
return helper(n)
Example 2: Climbing Stairs (LC 70)
# Without memoization: O(2^n)
class Solution:
def climbStairs(self, n):
if n == 1:
return 1
if n == 2:
return 2
return self.climbStairs(n - 1) + self.climbStairs(n - 2)
# With memoization: O(n)
class Solution:
def climbStairs(self, n):
cache = {}
def helper(n):
if n in cache:
return cache[n]
if n <= 2:
res = n
else:
res = helper(n - 2) + helper(n - 1)
cache[n] = res
return res
return helper(n)
Reference: https://leetcode.com/explore/learn/card/recursion-i/255/recursion-memoization/1495/
3-2) Divide & Conquer
Template:
1. Divide: Split problem into subproblems
2. Conquer: Solve each subproblem recursively
3. Combine: Merge subproblem results
Pseudo-code:
def divide_and_conquer(problem):
# (1) Divide
subproblems = divide(problem)
# (2) Conquer
results = [divide_and_conquer(sub) for sub in subproblems]
# (3) Combine
return combine(results)
Common Examples:
- Merge Sort — O(n log n)
- Quick Sort — O(n log n) average
- Binary Search — O(log n)
Common LeetCode Problems:
- LC 22: Generate Parentheses
- LC 84: Largest Rectangle in Histogram
- LC 315: Count of Smaller Numbers After Self
- LC 493: Reverse Pairs
- LC 1649: Create Sorted Array Through Instructions
Reference: https://leetcode.com/explore/learn/card/recursion-ii/470/divide-and-conquer/2869/
3-3) Recursion to Iteration (Unfold Recursion)
Why Convert:
- Avoid stack overflow risk
- Improve space/time efficiency
- Reduce function call overhead
How to Convert:
1. Use a stack or queue to replace the system call stack
2. At each recursion point, push parameters onto data structure
3. Replace recursive chain with loop over the data structure
Example: https://leetcode.com/explore/learn/card/recursion-ii/503/recursion-to-iteration/2693/
4) Complete LeetCode Examples
4-1) Symmetric Tree (LC 101)
Pattern: Bottom-up recursion, comparing two subtrees in parallel.
class Solution:
def isSymmetric(self, root):
if not root:
return True
return self.mirror(root.left, root.right)
def mirror(self, left, right):
if not left or not right:
return left == right
if left.val != right.val:
return False
return self.mirror(left.left, right.right) and self.mirror(left.right, right.left)
4-2) One Edit Distance (LC 161)
Pattern: Pruning branches early (abs difference > 1), then checking each position.
class Solution:
def isOneEditDistance(self, s, t):
m, n = len(s), len(t)
if abs(m - n) > 1:
return False
if m > n:
return self.isOneEditDistance(t, s)
for i in range(m):
if s[i] != t[i]:
if m == n:
return s[i + 1:] == t[i + 1:]
return s[i:] == t[i + 1:]
return m != n
4-3) Merge Two Sorted Lists (LC 21)
Pattern: Simple recursion with local state update.
class Solution:
def mergeTwoLists(self, l1, l2):
if not l1 or not l2:
return l1 or l2
if l1.val < l2.val:
l1.next = self.mergeTwoLists(l1.next, l2)
return l1
else:
l2.next = self.mergeTwoLists(l1, l2.next)
return l2
4-4) Subtree of Another Tree (LC 572)
Pattern: Any-true status with recursive helper.
class Solution:
def isSubtree(self, root, subRoot):
def isSameTree(p, q):
if not p and not q:
return True
if not p or not q:
return False
return (p.val == q.val and
isSameTree(p.left, q.left) and
isSameTree(p.right, q.right))
if not root and not subRoot:
return True
if not root or not subRoot:
return False
# Use OR: if any recursive call returns True, stop early
return (isSameTree(root, subRoot) or
self.isSubtree(root.left, subRoot) or
self.isSubtree(root.right, subRoot))
Java Version:
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
if (root == null) {
return false;
}
if (isIdentical(root, subRoot)) {
return true;
}
return isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot);
}
private boolean isIdentical(TreeNode node1, TreeNode node2) {
if (node1 == null || node2 == null) {
return node1 == null && node2 == null;
}
return node1.val == node2.val &&
isIdentical(node1.left, node2.left) &&
isIdentical(node1.right, node2.right);
}