◐ ai-teacher

🧩 Data Structures & Algorithms · Chapter 11

Sorting algorithms

Every sort ends with the same list. What differs is how many comparisons it took, how much memory it borrowed, and whether equal items kept their order.

Study this lesson free in the app →

The basics

A sorting algorithm rearranges items into order — smallest to largest, A to Z, oldest to newest.

The problem it solves: sorted data is what makes everything else fast. Binary search, finding duplicates, merging two lists, the median, "top 10" — all are easy once data is sorted. So the cost of sorting is paid constantly, and the choice of algorithm decides whether 1 million items take a fraction of a second or many minutes.

The mental model: two ways people sort a hand of playing cards. Pick up one card at a time and slide it into place among the cards you already hold — that's insertion sort, O(n²) but great when the hand is nearly sorted. Or split the deck between friends, have each sort their half, then merge the two sorted piles by repeatedly taking the smaller top card — that's merge sort, O(n log n).

Words you must know 5

Comparison sort
decides order only by comparing pairs; can't beat O(n log n) in the worst case
In-place
uses only O(1) (or O(log n)) extra memory
Stable
items with equal keys keep their original relative order
Pivot
the element quick sort partitions around
Partition / merge
the core step of quick sort / merge sort

Explained

Sort 1,000,000 numbers with an O(n²) algorithm: about 500 billion comparisons. Minutes to hours.

With an O(n log n) one: about 20 million. A blink.

Same result, 25,000× less work. Below you'll watch four algorithms sort the same seven numbers — compare how each one moves.

Two ways to sort exam papers

A teacher has 40 marked papers to put in score order.

Way 1 — the card player. Take papers one at a time and slide each into its place in the sorted pile in your hand. If the papers were almost in order already, each one barely moves. If they were reversed, each one travels all the way down. That's insertion sort.

Way 2 — the team. Split the stack between two assistants, who split theirs again… until everyone holds one paper (already sorted!). Then pairs merge: look at the two top papers, take the lower one, repeat. Merging two sorted piles is a single pass. That's merge sort — it needs a table to lay the merged pile on (extra space), but it's fast no matter how messy the stack was.

Bubble sort: big values float right

Compare neighbours, swap if out of order. After each pass the largest remaining value has bubbled to the end. O(n²) comparisons; an early-exit check (no swaps in a pass → stop) makes it O(n) on sorted input.

Insertion sort: grow a sorted prefix

Take the next item and shift bigger items in the sorted prefix one step right until its slot appears. Each item moves only as far as it's out of place — so nearly-sorted data finishes in about n steps.

Merge sort: split, then merge

Halve until single items, then merge sorted halves back together. log₂ n levels of splitting, n work to merge each level → O(n log n), whatever the input. The merged output needs its own buffer: O(n) extra space.

Quick sort: place the pivot, recurse

Pick a pivot, move smaller values to its left and larger to its right — now the pivot is in its final place. Recurse on both sides. Good pivots split evenly (O(n log n)); a pivot that's always the smallest or largest leaves one side empty (O(n²)).

The comparison table

            best        average     worst       extra   stable
──────────────────────────────────────────────────────────────
bubble*     O(n)        O(n²)       O(n²)       O(1)    yes
selection   O(n²)       O(n²)       O(n²)       O(1)    no
insertion   O(n)        O(n²)       O(n²)       O(1)    yes
merge       O(n log n)  O(n log n)  O(n log n)  O(n)    yes
quick       O(n log n)  O(n log n)  O(n²)       O(log n) no
heap        O(n log n)  O(n log n)  O(n log n)  O(1)    no
Timsort     O(n)        O(n log n)  O(n log n)  O(n)    yes
counting    O(n + k)    O(n + k)    O(n + k)    O(n + k) yes

\* with the 'stop if no swaps' check. k = range of key values.

Why quick sort is still popular despite its worst case: it works in place and touches memory sequentially, so it's often fastest in practice. Libraries pick pivots randomly or by median-of-three to make O(n²) vanishingly unlikely.

Insertion and merge sort in Python

def insertion_sort(a):
    for i in range(1, len(a)):
        x, j = a[i], i - 1
        while j >= 0 and a[j] > x:   # strict > keeps equal items in order
            a[j + 1] = a[j]
            j -= 1
        a[j + 1] = x
    return a

