Algo Infinity Verse

Think Recursively, Solve Efficiently Master Recursion Learn Base Cases, Recursive Functions, Call Stacks & Problem Solving Patterns

|
0 Topics
0 Examples
0 Exercises

🔁 Learn Recursion

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.

Python
def countdown(n):
    if n <= 0:          # base case
        print('Go!')
    else:
        print(n)
        countdown(n - 1)  # recursive case

countdown(3)
Java
public static void countdown(int n) {
    if (n <= 0) {       // base case
        System.out.println("Go!");
    } else {
        System.out.println(n);
        countdown(n - 1); // recursive case
    }
}
C++
void countdown(int n) {
    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.

Classic example: Factorialn! = n × (n-1)!

Python
def factorial(n):
    if n <= 1:
        return 1
    return n * factorial(n - 1)
Java
public static int factorial(int n) {
    if (n <= 1) {
        return 1;
    }
    return n * factorial(n - 1);
}
C++
int factorial(int n) {
    if (n <= 1) {
        return 1;
    }
    return n * factorial(n - 1);
}
Execution Trace
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.
factorial(1) returns 1 ← base case factorial(2) waiting for factorial(1) factorial(3) waiting for factorial(2) main() called factorial(3) TOP BOTTOM
⚠️

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.

Example: Fibonaccifib(n) = fib(n-1) + fib(n-2)

Python
def fib(n):
            if n <= 1:
                return n
            return fib(n-1) + fib(n-2)
Java
public static int fib(int n) {
            if (n <= 1) {
                return n;
            }
            return fib(n - 1) + fib(n - 2);
        }
C++
int fib(int n) {
            if (n <= 1) {
                return n;
            }
            return fib(n - 1) + fib(n - 2);
        }
fib(4) fib(3) fib(2) fib(2) fib(1)=1 fib(1)=1 fib(0)=0 fib(1)=1 fib(0)=0 Recursive call Base case
05

Common Recursion Problems

Recursion shines in certain problem categories. Here are the most important ones you'll encounter:

1. Factorial & Power

Direct mathematical calculation pipelines processing linear reduction transitions towards base evaluation limits.

Python
def factorial(n):
    if n <= 1: return 1
    return n * factorial(n - 1)
Java
public static int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}
C++
int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

2. Binary Search

Divide tracking spaces in half cleanly down to O(log n) execution thresholds.

Python
def binary_search(arr, target, low, high):
    if low > high: return -1
    mid = (low + high) // 2
    if arr[mid] == target: return mid
    if arr[mid] > target: return binary_search(arr, target, low, mid - 1)
    return binary_search(arr, target, mid + 1, high)
Java
public static int binarySearch(int[] arr, int target, int low, int high) {
    if (low > high) return -1;
    int mid = low + (high - low) / 2;
    if (arr[mid] == target) return mid;
    if (arr[mid] > target) return binarySearch(arr, target, low, mid - 1);
    return binarySearch(arr, target, mid + 1, high);
}
C++
int binarySearch(int arr[], int target, int low, int high) {
    if (low > high) return -1;
    int mid = low + (high - low) / 2;
    if (arr[mid] == target) return mid;
    if (arr[mid] > target) return binarySearch(arr, target, low, mid - 1);
    return binarySearch(arr, target, mid + 1, high);
}

3. Merge Sort & Quick Sort

Divide-and-conquer processing architectures that break an unsorted collection down into single individual items before assembling sorted frameworks sequentially.

Python
def merge_sort(arr):
    if len(arr) <= 1: return arr
    mid = len(arr) // 2
    left = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])
    return merge(left, right)
