Home / Algorithms

Divide and Conquer

Split the problem, solve the pieces, combine the answers. It is the paradigm behind merge sort, quick sort and binary search — and the recursion tree shows exactly where the log n comes from.

Controls

array size n8

The Recursion Tree

step 0

Insight

Three phases at every level: divide into subproblems, conquer them recursively, combine the results.

T(n) = a·T(n/b) + f(n) ↑ ↑ ↑ pieces size combine
levels0
work / level0
total0

Complexity

Merge sort O(n log n)
Binary search O(log n)
Depth O(log n)

Divide and Conquer

Break it up, solve the pieces, stitch them back — and the log n falls out of the tree depth.

The problem it solves

Divide and conquer solves a problem by splitting it into smaller instances of itself, solving those recursively, and combining the results. Three phases: divide, conquer, combine.

Where log n Comes From

The recursion tree makes it obvious. Each level halves the problem size, so it takes log₂n levels to reach size 1 — that is the height of the tree.

In merge sort, every level does O(n) total work (each element is touched once during merging), and there are log n levels. Multiply: O(n log n). Raise n in the lab and watch the level count grow by exactly one each time you double.

The Master Theorem

For recurrences of the form T(n) = a·T(n/b) + f(n) — a subproblems of size n/b, plus f(n) to combine — the answer depends on which dominates:

  • Leaves dominate — most work at the bottom.
  • Balanced — equal work per level, giving the extra log factor. Merge sort sits here: T(n) = 2T(n/2) + O(n) → O(n log n).
  • Root dominates — the combine step is the expensive part.

Binary search is T(n) = T(n/2) + O(1): one subproblem, trivial combine — so O(log n) total. It discards half the work rather than recursing into both halves, which is why it beats merge sort's complexity.

The Combine Step Is Where the Work Lives

Dividing is usually trivial — compute a midpoint. The interesting engineering is almost always in combining.

  • Merge sort — merging two sorted halves is O(n) and needs O(n) scratch space.
  • Quick sort — inverts the pattern: it partitions before recursing, so the combine step is free. That is why it sorts in place.
  • Maximum subarray — the answer may straddle the midpoint, so combining must scan outward from the centre.

Why It Matters Beyond Sorting

Divide and conquer parallelises naturally — independent subproblems can run on different cores, which is why it underpins MapReduce and most parallel frameworks. It is also the pattern behind Karatsuba multiplication, the Fast Fourier Transform, and closest-pair-of-points.

Split, solve, combine

Divide and conquer solves a problem by breaking it into independent subproblems of the same kind, solving those recursively, and combining the results.

Three steps, and the third is where the algorithms differ:

Divide the problem into smaller instances. Conquer each recursively, until a base case is trivial. Combine the sub-results into the answer.

AlgorithmDivideCombine
Merge sortSplit in halfMerge two sorted halves — O(n)
Quick sortPartition around a pivotNothing — work done in the divide
Binary searchHalve the rangeNothing — one side is discarded
Karatsuba multiplicationSplit the digitsThree products and some additions
Strassen's matrix productSplit into quadrantsSeven products and additions
Closest pair of pointsSplit by x-coordinateCheck the strip near the boundary

Note that quick sort and merge sort are mirror images: quick sort does its work partitioning before recursing, merge sort does it merging afterwards. Binary search is the degenerate case where one subproblem is discarded entirely, which is why it is O(log n) rather than O(n log n).

The master theorem, informally

For a recurrence of the form

T(n) = a · T(n/b) + f(n)

a subproblems, each of size n/b, plus f(n) work to divide and combine — the complexity depends on which term dominates.

CaseConditionResult
Combine dominatesf(n) grows faster than n^(log_b a)O(f(n))
Balancedf(n) ≈ n^(log_b a)O(f(n) log n)
Recursion dominatesf(n) grows slowerO(n^(log_b a))

Applied to merge sort: two subproblems of half the size plus O(n) merging — a = 2, b = 2, so n^(log₂2) = n, which matches f(n) = n. Balanced case, giving O(n log n).

Applied to binary search: one subproblem of half the size plus O(1) — a = 1, b = 2, so n^(log₂1) = n⁰ = 1, matching f(n) = 1. Balanced case, giving O(log n).

