Core CS Concept

Recursion for CS Students

Recursive functions explained through call-stack mechanics, base-case discipline, and tail-recursion optimization. Worked examples in Python, Java, C++, and JavaScript, including the exact maximum recursion depth each language allows before stack overflow. Verified CS graduates from CMU, MIT, and Stanford, starting at $20 per task.

Recursion concept visualization showing recursive functions and call-stack semantics
4 Languages covered
9 FAQ answers
2 Related subjects
4 Code examples

What it means

A working definition of Recursion

Recursion is a function calling itself with a smaller version of the same problem until a base case terminates the chain. Every recursive function needs a base case, a recursive case, and a contract that makes induction work.

Same factorial in 4 languages, same shape, different stack-depth ceilings: Python 1000 frames, Java 10000-20000, C++ 60000-100000 on Linux. Scala alone hits O(1) stack via @tailrec.

Primary example

factorial with a trace

factorial with a trace

          
          def factorial(n):
        
          
              # Base case: factorial of 0 is 1
        
          
              if n == 0:
        
          
                  return 1
        
          
              # Recursive case: n * factorial(n - 1)
        
          
              return n * factorial(n - 1)
        
          
           
        
          
          # Trace: factorial(4)
        
          
          # = 4 * factorial(3)
        
          
          # = 4 * (3 * factorial(2))
        
          
          # = 4 * (3 * (2 * factorial(1)))
        
          
          # = 4 * (3 * (2 * (1 * factorial(0))))
        
          
          # = 4 * (3 * (2 * (1 * 1)))
        
          
          # = 24
        

Three recursion patterns

Common recursion patterns

Base case discipline

Every recursive function needs a base case where the answer is returned directly. Missing or wrong base cases cause infinite recursion (stack overflow) or wrong answers on the smallest input. Write the base case first, before the recursive call.

Tail recursion

A tail-recursive function makes the recursive call its final action. Languages with TCO (Scheme, Haskell, Scala with @tailrec) reuse the stack frame, giving O(1) stack space. Python and Java do not optimize tail calls, so manual conversion to iteration is required for deep recursion.

Memoization

When recursion has overlapping subproblems (the same input computed many times), wrap the function with @lru_cache or a manual dictionary to cache results. Naive Fibonacci drops from Theta(phi^n) to Theta(n) once memoization eliminates the duplicate work.

Wrong way vs right way

Fix this recursion bug

Exponential, recomputes subproblems Python
# O(2^n) - blows up around n=35
def fib_slow(n):
    if n < 2:
        return n
    return fib_slow(n - 1) + fib_slow(n - 2)

# n=35 runtime: ~3.5 seconds
# n=40 runtime: ~40 seconds
# n=45 runtime: hours
Linear time, cached subproblems Python
from functools import lru_cache

# O(n) - handles n=1000 instantly
@lru_cache(maxsize=None)
def fib_fast(n):
    if n < 2:
        return n
    return fib_fast(n - 1) + fib_fast(n - 2)

# n=35 runtime: ~0.00005 seconds
# 70000x speedup over the naive version
Naive recursive Fibonacci recomputes fib(2) approximately 2^(n-2) times. Memoization caches results and drops the runtime from Theta(phi^n) to Theta(n). On n=35: 3.5 seconds becomes 0.00005 seconds, a 70000x speedup.

Cross-language

Same concept across languages

Scala: tail-recursive with @tailrec
import scala.annotation.tailrec

// Not tail-recursive (multiplication happens after the call)
def factorial(n: Int): Int =
  if (n == 0) 1
  else n * factorial(n - 1)

// Tail-recursive with accumulator
@tailrec
def factorialTR(n: Int, acc: Int = 1): Int =
  if (n == 0) acc
  else factorialTR(n - 1, n * acc)

// @tailrec annotation fails compilation if Scala
// cannot optimize the call. Use it as a guarantee.

FAQ

Recursion FAQ

