CO3 Topic 20 of 34
Divide and conquer and merge sort
Split a problem into smaller copies of itself, solve them recursively, and combine the answers. Merge sort is the model example: Θ(n log n) on every input.
You will be able to
- Describe the divide, conquer and combine steps of a divide-and-conquer algorithm
- Trace merge sort and the merge step on an array
- Write and solve the recurrence T(n) = 2T(n/2) + Θ(n) with a recursion tree and the master theorem
- Use merge sort's structure for related problems such as counting inversions
Read
- Textbook 1, §2.3 Designing algorithms (merge sort) and §4.3–4.5 Solving recurrences
- Textbook 2, §3.1–3.4 Divide and conquer; merge sort
- Textbook 5, Ch. on divide and conquer
Watch
Intuition
Sorting 1,000 exam scripts alone is slow. Split the pile in two, give each half to a friend (who splits theirs again …), and when two sorted piles come back, merge them by repeatedly taking the smaller top script. That is divide and conquer:
- Divide the problem into smaller subproblems of the same kind.
- Conquer them recursively (tiny ones are solved directly).
- Combine their answers.
digraph {
rankdir=TB; node [shape=record, fontname="JetBrains Mono", fontsize=12]; edge [dir=none];
a [label="38 | 27 | 43 | 3 | 9 | 82 | 10"];
b [label="38 | 27 | 43"]; c [label="3 | 9 | 82 | 10"];
d [label="38"]; e [label="27 | 43"]; f [label="3 | 9"]; g [label="82 | 10"];
m1 [label="27 | 38 | 43", style=filled, fillcolor="#99f6e4"]; m2 [label="3 | 9 | 10 | 82", style=filled, fillcolor="#99f6e4"];
r [label="3 | 9 | 10 | 27 | 38 | 43 | 82", style=filled, fillcolor="#2dd4bf"];
a -> b; a -> c; b -> d; b -> e; c -> f; c -> g; d -> m1; e -> m1; f -> m2; g -> m2; m1 -> r; m2 -> r;
}
Splitting down to small pieces, then merging back up (smaller pieces such as [27 43] are sorted by the same process).
How it works
Merge two sorted lists: compare their front elements, move the smaller one to the output, repeat; when one list empties, copy the rest of the other.
Merging [27, 38, 43] and [3, 9, 10, 82]:
| Compare | Take | Output so far |
|---|---|---|
| 27 vs 3 | 3 | 3 |
| 27 vs 9 | 9 | 3 9 |
| 27 vs 10 | 10 | 3 9 10 |
| 27 vs 82 | 27 | 3 9 10 27 |
| 38 vs 82 | 38 | … 38 |
| 43 vs 82 | 43 | … 43 |
| (left empty) | 82 | 3 9 10 27 38 43 82 |
Merging lists of sizes m and n uses at most m + n − 1 comparisons.
Recurrence. Merge sort on n items does two half-size sorts plus a linear merge:
T(n) = 2T(n/2) + cn, T(1) = c
Recursion tree: every level does cn work in total, and there are log₂ n levels, so T(n) = Θ(n log n).
Master theorem for T(n) = aT(n/b) + f(n): compare f(n) with n^(log_b a).
| Case | Condition | Result | Example |
|---|---|---|---|
| 1 | f(n) = O(n^(log_b a − ε)) | Θ(n^(log_b a)) | T(n) = 8T(n/2) + n² → Θ(n³) |
| 2 | f(n) = Θ(n^(log_b a)) | Θ(n^(log_b a) log n) | merge sort: a = 2, b = 2, f = n → Θ(n log n) |
| 3 | f(n) = Ω(n^(log_b a + ε)), plus regularity | Θ(f(n)) | T(n) = 2T(n/2) + n² → Θ(n²) |
Python implementation
def merge(left, right):
out, i, j = [], 0, 0
while i < len(left) and j < len(right):
if left[i] <= right[j]: # <= keeps equal keys in order: stable
out.append(left[i]); i += 1
else:
out.append(right[j]); j += 1
return out + left[i:] + right[j:]
def merge_sort(a):
if len(a) <= 1:
return a
mid = len(a) // 2
return merge(merge_sort(a[:mid]), merge_sort(a[mid:]))
def count_inversions(a):
"""Number of pairs i < j with a[i] > a[j], in O(n log n), using merge sort."""
if len(a) <= 1:
return a, 0
mid = len(a) // 2
left, x = count_inversions(a[:mid])
right, y = count_inversions(a[mid:])
merged, i, j, cross = [], 0, 0, 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
cross += len(left) - i # right[j] is smaller than every remaining left item
merged += left[i:] + right[j:]
return merged, x + y + cross
if __name__ == "__main__":
print(merge_sort([38, 27, 43, 3, 9, 82, 10])) # [3, 9, 10, 27, 38, 43, 82]
print(count_inversions([2, 4, 1, 3, 5])[1]) # 3
Complexity
| Merge sort | |
|---|---|
| time (best, average, worst) | Θ(n log n) — the split never depends on the data |
| extra space | Θ(n) for the merge buffers |
| stable? | yes (equal keys keep their order) |
| in-place? | no (standard array version) |
Common mistakes
- Using
<instead of<=in merge — still sorts, but loses stability. - Forgetting the leftovers after one list runs out.
- Master theorem misuse. It needs subproblems of equal size n/b; it does not apply to T(n) = T(n−1) + n (that is Θ(n²)).
- Claiming merge sort is in-place. The usual version needs Θ(n) extra memory.
Problems
- Trace merge sort on [12, 11, 13, 5, 6, 7], drawing the split and merge tree.
- Solve with the master theorem: T(n) = 4T(n/2) + n, T(n) = 4T(n/2) + n², T(n) = 3T(n/4) + n log n.
- Lab: count inversions in an array of 10⁵ numbers in O(n log n) and compare with the O(n²) double loop.
- Lab: merge k sorted lists of total size N in O(N log k).
- How many comparisons does merge sort make in the worst case for n = 8? (Hint: a merge of m + n items needs at most m + n − 1.)
Quiz
GATE-style practice
Questions in the GATE CSE formats: MCQ (one correct option), MSQ (one or more correct options) and NAT (type a number). Each comes with a full worked solution. These are original practice questions, not past papers; for the real thing, see the official GATE site and its previous question papers.