The useful intuition without the algebra: count the levels of recursion (log_b n) and the work per level. If the work per level is constant in total, the answer is that work times the number of levels.

Divide and conquer against dynamic programming

The distinction is one word: independence.

Divide and conquer applies when subproblems do not overlap. Merge sort's two halves share nothing, so each is solved once and there is nothing to cache.

Dynamic programming applies when they do overlap. Fibonacci's fib(n-1) and fib(n-2) share almost everything, so solving each independently is exponentially wasteful and memoisation is essential.

 Divide and conquerDynamic programming
SubproblemsIndependentOverlapping
CachingPointlessEssential
Typical directionTop-down recursionEither
ExamplesMerge sort, binary searchFibonacci, edit distance, knapsack

Recursive Fibonacci written as divide and conquer is O(2ⁿ) precisely because the subproblems overlap and nothing is cached. Adding @lru_cache converts it to dynamic programming and O(n). Recognising which situation you are in decides whether caching is the fix.

Where the log comes from, and which step decides the total

Divide and conquer splits a problem, solves the pieces and combines them. The complexity is decided almost entirely by the combine step, and the Master Theorem is a way of reading that off. It is easier to believe after counting the work at each level of an actual recursion.

example_01.pyPython
Output

Experiments to try

  1. Step through merge sort at n = 8. Watch the tree split down to single elements, then merge back upward.
  2. Count the levels: 3 for n = 8. That is log₂8, and it is exactly where the log n factor comes from.
  3. Double n to 16. The level count rises by one, not by double — logarithmic growth, visible.
  4. Switch to binary search. Only one branch is followed at each level, so the tree is a path — O(log n) rather than O(n log n).
  5. Compare work per level. Merge sort does O(n) at every level; binary search does O(1). That single difference produces the whole complexity gap.

Where that leaves you

Divide and conquer gets its log factor from tree depth: halving repeatedly takes log n levels. Multiply that by the work per level and you have the complexity. Recursing into every branch gives O(n log n); discarding all but one gives O(log n).

Why it parallelises well

Independent subproblems can be solved simultaneously, which is the structural reason divide and conquer maps well onto multiple cores.

Merge sort's two halves can be sorted concurrently; only the merge must wait. Quick sort's two partitions likewise. That is why parallel sorting libraries are built on these algorithms rather than on insertion or bubble sort.

The practical pattern is a threshold: recurse in parallel while the subproblem is large, and switch to sequential below a size where thread overhead dominates.

from concurrent.futures import ThreadPoolExecutor

def parallel_merge_sort(arr, threshold=10_000, pool=None):
    if len(arr) <= 1:
        return arr
    if len(arr) < threshold:
        return sorted(arr)                     # sequential below the threshold
    mid = len(arr) // 2
    with ThreadPoolExecutor(2) as ex:
        left = ex.submit(parallel_merge_sort, arr[:mid], threshold)
        right = ex.submit(parallel_merge_sort, arr[mid:], threshold)
    return merge(left.result(), right.result())

In Python the GIL limits the benefit for pure-Python work, so this pattern matters more with multiprocessing, or in languages without a global lock. The structure is the point: the recursion tree is a natural task graph.

The same shape underlies MapReduce — divide the data, process independently, combine — which is divide and conquer across machines rather than cores.

Algorithms worth knowing beyond sorting

Karatsuba multiplication. Multiplying two n-digit numbers naively takes O(n²) digit products. Karatsuba splits each number in half and observes that three products suffice rather than four, giving O(n^1.585). Python's integers use it above a threshold, which is why multiplying very large integers is faster than the schoolbook method would suggest.

Strassen's algorithm. The same trick for matrices: seven multiplications of quadrants instead of eight, giving O(n^2.807) instead of O(n³). Rarely used in practice because of numerical stability and constant factors, and it proved that the obvious bound was not the limit.

Closest pair of points. Sort by x, split, recurse, then check only the narrow strip near the dividing line. O(n log n) rather than the obvious O(n²).

Fast Fourier transform. Divide and conquer on the frequency domain, O(n log n) instead of O(n²) — and arguably the most consequential algorithm in the list, underlying signal processing, audio and image compression, and fast polynomial multiplication.