What is a base case and why does my function need one?
A base case is the smallest input where the function returns directly without recursing. Without a base case, the function recurses forever and crashes with RecursionError in Python, StackOverflowError in Java, or a segfault in C or C++. The base case must be checked first, before the recursive call. Factorial(0) = 1; Fibonacci(0) = 0 and Fibonacci(1) = 1; sum of empty list = 0; in-order traversal of a null node returns immediately. Write the base case before the recursive case; trace on a 3-element example to verify termination.
How does the call stack work for recursive functions?
Every function call pushes a new stack frame containing local variables, parameters, return address, and saved registers. Recursive calls stack their frames sequentially: factorial(4) pushes a frame, calls factorial(3) which pushes another frame, and so on until factorial(0) returns and the frames pop in reverse order. Maximum stack depth at any instant equals the longest in-flight path. The total memory cost is depth times frame size (typically 32 to 256 bytes per frame depending on local variables). When depth exceeds the language stack limit (Python 1000, Java 10000-20000, C++ 60000-100000 on Linux), the runtime throws a stack overflow exception.
What is tail recursion and which languages optimize it?
A tail call is the last action a function takes before returning, with no further computation on the result. Tail-call optimization (TCO) reuses the current stack frame for the call instead of pushing a new one, making tail-recursive functions run in O(1) stack space. Scheme, Standard ML, Haskell, Erlang, F# perform TCO automatically per language spec. Scala does TCO when @tailrec is present. JavaScript ES6 specifies TCO in strict mode but no engine implements it. Python, Java, and most C and C++ compilers do not reliably perform TCO. For Python, Java, JavaScript: convert tail-recursive functions to iterative loops manually if deep recursion matters.
When do I need memoization?
When the recursion has overlapping subproblems (the same input is computed multiple times in the recursion tree). Naive recursive Fibonacci computes fib(2) approximately 2^(n-2) times for fib(n); memoized Fibonacci computes fib(2) once. Memoization candidates: Fibonacci, longest common subsequence, edit distance, 0/1 knapsack, matrix chain multiplication, coin change, any DP problem. Add memoization with @lru_cache(maxsize=None) in Python, ConcurrentHashMap or Caffeine in Java, std::unordered_map in C++. Bottom-up DP achieves the same complexity without recursion overhead; the choice is stylistic.
How do I fix a stack overflow in a recursive function?
Three options. First: convert to iteration with an explicit stack (std::stack in C++, java.util.Deque in Java, list in Python). Second: rewrite as tail recursion if the language supports TCO (Scheme, Haskell, Scala with @tailrec). Third: raise the language stack limit (sys.setrecursionlimit(100000) in Python, java -Xss4m in Java, ulimit -s unlimited in shell for C and C++). The first option is the most reliable and portable. The third option works for one-off scripts but does not generalize, and Python's setrecursionlimit can still segfault if the C stack underneath runs out around 30000 to 50000 frames.
What is the maximum recursion depth in Python?
CPython 3.12 default is 1000 frames (sys.getrecursionlimit returns 1000). Raise with sys.setrecursionlimit(N) up to roughly 30000 before the C stack underneath segfaults, depending on platform and per-frame variable sizes. For deeper recursion (DFS on a million-node graph, deep parsing), convert to iteration with an explicit stack. PyPy has a similar default but supports larger limits because of its stack-management strategy. The recursion limit is a global setting; raising it affects all subsequent calls in the interpreter session.
Can you help with recursive algorithms in CS61A or CS50?
Yes. CS61A uses Python and Scheme for recursion-heavy problem sets including the Hog game (recursive game-tree evaluation), the Scheme interpreter (recursive evaluation of S-expressions), and the streams lab (lazy recursive sequences). CS50 uses C and Python for recursive problem sets including the Mario pset (recursive pyramid drawing), the recover pset (recursive file recovery), and the speller pset (recursive trie traversal). Both courses grade base-case correctness, recursive-case correctness, and edge-case handling explicitly. Our tutors produce solutions with traced execution on a 3-element example, plus pytest or check50 tests covering the base case, recursive case, and 3 adversarial inputs.
How do I trace a recursive function by hand?
Draw a tree where each node represents one call. Label the node with the parameters; label edges with the return value. For factorial(4): factorial(4) -> factorial(3) -> factorial(2) -> factorial(1) -> factorial(0) returns 1; factorial(1) returns 1; factorial(2) returns 2; factorial(3) returns 6; factorial(4) returns 24. For binary recursion like fibonacci: each node has 2 children. The tree depth equals the maximum recursion depth; the tree size equals the total number of calls. Tracing by hand on a 3 or 4 element example catches most base-case and recursive-case bugs before the code is run.
When should I use recursion vs iteration?
Use recursion when the problem has self-similar structure: tree traversal, divide-and-conquer sorting, backtracking, parsing of nested expressions, graph DFS at small depths. Use iteration when stack depth is a concern (DFS on million-node graphs, deep linked-list traversal) or when the iterative version is genuinely simpler (summing an array, linear search, finding the max). Iteration is 2x to 5x faster than equivalent recursion due to per-call overhead. The default: write recursion for clarity, convert to iteration when stack depth or performance demands it.

Stuck on recursion?

Submit your assignment and get expert, pedagogical help within 12 hours. Every solution ships with line-by-line comments, complexity analysis, and unlimited revisions.

Get Recursion Help