Skip to content

Merge Sort

Every algorithm in the previous lesson does roughly n2n^2 comparisons in the worst case. Merge sort breaks that ceiling with one idea: split the problem in half, solve each half, then spend a single linear pass merging the two sorted halves back together. That’s divide and conquer, and it’s the same pattern behind binary search, quicksort, and a huge share of efficient algorithms you’ll meet later.

  • The merge step — combining two already-sorted lists into one sorted list in a single O(n)O(n) pass.
  • The recursive split — dividing the array in half until pieces are trivially sorted (size 0 or 1).
  • Why the recurrence T(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n) solves to O(nlogn)O(n \log n).
  • Why merge sort is stable and needs O(n)O(n) extra space — unlike the in-place O(n2)O(n^2) sorts from the last lesson.
  • Where Python’s own sorted() fits into this story.

Merge sort is rarely the literal ask. The merge step is, and so is the divide-and-conquer count.

When it is the wrong tool. For an in-memory array in Python, list.sort() is Timsort — which is a merge sort, tuned for real data, written in C. Hand-rolling loses. If you need O(1)O(1) space on an array, merge sort’s O(n)O(n) buffer rules it out; use heap sort. And if you only need the kth element, quickselect is O(n)O(n).

The merge step: combine two sorted lists in one pass

Section titled “The merge step: combine two sorted lists in one pass”

If you already have two sorted lists, you never need to look backward to merge them: walk both with a pointer, always take the smaller of the two current fronts, and advance that pointer. When one list runs out, the rest of the other list is already in order — just copy it over.

merge_step.py
def merge(left, right):
    merged = []
    i = j = 0
 
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            merged.append(left[i])
            i += 1
        else:
            merged.append(right[j])
            j += 1
 
    merged.extend(left[i:])    # copy whatever's left, if anything
    merged.extend(right[j:])
    return merged
 
 
left = [1, 4, 7]
right = [2, 3, 9]
print(merge(left, right))   # expect [1, 2, 3, 4, 7, 9]

Every element is looked at exactly once, so merge runs in O(n)O(n) time where nn is the combined length of both lists — this is the linear “combine” step that makes the whole algorithm work.

sketch Merging two sorted lists p5.js
Two pointers walk left and right. The smaller of the two current fronts (amber) is copied into merged -- blue if it came from left, green if it came from right. Once one side runs out, the rest of the other side is copied straight across.

Merge only helps if you already have two sorted halves. Merge sort gets those halves the same way it got the whole array sorted: recursively. Split the array in half, sort each half (recursively), then merge the results. The recursion bottoms out at size 0 or 1 — a list of one element is trivially already sorted.

merge_sort.py
def merge(left, right):
    merged = []
    i = j = 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            merged.append(left[i])
            i += 1
        else:
            merged.append(right[j])
            j += 1
    merged.extend(left[i:])
    merged.extend(right[j:])
    return merged
 
 
def merge_sort(arr):
    if len(arr) <= 1:          # base case: already sorted
        return arr
 
    mid = len(arr) // 2
    left = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])
    return merge(left, right)
 
 
nums = [8, 3, 5, 1, 9, 2]
print(merge_sort(nums))   # expect [1, 2, 3, 5, 8, 9]

The recursion builds a tree log2n\log_2 n levels deep — each level does a total of O(n)O(n) merge work across all its calls, since every element is touched exactly once per level:

diagram Merge sort recursion tree for [8, 3, 5, 1] mermaid

Splitting stops at single elements (the base case), and then merging climbs back up: [8] and [3] merge into [3, 8]; [5] and [1] merge into [1, 5]; finally [3, 8] and [1, 5] merge into [1, 3, 5, 8].

Every recursive call splits the array in half and does O(n)O(n) work in the merge step to recombine the results. That’s exactly the shape of recurrence covered in Recurrences and the Master Theorem:

T(n)=2T ⁣(n2)+O(n)T(n) = 2\,T\!\left(\frac{n}{2}\right) + O(n)

There are log2n\log_2 n levels of recursion (each halving shrinks nn down to 1 after log2n\log_2 n steps), and every level does O(n)O(n) total merge work across all the calls at that depth. Multiply the two together:

