Chapter 05

Recursion and Divide & Conquer

This chapter begins with functions, then introduces recursive functions and their essential base and recursive cases. It then describes recursive running time using recurrence relations, solves those recurrences by substitution, recursion trees, and the Master Theorem, and applies the ideas to binary search and divide and conquer.

5.1Functions

Function A function is a reusable block of code that performs a specific task. It may take input, process it, and return an output.

Functions improve reusability, modularity, readability, and maintainability.

def function(arguments):
    steps
    return result

5.2Recursive Functions

Recursive function A recursive function is a function that calls itself.

Each recursive call works on a smaller instance of the same problem. Correct recursion requires both a stopping condition and progress toward that condition.

Base case

The condition that terminates the recursion.

Recursive case

Reduces the current problem to one or more smaller instances of the same problem.

Common uses

Factorial computation, Fibonacci sequences, tree traversals, and binary search.

def recursive_function(parameters):
    if base_case_condition:
        return result
    else:
        return recursive_function(smaller_problem)
Two questions to ask Does every call move closer to the base case? Is the base case correct for the smallest valid input?

5.3Recursive Factorial

Factorial is defined for nonnegative integers. The base value is 0! = 1; for n ≥ 1, n! = n × (n − 1) × ··· × 1. Therefore its recursive definition is n! = n × (n − 1)!.

def fact(n):
    if n <= 1:
        return 1
    return n * fact(n - 1)

How recursive calls use memory

Every active function call has a stack frame containing its local data and return information. A recursive call pushes a new frame; completing the base case begins the top-to-bottom sequence of returns.

Process virtual address space. Program code, read-only data, and global/static data occupy the lower regions. The heap grows upward, while the call stack grows downward and stores active function frames.
Call stack for fact(5). fact(5) is the oldest active frame at the bottom. The base-case frame fact(1) is on top and returns first.
Exact expression tree for fact(5) fact(n) = n × fact(n−1)

Ready to expand fact(5).

fact(5)T(5); for n, T(n) fact(4)T(n−1) + c fact(3)T(n−2) + c + c fact(1)T(1) + ··· + c + c + c

Once the base case returns 1, the calls unwind: 2 × 1, 3 × 2, 4 × 6, and finally 5 × 24 = 120.

Trace the call stack
Choose a small value and step through the calls and returns.

Running time

Each call performs constant work and reduces n by one. The recurrence is:

T(n) = T(n − 1) + c, with T(1) = c

There are n calls, so the running time is Θ(n).

5.4Recurrence Relations

Recurrence A recurrence expresses the running time of a recursive algorithm in terms of the running time on smaller inputs.
T(n) = 1, if n ≤ 1;   T(n) = T(n − 1) + 1, if n > 1

The four methods introduced in the lecture are:

  1. Substitution: repeatedly expand the recurrence until a pattern appears.
  2. Recursion tree: show the work at every level and add the level costs.
  3. Master Theorem: compare the recursive work with the non-recursive work.
  4. Akra–Bazzi: handle more general divide-and-conquer recurrences.
Before solving Identify the base case, the number and size of recursive calls, and the work done outside those calls.

5.5Substitution Method

Start with the factorial recurrence and expand it one step at a time:

T(n) = T(n - 1) + c
     = T(n - 2) + 2c
     = T(n - 3) + 3c
     ...
     = T(n - k) + kc

The expansion reaches the base case when n − k = 1, so k = n − 1. Therefore:

T(n) = T(1) + (n − 1)c = Θ(n)

Exercises

1. T(n) = T(n − 1) + n
show answer

Expanding gives n + (n−1) + ··· + 1, the sum of the first n integers. Therefore Θ(n²).

2. T(n) = T(n − 1) + log n
show answer

The expansion is log n + log(n−1) + ··· + log 2. This is log(n!), which is Θ(n log n).

5.6Recursion-Tree Method

A recursion tree turns a recurrence into levels. Each node represents one subproblem; the value written at the node is the work done outside its recursive calls.

T(n) = 2T(n/2) + cn, with T(1) = c
Substitution-method solution

Assume n is a power of 2. Repeatedly substitute the recurrence into itself:

T(n) = 2T(n/2) + cn
     = 2[2T(n/4) + c(n/2)] + cn
     = 4T(n/4) + 2cn
     = 8T(n/8) + 3cn
     ⋮
     = 2^k T(n/2^k) + kcn

The expansion reaches the base case when n/2k = 1. Therefore k = log2n and 2k = n.

T(n) = nT(1) + cn log₂n = cn + cn log₂n = Θ(n log n)

The same asymptotic result holds when n is not an exact power of 2; floors and ceilings change only constant factors.

Exact binary recursion tree T(n) = 2T(n/2) + c₂n
level 0c₂n level 1c₂n level 2c₂n heightlog₂n leavesc₁n

