Merge Sort
Every algorithm in the previous lesson does roughly 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.
What you’ll learn
Section titled “What you’ll learn”- The merge step — combining two already-sorted lists into one sorted list in a single pass.
- The recursive split — dividing the array in half until pieces are trivially sorted (size 0 or 1).
- Why the recurrence solves to .
- Why merge sort is stable and needs extra space — unlike the in-place sorts from the last lesson.
- Where Python’s own
sorted()fits into this story.
The cue
Section titled “The cue”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 space on
an array, merge sort’s buffer rules it out; use heap sort. And if you only
need the kth element,
quickselect is .
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.
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 time
where is the combined length of both lists — this is the linear “combine”
step that makes the whole algorithm work.
The recursive split
Section titled “The recursive split”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.
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 levels deep — each level does a total of merge work across all its calls, since every element is touched exactly once per level:
graph TD
N0["[8, 3, 5, 1]"] --> N1["[8, 3]"]
N0 --> N2["[5, 1]"]
N1 --> N3["[8]"]
N1 --> N4["[3]"]
N2 --> N5["[5]"]
N2 --> N6["[1]"]
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].
Why this is O(n log n)
Section titled “Why this is O(n log n)”Every recursive call splits the array in half and does work in the merge step to recombine the results. That’s exactly the shape of recurrence covered in Recurrences and the Master Theorem:
There are levels of recursion (each halving shrinks down to 1 after steps), and every level does total merge work across all the calls at that depth. Multiply the two together:
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.
Dry run
Section titled “Dry run”One merge of [1, 4, 7] and [2, 4, 6, 8]
Section titled “One merge of [1, 4, 7] and [2, 4, 6, 8]”| Pick | From | Why |
|---|---|---|
| 1 | left | 1 <= 2 |
| 2 | right | 4 > 2 |
| 4 | left | 4 <= 4 — the tie goes left |
| 4 | right | left is exhausted at this key |
| 6 | right | 7 > 6 |
| 7 | left | 7 <= 8 |
| 8 | right | drained 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 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:
| Comparison | Merging [(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
Section titled “Why the recursion is O(nlogn)O(n \log n)O(nlogn)”The tree has 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 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 . That is the property quicksort lacks
and the reason merge sort is what you reach for when the bound must be a guarantee.
Time and space complexity
Section titled “Time and space complexity”| Case | Complexity |
|---|---|
| Best | |
| Average | |
| Worst | |
| Space | |
| Stable? | Yes |
The variant map
Section titled “The variant map”| Variant | The change | Canonical problem |
|---|---|---|
| Merge two sorted lists | The merge step alone | 21 · 88 |
Merge k sorted lists | Heap of heads, or balanced pairwise merging — both | 23 |
| Merge in place from the back | Write into the tail of the larger array, right to left, so nothing is overwritten | 88 Merge Sorted Array |
| Count inversions | Add count += len(l) - i when taking from the right | 493 Reverse Pairs |
| Count smaller elements to the right | Merge sort on (value, original_index) pairs, accumulating per index | 315 |
| Sort a linked list | Split with fast/slow pointers, merge by rewiring — extra space | 148 |
| External sort | Sort chunks that fit in memory, then k-way merge the files | — |
| Timsort | Merge sort that finds existing runs and uses insertion sort below ~64 elements | Python’s list.sort |
| Bottom-up merge sort | Iterate widths 1, 2, 4, 8 — no recursion, so no stack limit | — |
LeetCode problem set
Section titled “LeetCode problem set”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.
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.
- 21Merge Two Sorted ListseasyThe `merge` step above, applied to a linked list instead of a Python list
- 912Sort an ArraymediumImplement merge sort (or any $O(n \log n)$ sort) from scratch
- 315Count of Smaller Numbers After SelfhardA classic "augment the merge step" problem: count cross-inversions while merging
- 493Reverse Pairshard
Practice — real LeetCode problems
Section titled “Practice — real LeetCode problems”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.
LC 88 — Merge Sorted Array · Easy
Section titled “LC 88 — Merge Sorted Array · Easy”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 . Space .
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 >= 0is the right loop condition. Oncenums2is exhausted, any remainingnums1values are already in their correct positions, so no copying is needed. Looping oni >= 0 or j >= 0also works but does pointless writes.i >= 0inside the comparison guards the case wherenums1’s values run out first and onlynums2remains.
([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
, 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 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 .
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 . Space 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 and perfectly fine to
mention — the problem explicitly asks for an alternative, which is the whole
exercise.
Follow-ups: “Do it in place?” — awkward, since squares can exceed the space freed; output is expected. “What if the input were not sorted?” — then is unavoidable. “Cubes instead of squares?” — cubing preserves order, so the answer is just the mapped array.
LC 148 — Sort List · Medium
Section titled “LC 148 — Sort List · Medium”Problem. Sort a linked list in time and, ideally, 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 . Space for the recursion (a bottom-up iterative version reaches genuine ).
Two details decide whether it works:
fast = head.next, nothead. Withfast = headand two nodes,slowstays athead,midis the second node, and the recursion makes no progress — infinite recursion. Startingfastone 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 space?” — bottom-up merge sort, merging runs of size 1, 2, 4, … iteratively. “Why not quicksort?” — no random access, and worst-case . “Copy the values into an array and sort?” — works, space, and defeats the exercise.
Interview follow-ups
Section titled “Interview follow-ups”| They ask | What they’re checking | The answer |
|---|---|---|
| “Why merge sort over quicksort?” | Knowing the trade | Guaranteed on every input, and stable. Quicksort risks and is not stable. You pay space and a worse constant factor |
| “Prove it is ” | The recursion-tree argument | 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 label | Yes, because the merge takes from the left on a tie (<=). Verified: switching to < reverses two equal elements while still producing sorted output |
| “Space complexity?” | Precision | for the merge buffer plus for the stack. Only the linked-list version is extra, since merging there is pointer rewiring |
| “Sort a linked list in ” | Recognising the natural fit | Merge 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 result | Inversion counting during the merge: count += len(left) - i when taking from the right. LC 493, 315 — and there is no simpler route |
| “The data does not fit in memory” | External sorting | Sort 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 fluency | Timsort — a merge sort that detects existing runs and insertion-sorts short ones. Which is why nearly-sorted input is close to in practice |
| “Reduce the allocation” | Implementation quality | Allocate one scratch array up front and merge between it and the original, alternating direction per level. Slicing at every level costs in allocation on top of the sort |
| “Can merge sort be done in space on an array?” | Honesty about the hard case | In-place merging exists but is complex and slow enough that nobody uses it. The practical answer is: no, use heap sort if space is the requirement |
Pitfalls
Section titled “Pitfalls”- 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 space for the array version. It is for the buffer plus for the stack. Only the linked-list version is extra, because merging is pointer rewiring.
- Recursion depth on a large list. 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) // 2and then slicing. Slices copy, so an algorithm gains an 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. Usingicounts the consumed ones instead.
Self-check
Section titled “Self-check”-
In the merge step, why is the comparison `left[i] <= right[j]` rather than `<`?
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.
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.
-
Why is merge sort O(n log n) in the best, average AND worst case?
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.
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.
-
The merge loop ends and you write `out += left[i:]`. What is missing?
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.
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.
-
What is merge sort's space complexity on an array?
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.
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.
-
How do you count inversions during a merge sort?
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.
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.
-
LC 88 asks you to merge into the first array, which has spare capacity at the end. Which direction do you write?
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.
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.
-
Should you worry about RecursionError when merge sorting a million elements in Python?
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.
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.
Recall card
Section titled “Recall card”- Merge sort = split in half, sort both, merge. The merge is what interviews actually ask for.
- The merge is one pass, : 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.- in every case — the tree’s shape depends on
nalone, so there is no bad input. That is the guarantee quicksort lacks. - Space is buffer + stack on arrays. Only the linked-list version is extra.
- Depth is — about 20 at a million, so no
RecursionErrorrisk. - Inversion counting is free: taking from the right adds
len(left) - i. This is LC 493 and 315, and there is no simpler way. - LC 88 merges backwards, into the spare capacity at the end.
- Merge sort is the stable, guaranteed sort — and Python’s
list.sortis 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 pass by always taking the smaller current front.
- Merge sort recursively splits in half down to single elements, then merges back up — the recurrence solves to .
- It’s stable ( time, guaranteed, in every case) but needs extra space — unlike the 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.coffeeCtapch.feedbackHeading
pch.feedbackSubheading