def merge_sort(a):
    if len(a) <= 1:
        return a
    mid = len(a) // 2
    left, right = merge_sort(a[:mid]), merge_sort(a[mid:])
    out, i, j = [], 0, 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:      # <= takes from the left first → stable
            out.append(left[i]); i += 1
        else:
            out.append(right[j]); j += 1
    return out + left[i:] + right[j:]

# In real code:
people = [("Ana", 31), ("Ben", 25), ("Chen", 31)]
sorted(people, key=lambda p: p[1])         # Ben, Ana, Chen — Ana stays before Chen
sorted(people, key=lambda p: (-p[1], p[0]))  # age desc, then name

Words to know

Stable sortA sort that keeps items with equal keys in their original relative order.
In-placeSorting that needs only O(1) or O(log n) extra memory beyond the input.
PivotThe element quick sort partitions around; it ends in its final sorted position.
PartitionRearranging so smaller items are left of the pivot and larger items right of it.
MergeCombining two sorted lists into one sorted list in a single linear pass.
Comparison sortA sort that orders items only by comparing pairs; its worst case is at least Ω(n log n).
TimsortPython's and Java's object sort: a merge/insertion hybrid that exploits existing sorted runs; stable.

In two minutes

Test yourself

1. Time and extra space of merge sort?

  1. O(n²) time, O(1) space
  2. O(n log n) time in every case, O(n) extra space
  3. O(n log n) on average, O(n²) in the worst case
  4. O(n) time, O(log n) space
Show the answer

O(n log n) time in every case, O(n) extra space — Merge sort always splits evenly: log n levels × O(n) merging. The merge needs a buffer, so O(n) extra space. 'Average n log n, worst n²' describes quick sort.

2. Which of these sorts is stable?

  1. Selection sort
  2. Quick sort (typical in-place version)
  3. Heap sort
  4. Insertion sort
Show the answer

Insertion sort — Insertion sort only shifts items past strictly larger ones, so equal items never jump each other. Selection sort's long-distance swaps, quick sort's partitioning and heap sort's sifting can all reorder equal keys.

3. What does Python's sorted() use?

  1. Quick sort
  2. Heap sort
  3. Timsort — stable, O(n log n) worst case, O(n) on already-sorted data
  4. Bubble sort for lists under 100 items
Show the answer

Timsort — stable, O(n log n) worst case, O(n) on already-sorted data — Timsort finds existing sorted runs and merges them, using insertion sort for short runs. That's why sorting already-sorted data is nearly linear, and why multi-key sorting by successive sorts works.

4. A list of 1 million events is already sorted except for a handful that arrived a few positions late. Which simple algorithm handles it in close to linear time?

  1. Insertion sort — each item shifts only as far as it's out of place
  2. Selection sort
  3. Quick sort with first-element pivot
  4. Bubble sort without an early-exit check
Show the answer

Insertion sort — each item shifts only as far as it's out of place — Insertion sort's cost is n plus the number of inversions; a few late items means few inversions. Selection sort always does n²/2 comparisons, and first-element-pivot quick sort degrades to O(n²) on sorted input.

5. An embedded device must sort 100,000 readings with a guaranteed O(n log n) worst case and almost no extra memory. Stability doesn't matter. Pick:

  1. Heap sort — O(n log n) worst case, O(1) extra space
  2. Merge sort
  3. Quick sort
  4. Insertion sort
Show the answer

Heap sort — O(n log n) worst case, O(1) extra space — Merge sort guarantees n log n but needs O(n) extra memory. Quick sort can hit O(n²). Insertion is O(n²). Heap sort meets both constraints; its cost is being unstable and somewhat slower in practice.

6. You want orders sorted by date, and orders on the same date sorted by customer name. With a stable sort, which sequence works?

  1. Sort by date, then sort by customer
  2. Quick sort twice in either order
  3. Sort by customer, then stable-sort by date
  4. It can't be done without writing a custom comparator
Show the answer

Sort by customer, then stable-sort by date — The last sort decides the primary order; stability keeps the earlier (customer) order within each date. Doing date first then customer would make customer the primary key. In Python you can also do it in one call: key=lambda o: (o.date, o.customer).

There are 8 questions, 4 puzzles and 6 recall cards for this lesson in the app — the recall cards come back on later days, just before you would forget.

Learn it so it stays. Read it, test yourself, then recall it on two different days. The app schedules that for you, free.

Start this lesson →