T(n)=O(nlogn)T(n) = O(n \log n)

Unlike bubble, selection, and insertion sort, this bound holds in the best, average, and worst case — merge sort never degrades, because the split is always exactly in half regardless of the input’s order.

PickFromWhy
1left1 <= 2
2right4 > 2
4left4 <= 4 — the tie goes left
4rightleft is exhausted at this key
6right7 > 6
7left7 <= 8
8rightdrained from the right tail

Result [1, 2, 4, 4, 6, 7, 8] in 7 picks for 7 elements — one comparison-and-append per output slot, which is the O(n)O(n) merge. The final out += l[i:]; out += r[j:] is not an optimisation: when one side runs out, the other is already sorted and can be appended wholesale.

The tie on row 3 is where stability lives. 4 <= 4 takes from the left, and the left half holds the elements that came earlier in the original array. Change it to < and the right-hand 4 goes first:

ComparisonMerging [(1,'a')] with [(1,'b')]
l[i] <= r[j][(1,'a'), (1,'b')]stable
l[i] < r[j][(1,'b'), (1,'a')] — not stable

Verified. One character decides whether merge sort keeps its defining property. Nothing crashes, and the output is still correctly sorted — the only symptom is that equal elements swap, which surfaces much later in a multi-key sort that depended on the earlier pass.

Why the recursion is O(nlogn)O(n \log n)

Section titled “Why the recursion is O(nlog⁡n)O(n \log n)O(nlogn)”

The tree has log2n\log_2 n levels, because halving n reaches 1 after that many steps. Every level merges a total of n elements — the pieces get smaller but there are proportionally more of them. So the work is n per level times log2n\log_2 n levels.

For n = 8: levels of size 8, 4+4, 2+2+2+2, 1x8 — four levels, 8 elements merged at each. The recursion tree’s shape is fixed by the input size alone, which is why merge sort has no bad input: its best, average and worst cases are all Θ(nlogn)\Theta(n \log n). That is the property quicksort lacks and the reason merge sort is what you reach for when the bound must be a guarantee.

CaseComplexity
BestO(nlogn)O(n \log n)
AverageO(nlogn)O(n \log n)
WorstO(nlogn)O(n \log n)
SpaceO(n)O(n)
Stable?Yes
VariantThe changeCanonical problem
Merge two sorted listsThe merge step alone21 · 88
Merge k sorted listsHeap of heads, or balanced pairwise merging — both O(Nlogk)O(N \log k)23
Merge in place from the backWrite into the tail of the larger array, right to left, so nothing is overwritten88 Merge Sorted Array
Count inversionsAdd count += len(l) - i when taking from the right493 Reverse Pairs
Count smaller elements to the rightMerge sort on (value, original_index) pairs, accumulating per index315
Sort a linked listSplit with fast/slow pointers, merge by rewiring — O(1)O(1) extra space148
External sortSort chunks that fit in memory, then k-way merge the files
TimsortMerge sort that finds existing runs and uses insertion sort below ~64 elementsPython’s list.sort
Bottom-up merge sortIterate widths 1, 2, 4, 8 — no recursion, so no stack limit

Generated from the problem database, so each entry carries its sheet membership and reported companies. Tick them off as you go — progress is saved in this browser, and the Export button writes it to a file you can keep.

4 problems
1 easy1 medium2 hard

Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.

Each exercise is the actual LeetCode problem with its real method signature and LeetCode’s own examples as the test. Write the body, press Run, and match the expected output.

Problem. nums1 has length m + n, with its first m slots holding sorted values and the rest set to 0. Merge the n sorted values of nums2 into nums1 in place.

Constraints. 0 <= m, n <= 200, both inputs sorted non-decreasing.

Examples. nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3 gives [1,2,2,3,5,6]

Editorial

This is the merge step of merge sort, with the twist that the output must go into one of the inputs.

Time O(m+n)O(m + n). Space O(1)O(1).

Merging backwards is the key. Forwards, writing to nums1[0] would clobber a value still needed. Backwards, the write pointer starts in the zero-padding and always stays at or ahead of both readers — so a collision is impossible.