Total: c₂n log₂n + c₁n = Θ(n log n)

Level
Subproblems
Total level cost
0
1 × n
cn
1
2 × n/2
cn
2
4 × n/4
cn
log₂ n
n leaves × 1
cn

Every level costs cn, and the tree has log₂ n + 1 levels. Hence the total is Θ(n log n).

Three-way recurrence tree

Substitution-method solution

Write the non-recursive work as cn², use T(1)=Θ(1), and assume n is a power of 4. Repeated substitution gives:

T(n) = 3T(n/4) + cn²
     = 3[3T(n/16) + c(n/4)²] + cn²
     = 9T(n/16) + cn²(1 + 3/16)
     = 27T(n/64) + cn²(1 + 3/16 + (3/16)²)
     ⋮
     = 3^k T(n/4^k) + cn² Σ(i=0 to k−1)(3/16)^i

The base case is reached when n/4k = 1, so k = log4n and 3k = nlog43. The geometric sum has ratio 3/16 < 1 and is therefore bounded by a constant.

T(n) = Θ(nlog43) + Θ(n²) = Θ(n²)

Since log43 < 2, the non-recursive work dominates the leaf cost.

Exact three-way tree T(n) = 3T(n/4) + Θ(n²)
level 0cn² level 1(3/16)cn² level 2(3/16)²cn² heightlog₄n leavesΘ(nlog₄3)

Total: cn² Σ(3/16)i + Θ(nlog₄3) = Θ(n²)

Uneven split recurrence tree

Exact n/3–2n/3 tree T(n) = T(n/3) + T(2n/3) + Θ(n)
level 0cn level 1cn level 2cn longest pathlog3/2n leaf costΘ(n)

Each level costs cn; the total is O(n log n).

Tree patterns and results

RecurrenceWhat the tree showsResult
T(n) = 3T(n/4) + Θ(n²) Level costs form the geometric series n²(1 + 3/16 + (3/16)² + ···). Θ(n²)
T(n) = T(n/3) + T(2n/3) + Θ(n) The total subproblem size remains n per level, for logarithmically many levels. Θ(n log n)
T(n) = T(n−1) + T(n−2) + Θ(1) The Fibonacci recursion tree grows exponentially because many subproblems are repeated. Θ(φⁿ)
and therefore O(2ⁿ)

5.7Master Theorem

The Master Theorem applies to recurrences of the form:

T(n) = aT(n/b) + f(n), where a ≥ 1 and b > 1
  • a is the number of recursive subproblems.
  • n/b is the size of each subproblem.
  • f(n) is the divide/combine work.
  • nlogba represents the recursive tree’s leaf growth.
Exact general recurrence tree T(n) = aT(n/b) + f(n)
level 0f(n) level 1 · a nodesaf(n/b) level 2 · a² nodesa²f(n/b²) heightlogbn number of leavesnlogba leaf costΘ(nlogba)

Total: Θ(nlogba) + Σj=0logbn−1 ajf(n/bj)

Compare f(n) with nlogba:

CaseConditionSolution
1
Recursive work dominates
f(n) = O(nlogba − ε) for some ε > 0 T(n) = Θ(nlogba)
2
Balanced
f(n) = Θ(nlogba logkn), k ≥ 0 T(n) = Θ(nlogba logk+1n)
3
Outside work dominates
f(n) = Ω(nlogba + ε) and af(n/b) ≤ cf(n) for some c < 1 T(n) = Θ(f(n))

5.8Master-Theorem Examples

Example 1 · T(n) = 4T(n/2) + n

Here a = 4, b = 2, and nlog₂4 = n². Since n is polynomially smaller than , this is Case 1. Therefore Θ(n²).

Example 2 · T(n) = 4T(n/2) + n²

Now f(n) = n² matches nlog₂4. Case 2 gives Θ(n² log n).

Example 3 · T(n) = 4T(n/2) + n³

The term is polynomially larger than , and the regularity condition holds: 4(n/2)³ = n³/2. Case 3 gives Θ(n³).

When the basic theorem does not apply For T(n) = 4T(n/2) + n²/log n, the non-recursive term is only logarithmically—not polynomially—smaller than . Use a recursion tree, a generalized Master Theorem, or Akra–Bazzi.

5.9Decreasing Recurrences

Some recursive algorithms reduce the input by a fixed amount rather than dividing it:

T(n) = aT(n − b) + f(n), where a > 0 and b > 0

For tight bounds, assume f(n) = Θ(nk), k ≥ 0, and a positive constant base-case cost. The lecture’s Big-O forms remain valid upper bounds.

ConditionTight boundExample
a = 1 Θ(nk+1), equivalently Θ(nf(n)) T(n)=T(n−1)+n → Θ(n²)
a > 1 Θ(an/b) T(n)=2T(n−1)+n → Θ(2ⁿ)
0 < a < 1 Θ(nk) The non-recursive work dominates the shrinking geometric sum.

