Merge sort
Split the list in half, sort each half, then merge the two sorted halves into one sorted list.
O(n log n)Space O(n)8 practice problemsStep-through pictureThe idea
Merge sort uses one powerful fact: two lists that are already sorted can be combined into one sorted list quickly. You look at the front of each list, take the smaller item, and repeat. This is called merging, and it takes one pass.
So merge sort splits the list into two halves, sorts each half (by splitting them again, and again, until each piece has one item), and then merges the pieces back together. A list of one item is already sorted, so the splitting stops there.
Merge sort always takes O(n log n) time, even on the worst input, and it is stable: equal items keep their order. The price is extra memory for the merged lists.
An everyday example
Two sorted stacks of exam papers
Two teachers each sort half of the class's exams by score. To combine them, you put both stacks face up and compare the top papers. You take whichever has the lower score and put it on a new pile. Repeat until one stack is empty, then put the rest of the other stack on top. To get two sorted stacks in the first place, each teacher splits their own pile and hands it to helpers, all the way down to single papers.
| In the example | In the code |
|---|---|
| Splitting the pile among helpers | mergeSort(items.slice(0, middle)) and mergeSort(items.slice(middle)) |
| A single paper: already sorted | if (items.length <= 1) return items |
| Comparing the two top papers | left[i] <= right[j] |
| Putting the lower one on the new pile | result.push(...) |
| Adding what is left of the other stack | The two while loops at the end of merge |
From the slow way to merge sort
- Simple sorts like insertion sort cost
O(n²)because each comparison fixes at most one out-of-order pair, and a random list has aboutn² / 4of them. To go faster, each step has to settle many pairs at once. So look for a job that is easy when part of the order is already known. - Here is one: two lists that are each sorted can be combined into one sorted list in a single pass. The smallest item overall must be at the front of one of the two lists, so compare the two fronts, take the smaller, and repeat. Combining
nitems this way, called merging, takes at mostn - 1comparisons. - Merging needs sorted lists to start with. Where do they come from? Split the list in half and sort each half the same way. Each half splits again, and again, until the pieces have one item. A list of one item is already sorted, so the splitting stops there and the merging can begin.
- How much work is that? Halving
nitems down to single items takes aboutlog₂ nlevels. On each level, the merges together touch every item once. So the total is aboutn × log₂ nsteps, whatever the input looks like. Split in half, sort both halves with the same function, and merge: that recursion is merge sort.
How it works
- If the list has 0 or 1 items, it is sorted: return it.
- Find the middle index,
middle = Math.floor(items.length / 2). - Sort the left half and the right half with the same function (recursion).
- Merge: keep an index
iintoleftandjintoright. Push the smaller ofleft[i]andright[j]intoresultand move that index forward. - When one side runs out, push the rest of the other side.
- Return
result, which holds every item in sorted order.
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 mergeSort(items) {if (items.length <= 1) return items;const middle = Math.floor(items.length / 2);const left = mergeSort(items.slice(0, middle));const right = mergeSort(items.slice(middle));return merge(left, right);}function merge(left, right) {const result = [];let i = 0;let j = 0;while (i < left.length && j < right.length) {if (left[i] <= right[j]) {result.push(left[i++]);} else {result.push(right[j++]);}}while (i < left.length) result.push(left[i++]);while (j < right.length) result.push(right[j++]);return result;}
How fast it is
| Time | O(n log n) |
|---|---|
| Extra memory | O(n) |
There are about log₂ n levels of splitting, and each level merges all n items once. The merged arrays need O(n) extra memory.
- Best case
O(n log n)An already sorted list like
[1, 2, 3, 4, 5, 6, 7, 8]. Each merge emptiesleftafter about half the comparisons, but the code still splits, copies, and merges every level. - Average case
O(n log n)A random list:
log₂ nlevels, each merging allnitems once. - Worst case
O(n log n)Lists where the fronts keep alternating, so each merge makes the full
n - 1comparisons. It is stilllog₂ nlevels: about 20 million steps for a million items.
Common mistakes
- Leaving out the base case, or writing
items.length === 0. A one-item list then splits into an empty list and itself forever. - Using
<instead of<=in the merge. The sort still sorts, but equal items can change order, so it is no longer stable. - Forgetting the leftover loops after the main merge loop. The tail of one half is silently dropped.
- Calling
shift()on arrays to take the front item. Eachshiftmoves every other item, making the mergeO(n²). Use indexes.
When to use it
Look for these clues in a problem:
- You need a guaranteed
O(n log n)sort, or a stable sort. - The data is a linked list, where merging is easy and random access is slow.
- The question counts pairs across two halves, such as out-of-order pairs, which can be counted during the merge.
- The input is two or more lists that are already sorted and must be combined.
Check yourself
Answer each one in your head first, then open it.
Why is the base case items.length <= 1 and not items.length === 0?
A one-item list would split into [] and itself, and calling mergeSort on itself never makes progress. Stopping at one item, which is always sorted, ends the recursion.
What is the most comparisons one merge of a 3-item and a 4-item list can make?
Six, which is 3 + 4 - 1. Each comparison places one item, and the last item is placed without a comparison once the other side is empty.
Why does merge sort take O(n log n) even on a sorted list?
The code always splits down to single items and copies every item at every level. Nothing checks whether a half is already in order.
What makes merge sort stable, and what single change breaks it?
Taking from left when items are equal, with left[i] <= right[j]. Changing it to < takes the right copy first, so equal items can swap order.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 8 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingGo 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 proof has two parts: merging is correct, and the recursion is correct. For merging, the rule at the top of the main loop is: result holds the smallest i + j items of left and right, in sorted order. It starts empty, so it is true.
Each step keeps that rule. Both inputs are sorted, so the smallest item not yet taken is left[i] or right[j], whichever is smaller. Pushing it adds the next smallest item to the end of result. When one side runs out, everything left on the other side is at least as big as what was taken, and already sorted, so pushing it all keeps result sorted.
For the recursion, use induction on the length. A list of 0 or 1 items is returned as it is, and it is sorted. A longer list splits into two strictly shorter halves, because middle is at least 1 and less than the length. If mergeSort sorts every shorter list, both halves come back sorted, and merging them gives the whole list sorted. The halves keep getting shorter, so the recursion always reaches the base case.
Stability comes from the tie rule. Every item in left was before every item in right in the original list. Taking from left when left[i] <= right[j] keeps that order for equal items at every level.
Going deeper
3 more ideas
Why merging works
Both halves are sorted, so the smallest item not yet merged is always at the front of one of them: left[i] or right[j]. Taking the smaller of the two is always right. Using <= takes from the left when they are equal, so an item from the left half stays ahead of its equal twin from the right half. That is what makes merge sort stable.
Why it is O(n log n)
Halving n items down to single items takes about log₂ n levels. At each level, the merges together touch every item once, which is O(n) work per level. So the total is O(n log n) in every case: a million items need about 20 levels of a million steps each.
Bottom-up merge sort
You can skip the recursion. Treat each item as a sorted run of length 1, merge neighboring runs into runs of length 2, then 4, then 8, until one run covers the list. This uses the same merge step and is how many library sorts, including TimSort, combine runs.
Work it by hand
Every step in a table, with why
Sort 6 numbers with two 6s. The recursion is shown merge by merge, then the final merge step by step.
items = [5, 8, 1, 6, 2, 6]
| Step | Step | left | right | result | What happens | Why |
|---|---|---|---|---|---|---|
| 1 | mergeSort of all 6 | - | - | - | middle = 3 | The list splits into [5, 8, 1] and [6, 2, 6]. The left half is sorted first. |
| 2 | mergeSort([5, 8, 1]) | - | - | - | middle = 1 | [5] is a base case and comes back as it is. [8, 1] splits into [8] and [1]. |
| 3 | merge | [8] | [1] | [1, 8] | 8 <= 1 is false: take 1 | One comparison. Then right is empty, so the leftover loop adds 8. |
| 4 | merge | [5] | [1, 8] | [1, 5, 8] | Take 1, then 5 | 5 <= 1 is false, then 5 <= 8 is true. left is empty, so 8 is added. The left half is sorted. |
| 5 | mergeSort([6, 2, 6]) | - | - | - | middle = 1 | [6] is a base case, and [2, 6] splits into [2] and [6]. |
| 6 | merge | [2] | [6] | [2, 6] | 2 <= 6: take 2 | Then the leftover 6 is added. |
| 7 | merge | [6] | [2, 6] | [2, 6, 6] | Tie: take the left 6 first | 6 <= 2 is false, so take 2. Then 6 <= 6 is true, so the 6 from left goes first. That is what keeps equal items in their original order. |
| 8 | Final, i = 0, j = 0 | [1, 5, 8] | [2, 6, 6] | [1] | 1 <= 2: take 1 | Both halves are sorted. Now merge them. |
| 9 | i = 1, j = 0 | [1, 5, 8] | [2, 6, 6] | [1, 2] | 5 <= 2 is false: take 2 | The right front is smaller, so j moves. |
| 10 | i = 1, j = 1 | [1, 5, 8] | [2, 6, 6] | [1, 2, 5] | 5 <= 6: take 5 | The left front is smaller again. |
| 11 | i = 2, j = 1 | [1, 5, 8] | [2, 6, 6] | [1, 2, 5, 6] | 8 <= 6 is false: take 6 | 8 waits while the right side has smaller items. |
| 12 | i = 2, j = 2 | [1, 5, 8] | [2, 6, 6] | [1, 2, 5, 6, 6] | Take the second 6 | Now j = 3, so right is used up and the main loop ends. |
| 13 | leftovers | [1, 5, 8] | [2, 6, 6] | [1, 2, 5, 6, 6, 8] | Push the rest of left | while (i < left.length) adds 8. The other leftover loop does nothing. |
It returns [1, 2, 5, 6, 6, 8] after 11 comparisons in 5 merges. The recursion ends at the 6 one-item base cases, and each merge ends when one side is used up.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| Merge sort | O(n log n) | O(n) | You need a guaranteed O(n log n) and a stable result, or you are sorting a linked list or data too large for memory. |
| Quick sort | O(n log n) | O(log n) | You sort numbers in place, don't need stability, and want the fastest average time. Use random pivots to avoid the O(n²) case. |
| Heap sort | O(n log n) | O(1) | You need guaranteed O(n log n) with no extra memory, and stability doesn't matter. |
| Insertion sort | O(n²) | O(1) | The list has fewer than a few dozen items or is nearly sorted. Merge sort implementations often switch to it for tiny pieces. |
Extra memory: O(n): the merged arrays and the slices together hold a few copies of the input at most. The recursion is only log₂ n calls deep, about 20 for a million items.
How the steps grow
- Merge sort, Quick sort, Heap sort
O(n log n) - Insertion sort
O(n²)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| Merge sort | 33 | 9,966 | 20 million |
| Quick sort | 33 | 9,966 | 20 million |
| Heap sort | 33 | 9,966 | 20 million |
| Insertion sort | 100 | 1 million | 1 trillion |
Inputs that need care
5 tricky inputs
- An empty list
[]or one item[3] items.length <= 1returns the same array at once. Note that for longer lists the result is a new array, while these two cases return the input itself.- Duplicates:
[4, 1, 4, 1] - The
<=inmergetakes the left copy first, so equal items keep their original order. The result is[1, 1, 4, 4]. - Already sorted or reversed lists
- Both take
O(n log n). Merge sort does not get faster on sorted data unless you add a check, such as skipping the merge whenleft's last item is<=right's first. - Objects, like tasks with a
duedate left[i] <= right[j]compares objects as strings or numbers, which is wrong. Pass a compare function and usecompare(left[i], right[j]) <= 0instead.- A very large list, like 5 million numbers
- The recursion is only about 23 calls deep, so the stack is safe. The memory is the problem:
sliceandresultallocate many short-lived arrays. A bottom-up version with one reusable buffer avoids that.
Variations and follow-ups
Twists interviewers add
Bottom-up merge sort
Skip the recursion: merge neighboring runs of length 1, then 2, then 4, until one run covers the list. With one extra buffer that is swapped back and forth with the input each round, it allocates only once.
Natural merge sort
Instead of splitting at fixed midpoints, find the runs that are already sorted in the input and merge those. A sorted list is one run and needs no merging, so it takes O(n). TimSort is a refined version of this idea.
K-way merge
Merge k sorted lists at once by keeping the front item of each in a min-heap. Each step pops the smallest and pushes the next item from the same list: O(n log k) in total. It is used to merge sorted files and paginated results.
Merge sort on linked lists
A linked list can be split with a slow and fast pointer and merged by relinking nodes, with no extra arrays. That makes merge sort the usual choice for sorting linked lists, where quick sort's random access is slow.
In an interview
Spot it, explain it, answer follow-ups
- Spot it when the problem says "stable", "linked list", "merge k sorted", or asks to count something about pairs across two halves, like inversions. Those are all merge steps in disguise.
- Explain it in two sentences: "merging two sorted lists is one linear pass, and a one-item list is sorted. So split in half, sort each half the same way, and merge." Then give the levels-times-work argument for
O(n log n). - Test by hand with an odd length (to check
middleand the base case), a list with duplicates (to check<=and stability), and a list where one half is used up early (to check the leftover loops). - Expect "can you do it with less memory?". Answer: bottom-up merge sort with one reused buffer still needs
O(n)extra space. True in-place merging exists but is complex and slower, so heap sort is the usual answer when memory must beO(1).
Where frontend code uses it
The same idea in real UI code
- V8's
Array.prototype.sortuses TimSort, a merge sort hybrid. Since ES2019 the language requiressortto be stable, which merge-based sorts give naturally. - Combining two already sorted lists, such as messages from a local cache and from the server (both sorted by time), is one merge step:
O(n)instead of sorting again. - Sorting data too large to hold at once, such as a big CSV export processed in chunks, sorts each chunk and then merges them.