06. Merge Sort — The O(n log n) Breakthrough
1669 words
8 min read
Visual companion
Python
Type and operator map
Python Week 1: the first filter for runtime behavior
View
Revision summary
What this note is really saying
Short form
# 06. Merge Sort — The O(n log n) Breakthrough > **What problem does this solve?** Selection sort and insertion sort are (O(n^2)) — they take ~10¹² operations for a million items.

06. Merge Sort — The O(n log n) Breakthrough
What problem does this solve? Selection sort and insertion sort are (O(n^2)) — they take ~10¹² operations for a million items. Merge sort is (O(n \log n)) — ~20 million operations for the same input. This is the first "fast" sorting algorithm that scales to large datasets.
1. The Divide-and-Conquer Strategy
Mental Model
You need to sort a pile of 1000 exam papers. Instead of scanning all 1000 papers repeatedly, you split the pile into two piles of 500. You ask two friends to sort each pile. When they return sorted piles, you merge them together in one quick pass.
Each friend splits their 500 into 250 each, and asks two more friends... This recursive splitting continues until you have piles of size 1 (which are trivially sorted). Then the merging begins.
The Two Key Insights
- Divide: A list of 0 or 1 elements is already sorted.
- Merge: Two sorted lists can be combined into one sorted list with a single pass ((O(n))). (Diagram)
2. The Merge Process
Mental Model
You have two sorted piles on your desk. You look at the top of each pile, pick the smaller one, and place it on the output pile. Repeat until both piles are empty.
(Diagram)
Implementation
python# runnable def merge(A, B): """Merge two sorted lists into one sorted list. Time: O(m + n) where m = len(A), n = len(B) Space: O(m + n) for the output """ m, n = len(A), len(B) C = [] i = j = 0 # Compare and merge while i < m and j < n: if A[i] <= B[j]: # Using <= makes merge sort stable C.append(A[i]) i += 1 else: C.append(B[j]) j += 1 # Copy remaining elements if i < m: C.extend(A[i:]) if j < n: C.extend(B[j:]) return C # Test merge A = [32, 43, 78] B = [13, 57, 63] print(f"Merged: {merge(A, B)}") # [13, 32, 43, 57, 63, 78] # Edge cases print(merge([], [1, 2, 3])) # [1, 2, 3] print(merge([1, 2, 3], [])) # [1, 2, 3] print(merge([1], [2])) # [1, 2] print(merge([2], [1])) # [1, 2]
Maximum Comparisons in Merge
When merging two lists of sizes m and n, the maximum number of comparisons is (m + n - 1). This happens when the last element of both lists is the largest, and you compare them right until the end.
python# runnable def merge_with_count(A, B): """Merge and count comparisons.""" m, n = len(A), len(B) C = [] i = j = 0 comparisons = 0 while i < m and j < n: comparisons += 1 if A[i] <= B[j]: C.append(A[i]) i += 1 else: C.append(B[j]) j += 1 # No more comparisons needed for remaining elements C.extend(A[i:]) C.extend(B[j:]) return C, comparisons # Worst case: alternating values → many comparisons print(merge_with_count([1, 3, 5], [2, 4, 6])) # ([1, 2, 3, 4, 5, 6], 5 comparisons = 3+3-1)
3. Full Merge Sort Implementation
python# runnable def merge_sort(arr): """Sort arr using merge sort. Returns new sorted list. Time: O(n log n) — best, average, and worst Space: O(n) — creates new lists for merging Stable: Yes Recurrence: T(n) = 2T(n/2) + O(n) → O(n log n) """ n = len(arr) # Base case: single element is already sorted if n <= 1: return arr # Divide mid = n // 2 left = merge_sort(arr[:mid]) # Sort left half right = merge_sort(arr[mid:]) # Sort right half # Conquer return merge(left, right) # Merge sorted halves # Test arr = [38, 27, 43, 3, 9, 82, 10] sorted_arr = merge_sort(arr) print(f"Original: {arr}") print(f"Sorted: {sorted_arr}")
Step-by-Step Trace for [38, 27, 43, 3, 9, 82, 10]
pseudomerge_sort([38, 27, 43, 3, 9, 82, 10]) ├── merge_sort([38, 27, 43, 3]) │ ├── merge_sort([38, 27]) │ │ ├── merge_sort([38]) → [38] │ │ └── merge_sort([27]) → [27] │ │ └── merge([38], [27]) → [27, 38] │ └── merge_sort([43, 3]) │ ├── merge_sort([43]) → [43] │ └── merge_sort([3]) → [3] │ └── merge([43], [3]) → [3, 43] │ └── merge([27, 38], [3, 43]) → [3, 27, 38, 43] └── merge_sort([9, 82, 10]) ├── merge_sort([9, 82]) │ ├── merge_sort([9]) → [9] │ └── merge_sort([82]) → [82] │ └── merge([9], [82]) → [9, 82] └── merge_sort([10]) → [10] └── merge([9, 82], [10]) → [9, 10, 82] └── merge([3, 27, 38, 43], [9, 10, 82]) → [3, 9, 10, 27, 38, 43, 82]
4. Complexity Analysis
Recursion Tree
(Diagram)
| Level | Subarrays | Size per subarray | Work per subarray | Total work |
|---|---|---|---|---|
| 0 | 1 | n | n | (n) |
| 1 | 2 | n/2 | n/2 | (n) |
| 2 | 4 | n/4 | n/4 | (n) |
| ... | ... | ... | ... | ... |
| (\log n) | (n) | 1 | 1 | (n) |
Total work = (n \times (\log n + 1) = O(n \log n))
Recurrence Solution
T(n)=2T(n/2)+n=2[2T(n/4)+n/2]+n=4T(n/4)+2n=4[2T(n/8)+n/4]+2n=8T(n/8)+3n=2kT(n/2k)+knWhen (n/2^k = 1) (i.e., (k = \log_2 n)):
5. Variations
3-Way Merge Sort
python# runnable def merge_sort_3way(arr): """3-way merge sort: divide into 3 equal parts.""" n = len(arr) if n <= 1: return arr mid1 = n // 3 mid2 = 2 * n // 3 left = merge_sort_3way(arr[:mid1]) middle = merge_sort_3way(arr[mid1:mid2]) right = merge_sort_3way(arr[mid2:]) return merge(merge(left, middle), right) # Still O(n log n) but with different constants
In-Place Merge Sort (Not Practical)
Standard merge sort requires (O(n)) extra space for merging. In-place merge sort exists but is complex and slower in practice.
Bottom-Up (Iterative) Merge Sort
python# runnable def merge_sort_bottom_up(arr): """Iterative merge sort — no recursion.""" n = len(arr) size = 1 while size < n: for left_start in range(0, n, 2 * size): mid = left_start + size right_end = min(left_start + 2 * size, n) if mid < right_end: # Merge arr[left_start:mid] and arr[mid:right_end] left = arr[left_start:mid] right = arr[mid:right_end] merged = merge(left, right) arr[left_start:right_end] = merged size *= 2 return arr arr = [38, 27, 43, 3, 9, 82, 10] print(f"Bottom-up: {merge_sort_bottom_up(arr)}")
6. When to Use Merge Sort
| ✅ Use when | ❌ Don't use when |
|---|---|
| You need guaranteed O(n log n) performance | Memory is limited (uses O(n) extra space) |
| You need a stable sort | The data fits easily in RAM and is small |
| You're sorting linked lists (no random access needed) | The data is nearly sorted (insertion sort is faster) |
| You're sorting large files (external sorting) | You need an in-place sort |
Practice Questions
Q1. Trace merge sort on [6, 4, 2, 8, 1, 5, 3, 7]. Show the merge tree.
Q2. How many comparisons does merge sort make in the worst case for n = 8?
Q3. Why is merge sort's worst case O(n log n) while quicksort's worst case is O(n²)?
Q4. Prove that merging two sorted lists of sizes m and n takes at most (m + n - 1) comparisons.
Q5. Write a function
merge_three(A, B, C) that merges three sorted lists.
Q6. What is the space complexity of merge sort? Why?
Q7. Can you implement merge sort without recursion? (Hint: bottom-up)
Q8. For n = 1,000,000, compare the approximate number of operations: merge sort vs selection sort.
Q9. Given two sorted lists A and B, find the number of pairs (a, b) such that a > b efficiently.
Q10. How would you external sort a 100 GB file that doesn't fit in RAM using merge sort principles?AnswersA1.pseudo├── [6, 4, 2, 8, 1, 5, 3, 7] │ ├── [6, 4, 2, 8] │ │ ├── [6, 4] → [4, 6] │ │ └── [2, 8] → [2, 8] │ │ └── merge → [2, 4, 6, 8] │ └── [1, 5, 3, 7] │ ├── [1, 5] → [1, 5] │ └── [3, 7] → [3, 7] │ └── merge → [1, 3, 5, 7] │ └── merge → [1, 2, 3, 4, 5, 6, 7, 8]A2. Worst case: At each merge of n elements, make n-1 comparisons. With log n levels, total ≈ 8 × 3 = 24 comparisons. More precisely: At level 1 (four merges of size 2): 4 × 1 = 4. Level 2 (two merges of size 4): 2 × 3 = 6. Level 3 (one merge of size 8): 1 × 7 = 7. Total = 17 comparisons.A3. Merge sort always divides exactly in half ((T(n) = 2T(n/2) + n)), guaranteeing log n levels. Quicksort can divide unevenly ((T(n) = T(n-1) + n)), creating n levels.A4. Each comparison places one element in the output. After (m+n-1) comparisons, at most (m+n-1) elements are placed. The last element needs no comparison (it's the only one left). So maximum comparisons = (m+n-1).A5. Merge A and B first, then merge the result with C. Or merge all three simultaneously with three pointers.A6. (O(n)). Each merge creates a new list. But the total memory at any recursion depth is (O(n)) (all active merges together total n).A7. Yes — bottom-up merge sort (shown above) starts with size=1 and doubles, avoiding recursion.A8. Merge sort: ~20 million operations (n log₂ n). Selection sort: ~5 × 10¹¹ operations (n²). Merge sort is ~25,000 times faster.A9. Use binary search: For each element in A, find how many in B are less than it. Complexity O(m log n).A10. Divide into chunks that fit in RAM (e.g., 1 GB). Sort each chunk (using merge sort). Then use a k-way merge (priority queue) to merge all sorted chunks into one file. Join Discord Previous05. Selection Sort & Insertion SortNext07. Quick Sort — The Fastest Practical Sort