Two details:

  • while j >= 0 is the right loop condition. Once nums2 is exhausted, any remaining nums1 values are already in their correct positions, so no copying is needed. Looping on i >= 0 or j >= 0 also works but does pointless writes.
  • i >= 0 inside the comparison guards the case where nums1’s values run out first and only nums2 remains.

([0], 0, [1], 1) is that case: m = 0, so everything comes from nums2.

Follow-ups: “Why not use sorted(nums1[:m] + nums2)?” — correct in Python and O((m+n)log(m+n))O((m+n)\log(m+n)), but it defeats the point and is not in place. “Merge k sorted arrays?” — a heap; see K-way Merge. “What if nums1 had no spare room?” — you would need O(n)O(n) extra space, or the much harder in-place merge algorithms.

LC 977 — Squares of a Sorted Array · Easy

Section titled “LC 977 — Squares of a Sorted Array · Easy”

Problem. Given an array sorted in non-decreasing order, return an array of the squares of each number, also sorted non-decreasing. Aim for O(n)O(n).

Constraints. 1 <= len(nums) <= 10^4, -10^4 <= nums[i] <= 10^4, sorted.

Examples. [-4,-1,0,3,10] gives [0,1,9,16,100] · [-7,-3,2,3,11] gives [4,9,9,49,121]

Editorial

Squaring destroys the ordering in the middle but preserves a useful fact: the input is sorted, so the largest magnitude is at one of the two ends. Comparing the ends and emitting the larger square — into the output’s back — is exactly a merge of two sorted sequences: the negatives read right-to-left and the non-negatives left-to-right.

Time O(n)O(n). Space O(n)O(n) for the output.

while left <= right (not <) matters: with an odd length the two pointers meet on the middle element, which still needs writing. [-1] and [1] are the minimal tests.

[-2,-1] giving [1,4] is the all-negative case, where the left end holds the largest square throughout.

The obvious sorted(x*x for x in nums) is O(nlogn)O(n \log n) and perfectly fine to mention — the problem explicitly asks for an O(n)O(n) alternative, which is the whole exercise.

Follow-ups: “Do it in place?” — awkward, since squares can exceed the space freed; O(n)O(n) output is expected. “What if the input were not sorted?” — then O(nlogn)O(n \log n) is unavoidable. “Cubes instead of squares?” — cubing preserves order, so the answer is just the mapped array.

Problem. Sort a linked list in O(nlogn)O(n \log n) time and, ideally, O(1)O(1) extra space.

Constraints. 0 <= number of nodes <= 5 * 10^4, -10^5 <= Node.val <= 10^5.

Examples. [4,2,1,3] gives [1,2,3,4] · [-1,5,3,4,0] gives [-1,0,3,4,5] · [] gives []

Editorial

Merge sort is the right choice for a linked list, because both of its operations — splitting and merging — need only sequential access. Quicksort needs random access to partition efficiently, so it fares badly here.

Time O(nlogn)O(n \log n). Space O(logn)O(\log n) for the recursion (a bottom-up iterative version reaches genuine O(1)O(1)).

Two details decide whether it works:

  • fast = head.next, not head. With fast = head and two nodes, slow stays at head, mid is the second node, and the recursion makes no progress — infinite recursion. Starting fast one ahead splits [2,1] into [2] and [1]. That case is in the tests for exactly this reason.
  • slow.next = None. Without the cut the first half still runs into the second, and the recursion never shrinks. This is the same warning as in Reorder List.

This composes three things already covered: fast/slow pointers to find the middle, a dummy head to merge, and the divide-and-conquer skeleton.

Follow-ups: “Truly O(1)O(1) space?” — bottom-up merge sort, merging runs of size 1, 2, 4, … iteratively. “Why not quicksort?” — no random access, and worst-case O(n2)O(n^2). “Copy the values into an array and sort?” — works, O(n)O(n) space, and defeats the exercise.

