Skip to main content

Updated Sep 11, 2026

Divide & Conquer

Divide and conquer breaks a problem into independent subproblems of the same kind, solves those recursively, and combines the results. The leverage comes from the subproblems being independent — no shared state, no communication — which is also what makes the pattern parallelise so naturally.

Three steps, always: divide, conquer, combine. Which step does the real work varies, and that variation is what distinguishes mergesort from quicksort.

Core Concepts​

AlgorithmDivideConquerCombine
MergesortTrivial — split in halfSort each halfMerge — O(n)O(n)
QuicksortPartition — O(n)O(n)Sort each sideTrivial — nothing to do
Binary searchCompare to the midpointOne side onlyTrivial
Karatsuba multiplicationSplit the digits3 subproductsShift and add
Strassen's matrix multiplySplit into quadrants7 subproductsAdd submatrices

Mergesort and quicksort are exact mirrors: one does its work combining, the other dividing. Binary search is the degenerate case that discards a subproblem instead of solving it, which is why it is logarithmic rather than linear.

Mechanism​

def divide_and_conquer(problem):
if is_small_enough(problem):
return solve_directly(problem) # base case
subproblems = divide(problem)
results = [divide_and_conquer(p) for p in subproblems]
return combine(results)

Traced on the shared sorting input [5, 1, 8, 3, 9, 2, 7, 4], mergesort's recursion tree — divide splits every array in half down to single elements, then combine merges pairs back up in sorted order, level by level:

level 0 (divide) [5,1,8,3,9,2,7,4]
level 1 (divide) [5,1,8,3] [9,2,7,4]
level 2 (divide) [5,1] [8,3] [9,2] [7,4]
level 3 (base case) [5][1] [8][3] [9][2] [7][4]

level 2 (combine) [1,5] [3,8] [2,9] [4,7]
level 1 (combine) [1,3,5,8] [2,4,7,9]
level 0 (combine) [1,2,3,4,5,7,8,9]

Three divide levels turn 8 elements into 8 singletons (log2(8) = 3), and each of the 4 combine steps does O(n)O(n) work summed across its level — 3 levels of O(n)O(n) merging is the Θ(nlog⁡n)Θ(n \log n) the Master Theorem predicts for this recurrence; see below for the general method.

Reading the recurrence​

Every divide-and-conquer algorithm has a recurrence of the shape T(n)=a⋅T(n/b)+f(n)T(n) = a \cdot T(n/b) + f(n) — a subproblems, each of size n/b, plus f(n) work to divide and combine. Mergesort's is T(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n), which the tree above traces directly: 2 subproblems per level, halving in size, with O(n)O(n) total merge work at every level. Working out the closed-form result for an arbitrary recurrence — and the three cases that decide whether the leaves, every level, or the root dominates — is not repeated here; see Recurrences & the Master Theorem for the full method and worked examples including Karatsuba and Strassen's, both of which win by reducing a rather than shrinking the subproblem.

Practical Usage​

# Maximum subarray, divide and conquer — O(n log n)
# (Kadane's algorithm solves this in O(n); this version shows the pattern.)
def max_subarray(a, lo, hi):
if lo == hi:
return a[lo]
mid = (lo + hi) // 2
left = max_subarray(a, lo, mid) # entirely in the left half
right = max_subarray(a, mid + 1, hi) # entirely in the right half

# The third case: crossing the midpoint. This is the "combine" step.
best_left, total = float("-inf"), 0
for i in range(mid, lo - 1, -1):
total += a[i]
best_left = max(best_left, total)
best_right, total = float("-inf"), 0
for i in range(mid + 1, hi + 1):
total += a[i]
best_right = max(best_right, total)

return max(left, right, best_left + best_right)


A = [5, 1, 8, 3, 9, 2, 7, 4]
assert max_subarray(A, 0, len(A) - 1) == 39 # the whole array — no negative numbers to avoid

Where the pattern shows up beyond sorting:

  • Parallel processing. MapReduce is divide and conquer with the subproblems distributed across machines; the independence of subproblems is exactly what makes the distribution safe.
  • Fast Fourier Transform — O(nlog⁡n)O(n \log n) instead of O(n2)O(n^2), by splitting into even and odd indices.
  • Closest pair of points — O(nlog⁡n)O(n \log n) instead of the O(n2)O(n^2) of checking all pairs.
  • Quickselect — quicksort that recurses into only one side, giving O(n)O(n) average for the k-th smallest element.
  • Binary search and every balanced-tree operation.

Edge Cases & Pitfalls​

Divide and conquer requires independent subproblems

When subproblems overlap — the same sub-computation appearing in several branches — plain recursion recomputes it exponentially often. Naive Fibonacci is the standard demonstration: fib(n-1) and fib(n-2) share almost all their work, and the runtime is O(2n)O(2^n) for an O(n)O(n) problem.

Overlapping subproblems mean you want dynamic programming, which is precisely divide and conquer plus memoisation.

  • The base case must be reachable. A "divide" that can produce an empty or full-size subproblem recurses forever. Quicksort's mid + 1 and mid - 1 exist for exactly this reason.
  • Recursion depth is O(log⁡n)O(\log n) when balanced and O(n)O(n) when not. Unbalanced quicksort overflows the stack rather than merely running slowly.
  • Switch to an iterative algorithm at small sizes. Recursion overhead dominates below ~16 elements, which is why production sorts fall back to insertion sort.
  • The Master Theorem does not cover everything — it requires subproblems of equal size and well-behaved f(n). Unequal splits need the Akra–Bazzi method or a recursion tree.

Recall​

References​

  • Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms, Ch. 4 — divide and conquer, the substitution and recursion-tree methods, and the Master Theorem with proof.
  • Karatsuba, A. (1962) — the multiplication algorithm that first beat the schoolbook O(n2)O(n^2) bound.
  • Strassen, V. (1969), "Gaussian elimination is not optimal" — the matrix-multiplication result.

Books & Videos​

  • Bentley, J., Programming Pearls, Ch. 8 — the maximum-subarray problem worked through four algorithms, including the divide-and-conquer one above.