Recursion Tutorial: Master Recursive Thinking & Divide-Conquer for Interviews

Complete recursion guide with base cases, divide-and-conquer patterns, and backtracking in Python. Learn recursive problem-solving, tail recursion, and when to use iteration for coding interviews

📅 Published: July 10, 2025 ✏️ Updated: August 25, 2025 By Ojaswi Athghara
#recursion #divide-conquer #tail-recursion #iteration #backtracking #dsa

Recursion Tutorial: Master Recursive Thinking & Divide-Conquer for Interviews

When Recursion Finally Clicked

I used to hate recursion. The call stack visualization made no sense. Why would you call a function from within itself? It felt like circular logic that should cause infinite loops.

Then someone told me: "Don't think about how—think about what." Don't trace every recursive call. Just trust that if your function works for smaller inputs, it works for larger ones.

That mental shift changed everything. Suddenly, problems that seemed impossibly complex became elegant one-liners.

In this guide, I'll share the mindset that made recursion intuitive for me. You'll learn to recognize recursive patterns, write base cases correctly, and transform complex problems into simple recursive solutions.

The Recursive Mindset

The key insight: Solve the problem for the smallest case, then assume you can solve it for smaller inputs. Combine those solutions for the current input.

The three-step framework:

  1. Base case: What's the simplest input? What should we return?
  2. Recursive case: Assume we can solve smaller problems. How do we combine them?
  3. Progress: Each recursive call must move toward the base case.

Example: Factorial

def factorial(n):
    """
    Calculate n! recursively.
    Trust that factorial(n-1) works!
    """
    # Base case: simplest input
    if n == 0 or n == 1:
        return 1

    # Recursive case: combine with smaller problem
    return n * factorial(n - 1)

Mental model:

  • factorial(5) = 5 * factorial(4)
  • Don't trace all the way down
  • Just trust that factorial(4) gives the right answer

Pattern 1: Simple Recursion

Sum of Array

def array_sum(arr):
    """Sum array recursively."""
    # Base case: empty array
    if not arr:
        return 0

    # Recursive case: first element + sum of rest
    return arr[0] + array_sum(arr[1:])

Reverse String

def reverse_string(s):
    """Reverse string recursively."""
    # Base case: empty or single character
    if len(s) <= 1:
        return s

    # Recursive case: last char + reverse of rest
    return s[-1] + reverse_string(s[:-1])

Pattern: Process one element, recurse on the rest.

Pattern 2: Divide and Conquer

Break problem into independent subproblems, solve each, combine results.

Merge Sort

def merge_sort(arr):
    """
    Sort array using divide and conquer.
    Divide, sort halves, merge.
    """
    # Base case: array of 0 or 1 element
    if len(arr) <= 1:
        return arr

    # Divide
    mid = len(arr) // 2
    left = merge_sort(arr[:mid])   # Solve left half
    right = merge_sort(arr[mid:])  # Solve right half

    # Conquer: merge sorted halves
    return merge(left, right)

def merge(left, right):
    """Merge two sorted arrays."""
    result = []
    i = j = 0

    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1

    result.extend(left[i:])
    result.extend(right[j:])
    return result

Pattern: Divide → Solve independently → Combine results

Quick Sort

def quick_sort(arr):
    """
    Sort using divide and conquer.
    Pick pivot, partition, recurse.
    """
    # Base case
    if len(arr) <= 1:
        return arr

    # Choose pivot
    pivot = arr[len(arr) // 2]

    # Partition
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]

    # Recurse and combine
    return quick_sort(left) + middle + quick_sort(right)

Pattern 3: Binary Search (Recursive)

def binary_search_recursive(arr, target, left=0, right=None):
    """
    Binary search implemented recursively.
    Divide search space in half each time.
    """
    if right is None:
        right = len(arr) - 1

    # Base case: search space exhausted
    if left > right:
        return -1

    mid = left + (right - left) // 2

    if arr[mid] == target:
        return mid
    elif arr[mid] < target:
        return binary_search_recursive(arr, target, mid + 1, right)
    else:
        return binary_search_recursive(arr, target, left, mid - 1)

Pattern: Eliminate half the search space each call.

Pattern 4: Tree Recursion

Trees are naturally recursive structures—perfect for recursion!

Tree Height

def max_depth(root):
    """
    Find height of binary tree.
    Height = 1 + max(left_height, right_height).
    """
    # Base case: empty tree
    if not root:
        return 0

    # Recursive case: trust that it works for children
    left_height = max_depth(root.left)
    right_height = max_depth(root.right)

    return 1 + max(left_height, right_height)

Tree Paths

def all_paths(root):
    """
    Find all root-to-leaf paths.
    Build path as we recurse.
    """
    def dfs(node, current_path, all_paths):
        if not node:
            return

        # Add current node
        current_path.append(node.val)

        # Leaf node: save path
        if not node.left and not node.right:
            all_paths.append(current_path[:])
        else:
            # Recurse on children
            dfs(node.left, current_path, all_paths)
            dfs(node.right, current_path, all_paths)

        # Backtrack
        current_path.pop()

    result = []
    dfs(root, [], result)
    return result

Pattern 5: Backtracking with Recursion

Explore all possibilities by making choices, recursing, and undoing choices.

Generate Permutations

def permutations(nums):
    """
    Generate all permutations recursively.
    Choose → Recurse → Unchoose.
    """
    result = []

    def backtrack(current, remaining):
        # Base case: used all numbers
        if not remaining:
            result.append(current[:])
            return

        # Try each remaining number
        for i in range(len(remaining)):
            # Choose
            current.append(remaining[i])
            # Recurse with one fewer option
            backtrack(current, remaining[:i] + remaining[i+1:])
            # Unchoose
            current.pop()

    backtrack([], nums)
    return result

Generate Subsets

def subsets(nums):
    """
    Generate all subsets recursively.
    For each element: include it or don't.
    """
    result = []

    def backtrack(index, current):
        # Base case: processed all elements
        if index == len(nums):
            result.append(current[:])
            return

        # Don't include current element
        backtrack(index + 1, current)

        # Include current element
        current.append(nums[index])
        backtrack(index + 1, current)
        current.pop()

    backtrack(0, [])
    return result

Pattern 6: Multiple Recursions (Fibonacci-style)

Fibonacci

def fibonacci(n):
    """
    Calculate nth Fibonacci number.
    Two recursive calls (exponential without memoization!).
    """
    # Base cases
    if n <= 1:
        return n

    # Recursive case: sum of two previous
    return fibonacci(n - 1) + fibonacci(n - 2)

# With memoization for efficiency
def fibonacci_memo(n, memo={}):
    """Fibonacci with memoization (top-down DP)."""
    if n in memo:
        return memo[n]

    if n <= 1:
        return n

    memo[n] = fibonacci_memo(n - 1, memo) + fibonacci_memo(n - 2, memo)
    return memo[n]

Climbing Stairs

LeetCode #70 - Climbing Stairs

def climb_stairs(n):
    """
    Ways to climb n stairs (1 or 2 steps at a time).
    Same structure as Fibonacci!
    """
    if n <= 2:
        return n

    return climb_stairs(n - 1) + climb_stairs(n - 2)

Pattern: Two recursive calls that combine results.

Pattern 7: Tail Recursion

Recursion where recursive call is the last operation (can be optimized to iteration).

Factorial (Tail Recursive)

def factorial_tail(n, accumulator=1):
    """
    Tail-recursive factorial.
    No operations after recursive call.
    """
    if n == 0:
        return accumulator

    return factorial_tail(n - 1, n * accumulator)

Benefit: Some compilers optimize tail recursion to avoid stack overflow.

Sum (Tail Recursive)

def sum_tail(arr, accumulator=0):
    """Tail-recursive sum."""
    if not arr:
        return accumulator

    return sum_tail(arr[1:], accumulator + arr[0])

Common Recursion Patterns

1. Process and Recurse

def process_and_recurse(data):
    if not data:
        return base_value

    # Process first element
    result = process(data[0])

    # Recurse on rest
    return combine(result, process_and_recurse(data[1:]))

2. Divide and Conquer

def divide_and_conquer(data):
    if len(data) <= 1:
        return data

    mid = len(data) // 2
    left = divide_and_conquer(data[:mid])
    right = divide_and_conquer(data[mid:])

    return merge(left, right)

3. Multiple Paths

def multiple_paths(n):
    if n <= base:
        return base_value

    path1 = multiple_paths(n - 1)
    path2 = multiple_paths(n - 2)

    return combine(path1, path2)

Recursion vs Iteration

When to use recursion:

  • Problem naturally recursive (trees, graphs)
  • Divide and conquer
  • Backtracking
  • Makes code cleaner and more readable

When to use iteration:

  • Simple loops
  • Stack overflow concerns (very deep recursion)
  • Performance critical (recursion has overhead)

Converting recursion to iteration:

# Recursive
def factorial_recursive(n):
    if n <= 1:
        return 1
    return n * factorial_recursive(n - 1)

# Iterative
def factorial_iterative(n):
    result = 1
    for i in range(2, n + 1):
        result *= i
    return result

Common Recursion Mistakes

Mistake 1: Missing Base Case

# WRONG: No base case → infinite recursion!
def countdown(n):
    print(n)
    countdown(n - 1)  # Never stops!

# RIGHT: Base case stops recursion
def countdown(n):
    if n <= 0:
        return
    print(n)
    countdown(n - 1)

Mistake 2: Not Making Progress

# WRONG: Not moving toward base case
def buggy(n):
    if n == 0:
        return
    buggy(n)  # Infinite recursion!

# RIGHT: Each call moves toward base case
def fixed(n):
    if n == 0:
        return
    fixed(n - 1)  # Progress!

Mistake 3: Modifying and Using Same Variable

# WRONG: Modifying list being recursed on
def buggy_sum(arr):
    if not arr:
        return 0
    first = arr.pop(0)  # Modifies arr!
    return first + buggy_sum(arr)

# RIGHT: Don't modify, slice instead
def correct_sum(arr):
    if not arr:
        return 0
    return arr[0] + correct_sum(arr[1:])

Mistake 4: Stack Overflow

# For very large n, this crashes
def factorial(n):
    if n <= 1:
        return 1
    return n * factorial(n - 1)

# Solution: Use iteration or increase recursion limit
import sys
sys.setrecursionlimit(10000)  # Use cautiously!

Recursion Complexity Analysis

Time complexity:

  • Count recursive calls × work per call
  • Tree structure: O(branches^depth)
  • Linear recursion: O(n)

Space complexity:

  • Recursion depth (call stack)
  • O(n) for linear, O(log n) for binary

Examples:

  • factorial(n): Time O(n), Space O(n)
  • fibonacci(n) (naive): Time O(2^n), Space O(n)
  • fibonacci(n) (memo): Time O(n), Space O(n)
  • merge_sort(n): Time O(n log n), Space O(n)

The Recursion Decision Tree

When you see a problem:

1. Can it be broken into smaller identical problems?

  • YES → Consider recursion

2. Is there a natural recursive structure? (tree, graph)

  • YES → Recursion likely best

3. Do you need to explore all possibilities?

  • YES → Backtracking with recursion

4. Is it divide and conquer?

  • YES → Recursive divide

5. Will recursion depth be very large? (> 1000)

  • YES → Use iteration or memoization

My Practice Strategy

Week 1: Simple Recursion

  • Factorial, sum, reverse
  • Master base cases
  • Practice tracing (but don't rely on it!)
  • 20-30 simple problems

Week 2: Tree Recursion

  • Binary tree problems
  • Height, paths, properties
  • 15-20 tree problems

Week 3: Divide and Conquer

  • Merge sort, quick sort
  • Binary search recursive
  • 15-20 problems

Week 4: Backtracking

  • Permutations, combinations, subsets
  • N-Queens, Sudoku
  • 20-30 problems

Real Interview Success

I was asked: "Print all valid combinations of n pairs of parentheses."

My approach:

  1. "Generate all valid combinations" → Backtracking
  2. At each step: add '(' or ')'
  3. Constraints: can't have more ')' than '(', can't exceed n

Coded recursively in 10 minutes:

def generate_parentheses(n):
    result = []

    def backtrack(current, open_count, close_count):
        if len(current) == 2 * n:
            result.append(current)
            return

        if open_count < n:
            backtrack(current + '(', open_count + 1, close_count)

        if close_count < open_count:
            backtrack(current + ')', open_count, close_count + 1)

    backtrack('', 0, 0)
    return result

The recursive solution was elegant and clear!

Conclusion: Your Recursion Mastery Roadmap

Recursion isn't magic—it's trusting that smaller problems are solved:

  1. Think "what," not "how" - Don't trace every call
  2. Master base cases - Where recursion stops
  3. Ensure progress - Each call moves toward base case
  4. Practice patterns - Simple, divide-and-conquer, backtracking

Start with simple problems (factorial, sum). Build confidence. Move to trees (natural recursion). Finally, tackle backtracking.

Within 4 weeks, recursive thinking becomes natural. You'll see elegant recursive solutions where others see complexity.

Remember: every recursive expert once struggled to understand the call stack. The difference is they learned to trust the recursion!

Happy coding, and may your base cases always be reached!


If you found this guide helpful, share it with others learning recursion. Recursive thinking is a superpower once it clicks. If this guide helped you master recursion or understand divide-and-conquer dsa, I'd love to hear about it! Connect with me on Twitter or LinkedIn.

Support My Work

If this guide helped you finally understand recursion, master recursive thinking, or ace recursion interview questions, I'd really appreciate your support! Creating comprehensive, free content like this takes significant time and effort. Your support helps me continue sharing knowledge and creating more helpful resources for students like you.

☕ Buy me a coffee - Every contribution, big or small, means the world to me and keeps me motivated to create more content!


Cover image by Marcus Urbenz on Unsplash

Related Blogs

Ojaswi Athghara

SDE, 4+ Years

© ojaswiat.com 2025-2027