Divide and Conquer

Introduction to Divide and Conquer

What Is Divide and Conquer?

Divide and conquer is a strategy for designing algorithms, not an algorithm itself. The idea is that a problem you cannot answer directly often becomes easy once it is small enough, so instead of attacking the whole input at once, you break it into smaller copies of the same problem, solve those, and assemble the results.

Merge sort is the standard illustration. To sort an array, sort its left half, sort its right half, then merge the two sorted halves into one. Notice that the instructions for sorting a half are the same instructions you started with. That self-similarity is what makes the recursion work.

A technique counts as divide and conquer only when the subproblems are the same problem on smaller input and are independent of one another. If the pieces overlap and get re-solved, you are looking at dynamic programming instead.

The Three Steps

Every divide-and-conquer algorithm is built from the same three moves:

  1. Divide: break the problem into smaller instances of the same problem.
    • The subproblems must be the same kind of problem, just smaller. Sorting half an array is still sorting; that is what makes the recursion legal.
  2. Conquer: solve the subproblems recursively.
    • Once a piece is small enough to answer outright (one element, an empty range), you stop recursing and return directly. That is the base case.
  3. Combine: build the answer to the original from the subproblem answers.
    • This is where the real work usually lives. Merge sort's merge step is the combine; binary search's combine is free, because it throws one half away.
nn/2n/2n/4n/4n/4n/4divideconquercombine
Divide until the problem is trivial, conquer the small pieces, then combine the answers on the way back up.

The amount of work each algorithm does varies enormously between the divide step and the combine step, and that balance is what decides its running time. Binary search does almost nothing in either, but throws away half the input. Merge sort divides for free and pays for the merge. Quick sort pays during the divide and combines for free.

The General Template

Nearly every divide-and-conquer function has this skeleton. If you can fill in the four marked pieces, you have designed the algorithm:

function solve(problem) {
  if (isSmallEnough(problem)) {      // 1. base case
    return solveDirectly(problem);
  }

  const pieces = divide(problem);     // 2. divide

  const answers = pieces.map(solve);  // 3. conquer (recurse)

  return combine(answers);            // 4. combine
}

The base case is written first on purpose: without it, the recursion never terminates.

The recursion is easiest to reason about if you trust it: assume the recursive calls return correct answers for smaller inputs, and only verify that your combine step is right. Trying to trace every level of the call stack in your head is how divide and conquer starts to feel harder than it is.

Where the Running Time Comes From

Because the function calls itself, its cost is defined in terms of itself. That is a recurrence relation, and almost every divide-and-conquer algorithm produces one of the same shape:

T(n) = a·T(n/b) + f(n)
  • a — how many subproblems each call creates.
    • Merge sort makes two recursive calls, so a = 2.
  • b — the factor by which the input shrinks.
    • Halving the array means b = 2.
  • f(n) — the work done outside the recursive calls.
    • The dividing plus the combining. For merge sort, the O(n) merge.

Reading a, b and f(n) off the code is mechanical, and once you have them the Master Theorem turns the recurrence straight into a complexity class. That is why the two topics are almost always taught together.

Classic Divide-and-Conquer Algorithms

The same shape, with different values of a, b and f(n), produces very different running times:

AlgorithmRecurrenceComplexityWhat the split does
Binary SearchT(n) = T(n/2) + O(1)Θ(log n)Discards half the search range each step
Merge SortT(n) = 2T(n/2) + O(n)Θ(n log n)Sorts two halves, then merges
Quick Sort (average)T(n) = 2T(n/2) + O(n)Θ(n log n)Partitions, then sorts both sides
Maximum SubarrayT(n) = 2T(n/2) + O(n)Θ(n log n)Best in left, right, or crossing the middle
Karatsuba MultiplicationT(n) = 3T(n/2) + O(n)Θ(n^1.585)Three half-size products instead of four
Strassen's Matrix MultiplyT(n) = 7T(n/2) + O(n²)Θ(n^2.807)Seven half-size products instead of eight
Closest Pair of PointsT(n) = 2T(n/2) + O(n)Θ(n log n)Closest in each half, then across the strip

Karatsuba and Strassen are worth a second look. Both beat the obvious algorithm purely by reducing a: doing three half-size multiplications instead of four, or seven instead of eight. Nothing else changes, and the exponent drops.

A Worked Example: Maximum Subarray

Given an array of positive and negative numbers, find the contiguous slice with the largest sum. Checking every slice costs O(n²). Divide and conquer does better by noticing that the best slice must be in exactly one of three places:

  1. Entirely inside the left half — solve recursively.
  2. Entirely inside the right half — solve recursively.
  3. Crossing the midpoint.
    • This case cannot be recursive, but it is easy: extend left from the middle taking the best running sum, extend right the same way, and add the two. That is a single O(n) scan.

