One tool

Imagine you're given a shuffled deck of cards and told to sort it. Normally you'd have lots of strategies — find the smallest card and move it to the front, bubble the largest card to the back, compare adjacent pairs and swap.

But today, you only have one operation: take two already-sorted piles and merge them into one sorted pile. That's it. No swapping. No scanning. No comparing individual cards against each other in an unsorted pile. Just merging sorted piles.

There's a catch: the array you're starting with is completely unsorted. So how can you merge anything when nothing is sorted to begin with?

Give it a try. You have an unsorted array and a merge button. See what happens when you try to use your only tool.

The problem with merging

You have an unsorted array and exactly one tool: merge two sorted sequences into one sorted sequence. No swapping, no scanning, no comparing individual elements in an unsorted pile. Just merging. Try to use it on the unsorted input and see what goes wrong. The failure is the point — it reveals what merge needs before it can do its job.

You have one tool: MERGE. Tap cells to build a sorted output.

Input

The smallest sorted thing

Merge failed — and that failure is the whole point. Merge only works when both inputs are already sorted. You can't merge chaos into order.

But here's the thing: what's the smallest possible sorted array? A single element. An array of one item has nothing to be out of order with. [7] is sorted. [-3] is sorted. Every single element is trivially sorted.

7-34219tap any cell

So if you keep splitting your unsorted array until every piece is just one element, you suddenly have a collection of sorted inputs. Watch how a four-element array breaks apart and reassembles:

5281unsorted arraytap to split

Now your merge tool works perfectly — merge pairs of single elements into sorted pairs, merge pairs of sorted pairs into sorted quads, and keep going until everything is back together.

Let's see if you can do the merging yourself. You'll get pre-split single elements and need to merge them back together, level by level.

Merge it up

The array has been split into individual elements — each one trivially sorted. Now your merge tool works. Start at the bottom: merge pairs of single elements into sorted pairs, then merge sorted pairs into sorted quads, and keep going until the entire array is reassembled in order. At each step, you choose which element goes next. Pay attention to how many comparisons each level costs.

1 / 3

Merge two sorted halves

Tap the smaller head. Which one goes first?

Left
Right
Merged

Counting the work

That was satisfying — everything clicked into place as you merged up the tree. But let's think carefully about how much work you actually did.

At the bottom level, you merged pairs of single elements. Each merge compared 2 elements. At the next level, you merged pairs of 2-element arrays — each merge compared up to 4 elements. At the top, you merged two halves of the full array — that compared up to n elements.

Here's the question that matters: does the total work grow as you go up the tree, or does it stay roughly the same at every level? The answer determines whether merge sort is fast or just a clever rearrangement of the same brute-force work.

Explore the tree. Pay attention to the total comparisons at each level.

The work budget

At the bottom level of the tree, you did a bunch of tiny merges. One level up, fewer merges but each one was bigger. At the top, one massive merge of the entire array. The crucial question: does the total work per level grow, shrink, or stay the same? Explore the tree and tally up the comparisons at each level. The answer determines whether merge sort is genuinely efficient or just rearranges the same brute-force work.

Expand the tree. Watch what happens to the total work at each level.

The constant-work miracle

Every level does the same total amount of work: O(n). The bottom level has n/2 tiny merges. The next has n/4 slightly bigger merges. The top has 1 big merge. But they all add up to roughly n comparisons per level.

Level 01 × 8= 8
Level 12 × 4= 8
Level 24 × 2= 8
Always 8 — every level, 3 levels total = 8 × 3 = 24 work

Scrub through the merge below. Two sorted halves become one sorted whole — each element placed costs exactly one comparison.

left
L
1
3
5
7
right
R
2
4
6
8
output
·
·
·
·
·
·
·
·

Two sorted halves ready to merge. Compare front elements at each step.

1 / 9

Let's count it concretely for an array of 16 elements. At the bottom level, you do 8 merges of 2 elements each — roughly 8 comparisons per merge, so about 16 total. One level up, 4 merges of 4 elements — about 4 comparisons each, so 16 again. Then 2 merges of 8 — about 8 comparisons each, 16 total. Finally, 1 merge of 16 — about 16 comparisons. Every single level: 16. That's not a coincidence. Each level has fewer merges but each merge is proportionally bigger. The work exactly redistributes — it never concentrates at any single level.

Since the tree has O(log n) levels and each level does O(n) work, the total is O(n log n). That's the same complexity as the maximum-subarray solution from the previous lesson, but for a completely different reason. There, the combine step was an O(n) boundary scan. Here, the combine step is the merge — and merge is the entire point of the algorithm.

In merge sort, the divide step is trivial (split at the midpoint — no comparisons needed) and all the real work happens during the combine. It's the mirror image of quicksort, where all the work happens during the divide (partitioning) and the combine step is trivial (just concatenate).

Divide
0 comparisons
Merge
n comparisons
all the work lives in the combine step

Speaking of which — there's actually more than one way to build merge sort. The top-down recursive version we just explored isn't the only option. Let's see if you can tell the two approaches apart.

Two paths to sorted

There are two ways to build merge sort, and they arrive at the same answer through opposite directions. One starts at the full array and splits downward. The other starts at individual elements and merges upward. They both do O(n log n) work, but their execution patterns look completely different. Below you'll see both approaches animated side by side — match each description to the correct approach.

Top-down or bottom-up. Same merges?

Which tree level does bottom-up Pass 2 correspond to?

Same destination, different journey

Top-down merge sort splits first, then merges on the way back up. Bottom-up merge sort skips the splitting entirely — it just starts merging adjacent pairs of size 1, then pairs of size 2, then 4, and so on. Both do exactly O(n log n) comparisons. Both produce the same result.

Top-downnn/2n/41split down, merge up
Bottom-upnn/2n/41merge up only

So why would you ever choose one over the other? Bottom-up has real practical advantages that matter outside of textbook analysis.

No stack overflow risk. Top-down merge sort recurses O(log n) levels deep. For most array sizes that's fine — log2 of a million is only 20. But in embedded systems or languages with shallow stack limits, bottom-up's iterative loop sidesteps the issue entirely. Python's sys.setrecursionlimit exists for a reason.

Cache-friendlier access patterns. Bottom-up starts by merging adjacent pairs — elements that sit next to each other in memory. Those early passes are almost entirely L1 cache hits. Top-down's recursive splitting, by contrast, jumps to widely separated halves before working back to adjacent elements. The asymptotic work is identical, but the constant factor is smaller when your memory access pattern respects the cache hierarchy.

External sorting. When your data lives on disk and doesn't fit in RAM, bottom-up is the natural choice. You load chunks that fit in memory, sort them internally, write them back, then merge pairs of sorted chunks in successive passes. There's no recursive call stack to manage — just a loop that doubles the chunk size each round. This is essentially how sort works on Unix when given files larger than memory.

Top-down is still the version you'll see in interviews and textbooks because it maps cleanly to the recursive D&C template — and interviewers care about whether you can think recursively. But in production codebases (Java's Arrays.sort for objects, Python's Timsort), the bottom-up structure dominates.

Now let's write the merge function itself — the heart of the algorithm, the combine step where all the real work happens.

Write the merge

You've been merging by hand — now translate that muscle memory into code. The merge function takes two sorted arrays and produces one sorted output. At each step, you compare the front elements of both arrays and take the smaller one. When one array is exhausted, you append the remainder of the other. It sounds simple, but the details (index management, the exhaustion case) trip people up in LC 88 (Merge Sorted Array) and every merge sort implementation.

You built the merge by hand. Now fill in the code. Each blank maps to something you discovered in the Merge Lab.

function merge(left: number[], right: number[]): number[] { const result: number[] = [] let i = 0, j = 0 while (i < left.length && j < right.length) { if () // blank 1 result.push(left[i++]) else result.push(right[j++]) } while () // blank 2 result.push(left[i++]) while () // blank 3 result.push(right[j++]) return result }

The merge sort story

You've built merge sort from first principles. Let's trace the full arc of what happened:

You started with one tool — merge — and discovered it was useless on unsorted input. That forced you to think about what input merge actually needs. The answer led you to the base case: single elements are always sorted. From there, the algorithm assembled itself — split until trivial, merge back up.

The big lesson isn't the algorithm. It's the design pattern: when your combine tool has a precondition (sorted inputs), the divide step's job is to break the problem down until that precondition is trivially satisfied. The recursion handles the rest.

Next up: what if you don't need the entire array sorted? What if you only care about a single element's final position?