Intermediate: core algorithms · Sorting and searching · topic 27 of 61 · 40 min
Merge sort and quick sort
Explanation
Beating O(n²)
Simple sorts fix one inversion at a time, and a list can have about n²/2 inversions. To do better, an algorithm must move items far in one step. Both merge sort and quick sort use divide and conquer from Module 5: split the problem, solve the halves recursively, and combine. Any sort that only compares items needs about n log n comparisons in the worst case, so O(n log n) is the best a comparison sort can do.
Merge sort
Split the list in the middle, sort the left half and the right half recursively, then merge the two sorted halves with two pointers (you wrote merge in Module 5). A list of 0 or 1 items is already sorted: that is the base case.
def merge_sort(nums):
if len(nums) <= 1:
return list(nums)
mid = len(nums) // 2
left = merge_sort(nums[:mid])
right = merge_sort(nums[mid:])
out, i, j = [], 0, 0
while i < len(left) and j < len(right):
if left[i] <= right[j]: # <= takes from the left on ties: stable
out.append(left[i]); i += 1
else:
out.append(right[j]); j += 1
out.extend(left[i:])
out.extend(right[j:])
return out
print(merge_sort([38, 27, 43, 3, 9, 82, 10])) # Output: [3, 9, 10, 27, 38, 43, 82]There are about log₂ n levels of splitting, and each level merges n items in total, so merge sort is O(n log n) in the best, average AND worst case. The merge needs a new list, so it uses O(n) extra memory (plus O(log n) for the recursion). Because ties take from the left half first, it is stable. It also suits data too big for memory (external sorting) and linked lists, where splitting and merging need no random access.
Quick sort
Pick one item as the pivot. Partition: rearrange the list so items smaller than the pivot come before it and bigger items after it. The pivot is now in its final place. Then quick sort the left part and the right part. There is no combine step; the work happens in the partition.
import random
def quick_sort(a, lo=0, hi=None):
if hi is None:
hi = len(a) - 1
if lo >= hi:
return a
p = random.randint(lo, hi) # random pivot
a[p], a[hi] = a[hi], a[p] # move it to the end
pivot, store = a[hi], lo
for i in range(lo, hi): # Lomuto partition
if a[i] < pivot:
a[i], a[store] = a[store], a[i]
store += 1
a[store], a[hi] = a[hi], a[store] # pivot to its final place
quick_sort(a, lo, store - 1)
quick_sort(a, store + 1, hi)
return a
print(quick_sort([38, 27, 43, 3, 9, 82, 10])) # Output: [3, 9, 10, 27, 38, 43, 82]Why the pivot matters
If the pivot lands near the middle each time, the parts halve and quick sort is O(n log n). If it is always the smallest or largest item, one side is empty and the other has n − 1 items, so there are n levels: O(n²) time and O(n) recursion depth. That happens with "always take the last item" on a list that is already sorted, a very common input. Two fixes: a random pivot (bad splits become extremely unlikely on any input) or median-of-three (the middle value of the first, middle and last items). Many equal values cause the same trouble with the simple partition above; a three-way partition (less, equal, greater) fixes it.
Lomuto and Hoare partition, in words
- Lomuto: the pivot sits at the end; one pointer scans left to right and a store pointer marks where the next smaller item goes. Easy to write, but it does more swaps.
- Hoare: two pointers start at both ends and move towards each other, swapping a too-big item on the left with a too-small item on the right. Fewer swaps and better with duplicates, but the boundaries are trickier, and the pivot does not necessarily end in its final place.
Comparing them
| Merge sort | Quick sort | |
|---|---|---|
| Average time | O(n log n) | O(n log n) |
| Worst time | O(n log n) | O(n²), rare with random pivots |
| Extra space | O(n) | O(log n) stack on average |
| Stable? | Yes | No (the usual in-place versions) |
| In practice | Linked lists, external sorting, when stability matters | Arrays in memory; often the fastest in practice |
Common mistakes
- Using < instead of <= in the merge, which takes from the right on ties and breaks stability. Fix: take from the left when equal.
- Forgetting to copy the leftovers after the merge loop. Fix: extend with left[i:] and right[j:].
- Always picking the first or last item as pivot, so sorted input becomes O(n²) and may hit the recursion limit. Fix: random or median-of-three pivot.
- Recursing on a range that includes the pivot, so the range never shrinks. Fix: recurse on lo..store − 1 and store + 1..hi.
- Claiming quick sort is "always O(n log n)". Fix: say "O(n log n) on average, O(n²) worst case".
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Merge sort and quick sort · 1/8
This lesson covers the two classic fast sorts: merge sort and quick sort. Both use divide and conquer.
Prefer a video by a teacher? Find free videos on “merge sort quick sort explained partition pivot visualisation” on YouTube ↗
In real life: Merging sorted lists of transactions
A bank's nightly job may receive transactions from several branches, each file already sorted by time. Producing one time-ordered statement is a merge, not a full sort: walk through the sorted lists with pointers and always take the earliest next transaction. This is the merge step of merge sort, and it is also how very large files that do not fit in memory are sorted: sort chunks, write them to disk, then merge them. Python's heapq.merge does this lazily for many inputs.
import heapq
branch_a = [("09:05", "A", 500), ("11:30", "A", 1200)]
branch_b = [("08:45", "B", 300), ("10:10", "B", 50), ("12:00", "B", 900)]
for t in heapq.merge(branch_a, branch_b):
print(t)Practice (0/4 done)
Write merge_sorted(a, b) where a and b are each sorted in non-decreasing order. Return ONE new sorted list with all the items of both, in O(len(a) + len(b)) time using two pointers. Do not call sorted() or .sort(), and do not change a or b. Example: merge_sorted([1, 4, 9], [2, 3, 10]) returns [1, 2, 3, 4, 9, 10].
Write merge_sort(nums) that returns a NEW sorted list using merge sort (split in half, sort both halves recursively, merge). Do not call sorted() or .sort() (the tests compare with sorted() but cannot see inside your code). It must sort 20,000 numbers quickly. Example: merge_sort([38, 27, 43, 3, 9, 82, 10]) returns [3, 9, 10, 27, 38, 43, 82].
Write quick_sort(nums) that returns a NEW sorted list using quick sort (any correct version: in place with Lomuto or Hoare, or the simple version that builds "less", "equal" and "greater" lists). Do not call sorted() or .sort(). It must handle 3,000 numbers that are ALREADY SORTED and 2,000 EQUAL numbers without hitting Python's recursion limit, so choose the pivot from the middle or at random, and keep values equal to the pivot in their own group. Example: quick_sort([3, 6, 1, 8, 2]) returns [1, 2, 3, 6, 8].
An inversion is a pair of positions i < j with nums[i] > nums[j] (equal values do NOT count). Write count_inversions(nums) that returns the number of inversions in O(n log n) time, by counting during merge sort: when you take an item from the right half while the left half still has items from position i onwards, add len(left) − i. Do not change nums. The tests include a list of 20,000 items, too big for checking every pair. Example: count_inversions([2, 4, 1, 3, 5]) returns 3.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Sorting in real code: keys, comparators, counting sortSorting and searching · same module
- Binary searchSorting and searching · same module
- Recursion and sorting algorithmsPython Programming · Algorithms, data structures and testing
- Grouping, joining and reshapingData Science, Machine Learning and AI · Cleaning, exploring and visualising data
- Heap patterns: top-k and streamsHeaps and greedy algorithms
- Two pointersHashing, two pointers and sliding window
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What is the WORST-case time of merge sort?
Answer: O(n log n)
It always splits in half and merges n items per level, whatever the input.
Quick sort always picks the last item as pivot. Which input is slowest?
Answer: A list that is already sorted
Each pivot is the largest item, so one side is empty every time: n levels, O(n²).
Why is merge sort stable?
Answer: On ties the merge takes the item from the left half first
Taking from the left on ties keeps equal items in their original order.
What is the extra space of a typical merge sort on an array?
Answer: O(n)
Merging needs a separate output list of up to n items.
What does a median-of-three pivot do?
Answer: Takes the middle value of the first, middle and last items, avoiding the worst case on sorted input
On sorted input the median of three is the true middle, so the split is balanced.
Quicksort with a pivot that is always the smallest item does what?Related
Answer: Degrades to O(n²)
Each split removes only one item, so there are n levels of up to n work each.
From Best, average and worst case; analysing loops and recursion
What are the three steps of divide and conquer?Related
Answer: Divide into parts, solve each part recursively, combine the answers
Divide splits the input, conquer solves smaller copies, combine builds the full answer.
From Divide and conquer
What is the Big-O of merge sort?Related
Answer: O(n log n)
About log n levels of splitting, with n items merged at each level.
You merge customers (left) with orders using how="left". What happens to customers with no orders?Related
Answer: They are kept, with missing (NaN) order columns
A left join keeps every row of the left table.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 3 more topics in “Sorting and searching” before its exam: Sorting in real code: keys, comparators, counting sort, Binary search, Binary search on the answer.
- Module 6 exam: Sorting and searching · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Trees and binary search trees. 61 topics left in the course before the final exam.