Skip to content

Intermediate: core algorithms · Sorting and searching · topic 27 of 61 · 40 min

Merge sort and quick sort

Explanation

Merge sort splits the list in half, sorts each half and merges them: always O(n log n), stable, but it needs O(n) extra memory. Quick sort picks a pivot, moves smaller items to its left and bigger ones to its right, and sorts each side: O(n log n) on average and in place, but O(n²) in the worst case when pivots are chosen badly. Random or median-of-three pivots avoid that in practice.

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.

python
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.

python
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]
In place: only swaps inside the list. The random pivot does not change the output, only the speed.

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 sortQuick sort
Average timeO(n log n)O(n log n)
Worst timeO(n log n)O(n²), rare with random pivots
Extra spaceO(n)O(log n) stack on average
Stable?YesNo (the usual in-place versions)
In practiceLinked lists, external sorting, when stability mattersArrays 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

Merge sort · Quick sort

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.

python
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)
Prints the five transactions in time order: 08:45, 09:05, 10:10, 11:30, 12:00.

Practice (0/4 done)

Task 1 · Code · Python

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].

Task 2 · Code · Python

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].

Task 3 · Code · Python

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].

Task 4 · Code · Python

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

Question 1

1.What is the WORST-case time of merge sort?

Question 2

2.Quick sort always picks the last item as pivot. Which input is slowest?

Question 3

3.Why is merge sort stable?

Question 4

4.What is the extra space of a typical merge sort on an array?

Question 5

5.What does a median-of-three pivot do?

Read more (free)

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.

From Recursion and sorting algorithms

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.

From Grouping, joining and reshaping

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

  1. 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.
  2. Module 6 exam: Sorting and searching · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Trees and binary search trees. 61 topics left in the course before the final exam.

← Previous: Simple sorts: bubble, selection and insertion