Table of Contents
729 words
4 minutes
Recursion: The Art of Breaking Down Problems from the Top

Overview#

Recursion is a programming method in which a function calls itself directly or indirectly. It breaks a large problem into smaller problems of the same structure, stops splitting through a termination condition, and combines the results during the return process.

Prerequisites

  • Call stack: each recursive call saves the current execution context
  • Divide and conquer: recursion is often used to break a problem into subproblems of the same structure
  • Tree structure: trees are naturally recursive by definition, and traversal is best expressed recursively

Problem Definition#

When a problem can be described as “the current answer depends on a smaller instance of the same kind of problem,” recursion can be considered.

ElementDescription
InputThe current problem size or current node
OutputThe result at the current size
Termination conditionThe direct answer to the smallest problem
Recurrence relationHow the current result is obtained from subproblems

Core Principle: Step-by-Step Illustration#

Take factorial factorial(4) as an example:

factorial 4 factorial 3 factorial 2 factorial 1 Return 1 Return 2 Return 6 Return 24

Recursion involves two directions:

  1. Descending: continuously break the problem into smaller pieces until the termination condition is hit.
  2. Returning: after subproblems return their results, merge them layer by layer into the final answer.

If the termination condition is unreachable, recursion will call indefinitely and eventually cause a stack overflow.


Detailed Algorithm Steps#

Algorithm: RecursiveSolve(problem)
Input: current problem problem
Output: answer to problem
1. If problem is the smallest problem, return the known answer
2. Transform problem into one or more smaller problems of the same kind
3. Recursively solve the subproblems
4. Combine the subproblem results into the answer to the current problem
5. Return the answer

Complexity analysis:

ExampleTime complexitySpace complexityDescription
FactorialO(n)O(n)Recursion depth is n
Naive FibonacciO(2^n)O(n)Massive repeated computation
Memoized FibonacciO(n)O(n)Cache each subproblem
Binary tree depthO(n)O(h)h is the tree height
Fast exponentiationO(log n)O(log n)Size halves each time

TypeScript Implementation#

1. Factorial#

function factorial(n: number): number {
if (n <= 1) return 1;
return n * factorial(n - 1);
}

2. Fibonacci and Memoization#

function fib(n: number): number {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
function fibMemo(n: number, memo = new Map<number, number>()): number {
if (n <= 1) return n;
if (memo.has(n)) return memo.get(n)!;
const value = fibMemo(n - 1, memo) + fibMemo(n - 2, memo);
memo.set(n, value);
return value;
}

3. Tower of Hanoi#

function hanoi(n: number, from: string, to: string, via: string): void {
if (n === 1) {
console.log(`${from} -> ${to}`);
return;
}
hanoi(n - 1, from, via, to);
console.log(`${from} -> ${to}`);
hanoi(n - 1, via, to, from);
}

4. Fast Exponentiation#

function myPow(x: number, n: number): number {
if (n === 0) return 1;
if (n < 0) return 1 / myPow(x, -n);
const half = myPow(x, Math.floor(n / 2));
return n % 2 === 0 ? half * half : half * half * x;
}

5. Maximum Depth of a Binary Tree#

interface TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
}
function maxDepth(root: TreeNode | null): number {
if (root === null) return 0;
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}

6. Converting Recursion to Iteration#

function sumRecursive(n: number): number {
if (n === 0) return 0;
return n + sumRecursive(n - 1);
}
function sumIterative(n: number): number {
let sum = 0;
for (let i = 1; i <= n; i++) {
sum += i;
}
return sum;
}

Engineering Optimization: Memoization and Stack Depth Control#

ProblemOptimization methodDescription
Repeated computationMemoized searchUse a Map to cache subproblem results
Excessive stack depthRewrite as iterationAvoid call stack overflow
Tail recursionAccumulator parameterJavaScript engines do not guarantee tail call optimization
Global state pollutionParameter passing / local variablesRecursive functions become more reusable

Tail-recursive code is closer to iteration, but in JavaScript/TypeScript environments you cannot rely on the runtime to automatically optimize the call stack.


Applications and Limitations#

Typical Applications#

  • Traversal of trees and graphs
  • Divide-and-conquer algorithms: merge sort, quicksort, fast exponentiation
  • Memoized search versions of dynamic programming
  • Backtracking and combinatorial enumeration

Limitations#

LimitationDescription
Stack overflowThe call stack becomes uncontrollable when depth is too large
Repeated computationNaive recursion may degrade exponentially
Debugging costState tracking is harder with multiple call layers

Summary#

Define the base case Reduce the problem size Recursively solve the subproblem Merge the return values Cache or convert to iteration when necessary

Key points:

  1. The three essential elements of recursion are the termination condition, the recurrence relation, and the return value.
  2. Recursion is suitable for problems that are naturally hierarchical, such as tree-shaped, divide-and-conquer, and backtracking problems.
  3. Overlapping subproblems require memoization to avoid exponential repeated computation.
  4. When depth is uncontrollable, prefer rewriting to an explicit stack or iteration.
Recursion: The Art of Breaking Down Problems from the Top
https://www.hehonglei.com/technology/recursion/
Author
HHLBlog
Published at
2026-04-26
License
CC BY-NC-SA 4.0