They askWhat they’re checkingThe answer
“Why merge sort over quicksort?”Knowing the tradeGuaranteed Θ(nlogn)\Theta(n \log n) on every input, and stable. Quicksort risks O(n2)O(n^2) and is not stable. You pay O(n)O(n) space and a worse constant factor
“Prove it is O(nlogn)O(n \log n)The recursion-tree argumentlog2n\log_2 n levels because halving reaches 1 in that many steps, and every level merges n elements in total. Crucially the tree’s shape depends on n alone, so no input can unbalance it
“Is it stable? Why?”The mechanism, not the labelYes, because the merge takes from the left on a tie (<=). Verified: switching to < reverses two equal elements while still producing sorted output
“Space complexity?”PrecisionO(n)O(n) for the merge buffer plus O(logn)O(\log n) for the stack. Only the linked-list version is O(1)O(1) extra, since merging there is pointer rewiring
“Sort a linked list in O(nlogn)O(n \log n)Recognising the natural fitMerge sort: split with fast/slow pointers, merge by rewiring. No random access needed, which rules out quicksort and heap sort. LC 148
“Count how many pairs are out of order”Whether you see the free resultInversion counting during the merge: count += len(left) - i when taking from the right. LC 493, 315 — and there is no simpler O(nlogn)O(n \log n) route
“The data does not fit in memory”External sortingSort chunks that do fit, write them out, then k-way merge the files with a heap. This is what merge sort is actually used for at scale
“What does Python’s sorted use?”Stdlib fluencyTimsort — a merge sort that detects existing runs and insertion-sorts short ones. Which is why nearly-sorted input is close to O(n)O(n) in practice
“Reduce the allocation”Implementation qualityAllocate one scratch array up front and merge between it and the original, alternating direction per level. Slicing at every level costs O(nlogn)O(n \log n) in allocation on top of the sort
“Can merge sort be done in O(1)O(1) space on an array?”Honesty about the hard caseIn-place merging exists but is complex and slow enough that nobody uses it. The practical answer is: no, use heap sort if O(1)O(1) space is the requirement
  • Using < instead of <= in the merge comparison. This is the one that matters: verified, merging [(1,'a')] with [(1,'b')] gives [(1,'a'),(1,'b')] with <= and the reverse with <. The output is still sorted, so nothing fails visibly — the loss only shows up in a later multi-key sort that relied on the earlier order.
  • Forgetting to drain both tails. After the main loop exactly one side has leftovers, but writing only out += l[i:] silently truncates whenever the right side is the longer one. Append both; one is always empty.
  • Allocating a new buffer at every recursion level. Correct but wasteful. Real implementations allocate one scratch array up front and merge into alternating halves.
  • Claiming O(1)O(1) space for the array version. It is O(n)O(n) for the buffer plus O(logn)O(\log n) for the stack. Only the linked-list version is O(1)O(1) extra, because merging is pointer rewiring.
  • Recursion depth on a large list. log2n\log_2 n frames is fine — 20 at a million — so this is the one common sort that does not risk a RecursionError. Worth knowing so you do not offer a fix that is not needed.
  • Splitting with mid = len(arr) // 2 and then slicing. Slices copy, so an O(nlogn)O(n \log n) algorithm gains an O(nlogn)O(n \log n) allocation cost on top. Passing (lo, hi) indices avoids it.
  • Merging in place from the front on LC 88. Writing forward overwrites unread elements. Fill from the back of the destination array, right to left, where the spare capacity already is.
  • Off-by-one in the inversion count. It is len(left) - i, the number of left-half elements still unconsumed, added when you take from the right. Using i counts the consumed ones instead.