When 0 < a < 1, this is an algebraic recurrence coefficient—not a literal fractional number of recursive calls.

Fibonacci recursion The naïve Fibonacci program makes two calls on nearly the same input size, leading to the recurrence T(n) = T(n−1) + T(n−2) + Θ(1). Its running time is Θ(φⁿ), where φ = (1 + √5)/2; the commonly stated O(2ⁿ) bound is valid but not tight. Memoization reduces the running time to Θ(n).

5.10Dividing Recurrences

The lecture’s generalized form is:

T(n) = aT(n/b) + Θ(nk logpn), where a ≥ 1 and b > 1

First compute logba and compare it with k.

ComparisonCondition on pResult
logba > k Any fixed p Θ(nlogba)
logba = k p > −1 Θ(nk logp+1n)
p = −1 Θ(nk log log n)
p < −1 Θ(nk)
logba < k Any fixed p Θ(nk logpn)

For example, T(n)=2T(n/2)+Θ(n) has log₂2 = 1 = k and p=0, so Θ(n log n).

The earlier recurrence T(n)=4T(n/2)+n²/log n has log₂4=k=2 and p=−1. The generalized theorem therefore gives Θ(n² log log n).

5.11Binary Search

Binary search finds a target in a sorted array. Each unsuccessful comparison discards half of the remaining search interval. Its best-case time is Θ(1); its average- and worst-case time is Θ(log n).

Iterative version

def binary_search(array, key):
    low, high = 0, len(array) - 1

    while low <= high:
        mid = low + (high - low) // 2
        if array[mid] == key:
            return mid
        if array[mid] < key:
            low = mid + 1
        else:
            high = mid - 1

    return -1

Recursive version

def binary_search_recursive(arr, low, high, target):
    if low > high:
        return -1

    mid = (low + high) // 2
    if arr[mid] == target:
        return mid
    if target < arr[mid]:
        return binary_search_recursive(arr, low, mid - 1, target)
    return binary_search_recursive(arr, mid + 1, high, target)
T(n) = T(n/2) + Θ(1) = Θ(log n)

5.12Recursion Practice

Write a base case and a recursive case for each problem:

  1. Generate the Fibonacci sequence.
  2. Find the sum of the first n natural numbers.
  3. Reverse a string.
  4. Check whether a string is a palindrome.
  5. Count the digits in a number.
  6. Print every element of an array.
Checkpoint · Sum of the first n natural numbers
show one solution
def sum_n(n):
    if n <= 0:
        return 0
    return n + sum_n(n - 1)

The recurrence is T(n)=T(n−1)+Θ(1), so the running time is Θ(n).

5.13What Is Divide and Conquer?

Divide and conquer A problem-solving strategy that breaks a problem into smaller subproblems, solves them recursively, and combines their solutions.

1 · Divide

Split the original problem into smaller, usually independent subproblems.

2 · Conquer

Solve each subproblem recursively, stopping when it reaches a base case.

3 · Combine

Merge the subproblem solutions to obtain the answer to the original problem.

Binary search as divide and conquer

  • Divide: split the sorted array around its middle element.
  • Conquer: search only the half that can contain the target.
  • Combine: no merge is needed; return the result directly.

5.14Characteristics of Divide and Conquer

CharacteristicMeaning
Smaller subproblems Each subproblem is a simpler version of the original problem.
Independent subproblems The subproblems can be solved separately without repeatedly solving the same state.
Effective combine step There is a clear way to assemble the partial answers into a complete solution.

Standard examples include binary search, merge sort, quicksort, and Strassen’s matrix-multiplication algorithm.

5.15General Algorithmic Template

def divide_and_conquer(problem):
    if base_case(problem):
        return base_solution(problem)

    subproblems = divide(problem)
    subresults = [
        divide_and_conquer(sub)
        for sub in subproblems
    ]
    return combine(subresults)

The recurrence is determined by three design choices: how many subproblems are created, how large they are, and how much work the divide/combine steps require.

number of subproblems → a   ·   subproblem size → n/b   ·   extra work → f(n)

5.16Strengths and Limitations

Efficiency

Balanced division reduces the input by a constant factor per level, giving logarithmic recursion depth and opportunities for parallel execution.

Clarity

The divide–conquer–combine structure gives many algorithms a clean and understandable design.

Broad use

The strategy appears in searching, sorting, computational geometry, transforms, and matrix algorithms.

Limitations

  • Recursive calls consume stack space and add call overhead.
  • The combine step can be expensive or difficult to design.
  • Unbalanced splits may destroy the expected performance.
  • Problems with heavily overlapping subproblems are often better handled by dynamic programming.
Chapter takeaway Recursion describes the solution; a recurrence describes its cost. Divide and conquer becomes powerful when the subproblems shrink predictably and their results can be combined efficiently.