Take the largest of the three and you have the answer:

T(n) = 2T(n/2) + O(n) → Θ(n log n)

Two recursive calls on half the input, plus a linear crossing scan. The result improves on the O(n²) brute force, and the reasoning is the same reasoning merge sort uses.

When Divide and Conquer Is the Right Tool

ConditionWhy it matters
The problem splits cleanlyThe pieces are the same problem on less data, and they do not depend on each other.
Combining is cheaper than solvingIf merging two answers costs O(n) but brute force costs O(n²), the recursion pays for itself.
The subproblems are independentIf they overlap and get recomputed, you want dynamic programming instead.
The data supports splittingRandom access makes halving an array free. Splitting a linked list in half costs a traversal.

When these do not hold, the strategy stops paying. A problem whose subproblems overlap belongs to dynamic programming. A problem where combining costs as much as solving gains nothing from the split.

How It Compares to Other Paradigms

ParadigmSubproblemsHow it proceedsExamples
Divide and ConquerIndependent subproblemsSplits, recurses, combinesMerge sort, binary search
Dynamic ProgrammingOverlapping subproblemsSolves each subproblem once and stores itFibonacci, edit distance
GreedyLocally optimal choice is globally safeCommits to one choice and never revisitsDijkstra, Huffman coding
Decrease and ConquerOne subproblem, not severalShrinks the input by a constant or factorInsertion sort, Euclid's GCD

Decrease and conquer is the one most often confused with divide and conquer. It also shrinks the input, but produces a single subproblem rather than several. By that strict definition binary search is decrease and conquer, though almost every textbook still presents it as the introductory divide-and-conquer example, because the divide step and the analysis are identical.

Advantages and Trade-offs

  • It often lowers the complexity class outright.
    • O(n²) sorting becomes O(n log n); O(n) searching becomes O(log n).
  • It parallelises naturally.
    • Independent subproblems can run on separate cores with no coordination, which is exactly what makes merge sort a good parallel sort.
  • It uses the memory hierarchy well.
    • Subproblems eventually shrink enough to fit in cache, so the deeper levels run faster than their operation count suggests.
  • But recursion costs stack space.
    • Depth is usually O(log n), which is fine, but an unbalanced split can reach O(n) and overflow.
  • And the constant factors can bite.
    • For small inputs the call overhead outweighs the better complexity, which is why real sort implementations switch to insertion sort below roughly 16 elements.

A Recipe You Can Follow Every Time

  1. Identify the base case first.
    • The smallest input you can answer without recursing, usually n = 0 or n = 1.
  2. Decide how to divide.
    • How many pieces (a), and how much smaller each one is (b).
  3. Recurse on the pieces.
    • Trust the recursion to return correct answers for smaller inputs.
  4. Write the combine step.
    • Turn the subproblem answers into the answer for the whole input.
  5. Write down the recurrence and solve it.
    • T(n) = a·T(n/b) + f(n), where f(n) is the divide plus combine cost. Then apply the Master Theorem.

Common Mistakes

  • Forgetting the base case, or getting its boundary wrong.
    • Every recursive branch must eventually reach a case that returns without recursing. A base case of n == 1 in a function that can be called with an empty range will recurse forever.
  • Assuming the split is always free.
    • Slicing an array to pass halves around copies them, which quietly turns an O(1) divide step into O(n). Passing low and high indices instead keeps it free.
  • Using divide and conquer on overlapping subproblems.
    • Computing Fibonacci by recursing on n-1 and n-2 re-solves the same values exponentially many times. Overlap is the signal to switch to dynamic programming.
  • Expecting recursion alone to make something faster.
    • The speedup comes from discarding work or from combining cheaply, not from recursing. T(n) = 2T(n/2) + O(n²) is still Θ(n²); splitting it gained nothing.
  • Ignoring the cost of the call stack.
    • A depth of log n is harmless, but an unbalanced split can reach depth n and overflow the stack. This is why quick sort recurses into the smaller side first.

Frequently Asked Questions

What is divide and conquer in simple terms?

It is a problem-solving strategy with three steps: break a problem into smaller versions of the same problem, solve those recursively, and combine their answers into the answer for the original. Merge sort is the standard example: sort the left half, sort the right half, then merge the two sorted halves.

What are the three steps of divide and conquer?

Divide, conquer and combine. Divide splits the input into smaller instances of the same problem. Conquer solves those instances recursively, stopping at a base case small enough to answer directly. Combine merges the subproblem answers into the final result.

