Divide and conquer
Divide and conquer means: split a big problem into smaller pieces, solve the pieces, then combine the answers. Merge sort is the classic example:
- Divide – cut the list in half.
- Conquer – sort each half (by calling merge sort again – recursion!).
- Combine – merge the two sorted halves into one sorted list.
Think of it like this: Sorting a huge pile of exam papers with friends: split the pile, each friend sorts a small pile, then you merge the sorted piles by always taking the paper with the smaller roll number from the top.
def merge(left, right):
out = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
out.append(left[i])
i += 1
else:
out.append(right[j])
j += 1
return out + left[i:] + right[j:]
def merge_sort(nums):
if len(nums) <= 1: # base case: already sorted
return nums
mid = len(nums) // 2
return merge(merge_sort(nums[:mid]), merge_sort(nums[mid:]))
print(merge_sort([38, 27, 43, 3, 9, 82, 10]))Output
[3, 9, 10, 27, 38, 43, 82]
Why O(n log n)?
The list can be halved about log₂ n times before the pieces have one item each. At every level, merging all the pieces together touches each of the n items once. So: n work × log n levels = O(n log n).
| n | n² (simple sorts) | n log n (merge sort) |
|---|---|---|
| 1,000 | 1,000,000 | about 10,000 |
| 1,000,000 | 1,000,000,000,000 | about 20,000,000 |
Merge sort is always O(n log n) and it's stable, but it needs O(n) extra memory for the merged lists.