Merge sort

Split the array in half until each piece is sorted, then merge the pieces back in order.

Pattern: Divide and conquer25 min read
Quick recall3 questions
  1. 01Which of the sorts from the last lesson is fast on nearly sorted data, and why?

    Insertion sort. Each card stops as soon as it meets a smaller one, so most cards move zero or one slot.

  2. 02What does a stable sort promise?

    Equal values keep the order they had before the sort.

  3. 03In recursion, when does the code after the recursive call run?

    After the call returns, on the way back up the call stack.

The three sorts from the last lesson all do about n² work. At n = 10⁵ that is 10 billion steps, far over the budget. Merge sort does the same job in under 2 million comparisons.

The idea is short. The part that confuses most learners is the recursion: how can you "sort each half" when nothing is sorted yet? The answer is the base case, and this lesson draws it.

Read the problem

Problem: Merge Sortingn ≤ 10⁵
it asks
Sort the array in increasing order. This time the array can hold 100,000 numbers.
example
in a = [13, 46, 24, 52, 20, 9] out [9, 13, 20, 24, 46, 52]
limits
  • 1 ≤ n ≤ 10⁵
  • −10⁹ ≤ a[i] ≤ 10⁹
  • values can repeat
budget
About 10⁸ steps run in one second. With n up to 10⁵, aim for O(n log n) or faster.
Which pattern? Decide first, then open.

“sort, n up to 10⁵”O(n²) is out: split in half and merge

pattern: Divide and conquer

It is the same problem as in the last lesson. Only the limit changed, and the limit decides the approach:

n = 10⁵about 10⁸ steps fit in 1 second
  • O(n log n)fits
  • O(n²)too slow

The line marks 10⁸ steps. Aim for O(n log n) or faster.

At n = 10⁵, O(n²) is 10¹⁰ steps. O(n log n) is about 1.7 million.

The questions to ask are the same as in the last lesson: repeated values, negative values, one item. One new question matters here: may you use extra memory? Merge sort needs a second array.

The slow way

Any sort from the last lesson works, but it is too slow. A run of insertion sort on 100,000 random numbers made about 2.5 billion comparisons and took about 3 seconds in Node.js.

The waste: every item gets compared with almost every other item, one at a time.

The key insight: merging sorted piles is cheap

Picture two piles of exam papers, each already sorted by roll number. To make one sorted pile:

  1. Look at the top paper of each pile.
  2. Take the smaller one and put it on the new pile.
  3. Repeat until both piles are empty.

Each paper is touched once. Two sorted piles of total size n merge in about n steps, not n².

Here is that merge on our example, with both halves already sorted. i reads the left half and j reads the right half:

merge two sorted halvescomparisons 0
13
0
24
1
46
2
9
3
20
4
52
5
temp{}

1/8Left half 13 24 46 and right half 9 20 52 are each sorted. i and j start at their fronts.

The smaller front value always goes next. Neither pointer ever moves back.

So if the two halves were sorted, one cheap merge would finish the job. How do you sort each half? The same way: split it and merge it.

Keep splitting until each piece has one item. One item is always sorted. That is the base case.

Divide and conquer

This plan has a name: . Merge sort does it in two moves.

Split on the way down

Read top to bottom. Each piece splits in half until it holds one item. No values are compared yet.

Merge on the way back up

Read top to bottom, the same pieces in the same places. Each pair of sorted pieces merges into one. The last merge is the dry run above.

All the real work happens in the merges, after the recursive calls return. On our six numbers, the five merges made 10 comparisons in total.

The code

The split does not copy the array. It works on index ranges: low and high mark the piece this call sorts.