pch.quizTag pch.quizDefaultTitle
  1. In the merge step, why is the comparison `left[i] <= right[j]` rather than `<`?

    pch.quizShowAnswer

    B — It preserves stability: on a tie the left half wins, and the left half holds the elements that came earlier — Verified: merging [(1,'a')] with [(1,'b')] gives [(1,'a'),(1,'b')] with <= and [(1,'b'),(1,'a')] with <. Both outputs are correctly sorted, which is what makes this dangerous -- nothing fails until a later multi-key sort depends on the earlier ordering surviving. Stability is merge sort's defining advantage and one character controls it.

  2. Why is merge sort O(n log n) in the best, average AND worst case?

    pch.quizShowAnswer

    B — The recursion tree's shape depends only on n -- log n levels, n elements merged per level -- so no input can unbalance it — Halving is driven by the array length, never by the values, so there is no adversarial input. That is exactly the property quicksort lacks -- its partition depends on the data, which is why sorted input costs it O(n^2). When the bound has to be a guarantee, this is why merge sort is the answer.

  3. The merge loop ends and you write `out += left[i:]`. What is missing?

    pch.quizShowAnswer

    B — `out += right[j:]` as well; exactly one side has leftovers and it may be either — The loop exits as soon as *either* index runs out, so the survivor may be either side -- in the traced merge of [1,4,7] and [2,4,6,8] it was the right, leaving [8]. Append both; one is always empty, so it costs nothing. Draining only one silently drops elements whenever you guess wrong.

  4. What is merge sort's space complexity on an array?

    pch.quizShowAnswer

    B — O(n) for the merge buffer plus O(log n) for the recursion stack — The O(n) buffer is unavoidable for arrays -- merging cannot be done in place efficiently -- and it is the reason to reach for heap sort when O(1) space is required. A naive implementation allocating fresh lists at every level does cost O(n log n) in total allocation; real ones use a single scratch array. The linked-list version is genuinely O(1) extra, since merging is pointer rewiring.

  5. How do you count inversions during a merge sort?

    pch.quizShowAnswer

    B — When taking an element from the RIGHT half, add len(left) - i -- the left elements still unconsumed — Every left-half element still waiting is both greater than the one you just took and earlier in the original array, so all of those inversions are discovered in a single addition. That is what makes LC 493 and 315 tractable -- there is no simpler O(n log n) route. Using `i` counts the consumed elements instead, which is the mirror image and wrong.

  6. LC 88 asks you to merge into the first array, which has spare capacity at the end. Which direction do you write?

    pch.quizShowAnswer

    B — Backwards from the end, largest first -- the spare capacity is there, so nothing unread is overwritten — Writing forward overwrites elements of the first array that have not been read yet, so you would need a copy -- which defeats the point of the in-place framing. Filling from the back means every slot you write is either spare capacity or a slot whose value you have already consumed.

  7. Should you worry about RecursionError when merge sorting a million elements in Python?

    pch.quizShowAnswer

    B — No -- the depth is log2(n), about 20 at a million, far below the ~1,000-frame limit — Merge sort halves, so the depth is logarithmic and comfortably safe -- unlike quicksort, whose worst-case depth is n-1 and does hit the limit on adversarial input. Knowing this matters so you do not offer a fix that is not needed; a bottom-up iterative merge sort exists, but stack depth is not the reason to want it.

  • Merge sort = split in half, sort both, merge. The merge is what interviews actually ask for.
  • The merge is one pass, O(n)O(n): compare the two fronts, append the smaller, then drain both tails — exactly one is non-empty.
  • <=, not <. The tie must go left to keep stability. Verified: < swaps equal elements while still producing sorted output, so the loss is silent.
  • Θ(nlogn)\Theta(n \log n) in every case — the tree’s shape depends on n alone, so there is no bad input. That is the guarantee quicksort lacks.
  • Space is O(n)O(n) buffer + O(logn)O(\log n) stack on arrays. Only the linked-list version is O(1)O(1) extra.
  • Depth is log2n\log_2 n — about 20 at a million, so no RecursionError risk.
  • Inversion counting is free: taking from the right adds len(left) - i. This is LC 493 and 315, and there is no simpler O(nlogn)O(n \log n) way.
  • LC 88 merges backwards, into the spare capacity at the end.
  • Merge sort is the stable, guaranteed O(nlogn)O(n \log n) sort — and Python’s list.sort is Timsort, which is a merge sort that finds existing runs.
  • External sorting is merge sort: sort runs that fit in memory, then k-way merge.
  • Merge combines two sorted lists in one O(n)O(n) pass by always taking the smaller current front.
  • Merge sort recursively splits in half down to single elements, then merges back up — the recurrence T(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n) solves to O(nlogn)O(n \log n).
  • It’s stable (O(n)O(n) time, guaranteed, in every case) but needs O(n)O(n) extra space — unlike the O(1)O(1) in-place sorts from the previous lesson.
  • Python’s sorted() uses Timsort, a hybrid built on these same ideas plus insertion sort for small runs — covered in a later lesson.

Next: Quick Sort — another divide-and-conquer sort, in-place this time, with an average case just as fast but a worst case that depends entirely on how you pick the pivot.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading