Base case
The condition that terminates the recursion.
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.
Functions improve reusability, modularity, readability, and maintainability.
def function(arguments):
steps
return result
Each recursive call works on a smaller instance of the same problem. Correct recursion requires both a stopping condition and progress toward that condition.
The condition that terminates the recursion.
Reduces the current problem to one or more smaller instances of the same problem.
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)
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)
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.
fact(1)
n = 1
return to fact(2)
fact(2)
n = 2
return to fact(3)
fact(3)
n = 3
return to fact(4)
fact(4)
n = 4
return to fact(5)
fact(5)
n = 5
return to main()
fact(5).
fact(5) is the oldest active frame at the bottom.
The base-case frame fact(1) is on top and returns first.
fact(n) = n × fact(n−1)
Ready to expand fact(5).
Once the base case returns 1, the calls unwind:
2 × 1,
3 × 2,
4 × 6, and finally
5 × 24 = 120.
Each call performs constant work and reduces n
by one. The recurrence is:
There are n calls, so the running time is Θ(n).
The four methods introduced in the lecture are:
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:
Expanding gives n + (n−1) + ··· + 1,
the sum of the first n integers.
Therefore Θ(n²).
The expansion is
log n + log(n−1) + ··· + log 2.
This is log(n!), which is
Θ(n log n).
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
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.
The same asymptotic result holds when n
is not an exact power of 2; floors and ceilings change only constant factors.
T(n) = 2T(n/2) + c₂n
Total: c₂n log₂n + c₁n = Θ(n log n)
Every level costs cn, and the tree has
log₂ n + 1 levels. Hence the total is
Θ(n log n).
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.
Since log43 < 2, the
n² non-recursive work dominates the leaf cost.
T(n) = 3T(n/4) + Θ(n²)
Total: cn² Σ(3/16)i + Θ(nlog₄3) = Θ(n²)
T(n) = T(n/3) + T(2n/3) + Θ(n)
Each level costs cn; the total is O(n log n).
| Recurrence | What the tree shows | Result |
|---|---|---|
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ⁿ) |
The Master Theorem applies to recurrences of the form:
T(n) = aT(n/b) + f(n), where a ≥ 1 and b > 1a 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.T(n) = aT(n/b) + f(n)
Total: Θ(nlogba) + Σj=0logbn−1 ajf(n/bj)
Compare f(n) with nlogba:
| Case | Condition | Solution |
|---|---|---|
| 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)) |
Here a = 4, b = 2, and
nlog₂4 = n². Since n is
polynomially smaller than n², this is Case 1.
Therefore Θ(n²).
Now f(n) = n² matches nlog₂4.
Case 2 gives Θ(n² log n).
The n³ term is polynomially larger than n²,
and the regularity condition holds:
4(n/2)³ = n³/2. Case 3 gives
Θ(n³).
T(n) = 4T(n/2) + n²/log n, the
non-recursive term is only logarithmically—not polynomially—smaller
than n². Use a recursion tree, a generalized
Master Theorem, or Akra–Bazzi.
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.
| Condition | Tight bound | Example |
|---|---|---|
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.
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).
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.
| Comparison | Condition on p | Result |
|---|---|---|
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).
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).
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
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)
Write a base case and a recursive case for each problem:
n natural numbers.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).
Split the original problem into smaller, usually independent subproblems.
Solve each subproblem recursively, stopping when it reaches a base case.
Merge the subproblem solutions to obtain the answer to the original problem.
| Characteristic | Meaning |
|---|---|
| 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.
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)Balanced division reduces the input by a constant factor per level, giving logarithmic recursion depth and opportunities for parallel execution.
The divide–conquer–combine structure gives many algorithms a clean and understandable design.
The strategy appears in searching, sorting, computational geometry, transforms, and matrix algorithms.