Divide and conquer
Split a problem into smaller halves, solve each half the same way, and combine the two answers into the answer for the whole.
O(n log n)Space O(log n)7 practice problemsStep-through pictureThe idea
Divide and conquer solves a big problem by splitting it into smaller problems of the same kind. It solves each smaller problem with the same method, again by splitting, until the pieces are so small the answer is obvious. Then it combines the small answers into bigger ones.
Take this question: in [-2, 1, -3, 4, -1, 2, 1, -5], which run of neighboring items has the largest sum? Split the list in the middle. The best run is either fully in the left half, fully in the right half, or it crosses the middle. The first two are the same question on a smaller list. The third is easy: grow a run out from the middle to the left and to the right.
The three parts always appear: a base case (one item), a split (find mid, solve both halves), and a combine step. Merge sort, quick sort, and binary search all follow this shape.
An everyday example
Counting votes in a large election
No single person counts every vote in a country. The country splits into regions, regions into towns, towns into polling stations. Each station counts its own small pile. Then towns add up their stations, regions add up their towns, and the country adds up its regions. Every level does the same simple job: split the work, wait for the answers, and combine them.
| In the example | In the code |
|---|---|
| One polling station counting its pile | The base case: if (low === high) return nums[low] |
| Splitting a region into two parts | mid = Math.floor((low + high) / 2) |
| Each part counts on its own | maxSubarraySum(nums, low, mid) and maxSubarraySum(nums, mid + 1, high) |
| Adding up the parts' results | Math.max(leftBest, rightBest, leftReach + rightReach) |
From the slow way to divide and conquer
- The obvious way to find the run of neighboring items with the largest sum: try every pair of ends. For each start
i, extend the run one item at a time and keep a running sum. That's aboutn² / 2sums, which is 5 billion for 100,000 items. Every pair is looked at, even though most runs are clearly bad. - Cut the list in the middle, at
mid. Any run in it is in one of three places: fully in the left half, fully in the right half, or crossing the middle, which means it holds bothmidandmid + 1. The first two are the same question on a list half as long, so the function can call itself on each half. - The crossing case is the new part, and it is easier than it looks. A crossing run is a left piece that ends at
midplus a right piece that starts atmid + 1. The two pieces don't affect each other, so pick each one separately: grow left frommidand keep the best sum asleftReach, grow right frommid + 1and keeprightReach. One pass over the range finds both. - Now count the cost. Each level of splitting does one pass over its ranges,
nsteps in total, and halving reaches one-item ranges after aboutlog nlevels. So the work isn log ninstead ofn². Stop at one item, whose best run is itself, and return the largest of the three answers at every level. That recursion is divide and conquer.
How it works
- Work on a range of the list, from index
lowto indexhigh. - Base case: if
low === high, the range has one item, and the best run is that item. - Split:
mid = Math.floor((low + high) / 2). - Solve each half with the same function:
leftBestforlow..mid,rightBestformid + 1..high. - Combine, part 1: grow a run left from
midand keep the best sum inleftReach. - Combine, part 2: grow a run right from
mid + 1and keep the best sum inrightReach. - Return the largest of
leftBest,rightBest, andleftReach + rightReach.
Watch it run
This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
function maxSubarraySum(nums, low = 0, high = nums.length - 1) {if (low === high) return nums[low];const mid = Math.floor((low + high) / 2);const leftBest = maxSubarraySum(nums, low, mid);const rightBest = maxSubarraySum(nums, mid + 1, high);let sum = 0;let leftReach = -Infinity;for (let i = mid; i >= low; i--) {sum += nums[i];leftReach = Math.max(leftReach, sum);}sum = 0;let rightReach = -Infinity;for (let i = mid + 1; i <= high; i++) {sum += nums[i];rightReach = Math.max(rightReach, sum);}return Math.max(leftBest, rightBest, leftReach + rightReach);}
How fast it is
| Time | O(n log n) |
|---|---|
| Extra memory | O(log n) |
There are about log n levels of halving, and each level does n steps of combine work in total. The call chain is one call per level, so log n deep.
- Best case
O(n log n)Every input of size
n: the code always splits down to single items and always runs both combine loops. Only a single item finishes at once, in one call. - Average case
O(n log n)Any values. Positive, negative, or mixed, the number of calls is always
2n - 1and each level's loops covernitems. - Worst case
O(n log n)Any input, such as 1,000,000 items: about 20 levels, each with about 1,000,000 steps of combine work, or 20 million steps instead of a trillion.
Common mistakes
- Splitting into
low..mid - 1andmid..highwithmidrounded down. Whenhigh = low + 1,midequalslowand one side is empty, which breaks the base case. - Forgetting the crossing case and only taking the larger half. A best run that spans the middle is missed.
- Starting the crossing sums at
0instead of-Infinity. In an all-negative list, the answer would wrongly become0. - Copying the halves with
sliceon every call. That adds a lot of memory. Passlowandhighinstead.
When to use it
Look for these clues in a problem:
- The answer for a list can be built from the answers for its two halves plus something that crosses the middle.
- A simple method is
O(n²)and the question hints atO(n log n). - The problem is about splitting: a range, an expression around an operator, or points split by a line.
- The same question naturally applies to a smaller piece of the input.
Check yourself
Answer each one in your head first, then open it.
How many calls does maxSubarraySum make for 5 items?
9, which is 2n - 1: 5 one-item calls and 4 calls that split and combine.
Why do leftReach and rightReach start at -Infinity instead of 0?
Starting at 0 would act like an empty piece is allowed. For [-7, -3, -9] the answer would wrongly be 0 instead of -3.
Why must a crossing run contain both mid and mid + 1?
A run is a block of neighbors. If it has items on both sides of the cut, it must pass through the two items next to the cut. Otherwise it lies fully on one side.
In the trace, why is the answer for 0..2 equal to 4 and not the crossing sum 1?
A crossing run there must include index 1, the -5, and the best one is [2, -5, 4] with sum 1. The single item 4, found as rightBest, is larger, so it wins.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 7 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingmedium
Go further
You don't need these to start practicing. Open one when you want more.
Why it works
The reasoning that makes it always right
The claim: maxSubarraySum(nums, low, high) returns the largest sum of a non-empty run inside low..high. For one item, it's true, because the only run is the item. For longer ranges, assume the claim holds for both halves, which are shorter. Then leftBest and rightBest are the best runs fully inside each half.
The three cases cover every run: a run either ends at or before mid, or starts at or after mid + 1, or contains both mid and mid + 1. A crossing run's sum is the sum of its left piece, ending at mid, plus its right piece, starting at mid + 1. The loops try every possible left piece and every possible right piece and keep the best of each, so leftReach + rightReach is the best crossing sum. The largest of the three is the best run overall.
The recursion always ends. When low < high, mid = Math.floor((low + high) / 2) satisfies low <= mid < high, so both halves are non-empty and strictly shorter. Every chain of calls reaches a one-item range. The halves are solved first, then combined, so each combine step uses answers that are already correct.
Going deeper
3 more ideas
Why three cases cover every possible run
Any run of neighboring items in low..high either ends at or before mid (fully left), starts after mid (fully right), or includes both mid and mid + 1 (crossing). There is no fourth option. A crossing run is a left piece ending at mid plus a right piece starting at mid + 1, and the two pieces can be chosen separately, so the best crossing run is leftReach + rightReach.
Why it costs O(n log n)
Halving n items until each piece has one item takes about log n levels. On each level, the combine loops together walk over all n items once. So the total work is n per level times log n levels. For 1,000,000 items that is about 20 million steps, instead of a trillion for checking every pair of ends.
When the split isn't in the middle
Some problems split around something other than the middle. To find all values of 2*3-4*5 with different brackets, split at each operator, solve the left and right sides, and combine every left value with every right value. To find the closest pair of points, split by an x coordinate. The shape is the same: smaller copies of the problem plus a combine step.
Work it by hand
Every step in a table, with why
Find the largest run sum in 5 numbers. Each row is one call, listed in the order the calls finish, so a range's two halves always come before it.
nums = [2, -5, 4, -1, 3]
| Step | low..high | mid | leftBest | rightBest | leftReach + rightReach | Returns | Why |
|---|---|---|---|---|---|---|---|
| 1 | 0..0 | - | - | - | - | 2 | The first call splits down to the leftmost single item. A one-item range returns that item. |
| 2 | 1..1 | - | - | - | - | -5 | The other single item of the range 0..1. |
| 3 | 0..1 | 0 | 2 | -5 | 2 + -5 = -3 | 2 | Crossing must use both 2 and -5, giving -3. The left half alone, 2, is better. |
| 4 | 2..2 | - | - | - | - | 4 | The right half of 0..2 is one item. |
| 5 | 0..2 | 1 | 2 | 4 | -3 + 4 = 1 | 4 | Growing left from index 1 gives sums -5, then -3, so leftReach = -3. Crossing gives 1, but 4 alone is best. |
| 6 | 3..3 | - | - | - | - | -1 | Now the right half of the whole list, 3..4, splits into two single items. |
| 7 | 4..4 | - | - | - | - | 3 | The last single item. |
| 8 | 3..4 | 3 | -1 | 3 | -1 + 3 = 2 | 3 | Crossing gives 2, less than 3 alone. |
| 9 | 0..4 | 2 | 4 | 3 | 4 + 2 = 6 | 6 | Growing left from index 2: 4, -1, 1, so leftReach = 4. Growing right from index 3: -1, 2, so rightReach = 2. The crossing run [4, -1, 3] sums to 6 and beats both halves. |
It returns 6, from the run [4, -1, 3]. Neither half held it: the best fully-left run was 4 and the best fully-right run was 3. The recursion made 2 × 5 - 1 = 9 calls and ends when the top call, 0..4, combines its halves.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| Divide and conquer | O(n log n) | O(log n) | You want to practice the split, solve, combine shape, or the problem has a combine step that needs a pass over the range. |
| Every pair of ends with a running sum | O(n²) | O(1) | The list is tiny, or you want a simple version to test a faster one against. |
| Kadane's scan | O(n) | O(1) | You only need the answer for the whole list once. It is the fastest and shortest. |
| Divide and conquer with four values | O(n) | O(log n) | You want the split shape at linear time, or you plan to store the four values in a segment tree for range questions with updates. |
Extra memory: O(log n): the call chain is one call per level of halving. Passing low and high instead of copying halves keeps it that small.
How the steps grow
- Divide and conquer
O(n log n) - Every pair of ends with a running sum
O(n²) - Kadane's scan, Divide and conquer with four values
O(n)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| Divide and conquer | 33 | 9,966 | 20 million |
| Every pair of ends with a running sum | 100 | 1 million | 1 trillion |
| Kadane's scan | 10 | 1,000 | 1 million |
| Divide and conquer with four values | 10 | 1,000 | 1 million |
Inputs that need care
5 tricky inputs
- An empty list:
nums = [] highstarts at-1,low === highis never true, and the function keeps calling itself on the same range until the engine throws aRangeError. Checknums.length === 0first and return whatever the problem asks for.- All negative:
nums = [-7, -3, -9] - It returns
-3, the single largest item. This works becauseleftReachandrightReachstart at-Infinity, so an empty run, with sum0, is never counted. - One item:
nums = [8] low === highon the first call, so it returns8with no splitting.- An odd length, such as 5 items
midrounds down, so the left half gets the extra item:0..2and3..4. Both halves are still non-empty, so the base case is always reached.- Very large sums
- JavaScript numbers are exact only up to
Number.MAX_SAFE_INTEGER, about9 × 10^15. If sums can pass that, useBigIntvalues.
Variations and follow-ups
Twists interviewers add
Return four values to get O(n)
Let each call return the range's total, its best prefix, its best suffix, and its best run. The parent computes all four from its children's values in O(1), with no loops. The time becomes O(n), and this is exactly what a segment tree node stores to answer this question for any range.
Kadane's scan
Walk once from left to right, keeping the best sum of a run that ends at the current item: either extend the previous run or start fresh. It is O(n) time and O(1) space, and is the usual answer when the list is given all at once.
Count across the middle
The same shape counts pairs that cross the middle. Merge sort can count inversions, pairs where a bigger item comes first, by counting during each merge how many left items are bigger than each right item.
Return the run, not only the sum
Carry the start index with leftReach and the end index with rightReach, and return the indexes along with the best sum from each call. The parent picks the indexes of whichever case wins.
In an interview
Spot it, explain it, answer follow-ups
- Spot it when the answer for a list can be built from its two halves plus something that crosses the middle, or when the brute force is
O(n²)and the hint isO(n log n). - State the three cases out loud before coding: fully left, fully right, and crossing. Then explain why the crossing case is two separate loops: the left piece and the right piece don't affect each other.
- Test one item, an all-negative list, and two items where the best run crosses the middle, like
[3, 4]. Each tests a different line of the code. - Give the time with a recurrence:
T(n) = 2T(n/2) + n, two half-size calls plus a linear combine, which works out toO(n log n). - The usual follow-up is "can you do better?". Answer with Kadane's scan, or with the four-value version, which turns the combine step into
O(1)and the whole thing intoO(n).
Where frontend code uses it
The same idea in real UI code
- V8 sorts arrays with TimSort, which is built on merging sorted runs, a divide-and-conquer idea.
- Web map libraries split the world into tiles: each zoom level divides every tile into four smaller tiles, so the map only loads the tiles in view.
- Canvas and game engines often store shapes in a quadtree, which keeps dividing the screen into four parts so hit tests only look at shapes in the right part.