Break down complex problems by having functions call themselves — elegantly and efficiently.
0 / 7 topics completed
01
Introduction to Recursion
Recursion is a programming technique where a function calls itself
to solve a smaller version of the same problem. Instead of using explicit loops, we break a
problem into sub-problems until we reach a simple case we can solve directly.
Real-World Analogy
Imagine you're in a line of people and you want to know your position. You ask the person
in front, who asks the person in front of them, and so on — until the first person says
"I'm #1." Each person then adds 1 and passes the answer back. That chain of asking and
answering is recursion.
💡
Every recursive solution has two parts: a base case (the stopping
condition) and a recursive case (where the function calls itself with a
simpler input). Without a base case, you get infinite recursion and a stack overflow.
public static voidcountdown(intn) {
if (n <= 0) { // base case
System.out.println("Go!");
} else {
System.out.println(n);
countdown(n - 1); // recursive case
}
}
C++
voidcountdown(intn) {
if (n <= 0) { // base case
std::cout << "Go!" << std::endl;
} else {
std::cout << n << std::endl;
countdown(n - 1); // recursive case
}
}
02
Base Case & Recursive Case
These are the two pillars of every recursive function. Getting them right is the key to
writing correct recursion.
🛑 Base Case
The condition under which the function stops calling itself and returns
a direct answer. Without it, the function recurses forever. Think of it as the simplest
problem you already know the answer to.
🔄 Recursive Case
The part where the function calls itself with a slightly simpler or
smaller input, moving closer to the base case with every call. Each recursive call must
make progress toward the base case.
factorial(5)
→ 5 * factorial(4)
→ 4 * factorial(3)
→ 3 * factorial(2)
→ 2 * factorial(1)
→ 1 ← base case
Locating values back up: 120
03
The Call Stack
When a function calls itself, the computer must remember where it was so it can
continue after the inner call returns. It does this using a call stack — a
stack of frames, each storing the local variables and return address of one function call.
How it works
Each new recursive call pushes a frame onto the stack.
When a call returns, its frame is popped off and execution resumes
in the caller.
If recursion goes too deep without hitting a base case, the stack runs out of space
— a stack overflow.
⚠️
Most languages have a default recursion depth limit (Python: ~1000 frames). For very deep
recursion, consider an iterative approach or tail recursion if your
language supports it.
04
Recursion Tree Visualization
A recursion tree maps out every call a recursive function makes. Each node
is one function call, and its children are the calls it spawns. This is especially useful
for analyzing time complexity.
Divide-and-conquer processing architectures that break an unsorted collection down into
single individual items before assembling sorted frameworks sequentially.
public static voidmergeSort(int[] arr, intl, intr) {
if (l < r) {
intm = l + (r - l) / 2;
mergeSort(arr, l, m);
mergeSort(arr, m + 1, r);
merge(arr, l, m, r);
}
}
C++
voidmergeSort(intarr[], intl, intr) {
if (l < r) {
intm = l + (r - l) / 2;
mergeSort(arr, l, m);
mergeSort(arr, m + 1, r);
merge(arr, l, m, r);
}
}
4. Tree Traversals (Inorder, Preorder, Postorder)
Navigating inherently recursive data structures. Because every sub-tree is itself a valid
standalone tree, processing nodes via Depth-First Search (DFS) maps cleanly to call
stack operations.
Brute-force state space tree exploration where the function builds a solution
step-by-step and systematically retracts choices choices the moment validation bounds
fail.
The "Include/Exclude" pattern. At every recursive decision milestone, the code branches
into two paths: one tracking states with the current item, and one without it.
public static voidsubsets(int[] arr, intindex, List<Integer>currentPath) {
if (index == arr.length) {
System.out.println(currentPath);
return;
}
currentPath.add(arr[index]);
subsets(arr, index + 1, currentPath); // IncludecurrentPath.remove(currentPath.size() - 1);
subsets(arr, index + 1, currentPath); // Exclude
}
C++
voidsubsets(const vector<int>&arr, size_tindex, vector<int>currentPath) {
if (index == arr.size()) {
for(intx : currentPath) std::cout << x << " ";
std::cout << std::endl;
return;
}
currentPath.push_back(arr[index]);
subsets(arr, index + 1, currentPath); // IncludecurrentPath.pop_back();
subsets(arr, index + 1, currentPath); // Exclude
}
7. Flood Fill & Grid Exploration
Graph traversal variants where every cell triggers multi-directional recursive branches
to discover contiguous areas while avoiding infinite tracking loops via tracking states.
public static voiddfsGrid(intr, intc, int[][] grid, boolean[][] visited) {
if (r < 0 || c < 0 || r >= grid.length || c >= grid[0].length || visited[r][c]) {
return;
}
visited[r][c] = true;
dfsGrid(r + 1, c, grid, visited); // DowndfsGrid(r - 1, c, grid, visited); // UpdfsGrid(r, c + 1, grid, visited); // RightdfsGrid(r, c - 1, grid, visited); // Left
}
C++
voiddfsGrid(intr, intc, const vector<vector<int>>&grid, vector<vector<bool>>&visited) {
if (r < 0 || c < 0 || r >= (int)grid.size() || c >= (int)grid[0].size() || visited[r][c]) {
return;
}
visited[r][c] = true;
dfsGrid(r + 1, c, grid, visited); // DowndfsGrid(r - 1, c, grid, visited); // UpdfsGrid(r, c + 1, grid, visited); // RightdfsGrid(r, c - 1, grid, visited); // Left
}
Problem
Pattern
Time Complexity
Factorial
Linear Recursion
O(n)
Fibonacci (naïve)
Tree Recursion
O(2ⁿ)
Binary Search
Divide & Conquer
O(log n)
Merge Sort
Divide & Conquer
O(n log n)
Tree Traversals
Data Structure DFS
O(n)
Permutations (Backtracking)
State Space Search
O(n!)
Subset Generation
Include / Exclude Branching
O(2ⁿ)
Flood Fill / Grid Search
Multi-Directional Matrix DFS
O(V + E)
06
Advantages & Limitations
Recursion is a powerful tool, but knowing when not to use it is equally important.
Advantages
Elegance & Readability — Recursive solutions often mirror
the mathematical definitions or logic paths directly, significantly reducing
code bloat.
Natural Data Structure Mapping — Problems involving
hierarchical architectures like trees, graphs, and nested folder structures are
inherently recursive and much simpler to process without manual stack tracking.
Divide and Conquer Friendly — Algorithms like Merge Sort, Quick
Sort, and Binary Search split problems into independent tracking boundaries
naturally, making them cleaner to implement recursively.
State Immutability — Instead of updating global or outer scope
variables through loops, each recursive call creates its own local scope
parameters, minimizing state contamination risks.
Limitations
Stack Overflow Risks — Deep recursion levels can exhaust the
program's call stack boundaries, causing instant crashes if base conditions
aren't perfectly met or if the input scales excessively.
Memory Footprint Overhead — Unlike iterative loops which run in
$O(1)$ space, each recursive layer pushes a new stack frame onto memory,
resulting in $O(N)$ auxiliary space scaling.
Performance Call Overhead — Activating functions introduces
context-switching delays behind the scenes (pushing registers, maintaining
pointers), making it slower than basic loop executions.
Complex Debugging Pipelines — Tracing bugs across dozens of
identical, nested stack frames makes reading logs and mapping variables
considerably harder during diagnostic tests.
07
Practice Exercises
Put your recursive thinking to the test. Try each problem before revealing the solution.
Exercise 1: Sum of Array
Given an array of integers, write a recursive function sum_array(arr) that returns the
sum of all elements in the array. Do not use loops or built-in summation functions.
Python
defsum_array(arr):
if notarr: return0returnarr[0] + sum_array(arr[1:])
Java
public static intsumArray(int[] arr, intindex) {
if (index == arr.length) return0;
returnarr[index] + sumArray(arr, index + 1);
}
C++
intsumArray(intarr[], intn) {
if (n <= 0) return0;
returnsumArray(arr, n - 1) + arr[n - 1];
}
Exercise 2: Reverse a String
Given a string s, write a recursive function that returns the reverse of the string
without using iterative constructs or built-in reverse methods.
public static booleanisPalindrome(Strings) {
if (s.length() <= 1) returntrue;
if (s.charAt(0) != s.charAt(s.length() - 1)) returnfalse;
returnisPalindrome(s.substring(1, s.length() - 1));
}
C++
boolisPalindrome(strings) {
if (s.length() <= 1) returntrue;
if (s[0] != s[s.length() - 1]) returnfalse;
returnisPalindrome(s.substr(1, s.length() - 2));
}
Exercise 5: Binary Search
Given a sorted array and a target value, implement a recursive binary search function
that returns the index of the target if found, otherwise returns -1.
public static voidgetPermutations(List<Integer>arr, List<Integer>path) {
if (arr.isEmpty()) {
System.out.println(path);
return;
}
for (inti = 0; i < arr.size(); i++) {
List<Integer>rem = new ArrayList<>(arr);
rem.remove(i);
List<Integer>nextPath = new ArrayList<>(path);
nextPath.add(arr.get(i));
getPermutations(rem, nextPath);
}
}
C++
voidpermutations(vector<int>arr, vector<int>path) {
if (arr.empty()) {
for (intx : path) std::cout << x << " ";
std::cout << std::endl;
return;
}
for (size_ti = 0; i < arr.size(); i++) {
vector<int>rem = arr;
rem.erase(rem.begin() + i);
vector<int>nextPath = path;
nextPath.push_back(arr[i]);
permutations(rem, nextPath);
}
}
Exercise 7: Flatten Nested List
Given a nested list containing integers and other lists, write a recursive function that
returns a single flattened list containing all elements in their original order.
Python
defflatten(lst):
result = []
foriteminlst:
ifisinstance(item, list): result.extend(flatten(item))
else]: result.append(item)returnresult
Java
public static List<Object>flatten(List<Object>nestedList) {
List<Object>result = new ArrayList<>();
for (Objectitem : nestedList) {
if (iteminstanceof List) {
result.addAll(flatten((List<Object>) item));
} else {
result.add(item);
}
}
returnresult;
}
Given n disks and three rods, write a recursive solution that prints the sequence of
moves required to transfer all disks from the source rod to the target
rod while following the Tower of Hanoi rules.
public static booleanisSorted(int[] arr, intn) {
if (n <= 1) returntruereturnarr[n - 1] >= arr[n - 2] && isSorted(arr, n - 1);
}
C++
boolisSorted(intarr[], intn) {
if (n <= 1) returntruereturnarr[n - 1] >= arr[n - 2] && isSorted(arr, n - 1);
}
Exercise 15: Subset Sum Accumulator Count
Given an array of positive integers and a target sum, write a recursive function that
counts the number of subsets whose elements add up exactly to the target sum.
Python
defcount_subsets(arr, n, sum_val):
ifsum_val == 0: return1ifn == 0: return0ifarr[n - 1] > sum_val:
returncount_subsets(arr, n - 1, sum_val)
returncount_subsets(arr, n - 1, sum_val - arr[n - 1]) + count_subsets(arr, n - 1, sum_val)
Java
public static intcountSubsets(int[] arr, intn, intsum) {
if (sum == 0) return1if (n == 0) return0if (arr[n - 1] > sum) {
returncountSubsets(arr, n - 1, sum);
}
returncountSubsets(arr, n - 1, sum - arr[n - 1]) + countSubsets(arr, n - 1, sum);
}
C++
intcountSubsets(intarr[], intn, intsum) {
if (sum == 0) return1if (n == 0) return0if (arr[n - 1] > sum) {
returncountSubsets(arr, n - 1, sum);
}
returncountSubsets(arr, n - 1, sum - arr[n - 1]) + countSubsets(arr, n - 1, sum);
}