Setting Up Recurrence Relations
Imagine you write a recursive function to sort a million numbers. You want to know — before running it — whether it will finish in a second or a year. Reading the code line by line will not tell you this directly. This is because the function calls itself, and the cost of each call depends on the cost of smaller calls. You need a way to compress an entire recursive computation into a single symbolic equation. This equation describes how the running time at size relates to the running time at smaller sizes. That equation is a recurrence relation. Setting it up correctly is the first and most consequential step in analyzing any recursive or divide-and-conquer algorithm. This translation step matters enormously in practice. Every time an engineer implements merge sort, a balanced binary search, the Karatsuba multiplication trick, or a recursive tree traversal, the running time is governed by a recurrence. Compilers, database query optimizers, and standard library implementations of sorting and searching all depend on this. Programmers, and automated cost models, must get this translation right. A single misread base case or an uncounted "combine" step can matter greatly. It can turn an algorithm on paper into an accidental algorithm in reality. This is a bug that no amount of testing on small inputs will reveal. It only shows up at scale. The big picture is this: recursive code has a natural correspondence to a mathematical recurrence. This recurrence is built from three ingredients. These are how many subproblems are made, how much smaller each subproblem is, and how much extra work is done outside the recursive calls. Once you can read code and mechanically produce , you have converted an algorithmic question into a self-contained math problem. Downstream tools — the substitution method, recursion trees, the Master Theorem — can then solve it. This article teaches you to perform that translation reliably. It also teaches you to avoid the classic errors that corrupt it. These include missing or wrong base cases, undercounting combine work, and ignoring unequal subproblem sizes. They also include conflating the number of recursive calls with the number of leaves in the recursion. By the end, you will be able to look at almost any recursive algorithm. You will be able to write down its recurrence with confidence. It will then be ready to hand off to the solving techniques that follow.
- A recurrence expresses the running time of a recursive algorithm in terms of running times on smaller inputs.
- Setting one up requires three pieces: the base case, the recursive term (number and size of subproblems), and the combine cost (non-recursive work per call).
- The general divide-and-conquer template is , where = number of subproblems, = subproblem size, = combine cost.
- Base cases must match the code's actual stopping condition, including boundary values like or , or the recurrence silently misrepresents the algorithm.
- Combine cost must include everything done outside recursive calls: loops, comparisons, merges, partitioning — not just the "obvious" step.
- Getting right is a prerequisite for The Master Theorem, The Recursion-Tree Method, and The Substitution Method, which solve for a closed form.
A recurrence relation is a faithful, line-by-line translation of recursive code's cost — one term per recursive call, one term for everything else, and an exact base case.
The idea
A recurrence relation for an algorithm's running time is an equation that defines , the worst-case (or sometimes average-case) number of primitive operations on an input of size , in terms of evaluated at smaller arguments. It comes in two parts. The base case specifies directly for small, fixed values of — typically the input sizes at which the algorithm stops recursing and does work. The recursive case specifies for larger as a combination of calls to at smaller arguments, plus whatever extra work the algorithm performs outside those calls. The most common shape, arising from divide-and-conquer algorithms, is
read as: the algorithm divides a problem of size into subproblems, each of size roughly , solves each recursively, and then spends time combining the subproblem results (this also absorbs the cost of dividing the input in the first place). Here is the number of recursive calls, is the factor by which the input shrinks, and is called the combine cost or driver function. Not every recursive algorithm fits this exact template — some have unequal subproblem sizes (e.g., quicksort's worst case), some subtract rather than divide (e.g., ), and some make a variable number of calls depending on the input — but the discipline of identifying "how many calls, how much smaller, how much extra work" applies universally. The key terminology to keep straight: subproblem size is the argument passed to each recursive call, not the number of calls; branching factor () is how many separate recursive invocations occur, not how many times the function shrinks the problem; and combine cost () is measured per call at size , using the same operation-counting techniques used for iterative loops. Confusing any of these three produces a recurrence that looks plausible but describes a different algorithm than the one in front of you.
How it works
Setting up a recurrence is a mechanical, five-step reading of the code — you are not yet solving anything, only transcribing.
Step 1 — Identify the parameter of recursion. Decide what "" measures: array length, tree height, number of bits, distance between two indices, and so on. This must be a quantity that strictly decreases toward the base case along every recursive path.
Step 2 — Find every base case and its cost. Locate each `if`/guard that stops the recursion (e.g., `n <= 1`, `lo >= hi`, `node == null`). Count the operations performed there exactly as you would for straight-line code. This gives (or whatever constant work applies) for those small .
Step 3 — Count the recursive calls and their argument sizes. For each call to the function itself, note the size of the argument passed. If there are calls each on a problem of size , that contributes . If sizes are unequal (say sizes ), write instead of forcing them into a single term — do not paper over asymmetry.
Step 4 — Count everything else as . Sum the cost of every operation that is not a recursive call: splitting/partitioning the input, scanning it, merging results, allocating auxiliary arrays, comparisons used to decide how to recurse. Use ordinary loop-counting to get as an explicit function of (e.g., for a linear merge, for computing a midpoint).
Step 5 — Assemble and simplify. Write
and simplify to its tightest simple bound (e.g., write rather than " comparisons plus 2 assignments"). At this point is fully specified; solving it is a separate task handled by later techniques. ``` FUNCTION SetUpRecurrence(code): // Step 1: identify the recursion parameter n n := the quantity that shrinks on every recursive path // Step 2: locate base case(s) for each stopping condition in code: record cost of that branch as a constant // T(n) = Θ(1) there // Step 3: locate recursive calls calls := list of (argument size, count) for each self-call if all calls have the same size n/b and there are a of them: recursive_term := a * T(n / b) else: recursive_term := sum of T(size_i) over each distinct call // Step 4: count non-recursive work f_n := cost of everything in code EXCLUDING the recursive calls (splitting, scanning, merging, bookkeeping) // analyze this exactly as an iterative loop would be analyzed // Step 5: assemble T(n) := base_case_cost if n small T(n) := recursive_term + f_n otherwise return T(n) ``` To see this in action, consider merge sort on an array of length : ``` MERGE-SORT(A, lo, hi): if lo >= hi: // base case: 0 or 1 element return mid = (lo + hi) / 2 MERGE-SORT(A, lo, mid) // recursive call #1, size ~n/2 MERGE-SORT(A, mid+1, hi) // recursive call #2, size ~n/2 MERGE(A, lo, mid, hi) // combine step, Θ(n) work ``` Applying the five steps: the recursion parameter is ; the base case is costing ; there are recursive calls each on roughly elements, so ; the non-recursive work is the `MERGE` call, which scans and merges two sorted halves in time. Assembling gives
This is the recurrence that The Master Theorem later solves to — but notice that arriving at required nothing beyond careful reading; no solving happened yet.
Complexity & correctness
Setting up the recurrence is not itself an algorithm with a running time — it is an act of modeling. So "complexity" here means: how do we know the recurrence we wrote is a faithful and tight description of the algorithm's actual cost? Correctness of the setup rests on three checks, each corresponding to a place errors creep in.
Check 1 — base case fidelity. The base case in must trigger for exactly the same inputs as the base case in the code, and its stated cost must match the actual work done there (not assumed to be free). If code checks `n <= 1` but the recurrence assumes `n == 0`, sizes like get silently routed through the recursive branch of , which may not even be defined for such a small argument (e.g., under integer division could loop or misbehave). This is why Correctness of Recursive Algorithms by Induction and recurrence setup are companions: the induction proof of correctness and the recurrence for cost must use identical base cases, since both rely on the same well-founded decrease in .
Check 2 — recursive term fidelity. The number of calls and the subproblem size must be read directly from the call sites, not guessed from the algorithm's name or reputation. A common trap is writing when the code actually recurses on sizes and (fine, both are -close and usually absorbed), versus code that recurses on wildly unequal pieces, such as quicksort's worst case ( and ), which must be written as , not forced into a balanced template.
Check 3 — combine-cost completeness. must include all non-recursive work: splitting the array, computing a pivot, allocating temporary storage, and any post-processing after the recursive calls return — not just the step that "feels" like the combine phase. Undercounting here is the single most common source of an incorrect asymptotic bound; for instance, forgetting that array slicing (rather than passing indices) costs per call silently upgrades a recurrence to in practice, because was mis-recorded as . Given these checks, the recurrence is a correct model in the following precise sense: if denotes the true number of primitive steps the algorithm performs on an input of size , and is the recurrence built by the five-step procedure, then exactly (up to the constants hidden in -notation for the base case and ), by structural induction on : the base case matches by Check 1, and for the inductive step, the code's total work at size is by definition the sum of the work done in the recursive calls — which by the inductive hypothesis is — plus the non-recursive work by Check 3, which is exactly the recursive case of . The honest tradeoff is that this setup step produces but does not tell you its closed form or growth rate; that is deferred to The Substitution Method, The Recursion-Tree Method, and The Master Theorem. A perfectly set-up recurrence can still be hard to solve (e.g., , the Fibonacci recurrence, grows exponentially despite looking similar in shape to easier ones), so setup and solving are genuinely separate skills, and mistakes in either stage compound.
Worked example
Consider a recursive algorithm for finding the maximum of an array by splitting it into three (not two) unequal parts, a variant deliberately chosen to avoid the too-clean symmetric case: ``` MAX3(A, lo, hi): n = hi - lo + 1 if n == 1: return A[lo] if n == 2: return max(A[lo], A[hi]) third = n / 3 m1 = lo + third m2 = lo + 2third x = MAX3(A, lo, m1) // size ~ n/3 y = MAX3(A, m1+1, m2) // size ~ n/3 z = MAX3(A, m2+1, hi) // size ~ n/3 (or n - 2(n/3), possibly larger by rounding) return max(x, max(y, z)) // Θ(1) combine ``` Walk through the five steps explicitly.
| Step | What we read from the code | Contribution |
|---|---|---|
| 1. Recursion parameter | defines input size | |
| 2. Base cases | `n == 1` returns directly; `n == 2` does one comparison | , |
| 3. Recursive calls | 3 calls, sizes and (roughly each, but not exactly equal due to integer division) | |
| 4. Non-recursive work | two comparisons in the final `max(x, max(y,z))` line, plus index arithmetic | |
| 5. Assemble | combine pieces | , with |
A subtlety worth pausing on: the three subproblem sizes are not identical (integer division makes the last chunk possibly one or two elements larger), but since they differ by at most , it is standard and justified practice to write rather than — the discrepancy only affects lower-order terms, which the solving techniques (The Master Theorem in particular) are explicitly built to absorb. This is a judgment call that setup requires: know when rounding is safe to elide (bounded, -additive discrepancies) versus when it changes the asymptotics (as with unbalanced splits like vs. , which still needs the shape but with different constants per branch if sizes are genuinely proportionally unequal). Now trace the recurrence numerically for to double check it against direct code execution, assuming unit exactly and unit:
Cross-checking against the code: at , three recursive calls each on size 3 (so 9 total leaf-level base cases at , each costing 1, giving units), plus the internal combine steps at each of the 4 internal invocation levels shown above (the top call plus 3 middle calls = 4 combine steps of 1 unit each) gives — matching exactly. This numeric cross-check is a good habit: after setting up , unroll it two or three levels by hand and compare against a direct trace of the code on a small concrete input, as a sanity check before handing the recurrence off to a solving method.
Common pitfalls & traps
| Pitfall | Why it happens | Consequence |
|---|---|---|
| Base case mismatch (code checks `n<=1`, recurrence assumes `n==0`) | Copying a "textbook" base case instead of reading the actual guard | undefined or wrong at small ; can propagate errors upward |
| Forgetting to count the "split" step | Focusing only on the recursive calls and the final merge, ignoring e.g. an partition before recursing | silently missing a term, changing the growth rate (e.g. quicksort's partition is easy to undercount) |
| Treating unequal subproblems as if balanced | Wanting the clean template | Masks true worst-case behavior, e.g. writing for quicksort instead of |
| Counting number of leaves instead of number of calls at the top level | Confusing "how many times is eventually reached" with "" in | Produces a recurrence with the wrong branching factor, wrong solved growth rate |
| Ignoring cost of non-primitive operations passed to recursive calls (e.g. array slicing/copying vs. passing indices) | Treating a language's slice syntax as "free" | Turns into purely from unaccounted copying |
| Writing as a description instead of a function | Leaving it as "the merge step" without quantifying | Downstream solving methods cannot proceed without an explicit |
Pioneers & history
The systematic use of recurrence relations to analyze algorithms emerged alongside the formalization of divide-and-conquer as a design paradigm, though the mathematics of recurrences long predates computing.
Origins · 1 minJohn von Neumann and how this was discovered
Von Neumann is credited with one of the earliest divide-and-conquer sorting algorithms, merge sort, described in a 1945 report, which implicitly required reasoning about how the cost of sorting halves combines into the cost of sorting the whole — an early instance of exactly the recurrence-setup discipline this article formalizes, though the explicit notation came later with the systematic study of algorithm analysis.
Where this matters
Recurrence setup is the analytical backbone behind every divide-and-conquer library routine in practice: standard sorting implementations (merge sort variants used in Python's Timsort, Java's `Arrays.sort` for objects, and C++'s `std::stable_sort`) are all designed and tuned by first writing down and solving their recurrences, then choosing thresholds (like switching to insertion sort below some small ) informed by the base-case behavior of . Fast multiplication algorithms — Karatsuba's algorithm and the Fast Fourier Transform-based methods used in cryptographic libraries and big-integer arithmetic (as in Python's `int`, or GMP) — are justified by recurrences like , whose solved form directly explains why they beat naive multiplication. Computational geometry algorithms (closest-pair, convex hull) and recursive tree/graph algorithms (binary search tree operations, recursive descent parsers used in every compiler front-end) are all analyzed the same way. In distributed and parallel computing, recurrences of the form describe communication rounds in parallel algorithms, guiding real infrastructure decisions such as how MapReduce-style reduction trees or parallel prefix-sum implementations are structured. Anywhere a system recurses — compilers, database query planners choosing between recursive join strategies, or recursive descent in regular-expression engines — setting up the recurrence correctly is the first analytical step engineers take before trusting a recursive implementation at scale.
Ask the tutor: How do I set up the recurrence for an algorithm that recurses a variable number of times depending on the input, rather than a fixed count like 2 or 3?
What is the difference between setting up a recurrence and solving it?
Setting up a recurrence means translating recursive code into a symbolic equation that faithfully describes its cost — identifying the base case, the recursive calls, and the combine cost. Solving the recurrence means finding a closed-form or asymptotic bound (like ) for that , using techniques such as The Substitution Method, The Recursion-Tree Method, or The Master Theorem. These are deliberately separate skills: you can set up a correct recurrence and still find it hard to solve (e.g., recurrences with subtractive or unequal splits often resist the Master Theorem and need substitution or trees instead). Conversely, a beautifully solvable recurrence is worthless as an analysis if it was set up incorrectly and does not actually match the code's behavior. Mastering setup first, independent of solving, avoids conflating "I don't know how to solve this" with "I don't know what the recurrence is."
How do I handle recursive calls that produce unequal-sized subproblems?
Do not force unequal subproblems into a balanced template just because it looks cleaner. If the code recurses on pieces of size , write the recurrence as the literal sum . For example, quicksort with pivot producing a split of size and gives . For worst-case analysis, you then consider the value of (over all possible pivot choices) that maximizes , which for quicksort is or , giving . Forcing a false symmetry hides exactly the worst-case behavior you are usually trying to capture, so unequal splits should be preserved explicitly until you decide, during solving, whether to analyze best, worst, or average case.
Does the combine cost f(n) always have to be measured with big-Theta, or can it be a range?
It is often most useful to state as a tight -bound when you know the exact work (e.g., a linear merge is ), but in worst-case analysis you sometimes only have an upper bound, in which case is appropriate and the resulting is itself only an upper bound, i.e., rather than . What matters most is honesty: if the code's non-recursive work varies with input (e.g., a partition step whose comparisons depend on data), state the bound you can actually justify — often the worst case over all possible executions of that step — rather than assuming the friendliest case. Mixing best-case combine costs into a worst-case recurrence is a subtle but common error that produces an overly optimistic .
What if the recursive parameter isn't the array length — like a linked list, a tree, or a graph?
The five-step procedure still applies; only the choice of "" changes. For a binary tree traversal, is usually the number of nodes, the base case is an empty subtree (`node == null`), and the recursive calls are on the left and right subtrees, whose sizes sum to (since sizes are generally unequal unless the tree is balanced), giving where . For recursion on a linked list, is the remaining list length, and calls typically reduce by exactly 1 each time, giving recurrences like . The discipline is identical: pick a decreasing, well-defined size measure, find the exact base case, and count calls and non-recursive work precisely — the structure being recursed over doesn't change the method, only the bookkeeping.
Why does merge sort's recurrence use 2T(n/2) and not something like T(n/2) + T(n/2) written separately?
They are mathematically identical when the two subproblems are the same size, and is simply the standard shorthand once you've confirmed both calls really do operate on equal (or equal up to ) sized pieces — as merge sort's two halves do. The shorthand is only valid, however, once you have explicitly checked that all subproblems truly share the same size (or differ negligibly); if they do not, as in quicksort, you must fall back to writing each term separately, e.g. , rather than defaulting to the compact notation out of habit. The compactness is a convenience earned by verifying symmetry, not a default assumption.
Can a recurrence have more than one recursive term with different subproblem sizes, like T(n) = T(n/2) + T(n/4) + Θ(n)?
Yes, and such recurrences arise naturally from algorithms that split input unevenly by design (not just by bad luck, as in quicksort), for instance certain divide-and-conquer geometric or dynamic-programming-adjacent algorithms that recurse on differently sized partitions on purpose. These recurrences require more general solving tools than a single-term Master Theorem application — often the The Recursion-Tree Method handles them well because it lets you sum costs level by level even when branches have different sizes and thus different depths to their base cases. The setup procedure is unchanged: read off each distinct recursive call, record its exact argument size, and do not try to average or combine terms with different values into a single expression.
How does recurrence setup relate to proving an algorithm correct by induction?
They share the same backbone — a well-founded decrease in a size parameter down to an explicit base case — but answer different questions. Correctness of Recursive Algorithms by Induction uses this structure to prove the algorithm produces the right answer, with the inductive hypothesis being "the recursive calls return correct results." Recurrence setup uses the identical structure to track how much time is spent, with the inductive step being "the cost at is the sum of recursive costs plus ." Because both proofs rely on the same base case and the same decomposition into subproblems, any inconsistency between the base cases used for correctness and for cost is a red flag: it usually means one of the two analyses is using a stale or incorrect understanding of when the recursion actually stops.