void mergeSort(int[] a, int low, int high) {
    if (low >= high) return;
    int mid = (low + high) / 2;
    mergeSort(a, low, mid);
    mergeSort(a, mid + 1, high);
    merge(a, low, mid, high);
}
  • Base case: low >= high means the piece has one item (or none). It is sorted.
  • mid splits the piece. The left half is low..mid, the right half is mid + 1..high.
  • The two calls sort each half. Then merge combines them.
  • The first call: TypeScript fills in low and high by default. Java and Python have no default that can read the array, so call mergeSort(a, 0, a.length - 1) or merge_sort(a, 0, len(a) - 1).
  • Integer division: Java's / drops the fraction on int values. Python needs //, and TypeScript needs Math.floor.
void merge(int[] a, int low, int mid, int high) {
    int[] temp = new int[high - low + 1];
    int i = low, j = mid + 1, k = 0;
    while (i <= mid && j <= high) {
        if (a[i] <= a[j]) temp[k++] = a[i++];
        else temp[k++] = a[j++];
    }
    while (i <= mid) temp[k++] = a[i++];
    while (j <= high) temp[k++] = a[j++];
    for (k = 0; k < temp.length; k++) a[low + k] = temp[k];
}
  • temp collects the merged piece. Java sizes it once, high - low + 1 slots, and fills it with k.
  • i is the next unread item of the left half, j of the right half.
  • The first loop copies the smaller front value. The <= takes the left one on a tie, so equal values keep their order. That makes merge sort stable.
  • The two short loops copy whatever is left. Only one of them does any work. Python copies each leftover part in one extend call.
  • The last step writes temp back over a[low..high]. Python does it with one slice assignment.

Common mistakes

  • < instead of <= in the merge. The output is still sorted, but equal values can swap order. The sort is no longer stable.
  • Splitting into low..mid and mid..high. For a piece of two items, the right call gets the same piece again, and the recursion never ends.
  • Passing the length as high. high is the last index, so the first call passes the length minus 1.
  • Dropping the leftover copies. The values left in one half never reach temp.

Why it is O(n log n)

Every level of merging moves all n items once. Halving gives log n levels.

Count it out loud:

  • Work per level: the merges on one level touch every item once. That is n.
  • Number of levels: each split halves the pieces. You can halve n about log n times before you reach 1. For n = 10⁵, that is 17.
  • Total: n × log n. For 10⁵ items, about 1.7 million steps.

The split does not depend on the data. So sorted, reversed and random arrays all cost O(n log n). There is no bad input.

Space. temp holds up to n items, and the call stack holds about log n frames. Extra space is O(n). This is the price merge sort pays.

Side by side

Measured on the same 100,000 random numbers in Node.js:

SortIdeaTimeExtra spaceComparisonsRun time
Insertionslide each card leftO(n²)O(1)about 2.5 billionabout 3 s
Mergesplit, then mergeO(n log n)O(n)about 1.5 millionabout 11 ms

How to spot merge sort

Signal in the problemWhy merge sort
sort, n up to 10⁵ or 10⁶O(n log n) on every input
keep equal items in orderit is stable
sort a linked listmerging needs no jumps by index
count pairs that are out of ordercount them during the merge

The last one has its own lesson later: "Count inversions with merge sort". The merge step sees exactly which left items are bigger than a right item.

The template to remember: base case, split at mid, sort left, sort right, merge.

take U forwardYouTube
Merge Sort: Algorithm, Pseudocode, Dry Run, Code: Striver's A2Z DSA Course
Remember4 points
  1. 01

    Two sorted piles merge in one pass. So split the array until every piece has one item, then merge the pieces back up.

  2. 02

    log n levels, n work per level: O(n log n) on every input, about 1.7 million steps for n = 10⁵.

  3. 03

    The price is O(n) extra space for the temporary array.

  4. 04

    Taking the left item on a tie (<=) makes merge sort stable, which is why JavaScript engines build on it.

Read more

Next: quick sort, which skips the extra array by splitting around a pivot instead.

Practice

Solve these on the sheet after the lesson. Try each one for 20 minutes before you open its solution.

Striver's A2Z sheet0/1 solved
Open the sheet