Common mistakes

  • Using it where subproblems overlap. That is dynamic programming, and without caching the cost is exponential.
  • Forgetting the base case, giving infinite recursion.
  • An expensive combine step that dominates and negates the saving.
  • Recursion depth on large inputs in Python — convert to iteration or raise the limit.
  • Ignoring the constant factor. Recursion has overhead; below a threshold a simple loop wins, which is why real sorts switch to insertion sort for small subarrays.
  • Copying data at every level. arr[:mid] allocates; passing indices into one array avoids it.

Questions people ask

How is this different from plain recursion? Divide and conquer specifically splits into several independent subproblems and combines their results. Simple recursion may reduce the problem by one step.

When should I use dynamic programming instead? When subproblems overlap, so caching pays.

Does it always give O(n log n)? No — it depends on how many subproblems, how much smaller they are, and the combine cost. The master theorem gives the answer.

Why do real sorts switch to insertion sort? Recursion overhead exceeds the asymptotic benefit below roughly 32 elements.

Is binary search divide and conquer? Yes, in the degenerate form where one subproblem is discarded rather than solved.

Does it parallelise? Naturally, because the subproblems are independent — which is what makes it the basis for parallel sorting and MapReduce.

Recap in one screen

  • Divide into independent subproblems, solve recursively, combine the results.
  • The master theorem gives the complexity from the number of subproblems, their size, and the combine cost.
  • Independence is what distinguishes it from dynamic programming, where overlap makes caching essential.
  • Independent subproblems parallelise naturally, which is why parallel sorts and MapReduce have this shape.
  • Watch the constant factor: real implementations switch to a simple loop below a size threshold.

Where the parallel version actually helps

Divide and conquer looks like it should parallelise for free, and in Python it does not — the interpreter lock means threads cannot run bytecode simultaneously, so a recursive split across threads gains nothing. Two pages work through what does:

Run it in Python

Two problems that look nothing alike solved by the same shape: fast exponentiation, which turns 1,000 multiplications into 10, and counting inversions during a merge, which does in n log n what the obvious loop does in n².

divide_and_conquer.pyPython 3
Output

How the code works

  1. half = power(base, exp // 2, depth + 1)One recursive call, and its result is used twice. Writing power(b, n//2) * power(b, n//2) instead looks identical and is exponentially slower — the saving is in reusing the value, not in the halving.
  2. if exp % 2: result *= baseHandles the odd case, where halving loses a factor. This is where off-by-one errors live in every implementation of this function.
  3. left, x = sort_and_count(a[:mid])The divide step. Each half is solved independently, and the returned count is the number of inversions within that half.
  4. z += len(left) - iThe combine step, and the clever line. When an item from the right half wins, every remaining item on the left is greater than it, so they are all inversions — counted in one addition instead of one by one.
  5. x + y + zLeft, right, and across. Every inversion is in exactly one of those three categories, which is the proof that the count is complete. That decomposition is the divide-and-conquer pattern in general.

Change one thing

  • Compute power(2, 1000). Ten recursive calls, and Python's unbounded integers print the whole 302-digit result.
  • Run the inversion counter on a reversed list of 12 items. The answer is n(n−1)/2, the maximum possible — and it is the same number insertion sort would charge you in shifts.
  • Time both counters on a list of 2,000 random numbers. The brute force does two million comparisons; the merge does about twenty thousand.

Where this runs

Real CPython, compiled to WebAssembly and running on your own machine — nothing is uploaded. The first run takes a few seconds while the interpreter downloads; after that it is immediate. Need more room, or want to paste your own attempt? Use the Python compiler.

Check yourself

0 of 3

Answer without scrolling back up.

  1. Why does exponentiation by squaring make ONE recursive call and reuse the result?

  2. Counting inversions during a merge is O(n log n) because, when an item from the right half wins:

  3. Divide and conquer proves its answer complete by showing every case falls into:

Cheat sheet

Divide and Conquer

Split the problem, solve the pieces, combine the answers. It is the paradigm behind merge sort, quick sort and binary search — and the recursion tree shows exactly where the log n comes from.

ALGORITHMS · vizlearn.in/dsa/divide_and_conquer.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.