How is divide and conquer different from dynamic programming?

Both break a problem into subproblems, but divide and conquer assumes the subproblems are independent, so each is solved once and never seen again. Dynamic programming is for overlapping subproblems, where the same subproblem appears many times, so the answers are stored and reused instead of recomputed.

Why is divide and conquer often faster?

Because the work drops off quickly as you descend. If each level either discards part of the input (binary search) or does only linear work while the sizes halve (merge sort), the total is O(log n) or O(n log n) rather than O(n) or O(n²). The gain comes from the shape of the recursion, not from recursion itself.

How do you find the time complexity of a divide-and-conquer algorithm?

Write its recurrence in the form T(n) = aT(n/b) + f(n), where a is the number of recursive calls, b is the factor the input shrinks by, and f(n) is the work outside the calls. Then solve it with the Master Theorem, a recursion tree, or the substitution method.

Is binary search a divide-and-conquer algorithm?

Yes. It divides the sorted range in half, conquers by recursing into the half that could contain the target, and needs no combine step because the other half is discarded. Its recurrence is T(n) = T(n/2) + O(1), which solves to Θ(log n).

Key Takeaways

  • Divide and conquer is a design strategy: divide into smaller copies of the same problem, conquer them recursively, combine the answers.
  • It requires subproblems that are independent. Overlapping subproblems are dynamic programming's territory.
  • Every base case must be reachable, and it should be written before the recursive branch.
  • The cost always takes the form T(n) = aT(n/b) + f(n), where f(n) is the divide plus combine work.
  • Solve that recurrence with the Master Theorem to get the complexity.
  • The speedup comes from discarding work or combining cheaply — never from recursion on its own.

The Template and Three Algorithms That Follow It

// The divide-and-conquer template, then three algorithms that follow it.

// ---------- The shape every one of them shares ----------
// function solve(problem) {
//   if (isSmallEnough(problem)) return solveDirectly(problem);  // base case
//   const pieces  = divide(problem);                            // divide
//   const answers = pieces.map(solve);                          // conquer
//   return combine(answers);                                    // combine
// }

// ---------- 1. Binary search ----------
// T(n) = T(n/2) + O(1)  ->  Theta(log n)
// divide: look at the middle. conquer: recurse into one side.
// combine: nothing, the other half is discarded.
function binarySearch(arr, target, low = 0, high = arr.length - 1) {
  if (low > high) return -1;                     // base case

  const mid = low + Math.floor((high - low) / 2);
  if (arr[mid] === target) return mid;

  return arr[mid] < target
    ? binarySearch(arr, target, mid + 1, high)
    : binarySearch(arr, target, low, mid - 1);
}

// ---------- 2. Merge sort ----------
// T(n) = 2T(n/2) + O(n)  ->  Theta(n log n)
// divide: split in half (free). conquer: sort both.
// combine: merge, which is where the work is.
function mergeSort(arr) {
  if (arr.length <= 1) return arr;               // base case

  const mid = Math.floor(arr.length / 2);
  const left = mergeSort(arr.slice(0, mid));
  const right = mergeSort(arr.slice(mid));
  return merge(left, right);
}

function merge(left, right) {
  const out = [];
  let i = 0, j = 0;
  while (i < left.length && j < right.length) {
    out.push(left[i] <= right[j] ? left[i++] : right[j++]);
  }
  while (i < left.length) out.push(left[i++]);
  while (j < right.length) out.push(right[j++]);
  return out;
}

// ---------- 3. Maximum subarray ----------
// T(n) = 2T(n/2) + O(n)  ->  Theta(n log n)
// The best slice is in the left half, the right half, or across the middle.
function maxSubarray(arr, low = 0, high = arr.length - 1) {
  if (low === high) return arr[low];             // base case

  const mid = low + Math.floor((high - low) / 2);

  const leftBest = maxSubarray(arr, low, mid);
  const rightBest = maxSubarray(arr, mid + 1, high);
  const crossBest = maxCrossing(arr, low, mid, high);

  return Math.max(leftBest, rightBest, crossBest);
}

// The crossing case cannot recurse, but one linear scan settles it.
function maxCrossing(arr, low, mid, high) {
  let sum = 0, leftSum = -Infinity;
  for (let i = mid; i >= low; i--) {
    sum += arr[i];
    if (sum > leftSum) leftSum = sum;
  }

  sum = 0;
  let rightSum = -Infinity;
  for (let i = mid + 1; i <= high; i++) {
    sum += arr[i];
    if (sum > rightSum) rightSum = sum;
  }

  return leftSum + rightSum;
}