Why search and sort?
Computers search all day: your contacts, a shop's stock, a game's leaderboard, Google's index. Most of that data is sorted first, because sorted data is much faster to search.
An algorithm is a recipe. Exact, ordered steps that always finish. Searching and sorting are the classic examples.
We measure speed by counting. Not seconds: computers differ. We count comparisons: each time the algorithm asks "is this the one?" or "is this bigger than that?".
We care about big lists. On 8 items every method is instant. On 8 million, a bad choice can take days instead of a blink.
| Where | Search or sort? | Why it matters |
|---|---|---|
| Phone contacts | Sorted A–Z, then searched | Typing "Ma" jumps straight to the M names. |
| Game leaderboard | Sorted by score | Top 10 is just the first 10 items. |
| School register (SIMS) | Sorted by surname | Finding one pupil among 1,200 is quick. |
| Search engine | Huge sorted index | Billions of pages, answers in under a second. |
On these diagrams the colours always mean the same algorithm:
Linear search: one at a time
Linear search starts at the first item and checks each one in turn. It stops when it finds the item, or when it runs out of items.
Start at the first item (index 0).
Is it the target? Yes → stop and report where it is.
No → move to the next item and repeat.
If you pass the last item without finding it, report "not found".
| Case | When it happens | Checks for a list of n items | For n = 1,000 |
|---|---|---|---|
| Best | The target is the very first item | 1 | 1 |
| Worst | The target is last, or not there at all | n | 1,000 |
| Average | The target is somewhere in the list | about n ÷ 2 | about 500 |
def linear_search(items, target): for i in range(len(items)): if items[i] == target: return i # found: give back its index return -1 # checked them all: not there scores = [12, 45, 33, 7, 50, 28] print(linear_search(scores, 7))
Why bother with linear search at all? It works on any list, sorted or not. It is easy to write. On a short list it is perfectly fast. If you only search a messy list once, sorting it first would cost more than just looking.
Binary search: halve it every time
Binary search needs a sorted list. Like a dictionary: open it in the middle, decide which half your word is in, throw the other half away. Repeat.
Look at the middle item. (Middle index = (low + high) ÷ 2, rounded down.)
Is it the target? Stop, found.
Is the target bigger? Throw away the middle and everything left of it. Smaller? Throw away the middle and everything right of it.
Repeat with what's left. If nothing is left, the target is not in the list.
On an unsorted list binary search gives wrong answers. It might throw away the half that holds your item. Sorted first, always.
How many checks, at most?
Each check halves what's left. So the question is: how many times can you halve n before you get down to 1? That number has a name: log₂ n ("log base 2 of n").
By hand, for n = 1,000. Press Next line.
Halve, rounding down each time: 1,000 → 500 → 250 → 125 → 62 → 31 → 15 → 7 → 3 → 1.
Count the arrows: 9 halvings. So ⌊log₂ 1,000⌋ = 9.
Check it the other way, by doubling: 2⁹ = 512 is not more than 1,000, but 2¹⁰ = 1,024 is. So 9 is the biggest power that fits.
The last item left still needs one more check: 9 + 1 = 10 checks.
Compare: linear search could need 1,000 checks on the same list. Binary search needs at most 10.
| Doubling | 2¹ | 2² | 2³ | 2⁴ | 2⁵ | 2⁶ | 2⁷ | 2⁸ | 2⁹ | 2¹⁰ |
|---|---|---|---|---|---|---|---|---|---|---|
| Items | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1,024 |
| Worst checks | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
def binary_search(items, target): low = 0 high = len(items) - 1 while low <= high: mid = (low + high) // 2 if items[mid] == target: return mid elif items[mid] < target: low = mid + 1 # throw away the left half else: high = mid - 1 # throw away the right half return -1 # nothing left: not there
Why so fast? Doubling the list only adds one check, because one check halves it straight back. 2¹⁰ = 1,024 and 2²⁰ ≈ 1 million, so a million items need only about 20 checks.
Linear vs binary: the race
Plot "most checks needed" against "items in the list". Linear search climbs in a straight line. Binary search barely leaves the floor.
| Items (n) | Linear (worst) | Binary (worst) | How many times faster |
|---|---|---|---|
| 10 | 10 | 4 | 2.5 × |
| 1,000 | 1,000 | 10 | 100 × |
| 1,000,000 | 1,000,000 | 20 | 50,000 × |
| 8 billion (everyone on Earth) | 8,000,000,000 | 33 | about 240 million × |
| Linear search | Binary search | |
|---|---|---|
| Needs a sorted list? | No | Yes |
| Speed on big lists | Slow: up to n checks | Very fast: up to ⌊log₂ n⌋ + 1 |
| Easy to code? | Very | Fairly: the low/high/mid logic is easy to get wrong by one |
| Works on a linked list or a stream? | Yes | No: it must jump straight to the middle |
Binary search is faster only if the list is already sorted. If you'd have to sort it first just to search once, linear search may win overall.
Bubble sort: swap the neighbours
Bubble sort walks along the list comparing neighbours. If a pair is in the wrong order, it swaps them. One trip along the list is called a pass.
Compare items 0 and 1. Wrong order? Swap them.
Move one place right: compare items 1 and 2. Then 2 and 3, and so on to the end. That's one pass.
After pass 1, the biggest item is at the end, in its final place. After pass 2, the second biggest is too. So each pass can stop one place earlier.
Early stop: if a whole pass makes no swaps, the list is sorted. Stop.
[5, 1, 4, 2, 8] → pass 1 → [1, 4, 2, 5, 8] → pass 2 → [1, 2, 4, 5, 8] → pass 3: no swaps, done.
How many comparisons, at most?
The worst case is a list in reverse order: every pass swaps every pair. With n items, pass 1 compares n − 1 pairs, pass 2 compares n − 2, … down to 1.
By hand, for n = 5. Press Next line.
Passes compare 4, then 3, then 2, then 1 pairs. Total: 4 + 3 + 2 + 1.
Pair them up from the outside in: (4 + 1) and (3 + 2).
Every pair adds to 5, which is n. There are 4 numbers, so (n − 1) ÷ 2 = 2 pairs.
2 pairs × 5 = 10. That's n × (n − 1) ÷ 2 = 5 × 4 ÷ 2 = 10.
Same trick for n = 100: 100 × 99 ÷ 2 = 4,950 comparisons. For n = 1,000: 499,500. Ten times the items, about a hundred times the work.
def bubble_sort(items): n = len(items) swapped = True while swapped: swapped = False for i in range(n - 1): if items[i] > items[i + 1]: # swap the neighbours items[i], items[i + 1] = items[i + 1], items[i] swapped = True n = n - 1 # the last one is now in place return items
Easy to code, but very slow on big lists. The best case (already sorted) is fast: one pass, n − 1 comparisons, no swaps, stop. A version without the early stop and without shrinking always does (n − 1) × (n − 1) comparisons.
Insertion sort: like a hand of cards
Insertion sort keeps a sorted part on the left. It picks up the next item from the unsorted part and slides it left into its correct place.
The first item on its own counts as sorted.
Pick up the first unsorted item. This leaves a gap.
Compare it with the item to the left of the gap. Bigger than yours? Shift it right into the gap. Repeat.
Smaller (or the front of the list)? Drop your item into the gap. The sorted part has grown by one.
Insertion sort, one move at a time
[5 | 1, 4, 2, 8] → [1, 5 | 4, 2, 8] → [1, 4, 5 | 2, 8] → [1, 2, 4, 5 | 8] → [1, 2, 4, 5, 8].
def insertion_sort(items): for i in range(1, len(items)): key = items[i] # pick up the next card j = i - 1 while j >= 0 and items[j] > key: items[j + 1] = items[j] # shift right j = j - 1 items[j + 1] = key # drop it into the gap return items
Why is it fast on nearly sorted lists? Each item only slides as far as it needs to. If it's already in place, that's one comparison and no shifts. Try "Nearly sorted list" above and watch the counter. Worst case (reverse order) is the same as bubble sort: n × (n − 1) ÷ 2.
Merge sort: split, then merge
Merge sort splits the list in half again and again until every piece has 1 item. A list of 1 is already sorted. Then it merges pieces back together, in order, two at a time.
Split: [6, 3, 8, 1] [5, 2, 7, 4]
Split: [6, 3] [8, 1] [5, 2] [7, 4]
Split into singles. Each single is sorted.
Merge: [3, 6] [1, 8] [2, 5] [4, 7]
Merge: [1, 3, 6, 8] [2, 4, 5, 7]
Merge: [1, 2, 3, 4, 5, 6, 7, 8]
To merge two sorted lists, only ever compare the front item of each. Take the smaller one. Repeat. When one list is empty, copy the rest of the other straight across.
Merge sort, one comparison at a time
def merge(left, right): result = [] i = 0 j = 0 while i < len(left) and j < len(right): if left[i] <= right[j]: # compare the fronts result.append(left[i]) i = i + 1 else: result.append(right[j]) j = j + 1 return result + left[i:] + right[j:] # copy what's left def merge_sort(items): if len(items) <= 1: return items # 1 item: sorted mid = len(items) // 2 return merge(merge_sort(items[:mid]), merge_sort(items[mid:]))
Why is it fast? There are only about log₂ n levels (8 items → 3 levels, 1,000 → 10 levels). Each level merges all n items once, using at most about n comparisons. So the total is about n × log₂ n. For 1,000 items that's about 10,000, not 500,000.
Merge sort needs extra memory: the merged lists are new copies. Bubble and insertion sort work "in place", swapping inside the original list. Merge sort's function calls itself (that's called recursion). You don't need to write recursion at GCSE, only explain the split and merge steps.
Comparing the three sorts
There's rarely one best algorithm. There's only the best one for this data: how big it is, how messy it is, and how much memory you have.
| Sort | Comparisons: worst | Comparisons: best | Extra memory | Good for |
|---|---|---|---|---|
| Bubble | n(n − 1) ÷ 2 reverse order | n − 1 already sorted, early stop | None (in place) | Teaching. Tiny lists. Checking a list is sorted. |
| Insertion | n(n − 1) ÷ 2 reverse order | n − 1 already sorted | None (in place) | Small or nearly sorted lists. Adding new items to a sorted list. |
| Merge | about n log₂ n | about half of that | A copy of the list (about n) | Big lists. Steady speed whatever the order. |
The race: comparisons for a list of n
Python's own sorted() uses Timsort. It is a mix: insertion sort on small runs, then merging. Real programs combine the strengths of both.
AQA 8525 names linear search, binary search, bubble sort and merge sort. OCR J277 adds insertion sort to those. Both ask you to compare them in words; neither needs you to write formulas like n log n.
Big-O: how the work grows
Computer scientists describe speed by asking one question: if the list gets bigger, how fast does the work grow? The answer is written with a capital O, for "order of". (This is A-level, but the idea is simple.)
| Name | In plain words | Example | Double n, and the work… |
|---|---|---|---|
| O(log n) | Halves the problem each step | Binary search | goes up by just 1 |
| O(n) | Looks at every item once | Linear search | doubles |
| O(n log n) | About log n levels, each touching every item | Merge sort | a bit more than doubles |
| O(n²) | Compares (nearly) every item with every other | Bubble, insertion (worst) | goes up 4 times |
| n | log₂ n | n | n log₂ n | n² |
|---|---|---|---|---|
| 10 | about 3 | 10 | about 33 | 100 |
| 100 | about 7 | 100 | about 664 | 10,000 |
| 1,000 | about 10 | 1,000 | about 9,966 | 1,000,000 |
| 1,000,000 | about 20 | 1,000,000 | about 20 million | 1,000,000,000,000 |
Say a computer does 1 million comparisons a second.
Sorting 1 million items with an n² sort: 10¹² ÷ 10⁶ = 1,000,000 seconds. That's about 11.6 days.
With an n log n sort: 20,000,000 ÷ 1,000,000 = 20 seconds.
Same computer. Same data. The algorithm made the difference between a coffee break and a fortnight.
Why ignore the "÷ 2" in n(n − 1) ÷ 2? Big-O only cares about the shape of the growth. Halving a fortnight is still a week. Changing the shape (n² → n log n) turns it into seconds.
Sorting with no computer: networks and scales
A sorting network is a fixed set of "compare and swap" stations. Numbers travel along lanes. At each station two lanes meet: the smaller number goes up, the bigger goes down. At the end they're sorted, every time.
Run a 6-lane sorting network
12 stations in 5 stages. Stations in the same stage never share a lane, so they can all happen at the same time. That's parallel processing, like the many cores in a graphics card.
It never changes its plan. The same 12 comparisons happen whatever the numbers are. Bubble sort's comparisons depend on the data; a network's don't.
The weighing version. Give a group 8 identical pots with different amounts of sand, and one balance scale. The only allowed question is "which of these two is heavier?" That is exactly one comparison. Sort the pots, and count how many weighings it took.
CS Unplugged draws this network in chalk on the playground. Six pupils each hold a number card, walk along the lanes, and meet at the circles. Smaller goes left (or up), bigger goes right. Time a run, then try the same with bubble sort as a line of pupils.
Common mix-ups
The mistakes nearly everyone makes the first time. Read the left side, then the right.
Binary search works on any list.
Only on a sorted list. Otherwise it may throw away the half your item is in.
Binary search is always the better choice.
Not if the list is unsorted and you search once. Sorting first costs more than one linear search.
"Binary search" has something to do with binary numbers.
"Binary" here means two: it splits the list into two halves each time.
Bubble sort is done after one pass.
One pass only fixes the biggest item. You keep going until a pass makes no swaps.
Merge sort compares every item in one list with every item in the other when merging.
It only compares the two front items. That's why merging is quick.
The middle of a list of 10 (indexes 0–9) is index 5.
(0 + 9) ÷ 2 = 4.5, rounded down = index 4. Exam boards accept rounding up too, if you're consistent.
A faster computer fixes a slow algorithm.
A 10 × faster computer turns 11 days into 1 day. A better algorithm turns 11 days into 20 seconds.
Check yourself
Quick questions first. Then endless practice: a wrong answer shows you the working, step by step.