Java
public static void mergeSort(int[] arr, int l, int r) {
    if (l < r) {
        int m = l + (r - l) / 2;
        mergeSort(arr, l, m);
        mergeSort(arr, m + 1, r);
        merge(arr, l, m, r);
    }
}
C++
void mergeSort(int arr[], int l, int r) {
    if (l < r) {
        int m = 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.

Python
def inorder(root):
    if root:
        inorder(root.left)
        print(root.val)
        inorder(root.right)
Java
public static void inorder(Node root) {
    if (root != null) {
        inorder(root.left);
        System.out.println(root.val);
        inorder(root.right);
    }
}
C++
void inorder(Node* root) {
    if (root != nullptr) {
        inorder(root->left);
        std::cout << root->val << std::endl;
        inorder(root->right);
    }
}

5. Backtracking (Permutations)

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.

Python
def backtrack(choices, path):
    if not choices:
        print(path)
        return
    for i in range(len(choices)):
        backtrack(choices[:i] + choices[i+1:], path + [choices[i]])
Java
public static void backtrack(List<Integer> choices, List<Integer> path) {
    if (choices.isEmpty()) {
        System.out.println(path);
        return;
    }
    for (int i = 0; i < choices.size(); i++) {
        List<Integer> remaining = new ArrayList<>(choices);
        remaining.remove(i);
        List<Integer> nextPath = new ArrayList<>(path);
        nextPath.add(choices.get(i));
        backtrack(remaining, nextPath);
    }
}
C++
void backtrack(vector<int> choices, vector<int> path) {
    if (choices.empty()) {
        for(int x : path) std::cout << x << " ";
        std::cout << std::endl;
        return;
    }
    for (size_t i = 0; i < choices.size(); i++) {
        vector<int> remaining = choices;
        remaining.erase(remaining.begin() + i);
        vector<int> nextPath = path;
        nextPath.push_back(choices[i]);
        backtrack(remaining, nextPath);
    }
}

6. Subset & Combinatorial Generation

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.

Python
def subsets(arr, index, current_path):
    if index == len(arr):
        print(current_path)
        return
    subsets(arr, index + 1, current_path + [arr[index]]) # Include
    subsets(arr, index + 1, current_path)                  # Exclude
Java
public static void subsets(int[] arr, int index, List<Integer> currentPath) {
    if (index == arr.length) {
        System.out.println(currentPath);
        return;
    }
    currentPath.add(arr[index]);
    subsets(arr, index + 1, currentPath);  // Include
    currentPath.remove(currentPath.size() - 1);
    subsets(arr, index + 1, currentPath);  // Exclude
}
C++
void subsets(const vector<int>& arr, size_t index, vector<int> currentPath) {
    if (index == arr.size()) {
        for(int x : currentPath) std::cout << x << " ";
        std::cout << std::endl;
        return;
    }
    currentPath.push_back(arr[index]);
    subsets(arr, index + 1, currentPath);  // Include
    currentPath.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.

Python
def dfs_grid(r, c, grid, visited):
    if (r < 0 or c < 0 or r >= len(grid) or c >= len(grid[0]) or visited[r][c]):
        return
    visited[r][c] = True
    dfs_grid(r + 1, c, grid, visited) # Down
    dfs_grid(r - 1, c, grid, visited) # Up
    dfs_grid(r, c + 1, grid, visited) # Right
    dfs_grid(r, c - 1, grid, visited) # Left
Java
public static void dfsGrid(int r, int c, 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); // Down
    dfsGrid(r - 1, c, grid, visited); // Up
    dfsGrid(r, c + 1, grid, visited); // Right
    dfsGrid(r, c - 1, grid, visited); // Left
}
C++
void dfsGrid(int r, int c, 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); // Down
    dfsGrid(r - 1, c, grid, visited); // Up
    dfsGrid(r, c + 1, grid, visited); // Right
    dfsGrid(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
def sum_array(arr):
    if not arr: return 0
    return arr[0] + sum_array(arr[1:])
Java
public static int sumArray(int[] arr, int index) {
    if (index == arr.length) return 0;
    return arr[index] + sumArray(arr, index + 1);
}
C++
int sumArray(int arr[], int n) {
    if (n <= 0) return 0;
    return sumArray(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.

Python
def reverse(s):
    if len(s) <= 1: return s
    return reverse(s[1:]) + s[0]
Java
public static String reverse(String s) {
    if (s.isEmpty()) return s;
    return reverse(s.substring(1)) + s.charAt(0);
}
C++
string reverse(string s) {
    if (s.length() <= 1) return s;
    return reverse(s.substr(1)) + s[0];
}
Exercise 3: Power Function

Write a recursive function power(base, exp) that computes and returns base raised to the power exp, where exp is a non-negative integer.

Python
def power(base, exp):
    if exp == 0: return 1
    return base * power(base, exp - 1)
Java
public static int power(int base, int exp) {
    if (exp == 0) return 1;
    return base * power(base, exp - 1);
}
C++
int power(int base, int exp) {
    if (exp == 0) return 1;
    return base * power(base, exp - 1);
}
Exercise 4: Palindrome Check

Given a string s, determine recursively whether it is a palindrome. A palindrome reads the same forwards and backwards.

Python
def is_palindrome(s):
    if len(s) <= 1: return True
    if s[0] != s[-1]: return False
    return is_palindrome(s[1:-1])
Java
public static boolean isPalindrome(String s) {
    if (s.length() <= 1) return true;
    if (s.charAt(0) != s.charAt(s.length() - 1)) return false;
    return isPalindrome(s.substring(1, s.length() - 1));
}
C++
bool isPalindrome(string s) {
    if (s.length() <= 1) return true;
    if (s[0] != s[s.length() - 1]) return false;
    return isPalindrome(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.

Python
def binary_search(arr, target, lo, hi):
    if lo > hi: return -1
    mid = (lo + hi) // 2
    if arr[mid] == target: return mid
    elif arr[mid] < target: return binary_search(arr, target, mid + 1, hi)
    else]: return binary_search(arr, target, lo, mid - 1)
Java
public static int binarySearch(int[] arr, int target, int lo, int hi) {
    if (lo > hi) return -1;
    int mid = lo + (hi - lo) / 2;
    if (arr[mid] == target) return mid;
    if (arr[mid] < target) return binarySearch(arr, target, mid + 1, hi);
    return binarySearch(arr, target, lo, mid - 1);
}
C++
int binarySearch(int arr[], int target, int lo, int hi) {
    if (lo > hi) return -1;
    int mid = lo + (hi - lo) / 2;
    if (arr[mid] == target) return mid;
    if (arr[mid] < target) return binarySearch(arr, target, mid + 1, hi);
    return binarySearch(arr, target, lo, mid - 1);
}
Exercise 6: Permutations via Backtracking

Given a list of distinct integers, generate all possible permutations of the list using recursion and backtracking.

Python
def permutations(arr, path=[]):
    if not arr:
        print(path)
        return
    for i in range(len(arr)):
        permutations(arr[:i] + arr[i+1:], path + [arr[i]])
Java
public static void getPermutations(List<Integer> arr, List<Integer> path) {
    if (arr.isEmpty()) {
        System.out.println(path);
        return;
    }
    for (int i = 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++
void permutations(vector<int> arr, vector<int> path) {
    if (arr.empty()) {
        for (int x : path) std::cout << x << " ";
        std::cout << std::endl;
        return;
    }
    for (size_t i = 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
def flatten(lst):
    result = []
    for item in lst:
        if isinstance(item, list): result.extend(flatten(item))
        else]: result.append(item)
    return result
Java
public static List<Object> flatten(List<Object> nestedList) {
    List<Object> result = new ArrayList<>();
    for (Object item : nestedList) {
        if (item instanceof List) {
            result.addAll(flatten((List<Object>) item));
        } else {
            result.add(item);
        }
    }
    return result;
}
C++
// Simple structured node tracking representation inside C++ ecosystem
struct Node {
    bool isList;
    int value;
    vector<Node> list;
};

void flatten(const vector<Node>& nodes, vector<int>& result) {
    for (const auto& node : nodes) {
        if (node.isList) flatten(node.list, result);
        else result.push_back(node.value);
    }
}
Exercise 8: Count Occurrences

Given an array and a target value, write a recursive function that counts how many times the target appears in the array.

Python
def count(lst, target):
    if not lst: return 0
    match = 1 if lst[0] == target else 0
    return match + count(lst[1:], target)
Java
public static int count(int[] arr, int index, int target) {
    if (index == arr.length) return 0;
    int match = (arr[index] == target) ? 1 : 0;
    return match + count(arr, index + 1, target);
}
C++
int count(int arr[], int n, int target) {
    if (n <= 0) return 0;
    int match = (arr[n - 1] == target) ? 1 : 0;
    return match + count(arr, n - 1, target);
}
Exercise 9: Tower of Hanoi

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.

Python
def hanoi(n, source='A', target='C', helper='B'):
    if n == 1:
        print(f"Move disk 1: {source} -> {target}")
        return
    hanoi(n - 1, source, helper, target)
    print(f"Move disk {n}: {source} -> {target}")
    hanoi(n - 1, helper, target, source)
Java
public static void hanoi(int n, char source, char target, char helper) {
    if (n == 1) {
        System.out.println("Move disk 1: " + source + " -> " + target);
        return;
    }
    hanoi(n - 1, source, helper, target);
    System.out.println("Move disk " + n + ": " + source + " -> " + target);
    hanoi(n - 1, helper, target, source);
}
C++
void hanoi(int n, char source, char target, char helper) {
    if (n == 1) {
        std::cout << "Move disk 1: " << source << " -> " << target << std::endl;
        return;
    }
    hanoi(n - 1, source, helper, target);
    std::cout << "Move disk " << n << ": " << source << " -> " << target << std::endl;
    hanoi(n - 1, helper, target, source);
}
Exercise 10: Greatest Common Divisor (GCD)

Implement Euclid's Algorithm recursively to find the Greatest Common Divisor (GCD) of two positive integers.

Python
def gcd(a, b):
    if b == 0: return a
    return gcd(b, a % b)
Java
public static int gcd(int a, int b) {
    if (b == 0) return a;
    return gcd(b, a % b);
}
C++
int gcd(int a, int b) {
    if (b == 0) return a;
    return gcd(b, a % b);
}
Exercise 11: String Permutations

Given a string containing unique characters, generate all possible permutations of the string using recursion.

Python
def string_permutations(s, ans=""):
    if len(s) == 0:
        print(ans)
        return
    for i in range(len(s)):
        ch = s[i]
        left_substr = s[0:i] + s[i+1:]
        string_permutations(left_substr, ans + ch)
Java
public static void printPermutations(String str, String ans) {
    if (str.length() == 0) {
        System.out.print(ans + " ");
        return
    }
    for (int i = 0; i < str.length(); i++) {
        char ch = str.charAt(i);
        String ros = str.substring(0, i) + str.substring(i + 1);
        printPermutations(ros, ans + ch);
    }
}
C++
void printPermutations(string str, string ans = "") {
    if (str.length() == 0) {
        std::cout << ans << " ";
        return
    }
    for (size_t i = 0; i < str.length(); i++) {
        char ch = str[i];
        string ros = str.substr(0, i) + str.substr(i + 1);
        printPermutations(ros, ans + ch);
    }
}
Exercise 12: Fast Power (Binary Exponentiation)

Write a recursive function that computes base^exp using the Binary Exponentiation technique, achieving logarithmic time complexity.

Python
def fast_power(base, exp):
    if exp == 0: return 1
    half = fast_power(base, exp // 2)
    if exp % 2 == 0: return half * half
    return base * half * half
Java
public static long fastPower(long base, long exp) {
    if (exp == 0) return 1;
    long half = fastPower(base, exp / 2);
    if (exp % 2 == 0) return half * half
    return base * half * half;
}
C++
long long fastPower(long long base, long long exp) {
    if (exp == 0) return 1
    long long half = fastPower(base, exp / 2);
    if (exp % 2 == 0) return half * half
    return base * half * half;
}
Exercise 13: Reverse Array In-Place

Given an array, reverse its elements recursively without creating an additional array. The reversal should be performed in-place

Python
def reverse_array(arr, left, right):
    if left >= right: return
    arr[left], arr[right] = arr[right], arr[left]
    reverse_array(arr, left + 1, right - 1)
Java
public static void reverseArray(int[] arr, int left, int right) {
    if (left >= right) return
    int temp = arr[left];
    arr[left] = arr[right];
    arr[right] = temp
    reverseArray(arr, left + 1, right - 1);
}
C++
void reverseArray(int arr[], int left, int right) {
    if (left >= right) return
    swap(arr[left], arr[right]);
    reverseArray(arr, left + 1, right - 1);
}
Exercise 14: Check if Array is Sorted

Given an array of integers, determine recursively whether the array is sorted in non-decreasing order.

Python
def is_sorted(arr, n):
    if n <= 1: return True
    return arr[n - 1] >= arr[n - 2] and is_sorted(arr, n - 1)
Java
public static boolean isSorted(int[] arr, int n) {
    if (n <= 1) return true
    return arr[n - 1] >= arr[n - 2] && isSorted(arr, n - 1);
}
C++
bool isSorted(int arr[], int n) {
    if (n <= 1) return true
    return arr[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
def count_subsets(arr, n, sum_val):
    if sum_val == 0: return 1
    if n == 0: return 0
    if arr[n - 1] > sum_val:
        return count_subsets(arr, n - 1, sum_val)
    return count_subsets(arr, n - 1, sum_val - arr[n - 1]) + count_subsets(arr, n - 1, sum_val)
Java
public static int countSubsets(int[] arr, int n, int sum) {
    if (sum == 0) return 1
    if (n == 0) return 0
    if (arr[n - 1] > sum) {
        return countSubsets(arr, n - 1, sum);
    }
    return countSubsets(arr, n - 1, sum - arr[n - 1]) + countSubsets(arr, n - 1, sum);
}
C++
int countSubsets(int arr[], int n, int sum) {
    if (sum == 0) return 1
    if (n == 0) return 0
    if (arr[n - 1] > sum) {
        return countSubsets(arr, n - 1, sum);
    }
    return countSubsets(arr, n - 1, sum - arr[n - 1]) + countSubsets(arr, n - 1, sum);
}